From c38cd8ca2db95dfd47bb14fbc9938baaa98f02c6 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 22 Oct 2026 19:55:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E8=AE=A2=E5=8D=95=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=8F=AA=E5=B1=95=E7=A4=BA=E5=B9=B3=E5=8F=B0=E5=88=A4?= =?UTF-8?q?=E6=96=AD=20SKU?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/admin-order-read-helpers.test.ts | 47 ++++++++ .../admin/admin-order-read-helpers.ts | 10 +- .../admin/admin-read-shared-helpers.ts | 35 ++++++ apps/backend/src/types/admin/read-models.ts | 2 + .../src/pages/admin/AdminOrdersPage.tsx | 101 +++++++----------- apps/frontend/src/types/admin/orders.ts | 2 + 6 files changed, 135 insertions(+), 62 deletions(-) diff --git a/apps/backend/src/services/admin/admin-order-read-helpers.test.ts b/apps/backend/src/services/admin/admin-order-read-helpers.test.ts index 8fa4728d..1e56b95e 100644 --- a/apps/backend/src/services/admin/admin-order-read-helpers.test.ts +++ b/apps/backend/src/services/admin/admin-order-read-helpers.test.ts @@ -49,6 +49,7 @@ test('mapAdminOrderListItem formats itemTitle, skuNick, and summary correctly fo assert.equal(mapped.itemTitle, '和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻') assert.equal(mapped.skuNick, '安卓血宴2钻') + assert.equal(mapped.matchSku, '安卓血宴2钻') assert.equal( mapped.itemSummary, '和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻 (安卓血宴2钻)', @@ -59,6 +60,7 @@ test('mapAdminOrderListItem formats itemTitle, skuNick, and summary correctly fo { itemTitle: '和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻', skuNick: '安卓血宴2钻', + matchSku: '安卓血宴2钻', quantity: 1, }, ]) @@ -133,6 +135,7 @@ test('mapAdminOrderListItem falls back to PDD goods_name/out_sku_sn when orderIt assert.equal(mapped.itemTitle, '和平精英地铁逃生苏尔南暗潮怒嚎血宴') assert.equal(mapped.skuNick, '安卓怒嚎3钻') + assert.equal(mapped.matchSku, '安卓怒嚎3钻') assert.equal(mapped.itemCount, 1) assert.equal(mapped.totalQuantity, 1) }) @@ -211,4 +214,48 @@ test('抖音码速达:商品列展示内部 SKU,引流标题降级为 skuNic assert.equal(mapped.skuNick, '【测试专用】和平精英皮肤小店入口出售返场吃鸡皮肤浪漫天命专用') // 列表摘要同样以内部 SKU 为主 assert.match(summarizeOrderItems(orderItems, order), /^幸运币2个/) + // 列表只展示判断用的内部 SKU,不能退回引流标题 + assert.equal(mapped.matchSku, '幸运币2个') + assert.equal(mapped.items?.[0]?.matchSku, '幸运币2个') +}) + +test('拼多多匹配 SKU 优先 sku_name,不用带地区的长 spec', () => { + const order: OrderListRow = { + id: 100, + provider: 'pdd', + platform: 'pdd', + shop_id: 'pdd-shop', + shop_name: '拼多多店铺', + platform_order_id: '260915-72034516082531', + order_status: 'paid', + pay_status: 'paid', + buyer_id: '', + buyer_name: '', + receiver_contact: '', + total_amount: 100, + currency: 'CNY', + raw_payload_json: {}, + paid_at: '2026-10-09T04:55:05.000Z', + created_at: '2026-10-09T04:55:05.000Z', + updated_at: '2026-10-09T04:55:05.000Z', + } + + const orderItems: OrderItemRow[] = [ + { + id: 1, + order_id: 100, + sku_code: '安卓怒嚎1钻', + sku_name: '安卓怒嚎1钻', + quantity: 1, + spec_json: JSON.stringify({ + itemTitle: '和平精英地铁逃生苏尔南暗潮怒嚎血宴', + skuNick: '安卓区(qv互通),苏尔南-怒嚎1钻', + }), + item_snapshot_json: JSON.stringify({}), + }, + ] + + const mapped = mapAdminOrderListItem(order, { orderItems }) + assert.equal(mapped.matchSku, '安卓怒嚎1钻') + assert.equal(mapped.items?.[0]?.matchSku, '安卓怒嚎1钻') }) diff --git a/apps/backend/src/services/admin/admin-order-read-helpers.ts b/apps/backend/src/services/admin/admin-order-read-helpers.ts index e4cc500a..cdee990c 100644 --- a/apps/backend/src/services/admin/admin-order-read-helpers.ts +++ b/apps/backend/src/services/admin/admin-order-read-helpers.ts @@ -1,6 +1,7 @@ import { formatFenToAmount, normalizeFen } from '../../utils/money.js' import { resolveDisplayShopName, + resolveOrderItemMatchSku, resolveOrderItemSkuNick, resolveOrderItemTitle, } from './admin-read-shared-helpers.js' @@ -34,28 +35,34 @@ export function mapAdminOrderListItem( let resolvedItemTitle = '' let resolvedSkuNick = '' + let resolvedMatchSku = '' let itemsList: AdminOrderItemSummary[] = [] if (normalizedItems.length > 0) { itemsList = normalizedItems.map((orderItem) => { const itTitle = resolveOrderItemTitle(orderItem, item, vouchers) const itSkuNick = resolveOrderItemSkuNick(orderItem, item, vouchers) + const itMatchSku = resolveOrderItemMatchSku(orderItem, item, vouchers) return { itemTitle: itTitle, skuNick: itSkuNick, + matchSku: itMatchSku, quantity: Math.max(1, Number(orderItem.quantity || 1)), } }) resolvedItemTitle = itemsList[0]?.itemTitle || '' resolvedSkuNick = itemsList[0]?.skuNick || '' + resolvedMatchSku = itemsList[0]?.matchSku || '' } else { resolvedItemTitle = resolveOrderItemTitle(null, item, vouchers) resolvedSkuNick = resolveOrderItemSkuNick(null, item, vouchers) - if (resolvedItemTitle || resolvedSkuNick) { + resolvedMatchSku = resolveOrderItemMatchSku(null, item, vouchers) + if (resolvedItemTitle || resolvedSkuNick || resolvedMatchSku) { itemsList = [ { itemTitle: resolvedItemTitle, skuNick: resolvedSkuNick, + matchSku: resolvedMatchSku, quantity: 1, }, ] @@ -99,6 +106,7 @@ export function mapAdminOrderListItem( itemSummary, itemTitle: resolvedItemTitle, skuNick: resolvedSkuNick, + matchSku: resolvedMatchSku, items: itemsList, taskCount: tasks.length, workOrderCount, diff --git a/apps/backend/src/services/admin/admin-read-shared-helpers.ts b/apps/backend/src/services/admin/admin-read-shared-helpers.ts index 8b6b22f4..c0ad97ce 100644 --- a/apps/backend/src/services/admin/admin-read-shared-helpers.ts +++ b/apps/backend/src/services/admin/admin-read-shared-helpers.ts @@ -637,6 +637,41 @@ export function resolveOrderItemSkuNick( return '' } +/** + * 列表展示用的「平台判断 SKU」:只保留能用于匹配的短 SKU,不展示引流长标题。 + * 抖音的内部 SKU 存在 itemTitle(resolveOrderItemTitle 已优先返回 code), + * 拼多多用 sku_name/sku_code 更干净,91/快手优先用 skuNick。 + */ +export function resolveOrderItemMatchSku( + item: Partial | null | undefined, + fallbackOrder?: Partial | null | undefined, + vouchers?: KuaishouIndustryVoucherRow[] | null | undefined, +): string { + const provider = String(fallbackOrder?.provider || '') + .trim() + .toLowerCase() + const spec = item ? safeParseJson(item.spec_json) : {} + const snapshot = item ? safeParseJson(item.item_snapshot_json) : {} + const source = pickFirstNonEmpty([spec.source, snapshot.source]).toLowerCase() + const isDouyin = provider.includes('douyin') || source.includes('douyin') + + const itemTitle = resolveOrderItemTitle(item, fallbackOrder, vouchers) + const skuNick = resolveOrderItemSkuNick(item, fallbackOrder, vouchers) + const skuName = String(item?.sku_name || '').trim() + const skuCode = String(item?.sku_code || '').trim() + + if (isDouyin) { + // 抖音 skuNick 往往是引流标题,内部 SKU 才是判断依据。 + return itemTitle || skuName || skuCode + } + + if (provider === 'pdd') { + return skuName || skuCode || skuNick || itemTitle + } + + return skuNick || skuName || skuCode || itemTitle +} + export function resolveOrderItemDeliveryMode( tasks: TaskLike[] | null | undefined, orderItemId: number | string, diff --git a/apps/backend/src/types/admin/read-models.ts b/apps/backend/src/types/admin/read-models.ts index 5ec6bdd3..b152b472 100644 --- a/apps/backend/src/types/admin/read-models.ts +++ b/apps/backend/src/types/admin/read-models.ts @@ -9,6 +9,7 @@ export type AdminOrderFulfillmentProgress = { export type AdminOrderItemSummary = { itemTitle: string skuNick: string + matchSku: string quantity: number } @@ -35,6 +36,7 @@ export type AdminOrderListItem = { itemSummary: string itemTitle?: string skuNick?: string + matchSku?: string items?: AdminOrderItemSummary[] taskCount: number resourceStatus: string diff --git a/apps/frontend/src/pages/admin/AdminOrdersPage.tsx b/apps/frontend/src/pages/admin/AdminOrdersPage.tsx index 78bfb5bc..5e87935a 100644 --- a/apps/frontend/src/pages/admin/AdminOrdersPage.tsx +++ b/apps/frontend/src/pages/admin/AdminOrdersPage.tsx @@ -204,76 +204,55 @@ export default function AdminOrdersPage() { }, }, { - title: '商品 / 规格', - minWidth: 260, + title: '平台 SKU', + minWidth: 190, render: (_, row) => { - const itemTitle = row.itemTitle || row.itemSummary || '-' - const skuNick = row.skuNick const items = row.items && row.items.length > 0 ? row.items : null + const fallbackSku = row.matchSku || row.skuNick || '未识别 SKU' + + const renderSkuTag = (sku: string | undefined, title?: string, key?: string) => { + const label = sku || '未识别 SKU' + return ( + + + {label} + + + ) + } return (
{items && items.length > 1 ? (
- {items.slice(0, 2).map((item, idx) => ( -
- - {item.itemTitle || '-'} - - {item.skuNick ? ( - - - {item.skuNick} - - - ) : null} -
- ))} - {items.length > 2 ? 等 {items.length} 种商品 : null} + {items + .slice(0, 2) + .map((item, idx) => + renderSkuTag(item.matchSku || item.skuNick, item.itemTitle, `sku-${idx}`), + )} + {items.length > 2 ? 等 {items.length} 项 : null}
+ ) : items && items.length === 1 ? ( + renderSkuTag(items[0].matchSku || items[0].skuNick, items[0].itemTitle, 'sku-0') ) : ( - <> - - {itemTitle} - - {skuNick ? ( - - - {skuNick} - - - ) : null} - + renderSkuTag(fallbackSku, row.itemTitle || row.itemSummary, 'sku-0') )} 数量 {row.totalQuantity || 1} diff --git a/apps/frontend/src/types/admin/orders.ts b/apps/frontend/src/types/admin/orders.ts index 5adce6af..24f2efed 100644 --- a/apps/frontend/src/types/admin/orders.ts +++ b/apps/frontend/src/types/admin/orders.ts @@ -11,6 +11,7 @@ export interface AdminOrderFulfillmentProgress { export interface AdminOrderItemSummary { itemTitle: string skuNick: string + matchSku: string quantity: number } @@ -37,6 +38,7 @@ export interface AdminOrderListItem { itemSummary: string itemTitle?: string skuNick?: string + matchSku?: string items?: AdminOrderItemSummary[] taskCount: number resourceStatus: string