Files
silk/变更记录.md
T

79 KiB
Raw Blame History

变更记录

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.dumppg_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 模型(diseasesknowledge_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.tsxsrc/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=9knowledge_articles=1

风险与备注

  • start.shWVP_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.tsminiapp/src/pages/knowledge/index.tsx:知识库列表增加「LAMP 教程」入口
  • miniapp/src/pages/knowledge/detail/index.tsx:修复非 disease 类型(如 lamp_guide)误走病种详情接口的问题
  • Web 无需改动(文章 Tab 已支持 lamp_guide 类型)
  • 二次部署:后端二进制更新(sha256 742ffb54…),验证 ai_guide=1lamp_guide=1GET /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/imagesmultipart 字段 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 200all=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=1seasonal=4diseases=10(含 other 类别);回滚点见服务器 README(原位旧二进制 server-go-linux.old-20260812-7
  • 待办:专家经验沉淀(P1)依赖专家会诊(#18)落地,暂缓

AI 巡检闭环后端(计划 #5 ai-service + #6 Go 对接)

  • ai-service/(新增):FastAPI + ONNX Runtime 检测服务骨架——POST /detectmultipart 图片 → 框/类别/置信度)、GET /healthMockDetector(默认,开发联调)+ ONNXDetectorMODEL_MODE=onnx 加载 best.onnxYOLOv8 后处理含 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——图片存 S3silk-images/inspections/…)→ 调 AI /detect → 写记录;Idempotency-Key 头幂等(重复请求返回已有记录,并发冲突兜底);roomId 非 UUID 返回 400
  • 权限:新增 inspection:createfarmer/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-9ai-service 停 uvicorn 即可;记录见服务器 README

小程序拍照巡检(计划 #8

  • miniapp/src/api/inspections.ts(新增):uploadInspectionTaro.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
    • StageCoefficientRoom.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/bindwx.login code 换 openid 并绑定)、/wechat/subscribe(授权/取消场景订阅);WechatServicecode2session、access_token 缓存、订阅消息发送,未配置时返回明确错误);巡检风险非绿(黄/橙/红)时异步触发推送(未配置/未授权静默跳过)
  • 配置占位:WECHAT_APPIDWECHAT_SECRETWECHAT_TEMPLATE_ALARMWECHAT_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_KEYQWEATHER_LOCATIONQWEATHER_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/writeadmin/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_statuspending/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):LowStockAlertExpiringAlert(days)PurchaseSuggestion
  • 权限:新增 consumable:read/writeadmin/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/writeadmin/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 承担

分子检测扩展(计划 #19:qPCR / SERS / 高光谱预留)

  • 后端:lamp_tests 增加 methodlamp/qpcr/sers/hyperspectral,默认 lamp)与 extra_datajsonb);新增 spectrum_entries 光谱库表
  • qPCRPOST /lamp-tests/:id/judge-qpcrCt 值 + 可选阈值)→ 纯函数 JudgeQPCR 自动判读(任一 Ct<threshold 阳性,无 Ct 阴性,阈值默认 35),判读原因进 extraData,结果/状态/交叉验证自动联动
  • SERSPOST /lamp-tests/:id/spectrumcsv/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 testvitest run),vite.config.ts 增加 test 配置(jsdom
  • 新增公共格式化函数 web/src/utils/format.tsaiClassLabel/riskLevelLabel)+ 首批 3 个单测(TDD 红→绿);巡检记录页接入公共函数(删除内联映射)
  • 验证:npm test 3/3、npm run lintnpm 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 新增 CapacityStage 字段(修复蚕房管理表单字段不持久化问题)
路由装配 cmd/server/main.go 注册 SecurityHeaders 中间件

前端变更(web

类别 文件 变更
令牌刷新 src/api/http.ts 响应拦截器:401 时用 refreshToken 静默换新并重试;统一读取 error 字段为错误提示
认证服务 src/services/auth.tssrc/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.tsxsrc/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.capacityrooms.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 移动 APPe:\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.tsZustand + AsyncStorage 持久化)、src/store/appStore.ts

