diff --git a/apps/backend/src/services/worker-platform/admin-kuaishou-match-service.ts b/apps/backend/src/services/worker-platform/admin-kuaishou-match-service.ts
index 9e3a2a26..2ea08d82 100644
--- a/apps/backend/src/services/worker-platform/admin-kuaishou-match-service.ts
+++ b/apps/backend/src/services/worker-platform/admin-kuaishou-match-service.ts
@@ -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,
}
}
diff --git a/apps/backend/src/services/worker-platform/mappers.ts b/apps/backend/src/services/worker-platform/mappers.ts
index 17651a93..6b9162d3 100644
--- a/apps/backend/src/services/worker-platform/mappers.ts
+++ b/apps/backend/src/services/worker-platform/mappers.ts
@@ -98,6 +98,8 @@ export {
export {
type KuaishouWorkProductMatchContext,
+ type WorkProductRuleRejectSample,
+ diagnoseUnmatchedProductRules,
mapWorkProductRule,
matchesOptionalText,
normalizeWorkProductRuleMatch,
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 9913f4fb..bf8275e5 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
@@ -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) {
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 2967c7e1..e55c214e 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
@@ -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',
diff --git a/apps/frontend/src/pages/admin/panels/ProductMatchPanel.tsx b/apps/frontend/src/pages/admin/panels/ProductMatchPanel.tsx
index dc9d4320..1c936d5d 100644
--- a/apps/frontend/src/pages/admin/panels/ProductMatchPanel.tsx
+++ b/apps/frontend/src/pages/admin/panels/ProductMatchPanel.tsx
@@ -400,8 +400,19 @@ export default function ProductMatchPanel() {
+ {testResult.status !== 'matched' &&
+ testResult.diagnostics?.rejectSamples?.length ? (
+
+ 相近未命中模板原因见上方 diagnostics.rejectSamples(常见:来源/平台写成
+ 91kaquan/kuaishou、shop_id 绑错店、sku_code 与 out_sku_sn 不一致)。
+
+ ) : null}
) : null}
diff --git a/apps/frontend/src/services/admin/worker-platform.ts b/apps/frontend/src/services/admin/worker-platform.ts
index 3ed02648..3748f424 100644
--- a/apps/frontend/src/services/admin/worker-platform.ts
+++ b/apps/frontend/src/services/admin/worker-platform.ts
@@ -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 })
}