diff --git a/src/modules/energy/HydrogenOverview.tsx b/src/modules/energy/HydrogenOverview.tsx
index d2104af..0fb6d77 100644
--- a/src/modules/energy/HydrogenOverview.tsx
+++ b/src/modules/energy/HydrogenOverview.tsx
@@ -1,8 +1,8 @@
import { useEffect, useState } from 'react';
import {
- BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, PieChart, Pie, Tooltip, LabelList,
+ BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, PieChart, Pie, Tooltip, LabelList, Legend,
} from 'recharts';
-import { Fuel, Wallet, CalendarDays, Sparkles } from 'lucide-react';
+import { Fuel, Wallet, CalendarDays, Sparkles, TrendingUp } from 'lucide-react';
import { fetchHydrogenOverview, type HydrogenOverviewResponse } from './api';
import RotatingFooterHint from '../../components/RotatingFooterHint';
@@ -39,8 +39,9 @@ function fmtKg(kg: number): { value: string; unit: string } {
return { value: kg.toFixed(2), unit: 'Kg' };
}
function fmtYuan(yuan: number): { value: string; unit: string } {
- if (yuan >= 100_000_000) return { value: (yuan / 100_000_000).toFixed(2), unit: '亿元' };
- if (yuan >= 10_000) {
+ const abs = Math.abs(yuan);
+ if (abs >= 100_000_000) return { value: (yuan / 100_000_000).toFixed(2), unit: '亿元' };
+ if (abs >= 10_000) {
const w = yuan / 10_000;
return { value: w.toLocaleString('zh-CN', { maximumFractionDigits: 2 }), unit: '万元' };
}
@@ -52,7 +53,7 @@ interface KpiCardProps {
icon: React.ReactNode;
label: string;
hero: { value: string; unit: string };
- rows: { label: string; value: string }[];
+ rows: { label: string; value: string; valueClass?: string }[];
accentClass: string;
iconBg: string;
}
@@ -73,7 +74,7 @@ function KpiCard({ icon, label, hero, rows, accentClass, iconBg }: KpiCardProps)
{rows.map((r, i) => (
{r.label}
- {r.value}
+ {r.value}
))}
@@ -85,14 +86,15 @@ function KpiCard({ icon, label, hero, rows, accentClass, iconBg }: KpiCardProps)
export default function HydrogenOverview() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
+ const [year, setYear] = useState(null);
useEffect(() => {
let cancelled = false;
- fetchHydrogenOverview()
+ fetchHydrogenOverview(year ?? undefined)
.then(d => { if (!cancelled) setData(d); })
.catch(e => { if (!cancelled) setError(e instanceof Error ? e.message : String(e)); });
return () => { cancelled = true; };
- }, []);
+ }, [year]);
if (error) {
return 加载失败:{error}
;
@@ -104,9 +106,14 @@ export default function HydrogenOverview() {
const top5 = data.top5;
const regions = data.regions;
const monthly = data.monthly;
+ const customers = data.customers;
+ const stations = data.stations;
+ const availableYears = data.availableYears;
+ const activeYear = data.year;
const yearKgFmt = fmtKg(k.yearKg);
const yearFeeFmt = fmtYuan(k.yearFee);
+ const yearProfitFmt = fmtYuan(k.yearProfit);
const ourYearKgFmt = fmtKg(k.ourYearKg);
const customerYearKgFmt = fmtKg(k.customerYearKg);
const monthKgFmt = fmtKg(k.monthKg);
@@ -115,18 +122,41 @@ export default function HydrogenOverview() {
const todayFeeFmt = fmtYuan(k.todayFee);
const customerYearFee = Math.max(0, k.yearFee - k.ourYearFee);
const customerYearFeeFmt = fmtYuan(customerYearFee);
- const todayYear = new Date().getFullYear();
+ const yearRevenueFmt = fmtYuan(k.yearRevenue);
+
+ const profitColor = k.yearProfit >= 0 ? 'text-emerald-600' : 'text-red-600';
+
+ // 月度收支组合数据(推算"年内每月"图)
+ const monthlyDual = monthly.map(m => ({
+ ...m,
+ monthLabel: m.month.slice(5).replace(/^0/, '') + '月',
+ }));
return (
- {/* 顶部说明条 */}
-
+ {/* 顶部说明条 + 年份切换 */}
+
数据自 2025-01-01 起 · 每分钟刷新
-
{todayYear} 年累计口径
+
+ {availableYears.map(y => {
+ const active = y === activeYear;
+ return (
+
+ );
+ })}
+
- {/* KPI 4 卡 */}
-
+ {/* KPI 5 卡 */}
+
}
iconBg="bg-cyan-50"
@@ -149,6 +179,17 @@ export default function HydrogenOverview() {
{ label: '客户承担', value: `¥${customerYearFeeFmt.value} ${customerYearFeeFmt.unit}` },
]}
/>
+ }
+ iconBg="bg-emerald-50"
+ accentClass={profitColor}
+ label="时享加氢获利"
+ hero={{ value: `¥${yearProfitFmt.value}`, unit: yearProfitFmt.unit }}
+ rows={[
+ { label: '收入', value: `¥${yearRevenueFmt.value} ${yearRevenueFmt.unit}` },
+ { label: '成本', value: `¥${yearFeeFmt.value} ${yearFeeFmt.unit}` },
+ ]}
+ />
}
iconBg="bg-amber-50"
@@ -173,18 +214,17 @@ export default function HydrogenOverview() {
/>
- {/* 月度趋势:年内每月 */}
+ {/* 月度趋势:年内每月加氢量 */}
{monthly.length > 0 && (
- {todayYear} 年月度加氢量
+ {activeYear} 年月度加氢量
单位 Kg
-
+
v.slice(5).replace(/^0/, '') + '月'}
+ dataKey="monthLabel"
tick={{ fontSize: 10, fill: '#94a3b8' }}
tickLine={false}
axisLine={false}
@@ -198,7 +238,7 @@ export default function HydrogenOverview() {
cursor={{ fill: 'rgba(34, 211, 238, 0.06)' }}
/>
- {monthly.map((_, i) => (
+ {monthlyDual.map((_, i) => (
|
))}
@@ -213,6 +253,44 @@ export default function HydrogenOverview() {
)}
+ {/* 月度收支对比 */}
+ {monthly.length > 0 && (
+
+
+ {activeYear} 年月度收支对比
+ 单位 元
+
+
+
+
+
+
+ {
+ const f = fmtYuan(Number(v ?? 0));
+ return [`¥${f.value} ${f.unit}`, name];
+ }}
+ contentStyle={{ borderRadius: 12, fontSize: 12 }}
+ cursor={{ fill: 'rgba(148, 163, 184, 0.06)' }}
+ />
+
+
+
+
+
+ )}
+
{/* Top5 + 区域占比 */}
{/* Top5 加氢站 */}
@@ -299,6 +377,117 @@ export default function HydrogenOverview() {
+ {/* 加氢站加氢汇总(全量) */}
+ {stations.length > 0 && (
+
+
+ 加氢站加氢汇总
+ 共 {stations.length} 站
+
+
+
+
+
+ | # |
+ 加氢站 |
+ 加氢量 |
+ 占比 |
+ 氢费收入 |
+ 收入占比 |
+
+
+
+ {stations.map((s, i) => {
+ const kgFmt = fmtKg(s.kg);
+ const revFmt = fmtYuan(s.revenue);
+ return (
+
+ | {i + 1} |
+ {s.name} |
+
+ {kgFmt.value}{kgFmt.unit}
+ |
+
+
+
+ {(s.share * 100).toFixed(1)}%
+
+ |
+
+ ¥{revFmt.value}{revFmt.unit}
+ |
+
+
+
+ {(s.revenueShare * 100).toFixed(1)}%
+
+ |
+
+ );
+ })}
+
+
+
+
+ )}
+
+ {/* 客户账单汇总 Top */}
+ {customers.length > 0 && (
+
+
+ 客户账单汇总
+ Top {customers.length}
+
+
+
+
+
+ | # |
+ 客户 |
+ 承担方 |
+ 加氢量 |
+ 成本支出 |
+ 应收 |
+
+
+
+ {customers.map((c2, i) => {
+ const kgFmt = fmtKg(c2.kg);
+ const costFmt = fmtYuan(c2.cost);
+ const revFmt = fmtYuan(c2.revenue);
+ return (
+
+ | {i + 1} |
+ {c2.name} |
+
+ {c2.payer === 'lingniu' ? (
+ 羚牛
+ ) : (
+ 客户
+ )}
+ |
+
+ {kgFmt.value}{kgFmt.unit}
+ |
+
+ ¥{costFmt.value}{costFmt.unit}
+ |
+
+ ¥{revFmt.value}{revFmt.unit}
+ |
+
+ );
+ })}
+
+
+
+
+ )}
+
);
@@ -311,9 +500,9 @@ function HydrogenOverviewSkeleton() {
- {/* 4 卡占位 */}
-
- {Array.from({ length: 4 }).map((_, i) => (
+ {/* 5 卡占位 */}
+
+ {Array.from({ length: 5 }).map((_, i) => (
diff --git a/src/modules/energy/api.ts b/src/modules/energy/api.ts
index dc14282..d548702 100644
--- a/src/modules/energy/api.ts
+++ b/src/modules/energy/api.ts
@@ -1,6 +1,7 @@
import { fetchJson } from '../../auth/api-client';
import type {
HydrogenKpi, HydrogenStationTop, HydrogenRegionShare, HydrogenMonthlyPoint, HydrogenDailyRow,
+ HydrogenCustomerRow, HydrogenStationFull,
ElectricKpi, ElectricDailyRow, ElectricMonthGroup,
CustomerType, DateQuickPick,
} from './types';
@@ -12,10 +13,15 @@ export interface HydrogenOverviewResponse {
top5: HydrogenStationTop[];
regions: HydrogenRegionShare[];
monthly: HydrogenMonthlyPoint[];
+ customers: HydrogenCustomerRow[];
+ stations: HydrogenStationFull[];
+ availableYears: number[];
+ year: number;
}
-export function fetchHydrogenOverview(): Promise
{
- return fetchJson(`${BASE}/hydrogen/overview`);
+export function fetchHydrogenOverview(year?: number): Promise {
+ const q = year ? `?year=${year}` : '';
+ return fetchJson(`${BASE}/hydrogen/overview${q}`);
}
export function fetchHydrogenDaily(range: DateQuickPick, customer: CustomerType): Promise {
diff --git a/src/modules/energy/types.ts b/src/modules/energy/types.ts
index 7345394..a1c27c2 100644
--- a/src/modules/energy/types.ts
+++ b/src/modules/energy/types.ts
@@ -4,13 +4,19 @@ export type DateQuickPick = 'thisWeek' | 'thisMonth' | 'last15';
export interface HydrogenKpi {
yearKg: number;
yearFee: number;
+ yearRevenue: number;
+ yearProfit: number;
ourYearKg: number;
ourYearFee: number;
customerYearKg: number;
monthKg: number;
monthFee: number;
+ monthRevenue: number;
+ monthProfit: number;
todayKg: number;
todayFee: number;
+ todayRevenue: number;
+ todayProfit: number;
lingniuBornKg: number;
lingniuBornFee: number;
}
@@ -33,6 +39,24 @@ export interface HydrogenMonthlyPoint {
month: string; // YYYY-MM
kg: number;
fee: number;
+ revenue: number;
+ profit: number;
+}
+
+export interface HydrogenCustomerRow {
+ name: string;
+ payer: 'lingniu' | 'customer';
+ kg: number;
+ cost: number;
+ revenue: number;
+}
+
+export interface HydrogenStationFull {
+ name: string;
+ kg: number;
+ revenue: number;
+ share: number; // 加氢量占比
+ revenueShare: number;// 收入占比
}
export interface HydrogenStationRow {
diff --git a/src/server/routes/energy/index.ts b/src/server/routes/energy/index.ts
index e678a7c..c410804 100644
--- a/src/server/routes/energy/index.ts
+++ b/src/server/routes/energy/index.ts
@@ -60,52 +60,99 @@ function enumerateDates(range: Range): string[] {
// 氢能 总览:KPI + Top5 + 区域占比
// =========================================================
app.get('/hydrogen/overview', async (c) => {
- const data = await cached('hydrogen/overview', async () => {
- // KPI(年/月/日 + 我方/客户分解 + 累计羚牛承担)
+ const yearParam = c.req.query('year');
+ const today = new Date();
+ const todayYear = today.getFullYear();
+ const requestedYear = yearParam ? Number(yearParam) || todayYear : todayYear;
+
+ const data = await cached(`hydrogen/overview?year=${requestedYear}`, async () => {
+ // 可选年份(数据自 HYDROGEN_MIN_DATE 起)
+ const [yearListRows] = await pool.query(
+ `SELECT DISTINCT YEAR(${HYDROGEN_LOCAL}) AS y
+ FROM tab_energy_hydrogen_bill
+ WHERE is_deleted = 0 AND ${HYDROGEN_LOCAL} >= ?
+ ORDER BY y DESC`,
+ [HYDROGEN_MIN_DATE],
+ );
+ const availableYears = yearListRows.map(r => Number(r.y)).filter(y => y > 0);
+ const year = availableYears.includes(requestedYear) ? requestedYear : (availableYears[0] ?? todayYear);
+ const isCurrentYear = year === todayYear;
+
+ // KPI(按 year 分桶;月/日仅在 isCurrentYear 时取本月/今日)
const [kpiRows] = await pool.query(
`SELECT
- SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = YEAR(CURDATE())
+ SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ?
THEN hydrogen_quantity ELSE 0 END) AS yearKg,
- SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = YEAR(CURDATE())
+ SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ?
THEN cost_expense ELSE 0 END) AS yearFee,
- SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = YEAR(CURDATE()) AND truck_id IS NOT NULL
+ SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? AND cost_type = 2
+ THEN cost_expense ELSE 0 END) AS yearCustomerCost,
+ SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ?
+ THEN customer_expense ELSE 0 END) AS yearRevenue,
+ SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? AND cost_type = 3
THEN hydrogen_quantity ELSE 0 END) AS ourYearKg,
- SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = YEAR(CURDATE()) AND truck_id IS NOT NULL
+ SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? AND cost_type = 3
THEN cost_expense ELSE 0 END) AS ourYearFee,
- SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = YEAR(CURDATE()) AND truck_id IS NULL
+ SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? AND cost_type = 2
THEN hydrogen_quantity ELSE 0 END) AS customerYearKg,
- SUM(CASE WHEN DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
+ SUM(CASE WHEN ? = 1 AND DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
THEN hydrogen_quantity ELSE 0 END) AS monthKg,
- SUM(CASE WHEN DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
+ SUM(CASE WHEN ? = 1 AND DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
THEN cost_expense ELSE 0 END) AS monthFee,
- SUM(CASE WHEN DATE(${HYDROGEN_LOCAL}) = CURDATE()
+ SUM(CASE WHEN ? = 1 AND DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') AND cost_type = 2
+ THEN cost_expense ELSE 0 END) AS monthCustomerCost,
+ SUM(CASE WHEN ? = 1 AND DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
+ THEN customer_expense ELSE 0 END) AS monthRevenue,
+ SUM(CASE WHEN ? = 1 AND DATE(${HYDROGEN_LOCAL}) = CURDATE()
THEN hydrogen_quantity ELSE 0 END) AS todayKg,
- SUM(CASE WHEN DATE(${HYDROGEN_LOCAL}) = CURDATE()
+ SUM(CASE WHEN ? = 1 AND DATE(${HYDROGEN_LOCAL}) = CURDATE()
THEN cost_expense ELSE 0 END) AS todayFee,
+ SUM(CASE WHEN ? = 1 AND DATE(${HYDROGEN_LOCAL}) = CURDATE() AND cost_type = 2
+ THEN cost_expense ELSE 0 END) AS todayCustomerCost,
+ SUM(CASE WHEN ? = 1 AND DATE(${HYDROGEN_LOCAL}) = CURDATE()
+ THEN customer_expense ELSE 0 END) AS todayRevenue,
SUM(CASE WHEN truck_id IS NOT NULL
THEN hydrogen_quantity ELSE 0 END) AS lingniuBornKg,
SUM(CASE WHEN truck_id IS NOT NULL
THEN cost_expense ELSE 0 END) AS lingniuBornFee
FROM tab_energy_hydrogen_bill
- WHERE is_deleted = 0 AND hydrogen_time >= ?`,
- [HYDROGEN_MIN_DATE],
+ WHERE is_deleted = 0 AND ${HYDROGEN_LOCAL} >= ?`,
+ [year, year, year, year, year, year, year,
+ isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0,
+ isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0,
+ HYDROGEN_MIN_DATE],
);
const k = kpiRows[0] ?? {};
+ const yearFee = Number(k.yearFee) || 0;
+ const yearCustomerCost = Number(k.yearCustomerCost) || 0;
+ const yearRevenue = Number(k.yearRevenue) || 0;
+ const monthFee = Number(k.monthFee) || 0;
+ const monthCustomerCost = Number(k.monthCustomerCost) || 0;
+ const monthRevenue = Number(k.monthRevenue) || 0;
+ const todayFee = Number(k.todayFee) || 0;
+ const todayCustomerCost = Number(k.todayCustomerCost) || 0;
+ const todayRevenue = Number(k.todayRevenue) || 0;
const kpi = {
yearKg: Number(k.yearKg) || 0,
- yearFee: Number(k.yearFee) || 0,
+ yearFee,
+ yearRevenue,
+ yearProfit: yearRevenue - yearCustomerCost,
ourYearKg: Number(k.ourYearKg) || 0,
ourYearFee: Number(k.ourYearFee) || 0,
customerYearKg: Number(k.customerYearKg) || 0,
monthKg: Number(k.monthKg) || 0,
- monthFee: Number(k.monthFee) || 0,
+ monthFee,
+ monthRevenue,
+ monthProfit: monthRevenue - monthCustomerCost,
todayKg: Number(k.todayKg) || 0,
- todayFee: Number(k.todayFee) || 0,
+ todayFee,
+ todayRevenue,
+ todayProfit: todayRevenue - todayCustomerCost,
lingniuBornKg: Number(k.lingniuBornKg) || 0,
lingniuBornFee: Number(k.lingniuBornFee) || 0,
};
- // Top5 加氢站(本年)
+ // Top5 加氢站(指定年份)
const [top5Rows] = await pool.query(
`SELECT b.hydrogen_station_id AS id,
COALESCE(MAX(s.short_name), MAX(s.name),
@@ -120,12 +167,12 @@ app.get('/hydrogen/overview', async (c) => {
LEFT JOIN tab_outside_hydrogen_site os ON os.inner_site_id = b.hydrogen_station_id
LEFT JOIN tab_import_hydrogen_order i ON i.bill_code = b.bill_code
WHERE b.is_deleted = 0
- AND b.hydrogen_time >= ?
- AND YEAR(b.hydrogen_time) = YEAR(CURDATE())
+ AND b.${HYDROGEN_LOCAL} >= ?
+ AND YEAR(b.${HYDROGEN_LOCAL}) = ?
GROUP BY b.hydrogen_station_id
ORDER BY kg DESC
LIMIT 5`,
- [HYDROGEN_MIN_DATE],
+ [HYDROGEN_MIN_DATE, year],
);
const top5KgSum = kpi.yearKg || 1;
const top5 = top5Rows.map((r, i) => ({
@@ -136,7 +183,38 @@ app.get('/hydrogen/overview', async (c) => {
share: (Number(r.kg) || 0) / top5KgSum,
}));
- // 区域占比(按城市,本年)— 取前 8,其余合并为"其他"
+ // 加氢站全量汇总(同年所有站,按加氢量降序)
+ const [stationFullRows] = await pool.query(
+ `SELECT b.hydrogen_station_id AS id,
+ COALESCE(MAX(s.short_name), MAX(s.name),
+ MAX(os.fixed_station_name), MAX(os.station_name),
+ MAX(i.hydrogen_station_name),
+ CASE WHEN b.hydrogen_station_id IS NULL THEN '未关联站点'
+ ELSE CONCAT('未知站点 #', b.hydrogen_station_id) END) AS name,
+ SUM(b.hydrogen_quantity) AS kg,
+ SUM(b.customer_expense) AS revenue
+ FROM tab_energy_hydrogen_bill b
+ LEFT JOIN tab_hydrogen_site s ON s.id = b.hydrogen_station_id
+ LEFT JOIN tab_outside_hydrogen_site os ON os.inner_site_id = b.hydrogen_station_id
+ LEFT JOIN tab_import_hydrogen_order i ON i.bill_code = b.bill_code
+ WHERE b.is_deleted = 0
+ AND b.${HYDROGEN_LOCAL} >= ?
+ AND YEAR(b.${HYDROGEN_LOCAL}) = ?
+ GROUP BY b.hydrogen_station_id
+ ORDER BY kg DESC`,
+ [HYDROGEN_MIN_DATE, year],
+ );
+ const stationKgSum = stationFullRows.reduce((s, r) => s + (Number(r.kg) || 0), 0) || 1;
+ const stationRevSum = stationFullRows.reduce((s, r) => s + (Number(r.revenue) || 0), 0) || 1;
+ const stations = stationFullRows.map(r => ({
+ name: r.name as string,
+ kg: Number(r.kg) || 0,
+ revenue: Number(r.revenue) || 0,
+ share: (Number(r.kg) || 0) / stationKgSum,
+ revenueShare: (Number(r.revenue) || 0) / stationRevSum,
+ }));
+
+ // 区域占比(按城市,指定年份)— 取前 8,其余合并为"其他"
const [regionRows] = await pool.query(
`SELECT region, SUM(kg) AS kg FROM (
SELECT REPLACE(REPLACE(SUBSTRING_INDEX(COALESCE(s.city, os.city, '未知'), '-', -1), '市', ''), '省', '') AS region,
@@ -145,12 +223,12 @@ app.get('/hydrogen/overview', async (c) => {
LEFT JOIN tab_hydrogen_site s ON s.id = b.hydrogen_station_id
LEFT JOIN tab_outside_hydrogen_site os ON os.inner_site_id = b.hydrogen_station_id
WHERE b.is_deleted = 0
- AND b.hydrogen_time >= ?
- AND YEAR(b.hydrogen_time) = YEAR(CURDATE())
+ AND b.${HYDROGEN_LOCAL} >= ?
+ AND YEAR(b.${HYDROGEN_LOCAL}) = ?
) r
GROUP BY region
ORDER BY kg DESC`,
- [HYDROGEN_MIN_DATE],
+ [HYDROGEN_MIN_DATE, year],
);
const totalKg = regionRows.reduce((sum, r) => sum + (Number(r.kg) || 0), 0) || 1;
const TOP_REGIONS = 8;
@@ -165,33 +243,66 @@ app.get('/hydrogen/overview', async (c) => {
...(restKg > 0 ? [{ region: '其他', kg: restKg, share: restKg / totalKg }] : []),
];
- // 月度趋势(本年内 12 个月,缺失月补 0)
+ // 月度趋势(指定年份内 12 个月,缺失月补 0)含成本/收入/利润
+ // 利润 = 客户单收入 - 客户单成本(仅 cost_type = 2)
const [monthRows] = await pool.query(
- `SELECT DATE_FORMAT(hydrogen_time, '%Y-%m') AS m,
+ `SELECT DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') AS m,
ROUND(SUM(hydrogen_quantity), 2) AS kg,
- ROUND(SUM(cost_expense), 2) AS fee
+ ROUND(SUM(cost_expense), 2) AS fee,
+ ROUND(SUM(CASE WHEN cost_type = 2 THEN cost_expense ELSE 0 END), 2) AS customerCost,
+ ROUND(SUM(customer_expense), 2) AS revenue
FROM tab_energy_hydrogen_bill
WHERE is_deleted = 0
- AND hydrogen_time >= ?
- AND YEAR(hydrogen_time) = YEAR(CURDATE())
+ AND ${HYDROGEN_LOCAL} >= ?
+ AND YEAR(${HYDROGEN_LOCAL}) = ?
GROUP BY m
ORDER BY m`,
- [HYDROGEN_MIN_DATE],
+ [HYDROGEN_MIN_DATE, year],
);
- const monthMap = new Map();
+ const monthMap = new Map();
for (const r of monthRows) {
- monthMap.set(r.m as string, { kg: Number(r.kg) || 0, fee: Number(r.fee) || 0 });
+ monthMap.set(r.m as string, {
+ kg: Number(r.kg) || 0,
+ fee: Number(r.fee) || 0,
+ revenue: Number(r.revenue) || 0,
+ customerCost: Number(r.customerCost) || 0,
+ });
}
- const year = new Date().getFullYear();
- const currentMonth = new Date().getMonth() + 1;
- const monthly: { month: string; kg: number; fee: number }[] = [];
- for (let mi = 1; mi <= currentMonth; mi++) {
+ const lastMonth = isCurrentYear ? today.getMonth() + 1 : 12;
+ const monthly: { month: string; kg: number; fee: number; revenue: number; profit: number }[] = [];
+ for (let mi = 1; mi <= lastMonth; mi++) {
const key = `${year}-${String(mi).padStart(2, '0')}`;
- const v = monthMap.get(key) || { kg: 0, fee: 0 };
- monthly.push({ month: key, kg: v.kg, fee: v.fee });
+ const v = monthMap.get(key) || { kg: 0, fee: 0, revenue: 0, customerCost: 0 };
+ monthly.push({ month: key, kg: v.kg, fee: v.fee, revenue: v.revenue, profit: v.revenue - v.customerCost });
}
- return { kpi, top5, regions, monthly };
+ // 客户账单 Top(指定年份;按加氢量降序,前 30)
+ // payer:cost_type=2 → 客户承担;cost_type=3 → 羚牛承担;其他 → 客户(默认)
+ const [customerRows] = await pool.query(
+ `SELECT COALESCE(NULLIF(TRIM(customer_name), ''), '未指定客户') AS name,
+ CASE WHEN MAX(cost_type) = 3 AND MIN(cost_type) = 3 THEN 'lingniu'
+ ELSE 'customer' END AS payer,
+ SUM(hydrogen_quantity) AS kg,
+ SUM(cost_expense) AS cost,
+ SUM(customer_expense) AS revenue
+ FROM tab_energy_hydrogen_bill
+ WHERE is_deleted = 0
+ AND ${HYDROGEN_LOCAL} >= ?
+ AND YEAR(${HYDROGEN_LOCAL}) = ?
+ GROUP BY name
+ ORDER BY kg DESC
+ LIMIT 30`,
+ [HYDROGEN_MIN_DATE, year],
+ );
+ const customers = customerRows.map(r => ({
+ name: r.name as string,
+ payer: (r.payer as string) === 'lingniu' ? 'lingniu' as const : 'customer' as const,
+ kg: Number(r.kg) || 0,
+ cost: Number(r.cost) || 0,
+ revenue: Number(r.revenue) || 0,
+ }));
+
+ return { kpi, top5, regions, monthly, customers, stations, availableYears, year };
});
return c.json(data);
});