refactor(product-match): 抽离接单模板匹配引擎(来源适配器/引擎/价格收口 + 后台按订单项试匹配)

新增 services/product-match:来源适配器(kuaishou-91 合成 91+快手 / pdd / douyin-mashida)+ 平台无关匹配上下文 + 条件打分引擎(并列最高=ambiguous 不猜、逐维诊断)+ 价格数量拼单唯一实现。

- 商店键命名空间化(kuaishou:/pdd:/douyin:),店铺为可选作用域:留空即可按订单信息匹配
- 抖音匹配文本只取内部 SKU,引流标题只进 promoTitle,itemTitle 恒空(修复 0.85 元误命中碎梦浮光珍宝礼盒、拼单未开)
- work-product-match-mappers 改为门面(签名不变),work-order-pricing 改为转发
- 新增后台「按订单项试匹配」接口与面板:走发单同一套链路,返回模板/数量/单价/总额/拼单
- 生产样本回放:91/PDD 6000 条决策 0 差异;抖音 5 条全为修正

设计见 docs/接单模板匹配引擎重构设计.md
This commit is contained in:
yml2213
2026-10-14 15:58:21 +08:00
parent 4b081d88c5
commit 10464fea89
26 changed files with 3218 additions and 477 deletions
@@ -385,6 +385,55 @@ export function testAdminKuaishouProductMatch(rawPayload: string) {
}>('/api/v1/admin/worker-platform/product-match-test', { rawPayload })
}
export type AdminProductMatchTestByItemResult = {
order: {
id: number
provider: string
platform: string
shopId: string
shopName: string
platformOrderId: string
totalAmount: number
}
orderItem: {
id: number
skuCode: string
skuName: string
quantity: number
matchSkuTexts: string[]
}
sourceKey: string
hasSellerContext: boolean
allowTextFallback: boolean
shopKeys: string[]
status: 'matched' | 'unmatched' | 'ambiguous'
rule: WorkProductRule | null
mappingId: number | null
entryKey: string | null
score: number
candidates: Array<{ key: string; ruleKey: string; score: number; mappingId: number | null }>
diagnosis: Array<{ key: string; ruleKey: string; blockedBy: string[] }>
plan: {
skuQuantity: number
unitPriceFen: number
pricingMode: 'unit' | 'fixed'
rewardAmount: number
sharingEnabled: boolean
sharingTotalQuantity: number
sharingUnitReward: number
acceptanceMode: string
} | null
counts: { enabledRules: number; enabledMappings: number }
}
/** 按订单项 ID 试匹配:与发单侧完全一致的来源适配器 + 引擎 + 价格(任何来源都适用)。 */
export function testAdminProductMatchByItem(orderItemId: number) {
return apiPost<AdminProductMatchTestByItemResult>(
'/api/v1/admin/worker-platform/product-match-test-item',
{ orderItemId },
)
}
export function fetchAdminWorkProductMatchLogs(params?: Record<string, unknown>) {
return apiGet<WorkerListResponse<WorkProductMatchLog>>(
'/api/v1/admin/worker-platform/product-match-logs',