低空智控平台 后端go
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

38 KiB

嘉谷低空智控平台 — Go 后端设计文档

版本:v1.0
参考项目:pilot-train-server
MQTT 协议版本:v1(dock-edge)


1. 整体架构风格

单体服务,内部分模块,对齐 pilot-train-server 的代码风格:

  • 单一入口:一个 main.go,一个 go.mod,内部分模块组织代码
  • Controller → Service → Model/VO 分层,Service 直操 common.DB,无独立 Repository 层
  • 包级单例common.DBcommon.RedisPoolservice.DefaultXxxService
  • 本地 YAML 配置:一个 config.yaml,Viper 加载
  • JWT + 路由管理员鉴权:JWT 负责身份认证,user.roleAdminMiddleware 限制平台管理接口;业务数据由 service 按 user_id 过滤
  • 统一响应 {code, msg, data} + 中文验证错误翻译
  • Redigo Redis Pool(非 go-redis)

2. 技术选型

层面 选型 说明
Web 框架 Gin v1.10 轻量高性能,完全对齐 pilot-train-server
ORM GORM v1.25 + MySQL 驱动 完全对齐 pilot-train-server
数据库 MySQL 8.0 业务数据,完全对齐 pilot-train-server
时序数据库 TDengine 3.x 遥测/轨迹等时序数据,对齐 pilot-data 的用法
缓存 Redis 7+(Redigo Pool) 设备实时状态、Session、权限、流量余额
MQTT Broker EMQX 设备通信,由甲方/边缘侧部署,后台只作为客户端连接
对象存储 华为云 OBS S3 兼容 API,视频和升级包存储;通过 path-style SigV4 预签名直传
直播 阿里云直播 + SRT 推流 后台生成签名推流地址,通过 MQTT 下发给工控机
配置 Viper + config.yaml 本地配置文件
认证 JWT HS256 + 路由管理员鉴权 身份认证 + user.role 管理员接口限制,7 天 access / 30 天 refresh
日志 Zap + Lumberjack 结构化 + 日志轮转,对齐 pilot-train-server
验证 go-playground/validator 中文翻译(zh)
ID 生成 Snowflake tool/snowflake.go
WebSocket gorilla/websocket 前端实时推送
定时任务 robfig/cron 任务计划调度

3. 项目结构

laic-backend/
├── main.go                          # 入口,初始化 → MQTT + WS + HTTP
├── go.mod / go.sum
├── config.yaml                      # 本地配置文件
├── Makefile
│
├── common/                          # 基础设施
│   ├── gin.go                       # gin 引擎、统一响应 R、路由组
│   ├── mysql.go                     # GORM + OrgPlugin 多租户、Scopes
│   ├── tdengine.go                  # TDengine 连接池
│   ├── redis.go                     # Redigo Pool、Lua 原子操作
│   ├── config.go                    # Viper 加载 config.yaml
│   ├── busi_error.go                # BusiError + 错误码常量
│   ├── auth_util.go                 # admin 全量 / user 按 user_id 过滤
│   ├── pageutil.go                  # 分页
│   └── validator.go                 # 自定义验证器(phone, password 等)
│
├── middleware/                       # HTTP 中间件
│   ├── auth.go                      # JWT 校验 + 管理员角色鉴权
│   ├── cros.go                      # CORS
│   ├── trace.go                     # X-Trace-Id
│   ├── default_log.go               # 访问日志
│   ├── rate_limit.go                # 限流
│   └── validate.go                  # 验证错误翻译
│
├── cache/                           # Redis Key 管理
│   ├── cache_key.go                 # 所有 Redis Key 集中定义
│   └── cache_value.go               # 缓存值 DTO
│
├── token/                           # JWT
│   └── token.go                     # 生成/校验/刷新
│
├── logger/                          # 日志
│   ├── logger.go                    # 自定义 logger
│   └── gorm_log.go                  # GORM logger 适配
│
├── tool/                            # 工具
│   ├── snowflake.go                 # 分布式 ID
│   ├── geo.go                       # 地理计算(Haversine 距离)
│   └── str.go / time.go             # 字符串/时间工具
│
├── model/                           # GORM 数据模型
│   ├── user.go                      # User
│   ├── dock.go                      # Dock, Drone
│   ├── alarm.go                     # AlarmCode, Alarm
│   ├── command.go                   # DeviceCommandLog
│   ├── firmware.go                  # Firmware
│   ├── task.go                      # TaskPlan, TaskExecution, WorkflowState
│   ├── route.go                     # Route, RouteWaypoint
│   ├── media.go                     # LiveSession, Video, DownloadLog
│   └── billing.go                   # TrafficOrder, TrafficUsageLog, SimCard
│
├── vo/                              # 请求/响应 DTO
│   ├── user_vo.go
│   ├── dock_vo.go
│   ├── alarm_vo.go
│   ├── task_vo.go
│   ├── route_vo.go
│   └── media_vo.go
│
├── handler/                         # HTTP 处理器(Controller)
│   ├── auth_handler.go              # 登录/注册/短信/刷新
│   ├── user_handler.go              # 用户 CRUD
│   ├── account_handler.go           # 账户中心/流量/SIM卡
│   ├── dock_handler.go              # 机巢 CRUD/状态/指令/固件
│   ├── drone_handler.go             # 无人机 CRUD/遥测
│   ├── alarm_handler.go             # 告警列表/确认/关闭
│   ├── firmware_handler.go          # 固件管理(admin)
│   ├── task_handler.go              # 任务 CRUD/执行
│   ├── execution_handler.go         # 执行记录/轨迹
│   ├── route_handler.go             # 航线 CRUD
│   ├── live_handler.go              # 直播管理
│   ├── video_handler.go             # 视频/下载
│   └── system_handler.go            # 系统管理
│
├── service/                         # 业务逻辑(struct + Default 单例)
│   ├── auth_service.go
│   ├── user_service.go
│   ├── account_service.go
│   ├── dock_service.go
│   ├── drone_service.go
│   ├── command_service.go           # 指令构建/幂等/重试/超时
│   ├── alarm_service.go             # alarmCodes diff → 告警生命周期
│   ├── firmware_service.go          # 固件 CRUD + OTA 下发
│   ├── task_service.go
│   ├── execution_service.go
│   ├── route_service.go
│   ├── live_service.go              # 阿里云直播签名 + 推流会话
│   ├── video_service.go             # OSS 签名下载
│   └── billing_service.go           # 流量扣减
│
├── mqtt/                            # MQTT 客户端
│   ├── client.go                    # 连接管理(paho.mqtt.golang)
│   ├── subscriber.go                # 订阅设备上报 → Redis + 告警 diff
│   └── publisher.go                 # 发布指令到设备
│
├── websocket/                       # WebSocket
│   ├── hub.go                       # 连接池 + 广播
│   └── client.go                    # 单连接读写
│
├── route/                           # 路由注册
│   └── route.go                     # InitRouter
│
└── sql/                             # 数据库 DDL
    ├── 001_schema.sql               # 表结构
    ├── 002_alarm_codes.sql          # 告警码初始化数据
    └── 003_seed.sql                 # 种子数据

