Merge branch 'main' into dev

This commit is contained in:
yml2213
2026-09-15 14:17:36 +08:00
216 changed files with 5490 additions and 23718 deletions
@@ -16,6 +16,7 @@ import type { JsonObject } from '../../types/json.js'
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
import { createHttpError } from '../../utils/http.js'
import { nowIso } from '../../utils/time.js'
import { extractPddOutSkuSn, type PddRawOrderItem } from '../platforms/pdd/pdd-order-mapper.js'
import {
normalizeDateString,
normalizePage,
@@ -23,6 +24,7 @@ import {
safeParseJson,
} from '../admin/admin-query-utils.js'
import {
diagnoseUnmatchedProductRules,
mapWorkProductRule,
normalizeBoolean,
normalizeOptionalId,
@@ -251,15 +253,19 @@ export async function listAdminKuaishouMatchSources(payload: JsonObject = {}) {
for (const row of recentOrderItemsResult.rows) {
const snapshot = safeParseJson(row.item_snapshot_json)
const ksc = safeParseJson(snapshot.kuaishouSendCode)
const outSkuSn = String(snapshot.out_sku_sn || snapshot.outSkuSn || '').trim()
const sellerId = normalizeKuaishouMatchId(ksc.sellerId || row.shop_id || row.platform)
const relItemId = normalizeKuaishouMatchId(
ksc.relItemId || row.sku_code || snapshot.goodsId || snapshot.goods_id,
ksc.relItemId || snapshot.goodsId || snapshot.goods_id || row.sku_code,
)
const itemTitle = String(
ksc.itemTitle || snapshot.goodsName || snapshot.goods_name || row.sku_name || '',
).trim()
const relSkuId = normalizeKuaishouMatchId(ksc.relSkuId || snapshot.spec || row.sku_code)
const skuNick = String(ksc.skuNick || snapshot.spec || row.sku_name || '').trim()
// 拼多多优先展示/分组 out_sku_sn,与生产匹配字段对齐
const relSkuId = normalizeKuaishouMatchId(
ksc.relSkuId || outSkuSn || snapshot.spec || row.sku_code,
)
const skuNick = String(ksc.skuNick || outSkuSn || snapshot.spec || row.sku_name || '').trim()
if (!sellerId || (!relItemId && !itemTitle)) continue
@@ -296,30 +302,45 @@ export async function listAdminKuaishouMatchSources(payload: JsonObject = {}) {
export async function testAdminKuaishouProductMatch(payload: JsonObject = {}) {
const body = resolveKuaishouMatchPayload(payload.rawPayload ?? payload.raw_payload ?? payload)
const ext = safeParseJson(body.ext)
const outSkuSn = extractPddOutSkuSn(body as PddRawOrderItem)
const goodsId = String(body.goodsId || body.goods_id || '').trim()
const goodsName = String(body.goodsName || body.goods_name || body.itemTitle || '').trim()
const spec = String(body.spec || '').trim()
const isPdd =
body.platform === 'pdd' ||
String(body.provider || '').trim() === 'pdd' ||
String(body.sellerId || '').includes('pdd') ||
Boolean(body.goodsName || body.goods_name || body.goodsId || body.goods_id)
Boolean(outSkuSn) ||
Boolean(body.order_sn && (goodsName || goodsId))
// 与生产 mapper 对齐:匹配字段优先 out_sku_sn
const skuCode = String(body.sku_code || outSkuSn || body.itemId || goodsId || '').trim()
const skuName = String(body.sku_name || outSkuSn || spec || goodsName || '').trim()
const order = {
provider: String(body.provider || (isPdd ? 'pdd' : '91kaquan')).trim(),
platform: String(body.platform || (isPdd ? 'pdd' : 'kuaishou')).trim(),
shop_id: String(body.sellerId || body.shop_id || '').trim(),
} as OrderRow
const relItemId = normalizeKuaishouMatchId(ext.relItemId || body.goodsId || body.goods_id)
const relSkuId = normalizeKuaishouMatchId(ext.relSkuId || body.spec)
const itemTitle = String(body.itemTitle || body.goodsName || body.goods_name || '').trim()
const skuNick = String(ext.skuNick || body.skuNick || body.spec || '').trim()
const relItemId = normalizeKuaishouMatchId(ext.relItemId || goodsId)
const relSkuId = normalizeKuaishouMatchId(ext.relSkuId || outSkuSn || body.spec)
const itemTitle = String(body.itemTitle || goodsName || '').trim()
const skuNick = String(ext.skuNick || body.skuNick || outSkuSn || body.spec || '').trim()
const item = {
id: 0,
order_id: 0,
sku_code: String(body.sku_code || body.itemId || relItemId || '').trim(),
sku_name: String(body.sku_name || skuNick || itemTitle || '').trim(),
quantity: Math.max(1, Number(body.num || body.quantity) || 1),
sku_code: skuCode,
sku_name: skuName,
quantity: Math.max(1, Number(body.num || body.quantity || body.goods_number) || 1),
spec_json: {},
item_snapshot_json: {
goodsId: body.goodsId || body.goods_id,
goodsName: body.goodsName || body.goods_name,
goods_id: body.goods_id || body.goodsId,
goodsName,
goods_name: body.goods_name || body.goodsName || goodsName,
spec: body.spec,
out_sku_sn: outSkuSn,
outSkuSn,
externalSkuCode: skuCode,
externalSkuName: skuName,
kuaishouSendCode: {
sellerId: String(body.sellerId || order.shop_id || '').trim(),
itemId: String(body.itemId || '').trim(),
@@ -339,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,
@@ -9,6 +9,7 @@ import {
createWorkProductMatchLog,
createWorkOrder,
createWorkOrderEvent,
getLatestWorkProductMatchLogByOrderItemId,
getWorkOrderByOrderItemId,
listWorkOrdersByPlatformOrderId,
listWorkProductRuleMappings,
@@ -19,7 +20,7 @@ import type { JsonObject } from '../../types/json.js'
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
import { generateWorkOrderNo, randomId } from '../../utils/random.js'
import { nowIso } from '../../utils/time.js'
import { logInfo, logWarn } from '../../utils/logger.js'
import { logDebug, logInfo, logWarn } from '../../utils/logger.js'
import { safeParseJson } from '../admin/admin-query-utils.js'
import { createWorkOrderMaterialAdminNotification } from '../admin/admin-notification-service.js'
import {
@@ -62,11 +63,36 @@ export async function syncWorkerOrdersForSourceOrder(
const itemTitle =
matchContext.itemTitle ||
String(snapshot.goodsName || snapshot.goods_name || item.sku_name || '').trim()
const relSkuId = matchContext.relSkuId || String(snapshot.spec || item.sku_code || '').trim()
const skuNick = matchContext.skuNick || String(snapshot.spec || item.sku_name || '').trim()
const outSkuSn = String(snapshot.out_sku_sn || snapshot.outSkuSn || '').trim()
const relSkuId =
matchContext.relSkuId || String(outSkuSn || snapshot.spec || item.sku_code || '').trim()
const skuNick =
matchContext.skuNick || String(outSkuSn || snapshot.spec || item.sku_name || '').trim()
// Record product match diagnostics for all platform orders (including PDD)
if (sellerId || relItemId || itemTitle) {
// 已有工单:跳过匹配日志,避免 PDD 定时同步对旧单反复刷「命中」日志。
const existingWorkOrder = await getWorkOrderByOrderItemId(item.id)
if (existingWorkOrder) {
skipped.push({ orderItemId: Number(item.id), reason: 'already_exists' })
decisions.push(
buildWorkerConsumerDecision(item, {
result: 'existing',
reason: 'already_exists',
ruleId: match.rule?.id,
ruleKey: match.rule?.rule_key,
}),
)
continue
}
const latestLog = await getLatestWorkProductMatchLogByOrderItemId(item.id)
const sameMatchOutcome =
latestLog &&
String(latestLog.match_status || '') === match.reason &&
Number(latestLog.rule_id || 0) === Number(match.rule?.id || 0) &&
Number(latestLog.mapping_id || 0) === Number(match.mappingId || 0)
// 匹配结果未变时不重复写诊断日志(旧单未建工单也会被定时同步反复扫到)。
if (!sameMatchOutcome && (sellerId || relItemId || itemTitle)) {
await createWorkProductMatchLog({
orderId: Number(order.id),
orderItemId: Number(item.id),
@@ -119,18 +145,6 @@ export async function syncWorkerOrdersForSourceOrder(
)
continue
}
if (await getWorkOrderByOrderItemId(item.id)) {
skipped.push({ orderItemId: Number(item.id), reason: 'already_exists' })
decisions.push(
buildWorkerConsumerDecision(item, {
result: 'existing',
reason: 'already_exists',
ruleId: rule.id,
ruleKey: rule.rule_key,
}),
)
continue
}
// 订单号跨订单唯一:同一订单的多个 SKU 件允许共享订单号(按件建单是既有设计),
// 但订单号已挂在其他订单下时跳过,避免自动同步产生撞号工单。
const samePlatformOrders = await listWorkOrdersByPlatformOrderId(order.platform_order_id)
@@ -258,23 +272,29 @@ export async function syncWorkerOrdersForSourceOrder(
}
}
logInfo('[worker-order-consumer]', '发单平台消费者处理完成', {
const summary = {
source: options.source || 'source_order',
orderId: order.id,
platformOrderId: order.platform_order_id,
orderItemCount: orderItems.length,
createdCount: created.length,
skippedCount: skipped.length,
decisions,
})
skipReasons: formatDecisionReasonCounts(skipped),
}
// 常态「全跳过」每轮 PDD upsert 都会触发,降到 debug,避免日志按天被切碎。
if (created.length > 0) {
logInfo('[worker-order-consumer]', '发单平台消费者处理完成', summary)
} else {
logDebug('[worker-order-consumer]', '发单平台消费者无新建工单', summary)
}
const failedDecisions = decisions.filter((decision) => decision.result === 'failed')
if (failedDecisions.length > 0) {
logWarn('[worker-order-consumer]', '发单平台消费者存在未成功落库的订单项', {
source: options.source || 'source_order',
orderId: order.id,
platformOrderId: order.platform_order_id,
decisions: failedDecisions,
...summary,
failedCount: failedDecisions.length,
failedReasons: formatDecisionReasonCounts(failedDecisions),
})
}
@@ -309,3 +329,17 @@ function buildWorkerConsumerDecision(
...detail,
}
}
function formatDecisionReasonCounts(decisions: Array<Record<string, unknown>>) {
const counts = summarizeDecisionReasons(decisions)
return Object.keys(counts).length > 0 ? JSON.stringify(counts) : ''
}
function summarizeDecisionReasons(decisions: Array<Record<string, unknown>>) {
const counts: Record<string, number> = {}
for (const decision of decisions) {
const reason = String(decision.reason || decision.result || 'unknown').trim() || 'unknown'
counts[reason] = (counts[reason] || 0) + 1
}
return counts
}
@@ -149,14 +149,22 @@ export function resolveKuaishouWorkProductMatchContext(
): KuaishouWorkProductMatchContext {
const snapshot = safeParseJson(item.item_snapshot_json)
const source = safeParseJson(snapshot.kuaishouSendCode)
const outSkuSn = String(snapshot.out_sku_sn || snapshot.outSkuSn || '').trim()
return {
sellerId: String(source.sellerId || '').trim(),
itemId: String(source.itemId || '').trim(),
relItemId: normalizeExternalMatchId(source.relItemId),
skuId: String(source.skuId || '').trim(),
relSkuId: normalizeExternalMatchId(source.relSkuId),
itemTitle: String(source.itemTitle || '').trim(),
skuNick: String(source.skuNick || source.skuName || snapshot.externalSkuName || '').trim(),
relItemId: normalizeExternalMatchId(source.relItemId || snapshot.goodsId || snapshot.goods_id),
skuId: String(source.skuId || snapshot.sku_id || '').trim(),
relSkuId: normalizeExternalMatchId(source.relSkuId || outSkuSn || snapshot.spec),
itemTitle: String(source.itemTitle || snapshot.goodsName || snapshot.goods_name || '').trim(),
skuNick: String(
source.skuNick ||
source.skuName ||
outSkuSn ||
snapshot.externalSkuName ||
snapshot.spec ||
'',
).trim(),
}
}
@@ -228,19 +236,24 @@ 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)
const outSkuSn = String(snapshot.out_sku_sn || snapshot.outSkuSn || '').trim()
const context = {
sellerId: normalizeMatchId(source.sellerId),
itemId: normalizeMatchId(source.itemId),
relItemId: normalizeMatchId(source.relItemId),
relItemId: normalizeMatchId(source.relItemId || snapshot.goodsId || snapshot.goods_id),
skuId: normalizeMatchId(source.skuId),
relSkuId: normalizeMatchId(source.relSkuId),
itemTitle: normalizeMatchText(source.itemTitle),
skuNick: normalizeMatchText(source.skuNick || source.skuName || snapshot.externalSkuName),
relSkuId: normalizeMatchId(source.relSkuId || outSkuSn || snapshot.spec),
itemTitle: normalizeMatchText(
source.itemTitle || snapshot.goodsName || snapshot.goods_name || '',
),
// 拼多多等平台:结构化匹配也要读 out_sku_sn,避免有编码却未命中
skuNick: normalizeMatchText(
source.skuNick || source.skuName || outSkuSn || snapshot.externalSkuName || snapshot.spec,
),
}
const ruleShopId = normalizeMatchId(rule.shop_id)
if (
@@ -256,7 +269,10 @@ function scoreWorkProductRule(order: OrderRow, item: OrderItemRow, rule: WorkPro
!['itemTitleMatchType', 'skuNickMatchType'].includes(key) && String(value || '').trim(),
)
if (hasStructuredMatch) {
// 结构化 match_json(sellerId/skuNick 等)是快手发码路径;拼多多等无店铺上下文的订单
// 若误入该分支,常因缺 sellerId 直接失败,且不会回退到模板名/out_sku_sn。
// 仅当订单带有店铺上下文时才走结构化匹配。
if (hasStructuredMatch && context.sellerId) {
let score = 100
if (!matchCondition(context.sellerId, match.sellerId)) return null
if (match.sellerId) score += 1000
@@ -284,12 +300,14 @@ function scoreWorkProductRule(order: OrderRow, item: OrderItemRow, rule: WorkPro
return { rule, score }
}
const skuCodes = [item.sku_code, snapshot.externalSkuCode, snapshot.externalItemId]
const skuCodes = [item.sku_code, snapshot.externalSkuCode, outSkuSn, snapshot.externalItemId]
.map(normalizeMatchId)
.filter(Boolean)
const skuNames = [
item.sku_name,
snapshot.externalSkuName,
outSkuSn,
snapshot.spec,
snapshot.externalSkuNameNormalized,
source.skuNick,
source.itemTitle,
@@ -298,20 +316,131 @@ 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 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))),
)
if (ruleSkuCode && !skuCodes.includes(ruleSkuCode)) return null
if (!ruleProductName) {
return ruleSkuCode ? { rule, score: 120 } : null
// 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 (rule.match_type === 'exact') {
return skuNames.includes(ruleProductName) || skuCodes.includes(ruleProductName)
? { rule, score: ruleSkuCode ? 300 : 150 }
: null
if (skuCodeMatched) score += exactProductName || containsProductName ? 40 : 120
return { rule, score }
}
/** 拼多多常复用原快手模板;来源/平台残留 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) {
@@ -576,6 +576,196 @@ test('商品映射不以公共词包含匹配,完整大标题不同则不命
assert.equal(decision.rule, null)
})
test('拼多多订单使用 out_sku_sn 字段精准匹配接单规则', () => {
const rule = buildProductRule({
provider: 'pdd',
platform: 'pdd',
sku_code: '安卓血宴3钻',
product_name: '安卓血宴3钻',
match_type: 'exact',
})
const order = { ...buildOrderRow(), provider: 'pdd', platform: 'pdd' }
const item = {
...buildOrderItemRow(),
sku_code: '安卓血宴3钻',
sku_name: '安卓血宴3钻',
item_snapshot_json: {
goods_id: 12345678,
goods_name: '和平精英苏尔南手枪皮肤',
spec: '暗潮1钻',
out_sku_sn: '安卓血宴3钻',
},
}
const decision = resolveMatchingProductRuleDecision(order, item, [rule], [])
assert.equal(decision.reason, 'matched')
assert.equal(decision.rule?.id, rule.id)
})
test('拼多多订单在结构化 match_json 下仍可用 out_sku_sn 命中', () => {
const rule = buildProductRule({
provider: 'pdd',
platform: 'pdd',
product_name: '安卓怒嚎3钻',
match_type: 'contains',
match_json: {
skuNick: '安卓怒嚎3钻',
skuNickMatchType: 'contains',
},
})
const order = { ...buildOrderRow(), provider: 'pdd', platform: 'pdd', shop_id: 'pdd_shop' }
const item = {
...buildOrderItemRow(),
sku_code: '安卓怒嚎3钻',
sku_name: '安卓怒嚎3钻',
item_snapshot_json: {
goods_id: 961856378217,
goods_name: '和平精英地铁逃生苏尔南暗潮怒嚎血宴苏尔南1-6钻手枪传世武器',
spec: '安卓区(qv互通),苏尔南-怒嚎3钻',
out_sku_sn: '安卓怒嚎3钻',
},
}
const decision = resolveMatchingProductRuleDecision(order, item, [rule], [])
assert.equal(decision.reason, 'matched')
assert.equal(decision.rule?.id, rule.id)
})
test('拼多多订单忽略模板残留的快手结构化 match_json,按 out_sku_sn 命中模板名', () => {
const rule = buildProductRule({
provider: '',
platform: '',
product_name: '安卓怒嚎1钻',
match_type: 'contains',
// 历史保存残留的快手字段:若强制走结构化会因缺 sellerId 直接未命中
match_json: {
sellerId: '2174663396',
relItemId: '961856378217',
skuNick: '安卓怒嚎1钻',
skuNickMatchType: 'exact',
},
})
const order = { ...buildOrderRow(), provider: 'pdd', platform: 'pdd', shop_id: 'pdd_shop_1' }
const item = {
...buildOrderItemRow(),
sku_code: '安卓怒嚎1钻',
sku_name: '安卓怒嚎1钻',
item_snapshot_json: {
goodsId: '961856378217',
goods_id: '961856378217',
spec: '安卓区(qv互通),苏尔南-怒嚎1钻',
outSkuSn: '安卓怒嚎1钻',
out_sku_sn: '安卓怒嚎1钻',
},
}
const decision = resolveMatchingProductRuleDecision(order, item, [rule], [], {
legacyFallbackEnabled: false,
})
assert.equal(decision.reason, 'matched')
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 在短模板名 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',
platform: 'pdd',
product_name: '安卓怒嚎3钻',
match_type: 'contains',
})
const order = { ...buildOrderRow(), provider: 'pdd', platform: 'pdd' }
const item = {
...buildOrderItemRow(),
sku_code: '961856378217',
sku_name: '安卓区(qv互通),苏尔南-怒嚎3钻',
item_snapshot_json: {
goods_id: 961856378217,
goods_name: '和平精英地铁逃生苏尔南暗潮怒嚎血宴苏尔南1-6钻手枪传世武器',
spec: '安卓区(qv互通),苏尔南-怒嚎3钻',
},
}
const decision = resolveMatchingProductRuleDecision(order, item, [rule], [])
assert.equal(decision.reason, 'unmatched')
})
test('assertWorkerLoginAllowed blocks rejected (frozen) workers with worker_rejected', () => {
assert.throws(
() => assertWorkerLoginAllowed(buildWorkerUserRow({ status: 'rejected' })),