refactor(finance)!: 事实维度与接口字段统一改名 channel
- migration 114 将 sales_daily_facts.fulfillment_mode 重命名为 channel - DTO/查询参数/前端类型从 fulfillmentMode 统一为 channel - 成本中心类型 FulfillmentMode 更名为 FulfillmentChannel,并补齐卡券/待分类/归档取值 - 销售事实响应分组改为 byChannel
This commit is contained in:
@@ -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/)
|
||||
|
||||
@@ -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 这类状态;
|
||||
* · 「待分类」是状态,不是渠道;
|
||||
* · 「第三方卡券」是渠道,但没有履约载体,只做统计。
|
||||
|
||||
@@ -11,7 +11,7 @@ export type AdminOrderListQueryInput = {
|
||||
keyword?: string
|
||||
provider?: string
|
||||
executorKey?: string
|
||||
fulfillmentMode?: string
|
||||
channel?: string
|
||||
platformOrderId?: string
|
||||
payStatus?: string
|
||||
taskStatus?: string
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 表达「成本怎么算」,没有「适用载体」这一列。
|
||||
*/
|
||||
|
||||
@@ -32,7 +32,7 @@ export type AdminSalesProfitParams = {
|
||||
dateFrom?: string
|
||||
dateTo?: string
|
||||
provider?: string
|
||||
fulfillmentMode?: string
|
||||
channel?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
}>
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user