perf: reuse mileage monitoring snapshots
This commit is contained in:
@@ -162,6 +162,7 @@ SUM(fee) / NULLIF(SUM(kwh), 0)
|
|||||||
|
|
||||||
- 已完成电能数据截至时间和实际趋势月展示。
|
- 已完成电能数据截至时间和实际趋势月展示。
|
||||||
- 已完成氢能结构化故障状态与重试体验。
|
- 已完成氢能结构化故障状态与重试体验。
|
||||||
|
- 已完成里程今日仪表快照复用:普通查询命中分钟级快照,手动刷新生成一致的分页快照。
|
||||||
- 待恢复氢能数据库后进行总览、站点、客户三层对账。
|
- 待恢复氢能数据库后进行总览、站点、客户三层对账。
|
||||||
- 待 ETC 同步后进行通行记录、账单和收款三层对账。
|
- 待 ETC 同步后进行通行记录、账单和收款三层对账。
|
||||||
|
|
||||||
|
|||||||
@@ -565,7 +565,7 @@ export default function MonitoringView() {
|
|||||||
}, [filterTargetNames]);
|
}, [filterTargetNames]);
|
||||||
|
|
||||||
// 加载首页数据
|
// 加载首页数据
|
||||||
const loadFirstPage = useCallback((showPageLoading = true) => {
|
const loadFirstPage = useCallback((showPageLoading = true, force = false) => {
|
||||||
if (showPageLoading) setPageLoading(true);
|
if (showPageLoading) setPageLoading(true);
|
||||||
setPageError(null);
|
setPageError(null);
|
||||||
return fetchMonitoring({
|
return fetchMonitoring({
|
||||||
@@ -589,6 +589,7 @@ export default function MonitoringView() {
|
|||||||
endDate: rangeEnd || undefined,
|
endDate: rangeEnd || undefined,
|
||||||
brands: filterBrands.length > 0 ? filterBrands : undefined,
|
brands: filterBrands.length > 0 ? filterBrands : undefined,
|
||||||
sourcePriority,
|
sourcePriority,
|
||||||
|
force,
|
||||||
}).then(d => {
|
}).then(d => {
|
||||||
setVehicles(d.vehicles);
|
setVehicles(d.vehicles);
|
||||||
setStats(d.stats);
|
setStats(d.stats);
|
||||||
@@ -613,7 +614,7 @@ export default function MonitoringView() {
|
|||||||
if (manualRefreshing) return;
|
if (manualRefreshing) return;
|
||||||
setManualRefreshing(true);
|
setManualRefreshing(true);
|
||||||
try {
|
try {
|
||||||
await loadFirstPage(false);
|
await loadFirstPage(false, true);
|
||||||
} finally {
|
} finally {
|
||||||
setManualRefreshing(false);
|
setManualRefreshing(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export async function fetchMonitoring(params?: {
|
|||||||
endDate?: string;
|
endDate?: string;
|
||||||
brands?: string[];
|
brands?: string[];
|
||||||
sourcePriority?: MileageSourceGroup[];
|
sourcePriority?: MileageSourceGroup[];
|
||||||
|
force?: boolean;
|
||||||
}): Promise<MonitoringData> {
|
}): Promise<MonitoringData> {
|
||||||
const query = new URLSearchParams();
|
const query = new URLSearchParams();
|
||||||
if (params?.sortBy) query.set('sortBy', params.sortBy);
|
if (params?.sortBy) query.set('sortBy', params.sortBy);
|
||||||
@@ -76,6 +77,7 @@ export async function fetchMonitoring(params?: {
|
|||||||
if (params?.startDate) query.set('startDate', params.startDate);
|
if (params?.startDate) query.set('startDate', params.startDate);
|
||||||
if (params?.endDate) query.set('endDate', params.endDate);
|
if (params?.endDate) query.set('endDate', params.endDate);
|
||||||
if (params?.sourcePriority?.length) query.set('sourcePriority', params.sourcePriority.join(','));
|
if (params?.sourcePriority?.length) query.set('sourcePriority', params.sourcePriority.join(','));
|
||||||
|
if (params?.force) query.set('force', '1');
|
||||||
const qs = query.toString();
|
const qs = query.toString();
|
||||||
return fetchJson<MonitoringData>(`${BASE}/monitoring${qs ? `?${qs}` : ''}`);
|
return fetchJson<MonitoringData>(`${BASE}/monitoring${qs ? `?${qs}` : ''}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
clearManualMonitoringSnapshot,
|
||||||
|
getManualMonitoringSnapshot,
|
||||||
|
storeManualMonitoringSnapshot,
|
||||||
|
} from './manual-snapshot.js';
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
vehicles: [],
|
||||||
|
dailyTotals: [{ date: '2026-08-07', totalKm: 12 }],
|
||||||
|
start: '2026-08-07',
|
||||||
|
end: '2026-08-07',
|
||||||
|
};
|
||||||
|
|
||||||
|
test('reuses a manual snapshot only for its date range and TTL', () => {
|
||||||
|
clearManualMonitoringSnapshot();
|
||||||
|
storeManualMonitoringSnapshot(result, 1_000);
|
||||||
|
assert.ok(getManualMonitoringSnapshot('2026-08-07', '2026-08-07', 60_999));
|
||||||
|
assert.equal(getManualMonitoringSnapshot('2026-08-06', '2026-08-06', 60_999), null);
|
||||||
|
assert.equal(getManualMonitoringSnapshot('2026-08-07', '2026-08-07', 61_000), null);
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { RangeMileageResult } from './cache.js';
|
||||||
|
|
||||||
|
const MANUAL_SNAPSHOT_TTL_MS = 60 * 1000;
|
||||||
|
|
||||||
|
interface ManualSnapshot {
|
||||||
|
result: RangeMileageResult;
|
||||||
|
updatedAt: string;
|
||||||
|
expiresAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshot: ManualSnapshot | null = null;
|
||||||
|
|
||||||
|
export function storeManualMonitoringSnapshot(
|
||||||
|
result: RangeMileageResult,
|
||||||
|
now = Date.now(),
|
||||||
|
): ManualSnapshot {
|
||||||
|
snapshot = {
|
||||||
|
result,
|
||||||
|
updatedAt: new Date(now).toISOString(),
|
||||||
|
expiresAt: now + MANUAL_SNAPSHOT_TTL_MS,
|
||||||
|
};
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getManualMonitoringSnapshot(
|
||||||
|
start: string,
|
||||||
|
end: string,
|
||||||
|
now = Date.now(),
|
||||||
|
): ManualSnapshot | null {
|
||||||
|
if (!snapshot || snapshot.expiresAt <= now) {
|
||||||
|
snapshot = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (snapshot.result.start !== start || snapshot.result.end !== end) return null;
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearManualMonitoringSnapshot(): void {
|
||||||
|
snapshot = null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
isMonitoringCacheEligible,
|
||||||
|
isMonitoringSnapshotScope,
|
||||||
|
shanghaiDate,
|
||||||
|
} from './monitoring-cache-policy.js';
|
||||||
|
|
||||||
|
test('uses the monitoring snapshot only for the current instrument day', () => {
|
||||||
|
const base = {
|
||||||
|
startDate: '2026-08-07',
|
||||||
|
endDate: '2026-08-07',
|
||||||
|
today: '2026-08-07',
|
||||||
|
sourcePriority: ['instrument'] as const,
|
||||||
|
force: false,
|
||||||
|
};
|
||||||
|
assert.equal(isMonitoringCacheEligible({ ...base, sourcePriority: [...base.sourcePriority] }), true);
|
||||||
|
assert.equal(isMonitoringCacheEligible({ ...base, startDate: '2026-08-06', sourcePriority: ['instrument'] }), false);
|
||||||
|
assert.equal(isMonitoringCacheEligible({ ...base, sourcePriority: ['gps'] }), false);
|
||||||
|
assert.equal(isMonitoringCacheEligible({ ...base, sourcePriority: ['instrument'], force: true }), false);
|
||||||
|
assert.equal(isMonitoringSnapshotScope({ ...base, sourcePriority: ['instrument'] }), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formats the current date in Asia/Shanghai', () => {
|
||||||
|
assert.equal(shanghaiDate(new Date('2026-08-06T16:30:00.000Z')), '2026-08-07');
|
||||||
|
});
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { MileageSourceGroup } from './source-policy.js';
|
||||||
|
|
||||||
|
export function isMonitoringCacheEligible(input: {
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
today: string;
|
||||||
|
sourcePriority: MileageSourceGroup[];
|
||||||
|
force: boolean;
|
||||||
|
}): boolean {
|
||||||
|
return !input.force && isMonitoringSnapshotScope(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMonitoringSnapshotScope(input: {
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
today: string;
|
||||||
|
sourcePriority: MileageSourceGroup[];
|
||||||
|
}): boolean {
|
||||||
|
return input.startDate === input.today
|
||||||
|
&& input.endDate === input.today
|
||||||
|
&& input.sourcePriority.length === 1
|
||||||
|
&& input.sourcePriority[0] === 'instrument';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shanghaiDate(now = new Date()): string {
|
||||||
|
return new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: 'Asia/Shanghai',
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
}).format(now);
|
||||||
|
}
|
||||||
@@ -8,6 +8,15 @@ import {
|
|||||||
parseMileageSourcePriority,
|
parseMileageSourcePriority,
|
||||||
protocolsForSourcePriority,
|
protocolsForSourcePriority,
|
||||||
} from './source-policy.js';
|
} from './source-policy.js';
|
||||||
|
import {
|
||||||
|
isMonitoringCacheEligible,
|
||||||
|
isMonitoringSnapshotScope,
|
||||||
|
shanghaiDate,
|
||||||
|
} from './monitoring-cache-policy.js';
|
||||||
|
import {
|
||||||
|
getManualMonitoringSnapshot,
|
||||||
|
storeManualMonitoringSnapshot,
|
||||||
|
} from './manual-snapshot.js';
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
@@ -115,6 +124,7 @@ app.get('/', async (c) => {
|
|||||||
const range = normalizeRange(c.req.query('startDate') || '', c.req.query('endDate') || '');
|
const range = normalizeRange(c.req.query('startDate') || '', c.req.query('endDate') || '');
|
||||||
const sourcePriority = parseMileageSourcePriority(c.req.query('sourcePriority'));
|
const sourcePriority = parseMileageSourcePriority(c.req.query('sourcePriority'));
|
||||||
const protocolPriority = protocolsForSourcePriority(sourcePriority);
|
const protocolPriority = protocolsForSourcePriority(sourcePriority);
|
||||||
|
const force = c.req.query('force') === '1';
|
||||||
|
|
||||||
const filterParams = {
|
const filterParams = {
|
||||||
search: c.req.query('search') || '',
|
search: c.req.query('search') || '',
|
||||||
@@ -136,18 +146,67 @@ app.get('/', async (c) => {
|
|||||||
let filters: MonitoringFilters;
|
let filters: MonitoringFilters;
|
||||||
let rangeDailyTotals: { date: string; totalKm: number }[] | undefined;
|
let rangeDailyTotals: { date: string; totalKm: number }[] | undefined;
|
||||||
let dateRange: { start: string; end: string } | undefined;
|
let dateRange: { start: string; end: string } | undefined;
|
||||||
|
let dataUpdatedAt: string | undefined;
|
||||||
|
let cacheStatus: 'hit' | 'manual-hit' | 'refresh' | 'miss' | 'bypass' = 'bypass';
|
||||||
|
|
||||||
if (range) {
|
if (range) {
|
||||||
|
const cache = getCache();
|
||||||
|
const today = shanghaiDate();
|
||||||
|
const snapshotScope = isMonitoringSnapshotScope({
|
||||||
|
startDate: range.start,
|
||||||
|
endDate: range.end,
|
||||||
|
today,
|
||||||
|
sourcePriority,
|
||||||
|
});
|
||||||
|
const cacheEligible = isMonitoringCacheEligible({
|
||||||
|
startDate: range.start,
|
||||||
|
endDate: range.end,
|
||||||
|
today,
|
||||||
|
sourcePriority,
|
||||||
|
force,
|
||||||
|
});
|
||||||
|
const manualSnapshot = cacheEligible
|
||||||
|
? getManualMonitoringSnapshot(range.start, range.end)
|
||||||
|
: null;
|
||||||
|
if (manualSnapshot) {
|
||||||
|
cacheStatus = 'manual-hit';
|
||||||
|
allVehicles = manualSnapshot.result.vehicles;
|
||||||
|
rangeDailyTotals = manualSnapshot.result.dailyTotals;
|
||||||
|
dateRange = { start: manualSnapshot.result.start, end: manualSnapshot.result.end };
|
||||||
|
dataUpdatedAt = manualSnapshot.updatedAt;
|
||||||
|
filters = buildDateFilters(allVehicles);
|
||||||
|
} else if (cacheEligible && cache) {
|
||||||
|
cacheStatus = 'hit';
|
||||||
|
allVehicles = cache.vehicles.map(vehicle => ({
|
||||||
|
...vehicle,
|
||||||
|
dailyMileage: { [range.start]: vehicle.dailyKm },
|
||||||
|
dailySourceProtocols: { [range.start]: vehicle.sourceProtocol },
|
||||||
|
}));
|
||||||
|
rangeDailyTotals = [{
|
||||||
|
date: range.start,
|
||||||
|
totalKm: Math.round(allVehicles.reduce((sum, vehicle) => sum + vehicle.dailyKm, 0)),
|
||||||
|
}];
|
||||||
|
dateRange = { ...range };
|
||||||
|
dataUpdatedAt = cache.updatedAt;
|
||||||
|
filters = cache.filters;
|
||||||
|
} else {
|
||||||
|
cacheStatus = cacheEligible ? 'miss' : 'bypass';
|
||||||
try {
|
try {
|
||||||
const result = await queryRangeMileage(range.start, range.end, protocolPriority);
|
const result = await queryRangeMileage(range.start, range.end, protocolPriority);
|
||||||
allVehicles = result.vehicles;
|
allVehicles = result.vehicles;
|
||||||
rangeDailyTotals = result.dailyTotals;
|
rangeDailyTotals = result.dailyTotals;
|
||||||
dateRange = { start: result.start, end: result.end };
|
dateRange = { start: result.start, end: result.end };
|
||||||
filters = buildDateFilters(allVehicles);
|
filters = buildDateFilters(allVehicles);
|
||||||
|
if (force && snapshotScope) {
|
||||||
|
const stored = storeManualMonitoringSnapshot(result);
|
||||||
|
cacheStatus = 'refresh';
|
||||||
|
dataUpdatedAt = stored.updatedAt;
|
||||||
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
console.error('monitoring range query error:', e);
|
console.error('monitoring range query error:', e);
|
||||||
return c.json(EMPTY_RESPONSE, 500);
|
return c.json(EMPTY_RESPONSE, 500);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if (date) {
|
} else if (date) {
|
||||||
try {
|
try {
|
||||||
allVehicles = await queryDateMileage(date, protocolPriority);
|
allVehicles = await queryDateMileage(date, protocolPriority);
|
||||||
@@ -218,6 +277,7 @@ app.get('/', async (c) => {
|
|||||||
const paged = sorted.slice(offset, offset + limit);
|
const paged = sorted.slice(offset, offset + limit);
|
||||||
const total = filtered.length;
|
const total = filtered.length;
|
||||||
|
|
||||||
|
c.header('X-Mileage-Cache', cacheStatus);
|
||||||
return c.json({
|
return c.json({
|
||||||
vehicles: maskCustomerNames(paged),
|
vehicles: maskCustomerNames(paged),
|
||||||
stats,
|
stats,
|
||||||
@@ -227,7 +287,7 @@ app.get('/', async (c) => {
|
|||||||
total,
|
total,
|
||||||
page,
|
page,
|
||||||
totalPages: Math.ceil(total / limit),
|
totalPages: Math.ceil(total / limit),
|
||||||
updatedAt: dateRange?.end || date || getCache()?.updatedAt || new Date().toISOString(),
|
updatedAt: dataUpdatedAt || dateRange?.end || date || getCache()?.updatedAt || new Date().toISOString(),
|
||||||
sourcePriority,
|
sourcePriority,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user