feat(fulfillment): 渠道联动与管道兜底
- 人工配置保存/删除同步渠道规则,人工确认写 channel/status - 按 SKU 指定渠道默认同时落规则 - 管道补充:载体缺失、人工成本未记录、事实与总账按水位追赶
This commit is contained in:
@@ -110,12 +110,74 @@ export async function runCarrierDispatchJob(job: JsonObject = {}) {
|
||||
})
|
||||
}
|
||||
|
||||
// 已配置人工 SKU、但还没确认履约的单:进入「人工成本未记录」队列。
|
||||
const pendingManualCost = await query<{ id: number; sku_key: string }>(
|
||||
`
|
||||
SELECT oi.id, oi.manual_sku_key AS sku_key
|
||||
FROM order_items oi
|
||||
WHERE oi.fulfillment_status = 'assigned'
|
||||
AND oi.fulfillment_channel = 'manual'
|
||||
AND oi.fulfillment_config_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM cost_snapshots cs
|
||||
WHERE cs.subject_type = 'manual_case'
|
||||
AND cs.order_item_id = oi.id
|
||||
AND cs.superseded_at IS NULL
|
||||
)
|
||||
ORDER BY oi.id
|
||||
LIMIT $1
|
||||
`,
|
||||
[limit],
|
||||
)
|
||||
|
||||
for (const row of pendingManualCost.rows) {
|
||||
await upsertAggregatedException({
|
||||
reason: 'manual_cost_missing',
|
||||
scopeKey: `manual_cost_missing|-|-|${row.sku_key}|manual`,
|
||||
orderItemId: row.id,
|
||||
detail: { skuKey: row.sku_key },
|
||||
})
|
||||
}
|
||||
|
||||
// 自动发货 / 接单平台被运营指定后却没有载体:不能静默卡住,进问题队列。
|
||||
const missingCarrier = await query<{ id: number; channel: string; sku_code: string }>(
|
||||
`
|
||||
SELECT oi.id, oi.fulfillment_channel AS channel, oi.sku_code
|
||||
FROM order_items oi
|
||||
WHERE oi.fulfillment_status = 'assigned'
|
||||
AND oi.fulfillment_channel IN ('affiliate_dash', 'work_order')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM fulfillment_tasks ft
|
||||
WHERE ft.order_item_id = oi.id
|
||||
AND ft.executor_key IN ('affiliate_dash', 'third_party_voucher')
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id
|
||||
)
|
||||
ORDER BY oi.id
|
||||
LIMIT $1
|
||||
`,
|
||||
[limit],
|
||||
)
|
||||
|
||||
for (const row of missingCarrier.rows) {
|
||||
await upsertAggregatedException({
|
||||
reason: 'carrier_missing',
|
||||
scopeKey: `carrier_missing|${row.channel}|${row.sku_code}`,
|
||||
orderItemId: row.id,
|
||||
detail: { channel: row.channel, skuCode: row.sku_code },
|
||||
})
|
||||
}
|
||||
|
||||
await touchWatermark('carrier_dispatch')
|
||||
return {
|
||||
status: 'ok',
|
||||
message: `人工载体挂载 ${attached},缺配置 ${missingManual.rows.length}`,
|
||||
message:
|
||||
`人工载体挂载 ${attached},缺配置 ${missingManual.rows.length},` +
|
||||
`缺履约载体 ${missingCarrier.rows.length}`,
|
||||
attached,
|
||||
missingManual: missingManual.rows.length,
|
||||
missingCarrier: missingCarrier.rows.length,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +215,7 @@ export async function runSalesFactsJob(job: JsonObject = {}) {
|
||||
const config = jobConfig(job)
|
||||
const days = clampInt(config.days, 3, 1, 60)
|
||||
const dateTo = formatShanghaiDate(new Date())
|
||||
const dateFrom = shiftDate(dateTo, -(days - 1))
|
||||
const dateFrom = await resolveCatchUpStart('sales_facts', days, dateTo)
|
||||
const result = await rebuildChannelSalesDailyFacts({ dateFrom, dateTo })
|
||||
|
||||
await touchWatermark('sales_facts', { lastStatDate: result.dateTo })
|
||||
@@ -168,7 +230,7 @@ export async function runLedgerPostJob(job: JsonObject = {}) {
|
||||
const config = jobConfig(job)
|
||||
const days = clampInt(config.days, 3, 1, 60)
|
||||
const dateTo = formatShanghaiDate(new Date())
|
||||
const dateFrom = shiftDate(dateTo, -(days - 1))
|
||||
const dateFrom = await resolveCatchUpStart('ledger_post', days, dateTo)
|
||||
const result = await syncFinanceLedgerFromFacts({ dateFrom, dateTo })
|
||||
|
||||
await touchWatermark('ledger_post', { lastStatDate: dateTo })
|
||||
@@ -215,3 +277,25 @@ export async function runSingleFinancePipeline(
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算本轮起始日期:默认回看 N 天;若水位更早(例如停机数天),
|
||||
* 从水位那天补起,最多补 90 天,避免一次扫描过长。
|
||||
*/
|
||||
async function resolveCatchUpStart(
|
||||
pipeline: 'sales_facts' | 'ledger_post',
|
||||
days: number,
|
||||
dateTo: string,
|
||||
): Promise<string> {
|
||||
const windowStart = shiftDate(dateTo, -(days - 1))
|
||||
const result = await query<{ last_stat_date: string | null }>(
|
||||
`SELECT last_stat_date::text FROM finance_watermarks WHERE pipeline = $1`,
|
||||
[pipeline],
|
||||
)
|
||||
const lastDate = result.rows[0]?.last_stat_date || ''
|
||||
if (!lastDate) return windowStart
|
||||
|
||||
const earliest = shiftDate(dateTo, -89)
|
||||
const candidate = lastDate < windowStart ? lastDate : windowStart
|
||||
return candidate < earliest ? earliest : candidate
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user