This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
import { Hono } from 'hono';
|
||||
import pool from '../db.js';
|
||||
import heatmapPool from '../heatmap-db.js';
|
||||
import { getCache } from './mileage/cache.js';
|
||||
import {
|
||||
buildVehicleGrid,
|
||||
buildVehicleRanking,
|
||||
filterVehicleRecordsByRadius,
|
||||
parseNearbyQuery,
|
||||
parseVehicleHeatmapQuery,
|
||||
summarizeVehicleRecords,
|
||||
vehicleGridPrecision,
|
||||
type VehicleHeatmapRecord,
|
||||
} from './vehicle-heatmap-model.js';
|
||||
|
||||
type MetaRow = {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
location_count: string;
|
||||
vehicle_count: string;
|
||||
day_count: string;
|
||||
total_location_count: string;
|
||||
excluded_location_count: string;
|
||||
outside_mainland_count: string;
|
||||
tibet_count: string;
|
||||
};
|
||||
|
||||
type VehicleOptionRow = {
|
||||
vin: string;
|
||||
plate_number: string;
|
||||
};
|
||||
|
||||
type RecordRow = {
|
||||
date: string;
|
||||
vin: string;
|
||||
plate: string;
|
||||
time: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
source: string;
|
||||
source_record_id: string;
|
||||
};
|
||||
|
||||
type BatchModelRow = {
|
||||
target_name: string;
|
||||
plate_number: string;
|
||||
};
|
||||
|
||||
async function loadBatchModelPlates(): Promise<Map<string, Set<string>>> {
|
||||
const cached = getCache()?.targetPlatesMap;
|
||||
if (cached?.size) return cached;
|
||||
|
||||
try {
|
||||
const [rows] = await pool.execute(`
|
||||
select t.target_name, v.plate_number
|
||||
from lingniu_prod.tab_mileage_assessment_target t
|
||||
join lingniu_prod.tab_mileage_assessment_vehicle v
|
||||
on v.target_id = t.id and v.is_deleted = 0
|
||||
where t.is_deleted = 0
|
||||
`) as [BatchModelRow[], unknown];
|
||||
const result = new Map<string, Set<string>>();
|
||||
for (const row of rows) {
|
||||
const plates = result.get(row.target_name) || new Set<string>();
|
||||
plates.add(row.plate_number);
|
||||
result.set(row.target_name, plates);
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('[vehicle-heatmap] batch model lookup failed', error);
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecords(
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
query: string,
|
||||
batchModel: string,
|
||||
): Promise<VehicleHeatmapRecord[]> {
|
||||
let batchPlates: string[] = [];
|
||||
if (batchModel) {
|
||||
const modelPlates = await loadBatchModelPlates();
|
||||
batchPlates = [...(modelPlates.get(batchModel) || [])];
|
||||
if (batchPlates.length === 0) return [];
|
||||
}
|
||||
|
||||
const result = await heatmapPool.query<RecordRow>(`
|
||||
select
|
||||
to_char(stat_date, 'YYYY-MM-DD') as date,
|
||||
vin,
|
||||
plate_number as plate,
|
||||
to_char(first_event_time at time zone 'Asia/Shanghai',
|
||||
'YYYY-MM-DD HH24:MI:SS') as time,
|
||||
longitude,
|
||||
latitude,
|
||||
source_name as source,
|
||||
coalesce(source_record_id::text, '') as source_record_id
|
||||
from analytics.vehicle_daily_first_location
|
||||
where stat_date between $1::date and $2::date
|
||||
and is_heatmap_eligible
|
||||
and ($4::boolean = false or plate_number = any($5::text[]))
|
||||
and (
|
||||
$3 = ''
|
||||
or upper(vin) like '%' || upper($3) || '%'
|
||||
or upper(plate_number) like '%' || upper($3) || '%'
|
||||
)
|
||||
order by stat_date, vin
|
||||
`, [startDate, endDate, query.trim(), Boolean(batchModel), batchPlates]);
|
||||
|
||||
return result.rows.map((row) => ({
|
||||
...row,
|
||||
longitude: Number(row.longitude),
|
||||
latitude: Number(row.latitude),
|
||||
sourceRecordId: row.source_record_id,
|
||||
}));
|
||||
}
|
||||
|
||||
const router = new Hono();
|
||||
|
||||
router.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 });
|
||||
});
|
||||
|
||||
router.get('/meta', async (c) => {
|
||||
const [metaResult, vehiclesResult, batchModelPlates] = await Promise.all([
|
||||
heatmapPool.query<MetaRow>(`
|
||||
select
|
||||
to_char(min(stat_date) filter (where is_heatmap_eligible), 'YYYY-MM-DD') as start_date,
|
||||
to_char(max(stat_date) filter (where is_heatmap_eligible), 'YYYY-MM-DD') as end_date,
|
||||
count(*) filter (where is_heatmap_eligible)::text as location_count,
|
||||
count(distinct vin) filter (where is_heatmap_eligible)::text as vehicle_count,
|
||||
count(distinct stat_date) filter (where is_heatmap_eligible)::text as day_count,
|
||||
count(*)::text as total_location_count,
|
||||
count(*) filter (where not is_heatmap_eligible)::text as excluded_location_count,
|
||||
count(*) filter (where exclusion_reason = 'outside_mainland_china')::text as outside_mainland_count,
|
||||
count(*) filter (where exclusion_reason = 'tibet')::text as tibet_count
|
||||
from analytics.vehicle_daily_first_location
|
||||
`),
|
||||
heatmapPool.query<VehicleOptionRow>(`
|
||||
select distinct on (vin) vin, plate_number
|
||||
from analytics.vehicle_daily_first_location
|
||||
where is_heatmap_eligible
|
||||
order by vin, stat_date desc
|
||||
`),
|
||||
loadBatchModelPlates(),
|
||||
]);
|
||||
const meta = metaResult.rows[0];
|
||||
const vehicles = vehiclesResult.rows
|
||||
.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,
|
||||
});
|
||||
});
|
||||
|
||||
router.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 loadRecords(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),
|
||||
});
|
||||
});
|
||||
|
||||
router.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 loadRecords(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 default router;
|
||||
Reference in New Issue
Block a user