feat(worker-platform): 支持拼多多订单匹配诊断与通用发单规则匹配
1. 修复匹配诊断日志仅记录快手发单凭证的问题,支持记录拼多多等全平台订单的商品匹配结果; 2. 匹配诊断来源商品聚合查询 order_items 与 orders,拼多多商品正常展示并在测试中可用; 3. 接单模板规则平台默认值改为留空,支持跨平台通用匹配; 4. 接单模板重匹配功能支持重扫数据库中未建工单的订单并自动生成接单工单。
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { query } from '../../db/client.js'
|
||||||
import { listKuaishouIndustryVoucherWorkOrderSyncSources } from '../../repositories/kuaishou-industry-voucher-repo.js'
|
import { listKuaishouIndustryVoucherWorkOrderSyncSources } from '../../repositories/kuaishou-industry-voucher-repo.js'
|
||||||
import {
|
import {
|
||||||
deleteWorkProductRuleMapping,
|
deleteWorkProductRuleMapping,
|
||||||
@@ -183,7 +184,38 @@ export async function deleteAdminWorkProductRuleMapping(mappingId: number | stri
|
|||||||
|
|
||||||
export async function listAdminKuaishouMatchSources(payload: JsonObject = {}) {
|
export async function listAdminKuaishouMatchSources(payload: JsonObject = {}) {
|
||||||
const limit = Math.min(500, normalizePositiveInteger(payload.limit, 100))
|
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>()
|
const grouped = new Map<string, JsonObject>()
|
||||||
for (const source of sources) {
|
for (const source of sources) {
|
||||||
const rawPayload = safeParseJson(source.raw_payload_json)
|
const rawPayload = safeParseJson(source.raw_payload_json)
|
||||||
@@ -215,6 +247,51 @@ export async function listAdminKuaishouMatchSources(payload: JsonObject = {}) {
|
|||||||
rawPayload,
|
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 {
|
return {
|
||||||
items: Array.from(grouped.values()).sort((left, right) =>
|
items: Array.from(grouped.values()).sort((left, right) =>
|
||||||
String(right.lastSeenAt).localeCompare(String(left.lastSeenAt)),
|
String(right.lastSeenAt).localeCompare(String(left.lastSeenAt)),
|
||||||
@@ -225,27 +302,38 @@ export async function listAdminKuaishouMatchSources(payload: JsonObject = {}) {
|
|||||||
export async function testAdminKuaishouProductMatch(payload: JsonObject = {}) {
|
export async function testAdminKuaishouProductMatch(payload: JsonObject = {}) {
|
||||||
const body = resolveKuaishouMatchPayload(payload.rawPayload ?? payload.raw_payload ?? payload)
|
const body = resolveKuaishouMatchPayload(payload.rawPayload ?? payload.raw_payload ?? payload)
|
||||||
const ext = safeParseJson(body.ext)
|
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 = {
|
const order = {
|
||||||
provider: '91kaquan',
|
provider: String(body.provider || (isPdd ? 'pdd' : '91kaquan')).trim(),
|
||||||
platform: 'kuaishou',
|
platform: String(body.platform || (isPdd ? 'pdd' : 'kuaishou')).trim(),
|
||||||
shop_id: String(body.sellerId || '').trim(),
|
shop_id: String(body.sellerId || body.shop_id || '').trim(),
|
||||||
} as OrderRow
|
} 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 = {
|
const item = {
|
||||||
id: 0,
|
id: 0,
|
||||||
order_id: 0,
|
order_id: 0,
|
||||||
sku_code: String(body.itemId || '').trim(),
|
sku_code: String(body.sku_code || body.itemId || relItemId || '').trim(),
|
||||||
sku_name: String(ext.skuNick || '').trim(),
|
sku_name: String(body.sku_name || skuNick || itemTitle || '').trim(),
|
||||||
quantity: Math.max(1, Number(body.num) || 1),
|
quantity: Math.max(1, Number(body.num || body.quantity) || 1),
|
||||||
spec_json: {},
|
spec_json: {},
|
||||||
item_snapshot_json: {
|
item_snapshot_json: {
|
||||||
|
goodsId: body.goodsId || body.goods_id,
|
||||||
|
goodsName: body.goodsName || body.goods_name,
|
||||||
|
spec: body.spec,
|
||||||
kuaishouSendCode: {
|
kuaishouSendCode: {
|
||||||
sellerId: String(body.sellerId || '').trim(),
|
sellerId: String(body.sellerId || order.shop_id || '').trim(),
|
||||||
itemId: String(body.itemId || '').trim(),
|
itemId: String(body.itemId || '').trim(),
|
||||||
itemTitle: String(body.itemTitle || '').trim(),
|
itemTitle,
|
||||||
skuId: String(body.skuId || '').trim(),
|
skuId: String(body.skuId || '').trim(),
|
||||||
relItemId: normalizeKuaishouMatchId(ext.relItemId),
|
relItemId,
|
||||||
relSkuId: normalizeKuaishouMatchId(ext.relSkuId),
|
relSkuId,
|
||||||
skuNick: String(ext.skuNick || '').trim(),
|
skuNick,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as OrderItemRow
|
} as OrderItemRow
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { query } from '../../db/client.js'
|
||||||
/**
|
/**
|
||||||
* 快手 send-code 数据接入发单平台(工单侧)。
|
* 快手 send-code 数据接入发单平台(工单侧)。
|
||||||
*
|
*
|
||||||
@@ -202,7 +203,37 @@ export async function reprocessKuaishouSendCodeWorkOrders(limit = 100) {
|
|||||||
skippedCount += result.skipped.length
|
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(
|
async function backfillOrderShop(
|
||||||
|
|||||||
@@ -52,17 +52,34 @@ export async function syncWorkerOrdersForSourceOrder(
|
|||||||
legacyFallbackEnabled: productMatchConfig.legacyFallbackEnabled,
|
legacyFallbackEnabled: productMatchConfig.legacyFallbackEnabled,
|
||||||
})
|
})
|
||||||
const matchContext = resolveKuaishouWorkProductMatchContext(item)
|
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({
|
await createWorkProductMatchLog({
|
||||||
orderId: Number(order.id),
|
orderId: Number(order.id),
|
||||||
orderItemId: Number(item.id),
|
orderItemId: Number(item.id),
|
||||||
source: options.source || 'source_order',
|
source: options.source || 'source_order',
|
||||||
sellerId: matchContext.sellerId,
|
sellerId,
|
||||||
relItemId: matchContext.relItemId,
|
relItemId,
|
||||||
itemTitle: matchContext.itemTitle,
|
itemTitle,
|
||||||
relSkuId: matchContext.relSkuId,
|
relSkuId,
|
||||||
skuNick: matchContext.skuNick,
|
skuNick,
|
||||||
matchStatus: match.reason,
|
matchStatus: match.reason,
|
||||||
ruleId: match.rule?.id || null,
|
ruleId: match.rule?.id || null,
|
||||||
mappingId: match.mappingId,
|
mappingId: match.mappingId,
|
||||||
@@ -71,6 +88,7 @@ export async function syncWorkerOrdersForSourceOrder(
|
|||||||
options.sourceMetadata?.kuaishouSendCodeRawPayload ||
|
options.sourceMetadata?.kuaishouSendCodeRawPayload ||
|
||||||
snapshot.kuaishouSendCodeRawPayload ||
|
snapshot.kuaishouSendCodeRawPayload ||
|
||||||
snapshot.kuaishouSendCode ||
|
snapshot.kuaishouSendCode ||
|
||||||
|
snapshot ||
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
now: nowIso(),
|
now: nowIso(),
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ export default function ProductRulesPanel() {
|
|||||||
const response = await reprocessAdminKuaishouWorkOrderMatches()
|
const response = await reprocessAdminKuaishouWorkOrderMatches()
|
||||||
const result = response.data
|
const result = response.data
|
||||||
message.success(
|
message.success(
|
||||||
`已扫描 ${result.scannedCount} 笔快手订单,新增 ${result.createdCount} 个接单工单`,
|
`已扫描 ${result.scannedCount} 笔订单,新增 ${result.createdCount} 个接单工单`,
|
||||||
)
|
)
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-product-rules'] }),
|
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-product-rules'] }),
|
||||||
@@ -432,7 +432,7 @@ export default function ProductRulesPanel() {
|
|||||||
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-orders'] }),
|
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-orders'] }),
|
||||||
])
|
])
|
||||||
} catch (error) {
|
} 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"
|
layout="vertical"
|
||||||
className="product-rule-main-form"
|
className="product-rule-main-form"
|
||||||
initialValues={{
|
initialValues={{
|
||||||
platform: 'kuaishou',
|
platform: '',
|
||||||
matchType: 'contains',
|
matchType: 'contains',
|
||||||
itemTitleMatchType: 'exact',
|
itemTitleMatchType: 'exact',
|
||||||
skuNickMatchType: 'exact',
|
skuNickMatchType: 'exact',
|
||||||
@@ -914,7 +914,7 @@ export default function ProductRulesPanel() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label="平台" name="platform">
|
<Form.Item label="平台" name="platform">
|
||||||
<Input placeholder="如 kuaishou" />
|
<Input placeholder="留空通用所有平台,或如 pdd / kuaishou" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user