Files
2026-07-27 16:46:15 +08:00

44 KiB
Raw Permalink Blame History

Vehicle Data Platform API Contract

Response Envelope

All platform APIs return the same envelope:

{
  "data": {},
  "traceId": "trace-20260704000000.000000",
  "timestamp": 1783094400000
}

Pagination uses:

{
  "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. These public endpoints use their own fail-closed appKey authentication instead of the internal user-session middleware.

Unified Metric Catalog

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

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

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

GET /api/dashboard/summary

V2 Global Monitor

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

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.

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 1500 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.

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.

{
  "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

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

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

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.

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 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. 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 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

Short GET query:

GET /api/history/raw-frames?keyword=粤AG18312&protocol=GB32960&limit=20&offset=0&includeFields=true

Large field-filter query:

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

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:

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:

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 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

Planned API surface:

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

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

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

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

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

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.

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, 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.