diff --git a/apps/backend/src/db/migrations/114_rename_facts_channel.sql b/apps/backend/src/db/migrations/114_rename_facts_channel.sql new file mode 100644 index 00000000..ae22f9ab --- /dev/null +++ b/apps/backend/src/db/migrations/114_rename_facts_channel.sql @@ -0,0 +1,14 @@ +-- 114_rename_facts_channel.sql +-- +-- 命名统一:销售事实的维度列由 fulfillment_mode 改为 channel。 +-- 值本身在 112 之后已经是渠道(含 unclassified),旧名字只会误导。 +-- 唯一约束与索引会自动跟随列重命名,这里只额外修正索引名。 + +ALTER TABLE sales_daily_facts + RENAME COLUMN fulfillment_mode TO channel; + +ALTER INDEX IF EXISTS idx_sales_daily_facts_mode + RENAME TO idx_sales_daily_facts_channel; + +COMMENT ON COLUMN sales_daily_facts.channel IS + '渠道维度:work_order / affiliate_dash / third_party_voucher / manual / unclassified'; diff --git a/apps/backend/src/repositories/order-repo.test.ts b/apps/backend/src/repositories/order-repo.test.ts index 5d210642..686b3de1 100644 --- a/apps/backend/src/repositories/order-repo.test.ts +++ b/apps/backend/src/repositories/order-repo.test.ts @@ -104,7 +104,7 @@ test('订单列表按当前履约模式筛选,不再依赖任务执行器', as const calls: Array<{ sql: string; params: unknown[] }> = [] await listOrdersWithExecutor(createExecutor(calls, 0), { - fulfillmentMode: 'manual', + channel: 'manual', }) assert.match(calls[0]?.sql || '', /oi\.fulfillment_channel = \$1/) @@ -118,7 +118,7 @@ test('订单列表待判定筛选不扫描任务执行器', async () => { const calls: Array<{ sql: string; params: unknown[] }> = [] await listOrdersWithExecutor(createExecutor(calls, 0), { - fulfillmentMode: 'pending', + channel: 'pending', }) assert.match(calls[0]?.sql || '', /EXISTS/) @@ -131,7 +131,7 @@ test('订单列表历史平台筛选映射为 archived', async () => { const calls: Array<{ sql: string; params: unknown[] }> = [] await listOrdersWithExecutor(createExecutor(calls, 0), { - fulfillmentMode: 'legacy_platform', + channel: 'legacy_platform', }) assert.match(calls[0]?.sql || '', /oi\.fulfillment_status = 'archived'/) @@ -142,7 +142,7 @@ test('订单列表混合履约筛选按订单项聚合判定', async () => { const calls: Array<{ sql: string; params: unknown[] }> = [] await listOrdersWithExecutor(createExecutor(calls, 0), { - fulfillmentMode: 'mixed', + channel: 'mixed', }) assert.match(calls[0]?.sql || '', /HAVING COUNT\(DISTINCT oi\.fulfillment_channel\) > 1/) diff --git a/apps/backend/src/repositories/order-repo.ts b/apps/backend/src/repositories/order-repo.ts index 8a3b98eb..3431ddad 100644 --- a/apps/backend/src/repositories/order-repo.ts +++ b/apps/backend/src/repositories/order-repo.ts @@ -240,7 +240,7 @@ export async function listOrdersWithExecutor( keyword = '', provider = '', executorKey = '', - fulfillmentMode = '', + channel = '', platformOrderId = '', payStatus = '', taskStatus = '', @@ -334,9 +334,9 @@ export async function listOrdersWithExecutor( ) } - if (fulfillmentMode) { + if (channel) { // v2:渠道/状态拆列后按新列筛选;legacy_platform 统一映射成 archived。 - const normalizedMode = fulfillmentMode === 'legacy_platform' ? 'archived' : fulfillmentMode + const normalizedMode = channel === 'legacy_platform' ? 'archived' : channel if (normalizedMode === 'pending') { filters.push(`EXISTS ( SELECT 1 FROM order_items oi 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 6d4e9537..08413019 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 @@ -71,7 +71,7 @@ test('mapAdminOrderListItem formats itemTitle, skuNick, and summary correctly fo skuNick: '安卓血宴2钻', matchSku: '安卓血宴2钻', quantity: 1, - fulfillmentMode: 'pending', + channel: 'pending', }, ]) }) @@ -296,12 +296,11 @@ const CHANNEL_VALUES = new Set(['work_order', 'affiliate_dash', 'third_party_vou function buildOrderItemForFulfillmentTest( id: number, - fulfillmentMode: string | null, + value: string | null, patch: Partial = {}, ): OrderItemRow { - const channel = fulfillmentMode && CHANNEL_VALUES.has(fulfillmentMode) ? fulfillmentMode : null - const status = - fulfillmentMode === 'legacy_platform' ? 'archived' : channel ? 'assigned' : 'pending' + const channel = value && CHANNEL_VALUES.has(value) ? value : null + const status = value === 'legacy_platform' ? 'archived' : channel ? 'assigned' : 'pending' return { id, order_id: 501, @@ -322,7 +321,7 @@ test('人工单即使有已核销快照也仍然判为 manual,核销码单独 vouchers, }) - assert.equal(mapped.fulfillmentMode, 'manual') + assert.equal(mapped.channel, 'manual') assert.equal(mapped.manualItemCount, 1) assert.equal(mapped.hasVoucher, true) assert.equal(mapped.voucherState, 'consumed') @@ -337,9 +336,9 @@ test('自动发货与历史归档按渠道/状态区分,不再按 provider 猜 items: [buildOrderItemForFulfillmentTest(2, 'legacy_platform')], }) - assert.equal(affiliate.fulfillmentMode, 'affiliate_dash') + assert.equal(affiliate.channel, 'affiliate_dash') assert.equal(affiliate.canClaimH5, true) - assert.equal(legacy.fulfillmentMode, 'archived') + assert.equal(legacy.channel, 'archived') assert.equal(legacy.canClaimH5, false) }) @@ -351,7 +350,7 @@ test('多商品订单出现不同履约方式时显示 mixed,并保留人工 ], }) - assert.equal(summary.fulfillmentMode, 'mixed') + assert.equal(summary.channel, 'mixed') assert.equal(summary.manualItemCount, 1) assert.equal(summary.pendingItemCount, 0) }) @@ -367,14 +366,14 @@ test('核销码状态汇总:无、单一、混合', () => { ) }) -test('v2 读取口径:归档状态优先于旧 fulfillment_mode', () => { +test('v2 读取口径:归档状态优先于旧 channel', () => { const summary = resolveOrderFulfillmentSummary({ items: [buildOrderItemForFulfillmentTest(1, 'work_order', { fulfillment_status: 'archived' })], }) - assert.equal(summary.fulfillmentMode, 'archived') + assert.equal(summary.channel, 'archived') }) -test('v2 读取口径:渠道列优先于旧 fulfillment_mode', () => { +test('v2 读取口径:渠道列优先于旧 channel', () => { const summary = resolveOrderFulfillmentSummary({ items: [ buildOrderItemForFulfillmentTest(1, 'manual', { @@ -383,7 +382,7 @@ test('v2 读取口径:渠道列优先于旧 fulfillment_mode', () => { }), ], }) - assert.equal(summary.fulfillmentMode, 'third_party_voucher') + assert.equal(summary.channel, 'third_party_voucher') }) test('v2 读取口径:状态 pending 且无渠道时判为待分类', () => { @@ -395,6 +394,6 @@ test('v2 读取口径:状态 pending 且无渠道时判为待分类', () => { }), ], }) - assert.equal(summary.fulfillmentMode, 'pending') + assert.equal(summary.channel, '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 68f00aad..b2b48839 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: resolveOrderItemFulfillmentMode(orderItem), + channel: resolveOrderItemFulfillmentMode(orderItem), } }) resolvedItemTitle = itemsList[0]?.itemTitle || '' @@ -73,7 +73,7 @@ export function mapAdminOrderListItem( skuNick: resolvedSkuNick, matchSku: resolvedMatchSku, quantity: 1, - fulfillmentMode: 'pending', + channel: 'pending', }, ] } @@ -120,7 +120,7 @@ export function mapAdminOrderListItem( items: itemsList, taskCount: tasks.length, workOrderCount, - fulfillmentMode: fulfillment.fulfillmentMode, + channel: fulfillment.channel, manualItemCount: fulfillment.manualItemCount, pendingItemCount: fulfillment.pendingItemCount, manualPendingCount: manualConfig.pending, @@ -138,7 +138,7 @@ export function mapAdminOrderListItem( } } -const ORDER_ITEM_FULFILLMENT_MODES = new Set([ +const ORDER_ITEM_FULFILLMENT_CHANNELS = new Set([ 'work_order', 'affiliate_dash', 'third_party_voucher', @@ -152,11 +152,11 @@ export function normalizeOrderItemFulfillmentMode(value: unknown): AdminOrderIte const mode = String(value || '') .trim() .toLowerCase() as AdminOrderItemFulfillmentMode - return ORDER_ITEM_FULFILLMENT_MODES.has(mode) ? mode : 'pending' + return ORDER_ITEM_FULFILLMENT_CHANNELS.has(mode) ? mode : 'pending' } /** - * v2 读取口径:只认渠道/状态三列,不再回退旧 fulfillment_mode。 + * v2 读取口径:只认渠道/状态三列,不再回退旧 channel。 * * archived → archived * channel → 该渠道 @@ -215,7 +215,7 @@ export function resolveOrderFulfillmentSummary({ workOrderCount?: number vouchers?: KuaishouIndustryVoucherRow[] }): { - fulfillmentMode: AdminOrderFulfillmentMode + channel: AdminOrderFulfillmentMode manualItemCount: number pendingItemCount: number affiliateItemCount: number @@ -231,36 +231,36 @@ export function resolveOrderFulfillmentSummary({ ), ) - let fulfillmentMode: AdminOrderFulfillmentMode + let channel: AdminOrderFulfillmentMode if (knownModes.length === 1) { - fulfillmentMode = knownModes[0]! + channel = knownModes[0]! } else if (knownModes.length > 1) { - fulfillmentMode = 'mixed' + channel = 'mixed' } else if (normalizedItems.length > 0) { - fulfillmentMode = 'pending' + channel = 'pending' } else if (workOrderCount > 0) { // 极少数历史订单没有 order_items,只能用载体表兜底;这不是按 provider 猜。 - fulfillmentMode = 'work_order' + channel = 'work_order' } else if (tasks.some((task) => task.executor_key === 'affiliate_dash')) { - fulfillmentMode = 'affiliate_dash' + channel = 'affiliate_dash' } else if ( tasks.some( (task) => task.executor_key === 'kuaishou_ct_assisted' || task.executor_key === 'kuaishou_feifei', ) ) { - fulfillmentMode = 'archived' + channel = 'archived' } else { - fulfillmentMode = 'pending' + channel = 'pending' } const affiliateItemCount = itemModes.filter((mode) => mode === 'affiliate_dash').length const voucher = resolveVoucherSummary(vouchers) const canClaimH5 = - affiliateItemCount > 0 || (normalizedItems.length === 0 && fulfillmentMode === 'affiliate_dash') + affiliateItemCount > 0 || (normalizedItems.length === 0 && channel === 'affiliate_dash') return { - fulfillmentMode, + channel, manualItemCount: itemModes.filter((mode) => mode === 'manual').length, pendingItemCount: itemModes.filter((mode) => mode === 'pending').length, affiliateItemCount, diff --git a/apps/backend/src/services/admin/admin-read-service.ts b/apps/backend/src/services/admin/admin-read-service.ts index b35ac591..8e51c719 100644 --- a/apps/backend/src/services/admin/admin-read-service.ts +++ b/apps/backend/src/services/admin/admin-read-service.ts @@ -106,7 +106,7 @@ export async function getAdminOrders( keyword: String(query.keyword || '').trim(), provider: String(query.provider || '').trim(), executorKey: String(query.executorKey || '').trim(), - fulfillmentMode: normalizeOrderFulfillmentModeFilter(query.fulfillmentMode), + channel: normalizeOrderFulfillmentModeFilter(query.channel), platformOrderId: String(query.platformOrderId || '').trim(), payStatus: String(query.payStatus || '').trim(), taskStatus: String(query.taskStatus || '').trim(), @@ -147,7 +147,7 @@ export async function getAdminOrders( } } -const ADMIN_ORDER_FULFILLMENT_MODES = new Set([ +const ADMIN_ORDER_FULFILLMENT_CHANNELS = new Set([ 'work_order', 'affiliate_dash', 'third_party_voucher', @@ -162,7 +162,7 @@ function normalizeOrderFulfillmentModeFilter(value: unknown): string { const mode = String(value || '') .trim() .toLowerCase() - return ADMIN_ORDER_FULFILLMENT_MODES.has(mode) ? mode : '' + return ADMIN_ORDER_FULFILLMENT_CHANNELS.has(mode) ? mode : '' } function groupRowsByOrderId( @@ -255,7 +255,7 @@ export async function getAdminOrderDetail( claimStatus: order.claim_status || 'active', claimStatusReason: order.claim_status_reason || '', claimStatusUpdatedAt: order.claim_status_updated_at || null, - fulfillmentMode: fulfillment.fulfillmentMode, + channel: fulfillment.channel, manualItemCount: fulfillment.manualItemCount, pendingItemCount: fulfillment.pendingItemCount, manualPendingCount: manualConfig.pending, @@ -281,7 +281,7 @@ export async function getAdminOrderDetail( revenueFen, refundedFen, refundableFen: Math.max(0, revenueFen - refundedFen), - fulfillmentMode: resolveOrderItemFulfillmentMode(item), + channel: resolveOrderItemFulfillmentMode(item), deliveryMode: resolveOrderItemDeliveryMode(tasks, item.id), spec: item.spec_json && typeof item.spec_json === 'object' diff --git a/apps/backend/src/services/cost-center/cost-center-constants.ts b/apps/backend/src/services/cost-center/cost-center-constants.ts index 9a947ad7..daca936f 100644 --- a/apps/backend/src/services/cost-center/cost-center-constants.ts +++ b/apps/backend/src/services/cost-center/cost-center-constants.ts @@ -59,13 +59,23 @@ export const COST_MODEL_CODE_BASE: Record = { } /** 履约渠道:唯一真源是 order_items.fulfillment_channel + fulfillment_status。 */ -export type FulfillmentMode = 'work_order' | 'affiliate_dash' | 'legacy_platform' | 'manual' +export type FulfillmentChannel = + | 'work_order' + | 'affiliate_dash' + | 'third_party_voucher' + | 'manual' + | 'pending' + | 'archived' + | 'unclassified' -export const FULFILLMENT_MODES = new Set([ +export const FULFILLMENT_CHANNELS = new Set([ 'work_order', 'affiliate_dash', - 'legacy_platform', + 'third_party_voucher', 'manual', + 'pending', + 'archived', + 'unclassified', ]) /** diff --git a/apps/backend/src/services/cost-center/cost-center-mappers.ts b/apps/backend/src/services/cost-center/cost-center-mappers.ts index 24d0bffe..5fee88fa 100644 --- a/apps/backend/src/services/cost-center/cost-center-mappers.ts +++ b/apps/backend/src/services/cost-center/cost-center-mappers.ts @@ -4,7 +4,7 @@ * 把数据库行映射成接口 DTO,并提供「成本模型是否适用于履约方式」这类口径函数。 */ import { GENERIC_COST_MODELS, PRODUCT_SOURCE_PREFIXES } from './cost-center-constants.js' -import type { FulfillmentMode } from './cost-center-constants.js' +import type { FulfillmentChannel } from './cost-center-constants.js' import { parseJsonObject } from './cost-center-normalizers.js' import type { CostExceptionDraft, @@ -49,12 +49,12 @@ export function productSourceFilterSql(paramIndex: number, column = 'p.canonical * 专属模型只对对应履约方式生效;通用模型(平台费/支付费/售后损失/其他) * 对所有履约方式生效。 */ -export function costModelAppliesTo(costModel: string, fulfillmentMode: string) { +export function costModelAppliesTo(costModel: string, channel: string) { if ((GENERIC_COST_MODELS as readonly string[]).includes(costModel)) return true - if (costModel === 'affiliate_purchase') return fulfillmentMode === 'affiliate_dash' - if (costModel === 'work_order_reward') return fulfillmentMode === 'work_order' - if (costModel === 'third_party_voucher_cost') return fulfillmentMode === 'third_party_voucher' - if (costModel === 'manual_actual') return fulfillmentMode === 'manual' + if (costModel === 'affiliate_purchase') return channel === 'affiliate_dash' + if (costModel === 'work_order_reward') return channel === 'work_order' + if (costModel === 'third_party_voucher_cost') return channel === 'third_party_voucher' + if (costModel === 'manual_actual') return channel === 'manual' return false } @@ -62,17 +62,17 @@ export function costModelAppliesTo(costModel: string, fulfillmentMode: string) { * 成本模型适用范围的 SQL 片段。 * 参数是「履约方式」的 SQL 表达式(可以是占位符 $n,也可以是列引用)。 */ -export function costModelScopeSql(fulfillmentModeExpression: string): string { +export function costModelScopeSql(channelExpression: string): string { return `( ci.cost_model IN (${GENERIC_COST_MODELS.map((model) => `'${model}'`).join(', ')}) OR (ci.cost_model = 'affiliate_purchase' - AND ${fulfillmentModeExpression} = 'affiliate_dash') + AND ${channelExpression} = 'affiliate_dash') OR (ci.cost_model = 'work_order_reward' - AND ${fulfillmentModeExpression} = 'work_order') + AND ${channelExpression} = 'work_order') OR (ci.cost_model = 'third_party_voucher_cost' - AND ${fulfillmentModeExpression} = 'third_party_voucher') + AND ${channelExpression} = 'third_party_voucher') OR (ci.cost_model = 'manual_actual' - AND ${fulfillmentModeExpression} = 'manual') + AND ${channelExpression} = 'manual') )` } @@ -225,7 +225,7 @@ export function mapCostException(row: CostExceptionRow) { subjectId: Number(row.subject_id), orderItemId: row.order_item_id ? Number(row.order_item_id) : null, workOrderId: row.work_order_id ? Number(row.work_order_id) : null, - fulfillmentMode: row.fulfillment_mode || '', + channel: row.channel || '', reason: row.reason, status: row.status, detail: parseJsonObject(row.detail_json), @@ -246,7 +246,7 @@ export type CostPreview = { shopId: string skuCode: string skuName: string - fulfillmentMode: FulfillmentMode + channel: FulfillmentChannel quantity: number normalizedQuantity: number revenueFen: number diff --git a/apps/backend/src/services/cost-center/cost-center-types.ts b/apps/backend/src/services/cost-center/cost-center-types.ts index 95986db8..bb29e52c 100644 --- a/apps/backend/src/services/cost-center/cost-center-types.ts +++ b/apps/backend/src/services/cost-center/cost-center-types.ts @@ -1,7 +1,7 @@ /** * 成本中心共享层 · 行结构与 DTO 类型。 */ -import type { CostModel, FulfillmentMode } from './cost-center-constants.js' +import type { CostModel, FulfillmentChannel } from './cost-center-constants.js' // --------------------------------------------------------------------------- // 行类型 @@ -135,7 +135,7 @@ export type CostExceptionRow = { subject_id: number order_item_id: number | null work_order_id: number | null - fulfillment_mode: string | null + channel: string | null reason: string status: string detail_json: unknown @@ -161,7 +161,7 @@ export type OrderItemCostContext = { basisAt: string workOrderId: number | null workProductRuleId: number | null - fulfillmentMode: FulfillmentMode + channel: FulfillmentChannel } export type CostLine = { diff --git a/apps/backend/src/services/cost-center/cost-definition-service.ts b/apps/backend/src/services/cost-center/cost-definition-service.ts index 0b5c8964..ffbf857a 100644 --- a/apps/backend/src/services/cost-center/cost-definition-service.ts +++ b/apps/backend/src/services/cost-center/cost-definition-service.ts @@ -17,7 +17,7 @@ import { COST_VERSION_SCOPES, COST_VERSION_SOURCES, COST_VERSION_STATUSES, - FULFILLMENT_MODES, + FULFILLMENT_CHANNELS, PRICING_MODES, PRODUCT_STATUSES, assertKnownProvider, @@ -73,10 +73,10 @@ export async function listAdminCostItems(payload: JsonObject = {}) { clauses.push(`ci.status = $${params.length}`) } // 按履约方式筛选:只返回适用于该履约方式的成本模型。 - const fulfillmentMode = text(payload.fulfillmentMode ?? payload.fulfillment_mode) - if (fulfillmentMode) { - assertOneOf(fulfillmentMode, FULFILLMENT_MODES, '履约方式无效') - params.push(fulfillmentMode) + const channel = text(payload.channel ?? payload.channel) + if (channel) { + assertOneOf(channel, FULFILLMENT_CHANNELS, '履约方式无效') + params.push(channel) clauses.push(costModelScopeSql(`$${params.length}`)) } @@ -283,10 +283,10 @@ export async function listAdminCostVersions(payload: JsonObject = {}) { clauses.push(`ci.cost_model = $${params.length}`) } // 按履约方式筛选:只返回适用于该履约方式的成本模型。 - const fulfillmentMode = text(payload.fulfillmentMode ?? payload.fulfillment_mode) - if (fulfillmentMode) { - assertOneOf(fulfillmentMode, FULFILLMENT_MODES, '履约方式无效') - params.push(fulfillmentMode) + const channel = text(payload.channel ?? payload.channel) + if (channel) { + assertOneOf(channel, FULFILLMENT_CHANNELS, '履约方式无效') + params.push(channel) clauses.push(costModelScopeSql(`$${params.length}`)) } const keyword = text(payload.keyword) @@ -570,10 +570,10 @@ export async function listAdminCostExceptions(payload: JsonObject = {}) { // 履约渠道筛选:异常表不存渠道,改为 join order_items 读 channel/status。 // 用 IN 半连接而不是 EXISTS:248k 异常 × 316k 订单项下,EXISTS 会逐行回表, // 半连接能一次物化候选集合(实测 2.7s -> 0.4s)。 - const fulfillmentMode = text(payload.fulfillmentMode ?? payload.fulfillment_mode) - if (fulfillmentMode) { - assertOneOf(fulfillmentMode, FULFILLMENT_MODES, '履约方式无效') - params.push(fulfillmentMode) + const channel = text(payload.channel ?? payload.channel) + if (channel) { + assertOneOf(channel, FULFILLMENT_CHANNELS, '履约方式无效') + params.push(channel) clauses.push( `ce.order_item_id IN ( SELECT oi.id FROM order_items oi WHERE COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) = $${params.length} @@ -591,7 +591,7 @@ export async function listAdminCostExceptions(payload: JsonObject = {}) { ` SELECT ce.*, - COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) AS fulfillment_mode + COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) AS channel FROM cost_exceptions ce LEFT JOIN order_items oi ON oi.id = ce.order_item_id ${where} @@ -629,10 +629,10 @@ export async function summarizeAdminCostExceptions( const params: unknown[] = [] const clauses: string[] = [] - const fulfillmentMode = text(payload.fulfillmentMode ?? payload.fulfillment_mode) - if (fulfillmentMode) { - assertOneOf(fulfillmentMode, FULFILLMENT_MODES, '履约方式无效') - params.push(fulfillmentMode) + const channel = text(payload.channel ?? payload.channel) + if (channel) { + assertOneOf(channel, FULFILLMENT_CHANNELS, '履约方式无效') + params.push(channel) clauses.push( `ce.order_item_id IN ( SELECT oi.id FROM order_items oi WHERE COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) = $${params.length} diff --git a/apps/backend/src/services/cost-center/cost-resolution-service.ts b/apps/backend/src/services/cost-center/cost-resolution-service.ts index a1cb5798..934dfc39 100644 --- a/apps/backend/src/services/cost-center/cost-resolution-service.ts +++ b/apps/backend/src/services/cost-center/cost-resolution-service.ts @@ -14,7 +14,7 @@ import type { AdminSession } from '../admin/admin-auth-service.js' import { normalizeMatchText, normalizeSkuSeriesEquivalence } from '../product-match/normalize.js' import { resolveSkuNameQuantity } from '../product-match/pricing.js' import { - FULFILLMENT_MODES, + FULFILLMENT_CHANNELS, costModelScopeSql, mapCostVersion, mapMapping, @@ -32,7 +32,7 @@ import type { CostPreview, CostResolution, CostVersionRow, - FulfillmentMode, + FulfillmentChannel, MappingRow, MissingCostItem, OrderItemCostContext, @@ -49,7 +49,7 @@ export async function previewAdminCost(payload: JsonObject = {}) { ? await resolveCostLines({ provider: context.provider, shopId: context.shopId, - fulfillmentMode: context.fulfillmentMode, + channel: context.channel, canonicalProductId: productId, workProductRuleId: context.workProductRuleId, quantity: context.normalizedQuantity, @@ -76,22 +76,22 @@ export async function previewAdminCost(payload: JsonObject = {}) { .map((item) => `${item.costItemName}(${item.costItemCode})`) .join('、')}`, detail: { - fulfillmentMode: context.fulfillmentMode, + channel: context.channel, costItemIds: resolution.missingCostItems.map((item) => item.costItemId), }, }) } else if (resolution.applicableCostItemCount === 0) { exceptions.push({ reason: 'cost_missing', - message: `没有配置适用于「${context.fulfillmentMode}」的成本项`, - detail: { fulfillmentMode: context.fulfillmentMode }, + message: `没有配置适用于「${context.channel}」的成本项`, + detail: { channel: context.channel }, }) } // 人工履约的成本来自「人工 SKU 配置的消耗积分 × 平台换算率」,版本金额只是占位; // 这里单独给出提示,并从成本行里剔掉 0 元占位,避免预览与实际扣款对不上。 const manualCost = - context.fulfillmentMode === 'manual' ? await resolveManualCostHint(context.orderItemId) : null + context.channel === 'manual' ? await resolveManualCostHint(context.orderItemId) : null const lines = manualCost ? resolution.lines.filter((line) => line.costItemCode !== 'sys_manual_service') : resolution.lines @@ -233,7 +233,7 @@ export async function createAdminCostSnapshot(payload: JsonObject = {}, _session provider: preview.context.provider, shopId: preview.context.shopId, normalizedQuantity: preview.context.normalizedQuantity, - fulfillmentMode: preview.context.fulfillmentMode, + channel: preview.context.channel, basisAt: preview.context.basisAt, }), preview.context.basisAt, @@ -296,7 +296,7 @@ async function resolveCostContext(payload: JsonObject): Promise( ` SELECT @@ -315,7 +315,7 @@ async function resolveCostContext(payload: JsonObject): Promise>'provider' AS provider, ce.detail_json->>'shopId' AS shop_id, COALESCE(ce.detail_json->>'skuCode', '') AS sku_code, - COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) AS fulfillment_mode, + COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) AS channel, COUNT(*)::int AS item_count FROM cost_exceptions ce JOIN order_items oi ON oi.id = ce.order_item_id @@ -649,9 +649,7 @@ export async function getAdminChannelMappingDiagnostics(payload: JsonObject = {} } } - const modes = fulfillmentMode - ? [fulfillmentMode] - : ['work_order', 'affiliate_dash', 'legacy_platform', 'manual'] + const modes = channel ? [channel] : ['work_order', 'affiliate_dash', 'legacy_platform', 'manual'] // 顺序跑:并行会同时扫 cost_exceptions,实测 4 路并发比串行更慢(I/O 争抢)。 const aggregates: Array<{ groupTotal: number; itemTotal: number }> = [] for (const mode of modes) { @@ -661,7 +659,7 @@ export async function getAdminChannelMappingDiagnostics(payload: JsonObject = {} const unmappedItemTotal = aggregates.reduce((sum, item) => sum + item.itemTotal, 0) return { - fulfillmentMode: fulfillmentMode || null, + channel: channel || null, productTotal: toNumber(counts.rows[0]?.product_total), mappingTotal: toNumber(counts.rows[0]?.mapping_total), mappedProductTotal: toNumber(counts.rows[0]?.mapping_product_total), @@ -682,8 +680,8 @@ export async function listAdminUnmappedMappingGroups(payload: JsonObject = {}) { const params: unknown[] = [] const clauses = [`ce.reason = 'product_mapping_missing'`, `ce.status = 'open'`] - const fulfillmentMode = text(payload.fulfillmentMode ?? payload.fulfillment_mode) - if (fulfillmentMode) assertOneOf(fulfillmentMode, FULFILLMENT_MODES, '履约方式无效') + const channel = text(payload.channel ?? payload.channel) + if (channel) assertOneOf(channel, FULFILLMENT_CHANNELS, '履约方式无效') const provider = text(payload.provider) if (provider) { params.push(normalizeProviderKey(provider)) @@ -701,9 +699,7 @@ export async function listAdminUnmappedMappingGroups(payload: JsonObject = {}) { // 分组总数单独算:SQL 守卫禁止窗口函数分页(会在 LIMIT 前消费全部候选行)。 // 按履约方式拆开统计:命中索引,单模式约 90ms;「全部」时顺序跑四种模式。 - const modes = fulfillmentMode - ? [fulfillmentMode] - : ['work_order', 'affiliate_dash', 'legacy_platform', 'manual'] + const modes = channel ? [channel] : ['work_order', 'affiliate_dash', 'legacy_platform', 'manual'] const totals: Array<{ groupTotal: number; itemTotal: number }> = [] for (const mode of modes) { const totalParams = [...params, mode] @@ -742,10 +738,10 @@ export async function listAdminUnmappedMappingGroups(payload: JsonObject = {}) { } } - const modeIndex = fulfillmentMode ? params.length + 1 : null + const modeIndex = channel ? params.length + 1 : null const pageIndex = params.length + (modeIndex ? 2 : 1) const offsetIndex = pageIndex + 1 - const listParams = [...params, ...(fulfillmentMode ? [fulfillmentMode] : []), pageSize, offset] + const listParams = [...params, ...(channel ? [channel] : []), pageSize, offset] const modeClause = modeIndex ? `AND COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) = $${modeIndex}` : '' @@ -754,7 +750,7 @@ export async function listAdminUnmappedMappingGroups(payload: JsonObject = {}) { shop_id: string sku_code: string sku_name: string - fulfillment_mode: string + channel: string item_count: number last_seen_at: string | null }>( @@ -764,7 +760,7 @@ export async function listAdminUnmappedMappingGroups(payload: JsonObject = {}) { ce.detail_json->>'provider' AS provider, ce.detail_json->>'shopId' AS shop_id, COALESCE(ce.detail_json->>'skuCode', '') AS sku_code, - COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) AS fulfillment_mode, + COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) AS channel, MAX(ce.detail_json->>'skuName') AS sku_name, MAX(NULLIF(ce.detail_json->>'basisAt', '')) AS last_seen_at, COUNT(*)::int AS item_count @@ -788,7 +784,7 @@ export async function listAdminUnmappedMappingGroups(payload: JsonObject = {}) { shopId: row.shop_id || '', skuCode: row.sku_code || '', skuName: row.sku_name || '', - fulfillmentMode: row.fulfillment_mode || '', + channel: row.channel || '', itemCount: toNumber(row.item_count), lastSeenAt: row.last_seen_at || null, })), diff --git a/apps/backend/src/services/finance/manual-fulfillment-service.ts b/apps/backend/src/services/finance/manual-fulfillment-service.ts index 2087ef75..9586525d 100644 --- a/apps/backend/src/services/finance/manual-fulfillment-service.ts +++ b/apps/backend/src/services/finance/manual-fulfillment-service.ts @@ -69,7 +69,7 @@ type ConfirmContextRow = { revenue_fen: string basis_at: string canonical_product_id: number | null - fulfillment_mode: string + channel: string config_id: number | null platform_key: string credit_quantity: string @@ -225,7 +225,7 @@ export async function confirmManualFulfillment(payload: JsonObject, session: Adm COALESCE(oi.revenue_amount_fen, 0)::bigint AS revenue_fen, COALESCE(o.paid_at, o.created_at) AS basis_at, oi.canonical_product_id, - COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) AS fulfillment_mode, + COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) AS channel, oi.fulfillment_config_id AS config_id, COALESCE(cfg.platform_key, '') AS platform_key, COALESCE(cfg.credit_quantity, 0)::bigint AS credit_quantity, @@ -256,7 +256,7 @@ export async function confirmManualFulfillment(payload: JsonObject, session: Adm errorCode: 'manual_fulfillment_order_item_not_found', }) } - if (row.fulfillment_mode !== 'manual') { + if (row.channel !== 'manual') { throw createHttpError('该订单项不是人工履约', { statusCode: 409, errorCode: 'manual_fulfillment_not_manual', diff --git a/apps/backend/src/services/finance/manual-fulfillment.integration.test.ts b/apps/backend/src/services/finance/manual-fulfillment.integration.test.ts index 9223ea8a..7b244d14 100644 --- a/apps/backend/src/services/finance/manual-fulfillment.integration.test.ts +++ b/apps/backend/src/services/finance/manual-fulfillment.integration.test.ts @@ -544,7 +544,7 @@ async function createManualItem( const orderId = Number(order.rows[0]?.id) const item = await query( `INSERT INTO order_items ( - order_id, sku_code, sku_name, fulfillment_mode, spec_json, + order_id, sku_code, sku_name, channel, spec_json, revenue_amount_fen, revenue_basis, created_at, updated_at ) VALUES ($1, $2, $3, 'manual', $4::jsonb, 5000, 'order_total', NOW(), NOW()) RETURNING id`, diff --git a/apps/backend/src/services/finance/sales-facts-channel-service.ts b/apps/backend/src/services/finance/sales-facts-channel-service.ts index 18abecff..ff14e2f1 100644 --- a/apps/backend/src/services/finance/sales-facts-channel-service.ts +++ b/apps/backend/src/services/finance/sales-facts-channel-service.ts @@ -10,7 +10,7 @@ import { nowIso } from '../../utils/time.js' * 2. 归档订单(archived)不进事实; * 3. 成本来自 `cost_snapshots`,缺失就记 0 并计数,不把 0 当真实成本。 * - * 事实表暂时沿用 `fulfillment_mode` 列承载渠道值(113 再改列名)。 + * 事实表暂时沿用 `channel` 列承载渠道值(113 再改列名)。 */ export type ChannelFactsResult = { @@ -107,7 +107,7 @@ export async function rebuildChannelSalesDailyFacts(input: { ) c ON c.order_item_id = s.order_item_id ) INSERT INTO sales_daily_facts ( - stat_date, provider, shop_id, canonical_product_id, fulfillment_mode, + stat_date, provider, shop_id, canonical_product_id, channel, order_count, item_count, quantity, gross_revenue_fen, refund_amount_fen, net_revenue_fen, cost_amount_fen, cost_missing_count, cost_stage, diff --git a/apps/backend/src/services/finance/sales-facts-service.ts b/apps/backend/src/services/finance/sales-facts-service.ts index 6d93dbbf..c64887e5 100644 --- a/apps/backend/src/services/finance/sales-facts-service.ts +++ b/apps/backend/src/services/finance/sales-facts-service.ts @@ -18,7 +18,7 @@ type SalesFactRow = { canonical_product_id: number | null canonical_sku: string | null canonical_name: string | null - fulfillment_mode: string + channel: string order_count: number item_count: number quantity: string | number @@ -99,7 +99,7 @@ export async function getSalesProfitOverview(input: SalesFactsInput = {}) { [range.fromDate, range.toDate], ), query<{ - fulfillment_mode: string + channel: string item_count: number gross_revenue_fen: number cost_amount_fen: number @@ -108,7 +108,7 @@ export async function getSalesProfitOverview(input: SalesFactsInput = {}) { }>( ` SELECT - fulfillment_mode, + channel, SUM(item_count)::int AS item_count, SUM(gross_revenue_fen)::bigint AS gross_revenue_fen, SUM(cost_amount_fen)::bigint AS cost_amount_fen, @@ -116,7 +116,7 @@ export async function getSalesProfitOverview(input: SalesFactsInput = {}) { SUM(gross_profit_fen)::bigint AS gross_profit_fen FROM sales_daily_facts WHERE stat_date BETWEEN $1::date AND $2::date - GROUP BY fulfillment_mode + GROUP BY channel ORDER BY gross_revenue_fen DESC `, [range.fromDate, range.toDate], @@ -210,8 +210,8 @@ export async function getSalesProfitOverview(input: SalesFactsInput = {}) { costMissingCount: Number(row.cost_missing_count || 0), grossProfitFen: Number(row.gross_profit_fen || 0), })), - byFulfillmentMode: carrierResult.rows.map((row) => ({ - fulfillmentMode: row.fulfillment_mode, + byChannel: carrierResult.rows.map((row) => ({ + channel: row.channel, itemCount: Number(row.item_count || 0), grossRevenueFen: Number(row.gross_revenue_fen || 0), costAmountFen: Number(row.cost_amount_fen || 0), @@ -239,9 +239,7 @@ export async function listSalesDailyFacts(input: Record = {}) { normalizePositiveInteger(input.pageSize ?? input.page_size, 20, '每页数量'), ) const provider = String(input.provider || '').trim() - const fulfillmentMode = String( - input.fulfillmentMode || input.fulfillment_mode || input.carrierType || '', - ).trim() + const channel = String(input.channel || input.channel || input.carrierType || '').trim() const canonicalProductId = Number(input.canonicalProductId || input.canonical_product_id || 0) const conditions = ['f.stat_date BETWEEN $1::date AND $2::date'] @@ -250,9 +248,9 @@ export async function listSalesDailyFacts(input: Record = {}) { params.push(provider) conditions.push(`f.provider = $${params.length}`) } - if (fulfillmentMode) { - params.push(fulfillmentMode) - conditions.push(`f.fulfillment_mode = $${params.length}`) + if (channel) { + params.push(channel) + conditions.push(`f.channel = $${params.length}`) } if (Number.isInteger(canonicalProductId) && canonicalProductId > 0) { params.push(canonicalProductId) @@ -301,7 +299,7 @@ function mapSalesFact(row: SalesFactRow) { canonicalProductId: row.canonical_product_id ? Number(row.canonical_product_id) : null, canonicalSku: row.canonical_sku || '', canonicalName: row.canonical_name || '未归一', - fulfillmentMode: row.fulfillment_mode, + channel: row.channel, orderCount: Number(row.order_count || 0), itemCount: Number(row.item_count || 0), quantity: Number(row.quantity || 0), diff --git a/apps/backend/src/services/fulfillment/channel-assign-service.ts b/apps/backend/src/services/fulfillment/channel-assign-service.ts index 9e1cb18e..c79bae0c 100644 --- a/apps/backend/src/services/fulfillment/channel-assign-service.ts +++ b/apps/backend/src/services/fulfillment/channel-assign-service.ts @@ -20,7 +20,7 @@ import { * 管道 A:渠道判定。 * * 只写 `fulfillment_channel / fulfillment_status / fulfillment_source / - * fulfillment_decided_at`,绝不写商品、成本、事实,也绝不改旧 fulfillment_mode。 + * fulfillment_decided_at`,绝不写商品、成本、事实,也绝不改旧 channel。 * * 判定顺序: * 1. 运营锁定 → 跳过 diff --git a/apps/backend/src/services/fulfillment/channel-types.ts b/apps/backend/src/services/fulfillment/channel-types.ts index 3932b53c..ccaaaa74 100644 --- a/apps/backend/src/services/fulfillment/channel-types.ts +++ b/apps/backend/src/services/fulfillment/channel-types.ts @@ -1,7 +1,7 @@ /** * 履约渠道 v2:渠道 / 状态 / 决策来源的唯一枚举定义。 * - * 与旧 `fulfillment_mode` 的区别: + * 与旧 `channel` 的区别: * · 渠道只表达「谁来做」,不包含 pending/archived 这类状态; * · 「待分类」是状态,不是渠道; * · 「第三方卡券」是渠道,但没有履约载体,只做统计。 diff --git a/apps/backend/src/types/admin/read-inputs.ts b/apps/backend/src/types/admin/read-inputs.ts index 83c5296b..6b0e681f 100644 --- a/apps/backend/src/types/admin/read-inputs.ts +++ b/apps/backend/src/types/admin/read-inputs.ts @@ -11,7 +11,7 @@ export type AdminOrderListQueryInput = { keyword?: string provider?: string executorKey?: string - fulfillmentMode?: string + channel?: string platformOrderId?: string payStatus?: string taskStatus?: string diff --git a/apps/backend/src/types/admin/read-models.ts b/apps/backend/src/types/admin/read-models.ts index e2b8d275..08034c37 100644 --- a/apps/backend/src/types/admin/read-models.ts +++ b/apps/backend/src/types/admin/read-models.ts @@ -24,7 +24,7 @@ export type AdminOrderItemSummary = { skuNick: string matchSku: string quantity: number - fulfillmentMode: AdminOrderItemFulfillmentMode + channel: AdminOrderItemFulfillmentMode } export type AdminOrderListItem = { @@ -60,7 +60,7 @@ export type AdminOrderListItem = { claimStatus?: string claimStatusReason?: string claimStatusUpdatedAt?: string | null - fulfillmentMode: AdminOrderFulfillmentMode + channel: AdminOrderFulfillmentMode manualItemCount: number pendingItemCount: number manualPendingCount: number diff --git a/apps/backend/src/types/repository/inputs.ts b/apps/backend/src/types/repository/inputs.ts index 90825db9..986b4652 100644 --- a/apps/backend/src/types/repository/inputs.ts +++ b/apps/backend/src/types/repository/inputs.ts @@ -4,7 +4,7 @@ export type OrderListQueryInput = { keyword?: string provider?: string executorKey?: string - fulfillmentMode?: string + channel?: string platformOrderId?: string payStatus?: string taskStatus?: string diff --git a/apps/frontend/src/pages/admin/AdminOrderDetailPage.tsx b/apps/frontend/src/pages/admin/AdminOrderDetailPage.tsx index 82a2c690..4fe0ba95 100644 --- a/apps/frontend/src/pages/admin/AdminOrderDetailPage.tsx +++ b/apps/frontend/src/pages/admin/AdminOrderDetailPage.tsx @@ -271,10 +271,10 @@ export default function AdminOrderDetailPage() { detail.order.payStatus === 'refunded' || detail.order.orderStatus === 'cancelled' const canPauseOrClose = !isOrderFulfilled && !isOrderRefundedOrCancelled const isAutoClaimOrder = Boolean( - detail.order.fulfillmentMode === 'affiliate_dash' || - (detail.order.fulfillmentMode === 'mixed' && detail.order.canClaimH5), + detail.order.channel === 'affiliate_dash' || + (detail.order.channel === 'mixed' && detail.order.canClaimH5), ) - const fulfillmentMode = resolveOrderFulfillmentMode(detail.order) + const channel = resolveOrderFulfillmentMode(detail.order) const voucherLabel = resolveVoucherStateLabel(detail.order.voucherState) return ( @@ -358,8 +358,8 @@ export default function AdminOrderDetailPage() { {detail.order.platformOrderId} - - {fulfillmentMode.label} + + {channel.label} diff --git a/apps/frontend/src/pages/admin/AdminOrdersPage.tsx b/apps/frontend/src/pages/admin/AdminOrdersPage.tsx index f5aa9c78..9a7362cd 100644 --- a/apps/frontend/src/pages/admin/AdminOrdersPage.tsx +++ b/apps/frontend/src/pages/admin/AdminOrdersPage.tsx @@ -55,7 +55,7 @@ import { formatAdminDateTime } from '@/utils/admin-time' type OrderFilterForm = { keyword?: string provider?: string - fulfillmentMode?: string + channel?: string payStatus?: string taskStatus?: string dateRange?: unknown @@ -74,7 +74,7 @@ export default function AdminOrdersPage() { const filters = { keyword: searchParams.get('keyword') || '', provider: searchParams.get('provider') || '', - fulfillmentMode: searchParams.get('fulfillmentMode') || '', + channel: searchParams.get('channel') || '', payStatus: searchParams.get('payStatus') || '', taskStatus: searchParams.get('taskStatus') || '', dateFrom: searchParams.get('dateFrom') || '', @@ -86,7 +86,7 @@ export default function AdminOrdersPage() { [ filters.dateFrom, filters.dateTo, - filters.fulfillmentMode, + filters.channel, filters.keyword, filters.payStatus, filters.provider, @@ -106,7 +106,7 @@ export default function AdminOrdersPage() { form.setFieldsValue({ keyword: filters.keyword || undefined, provider: filters.provider || undefined, - fulfillmentMode: filters.fulfillmentMode || undefined, + channel: filters.channel || undefined, payStatus: filters.payStatus || undefined, taskStatus: filters.taskStatus || undefined, dateRange: @@ -117,7 +117,7 @@ export default function AdminOrdersPage() { }, [ filters.dateFrom, filters.dateTo, - filters.fulfillmentMode, + filters.channel, filters.keyword, filters.payStatus, filters.provider, @@ -277,14 +277,14 @@ export default function AdminOrdersPage() { title: '交付 / 状态', minWidth: 230, render: (_, row) => { - const fulfillmentMode = resolveOrderFulfillmentMode(row) + const channel = resolveOrderFulfillmentMode(row) const voucherLabel = resolveVoucherStateLabel(row.voucherState) return (
- - {fulfillmentMode.label} + + {channel.label} {row.hasVoucher && voucherLabel ? ( @@ -413,7 +413,7 @@ export default function AdminOrdersPage() { form.setFieldsValue({ keyword: undefined, provider: undefined, - fulfillmentMode: undefined, + channel: undefined, payStatus: undefined, taskStatus: undefined, dateRange: undefined, @@ -474,7 +474,7 @@ export default function AdminOrdersPage() { }} /> - + @@ -425,10 +423,10 @@ export default function AdminSalesProfitPage() { label: '履约方式', children: ( fetchAdminChannelMappingDiagnostics({ carrierType: productSourceFilter, - fulfillmentMode: productSourceFilter, + channel: productSourceFilter, }), }) const diagnostics = diagnosticsQuery.data?.data @@ -107,7 +107,7 @@ export function ChannelMappingPanel() { ], queryFn: () => fetchAdminUnmappedMappingGroups({ - fulfillmentMode: productSourceFilter, + channel: productSourceFilter, provider: queueProvider || undefined, keyword: queueKeyword || undefined, page: queuePage, diff --git a/apps/frontend/src/pages/admin/cost-center/ChannelMappingQueue.tsx b/apps/frontend/src/pages/admin/cost-center/ChannelMappingQueue.tsx index abe9792b..f49b7314 100644 --- a/apps/frontend/src/pages/admin/cost-center/ChannelMappingQueue.tsx +++ b/apps/frontend/src/pages/admin/cost-center/ChannelMappingQueue.tsx @@ -62,7 +62,7 @@ export default function ChannelMappingQueue({ onCreateMapping, }: Props) { const isEmpty = !loading && groupTotal === 0 - const hasManual = groups.some((row) => row.fulfillmentMode === 'manual') + const hasManual = groups.some((row) => row.channel === 'manual') const columns: TableColumnsType = [ { @@ -97,7 +97,7 @@ export default function ChannelMappingQueue({ }, { title: '履约方式', - dataIndex: 'fulfillmentMode', + dataIndex: 'channel', width: 120, render: (value: string) => ( @@ -199,7 +199,7 @@ export default function ChannelMappingQueue({ tableLayout="fixed" - rowKey={(row) => `${row.provider}-${row.shopId}-${row.fulfillmentMode}-${row.skuCode}`} + rowKey={(row) => `${row.provider}-${row.shopId}-${row.channel}-${row.skuCode}`} size="small" columns={columns} dataSource={groups} diff --git a/apps/frontend/src/pages/admin/cost-center/CostCenterContext.ts b/apps/frontend/src/pages/admin/cost-center/CostCenterContext.ts index df4e3017..f7150151 100644 --- a/apps/frontend/src/pages/admin/cost-center/CostCenterContext.ts +++ b/apps/frontend/src/pages/admin/cost-center/CostCenterContext.ts @@ -20,7 +20,7 @@ export type CostCenterContextValue = { /** 商品/映射按 canonical_sku 前缀筛选。 */ productSourceFilter?: ProductSource /** 成本项/版本/异常按履约方式筛选。 */ - fulfillmentModeFilter?: 'affiliate_dash' | 'work_order' | 'third_party_voucher' | 'manual' + channelFilter?: 'affiliate_dash' | 'work_order' | 'third_party_voucher' | 'manual' options?: { providers: string[] diff --git a/apps/frontend/src/pages/admin/cost-center/CostCenterShell.tsx b/apps/frontend/src/pages/admin/cost-center/CostCenterShell.tsx index 39ce86d5..d53ee3a0 100644 --- a/apps/frontend/src/pages/admin/cost-center/CostCenterShell.tsx +++ b/apps/frontend/src/pages/admin/cost-center/CostCenterShell.tsx @@ -108,7 +108,7 @@ export default function CostCenterShell() { const scopeLabel = SCOPES.find((item) => item.key === scope)?.label ?? '全部' const scopeHint = SCOPES.find((item) => item.key === scope)?.hint ?? '' const productSourceFilter = scope === 'all' ? undefined : scope - const fulfillmentModeFilter = + const channelFilter = scope === 'all' ? undefined : (scope as 'affiliate_dash' | 'work_order' | 'third_party_voucher' | 'manual') @@ -122,7 +122,7 @@ export default function CostCenterShell() { setScope, scopeLabel, productSourceFilter, - fulfillmentModeFilter, + channelFilter, options, refreshAll, }} diff --git a/apps/frontend/src/pages/admin/cost-center/CostExceptionPanel.tsx b/apps/frontend/src/pages/admin/cost-center/CostExceptionPanel.tsx index 8b66fb50..44a32559 100644 --- a/apps/frontend/src/pages/admin/cost-center/CostExceptionPanel.tsx +++ b/apps/frontend/src/pages/admin/cost-center/CostExceptionPanel.tsx @@ -30,7 +30,7 @@ import { fetchAdminCostExceptions, resolveAdminCostException, } from '@/services/admin' -import type { CostException, FulfillmentMode } from '@/types/admin' +import type { CostException, FulfillmentChannel } from '@/types/admin' import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' import { formatAdminDateTime } from '@/utils/admin-time' @@ -69,7 +69,7 @@ const RANGE_OPTIONS = [ export function CostExceptionPanel() { const { message } = App.useApp() - const { canConfig, fulfillmentModeFilter, refreshAll } = useCostCenter() + const { canConfig, channelFilter, refreshAll } = useCostCenter() const [status, setStatus] = useState('open') const [rangeDays, setRangeDays] = useState(1) @@ -88,12 +88,12 @@ export function CostExceptionPanel() { const summaryQuery = useQuery({ queryKey: [ 'admin-cost-center-exceptions-summary', - fulfillmentModeFilter, + channelFilter, rangeDays <= 0 ? 1 : rangeDays, ], queryFn: () => fetchAdminCostExceptionSummary({ - fulfillmentMode: fulfillmentModeFilter, + channel: channelFilter, recentDays: rangeDays <= 0 ? 1 : rangeDays, }), }) @@ -102,7 +102,7 @@ export function CostExceptionPanel() { const exceptionsQuery = useQuery({ queryKey: [ 'admin-cost-center-exceptions', - fulfillmentModeFilter, + channelFilter, status, rangeDays, reason, @@ -112,7 +112,7 @@ export function CostExceptionPanel() { queryFn: () => fetchAdminCostExceptions({ status: status || undefined, - fulfillmentMode: fulfillmentModeFilter, + channel: channelFilter, reason, since: rangeDays > 0 @@ -177,9 +177,9 @@ export function CostExceptionPanel() { }, { title: '履约方式', - dataIndex: 'fulfillmentMode', + dataIndex: 'channel', width: 120, - render: (value: FulfillmentMode | '') => + render: (value: FulfillmentChannel | '') => value ? ( {FULFILLMENT_MODE_LABELS[value] || value} diff --git a/apps/frontend/src/pages/admin/cost-center/CostPreviewModal.tsx b/apps/frontend/src/pages/admin/cost-center/CostPreviewModal.tsx index 79c54eaa..63d51614 100644 --- a/apps/frontend/src/pages/admin/cost-center/CostPreviewModal.tsx +++ b/apps/frontend/src/pages/admin/cost-center/CostPreviewModal.tsx @@ -121,8 +121,7 @@ export function CostPreviewModal({ open, orderItemId, onClose }: CostPreviewModa {preview.context.skuCode} · {preview.context.skuName || '-'} - {FULFILLMENT_MODE_LABELS[preview.context.fulfillmentMode] || - preview.context.fulfillmentMode} + {FULFILLMENT_MODE_LABELS[preview.context.channel] || preview.context.channel} {preview.product diff --git a/apps/frontend/src/pages/admin/cost-center/WorkOrderPricePanel.tsx b/apps/frontend/src/pages/admin/cost-center/WorkOrderPricePanel.tsx index 52a958f1..4f8d7753 100644 --- a/apps/frontend/src/pages/admin/cost-center/WorkOrderPricePanel.tsx +++ b/apps/frontend/src/pages/admin/cost-center/WorkOrderPricePanel.tsx @@ -37,7 +37,7 @@ export function WorkOrderPricePanel() { fetchAdminCostVersions({ keyword: keyword || undefined, status: status || undefined, - fulfillmentMode: 'work_order', + channel: 'work_order', page, pageSize, }), diff --git a/apps/frontend/src/pages/admin/cost-center/shared.ts b/apps/frontend/src/pages/admin/cost-center/shared.ts index 402a0a36..b11fc8a9 100644 --- a/apps/frontend/src/pages/admin/cost-center/shared.ts +++ b/apps/frontend/src/pages/admin/cost-center/shared.ts @@ -2,7 +2,7 @@ * 成本中心前端共享层:类型、常量、标签与页面上下文。 * * 口径(2026-10-23 重写): - * 1. 履约方式唯一真源是 order_items.fulfillment_mode,前端只读不改; + * 1. 履约渠道唯一真源是 order_items.fulfillment_channel + status,前端只读不改; * 2. 商品来源由 canonical_sku 前缀推导,前端不再存「商品载体」字段; * 3. 成本项用 cost_model 表达「成本怎么算」,没有「适用载体」这一列。 */ diff --git a/apps/frontend/src/services/admin/finance.ts b/apps/frontend/src/services/admin/finance.ts index 2cb31ed1..c7c29432 100644 --- a/apps/frontend/src/services/admin/finance.ts +++ b/apps/frontend/src/services/admin/finance.ts @@ -32,7 +32,7 @@ export type AdminSalesProfitParams = { dateFrom?: string dateTo?: string provider?: string - fulfillmentMode?: string + channel?: string page?: number pageSize?: number } diff --git a/apps/frontend/src/types/admin/cost-center.ts b/apps/frontend/src/types/admin/cost-center.ts index b817abd4..6363f8af 100644 --- a/apps/frontend/src/types/admin/cost-center.ts +++ b/apps/frontend/src/types/admin/cost-center.ts @@ -2,7 +2,7 @@ * 履约渠道:v2 唯一真源是 order_items.fulfillment_channel; * legacy_platform 仅历史数据兜底显示。 */ -export type FulfillmentMode = +export type FulfillmentChannel = | 'work_order' | 'affiliate_dash' | 'third_party_voucher' @@ -143,7 +143,7 @@ export interface CostException { subjectId: number orderItemId: number | null workOrderId: number | null - fulfillmentMode: FulfillmentMode | '' + channel: FulfillmentChannel | '' reason: string status: 'open' | 'resolved' | 'ignored' detail: Record @@ -265,7 +265,7 @@ export interface CostPreview { shopId: string skuCode: string skuName: string - fulfillmentMode: FulfillmentMode + channel: FulfillmentChannel quantity: number normalizedQuantity: number revenueFen: number @@ -343,7 +343,7 @@ export interface CostExceptionListResult { } export interface ChannelMappingDiagnostics { - fulfillmentMode: string | null + channel: string | null productTotal: number mappingTotal: number mappedProductTotal: number @@ -357,7 +357,7 @@ export interface UnmappedMappingGroup { shopId: string skuCode: string skuName: string - fulfillmentMode: string + channel: string itemCount: number lastSeenAt: string | null } diff --git a/apps/frontend/src/types/admin/finance.ts b/apps/frontend/src/types/admin/finance.ts index 72039c4d..5a86f624 100644 --- a/apps/frontend/src/types/admin/finance.ts +++ b/apps/frontend/src/types/admin/finance.ts @@ -93,8 +93,8 @@ export type AdminSalesProfitOverview = { costMissingCount: number grossProfitFen: number }> - byFulfillmentMode: Array<{ - fulfillmentMode: string + byChannel: Array<{ + channel: string itemCount: number grossRevenueFen: number costAmountFen: number @@ -121,7 +121,7 @@ export type AdminSalesDailyFact = { canonicalProductId: number | null canonicalSku: string canonicalName: string - fulfillmentMode: string + channel: string orderCount: number itemCount: number quantity: number diff --git a/apps/frontend/src/types/admin/orders.ts b/apps/frontend/src/types/admin/orders.ts index 5a679d89..56573d9f 100644 --- a/apps/frontend/src/types/admin/orders.ts +++ b/apps/frontend/src/types/admin/orders.ts @@ -26,7 +26,7 @@ export interface AdminOrderItemSummary { skuNick: string matchSku: string quantity: number - fulfillmentMode: AdminOrderItemFulfillmentMode + channel: AdminOrderItemFulfillmentMode } export interface AdminOrderListItem { @@ -62,7 +62,7 @@ export interface AdminOrderListItem { claimStatus?: string claimStatusReason?: string claimStatusUpdatedAt?: string | null - fulfillmentMode: AdminOrderFulfillmentMode + channel: AdminOrderFulfillmentMode manualItemCount: number pendingItemCount: number /** 可确认履约的人工项数量 */ @@ -88,7 +88,7 @@ export interface AdminOrderDetail { claimStatus?: string claimStatusReason?: string claimStatusUpdatedAt?: string | null - fulfillmentMode: AdminOrderFulfillmentMode + channel: AdminOrderFulfillmentMode manualItemCount: number pendingItemCount: number /** 可确认履约的人工项数量 */ @@ -128,7 +128,7 @@ export interface AdminOrderDetail { refundedFen: number /** 还可退金额(分) */ refundableFen: number - fulfillmentMode: AdminOrderItemFulfillmentMode + channel: AdminOrderItemFulfillmentMode deliveryMode: string spec: Record }> diff --git a/apps/frontend/src/utils/admin-display.ts b/apps/frontend/src/utils/admin-display.ts index 8b43a608..adffea7d 100644 --- a/apps/frontend/src/utils/admin-display.ts +++ b/apps/frontend/src/utils/admin-display.ts @@ -219,7 +219,7 @@ export function resolveOrderPlatformBadge(row: { platform?: string; provider?: s } export function resolveOrderFulfillmentMode(row: { - fulfillmentMode?: + channel?: | 'work_order' | 'affiliate_dash' | 'third_party_voucher' @@ -229,7 +229,7 @@ export function resolveOrderFulfillmentMode(row: { | 'archived' | 'mixed' }): { label: string; color: string } { - switch (row.fulfillmentMode) { + switch (row.channel) { case 'affiliate_dash': return { label: '⚡ 自动发货', color: 'blue' } case 'work_order': diff --git a/apps/frontend/src/utils/admin-options.ts b/apps/frontend/src/utils/admin-options.ts index 5d0cf19d..62cea4dc 100644 --- a/apps/frontend/src/utils/admin-options.ts +++ b/apps/frontend/src/utils/admin-options.ts @@ -28,7 +28,7 @@ export const adminOrderExecutorOptions = [ { label: '人工履约', value: 'manual_dispatch' }, ] -/** 订单列表按 order_items.fulfillment_mode 聚合后的履约方式筛选。 */ +/** 订单列表按 order_items.fulfillment_channel 聚合后的渠道筛选。 */ export const adminOrderFulfillmentModeOptions = [ { label: '自动发货', value: 'affiliate_dash' }, { label: '接单平台', value: 'work_order' },