# Vehicle Ingest TDengine 链路验收手册

这份手册用于验收当前 GB32960/JT808/Yutong MQTT 接入重构链路：

1. 协议 App 接收 TCP 报文。
2. 协议 App 归档原始帧元数据，并把事件信封发布到 Kafka。
3. `vehicle-history-app` 消费 Kafka。
4. history 写入 raw、location 到 TDengine；不写 telemetry_fields。
5. Swagger/API 通过 TDengine 做历史分页查询。

每一步都必须执行命令并检查输出后，才能标记为已验证。

## 必需运行参数

启动服务前先设置：

```bash
export KAFKA_BROKERS=114.55.58.251:9092
export TDENGINE_HISTORY_ENABLED=true
export TDENGINE_HISTORY_DATABASE=vehicle_ts
export TDENGINE_JDBC_URL='jdbc:TAOS-WS://<tdengine-host>:6041/vehicle_ts'
export TDENGINE_DRIVER_CLASS_NAME=com.taosdata.jdbc.ws.WebSocketDriver
export TDENGINE_USERNAME=root
export TDENGINE_PASSWORD='<tdengine-password>'
export TDENGINE_MIN_IDLE=0
export TDENGINE_MAX_POOL_SIZE=32
```

`TDENGINE_MIN_IDLE=0` 用于减少冷启动时 TDengine 连接重试日志。TDengine 地址稳定后，再按生产并发调大连接池。

高吞吐生产验收只验证 TDengine `raw_frames`、位置表和分页查询闭环。RAW 帧的 `parsedJson`、`metadataJson`、`rawUri` 直接进入 TDengine，GB32960 snapshot/fields 接口基于 `raw_frames` 与当前解析器即时返回结构化结果；接入服务写出的原始 `.bin` 只作为冷备复核材料。逐字段趋势宽表由独立字段解析服务消费 Kafka RAW/事件后维护，不由 history-app 写入。

## 构建

```bash
mvn -pl :gb32960-ingest-app,:jt808-ingest-app,:yutong-mqtt-app,:vehicle-history-app,:vehicle-analytics-app -am package -Dmaven.test.skip=true
```

预期产物：

- `modules/apps/gb32960-ingest-app/target/gb32960-ingest-app.jar`
- `modules/apps/jt808-ingest-app/target/jt808-ingest-app.jar`
- `modules/apps/yutong-mqtt-app/target/yutong-mqtt-app.jar`
- `modules/apps/vehicle-history-app/target/vehicle-history-app.jar`
- `modules/apps/vehicle-analytics-app/target/vehicle-analytics-app.jar`

## 启动 JT808 接入服务，监听 808 端口

```bash
HTTP_PORT=20400 \
JT808_PORT=808 \
KAFKA_CONSUMER_ENABLED=false \
java -jar modules/apps/jt808-ingest-app/target/jt808-ingest-app.jar
```

验证：

```bash
curl -sS http://127.0.0.1:20400/actuator/health
curl -sS http://127.0.0.1:20400/actuator/health/liveness
curl -sS http://127.0.0.1:20400/actuator/health/readiness
lsof -nP -iTCP:808 -sTCP:LISTEN
```

预期：健康检查为 `UP`，并且 Java 进程监听 TCP `808`。

## 启动 GB32960 接入服务

```bash
HTTP_PORT=20100 \
GB32960_PORT=32960 \
KAFKA_CONSUMER_ENABLED=false \
java -jar modules/apps/gb32960-ingest-app/target/gb32960-ingest-app.jar
```

验证：

```bash
curl -sS http://127.0.0.1:20100/actuator/health
curl -sS http://127.0.0.1:20100/actuator/health/liveness
curl -sS http://127.0.0.1:20100/actuator/health/readiness
lsof -nP -iTCP:32960 -sTCP:LISTEN
```

## 启动 Yutong MQTT 接入服务

```bash
HTTP_PORT=20500 \
YUTONG_MQTT_ENABLED=true \
YUTONG_MQTT_AUTO_STARTUP=true \
KAFKA_CONSUMER_ENABLED=false \
java -jar modules/apps/yutong-mqtt-app/target/yutong-mqtt-app.jar
```

验证：

