140 lines
4.7 KiB
JavaScript
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;
|