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