4. MQTT 接口设计

4.1 Topic 结构(由边缘端协议定义)

基础前缀:dock-edge/v1/dock/{dockId}

上行(边缘 → 后台):

Topic QoS Retain 说明
status/online 1 在线/离线/软件版本/bootId
state/dock 1 机巢当前完整状态 + alarmCodes
state/drone 1 无人机当前完整状态 + alarmCodes
state/workflow 1 任务/一键流程执行步骤
state/video 1 图传和推流状态
telemetry 0 1Hz 高频遥测(位置/姿态/电池/GPS)
command/ack 1 指令应答
ota/reported 1 升级进度

下行(后台 → 边缘):

Topic QoS Retain 说明
command 1 机巢/无人机/工作流/视频控制指令
ota/desired 1 升级任务

4.2 后台 MQTT 客户端职责

订阅:
  dock-edge/v1/dock/+/status/online    → 发现新设备 + 感知上下线
  dock-edge/v1/dock/+/state/dock       → Redis 状态 + alarmCodes diff
  dock-edge/v1/dock/+/state/drone      → Redis 状态 + alarmCodes diff
  dock-edge/v1/dock/+/state/workflow   → 任务步骤跟踪
  dock-edge/v1/dock/+/state/video      → 直播状态跟踪
  dock-edge/v1/dock/+/telemetry        → Redis 最新遥测 + TDengine 批量写入(500条/10s)
  dock-edge/v1/dock/+/command/ack      → 更新 device_command_log
  dock-edge/v1/dock/+/ota/reported     → 升级进度

发布:
  dock-edge/v1/dock/{dockId}/command   → 设备控制指令
  dock-edge/v1/dock/{dockId}/ota/desired → 升级任务

4.3 指令下发流程

POST /v1/docks/:id/command {type:"workflow.one_key_takeoff"}
  → command_service:
      1. 生成 commandId(Snowflake)
      2. 生成 requestId(UUID)
      3. 校验设备在线 + 安全条件
      4. INSERT device_command_log(id=commandId, status='sent')
      5. MQTT Publish → EMQX → 工控机
  → 工控机: 校验 → command/ack {accepted:true, resultCode:"OK"}
  → mqtt/subscriber 收到 ACK → 更新 device_command_log(status='acked')
  → 工控机: 逐步骤发布 state/workflow
  → mqtt/subscriber → WS 推送前端

超时重试(30s 未 ACK):
  → command_service: 生成新 requestId,复用 commandId
  → 工控机: commandId 幂等,不重复执行,返回原 ACK

4.4 告警处理流程

工控机 state/dock {alarmCodes:["LEFT_DOOR_POWER_FAULT","DOCK_EMERGENCY_STOP"]}
  → mqtt/subscriber: 解析 → Redis HMSET dock:{dockId}:alarm_codes
  → alarm_service:
      1. 对比上次 alarmCodes(从 Redis 读取)
      2. 新增 code → INSERT alarm(status='active')
      3. 移除 code → UPDATE alarm SET status='resolved', resolved_at=now()
      4. WS 推送告警变更给前端

5. 数据模型

5.1 用户

两个角色:管理员(admin) 看全部数据,普通用户(user) 只看自己的机巢。

