From 42725159565753a52159f9a4ea79f4ea29bb21f2 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 31 Oct 2026 19:52:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(orders):=20=E8=AE=A2=E5=8D=95=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=E4=B8=8E=E7=AD=9B=E9=80=89=E5=8F=AA=E8=AE=A4=E6=B8=A0?= =?UTF-8?q?=E9=81=93/=E7=8A=B6=E6=80=81=E4=B8=89=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 展示与筛选全部改读 fulfillment_channel/status,不再回退旧列 - 支持 third_party_voucher 与 archived,legacy_platform 映射为归档 - 前端类型、筛选项与标签同步 --- .../src/repositories/order-repo.test.ts | 21 +++++++-- apps/backend/src/repositories/order-repo.ts | 30 +++++++----- .../admin/admin-order-read-helpers.test.ts | 46 +++++++++++++++++-- .../admin/admin-order-read-helpers.ts | 29 +++++++++--- .../src/services/admin/admin-read-service.ts | 7 ++- apps/backend/src/types/admin/read-models.ts | 2 + apps/backend/src/types/repository/rows.ts | 5 +- apps/frontend/src/types/admin/orders.ts | 2 + apps/frontend/src/utils/admin-display.ts | 8 +++- apps/frontend/src/utils/admin-options.ts | 3 +- 10 files changed, 122 insertions(+), 31 deletions(-) diff --git a/apps/backend/src/repositories/order-repo.test.ts b/apps/backend/src/repositories/order-repo.test.ts index 27e94cbe..5d210642 100644 --- a/apps/backend/src/repositories/order-repo.test.ts +++ b/apps/backend/src/repositories/order-repo.test.ts @@ -107,8 +107,8 @@ test('订单列表按当前履约模式筛选,不再依赖任务执行器', as fulfillmentMode: 'manual', }) - assert.match(calls[0]?.sql || '', /oi\.fulfillment_mode = \$1/) - assert.match(calls[0]?.sql || '', /oi\.fulfillment_mode <> \$1/) + assert.match(calls[0]?.sql || '', /oi\.fulfillment_channel = \$1/) + assert.match(calls[0]?.sql || '', /oi\.fulfillment_channel IS DISTINCT FROM \$1/) assert.doesNotMatch(calls[0]?.sql || '', /COUNT\(DISTINCT/) assert.doesNotMatch(calls[0]?.sql || '', /ft\.executor_key/) assert.deepEqual(calls[0]?.params, ['manual']) @@ -121,12 +121,23 @@ test('订单列表待判定筛选不扫描任务执行器', async () => { fulfillmentMode: 'pending', }) - assert.match(calls[0]?.sql || '', /NOT EXISTS/) - assert.match(calls[0]?.sql || '', /oi\.fulfillment_mode IN/) + assert.match(calls[0]?.sql || '', /EXISTS/) + assert.match(calls[0]?.sql || '', /oi\.fulfillment_status = 'pending'/) assert.doesNotMatch(calls[0]?.sql || '', /COUNT\(DISTINCT/) assert.deepEqual(calls[0]?.params, []) }) +test('订单列表历史平台筛选映射为 archived', async () => { + const calls: Array<{ sql: string; params: unknown[] }> = [] + + await listOrdersWithExecutor(createExecutor(calls, 0), { + fulfillmentMode: 'legacy_platform', + }) + + assert.match(calls[0]?.sql || '', /oi\.fulfillment_status = 'archived'/) + assert.deepEqual(calls[0]?.params, []) +}) + test('订单列表混合履约筛选按订单项聚合判定', async () => { const calls: Array<{ sql: string; params: unknown[] }> = [] @@ -134,7 +145,7 @@ test('订单列表混合履约筛选按订单项聚合判定', async () => { fulfillmentMode: 'mixed', }) - assert.match(calls[0]?.sql || '', /HAVING COUNT\(DISTINCT oi\.fulfillment_mode\) > 1/) + assert.match(calls[0]?.sql || '', /HAVING COUNT\(DISTINCT oi\.fulfillment_channel\) > 1/) assert.deepEqual(calls[0]?.params, []) }) diff --git a/apps/backend/src/repositories/order-repo.ts b/apps/backend/src/repositories/order-repo.ts index 90b445ea..8a3b98eb 100644 --- a/apps/backend/src/repositories/order-repo.ts +++ b/apps/backend/src/repositories/order-repo.ts @@ -335,32 +335,40 @@ export async function listOrdersWithExecutor( } if (fulfillmentMode) { - const knownModes = `('work_order', 'affiliate_dash', 'legacy_platform', 'manual')` - if (fulfillmentMode === 'pending') { - filters.push(`NOT EXISTS ( + // v2:渠道/状态拆列后按新列筛选;legacy_platform 统一映射成 archived。 + const normalizedMode = fulfillmentMode === 'legacy_platform' ? 'archived' : fulfillmentMode + if (normalizedMode === 'pending') { + filters.push(`EXISTS ( SELECT 1 FROM order_items oi - WHERE oi.order_id = o.id AND oi.fulfillment_mode IN ${knownModes} + WHERE oi.order_id = o.id AND oi.fulfillment_status = 'pending' )`) - } else if (fulfillmentMode === 'mixed') { + } else if (normalizedMode === 'archived') { + filters.push(`EXISTS ( + SELECT 1 FROM order_items oi + WHERE oi.order_id = o.id AND oi.fulfillment_status = 'archived' + )`) + } else if (normalizedMode === 'mixed') { filters.push(`o.id IN ( SELECT oi.order_id FROM order_items oi - WHERE oi.fulfillment_mode IN ${knownModes} + WHERE oi.fulfillment_status = 'assigned' GROUP BY oi.order_id - HAVING COUNT(DISTINCT oi.fulfillment_mode) > 1 + HAVING COUNT(DISTINCT oi.fulfillment_channel) > 1 )`) } else { - params.push(fulfillmentMode) + params.push(normalizedMode) const modeParam = `$${params.length}` filters.push(`( EXISTS ( SELECT 1 FROM order_items oi - WHERE oi.order_id = o.id AND oi.fulfillment_mode = ${modeParam} + WHERE oi.order_id = o.id + AND oi.fulfillment_status = 'assigned' + AND oi.fulfillment_channel = ${modeParam} ) AND NOT EXISTS ( SELECT 1 FROM order_items oi WHERE oi.order_id = o.id - AND oi.fulfillment_mode IN ${knownModes} - AND oi.fulfillment_mode <> ${modeParam} + AND oi.fulfillment_status = 'assigned' + AND oi.fulfillment_channel IS DISTINCT FROM ${modeParam} ) )`) } diff --git a/apps/backend/src/services/admin/admin-order-read-helpers.test.ts b/apps/backend/src/services/admin/admin-order-read-helpers.test.ts index 79479996..6d4e9537 100644 --- a/apps/backend/src/services/admin/admin-order-read-helpers.test.ts +++ b/apps/backend/src/services/admin/admin-order-read-helpers.test.ts @@ -292,10 +292,16 @@ function buildOrderForFulfillmentTest(): OrderListRow { } } +const CHANNEL_VALUES = new Set(['work_order', 'affiliate_dash', 'third_party_voucher', 'manual']) + function buildOrderItemForFulfillmentTest( id: number, fulfillmentMode: string | null, + patch: Partial = {}, ): OrderItemRow { + const channel = fulfillmentMode && CHANNEL_VALUES.has(fulfillmentMode) ? fulfillmentMode : null + const status = + fulfillmentMode === 'legacy_platform' ? 'archived' : channel ? 'assigned' : 'pending' return { id, order_id: 501, @@ -303,7 +309,9 @@ function buildOrderItemForFulfillmentTest( sku_name: `商品-${id}`, quantity: 1, spec_json: { itemTitle: `商品-${id}`, skuNick: `规格-${id}` }, - fulfillment_mode: fulfillmentMode, + fulfillment_channel: channel, + fulfillment_status: status, + ...patch, } } @@ -321,7 +329,7 @@ test('人工单即使有已核销快照也仍然判为 manual,核销码单独 assert.equal(mapped.canClaimH5, false) }) -test('自动发货与历史平台按 fulfillment_mode 区分,不再按 provider 猜', () => { +test('自动发货与历史归档按渠道/状态区分,不再按 provider 猜', () => { const affiliate = resolveOrderFulfillmentSummary({ items: [buildOrderItemForFulfillmentTest(1, 'affiliate_dash')], }) @@ -331,7 +339,7 @@ test('自动发货与历史平台按 fulfillment_mode 区分,不再按 provide assert.equal(affiliate.fulfillmentMode, 'affiliate_dash') assert.equal(affiliate.canClaimH5, true) - assert.equal(legacy.fulfillmentMode, 'legacy_platform') + assert.equal(legacy.fulfillmentMode, 'archived') assert.equal(legacy.canClaimH5, false) }) @@ -358,3 +366,35 @@ test('核销码状态汇总:无、单一、混合', () => { { hasVoucher: true, voucherState: 'mixed' }, ) }) + +test('v2 读取口径:归档状态优先于旧 fulfillment_mode', () => { + const summary = resolveOrderFulfillmentSummary({ + items: [buildOrderItemForFulfillmentTest(1, 'work_order', { fulfillment_status: 'archived' })], + }) + assert.equal(summary.fulfillmentMode, 'archived') +}) + +test('v2 读取口径:渠道列优先于旧 fulfillment_mode', () => { + const summary = resolveOrderFulfillmentSummary({ + items: [ + buildOrderItemForFulfillmentTest(1, 'manual', { + fulfillment_status: 'assigned', + fulfillment_channel: 'third_party_voucher', + }), + ], + }) + assert.equal(summary.fulfillmentMode, 'third_party_voucher') +}) + +test('v2 读取口径:状态 pending 且无渠道时判为待分类', () => { + const summary = resolveOrderFulfillmentSummary({ + items: [ + buildOrderItemForFulfillmentTest(1, 'manual', { + fulfillment_status: 'pending', + fulfillment_channel: null, + }), + ], + }) + assert.equal(summary.fulfillmentMode, 'pending') + assert.equal(summary.pendingItemCount, 1) +}) diff --git a/apps/backend/src/services/admin/admin-order-read-helpers.ts b/apps/backend/src/services/admin/admin-order-read-helpers.ts index 233971e1..68f00aad 100644 --- a/apps/backend/src/services/admin/admin-order-read-helpers.ts +++ b/apps/backend/src/services/admin/admin-order-read-helpers.ts @@ -56,7 +56,7 @@ export function mapAdminOrderListItem( skuNick: itSkuNick, matchSku: itMatchSku, quantity: Math.max(1, Number(orderItem.quantity || 1)), - fulfillmentMode: normalizeOrderItemFulfillmentMode(orderItem.fulfillment_mode), + fulfillmentMode: resolveOrderItemFulfillmentMode(orderItem), } }) resolvedItemTitle = itemsList[0]?.itemTitle || '' @@ -141,9 +141,11 @@ export function mapAdminOrderListItem( const ORDER_ITEM_FULFILLMENT_MODES = new Set([ 'work_order', 'affiliate_dash', + 'third_party_voucher', 'legacy_platform', 'manual', 'pending', + 'archived', ]) export function normalizeOrderItemFulfillmentMode(value: unknown): AdminOrderItemFulfillmentMode { @@ -153,6 +155,23 @@ export function normalizeOrderItemFulfillmentMode(value: unknown): AdminOrderIte return ORDER_ITEM_FULFILLMENT_MODES.has(mode) ? mode : 'pending' } +/** + * v2 读取口径:只认渠道/状态三列,不再回退旧 fulfillment_mode。 + * + * archived → archived + * channel → 该渠道 + * 其余 → pending + */ +export function resolveOrderItemFulfillmentMode( + item: Pick | null | undefined, +): AdminOrderItemFulfillmentMode { + if (!item) return 'pending' + if (String(item.fulfillment_status || '').trim() === 'archived') return 'archived' + const channel = String(item.fulfillment_channel || '').trim() + if (channel) return normalizeOrderItemFulfillmentMode(channel) + return 'pending' +} + export function resolveVoucherSummary(vouchers: KuaishouIndustryVoucherRow[] = []): { hasVoucher: boolean voucherState: AdminVoucherState @@ -182,7 +201,7 @@ export function resolveVoucherSummary(vouchers: KuaishouIndustryVoucherRow[] = [ } /** - * 订单级履约摘要:只读 order_items.fulfillment_mode,不再按 provider 或核销码推断。 + * 订单级履约摘要:只读 order_items.fulfillment_channel / fulfillment_status。 * 多商品订单出现不同模式时显示 mixed;尚未判定的订单项单独计数。 */ export function resolveOrderFulfillmentSummary({ @@ -205,9 +224,7 @@ export function resolveOrderFulfillmentSummary({ canClaimH5: boolean } { const normalizedItems = Array.isArray(items) ? items : [] - const itemModes = normalizedItems.map((item) => - normalizeOrderItemFulfillmentMode(item.fulfillment_mode), - ) + const itemModes = normalizedItems.map((item) => resolveOrderItemFulfillmentMode(item)) const knownModes = Array.from( new Set( itemModes.filter((mode): mode is Exclude => mode !== 'pending'), @@ -232,7 +249,7 @@ export function resolveOrderFulfillmentSummary({ task.executor_key === 'kuaishou_ct_assisted' || task.executor_key === 'kuaishou_feifei', ) ) { - fulfillmentMode = 'legacy_platform' + fulfillmentMode = 'archived' } else { fulfillmentMode = 'pending' } diff --git a/apps/backend/src/services/admin/admin-read-service.ts b/apps/backend/src/services/admin/admin-read-service.ts index 5e2da147..b35ac591 100644 --- a/apps/backend/src/services/admin/admin-read-service.ts +++ b/apps/backend/src/services/admin/admin-read-service.ts @@ -67,6 +67,7 @@ import { mapAdminOrderListItem, normalizeOrderItemFulfillmentMode, resolveOrderFulfillmentSummary, + resolveOrderItemFulfillmentMode, summarizeOrderItems, } from './admin-order-read-helpers.js' import { @@ -149,10 +150,12 @@ export async function getAdminOrders( const ADMIN_ORDER_FULFILLMENT_MODES = new Set([ 'work_order', 'affiliate_dash', - 'legacy_platform', + 'third_party_voucher', 'manual', + 'legacy_platform', 'mixed', 'pending', + 'archived', ]) function normalizeOrderFulfillmentModeFilter(value: unknown): string { @@ -278,7 +281,7 @@ export async function getAdminOrderDetail( revenueFen, refundedFen, refundableFen: Math.max(0, revenueFen - refundedFen), - fulfillmentMode: normalizeOrderItemFulfillmentMode(item.fulfillment_mode), + fulfillmentMode: resolveOrderItemFulfillmentMode(item), deliveryMode: resolveOrderItemDeliveryMode(tasks, item.id), spec: item.spec_json && typeof item.spec_json === 'object' diff --git a/apps/backend/src/types/admin/read-models.ts b/apps/backend/src/types/admin/read-models.ts index 4bd01e3a..e2b8d275 100644 --- a/apps/backend/src/types/admin/read-models.ts +++ b/apps/backend/src/types/admin/read-models.ts @@ -7,6 +7,8 @@ export type AdminOrderFulfillmentProgress = { } export type AdminOrderItemFulfillmentMode = + | 'third_party_voucher' + | 'archived' | 'work_order' | 'affiliate_dash' | 'legacy_platform' diff --git a/apps/backend/src/types/repository/rows.ts b/apps/backend/src/types/repository/rows.ts index 770e458e..afdefdca 100644 --- a/apps/backend/src/types/repository/rows.ts +++ b/apps/backend/src/types/repository/rows.ts @@ -35,7 +35,10 @@ export type OrderItemRow = { canonical_product_id?: number | string | null spec_json: string | Record item_snapshot_json?: string | Record - fulfillment_mode?: string | null + fulfillment_channel?: string | null + fulfillment_status?: string | null + fulfillment_source?: string | null + fulfillment_locked?: boolean | null fulfillment_decided_at?: string | null fulfillment_reason?: string | null created_at?: string diff --git a/apps/frontend/src/types/admin/orders.ts b/apps/frontend/src/types/admin/orders.ts index 09ffcab1..5a679d89 100644 --- a/apps/frontend/src/types/admin/orders.ts +++ b/apps/frontend/src/types/admin/orders.ts @@ -11,9 +11,11 @@ export interface AdminOrderFulfillmentProgress { export type AdminOrderItemFulfillmentMode = | 'work_order' | 'affiliate_dash' + | 'third_party_voucher' | 'legacy_platform' | 'manual' | 'pending' + | 'archived' export type AdminOrderFulfillmentMode = AdminOrderItemFulfillmentMode | 'mixed' diff --git a/apps/frontend/src/utils/admin-display.ts b/apps/frontend/src/utils/admin-display.ts index acd913b8..8b43a608 100644 --- a/apps/frontend/src/utils/admin-display.ts +++ b/apps/frontend/src/utils/admin-display.ts @@ -222,9 +222,11 @@ export function resolveOrderFulfillmentMode(row: { fulfillmentMode?: | 'work_order' | 'affiliate_dash' + | 'third_party_voucher' | 'legacy_platform' | 'manual' | 'pending' + | 'archived' | 'mixed' }): { label: string; color: string } { switch (row.fulfillmentMode) { @@ -232,8 +234,10 @@ export function resolveOrderFulfillmentMode(row: { return { label: '⚡ 自动发货', color: 'blue' } case 'work_order': return { label: '👥 接单平台', color: 'purple' } - case 'legacy_platform': - return { label: '🗄 历史平台', color: 'default' } + case 'third_party_voucher': + return { label: '🎟 第三方卡券', color: 'cyan' } + case 'archived': + return { label: '🗄 历史归档', color: 'default' } case 'manual': return { label: '🙋 人工履约', color: 'orange' } case 'mixed': diff --git a/apps/frontend/src/utils/admin-options.ts b/apps/frontend/src/utils/admin-options.ts index fd6f2013..5d0cf19d 100644 --- a/apps/frontend/src/utils/admin-options.ts +++ b/apps/frontend/src/utils/admin-options.ts @@ -32,10 +32,11 @@ export const adminOrderExecutorOptions = [ export const adminOrderFulfillmentModeOptions = [ { label: '自动发货', value: 'affiliate_dash' }, { label: '接单平台', value: 'work_order' }, - { label: '历史平台', value: 'legacy_platform' }, + { label: '第三方卡券', value: 'third_party_voucher' }, { label: '人工履约', value: 'manual' }, { label: '混合履约', value: 'mixed' }, { label: '待判定', value: 'pending' }, + { label: '历史归档', value: 'archived' }, ] export const adminTaskStatusOptions = [