# 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-go`:`FramePayload -> PacketData -> JT808Msg -> Reply` 的分层处理、Gateway 模式、保活和版本兼容。 - TDengine 参考官方数据模型:按数据采集点建立 supertable,静态维度做 tags,明细数据按子表写入;Go 连接优先使用 WebSocket 驱动。 ## 非目标 - 第一阶段不继续扩展 Java 服务。 - 第一阶段不做统一大查询 API。 - 第一阶段不恢复信达 Push。 - 第一阶段不把所有协议字段展开成一张超宽 TDengine 表。 - Redis 不作为历史事实源。 - MySQL 不保存高频原始明细。 ## 目标目录 新的 Go 运行面放在 `go/vehicle-gateway`,不继续沿用实验性的多 module 分散结构。现有 `go/ingest-edge` 和 `go/vehicle-state` 可以作为迁移素材,最终归并或删除。 ```text 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 版本标记。 - 分层处理: - Frame:`0x7e` 包边界、转义、校验。 - 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 所有协议接入后输出同一结构。 ```json { "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,不直接依赖接入进程内存。 ## TDengine 数据库设计 如果现有 TDengine 库不符合目标,可以新建库并迁移。建议库名: ```sql CREATE DATABASE IF NOT EXISTS lingniu_vehicle_ts KEEP 7300 DURATION 10 BUFFER 256; ``` ### raw_frames 保存完整 RAW 和完整 parsed JSON。 ```sql 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 只保存位置查询核心字段。 ```sql 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 保存总里程采样点,用于回放重算和查询。 ```sql 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。 ```sql 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 独有的注册、鉴权和在线身份信息。 ```sql 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 的每日里程、每日总里程都进入同一张窄表。 ```sql 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 用于在线判断,不作为数据删除依据。 ## 数据流 ```mermaid 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 topic,`parse_status=BAD_FRAME`。 - 可部分解析的帧写 `parse_status=PARTIAL`,保留 parse error。 - Kafka 写失败时接入层不确认业务成功;协议是否应答按协议要求处理,但必须打错误日志和 metrics。 - 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_km`、`daily_total_mileage_km`。 - 验证 Redis: - 查询 VIN 在线。 - 查询 VIN 合并实时快照。 - 查询 VIN 分协议实时快照。 ## 验收标准 - Go gateway 能在 ECS 上接收真实 32960、808、宇通 MQTT 数据。 - 三种协议 RAW 都进入 Kafka。 - 三种协议 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. 按验收标准逐项验证并记录证据。