feat(worker-platform): 支持拼多多订单匹配诊断与通用发单规则匹配

1. 修复匹配诊断日志仅记录快手发单凭证的问题,支持记录拼多多等全平台订单的商品匹配结果;
2. 匹配诊断来源商品聚合查询 order_items 与 orders,拼多多商品正常展示并在测试中可用;
3. 接单模板规则平台默认值改为留空,支持跨平台通用匹配;
4. 接单模板重匹配功能支持重扫数据库中未建工单的订单并自动生成接单工单。
This commit is contained in:
yml2213
2026-09-12 15:42:43 +08:00
parent c8f7e48746
commit e7b51f09a6
4 changed files with 161 additions and 24 deletions
@@ -1,3 +1,4 @@
import { query } from '../../db/client.js'
import { listKuaishouIndustryVoucherWorkOrderSyncSources } from '../../repositories/kuaishou-industry-voucher-repo.js'
import {
deleteWorkProductRuleMapping,
@@ -183,7 +184,38 @@ export async function deleteAdminWorkProductRuleMapping(mappingId: number | stri
export async function listAdminKuaishouMatchSources(payload: JsonObject = {}) {
const limit = Math.min(500, normalizePositiveInteger(payload.limit, 100))
const sources = await listKuaishouIndustryVoucherWorkOrderSyncSources(limit)
const [sources, recentOrderItemsResult] = await Promise.all([
listKuaishouIndustryVoucherWorkOrderSyncSources(limit),
query<{
platform: string
shop_id: string
platform_order_id: string
sku_code: string
sku_name: string
item_snapshot_json: unknown
order_raw_payload: unknown
created_at: string
updated_at: string
}>(
`
SELECT
o.platform,
o.shop_id,
o.platform_order_id,
o.raw_payload_json AS order_raw_payload,
oi.sku_code,
oi.sku_name,
oi.item_snapshot_json,
oi.created_at,
oi.updated_at
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
ORDER BY oi.id DESC
LIMIT $1
`,
[limit],
),
])
const grouped = new Map<string, JsonObject>()
for (const source of sources) {
const rawPayload = safeParseJson(source.raw_payload_json)
@@ -215,6 +247,51 @@ export async function listAdminKuaishouMatchSources(payload: JsonObject = {}) {
rawPayload,
})
}
for (const row of recentOrderItemsResult.rows) {
const snapshot = safeParseJson(row.item_snapshot_json)
const ksc = safeParseJson(snapshot.kuaishouSendCode)
const sellerId = normalizeKuaishouMatchId(
ksc.sellerId || row.shop_id || row.platform,
)
const relItemId = normalizeKuaishouMatchId(
ksc.relItemId || row.sku_code || snapshot.goodsId || snapshot.goods_id,
)
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()
if (!sellerId || (!relItemId && !itemTitle)) continue
const key = [
sellerId,
relItemId,
normalizeMatchTextForAdmin(itemTitle),
relSkuId,
normalizeMatchTextForAdmin(skuNick),
].join('|')
const current = grouped.get(key)
const rawPayload =
Object.keys(snapshot).length > 0 ? snapshot : safeParseJson(row.order_raw_payload)
grouped.set(key, {
sellerId,
relItemId,
itemTitle,
relSkuId,
skuNick,
sampleOid: String(row.platform_order_id || '').trim(),
lastSeenAt: row.updated_at || row.created_at,
seenCount: Number(current?.seenCount || 0) + 1,
rawPayload,
})
}
return {
items: Array.from(grouped.values()).sort((left, right) =>
String(right.lastSeenAt).localeCompare(String(left.lastSeenAt)),
@@ -225,27 +302,38 @@ 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 isPdd =
body.platform === 'pdd' ||
String(body.sellerId || '').includes('pdd') ||
Boolean(body.goodsName || body.goods_name || body.goodsId || body.goods_id)
const order = {
provider: '91kaquan',
platform: 'kuaishou',
shop_id: String(body.sellerId || '').trim(),
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 item = {
id: 0,
order_id: 0,
sku_code: String(body.itemId || '').trim(),
sku_name: String(ext.skuNick || '').trim(),
quantity: Math.max(1, Number(body.num) || 1),
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),
spec_json: {},
item_snapshot_json: {
goodsId: body.goodsId || body.goods_id,
goodsName: body.goodsName || body.goods_name,
spec: body.spec,
kuaishouSendCode: {
sellerId: String(body.sellerId || '').trim(),
sellerId: String(body.sellerId || order.shop_id || '').trim(),
itemId: String(body.itemId || '').trim(),
itemTitle: String(body.itemTitle || '').trim(),
itemTitle,
skuId: String(body.skuId || '').trim(),
relItemId: normalizeKuaishouMatchId(ext.relItemId),
relSkuId: normalizeKuaishouMatchId(ext.relSkuId),
skuNick: String(ext.skuNick || '').trim(),
relItemId,
relSkuId,
skuNick,
},
},
} as OrderItemRow
@@ -1,3 +1,4 @@
import { query } from '../../db/client.js'
/**
* 快手 send-code 数据接入发单平台(工单侧)。
*
@@ -202,7 +203,37 @@ export async function reprocessKuaishouSendCodeWorkOrders(limit = 100) {
skippedCount += result.skipped.length
}
return { scannedCount: sources.length, createdCount, skippedCount }
// Also rescan general platform orders (e.g. PDD) that do not have work orders yet
const unfulfilledOrders = await query<OrderRow>(
`
SELECT o.*
FROM orders o
WHERE o.order_status IN ('paid', 'shipped')
AND NOT EXISTS (
SELECT 1 FROM work_orders wo WHERE wo.order_id = o.id
)
ORDER BY o.id DESC
LIMIT $1
`,
[limit],
)
for (const order of unfulfilledOrders.rows) {
const items = await listOrderItemsByOrderId(order.id)
if (items.length === 0) continue
const syncResult = await syncWorkerOrdersForSourceOrder(order, items, {
source: 'reprocess_matches',
autoOnly: true,
})
createdCount += syncResult.createdCount
skippedCount += syncResult.skippedCount
}
return {
scannedCount: sources.length + unfulfilledOrders.rows.length,
createdCount,
skippedCount,
}
}
async function backfillOrderShop(
@@ -52,17 +52,34 @@ export async function syncWorkerOrdersForSourceOrder(
legacyFallbackEnabled: productMatchConfig.legacyFallbackEnabled,
})
const matchContext = resolveKuaishouWorkProductMatchContext(item)
if (matchContext.sellerId) {
const snapshot = safeParseJson(item.item_snapshot_json)
const snapshot = safeParseJson(item.item_snapshot_json)
const sellerId =
matchContext.sellerId ||
String(order.shop_id || order.provider || order.platform || '').trim()
const relItemId =
matchContext.relItemId ||
String(item.sku_code || snapshot.goodsId || snapshot.goods_id || '').trim()
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()
// Record product match diagnostics for all platform orders (including PDD)
if (sellerId || relItemId || itemTitle) {
await createWorkProductMatchLog({
orderId: Number(order.id),
orderItemId: Number(item.id),
source: options.source || 'source_order',
sellerId: matchContext.sellerId,
relItemId: matchContext.relItemId,
itemTitle: matchContext.itemTitle,
relSkuId: matchContext.relSkuId,
skuNick: matchContext.skuNick,
sellerId,
relItemId,
itemTitle,
relSkuId,
skuNick,
matchStatus: match.reason,
ruleId: match.rule?.id || null,
mappingId: match.mappingId,
@@ -71,6 +88,7 @@ export async function syncWorkerOrdersForSourceOrder(
options.sourceMetadata?.kuaishouSendCodeRawPayload ||
snapshot.kuaishouSendCodeRawPayload ||
snapshot.kuaishouSendCode ||
snapshot ||
{},
),
now: nowIso(),
@@ -424,7 +424,7 @@ export default function ProductRulesPanel() {
const response = await reprocessAdminKuaishouWorkOrderMatches()
const result = response.data
message.success(
`已扫描 ${result.scannedCount} 笔快手订单,新增 ${result.createdCount} 个接单工单`,
`已扫描 ${result.scannedCount} 笔订单,新增 ${result.createdCount} 个接单工单`,
)
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-product-rules'] }),
@@ -432,7 +432,7 @@ export default function ProductRulesPanel() {
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-orders'] }),
])
} catch (error) {
message.error(error instanceof Error ? error.message : '快手订单重匹配失败')
message.error(error instanceof Error ? error.message : '订单重匹配失败')
}
}
@@ -744,7 +744,7 @@ export default function ProductRulesPanel() {
layout="vertical"
className="product-rule-main-form"
initialValues={{
platform: 'kuaishou',
platform: '',
matchType: 'contains',
itemTitleMatchType: 'exact',
skuNickMatchType: 'exact',
@@ -914,7 +914,7 @@ export default function ProductRulesPanel() {
</Form.Item>
<Form.Item label="平台" name="platform">
<Input placeholder="如 kuaishou" />
<Input placeholder="留空通用所有平台,或如 pdd / kuaishou" />
</Form.Item>
</div>