refactor: define electric BI metric semantics
This commit is contained in:
@@ -85,7 +85,6 @@ export default function ElectricDaily() {
|
||||
? '自定义区间'
|
||||
: QUICK_PICK_OPTIONS.find(item => item.id === pick)?.label ?? '当前时段';
|
||||
const rangeText = `${effectiveRange.start} 至 ${effectiveRange.end}`;
|
||||
const hasFeeDetail = totalFee > 0;
|
||||
const showExternalEmpty = customer === 'external' && months !== null && totalKwh === 0;
|
||||
|
||||
const applyQuickPick = (nextPick: DateQuickPick) => {
|
||||
@@ -172,9 +171,9 @@ export default function ElectricDaily() {
|
||||
<MetricTile
|
||||
icon={Wallet}
|
||||
label="充电费用"
|
||||
value={hasFeeDetail ? `¥${totalFee.toLocaleString('zh-CN', { maximumFractionDigits: 0 })}` : '待同步'}
|
||||
helper={hasFeeDetail ? `均价 ${avgPrice.toFixed(2)} 元/度` : '当前明细仅返回充电量'}
|
||||
tone={hasFeeDetail ? 'emerald' : 'slate'}
|
||||
value={`¥${totalFee.toLocaleString('zh-CN', { maximumFractionDigits: 0 })}`}
|
||||
helper={`综合费用强度 ${avgPrice.toFixed(2)} 元/度`}
|
||||
tone="emerald"
|
||||
/>
|
||||
<MetricTile icon={CalendarDays} label="有效天数" value={`${activeDays}`} unit="天" helper={`日均 ${avgKwh.toLocaleString('zh-CN', { maximumFractionDigits: 1 })} 度`} tone="amber" />
|
||||
<MetricTile icon={TrendingUp} label="波动提醒" value={abnormalDays} unit="天" helper="环比超过 30% 标记" tone={abnormalDays > 0 ? 'rose' : 'slate'} />
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function ElectricOverview() {
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
<MetricTile icon={Wallet} label="累计充电费" value={fmtYuan(k.totalFee)} helper={fmtKwh(k.totalKwh)} />
|
||||
<MetricTile icon={CalendarClock} label="本月充电费" value={fmtYuan(k.monthFee)} helper={fmtKwh(k.monthKwh)} tone="emerald" />
|
||||
<MetricTile icon={Gauge} label="累计均价" value={avgPrice.toFixed(2)} unit="元/度" helper={`本月 ${monthPrice.toFixed(2)} 元/度`} tone="amber" />
|
||||
<MetricTile icon={Gauge} label="综合费用强度" value={avgPrice.toFixed(2)} unit="元/度" helper={`本月 ${monthPrice.toFixed(2)} 元/度 · 含零费用订单`} tone="amber" />
|
||||
<MetricTile icon={BatteryCharging} label="今日充电" value={k.todayKwh.toLocaleString('zh-CN', { maximumFractionDigits: 1 })} unit="度" helper={`${fmtYuan(k.todayFee)} · ${k.todayChainPct >= 0 ? '+' : ''}${(k.todayChainPct * 100).toFixed(1)}%`} tone={Math.abs(k.todayChainPct) >= 0.3 ? 'rose' : 'slate'} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ test('returns the published mileage metric contract', async () => {
|
||||
const payload = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(payload.catalogVersion, 2);
|
||||
assert.equal(payload.catalogVersion, 3);
|
||||
assert.deepEqual(payload.domains, ['mileage']);
|
||||
assert.ok(payload.metrics.some((metric: { id: string }) => metric.id === 'mileage.assessment_completion_rate'));
|
||||
});
|
||||
@@ -22,6 +22,17 @@ test('returns the published hydrogen metric contract', async () => {
|
||||
assert.ok(payload.metrics.some((metric: { id: string }) => metric.id === 'hydrogen.customer_order_gross_profit'));
|
||||
});
|
||||
|
||||
test('returns the published electric metric contract', async () => {
|
||||
const response = await app.request('/metrics?domain=electric');
|
||||
const payload = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(payload.catalogVersion, 3);
|
||||
assert.deepEqual(payload.domains, ['electric']);
|
||||
assert.ok(payload.metrics.some((metric: { id: string }) => metric.id === 'electric.charge_total_fee'));
|
||||
assert.ok(payload.metrics.some((metric: { id: string }) => metric.id === 'electric.blended_cost_intensity'));
|
||||
});
|
||||
|
||||
test('rejects unpublished metric domains', async () => {
|
||||
const response = await app.request('/metrics?domain=energy');
|
||||
|
||||
|
||||
@@ -31,8 +31,20 @@ test('publishes hydrogen cost, revenue, and gross profit as separate metrics', (
|
||||
assert.equal(byId.get('hydrogen.customer_order_gross_profit')?.aggregation, 'derived');
|
||||
});
|
||||
|
||||
test('publishes electric fee components and blended cost semantics', () => {
|
||||
const metrics = listMetricDefinitions('electric');
|
||||
const byId = new Map(metrics.map(metric => [metric.id, metric]));
|
||||
|
||||
assert.ok(metrics.length >= 6);
|
||||
assert.equal(byId.get('electric.charge_total_fee')?.formula, 'SUM(fee)');
|
||||
assert.equal(byId.get('electric.electricity_fee')?.formula, 'SUM(e_fee)');
|
||||
assert.equal(byId.get('electric.service_fee')?.formula, 'SUM(service_fee)');
|
||||
assert.equal(byId.get('electric.blended_cost_intensity')?.aggregation, 'weighted-ratio');
|
||||
});
|
||||
|
||||
test('validates only published metric domains', () => {
|
||||
assert.equal(isMetricDomain('mileage'), true);
|
||||
assert.equal(isMetricDomain('hydrogen'), true);
|
||||
assert.equal(isMetricDomain('electric'), true);
|
||||
assert.equal(isMetricDomain('unknown'), false);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type MetricDomain = 'mileage' | 'hydrogen';
|
||||
export type MetricUnit = 'km' | 'kg' | 'cny' | 'percent' | 'vehicle' | 'station' | 'order' | 'timestamp';
|
||||
export type MetricDomain = 'mileage' | 'hydrogen' | 'electric';
|
||||
export type MetricUnit = 'km' | 'kg' | 'kwh' | 'cny' | 'cny-per-kwh' | 'percent' | 'vehicle' | 'station' | 'order' | 'timestamp';
|
||||
export type MetricAggregation = 'sum' | 'count' | 'derived' | 'weighted-ratio' | 'distinct-count' | 'latest';
|
||||
export type MetricTimeSemantics = 'flow' | 'snapshot' | 'freshness';
|
||||
|
||||
@@ -14,10 +14,10 @@ export interface MetricDefinition {
|
||||
formula: string;
|
||||
sources: readonly string[];
|
||||
dimensions: readonly string[];
|
||||
drillEntity: 'vehicle' | 'assessment-target' | 'hydrogen-station' | 'hydrogen-customer' | 'hydrogen-order';
|
||||
drillEntity: 'vehicle' | 'assessment-target' | 'hydrogen-station' | 'hydrogen-customer' | 'hydrogen-order' | 'electric-charge-order';
|
||||
}
|
||||
|
||||
export const METRIC_CATALOG_VERSION = 2;
|
||||
export const METRIC_CATALOG_VERSION = 3;
|
||||
|
||||
const MILEAGE_METRICS: readonly MetricDefinition[] = [
|
||||
{
|
||||
@@ -194,9 +194,91 @@ const HYDROGEN_METRICS: readonly MetricDefinition[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const ELECTRIC_METRICS: readonly MetricDefinition[] = [
|
||||
{
|
||||
id: 'electric.charge_energy',
|
||||
domain: 'electric',
|
||||
label: '充电量',
|
||||
description: '所选时间和车辆归属范围内,去重导入充电订单的电量之和。',
|
||||
unit: 'kwh',
|
||||
aggregation: 'sum',
|
||||
timeSemantics: 'flow',
|
||||
formula: 'SUM(kwh)',
|
||||
sources: ['bi_ele_charge_record'],
|
||||
dimensions: ['date', 'station', 'region', 'operating-company', 'vehicle-kind', 'customer', 'plate', 'order-status'],
|
||||
drillEntity: 'electric-charge-order',
|
||||
},
|
||||
{
|
||||
id: 'electric.charge_total_fee',
|
||||
domain: 'electric',
|
||||
label: '充电费用',
|
||||
description: '导入账单的充电费用之和;当前数据中等于充电电费与充电服务费之和,零费用订单仍参与统计。',
|
||||
unit: 'cny',
|
||||
aggregation: 'sum',
|
||||
timeSemantics: 'flow',
|
||||
formula: 'SUM(fee)',
|
||||
sources: ['bi_ele_charge_record'],
|
||||
dimensions: ['date', 'station', 'region', 'operating-company', 'vehicle-kind', 'customer', 'plate', 'order-status'],
|
||||
drillEntity: 'electric-charge-order',
|
||||
},
|
||||
{
|
||||
id: 'electric.electricity_fee',
|
||||
domain: 'electric',
|
||||
label: '充电电费',
|
||||
description: '导入账单中充电电费字段 e_fee 之和,不含服务费。',
|
||||
unit: 'cny',
|
||||
aggregation: 'sum',
|
||||
timeSemantics: 'flow',
|
||||
formula: 'SUM(e_fee)',
|
||||
sources: ['bi_ele_charge_record'],
|
||||
dimensions: ['date', 'station', 'region', 'operating-company', 'vehicle-kind', 'customer', 'plate'],
|
||||
drillEntity: 'electric-charge-order',
|
||||
},
|
||||
{
|
||||
id: 'electric.service_fee',
|
||||
domain: 'electric',
|
||||
label: '充电服务费',
|
||||
description: '导入账单中充电服务费字段 service_fee 之和。',
|
||||
unit: 'cny',
|
||||
aggregation: 'sum',
|
||||
timeSemantics: 'flow',
|
||||
formula: 'SUM(service_fee)',
|
||||
sources: ['bi_ele_charge_record'],
|
||||
dimensions: ['date', 'station', 'region', 'operating-company', 'vehicle-kind', 'customer', 'plate'],
|
||||
drillEntity: 'electric-charge-order',
|
||||
},
|
||||
{
|
||||
id: 'electric.blended_cost_intensity',
|
||||
domain: 'electric',
|
||||
label: '综合费用强度',
|
||||
description: '充电费用除以充电量;包含零费用订单,用于观察综合支出强度,不等同于合同电价或单笔订单单价。',
|
||||
unit: 'cny-per-kwh',
|
||||
aggregation: 'weighted-ratio',
|
||||
timeSemantics: 'flow',
|
||||
formula: 'SUM(fee) / NULLIF(SUM(kwh), 0)',
|
||||
sources: ['bi_ele_charge_record'],
|
||||
dimensions: ['date', 'station', 'region', 'operating-company', 'vehicle-kind', 'customer'],
|
||||
drillEntity: 'electric-charge-order',
|
||||
},
|
||||
{
|
||||
id: 'electric.charge_order_count',
|
||||
domain: 'electric',
|
||||
label: '充电订单数',
|
||||
description: '导入充电账单中按订单编号去重后的订单数量。',
|
||||
unit: 'order',
|
||||
aggregation: 'distinct-count',
|
||||
timeSemantics: 'flow',
|
||||
formula: 'COUNT(DISTINCT order_no)',
|
||||
sources: ['bi_ele_charge_record'],
|
||||
dimensions: ['date', 'station', 'region', 'operating-company', 'vehicle-kind', 'customer', 'plate', 'order-status'],
|
||||
drillEntity: 'electric-charge-order',
|
||||
},
|
||||
];
|
||||
|
||||
export const METRIC_CATALOG: readonly MetricDefinition[] = [
|
||||
...MILEAGE_METRICS,
|
||||
...HYDROGEN_METRICS,
|
||||
...ELECTRIC_METRICS,
|
||||
];
|
||||
|
||||
export function isMetricDomain(value: string): value is MetricDomain {
|
||||
|
||||
Reference in New Issue
Block a user