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

1635 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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": <data>
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `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`)。
### 续期 tokensilent 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 <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 头),仅用于以下两种途径:
```http
# 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不进入 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": "<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`
请求:
```http
POST /api/v1/auth/logout
Authorization: Bearer <access_token>
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: <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[] } }`
```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=<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": "<internal playlist path>",
"hls_url": "/hls/camera_1.m3u8",
"webrtc_url": "/api/v1/stream/webrtc/proxy?src=<encoded-uri>",
"webrtc_src": "rtsp://..."
}
}
```
```json
// 未启动
{
"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。
返回:
```json
{
"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
```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=<n>` 时走快速路径,直接返回单条 + `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_<id>|unassigned}/<timestamp>_<name>.<ext>`,最大 **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''<encoded-name>` 头。
文件缺失 → `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/<uuid>/file1.mp4",
"content_type": "mp4",
"s3_data": { /* optional JSON */ }
}
```
成功 `201`,返回 `SampleFile`
```json
{
"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[]` 分页:
```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=<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 字节 hexCLI 命令 `superbox jwt key generate -s` 可生成并写回)
- **access token TTL**`access_token_ttl`(默认 `15m`);由 JWT `exp` 声明携带,过期即失效
- **refresh token TTL**`refresh_token_ttl`(默认 `7d`);由 `refresh_tokens.expires_at` 列存 DB每次成功 refresh 都重置为 `now + 7d`**sliding window**
- **refresh token 格式**`crypto/rand` 32 字节 → `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** 颁发新 pairINSERT 一行 `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 时被检测 → 杀 family401
- **Logout**:删 `user_id + token_hash` 匹配的 1 行;找不到也返 `200`idempotent
- **无后台清理任务**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 不常见。下次重登即可。后续可加 `BroadcastChannel``Web Locks API`
### 事件查询
- `limit` 默认 20上限 **1000**
- `retrieve_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` 开关控制(默认启用)
- 缓存 TTL30 秒;`PUT /settings/by-name/<name>` 会主动失效
### Detector IP 信任
-`AuthOrDetector` 模式生效
- `detector.ip``size:64` 字符串(兼容 IPv4/IPv6
- 触发时 `gin.Context` 写入 `detector_ip`,业务层可读取
### 中间件链顺序(全局)
1. `middleware.Recovery(logger)` — panic 兜底
2. `middleware.Logger(logger)` — 请求日志
3. `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` / `postgresql`
- `mysql`(默认)