120 lines
4.8 KiB
TypeScript
120 lines
4.8 KiB
TypeScript
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<typeof hydrogenPool, 'query'>;
|
|
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<RowDataPacket[]>(
|
|
`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<RowDataPacket[]>(
|
|
`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);
|
|
});
|
|
}
|