diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index 7c015b6d..d8c17f2f 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -1054,7 +1054,7 @@ export async function getAdminWorkerPlatformSummary() { export async function syncWorkerOrdersForSourceOrder( order: OrderRow, orderItems: OrderItemRow[], - options: { source?: string; autoOnly?: boolean } = {}, + options: { source?: string; autoOnly?: boolean; sourceMetadata?: JsonObject } = {}, ) { const rules = await listWorkProductRules({ enabled: true }) const created: WorkOrderRow[] = [] @@ -1109,10 +1109,14 @@ export async function syncWorkerOrdersForSourceOrder( provider: order.provider, platform: order.platform, shopId: order.shop_id, + shopName: order.shop_name, skuCode: item.sku_code, skuName: item.sku_name, quantity: Number(item.quantity || 1), + orderAmountFen: Number(order.total_amount || 0), + currency: order.currency || 'CNY', syncSource: options.source || 'source_order', + ...options.sourceMetadata, }, }), requirementJson: JSON.stringify({ fields }), diff --git a/apps/backend/src/services/worker-platform/sync-work-orders-from-send-code.test.ts b/apps/backend/src/services/worker-platform/sync-work-orders-from-send-code.test.ts new file mode 100644 index 00000000..3e563b0d --- /dev/null +++ b/apps/backend/src/services/worker-platform/sync-work-orders-from-send-code.test.ts @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { + resolveKuaishouSendCodeShopName, + resolveKuaishouSendCodeWorkOrderData, +} from './sync-work-orders-from-send-code.js' + +test('快手发码工单优先使用 ext.skuNick 与 payment', () => { + const result = resolveKuaishouSendCodeWorkOrderData( + { + sellerId: '141242642', + itemId: '25873410102642', + itemTitle: '和平精英指挥官密钥秘钥隐藏款密钥暮色之下灰烬之翎珍宝礼盒礼卡', + skuId: '185469355612642', + paymentFen: 1400, + rawParams: { + ext: '{"skuNick":"指挥官密钥1个","payment":1400}', + }, + }, + [ + { + sellerId: '141242642', + shopId: '141242642', + shopName: '授权店铺名', + customShopName: '快手密钥店', + }, + ], + ) + + assert.deepEqual(result, { + sellerId: '141242642', + shopName: '快手密钥店', + itemId: '25873410102642', + skuId: '185469355612642', + skuName: '指挥官密钥1个', + paymentFen: 1400, + }) +}) + +test('未配置授权店铺名时保留 sellerId 兜底名称', () => { + assert.equal(resolveKuaishouSendCodeShopName('141242642'), '快手店铺-141242642') +}) diff --git a/apps/backend/src/services/worker-platform/sync-work-orders-from-send-code.ts b/apps/backend/src/services/worker-platform/sync-work-orders-from-send-code.ts index 1f7ad267..28b15df9 100644 --- a/apps/backend/src/services/worker-platform/sync-work-orders-from-send-code.ts +++ b/apps/backend/src/services/worker-platform/sync-work-orders-from-send-code.ts @@ -27,10 +27,15 @@ import { import { syncWorkerOrdersForSourceOrder } from './admin-service.js' import { createWorkOrderEvent } from '../../repositories/worker-platform/work-order-repo.js' import { OPEN_91_PLATFORM, OPEN_91_PROVIDER } from '../open-91/config.js' +import { + findKuaishouIndustryShopConfig, + type KuaishouIndustryShopConfig, +} from '../platforms/kuaishou-industry/source-config-service.js' import { asJsonObject, type JsonObject } from '../../types/json.js' import type { OrderUpdateInput } from '../../types/repository/inputs.js' import type { KuaishouIndustryVoucherRow, + OrderItemRow, OrderRow, } from '../../types/repository/rows.js' @@ -59,11 +64,21 @@ export type SyncWorkOrdersFromSendCodeResult = { skipped: string[] } +export type KuaishouSendCodeWorkOrderData = { + sellerId: string + shopName: string + itemId: string + skuId: string + skuName: string + paymentFen: number +} + export async function syncWorkOrdersFromKuaishouSendCode( input: SyncWorkOrdersFromSendCodeInput, ): Promise { const normalizedOid = String(input.oid || '').trim() const skipped: string[] = [] + const sourceData = resolveKuaishouSendCodeWorkOrderData(input) if (!normalizedOid) { return { @@ -83,13 +98,13 @@ export async function syncWorkOrdersFromKuaishouSendCode( }) if (order) { - const orderResult = await backfillOrderShopId(order, input) + const orderResult = await backfillOrderShop(order, input, sourceData) order = orderResult.order if (orderResult.skipped) { skipped.push(orderResult.skipped) } } else { - const created = await createFallbackOrder(input) + const created = await createFallbackOrder(input, sourceData) if (created.order) { order = created.order } else { @@ -101,11 +116,17 @@ export async function syncWorkOrdersFromKuaishouSendCode( let createdWorkOrderCount = 0 if (order) { - const orderItems = await listOrderItemsByOrderId(order.id) + const orderItems = applyKuaishouSendCodeItemData( + await listOrderItemsByOrderId(order.id), + sourceData, + ) const syncResult = await syncWorkerOrdersForSourceOrder(order, orderItems, { source: KUAISHOU_SEND_CODE_SOURCE, autoOnly: true, + sourceMetadata: { + kuaishouSendCode: sourceData, + }, }) createdWorkOrderCount = syncResult.createdCount for (const item of syncResult.skipped) { @@ -131,26 +152,35 @@ export async function syncWorkOrdersFromKuaishouSendCode( } } -async function backfillOrderShopId( +async function backfillOrderShop( order: OrderRow, input: SyncWorkOrdersFromSendCodeInput, + sourceData: KuaishouSendCodeWorkOrderData, ): Promise<{ order: OrderRow; skipped?: string }> { - const sellerId = String(input.sellerId || '').trim() + const sellerId = sourceData.sellerId const currentShopId = String(order.shop_id || '').trim() + const currentShopName = String(order.shop_name || '').trim() - if (!sellerId || currentShopId === sellerId) { + if (!sellerId) { return { order } } const isPlaceholderShopId = !currentShopId || currentShopId === OPEN_91_PROVIDER + const hasShopIdConflict = !isPlaceholderShopId && currentShopId !== sellerId - if (!isPlaceholderShopId) { + if (hasShopIdConflict) { return { order, skipped: `shop_id_conflict:${currentShopId}` } } + const shouldUpdateShopName = isPlaceholderShopName(currentShopName, sellerId) + if (currentShopId === sellerId && !shouldUpdateShopName) { + return { order } + } + const updated = await updateOrder(order.id, { ...buildOrderUpdateBase(order), shopId: sellerId, + shopName: shouldUpdateShopName ? sourceData.shopName : currentShopName, updatedAt: input.now, }) @@ -178,18 +208,18 @@ function buildOrderUpdateBase(order: OrderRow): Omit { const now = input.now - const sellerId = String(input.sellerId || '').trim() - const itemId = String(input.itemId || '').trim() - const itemTitle = String(input.itemTitle || '').trim() + const { sellerId, itemId, skuName, paymentFen } = sourceData const num = Math.max(1, Number(input.num) || 1) - const paymentFen = Math.max(0, Number(input.paymentFen) || 0) - if (!itemId && !itemTitle) { + if (!itemId && !skuName) { return { order: null, reason: 'order_item_missing' } } @@ -197,7 +227,7 @@ async function createFallbackOrder(input: SyncWorkOrdersFromSendCodeInput): Prom provider: OPEN_91_PROVIDER, platform: OPEN_91_PLATFORM, shopId: sellerId, - shopName: sellerId ? `快手店铺-${sellerId}` : '', + shopName: sourceData.shopName, platformOrderId: input.oid, orderStatus: KUAISHOU_SEND_CODE_ORDER_STATUS, payStatus: 'paid', @@ -223,11 +253,12 @@ async function createFallbackOrder(input: SyncWorkOrdersFromSendCodeInput): Prom await replaceOrderItems(order.id, [ { skuCode: itemId, - skuName: itemTitle, + skuName, quantity: num, specJson: JSON.stringify({ sellerId, - skuId: String(input.skuId || '').trim(), + skuId: sourceData.skuId, + skuNick: skuName, paymentFen, source: KUAISHOU_SEND_CODE_SOURCE, }), @@ -243,6 +274,113 @@ async function createFallbackOrder(input: SyncWorkOrdersFromSendCodeInput): Prom return { order } } +export function resolveKuaishouSendCodeWorkOrderData( + input: Pick< + SyncWorkOrdersFromSendCodeInput, + 'sellerId' | 'itemId' | 'itemTitle' | 'skuId' | 'paymentFen' | 'rawParams' + >, + shops: Array< + Pick + > | null = null, +): KuaishouSendCodeWorkOrderData { + const sellerId = String(input.sellerId || '').trim() + const ext = safeParseJson(input.rawParams?.ext) + const skuName = String(ext.skuNick || input.itemTitle || '').trim() + const paymentFen = + normalizePaymentFen(input.paymentFen) || + normalizePaymentFen(ext.payment ?? ext.totalGoodsValue ?? ext.goodsValue) + const matchedShop = shops + ? shops.find((shop) => String(shop.sellerId || shop.shopId || '').trim() === sellerId) || null + : findKuaishouIndustryShopConfig(sellerId) + + return { + sellerId, + shopName: resolveKuaishouSendCodeShopName(sellerId, matchedShop), + itemId: String(input.itemId || '').trim(), + skuId: String(input.skuId || '').trim(), + skuName, + paymentFen, + } +} + +function normalizePaymentFen(value: unknown): number { + const amount = Number(value) + return Number.isFinite(amount) && amount > 0 ? Math.trunc(amount) : 0 +} + +export function resolveKuaishouSendCodeShopName( + sellerId: unknown, + shop: Pick | null = null, +): string { + const normalizedSellerId = String(sellerId || '').trim() + return ( + String(shop?.customShopName || shop?.shopName || '').trim() || + (normalizedSellerId ? `快手店铺-${normalizedSellerId}` : '') + ) +} + +function applyKuaishouSendCodeItemData( + orderItems: OrderItemRow[], + sourceData: KuaishouSendCodeWorkOrderData, +): OrderItemRow[] { + if (!sourceData.skuName) { + return orderItems + } + + const matchedIds = findKuaishouSendCodeItemIds(orderItems, sourceData) + if (matchedIds.size === 0) { + return orderItems + } + + return orderItems.map((item) => { + if (!matchedIds.has(Number(item.id))) { + return item + } + + const snapshot = safeParseJson(item.item_snapshot_json) + return { + ...item, + sku_name: sourceData.skuName, + item_snapshot_json: { + ...snapshot, + externalSkuCode: sourceData.skuId || snapshot.externalSkuCode || '', + externalItemId: sourceData.itemId || snapshot.externalItemId || '', + externalSkuName: sourceData.skuName, + externalSkuNameNormalized: sourceData.skuName, + kuaishouSendCode: sourceData, + }, + } + }) +} + +function findKuaishouSendCodeItemIds( + orderItems: OrderItemRow[], + sourceData: KuaishouSendCodeWorkOrderData, +): Set { + const identifiers = new Set([sourceData.itemId, sourceData.skuId].filter(Boolean)) + const matched = orderItems.filter((item) => { + const snapshot = safeParseJson(item.item_snapshot_json) + return [ + item.sku_code, + snapshot.externalSkuCode, + snapshot.externalItemId, + snapshot.skuId, + snapshot.itemId, + ].some((value) => identifiers.has(String(value || '').trim())) + }) + + if (matched.length > 0) { + return new Set(matched.map((item) => Number(item.id))) + } + + return orderItems.length === 1 ? new Set([Number(orderItems[0]?.id || 0)]) : new Set() +} + +function isPlaceholderShopName(shopName: string, sellerId: string): boolean { + const normalized = String(shopName || '').trim() + return !normalized || normalized === '91卡券' || normalized === `快手店铺-${sellerId}` +} + async function backfillVouchersToWorkOrders( oid: string, vouchers: KuaishouIndustryVoucherRow[],