import type { Hono } from 'hono'; import type { RowDataPacket } from 'mysql2'; import type hydrogenPool from '../../db/hydrogen.js'; import type { cached } from './cache.js'; import { loadCustomerMonth, loadExternalCustomerMonth, loadExternalReceipts, loadRangePayments, loadStationDaily, loadStationDailyForStation, loadStationList, loadStationPaymentDaily, loadSummaryDaily, loadSummaryPaymentDaily, } from './repository.js'; import { dateRangeClause, enumerateDateRange, resolveDateRange } from './query-model.js'; export interface HydrogenStationBoardDependencies { hydrogenPool: Pick; cached: typeof cached; } function safeStationId(value: string | undefined): number | null { const parsed = Number(value); return Number.isInteger(parsed) && parsed > 0 ? parsed : null; } function numberValue(value: unknown): number { return Number(value) || 0; } // 单站接口没有租户入参,租户只能由服务端配置决定,不能接受浏览器传值。 // 单站经营看板只做只读聚合。列表仅返回所选区间内存在有效加氢记录的站点。 export function registerHydrogenStationBoardRoute( app: Hono, { hydrogenPool, cached }: HydrogenStationBoardDependencies, ) { app.get('/hydrogen/station-board', async (c) => { const range = resolveDateRange( 'last15', c.req.query('startDate'), c.req.query('endDate'), ); const stationId = safeStationId(c.req.query('stationId')); const force = c.req.query('force') === '1'; const cacheKey = `hydrogen/station-board?start=${range.start}&end=${range.end}${stationId ? `&station=${stationId}` : ''}`; const data = await cached(cacheKey, async () => { const stationRows = await loadStationList(hydrogenPool, { start: range.start, end: range.end }); const paymentRows = await loadRangePayments(hydrogenPool, { start: range.start, end: range.end }); const summaryDailyRows = await loadSummaryDaily(hydrogenPool, { start: range.start, end: range.end }); const stationDailyRows = await loadStationDaily(hydrogenPool, { start: range.start, end: range.end }); const summaryPaymentDailyRows = await loadSummaryPaymentDaily(hydrogenPool, { start: range.start, end: range.end }); const paymentByStation = new Map(paymentRows.map(row => [numberValue(row.stationId), row])); const dailyKgByStation = new Map>(); for (const row of stationDailyRows) { const id = numberValue(row.stationId); const values = dailyKgByStation.get(id) ?? new Map(); values.set(String(row.date), numberValue(row.kg)); dailyKgByStation.set(id, values); } const totalKg = stationRows.reduce((sum, row) => sum + numberValue(row.kg), 0); const stations = stationRows.map(row => { const id = numberValue(row.id); const payment = paymentByStation.get(id); return { id, name: String(row.name), province: String(row.province), city: String(row.city), kg: numberValue(row.kg), fee: numberValue(row.fee), recordCount: numberValue(row.recordCount), paymentAmount: numberValue(payment?.amount), paymentCount: numberValue(payment?.paymentCount), share: totalKg > 0 ? numberValue(row.kg) / totalKg : 0, latestLedgerTime: typeof row.latestLedgerTime === 'string' ? row.latestLedgerTime : null, latestPaymentDate: typeof payment?.latestPaymentDate === 'string' ? payment.latestPaymentDate : null, dailyKg: enumerateDateRange(range.start, range.end).map(date => ({ date, kg: dailyKgByStation.get(id)?.get(date) ?? 0, })), }; }).filter(station => station.recordCount > 0 || station.name.includes('东鹏大道') || (station.name.includes('佛山南海') && station.name.includes('羚牛'))); let selected = null; if (stationId) { const dailyRows = await loadStationDailyForStation(hydrogenPool, { stationId: stationId as number, start: range.start, end: range.end }); const dailyPaymentRows = await loadStationPaymentDaily(hydrogenPool, { stationId: stationId as number, start: range.start, end: range.end }); const paymentByDate = new Map(dailyPaymentRows.map(row => [String(row.date), row])); const ledgerByDate = new Map(dailyRows.map(row => [String(row.date), row])); let previousKg = 0; const daily = enumerateDateRange(range.start, range.end).map(date => { const ledger = ledgerByDate.get(date); const kg = numberValue(ledger?.kg); const payment = paymentByDate.get(date); const result = { date, kg, fee: numberValue(ledger?.fee), avgPrice: numberValue(ledger?.avgPrice), recordCount: numberValue(ledger?.recordCount), changeKg: kg - previousKg, paymentAmount: numberValue(payment?.amount), paymentCount: numberValue(payment?.paymentCount), }; previousKg = kg; return result; }); const customerMonthRows = await loadCustomerMonth(hydrogenPool, { stationId: stationId as number, end: range.end }); const externalCustomerMonthRows = await loadExternalCustomerMonth(hydrogenPool, { stationId: stationId as number, end: range.end }); const externalReceiptRows = await loadExternalReceipts(hydrogenPool, { start: range.start, end: range.end }); selected = { daily, customerMonths: customerMonthRows.map(row => ({ month: String(row.month), customerName: String(row.customerName), kg: numberValue(row.kg), fee: numberValue(row.fee), recordCount: numberValue(row.recordCount), })), externalCustomerMonths: externalCustomerMonthRows.map(row => ({ month: String(row.month), customerName: String(row.customerName), kg: numberValue(row.kg), fee: numberValue(row.fee), recordCount: numberValue(row.recordCount), })), // 快照和手工充值都没有可靠站点归属,不能把客户级进账误报为当前站点收益。 externalReceipts: { scope: 'customer', reason: '当前租户全部外部客户进账,不按站点归属,不计入本单站经营收益或现结 KPI。', rows: externalReceiptRows.map(row => ({ id: String(row.id), date: String(row.date), customerName: String(row.customerName), amount: numberValue(row.amount), payMethod: String(row.payMethod), source: String(row.source), sourceRecordCount: numberValue(row.sourceRecordCount), updatedAt: typeof row.updatedAt === 'string' ? row.updatedAt : null, })), }, }; } const paymentTotal = stations.reduce((sum, station) => sum + station.paymentAmount, 0); const summaryLedgerByDate = new Map(summaryDailyRows.map(row => [String(row.date), row])); const summaryPaymentByDate = new Map(summaryPaymentDailyRows.map(row => [String(row.date), row])); const summaryDaily = enumerateDateRange(range.start, range.end).map(date => { const ledger = summaryLedgerByDate.get(date); const payment = summaryPaymentByDate.get(date); return { date, kg: numberValue(ledger?.kg), fee: numberValue(ledger?.fee), recordCount: numberValue(ledger?.recordCount), paymentAmount: numberValue(payment?.amount), paymentCount: numberValue(payment?.paymentCount), }; }); const latestLedgerTime = stations .map(station => station.latestLedgerTime) .filter((value): value is string => Boolean(value)) .sort() .at(-1) ?? null; return { range, summary: { stationCount: stations.length, activeStationCount: stations.filter(station => station.kg > 0).length, totalKg, totalFee: stations.reduce((sum, station) => sum + station.fee, 0), recordCount: stations.reduce((sum, station) => sum + station.recordCount, 0), paymentAmount: paymentTotal, paymentCount: stations.reduce((sum, station) => sum + station.paymentCount, 0), latestLedgerTime, daily: summaryDaily, }, stations, selected, }; }, { force }); return c.json(data); }); }