CREATE TABLE user (
    id              BIGINT PRIMARY KEY,
    name            VARCHAR(32) NOT NULL,
    phone           VARCHAR(32) NOT NULL,
    email           VARCHAR(64),
    password        VARCHAR(64) NOT NULL,
    role            VARCHAR(16) DEFAULT 'user',   -- admin / user
    traffic_balance BIGINT DEFAULT 0,               -- 云媒体剩余流量(字节)
    status          TINYINT DEFAULT 1,
    last_login      DATETIME,
    created_at      DATETIME,
    updated_at      DATETIME,
    UNIQUE KEY idx_phone (phone)
);
  • 用户自行注册,默认角色 user
  • 管理员在系统初始化时手动指定,不需要组织/租户概念
  • 业务数据按 user_id 过滤,管理员通过路由 AdminMiddleware 访问平台管理接口

5.2 机巢 & 无人机

机巢与无人机是默认一对一绑定的关系。工控机上报机巢状态时携带当前无人机信息,后台自动关联。

CREATE TABLE dock (
    id              BIGINT PRIMARY KEY,
    user_id         BIGINT NOT NULL,              -- 所属用户
    dock_id         VARCHAR(64) NOT NULL,         -- MQTT dockId (dock-xxx)
    name            VARCHAR(64) DEFAULT '',
    code            VARCHAR(32) DEFAULT '',
    sn              VARCHAR(64) DEFAULT '',
    iccid           VARCHAR(32) DEFAULT '',        -- 冗余,权威见 sim_card.iccid
    longitude       DOUBLE DEFAULT 0,
    latitude        DOUBLE DEFAULT 0,
    altitude        DOUBLE DEFAULT 0,
    location        VARCHAR(128) DEFAULT '',
    status          VARCHAR(32) DEFAULT 'offline',
    register_status VARCHAR(16) DEFAULT 'pending', -- pending/registered
    dock_id_source  VARCHAR(32) DEFAULT '',
    software_ver    VARCHAR(16) DEFAULT '',
    protocol_ver    VARCHAR(8) DEFAULT '1.0',
    created_at      DATETIME,
    updated_at      DATETIME,
    UNIQUE KEY idx_dock_id (dock_id)
);

CREATE TABLE drone (
    id              BIGINT PRIMARY KEY,
    user_id         BIGINT NOT NULL,              -- 所属用户(继承 dock 的 user_id)
    drone_sn        VARCHAR(32) NOT NULL,         -- 飞控 HW_SN_NUM
    dock_id         VARCHAR(64) NOT NULL,         -- 绑定的机巢 dock_id(1:1)
    name            VARCHAR(64) DEFAULT '',
    code            VARCHAR(32) DEFAULT '',
    model           VARCHAR(32) DEFAULT '',
    status          VARCHAR(32) DEFAULT 'offline',
    battery         INT DEFAULT 0,
    firmware_ver    VARCHAR(16) DEFAULT '',
    created_at      DATETIME,
    updated_at      DATETIME,
    UNIQUE KEY idx_drone_sn (drone_sn),
    UNIQUE KEY idx_drone_dock (dock_id)
);

机巢上报 state/drone 时携带 droneSn,后台据此自动创建或更新无人机记录。dock_id 上唯一约束保证一对一绑定。换机巢时新 dock 上报不同 droneSn 即建立新关系,无需人工操作。

5.3 告警

通过 dock_id 关联到用户,不单独存 user_id

CREATE TABLE alarm_code (
    id          BIGINT PRIMARY KEY,
    code        VARCHAR(64) NOT NULL,
    category    VARCHAR(32) NOT NULL,             -- dock / drone
    message_cn  VARCHAR(256) NOT NULL,
    level       VARCHAR(16) DEFAULT 'critical',
    source      VARCHAR(128) DEFAULT '',
    created_at  DATETIME,
    updated_at  DATETIME,
    UNIQUE KEY idx_code (code)
);

CREATE TABLE alarm (
    id              BIGINT PRIMARY KEY,
    dock_id         VARCHAR(64) NOT NULL,
    code            VARCHAR(64) NOT NULL,
    device_type     VARCHAR(8) NOT NULL,
    message_cn      VARCHAR(256) NOT NULL,
    level           VARCHAR(16) NOT NULL,
    status          VARCHAR(16) DEFAULT 'active',
    triggered_at    DATETIME,
    resolved_at     DATETIME,
    created_at      DATETIME,
    KEY idx_alarm_dock (dock_id, status)
);

5.4 指令 & 工作流

CREATE TABLE device_command_log (
    id              BIGINT PRIMARY KEY,           -- = commandId (Snowflake)
    dock_id         VARCHAR(64) NOT NULL,
    command_type    VARCHAR(64) NOT NULL,         -- dock.open / drone.takeoff / workflow.start_task
    params          JSON,
    drone_sn        VARCHAR(32),
    request_id      VARCHAR(64),
    ttl_ms          INT DEFAULT 30000,
    ack_accepted    TINYINT,
    ack_result_code VARCHAR(64),
    status          VARCHAR(16) DEFAULT 'sent',   -- sent / acked / timeout / terminal
    retry_count     INT DEFAULT 0,
    sent_at         DATETIME,
    acked_at        DATETIME,
    created_at      DATETIME,
    KEY idx_cmd_log_dock (dock_id, created_at)
);

