fix(cost): 快照新单优先、费用口径与回填健壮性

- 成本快照拆热/冷双通道:新单走热通道分钟级入账,历史项冷通道轮询重算
- 通用成本只允许平台费/支付费自动全量,售后损失/其他必须按商品配置
- 比例费用按扣除已确认退款后的净收入计算
- 已入账 actual 快照不再被后续预估覆盖
- 受影响日期超过 31 天时按整段重算,不再静默丢弃;关账跳过会提示
- 通用费用解析不到时计数告警;已入队的成本缺失不再重复 upsert
- 快照替换与写入放入同一事务,避免事实任务读到空窗
This commit is contained in:
yml2213
2026-11-01 14:46:08 +08:00
parent 34832139b0
commit cf0d600c7d
4 changed files with 304 additions and 79 deletions
@@ -52,9 +52,11 @@ export async function runChannelAssignJob(job: JsonObject = {}) {
status: 'ok',
message:
`渠道判定:扫描 ${result.scanned},已定 ${result.assigned},` +
`冲突 ${result.conflicted},仍待定 ${result.stillPending},回填 ${refreshed} 天`,
`冲突 ${result.conflicted},仍待定 ${result.stillPending},回填 ${refreshed.days} 天` +
(refreshed.skippedClosed > 0 ? `(${refreshed.skippedClosed} 天已关账跳过)` : ''),
...result,
refreshedDays: refreshed,
refreshedDays: refreshed.days,
skippedClosedDays: refreshed.skippedClosed,
}
}
@@ -64,19 +66,49 @@ export async function runChannelAssignJob(job: JsonObject = {}) {
* 事实任务只回看最近几天;补配价格、迟到分类这类变更必须按受影响日期重算,
* 否则旧日期的毛利会一直停在缺失成本/未分类。
*/
async function refreshFactsForDates(dates: string[]): Promise<number> {
const unique = [...new Set(dates.filter(Boolean))].sort().slice(0, 31)
async function refreshFactsForDates(
dates: string[],
): Promise<{ days: number; skippedClosed: number }> {
const unique = [...new Set(dates.filter(Boolean))].sort()
if (unique.length === 0) return { days: 0, skippedClosed: 0 }
// 日期过多时按整段重算,避免截断导致部分日期永远不更新。
if (unique.length > 31) {
const dateFrom = unique[0]!
const dateTo = unique[unique.length - 1]!
try {
const facts = await rebuildChannelSalesDailyFacts({ dateFrom, dateTo })
await syncFinanceLedgerFromFacts({ dateFrom, dateTo })
if (facts.skippedClosedPeriods.length > 0) {
console.warn('[finance-pipeline] 部分日期已关账,事实未回填', {
dateFrom,
dateTo,
skipped: facts.skippedClosedPeriods,
})
}
return { days: unique.length, skippedClosed: facts.skippedClosedPeriods.length }
} catch {
return { days: 0, skippedClosed: 0 }
}
}
let done = 0
let skippedClosed = 0
for (const date of unique) {
try {
await rebuildChannelSalesDailyFacts({ dateFrom: date, dateTo: date })
const facts = await rebuildChannelSalesDailyFacts({ dateFrom: date, dateTo: date })
if (facts.skippedClosedPeriods.length > 0) {
skippedClosed += facts.skippedClosedPeriods.length
console.warn('[finance-pipeline] 日期已关账,事实未回填', { date })
continue
}
await syncFinanceLedgerFromFacts({ dateFrom: date, dateTo: date })
done += 1
} catch {
// 单日失败不影响管道本身,下一轮或人工重跑会再处理。
}
}
return done
return { days: done, skippedClosed }
}
export async function runCarrierDispatchJob(job: JsonObject = {}) {
@@ -368,14 +400,18 @@ export async function runCostSnapshotJob(job: JsonObject = {}) {
})
await touchWatermark('cost_snapshot', { lastItemId: result.nextCursor })
await touchWatermark('cost_snapshot_hot', { lastItemId: result.hotCursor })
const refreshed = await refreshFactsForDates(result.affectedDates)
return {
status: 'ok',
message:
`成本快照:扫描 ${result.scanned},生成 ${result.created},` +
`替换 ${result.superseded},缺失 ${result.missing},回填 ${refreshed} 天`,
`替换 ${result.superseded},缺失 ${result.missing},回填 ${refreshed.days} 天` +
(result.genericMissing > 0 ? `,通用费用未解析 ${result.genericMissing} 项` : '') +
(refreshed.skippedClosed > 0 ? `(${refreshed.skippedClosed} 天已关账跳过)` : ''),
...result,
refreshedDays: refreshed,
refreshedDays: refreshed.days,
skippedClosedDays: refreshed.skippedClosed,
}
}