```bash
curl -sS http://127.0.0.1:20500/actuator/health
curl -sS http://127.0.0.1:20500/actuator/health/liveness
curl -sS http://127.0.0.1:20500/actuator/health/readiness
```

预期：健康检查为 `UP`，并且 Kafka 中 `vehicle.raw.mqtt-yutong.v1`、`vehicle.event.mqtt-yutong.v1` 能随真实 MQTT 消息增长。

## 启动 TDengine 版历史服务

```bash
HTTP_PORT=20200 \
KAFKA_CONSUMER_ENABLED=true \
java -jar modules/apps/vehicle-history-app/target/vehicle-history-app.jar
```

验证：

```bash
curl -sS http://127.0.0.1:20200/actuator/health
curl -sS http://127.0.0.1:20200/actuator/health/liveness
curl -sS http://127.0.0.1:20200/actuator/health/readiness
curl -sS http://127.0.0.1:20200/v3/api-docs \
  | grep -E '/api/event-history/locations|/api/event-history/raw-frames'
```

预期：健康检查为 `UP`，OpenAPI 中包含通用位置分页查询和 RAW 帧查询接口。history-app 不暴露 `/api/event-history/telemetry/fields`，也不持续写入逐字段宽表。

## 启动 JT808 指标统计服务

```bash
HTTP_PORT=20300 \
KAFKA_CONSUMER_ENABLED=true \
VEHICLE_STAT_ENABLED=true \
VEHICLE_STAT_JT808_MILEAGE_ENABLED=true \
java -jar modules/apps/vehicle-analytics-app/target/vehicle-analytics-app.jar
```

验证：

```bash
curl -sS http://127.0.0.1:20300/actuator/health
curl -sS http://127.0.0.1:20300/actuator/health/liveness
curl -sS http://127.0.0.1:20300/actuator/health/readiness
```

预期：健康检查为 `UP`；JT808 位置帧中有 GPS 总里程时，服务按差值法写入 MySQL `vehicle_stat_metric` 的 `daily_mileage_km` 指标。

## 本机 launchctl 部署

当前本机生产验证建议使用仓库模板生成 plist：

```bash
deploy/local/launchctl/
```

关键约束：

- `jt808-ingest-app` 监听 TCP `808`，HTTP `20400`。
- `gb32960-ingest-app` 监听 TCP `32960`，HTTP `20100`。
- `vehicle-history-app` 监听 HTTP `20200`。
- Yutong MQTT 与 `vehicle-analytics-app` 可以按上面的 jar 启动命令手工验收；Portainer/ECS 生产部署已包含这两个服务。
- history 热查询只依赖 Kafka 和 TDengine `raw_frames`；本地模板不会给 history 注入 `SINK_ARCHIVE_PATH`。
- GB32960/JT808 接入服务可以继续使用 `SINK_ARCHIVE_PATH` 保存原始 `.bin` 冷备，但这不是 history API 的实时查询前置条件。
- 替换 jar 前先 `launchctl bootout`，再复制 jar 和 `launchctl bootstrap`，避免 KeepAlive 在 jar 拷贝中途重启。

JT808 注册身份绑定：

- 生产身份绑定固定使用 MySQL，不再提供 file/memory/sqlite 运行时切换入口。
- 生产需要把 0x0100 注册信息维护到 MySQL 时，只需要配置 `VEHICLE_IDENTITY_MYSQL_*` 连接参数。
- 需要提供 `VEHICLE_IDENTITY_MYSQL_JDBC_URL`、`VEHICLE_IDENTITY_MYSQL_USERNAME`、`VEHICLE_IDENTITY_MYSQL_PASSWORD`。
- 服务启动时自动创建 `vehicle_identity_binding` 和 `vehicle_identity_binding_registration` 两张表。`vehicle_identity_binding` 只维护 `plate`、`vin` 两列；`vehicle_identity_binding_registration` 按 `protocol + phone` 保存 JT808 0x0100 注册字段。
- 注册帧无真实 VIN 时也会写入 registration 表，`vin` 默认为 `unknown`。外部系统识别车辆后只需要维护车牌和 VIN，例如：`INSERT INTO vehicle_identity_binding (plate, vin) VALUES ('沪A61559F', 'LNVIN000000000001') ON DUPLICATE KEY UPDATE vin = VALUES(vin);`
- MySQL 绑定在启动时加载到内存索引，`plate/vin` 反写后会按 `VEHICLE_IDENTITY_MYSQL_REFRESH_INTERVAL` 周期刷新，默认 `60s`；帧解析热路径只查内存，不按帧访问 MySQL。
- 后续同一终端上报 raw/event 时，服务会用 registration 表里的 `phone/device_id/plate` 加绑定表里的 `plate/vin` 解析成真实 VIN。