CREATE TABLE workflow_state (
    id              BIGINT PRIMARY KEY,
    dock_id         VARCHAR(64) NOT NULL,
    command_id      VARCHAR(64),
    type            VARCHAR(64),
    task_id         VARCHAR(128),
    mission_id      VARCHAR(128),
    state           VARCHAR(16) NOT NULL,         -- idle / running / succeeded / failed / cancelled
    step            VARCHAR(64) NOT NULL,
    result_code     VARCHAR(64),
    updated_at      DATETIME
);

5.5 固件管理

管理员上传和管理固件版本,用户选择版本下发 OTA 升级指令。

CREATE TABLE firmware (
    id              BIGINT PRIMARY KEY,
    component       VARCHAR(32) NOT NULL,           -- 固定 dock-edge-agent
    version         VARCHAR(32) NOT NULL,           -- 1.3.0
    description     VARCHAR(512) DEFAULT '',        -- 版本说明/更新日志
    file_url        VARCHAR(512) NOT NULL,          -- 升级包下载地址
    sha256          VARCHAR(128) NOT NULL,          -- SHA-256 摘要
    signature       TEXT NOT NULL,                  -- 签名 Base64
    file_size       BIGINT DEFAULT 0,               -- 文件大小(字节)
    mandatory       TINYINT DEFAULT 0,              -- 是否强制升级
    status          VARCHAR(16) DEFAULT 'draft',    -- draft/released/deprecated
    created_by      BIGINT NOT NULL,                -- 上传者(admin)
    created_at      DATETIME,
    updated_at      DATETIME,
    UNIQUE KEY idx_component_ver (component, version)
);

5.6 任务 & 航线

CREATE TABLE task_plan (
    id              VARCHAR(128) PRIMARY KEY,
    user_id         BIGINT NOT NULL,
    name            VARCHAR(128) NOT NULL,
    dock_id         VARCHAR(64) NOT NULL,
    route_id        BIGINT,
    schedule_type   VARCHAR(16) DEFAULT 'once',
    schedule_cron   VARCHAR(32),
    video_policy    VARCHAR(16) DEFAULT 'raw',
    status          VARCHAR(16) DEFAULT 'draft',
    created_by      BIGINT,
    created_at      DATETIME,
    updated_at      DATETIME
);

CREATE TABLE task_execution (
    id              BIGINT PRIMARY KEY,
    task_id         VARCHAR(128),
    command_id      VARCHAR(64),
    dock_id         VARCHAR(64) NOT NULL,
    drone_sn        VARCHAR(32),
    start_time      DATETIME,
    end_time        DATETIME,
    status          VARCHAR(16) DEFAULT 'pending',
    trajectory_json JSON,
    created_at      DATETIME
);

CREATE TABLE route (
    id              BIGINT PRIMARY KEY,
    user_id         BIGINT NOT NULL,
    name            VARCHAR(128) NOT NULL,
    description     VARCHAR(256),
    created_at      DATETIME,
    updated_at      DATETIME
);

CREATE TABLE route_waypoint (
    id          BIGINT PRIMARY KEY,
    route_id    BIGINT NOT NULL,
    seq         INT NOT NULL,
    longitude   DOUBLE,
    latitude    DOUBLE,
    altitude    DOUBLE,
    speed       DOUBLE,
    yaw         DOUBLE,
    hold_sec    INT DEFAULT 0,
    created_at  DATETIME,
    UNIQUE KEY uk_route_seq (route_id, seq)
);

5.7 媒体

CREATE TABLE live_session (
    id              VARCHAR(128) PRIMARY KEY,      -- streamSessionId
    dock_id         VARCHAR(64) NOT NULL,
    provider        VARCHAR(16) DEFAULT 'aliyun',
    stream_name     VARCHAR(128) NOT NULL,         -- 直播流名(每次会话唯一,生成推流/播放地址的入参)
    push_url_hash   VARCHAR(128),
    expires_at      BIGINT NOT NULL,
    max_bitrate_bps BIGINT DEFAULT 1500000,
    phase           VARCHAR(16) DEFAULT 'idle',
    stop_reason     VARCHAR(32),
    error_code      VARCHAR(64),
    started_at      DATETIME,
    stopped_at      DATETIME,
    created_at      DATETIME
);

CREATE TABLE video (
    id              BIGINT PRIMARY KEY,
    user_id         BIGINT NOT NULL,
    drone_sn        VARCHAR(32),
    execution_id    BIGINT,
    file_name       VARCHAR(256),
    file_size       BIGINT,
    duration        INT,
    oss_key         VARCHAR(256),
    oss_bucket      VARCHAR(128),                 -- 预签名上传的目标 bucket
    thumbnail_key   VARCHAR(256),
    status          VARCHAR(16) DEFAULT 'uploading',
    upload_expire_at DATETIME,                    -- 预签名 PUT URL 过期时间
    created_at      DATETIME
);

CREATE TABLE download_log (
    id          BIGINT PRIMARY KEY,
    video_id    BIGINT NOT NULL,
    user_id     BIGINT NOT NULL,
    bytes       BIGINT NOT NULL,
    created_at  DATETIME
);

