Compare commits
2
Commits
fe2a28454d
...
076b113ae5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
076b113ae5 | ||
|
|
4544622cb3 |
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
} from './types.js'
|
||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||
|
||||
/**
|
||||
* @deprecated 待移除,快手飞飞直充渠道已废弃,不再使用。
|
||||
*/
|
||||
export const kuaishouFeifeiExecutor: FulfillmentExecutor = {
|
||||
key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI,
|
||||
preparePaidTask,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||
import { autoConsumeKuaishouIndustryVouchersForTask } from '../../platforms/kuaishou-industry/voucher-auto-consume-service.js'
|
||||
import {
|
||||
FULFILLMENT_EXECUTOR_KEYS,
|
||||
type FulfillmentExecutor,
|
||||
@@ -29,5 +30,12 @@ async function preparePaidTask(
|
||||
source: 'manual_dispatch_profile',
|
||||
})
|
||||
|
||||
const effectiveTask = updatedTask || task
|
||||
await autoConsumeKuaishouIndustryVouchersForTask(effectiveTask, {
|
||||
source: 'manual_dispatch_prepared',
|
||||
}).catch(() => {
|
||||
// 忽略异常,发码回调完成后仍会自动补核销
|
||||
})
|
||||
|
||||
return updatedTask
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ import { buildKuaishouIndustryVoucherContext } from '../../platforms/kuaishou-in
|
||||
import { normalizeShortLinkPayload } from '../../short-links/short-link-service.js'
|
||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||
|
||||
/**
|
||||
* @deprecated 待移除,快手飞飞直充渠道已废弃,不再使用。
|
||||
*/
|
||||
export function isKuaishouFeifeiTask(task: Partial<TaskRow> | null | undefined) {
|
||||
return String(task?.executor_key || '').trim() === 'kuaishou_feifei'
|
||||
}
|
||||
|
||||
@@ -11,10 +11,7 @@ import {
|
||||
buildIndustryErrorResponse,
|
||||
buildIndustryQueryCodeData,
|
||||
} from './response.js'
|
||||
import {
|
||||
buildKuaishouIndustryEticketFromVoucher,
|
||||
isKuaishouIndustryVoucherSendCallbackSuccess,
|
||||
} from './voucher-service.js'
|
||||
import { buildKuaishouIndustryEticketFromVoucher } from './voucher-service.js'
|
||||
|
||||
export async function handleQueryCode(rawBody: JsonObject = {}) {
|
||||
const config = getKuaishouIndustryConfig()
|
||||
@@ -32,9 +29,6 @@ export async function handleQueryCode(rawBody: JsonObject = {}) {
|
||||
if (!matched) {
|
||||
return buildIndustryErrorResponse(4012005, `卡券不存在: ${params.eticketId}`)
|
||||
}
|
||||
if (!isKuaishouIndustryVoucherSendCallbackSuccess(matched)) {
|
||||
return buildIndustryErrorResponse(4012005, `卡券尚未完成发码: ${params.eticketId}`)
|
||||
}
|
||||
|
||||
const eticket = buildKuaishouIndustryEticketFromVoucher(matched, params.eticketType)
|
||||
return buildIndustrySuccessResponse(
|
||||
@@ -47,9 +41,7 @@ export async function handleQueryCode(rawBody: JsonObject = {}) {
|
||||
)
|
||||
}
|
||||
|
||||
const vouchers = (await listKuaishouIndustryVouchersByOid(normalizedOid)).filter(
|
||||
isKuaishouIndustryVoucherSendCallbackSuccess,
|
||||
)
|
||||
const vouchers = await listKuaishouIndustryVouchersByOid(normalizedOid)
|
||||
if (vouchers.length === 0) {
|
||||
return buildIndustryErrorResponse(4012002, `订单不存在: ${normalizedOid}`)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
resolveKuaishouIndustryVoucherValidity,
|
||||
} from './voucher-service.js'
|
||||
import { bindKuaishouIndustryVouchersToOrderTasks } from './voucher-binding-service.js'
|
||||
import { autoConsumeKuaishouIndustryVouchersForOrder } from './voucher-auto-consume-service.js'
|
||||
import { parseAmountToFen } from '../../../utils/money.js'
|
||||
import {
|
||||
publishAdminOrderRealtimeChange,
|
||||
@@ -503,20 +504,45 @@ async function syncOpen91OrderAfterSendCallbackSuccess(
|
||||
now: normalizeTimestampIso(new Date().toISOString()),
|
||||
})
|
||||
}
|
||||
if (isOpen91OrderAwaitingFulfillmentConfig(error)) {
|
||||
return
|
||||
if (!isOpen91OrderAwaitingFulfillmentConfig(error)) {
|
||||
logIntegration(
|
||||
'[kuaishou-industry/send-code]',
|
||||
'电子凭证发货回调成功后重试 91 订单失败',
|
||||
{
|
||||
source,
|
||||
oid: normalizedOid,
|
||||
orderId: order.id,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ level: 'warn' },
|
||||
)
|
||||
}
|
||||
logIntegration(
|
||||
'[kuaishou-industry/send-code]',
|
||||
'电子凭证发货回调成功后重试 91 订单失败',
|
||||
{
|
||||
}
|
||||
|
||||
try {
|
||||
const autoConsumeResult = await autoConsumeKuaishouIndustryVouchersForOrder(normalizedOid, {
|
||||
source: `${source}_auto_consume`,
|
||||
})
|
||||
if (autoConsumeResult.consumedCount > 0 || autoConsumeResult.failedCount > 0) {
|
||||
logIntegration('[kuaishou-industry/send-code]', '发码回调成功后自动推进电子凭证核销完成', {
|
||||
source,
|
||||
oid: normalizedOid,
|
||||
orderId: order.id,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ level: 'warn' },
|
||||
)
|
||||
consumedCount: autoConsumeResult.consumedCount,
|
||||
alreadyConsumedCount: autoConsumeResult.alreadyConsumedCount,
|
||||
skippedCount: autoConsumeResult.skippedCount,
|
||||
failedCount: autoConsumeResult.failedCount,
|
||||
skippedReasons: autoConsumeResult.skippedReasons,
|
||||
errors: autoConsumeResult.errors,
|
||||
})
|
||||
}
|
||||
} catch (consumeError) {
|
||||
logWarn('[kuaishou-industry/send-code]', '发码回调成功后自动推进电子凭证核销异常', {
|
||||
source,
|
||||
oid: normalizedOid,
|
||||
orderId: order.id,
|
||||
error: consumeError instanceof Error ? consumeError.message : String(consumeError),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
autoConsumeKuaishouIndustryVouchersForOrder,
|
||||
autoConsumeKuaishouIndustryVouchersForTask,
|
||||
} from './voucher-auto-consume-service.js'
|
||||
import { WORK_ORDER_STATUS } from '../../../domain/work-order-status.js'
|
||||
import type {
|
||||
KuaishouIndustryVoucherRow,
|
||||
OrderRow,
|
||||
TaskRow,
|
||||
} from '../../../types/repository/rows.js'
|
||||
import type { WorkOrderRow } from '../../../repositories/worker-platform/index.js'
|
||||
|
||||
function buildMockVoucher(
|
||||
overrides: Partial<KuaishouIndustryVoucherRow> = {},
|
||||
): KuaishouIndustryVoucherRow {
|
||||
return {
|
||||
id: 1,
|
||||
voucher_code: 'KSV001',
|
||||
oid: '26250000000001',
|
||||
order_id: 100,
|
||||
task_id: 200,
|
||||
unit_index: 1,
|
||||
seller_id: '4269276762',
|
||||
token: 'token-1',
|
||||
eticket_type: 'GAME_OPEN_TICKET_CONSUME',
|
||||
status: 'UNUSED',
|
||||
valid_start_time: 1789000000000,
|
||||
valid_end_time: 1791592000000,
|
||||
consume_serial_num: '',
|
||||
consume_details_json: '[]',
|
||||
consumed_at: null,
|
||||
destroyed_at: null,
|
||||
raw_payload_json: '{}',
|
||||
send_callback_status: 'success',
|
||||
send_callback_attempt_count: 1,
|
||||
send_callback_last_error: '',
|
||||
send_callback_response_json: '{}',
|
||||
send_callback_sent_at: '2026-09-10T09:00:00.000Z',
|
||||
created_at: '2026-09-10T09:00:00.000Z',
|
||||
updated_at: '2026-09-10T09:00:00.000Z',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function buildMockTask(overrides: Partial<TaskRow> = {}): TaskRow {
|
||||
return {
|
||||
id: 200,
|
||||
order_id: 100,
|
||||
order_item_id: 300,
|
||||
provider: 'open_91',
|
||||
platform: '91kaquan',
|
||||
shop_id: '4269276762',
|
||||
platform_order_id: '26250000000001',
|
||||
task_no: 'TASK-001',
|
||||
unit_index: 1,
|
||||
task_status: 'manual_review',
|
||||
delivery_status: 'pending',
|
||||
user_action_status: 'not_required',
|
||||
executor_key: 'manual_dispatch',
|
||||
sku_code: 'SKU001',
|
||||
sku_name: '测试商品',
|
||||
quantity: 1,
|
||||
requires_claim: false,
|
||||
auto_dispatch: false,
|
||||
claim_token: '',
|
||||
claim_expires_at: null,
|
||||
primary_claim_token_id: null,
|
||||
primary_claim_token: '',
|
||||
primary_claim_expires_at: null,
|
||||
last_error: '',
|
||||
result_code: '',
|
||||
result_message: '',
|
||||
context_json: '{}',
|
||||
claimed_at: null,
|
||||
role_confirmed_at: null,
|
||||
redeemed_at: null,
|
||||
created_at: '2026-09-10T09:00:00.000Z',
|
||||
updated_at: '2026-09-10T09:00:00.000Z',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function buildMockWorkOrder(overrides: Partial<WorkOrderRow> = {}): WorkOrderRow {
|
||||
return {
|
||||
id: 500,
|
||||
work_order_no: 'WO-001',
|
||||
order_id: 100,
|
||||
order_item_id: 300,
|
||||
task_id: 200,
|
||||
source: 'test',
|
||||
status: WORK_ORDER_STATUS.UNASSIGNED,
|
||||
assigned_worker_id: null,
|
||||
assigned_at: null,
|
||||
completed_at: null,
|
||||
reward_amount: 1000,
|
||||
refund_amount: 0,
|
||||
platform_order_id: '26250000000001',
|
||||
product_name: '测试工单',
|
||||
material_json: '{}',
|
||||
created_at: '2026-09-10T09:00:00.000Z',
|
||||
updated_at: '2026-09-10T09:00:00.000Z',
|
||||
...overrides,
|
||||
} as unknown as WorkOrderRow
|
||||
}
|
||||
|
||||
test('缺少 oid 时直接返回 skipped', async () => {
|
||||
const result = await autoConsumeKuaishouIndustryVouchersForOrder('')
|
||||
assert.equal(result.skippedCount, 1)
|
||||
assert.deepEqual(result.skippedReasons, ['missing_oid'])
|
||||
})
|
||||
|
||||
test('未找到凭证时直接返回 skipped', async () => {
|
||||
const result = await autoConsumeKuaishouIndustryVouchersForOrder(
|
||||
'26250000000001',
|
||||
{},
|
||||
{
|
||||
listVouchersByOid: async () => [],
|
||||
},
|
||||
)
|
||||
assert.equal(result.voucherCount, 0)
|
||||
assert.equal(result.skippedCount, 1)
|
||||
assert.deepEqual(result.skippedReasons, ['voucher_not_found'])
|
||||
})
|
||||
|
||||
test('已核销的凭证不重复核销,跳过并记录 already_consumed', async () => {
|
||||
const voucher = buildMockVoucher({ status: 'CONSUMED' })
|
||||
let consumeCalled = false
|
||||
|
||||
const result = await autoConsumeKuaishouIndustryVouchersForOrder(
|
||||
'26250000000001',
|
||||
{},
|
||||
{
|
||||
listVouchersByOid: async () => [voucher],
|
||||
consumeVoucher: async () => {
|
||||
consumeCalled = true
|
||||
return { ok: true, voucher, callbackSuccess: true }
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert.equal(consumeCalled, false)
|
||||
assert.equal(result.alreadyConsumedCount, 1)
|
||||
assert.equal(result.skippedCount, 1)
|
||||
assert.match(result.skippedReasons[0] || '', /already_consumed/)
|
||||
})
|
||||
|
||||
test('发码出站回调未确认时(pending 或 failed),安全跳过不触发核销', async () => {
|
||||
const voucher = buildMockVoucher({ send_callback_status: 'pending' })
|
||||
let consumeCalled = false
|
||||
|
||||
const result = await autoConsumeKuaishouIndustryVouchersForOrder(
|
||||
'26250000000001',
|
||||
{},
|
||||
{
|
||||
listVouchersByOid: async () => [voucher],
|
||||
consumeVoucher: async () => {
|
||||
consumeCalled = true
|
||||
return { ok: true, voucher, callbackSuccess: true }
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert.equal(consumeCalled, false)
|
||||
assert.equal(result.consumedCount, 0)
|
||||
assert.equal(result.skippedCount, 1)
|
||||
assert.match(result.skippedReasons[0] || '', /send_callback_unconfirmed/)
|
||||
})
|
||||
|
||||
test('发单平台待发布大厅工单(unassigned/pending_material):跳过自动核销,保留在发布大厅时核销', async () => {
|
||||
const voucher = buildMockVoucher({ task_id: 200 })
|
||||
const task = buildMockTask({ id: 200 })
|
||||
const workOrder = buildMockWorkOrder({ task_id: 200, status: WORK_ORDER_STATUS.UNASSIGNED })
|
||||
let consumeCalled = false
|
||||
|
||||
const result = await autoConsumeKuaishouIndustryVouchersForOrder(
|
||||
'26250000000001',
|
||||
{},
|
||||
{
|
||||
listVouchersByOid: async () => [voucher],
|
||||
findOrder: async () => ({ id: 100 }) as OrderRow,
|
||||
listTasks: async () => [task],
|
||||
listWorkOrders: async () => [workOrder],
|
||||
consumeVoucher: async () => {
|
||||
consumeCalled = true
|
||||
return { ok: true, voucher, callbackSuccess: true }
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert.equal(consumeCalled, false)
|
||||
assert.equal(result.consumedCount, 0)
|
||||
assert.equal(result.skippedCount, 1)
|
||||
assert.match(result.skippedReasons[0] || '', /waiting_work_order_hall_publish/)
|
||||
})
|
||||
|
||||
test('常规非大厅订单(人工派单/其他直接任务):发码成功后立即自动核销', async () => {
|
||||
const voucher = buildMockVoucher({ task_id: 200, send_callback_status: 'success' })
|
||||
const task = buildMockTask({ id: 200, executor_key: 'manual_dispatch' })
|
||||
let consumeCalled = false
|
||||
let attachCalled = false
|
||||
|
||||
const result = await autoConsumeKuaishouIndustryVouchersForOrder(
|
||||
'26250000000001',
|
||||
{ source: 'test_auto_consume' },
|
||||
{
|
||||
listVouchersByOid: async () => [voucher],
|
||||
findOrder: async () => ({ id: 100 }) as OrderRow,
|
||||
listTasks: async () => [task],
|
||||
listWorkOrders: async () => [], // 无发单平台待发布工单
|
||||
consumeVoucher: async (v, input) => {
|
||||
consumeCalled = true
|
||||
assert.equal(input.source, 'test_auto_consume')
|
||||
return {
|
||||
ok: true,
|
||||
voucher: { ...v, status: 'CONSUMED' },
|
||||
callbackSuccess: true,
|
||||
}
|
||||
},
|
||||
attachVoucher: async () => {
|
||||
attachCalled = true
|
||||
return task
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert.equal(consumeCalled, true)
|
||||
assert.equal(attachCalled, true)
|
||||
assert.equal(result.consumedCount, 1)
|
||||
assert.equal(result.failedCount, 0)
|
||||
})
|
||||
|
||||
test('autoConsumeKuaishouIndustryVouchersForTask 正确提取任务 platform_order_id 并推进', async () => {
|
||||
const task = buildMockTask({ platform_order_id: '26250000000001' })
|
||||
const voucher = buildMockVoucher({ task_id: 200, send_callback_status: 'success' })
|
||||
|
||||
let consumeCalled = false
|
||||
const result = await autoConsumeKuaishouIndustryVouchersForTask(
|
||||
task,
|
||||
{ source: 'task_test' },
|
||||
{
|
||||
listVouchersByOid: async () => [voucher],
|
||||
findOrder: async () => ({ id: 100 }) as OrderRow,
|
||||
listTasks: async () => [task],
|
||||
listWorkOrders: async () => [],
|
||||
consumeVoucher: async (v) => {
|
||||
consumeCalled = true
|
||||
return {
|
||||
ok: true,
|
||||
voucher: { ...v, status: 'CONSUMED' },
|
||||
callbackSuccess: true,
|
||||
}
|
||||
},
|
||||
attachVoucher: async () => task,
|
||||
},
|
||||
)
|
||||
|
||||
assert.equal(consumeCalled, true)
|
||||
assert.equal(result.consumedCount, 1)
|
||||
})
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
import { listKuaishouIndustryVouchersByOid } from '../../../repositories/kuaishou-industry-voucher-repo.js'
|
||||
import { findLatestOrderByPlatformOrderId } from '../../../repositories/order-repo.js'
|
||||
import { listTasksByOrderId } from '../../../repositories/task-repo.js'
|
||||
import { listWorkOrdersByPlatformOrderId } from '../../../repositories/worker-platform/work-order-query-repo.js'
|
||||
import { attachKuaishouIndustryVoucherToTask } from './voucher-binding-service.js'
|
||||
import {
|
||||
consumeKuaishouIndustryVoucher,
|
||||
isKuaishouIndustryVoucherSendCallbackSuccess,
|
||||
normalizeVoucherStatus,
|
||||
} from './voucher-service.js'
|
||||
import { logIntegration, logWarn } from '../../../utils/logger.js'
|
||||
import { OPEN_91_PLATFORM, OPEN_91_PROVIDER } from '../../open-91/config.js'
|
||||
import { normalizeWorkOrderStatus, WORK_ORDER_STATUS } from '../../../domain/work-order-status.js'
|
||||
import type {
|
||||
KuaishouIndustryVoucherRow,
|
||||
OrderRow,
|
||||
TaskRow,
|
||||
} from '../../../types/repository/rows.js'
|
||||
import type { WorkOrderRow } from '../../../repositories/worker-platform/index.js'
|
||||
|
||||
export type VoucherAutoConsumeResult = {
|
||||
oid: string
|
||||
voucherCount: number
|
||||
consumedCount: number
|
||||
alreadyConsumedCount: number
|
||||
skippedCount: number
|
||||
failedCount: number
|
||||
skippedReasons: string[]
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
type VoucherAutoConsumeDeps = {
|
||||
listVouchersByOid?: typeof listKuaishouIndustryVouchersByOid
|
||||
findOrder?: typeof findLatestOrderByPlatformOrderId
|
||||
listTasks?: typeof listTasksByOrderId
|
||||
listWorkOrders?: typeof listWorkOrdersByPlatformOrderId
|
||||
consumeVoucher?: typeof consumeKuaishouIndustryVoucher
|
||||
attachVoucher?: typeof attachKuaishouIndustryVoucherToTask
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动推进快手电子凭证核销:
|
||||
*
|
||||
* 规则:
|
||||
* 1. 凭证必须已完成发码出站回调(send_callback_status = success),否则快手开放平台不可核销;
|
||||
* 2. 路由到发单平台的工单:如果工单处于待发布大厅/待派单状态(pending_material / unassigned),
|
||||
* 严格跳过,保留在「发布到大厅 / 指派接单员」时由 publish-voucher-service 触发核销;
|
||||
* 3. 其他所有订单(人工派单 manual_dispatch、分销代充 affiliate_dash、已在工单大厅等其他常规任务):
|
||||
* 在任务生成或发码确认后,立即执行出站核销并回写任务上下文,消除买家未消费极速退款窗口。
|
||||
*/
|
||||
export async function autoConsumeKuaishouIndustryVouchersForOrder(
|
||||
oid: string,
|
||||
options: { source?: string } = {},
|
||||
deps: VoucherAutoConsumeDeps = {},
|
||||
): Promise<VoucherAutoConsumeResult> {
|
||||
const normalizedOid = String(oid || '').trim()
|
||||
const source = String(options.source || 'auto_consume').trim() || 'auto_consume'
|
||||
|
||||
const result: VoucherAutoConsumeResult = {
|
||||
oid: normalizedOid,
|
||||
voucherCount: 0,
|
||||
consumedCount: 0,
|
||||
alreadyConsumedCount: 0,
|
||||
skippedCount: 0,
|
||||
failedCount: 0,
|
||||
skippedReasons: [],
|
||||
errors: [],
|
||||
}
|
||||
|
||||
if (!normalizedOid) {
|
||||
result.skippedCount += 1
|
||||
result.skippedReasons.push('missing_oid')
|
||||
return result
|
||||
}
|
||||
|
||||
const listVouchers = deps.listVouchersByOid || listKuaishouIndustryVouchersByOid
|
||||
const findOrder = deps.findOrder || findLatestOrderByPlatformOrderId
|
||||
const listTasks = deps.listTasks || listTasksByOrderId
|
||||
const listWorkOrders = deps.listWorkOrders || listWorkOrdersByPlatformOrderId
|
||||
const consumeVoucher = deps.consumeVoucher || consumeKuaishouIndustryVoucher
|
||||
const attachVoucher = deps.attachVoucher || attachKuaishouIndustryVoucherToTask
|
||||
|
||||
const vouchers = await listVouchers(normalizedOid)
|
||||
result.voucherCount = vouchers.length
|
||||
if (vouchers.length === 0) {
|
||||
result.skippedCount += 1
|
||||
result.skippedReasons.push('voucher_not_found')
|
||||
return result
|
||||
}
|
||||
|
||||
const candidateVouchers: KuaishouIndustryVoucherRow[] = []
|
||||
for (const voucher of vouchers) {
|
||||
const voucherCode = String(voucher.voucher_code || '').trim()
|
||||
const status = normalizeVoucherStatus(voucher.status)
|
||||
|
||||
if (status === 'CONSUMED') {
|
||||
result.alreadyConsumedCount += 1
|
||||
result.skippedCount += 1
|
||||
result.skippedReasons.push(`voucher_${voucherCode}:already_consumed`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (status === 'DESTROYED') {
|
||||
result.skippedCount += 1
|
||||
result.skippedReasons.push(`voucher_${voucherCode}:already_destroyed`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!isKuaishouIndustryVoucherSendCallbackSuccess(voucher)) {
|
||||
result.skippedCount += 1
|
||||
result.skippedReasons.push(`voucher_${voucherCode}:send_callback_unconfirmed`)
|
||||
continue
|
||||
}
|
||||
|
||||
candidateVouchers.push(voucher)
|
||||
}
|
||||
|
||||
if (candidateVouchers.length === 0) {
|
||||
return result
|
||||
}
|
||||
|
||||
const [order, workOrders] = await Promise.all([
|
||||
findOrder({
|
||||
provider: OPEN_91_PROVIDER,
|
||||
platform: OPEN_91_PLATFORM,
|
||||
platformOrderId: normalizedOid,
|
||||
}),
|
||||
listWorkOrders(normalizedOid),
|
||||
])
|
||||
|
||||
const tasks = order ? await listTasks(order.id) : []
|
||||
|
||||
for (const voucher of candidateVouchers) {
|
||||
const voucherCode = String(voucher.voucher_code || '').trim()
|
||||
const matchedTask = resolveTaskForVoucher(voucher, tasks)
|
||||
const matchedWorkOrder = resolveWorkOrderForVoucher(voucher, matchedTask, workOrders)
|
||||
|
||||
// 发单平台工单:待发布到大厅或待补充资料时,不提前核销,等待发布大厅动作
|
||||
if (matchedWorkOrder && isWorkOrderWaitingHallPublish(matchedWorkOrder)) {
|
||||
result.skippedCount += 1
|
||||
result.skippedReasons.push(
|
||||
`voucher_${voucherCode}:waiting_work_order_hall_publish(wo_${matchedWorkOrder.id})`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const consumeRes = await consumeVoucher(voucher, {
|
||||
source,
|
||||
consumeTime: Date.now(),
|
||||
...(matchedTask ? { task: matchedTask } : {}),
|
||||
})
|
||||
|
||||
if (!consumeRes.ok || !consumeRes.voucher) {
|
||||
result.failedCount += 1
|
||||
const errMsg = consumeRes.errorMessage || `电子凭证 ${voucherCode} 核销失败`
|
||||
result.errors.push(errMsg)
|
||||
logWarn('[kuaishou-industry/auto-consume]', `电子凭证 ${voucherCode} 自动核销失败`, {
|
||||
oid: normalizedOid,
|
||||
voucherCode,
|
||||
source,
|
||||
error: errMsg,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
result.consumedCount += 1
|
||||
|
||||
if (matchedTask) {
|
||||
await attachVoucher(matchedTask, consumeRes.voucher, {
|
||||
source,
|
||||
now: new Date().toISOString(),
|
||||
}).catch((err) => {
|
||||
logWarn('[kuaishou-industry/auto-consume]', '核销后回写任务上下文异常', {
|
||||
oid: normalizedOid,
|
||||
voucherCode,
|
||||
taskId: matchedTask.id,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
logIntegration('[kuaishou-industry/auto-consume]', `电子凭证 ${voucherCode} 自动核销成功`, {
|
||||
oid: normalizedOid,
|
||||
voucherCode,
|
||||
source,
|
||||
taskId: matchedTask?.id || null,
|
||||
})
|
||||
} catch (error) {
|
||||
result.failedCount += 1
|
||||
const errMsg = error instanceof Error ? error.message : String(error)
|
||||
result.errors.push(errMsg)
|
||||
logWarn('[kuaishou-industry/auto-consume]', `电子凭证 ${voucherCode} 自动核销异常`, {
|
||||
oid: normalizedOid,
|
||||
voucherCode,
|
||||
source,
|
||||
error: errMsg,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 针对指定履约任务执行电子凭证自动核销。
|
||||
*/
|
||||
export async function autoConsumeKuaishouIndustryVouchersForTask(
|
||||
task: TaskRow,
|
||||
options: { source?: string } = {},
|
||||
deps: VoucherAutoConsumeDeps = {},
|
||||
): Promise<VoucherAutoConsumeResult> {
|
||||
const oid = String(task.platform_order_id || '').trim()
|
||||
if (!oid) {
|
||||
return {
|
||||
oid: '',
|
||||
voucherCount: 0,
|
||||
consumedCount: 0,
|
||||
alreadyConsumedCount: 0,
|
||||
skippedCount: 1,
|
||||
failedCount: 0,
|
||||
skippedReasons: ['task_missing_platform_order_id'],
|
||||
errors: [],
|
||||
}
|
||||
}
|
||||
|
||||
return autoConsumeKuaishouIndustryVouchersForOrder(
|
||||
oid,
|
||||
{ source: options.source || 'task_prepared_auto_consume' },
|
||||
deps,
|
||||
)
|
||||
}
|
||||
|
||||
function resolveTaskForVoucher(
|
||||
voucher: KuaishouIndustryVoucherRow,
|
||||
tasks: TaskRow[],
|
||||
): TaskRow | null {
|
||||
const voucherTaskId = Number(voucher.task_id || 0)
|
||||
if (voucherTaskId > 0) {
|
||||
const byId = tasks.find((t) => Number(t.id) === voucherTaskId)
|
||||
if (byId) return byId
|
||||
}
|
||||
|
||||
const unitIndex = Number(voucher.unit_index || 0)
|
||||
if (unitIndex > 0) {
|
||||
const byUnit = tasks.find((t) => Number(t.unit_index || 0) === unitIndex)
|
||||
if (byUnit) return byUnit
|
||||
}
|
||||
|
||||
return tasks[0] || null
|
||||
}
|
||||
|
||||
function resolveWorkOrderForVoucher(
|
||||
voucher: KuaishouIndustryVoucherRow,
|
||||
task: TaskRow | null,
|
||||
workOrders: WorkOrderRow[],
|
||||
): WorkOrderRow | null {
|
||||
if (workOrders.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const voucherTaskId = Number(voucher.task_id || 0)
|
||||
if (voucherTaskId > 0) {
|
||||
const byTaskId = workOrders.find((wo) => Number(wo.task_id || 0) === voucherTaskId)
|
||||
if (byTaskId) return byTaskId
|
||||
}
|
||||
|
||||
if (task) {
|
||||
const taskId = Number(task.id || 0)
|
||||
const byTask = workOrders.find((wo) => Number(wo.task_id || 0) === taskId)
|
||||
if (byTask) return byTask
|
||||
|
||||
const orderItemId = Number(task.order_item_id || 0)
|
||||
if (orderItemId > 0) {
|
||||
const byItem = workOrders.find((wo) => Number(wo.order_item_id || 0) === orderItemId)
|
||||
if (byItem) return byItem
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function isWorkOrderWaitingHallPublish(workOrder: WorkOrderRow): boolean {
|
||||
const status = normalizeWorkOrderStatus(workOrder.status)
|
||||
return status === WORK_ORDER_STATUS.PENDING_MATERIAL || status === WORK_ORDER_STATUS.UNASSIGNED
|
||||
}
|
||||
@@ -497,7 +497,7 @@ function appendConsumeDetail(details: JsonObject[], nextDetail: JsonObject): Jso
|
||||
return [...details, nextDetail]
|
||||
}
|
||||
|
||||
function normalizeVoucherStatus(value: unknown): string {
|
||||
export function normalizeVoucherStatus(value: unknown): string {
|
||||
const normalized = String(value || '')
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
|
||||
@@ -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: '状态',
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user