fix(pdd): 彻底修复拼多多模板匹配被来源/平台/sku_code 误拦截

PDD 复用快手模板时,残留的 91kaquan/kuaishou 与误填 sku_code 会让 out_sku_sn 对上模板名仍未命中;改为 OR 匹配并放行常见残留,同时在匹配测试返回拒绝原因诊断。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-15 11:28:40 +08:00
co-authored by Cursor
parent d09cce5050
commit 03dffcfbb8
6 changed files with 209 additions and 14 deletions
@@ -24,6 +24,7 @@ import {
safeParseJson,
} from '../admin/admin-query-utils.js'
import {
diagnoseUnmatchedProductRules,
mapWorkProductRule,
normalizeBoolean,
normalizeOptionalId,
@@ -359,12 +360,26 @@ export async function testAdminKuaishouProductMatch(payload: JsonObject = {}) {
legacyFallbackEnabled: getWorkerProductMatchConfig().legacyFallbackEnabled,
})
const context = resolveKuaishouWorkProductMatchContext(item)
const diagnostics = {
outSkuSn,
orderProvider: order.provider,
orderPlatform: order.platform,
orderShopId: order.shop_id,
contextSellerId: context.sellerId,
legacyFallbackEnabled: getWorkerProductMatchConfig().legacyFallbackEnabled,
useLegacyFallback: !context.sellerId || getWorkerProductMatchConfig().legacyFallbackEnabled,
enabledRules: rules.length,
enabledMappings: mappings.length,
rejectSamples:
decision.reason === 'matched' ? [] : diagnoseUnmatchedProductRules(order, item, rules, 10),
}
return {
context,
status: decision.reason,
mappingId: decision.mappingId,
rule: decision.rule ? mapWorkProductRule(decision.rule) : null,
candidates: decision.candidates,
diagnostics,
}
}
@@ -98,6 +98,8 @@ export {
export {
type KuaishouWorkProductMatchContext,
type WorkProductRuleRejectSample,
diagnoseUnmatchedProductRules,
mapWorkProductRule,
matchesOptionalText,
normalizeWorkProductRuleMatch,
@@ -236,8 +236,7 @@ function scoreWorkProductRuleMapping(
}
function scoreWorkProductRule(order: OrderRow, item: OrderItemRow, rule: WorkProductRuleRow) {
if (!matchesOptionalText(rule.provider, order.provider)) return null
if (!matchesOptionalText(rule.platform, order.platform)) return null
if (!matchesRuleProviderPlatform(rule, order)) return null
const snapshot = safeParseJson(item.item_snapshot_json)
const source = safeParseJson(snapshot.kuaishouSendCode)
@@ -317,20 +316,121 @@ function scoreWorkProductRule(order: OrderRow, item: OrderItemRow, rule: WorkPro
.filter(Boolean)
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
if (ruleSkuCode && !skuCodes.includes(ruleSkuCode)) return null
if (!ruleProductName) {
return ruleSkuCode ? { rule, score: 120 } : null
// 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 }
}
if (rule.match_type === 'exact') {
return skuNames.includes(ruleProductName) || skuCodes.includes(ruleProductName)
? { rule, score: ruleSkuCode ? 300 : 150 }
: null
if (skuCodeMatched) return { rule, score: 120 }
return { rule, score: rule.match_type === 'exact' ? 150 : 100 }
}
/** 拼多多常复用原快手模板;来源/平台残留 91kaquan、kuaishou 时不拦截 out_sku_sn 模板名匹配。 */
function matchesRuleProviderPlatform(rule: WorkProductRuleRow, order: OrderRow) {
if (
matchesOptionalText(rule.provider, order.provider) &&
matchesOptionalText(rule.platform, order.platform)
) {
return true
}
return skuNames.some((value) => value.includes(ruleProductName)) ||
skuCodes.some((value) => value.includes(ruleProductName))
? { rule, score: ruleSkuCode ? 220 : 100 }
: null
const orderProvider = String(order.provider || '')
.trim()
.toLowerCase()
const orderPlatform = String(order.platform || '')
.trim()
.toLowerCase()
const orderIsPdd = orderProvider === 'pdd' || orderPlatform === 'pdd'
if (!orderIsPdd) return false
const ruleProvider = String(rule.provider || '')
.trim()
.toLowerCase()
const rulePlatform = String(rule.platform || '')
.trim()
.toLowerCase()
const providerOk = !ruleProvider || ruleProvider === 'pdd' || ruleProvider === '91kaquan'
const platformOk = !rulePlatform || rulePlatform === 'pdd' || rulePlatform === 'kuaishou'
return providerOk && platformOk
}
export type WorkProductRuleRejectSample = {
ruleKey: string
productName: string
provider: string
platform: string
shopId: string
skuCode: string
reason: string
}
/** 诊断未命中原因:找出与订单 SKU/标题相近但被过滤的模板。 */
export function diagnoseUnmatchedProductRules(
order: OrderRow,
item: OrderItemRow,
rules: WorkProductRuleRow[],
limit = 8,
): WorkProductRuleRejectSample[] {
const snapshot = safeParseJson(item.item_snapshot_json)
const outSkuSn = String(snapshot.out_sku_sn || snapshot.outSkuSn || '').trim()
const needle = normalizeMatchText(
outSkuSn || item.sku_name || item.sku_code || snapshot.spec || '',
)
const samples: WorkProductRuleRejectSample[] = []
for (const rule of rules) {
if (!rule.enabled) continue
const productName = normalizeMatchText(rule.product_name)
const skuCode = normalizeMatchId(rule.sku_code)
const related =
!needle ||
productName.includes(needle) ||
needle.includes(productName) ||
(skuCode && (skuCode.includes(needle) || needle.includes(skuCode)))
if (!related && needle) continue
const scored = scoreWorkProductRule(order, item, rule)
if (scored) continue
let reason = 'name_or_sku_not_matched'
if (!matchesRuleProviderPlatform(rule, order)) {
reason = `provider_platform_mismatch(rule=${rule.provider || '-'}/${rule.platform || '-'}, order=${order.provider || '-'}/${order.platform || '-'})`
} else {
const ruleShopId = normalizeMatchId(rule.shop_id)
const source = safeParseJson(snapshot.kuaishouSendCode)
const sellerId = normalizeMatchId(source.sellerId)
if (ruleShopId && ruleShopId !== normalizeMatchId(order.shop_id) && ruleShopId !== sellerId) {
reason = `shop_id_mismatch(rule=${rule.shop_id}, order=${order.shop_id || '-'})`
} else {
const match = safeParseJson(rule.match_json)
const hasStructuredMatch = Object.entries(match).some(
([key, value]) =>
!['itemTitleMatchType', 'skuNickMatchType'].includes(key) && String(value || '').trim(),
)
if (hasStructuredMatch && sellerId) {
reason = 'structured_match_failed'
}
}
}
samples.push({
ruleKey: rule.rule_key,
productName: rule.product_name || '',
provider: rule.provider || '',
platform: rule.platform || '',
shopId: rule.shop_id || '',
skuCode: rule.sku_code || '',
reason,
})
if (samples.length >= limit) break
}
return samples
}
function matchCondition(actual: string, expected: unknown) {
@@ -666,6 +666,53 @@ test('拼多多订单忽略模板残留的快手结构化 match_json,按 out_s
assert.equal(decision.rule?.id, rule.id)
})
test('拼多多订单可命中来源/平台残留为 91kaquan/kuaishou 的模板名', () => {
const rule = buildProductRule({
provider: '91kaquan',
platform: 'kuaishou',
product_name: '安卓怒嚎1钻',
match_type: 'contains',
})
const order = { ...buildOrderRow(), provider: 'pdd', platform: 'pdd' }
const item = {
...buildOrderItemRow(),
sku_code: '安卓怒嚎1钻',
sku_name: '安卓怒嚎1钻',
item_snapshot_json: {
out_sku_sn: '安卓怒嚎1钻',
spec: '安卓区(qv互通),苏尔南-怒嚎1钻',
},
}
const decision = resolveMatchingProductRuleDecision(order, item, [rule], [])
assert.equal(decision.reason, 'matched')
assert.equal(decision.rule?.id, rule.id)
})
test('拼多多订单在 sku_code 误填时仍可按 product_name/out_sku_sn 命中', () => {
const rule = buildProductRule({
provider: '',
platform: '',
sku_code: '961856378217',
product_name: '安卓怒嚎1钻',
match_type: 'contains',
})
const order = { ...buildOrderRow(), provider: 'pdd', platform: 'pdd' }
const item = {
...buildOrderItemRow(),
sku_code: '安卓怒嚎1钻',
sku_name: '安卓怒嚎1钻',
item_snapshot_json: {
out_sku_sn: '安卓怒嚎1钻',
goods_id: '961856378217',
},
}
const decision = resolveMatchingProductRuleDecision(order, item, [rule], [])
assert.equal(decision.reason, 'matched')
assert.equal(decision.rule?.id, rule.id)
})
test('拼多多订单缺少 out_sku_sn 且 spec 不含规则名时不命中', () => {
const rule = buildProductRule({
provider: 'pdd',