5.8 计费

商业模式:中间商赚差价。

  • 云媒体流量:平台自己有一个阿里云直播账号和流量包,所有用户共用。用户向平台订购流量(GB),平台在内部给该用户记账。用户消耗流量时从自己的余额扣,平台只需关注所有用户的总消耗,快用完时再向阿里云续购。平台赚取用户售价与阿里云成本之间的差价。
  • 4G 卡流量:用户为机巢的 SIM 卡订购套餐,付款后平台调用运营商接口给该卡充值,差价同上。

两类计费相互独立,付款成功后的处理:

  • 云媒体流量:付款 → user.traffic_balance 加字节数,用户即刻可用
  • 4G 卡流量:付款 → 调运营商接口充值 → 更新 sim_card.plan_gb
-- user 表流量字段
-- traffic_balance BIGINT DEFAULT 0   -- 云媒体剩余流量(字节)

CREATE TABLE traffic_order (
    id              BIGINT PRIMARY KEY,
    user_id         BIGINT NOT NULL,
    amount_gb       INT NOT NULL,                  -- 购买流量(GB)
    unit_price      DECIMAL(6,2),                  -- 平台售价(元/GB)
    total_price     DECIMAL(10,2),                 -- 用户实付(元)
    pay_status      VARCHAR(16) DEFAULT 'unpaid',  -- unpaid/paid/cancelled
    created_at      DATETIME,
    paid_at         DATETIME
);

-- 云媒体消费明细(直播/回放/下载)
CREATE TABLE traffic_usage_log (
    id              BIGINT PRIMARY KEY,
    user_id         BIGINT NOT NULL,
    source_type     VARCHAR(16),                   -- live / replay / download
    source_id       BIGINT,
    bytes_used      BIGINT,
    balance_before  BIGINT,                        -- 扣减前余额(字节)
    balance_after   BIGINT,                        -- 扣减后余额(字节)
    created_at      DATETIME
);

-- SIM 卡(用户为机巢的 4G 卡订购套餐)
CREATE TABLE sim_card (
    id              BIGINT PRIMARY KEY,
    user_id         BIGINT NOT NULL,
    dock_id         VARCHAR(64),
    carrier         VARCHAR(16),                   -- 移动/联通/电信
    phone           VARCHAR(16),
    iccid           VARCHAR(32),                   -- 权威字段(查询/充值的入参)
    plan_gb         INT,                           -- 当前套餐总流量(GB)
    used_gb         DECIMAL(10,4) DEFAULT 0,       -- 已用流量(GB,由同步任务回填)
    expired_at      DATE,
    status          VARCHAR(16) DEFAULT 'active',  -- 平台业务态: active/expired
    carrier_status  VARCHAR(16) DEFAULT 'normal',  -- 运营商侧状态: normal/suspended/arrears/cancelled
    last_sync_at    DATETIME,                      -- 上次从运营商同步用量时间
    created_at      DATETIME,
    updated_at      DATETIME
);

-- SIM 卡充值记录
CREATE TABLE sim_recharge_log (
    id              BIGINT PRIMARY KEY,
    sim_card_id     BIGINT NOT NULL,
    user_id         BIGINT NOT NULL,
    amount_gb       INT NOT NULL,
    unit_price      DECIMAL(6,2),                  -- 平台售价(元/GB)
    total_price     DECIMAL(10,2),                 -- 用户实付(元)
    pay_status      VARCHAR(16) DEFAULT 'paid',
    recharge_status VARCHAR(16) DEFAULT 'pending', -- pending/success/failed
    recharge_msg    VARCHAR(256) DEFAULT '',        -- 运营商返回信息
    idempotent_key  VARCHAR(64) NOT NULL,           -- 平台充值单号,超时重试去重
    created_at      DATETIME,
    UNIQUE KEY uk_recharge_idem (idempotent_key)
);

-- SIM 卡用量快照(每次从运营商同步留痕,可审计/画曲线/对账)
CREATE TABLE sim_usage_record (
    id              BIGINT PRIMARY KEY,
    sim_card_id     BIGINT NOT NULL,
    plan_gb         INT,                           -- 同步时套餐总量(GB)
    used_gb         DECIMAL(10,4),                 -- 已用(GB)
    remain_gb       DECIMAL(10,4),                 -- 剩余(GB)
    carrier_status  VARCHAR(16),
    synced_at       DATETIME,                      -- 同步时间
    KEY idx_record_sim (sim_card_id, synced_at)
);

平台采购成本(从阿里云/运营商的拿货价)不在本系统记录,由财务线下管理。系统只记录面向用户的售价和消费。

余额一致性与账本模型:流量余额以 Redis 为唯一实时真相(source of truth),MySQL 的 user.traffic_balance 仅作为 30 分钟刷一次的慢快照。traffic_usage_log 流水是账本(ledger),每次扣减同步写 MySQL、不可变、可重放;余额是派生值(derived balance),可由 快照 + 重放流水 + 订单 精确重建。因此 Redis 丢失最多损失 30 分钟余额变动,但账本仍在,可对账、可审计、可重建。

