feat(admin): 订单列表只展示平台判断 SKU

This commit is contained in:
yml2213
2026-10-22 19:55:13 +08:00
parent 74b62f3494
commit c38cd8ca2d
6 changed files with 135 additions and 62 deletions
@@ -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钻')
})
@@ -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,
@@ -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<OrderItemRow> | null | undefined,
fallbackOrder?: Partial<OrderRow> | 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,
@@ -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
@@ -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 (
<Tooltip
key={key}
title={
title && title !== label
? `平台 SKU:${label}|原商品标题:${title}`
: `平台 SKU:${label}`
}
>
<Tag
color={label === '未识别 SKU' ? 'default' : 'blue'}
style={{
margin: 0,
maxWidth: 200,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontSize: 12,
}}
>
{label}
</Tag>
</Tooltip>
)
}
return (
<div className="cell-stack" style={{ gap: 4 }}>
{items && items.length > 1 ? (
<div className="cell-stack" style={{ gap: 4 }}>
{items.slice(0, 2).map((item, idx) => (
<div key={idx} className="cell-stack" style={{ gap: 2 }}>
<Typography.Text
strong
ellipsis={{ tooltip: item.itemTitle }}
style={{ maxWidth: 360, fontSize: 13 }}
>
{item.itemTitle || '-'}
</Typography.Text>
{item.skuNick ? (
<Space size={4} wrap>
<Tag
color="blue"
style={{
margin: 0,
maxWidth: 320,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontSize: 12,
}}
title={`规格:${item.skuNick}`}
>
{item.skuNick}
</Tag>
</Space>
) : null}
</div>
))}
{items.length > 2 ? <span className="muted">等 {items.length} 种商品</span> : null}
{items
.slice(0, 2)
.map((item, idx) =>
renderSkuTag(item.matchSku || item.skuNick, item.itemTitle, `sku-${idx}`),
)}
{items.length > 2 ? <span className="muted">等 {items.length} 项</span> : null}
</div>
) : items && items.length === 1 ? (
renderSkuTag(items[0].matchSku || items[0].skuNick, items[0].itemTitle, 'sku-0')
) : (
<>
<Typography.Text
strong
ellipsis={{ tooltip: itemTitle }}
style={{ maxWidth: 360, fontSize: 13 }}
>
{itemTitle}
</Typography.Text>
{skuNick ? (
<Space size={4} wrap>
<Tag
color="blue"
style={{
margin: 0,
maxWidth: 320,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontSize: 12,
}}
title={`规格:${skuNick}`}
>
{skuNick}
</Tag>
</Space>
) : null}
</>
renderSkuTag(fallbackSku, row.itemTitle || row.itemSummary, 'sku-0')
)}
<span className="muted" style={{ fontSize: 12 }}>
数量 {row.totalQuantity || 1}
+2
View File
@@ -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