feat: expand vehicle data platform capabilities

This commit is contained in:
lingniu
2026-07-27 16:46:15 +08:00
parent e3a1f80f86
commit 3c4bece72c
650 changed files with 62155 additions and 2552 deletions

View File

@@ -39,6 +39,10 @@ If a keyword cannot be resolved to a VIN, data APIs must not fabricate a VIN. Th
## Core Query APIs
### Vehicle Open Platform
Partner-facing daily hydrogen-consumption and mileage APIs, 32-character appKey authentication, key validity, and per-vehicle grant intervals are documented in [open-platform-api.md](open-platform-api.md). These public endpoints use their own fail-closed appKey authentication instead of the internal user-session middleware.
### Unified Metric Catalog
```http
@@ -47,6 +51,24 @@ GET /api/v2/metrics
Returns the server-owned metric whitelist used by rule configuration and future telemetry/history surfaces. Production definitions come from `vehicle_metric_definition` plus `vehicle_metric_protocol_mapping`; migration `005_metric_catalog.sql` only seeds missing rows, so later database configuration is not overwritten by releases. Each metric includes `key`, localized `label`, `unit`, `category`, `valueType`, supported `protocols`, per-protocol `sourceFields`, and `searchable/chartable/alertable` capabilities. Alert rule writes reject unknown, disabled, non-alertable, type-mismatched, or evaluator-unsupported metrics; clients must not invent metric keys or derive SQL fields from user input. `/api/v2/history/metrics` remains the category/column compatibility catalog for the current history page.
### Alert automations
```http
GET /api/v2/alerts/rules
GET /api/v2/alerts/rules/library?lifecycle=current&keyword=&status=all&protocol=&limit=10&offset=0
POST /api/v2/alerts/rules
PUT /api/v2/alerts/rules/{id}
POST /api/v2/alerts/rules/{id}/archive
POST /api/v2/alerts/rules/{id}/restore
POST /api/v2/alerts/events
```
An alert rule is an automation with one `triggerType`: `metric`, `geofence`, `stationary`, or `offline`. Metric triggers use the unified metric catalog. Stationary triggers normalize to a bounded speed threshold plus a duration; offline triggers normalize to realtime freshness; geofence triggers store a WGS-84 circular center, radius and `enter|exit|inside|outside` mode. A geofence must select exactly one positioning protocol so independent 808/32960/MQTT coordinates cannot oscillate across the boundary or create duplicate events.
Every matching rule creates a traceable event. `notificationChannels=[]` records the event only; `in_app` creates an in-product notification. Rules store `notificationTargets[]` as `{channel, recipientId, label}` references to the server-provided recipient-group catalog, and `notificationChannels` is derived for backward compatibility. External channels cannot be published without a configured gateway and an explicit recipient group. Event records include the normalized `triggerType`, observed evidence, rule snapshot and lifecycle status.
The administrator rule library is server-filtered and paginated. `lifecycle` is `current|archived`, `status` is `all|enabled|disabled`, `protocol` matches the normalized rule protocol array, and `keyword` searches rule identity, condition, vehicle scope, and archive reason. Each page returns `summary.current|enabled|disabled|archived` from the complete rule set rather than the current page. Archiving is reversible but requires a disabled rule, the current optimistic `version`, and a 4200 character reason. The server increments the rule version, removes live evaluator candidates/state, keeps event history and configuration, and writes an immutable `archive` revision. Restore also requires the current version and a reason, returns the rule to the current library still disabled, and writes a `restore` revision. Archived rules are excluded from evaluator reads and reject edit, enable, and rollback writes until restored.
### Latest vehicle telemetry
`GET /api/v2/vehicles/{vin}/telemetry/latest`
@@ -90,6 +112,8 @@ Returns one vehicle service view with identity, realtime summary, source coverag
The response also carries `profile`, the supplemental business master record from `vehicle_profile`. Gateway-owned VIN, plate and OEM identity remain authoritative and are never overwritten by this record. Profile completeness is calculated across model, vehicle type, company, operation status, access provider, first access time and cumulative runtime.
When the active OneOS scope snapshot contains the VIN, the response carries `businessRelation`. It is a read-only projection from `ln_asset_management` containing the current business customer, contract, project, department, responsible user, OneOS operation status, relationship start time, source version and publication evidence. The projection only accepts completed deliveries without a completed return and is joined through `business_scope_state.active_version`; no request performs a live OneOS database query. OneOS BIGINT identifiers are encoded as JSON strings to avoid JavaScript precision loss. If no safe current relation exists, `businessRelation` is omitted and clients must show an explicit unbound state instead of inferring ownership from `vehicle_info.customer_id`.
```http
GET /api/v2/vehicles/{vin}/profile
PUT /api/v2/vehicles/{vin}/profile
@@ -148,6 +172,8 @@ Returns VIN-level source coverage rows for the vehicle service list. Each row in
Coverage summary also exposes `noDataVehicles`, so UI can show vehicles that exist in identity binding but have no GB32960, JT808, or Yutong MQTT source evidence. `/api/vehicles/coverage?serviceStatus=no_data` returns those bound vehicles for follow-up source onboarding.
Vehicle coverage accepts comma-separated multi-select filters: `departmentIds`, `responsibleUserIds`, `customerIds`, and `operationStatuses`. The server applies the authenticated principal VIN scope before these filters. `GET /api/vehicles/business-filters` returns `{departments,responsibleUsers,customers,statuses}` with `{value,label,count}` options already limited to the same principal scope.
Coverage rows also include lightweight `sourceConsistency` so list views can show the same vehicle-level source diagnosis without issuing per-row detail requests.
### History Locations
@@ -169,6 +195,8 @@ This keeps table pagination precise while preventing large routes from overloadi
### History Series And Controlled Export
`GET /api/v2/history/query` accepts one to five comma-separated `keywords`. In addition to aggregate `vehicleCount`, its summary preserves the full requested scope through `requestedVehicleCount` and an ordered `vehicles` receipt. Each receipt includes the original `keyword`, resolved `vin` / `plate` when current-range evidence exists, `rowCount`, and `matched` or `no_data` status. Clients should show these per-vehicle outcomes instead of inferring a missing vehicle from the aggregate count; `no_data` means no evidence matched the selected category, protocol and time window.
```http
GET /api/v2/history/series?keyword=AG18312&dateFrom=2026-07-14T00:00:00%2B08:00&dateTo=2026-07-14T06:00:00%2B08:00&targetPoints=240
@@ -182,18 +210,53 @@ Content-Type: application/json
"dateFrom": "2026-07-14T00:00:00+08:00",
"dateTo": "2026-07-14T06:00:00+08:00",
"metrics": ["speed_kmh", "total_mileage_km"],
"format": "csv"
"format": "csv",
"retentionDays": 14
}
GET /api/v2/exports
GET /api/v2/exports/page?search=AG18312&status=expiring&scope=archived&ownerScope=mine&sort=expiry&limit=20&offset=0
GET /api/v2/exports/cleanup/preview?olderThanDays=180&ownerScope=all
GET /api/v2/exports/cleanup/audit?limit=20
POST /api/v2/exports/cleanup
GET /api/v2/exports/cleanup/automation
PUT /api/v2/exports/cleanup/automation
POST /api/v2/exports/cleanup/automation/{runId}/approve
POST /api/v2/exports/cleanup/automation/{runId}/reject
POST /api/v2/exports/cleanup/automation/{runId}/retry
PUT /api/v2/exports/{id}/cleanup-protection
POST /api/v2/exports/{id}/rebuild
POST /api/v2/exports/{id}/cancel
POST /api/v2/exports/batch
GET /api/v2/exports/{id}/download
GET /api/v2/history/preferences
PUT /api/v2/history/preferences
Content-Type: application/json
{
"retentionDays": 14,
"fieldViews": [
{ "id": "location:", "name": "", "category": "location", "keys": ["speedKmh", "socPercent"] }
]
}
```
The series endpoint returns server-aggregated location telemetry with explicit bucket coverage and missingness. Requests are limited to 31 days and 60600 target points; timestamps must identify an absolute window rather than relying on a database session timezone.
Export creation and listing require `operator` or `admin`. A task accepts 15 vehicles, `location`, `raw`, or `mileage`, CSV format, at most 31 days and 32 metrics. The service runs one export at a time and enforces a 1,000,000-row and 30-minute ceiling. Location and RAW data use stable forward-only cursors; the implementation does not use growing `OFFSET` scans.
Export creation and listing require `operator` or `admin`. A task accepts 15 vehicles, `location`, `raw`, or `mileage`, CSV format, at most 31 days and 32 metrics. `retentionDays` accepts `1`, `3`, `7`, `14`, or `30`; when omitted, the current account preference is used and falls back to 7 days. The service runs one export at a time and enforces a 1,000,000-row and 30-minute ceiling. Location and RAW data use stable forward-only cursors; the implementation does not use growing `OFFSET` scans.
Job status values are `queued`, `running`, `completed`, and `failed`. During execution, clients should display the server-owned `processedRows`, `totalRows`, `progress`, and `evidence` fields rather than estimate progress from elapsed time. On completion, `rowCount`, `processedRows`, and `totalRows` are equal, `fileSizeBytes` and `completedAt` are populated, and `downloadUrl` becomes available. The final CSV is exposed only after its temporary `.part` file has been flushed, synchronized, closed, and atomically renamed. Completed jobs survive API restarts; jobs interrupted while queued or running become explicit failures and may be recreated.
`GET /api/v2/exports/page` is the bounded task-center contract. `search` matches task, vehicle, protocol, category, and creator fields; `status` accepts `active`, `failed` (failed or expired), `completed`, `expiring` (completed and expiring within the next 24 hours), and `cancelled`; `scope` accepts `current` or `archived`; `ownerScope=mine` limits an administrator's otherwise global audit view to jobs created by the current persistent subject; customer accounts remain owner-scoped regardless of this parameter; `sort` accepts `recent` (the default) or `expiry`; `limit` accepts 10, 20, or 50. `recent` orders current tasks by creation time and archived tasks by archive activity, with creation time as the stable batch-archive tie-breaker. `expiry` puts downloadable files with a future expiration first in ascending expiration order, then falls back to the selected scope's recent activity. The response contains the filtered `items`, `total`, `limit`, and `offset`. In `summary`, `current` and `archived` count the accessible owner scope, while `active`, `completed`, `expiring`, `recoverable`, and `cancelled` count only the selected `scope`, before status/search filtering. The legacy `GET /api/v2/exports` remains a first-20 compatibility view.
History preferences are private to the authenticated account and persist independently from browser storage. `fieldViews` supports at most eight named views, each containing 132 fields for `location`, `raw`, or `mileage`; `retentionDays` uses the same export whitelist. The server returns a monotonic `revision` and `updatedAt` receipt after each successful update.
Job status values are `queued`, `running`, `completed`, `failed`, `expired`, and `cancelled`. During execution, clients should display the server-owned `processedRows`, `totalRows`, `progress`, and `evidence` fields rather than estimate progress from elapsed time. Completed files receive an `expiresAt`; once the retention window ends or the published file is missing, the job becomes `expired`, clears `downloadUrl`, and retains its original scope for audit. `POST /api/v2/exports/{id}/rebuild` accepts only `failed` or `expired` jobs, revalidates current vehicle/time authorization, creates a new task linked by `rebuiltFrom`, and refuses a duplicate scope already queued or running.
The owner or an administrator may cancel a queued or running task. Cancellation is idempotent, releases the single execution slot, removes partial output, and preserves `cancelledAt`, `cancelledBy`, the original scope, progress, and evidence for audit; cancelled tasks never expose a download. `POST /api/v2/exports/batch` accepts 120 ids with `cancel` or `rebuild`, executes every item independently, and returns explicit `succeeded` and `skipped` arrays. On completion, `rowCount`, `processedRows`, and `totalRows` are equal, `fileSizeBytes`, `completedAt`, and `expiresAt` are populated, and `downloadUrl` becomes available. The final CSV is exposed only after its temporary `.part` file has been flushed, synchronized, closed, and atomically renamed. Terminal jobs survive API restarts; jobs interrupted while queued or running become explicit failures and may be rebuilt.
Archived task cleanup is an administrator-only, preview-gated operation. `olderThanDays` accepts 30, 90, 180, or 365 and only includes terminal jobs whose `archivedAt` is outside the selected window. Preview returns the full candidate/protected counts, the oldest-first batch of at most 500 records, up to 20 visible samples, remaining file impact, and a deterministic `previewToken`. `POST /api/v2/exports/cleanup` must echo that token; any archive, protection, or task update invalidates the token and forces a new preview. `PUT /api/v2/exports/{id}/cleanup-protection` requires a reason when enabling protection and records the actor, time, and reason in the task evidence. Protected jobs never enter cleanup. Permanent cleanup removes the task record and any remaining file, while an independent cleanup audit retains the actor, policy, counts, cutoff, impact, timestamp, and candidate digest. Export persistence no longer silently truncates records after 500 jobs; growth is handled only through this explicit lifecycle workflow.
Cleanup automation is approval-gated rather than unattended deletion. The singleton policy accepts `olderThanDays` 30/90/180/365, `intervalDays` 7/14/30, and `approvalWindowHours` 24/48/72, with `expectedRevision` optimistic concurrency. Each due review produces an immutable-impact run in `awaiting_approval` or `no_candidates`; no deletion occurs until an administrator submits a reason to the run-specific `approve` endpoint. Approval rechecks the deterministic preview token. If the candidate scope changed, the run becomes `needs_review`, receives the latest counts and a fresh approval window, and must be approved again. Approved runs are claimed with a filesystem-backed cross-process mutex and expiring execution lease. A crashed worker is recovered after lease expiry; technical failures retry at bounded backoff up to three attempts, preserve the original approval and error evidence, and then require the explicit `retry` endpoint with a new administrative reason. `reject` closes a pending run without affecting tasks. Run mutations use per-run revisions, so stale browser actions are rejected rather than overwriting another administrator's decision.
### RAW Frames
@@ -232,7 +295,7 @@ GET /api/mileage/summary?keyword=粤AG18312&protocol=JT808&dateFrom=2026-07-01&d
Returns daily mileage rows and aggregate mileage summary.
The production projection table stores `daily_mileage_km` and `latest_total_mileage_km`; it deliberately no longer stores a duplicate first-total column. The API derives `startMileageKm = latestTotalMileageKm - dailyMileageKm` and normalizes a missing latest value to zero, matching the current gateway storage contract.
The production projection table stores `daily_mileage_km`, `pure_hydrogen_mileage_km`, and `latest_total_mileage_km`; it deliberately no longer stores a duplicate first-total column. The API derives `startMileageKm = latestTotalMileageKm - dailyMileageKm`, returns `pureHydrogenMileageKm` for GB32960 and Yutong fuel-cell work-mode intervals, and normalizes a missing latest value to zero, matching the current gateway storage contract. Daily mileage rows also join quality-approved `vehicle_open_daily_energy` evidence and return nullable `hydrogenConsumptionKg` and `hydrogenConsumptionKgPer100Km`. The rate uses pure-hydrogen mileage only when both metrics are valid for the same vehicle day; missing or suspect hydrogen evidence remains `null` rather than being presented as zero.
### Alert Events
@@ -264,11 +327,18 @@ POST /api/v2/alerts/events
GET /api/v2/alerts/events/{id}
POST /api/v2/alerts/events/{id}/actions
GET /api/v2/alerts/rules
GET /api/v2/alerts/rules/library?lifecycle=current&keyword=&status=all&protocol=&limit=10&offset=0
POST /api/v2/alerts/rules
PUT /api/v2/alerts/rules/{id}
PUT /api/v2/alerts/rules/{id}/enabled
POST /api/v2/alerts/rules/{id}/archive
POST /api/v2/alerts/rules/{id}/restore
GET /api/v2/alerts/notifications?unreadOnly=true&limit=20&offset=0
GET /api/v2/alerts/notification-config
GET /api/v2/alerts/notifications/health
POST /api/v2/alerts/notifications/read
POST /api/v2/alerts/notifications/{id}/retry
GET /api/v2/alerts/notifications/{id}/retry-audit
```
Rule operators are type-aware: numeric metrics support `gt/gte/lt/lte/eq/neq/between/outside`; Boolean metrics support `eq/neq/changed`. Range rules carry `threshold` and `thresholdHigh`. Scopes support `scopeProtocols`, `scopeVins`, authoritative `scopeOems`, and `scopeModels/scopeCompanies` joined from `vehicle_profile`. Each scope list is capped at 500 normalized values with a 128-character per-value bound, protecting evaluator latency. Vehicles without the requested master-data dimension do not match a model/company-scoped rule. `changed` rules persist the previous Boolean state per rule/VIN/protocol, and repeat suppression uses `repeatIntervalSec` against the latest event fingerprint.
@@ -277,7 +347,13 @@ The rule editor reads its selectable metrics from `GET /api/v2/metrics`. The API
`summary` and `events` accept the same JSON filter: `keyword, severity, status, ruleId, protocol, dateFrom, dateTo, limit, offset`. Event states are `unprocessed, processing, recovered, closed, ignored`; event changes require the current `version` and actions are `acknowledge, close, ignore`. Every action writes an immutable timeline item with actor, before/after state, note, and timestamp. Stale event or rule writes return HTTP 409 with a `*_VERSION_CONFLICT` code.
Rules support numeric/Boolean values, bounded duration and repeat interval, recovery hysteresis, protocol/VIN scope, enable state, and a versioned audit snapshot. For telemetry metrics, duration is the difference between distinct, monotonic source event observations rather than time spent rereading one MySQL snapshot; `freshness_sec` is the explicit exception because staleness changes with platform time. Duplicate and late observations are counted but cannot advance a candidate. The evaluator deduplicates active rule+vehicle+protocol fingerprints and automatically recovers only when the configured recovery condition is true. Disabling a rule atomically clears its candidate and Boolean state together with the versioned disable audit. Station notifications have real unread/read state. `sms`, `email`, and `wecom` records are explicitly `reserved`; they are not reported as sent.
Rules support numeric/Boolean values, bounded duration and repeat interval, recovery hysteresis, protocol/VIN scope, enable state, and a versioned audit snapshot. The administrator-only `library` contract adds complete-set filtering, pagination, lifecycle summaries, and reversible archive/restore governance. A rule must be disabled before archive; archive and restore both require an optimistic version and 4200 character audit reason, and restoration never enables the rule automatically. For telemetry metrics, duration is the difference between distinct, monotonic source event observations rather than time spent rereading one MySQL snapshot; `freshness_sec` is the explicit exception because staleness changes with platform time. Duplicate and late observations are counted but cannot advance a candidate. The evaluator deduplicates active rule+vehicle+protocol fingerprints and automatically recovers only when the configured recovery condition is true. Disabling or archiving a rule atomically clears its candidate and Boolean state together with the versioned audit.
Notification queries return every delivery channel and preserve the provider-facing state instead of projecting only station messages. Customer sessions are limited to notifications joined to their granted VINs. Unread/read state applies only to `in_app`; external channels return a not-applicable reading state. A newly written station notification is immediately `sent`, because the durable database row is its delivery evidence. `sms`, `email`, and `wecom` records remain `reserved` until an external delivery worker reports a real outcome; the API never reports queue acceptance as provider delivery.
`notification-config` returns the recipient-group catalog and readiness of `in_app`, `sms`, `email`, and `wecom`; it never exposes gateway URLs, secrets, phone numbers, or email addresses. `notifications/health` returns queue-wide and per-channel counts for queued, failed, dead-letter (failed at the three-attempt cap), and active leased deliveries, plus the oldest queued timestamp. The dispatcher claims `reserved` rows with `FOR UPDATE SKIP LOCKED` and a bounded lease. It sends a JSON payload to the configured trusted gateway with `X-Lingniu-Timestamp`, `X-Lingniu-Signature: sha256=<HMAC-SHA256(timestamp + "\n" + body)>`, `X-Lingniu-Notification-ID`, and `X-Lingniu-Idempotency-Key: notification:{id}:attempt:{attempt}`. A 2xx response must include `messageId` in JSON or `X-Provider-Message-ID`; otherwise the attempt is failed rather than falsely marked sent.
Only administrators and operators can retry a failed notification. A retry requires the current `expectedAttemptCount`, a 4200 character reason, and a unique 1696 character `idempotencyKey`; each notification is capped at three attempts. The server locks the notification, rejects stale or non-failed requests, and writes an immutable retry audit in the same transaction. Reusing the same key returns the original receipt without creating another attempt. Retrying `in_app` writes a new delivered station attempt; retrying an external channel moves the record back to `reserved` so a separate provider worker can process it. `retry-audit` returns the latest 20 receipts without exposing the idempotency key.
### Online And Completeness Statistics
@@ -345,6 +421,61 @@ The diagnostic response exposes every current location candidate, including the
`providerEvidence` is mandatory whenever `providerName` changes, including removal. It is written to the immutable provider audit and is deliberately separate from `remark`, which belongs only to source enable/priority policy. A provider-only update must preserve the existing policy remark. Both change types share the vehicle-level optimistic `version`; stale writes return `SOURCE_POLICY_VERSION_CONFLICT`.
### V2 Reconciliation Batch Actions
```http
POST /api/v2/reconciliation/issues/batch-actions
Content-Type: application/json
{
"items": [
{"id": "reconciliation-issue-1", "version": 3},
{"id": "reconciliation-issue-2", "version": 1}
],
"status": "fixed",
"note": ""
}
```
The batch endpoint accepts 120 unique issue IDs. `status` is limited to `pending`, `no_action`, or `fixed`; non-pending conclusions require a note of at most 500 characters. Each item carries its own optimistic version and is written through the same immutable review history as the single-item action endpoint. Items are independent: a conflict or missing record is returned in `skipped` with its code and message while other valid items continue. `succeeded` returns the complete updated issue, and `requested` always reflects the submitted item count. The authenticated principal supplies the actor; a body actor is never trusted.
### V2 Reconciliation Ownership and SLA
```http
POST /api/v2/reconciliation/issues/{id}/assignment
Content-Type: application/json
{"version":3,"assignee":"","dueAt":"2026-07-24T08:00:00Z"}
POST /api/v2/reconciliation/issues/batch-assignments
Content-Type: application/json
{
"items":[{"id":"reconciliation-issue-1","version":3}],
"assignee":"",
"dueAt":"2026-07-24T00:00:00Z"
}
```
Assignment requires a non-empty assignee (maximum 128 characters), a future RFC 3339 deadline, and the issue's current optimistic version. Successful writes update `assignee`, `assignedBy`, `assignedAt`, and `dueAt`, increment `version`, and append an immutable `assign` action. Batch assignment accepts 120 unique issues and returns the same per-item `succeeded` / `skipped` contract as batch review. The authenticated principal is always the assigner.
The issue-list query accepts `owner=assigned|unassigned|<exact assignee>` and `sla=overdue|due_soon`; `due_soon` means an active issue whose explicit deadline is within eight hours. Summary SLA counts use an explicit `dueAt` when present and otherwise retain the legacy 24-hour first-seen fallback.
### V2 Reconciliation Directory and Export
```http
GET /api/v2/reconciliation/assignees?search=
POST /api/v2/reconciliation/issues/export
Content-Type: application/json
{"keyword":"A","status":"active","owner":"","sla":"overdue"}
```
The assignee directory merges the authenticated principal, enabled administrator accounts, and historical non-empty assignees. Results include `name`, optional `username`, source, active issue count, last assignment time, and a current-account marker; search matches name or username and returns at most 50 entries.
The CSV export uses the same keyword, rule, category, severity, status, exact owner, and SLA filters as the server-paginated queue, but intentionally ignores the visible page offset. It reads in bounded 200-row pages and rejects a result above 50,000 records. The response includes UTF-8 BOM, `Content-Disposition`, `X-Export-Name`, and `X-Export-Count`; spreadsheet-formula prefixes are neutralized before CSV encoding.
## Map Reverse Geocoding
`GET /api/map/reverse-geocode?longitude=<WGS-84>&latitude=<WGS-84>` is an authenticated server-side AMap Web Service adapter. It validates the source coordinate, converts WGS-84 to GCJ-02 exactly once, keeps the server API key out of the browser, and requests only the `base` reverse-geocode response documented by [AMap](https://lbs.amap.com/api/webservice/guide/api/georegeo).