SIM 卡用量来源:工控机不上报网络用量,used_gb / expired_at / carrier_status 均由后台定时任务(每 1 小时)调用运营商查询接口回填。sim_card.iccid 为权威字段(查询与充值的入参),dock.iccid(§5.2)降级为冗余。运营商接口抽象为 CarrierAPIQueryUsage / Recharge),暂定一家运营商,先做「接口 + 单实现」,换卡商时不改业务层。

5.9 TDengine 时序数据

遥测和轨迹等高频时序数据存入 TDengine,与 pilot-data 的用法保持一致。

遥测超级表:

CREATE STABLE device_telemetry (
    ts          TIMESTAMP,
    longitude   DOUBLE,
    latitude    DOUBLE,
    altitude    DOUBLE,
    ground_speed DOUBLE,
    roll        DOUBLE,
    pitch       DOUBLE,
    yaw         DOUBLE,
    battery_pct INT,
    battery_v   DOUBLE,
    satellites  INT,
    gps_quality VARCHAR(16),
    link_quality INT,
    flight_mode VARCHAR(16),
    armed       TINYINT
) TAGS (
    dock_id     VARCHAR(64),
    drone_sn    VARCHAR(32)
);

轨迹超级表(任务回放用):

CREATE STABLE task_trajectory (
    ts          TIMESTAMP,
    longitude   DOUBLE,
    latitude    DOUBLE,
    altitude    DOUBLE,
    ground_speed DOUBLE,
    yaw         DOUBLE,
    battery_pct INT
) TAGS (
    execution_id BIGINT,
    dock_id      VARCHAR(64),
    drone_sn     VARCHAR(32)
);

pilot-data 使用 TrackService 缓冲(sync.Map)达到 500 条或 10 秒后批量写入 TDengine。本项目对齐此模式。


6. Redis 缓存设计

# 设备实时状态
device:dock:{dockId}:status        → Hash (所有 state/dock 字段)
device:dock:{dockId}:alarm_codes   → Set (当前告警编码集合)
device:drone:{dockId}:telemetry    → Hash (所有 state/drone 字段)
device:drone:{dockId}:alarm_codes  → Set (当前告警编码集合)
device:workflow:{dockId}           → Hash (当前工作流状态)
device:video:{dockId}              → Hash (当前视频推流状态)

# Token & Session
laic:token:{user}:{shortId}        → token 会话标记(支持踢人)

# 权限缓存
laic:permission:{role}             → 角色权限列表

# 云媒体流量余额(以 Redis 为主,user.traffic_balance 为 30 分钟快照)
laic:traffic:{userId}              → String (INT 值,可原子 DECRBY/INCRBY)

# 在线设备集合
laic:devices:online:dock           → Set {dockId...}
laic:devices:online:drone          → Set {droneSn...}

7. API 路由

所有端点统一注册在一个 Gin router 上,api-port 默认 8080

7.1 认证(公开)

POST   /v1/auth/login
POST   /v1/auth/logout
POST   /v1/auth/refresh
POST   /v1/auth/sms-code
POST   /v1/auth/register
POST   /v1/auth/reset-password

7.2 账户中心(需登录)

GET    /v1/account/profile
PUT    /v1/account/profile
GET    /v1/account/traffic/balance
GET    /v1/account/traffic/usage
GET    /v1/account/traffic/orders
POST   /v1/account/traffic/orders
GET    /v1/account/sim-cards
POST   /v1/account/sim-cards/:id/recharge

7.3 机巢 & 无人机

GET    /v1/docks                        # 列表(支持筛选/分页)
POST   /v1/docks                        # 完善待登记机巢信息
GET    /v1/docks/:id                    # 详情(含实时状态 from Redis)
PUT    /v1/docks/:id
DELETE /v1/docks/:id
GET    /v1/docks/:id/status             # 纯实时状态
POST   /v1/docks/:id/command            # 下发指令
POST   /v1/docks/:id/firmware/upgrade        # body: {firmware_id}

GET    /v1/drones
GET    /v1/drones/:id
PUT    /v1/drones/:id
DELETE /v1/drones/:id
GET    /v1/drones/:id/telemetry

7.4 告警 & 指令日志

GET    /v1/alarms                       # 告警列表
PUT    /v1/alarms/:id/acknowledge
PUT    /v1/alarms/:id/resolve

GET    /v1/commands                     # 指令历史
GET    /v1/commands/:id

7.5 任务 & 航线

GET    /v1/tasks
POST   /v1/tasks
GET    /v1/tasks/:id
PUT    /v1/tasks/:id
DELETE /v1/tasks/:id
POST   /v1/tasks/:id/execute

GET    /v1/executions
GET    /v1/executions/:id
GET    /v1/executions/:id/trajectory

GET    /v1/routes
POST   /v1/routes
GET    /v1/routes/:id
PUT    /v1/routes/:id
DELETE /v1/routes/:id

7.6 媒体

GET    /v1/live
POST   /v1/live/:dockId/start
POST   /v1/live/:dockId/stop
GET    /v1/live/:dockId/play-url

POST   /v1/videos                       # 申请上传(返回 OSS 预签名 PUT URL)
POST   /v1/videos/:id/complete          # 上传完成确认(边缘侧回调)
GET    /v1/videos
GET    /v1/videos/:id
POST   /v1/videos/:id/download

