# Vehicle Data Platform API Contract ## Response Envelope All platform APIs return the same envelope: ```json { "data": {}, "traceId": "trace-20260704000000.000000", "timestamp": 1783094400000 } ``` Pagination uses: ```json { "items": [], "total": 0, "limit": 20, "offset": 0 } ``` ## Vehicle Keyword The platform is vehicle-first. Query APIs should accept `keyword` as the user-facing vehicle selector. `keyword` may be: - VIN - plate - JT808 phone The BFF resolves `keyword` through vehicle identity data before querying realtime, history, RAW, or mileage data. `vin` remains accepted as a compatibility alias on data query APIs, but new UI and integrations should send `keyword`. If a keyword cannot be resolved to a VIN, data APIs must not fabricate a VIN. They should return empty result pages for vehicle data, while `/api/vehicle-service` exposes `lookupResolved=false` and quality issues for follow-up binding work. ## 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 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 4–200 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` The path accepts the same VIN, plate or terminal identity resolution used by the vehicle service. After resolving identity once, the server reads `GB32960`, `JT808` and `YUTONG_MQTT` independently and concurrently, at most five newest frames per protocol and 15 frames overall, without running pagination counts. It then returns the newest scalar value for every unified metric or remaining source field. A field that matches `/api/v2/metrics.sourceFields` uses the unified `key`, localized label, unit, type, description and category while retaining its exact `sourceField`. Manufacturer/protocol extensions remain visible under a server-owned dynamic category instead of being discarded or classified by the browser. Every value includes `protocol`, optional `sourceEndpoint`, `frameId`, device and receive times, `freshnessSeconds`, optional `dataDelaySeconds`, and `quality=good|stale|warning` with an explicit `qualityReason`. `good` requires a successful parser status and a receive time within five minutes; `stale` means the newest value is older than five minutes; parse failures, missing/implausibly future receive times or device/receive divergence over five minutes are `warning`. The response also returns populated categories with counts, `scannedFrames`, `staleAfterSeconds`, `asOf` and selection evidence. Object/array fields are deliberately excluded from the compact latest-value surface and remain available through RAW evidence APIs. ### Dashboard Summary ```http GET /api/dashboard/summary ``` ### V2 Global Monitor ```http GET /api/v2/monitor/summary?keyword=粤A&protocol=JT808&status=driving GET /api/v2/monitor/map?zoom=5 GET /api/v2/monitor/map?zoom=13&bounds=113,22,114,24&status=online GET /api/v2/monitor/workspace?zoom=13&bounds=113,22,114,24&railLimit=200 ``` `summary` and `map` share keyword, protocol and `online|offline|driving|idle` status semantics. `map` accepts `bounds=minLongitude,minLatitude,maxLongitude,maxLatitude`; malformed, non-finite, reversed or out-of-world bounds return `MONITOR_BOUNDS_INVALID`. At zoom 5 and below, `map.mode=provinces`. The response includes `provincePoints`, a compact identity-free array containing only WGS-84 longitude, latitude and normalized status for every located vehicle in the filtered and authorized fleet. Nationwide province counts intentionally ignore the current slippy-map bounds; the browser converts the seeds to GCJ-02 and groups them against AMapUI `DistrictExplorer`'s official nationwide AreaNode. `clusters` remains populated as a visible fallback if that external administrative-boundary resource fails. No VIN, plate, terminal or full telemetry is present in a province seed. From zoom 6 through 10 the response remains clustered or mixed. At zoom 11 and above it returns lightweight MassMarks only when the visible point set is at most 2,000; otherwise it adaptively coarsens cells until no more than 2,000 clusters remain. It never returns full telemetry JSON in a point. The browser keeps the normal list at 200 rows, requests map data independently, waits 300ms after `moveend/zoomend`, and drops stale viewport bounds when a direct vehicle search must locate and focus its unique result. `workspace` is the production map-screen BFF response. It executes one bounded realtime snapshot read, then derives `summary`, the first `railLimit` realtime vehicle rows (hard-capped at 200), and the viewport-aware `map` payload from that same snapshot. The standalone `summary`, `map`, and realtime-vehicle APIs remain available for list mode and external compatibility, but the browser must not fan them out concurrently for one map refresh. Returns vehicle service KPIs, source distribution, vehicle service status distribution, link health, and realtime backlog. ### Vehicle Service ```http GET /api/vehicle-service?keyword=粤AG18312 ``` Returns one vehicle service view with identity, realtime summary, source coverage, history preview, RAW preview, mileage preview, and quality issues. 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 POST /api/v2/vehicle-profiles/sync ``` Profile reads require `viewer`; manual writes and bulk sync require `admin`. A manual write uses the returned `version` for optimistic concurrency, rejects unknown VINs and invalid status/time/runtime values, changes the source to `manual`, and creates a versioned `vehicle_profile_audit` snapshot. Supported operation statuses are `unknown`, `active`, `inactive`, `maintenance`, and `retired`. The controlled sync endpoint accepts 1–500 complete supplemental profiles per request with required `sourceSystem` and `sourceVersion`, `conflictPolicy=preserve|overwrite`, and `dryRun`. VIN identity must already exist in the gateway-owned `vehicle_identity_binding`; missing identities are reported per item and never invented. The default `preserve` policy skips manual records and records owned by another external system. `overwrite` is an explicit administrator takeover. Repeating the same source/version and identical payload is `unchanged`; changing a payload under the same source/version is `conflict_source_version`, preventing a mutable upstream snapshot from silently defeating idempotency. Created and updated profiles receive `source_system`, `source_version`, `synced_at`, optimistic profile version, authenticated actor, and an immutable `sync_created` or `sync_updated` audit snapshot. `dryRun=true` uses the same identity, ownership, validation and locking path but rolls back all writes. The admin UI accepts UTF-8 CSV with the exact header below, parses quoted fields and validates duplicate VIN, status and integer runtime before upload. It renders only aggregate counts plus the first 20 conflicts, so a 500-row batch does not create a large React table. ```text vin,modelName,vehicleType,companyName,operationStatus,accessProvider,firstAccessAt,runtimeSeconds ``` `/api/vehicles/detail` remains available as a compatibility alias. New UI and integrations should use `/api/vehicle-service` to make the vehicle-first boundary explicit. `sourceConsistency` is the vehicle-level diagnosis across GB32960, JT808, and Yutong MQTT sources. The three protocols are source evidence for one vehicle service, so callers should display the diagnosis result instead of asking users to compare protocol rows manually. ```json { "sourceConsistency": { "scope": "detail", "sourceCount": 3, "onlineSourceCount": 2, "locatedSourceCount": 2, "missingProtocols": ["YUTONG_MQTT"], "mileageDeltaKm": 0.8, "sourceTimeDeltaSeconds": 42, "status": "degraded", "severity": "warning", "title": "来源不完整", "detail": "2/3 个来源在线,车辆服务可用但缺少 YUTONG_MQTT 来源。" } } ``` `missingProtocols` lists canonical source evidence that is completely absent for the vehicle service, so callers can distinguish a missing source from an offline-but-known source. `status` values are `consistent`, `degraded`, `offline`, `single_source`, `no_source`, `mileage_divergent`, and `time_divergent`. `severity` values are `ok`, `warning`, and `error`. `/api/vehicle-service/overview` and `/api/vehicle-service/overviews` also return `sourceConsistency`; overview responses may only include lightweight counts and diagnosis, while detail responses include location, mileage, and source-time deltas when realtime source rows are available. ### Realtime Vehicles ```http GET /api/realtime/vehicles?keyword=粤AG18312&protocol=JT808&online=online&serviceStatus=degraded&limit=50&offset=0 ``` Returns VIN-level realtime rows with canonical vehicle-level `serviceStatus`. Protocol is a source filter, not a product boundary. ### Vehicle Coverage ```http GET /api/vehicles/coverage?keyword=粤AG18312&serviceStatus=degraded&limit=20&offset=0 ``` Returns VIN-level source coverage rows for the vehicle service list. Each row includes the canonical vehicle-level `serviceStatus` so frontend, exports, and external integrations share the same health definition. `serviceStatus` accepts `healthy`, `degraded`, `offline`, `no_data`, and `identity_required`. 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 ```http GET /api/history/locations?keyword=粤AG18312&protocol=JT808&dateFrom=2026-07-03%2000:00:00&dateTo=2026-07-03%2023:59:59&limit=20&offset=0 ``` Returns historical location points from TDengine, enriched with plate where possible. Location points include `socPercent`, `directionDeg` and `alarmFlag` when the source protocol supplies them. `socAvailable` distinguishes a real zero from an unavailable historical value. Device and receive timestamps are returned as absolute RFC3339 instants; query bounds must include an explicit offset (for example `+08:00`) so TDengine session timezone cannot shift the requested window. For trajectory replay, the BFF should eventually return two shapes from the same evidence set: - `items`: paginated table rows for audit and export. - `mapPoints`: bounded or sampled points for map rendering and playback. This keeps table pagination precise while preventing large routes from overloading the browser map. ### 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 POST /api/v2/exports Content-Type: application/json { "keywords": ["粤AG18312"], "category": "location", "protocol": "JT808", "dateFrom": "2026-07-14T00:00:00+08:00", "dateTo": "2026-07-14T06:00:00+08:00", "metrics": ["speed_kmh", "total_mileage_km"], "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 60–600 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 1–5 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. `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 1–32 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 1–20 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 Short GET query: ```http GET /api/history/raw-frames?keyword=粤AG18312&protocol=GB32960&limit=20&offset=0&includeFields=true ``` Large field-filter query: ```http POST /api/history/raw-frames/query Content-Type: application/json { "keyword": "粤AG18312", "protocol": "GB32960", "dateFrom": "2026-07-03 00:00:00", "dateTo": "2026-07-03 23:59:59", "fields": ["gb32960.vehicle.speed_kmh", "gb32960.vehicle.total_mileage_km"], "includeFields": true, "limit": 20, "offset": 0 } ``` Use POST when `fields` may be long. `parsedFields` is returned only when `includeFields=true` or specific `fields` are requested. ### Mileage ```http GET /api/mileage/daily?keyword=粤AG18312&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03&limit=20&offset=0 GET /api/mileage/summary?keyword=粤AG18312&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03 ``` Returns daily mileage rows and aggregate mileage summary. 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 Current API surface: ```http GET /api/alert-events?keyword=粤AG18312&protocol=JT808&issueType=NO_SOURCE&limit=20&offset=0 GET /api/alert-events/summary?keyword=粤AG18312&protocol=JT808&issueType=NO_SOURCE GET /api/alert-events/notification-plan?keyword=粤AG18312&protocol=JT808&issueType=NO_SOURCE&limit=20 ``` `/api/quality/issues`, `/api/quality/summary`, and `/api/quality/notification-plan` remain compatibility aliases. New UI and integrations should use `/api/alert-events*` so the API language matches the vehicle data management center. Alert event rows are vehicle-first and evidence-backed. Each event includes vehicle identity, issue type, severity, protocol/source when relevant, last seen time, source endpoint, detail, and links generated by the BFF/UI to realtime, trajectory, RAW, mileage, vehicle detail, or notification rule evidence. Initial alert types: - `NO_SOURCE` - `VIN_MISSING` - `LINK_GAP` - `FIELD_MISSING` - `CAPACITY_RISK` These legacy aliases remain read-only quality projections. The durable V2 alert center is a separate truth surface: ```http POST /api/v2/alerts/summary 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. The rule editor reads its selectable metrics from `GET /api/v2/metrics`. The API repeats the same catalog validation on every create/update, so a stale or manipulated client cannot configure an unsupported evaluator field. `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. 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 4–200 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=`, `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 4–200 character reason, and a unique 16–96 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 Planned API surface: ```http GET /api/statistics/online-rate?keyword=粤AG18312&dateFrom=2026-07-01&dateTo=2026-07-03&limit=20&offset=0 GET /api/statistics/completeness?keyword=粤AG18312&dateFrom=2026-07-01&dateTo=2026-07-03&limit=20&offset=0 GET /api/statistics/source-consistency?keyword=粤AG18312&dateFrom=2026-07-01&dateTo=2026-07-03&limit=20&offset=0 ``` These APIs should link back to `/api/history/locations` or `/api/history/raw-frames/query` evidence. The UI must not show statistics as untraceable numbers. ### V2 Access Management ```http POST /api/v2/access/summary Content-Type: application/json {"protocol":"JT808","onlineState":"offline","delayState":"abnormal"} POST /api/v2/access/vehicles Content-Type: application/json {"keyword":"粤AG18312","protocol":"JT808","model":"ZK6122","provider":"G7s","firstSeenFrom":"2026-07-14T00:00:00+08:00","latestSeenFrom":"2026-07-14T08:00:00+08:00","limit":50,"offset":0} POST /api/v2/access/unresolved-identities Content-Type: application/json {"keyword":"","protocol":"JT808","limit":20,"offset":0} GET /api/v2/access/thresholds PUT /api/v2/access/thresholds ``` `summary` and `vehicles` consume the same filter shape, so KPI, protocol/OEM distribution, and rows share one scope. Besides keyword, protocol, OEM and status, both accept substring filters `model` and `provider`, plus paired `firstSeenFrom`/`firstSeenTo` and `latestSeenFrom`/`latestSeenTo` bounds. A supplied pair must be ordered; malformed or reversed timestamps return `ACCESS_TIME_INVALID` or `ACCESS_TIME_RANGE_INVALID`. `onlineState` values are `online`, `offline`, `never_reported`, and `unknown`. Online state is calculated on every request from latest receive/update freshness and the current global/protocol threshold version; the stored snapshot boolean is not reused as the answer. The authoritative access-management population is the distinct VIN set in `vehicle_identity_binding`; realtime rows without a bound VIN stay in `unresolved-identities` and never inflate the main-vehicle count. Each main vehicle is returned once with the standard expected source slots `GB32960`, `JT808`, and `YUTONG_MQTT`. `expectedProtocols`, `actualProtocols`, `missingProtocols`, and `protocolStatuses` make the expected-versus-actual difference explicit. Every protocol status independently carries provider, first-seen evidence, latest event/receive time, report interval, freshness, delay, and online state. `connectionState` is one of `healthy`, `incomplete`, `degraded`, `offline`, or `not_connected`; the additional query value `attention` returns every non-healthy vehicle for the difference-first operations list. `unresolved-identities` returns terminals that have real access evidence but no authoritative VIN binding. The current implementation reads JT808 registration evidence and excludes terminals already present in `vehicle_identity_binding`. It exposes only a stable hashed ID and masked terminal identifier, never the raw phone number. Resolve `missing_vin_jt808` by verifying registration evidence and maintaining an authoritative `phone -> VIN` binding; do not infer or fabricate a VIN. Access rows expose both event and receive time, `dataDelaySec`, `freshnessSec`, the applied `thresholdSec`, latest realtime message semantics, event ID, source table, model/company master data, and evidence notes. The gateway snapshot upsert maintains `access_first_seen_at`, `access_previous_received_at`, `access_latest_received_at`, `access_report_interval_ms`, `access_sample_count`, and the latest received event ID independently from device event-time ordering. A strictly newer receipt with a different event ID advances the projection atomically; duplicates, equal timestamps, replay and older receipts do not change the interval or sample count. The API rounds milliseconds to `reportIntervalSec` and returns `reportSampleCount`, `firstSeenSource`, `firstSeenEvidence`, and `reportIntervalEvidence`. `live_writer` means first observation after the writer began maintaining the row. `snapshot_backfill` is only a deployment baseline reconstructed from the previous current-state row and must not be described as the vehicle's historical first-ever access. Threshold configuration is stored in `vehicle_access_threshold_config`; every successful update increments `version`, requires the caller's previous version, and writes `vehicle_access_threshold_audit`. Stale updates return `ACCESS_THRESHOLD_VERSION_CONFLICT`. Valid ranges are bounded server-side. ### V2 Source Diagnostic and Provider Maintenance ```http GET /api/v2/operations/vehicles/{vin}/sources PUT /api/v2/operations/vehicles/{vin}/sources/{sourceRef} Content-Type: application/json { "version": 2, "providerName": "G7s", "providerEvidence": "GPS 运维终端清单 2026-07-16", "enabled": true, "priority": 20, "remark": "保持当前优先级" } ``` The diagnostic response exposes every current location candidate, including the current verified `providerOverride` and gateway-policy `policyRemark`, but never exposes the raw source key. `sourceRef` is a SHA-256 reference resolved only by the server. A connected protocol that currently has no independent location candidate is represented by a `CANONICAL` protocol snapshot with an opaque `sourceRef`, so its verified provider can still be maintained. Canonical snapshots are provider-only targets: attempts to change `enabled`, `priority` or `remark` return `SOURCE_POLICY_CANONICAL_READ_ONLY`. Operator principals may read this diagnostic; only administrators may write. `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 1–20 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 1–20 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|` 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=&latitude=` 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). Successful responses use a process-local, one-hour LRU cache capped at 4,096 entries. Cache keys normalize coordinates to five decimal places (approximately one metre), while the response always echoes the exact WGS-84 coordinate supplied by the current request. Concurrent misses for one key are serialized, so a successful burst performs one upstream request. Failures are never cached. `X-Reverse-Geocode-Cache` is `MISS` for an upstream request and `HIT` for a reused result; this bounded cache reduces quota pressure described by AMap's [service pricing and quota documentation](https://lbs.amap.com/pages/base_service_price), but does not replace authentication or edge rate limiting. ## Compatibility Rule Existing callers may still send `vin`. The BFF treats `vin` as a vehicle lookup value for compatibility, so `vin=粤AG18312` still resolves through identity binding. New code should use `keyword` to avoid implying the value is already a real VIN. ## Track Replay Evidence `GET /api/v2/tracks` accepts `keyword|vin`, optional `protocol`, `dateFrom`, `dateTo` and `maxPoints`. - If either time bound is provided, both are required, the end must be later than the start, and the range is limited to seven days. Omitting both retains the bounded latest-slice compatibility mode. - The TDengine source read remains capped at 5,000 latest points per request. `coverage.complete=false` and `coverage.evidence` explicitly state when summary metrics describe only that slice rather than the whole requested window. - `coverage` reports total, fetched, processed and returned point counts plus `latest_source_slice`, `quality_filter` and `map_sampling` reasons. - `quality` reports invalid coordinates, exact duplicates, short-interval >220 km/h implied drift, source switches and gaps over ten minutes. Filtered points never silently disappear without these counters. - When multiple protocols report the same VIN, coordinate quality is evaluated within each protocol and the map selects one primary source (explicit `protocol`, otherwise highest point count then latest observation). Alternate valid points remain visible in `sources`, `quality.alternateSourcePoints`, and the `primary_source_projection` coverage reason; parallel source coordinates are never stitched into one route. - `segments` are inferred as `moving`, `stopped` or `gap`. A stop requires at least three minutes of consecutive <=3 km/h and <=150 m interval movement. The evidence explicitly says this is GPS inference and not ignition state. - Positive subsecond intervals remain continuous telemetry even when their displayed whole-second duration rounds to zero; only invalid/non-increasing timestamps or intervals over ten minutes become `gap`. - Sampling preserves the route endpoints, event points, stop centers and every retained segment boundary. `sampledIndex` fields point directly into the returned `points` array; clients must not approximate an original index by ratio when this field is present. - Playback points and events carry synchronized speed, SOC availability/value, direction and alarm fields. The browser resolves only the paused/current coordinate through the cached reverse-geocode endpoint; playback never triggers one address request per point.