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_FUEL_ONLY_WHERE_B, HYDROGEN_LOCAL, HYDROGEN_TABLE } from './constants.js'; import { customerClause, type CustomerKind } from './query-model.js'; export interface HydrogenDailyDetailDependencies { hydrogenPool: Pick; cached: typeof cached; } const YMD = /^\d{4}-\d{2}-\d{2}$/; export function registerHydrogenDailyDetailRoute( app: Hono, { hydrogenPool, cached }: HydrogenDailyDetailDependencies, ) { app.get('/hydrogen/daily-detail', async (c) => { const date = c.req.query('date') || ''; if (!YMD.test(date)) return c.json({ error: 'date must be YYYY-MM-DD' }, 400); const customer = (c.req.query('customer') || 'all') as CustomerKind; const verifyScope = c.req.query('verifyScope') === 'verified' ? 'verified' : 'all'; const force = c.req.query('force') === '1'; const stationValue = Number(c.req.query('stationId')); const stationId = Number.isInteger(stationValue) && stationValue > 0 ? stationValue : null; const stationClause = stationId ? ' AND b.station_id = ?' : ''; const params: unknown[] = [date, ...(stationId ? [stationId] : [])]; const data = await cached( `hydrogen/daily-detail?date=${date}&customer=${customer}${stationId ? `&station=${stationId}` : ''}${verifyScope === 'verified' ? '&verify=verified' : ''}`, async () => { const [rows] = await hydrogenPool.query( `SELECT b.id, DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%H:%i') AS time, COALESCE(b.station_id, 0) AS stationId, COALESCE(s.station_short_name, s.station_name, b.station_name, '未关联站点') AS stationName, COALESCE(s.station_type, 'unknown') AS stationType, COALESCE(b.system_customer_id, b.customer_id, 0) AS customerId, COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户') AS customerName, COALESCE(NULLIF(b.license_plate, ''), '无车牌') AS plateNo, b.vehicle_id AS vehicleId, COALESCE(NULLIF(b.record_source, ''), CAST(b.source AS CHAR), '未知来源') AS source, COALESCE(NULLIF(b.verify_status, ''), 'UNVERIFIED') AS verifyStatus, ROUND(COALESCE(b.cost_price, 0), 2) AS unitPrice, ROUND(COALESCE(b.amount_kg, 0), 3) AS kg, ROUND(COALESCE(b.cost_total, 0), 2) AS fee FROM ${HYDROGEN_TABLE} b LEFT JOIN hydrogen_station s ON s.id = b.station_id AND s.del_flag = '0' WHERE ${HYDROGEN_FUEL_ONLY_WHERE_B} AND DATE(b.${HYDROGEN_LOCAL}) = ? AND ${customerClause(customer).replaceAll('vehicle_id', 'b.vehicle_id')}${verifyScope === 'verified' ? " AND LOWER(COALESCE(NULLIF(TRIM(b.verify_status), ''), 'unverified')) = 'verified'" : ''}${stationClause} ORDER BY b.${HYDROGEN_LOCAL} ASC, b.id ASC LIMIT 3000`, params, ); type Vehicle = { id: string; time: string; plateNo: string; vehicleScope: 'lingniu' | 'external'; source: string; verifyStatus: string; unitPrice: number; kg: number; fee: number; }; type Customer = { id: number; name: string; kg: number; fee: number; vehicles: Vehicle[] }; type Station = { id: number; name: string; stationType: string; kg: number; fee: number; customers: Map }; const stations = new Map(); for (const row of rows) { const currentStationId = Number(row.stationId) || 0; const currentCustomerId = Number(row.customerId) || 0; let station = stations.get(currentStationId); if (!station) { station = { id: currentStationId, name: String(row.stationName), stationType: String(row.stationType), kg: 0, fee: 0, customers: new Map() }; stations.set(currentStationId, station); } let customerRow = station.customers.get(currentCustomerId); if (!customerRow) { customerRow = { id: currentCustomerId, name: String(row.customerName), kg: 0, fee: 0, vehicles: [] }; station.customers.set(currentCustomerId, customerRow); } const kg = Number(row.kg) || 0; const fee = Number(row.fee) || 0; station.kg += kg; station.fee += fee; customerRow.kg += kg; customerRow.fee += fee; customerRow.vehicles.push({ // Ledger ids can exceed Number.MAX_SAFE_INTEGER. Keep the database // identifier lossless so the final vehicle/source drill-down has // stable React keys. id: String(row.id), time: String(row.time), plateNo: String(row.plateNo), vehicleScope: row.vehicleId === null || row.vehicleId === undefined ? 'external' : 'lingniu', source: String(row.source), verifyStatus: String(row.verifyStatus), unitPrice: Number(row.unitPrice) || 0, kg, fee, }); } // `new_hydrogen_site_balance_record` is the only persisted station-balance // source. It is intentionally queried separately so a missing balance record // stays null instead of becoming a fabricated zero balance. const stationIds = [...stations.keys()].filter(id => id > 0); const balances = new Map(); if (stationIds.length > 0) { const placeholders = stationIds.map(() => '?').join(', '); const [balanceRows] = await hydrogenPool.query( `SELECT r.site_id AS stationId, r.ending_balance AS amount, DATE_FORMAT(r.effective_time, '%Y-%m-%d %H:%i:%s') AS effectiveTime FROM new_hydrogen_site_balance_record r INNER JOIN ( SELECT site_id, MAX(effective_time) AS maxEffectiveTime FROM new_hydrogen_site_balance_record WHERE del_flag = '0' AND effective_time < DATE_ADD(?, INTERVAL 1 DAY) AND site_id IN (${placeholders}) GROUP BY site_id ) latest ON latest.site_id = r.site_id AND latest.maxEffectiveTime = r.effective_time WHERE r.del_flag = '0'`, [date, ...stationIds], ); for (const row of balanceRows) { balances.set(Number(row.stationId), { amount: Number(row.amount), effectiveTime: row.effectiveTime === null || row.effectiveTime === undefined ? null : String(row.effectiveTime), }); } } return { date, recordCount: rows.length, stations: [...stations.values()].map(station => ({ ...station, balance: balances.get(station.id)?.amount ?? null, balanceEffectiveTime: balances.get(station.id)?.effectiveTime ?? null, customers: [...station.customers.values()], })), }; }, { force }, ); return c.json(data); }); }