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:
@@ -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',
|
||||
|
||||
@@ -400,8 +400,19 @@ export default function ProductMatchPanel() {
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<JsonPreview
|
||||
value={{ mappingId: testResult.mappingId, candidates: testResult.candidates }}
|
||||
value={{
|
||||
mappingId: testResult.mappingId,
|
||||
candidates: testResult.candidates,
|
||||
diagnostics: testResult.diagnostics,
|
||||
}}
|
||||
/>
|
||||
{testResult.status !== 'matched' &&
|
||||
testResult.diagnostics?.rejectSamples?.length ? (
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 0 }}>
|
||||
相近未命中模板原因见上方 diagnostics.rejectSamples(常见:来源/平台写成
|
||||
91kaquan/kuaishou、shop_id 绑错店、sku_code 与 out_sku_sn 不一致)。
|
||||
</Typography.Paragraph>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -349,6 +349,26 @@ export function testAdminKuaishouProductMatch(rawPayload: string) {
|
||||
mappingId: number | null
|
||||
rule: WorkProductRule | null
|
||||
candidates: Array<{ ruleKey: string; score: number; mappingId: number | null }>
|
||||
diagnostics?: {
|
||||
outSkuSn: string
|
||||
orderProvider: string
|
||||
orderPlatform: string
|
||||
orderShopId: string
|
||||
contextSellerId: string
|
||||
legacyFallbackEnabled: boolean
|
||||
useLegacyFallback: boolean
|
||||
enabledRules: number
|
||||
enabledMappings: number
|
||||
rejectSamples: Array<{
|
||||
ruleKey: string
|
||||
productName: string
|
||||
provider: string
|
||||
platform: string
|
||||
shopId: string
|
||||
skuCode: string
|
||||
reason: string
|
||||
}>
|
||||
}
|
||||
}>('/api/v1/admin/worker-platform/product-match-test', { rawPayload })
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user