import type { Hono } from 'hono'; import type { RowDataPacket } from 'mysql2'; import type hydrogenPool from '../../hydrogen-db.js'; import type { cached } from './cache.js'; import { resolveDateRange, type EnergyDateRangeKind } from './query-model.js'; type SettlementRange = EnergyDateRangeKind | 'latest' | 'custom'; export interface HydrogenSettlementDependencies { hydrogenPool: Pick; cached: typeof cached; } function toDateString(value: unknown): string | null { if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value)) return value; return null; } function minusDays(date: string, days: number): string { // 用 UTC 正午作为日历运算锚点,避免东八区转 ISO 日期时落到前一天。 const result = new Date(`${date}T12:00:00Z`); result.setUTCDate(result.getUTCDate() - days); return result.toISOString().slice(0, 10); } function normalizeMatchMode(value: unknown): 'exact' | 'manual' | 'group' | 'unmatched' { if (value === 'exact' || value === 'manual' || value === 'group') return value; return 'unmatched'; } // 站日现结只读台账:数据来自已入库的付款流水,不承担登记或审批职责。 export function registerHydrogenSettlementRoute( app: Hono, { hydrogenPool, cached }: HydrogenSettlementDependencies, ) { app.get('/hydrogen/settlement', async (c) => { const requestedRange = (c.req.query('range') || 'latest') as SettlementRange; const force = c.req.query('force') === '1'; const [latestRows] = await hydrogenPool.query( `SELECT DATE_FORMAT(MAX(payment_date), '%Y-%m-%d') AS latestPaymentDate FROM hydrogen_station_payment WHERE del_flag = '0'`, ); const latestPaymentDate = toDateString(latestRows[0]?.latestPaymentDate); const dateRange = requestedRange === 'latest' ? latestPaymentDate ? { start: minusDays(latestPaymentDate, 14), end: latestPaymentDate, mode: 'latest' as const } : { start: null, end: null, mode: 'latest' as const } : (() => { const resolved = resolveDateRange( requestedRange as EnergyDateRangeKind, c.req.query('startDate'), c.req.query('endDate'), ); return { ...resolved, mode: requestedRange === 'custom' ? 'custom' as const : requestedRange }; })(); if (!dateRange.start || !dateRange.end) { return c.json({ range: dateRange, summary: { amount: 0, paymentCount: 0, stationDayCount: 0, stationCount: 0, latestPaymentDate }, rows: [], }); } const data = await cached( `hydrogen/settlement?start=${dateRange.start}&end=${dateRange.end}`, async () => { const [rows] = await hydrogenPool.query( `SELECT DATE_FORMAT(p.payment_date, '%Y-%m-%d') AS date, p.station_id AS stationId, COALESCE(MAX(s.station_short_name), MAX(s.station_name), MAX(p.raw_station_name), '未匹配加氢站') AS stationName, ROUND(SUM(p.amount), 2) AS amount, COUNT(*) AS paymentCount, CASE WHEN SUM(p.match_mode = 'exact') = COUNT(*) THEN 'exact' WHEN SUM(p.match_mode = 'manual') = COUNT(*) THEN 'manual' WHEN SUM(p.match_mode = 'group') = COUNT(*) THEN 'group' ELSE 'unmatched' END AS matchMode FROM hydrogen_station_payment p LEFT JOIN hydrogen_station s ON s.id = p.station_id AND s.del_flag = '0' WHERE p.del_flag = '0' AND p.payment_date >= ? AND p.payment_date <= ? GROUP BY DATE_FORMAT(p.payment_date, '%Y-%m-%d'), p.station_id, p.raw_station_name ORDER BY date DESC, amount DESC LIMIT 500`, [dateRange.start, dateRange.end], ); const normalizedRows = rows.map(row => ({ date: String(row.date), stationId: row.stationId === null || row.stationId === undefined ? null : Number(row.stationId), stationName: String(row.stationName), amount: Number(row.amount) || 0, paymentCount: Number(row.paymentCount) || 0, matchMode: normalizeMatchMode(row.matchMode), })); const stationIds = new Set(normalizedRows.map(row => row.stationId ?? `name:${row.stationName}`)); return { range: dateRange, summary: { amount: normalizedRows.reduce((total, row) => total + row.amount, 0), paymentCount: normalizedRows.reduce((total, row) => total + row.paymentCount, 0), stationDayCount: normalizedRows.length, stationCount: stationIds.size, latestPaymentDate, }, rows: normalizedRows, }; }, { force }, ); return c.json(data); }); }