From 7834ee7bef87d8f11fd34a8f4095a6e73e722f9b Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 9 Oct 2026 13:31:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(douyin-mashida):=20=E5=95=86=E5=93=81?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E7=BB=9F=E4=B8=80=E6=94=B9=E4=B8=BA=E5=86=85?= =?UTF-8?q?=E9=83=A8=20SKU=EF=BC=88=E5=BC=95=E6=B5=81=E6=A0=87=E9=A2=98?= =?UTF-8?q?=E9=99=8D=E7=BA=A7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 平台 sku_order_list[].code 是商家维护的**内部 SKU**(如「幸运币2个」), product_name 只是店铺用来**引流**的长标题(如「【测试专用】和平精英皮肤小店入口…」)。 商家按内部 SKU 认货(后续履约匹配也用它),原来的展示把引流标题当商品名, 列表/详情/兑换页三处都会让人认错货。 改为以内部 SKU 为主、引流标题降级为副信息: - 后台订单列表「商品 / 任务」:itemTitle = 内部 SKU,skuNick = 引流标题 - 后台订单详情「商品明细」:商品 = 内部 SKU;SKU/规格 不再重复展示同一串 - 兑换中心 /redeem:商品 = 内部 SKU(粗体)+ 引流标题(灰色小字,新增 productTitle 字段) 实现只在 `source === 'douyin_mashida'` 时生效(resolveOrderItemTitle / resolveOrderItemSkuNick 加 douyin 分支),其它平台展示逻辑不变; 用真实订单形态补了单测。 --- .../admin/admin-order-read-helpers.test.ts | 52 +++++++++++++++++++ .../admin/admin-read-shared-helpers.ts | 32 ++++++++++++ .../src/services/redeem/redeem-service.ts | 8 ++- .../src/pages/admin/AdminOrderDetailPage.tsx | 5 +- apps/frontend/src/pages/redeem/RedeemPage.tsx | 12 ++++- apps/frontend/src/types/redeem.ts | 3 ++ 6 files changed, 109 insertions(+), 3 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 c4227628..8fa4728d 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 @@ -160,3 +160,55 @@ test('summarizeOrderItems formats multiple items cleanly', () => { const summary = summarizeOrderItems(items) assert.equal(summary, '商品1 (规格A) 等 2 项') }) + +test('抖音码速达:商品列展示内部 SKU,引流标题降级为 skuNick', () => { + const order: OrderListRow = { + id: 99, + provider: 'douyin_mashida', + platform: 'doudian', + shop_id: '305760014', + shop_name: '向日葵游戏交易店', + platform_order_id: '6930292726686514275', + 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:12.000Z', + } + + // 平台真实形态:code = 内部 SKU,product_name = 引流长标题 + const orderItems: OrderItemRow[] = [ + { + id: 93, + order_id: 99, + sku_code: '幸运币2个', + sku_name: '【测试专用】和平精英皮肤小店入口出售返场吃鸡皮肤浪漫天命专用', + quantity: 1, + spec_json: JSON.stringify({ + source: 'douyin_mashida', + code: '幸运币2个', + productId: '3847255376114352600', + }), + item_snapshot_json: JSON.stringify({ + source: 'douyin_mashida', + code: '幸运币2个', + productName: '【测试专用】和平精英皮肤小店入口出售返场吃鸡皮肤浪漫天命专用', + }), + } as unknown as OrderItemRow, + ] + + const mapped = mapAdminOrderListItem(order, { orderItems }) + + // 商品 = 内部 SKU(不是引流标题) + assert.equal(mapped.itemTitle, '幸运币2个') + // 引流标题降级到副信息位 + assert.equal(mapped.skuNick, '【测试专用】和平精英皮肤小店入口出售返场吃鸡皮肤浪漫天命专用') + // 列表摘要同样以内部 SKU 为主 + assert.match(summarizeOrderItems(orderItems, order), /^幸运币2个/) +}) 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 4380466d..d1a8fd63 100644 --- a/apps/backend/src/services/admin/admin-read-shared-helpers.ts +++ b/apps/backend/src/services/admin/admin-read-shared-helpers.ts @@ -438,6 +438,32 @@ export function parseExtRecord(value: unknown): JsonRecord { return {} } +/** + * 抖音码速达的商品展示口径:**内部 SKU 才是商品**,平台商品标题只是引流词。 + * + * 平台 `sku_order_list[].code` 是商家自己维护的内部 SKU(如「幸运币2个」), + * `product_name` 则是店铺里用来引流的长标题(如「【测试专用】和平精英皮肤小店入口…」)。 + * 商家侧按内部 SKU 认货(后续履约匹配也用它),所以列表/详情的「商品」应展示内部 SKU, + * 长标题降级为副信息。 + * + * 非 douyin_mashida 的订单返回 null,走原有逻辑,避免影响其它平台。 + */ +function resolveDouyinMashidaDisplay( + item: Partial, + spec: Record, + snapshot: Record, +): { internalSku: string; productTitle: string } | null { + const source = pickFirstNonEmpty([spec.source, snapshot.source]) + if (source !== 'douyin_mashida') { + return null + } + + return { + internalSku: pickFirstNonEmpty([spec.code, snapshot.code, item.sku_code]), + productTitle: pickFirstNonEmpty([snapshot.productName, item.sku_name]), + } +} + export function resolveOrderItemTitle( item: Partial | null | undefined, fallbackOrder?: Partial | null | undefined, @@ -450,6 +476,12 @@ export function resolveOrderItemTitle( const ksRawPayload = asJsonObject(snapshot.kuaishouSendCodeRawPayload) const ksParams = asJsonObject(snapshot.params) + // 抖音码速达:内部 SKU 优先(引流标题不进「商品」列) + const douyinDisplay = resolveDouyinMashidaDisplay(item, spec, snapshot) + if (douyinDisplay?.internalSku) { + return douyinDisplay.internalSku + } + const titleCandidate = pickFirstNonEmpty([ spec.itemTitle, spec.title, diff --git a/apps/backend/src/services/redeem/redeem-service.ts b/apps/backend/src/services/redeem/redeem-service.ts index e987f96f..53de18c6 100644 --- a/apps/backend/src/services/redeem/redeem-service.ts +++ b/apps/backend/src/services/redeem/redeem-service.ts @@ -36,7 +36,10 @@ export type RedeemOrderView = { orderNo: string /** 脱敏摘要(不含买家手机号/地址/open_id) */ summary: { + /** 商品名(抖音码速达口径:**内部 SKU**,即平台 `sku_order_list[].code`) */ productName: string + /** 平台商品标题(店铺里用来引流的标题,仅作副信息展示) */ + productTitle: string quantity: number amountFen: number paidAt: string | null @@ -260,7 +263,10 @@ function buildSummary(order: OrderRow, items: OrderItemRow[]) { const quantity = items.reduce((total, item) => total + Math.max(1, Number(item.quantity || 1)), 0) return { - productName: String(firstItem?.sku_name || order.shop_name || '').trim(), + // 内部 SKU(平台 code,如「幸运币2个」)才是商家认的商品; + // 平台标题(product_name)只是引流词,降级为副标题,避免买家/客服认错货。 + productName: String(firstItem?.sku_code || firstItem?.sku_name || order.shop_name || '').trim(), + productTitle: String(firstItem?.sku_name || '').trim(), quantity: Math.max(1, quantity), amountFen: Number(order.total_amount || 0), paidAt: order.paid_at || null, diff --git a/apps/frontend/src/pages/admin/AdminOrderDetailPage.tsx b/apps/frontend/src/pages/admin/AdminOrderDetailPage.tsx index 46148dc2..cbba3795 100644 --- a/apps/frontend/src/pages/admin/AdminOrderDetailPage.tsx +++ b/apps/frontend/src/pages/admin/AdminOrderDetailPage.tsx @@ -151,7 +151,10 @@ export default function AdminOrderDetailPage() { {item.skuNick} ) : null} - {item.skuCode || item.skuName || '-'} + {/* 商品列已是内部 SKU 时不再重复展示同一串 */} + {item.skuCode && item.skuCode !== item.itemTitle ? ( + {item.skuCode} + ) : null} ), }, diff --git a/apps/frontend/src/pages/redeem/RedeemPage.tsx b/apps/frontend/src/pages/redeem/RedeemPage.tsx index 110fd0fd..ecca47fe 100644 --- a/apps/frontend/src/pages/redeem/RedeemPage.tsx +++ b/apps/frontend/src/pages/redeem/RedeemPage.tsx @@ -153,7 +153,17 @@ function RedeemStatusResult({ status, message }: { status: RedeemOrderStatus; me function RedeemSummaryDetails({ summary }: { summary: RedeemOrderView['summary'] }) { return ( - {summary?.productName || '—'} + +
+ {summary?.productName || '—'} + {/* 平台标题只是引流词,降级为副信息,避免买家误认商品 */} + {summary?.productTitle && summary.productTitle !== summary.productName ? ( + + {summary.productTitle} + + ) : null} +
+
{summary ? summary.quantity : '—'} {summary ? formatRedeemAmount(summary.amountFen) : '—'} diff --git a/apps/frontend/src/types/redeem.ts b/apps/frontend/src/types/redeem.ts index 4796f368..95e4c9c1 100644 --- a/apps/frontend/src/types/redeem.ts +++ b/apps/frontend/src/types/redeem.ts @@ -15,7 +15,10 @@ export type RedeemOrderStatus = | 'verified' export interface RedeemOrderSummary { + /** 商品名:抖音码速达口径为**内部 SKU**(平台 `code`) */ productName: string + /** 平台商品标题(引流词),仅作副信息展示 */ + productTitle: string quantity: number amountFen: number paidAt: string | null