perf(energy): add 60s TTL cache for all 4 endpoints

Hydrogen overview was 1.8-2.0s (3 full-table aggregations on 66K rows).
With cache: cold 1 user/min eats the full query, all subsequent within
60s window return in ~10ms.

Implementation:
- New cache.ts with cached(key, loader) helper
- Per-key in-flight de-duplication: concurrent requests share one loader
- Each handler wrapped, cache key includes query params
  (e.g. "hydrogen/daily?range=last30&customer=external")
- TTL 60s as requested

Measured speedups:
- hydrogen/overview: 1.96s → 12ms (165x)
- hydrogen/daily:    270ms → 11ms (24x)
- electric/overview:  93ms →  9ms (10x)
- electric/monthly:   36ms →  9ms (4x)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-28 17:50:48 +08:00
parent 9a4f1945d9
commit c02c1aa62c
2 changed files with 63 additions and 4 deletions

View File

@@ -1,6 +1,7 @@
import { Hono } from 'hono';
import type { RowDataPacket } from 'mysql2';
import pool from '../../db.js';
import { cached } from './cache.js';
const app = new Hono();
@@ -35,6 +36,7 @@ function rangeClause(localExpr: string, range: Range): string {
// 氢能 总览KPI + Top5 + 区域占比
// =========================================================
app.get('/hydrogen/overview', async (c) => {
const data = await cached('hydrogen/overview', async () => {
// KPI年/月/日 + 我方/客户分解 + 累计羚牛承担)
const [kpiRows] = await pool.query<RowDataPacket[]>(
`SELECT
@@ -134,7 +136,9 @@ app.get('/hydrogen/overview', async (c) => {
...(restKg > 0 ? [{ region: '其他', kg: restKg, share: restKg / totalKg }] : []),
];
return c.json({ kpi, top5, regions });
return { kpi, top5, regions };
});
return c.json(data);
});
// =========================================================
@@ -144,6 +148,8 @@ app.get('/hydrogen/daily', async (c) => {
const range = (c.req.query('range') || 'last30') as Range;
const customer = (c.req.query('customer') || 'external') as CustomerKind;
const data = await cached(`hydrogen/daily?range=${range}&customer=${customer}`, async () => {
const where = [
'b.is_deleted = 0',
`b.hydrogen_time >= '${HYDROGEN_MIN_DATE}'`,
@@ -231,13 +237,16 @@ app.get('/hydrogen/daily', async (c) => {
}))
.sort((a, b) => b.date.localeCompare(a.date));
return c.json(result);
return result;
});
return c.json(data);
});
// =========================================================
// 电能 总览KPI + 本月每日柱图数据
// =========================================================
app.get('/electric/overview', async (c) => {
const data = await cached('electric/overview', async () => {
const [kpiRows] = await pool.query<RowDataPacket[]>(
`SELECT
SUM(charging_degree) AS totalKwh,
@@ -316,10 +325,12 @@ app.get('/electric/overview', async (c) => {
todayChainPct = prevKwh > 0 ? (todayKwh - prevKwh) / prevKwh : 0;
}
return c.json({
return {
kpi: { totalKwh, totalFee, monthKwh, monthFee, todayKwh, todayFee, todayChainPct },
trend: trendArr,
};
});
return c.json(data);
});
// =========================================================
@@ -328,6 +339,8 @@ app.get('/electric/overview', async (c) => {
app.get('/electric/monthly', async (c) => {
const customer = (c.req.query('customer') || 'external') as CustomerKind;
const data = await cached(`electric/monthly?customer=${customer}`, async () => {
const where = [
'is_deleted = 0',
customerClause('customer_id', customer),
@@ -384,7 +397,9 @@ app.get('/electric/monthly', async (c) => {
};
});
return c.json(months);
return months;
});
return c.json(data);
});
export default app;