refactor(finance): 成本模型与履约方式正交,拆分成本中心巨型文件

后端:
- 删除 services/admin/admin-cost-center-service.ts(4,248 行),拆为
  services/cost-center/ 下 5 个模块:
  cost-center-shared(类型/枚举/归一化/映射/costModelScopeSql)、
  cost-center-overview(总览与选项)、
  product-catalog(商品与渠道映射)、
  template-bootstrap(模板引导、自动发货同步、工单证据回填)、
  cost-definition(成本项/版本/异常/预览/快照)
- cost_items.carrier_type / cost_type 改为单一 cost_model;专属模型只对
  对应履约方式生效,通用模型对所有履约方式生效
- cost_snapshots / cost_exceptions 删除载体列;按履约方式筛异常改用
  IN 半连接(248k 异常下 EXISTS 逐行回表 2.7s -> 170ms)
- sales_daily_facts.carrier_type -> fulfillment_mode
- 人工支出按 cost_model 归类;履约方式只读 order_items.fulfillment_mode
- 移除 syncOpenCostExceptionCarrierTypes:异常表已无载体列可同步

前端:
- AdminCostCenterPage.tsx(2,887 行 -> 18 行转发),拆为
  pages/admin/cost-center/ 下 8 个 Panel + shared + CostCenterContext
- CostCarrierType 删除;新增 FulfillmentMode / ProductSource / CostModel
- 销售毛利页 carrierType -> fulfillmentMode

