feat(finance): 商品渠道能力、自动发货转人工与成本异常收口

- 商品可同时支持自动发货/人工,新增渠道能力与成本冲销字段
- 卡券缺商品时按内部中文 SKU 自动建 tpv: 商品并补渠道规则
- 自动发货失败转人工并冲销原渠道成本
- 配置成本后按最早缺失订单生效并立即补算、关闭异常
- tpv: 商品来源计入第三方卡券
This commit is contained in:
yml2213
2026-11-02 13:17:15 +08:00
parent 86b5347e40
commit 3c4c81003f
16 changed files with 532 additions and 34 deletions
@@ -1,8 +1,10 @@
import type { JsonObject } from '../../types/json.js'
import { query } from '../../db/client.js'
import { assignPendingOrderItemChannels } from '../fulfillment/channel-assign-service.js'
import { runAffiliateFallback } from '../fulfillment/affiliate-fallback-service.js'
import {
resolveExceptionsForItemsByReason,
resolveOrphanCostExceptions,
upsertAggregatedExceptionsBatch,
type AggregatedExceptionEntry,
} from '../cost-center/cost-exception-service.js'
@@ -167,7 +169,7 @@ export async function runCarrierDispatchJob(job: JsonObject = {}) {
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
AND cs.superseded_at IS NULL AND reversed_at IS NULL
)
AND NOT EXISTS (
SELECT 1
@@ -252,17 +254,23 @@ export async function runCarrierDispatchJob(job: JsonObject = {}) {
}
const resolved = await resolveCarrierDispatchExceptions()
const fallback = await runAffiliateFallback({ limit: 200 }).catch(() => ({
scanned: 0,
switched: 0,
}))
const nextCursor = maxId === 0 ? 0 : maxId
await touchWatermark('carrier_dispatch', { lastItemId: nextCursor })
return {
status: 'ok',
message:
`人工载体挂载 ${attached},缺配置 ${missingManual.rows.length},` +
`缺履约载体 ${missingCarrier.rows.length},异常收口 ${resolved}`,
`缺履约载体 ${missingCarrier.rows.length},异常收口 ${resolved},` +
`自动转人工 ${fallback.switched}`,
attached,
missingManual: missingManual.rows.length,
missingCarrier: missingCarrier.rows.length,
resolved,
affiliateFallback: fallback.switched,
}
}
@@ -335,7 +343,7 @@ async function resolveCarrierDispatchExceptions(): Promise<number> {
SELECT 1 FROM cost_snapshots cs
WHERE cs.subject_type = 'manual_case'
AND cs.order_item_id = i.order_item_id
AND cs.superseded_at IS NULL
AND cs.superseded_at IS NULL AND reversed_at IS NULL
)
LIMIT 500
`,
@@ -383,6 +391,7 @@ 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)
await resolveOrphanCostExceptions().catch(() => undefined)
return {
status: 'ok',
message: