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:
@@ -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>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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) : '—'}
|
||||
|
||||
@@ -15,7 +15,10 @@ export type RedeemOrderStatus =
|
||||
| 'verified'
|
||||
|
||||
export interface RedeemOrderSummary {
|
||||
/** 商品名:抖音码速达口径为**内部 SKU**(平台 `code`) */
|
||||
productName: string
|
||||
/** 平台商品标题(引流词),仅作副信息展示 */
|
||||
productTitle: string
|
||||
quantity: number
|
||||
amountFen: number
|
||||
paidAt: string | null
|
||||
|
||||
Reference in New Issue
Block a user