7.7 系统管理(需 admin)

GET    /v1/users
POST   /v1/users
PUT    /v1/users/:id
DELETE /v1/users/:id
GET    /v1/roles
GET    /v1/logs/operation

7.8 固件管理(需 admin)

GET    /v1/firmwares                      # 固件版本列表
POST   /v1/firmwares                      # 上传新固件
PUT    /v1/firmwares/:id                  # 编辑(状态/强制标记等)
DELETE /v1/firmwares/:id                  # 删除

7.9 WebSocket

WS  /v1/ws/monitor              # 前端实时监控(设备状态 + 告警推送)

7.10 统一响应格式

{
  "code": 200,
  "msg": "ok",
  "data": {}
}

8. 核心数据流

8.1 设备自发现 & 登记

工控机首次启动 → 生成 dockId → 连接 EMQX → 发布 retained status/online
  → mqtt/subscriber:
      1. 查询 dock 表 WHERE dock_id = {dockId}
      2. 不存在 → INSERT dock(dock_id, register_status='pending', user_id=0)
      3. 更新 Redis: device:dock:{dockId}:status
      4. WS 推送: 新设备上线
  → 用户在前端认领或管理员分配: PUT /v1/docks/:id {user_id, name, code, ...}

8.2 无人机自动关联(默认 1:1 绑定)

工控机上报 state/drone {droneSn:"23001582", online:true, ...}
  → mqtt/subscriber:
      1. 查询 drone 表 WHERE drone_sn = "23001582" OR dock_id = {上报dockId}
      2. 都不存在 → INSERT drone(drone_sn, dock_id, user_id=同dock的user_id)
      3. 存在 → UPDATE drone SET drone_sn, dock_id(替换绑定)
      4. Redis 更新状态
      5. WS 推送

8.3 固件升级

管理员 POST /v1/firmwares {version, file_url, sha256, signature, ...}
  → 上传并保存固件版本

用户 POST /v1/docks/:id/firmware/upgrade {firmware_id}
  → dock_service:
      1. 校验机巢属于当前用户
      2. 查询 firmware 表获取版本信息
      3. MQTT Publish → dock-edge/v1/dock/{dockId}/ota/desired
         {updateId, component, version, url, sha256, signature, mandatory, issuedAt}
  → 工控机: ota/reported 上报升级进度
  → mqtt/subscriber → 更新固件升级状态 → WS 推送

8.4 云媒体流量计费

扣减场景:直播推流(按推流时长 × 码率估算)、回放、原始视频下载。

POST /v1/videos/:id/download
  → video_service:
      1. 查询 video → 获取 file_size(字节), user_id
      2. ensureBalance(userId): key 不存在则从 MySQL 读余额 SetNX 初始化(兜底;启动时 WarmUp 已预热)
      3. Redis Lua 脚本: GET 余额 → 校验 ≥ file_size → DECRBY
         余额不足 → 返回错误 "流量不足,请充值"
      4. 同步 INSERT traffic_usage_log(bytes_used, balance_before, balance_after)  ← 账本,必须落库
      5. 生成 OSS 签名 URL(有效期 300s)
      6. INSERT download_log
      7. 返回签名 URL

直播扣减(后台定时任务):
  → 每 60 秒扫描进行中的 live_session
  → 按 elapsed × max_bitrate_bps / 8 估算流量
  → 批量扣减 Redis 余额(Lua),并同步写 usage_log
  → 余额耗尽 → MQTT video.stop_stream 停止推流

余额快照刷新(后台定时任务,30 分钟):
  → MGET 批量读 Redis 余额
  → 批量 UPDATE user.traffic_balance(CASE WHEN 逐行赋值,避免逐条 roundtrip)
  → 不清 Redis(Redis 仍是唯一实时真相)
  → 优雅退出时强制 flush 一次,避免丢失最后窗口

8.5 充值与发放

用户 POST /v1/account/traffic/orders {amount_gb, ...}
  → 生成 traffic_order(status=unpaid)
  → 用户完成付款(对接支付接口,当前先人工标记 paid)

付款成功后 — 云媒体流量:
  → account_service:
      1. Redis INCRBY user.traffic_balance (amount_gb × 1024³)   ← 充值走同一 Redis 真相
      2. UPDATE traffic_order SET pay_status='paid', paid_at=now()  ← 订单实时落库,可重放
      3. 用户立即可用

付款成功后 — 4G 卡:
  → account_service:
      1. 生成 idempotent_key(= 平台充值单号)
      2. 调用运营商充值 API(iccid, amount_gb, idempotent_key)
      3. 运营商返回成功 → UPDATE sim_card SET plan_gb = plan_gb + amount_gb
      4. INSERT sim_recharge_log(idempotent_key, recharge_status='success')
      5. 运营商返回失败 → recharge_status='failed',告警管理员人工处理(不同步更新 plan_gb)
      6. 超时重试 → 复用同一 idempotent_key,运营商侧去重,不重复充值

8.6 SIM 卡用量同步

