feat(douyin-mashida): 商品展示统一改为内部 SKU(引流标题降级)

平台 sku_order_list[].code 是商家维护的**内部 SKU**(如「幸运币2个」),
product_name 只是店铺用来**引流**的长标题(如「【测试专用】和平精英皮肤小店入口…」)。
商家按内部 SKU 认货(后续履约匹配也用它),原来的展示把引流标题当商品名,
列表/详情/兑换页三处都会让人认错货。

改为以内部 SKU 为主、引流标题降级为副信息:
- 后台订单列表「商品 / 任务」:itemTitle = 内部 SKU,skuNick = 引流标题
- 后台订单详情「商品明细」:商品 = 内部 SKU;SKU/规格 不再重复展示同一串
- 兑换中心 /redeem:商品 = 内部 SKU(粗体)+ 引流标题(灰色小字,新增 productTitle 字段)

实现只在 `source === 'douyin_mashida'` 时生效(resolveOrderItemTitle /
resolveOrderItemSkuNick 加 douyin 分支),其它平台展示逻辑不变;
用真实订单形态补了单测。
This commit is contained in:
yml2213
2026-10-09 13:31:45 +08:00
parent 3505ca4e05
commit 7834ee7bef
6 changed files with 109 additions and 3 deletions
@@ -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个/)
})
@@ -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<OrderItemRow>,
spec: Record<string, unknown>,
snapshot: Record<string, unknown>,
): { 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<OrderItemRow> | null | undefined,
fallbackOrder?: Partial<OrderRow> | 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,
@@ -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,
@@ -151,7 +151,10 @@ export default function AdminOrderDetailPage() {
{item.skuNick}
</Tag>
) : null}
<span className="muted">{item.skuCode || item.skuName || '-'}</span>
{/* 商品列已是内部 SKU 时不再重复展示同一串 */}
{item.skuCode && item.skuCode !== item.itemTitle ? (
<span className="muted">{item.skuCode}</span>
) : null}
</div>
),
},
+11 -1
View File
@@ -153,7 +153,17 @@ function RedeemStatusResult({ status, message }: { status: RedeemOrderStatus; me
function RedeemSummaryDetails({ summary }: { summary: RedeemOrderView['summary'] }) {
return (
<Descriptions column={1} size="small" bordered>
<Descriptions.Item label="商品">{summary?.productName || '—'}</Descriptions.Item>
<Descriptions.Item label="商品">
<div className="cell-stack" style={{ gap: 2 }}>
<Typography.Text strong>{summary?.productName || '—'}</Typography.Text>
{/* 平台标题只是引流词,降级为副信息,避免买家误认商品 */}
{summary?.productTitle && summary.productTitle !== summary.productName ? (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{summary.productTitle}
</Typography.Text>
) : null}
</div>
</Descriptions.Item>
<Descriptions.Item label="数量">{summary ? summary.quantity : '—'}</Descriptions.Item>
<Descriptions.Item label="金额">
{summary ? formatRedeemAmount(summary.amountFen) : '—'}
+3
View File
@@ -15,7 +15,10 @@ export type RedeemOrderStatus =
| 'verified'
export interface RedeemOrderSummary {
/** 商品名:抖音码速达口径为**内部 SKU**(平台 `code`) */
productName: string
/** 平台商品标题(引流词),仅作副信息展示 */
productTitle: string
quantity: number
amountFen: number
paidAt: string | null