1059 lines
65 KiB
Markdown
1059 lines
65 KiB
Markdown
# 变更记录
|
||
|
||
## 2026-08-12 Web 知识库图谱预览与保存修复(故障排查 #三十二)
|
||
|
||
### 变更背景
|
||
|
||
用户反馈 Web 知识库上传新症状图谱后编辑页预览图打不开、提交后图谱未保存。排查确认上传与存储正常(Ceph 对象存在、匿名 GET 200),问题在前端:CSP 拦截跨源预览 + 表单未注册 `imageUrl` 导致提交体缺失该字段。
|
||
|
||
### 变更内容
|
||
|
||
| 文件 | 变更 |
|
||
|------|------|
|
||
| `web/src/pages/Knowledge.tsx` | 新增隐藏 `Form.Item name="imageUrl"`,使 `validateFields()` 提交时携带图片 URL |
|
||
| `web/server.cjs` | CSP `img-src` 增加 `http://100.83.103.1:7480`(开发 S3 源) |
|
||
|
||
### 部署与验证
|
||
|
||
- 本地:`npm run lint`(通过)、`npm run build`(通过,新 bundle `index-CSVEb7gG.js`)。
|
||
- 部署:备份 `/home/pan/backups/web-20260812-1605/`(旧 dist、server.cjs、web.log),上传新产物并重启 node。
|
||
- 验证:`http://localhost:5174/` 200;CSP 头含 S3 源;新 bundle 200;`/api/v1/health` 200;DB 白僵病 `image_url` 已补绑(`UPDATE 1`),图片匿名 GET 200;admin 登录 API 冒烟返回白僵病 `imageUrl` 已绑定。
|
||
|
||
### 回滚点
|
||
|
||
- 前端:恢复 `/home/pan/backups/web-20260812-1605/` 旧产物后重启 node。
|
||
- 数据:`/home/pan/backups/diseases-20260812-1605.dump`(pg_restore),或执行反向 UPDATE 将 `image_url` 置空。
|
||
|
||
## 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/` 200;DB `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 承担公开访问,代码仅需改配置
|
||
|
||
### 补充:饲养阶段风险提示(计划 #13)
|
||
|
||
- `server-go/internal/model/knowledge_stage_hints.go`(新增):4 个阶段(小蚕期 young / 大蚕期 grown / 5龄后期 late5 / 蛹期 pupa)风险种子数据,内容取自规格书附录 11.1 高发阶段与 3.2.3 环境风险规则;`StageHintsByStage` 过滤函数
|
||
- `server-go/internal/model/knowledge_stage_hint_test.go`(新增):阶段 key 合法、引用的病种存在于知识库、按阶段过滤/未知阶段报错等 5 个测试(TDD 红→绿)
|
||
- `server-go/internal/handler/knowledge.go`:新增 `GET /api/v1/knowledge/stage-hints?stage=`(只读 GET,天然幂等;返回病种含 id,可直接跳知识详情)
|
||
- 小程序:知识库页新增「饲养阶段风险提示」区块(龄期选择器 → 提示卡片,点击跳病种详情)
|
||
- Web 本次未加(按方案保持 v1 范围)
|
||
- 第四次部署:health 200;`all=4` 阶段;`grown` 返回软化病 + 核型多角体病(含 id);未知阶段 400;回滚点见服务器 README(原位旧二进制 `server-go-linux.old-20260812-6`)
|
||
- 说明:自动按蚕房龄期提示需等「蚕匾/批次管理」(#7,含龄期记录)落地后联动,v1 用选择器
|
||
|
||
### 补充:知识库其余内容(SERS 教程 / 四季防控提醒 / 「其它未知」病种)
|
||
|
||
- 种子数据新增:`sers_guide` 教程 1 篇(原理/适用场景/设备清单/4 步流程/与 LAMP 对比,取自规格书 3.3.2.2);`seasonal_tip` 春夏秋冬 4 篇(取自规格书附录 11.1 季节性流行规律);`diseases` 新增「其它/未知(不确定但有异常)」(对应规格书 AI 输出类别,建议分子检测确认)
|
||
- 测试:新增 SERS 存在、四季齐全、其它/未知类别 3 个用例(TDD 红→绿),种子测试共 10 个全过
|
||
- 小程序:知识库页合并展示 AI/LAMP/SERS 教程入口(按类型徽标),新增「季节性防控提醒」区块(当前季节卡片 + 全部季节列表)
|
||
- Web 无需改动
|
||
- 第五次部署:`sers=1`、`seasonal=4`、`diseases=10`(含 other 类别);回滚点见服务器 README(原位旧二进制 `server-go-linux.old-20260812-7`)
|
||
- 待办:专家经验沉淀(P1)依赖专家会诊(#18)落地,暂缓
|
||
|
||
### AI 巡检闭环后端(计划 #5 ai-service + #6 Go 对接)
|
||
|
||
- `ai-service/`(新增):FastAPI + ONNX Runtime 检测服务骨架——`POST /detect`(multipart 图片 → 框/类别/置信度)、`GET /health`;`MockDetector`(默认,开发联调)+ `ONNXDetector`(`MODEL_MODE=onnx` 加载 `best.onnx`,YOLOv8 后处理含 letterbox/NMS);pytest 6 个用例(TDD 红→绿)
|
||
- `server-go/internal/service/ai_client.go`(新增):AI 服务 HTTP 客户端(multipart 上传、超时、非 200/非法 JSON 报错),httptest 3 个用例
|
||
- `server-go/internal/model/inspection.go`(新增):`inspection_records` 表(图片 URL、检测结果 jsonb、风险字段预留、`idempotency_key` 唯一索引)
|
||
- `server-go/internal/handler/inspection.go`(新增):`POST/GET /api/v1/inspections`——图片存 S3(`silk-images/inspections/…`)→ 调 AI `/detect` → 写记录;**`Idempotency-Key` 头幂等**(重复请求返回已有记录,并发冲突兜底);`roomId` 非 UUID 返回 400
|
||
- 权限:新增 `inspection:create`(farmer/admin/operator)、`inspection:read`(全角色读)
|
||
- 配置:`AI_SERVICE_BASE`(默认 `http://localhost:8000`);`.gitignore` 增加 ai-service Python 文件例外(原全局 `*.py` 规则)
|
||
- 部署:ai-service 部署至开发服务器 `/home/pan/ai-service`(:8000,mock 模式,`start.sh` 启动;服务器补装 `python3.10-venv`,pip 走清华镜像);Go 后端第六次部署(schema 变更前 pg_dump `silk-20260812-1635.dump`)
|
||
- 验证:ai-service 服务器 pytest 6/6;巡检冒烟——创建 201(mock 检测 healthy/0.95)→ 同 key 重试返回同记录 → 列表 1 条 → 图片 URL 200;非法 roomId 400
|
||
- 回滚点:Go 旧二进制 `server-go-linux.old-20260812-9`;ai-service 停 uvicorn 即可;记录见服务器 README
|
||
|
||
### 小程序拍照巡检(计划 #8)
|
||
|
||
- `miniapp/src/api/inspections.ts`(新增):`uploadInspection`(Taro.uploadFile,multipart 上传 + 自动生成 `Idempotency-Key`)、`listInspections`
|
||
- `miniapp/src/pages/inspection/`(新增):拍照/相册选图 → 预览 → 上传 AI 检测 → 结果卡片(类别 + 置信度 + 风险提示)→ 巡检历史记录(最近 10 条)
|
||
- `miniapp/src/types/index.ts`:新增 `InspectionRecord` / `AIDetection`
|
||
- 设置页新增「拍照巡检」入口;`app.config.ts` 注册页面
|
||
- 后端接口复用 #5/#6 已部署的 `POST/GET /api/v1/inspections`,无需服务端改动;`npm run build:weapp` 通过
|
||
- 说明:风险分级(绿/黄/橙/红)待 #9 评分引擎落地后接入;类名映射当前为 healthy/sick,7 类病种扩展后补充
|
||
|
||
### 风险评分引擎(计划 #9)
|
||
|
||
- `server-go/internal/service/risk.go`(新增):
|
||
- `ComputeRiskScore`:按规格书 3.1.4 公式 `0.5×AI 置信度 + 0.2×环境系数 + 0.15×阶段系数 + 0.15×整齐度偏离度`,结果钳制 0-100;
|
||
- `RiskLevel`:绿 0-30 / 黄 31-60 / 橙 61-80 / 红 81-100;
|
||
- `StageCoefficient`:Room.Stage 粗粒度映射(pupa 0.5 / larva 0.4 / moth 0.2 / egg 0.1),待 #7 龄期字段细化;
|
||
- `EnvCoefficient`:按规格书 3.2.3 规则(湿度 ≥80 → 0.8、75-80 → 0.5;温度 >30 或 <20 → 0.5),取最大值。
|
||
- `server-go/internal/service/risk_test.go`:5 个单元测试(权重/钳制/分级边界/阶段/环境规则,TDD 红→绿)
|
||
- `server-go/internal/handler/inspection.go`:巡检创建时计算风险——AI 置信度取检测结果最大值,房间阶段与环境系数在带 `roomId` 时按房间数据加载(`loadRoomRisk`),结果写入 `risk_score` / `risk_level`
|
||
- 第七次部署:冒烟——带房间 57.5(yellow)、不带房间 47.5(yellow);回滚点见服务器 README(旧二进制 `server-go-linux.old-20260812-10`)
|
||
- 说明:整齐度偏离度暂无数据源,本期恒为 0;小程序端风险分级展示待接入
|
||
|
||
### 蚕匾/批次/饲养记录管理(计划 #7)
|
||
|
||
- 后端:新增 `trays`(蚕匾)、`batches`(批次:品种/蚕种来源/批次号/检疫证明/龄期/入房/上蔟/状态)、`rearing_records`(饲养记录:日期/龄期/桑叶来源/密度/备注)三表;CRUD 接口 `/trays`、`/batches`、`/rearing-records`(按房间/批次过滤);删除批次级联删除饲养记录;权限 `tray/batch/rearing:read/write`
|
||
- Web:新增「批次管理」菜单页(批次管理 Tab + 蚕匾管理 Tab + 批次饲养记录弹窗,按蚕房筛选)
|
||
- 小程序:蚕房详情页新增「蚕匾」与「当前批次」只读区块
|
||
- 第八次部署(schema 变更前 pg_dump `silk-20260812-1720.dump`);冒烟:蚕匾/批次/饲养记录 创建→列表→删除(级联)均 200;回滚点见服务器 README(后端 `server-go-linux.old-20260812-11`、web `dist.old-20260812-3`)
|
||
- 说明:批次的龄期字段为 #9 阶段系数 / #13 自动风险提示提供数据来源(后续接线);消毒记录(P1)、蚕种来源全链(P2)等未做
|
||
|
||
### 微信订阅消息骨架(计划 #11)
|
||
|
||
- 后端:`wechat_bindings` 表(用户 openid + 授权模板列表);`/wechat/binding`(查询)、`/wechat/bind`(wx.login code 换 openid 并绑定)、`/wechat/subscribe`(授权/取消场景订阅);`WechatService`(code2session、access_token 缓存、订阅消息发送,未配置时返回明确错误);巡检风险非绿(黄/橙/红)时异步触发推送(未配置/未授权静默跳过)
|
||
- 配置占位:`WECHAT_APPID`、`WECHAT_SECRET`、`WECHAT_TEMPLATE_ALARM`、`WECHAT_TEMPLATE_INSPECTION`(默认空)
|
||
- 权限:新增 `notification:read/write`
|
||
- 小程序:「消息订阅」设置页(绑定微信、授权订阅、场景/模板状态展示;模板 ID 占位 `miniapp/src/api/wechat.ts`)
|
||
- 测试:微信服务 6 个单测(模板键/授权判断/数据构造/code2session/发送/错误码,TDD 红→绿)
|
||
- 第九次部署(schema 变更前 pg_dump `silk-20260812-1730.dump`);冒烟:binding 未绑定、bind 未配置返回 502 明确提示、巡检触发不崩溃;回滚点见服务器 README(旧二进制 `server-go-linux.old-20260812-12`)
|
||
- 待办:提供小程序 AppID/Secret 与订阅消息模板 ID 后联调真实推送;告警触发点(告警产生时)待接入
|
||
|
||
### 和风天气接入与高发病天气预警(计划 #12)
|
||
|
||
- 后端:`weather_alerts` 表 + `GET /weather/now`(实时天气 + 规则评估,可带 `stage` 参数)、`GET /weather/alerts`(最近预警);和风天气客户端(实时 + 3 天预报);规则引擎按规格书 3.2.3——白僵病(湿度≥80% + 连续阴雨≥2 天或当前降雨 → 高风险)、核型多角体病(温度 >30℃ 或 <20℃ → 注意;5龄后期升级高风险)、软化病(湿度>75% → 注意);启动时执行一次 + 每 30 分钟定时评估写入
|
||
- 配置占位:`QWEATHER_API_KEY`、`QWEATHER_LOCATION`、`QWEATHER_INTERVAL_MIN`(默认 30)
|
||
- 权限:新增 `weather:read`(全角色)
|
||
- Web:告警中心顶部「高发病天气预警」区块;小程序:告警页顶部天气预警卡片
|
||
- 测试:规则与客户端共 9 个单测(降雨天数/阴雨判断/白僵病/温度突变/综合规则/实时/预报/错误码/配置,TDD 红→绿)
|
||
- 第十次部署(schema 变更前 pg_dump `silk-20260812-1745.dump`);冒烟:`alerts=[]`、`now` 未配置返回 502 明确提示;回滚点见服务器 README(后端 `server-go-linux.old-20260812-13`、web `dist.old-20260812-4`)
|
||
- 待办:提供和风天气 API Key 与位置(经纬度或 LocationID)后启用真实数据;微粒子病(蚕种来源/消毒记录)规则待批次数据联动
|
||
|
||
### LAMP 检测管理(计划 #14)
|
||
|
||
- 后端:`lamp_tests`(任务单:蚕房/批次/病种多选/采样信息/结果/结果照片/状态)与 `lamp_test_steps`(标准 5 步:采样/DNA提取/反应体系配制/恒温反应/结果判读,自动生成)两张表;接口:任务单 CRUD、步骤列表/完成状态更新、结果照片上传(S3 `lamp/` 前缀)、结果录入(`positive/negative/invalid` 校验,录入后自动置状态 `resulted`);删除任务单级联删除步骤
|
||
- 权限:新增 `lamp:read/write`(admin/operator 可写,viewer/farmer 可读)
|
||
- Web:新增「LAMP 检测」菜单页——任务单列表(按蚕房/状态筛选)、新建(病种多选)、步骤勾选、结果录入 + 结果照片上传
|
||
- 小程序:设置页「LAMP 检测」入口——任务列表、步骤勾选、结果拍照上传、结果选择保存
|
||
- Web 构建优化:`vite.config.ts` 增加 `manualChunks` 代码拆分,解决 rolldown 压缩超大单 chunk 时 `WebAssembly.Memory.grow` 内存超限问题
|
||
- 测试:步骤顺序/结果枚举 2 个单测;第十一次部署(schema 变更前 pg_dump `silk-20260812-1800.dump`);冒烟:创建→5 步→步骤完成→照片→结果 resulted→列表→级联删除全 200;回滚点见服务器 README(后端 `server-go-linux.old-20260812-14`、web `dist.old-20260812-5`)
|
||
- 说明:AI 辅助判读(阳性/阴性/无效图像分类)待模型就绪后接入 ai-service;交叉验证(#15)与耗材联动(#16)未做
|
||
|
||
## 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 秒,152MB,29 个 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 17(AGP 要求) |
|
||
| 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 URL(127.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=1)DB 中该字段为 NULL,WVP 报告 `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 端口正常监听)
|
||
- 编辑摄像头不再崩溃,"认证密码"正确回显,"码流类型"字段已移除且不再出现残留值
|