diff --git a/apps/backend/src/services/worker-platform/work-product-match-mappers.ts b/apps/backend/src/services/worker-platform/work-product-match-mappers.ts index bf8275e5..6b67f7a3 100644 --- a/apps/backend/src/services/worker-platform/work-product-match-mappers.ts +++ b/apps/backend/src/services/worker-platform/work-product-match-mappers.ts @@ -317,20 +317,30 @@ function scoreWorkProductRule(order: OrderRow, item: OrderItemRow, rule: WorkPro const ruleSkuCode = normalizeMatchId(rule.sku_code) const ruleProductName = normalizeMatchText(rule.product_name) const skuCodeMatched = Boolean(ruleSkuCode && skuCodes.includes(ruleSkuCode)) - const productNameMatched = ruleProductName - ? rule.match_type === 'exact' - ? skuNames.includes(ruleProductName) || skuCodes.includes(ruleProductName) - : skuNames.some((value) => value.includes(ruleProductName)) || - skuCodes.some((value) => value.includes(ruleProductName)) - : false + const exactProductName = Boolean( + ruleProductName && (skuNames.includes(ruleProductName) || skuCodes.includes(ruleProductName)), + ) + const containsProductName = Boolean( + ruleProductName && + !exactProductName && + rule.match_type !== 'exact' && + (skuNames.some((value) => value.includes(ruleProductName)) || + skuCodes.some((value) => value.includes(ruleProductName))), + ) - // sku_code 与 product_name 任一命中即可(不再要求两者同时成立,避免误填的 sku_code 挡住 out_sku_sn) - if (!skuCodeMatched && !productNameMatched) return null - if (skuCodeMatched && productNameMatched) { - return { rule, score: rule.match_type === 'exact' ? 300 : 220 } + // sku_code 与 product_name 任一命中即可 + if (!skuCodeMatched && !exactProductName && !containsProductName) return null + + // 同分冲突常见场景:短名「安卓血宴5钻」contains 命中长编码「安卓血宴5钻带2璀璨」 + // 精确相等优先;contains 则更长的模板名更具体、分更高 + let score = 100 + if (exactProductName) { + score = 250 + } else if (containsProductName) { + score = 100 + Math.min(80, ruleProductName.length) } - if (skuCodeMatched) return { rule, score: 120 } - return { rule, score: rule.match_type === 'exact' ? 150 : 100 } + if (skuCodeMatched) score += exactProductName || containsProductName ? 40 : 120 + return { rule, score } } /** 拼多多常复用原快手模板;来源/平台残留 91kaquan、kuaishou 时不拦截 out_sku_sn 模板名匹配。 */ diff --git a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts index e55c214e..4632d64d 100644 --- a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts +++ b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts @@ -713,6 +713,36 @@ test('拼多多订单在 sku_code 误填时仍可按 product_name/out_sku_sn 命 assert.equal(decision.rule?.id, rule.id) }) +test('长 out_sku_sn 在短模板名 contains 时可优先命中精确相等的长模板,避免冲突', () => { + const longRule = buildProductRule({ + id: 101, + rule_key: 'rule-79bf8b3006ea', + product_name: '安卓血宴5钻带2璀璨', + match_type: 'contains', + }) + const shortRule = buildProductRule({ + id: 102, + rule_key: 'rule-f93c368547e1', + product_name: '安卓血宴5钻', + match_type: 'contains', + }) + const order = { ...buildOrderRow(), provider: 'pdd', platform: 'pdd' } + const item = { + ...buildOrderItemRow(), + sku_code: '安卓血宴5钻带2璀璨', + sku_name: '安卓血宴5钻带2璀璨', + item_snapshot_json: { + out_sku_sn: '安卓血宴5钻带2璀璨', + spec: '安卓区(qv互通),苏尔南-血宴5钻带2璀璨', + }, + } + + const decision = resolveMatchingProductRuleDecision(order, item, [longRule, shortRule], []) + assert.equal(decision.reason, 'matched') + assert.equal(decision.rule?.rule_key, 'rule-79bf8b3006ea') + assert.ok((decision.score || 0) > 100) +}) + test('拼多多订单缺少 out_sku_sn 且 spec 不含规则名时不命中', () => { const rule = buildProductRule({ provider: 'pdd',