perf: reuse mileage monitoring snapshots
This commit is contained in:
@@ -565,7 +565,7 @@ export default function MonitoringView() {
|
||||
}, [filterTargetNames]);
|
||||
|
||||
// 加载首页数据
|
||||
const loadFirstPage = useCallback((showPageLoading = true) => {
|
||||
const loadFirstPage = useCallback((showPageLoading = true, force = false) => {
|
||||
if (showPageLoading) setPageLoading(true);
|
||||
setPageError(null);
|
||||
return fetchMonitoring({
|
||||
@@ -589,6 +589,7 @@ export default function MonitoringView() {
|
||||
endDate: rangeEnd || undefined,
|
||||
brands: filterBrands.length > 0 ? filterBrands : undefined,
|
||||
sourcePriority,
|
||||
force,
|
||||
}).then(d => {
|
||||
setVehicles(d.vehicles);
|
||||
setStats(d.stats);
|
||||
@@ -613,7 +614,7 @@ export default function MonitoringView() {
|
||||
if (manualRefreshing) return;
|
||||
setManualRefreshing(true);
|
||||
try {
|
||||
await loadFirstPage(false);
|
||||
await loadFirstPage(false, true);
|
||||
} finally {
|
||||
setManualRefreshing(false);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export async function fetchMonitoring(params?: {
|
||||
endDate?: string;
|
||||
brands?: string[];
|
||||
sourcePriority?: MileageSourceGroup[];
|
||||
force?: boolean;
|
||||
}): Promise<MonitoringData> {
|
||||
const query = new URLSearchParams();
|
||||
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?.endDate) query.set('endDate', params.endDate);
|
||||
if (params?.sourcePriority?.length) query.set('sourcePriority', params.sourcePriority.join(','));
|
||||
if (params?.force) query.set('force', '1');
|
||||
const qs = query.toString();
|
||||
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,
|
||||
protocolsForSourcePriority,
|
||||
} from './source-policy.js';
|
||||
import {
|
||||
isMonitoringCacheEligible,
|
||||
isMonitoringSnapshotScope,
|
||||
shanghaiDate,
|
||||
} from './monitoring-cache-policy.js';
|
||||
import {
|
||||
getManualMonitoringSnapshot,
|
||||
storeManualMonitoringSnapshot,
|
||||
} from './manual-snapshot.js';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
@@ -115,6 +124,7 @@ app.get('/', async (c) => {
|
||||
const range = normalizeRange(c.req.query('startDate') || '', c.req.query('endDate') || '');
|
||||
const sourcePriority = parseMileageSourcePriority(c.req.query('sourcePriority'));
|
||||
const protocolPriority = protocolsForSourcePriority(sourcePriority);
|
||||
const force = c.req.query('force') === '1';
|
||||
|
||||
const filterParams = {
|
||||
search: c.req.query('search') || '',
|
||||
@@ -136,17 +146,66 @@ app.get('/', async (c) => {
|
||||
let filters: MonitoringFilters;
|
||||
let rangeDailyTotals: { date: string; totalKm: number }[] | undefined;
|
||||
let dateRange: { start: string; end: string } | undefined;
|
||||
let dataUpdatedAt: string | undefined;
|
||||
let cacheStatus: 'hit' | 'manual-hit' | 'refresh' | 'miss' | 'bypass' = 'bypass';
|
||||
|
||||
if (range) {
|
||||
try {
|
||||
const result = await queryRangeMileage(range.start, range.end, protocolPriority);
|
||||
allVehicles = result.vehicles;
|
||||
rangeDailyTotals = result.dailyTotals;
|
||||
dateRange = { start: result.start, end: result.end };
|
||||
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);
|
||||
} catch (e: unknown) {
|
||||
console.error('monitoring range query error:', e);
|
||||
return c.json(EMPTY_RESPONSE, 500);
|
||||
} 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 {
|
||||
const result = await queryRangeMileage(range.start, range.end, protocolPriority);
|
||||
allVehicles = result.vehicles;
|
||||
rangeDailyTotals = result.dailyTotals;
|
||||
dateRange = { start: result.start, end: result.end };
|
||||
filters = buildDateFilters(allVehicles);
|
||||
if (force && snapshotScope) {
|
||||
const stored = storeManualMonitoringSnapshot(result);
|
||||
cacheStatus = 'refresh';
|
||||
dataUpdatedAt = stored.updatedAt;
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
console.error('monitoring range query error:', e);
|
||||
return c.json(EMPTY_RESPONSE, 500);
|
||||
}
|
||||
}
|
||||
} else if (date) {
|
||||
try {
|
||||
@@ -218,6 +277,7 @@ app.get('/', async (c) => {
|
||||
const paged = sorted.slice(offset, offset + limit);
|
||||
const total = filtered.length;
|
||||
|
||||
c.header('X-Mileage-Cache', cacheStatus);
|
||||
return c.json({
|
||||
vehicles: maskCustomerNames(paged),
|
||||
stats,
|
||||
@@ -227,7 +287,7 @@ app.get('/', async (c) => {
|
||||
total,
|
||||
page,
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user