同步补两个索引:idx_order_items_fulfillment_id(异常半连接)、
idx_work_categories_name(商品目录分类 lateral join)。
This commit is contained in:
yml2213
2026-10-23 16:06:25 +08:00
parent 5c549a2dc5
commit 63ae32d18c
33 changed files with 8107 additions and 7335 deletions
@@ -180,18 +180,17 @@ export async function listManualExpenseCostItems() {
id: number
code: string
name: string
cost_type: string
carrier_type: string
cost_model: string
pricing_mode: string
}>(
`
SELECT id, code, name, cost_type, carrier_type, pricing_mode
SELECT id, code, name, cost_model, pricing_mode
FROM cost_items
WHERE status = 'active'
AND code <> 'sys_work_order_reward'
ORDER BY
CASE WHEN code = 'sys_manual_service' THEN 0 ELSE 1 END,
carrier_type,
cost_model,
id
`,
)
@@ -200,8 +199,7 @@ export async function listManualExpenseCostItems() {
id: Number(row.id),
code: row.code,
name: row.name,
costType: row.cost_type,
carrierType: row.carrier_type,
costModel: row.cost_model,
pricingMode: row.pricing_mode,
})),
}
@@ -369,7 +367,7 @@ export async function createManualExpense(payload: JsonObject, session: AdminSes
const expenseCategory = normalizeStatus(
payload.expenseCategory ?? payload.expense_category,
EXPENSE_CATEGORIES,
inferExpenseCategory(costItem?.costType),
inferExpenseCategory(costItem?.costModel),
'支出分类无效',
)
const requestedFundAccountId = normalizeOptionalId(
@@ -758,7 +756,7 @@ async function attachCostSnapshotForExpense(
shop_id: string
work_order_id: number | null
work_product_rule_id: number | null
carrier_type: string
fulfillment_mode: string
}>(
`
SELECT
@@ -768,10 +766,9 @@ async function attachCostSnapshotForExpense(
o.shop_id,
wo.id AS work_order_id,
wo.product_rule_id AS work_product_rule_id,
ci.carrier_type
COALESCE(oi.fulfillment_mode, 'manual') AS fulfillment_mode
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
JOIN cost_items ci ON ci.id = $2
LEFT JOIN LATERAL (
SELECT id, product_rule_id
FROM work_orders
@@ -838,16 +835,15 @@ async function attachCostSnapshotForExpense(
`
INSERT INTO cost_exceptions (
subject_type, subject_id, order_item_id, work_order_id,
carrier_type, reason, status, detail_json, created_at
reason, status, detail_json, created_at
) VALUES (
'order_item', $1, $1, $2, $3, 'manual_expense_cost_missing', 'open', $4::jsonb, NOW()
'order_item', $1, $1, $2, 'manual_expense_cost_missing', 'open', $3::jsonb, NOW()
)
ON CONFLICT (subject_type, subject_id, reason) DO NOTHING
`,
[
expense.order_item_id,
context.work_order_id,
context.carrier_type,
JSON.stringify({
expenseId: expense.id,
expenseNo: expense.expense_no,
@@ -862,14 +858,14 @@ async function attachCostSnapshotForExpense(
`
INSERT INTO cost_snapshots (
subject_type, subject_id, order_id, order_item_id, work_order_id,
carrier_type, cost_item_id, cost_version_id, canonical_product_id,
cost_item_id, cost_version_id, canonical_product_id,
quantity, unit_amount_fen, total_amount_fen, snapshot_stage, source,
metadata_json, cost_basis_at, resolved_at, created_at
) VALUES (
'manual_case', $1, $2, $3, $4,
$5, $6, $7, $8,
1, $9, $9, 'actual', 'manual_input',
$10::jsonb, $11, $12, $12
$5, $6, $7,
1, $8, $8, 'actual', 'manual_input',
$9::jsonb, $10, $11, $11
)
ON CONFLICT (subject_type, subject_id, cost_item_id)
WHERE superseded_at IS NULL
@@ -881,7 +877,6 @@ async function attachCostSnapshotForExpense(
context.order_id,
expense.order_item_id,
context.work_order_id,
context.carrier_type,
expense.cost_item_id,
versionId,
context.canonical_product_id,
@@ -893,6 +888,7 @@ async function attachCostSnapshotForExpense(
paidByName: expense.paid_by_name,
payeeName: expense.payee_name,
approvedBy: session.displayName,
fulfillmentMode: context.fulfillment_mode,
}),
expense.paid_at,
nowIso(),
@@ -1008,12 +1004,9 @@ async function resolveCostItem(costItemId: number | null) {
const result = await query<{
id: number
code: string
cost_type: string
carrier_type: string
cost_model: string
status: string
}>(`SELECT id, code, cost_type, carrier_type, status FROM cost_items WHERE id = $1 LIMIT 1`, [
costItemId,
])
}>(`SELECT id, code, cost_model, status FROM cost_items WHERE id = $1 LIMIT 1`, [costItemId])
const row = result.rows[0]
if (!row) {
throw createHttpError('成本项目不存在', {
@@ -1033,7 +1026,7 @@ async function resolveCostItem(costItemId: number | null) {
errorCode: 'manual_expense_cost_item_archived',
})
}
return { id: Number(row.id), costType: row.cost_type, carrierType: row.carrier_type }
return { id: Number(row.id), costModel: row.cost_model }
}
function mapManualExpense(row: ManualExpenseRow) {
@@ -1085,11 +1078,11 @@ function mapManualExpense(row: ManualExpenseRow) {
}
}
function inferExpenseCategory(costType: string | undefined) {
if (costType === 'auto_purchase') return 'goods_purchase'
if (costType === 'manual_service') return 'service_fee'
if (costType === 'shipping_other') return 'freight'
if (costType === 'payment_fee' || costType === 'platform_fee') return 'service_fee'
function inferExpenseCategory(costModel: string | undefined) {
if (costModel === 'affiliate_purchase') return 'goods_purchase'
if (costModel === 'manual_actual') return 'service_fee'
if (costModel === 'other') return 'freight'
if (costModel === 'payment_fee' || costModel === 'platform_fee') return 'service_fee'
return 'other'
}
@@ -26,7 +26,7 @@ type SalesFactRow = {
canonical_product_id: number | null
canonical_sku: string | null
canonical_name: string | null
carrier_type: string
fulfillment_mode: string
order_count: number
item_count: number
quantity: string | number
@@ -64,7 +64,7 @@ export async function rebuildSalesDailyFacts(
o.provider,
o.shop_id,
oi.canonical_product_id,
COALESCE(oi.fulfillment_mode, 'manual') AS carrier_type,
COALESCE(oi.fulfillment_mode, 'manual') AS fulfillment_mode,
oi.order_id,
COALESCE(oi.revenue_amount_fen, 0)::bigint AS revenue_fen,
CASE
@@ -91,7 +91,7 @@ export async function rebuildSalesDailyFacts(
BETWEEN $1::date AND $2::date
)
INSERT INTO sales_daily_facts (
stat_date, provider, shop_id, canonical_product_id, carrier_type,
stat_date, provider, shop_id, canonical_product_id, fulfillment_mode,
order_count, item_count, quantity,
gross_revenue_fen, refund_amount_fen, net_revenue_fen,
cost_amount_fen, cost_missing_count, cost_stage, gross_profit_fen,
@@ -102,7 +102,7 @@ export async function rebuildSalesDailyFacts(
provider,
shop_id,
canonical_product_id,
carrier_type,
fulfillment_mode,
COUNT(DISTINCT order_id)::int,
COUNT(*)::int,
SUM(quantity),
@@ -123,7 +123,7 @@ export async function rebuildSalesDailyFacts(
$3::timestamptz,
$3::timestamptz
FROM item_base
GROUP BY stat_date, provider, shop_id, canonical_product_id, carrier_type
GROUP BY stat_date, provider, shop_id, canonical_product_id, fulfillment_mode
`,
[range.fromDate, range.toDate, computedAt],
)
@@ -196,7 +196,7 @@ export async function getSalesProfitOverview(input: SalesFactsInput = {}) {
[range.fromDate, range.toDate],
),
query<{
carrier_type: string
fulfillment_mode: string
item_count: number
gross_revenue_fen: number
cost_amount_fen: number
@@ -205,7 +205,7 @@ export async function getSalesProfitOverview(input: SalesFactsInput = {}) {
}>(
`
SELECT
carrier_type,
fulfillment_mode,
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,
@@ -213,7 +213,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 carrier_type
GROUP BY fulfillment_mode
ORDER BY gross_revenue_fen DESC
`,
[range.fromDate, range.toDate],
@@ -302,8 +302,8 @@ export async function getSalesProfitOverview(input: SalesFactsInput = {}) {
costMissingCount: Number(row.cost_missing_count || 0),
grossProfitFen: Number(row.gross_profit_fen || 0),
})),
byCarrier: carrierResult.rows.map((row) => ({
carrierType: row.carrier_type,
byFulfillmentMode: carrierResult.rows.map((row) => ({
fulfillmentMode: row.fulfillment_mode,
itemCount: Number(row.item_count || 0),
grossRevenueFen: Number(row.gross_revenue_fen || 0),
costAmountFen: Number(row.cost_amount_fen || 0),
@@ -331,7 +331,9 @@ export async function listSalesDailyFacts(input: Record<string, unknown> = {}) {
normalizePositiveInteger(input.pageSize ?? input.page_size, 20, '每页数量'),
)
const provider = String(input.provider || '').trim()
const carrierType = String(input.carrierType || input.carrier_type || '').trim()
const fulfillmentMode = String(
input.fulfillmentMode || input.fulfillment_mode || input.carrierType || '',
).trim()
const canonicalProductId = Number(input.canonicalProductId || input.canonical_product_id || 0)
const conditions = ['f.stat_date BETWEEN $1::date AND $2::date']
@@ -340,9 +342,9 @@ export async function listSalesDailyFacts(input: Record<string, unknown> = {}) {
params.push(provider)
conditions.push(`f.provider = $${params.length}`)
}
if (carrierType) {
params.push(carrierType)
conditions.push(`f.carrier_type = $${params.length}`)
if (fulfillmentMode) {
params.push(fulfillmentMode)
conditions.push(`f.fulfillment_mode = $${params.length}`)
}
if (Number.isInteger(canonicalProductId) && canonicalProductId > 0) {
params.push(canonicalProductId)
@@ -391,7 +393,7 @@ function mapSalesFact(row: SalesFactRow) {
canonicalProductId: row.canonical_product_id ? Number(row.canonical_product_id) : null,
canonicalSku: row.canonical_sku || '',
canonicalName: row.canonical_name || '未归一',
carrierType: row.carrier_type,
fulfillmentMode: row.fulfillment_mode,
orderCount: Number(row.order_count || 0),
itemCount: Number(row.item_count || 0),
quantity: Number(row.quantity || 0),