优化后台订单列表展示并支持商品标题与SKU规格

This commit is contained in:
yml2213
2026-09-10 09:39:19 +08:00
parent fe2a28454d
commit 4544622cb3
13 changed files with 655 additions and 51 deletions
@@ -201,6 +201,27 @@ export async function listKuaishouIndustryVouchersByOid(
return result.rows
}
export async function listKuaishouIndustryVouchersByOids(
oids: string[],
): Promise<KuaishouIndustryVoucherRow[]> {
const normalizedOids = oids.map((oid) => String(oid || '').trim()).filter(Boolean)
if (normalizedOids.length === 0) {
return []
}
const result = await query<KuaishouIndustryVoucherRow>(
`
SELECT *
FROM kuaishou_industry_vouchers
WHERE oid = ANY($1::text[])
ORDER BY unit_index ASC, id ASC
`,
[normalizedOids],
)
return result.rows
}
export type KuaishouIndustryVoucherWorkOrderSyncSource = {
oid: string
raw_payload_json: string | Record<string, unknown>
@@ -235,16 +235,27 @@ export async function getOrderItemById(orderItemId: number | string): Promise<Or
export async function updateOrderItemSourceSnapshot(
orderItemId: number | string,
input: { skuName: string; itemSnapshotJson: string; updatedAt: string },
input: { skuName: string; itemSnapshotJson: string; updatedAt: string; specJson?: string },
): Promise<OrderItemRow | null> {
const specClause = input.specJson ? ', spec_json = $5::jsonb' : ''
const params: unknown[] = [
input.skuName,
input.itemSnapshotJson,
input.updatedAt,
Number(orderItemId),
]
if (input.specJson) {
params.push(input.specJson)
}
const result = await query<{ id: number }>(
`
UPDATE order_items
SET sku_name = $1, item_snapshot_json = $2::jsonb, updated_at = $3
SET sku_name = $1, item_snapshot_json = $2::jsonb, updated_at = $3 ${specClause}
WHERE id = $4
RETURNING id
`,
[input.skuName, input.itemSnapshotJson, input.updatedAt, Number(orderItemId)],
params,
)
return result.rows[0] ? getOrderItemById(orderItemId) : null
}
+4 -2
View File
@@ -243,7 +243,8 @@ export async function listOrdersWithExecutor(
filters.push(`(
(
o.platform_order_id || ' ' || o.shop_id || ' ' || o.shop_name || ' ' ||
o.buyer_id || ' ' || o.buyer_name || ' ' || o.receiver_contact
o.buyer_id || ' ' || o.buyer_name || ' ' || o.receiver_contact || ' ' ||
o.raw_payload_json::text
) ILIKE ${keywordParam}
OR EXISTS (
SELECT 1
@@ -252,7 +253,8 @@ export async function listOrdersWithExecutor(
AND (
oi.sku_code || ' ' || oi.sku_name || ' ' ||
COALESCE(oi.item_snapshot_json->'kuaishouSendCode'->>'itemTitle', '') || ' ' ||
COALESCE(oi.item_snapshot_json->'kuaishouSendCode'->>'skuNick', '')
COALESCE(oi.item_snapshot_json->'kuaishouSendCode'->>'skuNick', '') || ' ' ||
COALESCE(oi.spec_json::text, '')
) ILIKE ${keywordParam}
)
)`)
@@ -0,0 +1,126 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { mapAdminOrderListItem, summarizeOrderItems } from './admin-order-read-helpers.js'
import type { OrderItemRow, OrderListRow } from '../../types/repository/rows.js'
test('mapAdminOrderListItem formats itemTitle, skuNick, and summary correctly for single item', () => {
const order: OrderListRow = {
id: 1,
provider: '91',
platform: 'kuaishou',
shop_id: '4269276762',
shop_name: '独角兽游戏专营店',
platform_order_id: '2625301761098133',
order_status: 'paid',
pay_status: 'paid',
buyer_id: '',
buyer_name: '',
receiver_contact: '',
total_amount: 16900,
currency: 'CNY',
raw_payload_json: {},
paid_at: '2026-09-10T09:15:00.000Z',
created_at: '2026-09-10T09:15:00.000Z',
updated_at: '2026-09-10T09:15:00.000Z',
}
const orderItems: OrderItemRow[] = [
{
id: 10,
order_id: 1,
sku_code: '26079757920762',
sku_name: '安卓血宴2钻',
quantity: 1,
spec_json: JSON.stringify({
itemTitle: '和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻',
skuNick: '安卓血宴2钻',
}),
item_snapshot_json: JSON.stringify({
kuaishouSendCode: {
itemTitle: '和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻',
skuNick: '安卓血宴2钻',
},
}),
},
]
const mapped = mapAdminOrderListItem(order, { orderItems, tasks: [] })
assert.equal(mapped.itemTitle, '和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻')
assert.equal(mapped.skuNick, '安卓血宴2钻')
assert.equal(
mapped.itemSummary,
'和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻 (安卓血宴2钻)',
)
assert.equal(mapped.itemCount, 1)
assert.equal(mapped.totalQuantity, 1)
assert.deepEqual(mapped.items, [
{
itemTitle: '和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻',
skuNick: '安卓血宴2钻',
quantity: 1,
},
])
})
test('mapAdminOrderListItem falls back to order raw_payload_json when orderItems is empty', () => {
const order: OrderListRow = {
id: 2,
provider: '91',
platform: 'kuaishou',
shop_id: '4269276762',
shop_name: '独角兽游戏专营店',
platform_order_id: '2625301841249202',
order_status: 'paid',
pay_status: 'paid',
buyer_id: '',
buyer_name: '',
receiver_contact: '',
total_amount: 5200,
currency: 'CNY',
raw_payload_json: {
source: 'kuaishou_send_code',
body: {
itemTitle: '和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪',
num: 1,
ext: '{"skuNick":"安卓血宴1钻"}',
},
},
paid_at: '2026-09-10T09:18:40.000Z',
created_at: '2026-09-10T09:18:40.000Z',
updated_at: '2026-09-10T09:18:40.000Z',
}
const mapped = mapAdminOrderListItem(order, { orderItems: [], tasks: [] })
assert.equal(mapped.itemTitle, '和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪')
assert.equal(mapped.skuNick, '安卓血宴1钻')
assert.equal(mapped.itemCount, 1)
assert.equal(mapped.totalQuantity, 1)
assert.equal(mapped.itemSummary, '和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪 (安卓血宴1钻)')
})
test('summarizeOrderItems formats multiple items cleanly', () => {
const items: OrderItemRow[] = [
{
id: 1,
order_id: 1,
sku_code: 'SKU1',
sku_name: '规格A',
quantity: 1,
spec_json: { itemTitle: '商品1', skuNick: '规格A' },
},
{
id: 2,
order_id: 1,
sku_code: 'SKU2',
sku_name: '规格B',
quantity: 1,
spec_json: { itemTitle: '商品2', skuNick: '规格B' },
},
]
const summary = summarizeOrderItems(items)
assert.equal(summary, '商品1 (规格A) 等 2 项')
})
@@ -1,16 +1,71 @@
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
import { resolveDisplayShopName, resolveOrderItemTitle } from './admin-read-shared-helpers.js'
import {
resolveDisplayShopName,
resolveOrderItemSkuNick,
resolveOrderItemTitle,
} from './admin-read-shared-helpers.js'
import { buildOrderFulfillmentProgress } from './admin-task-read-helpers.js'
import type { AdminOrderListItem } from '../../types/admin/read-models.js'
import type { OrderItemRow, OrderListRow, TaskRow } from '../../types/repository/rows.js'
import type { AdminOrderItemSummary, AdminOrderListItem } from '../../types/admin/read-models.js'
import type {
KuaishouIndustryVoucherRow,
OrderItemRow,
OrderListRow,
TaskRow,
} from '../../types/repository/rows.js'
export function mapAdminOrderListItem(
item: OrderListRow,
{ tasks = [], orderItems = [] }: { tasks?: TaskRow[]; orderItems?: OrderItemRow[] } = {},
{
tasks = [],
orderItems = [],
vouchers = [],
}: {
tasks?: TaskRow[]
orderItems?: OrderItemRow[]
vouchers?: KuaishouIndustryVoucherRow[]
} = {},
): AdminOrderListItem {
const fulfillmentProgress = buildOrderFulfillmentProgress(tasks)
const itemSummary = summarizeOrderItems(orderItems)
const normalizedItems = Array.isArray(orderItems) ? orderItems : []
const itemSummary = summarizeOrderItems(normalizedItems, item, vouchers)
let resolvedItemTitle = ''
let resolvedSkuNick = ''
let itemsList: AdminOrderItemSummary[] = []
if (normalizedItems.length > 0) {
itemsList = normalizedItems.map((orderItem) => {
const itTitle = resolveOrderItemTitle(orderItem, item, vouchers)
const itSkuNick = resolveOrderItemSkuNick(orderItem, item, vouchers)
return {
itemTitle: itTitle,
skuNick: itSkuNick,
quantity: Math.max(1, Number(orderItem.quantity || 1)),
}
})
resolvedItemTitle = itemsList[0]?.itemTitle || ''
resolvedSkuNick = itemsList[0]?.skuNick || ''
} else {
resolvedItemTitle = resolveOrderItemTitle(null, item, vouchers)
resolvedSkuNick = resolveOrderItemSkuNick(null, item, vouchers)
if (resolvedItemTitle || resolvedSkuNick) {
itemsList = [
{
itemTitle: resolvedItemTitle,
skuNick: resolvedSkuNick,
quantity: 1,
},
]
}
}
const rawItemCount = normalizedItems.length
const rawTotalQuantity = normalizedItems.reduce(
(sum, orderItem) => sum + Math.max(1, Number(orderItem.quantity || 1)),
0,
)
const hasResolvedFallback = (resolvedItemTitle || resolvedSkuNick) && rawItemCount === 0
return {
orderId: item.id,
@@ -30,12 +85,12 @@ export function mapAdminOrderListItem(
paidAt: item.paid_at,
createdAt: item.created_at,
updatedAt: item.updated_at,
itemCount: orderItems.length,
totalQuantity: orderItems.reduce(
(sum, orderItem) => sum + Math.max(1, Number(orderItem.quantity || 1)),
0,
),
itemCount: hasResolvedFallback ? 1 : rawItemCount,
totalQuantity: hasResolvedFallback ? 1 : rawTotalQuantity,
itemSummary,
itemTitle: resolvedItemTitle,
skuNick: resolvedSkuNick,
items: itemsList,
taskCount: tasks.length,
resourceStatus: fulfillmentProgress.resourceStatus,
customerStatus: fulfillmentProgress.customerStatus,
@@ -44,17 +99,28 @@ export function mapAdminOrderListItem(
}
}
export function summarizeOrderItems(items: OrderItemRow[] | null | undefined): string {
export function summarizeOrderItems(
items: OrderItemRow[] | null | undefined,
fallbackOrder?: OrderListRow | null | undefined,
vouchers?: KuaishouIndustryVoucherRow[] | null | undefined,
): string {
const normalizedItems = Array.isArray(items) ? items : []
if (normalizedItems.length === 0) {
return ''
const fallbackTitle = resolveOrderItemTitle(null, fallbackOrder, vouchers)
const fallbackSkuNick = resolveOrderItemSkuNick(null, fallbackOrder, vouchers)
if (!fallbackTitle && !fallbackSkuNick) {
return ''
}
return formatItemTitleWithSku(fallbackTitle, fallbackSkuNick)
}
const [firstItem] = normalizedItems
const firstLabel =
resolveOrderItemTitle(firstItem) ||
const firstTitle =
resolveOrderItemTitle(firstItem, fallbackOrder, vouchers) ||
String(firstItem?.sku_name || firstItem?.sku_code || '').trim()
const firstSkuNick = resolveOrderItemSkuNick(firstItem, fallbackOrder, vouchers)
const firstLabel = formatItemTitleWithSku(firstTitle, firstSkuNick)
if (normalizedItems.length === 1) {
return firstLabel
@@ -62,3 +128,14 @@ export function summarizeOrderItems(items: OrderItemRow[] | null | undefined): s
return `${firstLabel} 等 ${normalizedItems.length} 项`
}
function formatItemTitleWithSku(title: string, skuNick: string): string {
const cleanTitle = String(title || '').trim()
const cleanSku = String(skuNick || '').trim()
if (cleanTitle && cleanSku && !cleanTitle.includes(cleanSku)) {
return `${cleanTitle} (${cleanSku})`
}
return cleanTitle || cleanSku
}
@@ -16,6 +16,7 @@ import { listTaskEventsByTaskId } from '../../repositories/task-event-repo.js'
import type { JsonRecord } from '../../types/json.js'
import {
listKuaishouIndustryVouchersByOid,
listKuaishouIndustryVouchersByOids,
listKuaishouIndustryVouchersByTaskId,
} from '../../repositories/kuaishou-industry-voucher-repo.js'
import { listCloudtentaclesSources } from '../platforms/cloudtentacles/source-config-service.js'
@@ -52,6 +53,7 @@ import {
resolveAdminTaskScreenshotUrl,
resolveDisplayShopName,
resolveOrderItemDeliveryMode,
resolveOrderItemSkuNick,
resolveOrderItemTitle,
} from './admin-read-shared-helpers.js'
import { mapAdminOrderListItem, summarizeOrderItems } from './admin-order-read-helpers.js'
@@ -96,18 +98,22 @@ export async function getAdminOrders(
})
const orderIds = items.map((item) => item.id)
const [orderItems, tasks] = await Promise.all([
const oids = items.map((item) => item.platform_order_id).filter(Boolean)
const [orderItems, tasks, vouchers] = await Promise.all([
listOrderItemsByOrderIds(orderIds),
listTasksByOrderIds(orderIds),
listKuaishouIndustryVouchersByOids(oids),
])
const orderItemsByOrderId = groupRowsByOrderId(orderItems)
const tasksByOrderId = groupRowsByOrderId(tasks)
const vouchersByOid = groupRowsByOid(vouchers)
return {
items: items.map((item) =>
mapAdminOrderListItem(item, {
orderItems: orderItemsByOrderId.get(item.id) || [],
tasks: tasksByOrderId.get(item.id) || [],
vouchers: vouchersByOid.get(item.platform_order_id) || [],
}),
),
pagination: { page, pageSize, total },
@@ -124,6 +130,16 @@ function groupRowsByOrderId<T extends { order_id: number }>(rows: T[]): Map<numb
return grouped
}
function groupRowsByOid<T extends { oid: string }>(rows: T[]): Map<string, T[]> {
const grouped = new Map<string, T[]>()
for (const row of rows) {
const group = grouped.get(row.oid) || []
group.push(row)
grouped.set(row.oid, group)
}
return grouped
}
export async function getAdminOrderDetail(
orderId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
@@ -142,7 +158,9 @@ export async function getAdminOrderDetail(
listTasksByOrderId(order.id),
])
const kuaishouIndustryVouchers = await listKuaishouIndustryVouchersByOid(order.platform_order_id)
const itemSummary = summarizeOrderItems(items)
const itemSummary = summarizeOrderItems(items, order, kuaishouIndustryVouchers)
const orderItemTitle = resolveOrderItemTitle(items[0], order, kuaishouIndustryVouchers)
const orderSkuNick = resolveOrderItemSkuNick(items[0], order, kuaishouIndustryVouchers)
const viewerContext = createAdminViewerContext(session)
return {
@@ -165,6 +183,8 @@ export async function getAdminOrderDetail(
createdAt: order.created_at,
updatedAt: order.updated_at,
itemSummary,
itemTitle: orderItemTitle,
skuNick: orderSkuNick,
rawPayload:
order.raw_payload_json && typeof order.raw_payload_json === 'object'
? order.raw_payload_json
@@ -175,7 +195,8 @@ export async function getAdminOrderDetail(
orderItemId: item.id,
skuCode: item.sku_code,
skuName: item.sku_name,
itemTitle: resolveOrderItemTitle(item),
itemTitle: resolveOrderItemTitle(item, order, kuaishouIndustryVouchers),
skuNick: resolveOrderItemSkuNick(item, order, kuaishouIndustryVouchers),
quantity: item.quantity,
deliveryMode: resolveOrderItemDeliveryMode(tasks, item.id),
spec:
@@ -6,6 +6,8 @@ import {
createAdminViewerContext,
mapKuaishouCloudFulfillmentContext,
resolveAdminTaskScreenshotUrl,
resolveOrderItemSkuNick,
resolveOrderItemTitle,
} from './admin-read-shared-helpers.js'
test('resolveAdminTaskScreenshotUrl lets support view final redeemed screenshot', async () => {
@@ -86,3 +88,65 @@ test('mapKuaishouCloudFulfillmentContext exposes cloud account display labels',
assert.deepEqual(flow?.binding.cloudSourceLabels, ['白班账号', '夜班账号'])
assert.equal(flow?.binding.resolvedSourceLabel, '夜班账号')
})
test('resolveOrderItemTitle and resolveOrderItemSkuNick with spec_json and snapshot', () => {
const item = {
id: 1,
order_id: 100,
sku_code: '26079757920762',
sku_name: '安卓血宴2钻',
quantity: 1,
spec_json: JSON.stringify({
itemTitle: '和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻',
skuNick: '安卓血宴2钻',
}),
item_snapshot_json: JSON.stringify({
kuaishouSendCode: {
itemTitle: '和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻',
skuNick: '安卓血宴2钻',
},
}),
}
assert.equal(
resolveOrderItemTitle(item),
'和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻',
)
assert.equal(resolveOrderItemSkuNick(item), '安卓血宴2钻')
})
test('resolveOrderItemTitle and resolveOrderItemSkuNick fallback to order raw_payload_json with stringified ext', () => {
const order = {
id: 100,
raw_payload_json: {
source: 'kuaishou_send_code',
body: {
itemTitle: '和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻',
ext: '{"relItemId":26079757920762,"relSkuId":0,"skuNick":"安卓血宴2钻"}',
},
},
} as any
assert.equal(
resolveOrderItemTitle(null, order),
'和平精英地铁逃生装备满钻低价苏尔南怒嚎暗潮手枪璀璨无暇1到7钻',
)
assert.equal(resolveOrderItemSkuNick(null, order), '安卓血宴2钻')
})
test('resolveOrderItemTitle and resolveOrderItemSkuNick fallback to vouchers when order item and order payload are missing', () => {
const vouchers = [
{
oid: '2625301761098133',
raw_payload_json: {
body: {
itemTitle: '测试商品标题',
ext: '{"skuNick":"测试SKU规格"}',
},
},
},
] as any
assert.equal(resolveOrderItemTitle(null, null, vouchers), '测试商品标题')
assert.equal(resolveOrderItemSkuNick(null, null, vouchers), '测试SKU规格')
})
@@ -8,7 +8,12 @@ import {
import { canRegenerateClaimLinkStatus, isTaskFinalStatus } from '../../domain/task-status.js'
import type { AdminViewerSessionInput } from '../../types/admin/read-inputs.js'
import type { OrderItemRow, TaskRow } from '../../types/repository/rows.js'
import type {
KuaishouIndustryVoucherRow,
OrderItemRow,
OrderRow,
TaskRow,
} from '../../types/repository/rows.js'
type CloudSourceLabelMap = Map<string, string> | Record<string, string>
@@ -422,23 +427,154 @@ export function parseTaskState(task: TaskLike | null | undefined): JsonRecord {
return parseTaskStateValue(task)
}
export function resolveOrderItemTitle(item: Partial<OrderItemRow> | null | undefined): string {
if (!item) {
return ''
export function parseExtRecord(value: unknown): JsonRecord {
if (!value) return {}
if (typeof value === 'object' && !Array.isArray(value)) {
return value as JsonRecord
}
if (typeof value === 'string') {
return safeParseJson(value)
}
return {}
}
export function resolveOrderItemTitle(
item: Partial<OrderItemRow> | null | undefined,
fallbackOrder?: Partial<OrderRow> | null | undefined,
vouchers?: KuaishouIndustryVoucherRow[] | null | undefined,
): string {
if (item) {
const spec = safeParseJson(item.spec_json)
const snapshot = safeParseJson(item.item_snapshot_json)
const ksSendCode = asJsonObject(snapshot.kuaishouSendCode)
const ksRawPayload = asJsonObject(snapshot.kuaishouSendCodeRawPayload)
const ksParams = asJsonObject(snapshot.params)
const titleCandidate = pickFirstNonEmpty([
spec.itemTitle,
spec.title,
spec.Title,
spec.item_title,
ksSendCode.itemTitle,
ksRawPayload.itemTitle,
ksParams.itemTitle,
snapshot.itemTitle,
spec.goods_name,
spec.goodsName,
spec.productName,
snapshot.productName,
])
if (titleCandidate) {
return titleCandidate
}
}
const spec = safeParseJson(item.spec_json)
if (fallbackOrder) {
const orderPayload = safeParseJson(fallbackOrder.raw_payload_json)
const body = asJsonObject(
orderPayload.body && typeof orderPayload.body === 'object' ? orderPayload.body : orderPayload,
)
const fromOrder = pickFirstNonEmpty([
body.itemTitle,
orderPayload.itemTitle,
body.productName,
orderPayload.productName,
body.title,
orderPayload.title,
])
if (fromOrder) {
return fromOrder
}
}
return pickFirstNonEmpty([
spec.title,
spec.Title,
spec.itemTitle,
spec.item_title,
spec.goods_name,
spec.goodsName,
item.sku_name,
item.sku_code,
])
if (Array.isArray(vouchers) && vouchers.length > 0) {
for (const voucher of vouchers) {
const voucherPayload = safeParseJson(voucher.raw_payload_json)
const body = asJsonObject(
voucherPayload.body && typeof voucherPayload.body === 'object'
? voucherPayload.body
: voucherPayload,
)
const fromVoucher = pickFirstNonEmpty([body.itemTitle, voucherPayload.itemTitle, body.title])
if (fromVoucher) {
return fromVoucher
}
}
}
if (item) {
return pickFirstNonEmpty([item.sku_name, item.sku_code])
}
return ''
}
export function resolveOrderItemSkuNick(
item: Partial<OrderItemRow> | null | undefined,
fallbackOrder?: Partial<OrderRow> | null | undefined,
vouchers?: KuaishouIndustryVoucherRow[] | null | undefined,
): string {
if (item) {
const spec = safeParseJson(item.spec_json)
const snapshot = safeParseJson(item.item_snapshot_json)
const ksSendCode = asJsonObject(snapshot.kuaishouSendCode)
const ksRawPayload = asJsonObject(snapshot.kuaishouSendCodeRawPayload)
const ksParams = asJsonObject(snapshot.params)
const extFromSnapshot = parseExtRecord(ksRawPayload.ext || ksParams.ext || snapshot.ext)
const extFromSpec = parseExtRecord(spec.ext)
const candidateSkuNick = pickFirstNonEmpty([
spec.skuNick,
spec.sku_nick,
ksSendCode.skuNick,
extFromSnapshot.skuNick,
extFromSpec.skuNick,
snapshot.externalSkuName,
snapshot.externalSkuNameNormalized,
snapshot.skuNick,
spec.skuName,
])
if (candidateSkuNick) {
return candidateSkuNick
}
const resolvedTitle = resolveOrderItemTitle(item, fallbackOrder, vouchers)
if (item.sku_name && item.sku_name.trim() !== resolvedTitle) {
return item.sku_name.trim()
}
}
if (fallbackOrder) {
const orderPayload = safeParseJson(fallbackOrder.raw_payload_json)
const body = asJsonObject(
orderPayload.body && typeof orderPayload.body === 'object' ? orderPayload.body : orderPayload,
)
const ext = parseExtRecord(body.ext || orderPayload.ext)
const fromOrder = pickFirstNonEmpty([ext.skuNick, body.skuNick, orderPayload.skuNick])
if (fromOrder) {
return fromOrder
}
}
if (Array.isArray(vouchers) && vouchers.length > 0) {
for (const voucher of vouchers) {
const voucherPayload = safeParseJson(voucher.raw_payload_json)
const body = asJsonObject(
voucherPayload.body && typeof voucherPayload.body === 'object'
? voucherPayload.body
: voucherPayload,
)
const ext = parseExtRecord(body.ext || voucherPayload.ext)
const fromVoucher = pickFirstNonEmpty([ext.skuNick, body.skuNick, voucherPayload.skuNick])
if (fromVoucher) {
return fromVoucher
}
}
}
return ''
}
export function resolveOrderItemDeliveryMode(
@@ -130,6 +130,7 @@ export async function syncWorkOrdersFromKuaishouSendCode(
if (order) {
const orderItems = await applyKuaishouSendCodeItemData(
order.id,
await listOrderItemsByOrderId(order.id),
sourceData,
input.rawParams,
@@ -381,6 +382,7 @@ export function resolveKuaishouSendCodeShopName(
}
async function applyKuaishouSendCodeItemData(
orderId: number,
orderItems: OrderItemRow[],
sourceData: KuaishouSendCodeWorkOrderData,
rawPayload: JsonObject,
@@ -390,6 +392,34 @@ async function applyKuaishouSendCodeItemData(
return orderItems
}
if (orderItems.length === 0) {
return replaceOrderItems(orderId, [
{
skuCode: sourceData.itemId || sourceData.skuId || 'default',
skuName: sourceData.skuNick || sourceData.itemTitle,
quantity: sourceData.quantity || 1,
specJson: JSON.stringify({
sellerId: sourceData.sellerId,
skuId: sourceData.skuId,
itemTitle: sourceData.itemTitle,
relItemId: sourceData.relItemId,
relSkuId: sourceData.relSkuId,
skuNick: sourceData.skuNick,
quantity: sourceData.quantity,
paymentFen: sourceData.paymentFen,
source: KUAISHOU_SEND_CODE_SOURCE,
}),
itemSnapshotJson: JSON.stringify({
source: KUAISHOU_SEND_CODE_SOURCE,
params: rawPayload,
kuaishouSendCode: sourceData,
}),
createdAt: now,
updatedAt: now,
},
])
}
const matchedIds = findKuaishouSendCodeItemIds(orderItems, sourceData)
if (matchedIds.size === 0) {
return orderItems
@@ -402,6 +432,12 @@ async function applyKuaishouSendCodeItemData(
}
const snapshot = safeParseJson(item.item_snapshot_json)
const spec = safeParseJson(item.spec_json)
const mergedSpec = {
...spec,
itemTitle: sourceData.itemTitle || spec.itemTitle,
skuNick: sourceData.skuNick || spec.skuNick,
}
const snapshotJson = {
...item,
item_snapshot_json: {
@@ -417,6 +453,7 @@ async function applyKuaishouSendCodeItemData(
const updated = await updateOrderItemSourceSnapshot(item.id, {
skuName: sourceData.skuNick || item.sku_name,
itemSnapshotJson: JSON.stringify(snapshotJson.item_snapshot_json),
specJson: JSON.stringify(mergedSpec),
updatedAt: now,
})
return updated || item
@@ -6,6 +6,12 @@ export type AdminOrderFulfillmentProgress = {
customerStatus: string
}
export type AdminOrderItemSummary = {
itemTitle: string
skuNick: string
quantity: number
}
export type AdminOrderListItem = {
orderId: number
provider: string
@@ -27,6 +33,9 @@ export type AdminOrderListItem = {
itemCount: number
totalQuantity: number
itemSummary: string
itemTitle?: string
skuNick?: string
items?: AdminOrderItemSummary[]
taskCount: number
resourceStatus: string
customerStatus: string
@@ -1,5 +1,5 @@
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Alert, Button, Card, Descriptions, Skeleton, Space, Table, Typography } from 'antd'
import { Alert, Button, Card, Descriptions, Skeleton, Space, Table, Tag, Typography } from 'antd'
import type { TableColumnsType } from 'antd'
import { useQuery } from '@tanstack/react-query'
import { useNavigate, useParams } from 'react-router'
@@ -115,7 +115,16 @@ export default function AdminOrderDetailPage() {
<Descriptions.Item label="更新时间">
{formatAdminDateTime(detail.order.updatedAt)}
</Descriptions.Item>
<Descriptions.Item label="商品摘要">{detail.order.itemSummary || '-'}</Descriptions.Item>
<Descriptions.Item label="商品摘要">
<div className="cell-stack" style={{ gap: 4 }}>
<span>{detail.order.itemTitle || detail.order.itemSummary || '-'}</span>
{detail.order.skuNick ? (
<Tag color="blue" style={{ margin: 0, width: 'fit-content' }}>
{detail.order.skuNick}
</Tag>
) : null}
</div>
</Descriptions.Item>
</Descriptions>
</Card>
@@ -126,8 +135,26 @@ export default function AdminOrderDetailPage() {
dataSource={detail.items}
scroll={{ x: 760 }}
columns={[
{ title: '商品', dataIndex: 'itemTitle', minWidth: 240 },
{ title: 'SKU', dataIndex: 'skuCode', minWidth: 180 },
{
title: '商品',
dataIndex: 'itemTitle',
minWidth: 240,
render: (value) => <strong>{value || '-'}</strong>,
},
{
title: 'SKU / 规格',
minWidth: 180,
render: (_, item) => (
<div className="cell-stack" style={{ gap: 4 }}>
{item.skuNick ? (
<Tag color="blue" style={{ margin: 0, width: 'fit-content' }}>
{item.skuNick}
</Tag>
) : null}
<span className="muted">{item.skuCode || item.skuName || '-'}</span>
</div>
),
},
{ title: '数量', dataIndex: 'quantity', width: 90 },
{ title: '履约模式', dataIndex: 'deliveryMode', minWidth: 140 },
]}
@@ -1,5 +1,5 @@
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons'
import { Button, Card, Form, Input, Select, Space, Table, Typography } from 'antd'
import { Button, Card, Form, Input, Select, Space, Table, Tag, Typography } from 'antd'
import type { TableColumnsType } from 'antd'
import { useQuery } from '@tanstack/react-query'
import { useEffect, useMemo } from 'react'
@@ -102,15 +102,76 @@ export default function AdminOrdersPage() {
},
{
title: '商品 / 任务',
minWidth: 220,
render: (_, row) => (
<div className="cell-stack">
<span>{row.itemSummary || '-'}</span>
<span className="muted">
商品 {row.itemCount} · 数量 {row.totalQuantity} · 任务 {row.taskCount}
</span>
</div>
),
minWidth: 260,
render: (_, row) => {
const itemTitle = row.itemTitle || row.itemSummary || '-'
const skuNick = row.skuNick
const items = row.items && row.items.length > 0 ? row.items : null
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 }}
>
{item.itemTitle || '-'}
</Typography.Text>
{item.skuNick ? (
<Space size={4} wrap>
<Tag
color="blue"
style={{
margin: 0,
maxWidth: 320,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={`规格:${item.skuNick}`}
>
{item.skuNick}
</Tag>
</Space>
) : null}
</div>
))}
{items.length > 2 ? <span className="muted">等 {items.length} 种商品</span> : null}
</div>
) : (
<>
<Typography.Text strong ellipsis={{ tooltip: itemTitle }} style={{ maxWidth: 360 }}>
{itemTitle}
</Typography.Text>
{skuNick ? (
<Space size={4} wrap>
<Tag
color="blue"
style={{
margin: 0,
maxWidth: 320,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={`规格:${skuNick}`}
>
{skuNick}
</Tag>
</Space>
) : null}
</>
)}
<span className="muted">
商品 {row.itemCount} · 数量 {row.totalQuantity} · 任务 {row.taskCount}
</span>
</div>
)
},
},
{
title: '状态',
+12
View File
@@ -8,6 +8,12 @@ export interface AdminOrderFulfillmentProgress {
customerStatus: string
}
export interface AdminOrderItemSummary {
itemTitle: string
skuNick: string
quantity: number
}
export interface AdminOrderListItem {
orderId: number
provider: string
@@ -29,6 +35,9 @@ export interface AdminOrderListItem {
itemCount: number
totalQuantity: number
itemSummary: string
itemTitle?: string
skuNick?: string
items?: AdminOrderItemSummary[]
taskCount: number
resourceStatus: string
customerStatus: string
@@ -56,6 +65,8 @@ export interface AdminOrderDetail {
createdAt: string
updatedAt: string
itemSummary: string
itemTitle?: string
skuNick?: string
rawPayload: Record<string, unknown>
fulfillmentProgress: AdminOrderFulfillmentProgress
}
@@ -64,6 +75,7 @@ export interface AdminOrderDetail {
skuCode: string
skuName: string
itemTitle: string
skuNick?: string
quantity: number
deliveryMode: string
spec: Record<string, unknown>