const crypto = require('crypto'); const express = require('express'); const { getPlanningTableData, } = require('../services/planningUploadService'); const { getPlanningMayProductSnapshotStatus, } = require('../services/planningMayProductDataService'); const router = express.Router(); function getConfiguredApiKeys() { return [ process.env.PLANNING_MAY_PRODUCT_API_KEY, process.env.PLANNING_MAY_PRODUCT_API_KEYS, ] .filter(Boolean) .flatMap((value) => String(value).split(',')) .map((value) => value.trim()) .filter(Boolean); } function safeEquals(left, right) { const leftBuffer = Buffer.from(String(left || '')); const rightBuffer = Buffer.from(String(right || '')); if (leftBuffer.length !== rightBuffer.length) { return false; } return crypto.timingSafeEqual(leftBuffer, rightBuffer); } function extractApiKey(req) { const headerKey = req.get('x-api-key'); if (headerKey) { return headerKey.trim(); } const authHeader = req.get('authorization') || ''; const match = authHeader.match(/^(?:ApiKey|Bearer)\s+(.+)$/i); if (match) { return match[1].trim(); } return String(req.query.apiKey || req.query.api_key || '').trim(); } function requireApiKey(req, res, next) { const configuredKeys = getConfiguredApiKeys(); if (configuredKeys.length === 0) { return res.status(503).json({ success: false, error: '停产分析第三方 API key 尚未配置', }); } const providedKey = extractApiKey(req); if (!providedKey || !configuredKeys.some((key) => safeEquals(providedKey, key))) { return res.status(401).json({ success: false, error: 'API key 无效或未提供', }); } return next(); } function normalizePositiveInteger(value, fallback, maxValue) { const parsed = Number.parseInt(value, 10); if (!Number.isFinite(parsed) || parsed <= 0) { return fallback; } return Math.min(parsed, maxValue); } function buildPublicTablePayload(result = {}, page, pageSize) { return { snapshotDate: result.snapshotDate || result.template?.snapshotDate || null, sourceLatestBizDate: result.sourceLatestBizDate || result.template?.sourceLatestBizDate || null, sourceMode: result.sourceMode || result.template?.sourceMode || null, generatedAt: result.template?.generatedAt || null, calcVersion: result.template?.calcVersion || null, total: Number(result.total || 0), page, pageSize, hasMore: page * pageSize < Number(result.total || 0), reviewDashboard: result.reviewDashboard || null, columns: result.columns || [], rows: result.rows || [], }; } router.get('/status', requireApiKey, async (_req, res) => { try { const data = await getPlanningMayProductSnapshotStatus(); res.setHeader('Cache-Control', 'no-store'); return res.json({ success: true, data }); } catch (error) { console.error('[PlanningMayProductPublicAPI] 获取状态失败:', error); return res.status(error.statusCode || error.status || 500).json({ success: false, error: error.message || '获取停产分析状态失败', }); } }); router.get('/stop-production', requireApiKey, async (req, res) => { try { const page = normalizePositiveInteger(req.query.page, 1, 100000); const pageSize = normalizePositiveInteger(req.query.pageSize || req.query.page_size, 100, 100); const result = await getPlanningTableData('may_product_data', { page, pageSize, mayMatch: req.query.keyword || req.query.q || req.query.mayMatch, snapshotDate: req.query.snapshotDate || req.query.snapshot_date, reviewAction: req.query.reviewAction || req.query.review_action || 'all', virtualCategory: req.query.virtualCategory || req.query.virtual_category || req.query.mode, inventoryScope: req.query.inventoryScope || req.query.inventory_scope || req.query.stockPath || req.query.stock_path, brandGroup: req.query.brandGroup || req.query.brand_group, brandName: req.query.brandName || req.query.brand || req.query.brand_name, categoryName: req.query.categoryName || req.query.category || req.query.category_name, supplierName: req.query.supplierName || req.query.supplier || req.query.supplier_name, missingMetric: req.query.missingMetric || req.query.missing_metric, }); res.setHeader('Cache-Control', 'no-store'); return res.json({ success: true, data: buildPublicTablePayload(result, page, pageSize), }); } catch (error) { console.error('[PlanningMayProductPublicAPI] 查询停产分析失败:', error); return res.status(error.statusCode || error.status || 500).json({ success: false, error: error.message || '查询停产分析失败', }); } }); module.exports = router;