forked from lunanting/cangku
700 lines
25 KiB
JavaScript
700 lines
25 KiB
JavaScript
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,
|
|
};
|