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.DB、common.RedisPool、service.DefaultXxxService - 本地 YAML 配置:一个
config.yaml,Viper 加载 - JWT + 路由管理员鉴权:JWT 负责身份认证,
user.role和AdminMiddleware限制平台管理接口;业务数据由 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)降级为冗余。运营商接口抽象为CarrierAPI(QueryUsage/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. 待确认项
- TDengine 部署:需确认 TDengine 版本(pilot-train-server 用的 3.x),以及遥测数据保留周期
- 告警码初始化:
alarm_code表需要根据 MQTT 文档 7.6 节全部枚举值初始化,共约 90+ 条 - SIM 卡流量:MQTT 文档明确工控机不上报网络用量,需确认运营商 API 或人工录入
- 视频上传通路:MQTT 协议只定义了推流,原始视频如何从工控机到达 OSS 需确认(可能由边缘侧直接上传 OSS,通过 state/video 或独立接口通知后台)