const fs = require('fs'); const path = require('path'); const dayjs = require('dayjs'); const cloudDb = require('../config/cloudDatabase'); const PICKUP_EXCLUDED_LABEL_KEYWORDS = [ '配件', '经管大单', '不发', '等通知', '延期', '延迟', '武进安能', '中铁', '物流', '自提', '外贸', '采购单', '备货', '定制', '中转集运', '中转订单', '新疆西藏单', 'ECLP订单', ]; const PICKUP_EXCLUDED_COURIER_KEYWORDS = [ '武进安能', '中铁', '物流', '自提', '线下快递', '专线', ]; const PICKUP_EXCLUDED_REMARK_KEYWORDS = [ 'bbff', 'ccdd', '转X00006', ]; const PICKUP_TIMELY_NO_FETCH_COURIER_KEYWORDS = [ '京东快运', '京东自营', '顺丰', ]; const PICKUP_TIMELY_LABEL_KEYWORDS = [ 'CLPS订单', 'ECLP订单', ]; const UNKNOWN_WAREHOUSE = '未知仓库'; const UNKNOWN_SUPPLIER = '未知供应商'; const UNKNOWN_PURCHASER = '未配置'; const SNAPSHOT_DIR = path.resolve(__dirname, '../../runtime-cache/logistics-pickup-board'); function q(value) { return `N'${String(value).replace(/'/g, "''")}'`; } function buildSqlLikeAny(column, keywords) { return keywords .map((keyword) => `${column} LIKE ${q(`%${keyword}%`)}`) .join(' OR '); } const excludedLabelSql = buildSqlLikeAny(`ISNULL(src.order_labels, N'')`, PICKUP_EXCLUDED_LABEL_KEYWORDS); const excludedCourierSql = buildSqlLikeAny(`ISNULL(src.courier_name, N'')`, PICKUP_EXCLUDED_COURIER_KEYWORDS); const excludedRemarkSql = buildSqlLikeAny( `ISNULL(src.buyer_message, N'') + N' ' + ISNULL(src.remark, N'') + N' ' + ISNULL(src.remark_content, N'')`, PICKUP_EXCLUDED_REMARK_KEYWORDS ); const timelyNoFetchCourierSql = buildSqlLikeAny(`ISNULL(src.courier_name, N'')`, PICKUP_TIMELY_NO_FETCH_COURIER_KEYWORDS); const timelyLabelSql = buildSqlLikeAny(`ISNULL(src.order_labels, N'')`, PICKUP_TIMELY_LABEL_KEYWORDS); const PICKUP_BOARD_SQL = ` SET NOCOUNT ON; DECLARE @payStart DATETIME = DATEADD(DAY, -1, CAST(@startDate AS DATETIME)); DECLARE @payEnd DATETIME = DATEADD(DAY, -1, CAST(@endDate AS DATETIME)); SELECT CAST(DATEADD(DAY, 1, TRY_CONVERT(datetime, REPLACE(undefined5, '/', '-'))) AS date) AS stat_date, NULLIF(LTRIM(RTRIM(undefined1)), '') AS inner_order_id, NULLIF(LTRIM(RTRIM(undefined2)), '') AS order_id, NULLIF(LTRIM(RTRIM(undefined10)), '') AS sku_id, TRY_CONVERT(datetime, REPLACE(undefined5, '/', '-')) AS pay_time, TRY_CONVERT(datetime, REPLACE(undefined6, '/', '-')) AS afs_send_time, NULLIF(LTRIM(RTRIM(undefined8)), '') AS shop_name, NULLIF(LTRIM(RTRIM(undefined62)), '') AS order_labels, NULLIF(LTRIM(RTRIM(undefined72)), '') AS express_no, NULLIF(LTRIM(RTRIM(undefined73)), '') AS afs_courier_name, NULLIF(LTRIM(RTRIM(undefined74)), '') AS province, NULLIF(LTRIM(RTRIM(undefined75)), '') AS city, NULLIF(LTRIM(RTRIM(undefined66)), '') AS afs_warehouse_name, NULLIF(LTRIM(RTRIM(undefined32)), '') AS order_type, TRY_CONVERT(datetime, REPLACE(undefined33, '/', '-')) AS aftersale_apply_time INTO #afs_raw FROM dbo.jst_goods_profit_kpi_items_afs WHERE TRY_CONVERT(datetime, REPLACE(undefined5, '/', '-')) >= @payStart AND TRY_CONVERT(datetime, REPLACE(undefined5, '/', '-')) < DATEADD(DAY, 1, @payEnd); CREATE NONCLUSTERED INDEX IX_afs_raw_stat_dedup ON #afs_raw(stat_date, inner_order_id, sku_id); WITH ranked AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY stat_date, inner_order_id, sku_id ORDER BY CASE WHEN express_no IS NULL OR express_no = '' THEN 1 ELSE 0 END, CASE WHEN afs_send_time IS NULL THEN 1 ELSE 0 END, afs_send_time DESC, pay_time DESC ) AS rn FROM #afs_raw WHERE stat_date BETWEEN @startDate AND @endDate AND inner_order_id IS NOT NULL AND inner_order_id <> '' AND sku_id IS NOT NULL AND sku_id <> '' ) SELECT stat_date, inner_order_id, order_id, sku_id, pay_time, afs_send_time, shop_name, order_labels, express_no, afs_courier_name, province, city, afs_warehouse_name, order_type, aftersale_apply_time INTO #afs_dedup FROM ranked WHERE rn = 1; CREATE NONCLUSTERED INDEX IX_afs_dedup_order_id ON #afs_dedup(order_id); CREATE NONCLUSTERED INDEX IX_afs_dedup_sku_id ON #afs_dedup(sku_id); CREATE NONCLUSTERED INDEX IX_afs_dedup_express_no ON #afs_dedup(express_no); SELECT DISTINCT express_no INTO #express_keys FROM #afs_dedup WHERE express_no IS NOT NULL AND express_no <> ''; SELECT DISTINCT order_id INTO #order_keys FROM #afs_dedup WHERE order_id IS NOT NULL AND order_id <> ''; CREATE UNIQUE CLUSTERED INDEX IX_order_keys ON #order_keys(order_id); SELECT DISTINCT sku_id INTO #sku_keys FROM #afs_dedup WHERE sku_id IS NOT NULL AND sku_id <> ''; CREATE UNIQUE CLUSTERED INDEX IX_sku_keys ON #sku_keys(sku_id); SELECT NULLIF(LTRIM(RTRIM(e.logistic_id)), '') AS logistic_id, MAX(NULLIF(LTRIM(RTRIM(e.so_id)), '')) AS so_id, MAX(NULLIF(LTRIM(RTRIM(e.logistic_company_name)), '')) AS logistic_company_name, MAX(NULLIF(LTRIM(RTRIM(e.order_status_name)), '')) AS order_status_name, MAX(NULLIF(LTRIM(RTRIM(e.aftersale_status_name)), '')) AS aftersale_status_name, MAX(NULLIF(LTRIM(RTRIM(e.remark)), '')) AS remark, MAX(NULLIF(LTRIM(RTRIM(e.buyer_message)), '')) AS buyer_message, MAX(NULLIF(LTRIM(RTRIM(e.remark_content)), '')) AS remark_content, MAX(NULLIF(LTRIM(RTRIM(e.shop_name)), '')) AS shop_name, MAX(NULLIF(LTRIM(RTRIM(e.warehouse_name)), '')) AS warehouse_name, MIN(e.send_time) AS send_time, MIN(e.fetch_time) AS fetch_time, MAX(NULLIF(LTRIM(RTRIM(e.receiver_address)), '')) AS receiver_address INTO #express_by_logistic FROM dbo.jst_order_express_details e INNER JOIN #express_keys k ON k.express_no = NULLIF(LTRIM(RTRIM(e.logistic_id)), '') GROUP BY NULLIF(LTRIM(RTRIM(e.logistic_id)), ''); CREATE UNIQUE CLUSTERED INDEX IX_express_by_logistic ON #express_by_logistic(logistic_id); SELECT NULLIF(LTRIM(RTRIM(e.so_id)), '') AS so_id, MAX(NULLIF(LTRIM(RTRIM(e.logistic_id)), '')) AS logistic_id, MAX(NULLIF(LTRIM(RTRIM(e.logistic_company_name)), '')) AS logistic_company_name, MAX(NULLIF(LTRIM(RTRIM(e.order_status_name)), '')) AS order_status_name, MAX(NULLIF(LTRIM(RTRIM(e.aftersale_status_name)), '')) AS aftersale_status_name, MAX(NULLIF(LTRIM(RTRIM(e.remark)), '')) AS remark, MAX(NULLIF(LTRIM(RTRIM(e.buyer_message)), '')) AS buyer_message, MAX(NULLIF(LTRIM(RTRIM(e.remark_content)), '')) AS remark_content, MAX(NULLIF(LTRIM(RTRIM(e.shop_name)), '')) AS shop_name, MAX(NULLIF(LTRIM(RTRIM(e.warehouse_name)), '')) AS warehouse_name, MIN(e.send_time) AS send_time, MIN(e.fetch_time) AS fetch_time, MAX(NULLIF(LTRIM(RTRIM(e.receiver_address)), '')) AS receiver_address INTO #express_by_order FROM dbo.jst_order_express_details e INNER JOIN #order_keys k ON k.order_id = NULLIF(LTRIM(RTRIM(e.so_id)), '') GROUP BY NULLIF(LTRIM(RTRIM(e.so_id)), ''); CREATE UNIQUE CLUSTERED INDEX IX_express_by_order ON #express_by_order(so_id); SELECT NULLIF(LTRIM(RTRIM(s.sku_id)), '') AS sku_id, NULLIF(LTRIM(RTRIM(s.supplier_name)), '') AS supplier_name, NULLIF(LTRIM(RTRIM(s.other_1)), '') AS spu_key, NULLIF(LTRIM(RTRIM(s.other_3)), '') AS default_warehouse INTO #sku_meta FROM dbo.jst_sku s INNER JOIN #sku_keys k ON k.sku_id = NULLIF(LTRIM(RTRIM(s.sku_id)), ''); CREATE UNIQUE CLUSTERED INDEX IX_sku_meta ON #sku_meta(sku_id); SELECT d.stat_date, d.inner_order_id, d.order_id, d.sku_id, d.pay_time, COALESCE(el.send_time, eo.send_time, d.afs_send_time) AS send_time, COALESCE(el.fetch_time, eo.fetch_time) AS fetch_time, COALESCE(el.shop_name, eo.shop_name, d.shop_name) AS shop_name, COALESCE(el.warehouse_name, eo.warehouse_name, d.afs_warehouse_name) AS raw_warehouse_name, COALESCE(el.logistic_company_name, eo.logistic_company_name, d.afs_courier_name) AS courier_name, COALESCE(d.express_no, el.logistic_id, eo.logistic_id, N'') AS express_no, COALESCE(el.order_status_name, eo.order_status_name, N'') AS order_status_name, COALESCE(el.aftersale_status_name, eo.aftersale_status_name, N'') AS aftersale_status_name, COALESCE(el.remark, eo.remark, N'') AS remark, COALESCE(el.buyer_message, eo.buyer_message, N'') AS buyer_message, COALESCE(el.remark_content, eo.remark_content, N'') AS remark_content, COALESCE(d.order_labels, N'') AS order_labels, COALESCE(d.province, N'') AS province, COALESCE(d.city, N'') AS city, COALESCE(el.receiver_address, eo.receiver_address, N'') AS receiver_address, COALESCE(d.order_type, N'') AS order_type, d.aftersale_apply_time, COALESCE(s.supplier_name, N'') AS supplier_name, COALESCE(s.spu_key, N'') AS spu_key, COALESCE(s.default_warehouse, N'') AS default_warehouse, COALESCE(cfg.purchaser, N'') AS purchaser_name, COALESCE(shop_meta.responsible_department, N'') AS shop_responsible_department INTO #pickup_enriched FROM #afs_dedup d LEFT JOIN #express_by_logistic el ON d.express_no = el.logistic_id LEFT JOIN #express_by_order eo ON d.order_id = eo.so_id LEFT JOIN #sku_meta s ON d.sku_id = s.sku_id OUTER APPLY ( SELECT TOP 1 purchaser FROM dbo.fys_SkuDataLibrary_SpuConfig WHERE COALESCE(NULLIF(s.spu_key, N''), d.sku_id) LIKE spu + N'%' AND purchaser IS NOT NULL AND purchaser != N'' ORDER BY LEN(spu) DESC ) cfg OUTER APPLY ( SELECT TOP 1 NULLIF(LTRIM(RTRIM(responsibleDepartment)), N'') AS responsible_department FROM dbo.fys_shopInfo WHERE NULLIF(LTRIM(RTRIM(erpShopName)), N'') = COALESCE(el.shop_name, eo.shop_name, d.shop_name) ) shop_meta; SELECT src.stat_date, ISNULL(NULLIF(src.shop_name, N''), N'未知店铺') AS shop_name, CASE WHEN ISNULL(NULLIF(src.raw_warehouse_name, N''), N'') = N'常州简尚家居有限公司' AND ISNULL(NULLIF(src.default_warehouse, N''), N'') <> N'' THEN src.default_warehouse WHEN ISNULL(NULLIF(src.raw_warehouse_name, N''), N'') <> N'' THEN src.raw_warehouse_name WHEN ISNULL(NULLIF(src.default_warehouse, N''), N'') <> N'' THEN src.default_warehouse ELSE ${q(UNKNOWN_WAREHOUSE)} END AS warehouse_name, ISNULL(NULLIF(src.purchaser_name, N''), ${q(UNKNOWN_PURCHASER)}) AS purchaser_name, ISNULL(NULLIF(src.supplier_name, N''), ${q(UNKNOWN_SUPPLIER)}) AS supplier_name, src.sku_id, CASE WHEN (${excludedLabelSql}) THEN 1 WHEN ISNULL(src.shop_responsible_department, N'') LIKE N'%灞变笢椤圭洰缁?%' THEN 1 WHEN src.supplier_name IN (${q('虚拟')}, ${q('景瓴十方')}) THEN 1 WHEN src.order_status_name IN (${q('被拆分')}, ${q('被合并')}) THEN 1 WHEN (${excludedCourierSql}) THEN 1 WHEN ISNULL(src.express_no, N'') LIKE ${q('%F800%')} THEN 1 WHEN (${excludedRemarkSql}) THEN 1 WHEN ISNULL(src.shop_name, N'') LIKE ${q('%京喜%')} AND ISNULL(src.aftersale_status_name, ${q('暂无售后')}) <> ${q('暂无售后')} THEN 1 WHEN ISNULL(src.shop_name, N'') LIKE ${q('%样品专用店铺%')} THEN 1 WHEN ISNULL(src.order_status_name, N'') LIKE ${q('%取消%')} AND src.send_time IS NULL AND ISNULL(src.aftersale_status_name, ${q('暂无售后')}) IN (N'', ${q('暂无售后')}, ${q('未申请退款')}) THEN 1 WHEN src.aftersale_apply_time IS NOT NULL AND src.pay_time IS NOT NULL AND src.aftersale_apply_time >= src.pay_time AND DATEDIFF(SECOND, src.pay_time, src.aftersale_apply_time) <= 24 * 3600 THEN 1 WHEN ISNULL(src.order_type, N'') LIKE ${q('%预售%')} OR ISNULL(src.order_status_name, N'') LIKE ${q('%预售%')} THEN 1 ELSE 0 END AS is_excluded, CASE WHEN src.pay_time IS NULL THEN 1 WHEN ISNULL(src.shop_name, N'') LIKE ${q('%京喜%')} AND src.send_time IS NOT NULL AND src.send_time >= src.pay_time AND DATEDIFF(SECOND, src.pay_time, src.send_time) <= 24 * 3600 THEN 0 WHEN (${timelyNoFetchCourierSql}) AND src.fetch_time IS NULL AND src.send_time IS NOT NULL AND src.send_time >= src.pay_time AND DATEDIFF(SECOND, src.pay_time, src.send_time) <= 24 * 3600 THEN 0 WHEN (${timelyLabelSql}) AND src.send_time IS NOT NULL AND src.send_time >= src.pay_time AND DATEDIFF(SECOND, src.pay_time, src.send_time) <= 24 * 3600 THEN 0 WHEN src.fetch_time IS NOT NULL AND src.fetch_time >= src.pay_time AND DATEDIFF(SECOND, src.pay_time, src.fetch_time) <= 24 * 3600 THEN 0 ELSE 1 END AS is_timeout INTO #pickup_scored FROM #pickup_enriched src; SELECT stat_date, shop_name, warehouse_name, purchaser_name, supplier_name, sku_id, COUNT_BIG(*) AS total_count, SUM(CASE WHEN is_timeout = 1 THEN 1 ELSE 0 END) AS timeout_count FROM #pickup_scored WHERE is_excluded = 0 GROUP BY stat_date, shop_name, warehouse_name, purchaser_name, supplier_name, sku_id ORDER BY stat_date, total_count DESC; `; function parseListParam(value) { if (!value) { return []; } if (Array.isArray(value)) { return [...new Set(value.flatMap((item) => String(item).split(',')).map((item) => item.trim()).filter(Boolean))]; } return [...new Set(String(value).split(',').map((item) => item.trim()).filter(Boolean))]; } function toNum(value) { return Number(value) || 0; } function calcRate(totalCount, timeoutCount) { if (!totalCount) { return null; } return Number((((totalCount - timeoutCount) / totalCount) * 100).toFixed(2)); } function clampEndDateToYesterday(endDate) { const requestedEnd = dayjs(endDate); const yesterday = dayjs().subtract(1, 'day').startOf('day'); return requestedEnd.isAfter(yesterday, 'day') ? yesterday.format('YYYY-MM-DD') : requestedEnd.format('YYYY-MM-DD'); } function buildPeriods(startDate, endDate, mode) { const start = dayjs(startDate); const end = dayjs(endDate); const periods = []; let cursor = start.startOf(mode === 'monthly' ? 'month' : 'day'); while (cursor.isSame(end, mode === 'monthly' ? 'month' : 'day') || cursor.isBefore(end, mode === 'monthly' ? 'month' : 'day')) { periods.push( mode === 'monthly' ? { key: cursor.format('YYYY-MM'), label: cursor.format('YYYY年M月'), } : { key: cursor.format('YYYY-MM-DD'), label: cursor.format('M月D日'), } ); cursor = cursor.add(1, mode === 'monthly' ? 'month' : 'day'); } return periods; } function bucketKey(statDate, mode) { return mode === 'monthly' ? dayjs(statDate).format('YYYY-MM') : dayjs(statDate).format('YYYY-MM-DD'); } function applyFilters(rows, filters) { return rows.filter((row) => { if (filters.shops.length > 0 && !filters.shops.includes(row.shopName)) return false; if (filters.warehouses.length > 0 && !filters.warehouses.includes(row.warehouseName)) return false; if (filters.purchasers.length > 0 && !filters.purchasers.includes(row.purchaserName)) return false; if (filters.suppliers.length > 0 && !filters.suppliers.includes(row.supplierName)) return false; if (filters.skuIds.length > 0 && !filters.skuIds.includes(row.skuId)) return false; return true; }); } function buildFilterOptions(rows) { const unique = (selector) => [...new Set(rows.map(selector).filter(Boolean))].sort((a, b) => a.localeCompare(b, 'zh-CN')); return { shops: unique((row) => row.shopName), warehouses: unique((row) => row.warehouseName), purchasers: unique((row) => row.purchaserName), suppliers: unique((row) => row.supplierName), skuIds: unique((row) => row.skuId), }; } function aggregateTable(rows, periods, mode, config) { const map = new Map(); const ensureRow = (key, base) => { if (!map.has(key)) { const row = { key, ...base, totalCount: 0, timeoutCount: 0, periodMap: {}, }; for (const period of periods) { row.periodMap[period.key] = { totalCount: 0, timeoutCount: 0, rate: null, }; } map.set(key, row); } return map.get(key); }; for (const item of rows) { const period = bucketKey(item.statDate, mode); const key = config.buildKey(item); const row = ensureRow(key, config.project(item)); row.totalCount += item.totalCount; row.timeoutCount += item.timeoutCount; if (!row.periodMap[period]) { row.periodMap[period] = { totalCount: 0, timeoutCount: 0, rate: null }; } row.periodMap[period].totalCount += item.totalCount; row.periodMap[period].timeoutCount += item.timeoutCount; } const finalize = (row) => { const orderedRates = periods.map((period) => { const bucket = row.periodMap[period.key] || { totalCount: 0, timeoutCount: 0 }; bucket.rate = calcRate(bucket.totalCount, bucket.timeoutCount); return bucket.rate; }); const lastRate = [...orderedRates].reverse().find((value) => value !== null); const previousRate = [...orderedRates].reverse().find((value, index, arr) => value !== null && arr.indexOf(value) !== arr.lastIndexOf(value)); const firstRate = orderedRates.find((value) => value !== null); row.totalRate = calcRate(row.totalCount, row.timeoutCount); row.trendDelta = firstRate !== null && lastRate !== null ? Number((lastRate - firstRate).toFixed(2)) : null; row.momDelta = orderedRates.length >= 2 && orderedRates[orderedRates.length - 2] !== null && orderedRates[orderedRates.length - 1] !== null ? Number((orderedRates[orderedRates.length - 1] - orderedRates[orderedRates.length - 2]).toFixed(2)) : previousRate !== undefined && previousRate !== null && lastRate !== null ? Number((lastRate - previousRate).toFixed(2)) : null; return row; }; const dataRows = Array.from(map.values()).map(finalize).sort((a, b) => { const rateA = Number.isFinite(a.totalRate) ? a.totalRate : Number.POSITIVE_INFINITY; const rateB = Number.isFinite(b.totalRate) ? b.totalRate : Number.POSITIVE_INFINITY; if (rateA !== rateB) { return rateA - rateB; } return b.totalCount - a.totalCount; }); const totalRow = ensureRow('__total__', config.totalProject()); for (const item of rows) { totalRow.totalCount += item.totalCount; totalRow.timeoutCount += item.timeoutCount; const period = bucketKey(item.statDate, mode); if (!totalRow.periodMap[period]) { totalRow.periodMap[period] = { totalCount: 0, timeoutCount: 0, rate: null }; } totalRow.periodMap[period].totalCount += item.totalCount; totalRow.periodMap[period].timeoutCount += item.timeoutCount; } const finalizedTotalRow = finalize(totalRow); const limit = config.limit || dataRows.length; return [finalizedTotalRow, ...dataRows.slice(0, limit)]; } function buildBoard(rows, startDate, endDate, mode) { const periods = buildPeriods(startDate, endDate, mode); return { periods, shopRows: aggregateTable(rows, periods, mode, { buildKey: (row) => `shop:${row.shopName}`, project: (row) => ({ shopName: row.shopName }), totalProject: () => ({ shopName: '总计' }), limit: 120, }), warehouseRows: aggregateTable(rows, periods, mode, { buildKey: (row) => `warehouse:${row.warehouseName}`, project: (row) => ({ warehouseName: row.warehouseName }), totalProject: () => ({ warehouseName: '总计' }), limit: 80, }), purchaserRows: aggregateTable(rows, periods, mode, { buildKey: (row) => `purchaser:${row.purchaserName}`, project: (row) => ({ purchaserName: row.purchaserName }), totalProject: () => ({ purchaserName: '总计' }), limit: 60, }), supplierRows: aggregateTable(rows, periods, mode, { buildKey: (row) => `supplier:${row.purchaserName}:${row.supplierName}`, project: (row) => ({ purchaserName: row.purchaserName, supplierName: row.supplierName }), totalProject: () => ({ purchaserName: '', supplierName: '总计' }), limit: 200, }), skuRows: aggregateTable(rows, periods, mode, { buildKey: (row) => `sku:${row.purchaserName}:${row.supplierName}:${row.skuId}`, project: (row) => ({ purchaserName: row.purchaserName, supplierName: row.supplierName, skuId: row.skuId, }), totalProject: () => ({ purchaserName: '', supplierName: '', skuId: '总计' }), limit: 300, }), }; } function normalizeSnapshotRows(rows = [], periods = []) { const safePeriods = Array.isArray(periods) ? periods : []; const recomputeRow = (row) => { const nextRow = { ...row, periodMap: { ...(row.periodMap || {}) }, }; let totalCount = 0; let timeoutCount = 0; const rates = []; for (const period of safePeriods) { const bucket = nextRow.periodMap[period.key] || { totalCount: 0, timeoutCount: 0, rate: null }; const normalizedBucket = { totalCount: Number(bucket.totalCount || 0), timeoutCount: Number(bucket.timeoutCount || 0), rate: calcRate(Number(bucket.totalCount || 0), Number(bucket.timeoutCount || 0)), }; nextRow.periodMap[period.key] = normalizedBucket; totalCount += normalizedBucket.totalCount; timeoutCount += normalizedBucket.timeoutCount; rates.push(normalizedBucket.rate); } nextRow.totalCount = totalCount; nextRow.timeoutCount = timeoutCount; nextRow.totalRate = calcRate(totalCount, timeoutCount); const validRates = rates.filter((value) => value !== null); const firstRate = validRates[0] ?? null; const lastRate = validRates.length > 0 ? validRates[validRates.length - 1] : null; const previousRate = validRates.length > 1 ? validRates[validRates.length - 2] : null; nextRow.trendDelta = firstRate !== null && lastRate !== null ? Number((lastRate - firstRate).toFixed(2)) : null; nextRow.momDelta = previousRate !== null && lastRate !== null ? Number((lastRate - previousRate).toFixed(2)) : null; return nextRow; }; const normalizedRows = rows.map(recomputeRow); const [totalRow, ...dataRows] = normalizedRows; const sortedRows = dataRows.sort((a, b) => { const rateA = Number.isFinite(a.totalRate) ? a.totalRate : Number.POSITIVE_INFINITY; const rateB = Number.isFinite(b.totalRate) ? b.totalRate : Number.POSITIVE_INFINITY; if (rateA !== rateB) { return rateA - rateB; } return b.totalCount - a.totalCount; }); return totalRow ? [totalRow, ...sortedRows] : sortedRows; } function normalizeSnapshotData(data, effectiveEndDate) { if (!data) { return null; } const trimPeriods = (board) => { const nextPeriods = (board?.periods || []).filter((period) => period.key <= effectiveEndDate); return { periods: nextPeriods, shopRows: normalizeSnapshotRows(board?.shopRows || [], nextPeriods), warehouseRows: normalizeSnapshotRows(board?.warehouseRows || [], nextPeriods), purchaserRows: normalizeSnapshotRows(board?.purchaserRows || [], nextPeriods), supplierRows: normalizeSnapshotRows(board?.supplierRows || [], nextPeriods), skuRows: normalizeSnapshotRows(board?.skuRows || [], nextPeriods), }; }; return { ...data, daily: trimPeriods(data.daily), monthly: trimPeriods(data.monthly), meta: { ...(data.meta || {}), endDate: effectiveEndDate, }, }; } function normalizeRows(rows) { return rows.map((row) => ({ statDate: dayjs(row.stat_date).format('YYYY-MM-DD'), shopName: row.shop_name || '未知店铺', warehouseName: row.warehouse_name || UNKNOWN_WAREHOUSE, purchaserName: row.purchaser_name || UNKNOWN_PURCHASER, supplierName: row.supplier_name || UNKNOWN_SUPPLIER, skuId: row.sku_id || '未知商品编码', totalCount: toNum(row.total_count), timeoutCount: toNum(row.timeout_count), })); } async function queryLogisticsPickupBoard({ startDate, endDate, shops, warehouses, purchasers, suppliers, skuIds }) { const effectiveEndDate = clampEndDateToYesterday(endDate); const rawRows = await cloudDb.query(PICKUP_BOARD_SQL, { startDate, endDate: effectiveEndDate }); const rows = normalizeRows(rawRows); const filters = { shops: parseListParam(shops), warehouses: parseListParam(warehouses), purchasers: parseListParam(purchasers), suppliers: parseListParam(suppliers), skuIds: parseListParam(skuIds), }; const filteredRows = applyFilters(rows, filters); return { filters: buildFilterOptions(filteredRows.length > 0 ? filteredRows : rows), daily: buildBoard(filteredRows, startDate, effectiveEndDate, 'daily'), monthly: buildBoard(filteredRows, startDate, effectiveEndDate, 'monthly'), meta: { startDate, endDate: effectiveEndDate, requestedEndDate: endDate, filteredCount: filteredRows.length, totalCount: rows.length, }, }; } function getSnapshotPath(startDate, endDate) { return path.join(SNAPSHOT_DIR, `${startDate}_${endDate}.json`); } function readLogisticsPickupBoardSnapshot(startDate, endDate) { const filePath = getSnapshotPath(startDate, endDate); if (!fs.existsSync(filePath)) { return null; } return JSON.parse(fs.readFileSync(filePath, 'utf8')); } function writeLogisticsPickupBoardSnapshot(startDate, endDate, data) { fs.mkdirSync(SNAPSHOT_DIR, { recursive: true }); const filePath = getSnapshotPath(startDate, endDate); fs.writeFileSync(filePath, JSON.stringify(data), 'utf8'); return filePath; } module.exports = { getSnapshotPath, queryLogisticsPickupBoard, readLogisticsPickupBoardSnapshot, writeLogisticsPickupBoardSnapshot, normalizeSnapshotData, };