健康检查：

```bash
curl -sS http://127.0.0.1:20400/actuator/health
curl -sS http://127.0.0.1:20400/actuator/health/liveness
curl -sS http://127.0.0.1:20400/actuator/health/readiness
curl -sS http://127.0.0.1:20100/actuator/health
curl -sS http://127.0.0.1:20100/actuator/health/liveness
curl -sS http://127.0.0.1:20100/actuator/health/readiness
curl -sS http://127.0.0.1:20500/actuator/health
curl -sS http://127.0.0.1:20500/actuator/health/liveness
curl -sS http://127.0.0.1:20500/actuator/health/readiness
curl -sS http://127.0.0.1:20200/actuator/health
curl -sS http://127.0.0.1:20200/actuator/health/liveness
curl -sS http://127.0.0.1:20200/actuator/health/readiness
curl -sS http://127.0.0.1:20300/actuator/health
curl -sS http://127.0.0.1:20300/actuator/health/liveness
curl -sS http://127.0.0.1:20300/actuator/health/readiness
```

API / Swagger：

- JT808 ingest health: `http://127.0.0.1:20400/actuator/health`
- GB32960 ingest health: `http://127.0.0.1:20100/actuator/health`
- Yutong MQTT ingest health: `http://127.0.0.1:20500/actuator/health`
- History query: `http://127.0.0.1:20200/swagger-ui/index.html`
- Analytics metrics: `http://127.0.0.1:20300/swagger-ui/index.html`

## Kafka Topic 验证

历史服务必须消费事件 topic 和 raw topic：

- `vehicle.event.gb32960.v1`
- `vehicle.raw.gb32960.v1`
- `vehicle.event.jt808.v1`
- `vehicle.raw.jt808.v1`
- `vehicle.event.mqtt-yutong.v1`
- `vehicle.raw.mqtt-yutong.v1`

使用 Kafka CLI 或管理工具检查 topic 是否存在，并确认 history consumer group 在测试流量后没有持续堆积。默认 `KAFKA_GROUP_HISTORY=vehicle-history` 时，实际 group 会拆成：

- `vehicle-history-gb32960-event`
- `vehicle-history-gb32960-raw`
- `vehicle-history-jt808-event`
- `vehicle-history-jt808-raw`
- `vehicle-history-yutong-mqtt-event`
- `vehicle-history-yutong-mqtt-raw`

## JT808 实时转发验收

如果外部平台已经把 JT808 报文转发到本机 TCP `808`，至少观察 60 秒日志和 Kafka 数据。

必须拿到以下证据：

- `jt808-ingest-app` 日志显示接入连接或解析到上游消息 ID。
- Kafka 的 `vehicle.event.jt808.v1` 和 `vehicle.raw.jt808.v1` 有新增记录。
- `vehicle-history-app` 日志没有持续出现 consumer 或 TDengine 写入失败。

消费完成后，使用已知终端手机号查询：

```bash
curl -sS 'http://127.0.0.1:20200/api/event-history/jt808/locations?phone=<phone>&dateFrom=2026-06-29T00:00:00%2B08:00&dateTo=2026-06-29T23:59:59%2B08:00&limit=10'
```

预期：响应包含 `items`，并且每条记录包含 `eventTime`、`phone`、`longitude`、`latitude`、`speedKmh`、`rawUri`、`metadataJson`。

## GB32960 Snapshot / 字段投影验收

默认高吞吐模式下，GB32960 全字段查询不依赖 telemetry_fields 宽表。history 先查 TDengine `raw_frames`，直接使用 RAW 行中的 `parsedJson`/`parsedFields` 生成 snapshot 和字段投影：