定时任务(robfig/cron,每 1 小时):
  → 分页扫描 status='active' 的 sim_card
  → 按 carrier 路由到 CarrierAPI.QueryUsage(iccid)
  → 回填 sim_card: used_gb / expired_at / carrier_status / last_sync_at
  → INSERT sim_usage_record(plan_gb, used_gb, remain_gb, carrier_status, synced_at)  ← 留痕
  → 阈值检查:
      remain_gb < plan_gb * 10%         → 提醒用户充值
      expired_at 距今 < 3 天            → 提醒续费
      carrier_status = suspended/arrears → 告警管理员
      carrier_status = cancelled 或已过期 → 置 status='expired'

限流: 串行 + 可控并发 + 失败退避,避免打爆运营商查询接口

8.7 直播地址生成与按需拉流

按需拉流 = 控制推流起停:用户不点播放不推流(省流量/省电),点了才开始推。

用户点「播放」
  → GET /v1/live/:dockId/start
  → live_service:
      1. 生成唯一 stream_name(= live_session.id,Snowflake/UUID)
      2. 用阿里云直播 Go SDK 在服务端计算推流地址(RTMP/SRT + 鉴权,有效期 2h)
      3. INSERT live_session(stream_name, phase='starting')
      4. MQTT 下发 video.start_stream {push_url} 给工控机
  → 工控机: 用 push_url 向阿里云推流 → MQTT 上报 state/video {streaming:true}
  → live_service: 计算播放地址(HLS/FLV + URL 鉴权)→ 返回 {play_url}
  → 前端: Aliplayer / hls.js / flv.js 拉 play_url 播放

用户点「关闭」
  → POST /v1/live/:dockId/stop
  → live_service: MQTT 下发 video.stop_stream → 工控机停止推流
  → 更新 live_session(phase='stopped')

地址格式(阿里云直播):

  • 推流:rtmp://{推流域名}/live/{AppName}/{stream_name}?auth_key=...(或 SRT)
  • 播放:HLS http://{播流域名}/live/{AppName}/{stream_name}.m3u8 / FLV .flv(低延迟)

关键约束:

  • 推流域名、播流域名需在阿里云控制台备案 + CNAME,提前申请
  • AccessKey 只在后台,设备只拿「这一次」的推流地址,绝不下发长期凭证
  • 网页播放推荐 HLS(兼容最好),对延迟敏感再上 HTTP-FLV
  • 服务端集成用阿里云直播 Go SDK(视频直播 OpenAPI + 鉴权计算),前端用阿里云播放器 SDK(Aliplayer)

9. 启动流程

func main() {
    // 1. 初始化日志
    logger.InitCustomLog(logger.Level_D, nil, "laic-backend")

    // 2. 加载 config.yaml
    conf := &AppConfig{}
    common.LoadConfig("config.yaml", conf)

    // 3. 初始化基础设施
    common.InitConnection(&conf.Mysql)      // GORM + MySQL
    common.InitTDengine(&conf.TDengine)    // TDengine 连接池
    common.InitRedis(conf.Redis)            // Redis Pool

    // 4. 预热缓存
    go cache.WarmUp()

    // 5. 连接 MQTT Broker
    go mqtt.Connect(conf.MQTT)

    // 6. 启动 WebSocket Hub
    go websocket.Hub.Run()

    // 7. 启动 HTTP
    route.InitRouter(conf.ApiPort)
    common.ServerRun(conf.ApiPort)

    // 8. 优雅退出
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
}

config.yaml 示例:

api-port: 8080
mysql:
  url: root:password@tcp(127.0.0.1:3306)/laic?charset=utf8mb4&parseTime=True&loc=Local
  idle: 10
  max-conn: 50
  max-wait: 3600
tdengine:
  dsn: root:taosdata@tcp(127.0.0.1:6030)/laic
redis: redis://127.0.0.1:6379/0
mqtt:
  broker: tcp://127.0.0.1:1883
  client-id: laic-backend
  username: laic
  password: ""
log:
  level: debug
  path: ./logs/

10. 与 pilot-train-server 的主要差异

维度 pilot-train-server laic-backend
架构 多服务单体仓库(5 个 main.go) 单服务分模块(1 个 main.go)
数据库 MySQL MySQL(相同)
时序数据 TDengine TDengine(相同)
设备协议 MAVLink over TCP MQTT(EMQX broker)
对象存储 华为云 OBS 华为云 OBS(S3 兼容,path-style SigV4 预签名)
直播 阿里云直播 SRT 推流
计费 流量余额 + 订单 + 消费明细
告警 alarmCodes diff 引擎 + 告警生命周期
指令体系 简单 TCP 指令 commandId 幂等 + TTL + ACK + 重试
配置 Consul 本地 config.yaml
服务间通信 HTTP + Consul 内部函数调用

11. 待确认项

  1. TDengine 部署:需确认 TDengine 版本(pilot-train-server 用的 3.x),以及遥测数据保留周期
  2. 告警码初始化alarm_code 表需要根据 MQTT 文档 7.6 节全部枚举值初始化,共约 90+ 条
  3. SIM 卡流量:MQTT 文档明确工控机不上报网络用量,需确认运营商 API 或人工录入
  4. 视频上传通路:MQTT 协议只定义了推流,原始视频如何从工控机到达 OSS 需确认(可能由边缘侧直接上传 OSS,通过 state/video 或独立接口通知后台)