cangku/backend/src/services/aftersalesRefundExportOverride.js

364 lines
9.7 KiB
JavaScript

'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,
};