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