```bash
curl -sS 'http://127.0.0.1:20200/api/event-history/gb32960/snapshots?vin=<vin>&dateFrom=2026-06-29T00:00:00%2B08:00&dateTo=2026-06-29T23:59:59%2B08:00&limit=10'

curl -sS 'http://127.0.0.1:20200/api/event-history/gb32960/snapshots/fields?vin=<vin>&fields=VEHICLE.speedKmh,VEHICLE.totalMileageKm,POSITION_V2016.longitude,POSITION_V2016.latitude&dateFrom=2026-06-29T00:00:00%2B08:00&dateTo=2026-06-29T23:59:59%2B08:00&limit=10'
```

预期：snapshot 返回 `sourceFrames.rawArchiveUri` 和解析后的 `blocks`；字段投影返回所选字段。新增协议字段后，先通过 replay 或专用解析任务补写 `raw_frames.parsedJson`，再基于历史 RAW 结构化结果查询。

## TDengine 直接检查

使用 TDengine CLI 或 JDBC 客户端检查超级表和子表是否创建：

```sql
USE vehicle_ts;
SHOW STABLES;
SELECT COUNT(*) FROM raw_frames;
SELECT COUNT(*) FROM vehicle_locations;
SELECT COUNT(*) FROM jt808_locations;
```

预期：

- `raw_frames`、`vehicle_locations`、`jt808_locations` 存在；有对应协议实时流量后，计数持续增长。
- 字段趋势宽表不属于 history-app 验收范围，由独立字段解析服务负责。
- 有实时流量后，计数持续增加。

## 失败语义

- TDengine 不可达时，TDengine 查询接口应该返回 HTTP `503`，消息类似 `tdengine history query failed`。
- TDengine 未启用时，TDengine 专属接口不应该出现在 OpenAPI 中。
- 路径不存在时，API 层应该返回 HTTP `404`，而不是泛化的存储失败。

## 2026-06-29 本机实时链路验证结果

运行服务：

- `com.lingniu.jt808.808`: `jt808-ingest-app`，TCP `808`，HTTP `20400`
- `com.lingniu.gb32960.32960`: `gb32960-ingest-app`，TCP `32960`，HTTP `20100`
- `com.lingniu.vehicle-history.jt808`: `vehicle-history-app`，HTTP `20200`

当次健康检查均返回 `{"status":"UP"}`。

可重复运行 live 验收工具：

```bash
python3 tools/vehicle_ingest_live_verify.py \
  --tdengine-rest-url 'http://115.29.185.82:6041/rest/sql/vehicle_ts' \
  --tdengine-username root \
  --tdengine-password '<tdengine-password>' \
  --history-base-url 'http://127.0.0.1:20200' \
  --date-from '2026-06-29 00:00:00' \
  --date-to '2026-06-29 23:59:59' \
  --jt808-peer-like '222.66.200.68:%' \
  --gb32960-peer-like '115.29.187.205:%'
```

该工具输出 `pass`、`warn` 或 `fail`：当前正式 GB32960 只有平台登录、没有车辆 `0x02` 时会输出 `warn`，避免把平台在线误判为车辆数据在线。正式车辆数据验收时增加 `--require-gb32960-vehicle-realtime`，缺少非测试 VIN 的 `0x02` 会直接返回非 0 退出码。

JT808 真实转发链路已验证：

- 本机 TCP `808` 有外部平台 `222.66.200.68` 持续发送 JT808 报文。
- `vehicle.raw.jt808.v1` 和 `vehicle.event.jt808.v1` 被 history 消费。
- `raw_frames` 中 `protocol='JT808'` 的真实行数已超过 `36000`。
- 外部平台真实 0x0100 注册帧已超过 `347` 条，真实 0x0200 位置帧已超过 `12400` 条。
- `jt808_locations` 中位置行数已超过 `8783`。
- 当前高吞吐默认链路不维护 `telemetry_fields`；JT808 明细验证以 `raw_frames`、`vehicle_locations` 和 API 分页结果为准。
- 分页 API 已用终端号 `13079963301` 验证第一页和 nextCursor 第二页，样例位置包含 `longitude=118.913846`、`latitude=31.927309`、`statusFlag=3` 和 `archive://2026/06/29/JT808/...` 原始帧引用。
- 注册帧样例：终端号 `13079963320`、deviceId `9963320`、deviceType `SEG-9888G`、plate `沪A61559F`、maker `70112`、province `31`、city `113`、plateColor `2`。生产 VIN 反写依赖 MySQL identity store，后续 raw/event 的 VIN 解析也从 MySQL 绑定读取。

