48 KiB
Superbox Go API Reference
后端 API 端点详尽文档。源自动词检索表 + 路由注册表 (
internal/router/router.go) + handler/model 实现。
目录
基础信息
- 基础路径:
/api/v1(/health例外,直接挂在根路径) - 默认监听地址:
0.0.0.0:8000(可在config.json → server.addr修改或serve --addr覆盖) - 内容类型:
application/json; charset=utf-8(除文件上传、媒体文件、HLS 之外) - 字符编码: UTF-8
- 跨域:
Access-Control-Allow-Origin: *,Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS - 限流: 暂未实现
全部错误均带 JSON 状态码/详情字段(见下)。所有受保护接口需先
/api/v1/auth/login拿到 JWT。
统一响应信封
所有非文件流的成功响应均使用 response/response.go 中定义的信封:
{
"err": {
"ec": 0,
"dm": "ok",
"em": ""
},
"ret": <data>
}
| 字段 | 类型 | 说明 |
|---|---|---|
err.ec |
int | 自定义错误码,0 = 成功;与 HTTP 状态码同步 |
err.dm |
string | 错误描述宏(ok / Bad Request / Not Found / Internal Server Error 等) |
err.em |
string | 错误详情原文(成功时省略) |
ret |
any | 业务负载,结构视端点而异 |
列表型分页负载(response.RetList)
{
"err": { "ec": 0, "dm": "ok" },
"ret": {
"count": 42,
"next": null,
"previous": null,
"results": [ ... ]
}
}
count 为符合条件的总行数;results 为当前页的对象数组。next / previous 始终为 null(Django 兼容字段),分页通过 limit / offset query 参数控制。
数组型负载(response.RetArray)
/api/v1/algorithms 的 GET 接口直接返回数组,不带分页包装:
{ "err": { "ec": 0, "dm": "ok" }, "ret": [ {...}, {...} ] }
空响应
response.NoContent 直接返回 204 No Content,无 body(用于 DELETE、PATCH 等无业务数据的接口)。
认证机制
Token 体系(access + refresh)
后端使用 stateful 双 token 模型:
| Token | 格式 | 存储 | TTL | 用途 |
|---|---|---|---|---|
access_token |
HS256 JWT(含 user_id / username / is_admin / exp / iat claims) |
仅客户端 | jwt.access_token_ttl(默认 15m) |
访问受保护 API |
refresh_token |
32 字节随机 → 43 字符 base64-url | 客户端明文 + 服务端存 sha256 哈希 | jwt.refresh_token_ttl(默认 7d,sliding) |
换新 access + refresh 对 |
关键属性:
- 严格轮换(strict rotation):每次
POST /api/v1/auth/refresh成功都会签发新 pair,旧的 refresh 立即失效(在 DB 行上标记rotated_at = now(),不删除)。 - Reuse detection:被轮换过的 refresh token 若被再次使用,整个 family(同一登录会话的 token 链)全部吊销,原持有者被迫重新登录。视为凭据泄露的应对策略。
- Sliding window:每次成功 refresh 都会把
expires_at重新设为now + 7d,活跃用户不需要重新输入密码。真正决定"多久要重新登录"的是 refresh token 的不活跃时长。 - 7 天不活跃就要重新登录;7 天内活跃用户永不重新登录(直到主动 logout 或 reuse detection 触发)。
登录获取 token pair
调用 POST /api/v1/auth/login,返回:
{
"err": { "ec": 0, "dm": "ok" },
"ret": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "Z3pFmT7uVxKqY8sN2Lc4hJ1bP9aD5wE0rM6iO3yU2kX8vN4cB1gQ7zH5fS9a",
"expires_at": "2026-06-03T12:34:56Z",
"refresh_expires_at": "2026-06-10T12:34:56Z",
"user": { "id": 1, "username": "admin", "is_admin": true, "is_active": true }
}
}
JWT 负载 (Claims):
{
"user_id": 1,
"username": "admin",
"is_admin": true,
"exp": 1748944496,
"iat": 1748943596
}
签名算法:HS256,密钥取自 config.json → jwt.secret_key,TTL 由 jwt.access_token_ttl 控制(默认 15m)。
续期 token(silent refresh)
access 过期前无需主动操作;前端 axios 拦截器会在 401 时自动:
- 用
localStorage.refresh_token调POST /api/v1/auth/refresh - 成功后更新 localStorage + 重发原请求
- 失败(refresh 也过期 / 被吊销 / reuse detected)则清状态并跳
/admin/login
5 个并发 401 共用一个 in-flight refresh 请求(模块级 Promise 缓存),避免 strict rotation 误触发 reuse detection 把整个 family 杀光。
携带 access token
任选其一:
# Header 方式(推荐)
Authorization: Bearer <access_token>
# Query 方式(用于浏览器 HLS / SSE / WebSocket)
GET /api/v1/events/stream?token=<access_token>
GET /api/v1/cameras/1/stream?token=<access_token>
GET /hls/camera_1.m3u8?token=<access_token>
中间件优先读
Authorization头,缺失时回退到?token=query。
携带 refresh token
refresh token 不通过 header 传递(永远不进 URL / Authorization 头),仅用于以下两种途径:
# 1. 主动调 /auth/refresh 时放在 body
POST /api/v1/auth/refresh
Content-Type: application/json
{ "refresh_token": "<raw>" }
# 2. 主动 logout 时放在 body(让服务端能删 DB 行)
POST /api/v1/auth/logout
Authorization: Bearer <access>
{ "refresh_token": "<raw>" }
鉴权模式
| 模式 | 描述 | 失败返回 |
|---|---|---|
Auth |
必须携带有效 access JWT | 401 Unauthorized |
AuthOrDetector |
access JWT 有效 或 调用方 IP 在 detectors 表中注册过 |
401 Unauthorized |
AdminOnly |
紧接 Auth 使用,要求 is_admin == true |
403 Forbidden |
| 公开 | 无任何校验 | — |
Detector 接入
当 AuthOrDetector 中间件检测到 Authorization 无效时,会读取 c.ClientIP(),查询 detectors 表的 ip 字段;命中即放行(在 gin context 写入 detector_ip),否则 401。Detector 自身用 /auth/login 登录后也走同一 refresh 机制。
访问控制矩阵
下表汇总每个端点的访问控制(C = 受 CORS 公开;A = JWT 必填;AD = 仅管理员;AO = JWT 或 Detector IP;AOO = JWT 或 Detector IP)。
| 路径 | 方法 | 鉴权 | 备注 |
|---|---|---|---|
/health |
GET | C | — |
/api/v1/health |
GET | C | — |
/api/v1/setup/config |
POST | C | 初始化写入 |
/api/v1/init |
GET, POST | C | 系统初始化 |
/api/v1/auth/login |
POST | C | 登录,颁发 access + refresh pair |
/api/v1/auth/refresh |
POST | C | 用 refresh token 换新 access + refresh pair |
/api/v1/auth/logout |
POST | A | 注销当前会话(access + refresh 双 token) |
/api/v1/events/stream |
GET (SSE) | A | 需 query token |
/api/v1/events/ws |
GET (WS) | A | 需 query token |
/api/v1/detectors |
GET, POST, DELETE | AD | 按角色限制 |
/api/v1/detectors/register |
POST | A | 检测器自助注册 |
/api/v1/detectors/:ip |
GET, PUT, PATCH, DELETE | AD | — |
/api/v1/cameras |
GET | A | — |
/api/v1/cameras |
POST | AD | — |
/api/v1/cameras/:id |
GET, PATCH | AO | — |
/api/v1/cameras/:id |
PUT | AD | — |
/api/v1/cameras/:id |
DELETE | AD | — |
/api/v1/cameras/:id/enable_sampling |
POST | AD | — |
/api/v1/cameras/:id/disable_sampling |
POST | AD | — |
/api/v1/cameras/:id/stream |
GET | A | 拉取 / 启动信息 |
/api/v1/cameras/:id/stream/start |
POST | AD | 启动转码 |
/api/v1/cameras/:id/stream/stop |
POST | AD | 停止转码 |
/api/v1/cameras/:id/snapshot |
GET | A | 拉取 JPEG 快照 |
/api/v1/events |
GET | A | 分页过滤 |
/api/v1/events |
POST | AO | 创建事件 |
/api/v1/events/:id |
GET | A | — |
/api/v1/events/:id |
PUT, PATCH, DELETE | AD | — |
/api/v1/events/:id/close_event |
POST | AD | — |
/api/v1/events/:id/push_event |
POST | AO | — |
/api/v1/events/queue_analysis |
POST | AO | — |
/api/v1/events/people_count |
POST | AO | — |
/api/v1/events/unmarked |
GET | A | — |
/api/v1/events/retrieve_ids |
GET | A | 拉取 ID 列表 |
/api/v1/events/retrieves |
GET | A | 拉取对象列表 |
/api/v1/events/delete_all |
POST | AD | 批量删除 |
/api/v1/rules |
GET | A | — |
/api/v1/rules |
POST | AD | — |
/api/v1/rules/:id |
GET | A | — |
/api/v1/rules/:id |
PUT, PATCH, DELETE | AD | — |
/api/v1/rules/camera/:camera_id |
GET | A | — |
/api/v1/rules/camera/:camera_id |
POST | AD | — |
/api/v1/mediums |
GET | A | — |
/api/v1/mediums |
POST | AO | multipart/form-data |
/api/v1/mediums/:id |
GET | A | — |
/api/v1/mediums/:id |
PUT, PATCH, DELETE | AD | — |
/api/v1/mediums/:id/file |
GET | A | 文件下载 |
/api/v1/webhooks |
GET, POST | AD | — |
/api/v1/webhooks/:id |
GET, PUT, PATCH, DELETE | AD | — |
/api/v1/webhooks/echo |
POST | AD | 回声测试 |
/api/v1/algorithms |
GET | AO | 数组返回 |
/api/v1/algorithms |
POST | AD | — |
/api/v1/algorithms/:code_name |
GET | AO | — |
/api/v1/algorithms/:code_name |
DELETE | AD | — |
/api/v1/samples |
GET, POST | AO | — |
/api/v1/samples/:id |
GET, PUT, PATCH, DELETE | AO | — |
/api/v1/samples/:id/upload_file |
POST | AO | 创建 SampleFile |
/api/v1/samples/files/:id/mark_uploaded |
POST | AO | 标记完成 |
/api/v1/users |
GET, POST | AD | — |
/api/v1/users/:id |
GET, PUT, PATCH, DELETE | AD | — |
/api/v1/users/:id/change_password |
POST | AD | — |
/api/v1/settings |
GET, POST | AD | — |
/api/v1/settings/:id |
GET, PUT, PATCH, DELETE | AD | — |
/api/v1/settings/name/:name |
GET | AD | — |
/api/v1/settings/by-name/:name |
PUT | AD | Upsert |
/api/v1/stream/webrtc/proxy |
GET | A | WebRTC → go2rtc 代理 |
/hls/*filepath |
GET | A | HLS 切片(query token) |
/storage/*filepath |
GET | C | 静态文件 |
/, /admin, /admin/*, /assets/* |
GET | C | 前端 SPA |
警告:
Auth中间件对未带 token 的请求直接 401,不进入 handler;AuthOrDetector失败也回 401。
错误码与 HTTP 状态码
| HTTP | 场景 |
|---|---|
200 OK |
默认成功 |
201 Created |
POST 创建成功(response.RetCreated) |
204 No Content |
DELETE / 无业务数据 PATCH |
400 Bad Request |
参数缺失、JSON 解析失败、字段校验未通过 |
401 Unauthorized |
Token 缺失或无效 |
403 Forbidden |
非管理员访问受控端点 |
404 Not Found |
资源不存在 |
500 Internal Server Error |
未捕获异常 / 服务层错误 / panic(被 Recovery 中间件捕获) |
SSE/WS/HLS 在 token 缺失或无效时返回 401 Unauthorized({"error": "..."},信封外)。
公共端点
健康检查
GET /health
无鉴权。仅返回 {"status":"ok"}。
GET /health HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{ "status": "ok" }
GET /api/v1/health
同上的别名挂载(/api/v1/health)。
系统初始化
仅用于系统首次部署;正常情况下使用
superbox config initCLI。
GET /api/v1/init
读取数据库中管理员用户数量。
// Response 200
{
"err": { "ec": 0, "dm": "ok" },
"ret": { "initialized": true, "needs_setup": false }
}
数据库不可达时也返回
{"initialized": false, "needs_setup": true}(兜底)。
POST /api/v1/init
创建初始管理员(仅当尚无管理员用户时成功)。
请求体:
{ "username": "ops", "password": "Str0ng-Pass!" }
校验规则:
- username 非空
- password 长度 ≥ 8,且不等于 username
- 命中弱密码黑名单(
password、12345678、qwerty、admin、letmein、welcome,不区分大小写)会被拒
成功:
{ "err": { "ec": 0, "dm": "ok" }, "ret": { "success": true } }
失败:
- 管理员已存在 →
400 admin user already exists - 密码过弱 →
400 password too weak
POST /api/v1/setup/config
无数据库时仍可调用:写 config.json + 连接数据库 + AutoMigrate + 创建管理员,一次性走完。
请求体(SetupConfigRequest):
{
"database": {
"type": "sqlite | postgres | mysql",
"host": "127.0.0.1",
"port": 5432,
"name": "superbox.db",
"user": "superbox",
"password": "secret"
},
"cloud": { "box_file": "...", "http_server": "...", "ws_server": "..." },
"storage": {
"type": "local",
"local": { "path": "./data" }
},
"admin": { "username": "ops", "password": "Str0ng-Pass!" }
}
成功:
{ "err": { "ec": 0, "dm": "ok" }, "ret": { "success": true, "config_path": "/etc/superbox/config.json" } }
副作用:
- 写入文件权限
0600的config.json - 自动生成 32 字节随机
jwt.secret_key(hex) - 默认
access_token_ttl=15m、refresh_token_ttl=7d、storage.type=local、go2rtc 默认参数
认证
全部 3 个端点走
application/json信封({"err":{...},"ret":...}),见前文「统一响应信封」。
POST /api/v1/auth/login
- 鉴权:公开(无 token)
- Content-Type:
application/json - 副作用:
- 在
refresh_tokens表 INSERT 一行(family_id为新 UUIDv4,expires_at = now + 7d) - 更新
users.last_login = now(失败时记 WARN,不影响登录结果)
- 在
请求:
{ "username": "ops", "password": "Str0ng-Pass!" }
字段约束:
username、password均为binding:"required",缺失 →400 Bad Request- 用户名 / 密码错误 或 用户
is_active=false→401 invalid credentials(不区分两种情况,避免账号枚举)
成功响应(LoginResult):
{
"err": { "ec": 0, "dm": "ok" },
"ret": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "Z3pFmT7uVxKqY8sN2Lc4hJ1bP9aD5wE0rM6iO3yU2kX8vN4cB1gQ7zH5fS9a",
"expires_at": "2026-06-03T12:34:56Z",
"refresh_expires_at": "2026-06-10T12:34:56Z",
"user": {
"id": 1,
"username": "ops",
"is_admin": true,
"is_active": true
}
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
access_token |
string | HS256 JWT;客户端用于 Authorization: Bearer ... |
refresh_token |
string | 43 字符 base64-url 随机串;客户端必须持久化(localStorage);服务端只存 sha256 哈希 |
expires_at |
RFC 3339 string | access token 过期时间(默认 = now + 15m) |
refresh_expires_at |
RFC 3339 string | refresh token 过期时间(默认 = now + 7d) |
user |
object | 登录用户对象(同 UserResponse) |
失败:
- 缺字段 / JSON 解析失败 →
400 Bad Request - 用户名 / 密码错 / 用户禁用 →
401 invalid credentials - 内部错误(DB 写失败等)→
500 Internal server error
POST /api/v1/auth/refresh
- 鉴权:公开(无 access token);用 body 里的 refresh_token 验真
- 作用:用 refresh token 换新 access + refresh pair(strict rotation)
- 副作用:
- 在事务中:把旧行的
rotated_at设为now(),并 INSERT 新行(同family_id,新token_hash,新expires_at = now + 7d) - 不更新
users.last_login(refresh 不算"登录")
- 在事务中:把旧行的
- 行为(按结果分类):
| 条件 | 行为 | HTTP |
|---|---|---|
body 缺 refresh_token |
400 | 400 Bad Request |
DB 找不到该 sha256(refresh_token) |
401(= invalid) | 401 refresh failed |
旧行 rotated_at 非空(reuse) |
杀整个 family(DELETE WHERE family_id = ?),401 | 401 refresh failed |
expires_at 已过 |
401 | 401 refresh failed |
用户不存在 / is_active=false |
杀 family,401 | 401 refresh failed |
| 其它 DB 错误 | 500 | 500 Internal server error |
| 成功 | 200 + 新 pair | 200 OK |
故意统一 401 响应(无论 invalid / expired / reused),前端不区分具体原因,UX 一致即可。
请求:
{ "refresh_token": "Z3pFmT7uVxKqY8sN2Lc4hJ1bP9aD5wE0rM6iO3yU2kX8vN4cB1gQ7zH5fS9a" }
成功响应(同 LoginResult 形状):
{
"err": { "ec": 0, "dm": "ok" },
"ret": {
"access_token": "<NEW access JWT>",
"refresh_token": "<NEW refresh token>",
"expires_at": "2026-06-03T12:50:00Z",
"refresh_expires_at": "2026-06-10T12:50:00Z",
"user": { "id": 1, "username": "ops", "is_admin": true, "is_active": true }
}
}
旧 refresh token 在响应后立即失效;继续用旧 token 调本端点会触发 reuse detection,杀 family。
POST /api/v1/auth/logout
- 鉴权:必填 access token(
Authorization: Bearer <access>)—— 中间件从中解析user_id - 作用:删
refresh_tokens表中当前会话对应的那一行;不删其它会话(多端点登录互不影响) - Idempotent:找不到行或
refresh_token为空都返200
请求:
POST /api/v1/auth/logout
Authorization: Bearer <access_token>
Content-Type: application/json
{ "refresh_token": "Z3pFmT7uVxKqY8sN2Lc4hJ1bP9aD5wE0rM6iO3yU2kX8vN4cB1gQ7zH5fS9a" }
refresh_token在 body 里是可选的(缺省 / 解析失败都容忍),便于前端在 access 已过期的情况下也能 logout。
成功响应:
{ "err": { "ec": 0, "dm": "ok" }, "ret": {} }
失败:
- 缺
Authorization头 / access token 无效 →401 Unauthorized(由中间件返) - service 层 DB 错误 →
500 Internal server error
当前只退出"当前会话"。没有"退出所有会话"端点,也不主动吊销其它 family 的 token。要彻底吊销需要
DELETE FROM refresh_tokens WHERE user_id = ?手工执行(DB 层)。
事件流 (SSE / WebSocket)
GET /api/v1/events/stream (Server-Sent Events)
- 鉴权:通过
?token=query 传递 JWT - Content-Type:
text/event-stream - 帧格式:
event: <type>\ndata: <json>\n\n
事件类型(sse.EventType):
type |
触发时机 | data 字段 |
|---|---|---|
event_create |
新事件入库(POST /events、POST /events/:id/push_event、detector 上报成功) |
{ id, camera_id, camera_name, types, status, started_at, thumbnail_url } |
event_update |
事件字段更新(保留) | 与 event_create 同结构 |
控制开关:
- 读取
settings.frontend.event_popup_enabled(值"false"时停止推送,默认/缺失 → 启用) - 设置后由
SettingHandler.UpsertByName同步失效 SSE 内部缓存(30s TTL)
详情:见
internal/sse/events.go与internal/handler/event_handler.go的publishEventCreate。
GET /api/v1/events/ws (WebSocket)
- 鉴权:
?token=query JWT - 同源策略已禁用(
CheckOrigin: return true) - 心跳:服务端每 54s 发 Ping;客户端需在 60s 内响应 Pong
- 接收消息格式与 SSE 一致(
type+data),由ws.Manager.BroadcastJSON转发
握手失败(无 token / 无效 token)返回 401 {"error": "..."}。
API 端点
Detectors 检测器管理
边缘 AI 检测器注册表。
AuthOrDetector中间件用这里的 IP 实现"凭 IP 信任"。
GET /api/v1/detectors
鉴权: Admin
响应: { ret: { count, results: DetectorResponse[] } }
// DetectorResponse
{ "ip": "192.168.1.20", "type": "jetson", "version": "1.4.0" }
POST /api/v1/detectors
鉴权: Admin
请求体:
{ "ip": "192.168.1.20", "type": "jetson", "version": "1.4.0" }
ip必填(数据库唯一索引)。
成功返回 201 Created + 新建对象。
GET /api/v1/detectors/:ip
鉴权: Admin
路径参数: ip — 字符串 IP 地址
成功返回单个 DetectorResponse,未找到 → 404 detector not found。
PUT /api/v1/detectors/:ip / PATCH /api/v1/detectors/:ip
鉴权: Admin
Body: 局部更新(type、version),ip 不可改。
返回更新后的对象。
DELETE /api/v1/detectors/:ip
鉴权: Admin
返回 204 No Content。
POST /api/v1/detectors/register
鉴权: 任意已登录用户 用途: 检测器自服务注册/心跳。
请求体:
{ "ip": "192.168.1.20", "type": "jetson", "version": "1.4.0" }
不存在则创建、存在则更新
type+version(凭 IP)。返回DetectorResponse。
DELETE /api/v1/detectors?id=<id>
鉴权: Admin
Query: id (uint, 必填)
返回 204 No Content。未找到 → 404 detector not found。
Cameras 摄像头管理
GET /api/v1/cameras
鉴权: Auth
响应: CameraResponse[] 的分页包装。
// CameraResponse
{
"id": 1,
"name": "Front Door",
"uri": "rtsp://user:pass@192.168.1.10/stream1",
"streaming_uri": "rtsp://192.168.1.10/stream1",
"mode": "detect",
"schedule": { /* JSON, 见 model.Camera.Schedule */ },
"status": "online",
"detect_params": { /* JSON */ },
"default_params": { /* JSON */ },
"rules": [ /* RuleResponse[] */ ],
"should_push": false,
"config_params": { /* JSON */ },
"sampling": "5m",
"note": null,
"snapshot": "snapshots/cam1.jpg",
"remote_id": "",
"raw_address": "192.168.1.10",
"ip": "192.168.1.10",
"port": 554
}
POST /api/v1/cameras
鉴权: Admin
Body: Camera JSON(除 id/created_at/updated_at 外)。
成功 201,返回带数据库写入后字段的对象。
GET /api/v1/cameras/:id
鉴权: Auth 或 Detector IP
路径参数: id uint
未找到 → 404 camera not found。
PUT /api/v1/cameras/:id
鉴权: Admin
全量替换(id 来自 URL)。
PATCH /api/v1/cameras/:id
鉴权: Auth 或 Detector IP 局部更新(仅出现在 body 里的字段被覆盖,handler 未做 diff,传整个对象即可)。
DELETE /api/v1/cameras/:id
鉴权: Admin
返回 204。
POST /api/v1/cameras/:id/enable_sampling
鉴权: Admin
将 Camera.Sampling 设为启用(enableSampling 在 service 层实现具体策略)。返回 200 { ret: {} }。
POST /api/v1/cameras/:id/disable_sampling
鉴权: Admin 对应关闭采样。
GET /api/v1/cameras/:id/stream
鉴权: Auth
返回当前拉流状态:
// 已有 HLS 转码
{
"err": { "ec": 0, "dm": "ok" },
"ret": {
"status": "running",
"playlist": "<internal playlist path>",
"hls_url": "/hls/camera_1.m3u8",
"webrtc_url": "/api/v1/stream/webrtc/proxy?src=<encoded-uri>",
"webrtc_src": "rtsp://..."
}
}
// 未启动
{
"err": { "ec": 0, "dm": "ok" },
"ret": {
"status": "stopped",
"webrtc_url": "/api/v1/stream/webrtc/proxy?src=<encoded-uri>",
"webrtc_src": "rtsp://..."
}
}
优先使用
camera.streaming_uri,未设置时退回camera.uri。
POST /api/v1/cameras/:id/stream/start
鉴权: Admin 启动 ffmpeg → go2rtc → HLS。
返回:
{
"err": { "ec": 0, "dm": "ok" },
"ret": {
"status": "started",
"playlist": "<internal playlist path>",
"hls_url": "/hls/camera_1.m3u8",
"webrtc_url": "/api/v1/stream/webrtc/proxy?src=<encoded-uri>",
"webrtc_src": "rtsp://..."
}
}
HLS 切片存放:
{storage.local.path}/hls/camera_<id>.{ts,m3u8},经/hls/*路由鉴权后由ServeHLS读取。
POST /api/v1/cameras/:id/stream/stop
鉴权: Admin
{ "err": { "ec": 0, "dm": "ok" }, "ret": { "status": "stopped" } }
GET /api/v1/cameras/:id/snapshot
鉴权: Auth
响应: image/jpeg 二进制(c.Data(200, "image/jpeg", snapshot)),由 stream.Manager.TakeSnapshot 通过 ffmpeg 单帧抓取。
未配置 URI → 400 camera has no stream URI configured。
Events 事件管理
GET /api/v1/events
鉴权: Auth 支持的 Query 参数:
| 参数 | 类型 | 说明 |
|---|---|---|
camera_id |
uint / 逗号分隔 uint | 单值精确匹配;多值转 camera_id__in 语义 |
status |
string | 精确匹配 events.status |
types |
逗号分隔 string | 不区分大小写;空段会被丢弃 |
time_after |
RFC3339 | started_at >= value |
time_before |
RFC3339 | started_at <= value |
limit |
int | 默认 20,上限 1000 |
offset |
int | 默认 0 |
ordering |
string | "field"(ASC) / "-field"(DESC),字段白名单见 repo;非法 → id DESC |
响应: 标准分页。results: EventResponse[]:
{
"id": 123,
"camera_id": 1,
"started_at": "2026-06-03T08:30:00Z",
"ended_at": null,
"mediums": [
{ "id": 456, "name": "frame.jpg", "file": "mediums/event_1/.../frame.jpg", "event_id": 1 }
],
"camera": { /* CameraResponse */ },
"types": "person,vehicle",
"types_bits": 6,
"obj_types": null,
"uuid": "0c1b...",
"status": "open",
"remark": "",
"should_push": false,
"metadata": null
}
任意 query 参数解析失败会静默忽略(不返回 400),与 Python 版本行为一致。
GET /api/v1/events/:id
鉴权: Auth
返回单个 EventResponse。未找到 → 404 event not found。
POST /api/v1/events
鉴权: Auth 或 Detector IP
Body: Event JSON
必填字段:
camera_id(uint)types(string,去空白后非空)
可选:
mediums: [{id, name, file, ...}]— 仅id会被采纳,自动设置event.medium_id = mediums[0].id,然后event.mediums在 handler 中清空(重新加载时由 service 预加载)。
成功 201,返回 EventResponse,并广播 SSE/WS event_create(受 frontend.event_popup_enabled 开关控制)。
PUT /api/v1/events/:id / PATCH /api/v1/events/:id
鉴权: Admin
Body: 完整 Event(id 来自 URL)。
当前实现:把
id之外的字段全量覆盖到数据库行(service 层Save/Updates),不是真正的 PATCH 语义。
DELETE /api/v1/events/:id
鉴权: Admin
返回 204。
POST /api/v1/events/:id/close_event
鉴权: Admin
关闭事件(设置 ended_at 等,具体见 EventService.CloseEvent)。
POST /api/v1/events/delete_all
鉴权: Admin
Body: [1, 2, 3] — uint ID 数组
返回 204。
POST /api/v1/events/:id/push_event
鉴权: Auth 或 Detector IP
同 POST /events,但在 service 层走 PushEvent(语义:可能触发"事件升级 / 推送规则"链路)。成功同样广播 SSE event_create。
POST /api/v1/events/queue_analysis
鉴权: Auth 或 Detector IP Body:
{ "camera_name": "Front Door", "type": "motion" }
成功 201,返回 EventResponse。camera_name 与 type 均必填。
POST /api/v1/events/people_count
鉴权: Auth 或 Detector IP Body:
{
"macAddress": "AA:BB:CC:DD:EE:FF",
"eventType": "PeopleCounting",
"enter": 3,
"exit": 1,
"startTime": "2026-06-03T08:00:00Z",
"endTime": "2026-06-03T08:30:00Z"
}
校验:
macAddress必填eventType必须严格等于"PeopleCounting"(其他值 →400 event_type must be 'PeopleCounting')
成功 201,返回 EventResponse。
GET /api/v1/events/unmarked
鉴权: Auth
返回每个摄像头未标记(remark == '')的事件数:
{
"err": { "ec": 0, "dm": "ok" },
"ret": [
{ "camera_id": 1, "count": 5 },
{ "camera_id": 2, "count": 0 }
]
}
数据结构由
EventService.CountUnmarkedByCamera返回。
GET /api/v1/events/retrieve_ids
鉴权: Auth
用途: Python 兼容 cursor 风格拉取 ID 列表(id >= start_id,升序,最多 10000)。
Query 参数:
start_id(uint, 默认1)id(uint) — 限定单条- 其它同
GET /events(camera_id/status/types/ 时间范围 /limit/offset/ordering)
响应:
{
"err": { "ec": 0, "dm": "ok" },
"ret": { "ids": [101, 102, 103], "total": 3 }
}
GET /api/v1/events/retrieves
鉴权: Auth 用途: Python 兼容"倒序 + has_next"分页。
Query 参数:
start_id(uint, 默认0xffffffff) — 拉取id < start_idid(uint) — 限定单条limit(int, 默认 20, 上限 1000, 负数 → 0)- 其它同
GET /events
响应:
{
"err": { "ec": 0, "dm": "ok" },
"ret": {
"objects": [ /* EventResponse[] */ ],
"total": 7,
"has_next": true
}
}
当传
?id=<n>时走快速路径,直接返回单条 +has_next: false。
Rules 规则管理
GET /api/v1/rules
鉴权: Auth
响应: RuleResponse[] 分页包装。
// RuleResponse
{
"id": 7,
"camera": 1,
"unique_id": "uuid-v4",
"name": "person-rule",
"mode": "detect",
"algo": "yolov8n",
"params": { /* JSON */ },
"params_base": { /* JSON */ },
"schedule": { /* JSON */ },
"event_types": { "person": "Person" }
}
event_types是从数据库JSON列中只保留 string→string 键值对后的精简版(model.RuleResponse注释)。
POST /api/v1/rules
鉴权: Admin
Body: 完整 Rule(camera_id 必填)。返回 201 + RuleResponse。
GET /api/v1/rules/:id
鉴权: Auth
未找到 → 404 rule not found。
PUT /api/v1/rules/:id / PATCH /api/v1/rules/:id
鉴权: Admin
Body: 局部覆盖(name / mode / algo / schedule 出现即覆盖)。
注意:
camera_id一旦设置后通过此接口不能修改。
DELETE /api/v1/rules/:id
鉴权: Admin
返回 204。
GET /api/v1/rules/camera/:camera_id
鉴权: Auth 返回该摄像头下的所有规则(标准分页包装)。
POST /api/v1/rules/camera/:camera_id
鉴权: Admin
Body: Rule JSON(无需包含 camera_id,URL 提供)。
返回 201。
Mediums 媒体文件
文件存于
{storage.local.path}/mediums/{event_<id>|unassigned}/<timestamp>_<name>.<ext>,最大 100MB。
GET /api/v1/mediums
鉴权: Auth
响应: MediumResponse[] 分页包装:
{
"id": 100,
"name": "frame.jpg",
"file": "mediums/event_1/1717400000_frame.jpg",
"event_id": 1
}
POST /api/v1/mediums
鉴权: Auth 或 Detector IP
Content-Type: multipart/form-data
字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
file |
file | 是 | 最大 100MB |
name |
string | 否 | 默认取 file.Filename |
event_id |
uint (form 字段) | 否 | 关联事件 ID |
成功 201,返回 MediumResponse。
GET /api/v1/mediums/:id
鉴权: Auth
未找到 → 404 medium not found。
PUT /api/v1/mediums/:id / PATCH /api/v1/mediums/:id
鉴权: Admin
Body: 局部更新(name / file)。
⚠️ 当前实现直接以
file字符串覆盖原相对路径,不会把文件复制/移动到新位置。
DELETE /api/v1/mediums/:id
鉴权: Admin
副作用: 同时删除磁盘文件(若 medium.file 非空)。返回 204。
GET /api/v1/mediums/:id/file
鉴权: Auth
响应: 二进制流,附带 Content-Disposition: inline; filename*=UTF-8''<encoded-name> 头。
文件缺失 → 404 file not found。
Webhooks 回调配置
GET /api/v1/webhooks
鉴权: Admin
响应: WebhookResponse[] 分页:
{
"id": 1,
"types": "person,vehicle",
"url": "https://example.com/hook",
"serializer": "default"
}
POST /api/v1/webhooks
鉴权: Admin
Body: 完整 Webhook JSON。返回 201。
GET /api/v1/webhooks/:id
鉴权: Admin
PUT /api/v1/webhooks/:id / PATCH /api/v1/webhooks/:id
鉴权: Admin
Body: 局部覆盖(url / types / serializer)。
DELETE /api/v1/webhooks/:id
鉴权: Admin
返回 204。
POST /api/v1/webhooks/echo
鉴权: Admin Body: 任意 JSON / 文本(最多读取 1MB)。 响应: 把请求体原样以字符串形式回显:
{ "err": { "ec": 0, "dm": "ok" }, "ret": "{\"foo\":\"bar\"}" }
用于配置/连通性测试。
Algorithms 算法管理
与
Rule.algo字段关联的算法字典表(code_name为主键)。
GET /api/v1/algorithms
鉴权: Auth 或 Detector IP
响应: 直接数组(无分页包装,RetArray):
{ "err": { "ec": 0, "dm": "ok" }, "ret": [ { "code_name": "yolov8n", "name": "YOLOv8 Nano" } ] }
POST /api/v1/algorithms
鉴权: Admin Body:
{ "code_name": "yolov8n", "name": "YOLOv8 Nano" }
GET /api/v1/algorithms/:code_name
鉴权: Auth 或 Detector IP
路径参数: code_name (string, 主键)
未找到 → 404 algorithm not found。
DELETE /api/v1/algorithms/:code_name
鉴权: Admin
返回 204。
Samples 样本管理
视频样本聚合(含若干
SampleFile),用于后续分析、上传到云端。
GET /api/v1/samples
鉴权: Auth 或 Detector IP
响应: Sample[] 分页包装。
// Sample
{
"id": "uuid-string",
"camera_id": 1,
"started_at": "2026-06-03T08:00:00Z",
"ended_at": "2026-06-03T08:05:00Z",
"notified_cloud": false,
"notified_cloud_at": null,
"expired": 0,
"file_count": 3,
"types": "person,vehicle",
"types_at": null,
"metadata": null,
"notified_guardian": false,
"notified_guardian_at": null,
"event_at": null,
"created_at": "2026-06-03T08:00:00Z",
"updated_at": "2026-06-03T08:00:00Z",
"sample_files": []
}
id是char(36)字符串(UUID),区别于其它资源的 uint 主键。
POST /api/v1/samples
鉴权: Auth 或 Detector IP
Body: 完整 Sample(id 必填,需为合法 UUID)。
GET /api/v1/samples/:id
鉴权: Auth 或 Detector IP
未找到 → 404 sample not found。
PUT /api/v1/samples/:id / PATCH /api/v1/samples/:id
鉴权: Auth 或 Detector IP
Body: 完整 Sample(id 来自 URL)。
DELETE /api/v1/samples/:id
鉴权: Auth 或 Detector IP
返回 204。
POST /api/v1/samples/:id/upload_file
鉴权: Auth 或 Detector IP
Body: 完整 SampleFile JSON(id 字段填入 URL 中的数字 ID):
{
"path": "samples/<uuid>/file1.mp4",
"content_type": "mp4",
"s3_data": { /* optional JSON */ }
}
成功 201,返回 SampleFile:
{
"id": 12,
"sample_id": "<uuid>",
"path": "samples/<uuid>/file1.mp4",
"content_type": "mp4",
"s3_data": null,
"uploaded_at": null,
"file_deleted": false,
"created_at": "2026-06-03T08:00:00Z",
"updated_at": "2026-06-03T08:00:00Z"
}
常量
ContentTypeJPEG = "jpeg"/ContentTypePNG = "png"/ContentTypeMP4 = "mp4"(model.Sample)。
POST /api/v1/samples/files/:id/mark_uploaded
鉴权: Auth 或 Detector IP
路径参数: id (uint, SampleFile.ID)
将 SampleFile.UploadedAt 设为当前时间。返回 200 { ret: {} }。
Users 用户管理
GET /api/v1/users
鉴权: Admin
响应: UserResponse[] 分页:
{ "id": 1, "username": "ops", "is_admin": true, "is_active": true }
password不会出现在响应中(json:"-")。
POST /api/v1/users
鉴权: Admin
Body: 完整 User JSON。返回 201。
创建时密码通过
AuthService.CreateUser加 bcrypt(默认 cost)后存储。重复用户名 → 服务层ErrUserExists→500(当前实现未专门捕获 409 语义)。
GET /api/v1/users/:id
鉴权: Admin
PUT /api/v1/users/:id / PATCH /api/v1/users/:id
鉴权: Admin
Body: 局部覆盖(username / is_active / is_admin)。
通过此接口不能修改密码 — 使用
POST /api/v1/users/:id/change_password。
DELETE /api/v1/users/:id
鉴权: Admin
返回 204。
POST /api/v1/users/:id/change_password
鉴权: Admin Body:
{ "old_password": "old", "new_password": "new-Str0ng!" }
AuthService.UpdatePassword(user_service.go)的实际语义是仅验证old_password与新密码均非空后覆盖哈希。返回200 { ret: {} }。
Settings 系统设置
KV 表(
name唯一索引)。SSE 推送开关即存于此。
GET /api/v1/settings
鉴权: Admin
响应: SettingResponse[] 分页:
{ "id": 1, "name": "frontend.event_popup_enabled", "value": "true" }
POST /api/v1/settings
鉴权: Admin
Body: 完整 Setting JSON。返回 201。
GET /api/v1/settings/:id
鉴权: Admin
GET /api/v1/settings/name/:name
鉴权: Admin
路径参数: name (string)
未找到 → 404 setting not found。
PUT /api/v1/settings/:id / PATCH /api/v1/settings/:id
鉴权: Admin
Body: 局部覆盖(name / value)。
PUT /api/v1/settings/by-name/:name
鉴权: Admin Body:
{ "value": "true" }
Upsert 语义:若 name 存在则更新 value,否则创建新行。
副作用:调用成功后立即失效 SSE 内部的
frontend.event_popup_enabled缓存(30s TTL),使管理界面开关立即生效。
DELETE /api/v1/settings/:id
鉴权: Admin
返回 204。
预置默认设置(启动时由
cmd/superbox自动创建):
frontend.event_popup_enabled="true"
Stream 视频流
GET /api/v1/stream/webrtc/proxy?src=<uri>
鉴权: Auth
Query: src (string, 必填) — 原始媒体 URI
内部把 src 包装为 ffmpeg:<src>#video=h264,转发到 go2rtc 的 http://127.0.0.1:<port>/api/stream.mp4?src=<encoded>,并将响应体流式复制回客户端,保留 Content-Type / Content-Length 头。
错误:
src缺失 →400 missing src parameter- go2rtc 返回非 200 →
500 go2rtc returned status <n> - 网络错误 →
500 failed to fetch stream: ...
GET /hls/*filepath
鉴权: 通过 ?token= query JWT
用途: 暴露 {storage.local.path}/hls/ 下的转码产物。
限制: 文件后缀必须为 .m3u8 或 .ts,否则 400 invalid file type。
浏览器
<video>元素播放 HLS 时,URL 模板:/hls/camera_<id>.m3u8?token=<JWT>
静态资源
| 路径 | 说明 |
|---|---|
/storage/* |
{storage.local.path} 下的所有文件(公开) |
/assets/* |
嵌入的前端静态资源(公开) |
/admin, /admin/* |
前端 SPA 入口(公开;带路径时回落到 index.html) |
/ |
前端根入口(公开) |
静态资源未走认证,直接 200。
数据模型
完整字段以 GORM tag 为准;以下 JSON 字段是 API 响应里出现的精简版。
CameraResponse
| 字段 | 类型 | 说明 |
|---|---|---|
id |
uint | 主键 |
name |
string | 名称 |
uri |
string | 主码流 RTSP / HTTP URI |
streaming_uri |
string | 备用流地址(缺省时回退到 uri) |
mode |
string | detect / sample 等 |
schedule |
JSON | 排期(具体 schema 见 Rule.Schedule) |
status |
string | online / offline 等(由 CameraMonitor 周期刷新) |
detect_params |
JSON | 检测相关参数 |
default_params |
JSON | 默认参数 |
rules |
RuleResponse[] |
该摄像头的规则(外键) |
should_push |
bool | 是否推送至云 |
config_params |
JSON | 其它自定义配置 |
sampling |
string | 采样频率字符串(如 5m) |
note |
JSON | 备注 |
snapshot |
string | 静态预览图相对路径(/storage/<snapshot> 访问) |
remote_id |
string | 云端 ID(边缘同步) |
raw_address |
string | 裸 IP / 域名 |
ip |
string | 等同于 raw_address(保持 Django 字段兼容) |
port |
int | RTSP 端口 |
EventResponse
| 字段 | 类型 | 说明 |
|---|---|---|
id |
uint | 主键 |
camera_id |
uint | 外键 |
started_at |
string (RFC3339Z) | 事件开始 |
ended_at |
string? (RFC3339Z) | 事件结束(可空) |
mediums |
MediumResponse[] |
关联媒体(外键 EventID) |
camera |
CameraResponse? |
嵌套摄像头(service 预加载) |
types |
string | 逗号分隔事件类型 |
types_bits |
uint64 | 类型位掩码 |
obj_types |
JSON | 目标类别详情 |
uuid |
string | 唯一 UUID |
status |
string | open / closed / marked 等 |
remark |
string | 备注(用于"标记"语义) |
should_push |
bool | 是否推送至云 |
metadata |
JSON | 任意附加元数据 |
RuleResponse
| 字段 | 类型 | 说明 |
|---|---|---|
id |
uint | 主键 |
camera |
uint | camera_id(字段名 camera 保持 Django 兼容) |
unique_id |
string | UUID |
name |
string | — |
mode |
string | detect / sample |
algo |
string | 对应 algorithms.code_name |
params |
JSON | 当前参数 |
params_base |
JSON | 基础参数 |
schedule |
JSON | 排期 |
event_types |
map[string]string | 仅保留字符串键值对 |
MediumResponse
| 字段 | 类型 | 说明 |
|---|---|---|
id |
uint | 主键 |
name |
string | 文件名 |
file |
string | 相对 storage.local.path 的路径 |
event_id |
uint* | 关联事件(可空) |
DetectorResponse
| 字段 | 类型 | 说明 |
|---|---|---|
ip |
string | 唯一索引 |
type |
string | 设备类型 |
version |
string | 固件 / 软件版本 |
与 Django 一致:不返回
id、created_at、updated_at。
WebhookResponse
| 字段 | 类型 | 说明 |
|---|---|---|
id |
uint | 主键 |
types |
string | 触发事件类型,逗号分隔 |
url |
string | 目标 URL |
serializer |
string | 序列化器名(默认 default) |
UserResponse
| 字段 | 类型 | 说明 |
|---|---|---|
id |
uint | 主键 |
username |
string | 唯一索引 |
is_admin |
bool | 是否管理员 |
is_active |
bool | 是否启用 |
SettingResponse
| 字段 | 类型 | 说明 |
|---|---|---|
id |
uint | 主键 |
name |
string | 唯一索引 |
value |
string | 字符串值(无类型) |
AlgorithmResponse
| 字段 | 类型 | 说明 |
|---|---|---|
code_name |
string | 主键 |
name |
string | 显示名 |
Sample / SampleFile
见 Samples 章节中的字段说明。
附录:约束与默认值
JWT 与登录
- access token 签名:HS256
- 密钥:
config.json → jwt.secret_key(32 字节 hex,CLI 命令superbox jwt key generate -s可生成并写回) - access token TTL:
access_token_ttl(默认15m);由 JWTexp声明携带,过期即失效 - refresh token TTL:
refresh_token_ttl(默认7d);由refresh_tokens.expires_at列存 DB,每次成功 refresh 都重置为now + 7d(sliding window) - refresh token 格式:
crypto/rand32 字节 →base64.RawURLEncoding→ 43 字符[A-Za-z0-9_-] - refresh token 存储:服务端只存 sha256 哈希(
refresh_tokens.token_hash),DB 泄露也不暴露明文 - 密码:bcrypt
DefaultCost(10) - 弱密码黑名单:
password、12345678、qwerty、admin、letmein、welcome(不区分大小写) - 用户名不可等于密码,不可为
admin
Refresh token 行为
- Login 颁发新 pair,INSERT 一行
refresh_tokens(family_id= 新 UUIDv4) - Refresh(成功):事务里把旧行
rotated_at = now(),INSERT 新行(同family_id、新 hash、expires_at = now + 7d) - Reuse detection:被轮换过的旧 token 再次被提交 →
DELETE FROM refresh_tokens WHERE family_id = ?(杀整个 family),返 401 - User deactivation / deletion 在 refresh 时被检测 → 杀 family,401
- Logout:删
user_id + token_hash匹配的 1 行;找不到也返200(idempotent) - 无后台清理任务(YAGNI):过期行自然累积,可手工
DELETE FROM refresh_tokens WHERE expires_at < now()或跑RefreshTokenRepository.DeleteExpired()
客户端 silent refresh 协议
- 前端 axios 响应拦截器在收到
401时:- 若
localStorage.refresh_token存在且请求不是/auth/refresh本身且未重试过:调/auth/refresh,成功后用新 token 重发原请求 - 否则:清 localStorage、
window.location.href = '/admin/login'
- 若
- in-flight 去重:5 个并发 401 共享一个
/auth/refresh请求(模块级 Promise 缓存),避免 strict rotation 误触发 reuse detection
多 tab 已知限制
in-flight 去重是单 page-load 级别。两个 tab 同时触发 401 时:
- Tab 1 调
/auth/refresh成功,family 已轮换 - Tab 2 调
/auth/refresh命中 reuse detection,family 杀光 - 结果:两个 tab 都被强制重新登录
v1 不修。NVR admin 多 tab 不常见。下次重登即可。后续可加 BroadcastChannel 或 Web Locks API。
事件查询
limit默认 20,上限 1000retrieve_ids返回最多 10000 条retrieves的start_id默认0xffffffff- 任何 query 参数解析失败 → 静默忽略(不返回 400)
上传与存储
mediums文件大小上限 100MB(storage.maxUploadSize)- 路径规则:
{storage.local.path}/mediums/{event_<id>|unassigned}/<ts_ms>_<safe-name>.<ext> - 文件名清洗:保留
[a-zA-Z0-9_-],其它字符 →_,最长 50 字符 Medium.ServeFile时使用http.ServeContent(支持 Range 与Last-Modified)
HLS 切片
- 文件后缀必须为
.m3u8或.ts,否则 400 - 路径限制在
{storage.local.path}/hls/下
WebRTC 代理
- 始终以
ffmpeg:<src>#video=h264形式包装原始流,确保浏览器 H264 兼容 - go2rtc 默认端口
1984(由config.json → stream.go2rtc_port修改)
SSE 推送
- 事件类型:
event_create(新增)、event_update(保留) - 触发点:
POST /api/v1/events、POST /api/v1/events/:id/push_event(detector 上报 / 业务方创建) - 受
settings.frontend.event_popup_enabled开关控制(默认启用) - 缓存 TTL:30 秒;
PUT /settings/by-name/<name>会主动失效
Detector IP 信任
- 仅
AuthOrDetector模式生效 detector.ip是size:64字符串(兼容 IPv4/IPv6)- 触发时
gin.Context写入detector_ip,业务层可读取
中间件链顺序(全局)
middleware.Recovery(logger)— panic 兜底middleware.Logger(logger)— 请求日志middleware.CORS()— 跨域
启动行为(cmd/superbox)
- 配置文件查找顺序:
--config→viper.AddConfigPath(".", "./config", "/etc/superbox/")→ 默认数据目录 - 缺失时自动生成默认
config.json(含随机 JWT key) - 启动时执行
AutoMigrate+ensureDefaultSettings(注入frontend.event_popup_enabled=true) CameraMonitor默认enabled=true,interval_sec=30
数据库支持
sqlite/sqlite3(默认)postgres/postgresqlmysql(默认)