# Superbox Go API Reference > 后端 API 端点详尽文档。源自动词检索表 + 路由注册表 (`internal/router/router.go`) + handler/model 实现。 ## 目录 - [基础信息](#基础信息) - [统一响应信封](#统一响应信封) - [认证机制](#认证机制) - [访问控制矩阵](#访问控制矩阵) - [错误码与 HTTP 状态码](#错误码与-http-状态码) - [公共端点](#公共端点) - [健康检查](#健康检查) - [系统初始化](#系统初始化) - [认证](#认证) - [事件流 (SSE / WebSocket)](#事件流-sse--websocket) - [API 端点](#api-端点) - [Detectors 检测器管理](#detectors-检测器管理) - [Cameras 摄像头管理](#cameras-摄像头管理) - [Events 事件管理](#events-事件管理) - [Rules 规则管理](#rules-规则管理) - [Mediums 媒体文件](#mediums-媒体文件) - [Webhooks 回调配置](#webhooks-回调配置) - [Algorithms 算法管理](#algorithms-算法管理) - [Samples 样本管理](#samples-样本管理) - [Users 用户管理](#users-用户管理) - [Settings 系统设置](#settings-系统设置) - [Stream 视频流](#stream-视频流) - [静态资源](#静态资源) - [数据模型](#数据模型) - [附录:约束与默认值](#附录约束与默认值) - [JWT 与登录](#jwt-与登录) - [Refresh token 行为](#refresh-token-行为) - [客户端 silent refresh 协议](#客户端-silent-refresh-协议) - [多 tab 已知限制](#多-tab-已知限制) --- ## 基础信息 - 基础路径: `/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` 中定义的信封: ```json { "err": { "ec": 0, "dm": "ok", "em": "" }, "ret": } ``` | 字段 | 类型 | 说明 | |------|------|------| | `err.ec` | int | 自定义错误码,`0` = 成功;与 HTTP 状态码同步 | | `err.dm` | string | 错误描述宏(`ok` / `Bad Request` / `Not Found` / `Internal Server Error` 等) | | `err.em` | string | 错误详情原文(成功时省略) | | `ret` | any | 业务负载,结构视端点而异 | ### 列表型分页负载(`response.RetList`) ```json { "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 接口直接返回数组,不带分页包装: ```json { "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`,返回: ```json { "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`): ```json { "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 时自动: 1. 用 `localStorage.refresh_token` 调 `POST /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 任选其一: ```http # Header 方式(推荐) Authorization: Bearer # Query 方式(用于浏览器 HLS / SSE / WebSocket) GET /api/v1/events/stream?token= GET /api/v1/cameras/1/stream?token= GET /hls/camera_1.m3u8?token= ``` > 中间件优先读 `Authorization` 头,缺失时回退到 `?token=` query。 ### 携带 refresh token refresh token **不通过 header 传递**(永远不进 URL / Authorization 头),仅用于以下两种途径: ```http # 1. 主动调 /auth/refresh 时放在 body POST /api/v1/auth/refresh Content-Type: application/json { "refresh_token": "" } # 2. 主动 logout 时放在 body(让服务端能删 DB 行) POST /api/v1/auth/logout Authorization: Bearer { "refresh_token": "" } ``` ### 鉴权模式 | 模式 | 描述 | 失败返回 | |------|------|----------| | `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"}`。 ```http 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` 读取数据库中管理员用户数量。 ```json // Response 200 { "err": { "ec": 0, "dm": "ok" }, "ret": { "initialized": true, "needs_setup": false } } ``` > 数据库不可达时也返回 `{"initialized": false, "needs_setup": true}`(兜底)。 #### `POST /api/v1/init` 创建初始管理员(仅当尚无管理员用户时成功)。 请求体: ```json { "username": "ops", "password": "Str0ng-Pass!" } ``` 校验规则: - username 非空 - password 长度 ≥ 8,且不等于 username - 命中弱密码黑名单(`password`、`12345678`、`qwerty`、`admin`、`letmein`、`welcome`,不区分大小写)会被拒 成功: ```json { "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`): ```json { "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!" } } ``` 成功: ```json { "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,不影响登录结果) 请求: ```json { "username": "ops", "password": "Str0ng-Pass!" } ``` 字段约束: - `username`、`password` 均为 `binding:"required"`,缺失 → `400 Bad Request` - 用户名 / 密码错误 或 用户 `is_active=false` → `401 invalid credentials`(不区分两种情况,避免账号枚举) 成功响应(`LoginResult`): ```json { "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 一致即可。 请求: ```json { "refresh_token": "Z3pFmT7uVxKqY8sN2Lc4hJ1bP9aD5wE0rM6iO3yU2kX8vN4cB1gQ7zH5fS9a" } ``` 成功响应(同 `LoginResult` 形状): ```json { "err": { "ec": 0, "dm": "ok" }, "ret": { "access_token": "", "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 `)—— 中间件从中解析 `user_id` - **作用**:删 `refresh_tokens` 表中**当前会话**对应的那一行;不删其它会话(多端点登录互不影响) - **Idempotent**:找不到行或 `refresh_token` 为空都返 `200` 请求: ```http POST /api/v1/auth/logout Authorization: Bearer Content-Type: application/json { "refresh_token": "Z3pFmT7uVxKqY8sN2Lc4hJ1bP9aD5wE0rM6iO3yU2kX8vN4cB1gQ7zH5fS9a" } ``` > `refresh_token` 在 body 里是**可选**的(缺省 / 解析失败都容忍),便于前端在 access 已过期的情况下也能 logout。 成功响应: ```json { "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: \ndata: \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[] } }` ```json // DetectorResponse { "ip": "192.168.1.20", "type": "jetson", "version": "1.4.0" } ``` #### `POST /api/v1/detectors` **鉴权**: Admin 请求体: ```json { "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` **鉴权**: 任意已登录用户 **用途**: 检测器自服务注册/心跳。 请求体: ```json { "ip": "192.168.1.20", "type": "jetson", "version": "1.4.0" } ``` > 不存在则创建、存在则更新 `type` + `version`(凭 IP)。返回 `DetectorResponse`。 #### `DELETE /api/v1/detectors?id=` **鉴权**: Admin **Query**: `id` (uint, 必填) 返回 `204 No Content`。未找到 → `404 detector not found`。 --- ### Cameras 摄像头管理 #### `GET /api/v1/cameras` **鉴权**: Auth **响应**: `CameraResponse[]` 的分页包装。 ```json // 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 返回当前拉流状态: ```json // 已有 HLS 转码 { "err": { "ec": 0, "dm": "ok" }, "ret": { "status": "running", "playlist": "", "hls_url": "/hls/camera_1.m3u8", "webrtc_url": "/api/v1/stream/webrtc/proxy?src=", "webrtc_src": "rtsp://..." } } ``` ```json // 未启动 { "err": { "ec": 0, "dm": "ok" }, "ret": { "status": "stopped", "webrtc_url": "/api/v1/stream/webrtc/proxy?src=", "webrtc_src": "rtsp://..." } } ``` > 优先使用 `camera.streaming_uri`,未设置时退回 `camera.uri`。 #### `POST /api/v1/cameras/:id/stream/start` **鉴权**: Admin 启动 ffmpeg → go2rtc → HLS。 返回: ```json { "err": { "ec": 0, "dm": "ok" }, "ret": { "status": "started", "playlist": "", "hls_url": "/hls/camera_1.m3u8", "webrtc_url": "/api/v1/stream/webrtc/proxy?src=", "webrtc_src": "rtsp://..." } } ``` > HLS 切片存放:`{storage.local.path}/hls/camera_.{ts,m3u8}`,经 `/hls/*` 路由鉴权后由 `ServeHLS` 读取。 #### `POST /api/v1/cameras/:id/stream/stop` **鉴权**: Admin ```json { "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[]`: ```json { "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**: ```json { "camera_name": "Front Door", "type": "motion" } ``` 成功 `201`,返回 `EventResponse`。`camera_name` 与 `type` 均必填。 #### `POST /api/v1/events/people_count` **鉴权**: Auth **或** Detector IP **Body**: ```json { "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 == ''`)的事件数: ```json { "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`) 响应: ```json { "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` 响应: ```json { "err": { "ec": 0, "dm": "ok" }, "ret": { "objects": [ /* EventResponse[] */ ], "total": 7, "has_next": true } } ``` > 当传 `?id=` 时走快速路径,直接返回单条 + `has_next: false`。 --- ### Rules 规则管理 #### `GET /api/v1/rules` **鉴权**: Auth **响应**: `RuleResponse[]` 分页包装。 ```json // 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_|unassigned}/_.`,最大 **100MB**。 #### `GET /api/v1/mediums` **鉴权**: Auth **响应**: `MediumResponse[]` 分页包装: ```json { "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''` 头。 文件缺失 → `404 file not found`。 --- ### Webhooks 回调配置 #### `GET /api/v1/webhooks` **鉴权**: Admin **响应**: `WebhookResponse[]` 分页: ```json { "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)。 **响应**: 把请求体原样以字符串形式回显: ```json { "err": { "ec": 0, "dm": "ok" }, "ret": "{\"foo\":\"bar\"}" } ``` > 用于配置/连通性测试。 --- ### Algorithms 算法管理 > 与 `Rule.algo` 字段关联的算法字典表(`code_name` 为主键)。 #### `GET /api/v1/algorithms` **鉴权**: Auth **或** Detector IP **响应**: 直接数组(**无**分页包装,`RetArray`): ```json { "err": { "ec": 0, "dm": "ok" }, "ret": [ { "code_name": "yolov8n", "name": "YOLOv8 Nano" } ] } ``` #### `POST /api/v1/algorithms` **鉴权**: Admin **Body**: ```json { "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[]` 分页包装。 ```json // 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): ```json { "path": "samples//file1.mp4", "content_type": "mp4", "s3_data": { /* optional JSON */ } } ``` 成功 `201`,返回 `SampleFile`: ```json { "id": 12, "sample_id": "", "path": "samples//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[]` 分页: ```json { "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**: ```json { "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[]` 分页: ```json { "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**: ```json { "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=` **鉴权**: Auth **Query**: `src` (string, 必填) — 原始媒体 URI 内部把 `src` 包装为 `ffmpeg:#video=h264`,转发到 go2rtc 的 `http://127.0.0.1:/api/stream.mp4?src=`,并将响应体流式复制回客户端,保留 `Content-Type` / `Content-Length` 头。 错误: - `src` 缺失 → `400 missing src parameter` - go2rtc 返回非 200 → `500 go2rtc returned status ` - 网络错误 → `500 failed to fetch stream: ...` #### `GET /hls/*filepath` **鉴权**: 通过 `?token=` query JWT **用途**: 暴露 `{storage.local.path}/hls/` 下的转码产物。 **限制**: 文件后缀必须为 `.m3u8` 或 `.ts`,否则 `400 invalid file type`。 > 浏览器 `