import type { Hono } from 'hono'; import type { RowDataPacket } from 'mysql2'; import type hydrogenPool from '../../hydrogen-db.js'; import type { cached } from './cache.js'; import { HYDROGEN_BASE_WHERE_B, HYDROGEN_LOCAL, HYDROGEN_MIN_DATE, HYDROGEN_TABLE, } from './constants.js'; import { customerClause, dateRangeClause, enumerateDateRange, resolveDateRange, type CustomerKind, type EnergyDateRangeKind as Range, } from './query-model.js'; export interface HydrogenDailyDependencies { hydrogenPool: Pick; cached: typeof cached; } // 氢能每日:日期范围 + 客户类型 + 站点级下钻。 export function registerHydrogenDailyRoute( app: Hono, { hydrogenPool, cached }: HydrogenDailyDependencies, ) { app.get('/hydrogen/daily', async (c) => { const range = (c.req.query('range') || 'last15') as Range; const dateRange = resolveDateRange(range, c.req.query('startDate'), c.req.query('endDate')); const customer = (c.req.query('customer') || 'external') as CustomerKind; const force = c.req.query('force') === '1'; const data = await cached(`hydrogen/daily?start=${dateRange.start}&end=${dateRange.end}&customer=${customer}`, async () => { const where = [ HYDROGEN_BASE_WHERE_B, `b.${HYDROGEN_LOCAL} >= '${HYDROGEN_MIN_DATE}'`, dateRangeClause(`b.${HYDROGEN_LOCAL}`), customerClause(customer).replaceAll('customer_price', 'b.customer_price').replaceAll('fee_total', 'b.fee_total'), ].join(' AND '); // 站点级聚合(每日 × 每站)。前端组装成 day → stations // 站点名 fallback:站点主数据 → 账本冗余站点名 → 未关联站点 // 单价不重算:直接取账本成本价。 const [stationRows] = await hydrogenPool.query( `SELECT DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d') AS d, COALESCE(b.station_id, 0) AS stationId, COALESCE(MAX(s.station_short_name), MAX(s.station_name), MAX(b.station_name), CASE WHEN MAX(b.station_id) IS NULL THEN '未关联站点' ELSE CONCAT('未知站点 #', MAX(b.station_id)) END) AS stationName, ROUND(SUM(b.amount_kg), 2) AS kg, -- 单价:直接取订单中的成本价(不重算)。MAX 自然忽略 0 元的免费/赠送单 MAX(b.cost_price) AS pricePerKg FROM ${HYDROGEN_TABLE} b LEFT JOIN hydrogen_station s ON s.id = b.station_id AND s.del_flag = '0' WHERE ${where} GROUP BY d, COALESCE(b.station_id, 0) ORDER BY d DESC, kg DESC`, [dateRange.start, dateRange.end], ); // 站点环比:同站点上一条记录的 kg // 按 stationId 分组、按日期升序计算 type StationRow = { date: string; stationId: number; name: string; kg: number; pricePerKg: number }; const flat: StationRow[] = stationRows.map(r => ({ date: r.d as string, stationId: Number(r.stationId), name: r.stationName as string, kg: Number(r.kg) || 0, pricePerKg: Number(r.pricePerKg) || 0, })); // 计算日级总量 + 日级环比 const dayMap = new Map(); for (const s of flat) { if (!dayMap.has(s.date)) dayMap.set(s.date, { totalKg: 0, stations: [] }); const e = dayMap.get(s.date)!; e.totalKg += s.kg; e.stations.push(s); } const dates = Array.from(dayMap.keys()).sort(); // ASC for chain const dayChainPct = new Map(); let prev = 0; for (const d of dates) { const cur = dayMap.get(d)!.totalKg; dayChainPct.set(d, prev > 0 ? (cur - prev) / prev : 0); prev = cur; } // 站点级环比:按 stationId 分组按日期升序 const stationPrev = new Map(); const stationChain = new Map(); // key = `${date}|${stationId}` // 需要按 stationId 分组排序 const byStation = new Map(); for (const s of flat) { if (!byStation.has(s.stationId)) byStation.set(s.stationId, []); byStation.get(s.stationId)!.push(s); } for (const [, list] of byStation) { list.sort((a, b) => a.date.localeCompare(b.date)); let p = 0; for (const r of list) { stationChain.set(`${r.date}|${r.stationId}`, p > 0 ? (r.kg - p) / p : 0); p = r.kg; } } // 补零:列出 range 内全部日期,缺失日期返回 totalKg=0、stations=[] const allDates = enumerateDateRange(dateRange.start, dateRange.end); const fullDays = allDates.map(date => { const info = dayMap.get(date); return { date, totalKg: info ? Math.round(info.totalKg * 100) / 100 : 0, chainPct: dayChainPct.get(date) ?? 0, customerType: customer, stations: info ? info.stations.slice().sort((a, b) => b.kg - a.kg).map(s => ({ name: s.name, pricePerKg: Math.round(s.pricePerKg * 100) / 100, kg: Math.round(s.kg * 100) / 100, chainPct: stationChain.get(`${s.date}|${s.stationId}`) ?? 0, })) : [], }; }); // 全量日期重算环比(含补零日,0→上一日有值时显示 -100%) const ascDays = [...fullDays].sort((a, b) => a.date.localeCompare(b.date)); let prevKg = 0; for (const d of ascDays) { d.chainPct = prevKg > 0 ? (d.totalKg - prevKg) / prevKg : 0; prevKg = d.totalKg; } // 按日期降序返回 const result = ascDays.slice().sort((a, b) => b.date.localeCompare(a.date)); return result; }, { force }); return c.json(data); }); }