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); });