cangku/backend/src/api/planningMayProductPublic.js

140 lines
4.7 KiB
JavaScript

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;