feat(cost-center): 总览统计支持按来源收敛
页面顶部的 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
This commit is contained in:
@@ -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]',
|
||||
|
||||
@@ -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<string, number> = {
|
||||
|
||||
@@ -31,8 +31,10 @@ import type {
|
||||
UnmappedMappingGroupListResult,
|
||||
} from '@/types/admin'
|
||||
|
||||
export function fetchAdminCostCenterOverview() {
|
||||
return apiGet<CostCenterOverview>('/api/v1/admin/cost-center/overview')
|
||||
export function fetchAdminCostCenterOverview(params: { scope?: string } = {}) {
|
||||
return apiGet<CostCenterOverview>('/api/v1/admin/cost-center/overview', {
|
||||
scope: params.scope && params.scope !== 'all' ? params.scope : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchAdminCostCenterOptions() {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user