Files
lingniu-vehicle-ingest/docs/superpowers/specs/2026-07-01-go-ingest-redesign-design.md
2026-07-01 23:00:06 +08:00

16 KiB
Raw Blame History

Go 车辆接入重构设计规格

目标

使用 Go 重新设计车辆数据接入运行面,让系统回到第一性原则:

  • 接入层只负责可靠收包、协议解析、应答、身份解析和投递。
  • Kafka 是唯一在线消息通道。
  • TDengine 保存高吞吐时序明细和完整 RAW 解析 JSON。
  • MySQL 保存低频配置、身份映射和每日统计窄表。
  • Redis 保存可重建的准实时状态。
  • 32960、JT808、宇通 MQTT 使用同一套数据层接口,避免每个协议各自落库。

第一阶段只做生产链路的骨架和核心数据闭环接收、解析、Kafka、TDengine、MySQL 统计、Redis 实时查询、ECS 验证。历史查询 API 和业务侧复杂分析放在后续阶段。

参考原则

本设计只吸收开源项目的边界思想,不复制外部实现代码。

  • GB32960 参考 DarkInno/gb32960-go-sdk连接管理、Handler、Forwarder、鉴权接口分离。
  • JT808 参考 cuteLittleDevil/go-jt808:高并发 Go 原生 TCP、协议核心小而可扩展、事件/适配器机制。
  • JT808 参考 fakeyanss/jt808-server-goFramePayload -> PacketData -> JT808Msg -> Reply 的分层处理、Gateway 模式、保活和版本兼容。
  • TDengine 参考官方数据模型:按数据采集点建立 supertable静态维度做 tags明细数据按子表写入Go 连接优先使用 WebSocket 驱动。

非目标

  • 第一阶段不继续扩展 Java 服务。
  • 第一阶段不做统一大查询 API。
  • 第一阶段不恢复信达 Push。
  • 第一阶段不把所有协议字段展开成一张超宽 TDengine 表。
  • Redis 不作为历史事实源。
  • MySQL 不保存高频原始明细。

目标目录

新的 Go 运行面放在 go/vehicle-gateway,不继续沿用实验性的多 module 分散结构。现有 go/ingest-edgego/vehicle-state 可以作为迁移素材,最终归并或删除。

go/vehicle-gateway/
  cmd/
    gateway/          # 协议接入进程32960 TCP、JT808 TCP、宇通 MQTT
    history-writer/   # Kafka -> TDengine RAW/location/mileage points
    stat-writer/      # Kafka -> MySQL 每日统计窄表
    realtime-api/     # Redis 准实时查询 API
  internal/
    config/           # 环境变量、配置文件、校验
    gateway/          # TCP/MQTT 生命周期、连接限制、优雅停机
    protocol/
      gb32960/        # 32960 编解码
      jt808/          # 808 编解码
      yutongmqtt/     # 宇通 MQTT payload 解析
    identity/         # VIN、phone、device_id、plate 绑定
    envelope/         # 统一事件模型
    eventbus/         # Kafka producer/consumer
    history/          # TDengine adapter
    stats/            # 每日里程聚合
    realtime/         # Redis adapter 与快照合并
    observability/    # 日志、metrics、健康检查

协议接入设计

GB32960

职责:

  • 监听 TCP 32960
  • 支持车辆登入、实时信息上报、补发信息上报、车辆登出、平台登入/登出、心跳、校时。
  • 正确返回协议应答。
  • 完整解析 2016 数据单元:
    • 整车数据
    • 驱动电机数据
    • 燃料电池数据
    • 发动机数据
    • 车辆位置数据
    • 极值数据
    • 报警数据
    • 可充电储能装置电压数据
    • 可充电储能装置温度数据
  • RAW 中保存完整 parsed JSON。
  • 关键字段标准化为统一 field
    • speed_kmh
    • total_mileage_km
    • soc_percent
    • longitude
    • latitude
    • vehicle_status
    • charge_status

JT808

职责:

  • 监听 TCP 808
  • 支持 2011/2013 基础兼容,预留 2019 版本标记。
  • 分层处理:
    • Frame0x7e 包边界、转义、校验。
    • Packet消息头、手机号 BCD、流水号、分包。
    • Message注册、鉴权、心跳、注销、位置、批量位置、未知上行。
    • Reply注册应答、平台通用应答。
  • 终端手机号按协议头 BCD 解析,内部同时保留原始 BCD、规范化 phone、去前导零 phone。
  • 注册帧和鉴权帧写入 808 registration 表。
  • 如果设备直接上报位置帧,也要走 identity 解析,尝试用 phone、device_id、plate 找到 VIN。
  • 0200 附加信息必须解析表 27
    • 0x01 GPS 总里程DWORD单位 0.1 km。
    • 其他已知附加项结构化放入 parsed_json.additional
  • 标准化字段:
    • speed_kmh
    • total_mileage_km
    • longitude
    • latitude
    • altitude_m
    • direction_deg
    • alarm_flag
    • status_flag

