perf: 实时监控改为缓存+分页架构
后端: - 每2分钟刷新全量数据到内存缓存(并行查询两库) - 预计算统计信息(totalToday/totalAll/onlineCount/vehicleCount) - 预提取筛选选项(departments/customers/plates) - API 直接从缓存读取,支持分页(每页50条)+筛选+排序 前端: - KPI 统计使用后端返回的 stats - 车辆列表分页,带翻页控件 - 筛选选项从后端 filters 获取 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,37 +18,36 @@ LEFT JOIN tab_user u ON u.id = c.bd AND u.is_deleted = 0
|
||||
LEFT JOIN tab_department dep ON dep.id = u.dep_id AND dep.is_deleted = 0
|
||||
WHERE truck.is_deleted = 0 AND truck.is_operation = 1`;
|
||||
|
||||
// 车辆关联信息缓存(5分钟过期)
|
||||
let vehicleInfoCache: { data: Map<string, any>; expiry: number } | null = null;
|
||||
const CACHE_TTL = 5 * 60 * 1000;
|
||||
|
||||
async function getVehicleInfoMap(): Promise<Map<string, any>> {
|
||||
const now = Date.now();
|
||||
if (vehicleInfoCache && now < vehicleInfoCache.expiry) {
|
||||
return vehicleInfoCache.data;
|
||||
}
|
||||
const [rows] = await pool.execute(VEHICLE_INFO_SQL) as any;
|
||||
const map = new Map<string, any>();
|
||||
for (const row of rows) {
|
||||
map.set(row.plate, row);
|
||||
}
|
||||
vehicleInfoCache = { data: map, expiry: now + CACHE_TTL };
|
||||
return map;
|
||||
// ========== 实时监控缓存(每2分钟刷新) ==========
|
||||
interface CachedVehicle {
|
||||
plate: string;
|
||||
vin: string;
|
||||
dailyKm: number;
|
||||
totalKm: number | null;
|
||||
source: string;
|
||||
isOnline: boolean;
|
||||
isDataSynced: boolean;
|
||||
customer: string | null;
|
||||
department: string | null;
|
||||
manager: string | null;
|
||||
}
|
||||
|
||||
// GET /monitoring — 实时监控数据
|
||||
app.get('/monitoring', async (c) => {
|
||||
interface MonitoringCache {
|
||||
vehicles: CachedVehicle[];
|
||||
stats: { totalToday: number; totalAll: number; onlineCount: number; vehicleCount: number };
|
||||
filters: { departments: string[]; customers: string[]; plates: string[] };
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
let monitoringCache: MonitoringCache | null = null;
|
||||
|
||||
async function refreshMonitoringCache() {
|
||||
try {
|
||||
const sortBy = c.req.query('sortBy') || 'today';
|
||||
const sortOrder = c.req.query('sortOrder') || 'desc';
|
||||
const limit = Number(c.req.query('limit')) || 100;
|
||||
const offset = Number(c.req.query('offset')) || 0;
|
||||
const search = c.req.query('search') || '';
|
||||
const dept = c.req.query('dept') || '';
|
||||
const customer = c.req.query('customer') || '';
|
||||
console.log('[mileage] refreshing monitoring cache...');
|
||||
const start = Date.now();
|
||||
|
||||
// 并行查询两个数据库
|
||||
const [mileageResult, infoMap] = await Promise.all([
|
||||
const [mileageResult, infoRows] = await Promise.all([
|
||||
(async () => {
|
||||
const [dateRows] = await mileagePool.execute(
|
||||
'SELECT MAX(stat_date) as latest FROM v_vehicle_daily_stats'
|
||||
@@ -62,9 +61,15 @@ app.get('/monitoring', async (c) => {
|
||||
) as any;
|
||||
return rows;
|
||||
})(),
|
||||
getVehicleInfoMap(),
|
||||
pool.execute(VEHICLE_INFO_SQL).then(([rows]) => rows as any[]),
|
||||
]);
|
||||
|
||||
// 车辆关联信息 map
|
||||
const infoMap = new Map<string, any>();
|
||||
for (const row of infoRows) {
|
||||
infoMap.set(row.plate, row);
|
||||
}
|
||||
|
||||
// 去重:同一 plate 取 daily_km 最大的
|
||||
const mileageMap = new Map<string, any>();
|
||||
for (const row of mileageResult) {
|
||||
@@ -74,8 +79,8 @@ app.get('/monitoring', async (c) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 合并 + 筛选
|
||||
let vehicles = Array.from(mileageMap.values()).map((m: any) => {
|
||||
// 合并
|
||||
const vehicles: CachedVehicle[] = Array.from(mileageMap.values()).map((m: any) => {
|
||||
const info = infoMap.get(m.plate);
|
||||
const dailyKm = Number(m.daily_km) || 0;
|
||||
const source = m.source || 'NONE';
|
||||
@@ -93,34 +98,82 @@ app.get('/monitoring', async (c) => {
|
||||
};
|
||||
});
|
||||
|
||||
// 服务端筛选
|
||||
if (search) {
|
||||
const q = search.toLowerCase();
|
||||
vehicles = vehicles.filter(v =>
|
||||
v.plate.toLowerCase().includes(q) ||
|
||||
(v.customer || '').toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
if (dept) vehicles = vehicles.filter(v => v.department === dept);
|
||||
if (customer) vehicles = vehicles.filter(v => v.customer === customer);
|
||||
// 预计算统计信息
|
||||
const totalToday = vehicles.reduce((sum, v) => sum + v.dailyKm, 0);
|
||||
const totalAll = vehicles.reduce((sum, v) => sum + (v.totalKm || 0), 0);
|
||||
const onlineCount = vehicles.filter(v => v.isOnline).length;
|
||||
|
||||
const total = vehicles.length;
|
||||
// 预提取筛选选项
|
||||
const departments = Array.from(new Set(vehicles.map(v => v.department).filter(Boolean))) as string[];
|
||||
const customers = Array.from(new Set(vehicles.map(v => v.customer).filter(Boolean))) as string[];
|
||||
const plates = vehicles.map(v => v.plate);
|
||||
|
||||
// 服务端排序
|
||||
vehicles.sort((a, b) => {
|
||||
const valA = sortBy === 'today' ? a.dailyKm : (a.totalKm || 0);
|
||||
const valB = sortBy === 'today' ? b.dailyKm : (b.totalKm || 0);
|
||||
return sortOrder === 'desc' ? valB - valA : valA - valB;
|
||||
});
|
||||
monitoringCache = {
|
||||
vehicles,
|
||||
stats: { totalToday, totalAll, onlineCount, vehicleCount: vehicles.length },
|
||||
filters: { departments, customers, plates },
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 分页
|
||||
const paged = vehicles.slice(offset, offset + limit);
|
||||
|
||||
return c.json({ vehicles: paged, total, updatedAt: new Date().toISOString() });
|
||||
console.log(`[mileage] cache refreshed: ${vehicles.length} vehicles in ${Date.now() - start}ms`);
|
||||
} catch (e) {
|
||||
console.error('monitoring error:', e);
|
||||
return c.json({ vehicles: [], total: 0, updatedAt: new Date().toISOString() }, 500);
|
||||
console.error('[mileage] cache refresh error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 启动时立即刷新,之后每2分钟刷新
|
||||
refreshMonitoringCache();
|
||||
setInterval(refreshMonitoringCache, 2 * 60 * 1000);
|
||||
|
||||
// GET /monitoring — 从缓存取数据,支持筛选/排序/分页
|
||||
app.get('/monitoring', (c) => {
|
||||
if (!monitoringCache) {
|
||||
return c.json({ vehicles: [], stats: { totalToday: 0, totalAll: 0, onlineCount: 0, vehicleCount: 0 }, filters: { departments: [], customers: [], plates: [] }, total: 0, updatedAt: new Date().toISOString() });
|
||||
}
|
||||
|
||||
const sortBy = c.req.query('sortBy') || 'today';
|
||||
const sortOrder = c.req.query('sortOrder') || 'desc';
|
||||
const limit = Number(c.req.query('limit')) || 50;
|
||||
const page = Number(c.req.query('page')) || 1;
|
||||
const search = c.req.query('search') || '';
|
||||
const dept = c.req.query('dept') || '';
|
||||
const customer = c.req.query('customer') || '';
|
||||
|
||||
let vehicles = monitoringCache.vehicles;
|
||||
|
||||
// 筛选
|
||||
if (search) {
|
||||
const q = search.toLowerCase();
|
||||
vehicles = vehicles.filter(v =>
|
||||
v.plate.toLowerCase().includes(q) ||
|
||||
(v.customer || '').toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
if (dept) vehicles = vehicles.filter(v => v.department === dept);
|
||||
if (customer) vehicles = vehicles.filter(v => v.customer === customer);
|
||||
|
||||
const total = vehicles.length;
|
||||
|
||||
// 排序
|
||||
vehicles = [...vehicles].sort((a, b) => {
|
||||
const valA = sortBy === 'today' ? a.dailyKm : (a.totalKm || 0);
|
||||
const valB = sortBy === 'today' ? b.dailyKm : (b.totalKm || 0);
|
||||
return sortOrder === 'desc' ? valB - valA : valA - valB;
|
||||
});
|
||||
|
||||
// 分页
|
||||
const offset = (page - 1) * limit;
|
||||
const paged = vehicles.slice(offset, offset + limit);
|
||||
|
||||
return c.json({
|
||||
vehicles: paged,
|
||||
stats: monitoringCache.stats,
|
||||
filters: monitoringCache.filters,
|
||||
total,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
updatedAt: monitoringCache.updatedAt,
|
||||
});
|
||||
});
|
||||
|
||||
// GET /targets — 考核项目列表 + 汇总
|
||||
|
||||
Reference in New Issue
Block a user