Files
turing_alert_web/API.md
2026-06-10 11:46:45 +08:00

48 KiB
Raw Blame History

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 始终为 nullDjango 兼容字段),分页通过 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 JWTuser_id / username / is_admin / exp / iat claims 仅客户端 jwt.access_token_ttl(默认 15m 访问受保护 API
refresh_token 32 字节随机 → 43 字符 base64-url 客户端明文 + 服务端存 sha256 哈希 jwt.refresh_token_ttl(默认 7dsliding 换新 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_keyTTL 由 jwt.access_token_ttl 控制(默认 15m)。

续期 tokensilent refresh

access 过期前无需主动操作;前端 axios 拦截器会在 401 时自动:

  1. localStorage.refresh_tokenPOST /api/v1/auth/refresh
  2. 成功后更新 localStorage + 重发原请求
  3. 失败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 IPAOO = 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不进入 handlerAuthOrDetector 失败也回 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 init CLI。

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
  • 命中弱密码黑名单(password12345678qwertyadminletmeinwelcome,不区分大小写)会被拒

成功:

{ "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" } }

副作用:

  • 写入文件权限 0600config.json
  • 自动生成 32 字节随机 jwt.secret_keyhex
  • 默认 access_token_ttl=15mrefresh_token_ttl=7dstorage.type=local、go2rtc 默认参数

认证

全部 3 个端点走 application/json 信封({"err":{...},"ret":...}),见前文「统一响应信封」。

POST /api/v1/auth/login

  • 鉴权:公开(无 token
  • Content-Type: application/json
  • 副作用
    • refresh_tokens 表 INSERT 一行(family_id 为新 UUIDv4expires_at = now + 7d
    • 更新 users.last_login = now(失败时记 WARN不影响登录结果

请求:

{ "username": "ops", "password": "Str0ng-Pass!" }

字段约束:

  • usernamepassword 均为 binding:"required",缺失 → 400 Bad Request
  • 用户名 / 密码错误 或 用户 is_active=false401 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 tokenbody 里的 refresh_token 验真
  • 作用:用 refresh token 换新 access + refresh pairstrict rotation
  • 副作用
    • 在事务中:把旧行的 rotated_at 设为 now(),并 INSERT 新行(同 family_id,新 token_hash,新 expires_at = now + 7d
    • 更新 users.last_loginrefresh 不算"登录"
  • 行为(按结果分类):
条件 行为 HTTP
body 缺 refresh_token 400 400 Bad Request
DB 找不到该 sha256(refresh_token) 401= invalid 401 refresh failed
旧行 rotated_at 非空reuse 杀整个 familyDELETE WHERE family_id = ?401 401 refresh failed
expires_at 已过 401 401 refresh failed
用户不存在 / is_active=false 杀 family401 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 tokenAuthorization: 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.gointernal/handler/event_handler.gopublishEventCreate

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: 局部更新(typeversionip 不可改。

返回更新后的对象。

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 JSONid/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: 完整 Eventid 来自 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,返回 EventResponsecamera_nametype 均必填。

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 /eventscamera_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_id
  • id (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: 完整 Rulecamera_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_idURL 提供)。 返回 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": []
}

idchar(36) 字符串UUID区别于其它资源的 uint 主键。

POST /api/v1/samples

鉴权: Auth Detector IP Body: 完整 Sampleid 必填,需为合法 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: 完整 Sampleid 来自 URL

DELETE /api/v1/samples/:id

鉴权: Auth Detector IP 返回 204

POST /api/v1/samples/:id/upload_file

鉴权: Auth Detector IP Body: 完整 SampleFile JSONid 字段填入 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后存储。重复用户名 → 服务层 ErrUserExists500(当前实现未专门捕获 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.UpdatePassworduser_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 一致:返回 idcreated_atupdated_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_key32 字节 hexCLI 命令 superbox jwt key generate -s 可生成并写回)
  • access token TTLaccess_token_ttl(默认 15m);由 JWT exp 声明携带,过期即失效
  • refresh token TTLrefresh_token_ttl(默认 7d);由 refresh_tokens.expires_at 列存 DB每次成功 refresh 都重置为 now + 7dsliding window
  • refresh token 格式crypto/rand 32 字节 → base64.RawURLEncoding → 43 字符 [A-Za-z0-9_-]
  • refresh token 存储:服务端只存 sha256 哈希refresh_tokens.token_hashDB 泄露也不暴露明文
  • 密码bcrypt DefaultCost10
  • 弱密码黑名单password12345678qwertyadminletmeinwelcome(不区分大小写)
  • 用户名不可等于密码,不可为 admin

Refresh token 行为

  • Login 颁发新 pairINSERT 一行 refresh_tokensfamily_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 时被检测 → 杀 family401
  • Logout:删 user_id + token_hash 匹配的 1 行;找不到也返 200idempotent
  • 无后台清理任务YAGNI过期行自然累积可手工 DELETE FROM refresh_tokens WHERE expires_at < now() 或跑 RefreshTokenRepository.DeleteExpired()

客户端 silent refresh 协议

  • 前端 axios 响应拦截器在收到 401 时:
    1. localStorage.refresh_token 存在且请求不是 /auth/refresh 本身且重试过:调 /auth/refresh,成功后用新 token 重发原请求
    2. 否则:清 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 detectionfamily 杀光
  • 结果:两个 tab 都被强制重新登录

v1 不修。NVR admin 多 tab 不常见。下次重登即可。后续可加 BroadcastChannelWeb Locks API

事件查询

  • limit 默认 20上限 1000
  • retrieve_ids 返回最多 10000
  • retrievesstart_id 默认 0xffffffff
  • 任何 query 参数解析失败 → 静默忽略(不返回 400

上传与存储

  • mediums 文件大小上限 100MBstorage.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/eventsPOST /api/v1/events/:id/push_eventdetector 上报 / 业务方创建)
  • settings.frontend.event_popup_enabled 开关控制(默认启用)
  • 缓存 TTL30 秒;PUT /settings/by-name/<name> 会主动失效

Detector IP 信任

  • AuthOrDetector 模式生效
  • detector.ipsize:64 字符串(兼容 IPv4/IPv6
  • 触发时 gin.Context 写入 detector_ip,业务层可读取

中间件链顺序(全局)

  1. middleware.Recovery(logger) — panic 兜底
  2. middleware.Logger(logger) — 请求日志
  3. middleware.CORS() — 跨域

启动行为(cmd/superbox

  • 配置文件查找顺序:--configviper.AddConfigPath(".", "./config", "/etc/superbox/") → 默认数据目录
  • 缺失时自动生成默认 config.json(含随机 JWT key
  • 启动时执行 AutoMigrate + ensureDefaultSettings(注入 frontend.event_popup_enabled=true
  • CameraMonitor 默认 enabled=trueinterval_sec=30

数据库支持

  • sqlite / sqlite3(默认)
  • postgres / postgresql
  • mysql(默认)