宇通 MQTT

职责:

  • 使用正式 MQTT 配置订阅生产 topic。
  • 接收 payload 后解析为统一 envelope。
  • 按 payload 中的车辆标识解析 VIN、车牌、设备号。
  • 保存完整 payload 和 parsed JSON。
  • 标准化字段向 32960/808 对齐:
    • speed_kmh
    • total_mileage_km
    • longitude
    • latitude
    • soc_percent
    • vehicle_status

统一 Envelope

所有协议接入后输出同一结构。

{
  "event_id": "string",
  "trace_id": "string",
  "protocol": "GB32960|JT808|YUTONG_MQTT",
  "message_id": "string",
  "vin": "string",
  "vehicle_key": "string",
  "phone": "string",
  "device_id": "string",
  "plate": "string",
  "source_endpoint": "ip:port or mqtt://broker/topic",
  "event_time_ms": 0,
  "received_at_ms": 0,
  "raw_hex": "string",
  "raw_text": "string",
  "parsed": {},
  "fields": {
    "speed_kmh": 0,
    "total_mileage_km": 0,
    "longitude": 0,
    "latitude": 0
  },
  "parse_status": "OK|PARTIAL|BAD_FRAME",
  "parse_error": ""
}

规则:

  • event_id 使用协议、设备标识、消息流水、事件时间、RAW hash 生成,保证幂等。
  • Kafka partition key 优先使用 VIN没有 VIN 时使用 protocol:phone/device_id
  • parsed 保存协议完整结构化字段。
  • fields 只保存跨协议核心字段,用于统计、位置和实时快照。

Kafka Topic

生产只支持 Kafka。

Topic 内容 Key
vehicle.raw.gb32960.v1 32960 完整 RAW envelope VIN 或 vehicle_key
vehicle.raw.jt808.v1 808 完整 RAW envelope VIN 或 phone
vehicle.raw.yutong-mqtt.v1 宇通 MQTT 完整 RAW envelope VIN 或 vehicle_key
vehicle.event.unified.v1 统一轻量事件,用于业务消费 VIN 或 vehicle_key

接入层必须先写 RAW topic再写 unified event。后续消费者只依赖 Kafka不直接依赖接入进程内存。

发布可靠性:

  • Kafka 生产端必须开启同步写入RAW 写成功后才允许写 unified event。
  • Kafka 写入必须支持可配置重试、单次写超时和短退避,默认值为 KAFKA_PUBLISH_ATTEMPTS=3KAFKA_PUBLISH_TIMEOUT_MS=3000KAFKA_PUBLISH_BACKOFF_MS=100
  • gateway 支持本地磁盘 spool/WAL配置 KAFKA_SPOOL_DIR 后启用Kafka 长时间不可用时,发布失败的 envelope 先原子写入本地 JSON 文件。
  • spool 补发按文件名顺序执行,补发成功后删除文件;默认补发间隔由 KAFKA_SPOOL_REPLAY_INTERVAL_MS=1000 控制。
  • 如果 RAW 写 Kafka 失败并已落盘,同一个 event 的 unified event 不允许抢先写 Kafka也必须进入 spool确保恢复后按 RAW -> unified 顺序补发。
  • Kafka topic 不允许自动创建topic 和分区数由部署脚本或运维初始化,避免生产拼写错误造成隐性分流。

TDengine 数据库设计

如果现有 TDengine 库不符合目标,可以新建库并迁移。建议库名:

CREATE DATABASE IF NOT EXISTS lingniu_vehicle_ts KEEP 7300 DURATION 10 BUFFER 256;

raw_frames

保存完整 RAW 和完整 parsed JSON。

CREATE STABLE IF NOT EXISTS raw_frames (
  ts TIMESTAMP,
  frame_id NCHAR(64),
  event_id NCHAR(64),
  message_id INT,
  event_time TIMESTAMP,
  received_at TIMESTAMP,
  raw_size_bytes INT,
  raw_hex BINARY(16374),
  raw_text BINARY(16374),
  parsed_json BINARY(16374),
  fields_json BINARY(4096),
  parse_status NCHAR(16),
  parse_error BINARY(1024),
  source_endpoint NCHAR(128)
) TAGS (
  protocol NCHAR(32),
  vehicle_key NCHAR(64),
  vin NCHAR(32),
  phone NCHAR(32),
  device_id NCHAR(64)
);

说明:

  • 子表按 protocol + vehicle_key hash 创建。
  • RAW 查询优先按 tags 和时间范围过滤。
  • parsed_json 是完整协议字段;查询 API 可选择是否返回,避免默认大字段拖慢。

