feat(admin): 订单列表只展示平台判断 SKU
This commit is contained in:
@@ -49,6 +49,7 @@ test('mapAdminOrderListItem formats itemTitle, skuNick, and summary correctly fo
|
|||||||
|
|
||||||
assert.equal(mapped.itemTitle, '和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻')
|
assert.equal(mapped.itemTitle, '和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻')
|
||||||
assert.equal(mapped.skuNick, '安卓血宴2钻')
|
assert.equal(mapped.skuNick, '安卓血宴2钻')
|
||||||
|
assert.equal(mapped.matchSku, '安卓血宴2钻')
|
||||||
assert.equal(
|
assert.equal(
|
||||||
mapped.itemSummary,
|
mapped.itemSummary,
|
||||||
'和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻 (安卓血宴2钻)',
|
'和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻 (安卓血宴2钻)',
|
||||||
@@ -59,6 +60,7 @@ test('mapAdminOrderListItem formats itemTitle, skuNick, and summary correctly fo
|
|||||||
{
|
{
|
||||||
itemTitle: '和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻',
|
itemTitle: '和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻',
|
||||||
skuNick: '安卓血宴2钻',
|
skuNick: '安卓血宴2钻',
|
||||||
|
matchSku: '安卓血宴2钻',
|
||||||
quantity: 1,
|
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.itemTitle, '和平精英地铁逃生苏尔南暗潮怒嚎血宴')
|
||||||
assert.equal(mapped.skuNick, '安卓怒嚎3钻')
|
assert.equal(mapped.skuNick, '安卓怒嚎3钻')
|
||||||
|
assert.equal(mapped.matchSku, '安卓怒嚎3钻')
|
||||||
assert.equal(mapped.itemCount, 1)
|
assert.equal(mapped.itemCount, 1)
|
||||||
assert.equal(mapped.totalQuantity, 1)
|
assert.equal(mapped.totalQuantity, 1)
|
||||||
})
|
})
|
||||||
@@ -211,4 +214,48 @@ test('抖音码速达:商品列展示内部 SKU,引流标题降级为 skuNic
|
|||||||
assert.equal(mapped.skuNick, '【测试专用】和平精英皮肤小店入口出售返场吃鸡皮肤浪漫天命专用')
|
assert.equal(mapped.skuNick, '【测试专用】和平精英皮肤小店入口出售返场吃鸡皮肤浪漫天命专用')
|
||||||
// 列表摘要同样以内部 SKU 为主
|
// 列表摘要同样以内部 SKU 为主
|
||||||
assert.match(summarizeOrderItems(orderItems, order), /^幸运币2个/)
|
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 { formatFenToAmount, normalizeFen } from '../../utils/money.js'
|
||||||
import {
|
import {
|
||||||
resolveDisplayShopName,
|
resolveDisplayShopName,
|
||||||
|
resolveOrderItemMatchSku,
|
||||||
resolveOrderItemSkuNick,
|
resolveOrderItemSkuNick,
|
||||||
resolveOrderItemTitle,
|
resolveOrderItemTitle,
|
||||||
} from './admin-read-shared-helpers.js'
|
} from './admin-read-shared-helpers.js'
|
||||||
@@ -34,28 +35,34 @@ export function mapAdminOrderListItem(
|
|||||||
|
|
||||||
let resolvedItemTitle = ''
|
let resolvedItemTitle = ''
|
||||||
let resolvedSkuNick = ''
|
let resolvedSkuNick = ''
|
||||||
|
let resolvedMatchSku = ''
|
||||||
let itemsList: AdminOrderItemSummary[] = []
|
let itemsList: AdminOrderItemSummary[] = []
|
||||||
|
|
||||||
if (normalizedItems.length > 0) {
|
if (normalizedItems.length > 0) {
|
||||||
itemsList = normalizedItems.map((orderItem) => {
|
itemsList = normalizedItems.map((orderItem) => {
|
||||||
const itTitle = resolveOrderItemTitle(orderItem, item, vouchers)
|
const itTitle = resolveOrderItemTitle(orderItem, item, vouchers)
|
||||||
const itSkuNick = resolveOrderItemSkuNick(orderItem, item, vouchers)
|
const itSkuNick = resolveOrderItemSkuNick(orderItem, item, vouchers)
|
||||||
|
const itMatchSku = resolveOrderItemMatchSku(orderItem, item, vouchers)
|
||||||
return {
|
return {
|
||||||
itemTitle: itTitle,
|
itemTitle: itTitle,
|
||||||
skuNick: itSkuNick,
|
skuNick: itSkuNick,
|
||||||
|
matchSku: itMatchSku,
|
||||||
quantity: Math.max(1, Number(orderItem.quantity || 1)),
|
quantity: Math.max(1, Number(orderItem.quantity || 1)),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
resolvedItemTitle = itemsList[0]?.itemTitle || ''
|
resolvedItemTitle = itemsList[0]?.itemTitle || ''
|
||||||
resolvedSkuNick = itemsList[0]?.skuNick || ''
|
resolvedSkuNick = itemsList[0]?.skuNick || ''
|
||||||
|
resolvedMatchSku = itemsList[0]?.matchSku || ''
|
||||||
} else {
|
} else {
|
||||||
resolvedItemTitle = resolveOrderItemTitle(null, item, vouchers)
|
resolvedItemTitle = resolveOrderItemTitle(null, item, vouchers)
|
||||||
resolvedSkuNick = resolveOrderItemSkuNick(null, item, vouchers)
|
resolvedSkuNick = resolveOrderItemSkuNick(null, item, vouchers)
|
||||||
if (resolvedItemTitle || resolvedSkuNick) {
|
resolvedMatchSku = resolveOrderItemMatchSku(null, item, vouchers)
|
||||||
|
if (resolvedItemTitle || resolvedSkuNick || resolvedMatchSku) {
|
||||||
itemsList = [
|
itemsList = [
|
||||||
{
|
{
|
||||||
itemTitle: resolvedItemTitle,
|
itemTitle: resolvedItemTitle,
|
||||||
skuNick: resolvedSkuNick,
|
skuNick: resolvedSkuNick,
|
||||||
|
matchSku: resolvedMatchSku,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -99,6 +106,7 @@ export function mapAdminOrderListItem(
|
|||||||
itemSummary,
|
itemSummary,
|
||||||
itemTitle: resolvedItemTitle,
|
itemTitle: resolvedItemTitle,
|
||||||
skuNick: resolvedSkuNick,
|
skuNick: resolvedSkuNick,
|
||||||
|
matchSku: resolvedMatchSku,
|
||||||
items: itemsList,
|
items: itemsList,
|
||||||
taskCount: tasks.length,
|
taskCount: tasks.length,
|
||||||
workOrderCount,
|
workOrderCount,
|
||||||
|
|||||||
@@ -637,6 +637,41 @@ export function resolveOrderItemSkuNick(
|
|||||||
return ''
|
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(
|
export function resolveOrderItemDeliveryMode(
|
||||||
tasks: TaskLike[] | null | undefined,
|
tasks: TaskLike[] | null | undefined,
|
||||||
orderItemId: number | string,
|
orderItemId: number | string,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type AdminOrderFulfillmentProgress = {
|
|||||||
export type AdminOrderItemSummary = {
|
export type AdminOrderItemSummary = {
|
||||||
itemTitle: string
|
itemTitle: string
|
||||||
skuNick: string
|
skuNick: string
|
||||||
|
matchSku: string
|
||||||
quantity: number
|
quantity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ export type AdminOrderListItem = {
|
|||||||
itemSummary: string
|
itemSummary: string
|
||||||
itemTitle?: string
|
itemTitle?: string
|
||||||
skuNick?: string
|
skuNick?: string
|
||||||
|
matchSku?: string
|
||||||
items?: AdminOrderItemSummary[]
|
items?: AdminOrderItemSummary[]
|
||||||
taskCount: number
|
taskCount: number
|
||||||
resourceStatus: string
|
resourceStatus: string
|
||||||
|
|||||||
@@ -204,76 +204,55 @@ export default function AdminOrdersPage() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '商品 / 规格',
|
title: '平台 SKU',
|
||||||
minWidth: 260,
|
minWidth: 190,
|
||||||
render: (_, row) => {
|
render: (_, row) => {
|
||||||
const itemTitle = row.itemTitle || row.itemSummary || '-'
|
|
||||||
const skuNick = row.skuNick
|
|
||||||
const items = row.items && row.items.length > 0 ? row.items : null
|
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 (
|
return (
|
||||||
<div className="cell-stack" style={{ gap: 4 }}>
|
<div className="cell-stack" style={{ gap: 4 }}>
|
||||||
{items && items.length > 1 ? (
|
{items && items.length > 1 ? (
|
||||||
<div className="cell-stack" style={{ gap: 4 }}>
|
<div className="cell-stack" style={{ gap: 4 }}>
|
||||||
{items.slice(0, 2).map((item, idx) => (
|
{items
|
||||||
<div key={idx} className="cell-stack" style={{ gap: 2 }}>
|
.slice(0, 2)
|
||||||
<Typography.Text
|
.map((item, idx) =>
|
||||||
strong
|
renderSkuTag(item.matchSku || item.skuNick, item.itemTitle, `sku-${idx}`),
|
||||||
ellipsis={{ tooltip: item.itemTitle }}
|
)}
|
||||||
style={{ maxWidth: 360, fontSize: 13 }}
|
{items.length > 2 ? <span className="muted">等 {items.length} 项</span> : null}
|
||||||
>
|
|
||||||
{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}
|
|
||||||
</div>
|
</div>
|
||||||
|
) : items && items.length === 1 ? (
|
||||||
|
renderSkuTag(items[0].matchSku || items[0].skuNick, items[0].itemTitle, 'sku-0')
|
||||||
) : (
|
) : (
|
||||||
<>
|
renderSkuTag(fallbackSku, row.itemTitle || row.itemSummary, '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}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
<span className="muted" style={{ fontSize: 12 }}>
|
<span className="muted" style={{ fontSize: 12 }}>
|
||||||
数量 {row.totalQuantity || 1}
|
数量 {row.totalQuantity || 1}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface AdminOrderFulfillmentProgress {
|
|||||||
export interface AdminOrderItemSummary {
|
export interface AdminOrderItemSummary {
|
||||||
itemTitle: string
|
itemTitle: string
|
||||||
skuNick: string
|
skuNick: string
|
||||||
|
matchSku: string
|
||||||
quantity: number
|
quantity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ export interface AdminOrderListItem {
|
|||||||
itemSummary: string
|
itemSummary: string
|
||||||
itemTitle?: string
|
itemTitle?: string
|
||||||
skuNick?: string
|
skuNick?: string
|
||||||
|
matchSku?: string
|
||||||
items?: AdminOrderItemSummary[]
|
items?: AdminOrderItemSummary[]
|
||||||
taskCount: number
|
taskCount: number
|
||||||
resourceStatus: string
|
resourceStatus: string
|
||||||
|
|||||||
Reference in New Issue
Block a user