Files
silk/变更记录.md
T
2026-08-17 21:43:26 +08:00

1243 lines
83 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-17 对象授权、运营能力与组织管理(本地 v9,未部署)
### 变更背景
按《后续工作计划》推进 IAM-003 组织/房间级对象授权及 P2 业务能力。本地已完成 v9 迁移、对象授权代码、Web 运营能力页和 Web 组织管理页;开发服务器仍为已部署的 schema v8,本次变更未发布。
### 变更内容
| 文件 | 变更 |
|------|------|
| `server-go/migrations/000009_object_authority_and_business_capabilities.up/down.sql`(新增) | `organizations``organization_members``device_maintenance_records``production_loss_records``case_studies``laboratory_results``rooms.org_id``rearing_records.death_count/culled_count` |
| `server-go/internal/handler/access.go`(新增) | 组织/房间对象授权、设备 key 授权、统一对象归属解析 |
| `server-go/internal/handler/organization.go`(新增) | 组织 CRUD、成员列表/添加/移除 |
| `server-go/internal/ws/authorizer.go` | WebSocket 设备订阅改为 RBAC + 组织成员校验 |
| `server-go/internal/handler/alarm.go``sensor.go``threshold.go``telemetry.go``control.go``video_record.go` 等 | 传感器/阈值/告警/遥测/控制/录像等设备链路按组织房间隔离 |
| `web/src/pages/Organizations.tsx``web/src/dal/organization.ts`(新增) | 组织列表/编辑、成员添加/移除 |
| `web/src/pages/BusinessExtensions.tsx``web/src/dal/business.ts` | 设备维护、产量损失编辑/删除闭环 |
### 验证结果
- `scripts/verify.ps1` exit 0Go test/vet/build、Web test/lint/build、小程序 test/typecheck/build、APP typecheck/lint、AI pytest 均通过。
- 新增测试覆盖传感器设备归属、跨组织设备 403、组织范围蚕房选择、WS 组织成员放行/拒绝。
- 未部署开发服务器,未执行 `000009` 迁移,未做真实多组织越权联调。
### 回滚点
- 本地代码尚未提交,回滚可恢复工作区修改;
- 若已执行 v9,数据库回滚执行 `000009_object_authority_and_business_capabilities.down.sql` 或恢复迁移前 `pg_dump`,随后回退旧二进制。
## 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/` 200CSP 头含 S3 源;新 bundle 200`/api/v1/health` 200DB 白僵病 `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/` 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 承担公开访问,代码仅需改配置
### 补充:饲养阶段风险提示(计划 #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`:8000mock 模式,`start.sh` 启动;服务器补装 `python3.10-venv`,pip 走清华镜像);Go 后端第六次部署(schema 变更前 pg_dump `silk-20260812-1635.dump`
- 验证:ai-service 服务器 pytest 6/6;巡检冒烟——创建 201mock 检测 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.uploadFilemultipart 上传 + 自动生成 `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.5yellow);回滚点见服务器 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)未做
### 交叉验证 AI vs LAMP(计划 #15
- 后端:`lamp_tests` 增加 `cross_status`pending/consistent/inconsistent)与 `cross_reason`;LAMP 结果录入后自动与同房间最近巡检记录比对(`runCrossValidation`);新接口 `GET /lamp-tests/:id/cross-validation`(返回比对状态、原因与关联巡检摘要)
- 纯逻辑 `CrossValidate`(规格书:一致→确认诊断,不一致→建议专家会诊):AI 异常+LAMP 阳性 / AI 健康+LAMP 阴性 → 一致;其余组合(含 LAMP 无效、无关联巡检)→ 不一致;`AIClassFromDetections`(任一非 healthy 视为 sick
- Web:LAMP 检测列表新增「交叉验证」列,结果弹窗展示比对结论;小程序:结果区展示一致/不一致提示
- 测试:交叉验证矩阵 + AI 归纳 2 组单测(TDD 红→绿);第十二次部署(schema 变更前 pg_dump `silk-20260812-1815.dump`);冒烟:LAMP 阴性+AI 健康→consistent、LAMP 阳性+AI 健康→inconsistent(建议会诊);回滚点见服务器 README(后端 `server-go-linux.old-20260812-15`、web `dist.old-20260812-6`
### 耗材管理(计划 #16
- 后端:`consumables` 表(名称/类别/规格/库存/单位/安全阈值/效期/供应商)+ CRUD;`GET /consumables/alerts`(低库存 + 30 天临期/过期预警);`GET /consumables/purchase-suggestions`(低于安全阈值时建议补足差额、向上取整)
- 规则方法(模型层,TDD):`LowStockAlert``ExpiringAlert(days)``PurchaseSuggestion`
- 权限:新增 `consumable:read/write`admin/operator 可写,viewer/farmer 可读)
- Web:新增「耗材管理」菜单页(列表增改删 + 库存/效期预警与采购建议区块)
- 小程序:按方案暂不做(技术员 Web 后台为主,待 #17 统一考虑)
- 测试:3 个单测(低库存/临期/采购建议,TDD 红→绿);第十三次部署(schema 变更前 pg_dump `silk-20260812-1830.dump`);冒烟:低库存→low_stock + 建议采购 7、临期→expiring;回滚点见服务器 README(后端 `server-go-linux.old-20260812-16`、web `dist.old-20260812-7`
### 技术员巡检记录页(计划 #17)
- 后端:`GET /api/v1/inspections` 列表增加 `roomName` 联查(非持久化字段)
- Web:新增「巡检记录」菜单页——列表(时间/蚕房/AI 结论/风险等级与分数/图片缩略/状态)+ 详情抽屉(原图、检测框明细、置信度、风险分)
- 无 schema 变更、无小程序改动;第十四次部署;冒烟:列表 `roomName=蚕房1#``detections=1`;回滚点见服务器 README(后端 `server-go-linux.old-20260812-17`、web `dist.old-20260812-8`
- 说明:阶段二(环境监测 + LAMP + 技术员后台)至此全部落地;检测任务处理(#14)、耗材(#16)、巡检复查(#17)均已在 Web 提供
### 专家会诊(计划 #18
- 后端:`consultations` 表(房间/批次/LAMP 关联、标题/摘要、病例快照 jsonb、状态、专家意见/防控方案、时间戳)+ CRUD;创建时可关联 LAMP 任务单,自动打包病例快照(房间名 + 最近巡检 + LAMP + 批次 + 最近天气预警);`POST /:id/resolve`(专家意见 + 防控方案 → resolved)、`POST /:id/archive`(→ archived);状态流转用 `ValidConsultationTransition` 校验(pending→consulting/resolvedconsulting→resolvedresolved→archived
- 权限:新增 `consultation:read/write`admin/operator 可写,viewer/farmer 可读)
- Web:新增「专家会诊」菜单页——列表(按状态筛选)、详情抽屉(病例快照:巡检图片/风险、LAMP 结果/交叉验证、批次、天气预警)、受理/出方案/归档操作
- 小程序:按方案不做(专家端 Web 优先)
- 测试:状态流转 + 快照序列化 2 组单测(TDD 红→绿);第十五次部署(schema 变更前 pg_dump `silk-20260812-1845.dump`);冒烟:创建(快照含 lamp+inspection+roomName)→受理→出方案→归档全通过;回滚点见服务器 README(后端 `server-go-linux.old-20260812-18`、web `dist.old-20260812-9`
- 说明:会诊通知(微信订阅消息触发点)后续接入;专家角色暂用 admin/operator 承担
### 分子检测扩展(计划 #19qPCR / SERS / 高光谱预留)
- 后端:`lamp_tests` 增加 `method`lamp/qpcr/sers/hyperspectral,默认 lamp)与 `extra_data`jsonb);新增 `spectrum_entries` 光谱库表
- qPCR`POST /lamp-tests/:id/judge-qpcr`Ct 值 + 可选阈值)→ 纯函数 `JudgeQPCR` 自动判读(任一 Ct<threshold 阳性,无 Ct 阴性,阈值默认 35),判读原因进 extraData,结果/状态/交叉验证自动联动
- SERS`POST /lamp-tests/:id/spectrum`csv/txt/json ≤5MB → S3 `spectrum/` 前缀,URL 进 extraData);光谱库 `spectrum_entries` CRUD + `POST /spectrum-entries/upload`
- 高光谱:`method=hyperspectral` 接口位预留
- Web:「LAMP 检测」升级为「分子检测」(新建选方式、qPCR Ct 录入自动判读、SERS 光谱上传)+ 新增「光谱库」Tab(增删查 + 文件上传)
- 小程序:任务列表展示检测方式
- 测试:`JudgeQPCR` 矩阵、光谱文件校验、`buildDataKey` 3 组单测(TDD 红→绿);第十六次部署(schema 变更前 pg_dump `silk-20260812-1900.dump`);冒烟:qPCR Ct[32,38]→positive、SERS 光谱 URL、光谱库增查删全通过;回滚点见服务器 README(后端 `server-go-linux.old-20260812-19`、web `dist.old-20260812-10`
### 多检测方式推荐引擎(计划 #20)
- 后端:`RecommendMethod` 纯逻辑——输入设备可用性(LAMP/SERS/qPCR/高光谱)+ 紧急程度 + 操作者水平 + 成本偏好,按加权评分排序返回推荐(方法/名称/理由/时间/成本/难度),规则取自规格书 3.3.2 与 11.2 参数对比;高光谱理论可行待验证,仅在其他方式都不可用时推荐
- 接口:`GET /api/v1/detection-methods/recommend`(纯计算,无 DB
- Web:分子检测「新建任务单」弹窗内置「推荐检测方式」工具(设备勾选 + 紧急/操作者/成本偏好 → 获取推荐)
- 测试:6 个推荐矩阵单测(无设备/低成本/最快/紧急降级/新手避高难度/仅高光谱,TDD 红→绿);第十七次部署(无 schema 变更);冒烟:low_cost→LAMP 优先、fastest→SERS、紧急无 SERS→LAMP、仅高光谱→带待验证提示;回滚点见服务器 README(后端 `server-go-linux.old-20260812-20`、web `dist.old-20260812-11`
### 疫病溯源(计划 #21,规格书 3.8 三级溯源)
- 后端:`trace_records` 表(关联房间/LAMP/会诊、病种、状态、来源判定、置信度、一级初报/二级清单/二级报告 jsonb、三级专家/实验室备注)
- 一级自动溯源 `POST /trace-records/:id/auto`:环境回溯(房间最近温湿度:高湿 ≥80% 或温度突变)→ 历史发病关联(同房间同病种 LAMP 记录,90 天)→ 传播途径推断(按病种知识库)→ 生成《溯源初报》并初步判定来源(内源/外源/待确认 + 置信度)
- 二级:`GET /trace-records/:id/checklist`(分病种排查清单,取自规格书 3.8.3:核型多角体病/白僵病/微粒子病/软化病/浓核病/细菌性败血病)+ `POST /:id/checklist`(作答 → 内源/外源倾向统计 → 《溯源分析报告》)
- 三级:`expertNote` / `labNote` 字段(病原分型、母蛾镜检等)
- 权限:新增 `trace:read/write`;Web:新增「疫病溯源」菜单页(列表 + 详情:一级初报/二级清单作答/三级记录 + 自动溯源/归档)
- 测试:5 组溯源规则单测(环境回溯/历史关联/传播推断/清单/分析,TDD 红→绿);第十八次部署(schema 变更前 pg_dump `silk-20260812-1920.dump`);冒烟:创建→清单 4 项→autoreported/内源/conf 0.6)→清单提交(analysis/conf 0.725);回滚点见服务器 README(后端 `server-go-linux.old-20260812-21`、web `dist.old-20260812-12`
### 区域发病统计(计划 #22
- 后端:`rooms` 增加 `region` 字段(乡镇/村,蚕房表单录入);`GET /api/v1/trace-records/region-stats?days=&disease=`——近 N 天(默认 90)按蚕房区域聚合发病数与病种分布(`AggregateRegionStats` 纯逻辑:分组/降序/空区域剔除)
- Web:疫病溯源页顶部新增「区域发病统计(近 90 天)」柱状图 + 病种分布饼图(ECharts);蚕房管理表单新增「区域」字段与列表列
- 小程序:不做;真正的乡镇/县级地图(GeoJSON 底图)本期未接入,先用统计图表表达热力趋势
- 测试:区域聚合 2 组单测(分组/降序/空区域剔除/空输入,TDD 红→绿);2026-08-13 第一次部署(schema 变更前 pg_dump `silk-20260813-0010.dump`);冒烟:SmokeTown 聚合 total=2、DiseaseA/B 各 1;回滚点见服务器 README(后端 `server-go-linux.old-20260813-1`、web `dist.old-20260813-1`
### 蚕房健康画像与年度发病统计(计划 #24)
- 后端:`GET /api/v1/health-profiles/:roomId`——聚合近 30 天巡检风险分布、分子检测结果、会诊/溯源事件、当前批次 → `ComputeHealthScore`(0-100,扣分制)与评级(优/良/中/差);`GET /api/v1/trace-records/monthly-stats?year=`(按月聚合发病数与病种分布)
- Web:蚕房管理「健康画像」抽屉(环形分数 + 详情);疫病溯源页新增「年度发病规律」折线图
- 测试:健康分计算(空输入/扣分/钳制)+ 月度聚合 3 组单测(TDD 红→绿)
- 部署与踩坑(2026-08-13 第二次发布):①Gin 路由 `/rooms/:id/health-profile` 与已有 `/rooms/:id` 冲突导致启动 panic → 改独立路径 `/health-profiles/:roomId`;②`RegisterHealthRoutes` 曾重复注册 panic;③新增 health.go 时覆盖了原 `/health` 路由导致 health 404 → 恢复原函数并拆分 `RegisterHealthProfileRoutes`;④handler 参数名由 `id` 改为 `roomId` 修正 404。最终冒烟:蚕房1# score=96 优(risk 黄 1
- 回滚点见服务器 README(后端 `server-go-linux.old-20260813-6`、web `dist.old-20260813-3`
### Web 测试基建(计划 #27
- `web` 引入 Vitest + jsdom + @testing-library/react/jest-dom;新增 `npm test``vitest run`),`vite.config.ts` 增加 `test` 配置(jsdom
- 新增公共格式化函数 `web/src/utils/format.ts``aiClassLabel`/`riskLevelLabel`)+ 首批 3 个单测(TDD 红→绿);巡检记录页接入公共函数(删除内联映射)
- 验证:`npm test` 3/3、`npm run lint``npm run build` 全过;无后端/schema 变更,Web 已同步部署(`dist.old-20260813-4` 为回滚点)
- 说明:Go 测试骨架此前已随各模块落地;后续新增 Web 纯逻辑/工具函数时按 TDD 补充单测
### ai-service 扩展:摄像头流巡检骨架 + 监控指标(计划 #23 骨架 / #26)
- `ai-service`:新增 `POST /stream-detect`(拉流抽帧检测骨架——OpenCV 拉取 rtsp/http-flv,抽 3 帧检测返回结果;缺 url 返回 400、流不可达返回 502、未装 OpenCV 返回 503);新增 `GET /metrics`(模型模式、uptime、请求数、平均延迟、nvidia-smi GPU 信息,无 GPU 时为 null
- 依赖:`opencv-python-headless` 加入 requirements
- 测试:新增 2 个用例(stream 缺 url 拒绝、metrics 字段完整),pytest 8/8TDD 红→绿)
- 部署:ai-service 更新至开发服务器(:8000mock);冒烟:health ok、metrics 返回 GPU 信息(开发服务器实有 NVIDIA 940MX 2GB)、stream 无 url 被拒绝
- 说明:#23 正式摄像头流巡检(视角数据 + 巡检任务编排)仍按计划二期;#26 生产 GPU 监控待 T4 主机,当前骨架可用 nvidia-smi 做基础监控
### 时序存储决策(计划 #25
- **决策(2026-08-13):MVP 沿用 IoTDB(现状)**TDengine 作为生产规模化候选
- 依据:IoTDB 2.0.8 已在开发服务器运行且 Go 后端已接入(含 PostgreSQL 降级兜底);当前遥测量级远未到性能瓶颈;引入 TDengine 需重写时序读写层、迁移存量数据、新增运维面,MVP 阶段无眼前收益
- 切换触发条件:聚合报表/BI 需求复杂化且团队更熟标准 SQL、单机写入/查询出现瓶颈、生产上云规划;届时先做同数据量基准测试(写入吞吐/查询延迟/成本/许可)再定,并建议「双写灰度 + 数据对账」迁移
- 影响面(若切换):存量数据迁移、`server-go` 时序读写服务重写、健康画像/天气回溯等查询调整
### 文档同步(README / 后续工作计划)
- `README.md`:新增 1.8-1.17 核心能力(蚕匾批次/巡检闭环/知识库/分子检测/会诊/溯源/耗材/天气/健康画像/微信订阅);权限码 15→36;技术栈补 Go 1.23、FastAPI/ONNX/OpenCV、Vitest;目录结构补 `ai-service/`;后端模块表补 12 个模块;环境变量表补 `S3_BUCKET_IMAGES/AI_SERVICE_BASE/WECHAT_*/QWEATHER_*`;本地开发补 `npm test`
- `后续工作计划.md`:勾选 #5-#24#27 完成状态,#23/#26 标记骨架完成,#1-4 标注挂起(物理机),顶部加完成状态说明
- `README.md`(第二轮):API 概览表补 12 组新接口;权限矩阵补 20 个新权限码;12.6/12.7 补 ai-service 本地开发与开发服务器部署摘要
- `部署指南(物理机).md`:架构图补 ai-service:8000;新增「13. 部署 ai-service」章节(venv/start.sh/health 验证)
- `故障排查处理记录.md`:追加 2026-08-12/13 部署与开发环境踩坑合集(NetBird 过期、pip 清华源、rolldown WASM、ETXTBSY、Gin 路由三坑、中文 JSON 编码、tar 时间戳、命令后台化)
## 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 端口正常监听)
- 编辑摄像头不再崩溃,"认证密码"正确回显,"码流类型"字段已移除且不再出现残留值
---
## 2026-08-13 版本化数据库迁移部署(Task 2)
### 变更内容
- 后端采用 `golang-migrate/migrate/v4 v4.18.2`,启动时执行 `server-go/migrations/` 下 SQL 迁移并校验 `schema_migrations`
- `000001_baseline` 覆盖当前 26 张业务表、索引和唯一约束,并已调整为幂等建表/建索引;
- `database.Init` 不再无条件 AutoMigrate;生产环境忽略 `ALLOW_DEV_AUTOMIGRATE`
- 开发服务器 `100.83.103.1` 已从 AutoMigrate 旧二进制切换为迁移版本二进制。
### 验证结果
- 开发服务器后端 `:3000` 与 Web `:5174` 均监听正常,`/api/v1/health` 返回 200
- `schema_migrations``1|f`
- 后端日志:`数据库连接成功,版本化迁移完成 schemaVersion=1`
- 新二进制 SHA256`9acb5fbfdb854b90b821d456f0f05177cd34dd8f3161686e1c472170af5d1201`
### 回滚点
- 数据库备份:`/home/pan/backups/silk-20260813-234221.dump`
- 旧二进制:`/home/pan/silk/server-go/server-go-linux.bak-20260813-234221`
- 回滚步骤:恢复旧二进制并重启,或先 `pg_restore` 恢复数据库备份;开发服务器无 git 仓库,不依赖 git tag。
---
## 2026-08-14 整改 Task 0-14 基线部署
### 变更内容
- 将 Task 0~14 整改基线合并到 `dev_wjs` 并部署到开发服务器;
- Go 后端、Web、AI 服务均切换到最新制品;
- 后端迁移到 `schemaVersion=8`,新增检测任务、样本、发病事件、生物安全、通知/Outbox、治理/效果等表;
- 修复 Outbox worker 阻塞 HTTP 启动问题:`outbox.Start` 改为后台 goroutine。
### 验证结果
- 数据库备份:`/home/pan/backups/silk-20260814-191754.dump`
- Go `/api/v1/health` 200、Web `:5174` 200、AI `/health` 200
- 新路由未登录返回 401,确认路由已注册;
- `admin/silk@123` 登录成功;登录态下检测任务、发病事件、生物安全、会诊、效果报告、运维指标接口均返回 200,二维码空载荷返回 400。
- E2E 结果:E2E-01/03/04/05/06/08/09/10 通过;E2E-02 因 mock 隔离 blockedE2E-07 队列单测通过。
- 负载冒烟:500 HTTP 请求 0% 失败;1000 WebSocket 连接 0% 失败。
- 备份恢复:当前 schema 备份 `/home/pan/backups/silk-20260814-194300.dump` 已恢复到隔离库并核对一致。
### 回滚点
- Go 旧二进制:`/home/pan/silk/server-go/server-go-linux.pre-20260814-191754`
- Web 旧目录:`/home/pan/silk/web/dist.pre-20260814-191754`
- AI 旧目录:`/home/pan/ai-service/app.pre-20260814-191754`
- 数据库回滚:`pg_restore` 恢复 `/home/pan/backups/silk-20260814-191754.dump`