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,
|
safeParseJson,
|
||||||
} from '../admin/admin-query-utils.js'
|
} from '../admin/admin-query-utils.js'
|
||||||
import {
|
import {
|
||||||
|
diagnoseUnmatchedProductRules,
|
||||||
mapWorkProductRule,
|
mapWorkProductRule,
|
||||||
normalizeBoolean,
|
normalizeBoolean,
|
||||||
normalizeOptionalId,
|
normalizeOptionalId,
|
||||||
@@ -359,12 +360,26 @@ export async function testAdminKuaishouProductMatch(payload: JsonObject = {}) {
|
|||||||
legacyFallbackEnabled: getWorkerProductMatchConfig().legacyFallbackEnabled,
|
legacyFallbackEnabled: getWorkerProductMatchConfig().legacyFallbackEnabled,
|
||||||
})
|
})
|
||||||
const context = resolveKuaishouWorkProductMatchContext(item)
|
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 {
|
return {
|
||||||
context,
|
context,
|
||||||
status: decision.reason,
|
status: decision.reason,
|
||||||
mappingId: decision.mappingId,
|
mappingId: decision.mappingId,
|
||||||
rule: decision.rule ? mapWorkProductRule(decision.rule) : null,
|
rule: decision.rule ? mapWorkProductRule(decision.rule) : null,
|
||||||
candidates: decision.candidates,
|
candidates: decision.candidates,
|
||||||
|
diagnostics,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ export {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
type KuaishouWorkProductMatchContext,
|
type KuaishouWorkProductMatchContext,
|
||||||
|
type WorkProductRuleRejectSample,
|
||||||
|
diagnoseUnmatchedProductRules,
|
||||||
mapWorkProductRule,
|
mapWorkProductRule,
|
||||||
matchesOptionalText,
|
matchesOptionalText,
|
||||||
normalizeWorkProductRuleMatch,
|
normalizeWorkProductRuleMatch,
|
||||||
|
|||||||
@@ -236,8 +236,7 @@ function scoreWorkProductRuleMapping(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scoreWorkProductRule(order: OrderRow, item: OrderItemRow, rule: WorkProductRuleRow) {
|
function scoreWorkProductRule(order: OrderRow, item: OrderItemRow, rule: WorkProductRuleRow) {
|
||||||
if (!matchesOptionalText(rule.provider, order.provider)) return null
|
if (!matchesRuleProviderPlatform(rule, order)) return null
|
||||||
if (!matchesOptionalText(rule.platform, order.platform)) return null
|
|
||||||
|
|
||||||
const snapshot = safeParseJson(item.item_snapshot_json)
|
const snapshot = safeParseJson(item.item_snapshot_json)
|
||||||
const source = safeParseJson(snapshot.kuaishouSendCode)
|
const source = safeParseJson(snapshot.kuaishouSendCode)
|
||||||
@@ -317,20 +316,121 @@ function scoreWorkProductRule(order: OrderRow, item: OrderItemRow, rule: WorkPro
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
const ruleSkuCode = normalizeMatchId(rule.sku_code)
|
const ruleSkuCode = normalizeMatchId(rule.sku_code)
|
||||||
const ruleProductName = normalizeMatchText(rule.product_name)
|
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
|
// sku_code 与 product_name 任一命中即可(不再要求两者同时成立,避免误填的 sku_code 挡住 out_sku_sn)
|
||||||
if (!ruleProductName) {
|
if (!skuCodeMatched && !productNameMatched) return null
|
||||||
return ruleSkuCode ? { rule, score: 120 } : null
|
if (skuCodeMatched && productNameMatched) {
|
||||||
|
return { rule, score: rule.match_type === 'exact' ? 300 : 220 }
|
||||||
}
|
}
|
||||||
if (rule.match_type === 'exact') {
|
if (skuCodeMatched) return { rule, score: 120 }
|
||||||
return skuNames.includes(ruleProductName) || skuCodes.includes(ruleProductName)
|
return { rule, score: rule.match_type === 'exact' ? 150 : 100 }
|
||||||
? { rule, score: ruleSkuCode ? 300 : 150 }
|
}
|
||||||
: null
|
|
||||||
|
/** 拼多多常复用原快手模板;来源/平台残留 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)) ||
|
const orderProvider = String(order.provider || '')
|
||||||
skuCodes.some((value) => value.includes(ruleProductName))
|
.trim()
|
||||||
? { rule, score: ruleSkuCode ? 220 : 100 }
|
.toLowerCase()
|
||||||
: null
|
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) {
|
function matchCondition(actual: string, expected: unknown) {
|
||||||
|
|||||||
@@ -666,6 +666,53 @@ test('拼多多订单忽略模板残留的快手结构化 match_json,按 out_s
|
|||||||
assert.equal(decision.rule?.id, rule.id)
|
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 不含规则名时不命中', () => {
|
test('拼多多订单缺少 out_sku_sn 且 spec 不含规则名时不命中', () => {
|
||||||
const rule = buildProductRule({
|
const rule = buildProductRule({
|
||||||
provider: 'pdd',
|
provider: 'pdd',
|
||||||
|
|||||||
@@ -400,8 +400,19 @@ export default function ProductMatchPanel() {
|
|||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
<JsonPreview
|
<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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -349,6 +349,26 @@ export function testAdminKuaishouProductMatch(rawPayload: string) {
|
|||||||
mappingId: number | null
|
mappingId: number | null
|
||||||
rule: WorkProductRule | null
|
rule: WorkProductRule | null
|
||||||
candidates: Array<{ ruleKey: string; score: number; mappingId: number | 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 })
|
}>('/api/v1/admin/worker-platform/product-match-test', { rawPayload })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user