feat: integrate OneOS mileage APIs and release v1.1.10
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
kkfluous
2026-07-23 12:19:11 +08:00
parent 4d523cbce0
commit 3f29bed5fa
14 changed files with 863 additions and 325 deletions

View File

@@ -1,7 +1,7 @@
import { Hono } from 'hono';
import pool from '../../db.js';
import mileagePool from '../../mileage-db.js';
import { fetchVehicleInfoMap } from '../mileage/vehicle-info.js';
import { fetchOneOsMileageDates } from '../mileage/oneos-api.js';
import { mapRegion } from '../vehicles.js';
import { filterByPermission, maskCustomerNames } from '../../auth/permissions.js';
import { classifyVehicle, generateSuggestions } from './algorithm.js';
@@ -42,6 +42,24 @@ function classifyVehicleType(typeName: string, _modelRaw: string): string {
return t || '其他';
}
function recentCompletedDates(count: number): string[] {
const today = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(new Date());
const [year, month, day] = today.split('-').map(Number);
const base = new Date(Date.UTC(year, month - 1, day));
const dates: string[] = [];
for (let offset = count; offset >= 1; offset -= 1) {
const date = new Date(base);
date.setUTCDate(base.getUTCDate() - offset);
dates.push(date.toISOString().slice(0, 10));
}
return dates;
}
// ---------------------------------------------------------------------------
// Route
// ---------------------------------------------------------------------------
@@ -114,29 +132,33 @@ app.get('/', async (c) => {
// ---- Collect all plates for Query 6 ----
const allPlates = assessmentRows.map((r: any) => r.plate_number as string);
// ---- Query 6: Customer daily avg (from mileage DB) — 30d baseline + 7d recent ----
// ---- Query 6: Customer daily avg (OneOS only) — 30d baseline + 7d recent ----
const customerAvgDailyMap = new Map<string, number>();
const customerAvgDaily7dMap = new Map<string, number>();
if (allPlates.length > 0) {
const placeholders = allPlates.map(() => '?').join(',');
// Single query returning both windows per plate.
const [dailyRows] = await mileagePool.execute(
`SELECT plate,
AVG(CASE WHEN stat_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) THEN daily_km END) AS avg_30d,
AVG(CASE WHEN stat_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) THEN daily_km END) AS avg_7d
FROM v_vehicle_daily_stats
WHERE stat_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
AND stat_date < CURDATE()
AND plate IN (${placeholders})
GROUP BY plate`,
allPlates,
) as [any[], unknown];
const dates = recentCompletedDates(30);
const sevenDayDates = new Set(dates.slice(-7));
const allowedPlates = new Set(allPlates);
const rowsByDate = await fetchOneOsMileageDates(dates);
const aggregates = new Map<string, { sum30: number; count30: number; sum7: number; count7: number }>();
for (const [date, rows] of rowsByDate) {
for (const row of rows) {
if (!allowedPlates.has(row.plateNumber) || row.status !== 'NORMAL' || row.dailyMileageKm === null) continue;
const current = aggregates.get(row.plateNumber) || { sum30: 0, count30: 0, sum7: 0, count7: 0 };
current.sum30 += row.dailyMileageKm;
current.count30 += 1;
if (sevenDayDates.has(date)) {
current.sum7 += row.dailyMileageKm;
current.count7 += 1;
}
aggregates.set(row.plateNumber, current);
}
}
const plateAvg30Map = new Map<string, number>();
const plateAvg7Map = new Map<string, number>();
for (const row of dailyRows) {
if (row.avg_30d !== null) plateAvg30Map.set(row.plate, Number(row.avg_30d));
if (row.avg_7d !== null) plateAvg7Map.set(row.plate, Number(row.avg_7d));
for (const [plate, values] of aggregates) {
if (values.count30 > 0) plateAvg30Map.set(plate, values.sum30 / values.count30);
if (values.count7 > 0) plateAvg7Map.set(plate, values.sum7 / values.count7);
}
const customerPlates30 = new Map<string, number[]>();