## JT808 本机 smoke / 轻压测工具

仓库提供 `tools/jt808_e2e_smoke.py`，用于重复验证 TCP `808` 到 TDengine history 的闭环：

```bash
export TDENGINE_REST_URL='http://<tdengine-host>:6041/rest/sql/vehicle_ts'
export TDENGINE_USERNAME='root'
export TDENGINE_PASSWORD='<tdengine-password>'

python3 tools/jt808_e2e_smoke.py \
  --connection-mode session \
  --start-phone 13079962000 \
  --frames 3 \
  --expect-history-count 3 \
  --verify-pagination \
  --archive-root "$PROJECT_ROOT/data/archive-jt808" \
  --tdengine-rest-url "$TDENGINE_REST_URL" \
  --tdengine-username "$TDENGINE_USERNAME" \
  --tdengine-password "$TDENGINE_PASSWORD"
```

默认 `--connection-mode per-frame`，适合单帧连接 smoke。生产验收建议使用 `--connection-mode session` 覆盖同一连接内连续位置帧。脚本会输出发送数量、history 可见数量、分页验证结果、raw archive 检查结果和 `tdengineRawFrames`。`tdengineRawFrames` 表示本次可见位置记录中的唯一 `rawUri` 有多少个已确认写入 TDengine `raw_frames`。
- 合成 0x0100 注册帧终端号 `13079969999` 已验证：TCP `808` 返回 `0x8100` 注册 ACK，`raw_frames.metadata_json` 包含 `jt808.register.province`、`jt808.register.city`、`jt808.register.maker`、`jt808.register.deviceType`、`jt808.register.deviceId`、`jt808.register.plateColor`、`jt808.register.plate`，对应 raw archive 文件存在。

复查 SQL：

```sql
USE vehicle_ts;
SELECT COUNT(*) FROM raw_frames WHERE protocol = 'JT808';
SELECT COUNT(*) FROM jt808_locations WHERE protocol = 'JT808';
SELECT event_time, vehicle_key, phone, message_id, raw_uri
  FROM raw_frames
  WHERE protocol = 'JT808'
  ORDER BY event_time DESC
  LIMIT 5;
SELECT ts, frame_id, phone, message_id, metadata_json, raw_uri
  FROM raw_frames
  WHERE protocol = 'JT808' AND phone = '13079969999' AND message_id = 256
  ORDER BY ts DESC
  LIMIT 3;
```

复查 API：

```bash
curl -sS 'http://127.0.0.1:20200/api/event-history/jt808/locations?phone=13079963301&dateFrom=2026-06-29T00:00:00%2B08:00&dateTo=2026-06-29T23:59:59%2B08:00&order=DESC&limit=3'
curl -sS 'http://127.0.0.1:20200/api/event-history/jt808/raw-frames?phone=13079963320&messageId=256&dateFrom=2026-06-29T00:00:00%2B08:00&dateTo=2026-06-29T23:59:59%2B08:00&order=DESC&limit=3'
```

`/api/event-history/jt808/raw-frames` 用于排查注册、鉴权、心跳、位置等原始帧索引。返回项会保留 `messageId`、`messageIdHex`、`rawUri`、`parseStatus`、`peer`、`vin`、`phone` 和完整 `metadataJson`；注册帧的 `metadataJson` 包含 `jt808.register.province`、`jt808.register.city`、`jt808.register.maker`、`jt808.register.deviceType`、`jt808.register.deviceId`、`jt808.register.plateColor`、`jt808.register.plate`、`identitySource`、`identityResolved` 等字段。

GB32960 链路已用正式平台登录和合成实时帧验证：

