diff --git a/docs/superpowers/specs/2026-03-17-mqtt-raw-ingestion-design.md b/docs/superpowers/specs/2026-03-17-mqtt-raw-ingestion-design.md new file mode 100644 index 0000000..7d8c2b1 --- /dev/null +++ b/docs/superpowers/specs/2026-03-17-mqtt-raw-ingestion-design.md @@ -0,0 +1,252 @@ +# MQTT 原始数据接收与保存设计(双阶段) + +## 1. 背景与目标 + +在现有 `full-stack-fastapi-template` 工程基础上,新增 MQTT 采集能力,用于接收终端上报的 GNSS 数据并可靠保存,支持后续: + +- 前端秒级实时展示(设备看板 + 逐条观测流) +- 按天/时间段下载原始数据供外部软件分析 + +本设计阶段仅实现“接收 + 保存 + 可解析落库链路”,不实现前端页面。 + +## 2. 已确认约束 + +- 部署环境:腾讯云,同机已有 EMQX +- Broker 地址:`101.35.119.226:1883` +- 鉴权方式:用户名/密码 +- Topic:仅 `terminal/{device_id}/raw` +- `device_id`:直接从 topic 中提取 +- 上报形态:每 1~10 秒一条消息,每条消息为“完整帧/完整语句集合” +- 负载内容:可能为 NMEA only、RTCM3.2 only 或二者混合 +- NMEA 类型:GGA / RMC / GSV / GSA / GST +- RTCM:本期仅做“消息类型级别摘要” +- 存储策略:90 天热数据 + 本地冷归档 +- 下载要求:下载内容必须是原始 MQTT payload 字节流,不附加元信息 +- 下载文件后缀:`.txt`(内容可包含二进制字节) + +## 3. 架构方案与结论 + +### 3.1 备选方案 + +1. FastAPI 内嵌 MQTT 客户端 +2. DB 队列双阶段(独立 ingestor + parser worker) +3. 外部消息队列三阶段 + +### 3.2 选型结论 + +采用 **方案 2:DB 队列双阶段**。 + +理由: + +- 避免 FastAPI 多 worker 下重复订阅/重复入库风险 +- 接收链路与解析链路解耦,稳定性更高 +- 原始数据先落库,可重试、可回放、可追溯 +- 为后续实时展示和下载提供稳定基础 + +## 4. 组件职责 + +### 4.1 mqtt-ingestor(采集层) + +- 常驻独立进程,连接 EMQX 并订阅 `terminal/+/raw` +- 回调中仅执行轻量逻辑: + - 提取 `device_id` + - 记录 `topic/qos/retain/received_at` + - 原样保存 `payload_bytes` + - 计算 `payload_sha256` 与 `payload_size` + - 插入 `terminal_message_raw`(`parse_status='pending'`) +- 不执行 NMEA/RTCM 深解析 + +### 4.2 parser-worker(解析层) + +- 轮询 `terminal_message_raw` 中待解析记录 +- 解析 NMEA + RTCM3.2 摘要 +- 写入事件表 `terminal_observation_event` +- Upsert 最新状态表 `terminal_device_state` +- 失败可重试,错误可观测,不丢原始数据 + +### 4.3 backend API(服务层) + +- 本期不新增前端页面 +- 后续基于 `event + state + archive manifest` 增加秒级展示与下载接口 + +## 5. 数据模型设计 + +### 5.1 terminal_message_raw(热数据原始表) + +用途:保存原始消息,作为解析队列与下载来源。 + +建议字段: + +- `id` (uuid, pk) +- `device_id` (text, index) +- `topic` (text) +- `received_at` (timestamptz, index) +- `payload_bytes` (bytea) +- `payload_size` (int) +- `payload_sha256` (char(64), index) +- `qos` (smallint) +- `retain` (bool) +- `parse_status` (`pending|processing|succeeded|failed`, index) +- `parse_attempts` (int, default 0) +- `next_retry_at` (timestamptz, nullable, index) +- `parse_error` (text, nullable) +- `created_at` (timestamptz) + +### 5.2 terminal_observation_event(解析事件流) + +用途:支撑逐条观测流展示与历史分析。 + +建议字段: + +- `id` (uuid, pk) +- `raw_id` (uuid, fk -> terminal_message_raw.id, index) +- `device_id` (text, index) +- `event_time` (timestamptz, index) +- `event_type` (text) + 示例:`nmea_gga` / `nmea_rmc` / `nmea_gsv` / `nmea_gsa` / `nmea_gst` / `rtcm_summary` +- `payload_json` (jsonb) +- `created_at` (timestamptz) + +### 5.3 terminal_device_state(设备最新状态) + +用途:支撑前端设备看板秒级刷新。 + +建议字段: + +- `device_id` (text, pk) +- `last_event_time` (timestamptz) +- `fix_status` (text) +- `lat` / `lon` (double precision) +- `alt_m` (double precision) +- `satellites_used` (int) +- `satellite_signal_summary` (jsonb) +- `rtcm_type_summary` (jsonb) +- `updated_at` (timestamptz) + +### 5.4 terminal_raw_archive_manifest(冷归档索引) + +用途:记录本地归档文件与时间范围映射。 + +建议字段: + +- `id` (uuid, pk) +- `device_id` (text, index) +- `range_start` / `range_end` (timestamptz, index) +- `file_path` (text, unique) +- `file_size` (bigint) +- `sha256` (char(64)) +- `record_count` (bigint) +- `created_at` (timestamptz) + +## 6. 双阶段流水线细节 + +### 6.1 入站(ingestor) + +- MQTT 建议参数:`clean_session=False`、固定 `client_id`、QoS=1 +- 批量入库建议:每 100 条或 200ms flush 一次事务 +- 目标:尽快持久化原始字节,避免解析阻塞接收 + +### 6.2 解析(parser-worker) + +批处理取数: + +- 条件:`parse_status='pending'` 且 `next_retry_at` 到期 +- 顺序:`ORDER BY received_at` +- 锁:`FOR UPDATE SKIP LOCKED` +- 批量:`LIMIT 100~500` + +处理规则: + +- 成功:`parse_status='succeeded'` +- 失败:`parse_status='failed'` + `parse_attempts+1` + `parse_error` +- 重试:退避后设置 `next_retry_at` + +幂等建议: + +- 事件表增加防重策略(如 `raw_id + event_type + event_time` 约束或哈希去重) +- `terminal_device_state` 用 upsert,仅更新更晚 `event_time` + +## 7. 解析策略(本期) + +### 7.1 NMEA + +- 解析 GGA/RMC/GSV/GSA/GST +- 每条语句产出一条 `terminal_observation_event` + +### 7.2 RTCM3.2 + +- 仅提取消息类型摘要(如 1005/1077/1087) +- 每条 raw payload 产出一条 `rtcm_summary` 事件,包含类型计数与最近出现时间 + +### 7.3 混合负载 + +- 单 payload 中可同时包含 NMEA 与 RTCM 片段 +- 解析器按字节扫描拆分,分别产出事件 +- 部分片段解析失败不影响其余片段;原始 payload 永远保留 + +## 8. 热数据与本地冷归档 + +### 8.1 热数据 + +- `terminal_message_raw` 保留 90 天 + +### 8.2 本地冷归档 + +- 定时任务离峰执行 +- 归档粒度:`device_id + 日`(必要时可按小时分片) +- 路径示例:`/data/archive/gnss//YYYY-MM-DD/` +- 文件内容:仅原始 payload 字节流拼接(无额外元信息) +- 文件名示例:`device___.txt` + +### 8.3 校验与清理 + +- 写入 `manifest`:时间范围、条数、sha256、文件路径 +- 校验通过后,清理超过 90 天热数据 + +## 9. 下载行为约束(后续接口实现遵循) + +- 下载内容必须与原始 MQTT payload 字节一致 +- 近 90 天:从热表按时间顺序拼接输出 +- 超过 90 天:从本地归档读取输出 +- 跨热/冷边界:按时间顺序拼接两段数据 + +## 10. 运维与可观测性 + +必须监控: + +- ingestor:连接状态、入站速率、入库失败 +- parser:pending backlog、解析成功率、平均延迟、失败重试 +- archive:每日归档成功/失败、磁盘余量 + +建议告警: + +- `pending` 队列持续增长 +- parser 失败率超阈值 +- 归档目录磁盘不足 + +## 11. 配置项建议(.env) + +- `MQTT_HOST=101.35.119.226` +- `MQTT_PORT=1883` +- `MQTT_USERNAME=...` +- `MQTT_PASSWORD=...` +- `MQTT_TOPIC_PATTERN=terminal/+/raw` +- `MQTT_CLIENT_ID=spatialhub-mqtt-ingestor` +- `RAW_HOT_RETENTION_DAYS=90` +- `RAW_ARCHIVE_BASE_DIR=/data/archive/gnss` +- `PARSER_BATCH_SIZE=200` +- `PARSER_POLL_INTERVAL_MS=300` + +## 12. 风险与后续演进 + +- 使用 `.txt` 扩展名存放二进制流可能被文本工具误打开;但不影响下游软件按字节读取 +- 当设备量增长时,可水平扩展 parser-worker(依赖 `SKIP LOCKED`) +- 后续实时能力:新增 SSE/WebSocket 推送层,数据源直接复用 `event/state` + +## 13. 验收标准(本期) + +- 能稳定接收 `terminal/{device_id}/raw` 消息并原样入库 +- 混合 payload 可提取 NMEA 事件和 RTCM 类型摘要 +- 设备最新状态可持续更新 +- 支持 90 天热数据保留与本地冷归档落盘 +- 能按时间段导出原始数据(文件后缀 `.txt`,内容无附加字段)