工具src/utils/ws.ts(WebSocket 管理器,自动重连)、src/utils/format.ts(格式化)

组件src/components/StatCard.tsxsrc/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.tsTaro.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 APPtsc --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 字段作为 deviceKeysource=="command" 命令响应处理;定时上报提取 temperature/humidity/signal 等字段入库;新增 deviceTopics 内存表和 PublishToDevice 方法
server-go/internal/handler/control.go 新增 APIPOST /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/cmdPublishToDevice 自动检测主题方向,反配设备发送到 /up/telemetry;指标提取增加 voltage/current/power/energy/key/onState;命令响应处理增加 info-statisticcontroller-event
server-go/internal/handler/control.go 新增插座 APIPOST /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.htmlCache-Control: no-cache, no-store, must-revalidate(原为 max-age=86400 导致更新后浏览器不刷新)
  • 静态资源(带 hash 文件名):Cache-Control: public, max-age=6048007 天)
  • 部署路径修正为 /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 分钟内未响应才标记离线

心跳命令

{"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/telemetrysilk/+/+/+/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,导致后端 irLearnedCodesno > 0 条件未记录编号。

修复

  • 新增 pendingLearnNo map[string]int 记录发送 learn 命令时的编号
  • PublishToDevice 检测 learn 命令时记录编号到 pendingLearnNo
  • handleMessage 收到 learn 成功响应且 no=0 时,从 pendingLearnNo 还原编号,正确记录到 irLearnedCodes

5. 擦除按钮修复

问题:擦除按钮使用 Modal.confirm 确认对话框,用户可能没注意到需要二次点击"确认擦除",且设备对 erase 命令可能不返回响应,导致"无反馈"。

修复

  • 改为 Popconfirm 气泡确认(更直观,出现在按钮旁边)
  • 点击确认后立即清空本地 learnedCodes 列表
  • 新增 pollEraseResult 函数:短时间轮询(10 秒,5 次)确认擦除结果
  • 超时显示"设备未响应擦除结果,本地红外码列表已清空"
  • pollIRResultsuccess=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-linux3000 端口已重启

验证结果

  • 空调遥控面板按钮布局正确,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 /permissionsGET /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.gofixZlmPort 函数

变更前: 返回绝对路径 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.golistActiveRecordings 函数

变更前: 仅读 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 36000001 小时) 避免短时间关流
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.tsxtoken ? ... : <Login> 的条件渲染导致当 token 存在时(即使过期),导航器只注册 Main 屏幕,不包含 Login 屏幕。当后端返回 401 时,拦截器尝试导航到 Login,但导航器中找不到该屏幕。
  2. 恢复会话逻辑缺陷authStore.tsrestoreSession 设置过期 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,让错误向上抛出;restoreSessionfetchUser 失败时调用 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 服务器 ID34020000002000000001)与摄像头的 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

- export WVP_API_BASE=http://localhost:18080
+ export WVP_API_BASE=http://localhost:18978

原因: 旧 WVP 容器映射了 18080 端口,新容器只映射 18978。

3. Go 后端数据库通道 ID 更新

数据库: PostgreSQL silkcameras

UPDATE cameras SET gb_channel_id = '44010201001320000001' WHERE gb_device_id = '34020000002000000001';

原因: 摄像头重新配置后通道 ID 从 34020000001310000001 变为 44010201001320000001

4. WVP 媒体服务器 sdpIp/streamIp 修正

每次 WVP 重启后通过 API 修正 sdpIpstreamIp100.83.103.1Tailscale 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-1ZLM 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.PasswordSpace.Compactform.setFieldValue 不生效的问题,改用 Input + addonAfter 实现。

