feat(pipeline): 渠道判定后立即归一商品并算成本

- 新增按订单项定向归一,避免新判定订单等冷通道
- 判定/指定渠道后立即归一 → 算成本 → 按业务日回填事实与总账
- 提取 refreshFactsForDates 供管道与渠道服务复用
This commit is contained in:
yml2213
2026-11-01 15:21:53 +08:00
parent 9e154cb16d
commit fd6d2d561b
4 changed files with 174 additions and 55 deletions
@@ -319,3 +319,69 @@ async function loadMappingIndex(): Promise<MappingIndex> {
})),
)
}
/** 指定订单项立即归一(渠道判定后调用,保证成本能马上算出来)。 */
export async function normalizeProductsForItems(itemIds: number[]): Promise<{
matched: number
unmatched: number
}> {
const ids = [
...new Set(itemIds.map(Number).filter((id) => Number.isInteger(id) && id > 0)),
].slice(0, 2000)
if (ids.length === 0) return { matched: 0, unmatched: 0 }
const index = await loadMappingIndex()
const rows = await query<ItemRow>(
`
SELECT
oi.id,
o.provider,
o.shop_id,
oi.sku_code,
oi.sku_name,
oi.normalized_sku_name,
oi.normalized_sku_series,
oi.fulfillment_channel,
wpr.rule_key AS work_rule_key,
cfg.canonical_product_id AS manual_config_product_id
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
LEFT JOIN work_orders wo ON wo.order_item_id = oi.id
LEFT JOIN work_product_rules wpr ON wpr.id = wo.product_rule_id
LEFT JOIN manual_fulfillment_configs cfg ON cfg.id = oi.fulfillment_config_id
WHERE oi.fulfillment_status = 'assigned'
AND oi.canonical_product_id IS NULL
AND oi.id = ANY($1::bigint[])
`,
[ids],
)
const matched: Array<{ id: number; productId: number }> = []
let unmatched = 0
for (const item of rows.rows) {
const productId = resolveProductId(item, index)
if (!productId) {
unmatched += 1
await upsertAggregatedException({
reason: 'product_mapping_missing',
scopeKey: `product_mapping_missing|${item.provider}|${item.shop_id}|${item.sku_code}|${item.fulfillment_channel || '-'}`,
orderItemId: item.id,
detail: { skuName: item.sku_name, channel: item.fulfillment_channel },
})
continue
}
matched.push({ id: item.id, productId })
}
if (matched.length > 0) {
await query(
`
UPDATE order_items oi
SET canonical_product_id = input.product_id, updated_at = NOW()
FROM unnest($1::bigint[], $2::bigint[]) AS input(id, product_id)
WHERE oi.id = input.id
AND oi.canonical_product_id IS NULL
`,
[matched.map((row) => row.id), matched.map((row) => row.productId)],
)
await resolveExceptionsForItems(matched.map((row) => row.id))
}
return { matched: matched.length, unmatched }
}