fix(pdd): 匹配诊断对齐 out_sku_sn,并加速拼多多订单号搜索
匹配测试/结构化规则补读商家编码,诊断日志标出缺失字段;订单/任务/工单关键词支持带连字符的 PDD order_sn 走索引快路径。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -56,6 +56,38 @@ test('订单列表保留订单号和精确 SKU 旧筛选参数兼容', async ()
|
||||
assert.deepEqual(calls[0]?.params, ['%26245%', 'SKU-1'])
|
||||
})
|
||||
|
||||
test('订单列表关键词为拼多多 order_sn 时走平台订单号索引快路径', async () => {
|
||||
const calls: Array<{ sql: string; params: unknown[] }> = []
|
||||
const orderSn = '260915-091435834962130'
|
||||
|
||||
await listOrdersWithExecutor(createExecutor(calls, 0), {
|
||||
keyword: orderSn,
|
||||
})
|
||||
|
||||
assert.match(
|
||||
calls[0]?.sql || '',
|
||||
/o\.platform_order_id = \$1 OR o\.platform_order_id LIKE \$1 \|\| '%'/s,
|
||||
)
|
||||
assert.doesNotMatch(calls[0]?.sql || '', /oi\.sku_code/i)
|
||||
assert.doesNotMatch(calls[0]?.sql || '', /item_snapshot_json/i)
|
||||
assert.deepEqual(calls[0]?.params, [orderSn])
|
||||
})
|
||||
|
||||
test('订单列表 platformOrderId 为拼多多 order_sn 时走索引快路径', async () => {
|
||||
const calls: Array<{ sql: string; params: unknown[] }> = []
|
||||
const orderSn = '260915-091435834962130'
|
||||
|
||||
await listOrdersWithExecutor(createExecutor(calls, 0), {
|
||||
platformOrderId: orderSn,
|
||||
})
|
||||
|
||||
assert.match(
|
||||
calls[0]?.sql || '',
|
||||
/o\.platform_order_id = \$1 OR o\.platform_order_id LIKE \$1 \|\| '%'/s,
|
||||
)
|
||||
assert.deepEqual(calls[0]?.params, [orderSn])
|
||||
})
|
||||
|
||||
function createExecutor(
|
||||
calls: Array<{ sql: string; params: unknown[] }>,
|
||||
total: number,
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
OrderUpdateInput,
|
||||
} from '../types/repository/inputs.js'
|
||||
import type { OrderListQueryResult, OrderListRow, OrderRow } from '../types/repository/rows.js'
|
||||
import { isPlatformOrderIdLookupKeyword } from '../utils/platform-order-id.js'
|
||||
|
||||
export type OrderQueryExecutor = <T extends QueryResultRow = QueryResultRow>(
|
||||
text: string,
|
||||
@@ -251,9 +252,9 @@ export async function listOrdersWithExecutor(
|
||||
|
||||
if (keyword) {
|
||||
const trimmedKeyword = String(keyword).trim()
|
||||
const isPureOrderNo = /^\d{6,}$/.test(trimmedKeyword)
|
||||
const isOrderNoLookup = isPlatformOrderIdLookupKeyword(trimmedKeyword)
|
||||
|
||||
if (isPureOrderNo) {
|
||||
if (isOrderNoLookup) {
|
||||
params.push(trimmedKeyword)
|
||||
const paramIndex = `$${params.length}`
|
||||
filters.push(
|
||||
@@ -299,8 +300,17 @@ export async function listOrdersWithExecutor(
|
||||
}
|
||||
|
||||
if (platformOrderId) {
|
||||
params.push(`%${platformOrderId}%`)
|
||||
filters.push(`o.platform_order_id ILIKE $${params.length}`)
|
||||
const trimmedPlatformOrderId = String(platformOrderId).trim()
|
||||
if (isPlatformOrderIdLookupKeyword(trimmedPlatformOrderId)) {
|
||||
params.push(trimmedPlatformOrderId)
|
||||
const paramIndex = `$${params.length}`
|
||||
filters.push(
|
||||
`(o.platform_order_id = ${paramIndex} OR o.platform_order_id LIKE ${paramIndex} || '%')`,
|
||||
)
|
||||
} else {
|
||||
params.push(`%${trimmedPlatformOrderId}%`)
|
||||
filters.push(`o.platform_order_id ILIKE $${params.length}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (payStatus) {
|
||||
|
||||
@@ -62,6 +62,20 @@ test('任务列表关键词为纯数字时智能路由到平台订单号索引
|
||||
assert.deepEqual(result.params, ['20247230034021572'])
|
||||
})
|
||||
|
||||
test('任务列表关键词为拼多多 order_sn 时智能路由到平台订单号索引条件', () => {
|
||||
const result = buildTaskListFilters({
|
||||
keyword: '260915-091435834962130',
|
||||
})
|
||||
|
||||
assert.match(
|
||||
result.whereClause,
|
||||
/ft\.platform_order_id = \$1 OR ft\.platform_order_id LIKE \$1 \|\| '%'/s,
|
||||
)
|
||||
assert.doesNotMatch(result.whereClause, /oi\.sku_code/i)
|
||||
assert.doesNotMatch(result.whereClause, /item_snapshot_json/i)
|
||||
assert.deepEqual(result.params, ['260915-091435834962130'])
|
||||
})
|
||||
|
||||
test('任务列表关键词为 DT 任务号时智能路由到任务号索引条件', () => {
|
||||
const result = buildTaskListFilters({
|
||||
keyword: 'DT881aa66294cc',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { query, withTransaction } from '../db/client.js'
|
||||
import type { PoolClient } from 'pg'
|
||||
|
||||
import { isPlatformOrderIdLookupKeyword } from '../utils/platform-order-id.js'
|
||||
import { normalizeTimestampIso } from '../utils/time.js'
|
||||
import type {
|
||||
TaskCreateInput,
|
||||
@@ -471,10 +472,10 @@ export function buildTaskListFilters({
|
||||
|
||||
if (keyword) {
|
||||
const trimmedKeyword = String(keyword).trim()
|
||||
const isPureOrderNo = /^\d{6,}$/.test(trimmedKeyword)
|
||||
const isOrderNoLookup = isPlatformOrderIdLookupKeyword(trimmedKeyword)
|
||||
const isTaskNo = /^DT[a-zA-Z0-9_-]+$/i.test(trimmedKeyword)
|
||||
|
||||
if (isPureOrderNo) {
|
||||
if (isOrderNoLookup) {
|
||||
params.push(trimmedKeyword)
|
||||
const paramIndex = `$${params.length}`
|
||||
filters.push(
|
||||
@@ -510,8 +511,17 @@ export function buildTaskListFilters({
|
||||
}
|
||||
|
||||
if (platformOrderId) {
|
||||
params.push(`%${platformOrderId}%`)
|
||||
filters.push(`ft.platform_order_id ILIKE $${params.length}`)
|
||||
const trimmedPlatformOrderId = String(platformOrderId).trim()
|
||||
if (isPlatformOrderIdLookupKeyword(trimmedPlatformOrderId)) {
|
||||
params.push(trimmedPlatformOrderId)
|
||||
const paramIndex = `$${params.length}`
|
||||
filters.push(
|
||||
`(ft.platform_order_id = ${paramIndex} OR ft.platform_order_id LIKE ${paramIndex} || '%')`,
|
||||
)
|
||||
} else {
|
||||
params.push(`%${trimmedPlatformOrderId}%`)
|
||||
filters.push(`ft.platform_order_id ILIKE $${params.length}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (taskNo) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
normalizeWorkOrderAcceptanceMode,
|
||||
WORK_ORDER_GIFT_PHASE,
|
||||
} from '../../domain/work-order-acceptance-mode.js'
|
||||
import { isPlatformOrderIdLookupKeyword } from '../../utils/platform-order-id.js'
|
||||
export const WORK_ORDER_SELECT = `
|
||||
SELECT
|
||||
wo.*,
|
||||
@@ -819,8 +820,7 @@ export function buildWorkOrderWhere({
|
||||
filters.push(`wo.id = $${params.length}`)
|
||||
}
|
||||
if (normalizedKeyword) {
|
||||
const isPureOrderNo = /^\d{6,}$/.test(normalizedKeyword)
|
||||
if (isPureOrderNo) {
|
||||
if (isPlatformOrderIdLookupKeyword(normalizedKeyword)) {
|
||||
params.push(normalizedKeyword)
|
||||
const paramIndex = `$${params.length}`
|
||||
filters.push(
|
||||
|
||||
@@ -16,6 +16,10 @@ 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,
|
||||
@@ -251,15 +255,21 @@ 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 +306,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(),
|
||||
|
||||
@@ -241,14 +241,20 @@ function scoreWorkProductRule(order: OrderRow, item: OrderItemRow, rule: WorkPro
|
||||
|
||||
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 (
|
||||
@@ -292,7 +298,6 @@ function scoreWorkProductRule(order: OrderRow, item: OrderItemRow, rule: WorkPro
|
||||
return { rule, score }
|
||||
}
|
||||
|
||||
const outSkuSn = String(snapshot.out_sku_sn || snapshot.outSkuSn || '').trim()
|
||||
const skuCodes = [item.sku_code, snapshot.externalSkuCode, outSkuSn, snapshot.externalItemId]
|
||||
.map(normalizeMatchId)
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -602,6 +602,58 @@ test('拼多多订单使用 out_sku_sn 字段精准匹配接单规则', () => {
|
||||
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('拼多多订单缺少 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' })),
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { isPlatformOrderIdLookupKeyword } from './platform-order-id.js'
|
||||
|
||||
test('识别纯数字平台订单号', () => {
|
||||
assert.equal(isPlatformOrderIdLookupKeyword('20247230034021572'), true)
|
||||
assert.equal(isPlatformOrderIdLookupKeyword('123456'), true)
|
||||
assert.equal(isPlatformOrderIdLookupKeyword('12345'), false)
|
||||
})
|
||||
|
||||
test('识别拼多多带连字符的 order_sn', () => {
|
||||
assert.equal(isPlatformOrderIdLookupKeyword('260915-091435834962130'), true)
|
||||
assert.equal(isPlatformOrderIdLookupKeyword('260912-123456789012345'), true)
|
||||
})
|
||||
|
||||
test('拒绝普通关键词', () => {
|
||||
assert.equal(isPlatformOrderIdLookupKeyword('皮肤店'), false)
|
||||
assert.equal(isPlatformOrderIdLookupKeyword('安卓怒嚎3钻'), false)
|
||||
assert.equal(isPlatformOrderIdLookupKeyword('DT881aa66294cc'), false)
|
||||
assert.equal(isPlatformOrderIdLookupKeyword(''), false)
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Detect platform order IDs that should use indexed equality/prefix lookup.
|
||||
* Covers pure numeric IDs (快手等) and PDD style `digits-digits` (e.g. 260915-091435834962130).
|
||||
*/
|
||||
export function isPlatformOrderIdLookupKeyword(value: unknown): boolean {
|
||||
const trimmed = String(value || '').trim()
|
||||
return /^\d{6,}(-\d{6,})?$/.test(trimmed)
|
||||
}
|
||||
Reference in New Issue
Block a user