cangku/backend/src/services/aftersalesRefundDetail.js

281 lines
9.7 KiB
JavaScript

'use strict';
const dayjs = require('dayjs');
const cloudDb = require('../config/cloudDatabase');
const database = require('../config/database');
const { applyAftersalesRefundExportOverrides } = require('./aftersalesRefundExportOverride');
const SYNC_VERSION = 'aftersales-refund-union-v2';
const INSERT_BATCH_SIZE = 500;
const TARGET_COLUMNS = [
'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',
];
function normalizeDate(value, fallback = dayjs().subtract(1, 'day').format('YYYY-MM-DD')) {
const parsed = dayjs(value || fallback);
if (!parsed.isValid()) {
throw new Error(`Invalid date: ${value}`);
}
return parsed.format('YYYY-MM-DD');
}
function listDatesInRange(startDate, endDate) {
const start = dayjs(normalizeDate(startDate));
const end = dayjs(normalizeDate(endDate || startDate));
if (end.isBefore(start, 'day')) {
throw new Error(`Invalid date range: ${startDate} -> ${endDate}`);
}
const dates = [];
let cursor = start;
while (!cursor.isAfter(end, 'day')) {
dates.push(cursor.format('YYYY-MM-DD'));
cursor = cursor.add(1, 'day');
}
return dates;
}
function decimalOrZero(value) {
const number = Number(value);
return Number.isFinite(number) ? number : 0;
}
function textOrEmpty(value) {
return value === null || value === undefined ? '' : String(value);
}
function toInsertRow(row) {
return TARGET_COLUMNS.map((column) => {
if (['freight', 'refund', 'payment', 'free_amount', 'total_refund_money'].includes(column)) {
return decimalOrZero(row[column]);
}
if (['created', 'modified', 'confirm_date'].includes(column)) {
return row[column] || null;
}
if (column === 'stat_date') {
return normalizeDate(row[column]);
}
return textOrEmpty(row[column]);
});
}
const UNION_SQL = `
WITH unified_refunds AS (
SELECT
CONVERT(varchar(10), CONVERT(date, created), 120) AS stat_date,
N'jst_returnAndRefundInfo' AS source_key,
N'dbo.jst_returnAndRefundInfo' AS source_table,
CAST(as_id AS nvarchar(80)) AS as_id,
CAST(outer_as_id AS nvarchar(120)) AS outer_as_id,
CAST(o_id AS nvarchar(80)) AS o_id,
CAST(so_id AS nvarchar(120)) AS so_id,
CAST(type AS nvarchar(50)) AS type,
CAST(node AS nvarchar(100)) AS node,
CAST(order_labelsText AS nvarchar(max)) AS order_labels,
CAST(shop_name AS nvarchar(255)) AS shop_name,
TRY_CONVERT(decimal(18,2), freight) AS freight,
TRY_CONVERT(decimal(18,2), refund) AS refund,
TRY_CONVERT(decimal(18,2), payment) AS payment,
TRY_CONVERT(decimal(18,2), free_amount) AS free_amount,
(
COALESCE(TRY_CONVERT(decimal(18,2), freight), 0)
+ COALESCE(TRY_CONVERT(decimal(18,2), refund), 0)
- COALESCE(TRY_CONVERT(decimal(18,2), payment), 0)
- COALESCE(TRY_CONVERT(decimal(18,2), free_amount), 0)
) AS total_refund_money,
CAST(shop_status AS nvarchar(100)) AS shop_status,
CAST(question_type AS nvarchar(255)) AS question_type,
CAST(result AS nvarchar(255)) AS process_result,
CAST(status AS nvarchar(100)) AS status,
CONVERT(varchar(19), created, 120) AS created,
CONVERT(varchar(19), modified, 120) AS modified,
CAST(l_id AS nvarchar(120)) AS l_id,
CONVERT(varchar(19), confirm_date, 120) AS confirm_date,
CAST(creator_name AS nvarchar(100)) AS creator_name,
CAST(N'' AS nvarchar(255)) AS original_order_no,
CAST(N'' AS nvarchar(120)) AS original_courier_name,
CAST(N'' AS nvarchar(120)) AS original_waybill_no
FROM dbo.jst_returnAndRefundInfo WITH (NOLOCK)
WHERE shop_site <> N'拼多多'
AND CONVERT(date, created) = @statDate
UNION ALL
SELECT
CONVERT(varchar(10), CONVERT(date, created), 120) AS stat_date,
N'jst_qmcust_refundlist' AS source_key,
N'dbo.jst_qmcust_refundlist' AS source_table,
CAST(as_id AS nvarchar(80)) AS as_id,
CAST(outer_as_id AS nvarchar(120)) AS outer_as_id,
CAST(o_id AS nvarchar(80)) AS o_id,
CAST(so_id AS nvarchar(120)) AS so_id,
CAST(type AS nvarchar(50)) AS type,
CAST(node AS nvarchar(100)) AS node,
CAST(order_label AS nvarchar(max)) AS order_labels,
CAST(shop_name AS nvarchar(255)) AS shop_name,
TRY_CONVERT(decimal(18,2), freight) AS freight,
TRY_CONVERT(decimal(18,2), refund) AS refund,
TRY_CONVERT(decimal(18,2), payment) AS payment,
TRY_CONVERT(decimal(18,2), free_amount) AS free_amount,
(
COALESCE(TRY_CONVERT(decimal(18,2), freight), 0)
+ COALESCE(TRY_CONVERT(decimal(18,2), refund), 0)
- COALESCE(TRY_CONVERT(decimal(18,2), payment), 0)
- COALESCE(TRY_CONVERT(decimal(18,2), free_amount), 0)
) AS total_refund_money,
CAST(shop_status AS nvarchar(100)) AS shop_status,
CAST(question_type AS nvarchar(255)) AS question_type,
CAST(N'' AS nvarchar(255)) AS process_result,
CAST(status AS nvarchar(100)) AS status,
CONVERT(varchar(19), created, 120) AS created,
CONVERT(varchar(19), modified, 120) AS modified,
CAST(l_id AS nvarchar(120)) AS l_id,
CONVERT(varchar(19), confirm_date, 120) AS confirm_date,
CAST(creator_name AS nvarchar(100)) AS creator_name,
CAST(N'' AS nvarchar(255)) AS original_order_no,
CAST(N'' AS nvarchar(120)) AS original_courier_name,
CAST(N'' AS nvarchar(120)) AS original_waybill_no
FROM dbo.jst_qmcust_refundlist WITH (NOLOCK)
WHERE CONVERT(date, created) = @statDate
UNION ALL
SELECT
CONVERT(varchar(10), CONVERT(date, created), 120) AS stat_date,
N'jst_refund_pdd_fz_view' AS source_key,
N'dbo.jst_refund_pdd_fz_view' AS source_table,
CAST(as_id AS nvarchar(80)) AS as_id,
CAST(outer_as_id AS nvarchar(120)) AS outer_as_id,
CAST(o_id AS nvarchar(80)) AS o_id,
CAST(so_id AS nvarchar(120)) AS so_id,
CAST(type AS nvarchar(50)) AS type,
CAST(node AS nvarchar(100)) AS node,
CAST(order_labels_str AS nvarchar(max)) AS order_labels,
CAST(shop_name AS nvarchar(255)) AS shop_name,
TRY_CONVERT(decimal(18,2), freight) AS freight,
TRY_CONVERT(decimal(18,2), refund) AS refund,
TRY_CONVERT(decimal(18,2), payment) AS payment,
TRY_CONVERT(decimal(18,2), free_amount) AS free_amount,
(
COALESCE(TRY_CONVERT(decimal(18,2), freight), 0)
+ COALESCE(TRY_CONVERT(decimal(18,2), refund), 0)
- COALESCE(TRY_CONVERT(decimal(18,2), payment), 0)
- COALESCE(TRY_CONVERT(decimal(18,2), free_amount), 0)
) AS total_refund_money,
CAST(shop_status AS nvarchar(100)) AS shop_status,
CAST(question_type AS nvarchar(255)) AS question_type,
CAST(result AS nvarchar(255)) AS process_result,
CAST(status AS nvarchar(100)) AS status,
CONVERT(varchar(19), created, 120) AS created,
CONVERT(varchar(19), modified, 120) AS modified,
CAST(l_id AS nvarchar(120)) AS l_id,
CONVERT(varchar(19), confirm_date, 120) AS confirm_date,
CAST(creator_name AS nvarchar(100)) AS creator_name,
CAST(N'' AS nvarchar(255)) AS original_order_no,
CAST(N'' AS nvarchar(120)) AS original_courier_name,
CAST(N'' AS nvarchar(120)) AS original_waybill_no
FROM dbo.jst_refund_pdd_fz_view
WHERE CONVERT(date, created) = @statDate
)
SELECT *
FROM unified_refunds
ORDER BY created ASC, source_key ASC, as_id ASC;
`;
async function writeSyncLog({ statDate, rowCount, status, errorMessage = '' }) {
await database.local.query(
`INSERT INTO aftersales_refund_detail_sync
(stat_date, sync_version, row_count, status, error_message, synced_at)
VALUES (?, ?, ?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
sync_version = VALUES(sync_version),
row_count = VALUES(row_count),
status = VALUES(status),
error_message = VALUES(error_message),
synced_at = NOW()`,
[statDate, SYNC_VERSION, rowCount, status, errorMessage.slice(0, 1000)]
);
}
async function insertRows(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_detail_read (${columnSql}) VALUES ?`,
[batch]
);
}
}
async function refreshAftersalesRefundDetailDate(inputDate) {
const statDate = normalizeDate(inputDate);
console.log(`[aftersales-refund-detail] sync start ${statDate}`);
try {
const rows = await cloudDb.query(UNION_SQL, { statDate });
await database.local.query('DELETE FROM aftersales_refund_detail_read WHERE stat_date = ?', [statDate]);
await insertRows(rows);
await applyAftersalesRefundExportOverrides(statDate);
await writeSyncLog({ statDate, rowCount: rows.length, status: 'success' });
console.log(`[aftersales-refund-detail] sync done ${statDate}, rows=${rows.length}`);
return { statDate, rowCount: rows.length };
} catch (error) {
await writeSyncLog({
statDate,
rowCount: 0,
status: 'failed',
errorMessage: error.message || String(error),
}).catch((logError) => {
console.warn('[aftersales-refund-detail] sync log failed:', logError.message || logError);
});
throw error;
}
}
async function refreshAftersalesRefundDetailRange(startDate, endDate = startDate) {
const results = [];
for (const statDate of listDatesInRange(startDate, endDate)) {
results.push(await refreshAftersalesRefundDetailDate(statDate));
}
return results;
}
module.exports = {
SYNC_VERSION,
refreshAftersalesRefundDetailDate,
refreshAftersalesRefundDetailRange,
};