Files
silk/变更记录.md
T

945 lines
52 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.
# 变更记录
## 2026-08-12 知识库初始版开发与部署
### 变更背景
依据《后续工作计划》#10,先跳过 YOLO 训练相关项(物理机问题),优先交付独立、见效快的模块「知识库初始版」(蚕病百科 + AI 结果解读)。蚕病内容全部取自规格书 3.6、3.1.4(风险分级阈值)与附录 11.1,不自行编造。
### 后端变更(server-go
| 文件 | 变更 |
|------|------|
| `internal/model/knowledge.go`(新增) | `Disease` / `KnowledgeArticle` 模型(`diseases``knowledge_articles` 表) |
| `internal/model/knowledge_seed.go`(新增) | 种子数据:9 个蚕病条目 + 1 篇 AI 结果解读,启动幂等写入 |
| `internal/model/knowledge_seed_test.go`(新增) | seed 数据完整性单元测试(TDD 红→绿) |
| `internal/handler/knowledge.go`(新增) | 知识库 CRUD API:`/api/v1/knowledge/diseases``/api/v1/knowledge/articles` |
| `internal/model/permission_seed.go` | 新增权限 `knowledge:read` / `knowledge:write` 及角色映射 |
| `internal/database/db.go` | AutoMigrate 注册两表 + `seedKnowledge` 幂等种子 |
| `cmd/server/main.go` | 注册知识库路由 |
### Web 变更(web
- `src/dal/knowledge.ts`(新增):知识库 API 封装与类型
- `src/pages/Knowledge.tsx`(新增):蚕病百科 + AI 结果解读双 Tab,可增删改(按权限)
- `src/router.tsx``src/layout/BasicLayout.tsx`:新增 `/knowledge` 路由与「知识库」菜单
### 小程序变更(miniapp
- `src/api/knowledge.ts`(新增)、`src/types/index.ts`:知识库接口与类型
- `src/pages/knowledge/`(新增):知识库列表页 + 详情页
- `src/pages/settings/index.tsx`:设置页新增「知识库」入口
- `src/app.config.ts`:注册新页面
### 部署(开发服务器 100.83.103.1
- 部署内容:Go 后端二进制 `server-go-linux`(含知识库 API 与两表迁移)+ Web `dist`(知识库页面)
- 回滚点:数据库 `pg_dump` `/home/pan/backups/silk-20260812-1111.dump`;旧二进制 `/home/pan/backups/rollback-20260812/server-go-linux.bak`(原位另有 `server-go-linux.old-20260812`);旧 dist `/home/pan/backups/rollback-20260812/web-dist-20260812.tar.gz`(原位另有 `dist.old-20260812`);完整回滚命令见服务器 `/home/pan/backups/rollback-20260812/README.md`
- 验证结果:`/api/v1/health` 200;登录 admin → `GET /api/v1/knowledge/diseases` 返回 9 条;`GET /api/v1/knowledge/articles?kind=ai_guide` 返回 1 条;`http://localhost:5174/` 200DB `diseases=9``knowledge_articles=1`
### 风险与备注
- `start.sh``WVP_API_BASE=18978`,与当前 WVP 实际端口 18080 不一致(既有问题,未随本次修改)
- 服务器 PostgreSQL 为 14(部署指南记录为 16,以实际为准)
- 服务器无 silk 主仓库 git(仅有 wvp-src),代码回滚以文件备份 + 本地 commit 为准
- 前端测试基建(Vitest)未配置,属计划 #27,UI 验证以 lint + 构建为准
### 补充:LAMP 操作教程(P0
- `server-go/internal/model/knowledge_seed.go`:新增 `lamp_guide` 种子文章(原理、适用场景、设备清单、5 步操作流程、结果判读、各病种检测靶标现状),内容取自规格书 3.3.2.1
- `server-go/internal/model/knowledge_seed_test.go`:新增 LAMP 教程存在性测试(TDD 红→绿)
- `miniapp/src/api/knowledge.ts``miniapp/src/pages/knowledge/index.tsx`:知识库列表增加「LAMP 教程」入口
- `miniapp/src/pages/knowledge/detail/index.tsx`:修复非 disease 类型(如 lamp_guide)误走病种详情接口的问题
- Web 无需改动(文章 Tab 已支持 `lamp_guide` 类型)
- 二次部署:后端二进制更新(sha256 `742ffb54…`),验证 `ai_guide=1``lamp_guide=1``GET /api/v1/knowledge/articles?kind=lamp_guide` 返回 1 条;回滚点见服务器 `/home/pan/backups/rollback-20260812/README.md`(原位旧二进制 `server-go-linux.old-20260812-2`
### 补充:症状图谱图片上传(P0)
- `server-go/internal/handler/knowledge_image.go`(新增):`POST /api/v1/knowledge/images`multipart 字段 `file`,限 jpg/jpeg/png/webp、≤5MB),对象键 `knowledge/<日期>/<随机>.ext`,权限 `knowledge:write`
- `server-go/internal/handler/knowledge_image_test.go`(新增):校验/类型/键生成 5 个单元测试(TDD 红→绿)
- `server-go/internal/service/s3.go`:新增 `EnsureBucket`(不存在则创建 + bucket 公开读)、`UploadImage`(对象公开读,Ceph 需对象级 ACL)、`Endpoint`
- `server-go/internal/config/config.go`:新增 `S3_BUCKET_IMAGES`(默认 `silk-images`
- Web:病种表单增加「症状图谱图片」上传 + 预览(antd Upload 自定义请求)
- 小程序:病种详情页顶部展示症状图(有 `imageUrl` 时)
- 三次部署(含 ACL 修复):上传 1x1 PNG 冒烟通过——URL 匿名 GET 200(`image/png`),病种 `imageUrl` 绑定成功(测试数据已清空);回滚点见服务器 `/home/pan/backups/rollback-20260812/README.md`(原位旧二进制 `server-go-linux.old-20260812-5`
- 说明:开发环境图片存 Ceph `silk-images` 桶(公开读),生产切云 OSS/COS 时由桶策略或 CDN 承担公开访问,代码仅需改配置
## 2026-07-17 安全与界面优化整改
### 变更背景
依据 `e:\silk\优化.md` 中的安全测试结果与界面优化建议,对前后端进行整改。经核验,绝大多数问题属实,部分(如仪表盘英文字段名)在当前代码中已不存在。
### 后端变更(server-go
| 类别 | 文件 | 变更 |
|------|------|------|
| 安全头 | `internal/middleware/security.go`(新增) | 统一补齐 `X-Content-Type-Options`/`X-Frame-Options`/`Referrer-Policy`/`Content-Security-Policy`/`Strict-Transport-Security` |
| 登录限流 | `internal/middleware/ratelimit.go`(新增) | 按 IP+用户名维度计数,连续失败 5 次锁定 15 分钟,返回 429 |
| 令牌黑名单 | `internal/middleware/token_blacklist.go`(新增) | 登出令牌加入内存黑名单(按 jti),鉴权中间件校验 |
| 鉴权中间件 | `internal/middleware/auth.go` | 校验黑名单;拒绝 refresh 令牌访问业务接口;白名单新增 `/auth/refresh` |
| 认证接口 | `internal/handler/auth.go` | 新增 `/auth/logout``/auth/refresh``/auth/change-password`;登录失败/成功联动限流;错误提示统一中文("用户名或密码错误");令牌加入 jti 与 tokenType |
| 配置 | `internal/config/config.go` | `JWT_EXPIRES_IN` 默认值由 `7d` 改为 `2h`(刷新令牌 7d,访问令牌 2h) |
| 房间模型 | `internal/model/models.go` | `Room` 新增 `Capacity``Stage` 字段(修复蚕房管理表单字段不持久化问题) |
| 路由装配 | `cmd/server/main.go` | 注册 `SecurityHeaders` 中间件 |
### 前端变更(web
| 类别 | 文件 | 变更 |
|------|------|------|
| 令牌刷新 | `src/api/http.ts` | 响应拦截器:401 时用 refreshToken 静默换新并重试;统一读取 `error` 字段为错误提示 |
| 认证服务 | `src/services/auth.ts``src/dal/auth.ts` | 登录存储 refreshToken;登出调用后端 `/auth/logout` 吊销令牌;新增 `changePassword` |
| 登录页 | `src/pages/Login.tsx` | 密码框补 `autocomplete="current-password"`,用户名框补 `autocomplete="username"`;登录失败显示中文错误 |
| 修改密码 | `src/layout/BasicLayout.tsx` | 头像下拉菜单新增"修改密码"入口与弹窗,修改成功后强制重新登录;登出改为调用后端 |
| 表格 UUID 列 | `Silkworm/Device/Threshold/Alert/Log.tsx` | 隐藏 ID 列,改用"序号"列(`indexBorder` |
| 控制日志 | `src/pages/Log.tsx` | 移除冗余"设备ID"列 |
| 告警中心 | `src/pages/Alert.tsx` | 移除占位按钮"保留告警视频入口" |
| 设备控制 | `src/pages/Control.tsx` | 离线设备电压/功率/电量统一显示 `--` 并加"数据为历史缓存,已隐藏"提示 |
| 仪表盘 | `src/pages/Dashboard.tsx``src/dal/dashboard.ts` | "蚕房状态概览"文案由"在线/离线"改为"运行中/停用",避免与设备在线状态混淆 |
| 蚕房表单 | `src/pages/Silkworm.tsx` | 位置/容量/阶段改为必填,配合后端新字段持久化 |
| 404 页 | `src/pages/NotFound.tsx`(新增)、`src/router.tsx` | 未知路由展示"页面不存在"提示,不再静默跳转仪表盘 |
### 未在本次整改范围
- **明文 HTTP 传输凭据(中风险#6)**:需在网关层终止 TLS,属基础设施变更,未在代码中处理。
- **令牌改用 HttpOnly Cookie(高风险#3)**:涉及前后端鉴权架构重写与 CSRF 防护,本次以"缩短有效期 + 服务端登出黑名单 + 刷新令牌"缓解。
- **仪表盘英文字段名(UI#1)**:核验当前 Web 仪表盘已使用中文标签(温度/湿度/CO₂/光照),该问题在 Web 端不成立。
### 验证
- 后端:`go build ./...``go vet ./...` 均通过
- 前端:`npm run build` 通过(tsc + vite build
### 部署提示
- 后端重启后内存黑名单清空(令牌自然过期兜底);新访问令牌有效期缩短为 2h,旧 7d 令牌在过期前仍有效
- 数据库需执行 GORM AutoMigrate(启动时自动)以新增 `rooms.capacity``rooms.stage`
## 2026-07-04 视频录制改为不落地方案
### 变更背景
原方案中,视频录制分三步:ZLMediaKit 录制到容器本地磁盘 → cron 脚本每分钟扫描 → 下载到本机再上传到 Ceph S3。录制文件经过两次网络传输,效率低,且容器磁盘占用持续增长。
### 变更内容
将录制流程改为**不落地模式**ZLMediaKit 做 RTP→fMP4 协议转换,Python 服务直接 HTTP 拉流,内存中缓冲到 5MB 分块即触发 S3 Multipart Upload,全程不写入本地磁盘。
### 架构变更
**变更前(落地方案):**
```
EasyGBD → ZLMediaKit(RTP→MP4, 录制到容器磁盘) → cron脚本(docker cp到/tmp) → boto3上传Ceph S3
```
**变更后(不落地方案):**
```
EasyGBD → ZLMediaKit(RTP→fMP4, 仅协议转换) → stream_to_s3.py(HTTP拉流→内存缓冲5MB→S3 Multipart Upload) → Ceph S3
```
### 新增文件
| 文件 | 位置 | 说明 |
|------|------|------|
| `stream_to_s3.py` | 100.83.103.1:/home/pan/ | Python 不落地录制服务,监听 9090 端口 |
| `stream_to_s3.py` | e:\silk\wvp\ | 本地副本 |
### 修改文件
| 文件 | 变更内容 |
|------|----------|
| `server/src/modules/video/recording.service.ts` | 移除 `startRecord`/`stopRecord`/`archiveRecording` 调用,改为通过 HTTP 调用 Python 服务的 `/record/start``/record/stop` 接口 |
| `server/.env` | 新增 `RECORDER_API_BASE=http://100.83.103.1:9090` |
### 删除/清理
| 清理项 | 说明 |
|--------|------|
| ZLMediaKit 容器内录制文件 | 清理 2.1GB 旧录制文件 |
| /tmp 临时 MP4 文件 | 清理约 2GB 临时文件 |
| cron `archive_recordings.py` 任务 | 不再需要,已从 crontab 移除 |
### 环境配置
| 配置项 | 值 |
|--------|-----|
| `RECORDER_API_BASE` | `http://100.83.103.1:9090` |
| Windows portproxy | `9090` 端口转发到 WSL2 `172.18.127.247:9090` |
| Python 服务启动 | `nohup python3 /home/pan/stream_to_s3.py > /home/pan/stream_to_s3.log 2>&1 &` |
### 工作流程
1. **自动检测**:Python 服务每 60 秒检查在线摄像头(通过 NestJS API)
2. **开始录制**HTTP 从 ZLMediaKit 拉 fMP4 流 → 内存缓冲 → 每 5MB 调用 `UploadPart`
3. **分段归档**:每 5 分钟 `CompleteMultipartUpload` 收尾 → 调用 NestJS API 建库记录 → 自动开始下一段
4. **手动控制**:前端「摄像头管理」Tab 或 NestJS API 可手动开始/停止录制
### 验证结果
- 24 个旧落地文件在 18 秒内全部归档完成
- 不落地模式首个分段:17:06:42 开始,311 秒,152MB29 个 UploadPart
- 上传速度:约 5MB/9 秒(localhost 传输)
- 数据库记录正确创建,前端「历史回看」可查询和播放
## 2026-07-10 新增移动端客户端(React Native APP + Taro 小程序)
### 变更背景
根据 spec.md「客户端」章节要求,系统需提供 Web 管理后台、微信小程序、移动 APP 三端客户端覆盖。此前仅有 Web 管理后台(`web/`)和 Go 后端(`server-go/`),缺少移动端 APP 和微信小程序。
### 变更内容
新增两个移动端客户端工程,完整对接现有 Go 后端 API(58 个 REST 端点 + WebSocket),实现登录认证、实时监测、设备控制、告警管理、阈值配置、视频监控等全功能。
### 新增工程
#### 1. React Native 移动 APP`e:\silk\app\`
**技术栈**React Native 0.74.5 + TypeScript + React Navigation 6 + Zustand + Axios + React Native Paper + react-native-video
**页面(11 个)**
| 页面 | 文件 | 功能 |
|------|------|------|
| 登录 | `src/screens/LoginScreen.tsx` | 账号密码登录,预填 admin/silk@123 |
| 仪表盘 | `src/screens/DashboardScreen.tsx` | 统计卡片 + 实时指标 + 趋势图 + 最近告警,30s 轮询 |
| 蚕房列表 | `src/screens/RoomsScreen.tsx` | 蚕房搜索、状态徽章 |
| 蚕房详情 | `src/screens/RoomDetailScreen.tsx` | 实时指标 + 趋势图 + 设备列表,30s 轮询 |
| 设备列表 | `src/screens/DevicesScreen.tsx` | 设备搜索、在线/离线状态、设备类型图标 |
| 设备控制 | `src/screens/DeviceControlScreen.tsx` | 开关、模式、风速、温度设置 |
| 告警中心 | `src/screens/AlertsScreen.tsx` | 未处理/全部筛选、确认告警、查看片段 |
| 阈值管理 | `src/screens/ThresholdsScreen.tsx` | 阈值 CRUD、启用/禁用开关 |
| 视频监控 | `src/screens/VideoScreen.tsx` | 摄像头列表 + 录像片段 Tab 切换 |
| 视频播放 | `src/screens/VideoPlayerScreen.tsx` | HLS 直播流播放 |
| 设置 | `src/screens/SettingsScreen.tsx` | 用户信息、API 地址配置、退出登录 |
**API 层**`src/api/`):client.ts、auth.ts、rooms.ts、devices.ts、telemetry.ts、alarms.ts、thresholds.ts、video.ts
**状态管理**`src/store/authStore.ts`Zustand + AsyncStorage 持久化)、`src/store/appStore.ts`
**工具**`src/utils/ws.ts`(WebSocket 管理器,自动重连)、`src/utils/format.ts`(格式化)
**组件**`src/components/StatCard.tsx``src/components/MiniChart.tsx`(纯 RN 组件实现的柱状图)
#### 2. Taro 微信小程序(`e:\silk\miniapp\`
**技术栈**Taro 4.1.9 + React 18 + TypeScript + SCSS + Zustand,一套代码输出微信小程序和 H5
**页面(10 个)**
| 页面 | 路径 | 功能 |
|------|------|------|
| 登录 | `pages/login/index` | 账号密码登录 |
| 仪表盘 | `pages/dashboard/index` | 概览卡片 + 蚕房选择 + 实时遥测 + 最近告警,30s 轮询 + WebSocket 推送 |
| 蚕房列表 | `pages/rooms/index` | 蚕房卡片列表 |
| 蚕房详情 | `pages/rooms/detail/index` | 蚕房信息 + 设备遥测 |
| 设备 | `pages/devices/index` | 设备列表 + 筛选 + 控制按钮 |
| 告警 | `pages/alerts/index` | 告警列表 + 确认 + 查看录像 |
| 阈值管理 | `pages/thresholds/index` | 阈值 CRUD + 弹窗表单 |
| 视频监控 | `pages/video/index` | 摄像头列表 + 录像回放 |
| 视频播放 | `pages/video/player/index` | 全屏视频播放,HLS/FLV/WebRTC 切换 |
| 设置 | `pages/settings/index` | 用户信息 + 退出登录 |
**TabBar**:5 个底部标签(仪表盘/蚕房/设备/告警/视频),含 10 个 SVG 图标
**API 层**`src/api/request.ts`Taro.request 封装,JWT 拦截器)、auth.ts、rooms.ts、devices.ts、telemetry.ts、alarms.ts、thresholds.ts、video.ts
### 构建与部署配置
#### React Native APP 构建配置
| 配置项 | 值 | 说明 |
|--------|-----|------|
| Gradle 镜像 | `https://mirrors.cloud.tencent.com/gradle/gradle-8.6-all.zip` | 解决 Gradle 下载超时 |
| Maven 镜像 | `https://maven.aliyun.com/repository/google` / `public` | 加速依赖下载 |
| JDK | `E:\openjdk-17.0.2_windows-x64_bin\jdk-17.0.2` | JDK 17AGP 要求) |
| NDK | `26.3.11579264` | 原 `26.1.10909125` 损坏,替换为可用版本 |
| media3 版本 | 强制 `1.4.1` | `react-native-video` 依赖的 media3 1.8.0 要求 SDK 35,降级兼容 SDK 34 |
| API 地址 | `http://100.83.103.1:3000/api/v1` | 后端 Tailscale IP |
| WebSocket | `ws://100.83.103.1:3000/ws` | 实时推送 |
| Android 包名 | `com.silkmonitor` | 显示名「蚕房监控」 |
#### Taro 小程序构建配置
| 配置项 | 值 | 说明 |
|--------|-----|------|
| H5 devServer proxy | `/api` -> `http://localhost:3000` | H5 调试时代理转发 |
| 小程序 API 地址 | `http://100.83.103.1:3000/api/v1` | 直连后端 |
| `project.config.json` | `urlCheck: false` | 允许 localhost 开发请求 |
### 共同特性
- JWT 认证,401 自动清除 Token 并跳转登录页
- WebSocket 实时推送(遥测数据、告警事件)
- 8 个 API 模块完整对接后端
- 设备远程控制(`POST /control/send`
- 视频流播放(HLS
- 中文 UI
- 30 秒轮询 + WebSocket 双重实时数据更新
### 验证结果
- React Native APP`tsc --noEmit` 零错误通过;Android APK 构建成功并安装到物理设备(OXF-AN10, Android 12),登录成功
- Taro 小程序:H5 和 weapp 均编译成功(`webpack 5.78.0 compiled successfully`
## 2026-07-14 物联网设备接入 + 录制状态修复 + 前端新增能耗管理和设备控制页面
### 变更背景
1. WVP 迁移到物理机 Docker 后,设备离线时录制流中断但前端一直显示"录制中",再次点击"开始录制"返回 503
2. VerneMQ 服务未设置开机自启,物理机重启后 MQTT 不可用
3. 需要接入 GSTMB1 温湿度传感器和 GSPE1B 智能插座
4. 前端缺少能耗管理和设备控制入口
### 变更内容
#### 1. 录制状态自动清理(recorder-go -> Go 后端通知)
录制流因设备离线等原因中断时,recorder-go 通知 Go 后端自动清理录制状态和 WVP play session,前端无需手动点击停止。
**修改文件:**
| 文件 | 变更内容 |
|------|----------|
| `server-go/internal/handler/video_record.go` | 新增内部接口 `POST /video/recordings/internal/end`,接收 recorder-go 的录制结束通知,清理 `activeRecordings` 内存表并调用 `media.StopPlay()` |
| `wvp/recorder-go/main.go` | 录制线程退出时(defer)调用 `notifyRecordingEnd()` 通知 Go 后端 |
| `server-go/internal/middleware/auth.go` | 白名单添加 `/video/recordings/internal/end` |
#### 2. VerneMQ MQTT 服务
- 启动 VerneMQ 并设置开机自启:`sudo systemctl enable --now vernemq`
- 修改监听地址为 `0.0.0.0:1883`(原绑定 `127.0.0.1`
- 创建用户 `pan/pan`
#### 3. GSTMB1 温湿度传感器适配
**修改文件:**
| 文件 | 变更内容 |
|------|----------|
| `server-go/internal/service/mqtt.go` | `handleMessage` 增加 GSTMB1 消息识别:用 `mac` 字段作为 deviceKey`source=="command"` 命令响应处理;定时上报提取 temperature/humidity/signal 等字段入库;新增 `deviceTopics` 内存表和 `PublishToDevice` 方法 |
| `server-go/internal/handler/control.go` | 新增 API`POST /devices/:id/gstmb1/command``/info``/restart``/interval` |
| `server-go/cmd/server/main.go` | 注入 `DeviceCommander` 到 control handler |
#### 4. GSPE1B 智能插座适配
**发现的问题:** 插座的 MQTT 主题方向与常规配置相反--设备发布到 `/down/cmd`,订阅 `/up/telemetry`
**修改文件:**
| 文件 | 变更内容 |
|------|----------|
| `server-go/internal/service/mqtt.go` | 增加订阅 `silk/+/+/+/down/cmd``PublishToDevice` 自动检测主题方向,反配设备发送到 `/up/telemetry`;指标提取增加 voltage/current/power/energy/key/onState;命令响应处理增加 `info-statistic``controller-event` |
| `server-go/internal/handler/control.go` | 新增插座 API`POST /devices/:id/plug/on``/off``/statistic``/info` |
| `server-go/internal/handler/device.go` | `listDevices` 增加 `kind` 查询参数过滤 |
**数据库变更:**
| 设备 | device_key | kind | 说明 |
|------|-----------|------|------|
| 传感器1# | `28562f8c35e8` | `sensor` | GSTMB1 温湿度传感器 |
| 智能插座 | `e8f60a2f01e8` | `actuator` | GSPE1B 智能插座 |
#### 5. 前端新增页面
**新增文件:**
| 文件 | 说明 |
|------|------|
| `web/src/pages/Energy.tsx` | 能耗管理页面:4 个数据卡片(电压/电流/功率/电量)+ 4 个趋势图表(ECharts),支持设备选择和时间范围(1h/6h/24h),15 秒自动刷新 |
| `web/src/pages/Control.tsx` | 设备控制页面:卡片式展示控制器设备,大开关按钮控制通/断电,实时显示电压/功率/电量,查询信息和电量按钮,15 秒自动刷新。本地状态管理提供即时视觉反馈 |
**修改文件:**
| 文件 | 变更内容 |
|------|----------|
| `web/src/layout/BasicLayout.tsx` | 原"设备控制"改名"设备管理",新增"能耗管理"(`/energy`)和"设备控制"(`/control`)菜单项 |
| `web/src/router.tsx` | 新增 `/energy``/control` 路由 |
| `web/src/pages/Device.tsx` | "类型"列改为"设备类型",用彩色 Tag 显示中文标签(传感器=蓝色、控制器=橙色、网关=紫色);表单选项"执行器"改为"控制器" |
#### 6. 遥测 API 修复
**修改文件:** `server-go/internal/handler/telemetry.go`
| 端点 | 变更前 | 变更后 |
|------|--------|--------|
| `GET /telemetry/:deviceKey/latest` | 返回单条最新记录(单个对象) | 返回所有指标的最新值数组(`SELECT DISTINCT ON (metric)` |
| `GET /telemetry/:deviceKey/:metric/history` | 返回 `{bucket, avg, min, max}`,依赖 TimescaleDB | 返回 `{time, value}`TimescaleDB 不可用时降级为原始数据查询 |
#### 7. 前端静态服务器缓存策略修复
**修改文件:** `web/server.cjs`
- `index.html``Cache-Control: no-cache, no-store, must-revalidate`(原为 `max-age=86400` 导致更新后浏览器不刷新)
- 静态资源(带 hash 文件名):`Cache-Control: public, max-age=604800`7 天)
- 部署路径修正为 `/home/pan/silk/web/dist/`
### 环境配置
| 配置项 | 值 | 说明 |
|--------|-----|------|
| VerneMQ 监听 | `0.0.0.0:1883` | 物理机直连,不再经过 WSL2 portproxy |
| VerneMQ 用户 | `pan/pan` | MQTT 认证 |
| VerneMQ 自启 | `systemctl enable vernemq` | 物理机重启后自动启动 |
### 新增文档
| 文件 | 说明 |
|------|------|
| `物联网设备接口和数据格式.md` | MQTT Topic 结构、GSTMB1 传感器、GSPE1B 智能插座、通用数据格式、后端处理逻辑 |
### 验证结果
- GSTMB1 传感器:温度 28.24°C、湿度 57.83% 已入库,设备在线
- GSPE1B 智能插座:电压 226.2V、电流 0.327A、功率 66.8W、电量 0.025kWh 已入库,通断控制正常
- `/control` 页面:只显示控制器设备(传感器不出现),开关控制有即时反馈
- `/energy` 页面:数据卡片和趋势图表正常显示
- `GET /telemetry/:deviceKey/latest` 返回所有指标数组
- `GET /telemetry/:deviceKey/:metric/history` 返回 `{time, value}[]` 格式
## 2026-07-15 红外控制器(GSCU1B-4G)心跳机制
### 变更背景
GSCU1B-4G 智能红外控制器是 4G LTE 短连接、命令驱动型设备,**无定时上报数据功能**:
- 不主动上报任何遥测数据
- 仅在收到后端命令时才回复(info/learn/emit 响应)
- 连接超时会自动断开 MQTT
原离线检测逻辑对所有设备一视同仁:超过 5 分钟未收到数据即标记离线。这导致红外设备一旦超过 5 分钟未操作就被标记为"离线",前端按钮被禁用,用户无法下发命令唤醒设备(形成死锁)。
### 变更内容
为红外控制器引入**主动心跳机制**:超时未收到数据时,先下发 `info` 命令查询设备状态,给 5 分钟宽限期等待响应;若心跳后仍未收到响应,才标记离线。
### 工作机制
| 设备类型 | 超时 5 分钟后的行为 |
|---------|---------------------|
| 普通设备(GSTMB1/GSPE1B/GSCW1M | 直接标记离线(原逻辑不变) |
| 红外设备(GSCU1B-4G)首次超时 | 下发 `info` 命令作心跳,**不标记离线** |
| 红外设备心跳后 5 分钟仍未响应 | 标记离线,清理心跳记录 |
### 流程
```
设备正常运行
5 分钟无数据上报
[红外设备] 下发 info 命令 → 设备响应 → last_seen 更新 → 重新计数
↓ (5 分钟内未收到响应)
标记离线
用户重启设备
下次 checkOffline 触发心跳 → 设备响应 → 自动恢复在线
```
### 设备识别方式
通过以下任一条件识别红外控制器:
- `model` 字段为 `"GSCU1B-4G"`
- `name` 字段包含"红外"
### 修改文件
| 文件 | 变更内容 |
|------|----------|
| `server-go/internal/service/mqtt.go` | `MQTTService` 新增 `irHeartbeat map[string]time.Time` 字段;新增 `isIRController()` 函数识别红外设备;`checkOffline()` 对红外设备做心跳处理:超时先下发 info 命令,5 分钟内未响应才标记离线 |
### 心跳命令
```json
{"type":"info","messageId":"<timestamp_ms>"}
```
与前端"查询信息"按钮下发的命令相同,设备响应后 `handleMessage` 自动更新 `last_seen` 和在线状态。
### 预期效果
1. **设备真正离线时**:约 10 分钟后(5 分钟超时 + 5 分钟心跳宽限)才标记离线
2. **设备重启后**:最多 5 分钟内自动恢复在线(下次 checkOffline 触发心跳)
3. **用户操作不变**:前端"查询信息"按钮仍可手动触发 info,与心跳互不影响
4. **checkOffline 频率不变**:仍为每 60 秒运行一次,心跳通过 `irHeartbeat` 内存表去重,约 5 分钟下发一次
### 部署状态
- 后端已交叉编译并上传到 `/home/pan/silk/server-go/silk-server-go-linux`
- 服务已重启,端口 3000 正常监听
- 日志显示 MQTT 已订阅 `silk/+/+/+/up/telemetry``silk/+/+/+/down/cmd`
## 2026-07-15 红外控制器空调遥控面板
### 变更背景
`/ir` 页面红外控制器卡片只提供"指定编号发射/学习"的通用功能,用户需要记忆每个编号对应什么功能,操作不便。空调遥控是红外控制器的主要使用场景,需要一个类似真实空调遥控器的可视化面板,让用户直接点击"关机/开机/制冷/制热/除湿"按钮操作,并支持一键顺序学习。
### 变更内容
#### 1. 前端空调遥控面板(`web/src/pages/IRControl.tsx`
**布局重构**:每张红外设备卡片改为左右分栏
- 左侧:保留原有发射/学习/查询信息/擦除全部等通用功能
- 右侧:新增空调遥控面板,分为三个区域
**按钮分区设计**
```
┌──────────────────────────────┐
│ 开始学习 / 取消学习 │ ← 顺序学习按钮(单独一行)
├──────────────┬───────────────┤
│ 关机(1) │ 开机(2) │ ← 空调控制按钮(顺序学习)
│ 制冷(6) │ 制热(7) │
│ 除湿(5) │ │
├────┬────┬────┬────┤
│23度│25度│27度│29度│ ← 温度按钮(单独学习)
│(3) │(4) │(8) │(9) │
└────┴────┴────┴────┘
```
**空调控制按钮**5 个,顺序学习):
- 关机(红外码 1
- 开机(红外码 2
- 制冷(红外码 6
- 制热(红外码 7
- 除湿(红外码 5
**温度按钮**4 个,单独学习):
- 23 度(红外码 3
- 25 度(红外码 4
- 27 度(红外码 8
- 29 度(红外码 9
#### 2. 学习模式
**顺序学习**5 个空调按钮):
1. 点击"开始学习" → 下发 `learn no=1`,关机按钮闪烁
2. 用户用空调遥控器对准红外控制器按下关机键
3. 听到"嘀"声 → 关机按钮变绿,自动下发下一个 `learn` 命令
4. 按顺序学习:关机 → 开机 → 制冷 → 制热 → 除湿
5. 全部完成显示"全部学习完成"
**单独学习**4 个温度按钮):
- 点击未学习的温度按钮 → 该按钮闪烁,开始单独学习
- 收到遥控信号后 → 按钮变绿,学习完成
- 点击已学习的按钮 → 发射红外码
**关键技术细节**
- 设备学习响应为两步:先返回 `success=false`(进入学习模式),再返回 `success=true`(学习成功)
- 前端 `success=false` 时继续轮询,不当作失败处理
- 使用 `startTime` 过滤旧结果,避免轮询匹配到上一个编号的结果
- 设备学习成功时返回 `no=0`(不是发送的编号),匹配条件兼容 `no=0`
- `learningDeviceRef` 使用 `useRef` 跟踪当前学习状态,规避轮询中的闭包陷阱
#### 3. 按钮状态
- **未学习**(默认灰色):空调按钮点击发射红外码;温度按钮点击开始单独学习
- **正在学习**(黄色 CSS 闪烁动画,`@keyframes ir-blink`):等待遥控器信号
- **已学习**(绿色边框+绿色文字):空调按钮点击发射;温度按钮点击发射
#### 4. 后端修复:已学习红外码编号记录(`server-go/internal/service/mqtt.go`
**问题**:设备学习成功时返回 `no=0`,导致后端 `irLearnedCodes``no > 0` 条件未记录编号。
**修复**
- 新增 `pendingLearnNo map[string]int` 记录发送 learn 命令时的编号
- `PublishToDevice` 检测 learn 命令时记录编号到 `pendingLearnNo`
- `handleMessage` 收到 learn 成功响应且 `no=0` 时,从 `pendingLearnNo` 还原编号,正确记录到 `irLearnedCodes`
#### 5. 擦除按钮修复
**问题**:擦除按钮使用 `Modal.confirm` 确认对话框,用户可能没注意到需要二次点击"确认擦除",且设备对 erase 命令可能不返回响应,导致"无反馈"。
**修复**
- 改为 `Popconfirm` 气泡确认(更直观,出现在按钮旁边)
- 点击确认后立即清空本地 `learnedCodes` 列表
- 新增 `pollEraseResult` 函数:短时间轮询(10 秒,5 次)确认擦除结果
- 超时显示"设备未响应擦除结果,本地红外码列表已清空"
- `pollIRResult``success=false` 时继续轮询,不当作失败(设备先返回进入擦除模式,再返回成功)
#### 6. 前端按钮 disabled 逻辑调整
- 空调面板按钮:移除 `disabled={!online}` 限制(红外设备为短连接,可能显示"离线"但实际可下发命令)
- 左侧通用按钮:保留原逻辑
### 涉及文件
| 文件 | 变更内容 |
|------|----------|
| `web/src/pages/IRControl.tsx` | 新增空调遥控面板、顺序学习、单独学习、按钮闪烁动画;`AIRCON_BUTTONS`/`TEMP_BUTTONS`/`LEARN_SEQUENCE`/`ALL_BUTTONS` 常量定义;`pollSequentialLearn`/`startSequentialLearn`/`cancelSequentialLearn`/`learnSingleTemp`/`emitAircon`/`pollEraseResult` 函数;`learningDeviceRef` 跟踪学习状态 |
| `server-go/internal/service/mqtt.go` | 新增 `pendingLearnNo` 内存表;`PublishToDevice` 记录 learn 编号;`handleMessage` 还原 `no=0` 时的学习编号到 `irLearnedCodes` |
### 部署状态
- 前端构建并上传到 `/home/pan/silk/web/dist/`5174 端口已重启
- 后端交叉编译并上传到 `/home/pan/silk/server-go/silk-server-go-linux`3000 端口已重启
### 验证结果
- 空调遥控面板按钮布局正确,5 个空调按钮 + 4 个温度按钮 + 开始学习按钮
- 顺序学习:关机 → 开机 → 制冷 → 制热 → 除湿,每个按钮学习成功后自动进入下一个
- 单独学习:点击温度按钮独立学习,不干扰顺序学习流程
- 擦除按钮使用 `Popconfirm`,确认后立即清空本地列表
- 已学习按钮显示绿色边框,点击可发射红外码
## 2026-07-18 RBAC 权限改造 + 视频播放与录制状态修复
### 变更背景
1. 系统原为多用户双角色(user/admin)架构,非严格 RBAC,需改造为多角色 + 权限码控制
2. RBAC 改造后用户反馈 `/videos` 实时监控 FLV 播放失败、历史回看查不到录像
3. 用户对录制状态有疑问,前端"摄像头管理"显示状态与实际录制情况不一致
### 变更内容
#### 1. RBAC 权限系统改造(前后端)
引入 4 个角色(admin/operator/viewer/farmer+ 15 个权限码,前端实现路由守卫+菜单+按钮级权限控制。
**后端新增文件:**
| 文件 | 说明 |
|------|------|
| `server-go/internal/model/permission.go` | 定义 Permission、RolePermission 模型和 4 个角色常量 |
| `server-go/internal/model/permission_seed.go` | 15 个权限码和角色-权限映射种子数据 |
| `server-go/internal/middleware/permission.go` | RequirePermission 中间件,带 5 分钟内存缓存,admin 自动放行 |
| `server-go/internal/handler/permission.go` | `GET /permissions``GET /roles` 查询 API |
**后端修改文件:**
| 文件 | 变更内容 |
|------|----------|
| `server-go/internal/database/db.go` | AutoMigrate 加入 Permission、RolePermission 表;新增 `seedPermissions()` 初始化种子数据 |
| `server-go/internal/handler/auth.go` | meHandler 与 buildLoginPayload 返回 permissions 列表;注册默认角色从 `user` 改为 `viewer`;新增 `getUserPermissionCodes()` |
| `server-go/internal/handler/user.go` | AdminMiddleware → `RequirePermission("user:manage")`;新增角色有效性校验 |
| `server-go/internal/handler/audit.go` | AdminMiddleware → `RequirePermission("audit:read")` |
| `server-go/internal/handler/{room,device,sensor,threshold,alarm,alarm_clip,control,video_camera,video_clip,video_record,telemetry,notification,storage}.go` | 各路由添加对应权限中间件 |
| `server-go/cmd/server/main.go` | 移除 admin 路由组;新增 RegisterPermissionRoutes 注册 |
**前端新增/修改文件:**
| 文件 | 说明 |
|------|------|
| `web/src/components/HasPermission.tsx`(新增) | 按钮级权限组件,根据 `authService.hasPermission(permission)` 控制显隐 |
| `web/src/dal/auth.ts` | LoginResp.user 加 `permissions` 字段;新增 `MeResp` 接口类型 |
| `web/src/services/auth.ts` | 新增 USER_KEY 存储、`getUser()``getPermissions()``hasPermission()``refreshUser()` |
| `web/src/router.tsx` | 新增 `RequirePermission` 路由守卫组件,每个路由配置对应权限码 |
| `web/src/layout/BasicLayout.tsx` | menuData 每项加 `permission` 字段;菜单渲染按权限过滤;头像显示真实用户名和角色中文名 |
**权限矩阵:**
| 角色 | 权限码 |
|------|--------|
| admin | 自动放行所有接口 |
| operator | dashboard:view, room:read/write, device:read/control, threshold:read/write, alarm:read/ack, video:read/record, energy:view, log:read, audit:read |
| viewer | dashboard:view, room:read, device:read, threshold:read, alarm:read, video:read, energy:view, log:read |
| farmer | dashboard:view, room:read, device:read/control, threshold:read, alarm:read/ack, video:read, energy:view |
#### 2. ZLMediaKit 流媒体配置修复
通过 ZLM `setServerConfig` API 修正运行时配置(WVP auto-config 未正确应用 `ZLM_HOOK_HOST` 环境变量):
| 配置项 | 修改前 | 修改后 | 说明 |
|--------|--------|--------|------|
| `general.streamNoneReaderDelayMS` | 20000 | 3600000 | 避免无人观看 20 秒后流被关闭 |
| `hook.on_publish`/`on_play` 等 11 项 | `http://127.0.0.1:18080/...` | `http://polaris-wvp:18080/...` | 修正 ZLM 容器内 hook URL127.0.0.1 指向 ZLM 自身而非 WVP |
**新增脚本:** `/home/pan/silk/fix_zlm_hook.py`(服务器留存)— 调用 ZLM API 修复 hook URL 和 streamNoneReaderDelayMS。WVP 重启后若 hook 配置被覆盖回 127.0.0.1,执行 `python3 /home/pan/silk/fix_zlm_hook.py` 即可修复。
**WVP 配置修改:** `stream-on-demand: true → false`(避免 hook 返回"关闭=true"导致流被关闭)
#### 3. 前端 FLV 播放跨域问题修复
**修改文件:** `server-go/internal/service/media.go``fixZlmPort` 函数
**变更前:** 返回绝对路径 `http://100.83.103.1:8081/rtp/xxx.live.flv?...`
**变更后:** 返回相对路径 `/rtp/xxx.live.flv?...`
**原因:** `web/server.cjs` 的 CSP 头 `connect-src 'self' ws: wss:` 只允许同源请求,flv.js 跨域 fetch ZLMediaKit 时被浏览器阻止,报 "NetworkError - Exception"。改为相对路径后,前端浏览器走 `server.cjs` 内置的 `/rtp/` 代理转发到 `127.0.0.1:8081`,避免跨域。
#### 4. 前端历史回看时间范围修复
**修改文件:** `web/src/pages/Video.tsx` PlaybackTab
| 修改点 | 变更前 | 变更后 |
|--------|--------|--------|
| `dateRange` state 类型 | `[Dayjs, Dayjs]` | `[Dayjs, Dayjs] \| null` |
| RangePicker `onChange` 清空 | 不更新 dateRange(保持默认今天) | 设为 `null` |
| `handleSearch` 查询参数 | 硬编码 `to: dayjs().toISOString()` | 仅当 dateRange 非空时传 from/to,为 null 时查全部 |
| `listClips` 调用 | 始终传 from/to | `ClipQuery` 仅在 dateRange 非空时设置 from/to |
**新增类型导入:** `web/src/pages/Video.tsx` 引入 `type ClipQuery`
#### 5. 录制状态查询改进
**修改文件:** `server-go/internal/handler/video_record.go``listActiveRecordings` 函数
**变更前:** 仅读 Go 后端内存 map `activeRecordings`,Go 后端重启后 map 清空,前端显示"开始录制"按钮(即使 recorder-go 仍在实际录制)。
**变更后:** 优先调用 recorder-go 的 `/record/status` 接口获取真实录制状态,并自愈内存 map。recorder-go 不可用时回退到内存 map。
### 环境配置
| 配置项 | 值 | 说明 |
|--------|-----|------|
| `WVP_API_BASE` | `http://localhost:18080` | WVP HTTP API 实际端口(start.sh 已正确配置) |
| admin 密码 | `admin123` | 已通过 bcrypt 重置 |
| ZLM hook URL | `http://polaris-wvp:18080/index/hook/...` | 容器间通过容器名访问 |
| ZLM streamNoneReaderDelayMS | `3600000`1 小时) | 避免短时间关流 |
| WVP stream-on-demand | `false` | 不按需拉流 |
### 验证结果
- 后端 `go build ./...` 通过
- 前端 `npm run build` 通过
- WVP `/api/play/start` 点播接口返回 `code=0` 成功,包含 FLV/HLS/FMP4 地址
- Go 后端 `/api/v1/video/cameras/1/live` 返回相对路径 FLV URL
- 通过 `server.cjs` 5174 端口代理访问 FLV 流,能正常返回视频二进制数据
- 前端清空时间范围查询历史录像,能查到 7月17日的 17 条录像
- recorder-go `/record/status` 反映真实录制状态
### 部署提示
- 系统启动时自动迁移 Permission、RolePermission 表并初始化种子数据
- 旧用户角色为 `user` 的将无权限,需管理员修改为 `viewer`/`operator`/`farmer`
- 新注册用户默认角色为 `viewer`
- WVP 重启后若 FLV 播放失败,执行 `python3 /home/pan/silk/fix_zlm_hook.py` 修复 ZLM hook 配置
## 2026-07-18 React Native APP 启动与登录流程修复
### 变更背景
安卓应用(React Native)首次启动时出现登录导航错误,无法正常显示登录页面。
### 问题根因
1. **导航器条件渲染**`AppNavigator.tsx``token ? ... : <Login>` 的条件渲染导致当 token 存在时(即使过期),导航器只注册 Main 屏幕,不包含 Login 屏幕。当后端返回 401 时,拦截器尝试导航到 Login,但导航器中找不到该屏幕。
2. **恢复会话逻辑缺陷**`authStore.ts``restoreSession` 设置过期 token 后,`fetchUser` catch 块静默忽略错误(不抛出异常),导致 `restoreSession` 的 try-catch 无法捕获到 token 无效,无法自动清除。
3. **拦截器竞态条件**:响应拦截器在导航器初始化完成前就尝试调用 `navigationRef.navigate('Login')`,导致导航失败。
### 修复内容
| 文件 | 变更内容 |
|------|----------|
| `app/src/navigation/AppNavigator.tsx` | 移除条件渲染,始终注册所有屏幕(包括 Login);改用 `initialRouteName={token ? 'Main' : 'Login'}` 控制初始页面 |
| `app/src/store/authStore.ts` | `fetchUser` 移除 try-catch,让错误向上抛出;`restoreSession``fetchUser` 失败时调用 `logout()` 清除过期 token |
| `app/src/api/client.ts` | 移除拦截器中的 `navigationRef.navigate('Login')` 逻辑,避免导航竞态条件 |
### 验证结果
- 应用成功安装并启动到物理设备(OXF-AN10, Android 12
- 登录页面正常显示,可输入账号密码登录
- 实时视频监控功能正常(FLV 流播放)
- 历史录像回看功能正常
- 密码已通过前端修改为 `silk@123`
## 2026-07-28 GB28181 视频点播故障修复(SIP ID 冲突 + Docker 重新部署)
### 故障现象
Web 后台无法播放摄像头实时视频,WVP `play/start` 返回 400 Bad Request 或"通道不存在"。
### 根本原因
WVP 的 SIP 服务器 ID`34020000002000000001`)与摄像头的 GB 设备 ID 完全相同,导致 GB28181 INVITE 请求中 From 和 To 头部用户部分一致,摄像头返回 400 Bad Request 拒绝点播。
### 变更内容
#### 1. WVP Docker 容器重新部署
**文件**: `/home/pan/silk/wvp/wvp-src/docker/.env`
| 配置项 | 旧值 | 新值 | 说明 |
|--------|------|------|------|
| `SIP_Id` | `34020000002000000001` | `35020000002000000001` | 避免与摄像头设备 ID 冲突 |
| `SIP_ShowIP` | `100.83.103.1` | `100.83.103.1`(最终值,中间曾改为 192.168.1.58 | 摄像头通过 Tailscale 网络接入 |
| `MediaRtmp` | `10935` | `10001` | 匹配 ZLM 容器内部实际监听端口 |
| `MediaRtsp` | `5540` | `10002` | 匹配 ZLM 容器内部实际监听端口 |
| `MediaRtp` | `10000` | `10003` | 匹配 ZLM 容器内部实际监听端口 |
**操作**: 停止所有旧容器(`polaris-wvp`/`polaris-media`/`polaris-mysql`/`polaris-redis`),使用 `docker compose up -d` 重新部署。新容器名前缀为 `docker-polaris-*`
#### 2. Go 后端 WVP_API_BASE 修正
**文件**: `/home/pan/silk/server-go/start.sh`
```diff
- export WVP_API_BASE=http://localhost:18080
+ export WVP_API_BASE=http://localhost:18978
```
**原因**: 旧 WVP 容器映射了 18080 端口,新容器只映射 18978。
#### 3. Go 后端数据库通道 ID 更新
**数据库**: PostgreSQL `silk``cameras`
```sql
UPDATE cameras SET gb_channel_id = '44010201001320000001' WHERE gb_device_id = '34020000002000000001';
```
**原因**: 摄像头重新配置后通道 ID 从 `34020000001310000001` 变为 `44010201001320000001`
#### 4. WVP 媒体服务器 sdpIp/streamIp 修正
每次 WVP 重启后通过 API 修正 `sdpIp``streamIp``100.83.103.1`Tailscale IP)。WVP auto-config 每次重启会用 .env 中的值覆盖。
#### 5. docker-compose.yml 端口映射修正
**文件**: `/home/pan/silk/wvp/wvp-src/docker/docker-compose.yml`
- WVP 添加 `18080:18080` 端口映射
- ZLM HTTP 端口映射从注释状态改为启用 `8081:80/tcp`
### 摄像头端配置变更
| 配置项 | 旧值 | 新值 |
|--------|------|------|
| SIP 服务器 ID | `34020000002000000001` | `35020000002000000001` |
| SIP 服务器 IP | `100.83.103.1` | `100.83.103.1`(通过 Tailscale 网络接入) |
| 本地 SIP 端口 | 随机 | `40533`(固定) |
### 网络拓扑
```
服务器 (100.83.103.1)
├── WVP Docker (:18978) - SIP 信令 (:8116)
├── ZLMediaKit Docker (:8081 HTTP, :10001 RTMP, :10002 RTSP, :10003 RTP)
├── Go 后端 (:3000)
└── 前端 (:5174)
摄像头 (100.83.26.113:40533) --Tailscale--> 服务器 (100.83.103.1:8116)
```
### 验证结果
- WVP `play/start` 返回 `code: 0`,成功返回 FLV 流地址
- Go 后端 `/video/cameras/1/live` 返回流 URL
- Web 前端实时视频播放正常
- 视频编码 H264,分辨率 1280x720
### 注意事项
1. **WVP 重启后需重新设置 sdpIp/streamIp**: WVP auto-config 会用 .env 中的值覆盖,需通过 API 改回 `100.83.103.1`
2. **容器名变更**: 旧容器名 `polaris-wvp` 变为 `docker-polaris-wvp-1`ZLM hook URL 中的主机名可能需要更新。
3. **端口映射匹配**: ZLM 容器内部监听端口(10001/10002/10003)与 Docker 端口映射必须一致,否则媒体流无法建立。
## 2026-08-08 摄像头管理优化与 WVP 独立密码同步
### 前端表单优化
1. **行政区域拆分**:将"行政区域"拆分为"省级行政区"(默认广西)和"地市行政区"(默认河池)两个级联下拉框,设备ID根据选择的行政区域动态生成前8位行政区域码。
2. **分辨率和帧率位置调整**:将"分辨率"和"帧率 (FPS)"移到"厂商ID"后面,并改为下拉框选择。
- 分辨率选项:3840×2160、2560×1440、1920×1080、1280×720、704×576、640×480
- 帧率选项:15、20、25、30、50、60
3. **移除厂商ID字段**GB28181协议中不存在"厂商ID",与"厂商"字段重复,已移除。"厂商"改为下拉框选择,预设海康威视、大华股份、宇视科技、华为、中兴、天地伟业、科达、三星、其他。
4. **视频通道编码ID改为平台生成**:新增 `generateChannelId` 函数,基于设备ID生成通道ID(节点类型137),序号默认与设备ID一致,冲突时递增填补空隙。移除了通道ID的"同步"按钮,交互与设备ID一致(AutoComplete + 生成按钮)。
5. **认证密码字段改为必填**:新增"生成"按钮,可自动生成8位字母数字随机密码。修复了 `Input.Password``Space.Compact``form.setFieldValue` 不生效的问题,改用 `Input` + `addonAfter` 实现。
### 后端 WVP 同步增强
6. **新增 AddWvpDevice 方法**`server-go/internal/service/media.go`):调用 WVP API `POST /api/device/query/device/add`,在摄像头注册前预先添加设备到 WVP,设置独立密码。
7. **新增 DeleteWvpDevice 方法**`server-go/internal/service/media.go`):调用 WVP API `DELETE /api/device/query/devices/{deviceId}/delete`,删除摄像头时同步从 WVP 删除设备。
8. **createCamera 改为预添加设备**`server-go/internal/handler/video_camera.go`):创建摄像头时先调用 `AddWvpDevice` 预添加到 WVP(含独立密码),若设备已存在则降级为 `UpdateWvpDevice`
9. **deleteCamera 改为同步删除 WVP**`server-go/internal/handler/video_camera.go`):删除摄像头时同步调用 `DeleteWvpDevice` 从 WVP 删除设备。
10. **改进 WVP 同步失败提示**:将"WVP 设备查询响应格式异常"改为"设备尚未在 WVP 注册,请在摄像头硬件配置设备ID后等待注册完成"。
### 涉及文件
| 文件 | 改动 |
|------|------|
| `web/src/pages/Video.tsx` | 行政区域拆分、厂商ID移除、厂商/分辨率/帧率改下拉框、通道ID平台生成、密码必填+生成按钮 |
| `server-go/internal/service/media.go` | 新增 `AddWvpDevice``DeleteWvpDevice` 方法 |
| `server-go/internal/handler/video_camera.go` | `createCamera` 改为预添加、`deleteCamera` 改为同步删除 |
### 新流程:每个摄像头独立密码
| 步骤 | 操作 | WVP 中状态 |
|------|------|-----------|
| 1 | silk 创建摄像头(生成设备ID + 通道ID + 密码) | 设备预添加,含独立密码 |
| 2 | 摄像头硬件配置 SIP 参数(设备ID、独立密码) | 设备等待注册 |
| 3 | 摄像头开机注册 → WVP | 设备在线,密码验证通过 |
| 4 | silk 编辑摄像头 → 同步 name/厂商/密码到 WVP | 设备已存在,可更新 |
| 5 | silk 删除摄像头 → 同步从 WVP 删除 | 设备已删除 |
### SIP 密码不再依赖 .env 文件
改造前,所有摄像头必须使用 WVP `.env` 中配置的全局 SIP 密码(`SIP_Password=12345678`),存在安全隐患——一个密码泄露则所有设备暴露。
改造后,silk 为每个摄像头生成独立密码,并通过 `AddWvpDevice` API 预先添加到 WVP。摄像头注册时 WVP 使用设备独立密码验证,不再依赖 `.env` 中的全局密码。
| 对比项 | 改造前 | 改造后 |
|--------|--------|--------|
| 密码来源 | WVP `.env` 全局 `SIP_Password` | silk 生成,每个摄像头独立 |
| 密码存储 | WVP 配置文件(明文) | silk DB(`gb_auth_password` 字段) + WVP 设备记录 |
| 密码同步 | 无 | silk → WVP(创建/编辑时同步) |
| 安全性 | 一码通吃,泄露影响所有设备 | 一码一设备,泄露仅影响单个设备 |
| `.env``SIP_Password` | 所有设备必须使用 | 仅作为旧设备/未配置设备的兼容默认值,新设备不再依赖 |
> 注:WVP `.env` 中的 `SIP_Password` 仍保留,用于兼容尚未在 silk 中管理的老设备。通过 silk 新增的摄像头使用独立密码,与 `.env` 无关。
## 2026-08-09 摄像头管理表单布局优化与编辑回显修复
### 变更背景
`/videos` → 摄像头管理 → 添加/编辑摄像头弹窗存在三类问题:表单字段排列过疏占用过多纵向空间;编辑摄像头时页面崩溃 `Cannot read properties of null (reading 'map')`;编辑时"认证密码"显示空白、"码流类型"显示错误的"辅码流"。
### 变更内容
#### 1. 表单布局紧凑化(`web/src/pages/Video.tsx`
| 分组 | 变更前 | 变更后 |
|------|--------|--------|
| 基本信息 | "摄像头名称"独占一行,"房间/安装位置"一行 | "摄像头名称/房间/安装位置"同一行(3 列 `Col span={8}` |
| 行政区域 | "省级行政区/地市行政区"一行,"行业"独占一行 | "省级行政区/地市行政区/行业"同一行(3 列 `Col span={8}` |
| GB28181 配置 | 含"报警输入编码ID"和"语音输出通道ID"两个字段 | 移除这两个字段及其所在行(`gbAlarmChannelId``gbVoiceChannelId` |
#### 2. 编辑摄像头崩溃修复(`web/src/pages/Video.tsx`
**根因**`handleEdit``listWvpChannels(cam.gbDeviceId).then(setWvpChannels)` 直接把后端返回值塞进 state。当 WVP 该设备无通道时接口返回 `null``wvpChannels` 变为 `null`,渲染时 `wvpChannels.map(...)``Cannot read properties of null (reading 'map')`。新增摄像头不触发此接口,故仅编辑时崩溃。
**修复**
- `handleEdit``.then(channels => setWvpChannels(channels || []))`(与下方 onChange 中已有的 `channels || []` 保护一致)
- 顺手加固 `useEffect``listWvpDevices().then(d => setWvpDevices(d || []))`,避免同类空值风险
#### 3. 认证密码回显修复(`web/src/pages/Video.tsx`
**根因**"认证密码"的 `Form.Item name="gbAuthPassword"` 直接包裹 `Space.Compact`antd 把 `value`/`onChange` 注入到 `Space.Compact`(不向下转发),内部 `Input` 未绑定 form store,导致编辑时 `setFieldsValue(cam)` 设的密码显示不出来,手输也不回写。
**修复**:改为与"设备ID"一致的嵌套写法——外层 `Form.Item` 只放 label,内层 `Form.Item name="gbAuthPassword" noStyle` 包住 `Input`,使 Input 真正绑定 form store。
#### 4. 编辑表单残留值修复(`web/src/pages/Video.tsx`
**根因**`handleEdit` 未调用 `form.resetFields()`,而 `useForm` 的表单状态在 Modal 多次开关间持久存在。`form.setFieldsValue(cam)` 只更新 `cam` 里有的字段,不会清掉缺失字段。摄像头 id=7("测试2"`gb_stream_type=sub`)编辑后,再编辑 id=1`gb_stream_type=NULL`)时"码流类型"残留 `sub``handleAdd` 本就有 `resetFields()`,编辑路径漏了。
**修复**`handleEdit``setFieldsValue(cam)` 前加 `form.resetFields()`
#### 5. 移除"码流类型"字段(`web/src/pages/Video.tsx`
**核查结论**:经直接查询 WVP API 确认,WVP **不暴露"主码流/辅码流"属性**,不存在"真实码流类型"可同步:
- 设备级 `/api/device/query/devices``streamMode` 字段为 null(且该字段实为 RTP 传输模式 TCP/UDP,非主/辅码流)
- 通道级 `/api/device/query/devices/{id}/channels``streamId`/`streamIdentification` 均为 null,响应无任何主/辅码流字段
- silk 的 `StartPlay(deviceId, channelId)` 只传设备和通道,不传码流类型;`GbStreamType` 在 Go 代码里仅在 model 定义,从未被读取使用
**处理**:从添加/编辑表单移除"码流类型"字段。保留 `dal/video.ts``Camera` 接口的 `gbStreamType?: string` 可选字段(后端模型仍有该列,对已存值的摄像头仍会返回)。摄像头 `34020000002000000001`id=1DB 中该字段为 NULLWVP 报告 `channelCount=1, subCount=0`(单通道即主码流)。
### 涉及文件
| 文件 | 改动 |
|------|------|
| `web/src/pages/Video.tsx` | 表单三字段合并同行、移除报警/语音通道ID、编辑崩溃修复(null 保护)、认证密码嵌套 Form.Item 修复、handleEdit 加 resetFields、移除码流类型字段 |
### 验证结果
- `npm run build` 通过,无 TS 诊断错误
- 前端已部署到 `http://100.83.103.1:5174/videos`5174 端口正常监听)
- 编辑摄像头不再崩溃,"认证密码"正确回显,"码流类型"字段已移除且不再出现残留值