From d53257c65c027316c1065b53d0b489fb00e4ea63 Mon Sep 17 00:00:00 2001 From: kkfluous Date: Fri, 7 Aug 2026 14:33:38 +0800 Subject: [PATCH] refactor: define hydrogen BI metric semantics --- src/modules/energy/HydrogenOverview.tsx | 14 +-- src/server/routes/analytics/index.test.ts | 12 ++- src/shared/analytics/catalog.test.ts | 11 ++ src/shared/analytics/catalog.ts | 122 ++++++++++++++++++++-- 4 files changed, 145 insertions(+), 14 deletions(-) diff --git a/src/modules/energy/HydrogenOverview.tsx b/src/modules/energy/HydrogenOverview.tsx index 02c847a..80070dc 100644 --- a/src/modules/energy/HydrogenOverview.tsx +++ b/src/modules/energy/HydrogenOverview.tsx @@ -213,22 +213,22 @@ export default function HydrogenOverview() { icon={} iconBg="bg-blue-50" accentClass="text-slate-800" - label="累计加氢费" + label="累计加氢成本" hero={{ value: `¥${yearFeeFmt.value}`, unit: yearFeeFmt.unit }} rows={[ - { label: '我司承担', value: `¥${fmtYuan(k.ourYearFee).value} ${fmtYuan(k.ourYearFee).unit}` }, - { label: '客户承担', value: `¥${customerYearFeeFmt.value} ${customerYearFeeFmt.unit}` }, + { label: '公司单成本', value: `¥${fmtYuan(k.ourYearFee).value} ${fmtYuan(k.ourYearFee).unit}` }, + { label: '客户单成本', value: `¥${customerYearFeeFmt.value} ${customerYearFeeFmt.unit}` }, ]} /> } iconBg="bg-emerald-50" accentClass={profitColor} - label="时享加氢获利" + label="客户单毛利" hero={{ value: `¥${yearProfitFmt.value}`, unit: yearProfitFmt.unit }} rows={[ { label: '收入', value: `¥${yearRevenueFmt.value} ${yearRevenueFmt.unit}` }, - { label: '成本', value: `¥${yearFeeFmt.value} ${yearFeeFmt.unit}` }, + { label: '客户单成本', value: `¥${customerYearFeeFmt.value} ${customerYearFeeFmt.unit}` }, ]} />
-
收支健康度
+
客户单毛利率
= 0 ? 'text-emerald-600' : 'text-rose-600'}`}> {profitYield.toFixed(1)}%
@@ -302,7 +302,7 @@ export default function HydrogenOverview() {
- 时享获利 {yearProfitFmt.value}{yearProfitFmt.unit} · 客户收入 {yearRevenueFmt.value}{yearRevenueFmt.unit} + 客户单毛利 {yearProfitFmt.value}{yearProfitFmt.unit} · 客户收入 {yearRevenueFmt.value}{yearRevenueFmt.unit} {profitYield < 0 ? ' · 需关注亏损站点与客户价格' : ' · 当前保持正向收益'}
diff --git a/src/server/routes/analytics/index.test.ts b/src/server/routes/analytics/index.test.ts index 45a3022..d2e4527 100644 --- a/src/server/routes/analytics/index.test.ts +++ b/src/server/routes/analytics/index.test.ts @@ -7,11 +7,21 @@ test('returns the published mileage metric contract', async () => { const payload = await response.json(); assert.equal(response.status, 200); - assert.equal(payload.catalogVersion, 1); + assert.equal(payload.catalogVersion, 2); assert.deepEqual(payload.domains, ['mileage']); assert.ok(payload.metrics.some((metric: { id: string }) => metric.id === 'mileage.assessment_completion_rate')); }); +test('returns the published hydrogen metric contract', async () => { + const response = await app.request('/metrics?domain=hydrogen'); + const payload = await response.json(); + + assert.equal(response.status, 200); + assert.deepEqual(payload.domains, ['hydrogen']); + assert.ok(payload.metrics.some((metric: { id: string }) => metric.id === 'hydrogen.customer_revenue')); + assert.ok(payload.metrics.some((metric: { id: string }) => metric.id === 'hydrogen.customer_order_gross_profit')); +}); + test('rejects unpublished metric domains', async () => { const response = await app.request('/metrics?domain=energy'); diff --git a/src/shared/analytics/catalog.test.ts b/src/shared/analytics/catalog.test.ts index 63365ac..f3efabd 100644 --- a/src/shared/analytics/catalog.test.ts +++ b/src/shared/analytics/catalog.test.ts @@ -21,7 +21,18 @@ test('lists mileage metrics without exposing the catalog array', () => { assert.ok(metrics.every(metric => metric.domain === 'mileage')); }); +test('publishes hydrogen cost, revenue, and gross profit as separate metrics', () => { + const metrics = listMetricDefinitions('hydrogen'); + const byId = new Map(metrics.map(metric => [metric.id, metric])); + + assert.ok(metrics.length >= 8); + assert.equal(byId.get('hydrogen.refuel_cost')?.formula, "SUM(cost_total) WHERE del_flag = '0'"); + assert.equal(byId.get('hydrogen.customer_revenue')?.formula, "SUM(fee_total) WHERE del_flag = '0'"); + assert.equal(byId.get('hydrogen.customer_order_gross_profit')?.aggregation, 'derived'); +}); + test('validates only published metric domains', () => { assert.equal(isMetricDomain('mileage'), true); + assert.equal(isMetricDomain('hydrogen'), true); assert.equal(isMetricDomain('unknown'), false); }); diff --git a/src/shared/analytics/catalog.ts b/src/shared/analytics/catalog.ts index 59813eb..c886772 100644 --- a/src/shared/analytics/catalog.ts +++ b/src/shared/analytics/catalog.ts @@ -1,6 +1,6 @@ -export type MetricDomain = 'mileage'; -export type MetricUnit = 'km' | 'percent' | 'vehicle' | 'timestamp'; -export type MetricAggregation = 'sum' | 'weighted-ratio' | 'distinct-count' | 'latest'; +export type MetricDomain = 'mileage' | 'hydrogen'; +export type MetricUnit = 'km' | 'kg' | 'cny' | 'percent' | 'vehicle' | 'station' | 'order' | 'timestamp'; +export type MetricAggregation = 'sum' | 'count' | 'derived' | 'weighted-ratio' | 'distinct-count' | 'latest'; export type MetricTimeSemantics = 'flow' | 'snapshot' | 'freshness'; export interface MetricDefinition { @@ -14,10 +14,10 @@ export interface MetricDefinition { formula: string; sources: readonly string[]; dimensions: readonly string[]; - drillEntity: 'vehicle' | 'assessment-target'; + drillEntity: 'vehicle' | 'assessment-target' | 'hydrogen-station' | 'hydrogen-customer' | 'hydrogen-order'; } -export const METRIC_CATALOG_VERSION = 1; +export const METRIC_CATALOG_VERSION = 2; const MILEAGE_METRICS: readonly MetricDefinition[] = [ { @@ -87,7 +87,117 @@ const MILEAGE_METRICS: readonly MetricDefinition[] = [ }, ]; -export const METRIC_CATALOG: readonly MetricDefinition[] = MILEAGE_METRICS; +const HYDROGEN_METRICS: readonly MetricDefinition[] = [ + { + id: 'hydrogen.refuel_mass', + domain: 'hydrogen', + label: '加氢量', + description: '有效加氢账本订单的实际加注质量之和。', + unit: 'kg', + aggregation: 'sum', + timeSemantics: 'flow', + formula: "SUM(amount_kg) WHERE del_flag = '0'", + sources: ['hydrogen_fuel_ledger'], + dimensions: ['date', 'station', 'region', 'customer', 'payer', 'plate'], + drillEntity: 'hydrogen-order', + }, + { + id: 'hydrogen.refuel_cost', + domain: 'hydrogen', + label: '加氢成本', + description: '有效加氢订单记录的站点结算成本之和,不代表客户应收。', + unit: 'cny', + aggregation: 'sum', + timeSemantics: 'flow', + formula: "SUM(cost_total) WHERE del_flag = '0'", + sources: ['hydrogen_fuel_ledger'], + dimensions: ['date', 'station', 'region', 'customer', 'payer', 'plate'], + drillEntity: 'hydrogen-order', + }, + { + id: 'hydrogen.customer_revenue', + domain: 'hydrogen', + label: '客户收入', + description: '有效加氢订单账本中客户应收金额 fee_total 之和。', + unit: 'cny', + aggregation: 'sum', + timeSemantics: 'flow', + formula: "SUM(fee_total) WHERE del_flag = '0'", + sources: ['hydrogen_fuel_ledger'], + dimensions: ['date', 'station', 'region', 'customer', 'plate'], + drillEntity: 'hydrogen-customer', + }, + { + id: 'hydrogen.customer_order_cost', + domain: 'hydrogen', + label: '客户单成本', + description: '存在客户单价或客户应收金额的订单,其站点结算成本之和。', + unit: 'cny', + aggregation: 'sum', + timeSemantics: 'flow', + formula: "SUM(cost_total) WHERE del_flag = '0' AND (customer_price > 0 OR fee_total > 0)", + sources: ['hydrogen_fuel_ledger'], + dimensions: ['date', 'station', 'region', 'customer', 'plate'], + drillEntity: 'hydrogen-customer', + }, + { + id: 'hydrogen.customer_order_gross_profit', + domain: 'hydrogen', + label: '客户单毛利', + description: '客户收入减去客户单对应的站点结算成本,不包含公司承担订单及其他经营费用。', + unit: 'cny', + aggregation: 'derived', + timeSemantics: 'flow', + formula: 'hydrogen.customer_revenue - hydrogen.customer_order_cost', + sources: ['hydrogen_fuel_ledger'], + dimensions: ['date', 'station', 'region', 'customer'], + drillEntity: 'hydrogen-customer', + }, + { + id: 'hydrogen.refuel_order_count', + domain: 'hydrogen', + label: '加氢订单数', + description: '有效加氢账本订单数量。', + unit: 'order', + aggregation: 'count', + timeSemantics: 'flow', + formula: "COUNT(*) WHERE del_flag = '0'", + sources: ['hydrogen_fuel_ledger'], + dimensions: ['date', 'station', 'region', 'customer', 'payer', 'plate'], + drillEntity: 'hydrogen-order', + }, + { + id: 'hydrogen.station_count', + domain: 'hydrogen', + label: '有加注站点数', + description: '所选时间和筛选范围内产生有效加氢订单的去重站点数量。', + unit: 'station', + aggregation: 'distinct-count', + timeSemantics: 'flow', + formula: "COUNT(DISTINCT station_id) WHERE del_flag = '0'", + sources: ['hydrogen_fuel_ledger', 'hydrogen_station'], + dimensions: ['date', 'region', 'payer'], + drillEntity: 'hydrogen-station', + }, + { + id: 'hydrogen.vehicle_count', + domain: 'hydrogen', + label: '加氢车辆数', + description: '所选时间和筛选范围内产生有效加氢订单的去重车牌数量。', + unit: 'vehicle', + aggregation: 'distinct-count', + timeSemantics: 'flow', + formula: "COUNT(DISTINCT plate_number) WHERE del_flag = '0'", + sources: ['hydrogen_fuel_ledger'], + dimensions: ['date', 'station', 'region', 'customer', 'payer'], + drillEntity: 'vehicle', + }, +]; + +export const METRIC_CATALOG: readonly MetricDefinition[] = [ + ...MILEAGE_METRICS, + ...HYDROGEN_METRICS, +]; export function isMetricDomain(value: string): value is MetricDomain { return METRIC_CATALOG.some(metric => metric.domain === value);