fix(fulfillment): 接单模板命中后自动补统一渠道配置

This commit is contained in:
yml2213
2026-11-03 14:00:38 +08:00
parent f336311ef8
commit 07c6a1ce22
2 changed files with 102 additions and 24 deletions
@@ -5,7 +5,10 @@ import type {
InternalSkuChannelPlan,
InternalSkuChannelRow,
} from '../fulfillment/internal-sku-channel-service.js'
import { resolveWorkOrderChannelGate } from './sync-worker-orders-service.js'
import {
resolveWorkOrderChannelGate,
shouldBootstrapWorkOrderChannel,
} from './sync-worker-orders-service.js'
function channel(patch: Partial<InternalSkuChannelRow> = {}): InternalSkuChannelRow {
return {
@@ -96,3 +99,32 @@ test('工单门禁:已分配其它渠道或已有其它真实载体时不放
true,
)
})
test('统一渠道未配置时允许用唯一命中的接单模板自动补齐 work_order', () => {
assert.equal(
shouldBootstrapWorkOrderChannel(undefined, {
allowed: false,
reason: 'no_channel_capability',
}),
true,
)
assert.equal(
shouldBootstrapWorkOrderChannel(
{
primary: null,
fallbacks: [],
attempts: [],
conflicts: [channel()],
},
{ allowed: false, reason: 'routing_conflict' },
),
false,
)
assert.equal(
shouldBootstrapWorkOrderChannel(plan(channel({ channel: 'manual' })), {
allowed: false,
reason: 'internal_sku_channel_manual',
}),
false,
)
})
@@ -12,6 +12,7 @@ import {
getLatestWorkProductMatchLogByOrderItemId,
getWorkOrderByOrderItemId,
listWorkOrdersByPlatformOrderId,
listWorkProductRuleMappings,
listWorkProductRules,
type WorkOrderRow,
} from '../../repositories/worker-platform/index.js'
@@ -37,6 +38,7 @@ import {
mapWorkOrderAdmin,
normalizeRequirementFields,
resolveKuaishouWorkProductMatchContext,
resolveMatchingProductRuleDecision,
resolveSkuNameQuantity,
resolveWorkOrderRulePricing,
resolveWorkOrderSharingEnabled,
@@ -45,6 +47,7 @@ import { registerWorkOrderShopName } from './work-order-shop-options-cache.js'
import { assignPendingOrderItemChannels } from '../fulfillment/channel-assign-service.js'
import {
loadInternalSkuChannelPlans,
upsertInternalSkuChannel,
type InternalSkuChannelPlan,
} from '../fulfillment/internal-sku-channel-service.js'
import {
@@ -52,6 +55,19 @@ import {
resolveInternalSkuKey,
} from '../fulfillment/internal-sku-resolver.js'
import { loadActiveCarrierEvidence } from '../fulfillment/carrier-evidence-service.js'
import { getWorkerProductMatchConfig } from './worker-product-match-config-service.js'
export function shouldBootstrapWorkOrderChannel(
plan: InternalSkuChannelPlan | undefined,
gate: { allowed: boolean; reason: string },
): boolean {
return (
!gate.allowed &&
gate.reason === 'no_channel_capability' &&
!plan?.primary &&
!plan?.conflicts.length
)
}
export function resolveWorkOrderChannelGate(
plan: InternalSkuChannelPlan | undefined,
@@ -107,7 +123,11 @@ export async function syncWorkerOrdersForSourceOrder(
preferredWorkRuleKey?: string
} = {},
) {
const rules = await listWorkProductRules({ enabled: true })
const [rules, ruleMappings] = await Promise.all([
listWorkProductRules({ enabled: true }),
listWorkProductRuleMappings({ enabled: true }),
])
const legacyFallbackEnabled = getWorkerProductMatchConfig().legacyFallbackEnabled
const created: WorkOrderRow[] = []
const skipped: Array<{ orderItemId: number; reason: string }> = []
const decisions: Array<Record<string, unknown>> = []
@@ -141,11 +161,35 @@ export async function syncWorkerOrdersForSourceOrder(
continue
}
const assignedChannel = String(item.fulfillment_channel || '').trim()
const channelGate = resolveWorkOrderChannelGate(channelPlan, options.preferredWorkRuleKey, {
let channelGate = resolveWorkOrderChannelGate(channelPlan, options.preferredWorkRuleKey, {
fulfillmentStatus: item.fulfillment_status || '',
assignedChannel,
existingCarrierChannels: carrierEvidence.get(Number(item.id))?.channels || [],
})
let matchedDecision: ReturnType<typeof resolveMatchingProductRuleDecision> | undefined
if (shouldBootstrapWorkOrderChannel(channelPlan, channelGate)) {
matchedDecision = resolveMatchingProductRuleDecision(order, item, rules, ruleMappings, {
legacyFallbackEnabled,
})
if (matchedDecision.rule) {
await upsertInternalSkuChannel({
internalSku,
channel: 'work_order',
priority: 10,
isFallback: false,
enabled: true,
configJson: { workRuleKey: matchedDecision.rule.rule_key },
source: 'template',
note: `自动匹配接单模板 ${matchedDecision.rule.rule_key}`,
})
channelGate = {
allowed: true,
reason: '',
workRuleKey: matchedDecision.rule.rule_key,
}
}
}
if (!channelGate.allowed) {
skipped.push({ orderItemId: Number(item.id), reason: channelGate.reason })
decisions.push(
@@ -162,27 +206,29 @@ export async function syncWorkerOrdersForSourceOrder(
const preferredRule = preferredRuleKey
? rules.find((rule) => String(rule.rule_key || '').trim() === preferredRuleKey) || null
: null
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 match =
matchedDecision ||
(!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 =