diff --git a/.env.local.example b/.env.local.example index 1972429..b34d807 100644 --- a/.env.local.example +++ b/.env.local.example @@ -12,3 +12,7 @@ DB_PORT=3306 DB_USER= DB_PASSWORD= DB_NAME= +# Local development uses the public OneOS endpoint; the ECS private IP is not reachable locally. +ONEOS_MILEAGE_API_BASE_URL=https://open.d.lnoneos.com +ONEOS_MILEAGE_API_KEY= +ONEOS_MILEAGE_API_TIMEOUT_MS=20000 diff --git a/src/server/routes/mileage/cache.ts b/src/server/routes/mileage/cache.ts index a54f227..045a0cb 100644 --- a/src/server/routes/mileage/cache.ts +++ b/src/server/routes/mileage/cache.ts @@ -52,7 +52,7 @@ export interface RangeMileageResult { } async function fetchTargetRows(): Promise { - return loadMonitoringTargetRows(pool).then(([rows]) => rows as TargetRow[]); + return loadMonitoringTargetRows(pool).then(rows => rows as unknown as TargetRow[]); } export async function refreshMonitoringCache(): Promise { diff --git a/src/server/routes/mileage/monitoring.test.ts b/src/server/routes/mileage/monitoring.test.ts new file mode 100644 index 0000000..549303e --- /dev/null +++ b/src/server/routes/mileage/monitoring.test.ts @@ -0,0 +1,45 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import pool from '../../db/mysql.js'; +import monitoring from './monitoring.js'; +import { clearOneOsMileageCache } from './oneos-api.js'; + +test('single-day monitoring merges all target rows and handles an empty target table', async (t) => { + const previousUrl = process.env.ONEOS_MILEAGE_API_BASE_URL; + const previousKey = process.env.ONEOS_MILEAGE_API_KEY; + process.env.ONEOS_MILEAGE_API_BASE_URL = 'https://oneos.test'; + process.env.ONEOS_MILEAGE_API_KEY = 'test-key'; + t.after(() => { + if (previousUrl === undefined) delete process.env.ONEOS_MILEAGE_API_BASE_URL; + else process.env.ONEOS_MILEAGE_API_BASE_URL = previousUrl; + if (previousKey === undefined) delete process.env.ONEOS_MILEAGE_API_KEY; + else process.env.ONEOS_MILEAGE_API_KEY = previousKey; + clearOneOsMileageCache(); + }); + let targets = [ + { id: 1, target_name: '目标A', plate_number: '粤A10001' }, + { id: 2, target_name: '目标B', plate_number: '粤A10001' }, + ]; + t.mock.method(pool, 'query', async (sql: string) => { + if (sql.includes('tab_mileage_assessment_target')) return [targets, []]; + assert.ok(sql.includes('FROM vehicle_info vi')); + return [[{ plate: '粤A10001', vin: 'test-vin' }], []]; + }); + t.mock.method(globalThis, 'fetch', async (_url: unknown, init: RequestInit) => { + const { date } = JSON.parse(String(init.body)); + return Response.json({ code: 'SUCCESS', data: [{ + plateNumber: '粤A10001', date, status: 'NORMAL', + dailyMileageKm: 12, totalMileageKm: 100, sourceProtocol: 'GB32960', + }] }); + }); + for (const expectedTargets of [['目标A', '目标B'], []]) { + clearOneOsMileageCache(); + const response = await monitoring.request('/?startDate=2026-09-16&endDate=2026-09-16&sourcePriority=instrument'); + assert.equal(response.status, 200); + const payload = await response.json(); + assert.equal(payload.total, 1); + assert.equal(payload.vehicles[0].dailyKm, 12); + assert.deepEqual(payload.vehicles[0].targetNames, expectedTargets); + targets = []; + } +});