- 正式平台 `115.29.187.205` 已连接 TCP `32960`，服务收到 `0x05 PLATFORM_LOGIN` 并返回 ACK；日志显示账号 `Hyundai`、策略 `ALLOW`。
- 2026-06-29 正式平台登录 raw 已有 `3` 条，均落入 TDengine `raw_frames`，可通过 RAW 查询看到 `parsedJson` 中的 `PLATFORM_LOGIN` 解析结果。
- 当前正式平台尚未发送车辆实时上报 `0x02`；非测试 VIN 的 `message_id=2` 计数为 `0`，因此不能把平台登录误判为车辆数据在线。
- TCP `32960` 返回 GB32960 `2323` ACK。
- `raw_frames` 中测试 VIN `LTEST202606290001` 已有 `2` 条 raw 记录。
- 当前默认通过 GB32960 snapshot/fields 即时解析 `raw_frames` 中的 RAW JSON，不维护字段宽表增长。
- latest raw 包含 `archive://2026/06/29/GB32960/LTEST202606290001/...`。
- snapshots API 可查到 `speedKmh=62.4`、`totalMileageKm=223456.7`、`longitude=116.397128`、`latitude=39.916527`。

复查 SQL：

```sql
USE vehicle_ts;
SELECT COUNT(*) FROM raw_frames
  WHERE protocol = 'GB32960' AND vin = 'LTEST202606290001';
SELECT event_time, vin, raw_uri, frame_id
  FROM raw_frames
  WHERE protocol = 'GB32960' AND vin = 'LTEST202606290001'
  ORDER BY event_time DESC
  LIMIT 5;
SELECT ts, vin, message_id, event_time, raw_uri, peer, metadata_json
  FROM raw_frames
  WHERE protocol = 'GB32960' AND peer LIKE '115.29.187.205:%'
  ORDER BY ts DESC
  LIMIT 5;
SELECT COUNT(*) FROM raw_frames
  WHERE protocol = 'GB32960' AND message_id = 2 AND vin <> '' AND vin NOT LIKE 'LTEST%';
```

也可以直接运行仓库 smoke 工具验证 TCP `32960`、GB32960 snapshot/fields、TDengine `raw_frames`。如果需要复核冷备 `.bin`，再传入接入服务 archive 根目录：

```bash
export TDENGINE_REST_URL='http://<tdengine-host>:6041/rest/sql/vehicle_ts'
export TDENGINE_USERNAME='root'
export TDENGINE_PASSWORD='<tdengine-password>'

python3 tools/gb32960_e2e_smoke.py \
  --archive-root "$PROJECT_ROOT/data/archive-jt808" \
  --tdengine-rest-url "$TDENGINE_REST_URL" \
  --tdengine-username "$TDENGINE_USERNAME" \
  --tdengine-password "$TDENGINE_PASSWORD"
```

预期输出包含 `records >= 1`、`fieldCounts` 中关键字段均大于 0、`tdengineRawFrames` 等于本次可见唯一 `rawUri` 数量。传入 `--archive-root` 时，`archiveChecked >= 1` 说明冷备文件也能复核；这里的 `records` 字段表示 snapshot 数量，用于兼容早期脚本输出名。

复查 API：

```bash
curl -sS 'http://127.0.0.1:20200/api/event-history/gb32960/snapshots?vin=LTEST202606290001&dateFrom=2026-06-29T00:00:00%2B08:00&dateTo=2026-06-29T23:59:59%2B08:00&limit=10'
curl -sS 'http://127.0.0.1:20200/api/event-history/gb32960/snapshots/fields?vin=LTEST202606290001&fields=VEHICLE.speedKmh,VEHICLE.totalMileageKm,POSITION_V2016.longitude,POSITION_V2016.latitude&dateFrom=2026-06-29T00:00:00%2B08:00&dateTo=2026-06-29T23:59:59%2B08:00&limit=10'
```

## 当前注意事项

- 当前本机使用 TDengine WebSocket JDBC，例如 `jdbc:TAOS-WS://<tdengine-host>:6041/vehicle_ts`。
- `KAFKA_CONSUMER_AUTO_OFFSET_RESET=latest` 适合生产接入新流量；如果要回放历史 Kafka 数据，需要切换 consumer group 或重置 offset。
- `vehicle-history-app` raw 和 event 使用不同 consumer binding；raw consumer 必须开启，否则 `raw_frames` 不会持续增长。
- JT808 设备没有 VIN 映射时，`vehicle_key` 使用 `jt808:<phone>`；启用 MySQL identity store 后，registration 表中已反写 VIN 的 phone/deviceId/plate 会用于后续 raw/event 的 VIN 解析。
- GB32960 正式对端目前只验证到平台登录；等待真实车辆 `0x02` 到达后，再用非测试 VIN 复查 `vehicle_locations`、snapshot/fields API 和导出能力。
