iotsaas/docs/中控屏对接设计.md
2995840376 0ce191c448 chore: 初始化 iotsaas 版本库基线
纳入范围:
- backend/  Spring Boot 多模块后端(iot-common/device/rule/data/alarm/auth)
- frontend/admin/  Vue3 + Element Plus 管理后台
- Android/   涂鸦原生 APP (com.xhai.iot)
- backend/sql/  Flyway 风格迁移脚本(V2~V21)
- docs/       对接文档与设计文档

排除范围:
- 构建产物: node_modules/ target/ build/ dist/ .gradle/
- 签名密钥: *.jks *.keystore (严禁入库)
- 本机配置: Android/local.properties
- 运行日志: logs/ *.log
- AI 协作本地数据 .codebuddy/ 与第三方 demo tuya-demo/

.gitattributes 统一以 LF 存储, Windows 批处理保留 CRLF
2026-10-27 12:00:41 +08:00

241 lines
13 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.

# 10.1 寸中控屏对接设计文档
> 状态:讨论定稿(待开发)
> 日期:2026-09-18
> 范围:中控屏(家庭内常驻大屏)与现有 IoT SaaS 后端的对接方案,涵盖认证、接口、实时推送、安全。
---
## 1. 背景与目标
现有系统(后端 `iot-device` 9081)已具备完整的 **家庭/房间/设备/涂鸦/动态** 能力,手机 APP 通过会员账号(JWT `userType=member`)访问,家庭权限靠 `homeMember` 表校验。
现在要对接一块 **10.1 寸中控屏**,功能与手机 APP 类似:家庭/房间管理、设备管理/控制、场景、近期动态。目标是让中控屏以**安全、隔离、实时**的方式复用现有业务能力,同时**不破坏手机端现有逻辑**。
---
## 2. 决策摘要(已确认方案)
| 维度 | 决策 | 说明 |
|------|------|------|
| 身份模型 | **专属设备凭证**(非会员账号登录) | 中控屏是家庭设备,不存个人密码;出厂 SN + 激活码,APP 扫码绑定家庭后签发 panel JWT |
| 接口组织 | **独立命名空间 `/api/v1/panel`** | 与手机接口清晰隔离,便于限流、审计、差异化(聚合接口) |
| 实时推送 | **WebSocket 推送** | 新增 WSS 端点,按 homeId 分组推送状态/场景结果,手机后续也可复用 |
| 家庭归属 | **单一固定家庭** | 激活后绑定一个家庭,所有接口强制限定该 homeId |
---
## 3. 整体架构
```
┌────────────┐ HTTPS/WSS ┌──────────────────────────────┐
│ 中控屏 │ ──────────────▶ │ iot-device (9081) │
│ (10.1"屏) │ │ /api/v1/panel/* 控制器 │
│ │ ◀──── WSS 推送 ── │ /panel/ws WebSocket 端点 │
└────────────┘ │ │ │
│ 激活(扫码) │ ▼ │
▼ │ PanelAuth 拦截器(deviceType+ │
┌────────────┐ │ homeId+session 校验) │
│ 手机 APP │ ── 扫码绑定 ───▶ │ │ │
└────────────┘ │ ▼ │
│ 复用现有 Service 层 │
│ (Home/Room/Device/Tuya/ │
│ Activity Service) │
│ │ │
│ ▼ │
│ DeviceEventPublisher ──▶ RabbitMQ
│ (设备数据变化) │ │
│ ▼ │
│ WS 广播(按homeId)
└──────────────────────────────┘
```
核心思路:**数据模型与业务 Service 完全复用**,新增的只是「设备身份鉴权层」+「panel 专属控制器(含聚合接口)」+「WebSocket 推送通道」。
---
## 4. 认证与安全设计
### 4.1 设备激活流程(出厂 SN + 激活码 + APP 扫码绑定)
1. 出厂阶段:每台中控屏烧录唯一 `SN`,并预置一次性 `激活码`(服务端可校验的凭证,哈希存储)。
2. 首次开机:中控屏展示 **二维码**(含 SN + 激活码),提示"用手机 APP 扫码绑定家庭"。
3. 手机 APP 扫码 → 调用后端绑定接口,选择一个家庭(homeId)→ 后端:
- 校验激活码有效且未使用;
- 创建/更新 `panel_device` 记录,写入 `home_id`、`bound_member_id`、`status=已绑定`;
- 标记激活码已消费(一次性)。
4. 中控屏用 `SN + 激活码` 调用 `/panel/auth/token` 换取 **panel JWT**(access + refresh)。
5. 后续所有请求携带 panel JWT;access 过期用 refresh 静默续期(与手机机制一致)。
> 安全优势:中控屏**从不接触任何个人会员密码**,只持有设备凭证 + 短期 JWT,即使设备被物理获取,也只能访问已绑定家庭的有限接口。
### 4.2 Panel Token 设计
在现有 `com.iot.common.util.JwtUtil` 基础上扩展 panel token 生成,claims 增加:
| claim | 值 | 用途 |
|-------|----|------|
| `sub` | panel_device.id | 设备主键 |
| `deviceType` | `panel` | 与 `userType=member/admin` 区分 |
| `homeId` | 绑定家庭 ID | 强制 home 隔离 |
| `scope` | `home:read,home:control` | 权限最小化 |
| `sessionId` | 本次会话 ID | 支持远程吊销 |
| `userType` | `panel`(兼容现有解析) | — |
access 有效期建议 **30 分钟**,refresh 滚动续期;refresh 接口校验 `deviceType=panel` 且 session 仍有效。
### 4.3 鉴权隔离
新增 **PanelAuth 拦截器**(或在现有 JwtAuthFilter 扩展分支):
- 校验 `deviceType=panel`;
- 从 token 取出 `homeId`,所有 `/panel` 接口请求参数中的 homeId **必须以 token 中的 homeId 为准**,禁止越权访问其它家庭;
- 校验 `sessionId` 未被吊销(`panel_device.status` 非禁用);
- **禁止** panel token 访问 `/auth/member`、`/admin`、`/users`、`/roles` 等会员/后台接口(路径白名单隔离)。
### 4.4 安全清单 / 风险缓解
- **传输安全**:全链路 HTTPS;中控屏出厂内置服务端证书指纹(证书固定 / pinning),防局域网中间人。
- **凭证安全**:激活码一次性、可吊销、限尝试次数;中控屏仅加密存储 refresh token,不存任何密码。
- **权限最小化**:panel 仅 `home-scoped`,不能访问会员/管理员接口;homeId 由 token 锁定。
- **设备吊销**:手机 APP「中控屏管理」可解绑/禁用某 SN → `panel_device.status=disabled` 且 session 失效,已签发 JWT 在下一次校验被拒。
- **限流**:激活、换 token、控制等敏感操作独立限流(如 5 次/分钟/IP)。
- **审计**:所有中控屏控制写入 `device_activity_log`,`source=central`,并关联 `panel_id`,可追溯。
- **防重放(可选二期)**:敏感写操作要求 `nonce + timestamp` 签名。
- **远程禁用**:管理员可在后台按 SN 拉黑设备。
---
## 5. 接口设计(`/api/v1/panel`)
### 5.1 认证类
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/v1/panel/auth/activate` | 提交 SN + 激活码,校验有效性,返回待绑定态 |
| POST | `/api/v1/panel/auth/token` | SN + 激活码 换取 access/refresh(绑定完成后) |
| POST | `/api/v1/panel/auth/refresh` | 刷新 token(滚动续期,校验 session) |
| POST | `/api/v1/panel/auth/unbind` | 解绑/注销(使 session 失效) |
> 手机 APP 侧的「扫码绑定家庭」接口:复用现有家庭接口,新增一个绑定确认端点(如 `/api/v1/homes/{homeId}/bind-panel`),仅 owner/admin 可操作。
### 5.2 家庭 / 房间 / 设备(复用现有 Service)
| 方法 | 路径 | 复用 |
|------|------|------|
| GET | `/api/v1/panel/homes/{homeId}/summary` | **新增聚合接口**:首页一屏拉取(房间、设备+状态、场景、近期动态) |
| GET | `/api/v1/panel/homes/{homeId}/rooms` | `RoomService` |
| GET | `/api/v1/panel/homes/{homeId}/devices` | `DeviceService`(含在线状态) |
| GET | `/api/v1/panel/homes/{homeId}/devices/{deviceId}` | `DeviceService.getDeviceInfo` |
| POST | `/api/v1/panel/devices/{deviceId}/commands` | `DeviceService.sendCommand` / `TuyaCloudService.sendCommand`(与 `DeviceController.sendCommand` 同源逻辑) |
| GET | `/api/v1/panel/homes/{homeId}/activities` | `DeviceActivityService`(近期动态) |
> 注:现有控制接口在 `DeviceController.sendCommand`(`/api/v1/devices/{deviceId}/commands`)与 `TuyaController.sendCommand`(`/api/v1/tuya/devices/{deviceId}/commands`)。panel 控制器内部复用同一 `DeviceService`/`TuyaCloudService`,仅替换鉴权维度为 panel+homeId。
### 5.3 场景(当前无 SceneController,需新建,与首页场景对齐)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/v1/panel/homes/{homeId}/scenes` | 场景列表(推荐场景) |
| POST | `/api/v1/panel/homes/{homeId}/scenes/{sceneId}/execute` | 执行场景 |
> 待确认:首页「推荐场景 / 灯光 / 空调 / 安防」当前后端落地程度(是否已有场景实体与执行链路),需在开发前与首页场景功能一并规划。
### 5.4 近期动态
复用 `DeviceActivityController`(`/api/v1/devices/activities`),panel 下限定 homeId 过滤;首页聚合接口 `summary` 直接内联最近 N 条动态,点击进入查看完整列表(复用 activities 列表 + 详情页)。
---
## 6. WebSocket 实时推送设计
### 6.1 端点与鉴权
- 端点:`/api/v1/panel/ws?token={panelJWT}`(WS 握手无法自定义 Header,token 放 query;配合 WSS 加密)。
- 握手时校验 panel JWT(deviceType + homeId + session),失败立即关闭。
- 连接成功后,将该 Session 加入内存 **`homeId → Set<Session>`** 路由表。
### 6.2 推送消息结构
```json
{
"type": "device.status | device.online | device.offline | scene.executed | home.updated",
"homeId": 123,
"payload": { "deviceId": "...", "dps": {...}, "ts": 1234567890 }
}
```
### 6.3 与现有事件机制衔接
现有 `DeviceEventPublisher` 已通过 **RabbitMQ**(`EXCHANGE` + `ROUTING_DEVICE_DATA_CHANGE`)发布设备数据变化事件。
- 新增一个 **WS 广播监听器**:消费设备数据变化事件 → 根据设备归属 homeId → 推送给该 home 所有在线 panel(及后续手机)WS 连接。
- 控制成功(`DeviceService.sendCommand`)后同样 `publishDeviceDataChange` → 同 home 所有客户端实时刷新。
- 上下线(涂鸦云推送 `TuyaMessageSubscribeService`)也纳入广播。
> 这样中控屏与手机控制/状态天然一致,无需各自轮询。
### 6.4 客户端职责
心跳保活 + 断线指数退避重连;重连后增量拉取(调用 `summary`/`devices`)补齐离线期间变更。
---
## 7. 数据模型变更
新增表 `panel_device`(MySQL):
| 字段 | 类型 | 说明 |
|------|------|------|
| id | bigint PK | 设备主键 |
| sn | varchar(64) UNIQUE | 出厂序列号 |
| activation_code_hash | varchar(128) | 激活码哈希(一次性) |
| home_id | bigint | 绑定家庭(单一固定) |
| bound_member_id | bigint | 操作绑定的会员(审计/解绑) |
| name | varchar(64) | 中控屏昵称 |
| status | tinyint | 0 未激活 / 1 已绑定 / 2 已禁用 |
| session_id | varchar(64) | 当前会话(用于吊销) |
| last_active_at | datetime | 最后活跃 |
| create_time / update_time | datetime | — |
> 现有 `device_activity_log` 已有 `source` 字段(app/panel/voice/scene/cloud_push),新增 `central` 取值并加 `panel_id` 列用于审计关联。
---
## 8. 与手机端一致性 & 不破坏现有
- **不改动**现有 `/api/v1/homes`、`/rooms`、`/devices`、`/tuya`、`/devices/activities` 及其鉴权(member 维度)。
- 新增 `/panel` 命名空间与 PanelAuth 拦截器,是**叠加**而非替换。
- 业务 Service 层复用,避免逻辑分叉。
- WebSocket 推送对手机端是**可选增强**:手机后续可接同一 WS 通道,移除轮询,逐步收敛到统一实时模型。
---
## 9. 分阶段实施计划
- **P0 基础**:`panel_device` 表 + 激活/换 token + PanelAuth 拦截器 + 证书固定准备。
- **P1 核心接口**:`/panel` 家庭/房间/设备/控制/动态(复用 Service),中控屏可完成基础操控。
- **P2 实时**:WebSocket 端点 + RabbitMQ 事件 → WS 广播,首页聚合 `summary` 实时刷新。
- **P3 场景**:场景列表/执行接口(与首页场景功能对齐落地)。
- **P4 安全加固 & 管理**:限流、审计、远程禁用;手机 APP「中控屏管理」页面(绑定/解绑/禁用)。
---
## 10. 待确认 / 开放问题
1. 激活码的生成与下发方式(出厂预置固件 vs 后台按 SN 生成)。
2. 首页「推荐场景 / 灯光 / 空调 / 安防」后端当前落地程度 —— 需先与首页场景功能统一规划(P3 依赖)。
3. 是否允许同一家庭绑定多台中控屏(建议允许 N 台,均 home-scoped)。
4. 中控屏是否要做「局域网内直连网关/设备」低延迟控制(本期先走云端,局域网优化留二期)。
5. 设备重置(恢复出厂)后激活码失效策略:需后台支持重新生成激活码。
---
## 附录:现有可复用代码锚点
- 控制:`DeviceController.sendCommand`(`/api/v1/devices/{deviceId}/commands`)、`TuyaController.sendCommand`(`/api/v1/tuya/devices/{deviceId}/commands`)
- 事件:`DeviceEventPublisher.publishDeviceDataChange`(RabbitMQ 设备数据变化)
- 鉴权:`com.iot.common.util.JwtUtil`(现有 JWT,已含 `userType` claim,可扩展 `deviceType/homeId/sessionId`)
- 动态:`DeviceActivityController` + `device_activity_log`(含 `source`)
- 家庭权限:`HomeService` / `RoomServiceImpl` 的 `homeMember` 校验逻辑(panel 改为 homeId 锁定)