'use strict'; const fs = require('fs'); const path = require('path'); const dayjs = require('dayjs'); const XLSX = require('xlsx'); const database = require('../config/database'); const INSERT_BATCH_SIZE = 500; const TARGET_COLUMNS = [ 'stat_date', 'as_id', 'outer_as_id', 'o_id', 'so_id', 'process_result', 'question_type', 'apply_time', 'original_order_no', 'original_courier_name', 'original_waybill_no', 'source_file', ]; function textOrEmpty(value) { if (value === null || value === undefined) { return ''; } return String(value).trim(); } function normalizeWaybill(value) { return textOrEmpty(value).replace(/^[@']+(?=[A-Za-z0-9])/u, ''); } function normalizeDate(value) { const parsed = dayjs(value); if (!parsed.isValid()) { throw new Error(`Invalid date: ${value}`); } return parsed.format('YYYY-MM-DD'); } function normalizeDateTime(value) { if (value === null || value === undefined || value === '') { return null; } if (value instanceof Date) { const parsed = dayjs(value); return parsed.isValid() ? parsed.format('YYYY-MM-DD HH:mm:ss') : null; } if (typeof value === 'number') { const decoded = XLSX.SSF.parse_date_code(value); if (!decoded) return null; return dayjs(new Date( decoded.y, decoded.m - 1, decoded.d, decoded.H, decoded.M, Math.floor(decoded.S) )).format('YYYY-MM-DD HH:mm:ss'); } const text = textOrEmpty(value).replace(/\//g, '-'); if (!text) return null; const parsed = dayjs(text); return parsed.isValid() ? parsed.format('YYYY-MM-DD HH:mm:ss') : null; } function normalizeSheetRow(row) { const normalized = {}; for (const [key, value] of Object.entries(row || {})) { normalized[textOrEmpty(key)] = value; } return normalized; } function getRowValue(row, keys) { for (const key of keys) { if (Object.prototype.hasOwnProperty.call(row, key)) { return row[key]; } } return ''; } function parseWorkbook(filePath) { const extension = path.extname(filePath).toLowerCase(); const workbook = extension === '.csv' || extension === '.tsv' ? XLSX.read(fs.readFileSync(filePath, 'utf8'), { type: 'string', raw: false, }) : XLSX.readFile(filePath, { cellDates: true, raw: false, }); const sheetName = workbook.SheetNames[0]; if (!sheetName) { throw new Error(`Workbook has no sheets: ${filePath}`); } return XLSX.utils.sheet_to_json(workbook.Sheets[sheetName], { defval: '', raw: false, }).map(normalizeSheetRow); } function toImportRow(row, sourceFile) { const applyTime = normalizeDateTime(getRowValue(row, ['申请日期'])); if (!applyTime) { return null; } const asId = textOrEmpty(getRowValue(row, ['售后单号'])); const outerAsId = textOrEmpty(getRowValue(row, ['退款编码'])); if (!asId && !outerAsId) { return null; } return { stat_date: normalizeDate(applyTime), as_id: asId, outer_as_id: outerAsId, o_id: textOrEmpty(getRowValue(row, ['内部订单号'])), so_id: textOrEmpty(getRowValue(row, ['线上订单号', '线上子订单编号'])), process_result: textOrEmpty(getRowValue(row, ['处理结果'])), question_type: textOrEmpty(getRowValue(row, ['问题类型'])), apply_time: applyTime, original_order_no: textOrEmpty(getRowValue(row, ['原始订单号'])), original_courier_name: textOrEmpty(getRowValue(row, ['原订单快递'])), original_waybill_no: normalizeWaybill(getRowValue(row, ['原订单快递单号'])), source_file: sourceFile, }; } function toInsertRow(row) { return TARGET_COLUMNS.map((column) => { if (column === 'apply_time') { return row[column] || null; } return row[column] || ''; }); } async function upsertRows(rows) { if (rows.length === 0) { return; } const columnSql = TARGET_COLUMNS.map((column) => `\`${column}\``).join(', '); for (let index = 0; index < rows.length; index += INSERT_BATCH_SIZE) { const batch = rows.slice(index, index + INSERT_BATCH_SIZE).map(toInsertRow); await database.local.query( `INSERT INTO aftersales_refund_export_override (${columnSql}) VALUES ? ON DUPLICATE KEY UPDATE o_id = VALUES(o_id), so_id = VALUES(so_id), process_result = VALUES(process_result), question_type = VALUES(question_type), apply_time = VALUES(apply_time), original_order_no = VALUES(original_order_no), original_courier_name = VALUES(original_courier_name), original_waybill_no = VALUES(original_waybill_no), source_file = VALUES(source_file), imported_at = NOW()`, [batch] ); } } function buildDateFilter(startDate, endDate) { if (!startDate && !endDate) { return () => true; } const start = startDate ? normalizeDate(startDate) : null; const end = endDate ? normalizeDate(endDate) : start; return (row) => { if (start && row.stat_date < start) return false; if (end && row.stat_date > end) return false; return true; }; } async function importAftersalesRefundExportOverrideFile(filePath, options = {}) { const resolvedPath = path.resolve(filePath); if (!fs.existsSync(resolvedPath)) { throw new Error(`File not found: ${resolvedPath}`); } const sourceFile = path.basename(resolvedPath); const shouldKeep = buildDateFilter(options.startDate, options.endDate); const rows = parseWorkbook(resolvedPath) .map((row) => toImportRow(row, sourceFile)) .filter(Boolean) .filter(shouldKeep); await upsertRows(rows); const dateSummary = rows.reduce((acc, row) => { acc[row.stat_date] = (acc[row.stat_date] || 0) + 1; return acc; }, {}); return { filePath: resolvedPath, rowCount: rows.length, dates: Object.keys(dateSummary).sort(), dateSummary, }; } function buildOverrideSetClause() { return ` t.process_result = CASE WHEN NULLIF(o.process_result, '') IS NOT NULL THEN o.process_result ELSE t.process_result END, t.original_order_no = CASE WHEN NULLIF(o.original_order_no, '') IS NOT NULL THEN o.original_order_no ELSE t.original_order_no END, t.original_courier_name = CASE WHEN NULLIF(o.original_courier_name, '') IS NOT NULL THEN o.original_courier_name ELSE t.original_courier_name END, t.original_waybill_no = CASE WHEN NULLIF(o.original_waybill_no, '') IS NOT NULL THEN o.original_waybill_no ELSE t.original_waybill_no END `; } async function applyAftersalesRefundExportOverrides(statDate) { const setClause = buildOverrideSetClause(); const updates = [ `UPDATE aftersales_refund_detail_read t JOIN aftersales_refund_export_override o ON t.stat_date = o.stat_date AND t.as_id <> '' AND o.as_id <> '' AND t.as_id = o.as_id SET ${setClause} WHERE t.stat_date = ?`, `UPDATE aftersales_refund_detail_read t JOIN aftersales_refund_export_override o ON t.stat_date = o.stat_date AND t.outer_as_id <> '' AND o.outer_as_id <> '' AND t.outer_as_id = o.outer_as_id SET ${setClause} WHERE t.stat_date = ? AND (t.process_result = '' OR t.original_waybill_no = '' OR t.original_courier_name = '')`, `UPDATE aftersales_refund_detail_read t JOIN aftersales_refund_export_override o ON t.stat_date = o.stat_date AND t.o_id <> '' AND o.o_id <> '' AND t.so_id <> '' AND o.so_id <> '' AND t.o_id = o.o_id AND t.so_id = o.so_id SET ${setClause} WHERE t.stat_date = ? AND (t.process_result = '' OR t.original_waybill_no = '' OR t.original_courier_name = '')`, ]; let affectedRows = 0; for (const sql of updates) { const result = await database.local.query(sql, [statDate]); affectedRows += Number(result.affectedRows || 0); } const insertMissingResult = await database.local.query( `INSERT INTO aftersales_refund_detail_read ( stat_date, source_key, source_table, as_id, outer_as_id, o_id, so_id, type, node, order_labels, shop_name, freight, refund, payment, free_amount, total_refund_money, shop_status, question_type, process_result, status, created, modified, l_id, confirm_date, creator_name, original_order_no, original_courier_name, original_waybill_no ) SELECT o.stat_date, 'aftersales_refund_export_override' AS source_key, CONCAT('local.', o.source_file) AS source_table, o.as_id, o.outer_as_id, o.o_id, o.so_id, '' AS type, '' AS node, '' AS order_labels, '' AS shop_name, 0 AS freight, 0 AS refund, 0 AS payment, 0 AS free_amount, 0 AS total_refund_money, '' AS shop_status, o.question_type, o.process_result, '' AS status, o.apply_time AS created, o.apply_time AS modified, '' AS l_id, NULL AS confirm_date, '' AS creator_name, o.original_order_no, o.original_courier_name, o.original_waybill_no FROM aftersales_refund_export_override o LEFT JOIN aftersales_refund_detail_read t ON t.stat_date = o.stat_date AND ( (o.as_id <> '' AND t.as_id = o.as_id) OR (o.outer_as_id <> '' AND t.outer_as_id = o.outer_as_id) OR (o.o_id <> '' AND o.so_id <> '' AND t.o_id = o.o_id AND t.so_id = o.so_id) ) WHERE o.stat_date = ? AND t.id IS NULL`, [statDate] ); return { statDate, affectedRows, insertedRows: Number(insertMissingResult.affectedRows || 0), }; } module.exports = { applyAftersalesRefundExportOverrides, importAftersalesRefundExportOverrideFile, };