feat(pipeline): 渠道判定后立即归一商品并算成本
- 新增按订单项定向归一,避免新判定订单等冷通道 - 判定/指定渠道后立即归一 → 算成本 → 按业务日回填事实与总账 - 提取 refreshFactsForDates 供管道与渠道服务复用
This commit is contained in:
@@ -3,10 +3,17 @@ import { query } from '../../db/client.js'
|
||||
import { assignPendingOrderItemChannels } from '../fulfillment/channel-assign-service.js'
|
||||
import { resolveExceptionsForItemsByReason } from '../cost-center/cost-exception-service.js'
|
||||
import { readWatermarkItemId } from '../watermark-store.js'
|
||||
import { refreshFactsForDates } from '../finance/refresh-facts.js'
|
||||
import { rebuildChannelSalesDailyFacts } from '../finance/sales-facts-channel-service.js'
|
||||
import { syncFinanceLedgerFromFacts } from '../finance/finance-ledger-service.js'
|
||||
import { normalizeOrderItemProducts } from '../cost-center/product-normalize-service.js'
|
||||
import { syncChannelCostSnapshots } from '../cost-center/cost-snapshot-service.js'
|
||||
import {
|
||||
normalizeOrderItemProducts,
|
||||
normalizeProductsForItems,
|
||||
} from '../cost-center/product-normalize-service.js'
|
||||
import {
|
||||
syncChannelCostSnapshots,
|
||||
syncCostSnapshotsForItems,
|
||||
} from '../cost-center/cost-snapshot-service.js'
|
||||
import { upsertAggregatedException } from '../cost-center/cost-exception-service.js'
|
||||
import { formatShanghaiDate, shiftDate } from '../../utils/finance-date-range.js'
|
||||
import {
|
||||
@@ -47,7 +54,16 @@ export async function runChannelAssignJob(job: JsonObject = {}) {
|
||||
})
|
||||
|
||||
await touchWatermark('channel_assign', { lastItemId: result.nextCursor })
|
||||
const refreshed = await refreshFactsForDates(result.affectedDates)
|
||||
|
||||
// 刚判定渠道的订单项立刻算成本,不等冷通道扫到。
|
||||
let costDates: string[] = []
|
||||
if (result.assignedItemIds.length > 0) {
|
||||
// 先归一商品,再算成本:卡券/自动发货没有商品就无法解析采购价。
|
||||
await normalizeProductsForItems(result.assignedItemIds).catch(() => undefined)
|
||||
const cost = await syncCostSnapshotsForItems(result.assignedItemIds).catch(() => null)
|
||||
costDates = cost?.affectedDates || []
|
||||
}
|
||||
const refreshed = await refreshFactsForDates([...result.affectedDates, ...costDates])
|
||||
return {
|
||||
status: 'ok',
|
||||
message:
|
||||
@@ -60,57 +76,6 @@ export async function runChannelAssignJob(job: JsonObject = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渠道/成本变更后,按业务日回填销售事实与总账。
|
||||
*
|
||||
* 事实任务只回看最近几天;补配价格、迟到分类这类变更必须按受影响日期重算,
|
||||
* 否则旧日期的毛利会一直停在缺失成本/未分类。
|
||||
*/
|
||||
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 {
|
||||
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 { days: done, skippedClosed }
|
||||
}
|
||||
|
||||
export async function runCarrierDispatchJob(job: JsonObject = {}) {
|
||||
const config = jobConfig(job)
|
||||
const limit = clampInt(config.limit, 200, 1, 2000)
|
||||
|
||||
Reference in New Issue
Block a user