vehicle_locations

只保存位置查询核心字段。

CREATE STABLE IF NOT EXISTS vehicle_locations (
  ts TIMESTAMP,
  event_id NCHAR(64),
  frame_id NCHAR(64),
  received_at TIMESTAMP,
  longitude DOUBLE,
  latitude DOUBLE,
  altitude_m DOUBLE,
  speed_kmh DOUBLE,
  direction_deg INT,
  alarm_flag BIGINT,
  status_flag BIGINT,
  total_mileage_km DOUBLE
) TAGS (
  protocol NCHAR(32),
  vehicle_key NCHAR(64),
  vin NCHAR(32),
  phone NCHAR(32),
  device_id NCHAR(64)
);

vehicle_mileage_points

保存总里程采样点,用于回放重算和查询。

CREATE STABLE IF NOT EXISTS vehicle_mileage_points (
  ts TIMESTAMP,
  event_id NCHAR(64),
  frame_id NCHAR(64),
  received_at TIMESTAMP,
  total_mileage_km DOUBLE,
  speed_kmh DOUBLE,
  longitude DOUBLE,
  latitude DOUBLE
) TAGS (
  protocol NCHAR(32),
  vehicle_key NCHAR(64),
  vin NCHAR(32),
  phone NCHAR(32),
  device_id NCHAR(64)
);

MySQL 数据库设计

MySQL 只保存配置、身份映射、808 注册鉴权和每日统计窄表。

vehicle_identity_binding

用于把外部标识定位到 VIN。

CREATE TABLE vehicle_identity_binding (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  vin VARCHAR(32) NOT NULL,
  plate VARCHAR(32) NULL,
  phone VARCHAR(32) NULL,
  device_id VARCHAR(64) NULL,
  remark VARCHAR(255) NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  UNIQUE KEY uk_vin (vin),
  KEY idx_plate (plate),
  KEY idx_phone (phone),
  KEY idx_device_id (device_id)
);

jt808_registration

只记录 808 独有的注册、鉴权和在线身份信息。

CREATE TABLE jt808_registration (
  phone VARCHAR(32) PRIMARY KEY,
  vin VARCHAR(32) NULL,
  plate VARCHAR(32) NULL,
  device_id VARCHAR(64) NULL,
  manufacturer_id VARCHAR(32) NULL,
  terminal_model VARCHAR(64) NULL,
  terminal_id VARCHAR(64) NULL,
  auth_code VARCHAR(128) NULL,
  source_endpoint VARCHAR(128) NULL,
  first_registered_at DATETIME NULL,
  latest_registered_at DATETIME NULL,
  latest_authed_at DATETIME NULL,
  latest_seen_at DATETIME NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  KEY idx_vin (vin),
  KEY idx_plate (plate),
  KEY idx_device_id (device_id)
);

vehicle_daily_metric

32960 和 808 的每日里程、每日总里程都进入同一张窄表。

CREATE TABLE vehicle_daily_metric (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  vin VARCHAR(32) NOT NULL,
  stat_date DATE NOT NULL,
  protocol VARCHAR(32) NOT NULL,
  metric_key VARCHAR(64) NOT NULL,
  metric_value DECIMAL(18,3) NOT NULL,
  metric_unit VARCHAR(16) NOT NULL,
  first_total_mileage_km DECIMAL(18,3) NULL,
  latest_total_mileage_km DECIMAL(18,3) NULL,
  sample_count BIGINT NOT NULL DEFAULT 0,
  calculation_method VARCHAR(64) NOT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  UNIQUE KEY uk_daily_metric (vin, stat_date, protocol, metric_key),
  KEY idx_stat_date (stat_date),
  KEY idx_protocol_metric (protocol, metric_key)
);

指标:

  • daily_mileage_km = max(total_mileage_km) - min(total_mileage_km)
  • daily_total_mileage_km = max(total_mileage_km)

统计消费者可幂等重放。每个样本 upsert 时只维护当天 min/max total_mileage_km,不依赖进程内状态。

Redis 实时数据设计

Redis 保存可从 Kafka 重建的数据。

Key 内容
vehicle:online:{vin} 在线状态、最后活跃时间、协议列表
vehicle:latest:{vin} 合并后的 VIN 最新快照
vehicle:latest:{vin}:{protocol} 单协议最新快照
vehicle:last_seen VIN 到最后接收时间的 sorted set

合并规则:

  • 位置以最新 event_time 为准。
  • 总里程取最新上报值,不做递增假设。
  • 32960 多帧字段按 field timestamp 合并。
  • 808 和 MQTT 不覆盖其他协议独有字段。
  • Redis TTL 用于在线判断,不作为数据删除依据。

数据流

