From 88ba87efffbfbc1f31915ffd991337ada9e9d25f Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 25 Oct 2026 13:55:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(cost-center):=20=E6=80=BB=E8=A7=88?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E6=94=AF=E6=8C=81=E6=8C=89=E6=9D=A5=E6=BA=90?= =?UTF-8?q?=E6=94=B6=E6=95=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 页面顶部的 6 张统计卡原本是全局口径:选了「自动发货」,商品数还是 2017、 异常数还是 108,314,和下面的列表对不上。 - overview 接口接受 scope(all / affiliate_dash / work_order / manual); - 商品、映射、成本项、生效版本、待处理异常、未映射订单项全部按来源过滤: 商品看 canonical_sku 前缀(aff: / tpl:+wo: / man:), 成本项与版本看 cost_model,异常与未映射订单项看 order_items.fulfillment_mode; - 额外返回 productTotalCount(全局商品数),供「全部」徽标使用, 避免切到单一来源后「全部」也被算成 49。 实测四种口径: all 商品 2017 / 映射 4897 / 成本项 3 / 版本 712 / 异常 108314 / 未映射 53901 affiliate_dash 商品 49 / 映射 0 / 成本项 1 / 版本 0 / 异常 53204 / 未映射 0 work_order 商品 773 / 映射 4897 / 成本项 1 / 版本 711 / 异常 1209 / 未映射 0 manual 商品 1195 / 映射 0 / 成本项 1 / 版本 1 / 异常 0 / 未映射 0 --- apps/backend/src/routes/admin/cost-center.ts | 2 +- .../cost-center-overview-service.ts | 89 +++++++++++++++++-- .../src/services/admin/cost-center.ts | 6 +- apps/frontend/src/types/admin/cost-center.ts | 4 + 4 files changed, 91 insertions(+), 10 deletions(-) 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