后端 WVP 同步增强

  1. 新增 AddWvpDevice 方法server-go/internal/service/media.go):调用 WVP API POST /api/device/query/device/add,在摄像头注册前预先添加设备到 WVP,设置独立密码。

  2. 新增 DeleteWvpDevice 方法server-go/internal/service/media.go):调用 WVP API DELETE /api/device/query/devices/{deviceId}/delete,删除摄像头时同步从 WVP 删除设备。

  3. createCamera 改为预添加设备server-go/internal/handler/video_camera.go):创建摄像头时先调用 AddWvpDevice 预添加到 WVP(含独立密码),若设备已存在则降级为 UpdateWvpDevice

  4. deleteCamera 改为同步删除 WVPserver-go/internal/handler/video_camera.go):删除摄像头时同步调用 DeleteWvpDevice 从 WVP 删除设备。

  5. 改进 WVP 同步失败提示:将"WVP 设备查询响应格式异常"改为"设备尚未在 WVP 注册,请在摄像头硬件配置设备ID后等待注册完成"。

涉及文件

文件 改动
web/src/pages/Video.tsx 行政区域拆分、厂商ID移除、厂商/分辨率/帧率改下拉框、通道ID平台生成、密码必填+生成按钮
server-go/internal/service/media.go 新增 AddWvpDeviceDeleteWvpDevice 方法
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 DBgb_auth_password 字段) + WVP 设备记录
密码同步 silk → WVP(创建/编辑时同步)
安全性 一码通吃,泄露影响所有设备 一码一设备,泄露仅影响单个设备
.envSIP_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"两个字段 移除这两个字段及其所在行(gbAlarmChannelIdgbVoiceChannelId

2. 编辑摄像头崩溃修复(web/src/pages/Video.tsx

根因handleEditlistWvpChannels(cam.gbDeviceId).then(setWvpChannels) 直接把后端返回值塞进 state。当 WVP 该设备无通道时接口返回 nullwvpChannels 变为 null,渲染时 wvpChannels.map(...)Cannot read properties of null (reading 'map')。新增摄像头不触发此接口,故仅编辑时崩溃。

修复

  • handleEdit.then(channels => setWvpChannels(channels || []))(与下方 onChange 中已有的 channels || [] 保护一致)
  • 顺手加固 useEffectlistWvpDevices().then(d => setWvpDevices(d || [])),避免同类空值风险

3. 认证密码回显修复(web/src/pages/Video.tsx

根因"认证密码"的 Form.Item name="gbAuthPassword" 直接包裹 Space.Compactantd 把 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=1gb_stream_type=NULL)时"码流类型"残留 subhandleAdd 本就有 resetFields(),编辑路径漏了。

修复handleEditsetFieldsValue(cam) 前加 form.resetFields()

5. 移除"码流类型"字段(web/src/pages/Video.tsx

核查结论:经直接查询 WVP API 确认,WVP 不暴露"主码流/辅码流"属性,不存在"真实码流类型"可同步:

  • 设备级 /api/device/query/devicesstreamMode 字段为 null(且该字段实为 RTP 传输模式 TCP/UDP,非主/辅码流)
  • 通道级 /api/device/query/devices/{id}/channelsstreamId/streamIdentification 均为 null,响应无任何主/辅码流字段
  • silk 的 StartPlay(deviceId, channelId) 只传设备和通道,不传码流类型;GbStreamType 在 Go 代码里仅在 model 定义,从未被读取使用

处理:从添加/编辑表单移除"码流类型"字段。保留 dal/video.tsCamera 接口的 gbStreamType?: string 可选字段(后端模型仍有该列,对已存值的摄像头仍会返回)。摄像头 34020000002000000001id=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/videos5174 端口正常监听)
  • 编辑摄像头不再崩溃,"认证密码"正确回显,"码流类型"字段已移除且不再出现残留值

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_migrations1|f
  • 后端日志:数据库连接成功,版本化迁移完成 schemaVersion=1
  • 新二进制 SHA2569acb5fbfdb854b90b821d456f0f05177cd34dd8f3161686e1c472170af5d1201

回滚点

  • 数据库备份:/home/pan/backups/silk-20260813-234221.dump
  • 旧二进制:/home/pan/silk/server-go/server-go-linux.bak-20260813-234221
  • 回滚步骤:恢复旧二进制并重启,或先 pg_restore 恢复数据库备份;开发服务器无 git 仓库,不依赖 git tag。