refactor: modularize application domains

This commit is contained in:
kkfluous
2026-08-13 12:03:34 +08:00
parent d610e4b841
commit 06fe75b4c7
142 changed files with 14645 additions and 8885 deletions
+49 -114
View File
@@ -2,25 +2,16 @@ import { Hono } from 'hono';
import pool from '../db.js';
import heatmapPool from '../heatmap-db.js';
import { getCache } from './mileage/cache.js';
type HeatmapRecord = {
date: string;
vin: string;
plate: string;
time: string;
longitude: number;
latitude: number;
source: string;
sourceRecordId: string;
};
type VehicleRank = {
vin: string;
plateNumber: string;
locationCount: number;
firstSeen: string;
lastSeen: string;
};
import {
buildVehicleGrid,
buildVehicleRanking,
filterVehicleRecordsByRadius,
parseNearbyQuery,
parseVehicleHeatmapQuery,
summarizeVehicleRecords,
vehicleGridPrecision,
type VehicleHeatmapRecord,
} from './vehicle-heatmap-model.js';
type MetaRow = {
start_date: string;
@@ -55,16 +46,6 @@ type BatchModelRow = {
plate_number: string;
};
function isDate(value: string | undefined): value is string {
return Boolean(value && /^\d{4}-\d{2}-\d{2}$/.test(value));
}
function daysInclusive(startDate: string, endDate: string): number {
const start = Date.parse(`${startDate}T00:00:00+08:00`);
const end = Date.parse(`${endDate}T00:00:00+08:00`);
return Math.max(0, Math.floor((end - start) / 86_400_000) + 1);
}
async function loadBatchModelPlates(): Promise<Map<string, Set<string>>> {
const cached = getCache()?.targetPlatesMap;
if (cached?.size) return cached;
@@ -95,7 +76,7 @@ async function loadRecords(
endDate: string,
query: string,
batchModel: string,
): Promise<HeatmapRecord[]> {
): Promise<VehicleHeatmapRecord[]> {
let batchPlates: string[] = [];
if (batchModel) {
const modelPlates = await loadBatchModelPlates();
@@ -134,41 +115,6 @@ async function loadRecords(
}));
}
function buildRanking(records: HeatmapRecord[], limit = 10): VehicleRank[] {
const vehicles = new Map<string, VehicleRank>();
for (const record of records) {
const current = vehicles.get(record.vin);
if (!current) {
vehicles.set(record.vin, {
vin: record.vin,
plateNumber: record.plate,
locationCount: 1,
firstSeen: record.time,
lastSeen: record.time,
});
continue;
}
current.locationCount += 1;
if (record.time < current.firstSeen) current.firstSeen = record.time;
if (record.time > current.lastSeen) current.lastSeen = record.time;
if (!current.plateNumber && record.plate) current.plateNumber = record.plate;
}
return [...vehicles.values()]
.sort((left, right) => right.locationCount - left.locationCount
|| left.plateNumber.localeCompare(right.plateNumber, 'zh-CN'))
.slice(0, limit);
}
function haversineKm(leftLng: number, leftLat: number, rightLng: number, rightLat: number): number {
const radians = (degrees: number) => degrees * Math.PI / 180;
const earthRadius = 6371;
const dLat = radians(rightLat - leftLat);
const dLng = radians(rightLng - leftLng);
const a = Math.sin(dLat / 2) ** 2
+ Math.cos(radians(leftLat)) * Math.cos(radians(rightLat)) * Math.sin(dLng / 2) ** 2;
return earthRadius * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
const router = new Hono();
router.get('/config', (c) => {
@@ -223,76 +169,65 @@ router.get('/meta', async (c) => {
});
router.get('/points', async (c) => {
const requestedStart = c.req.query('startDate');
const requestedEnd = c.req.query('endDate');
const startDate = isDate(requestedStart) ? requestedStart : '2026-01-01';
const endDate = isDate(requestedEnd) ? requestedEnd : '2026-07-13';
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 query = c.req.query('query') || '';
const batchModel = (c.req.query('batchModel') || '').trim();
const metric = c.req.query('metric') === 'vehicles' ? 'vehicles' : 'locations';
const records = await loadRecords(startDate, endDate, query, batchModel);
const precision = daysInclusive(startDate, endDate) <= 14 || Boolean(query || batchModel) ? 3 : 2;
const grid = new Map<string, { lng: number; lat: number; count: number; vins?: Set<string> }>();
for (const record of records) {
const lng = Number(record.longitude.toFixed(precision));
const lat = Number(record.latitude.toFixed(precision));
const key = `${lng},${lat}`;
let point = grid.get(key);
if (!point) {
point = { lng, lat, count: 0, vins: metric === 'vehicles' ? new Set<string>() : undefined };
grid.set(key, point);
}
if (point.vins) {
point.vins.add(record.vin);
point.count = point.vins.size;
} else {
point.count += 1;
}
}
const points = [...grid.values()].map(({ lng, lat, count }) => ({ lng, lat, count }));
let max = 1;
for (const point of points) max = Math.max(max, point.count);
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: records.length,
vehicleCount: new Set(records.map((record) => record.vin)).size,
dayCount: new Set(records.map((record) => record.date)).size,
locationCount: summary.locationCount,
vehicleCount: summary.vehicleCount,
dayCount: summary.dayCount,
points,
max,
topVehicles: buildRanking(records),
topVehicles: buildVehicleRanking(records),
});
});
router.get('/nearby', async (c) => {
const longitude = Number(c.req.query('lng'));
const latitude = Number(c.req.query('lat'));
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
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 = isDate(c.req.query('startDate')) ? c.req.query('startDate')! : '2026-01-01';
const endDate = isDate(c.req.query('endDate')) ? c.req.query('endDate')! : '2026-07-13';
const query = c.req.query('query') || '';
const batchModel = (c.req.query('batchModel') || '').trim();
const radiusKm = Math.min(200, Math.max(5, Number(c.req.query('radiusKm')) || 50));
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 = records.filter((record) => (
haversineKm(longitude, latitude, record.longitude, record.latitude) <= radiusKm
));
const nearby = filterVehicleRecordsByRadius(records, center, radiusKm);
const summary = summarizeVehicleRecords(nearby);
return c.json({
center: { lng: longitude, lat: latitude },
center,
radiusKm,
locationCount: nearby.length,
vehicleCount: new Set(nearby.map((record) => record.vin)).size,
topVehicles: buildRanking(nearby),
locationCount: summary.locationCount,
vehicleCount: summary.vehicleCount,
topVehicles: buildVehicleRanking(nearby),
});
});