feat(fulfillment): 统一中文内部 SKU 履约路由

This commit is contained in:
yml2213
2026-11-03 11:49:38 +08:00
parent f447b5d73a
commit 3ba808b814
49 changed files with 1643 additions and 1963 deletions
@@ -1,34 +1,73 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { isAffiliateDashCarrierSnapshot } from './sync-worker-orders-service.js'
import type {
InternalSkuChannelPlan,
InternalSkuChannelRow,
} from '../fulfillment/internal-sku-channel-service.js'
import { resolveWorkOrderChannelGate } from './sync-worker-orders-service.js'
// 载体唯一不变式:一个单子只有一个执行载体。
// 命中 affiliate_dash 自动发货的订单项(抖音按 SKU 分类后在 snapshot 标注)不再建工单。
function channel(patch: Partial<InternalSkuChannelRow> = {}): InternalSkuChannelRow {
return {
id: 1,
internal_sku: '幸运币2个',
channel: 'work_order',
priority: 10,
is_fallback: false,
enabled: true,
canonical_product_id: null,
config_json: { workRuleKey: 'rule-lucky-coin' },
source: 'operator',
note: '',
created_by_admin_user_id: null,
...patch,
}
}
test('载体唯一守卫:分类为 affiliate_dash 的订单项不建工单', () => {
function plan(primary: InternalSkuChannelRow | null): InternalSkuChannelPlan {
return {
primary,
fallbacks: [],
attempts: primary ? [primary] : [],
conflicts: [],
}
}
test('工单门禁:只有内部 SKU 明确配置 work_order 且提供模板才放行', () => {
assert.deepEqual(resolveWorkOrderChannelGate(plan(channel())), {
allowed: true,
reason: '',
workRuleKey: 'rule-lucky-coin',
})
})
test('工单门禁:其它统一渠道一律不放行', () => {
for (const targetChannel of ['affiliate_dash', 'manual', 'third_party_voucher'] as const) {
assert.equal(
resolveWorkOrderChannelGate(plan(channel({ channel: targetChannel, config_json: {} })))
.reason,
`internal_sku_channel_${targetChannel}`,
)
}
})
test('工单门禁:无配置、冲突、缺模板、模板不一致都不放行', () => {
assert.equal(resolveWorkOrderChannelGate(undefined).reason, 'no_channel_capability')
assert.equal(
isAffiliateDashCarrierSnapshot({ classification: { channel: 'affiliate_dash' } }),
true,
resolveWorkOrderChannelGate({
primary: null,
fallbacks: [],
attempts: [],
conflicts: [channel()],
}).reason,
'routing_conflict',
)
assert.equal(
resolveWorkOrderChannelGate(plan(channel({ config_json: {} }))).reason,
'work_rule_missing',
)
assert.equal(
resolveWorkOrderChannelGate(plan(channel()), 'another-rule').reason,
'work_rule_mismatch',
)
})
test('载体唯一守卫:多余的空白/大小写仍然命中(保守判定)', () => {
assert.equal(
isAffiliateDashCarrierSnapshot({ classification: { channel: ' affiliate_dash ' } }),
true,
)
})
test('载体唯一守卫:未命中分类 / 无字段 / 脏数据一律放行(不误伤发单)', () => {
assert.equal(isAffiliateDashCarrierSnapshot({ classification: { channel: 'unmatched' } }), false)
assert.equal(isAffiliateDashCarrierSnapshot({ classification: { channel: '' } }), false)
assert.equal(isAffiliateDashCarrierSnapshot({ classification: {} }), false)
assert.equal(isAffiliateDashCarrierSnapshot({ classification: 'affiliate_dash' }), false)
assert.equal(isAffiliateDashCarrierSnapshot({ classification: null }), false)
assert.equal(isAffiliateDashCarrierSnapshot({ classification: ['affiliate_dash'] }), false)
assert.equal(isAffiliateDashCarrierSnapshot({}), false)
assert.equal(isAffiliateDashCarrierSnapshot(null), false)
assert.equal(isAffiliateDashCarrierSnapshot(undefined), false)
assert.equal(isAffiliateDashCarrierSnapshot('affiliate_dash'), false)
})
@@ -12,7 +12,6 @@ import {
getLatestWorkProductMatchLogByOrderItemId,
getWorkOrderByOrderItemId,
listWorkOrdersByPlatformOrderId,
listWorkProductRuleMappings,
listWorkProductRules,
type WorkOrderRow,
} from '../../repositories/worker-platform/index.js'
@@ -38,14 +37,48 @@ import {
mapWorkOrderAdmin,
normalizeRequirementFields,
resolveKuaishouWorkProductMatchContext,
resolveMatchingProductRuleDecision,
resolveSkuNameQuantity,
resolveWorkOrderRulePricing,
resolveWorkOrderSharingEnabled,
} from './mappers.js'
import { getWorkerProductMatchConfig } from './worker-product-match-config-service.js'
import { registerWorkOrderShopName } from './work-order-shop-options-cache.js'
import { assignPendingOrderItemChannels } from '../fulfillment/channel-assign-service.js'
import {
loadInternalSkuChannelPlans,
type InternalSkuChannelPlan,
} from '../fulfillment/internal-sku-channel-service.js'
import {
canonicalizeInternalSku,
resolveInternalSkuKey,
} from '../fulfillment/internal-sku-resolver.js'
export function resolveWorkOrderChannelGate(
plan: InternalSkuChannelPlan | undefined,
preferredWorkRuleKey?: string,
): { allowed: boolean; reason: string; workRuleKey: string } {
if (!plan?.primary) {
return {
allowed: false,
reason: plan?.conflicts.length ? 'routing_conflict' : 'no_channel_capability',
workRuleKey: '',
}
}
if (plan.primary.channel !== 'work_order') {
return {
allowed: false,
reason: `internal_sku_channel_${plan.primary.channel}`,
workRuleKey: '',
}
}
const configuredWorkRuleKey = String(plan.primary.config_json.workRuleKey || '').trim()
if (!configuredWorkRuleKey) {
return { allowed: false, reason: 'work_rule_missing', workRuleKey: '' }
}
if (preferredWorkRuleKey && String(preferredWorkRuleKey).trim() !== configuredWorkRuleKey) {
return { allowed: false, reason: 'work_rule_mismatch', workRuleKey: '' }
}
return { allowed: true, reason: '', workRuleKey: configuredWorkRuleKey }
}
export async function syncWorkerOrdersForSourceOrder(
order: OrderRow,
@@ -58,59 +91,76 @@ export async function syncWorkerOrdersForSourceOrder(
preferredWorkRuleKey?: string
} = {},
) {
const [rules, mappings] = await Promise.all([
listWorkProductRules({ enabled: true }),
listWorkProductRuleMappings({ enabled: true }),
])
const productMatchConfig = getWorkerProductMatchConfig()
const rules = await listWorkProductRules({ enabled: true })
const created: WorkOrderRow[] = []
const skipped: Array<{ orderItemId: number; reason: string }> = []
const decisions: Array<Record<string, unknown>> = []
const internalSkuByItemId = new Map<number, string>()
for (const item of orderItems) {
internalSkuByItemId.set(
Number(item.id),
resolveInternalSkuKey({
manualSkuKey: item.manual_sku_key,
skuCode: item.sku_code,
skuName: item.sku_name,
spec: safeParseJson(item.spec_json),
snapshot: safeParseJson(item.item_snapshot_json),
}),
)
}
const channelPlans = await loadInternalSkuChannelPlans([...internalSkuByItemId.values()])
for (const item of orderItems) {
// 载体唯一不变式(一个单子只有一个执行载体):
// 命中 affiliate_dash 自动发货的订单项(抖音在 snapshot.classification 标注)不再建工单。
if (isAffiliateDashCarrierSnapshot(safeParseJson(item.item_snapshot_json))) {
skipped.push({ orderItemId: Number(item.id), reason: 'affiliate_dash_carrier_taken' })
const internalSku = canonicalizeInternalSku(internalSkuByItemId.get(Number(item.id)))
const channelPlan = channelPlans.get(internalSku)
if (!internalSku) {
skipped.push({ orderItemId: Number(item.id), reason: 'no_internal_sku' })
decisions.push(
buildWorkerConsumerDecision(item, {
result: 'skipped',
reason: 'affiliate_dash_carrier_taken',
reason: 'no_internal_sku',
}),
)
continue
}
const channelGate = resolveWorkOrderChannelGate(channelPlan, options.preferredWorkRuleKey)
if (!channelGate.allowed) {
skipped.push({ orderItemId: Number(item.id), reason: channelGate.reason })
decisions.push(
buildWorkerConsumerDecision(item, {
result: 'skipped',
reason: channelGate.reason,
internalSku,
}),
)
continue
}
const preferredRuleKey = String(options.preferredWorkRuleKey || '').trim()
const preferredRuleKey = channelGate.workRuleKey
const preferredRule = preferredRuleKey
? rules.find((rule) => String(rule.rule_key || '').trim() === preferredRuleKey) || null
: null
const match =
preferredRuleKey && !preferredRule
? {
rule: null,
mappingId: null,
reason: 'unmatched' as const,
score: 0,
candidates: [],
diagnosis: [
{
dimension: 'internal_sku_channel',
reason: 'configured_work_rule_missing',
},
],
}
: preferredRule
? {
rule: preferredRule,
mappingId: null,
reason: 'matched' as const,
score: 1,
candidates: [{ ruleKey: preferredRuleKey, score: 1, mappingId: null }],
}
: resolveMatchingProductRuleDecision(order, item, rules, mappings, {
legacyFallbackEnabled: productMatchConfig.legacyFallbackEnabled,
})
const match = !preferredRule
? {
rule: null,
mappingId: null,
reason: 'unmatched' as const,
score: 0,
candidates: [],
diagnosis: [
{
dimension: 'internal_sku_channel',
reason: 'configured_work_rule_missing',
},
],
}
: {
rule: preferredRule,
mappingId: null,
reason: 'matched' as const,
score: 1,
candidates: [{ ruleKey: preferredRuleKey, score: 1, mappingId: null }],
}
const matchContext = resolveKuaishouWorkProductMatchContext(item)
const snapshot = safeParseJson(item.item_snapshot_json)
const sellerId =
@@ -176,7 +226,7 @@ export async function syncWorkerOrdersForSourceOrder(
})
}
if (!match.rule) {
const reason = match.reason === 'ambiguous' ? 'rule_match_ambiguous' : 'rule_not_matched'
const reason = 'work_rule_not_found'
skipped.push({
orderItemId: Number(item.id),
reason,
@@ -435,26 +485,6 @@ async function resolveDouyinMashidaWorkOrderVoucher(
}
}
/**
* 该订单项的执行载体是否**已被 affiliate_dash 自动发货占用**。
*
* 依据:抖音按 SKU 分类后写入的 `snapshot.classification.channel === 'affiliate_dash'`
* (见 `fulfillment/product-resolution-service.ts`)。
* 一个单子只能有一个执行载体:自动发货与派打手互斥,命中前者时不再建工单。
*/
export function isAffiliateDashCarrierSnapshot(snapshot: unknown): boolean {
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
return false
}
const classification = (snapshot as { classification?: unknown }).classification
if (!classification || typeof classification !== 'object' || Array.isArray(classification)) {
return false
}
return String((classification as { channel?: unknown }).channel || '').trim() === 'affiliate_dash'
}
function buildWorkerConsumerDecision(
item: OrderItemRow,
detail: Record<string, unknown>,