flowchart LR
  source["车辆 / 平台"] --> gateway["Go gateway"]
  gateway --> parser["protocol parser"]
  parser --> identity["identity resolver"]
  identity --> kafkaRaw["Kafka RAW topics"]
  identity --> kafkaEvent["Kafka unified event"]
  kafkaRaw --> history["history-writer"]
  kafkaRaw --> stats["stat-writer"]
  kafkaEvent --> realtime["realtime consumer"]
  history --> td["TDengine raw_frames / locations / mileage_points"]
  stats --> mysql["MySQL vehicle_daily_metric"]
  realtime --> redis["Redis latest state"]

错误处理

  • 协议坏帧仍写 RAW topicparse_status=BAD_FRAME
  • 可部分解析的帧写 parse_status=PARTIAL,保留 parse error。
  • Kafka 写失败时接入层先按配置重试;重试耗尽后不写 unified event必须打错误日志和 metrics。
  • 启用 KAFKA_SPOOL_DIRKafka 重试耗尽会落本地 spool如果 spool 写失败,才视为接入层最终失败。
  • TDengine 写失败不提交 Kafka offset。
  • MySQL 统计写失败不提交 Kafka offset。
  • Redis 写失败不影响历史和统计,但要通过 metrics 暴露。
  • 808 相同 phone 新连接时,旧连接应被替换并记录 latest_seen。

部署设计

生产 ECS 运行 Go 二进制或容器:

  • vehicle-gateway
  • vehicle-history-writer
  • vehicle-stat-writer
  • vehicle-realtime-api

配置来源:

  • 环境变量优先。
  • Nacos 可作为后续配置中心,但 Go 第一阶段必须能只靠环境变量启动,便于 Portainer 部署和故障恢复。

TDengine 连接:

  • 优先 WebSocket taosWS
  • 若当前 ECS 只开放原生端口,可短期兼容 taosSql,但代码配置必须显式标记为兼容模式。

验证计划

本地验证

  • 用样例 808 报文验证:
    • phone BCD 解析。
    • 0200 经纬度、速度、方向、时间。
    • 表 27 0x01 GPS 总里程。
  • 用真实 32960 报文验证:
    • 登录应答正确。
    • 实时帧和补发帧都能写 RAW。
    • 位置和总里程字段进入统一 fields。
  • 用宇通 MQTT payload 验证:
    • payload 完整保存。
    • 核心字段归一化。
  • 不连 Kafka 时可输出 JSON log。
  • 连接 Kafka 时 RAW topic 可消费到 envelope。

ECS 验证

  • 部署前先旁路接收或短窗口切流。
  • 验证端口:
    • 32960
    • 808
    • MQTT 订阅连接。
  • 验证 Kafka topic 有持续写入。
  • 验证 TDengine
    • raw_frames 有三种协议数据。
    • vehicle_locations 能按 VIN、协议、时间分页查询。
    • vehicle_mileage_points 有 32960 和 808 总里程采样。
  • 验证 MySQL
    • vehicle_daily_metric 有 32960 和 808 的 daily_mileage_kmdaily_total_mileage_km
  • 验证 Redis
    • 查询 VIN 在线。
    • 查询 VIN 合并实时快照。
    • 查询 VIN 分协议实时快照。

验收标准

  • Go gateway 能在 ECS 上接收真实 32960、808、宇通 MQTT 数据。
  • 三种协议 RAW 都进入 Kafka。
  • Kafka 短暂写失败时gateway 按配置重试;单元测试覆盖首次失败后成功和重试耗尽返回错误。
  • Kafka 长时间不可用时gateway 可把 RAW/unified 写入本地 spool单元测试覆盖 RAW 已落盘时 unified 也落盘、恢复后按顺序补发并删除文件。
  • 三种协议 RAW 和 parsed JSON 都进入 TDengine。
  • 32960 和 808 的位置进入 vehicle_locations
  • 32960 和 808 的总里程采样进入 vehicle_mileage_points
  • 32960 和 808 的每日里程、每日总里程进入 MySQL 窄表。
  • Redis 可以查询 VIN 在线和实时数据。
  • 任一消费者宕机后可通过 Kafka offset 继续恢复。
  • 查询和统计验证使用 ECS 上的真实数据完成。

实施顺序

  1. 创建 go/vehicle-gateway 单 module迁移现有 Go 原型中可用的解析、Kafka、TDengine、MySQL、Redis 代码。
  2. 先写协议解析单元测试,覆盖 808 样例报文和 32960 核心字段。
  3. 完成统一 envelope 和 Kafka sink。
  4. 完成 TDengine schema bootstrap 与 history writer。
  5. 完成 MySQL schema bootstrap 与 stat writer。
  6. 完成 Redis realtime writer/API。
  7. 本地启动 gateway用 ECS 转发流量验证。
  8. 构建 Linux 镜像,部署 ECS。
  9. 按验收标准逐项验证并记录证据。