perf: 实时监控性能优化
- 后端:车辆关联信息缓存5分钟、两库并行查询、支持服务端 筛选/排序/分页(默认返回100条) - 前端:筛选和排序参数传给后端,不再加载全量数据 - 筛选选项(部门/客户/车牌)仅首次加载获取 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -106,14 +106,43 @@ export default function MonitoringView() {
|
|||||||
const [filterRegionCode, setFilterRegionCode] = useState('All');
|
const [filterRegionCode, setFilterRegionCode] = useState('All');
|
||||||
const [filterMileageRange, setFilterMileageRange] = useState({ min: '', max: '' });
|
const [filterMileageRange, setFilterMileageRange] = useState({ min: '', max: '' });
|
||||||
|
|
||||||
const [allVehicles, setAllVehicles] = useState<MonitoringVehicle[]>([]);
|
const [filteredVehicles, setFilteredVehicles] = useState<MonitoringVehicle[]>([]);
|
||||||
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
|
const [departments, setDepartments] = useState<string[]>([]);
|
||||||
|
const [plateNumbers, setPlateNumbers] = useState<string[]>([]);
|
||||||
|
const [projects, setProjects] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// 加载筛选选项(仅首次,用轻量请求)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const load = () => fetchMonitoring().then(d => setAllVehicles(d.vehicles)).catch(() => {});
|
fetchMonitoring({ limit: 2000 }).then(d => {
|
||||||
|
const depts = Array.from(new Set(d.vehicles.map(v => v.department).filter(Boolean))) as string[];
|
||||||
|
const plates = Array.from(new Set(d.vehicles.map(v => v.plate).filter(Boolean)));
|
||||||
|
const custs = Array.from(new Set(d.vehicles.map(v => v.customer).filter(Boolean))) as string[];
|
||||||
|
setDepartments(depts);
|
||||||
|
setPlateNumbers(plates);
|
||||||
|
setProjects(custs);
|
||||||
|
}).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 加载数据(带服务端筛选/排序/分页)
|
||||||
|
useEffect(() => {
|
||||||
|
const load = () => {
|
||||||
|
fetchMonitoring({
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
limit: 100,
|
||||||
|
search: searchTerm || undefined,
|
||||||
|
dept: filterDept !== 'All' ? filterDept : undefined,
|
||||||
|
customer: filterProject !== 'All' ? filterProject : undefined,
|
||||||
|
}).then(d => {
|
||||||
|
setFilteredVehicles(d.vehicles);
|
||||||
|
setTotalCount(d.total);
|
||||||
|
}).catch(() => {});
|
||||||
|
};
|
||||||
load();
|
load();
|
||||||
const interval = setInterval(load, 60000);
|
const interval = setInterval(load, 60000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, [sortBy, sortOrder, searchTerm, filterDept, filterProject]);
|
||||||
|
|
||||||
const toggleFullscreen = () => {
|
const toggleFullscreen = () => {
|
||||||
if (!isFullscreen) {
|
if (!isFullscreen) {
|
||||||
@@ -129,34 +158,6 @@ export default function MonitoringView() {
|
|||||||
setIsFullscreen(!isFullscreen);
|
setIsFullscreen(!isFullscreen);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredVehicles = useMemo(() => {
|
|
||||||
return allVehicles.filter(v => {
|
|
||||||
const matchesSearch = v.plate.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
(v.customer || '').toLowerCase().includes(searchTerm.toLowerCase());
|
|
||||||
const matchesDept = filterDept === 'All' || v.department === filterDept;
|
|
||||||
const matchesPlate = filterPlate === 'All' || v.plate === filterPlate;
|
|
||||||
const matchesProject = filterProject === 'All' || v.customer === filterProject;
|
|
||||||
const matchesEntity = filterEntity === 'All' || true;
|
|
||||||
const matchesRegion = filterRegionCode === 'All' || true;
|
|
||||||
|
|
||||||
// Mileage range filter
|
|
||||||
const mileage = v.dailyKm || 0;
|
|
||||||
const minMileage = filterMileageRange.min === '' ? -Infinity : Number(filterMileageRange.min);
|
|
||||||
const maxMileage = filterMileageRange.max === '' ? Infinity : Number(filterMileageRange.max);
|
|
||||||
const matchesMileage = mileage >= minMileage && mileage <= maxMileage;
|
|
||||||
|
|
||||||
return matchesSearch && matchesDept && matchesPlate && matchesProject && matchesEntity && matchesRegion && matchesMileage;
|
|
||||||
}).sort((a, b) => {
|
|
||||||
const valA = sortBy === 'today' ? (a.dailyKm || 0) : (a.totalKm || 0);
|
|
||||||
const valB = sortBy === 'today' ? (b.dailyKm || 0) : (b.totalKm || 0);
|
|
||||||
return sortOrder === 'desc' ? valB - valA : valA - valB;
|
|
||||||
});
|
|
||||||
}, [allVehicles, searchTerm, filterDept, sortBy, sortOrder, filterPlate, filterProject, filterEntity, filterRegionCode, filterMileageRange]);
|
|
||||||
|
|
||||||
const departments = Array.from(new Set(allVehicles.map(v => v.department).filter(Boolean))) as string[];
|
|
||||||
const plateNumbers = Array.from(new Set(allVehicles.map(v => v.plate).filter(Boolean)));
|
|
||||||
const projects = Array.from(new Set(allVehicles.map(v => v.customer).filter(Boolean))) as string[];
|
|
||||||
|
|
||||||
const stats = useMemo(() => {
|
const stats = useMemo(() => {
|
||||||
const totalToday = filteredVehicles.reduce((sum, v) => sum + (v.dailyKm || 0), 0);
|
const totalToday = filteredVehicles.reduce((sum, v) => sum + (v.dailyKm || 0), 0);
|
||||||
const totalAll = filteredVehicles.reduce((sum, v) => sum + (v.totalKm || 0), 0);
|
const totalAll = filteredVehicles.reduce((sum, v) => sum + (v.totalKm || 0), 0);
|
||||||
@@ -164,8 +165,8 @@ export default function MonitoringView() {
|
|||||||
const activeTotal = sortBy === 'today' ? totalToday : totalAll;
|
const activeTotal = sortBy === 'today' ? totalToday : totalAll;
|
||||||
const activeAvg = filteredVehicles.length > 0 ? activeTotal / filteredVehicles.length : 0;
|
const activeAvg = filteredVehicles.length > 0 ? activeTotal / filteredVehicles.length : 0;
|
||||||
|
|
||||||
return { totalToday, totalAll, activeTotal, activeAvg, count: filteredVehicles.length };
|
return { totalToday, totalAll, activeTotal, activeAvg, count: totalCount };
|
||||||
}, [filteredVehicles, sortBy]);
|
}, [filteredVehicles, sortBy, totalCount]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -8,8 +8,25 @@ async function fetchJson<T>(url: string): Promise<T> {
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchMonitoring(): Promise<MonitoringData> {
|
export async function fetchMonitoring(params?: {
|
||||||
return fetchJson<MonitoringData>(`${BASE}/monitoring`);
|
sortBy?: string;
|
||||||
|
sortOrder?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
search?: string;
|
||||||
|
dept?: string;
|
||||||
|
customer?: string;
|
||||||
|
}): Promise<MonitoringData> {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
if (params?.sortBy) query.set('sortBy', params.sortBy);
|
||||||
|
if (params?.sortOrder) query.set('sortOrder', params.sortOrder);
|
||||||
|
if (params?.limit) query.set('limit', String(params.limit));
|
||||||
|
if (params?.offset) query.set('offset', String(params.offset));
|
||||||
|
if (params?.search) query.set('search', params.search);
|
||||||
|
if (params?.dept) query.set('dept', params.dept);
|
||||||
|
if (params?.customer) query.set('customer', params.customer);
|
||||||
|
const qs = query.toString();
|
||||||
|
return fetchJson<MonitoringData>(`${BASE}/monitoring${qs ? `?${qs}` : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTargets(): Promise<TargetSummary[]> {
|
export async function fetchTargets(): Promise<TargetSummary[]> {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface MonitoringVehicle {
|
|||||||
|
|
||||||
export interface MonitoringData {
|
export interface MonitoringData {
|
||||||
vehicles: MonitoringVehicle[];
|
vehicles: MonitoringVehicle[];
|
||||||
|
total: number;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,41 +18,64 @@ 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
|
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`;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// GET /monitoring — 实时监控数据
|
// GET /monitoring — 实时监控数据
|
||||||
app.get('/monitoring', async (c) => {
|
app.get('/monitoring', async (c) => {
|
||||||
try {
|
try {
|
||||||
// 1. 从 hydrogen_energy 取最新日期的里程数据
|
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') || '';
|
||||||
|
|
||||||
|
// 并行查询两个数据库
|
||||||
|
const [mileageResult, infoMap] = await Promise.all([
|
||||||
|
(async () => {
|
||||||
const [dateRows] = await mileagePool.execute(
|
const [dateRows] = await mileagePool.execute(
|
||||||
'SELECT MAX(stat_date) as latest FROM v_vehicle_daily_stats'
|
'SELECT MAX(stat_date) as latest FROM v_vehicle_daily_stats'
|
||||||
) as any;
|
) as any;
|
||||||
const latestDate = dateRows[0]?.latest;
|
const latestDate = dateRows[0]?.latest;
|
||||||
if (!latestDate) return c.json({ vehicles: [], updatedAt: new Date().toISOString() });
|
if (!latestDate) return [];
|
||||||
|
const [rows] = await mileagePool.execute(
|
||||||
const [mileageRows] = await mileagePool.execute(
|
|
||||||
`SELECT plate, vin, daily_km, total_km, source
|
`SELECT plate, vin, daily_km, total_km, source
|
||||||
FROM v_vehicle_daily_stats
|
FROM v_vehicle_daily_stats WHERE stat_date = ?`,
|
||||||
WHERE stat_date = ?`,
|
|
||||||
[latestDate]
|
[latestDate]
|
||||||
) as any;
|
) as any;
|
||||||
|
return rows;
|
||||||
|
})(),
|
||||||
|
getVehicleInfoMap(),
|
||||||
|
]);
|
||||||
|
|
||||||
// 对于同一 plate 可能有多条记录(不同 source),取 daily_km 最大的
|
// 去重:同一 plate 取 daily_km 最大的
|
||||||
const mileageMap = new Map<string, any>();
|
const mileageMap = new Map<string, any>();
|
||||||
for (const row of mileageRows) {
|
for (const row of mileageResult) {
|
||||||
const existing = mileageMap.get(row.plate);
|
const existing = mileageMap.get(row.plate);
|
||||||
if (!existing || Number(row.daily_km) > Number(existing.daily_km)) {
|
if (!existing || Number(row.daily_km) > Number(existing.daily_km)) {
|
||||||
mileageMap.set(row.plate, row);
|
mileageMap.set(row.plate, row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 从 lingniu_prod 取车辆关联信息
|
// 合并 + 筛选
|
||||||
const [infoRows] = await pool.execute(VEHICLE_INFO_SQL) as any;
|
let vehicles = Array.from(mileageMap.values()).map((m: any) => {
|
||||||
const infoMap = new Map<string, any>();
|
|
||||||
for (const row of infoRows) {
|
|
||||||
infoMap.set(row.plate, row);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 合并
|
|
||||||
const vehicles = Array.from(mileageMap.values()).map((m: any) => {
|
|
||||||
const info = infoMap.get(m.plate);
|
const info = infoMap.get(m.plate);
|
||||||
const dailyKm = Number(m.daily_km) || 0;
|
const dailyKm = Number(m.daily_km) || 0;
|
||||||
const source = m.source || 'NONE';
|
const source = m.source || 'NONE';
|
||||||
@@ -70,10 +93,33 @@ app.get('/monitoring', async (c) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return c.json({ vehicles, updatedAt: new Date().toISOString() });
|
// 服务端筛选
|
||||||
|
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.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 paged = vehicles.slice(offset, offset + limit);
|
||||||
|
|
||||||
|
return c.json({ vehicles: paged, total, updatedAt: new Date().toISOString() });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('monitoring error:', e);
|
console.error('monitoring error:', e);
|
||||||
return c.json({ vehicles: [], updatedAt: new Date().toISOString() }, 500);
|
return c.json({ vehicles: [], total: 0, updatedAt: new Date().toISOString() }, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user