#!/usr/bin/env node require('dotenv').config(); const dayjs = require('dayjs'); const database = require('../src/config/database'); const cloudDb = require('../src/config/cloudDatabase'); const redis = require('../src/config/redis'); const { refreshAllCache } = require('../src/jobs/dailyRefresh'); const localDb = database.local; const PICKUP_EXCLUDED_LABEL_KEYWORDS = [ '\u914d\u4ef6', '\u7ecf\u7ba1\u5927\u5355', '\u4e0d\u53d1', '\u7b49\u901a\u77e5', '\u5ef6\u671f', '\u5ef6\u8fdf', '\u6b66\u8fdb\u5b89\u80fd', '\u4e2d\u94c1', '\u7269\u6d41', '\u81ea\u63d0', '\u5916\u8d38', '\u91c7\u8d2d\u5355', '\u5907\u8d27', '\u5b9a\u5236', '\u4e2d\u8f6c\u96c6\u8fd0', '\u4e2d\u8f6c\u8ba2\u5355', '\u65b0\u7586\u897f\u85cf\u5355', 'ECLP\u8ba2\u5355', ]; const PICKUP_EXCLUDED_COURIER_KEYWORDS = [ '\u6b66\u8fdb\u5b89\u80fd', '\u4e2d\u94c1', '\u7269\u6d41', '\u81ea\u63d0', '\u7ebf\u4e0b\u5feb\u9012', '\u4e13\u7ebf', ]; const PICKUP_EXCLUDED_REMARK_KEYWORDS = [ 'bbff', 'ccdd', '\u8f6cX00006', ]; const PICKUP_TIMELY_NO_FETCH_COURIER_KEYWORDS = [ '\u4eac\u4e1c\u5feb\u8fd0', '\u4eac\u4e1c\u81ea\u8425', '\u987a\u4e30', ]; const PICKUP_TIMELY_LABEL_KEYWORDS = [ 'CLPS\u8ba2\u5355', 'ECLP\u8ba2\u5355', ]; const UNKNOWN_WAREHOUSE = '\u672a\u77e5\u4ed3\u5e93'; const UNKNOWN_SUPPLIER = '\u672a\u77e5\u4f9b\u5e94\u5546'; const UNKNOWN_PROVINCE = '\u672a\u77e5\u7701\u4efd'; const UNKNOWN_COURIER = '\u672a\u77e5\u5feb\u9012'; const NO_AFTERSALE = '\u6682\u65e0\u552e\u540e'; const NO_REFUND_REQUEST = '\u672a\u7533\u8bf7\u9000\u6b3e'; const q = (value) => `N'${String(value).replace(/'/g, "''")}'`; const buildSqlLikeAny = (column, keywords) => 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 BACKFILL_PICKUP_SQL = ` SET NOCOUNT ON; SELECT 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 undefined5 IS NOT NULL AND undefined5 LIKE @payDatePrefix; CREATE NONCLUSTERED INDEX IX_afs_raw_dedup ON #afs_raw(inner_order_id, sku_id); WITH ranked AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY 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 inner_order_id IS NOT NULL AND inner_order_id <> '' AND sku_id IS NOT NULL AND sku_id <> '' ) SELECT 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); 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, MAX(NULLIF(LTRIM(RTRIM(s.supplier_name)), '')) AS supplier_name INTO #sku_supplier FROM dbo.jst_sku s INNER JOIN #sku_keys k ON k.sku_id = NULLIF(LTRIM(RTRIM(s.sku_id)), '') GROUP BY NULLIF(LTRIM(RTRIM(s.sku_id)), ''); CREATE UNIQUE CLUSTERED INDEX IX_sku_supplier ON #sku_supplier(sku_id); SELECT 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 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(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_supplier s ON d.sku_id = s.sku_id 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.*, CASE WHEN (${excludedLabelSql}) THEN 1 WHEN ISNULL(src.shop_responsible_department, N'') LIKE N'%灞变笢椤圭洰缁?%' THEN 1 WHEN src.supplier_name IN (${q('\u865a\u62df')}, ${q('\u666f\u74f4\u5341\u65b9')}) THEN 1 WHEN src.order_status_name IN (${q('\u88ab\u62c6\u5206')}, ${q('\u88ab\u5408\u5e76')}) 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('%\u4eac\u559c%')} AND ISNULL(src.aftersale_status_name, ${q(NO_AFTERSALE)}) <> ${q(NO_AFTERSALE)} THEN 1 WHEN ISNULL(src.shop_name, N'') LIKE ${q('%\u6837\u54c1\u4e13\u7528\u5e97\u94fa%')} THEN 1 WHEN ISNULL(src.order_status_name, N'') LIKE ${q('%\u53d6\u6d88%')} AND src.send_time IS NULL AND ISNULL(src.aftersale_status_name, ${q(NO_AFTERSALE)}) IN (N'', ${q(NO_AFTERSALE)}, ${q(NO_REFUND_REQUEST)}) 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('%\u9884\u552e%')} OR ISNULL(src.order_status_name, N'') LIKE ${q('%\u9884\u552e%')} 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('%\u4eac\u559c%')} 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, CASE WHEN src.fetch_time IS NOT NULL AND src.pay_time IS NOT NULL AND src.fetch_time >= src.pay_time THEN CAST(DATEDIFF(SECOND, src.pay_time, src.fetch_time) AS DECIMAL(20, 2)) / 3600.0 ELSE NULL END AS pickup_hours, CASE WHEN src.receiver_address LIKE ${q('%-%-%')} THEN REVERSE(LEFT(REVERSE(src.receiver_address), CHARINDEX('-', REVERSE(src.receiver_address)) - 1)) ELSE N'' END AS district INTO #pickup_scored FROM #pickup_enriched src; SELECT * INTO #pickup_valid FROM #pickup_scored WHERE is_excluded = 0; SELECT COUNT_BIG(*) AS total_count, SUM(CASE WHEN is_timeout = 1 THEN 1 ELSE 0 END) AS timeout_count, CAST(ROUND(AVG(pickup_hours), 2) AS DECIMAL(10, 2)) AS avg_hours FROM #pickup_valid; SELECT ISNULL(NULLIF(warehouse_name, N''), ${q(UNKNOWN_WAREHOUSE)}) AS warehouse_name, COUNT_BIG(*) AS total_count, SUM(CASE WHEN is_timeout = 1 THEN 1 ELSE 0 END) AS timeout_count, CAST(ROUND(AVG(pickup_hours), 2) AS DECIMAL(10, 2)) AS avg_hours FROM #pickup_valid GROUP BY ISNULL(NULLIF(warehouse_name, N''), ${q(UNKNOWN_WAREHOUSE)}); SELECT ISNULL(NULLIF(supplier_name, N''), ${q(UNKNOWN_SUPPLIER)}) AS supplier_name, COUNT_BIG(*) AS total_count, SUM(CASE WHEN is_timeout = 1 THEN 1 ELSE 0 END) AS timeout_count, CAST(ROUND(AVG(pickup_hours), 2) AS DECIMAL(10, 2)) AS avg_hours FROM #pickup_valid GROUP BY ISNULL(NULLIF(supplier_name, N''), ${q(UNKNOWN_SUPPLIER)}); SELECT ISNULL(NULLIF(province, N''), ${q(UNKNOWN_PROVINCE)}) AS province, COUNT_BIG(*) AS total_count, SUM(CASE WHEN is_timeout = 1 THEN 1 ELSE 0 END) AS timeout_count, CAST(ROUND(AVG(pickup_hours), 2) AS DECIMAL(10, 2)) AS avg_hours FROM #pickup_valid GROUP BY ISNULL(NULLIF(province, N''), ${q(UNKNOWN_PROVINCE)}); SELECT ISNULL(NULLIF(courier_name, N''), ${q(UNKNOWN_COURIER)}) AS courier_name, COUNT_BIG(*) AS total_count, SUM(CASE WHEN is_timeout = 1 THEN 1 ELSE 0 END) AS timeout_count, CAST(ROUND(AVG(pickup_hours), 2) AS DECIMAL(10, 2)) AS avg_hours FROM #pickup_valid GROUP BY ISNULL(NULLIF(courier_name, N''), ${q(UNKNOWN_COURIER)}); SELECT ISNULL(NULLIF(province, N''), ${q(UNKNOWN_PROVINCE)}) AS province, ISNULL(NULLIF(city, N''), N'') AS city, ISNULL(NULLIF(district, N''), N'') AS district, ISNULL(NULLIF(courier_name, N''), ${q(UNKNOWN_COURIER)}) AS courier_name, COUNT_BIG(*) AS total_count, SUM(CASE WHEN is_timeout = 1 THEN 1 ELSE 0 END) AS timeout_count, CAST(ROUND(AVG(pickup_hours), 2) AS DECIMAL(10, 2)) AS avg_hours FROM #pickup_valid GROUP BY ISNULL(NULLIF(province, N''), ${q(UNKNOWN_PROVINCE)}), ISNULL(NULLIF(city, N''), N''), ISNULL(NULLIF(district, N''), N''), ISNULL(NULLIF(courier_name, N''), ${q(UNKNOWN_COURIER)}); `; const TABLE_DELETES = [ 'summary_daily_pickup_region_courier', 'summary_daily_pickup_courier', 'summary_daily_pickup_region', 'summary_daily_pickup_supplier', 'summary_daily_pickup_warehouse', 'summary_daily_pickup', ]; const toInt = (value) => Number.parseInt(value, 10) || 0; const toFloat = (value) => Number.parseFloat(value) || 0; const calcPickupRate = (totalCount, timeoutCount) => ( totalCount > 0 ? Number((((totalCount - timeoutCount) / totalCount) * 100).toFixed(2)) : 0 ); const TRANSIENT_LOCAL_WRITE_ERROR_CODES = new Set([ 'ETIMEDOUT', 'ECONNRESET', 'ECONNREFUSED', 'EPIPE', 'PROTOCOL_CONNECTION_LOST', 'PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR', 'PROTOCOL_ENQUEUE_AFTER_QUIT', 'PROTOCOL_PACKETS_OUT_OF_ORDER', ]); const isDateString = (value) => /^\d{4}-\d{2}-\d{2}$/.test(value) && dayjs(value).format('YYYY-MM-DD') === value; const buildPayDatePrefix = (statDate) => `${dayjs(statDate).subtract(1, 'day').format('YYYY/M/D')}%`; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const isTransientLocalWriteError = (error) => { if (!error) { return false; } const message = String(error.message || '').toLowerCase(); const code = String(error.code || ''); return ( TRANSIENT_LOCAL_WRITE_ERROR_CODES.has(code) || message.includes('timed out') || message.includes('closed state') || message.includes('connection lost') || message.includes('socket hang up') ); }; const chunkRows = (rows, size) => { const chunks = []; for (let index = 0; index < rows.length; index += size) { chunks.push(rows.slice(index, index + size)); } return chunks; }; const buildInsertSql = (tableName, columns, rowCount) => { const valuePlaceholder = `(${columns.map(() => '?').join(', ')})`; const placeholders = new Array(rowCount).fill(valuePlaceholder).join(', '); const quotedColumns = columns.map((column) => `\`${column}\``).join(', '); return `INSERT INTO ${tableName} (${quotedColumns}) VALUES ${placeholders}`; }; const executeInsertChunks = async (connection, tableName, columns, rows, chunkSize = 500) => { if (rows.length === 0) { return; } for (const chunk of chunkRows(rows, chunkSize)) { const sql = buildInsertSql(tableName, columns, chunk.length); const params = chunk.flat(); await connection.query(sql, params); } }; const normalizeOverallRow = (statDate, row = {}) => { const totalCount = toInt(row.total_count); const timeoutCount = toInt(row.timeout_count); if (totalCount === 0 && timeoutCount === 0) { return null; } return [ statDate, totalCount, timeoutCount, calcPickupRate(totalCount, timeoutCount), toFloat(row.avg_hours), ]; }; const normalizeNamedRows = (statDate, rows, nameField) => rows .map((row) => { const totalCount = toInt(row.total_count); const timeoutCount = toInt(row.timeout_count); return [ statDate, row[nameField] || '', totalCount, timeoutCount, calcPickupRate(totalCount, timeoutCount), toFloat(row.avg_hours), ]; }); const normalizeRegionCourierRows = (statDate, rows) => rows .map((row) => { const totalCount = toInt(row.total_count); const timeoutCount = toInt(row.timeout_count); return [ statDate, row.province || UNKNOWN_PROVINCE, row.city || '', row.district || '', row.courier_name || UNKNOWN_COURIER, totalCount, timeoutCount, calcPickupRate(totalCount, timeoutCount), toFloat(row.avg_hours), ]; }); const fetchPickupRecordsets = async (statDate) => { const pool = await cloudDb.getPool(); const request = pool.request(); request.input('payDatePrefix', buildPayDatePrefix(statDate)); const result = await request.query(BACKFILL_PICKUP_SQL); return result.recordsets; }; const replacePickupSummaryForDateOnce = async (statDate, recordsets) => { const [ overallRows = [], warehouseRows = [], supplierRows = [], regionRows = [], courierRows = [], regionCourierRows = [], ] = recordsets; const overallRow = normalizeOverallRow(statDate, overallRows[0]); const warehouseInsertRows = normalizeNamedRows(statDate, warehouseRows, 'warehouse_name'); const supplierInsertRows = normalizeNamedRows(statDate, supplierRows, 'supplier_name'); const regionInsertRows = normalizeNamedRows(statDate, regionRows, 'province'); const courierInsertRows = normalizeNamedRows(statDate, courierRows, 'courier_name'); const regionCourierInsertRows = normalizeRegionCourierRows(statDate, regionCourierRows); const connection = await localDb.pool.getConnection(); try { await connection.beginTransaction(); for (const tableName of TABLE_DELETES) { await connection.query(`DELETE FROM ${tableName} WHERE stat_date = ?`, [statDate]); } if (overallRow) { await executeInsertChunks( connection, 'summary_daily_pickup', ['stat_date', 'total_order_count', 'timeout_count', 'pickup_rate', 'avg_pickup_hours'], [overallRow], 1 ); } await executeInsertChunks( connection, 'summary_daily_pickup_warehouse', ['stat_date', 'warehouse_name', 'total_order_count', 'timeout_count', 'pickup_rate', 'avg_pickup_hours'], warehouseInsertRows ); await executeInsertChunks( connection, 'summary_daily_pickup_supplier', ['stat_date', 'supplier_name', 'total_order_count', 'timeout_count', 'pickup_rate', 'avg_pickup_hours'], supplierInsertRows ); await executeInsertChunks( connection, 'summary_daily_pickup_region', ['stat_date', 'province', 'total_order_count', 'timeout_count', 'pickup_rate', 'avg_pickup_hours'], regionInsertRows ); await executeInsertChunks( connection, 'summary_daily_pickup_courier', ['stat_date', 'courier_name', 'total_order_count', 'timeout_count', 'pickup_rate', 'avg_pickup_hours'], courierInsertRows ); await executeInsertChunks( connection, 'summary_daily_pickup_region_courier', ['stat_date', 'province', 'city', 'district', 'courier_name', 'total_order_count', 'timeout_count', 'pickup_rate', 'avg_pickup_hours'], regionCourierInsertRows ); await connection.commit(); console.log( `[pickup-backfill] ${statDate} total=${overallRow ? overallRow[1] : 0} timeout=${overallRow ? overallRow[2] : 0} ` + `warehouse=${warehouseInsertRows.length} supplier=${supplierInsertRows.length} ` + `region=${regionInsertRows.length} courier=${courierInsertRows.length} regionCourier=${regionCourierInsertRows.length}` ); } catch (error) { try { await connection.rollback(); } catch (rollbackError) { console.warn(`[pickup-backfill] rollback failed for ${statDate}: ${rollbackError.message}`); } throw error; } finally { connection.release(); } }; const replacePickupSummaryForDate = async (statDate, recordsets) => { const maxAttempts = 3; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { try { await replacePickupSummaryForDateOnce(statDate, recordsets); return; } catch (error) { const shouldRetry = attempt < maxAttempts && isTransientLocalWriteError(error); if (!shouldRetry) { throw error; } console.warn( `[pickup-backfill] local write retry for ${statDate} (${attempt}/${maxAttempts}) due to: ${error.message}` ); await sleep(attempt * 1000); } } }; const backfillRange = async (startDate, endDate) => { const start = dayjs(startDate); const end = dayjs(endDate); const totalDays = end.diff(start, 'day') + 1; if (totalDays <= 0) { throw new Error(`Invalid date range: ${startDate} -> ${endDate}`); } for (let offset = 0; offset < totalDays; offset += 1) { const statDate = start.add(offset, 'day').format('YYYY-MM-DD'); const dayStart = Date.now(); console.log(`[pickup-backfill] start ${statDate} (${offset + 1}/${totalDays}) payPrefix=${buildPayDatePrefix(statDate)}`); const recordsets = await fetchPickupRecordsets(statDate); await replacePickupSummaryForDate(statDate, recordsets); console.log(`[pickup-backfill] done ${statDate} elapsed=${Date.now() - dayStart}ms`); } }; const main = async () => { const [startDate, endDate = startDate] = process.argv.slice(2); if (!isDateString(startDate) || !isDateString(endDate)) { throw new Error('Usage: node backend/scripts/backfillPickupHistory.js [YYYY-MM-DD]'); } console.log(`[pickup-backfill] range ${startDate} -> ${endDate}`); try { await backfillRange(startDate, endDate); try { await refreshAllCache(); console.log('[pickup-backfill] cache refresh done'); } catch (error) { console.warn(`[pickup-backfill] cache refresh skipped: ${error.message}`); } } finally { await Promise.allSettled([ cloudDb.close(), database.close(), redis.close(), ]); } }; main().catch((error) => { console.error('[pickup-backfill] failed'); console.error(error); process.exit(1); });