35 KiB
变更记录
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 & |
工作流程
- 自动检测:Python 服务每 60 秒检查在线摄像头(通过 NestJS API)
- 开始录制:HTTP 从 ZLMediaKit 拉 fMP4 流 → 内存缓冲 → 每 5MB 调用
UploadPart - 分段归档:每 5 分钟
CompleteMultipartUpload收尾 → 调用 NestJS API 建库记录 → 自动开始下一段 - 手动控制:前端「摄像头管理」Tab 或 NestJS API 可手动开始/停止录制
验证结果
- 24 个旧落地文件在 18 秒内全部归档完成
- 不落地模式首个分段:17:06:42 开始,311 秒,152MB,29 个 UploadPart
- 上传速度:约 5MB/9 秒(localhost 传输)
- 数据库记录正确创建,前端「历史回看」可查询和播放
2026-07-10 新增移动端客户端(React Native APP + Taro 小程序)
变更背景
根据 spec.md「客户端」章节要求,系统需提供 Web 管理后台、微信小程序、移动 APP 三端客户端覆盖。此前仅有 Web 管理后台(web/)和 Go 后端(server-go/),缺少移动端 APP 和微信小程序。
变更内容
新增两个移动端客户端工程,完整对接现有 Go 后端 API(58 个 REST 端点 + WebSocket),实现登录认证、实时监测、设备控制、告警管理、阈值配置、视频监控等全功能。
新增工程
1. React Native 移动 APP(e:\silk\app\)
技术栈:React Native 0.74.5 + TypeScript + React Navigation 6 + Zustand + Axios + React Native Paper + react-native-video
页面(11 个):
| 页面 | 文件 | 功能 |
|---|---|---|
| 登录 | src/screens/LoginScreen.tsx |
账号密码登录,预填 admin/silk@123 |
| 仪表盘 | src/screens/DashboardScreen.tsx |
统计卡片 + 实时指标 + 趋势图 + 最近告警,30s 轮询 |
| 蚕房列表 | src/screens/RoomsScreen.tsx |
蚕房搜索、状态徽章 |
| 蚕房详情 | src/screens/RoomDetailScreen.tsx |
实时指标 + 趋势图 + 设备列表,30s 轮询 |
| 设备列表 | src/screens/DevicesScreen.tsx |
设备搜索、在线/离线状态、设备类型图标 |
| 设备控制 | src/screens/DeviceControlScreen.tsx |
开关、模式、风速、温度设置 |
| 告警中心 | src/screens/AlertsScreen.tsx |
未处理/全部筛选、确认告警、查看片段 |
| 阈值管理 | src/screens/ThresholdsScreen.tsx |
阈值 CRUD、启用/禁用开关 |
| 视频监控 | src/screens/VideoScreen.tsx |
摄像头列表 + 录像片段 Tab 切换 |
| 视频播放 | src/screens/VideoPlayerScreen.tsx |
HLS 直播流播放 |
| 设置 | src/screens/SettingsScreen.tsx |
用户信息、API 地址配置、退出登录 |
API 层(src/api/):client.ts、auth.ts、rooms.ts、devices.ts、telemetry.ts、alarms.ts、thresholds.ts、video.ts
状态管理:src/store/authStore.ts(Zustand + AsyncStorage 持久化)、src/store/appStore.ts
工具:src/utils/ws.ts(WebSocket 管理器,自动重连)、src/utils/format.ts(格式化)
组件:src/components/StatCard.tsx、src/components/MiniChart.tsx(纯 RN 组件实现的柱状图)
2. Taro 微信小程序(e:\silk\miniapp\)
技术栈:Taro 4.1.9 + React 18 + TypeScript + SCSS + Zustand,一套代码输出微信小程序和 H5
页面(10 个):
| 页面 | 路径 | 功能 |
|---|---|---|
| 登录 | pages/login/index |
账号密码登录 |
| 仪表盘 | pages/dashboard/index |
概览卡片 + 蚕房选择 + 实时遥测 + 最近告警,30s 轮询 + WebSocket 推送 |
| 蚕房列表 | pages/rooms/index |
蚕房卡片列表 |
| 蚕房详情 | pages/rooms/detail/index |
蚕房信息 + 设备遥测 |
| 设备 | pages/devices/index |
设备列表 + 筛选 + 控制按钮 |
| 告警 | pages/alerts/index |
告警列表 + 确认 + 查看录像 |
| 阈值管理 | pages/thresholds/index |
阈值 CRUD + 弹窗表单 |
| 视频监控 | pages/video/index |
摄像头列表 + 录像回放 |
| 视频播放 | pages/video/player/index |
全屏视频播放,HLS/FLV/WebRTC 切换 |
| 设置 | pages/settings/index |
用户信息 + 退出登录 |
TabBar:5 个底部标签(仪表盘/蚕房/设备/告警/视频),含 10 个 SVG 图标
API 层:src/api/request.ts(Taro.request 封装,JWT 拦截器)、auth.ts、rooms.ts、devices.ts、telemetry.ts、alarms.ts、thresholds.ts、video.ts
构建与部署配置
React Native APP 构建配置
| 配置项 | 值 | 说明 |
|---|---|---|
| Gradle 镜像 | https://mirrors.cloud.tencent.com/gradle/gradle-8.6-all.zip |
解决 Gradle 下载超时 |
| Maven 镜像 | https://maven.aliyun.com/repository/google / public |
加速依赖下载 |
| JDK | E:\openjdk-17.0.2_windows-x64_bin\jdk-17.0.2 |
JDK 17(AGP 要求) |
| NDK | 26.3.11579264 |
原 26.1.10909125 损坏,替换为可用版本 |
| media3 版本 | 强制 1.4.1 |
react-native-video 依赖的 media3 1.8.0 要求 SDK 35,降级兼容 SDK 34 |
| API 地址 | http://100.83.103.1:3000/api/v1 |
后端 Tailscale IP |
| WebSocket | ws://100.83.103.1:3000/ws |
实时推送 |
| Android 包名 | com.silkmonitor |
显示名「蚕房监控」 |
Taro 小程序构建配置
| 配置项 | 值 | 说明 |
|---|---|---|
| H5 devServer proxy | /api -> http://localhost:3000 |
H5 调试时代理转发 |
| 小程序 API 地址 | http://100.83.103.1:3000/api/v1 |
直连后端 |
project.config.json |
urlCheck: false |
允许 localhost 开发请求 |
共同特性
- JWT 认证,401 自动清除 Token 并跳转登录页
- WebSocket 实时推送(遥测数据、告警事件)
- 8 个 API 模块完整对接后端
- 设备远程控制(
POST /control/send) - 视频流播放(HLS)
- 中文 UI
- 30 秒轮询 + WebSocket 双重实时数据更新
验证结果
- React Native APP:
tsc --noEmit零错误通过;Android APK 构建成功并安装到物理设备(OXF-AN10, Android 12),登录成功 - Taro 小程序:H5 和 weapp 均编译成功(
webpack 5.78.0 compiled successfully)
2026-07-14 物联网设备接入 + 录制状态修复 + 前端新增能耗管理和设备控制页面
变更背景
- WVP 迁移到物理机 Docker 后,设备离线时录制流中断但前端一直显示"录制中",再次点击"开始录制"返回 503
- VerneMQ 服务未设置开机自启,物理机重启后 MQTT 不可用
- 需要接入 GSTMB1 温湿度传感器和 GSPE1B 智能插座
- 前端缺少能耗管理和设备控制入口
变更内容
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 分钟内未响应才标记离线 |
心跳命令
{"type":"info","messageId":"<timestamp_ms>"}
与前端"查询信息"按钮下发的命令相同,设备响应后 handleMessage 自动更新 last_seen 和在线状态。
预期效果
- 设备真正离线时:约 10 分钟后(5 分钟超时 + 5 分钟心跳宽限)才标记离线
- 设备重启后:最多 5 分钟内自动恢复在线(下次 checkOffline 触发心跳)
- 用户操作不变:前端"查询信息"按钮仍可手动触发 info,与心跳互不影响
- 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 个空调按钮):
- 点击"开始学习" → 下发
learn no=1,关机按钮闪烁 - 用户用空调遥控器对准红外控制器按下关机键
- 听到"嘀"声 → 关机按钮变绿,自动下发下一个
learn命令 - 按顺序学习:关机 → 开机 → 制冷 → 制热 → 除湿
- 全部完成显示"全部学习完成"
单独学习(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 命令时记录编号到pendingLearnNohandleMessage收到 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 权限改造 + 视频播放与录制状态修复
变更背景
- 系统原为多用户双角色(user/admin)架构,非严格 RBAC,需改造为多角色 + 权限码控制
- RBAC 改造后用户反馈
/videos实时监控 FLV 播放失败、历史回看查不到录像 - 用户对录制状态有疑问,前端"摄像头管理"显示状态与实际录制情况不一致
变更内容
1. RBAC 权限系统改造(前后端)
引入 4 个角色(admin/operator/viewer/farmer)+ 15 个权限码,前端实现路由守卫+菜单+按钮级权限控制。
后端新增文件:
| 文件 | 说明 |
|---|---|
server-go/internal/model/permission.go |
定义 Permission、RolePermission 模型和 4 个角色常量 |
server-go/internal/model/permission_seed.go |
15 个权限码和角色-权限映射种子数据 |
server-go/internal/middleware/permission.go |
RequirePermission 中间件,带 5 分钟内存缓存,admin 自动放行 |
server-go/internal/handler/permission.go |
GET /permissions 和 GET /roles 查询 API |
后端修改文件:
| 文件 | 变更内容 |
|---|---|
server-go/internal/database/db.go |
AutoMigrate 加入 Permission、RolePermission 表;新增 seedPermissions() 初始化种子数据 |
server-go/internal/handler/auth.go |
meHandler 与 buildLoginPayload 返回 permissions 列表;注册默认角色从 user 改为 viewer;新增 getUserPermissionCodes() |
server-go/internal/handler/user.go |
AdminMiddleware → RequirePermission("user:manage");新增角色有效性校验 |
server-go/internal/handler/audit.go |
AdminMiddleware → RequirePermission("audit:read") |
server-go/internal/handler/{room,device,sensor,threshold,alarm,alarm_clip,control,video_camera,video_clip,video_record,telemetry,notification,storage}.go |
各路由添加对应权限中间件 |
server-go/cmd/server/main.go |
移除 admin 路由组;新增 RegisterPermissionRoutes 注册 |
前端新增/修改文件:
| 文件 | 说明 |
|---|---|
web/src/components/HasPermission.tsx(新增) |
按钮级权限组件,根据 authService.hasPermission(permission) 控制显隐 |
web/src/dal/auth.ts |
LoginResp.user 加 permissions 字段;新增 MeResp 接口类型 |
web/src/services/auth.ts |
新增 USER_KEY 存储、getUser()、getPermissions()、hasPermission()、refreshUser() |
web/src/router.tsx |
新增 RequirePermission 路由守卫组件,每个路由配置对应权限码 |
web/src/layout/BasicLayout.tsx |
menuData 每项加 permission 字段;菜单渲染按权限过滤;头像显示真实用户名和角色中文名 |
权限矩阵:
| 角色 | 权限码 |
|---|---|
| admin | 自动放行所有接口 |
| operator | dashboard:view, room:read/write, device:read/control, threshold:read/write, alarm:read/ack, video:read/record, energy:view, log:read, audit:read |
| viewer | dashboard:view, room:read, device:read, threshold:read, alarm:read, video:read, energy:view, log:read |
| farmer | dashboard:view, room:read, device:read/control, threshold:read, alarm:read/ack, video:read, energy:view |
2. ZLMediaKit 流媒体配置修复
通过 ZLM setServerConfig API 修正运行时配置(WVP auto-config 未正确应用 ZLM_HOOK_HOST 环境变量):
| 配置项 | 修改前 | 修改后 | 说明 |
|---|---|---|---|
general.streamNoneReaderDelayMS |
20000 | 3600000 | 避免无人观看 20 秒后流被关闭 |
hook.on_publish/on_play 等 11 项 |
http://127.0.0.1:18080/... |
http://polaris-wvp:18080/... |
修正 ZLM 容器内 hook URL(127.0.0.1 指向 ZLM 自身而非 WVP) |
新增脚本: /home/pan/silk/fix_zlm_hook.py(服务器留存)— 调用 ZLM API 修复 hook URL 和 streamNoneReaderDelayMS。WVP 重启后若 hook 配置被覆盖回 127.0.0.1,执行 python3 /home/pan/silk/fix_zlm_hook.py 即可修复。
WVP 配置修改: stream-on-demand: true → false(避免 hook 返回"关闭=true"导致流被关闭)
3. 前端 FLV 播放跨域问题修复
修改文件: server-go/internal/service/media.go 的 fixZlmPort 函数
变更前: 返回绝对路径 http://100.83.103.1:8081/rtp/xxx.live.flv?...
变更后: 返回相对路径 /rtp/xxx.live.flv?...
原因: web/server.cjs 的 CSP 头 connect-src 'self' ws: wss: 只允许同源请求,flv.js 跨域 fetch ZLMediaKit 时被浏览器阻止,报 "NetworkError - Exception"。改为相对路径后,前端浏览器走 server.cjs 内置的 /rtp/ 代理转发到 127.0.0.1:8081,避免跨域。
4. 前端历史回看时间范围修复
修改文件: web/src/pages/Video.tsx PlaybackTab
| 修改点 | 变更前 | 变更后 |
|---|---|---|
dateRange state 类型 |
[Dayjs, Dayjs] |
[Dayjs, Dayjs] | null |
RangePicker onChange 清空 |
不更新 dateRange(保持默认今天) | 设为 null |
handleSearch 查询参数 |
硬编码 to: dayjs().toISOString() |
仅当 dateRange 非空时传 from/to,为 null 时查全部 |
listClips 调用 |
始终传 from/to | ClipQuery 仅在 dateRange 非空时设置 from/to |
新增类型导入: web/src/pages/Video.tsx 引入 type ClipQuery
5. 录制状态查询改进
修改文件: server-go/internal/handler/video_record.go 的 listActiveRecordings 函数
变更前: 仅读 Go 后端内存 map activeRecordings,Go 后端重启后 map 清空,前端显示"开始录制"按钮(即使 recorder-go 仍在实际录制)。
变更后: 优先调用 recorder-go 的 /record/status 接口获取真实录制状态,并自愈内存 map。recorder-go 不可用时回退到内存 map。
环境配置
| 配置项 | 值 | 说明 |
|---|---|---|
WVP_API_BASE |
http://localhost:18080 |
WVP HTTP API 实际端口(start.sh 已正确配置) |
| admin 密码 | admin123 |
已通过 bcrypt 重置 |
| ZLM hook URL | http://polaris-wvp:18080/index/hook/... |
容器间通过容器名访问 |
| ZLM streamNoneReaderDelayMS | 3600000(1 小时) |
避免短时间关流 |
| WVP stream-on-demand | false |
不按需拉流 |
验证结果
- 后端
go build ./...通过 - 前端
npm run build通过 - WVP
/api/play/start点播接口返回code=0成功,包含 FLV/HLS/FMP4 地址 - Go 后端
/api/v1/video/cameras/1/live返回相对路径 FLV URL - 通过
server.cjs5174 端口代理访问 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)首次启动时出现登录导航错误,无法正常显示登录页面。
问题根因
- 导航器条件渲染:
AppNavigator.tsx中token ? ... : <Login>的条件渲染导致当 token 存在时(即使过期),导航器只注册 Main 屏幕,不包含 Login 屏幕。当后端返回 401 时,拦截器尝试导航到 Login,但导航器中找不到该屏幕。 - 恢复会话逻辑缺陷:
authStore.ts的restoreSession设置过期 token 后,fetchUsercatch 块静默忽略错误(不抛出异常),导致restoreSession的 try-catch 无法捕获到 token 无效,无法自动清除。 - 拦截器竞态条件:响应拦截器在导航器初始化完成前就尝试调用
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