fix(fulfillment): 接单模板命中后自动补统一渠道配置
This commit is contained in:
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user