refactor(finance)!: 事实维度与接口字段统一改名 channel

- migration 114 将 sales_daily_facts.fulfillment_mode 重命名为 channel
- DTO/查询参数/前端类型从 fulfillmentMode 统一为 channel
- 成本中心类型 FulfillmentMode 更名为 FulfillmentChannel,并补齐卡券/待分类/归档取值
- 销售事实响应分组改为 byChannel
This commit is contained in:
yml2213
2026-10-31 20:26:15 +08:00
parent 657e49f324
commit 6cd6401eca
38 changed files with 220 additions and 208 deletions
@@ -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';
@@ -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/)
+3 -3
View File
@@ -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
@@ -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> = {},
): 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)
})
@@ -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<AdminOrderItemFulfillmentMode>([
const ORDER_ITEM_FULFILLMENT_CHANNELS = new Set<AdminOrderItemFulfillmentMode>([
'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,
@@ -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<T extends { order_id?: number | string | null }>(
@@ -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'
@@ -59,13 +59,23 @@ export const COST_MODEL_CODE_BASE: Record<string, string> = {
}
/** 履约渠道:唯一真源是 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<string>([
export const FULFILLMENT_CHANNELS = new Set<string>([
'work_order',
'affiliate_dash',
'legacy_platform',
'third_party_voucher',
'manual',
'pending',
'archived',
'unclassified',
])
/**
@@ -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
@@ -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 = {
@@ -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}
@@ -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<OrderItemCostCon
basis_at: string
work_order_id: number | null
work_product_rule_id: number | null
fulfillment_mode: string | null
channel: string | null
}>(
`
SELECT
@@ -315,7 +315,7 @@ async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostCon
COALESCE(o.paid_at, o.created_at) AS basis_at,
wo.id AS work_order_id,
wo.product_rule_id AS work_product_rule_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
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
LEFT JOIN LATERAL (
@@ -358,7 +358,7 @@ async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostCon
basisAt: new Date(row.basis_at).toISOString(),
workOrderId: row.work_order_id ? Number(row.work_order_id) : null,
workProductRuleId: row.work_product_rule_id ? Number(row.work_product_rule_id) : null,
fulfillmentMode: resolveFulfillmentMode(row.fulfillment_mode),
channel: resolveFulfillmentMode(row.channel),
}
}
@@ -366,9 +366,7 @@ async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostCon
const skuCode = normalizeRequiredText(payload.skuCode ?? payload.sku_code, '平台 SKU', 120)
const quantity = Math.max(1, Math.trunc(Number(payload.quantity) || 1))
const skuName = normalizeOptionalText(payload.skuName ?? payload.sku_name, 200)
const fulfillmentMode = resolveFulfillmentMode(
payload.fulfillmentMode ?? payload.fulfillment_mode,
)
const channel = resolveFulfillmentMode(payload.channel ?? payload.channel)
return {
orderItemId: 0,
orderId: 0,
@@ -390,7 +388,7 @@ async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostCon
workProductRuleId: normalizeOptionalId(
payload.workProductRuleId ?? payload.work_product_rule_id,
),
fulfillmentMode,
channel,
}
}
@@ -398,9 +396,9 @@ async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostCon
* 履约渠道只读 order_items.fulfillment_channel / fulfillment_status。
* 订单项还没判定时按人工处理,不允许用 provider 或核销码兜底。
*/
function resolveFulfillmentMode(value: unknown): FulfillmentMode {
function resolveFulfillmentMode(value: unknown): FulfillmentChannel {
const normalized = text(value)
if (normalized && FULFILLMENT_MODES.has(normalized)) return normalized as FulfillmentMode
if (normalized && FULFILLMENT_CHANNELS.has(normalized)) return normalized as FulfillmentChannel
return 'manual'
}
@@ -455,7 +453,7 @@ async function findMappingForContext(context: OrderItemCostContext) {
async function resolveCostLines(input: {
provider: string
shopId: string
fulfillmentMode: FulfillmentMode
channel: FulfillmentChannel
canonicalProductId: number
workProductRuleId: number | null
quantity: number
@@ -514,7 +512,7 @@ WHERE ci.status = 'active'
input.shopId,
input.canonicalProductId,
input.workProductRuleId,
input.fulfillmentMode,
input.channel,
],
)
@@ -19,7 +19,7 @@ import { setAdminAffiliateProductPrice } from './affiliate-product-service.js'
import { syncAdminWorkProductRulePrice } from '../worker-platform/admin-worker-catalog-service.js'
import {
AFFILIATE_TASK_AGG_JOIN,
FULFILLMENT_MODES,
FULFILLMENT_CHANNELS,
MAPPING_CONFIDENCES,
MAPPING_SOURCES,
PRODUCT_CATEGORY_JOINS,
@@ -583,8 +583,8 @@ export async function listAdminChannelMappings(payload: JsonObject = {}) {
export async function getAdminChannelMappingDiagnostics(payload: JsonObject = {}) {
const source = text(payload.carrierType ?? payload.carrier_type ?? payload.source)
if (source) assertOneOf(source, PRODUCT_SOURCE_FILTERS, '商品来源无效')
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 productParams: unknown[] = []
let sourceSql = 'TRUE'
@@ -627,7 +627,7 @@ export async function getAdminChannelMappingDiagnostics(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,
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,
})),
@@ -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',
@@ -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`,
@@ -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,
@@ -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<string, unknown> = {}) {
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<string, unknown> = {}) {
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),
@@ -20,7 +20,7 @@ import {
* 管道 A:渠道判定。
*
* 只写 `fulfillment_channel / fulfillment_status / fulfillment_source /
* fulfillment_decided_at`,绝不写商品、成本、事实,也绝不改旧 fulfillment_mode。
* fulfillment_decided_at`,绝不写商品、成本、事实,也绝不改旧 channel。
*
* 判定顺序:
* 1. 运营锁定 → 跳过
@@ -1,7 +1,7 @@
/**
* 履约渠道 v2:渠道 / 状态 / 决策来源的唯一枚举定义。
*
* 与旧 `fulfillment_mode` 的区别:
* 与旧 `channel` 的区别:
* · 渠道只表达「谁来做」,不包含 pending/archived 这类状态;
* · 「待分类」是状态,不是渠道;
* · 「第三方卡券」是渠道,但没有履约载体,只做统计。
+1 -1
View File
@@ -11,7 +11,7 @@ export type AdminOrderListQueryInput = {
keyword?: string
provider?: string
executorKey?: string
fulfillmentMode?: string
channel?: string
platformOrderId?: string
payStatus?: string
taskStatus?: string
+2 -2
View File
@@ -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
+1 -1
View File
@@ -4,7 +4,7 @@ export type OrderListQueryInput = {
keyword?: string
provider?: string
executorKey?: string
fulfillmentMode?: string
channel?: string
platformOrderId?: string
payStatus?: string
taskStatus?: string
@@ -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() {
</Descriptions.Item>
<Descriptions.Item label="平台订单号">{detail.order.platformOrderId}</Descriptions.Item>
<Descriptions.Item label="履约模式">
<Tag color={fulfillmentMode.color} style={{ margin: 0 }}>
{fulfillmentMode.label}
<Tag color={channel.color} style={{ margin: 0 }}>
{channel.label}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="核销码">
@@ -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 (
<div className="cell-stack" style={{ gap: 4 }}>
<Space size={[0, 4]} wrap>
<Tag color={fulfillmentMode.color} style={{ margin: 0, fontSize: 11 }}>
{fulfillmentMode.label}
<Tag color={channel.color} style={{ margin: 0, fontSize: 11 }}>
{channel.label}
</Tag>
{row.hasVoucher && voucherLabel ? (
<Tag color="gold" style={{ margin: 0, fontSize: 11 }}>
@@ -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() {
}}
/>
</Form.Item>
<Form.Item name="fulfillmentMode">
<Form.Item name="channel">
<Select
allowClear
placeholder="履约模式"
@@ -32,7 +32,7 @@ import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admi
import { hasAdminPermission } from '@/utils/admin-auth'
import { formatMoney } from './panels/shared'
type FilterForm = { dateRange?: unknown; provider?: string; fulfillmentMode?: string }
type FilterForm = { dateRange?: unknown; provider?: string; channel?: string }
const FULFILLMENT_MODE_OPTIONS = [
{ value: '', label: '全部履约方式' },
@@ -71,8 +71,8 @@ export default function AdminSalesProfitPage() {
dateFrom: string
dateTo: string
provider: string
fulfillmentMode: string
}>({ dateFrom: defaultDateFrom, dateTo: defaultDateTo, provider: '', fulfillmentMode: '' })
channel: string
}>({ dateFrom: defaultDateFrom, dateTo: defaultDateTo, provider: '', channel: '' })
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
const [rebuilding, setRebuilding] = useState(false)
@@ -96,7 +96,7 @@ export default function AdminSalesProfitPage() {
dateFrom: range[0] ? dayjs(range[0]).format('YYYY-MM-DD') : '',
dateTo: range[1] ? dayjs(range[1]).format('YYYY-MM-DD') : '',
provider: values.provider || '',
fulfillmentMode: values.fulfillmentMode || '',
channel: values.channel || '',
})
}
@@ -107,7 +107,7 @@ export default function AdminSalesProfitPage() {
dateFrom: defaultDateFrom,
dateTo: defaultDateTo,
provider: '',
fulfillmentMode: '',
channel: '',
})
}
@@ -180,12 +180,10 @@ export default function AdminSalesProfitPage() {
},
]
const fulfillmentModeColumns: TableColumnsType<
NonNullable<typeof overview>['byFulfillmentMode'][number]
> = [
const channelColumns: TableColumnsType<NonNullable<typeof overview>['byChannel'][number]> = [
{
title: '履约方式',
dataIndex: 'fulfillmentMode',
dataIndex: 'channel',
render: (value: string) => FULFILLMENT_MODE_LABEL[value] || value,
},
{ title: '订单项', dataIndex: 'itemCount', width: 90 },
@@ -223,7 +221,7 @@ export default function AdminSalesProfitPage() {
},
{
title: '履约方式',
dataIndex: 'fulfillmentMode',
dataIndex: 'channel',
width: 100,
render: (value: string) => FULFILLMENT_MODE_LABEL[value] || value,
},
@@ -263,7 +261,7 @@ export default function AdminSalesProfitPage() {
initialValues={{
dateRange: [dayjs().subtract(29, 'day'), dayjs()],
provider: '',
fulfillmentMode: '',
channel: '',
}}
>
<Form.Item name="dateRange" label="销售日期">
@@ -280,7 +278,7 @@ export default function AdminSalesProfitPage() {
]}
/>
</Form.Item>
<Form.Item name="fulfillmentMode" label="履约方式">
<Form.Item name="channel" label="履约方式">
<Select style={{ width: 150 }} options={FULFILLMENT_MODE_OPTIONS} />
</Form.Item>
<Form.Item>
@@ -425,10 +423,10 @@ export default function AdminSalesProfitPage() {
label: '履约方式',
children: (
<PercentTable
rowKey="fulfillmentMode"
rowKey="channel"
size="small"
columns={fulfillmentModeColumns}
dataSource={overview?.byFulfillmentMode || []}
columns={channelColumns}
dataSource={overview?.byChannel || []}
loading={overviewQuery.isLoading}
pagination={false}
scroll={{ x: 800 }}
@@ -71,7 +71,7 @@ export function ChannelMappingPanel() {
queryFn: () =>
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,
@@ -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<UnmappedMappingGroup> = [
{
@@ -97,7 +97,7 @@ export default function ChannelMappingQueue({
},
{
title: '履约方式',
dataIndex: 'fulfillmentMode',
dataIndex: 'channel',
width: 120,
render: (value: string) => (
<Tag color={FULFILLMENT_MODE_COLORS[value] || 'default'}>
@@ -199,7 +199,7 @@ export default function ChannelMappingQueue({
<PercentTable<UnmappedMappingGroup>
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}
@@ -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[]
@@ -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,
}}
@@ -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 ? (
<Tag color={FULFILLMENT_MODE_TAG_COLORS[value] || 'default'}>
{FULFILLMENT_MODE_LABELS[value] || value}
@@ -121,8 +121,7 @@ export function CostPreviewModal({ open, orderItemId, onClose }: CostPreviewModa
{preview.context.skuCode} · {preview.context.skuName || '-'}
</Descriptions.Item>
<Descriptions.Item label="履约方式">
{FULFILLMENT_MODE_LABELS[preview.context.fulfillmentMode] ||
preview.context.fulfillmentMode}
{FULFILLMENT_MODE_LABELS[preview.context.channel] || preview.context.channel}
</Descriptions.Item>
<Descriptions.Item label="标准商品">
{preview.product
@@ -37,7 +37,7 @@ export function WorkOrderPricePanel() {
fetchAdminCostVersions({
keyword: keyword || undefined,
status: status || undefined,
fulfillmentMode: 'work_order',
channel: 'work_order',
page,
pageSize,
}),
@@ -2,7 +2,7 @@
* 成本中心前端共享层:类型、常量、标签与页面上下文。
*
* 口径(2026-10-23 重写):
* 1. 履约方式唯一真源是 order_items.fulfillment_mode,前端只读不改;
* 1. 履约渠道唯一真源是 order_items.fulfillment_channel + status,前端只读不改;
* 2. 商品来源由 canonical_sku 前缀推导,前端不再存「商品载体」字段;
* 3. 成本项用 cost_model 表达「成本怎么算」,没有「适用载体」这一列。
*/
+1 -1
View File
@@ -32,7 +32,7 @@ export type AdminSalesProfitParams = {
dateFrom?: string
dateTo?: string
provider?: string
fulfillmentMode?: string
channel?: string
page?: number
pageSize?: number
}
+5 -5
View File
@@ -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<string, unknown>
@@ -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
}
+3 -3
View File
@@ -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
+4 -4
View File
@@ -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<string, unknown>
}>
+2 -2
View File
@@ -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':
+1 -1
View File
@@ -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' },