diff --git a/apps/backend/src/routes/admin/cost-center.ts b/apps/backend/src/routes/admin/cost-center.ts index 287208ee..5ee8926b 100644 --- a/apps/backend/src/routes/admin/cost-center.ts +++ b/apps/backend/src/routes/admin/cost-center.ts @@ -29,7 +29,7 @@ router.use('/cost-center', requireAdminRoles(['admin', 'operator', 'finance', 's router.get( '/cost-center/overview', requireAdminPermission('finance.view'), - createJsonHandler(() => getAdminCostCenterOverview(), { + createJsonHandler((req) => getAdminCostCenterOverview({ scope: req.query.scope as string }), { successMessage: 'ok', errorMessage: '读取成本中心总览失败', scope: '[admin/cost-center/overview]', diff --git a/apps/backend/src/services/cost-center/cost-center-overview-service.ts b/apps/backend/src/services/cost-center/cost-center-overview-service.ts index 044882eb..56221c95 100644 --- a/apps/backend/src/services/cost-center/cost-center-overview-service.ts +++ b/apps/backend/src/services/cost-center/cost-center-overview-service.ts @@ -8,9 +8,24 @@ import { query } from '../../db/client.js' import { PROVIDERS, productSourceOf, toNumber } from './cost-center-shared.js' -export async function getAdminCostCenterOverview() { +export type CostCenterScope = 'affiliate_dash' | 'work_order' | 'manual' + +/** + * 成本中心总览。 + * + * scope 为空表示「全部来源」;传入具体来源时,商品/映射/成本项/版本/异常/未映射 + * 六项统计都按该来源收敛,页面顶部数字才会和下面的列表对得上。 + * + * 来源与成本模型的对应关系: + * affiliate_dash → affiliate_purchase + * work_order → work_order_reward + * manual → manual_actual + */ +export async function getAdminCostCenterOverview(params: { scope?: string } = {}) { + const scope = normalizeScope(params.scope) const result = await query<{ product_count: number + product_total_count: number mapping_count: number cost_item_count: number active_version_count: number @@ -18,14 +33,65 @@ export async function getAdminCostCenterOverview() { unmapped_order_item_count: number }>( ` + WITH scoped_products AS ( + SELECT p.id + FROM product_catalog p + WHERE p.status = 'active' + AND ( + $1::text IS NULL + OR ($1 = 'affiliate_dash' AND p.canonical_sku LIKE 'aff:%') + OR ($1 = 'work_order' AND (p.canonical_sku LIKE 'tpl:%' OR p.canonical_sku LIKE 'wo:%')) + OR ($1 = 'manual' AND p.canonical_sku LIKE 'man:%') + ) + ), + scoped_cost_models AS ( + SELECT CASE $1 + WHEN 'affiliate_dash' THEN 'affiliate_purchase' + WHEN 'work_order' THEN 'work_order_reward' + WHEN 'manual' THEN 'manual_actual' + END AS cost_model + ) SELECT - (SELECT COUNT(*)::int FROM product_catalog WHERE status = 'active') AS product_count, - (SELECT COUNT(*)::int FROM channel_product_mappings WHERE enabled = TRUE) AS mapping_count, - (SELECT COUNT(*)::int FROM cost_items WHERE status = 'active') AS cost_item_count, - (SELECT COUNT(*)::int FROM cost_versions WHERE status = 'active') AS active_version_count, - (SELECT COUNT(*)::int FROM cost_exceptions WHERE status = 'open') AS open_exception_count, - (SELECT COUNT(*)::int FROM order_items WHERE canonical_product_id IS NULL) AS unmapped_order_item_count + (SELECT COUNT(*)::int FROM scoped_products) AS product_count, + (SELECT COUNT(*)::int FROM product_catalog WHERE status = 'active') AS product_total_count, + ( + SELECT COUNT(*)::int + FROM channel_product_mappings m + JOIN scoped_products p ON p.id = m.canonical_product_id + WHERE m.enabled = TRUE + ) AS mapping_count, + ( + SELECT COUNT(*)::int + FROM cost_items ci + WHERE ci.status = 'active' + AND ($1::text IS NULL OR ci.cost_model = (SELECT cost_model FROM scoped_cost_models)) + ) AS cost_item_count, + ( + SELECT COUNT(*)::int + FROM cost_versions v + JOIN cost_items ci ON ci.id = v.cost_item_id + WHERE v.status = 'active' + AND ($1::text IS NULL OR ci.cost_model = (SELECT cost_model FROM scoped_cost_models)) + ) AS active_version_count, + ( + SELECT COUNT(*)::int + FROM cost_exceptions ce + WHERE ce.status = 'open' + AND ( + $1::text IS NULL + OR ce.order_item_id IN ( + SELECT oi.id FROM order_items oi WHERE oi.fulfillment_mode = $1 + ) + ) + ) AS open_exception_count, + ( + SELECT COUNT(*)::int + FROM order_items oi + WHERE oi.canonical_product_id IS NULL + AND ($1::text IS NULL OR oi.fulfillment_mode = $1) + ) AS unmapped_order_item_count `, + [scope], ) const row = result.rows[0] const [productSourceResult, costModelResult] = await Promise.all([ @@ -41,7 +107,9 @@ export async function getAdminCostCenterOverview() { ]) return { + scope: scope ?? 'all', productCount: toNumber(row?.product_count), + productTotalCount: toNumber(row?.product_total_count), mappingCount: toNumber(row?.mapping_count), costItemCount: toNumber(row?.cost_item_count), activeVersionCount: toNumber(row?.active_version_count), @@ -52,6 +120,13 @@ export async function getAdminCostCenterOverview() { } } +const COST_CENTER_SCOPES = new Set(['affiliate_dash', 'work_order', 'manual']) + +function normalizeScope(raw: unknown): CostCenterScope | null { + const value = String(raw ?? '').trim() + return COST_CENTER_SCOPES.has(value) ? (value as CostCenterScope) : null +} + /** 商品来源计数:按 canonical_sku 前缀归类,未分类单独成桶。 */ function productSourceCountsOf(rows: Array<{ canonical_sku: string; total: number }>) { const counts: Record = { diff --git a/apps/frontend/src/services/admin/cost-center.ts b/apps/frontend/src/services/admin/cost-center.ts index ff54cbbc..ea02c586 100644 --- a/apps/frontend/src/services/admin/cost-center.ts +++ b/apps/frontend/src/services/admin/cost-center.ts @@ -31,8 +31,10 @@ import type { UnmappedMappingGroupListResult, } from '@/types/admin' -export function fetchAdminCostCenterOverview() { - return apiGet('/api/v1/admin/cost-center/overview') +export function fetchAdminCostCenterOverview(params: { scope?: string } = {}) { + return apiGet('/api/v1/admin/cost-center/overview', { + scope: params.scope && params.scope !== 'all' ? params.scope : undefined, + }) } export function fetchAdminCostCenterOptions() { diff --git a/apps/frontend/src/types/admin/cost-center.ts b/apps/frontend/src/types/admin/cost-center.ts index d2e39b06..88886e21 100644 --- a/apps/frontend/src/types/admin/cost-center.ts +++ b/apps/frontend/src/types/admin/cost-center.ts @@ -20,7 +20,11 @@ export type CostModel = | 'other' export interface CostCenterOverview { + /** 统计口径:all 或具体来源;服务端回传便于核对。 */ + scope?: string productCount: number + /** 全局商品总数(不随 scope 变化),用于「全部」徽标。 */ + productTotalCount?: number mappingCount: number costItemCount: number activeVersionCount: number