import { Hono } from 'hono'; import mysqlPool from '../../db/mysql.js'; import heatmapPool from '../../db/heatmap.js'; import { getCache } from '../mileage/cache.js'; import { buildVehicleGrid, buildVehicleRanking, filterVehicleRecordsByRadius, parseNearbyQuery, parseVehicleHeatmapQuery, summarizeVehicleRecords, vehicleGridPrecision, type VehicleHeatmapRecord, } from './model.js'; import { loadBatchModelPlates, loadMeta, loadRecords, loadVehicleOptions, type MysqlDatabase, type PgDatabase, } from './repository.js'; export interface VehicleHeatmapDependencies { mysql: MysqlDatabase; pg: PgDatabase; /** 里程缓存里的考核批次车牌映射(可命中热缓存,避免每次查主库)。 */ cachedBatchPlates?: () => Map> | undefined; } export function registerVehicleHeatmapRoutes(app: Hono, deps: VehicleHeatmapDependencies): void { const { mysql, pg } = deps; /** * 批次 → 车牌映射。优先用里程缓存;缓存缺失时查主库。 * 查询失败按"无批次数据"降级,不影响其他筛选(与既有行为一致)。 */ async function resolveBatchModelPlates(): Promise>> { const cached = deps.cachedBatchPlates?.() ?? getCache()?.targetPlatesMap; if (cached?.size) return cached; try { return await loadBatchModelPlates(mysql); } catch (error) { console.error('[vehicle-heatmap] batch model lookup failed', error); return new Map(); } } async function collectRecords( startDate: string, endDate: string, query: string, batchModel: string, ): Promise { let batchPlates: string[] = []; if (batchModel) { const modelPlates = await resolveBatchModelPlates(); batchPlates = [...(modelPlates.get(batchModel) || [])]; if (batchPlates.length === 0) return []; } return loadRecords(pg, { startDate, endDate, query, batchModelSelected: Boolean(batchModel), batchPlates, }); } app.get('/config', (c) => { const key = process.env.AMAP_WEB_KEY; const securityCode = process.env.AMAP_SECURITY_JS_CODE; if (!key || !securityCode) { return c.json({ error: '高德地图配置缺失' }, 503); } return c.json({ key, securityCode }); }); app.get('/meta', async (c) => { const [meta, vehicleRows, batchModelPlates] = await Promise.all([ loadMeta(pg), loadVehicleOptions(pg), resolveBatchModelPlates(), ]); const vehicles = vehicleRows .map(({ vin, plate_number: plateNumber }) => ({ vin, plateNumber })) .sort((left, right) => left.plateNumber.localeCompare(right.plateNumber, 'zh-CN')); return c.json({ startDate: meta.start_date, endDate: meta.end_date, locationCount: Number(meta.location_count), vehicleCount: Number(meta.vehicle_count), dayCount: Number(meta.day_count), totalLocationCount: Number(meta.total_location_count), excludedLocationCount: Number(meta.excluded_location_count), outsideMainlandCount: Number(meta.outside_mainland_count), tibetCount: Number(meta.tibet_count), batchModels: [...batchModelPlates.keys()].sort((left, right) => left.localeCompare(right, 'zh-CN')), vehicles, }); }); app.get('/points', async (c) => { const { startDate, endDate, query, batchModel, metric } = parseVehicleHeatmapQuery({ startDate: c.req.query('startDate'), endDate: c.req.query('endDate'), query: c.req.query('query'), batchModel: c.req.query('batchModel'), metric: c.req.query('metric'), }); if (startDate > endDate) return c.json({ error: '开始日期不能晚于结束日期' }, 400); const records = await collectRecords(startDate, endDate, query, batchModel); const precision = vehicleGridPrecision(startDate, endDate, query, batchModel); const { points, max } = buildVehicleGrid(records, metric, precision); const summary = summarizeVehicleRecords(records); return c.json({ startDate, endDate, metric, locationCount: summary.locationCount, vehicleCount: summary.vehicleCount, dayCount: summary.dayCount, points, max, topVehicles: buildVehicleRanking(records), }); }); app.get('/nearby', async (c) => { const { center, radiusKm } = parseNearbyQuery({ lng: c.req.query('lng'), lat: c.req.query('lat'), radiusKm: c.req.query('radiusKm'), }); if (!center) { return c.json({ error: '经纬度参数无效' }, 400); } const { startDate, endDate, query, batchModel } = parseVehicleHeatmapQuery({ startDate: c.req.query('startDate'), endDate: c.req.query('endDate'), query: c.req.query('query'), batchModel: c.req.query('batchModel'), }); const records = await collectRecords(startDate, endDate, query, batchModel); const nearby = filterVehicleRecordsByRadius(records, center, radiusKm); const summary = summarizeVehicleRecords(nearby); return c.json({ center, radiusKm, locationCount: summary.locationCount, vehicleCount: summary.vehicleCount, topVehicles: buildVehicleRanking(nearby), }); }); } /** 生产用路由器:绑定真实连接池。 */ export function createVehicleHeatmapRouter(): Hono { const app = new Hono(); registerVehicleHeatmapRoutes(app, { mysql: mysqlPool, pg: heatmapPool }); return app; } const app = createVehicleHeatmapRouter(); export default app;