纳入范围: - 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
929 lines
38 KiB
Markdown
929 lines
38 KiB
Markdown
# 中控屏开放接口文档(第三方对接版)
|
||
|
||
> 版本:**v2.1**
|
||
> 状态:已实现契约(严格对齐后端 P1 / P2 / P3 实际代码)
|
||
> 适用对象:10.1 寸中控屏设备厂商 / 第三方接入方
|
||
> 更新日期:2026-10-22
|
||
> 说明:本文档取代 v1.0(WebSocket 设计稿)。生产环境已落地,**实时通道为「长轮询 + SSE 双通道」,不再使用 WebSocket**;v2.1 新增**会员侧 APP 扫码自助绑定**。
|
||
|
||
---
|
||
|
||
## 1. 概述
|
||
|
||
本接口用于 **中控屏(家庭内常驻大屏设备)** 接入 MumIoT 平台,实现家庭/房间管理、设备管理与本地执行、场景执行、OTA、运营配置、诊断上报以及**家庭增量事件实时推送**。
|
||
|
||
中控屏使用**专属设备身份**:出厂烧录 SN,由平台管理端预置一次性**初始化凭证 `bootstrapSecret`**(替代旧版的「激活码 + 扫码激活」流程)。屏与家庭的绑定支持两种模式:
|
||
|
||
- **后台预置即绑定**(装维/厂家场景):管理端预置屏时直接指定 `homeId`,屏用 `bootstrapSecret` 建会话即可拿到 `homeId`。
|
||
- **用户 APP 扫码自助绑定**(零售/用户场景):管理端预置屏时 `homeId` 留空(待绑定库存),用户用 APP 扫描屏上二维码(含 SN)调会员接口把屏绑定到自己拥有的家庭,详见 **3.5 节**。
|
||
|
||
无论哪种模式,屏均用 `bootstrapSecret` 建立会话,由平台签发设备级 JWT(与会员账号体系隔离)。中控屏**不持有任何个人会员密码**,仅持有短期设备 Token。
|
||
|
||
### 1.1 接入流程
|
||
|
||
```
|
||
[出厂] 设备烧录 SN;管理端预置屏终端并下发一次性 bootstrapSecret
|
||
│
|
||
▼
|
||
[开机] 中控屏展示绑定二维码(内容为 SN;若管理端已预置 homeId 则无需用户操作)
|
||
│ ◀── 待绑定库存的屏:用户 APP 扫码 → 预检 bindable → 选择家庭 → 绑定(见 3.5)
|
||
▼
|
||
[建会话] POST /api/v1/panels/session
|
||
│ Header: Authorization: PanelBootstrap <bootstrapSecret>
|
||
│ 返回 accessToken / refreshToken / homeId / panelId / stateEpoch …
|
||
▼
|
||
[业务] 携带 Authorization: Bearer <accessToken> 调用 /api/v1/panels/* 接口
|
||
│
|
||
▼
|
||
[实时] 二选一接收家庭增量事件:
|
||
├─ 长轮询 GET /api/v1/panels/{panelId}/events?cursor=…&timeout=…
|
||
└─ SSE 流 GET /api/v1/panels/{panelId}/events/stream?cursor=… (推荐)
|
||
```
|
||
|
||
### 1.2 环境
|
||
|
||
| 环境 | Base URL | 说明 |
|
||
|------|----------|------|
|
||
| 测试 | `https://dev-api.xhai.example.com` | 联调使用 |
|
||
| 生产 | `https://api.xhai.example.com` | 正式环境 |
|
||
|
||
- 所有接口基于 **HTTPS**。中控屏出厂建议内置服务端证书指纹(证书固定)以防局域网中间人。
|
||
- **不使用 WebSocket**;实时通道为 SSE(`text/event-stream`,HTTPS 长连接)或长轮询,二者共用同一事件源。
|
||
|
||
---
|
||
|
||
## 2. 通用约定
|
||
|
||
### 2.1 协议与编码
|
||
|
||
- 传输协议:HTTPS(SSE 亦为 HTTPS 长连接)
|
||
- 请求/响应体:`application/json; charset=UTF-8`(SSE 响应为 `text/event-stream`)
|
||
- 时间字段:`serverTime` 为**毫秒时间戳**(Long);部分字段为字符串时间
|
||
- 所有请求体字段使用 **小驼峰**(camelCase)
|
||
- ID 统一对外为字符串(`panelId` / `homeId` / `deviceId` / `memberId` 等)
|
||
|
||
### 2.2 统一响应结构
|
||
|
||
所有 HTTP 接口(SSE 除外)返回统一包装:
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "操作成功",
|
||
"data": { },
|
||
"timestamp": 1758200000000
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| code | Integer | `200` 表示业务成功,非 200 表示失败(见错误码表) |
|
||
| message | String | 提示信息 |
|
||
| data | Object | 业务数据,失败时为 null |
|
||
| timestamp | Long | 响应时间戳(毫秒) |
|
||
|
||
### 2.3 鉴权
|
||
|
||
- **建立会话**:`POST /api/v1/panels/session` 使用 `Authorization: PanelBootstrap <bootstrapSecret>`(不带 Bearer,不带 JWT)。
|
||
- **业务调用**:其余所有 `/api/v1/panels/**` 接口使用 `Authorization: Bearer <accessToken>`。
|
||
- **管理端接口**:`/api/v1/admin/**` 使用管理员 JWT(非设备 Token)。
|
||
|
||
> 中控屏绑定的 `homeId` 由 Token 锁定。接口路径中的 `{panelId}` 必须与当前会话一致(服务端 `requireSelf` 强制校验),家庭范围只从 Token 解析,忽略任何请求参数;传入不一致将返回 `403`。
|
||
|
||
### 2.4 标准 HTTP 语义(panel-v1)
|
||
|
||
屏端请求可带请求头 `X-MUM-Protocol: panel-v1`。带该头时,命令发起接口(见 4.5)在设备由本地执行屏执行的情况下,返回 **HTTP 202 ACCEPTED + `state=QUEUED`**(命令已受理但未执行,真正的物理动作由执行屏领取完成)。**旧客户端不带该头则保持原有同步语义,无感知**。
|
||
|
||
> 屏端判定成功应以「HTTP 状态码 + `data.result.state`」为准,**不得硬编码 `code === 0`**(平台统一 `code=200` 表示业务成功)。
|
||
|
||
### 2.5 分页
|
||
|
||
不同接口分页方式不同:
|
||
|
||
- **家庭快照设备分页**:游标式 `pageToken` + `limit`(见 4.1),非 page/size。
|
||
- 其余列表接口(管理端列表等)按需使用 `page` / `size`(默认 20,最大 100)。
|
||
|
||
### 2.6 幂等
|
||
|
||
- 设备登记(`localDeviceId` 稳定)天然幂等:重复登记返回既有映射(`created=0`)。
|
||
- 状态上报、命令回报、场景回报均带递增序号/ `reportId`,服务端去重(旧回执不覆盖新状态)。
|
||
- 命令发起带 `X-MUM-Protocol: panel-v1` 时生成唯一 `commandId`,可据此追踪。
|
||
|
||
---
|
||
|
||
## 3. 认证与会话
|
||
|
||
### 3.1 建立会话
|
||
|
||
`POST /api/v1/panels/session`
|
||
|
||
Header:`Authorization: PanelBootstrap <bootstrapSecret>`
|
||
|
||
**请求体**
|
||
|
||
```json
|
||
{
|
||
"serialNumber": "PANEL2026ABC123",
|
||
"installationId": "inst-001",
|
||
"bootId": "boot-20261022-1",
|
||
"model": "Panel10",
|
||
"appVersion": "1.0.0",
|
||
"protocolVersion": "1.0",
|
||
"features": ["matter", "zigbee"]
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| serialNumber | String | 是 | 屏序列号 |
|
||
| installationId | String | 是 | 安装实例 ID(一台设备重装后可能变化) |
|
||
| bootId | String | 是 | 当前启动实例 ID(每次冷启动变化;命令/上报需与之匹配) |
|
||
| model | String | 是 | 屏型号 |
|
||
| appVersion | String | 是 | 应用版本 |
|
||
| protocolVersion | String | 是 | 协议版本 |
|
||
| features | String[] | 否 | 本屏支持的协议/功能 |
|
||
|
||
**响应 data(Session)**
|
||
|
||
```json
|
||
{
|
||
"panelId": "3",
|
||
"gatewayDeviceId": "gw-xxxx",
|
||
"homeId": "1",
|
||
"memberId": "1001",
|
||
"role": "GATEWAY",
|
||
"accessToken": "eyJ...",
|
||
"refreshToken": "eyJ...",
|
||
"tokenType": "Bearer",
|
||
"expiresIn": 1800,
|
||
"refreshExpiresIn": 2592000,
|
||
"stateEpoch": 5,
|
||
"heartbeatIntervalSeconds": 30,
|
||
"leaseSeconds": 90,
|
||
"maxWaitSeconds": 20,
|
||
"serverTime": 1792668443000
|
||
}
|
||
```
|
||
|
||
| 字段 | 说明 |
|
||
|------|------|
|
||
| panelId | 平台中控 ID(后续路径使用) |
|
||
| homeId | 绑定的家庭 ID(锁定) |
|
||
| role | `GATEWAY`(可本地执行设备命令)/ `DISPLAY`(仅展示与提交控制) |
|
||
| accessToken / refreshToken | 访问/刷新令牌 |
|
||
| expiresIn / refreshExpiresIn | 有效期(秒),默认 1800 / 2592000 |
|
||
| stateEpoch | 状态世代,bootId 变化后递增 |
|
||
| heartbeatIntervalSeconds | 建议心跳间隔(秒) |
|
||
| leaseSeconds | 在线租期(秒),心跳续租 |
|
||
| maxWaitSeconds | 长轮询最大等待(秒) |
|
||
| serverTime | 服务器时间(毫秒),用于估算时差 |
|
||
|
||
### 3.2 刷新会话
|
||
|
||
`POST /api/v1/panels/session/refresh`
|
||
|
||
**请求体**
|
||
|
||
```json
|
||
{ "refreshToken": "eyJ...", "bootId": "boot-20261022-1" }
|
||
```
|
||
|
||
**响应 data(RefreshResponse)**:`accessToken` / `refreshToken` / `tokenType` / `expiresIn` / `refreshExpiresIn` / `serverTime`。旧的 refreshToken 立即作废(轮换)。
|
||
|
||
> 建议 accessToken 剩余 ~10% 有效期时主动刷新;刷新失败(refresh 过期/被吊销)则重新建立会话(3.1)。
|
||
|
||
### 3.3 屏心跳
|
||
|
||
`POST /api/v1/panels/{panelId}/heartbeat`
|
||
|
||
续在线租期(租期自收到有效心跳起算)。
|
||
|
||
**请求体(Heartbeat)**
|
||
|
||
```json
|
||
{
|
||
"bootId": "boot-20261022-1",
|
||
"appVersion": "1.0.0",
|
||
"networkType": "wifi",
|
||
"uptimeSeconds": 3600,
|
||
"deviceCounts": { "matter": 12, "zigbee": 3 },
|
||
"protocols": { "matter": "READY", "zigbee": "READY" }
|
||
}
|
||
```
|
||
|
||
**响应 data(HeartbeatResponse)**:`serverTime` / `leaseSeconds` / `configurationRevision` / `homeRevision`。
|
||
|
||
### 3.4 屏信息
|
||
|
||
`GET /api/v1/panels/{panelId}`
|
||
|
||
**响应 data(Info)**:`panelId` / `gatewayDeviceId` / `homeId` / `role` / `enabled` / `online` / `lastSeenAt` / `features`。
|
||
|
||
### 3.5 会员扫码绑定(APP)
|
||
|
||
> 适用「用户自助绑定」模式。屏身份由管理端预置(`serialNumber` + 一次性 `bootstrapSecret`,`homeId` 留空=待绑定库存)。用户用 APP 扫描屏上二维码(含 `serialNumber`)后,把屏绑定到自己拥有的家庭。所有接口前缀 `/api/v1/member/panels`,**要求会员 JWT(`Authorization: Bearer <会员token>`)**,`memberId` 由服务端从 token 解析(不信任请求参数,防越权)。
|
||
|
||
**绑定前置:屏必须已预置、启用;会员必须是目标家庭的 `accepted` 成员;若屏已绑定到别的账号则拒绝(`403`,防抢绑)。**
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| GET | `/api/v1/member/panels/bindable?serialNumber=` | 扫码后预检:该 SN 是否已预置/启用/已绑定(`Bindable`) |
|
||
| POST | `/api/v1/member/panels/bind` | 绑定:`{ serialNumber, homeId, pairingCode? }` |
|
||
| GET | `/api/v1/member/panels` | 列出当前会员绑定的所有屏 |
|
||
| POST | `/api/v1/member/panels/{panelId}/unbind` | 解绑(仅绑定者本人,释放家庭后屏需重新绑定方可使用) |
|
||
|
||
**bind 请求体**
|
||
|
||
```json
|
||
{ "serialNumber": "PANEL2026ABC123", "homeId": "2113207217787404289", "pairingCode": "123456" }
|
||
```
|
||
|
||
**bind 响应 data(BindResult)**:`panelId` / `serialNumber` / `homeId` / `memberId` / `role` / `boundAt`。绑定成功后屏端**重新建立会话**(`POST /api/v1/panels/session`)即可拿到新 `homeId`。
|
||
|
||
**bindable 响应 data(Bindable)**:`exists`(是否已预置) / `enabled`(是否启用) / `bound`(是否已绑定家庭) / `panelId`。
|
||
|
||
**典型 APP 流程**
|
||
|
||
```
|
||
APP 扫码拿到 SN
|
||
→ GET /member/panels/bindable?serialNumber=SN (展示「可绑定/已绑定」)
|
||
→ 用户在 APP 选择自己拥有的家庭
|
||
→ POST /member/panels/bind { serialNumber, homeId }
|
||
→ 提示屏重启/重新联网,屏自动建会话获得 homeId
|
||
```
|
||
|
||
> 解绑会**作废该屏的刷新凭证**,屏的现有会话立即失效,需重新绑定后才能再次建会话使用。
|
||
|
||
### 3.5.1 Android APP 接入示例(原生 Java)
|
||
|
||
> 本仓库 `Android/` 工程已实现该流程:`PanelListActivity`(列表 + 解绑 + 扫码绑定)。下面给出关键代码片段,供其它端参考。
|
||
|
||
**① 引入扫码(中控屏二维码内容即 `serialNumber`,本端用 zxing-android-embedded):**
|
||
|
||
```gradle
|
||
// app/build.gradle dependencies
|
||
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
|
||
```
|
||
|
||
**② 接口定义(`ApiService`)—— 与现有 Retrofit 调用一致,`Authorization` 走 `Bearer`:**
|
||
|
||
```java
|
||
@GET("v1/member/panels/bindable")
|
||
Call<Result<PanelBindable>> getPanelBindable(
|
||
@Header("Authorization") String auth,
|
||
@Query("serialNumber") String serialNumber);
|
||
|
||
@POST("v1/member/panels/bind")
|
||
Call<Result<PanelBindResult>> bindPanel(
|
||
@Header("Authorization") String auth,
|
||
@Body Map<String, Object> body); // { "serialNumber", "homeId" }
|
||
|
||
@GET("v1/member/panels")
|
||
Call<Result<List<PanelInfo>>> getMyPanels(@Header("Authorization") String auth);
|
||
|
||
@POST("v1/member/panels/{panelId}/unbind")
|
||
Call<Result<Void>> unbindPanel(
|
||
@Header("Authorization") String auth,
|
||
@Path("panelId") long panelId);
|
||
```
|
||
|
||
**③ 扫码 → 预检 → 选家庭 → 绑定:**
|
||
|
||
```java
|
||
// 1) 启动扫码
|
||
new IntentIntegrator(this)
|
||
.setPrompt("将中控屏二维码放入框内")
|
||
.setOrientationLocked(false)
|
||
.initiateScan();
|
||
|
||
// 2) onActivityResult 解析
|
||
IntentResult r = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
|
||
if (r != null && r.getContents() != null) {
|
||
String sn = extractSn(r.getContents()); // 兼容 "https://...?sn=XXX" 或纯 SN
|
||
checkBindable(sn);
|
||
}
|
||
|
||
// 3) 预检
|
||
api.getPanelBindable(auth, sn).enqueue(cb -> {
|
||
PanelBindable b = body.getData();
|
||
if (!b.isExists()) show("该屏未激活或不存在"); // 后端 10001
|
||
else if (!b.isEnabled()) show("该屏已禁用");
|
||
else if (b.isBound()) show("该屏已被其它账号绑定"); // 后端 403 防抢绑
|
||
else showHomePicker(sn); // 4) 选家庭后调用 bindPanel
|
||
});
|
||
```
|
||
|
||
**③-extra 兜底:手动输入 SN(屏固件暂未渲染二维码时使用)**
|
||
|
||
当屏固件还没把 SN 渲染成二维码时,提供「手动输入」入口,直接复用上面的 `checkBindable(sn)` 预检 → 选家庭 → 绑定流程:
|
||
|
||
```java
|
||
// 弹框让用户输入 SN,校验非空后走与扫码完全一致的预检逻辑
|
||
EditText input = new EditText(this);
|
||
input.setHint("请输入中控屏序列号 SN");
|
||
new AlertDialog.Builder(this)
|
||
.setTitle("手动绑定中控屏")
|
||
.setView(input)
|
||
.setPositiveButton("下一步", (d, w) -> {
|
||
String sn = input.getText().toString().trim();
|
||
if (sn.isEmpty()) { show("请输入序列号 SN"); return; }
|
||
checkBindable(sn); // 与扫码解析后同一入口
|
||
})
|
||
.setNegativeButton("取消", null)
|
||
.show();
|
||
```
|
||
|
||
> 注意:SN 必须与管理端预置的 `serialNumber` 完全一致(大小写/前后空格需 trim)。屏固件上线二维码后,扫码与手输两条路径等价。
|
||
|
||
**④ 错误码**:`10001`=屏未预置/不存在;`403`=已绑他人 / 非目标家庭成员 / 非绑定本人解绑。前端按 `Result.code` 区分提示即可。
|
||
|
||
---
|
||
|
||
## 4. 业务接口(屏端)
|
||
|
||
所有接口前缀 `/api/v1`,均需 `Authorization: Bearer <accessToken>`(除命令发起共享端点 4.5 注明外),且 `{panelId}` 须与会话一致。
|
||
|
||
### 4.1 家庭快照
|
||
|
||
`GET /api/v1/panels/{panelId}/snapshot?pageToken=&limit=200`
|
||
|
||
一次拉取家庭、房间与设备的完整视图(首屏数据)。家庭与房间始终全量返回;**设备支持游标分页**(设备多时分页拉取)。`stateEpoch` 为一致性游标。
|
||
|
||
**响应 data(Snapshot)**
|
||
|
||
```json
|
||
{
|
||
"home": { "homeId": "1", "name": "我的家" },
|
||
"rooms": [ { "roomId": "1", "homeId": "1", "name": "客厅" } ],
|
||
"devices": [
|
||
{
|
||
"deviceId": "pd-xxxx",
|
||
"name": "卧室灯",
|
||
"roomId": "2",
|
||
"homeId": "1",
|
||
"status": "ONLINE",
|
||
"executionSource": "LOCAL_PANEL",
|
||
"executorPanelId": "3",
|
||
"routeRevision": 2,
|
||
"schemaRevision": 1,
|
||
"localDeviceId": "local-001",
|
||
"protocol": "MATTER",
|
||
"states": { "power": true, "brightness": 80 },
|
||
"stateVersion": 5
|
||
}
|
||
],
|
||
"stateEpoch": 5,
|
||
"nextPageToken": null,
|
||
"serverTime": 1792668443000
|
||
}
|
||
```
|
||
|
||
| 字段 | 说明 |
|
||
|------|------|
|
||
| executionSource | `CLOUD`(云端执行)/ `LOCAL_PANEL`(由中控屏本地执行) |
|
||
| executorPanelId | 当前执行屏 ID(`LOCAL_PANEL` 时有效) |
|
||
| routeRevision | 路由世代(多屏交接时递增;旧屏据此判定 ROUTE_CHANGED 停止执行) |
|
||
| schemaRevision | 设备级物模型版本(物模型更新时递增,使旧命令失效) |
|
||
| states | 当前属性状态(key 为属性码) |
|
||
| stateVersion | 状态版本(最大序号,用于上报新旧判定) |
|
||
| nextPageToken | 设备下一页游标;为空表示已到末页 |
|
||
|
||
> 客户端应缓存 `stateEpoch` / `routeRevision` / `schemaRevision`,变化时刷新本地视图或失效旧命令。
|
||
|
||
### 4.2 登记本地设备
|
||
|
||
`POST /api/v1/panels/{panelId}/devices/register`
|
||
|
||
同一屏内同一 `localDeviceId` 重复登记返回既有映射(`created=0`);执行归属由平台计算,屏端不可指定。
|
||
|
||
**请求体(Registration)**
|
||
|
||
```json
|
||
{
|
||
"bootId": "boot-20261022-1",
|
||
"localDeviceId": "local-001",
|
||
"nativeNodeId": "0x1234",
|
||
"protocol": "MATTER",
|
||
"platformDeviceId": null,
|
||
"name": "卧室灯",
|
||
"roomId": "2",
|
||
"productId": 101,
|
||
"schema": {
|
||
"properties": [
|
||
{ "code": "power", "name": "开关", "type": "BOOLEAN", "access": "READ_WRITE", "writable": true }
|
||
],
|
||
"actions": [
|
||
{ "code": "toggle", "name": "翻转", "completionMode": "STATE" }
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
| 字段 | 说明 |
|
||
|------|------|
|
||
| localDeviceId | 屏内稳定设备 ID(重装后不变) |
|
||
| protocol | `MATTER` / `ZIGBEE` / `BLE_GATT` / `BLUETOOTH_MESH` |
|
||
| platformDeviceId | 可选,把平台已有设备挂到本屏执行 |
|
||
| schema.properties | 属性规格(code/name/type/access/min/max/step/unit/enumList/defaultValue/semantic/channel) |
|
||
| schema.actions | 动作规格(code/name/completionMode:`STATE` 需回读状态 / `ACK_ONLY` 仅确认) |
|
||
|
||
**响应 data(RegistrationResult)**:`deviceId` / `localDeviceId` / `routeRevision` / `schemaRevision` / `executionSource` / `executorPanelId` / `created`(1 新建 / 0 复用)。
|
||
|
||
### 4.3 更新设备标准控制模型
|
||
|
||
`PUT /api/v1/panels/{panelId}/devices/{deviceId}/schema`
|
||
|
||
更新后递增设备级 `schemaRevision`,用于让基于旧模型的命令失效。
|
||
|
||
**请求体(SchemaUpdateRequest)**:`bootId` + `schema`(同 4.2 的 schema 结构)。
|
||
**响应 data(SchemaUpdateResult)**:`deviceId` / `schemaRevision`。
|
||
|
||
### 4.4 批量状态上报
|
||
|
||
`POST /api/v1/panels/{panelId}/reports`
|
||
|
||
按 `(stateEpoch, sequence)` 判断新旧;旧状态后到不会覆盖已有新值(通过 `appliedToCurrentState` 返回)。
|
||
|
||
**请求体(ReportBatch)**
|
||
|
||
```json
|
||
{
|
||
"bootId": "boot-20261022-1",
|
||
"stateEpoch": 5,
|
||
"reports": [
|
||
{
|
||
"deviceId": "pd-xxxx",
|
||
"localDeviceId": "local-001",
|
||
"sequence": 3,
|
||
"occurredAt": "2026-10-22 11:00:00",
|
||
"properties": { "power": true, "brightness": 80 },
|
||
"online": "ONLINE"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**响应 data(ReportResult)**:`serverTime` / `stateEpoch` / `results[]`,每项 `deviceId` / `propertyCode` / `appliedToCurrentState`(是否生效) / `stateVersion` / `errorCode`。
|
||
|
||
### 4.5 发起控制(共享端点)
|
||
|
||
`POST /api/v1/devices/{deviceId}/commands`
|
||
|
||
> 该端点与手机/网页端共用。屏端发起时**必须**带请求头 `X-MUM-Protocol: panel-v1`。
|
||
|
||
- 若目标设备 `executionSource = LOCAL_PANEL`(由中控屏本地执行):返回 **HTTP 202 ACCEPTED**,且 `data.result.state = QUEUED`,真正的执行由执行屏通过 4.6 链路完成(此时 `success=false`,**不能当作执行成功**)。
|
||
- 其余情况(云端执行等):保持原同步语义,返回 200 + 执行结果。
|
||
|
||
**请求体(DeviceCommand)**
|
||
|
||
```json
|
||
{
|
||
"commandType": "set",
|
||
"params": { "power": true },
|
||
"timeout": 30000
|
||
}
|
||
```
|
||
|
||
**202 响应示例**
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "操作成功",
|
||
"data": {
|
||
"success": false,
|
||
"message": "命令已接受,等待执行屏执行",
|
||
"result": { "commandId": "cmd-xxx", "state": "QUEUED", "terminal": false }
|
||
}
|
||
}
|
||
```
|
||
|
||
`commandId` 用于执行屏领取与追踪。`commandType` 常见:`set`(下发属性)/ `action`(触发动作)/ `query`(查询)。
|
||
|
||
### 4.6 命令执行链路(网关屏)
|
||
|
||
仅 `executionSource = LOCAL_PANEL` 且屏 `role = GATEWAY` 时参与。状态机:`QUEUED → DELIVERED → EXECUTING → CONFIRMED/终态`。
|
||
|
||
**拉取待执行命令** `POST /api/v1/panels/{panelId}/commands/pull`
|
||
|
||
```json
|
||
{ "bootId": "boot-20261022-1", "maxCommands": 10 }
|
||
```
|
||
|
||
响应 `PullResponse`:`commands[]`(`CommandDelivery`:`commandId` / `deviceId` / `commandType` / `params` / `deliveryToken` / `deliveryExpiresAt` / `expiresAt` / `routeRevision` / `schemaRevision` / `completionMode` / `reportId`)、`serverTime`、`nextPullDelayMs`(无命令时退避用)。
|
||
|
||
> `bootId` 与当前有效启动实例不一致时拒绝;`routeRevision` 与本地不一致视为 `ROUTE_CHANGED`,应停止执行。
|
||
|
||
**申请执行命令** `POST /api/v1/panels/{panelId}/commands/{commandId}/start`
|
||
|
||
```json
|
||
{ "bootId": "boot-20261022-1", "deliveryToken": "<投递时下发的凭证>" }
|
||
```
|
||
|
||
响应 `StartResponse`:`commandId` / `state`(EXECUTING) / `executionToken`(回报时带回) / `expiresAt` / `serverTime`。
|
||
|
||
**命令执行回报** `POST /api/v1/panels/{panelId}/commands/{commandId}/report`
|
||
|
||
```json
|
||
{
|
||
"executionToken": "<申请执行返回的凭证>",
|
||
"reportId": "rpt-001",
|
||
"reportSequence": 1,
|
||
"state": "CONFIRMED",
|
||
"items": [
|
||
{ "propertyCode": "power", "state": "CONFIRMED", "actualValue": true, "evidenceSource": "DEVICE_REPORT" }
|
||
],
|
||
"result": { "power": true },
|
||
"error": null,
|
||
"occurredAt": "2026-10-22T11:00:05"
|
||
}
|
||
```
|
||
|
||
| 字段 | 说明 |
|
||
|------|------|
|
||
| reportId | 本次回报 ID,重试保持不变(幂等) |
|
||
| reportSequence | 同命令内单调递增,用于去重与排序 |
|
||
| state | `ACCEPTED` / `CONFIRMED` / `PARTIAL_FAILURE` / `FAILED` / `UNKNOWN` |
|
||
| items[].evidenceSource | `DEVICE_REPORT` / `DEVICE_ACK` / `READ_BACK` / `UNSUPPORTED` / `NO_EVIDENCE` |
|
||
| error | `{ code, message, retryable }` |
|
||
|
||
响应 `PanelCommandResult`:`commandId` / `state` / `terminal`(是否终态) / `result` / `evidenceSource` / `error` / `expiresAt` / `serverTime`。回报终态后服务端会向家庭内所有屏广播 `COMMAND_RESULT` 事件(见 5.3)。
|
||
|
||
### 4.7 OTA
|
||
|
||
**拉取下一个待升级任务** `GET /api/v1/panels/{panelId}/ota-tasks/next`
|
||
|
||
按本屏管理的设备匹配运行中的任务,返回首个待升级设备与固件包;无待升级时 `data` 为 `null`。
|
||
|
||
响应 `OtaNextTask`:`taskId` / `deviceId` / `targetVersion` / `fileUrl` / `size` / `fileMd5` / `packageType`(full/delta) / `targetModel` / `minVersion` / `releaseNote` / **`attemptId`**(上报时原样带回,用于幂等)。
|
||
|
||
**上报升级结果** `POST /api/v1/ota-tasks/{id}/report`(共享端点)
|
||
|
||
```json
|
||
{
|
||
"deviceId": "pd-xxxx",
|
||
"success": true,
|
||
"version": "1.2.3",
|
||
"attemptId": "<下发时的 attemptId>",
|
||
"reportId": "rpt-ota-001",
|
||
"reportSequence": 1,
|
||
"stage": "DOWNLOAD",
|
||
"progress": 100,
|
||
"errorCode": null,
|
||
"message": "ok",
|
||
"oldVersion": "1.2.2"
|
||
}
|
||
```
|
||
|
||
### 4.8 配置
|
||
|
||
`GET /api/v1/panels/{panelId}/configuration`
|
||
|
||
返回当前运营配置与版本/校验和;屏端据 `checksum` 判断是否需要更新本地缓存。
|
||
|
||
响应 `Configuration`:`panelId` / `version` / `config`(Map) / `checksum`。
|
||
|
||
### 4.9 诊断上报
|
||
|
||
`POST /api/v1/panels/{panelId}/diagnostics`
|
||
|
||
```json
|
||
{
|
||
"bootId": "boot-20261022-1",
|
||
"level": "ERROR",
|
||
"category": "mqtt",
|
||
"message": "连接断开",
|
||
"data": { "code": 500 }
|
||
}
|
||
```
|
||
|
||
`category`:`network` / `mqtt` / `ota` / `command` / `config`。
|
||
|
||
### 4.10 场景执行
|
||
|
||
**上报执行结果** `POST /api/v1/panels/{panelId}/scene-runs/report`
|
||
|
||
云端场景涉及本屏本地设备时,屏上报执行结果(按 `sceneRunId` 或 `sceneId + attemptId` 定位,`reportSequence` 去重)。
|
||
|
||
```json
|
||
{
|
||
"sceneRunId": 123,
|
||
"sceneId": 456,
|
||
"triggerSource": "manual",
|
||
"attemptId": "att-001",
|
||
"reportId": "rpt-scene-001",
|
||
"reportSequence": 1,
|
||
"status": "success",
|
||
"result": { "pd-xxxx": { "state": "success" } }
|
||
}
|
||
```
|
||
|
||
响应 `RunView`:`id` / `sceneId` / `homeId` / `triggerSource` / `status` / `executePanelId` / `result` / `reportSequence` / `serverTime`。
|
||
|
||
**查询场景执行单据** `GET /api/v1/scene-runs/{sceneRunId}`
|
||
|
||
屏侧按家庭校验,返回 `RunView`(家庭内进度追踪)。
|
||
|
||
---
|
||
|
||
## 5. 增量事件通道(长轮询 + SSE)
|
||
|
||
家庭内设备状态变化、命令结果等通过**事件通道**实时推送给屏。提供两种取用方式,屏端**二选一**:
|
||
|
||
| 方式 | 端点 | 方向 | 特点 |
|
||
|------|------|------|------|
|
||
| 长轮询 | `GET /api/v1/panels/{panelId}/events` | 请求-响应 | 兼容性好,无长连接;最坏等待 `timeout` |
|
||
| **SSE 流** | `GET /api/v1/panels/{panelId}/events/stream` | 服务端单向推送 | **推荐**;事件到达即推,毫秒级;一条长连接 |
|
||
|
||
两者**共用同一 `panel_event` 事件源**(跨实例一致,无需额外消息中间件)。事件按 `(homeId, panelId匹配 OR 广播)` 过滤:屏收到自身事件 + 家庭内广播事件。事件类型目前实现 `DEVICE_STATE`、`COMMAND_RESULT`,通道本身可扩展。
|
||
|
||
### 5.1 长轮询
|
||
|
||
`GET /api/v1/panels/{panelId}/events?cursor=<上次nextCursor>&timeout=20000&limit=100`
|
||
|
||
- `cursor`:上次返回的 `nextCursor`(事件 id),首次传 `0`。
|
||
- `timeout`:最长阻塞毫秒(默认 20000),或直到有事件到达。
|
||
- `limit`:单次最大返回条数(默认 100)。
|
||
|
||
**响应 data(EventsResponse)**
|
||
|
||
```json
|
||
{
|
||
"events": [
|
||
{
|
||
"id": 2113112213823905793,
|
||
"eventType": "DEVICE_STATE",
|
||
"deviceId": "pd-xxxx",
|
||
"payload": { "propertyCode": "power", "value": true, "stateEpoch": 5, "sequence": 3 },
|
||
"serverTime": 1792668986000
|
||
}
|
||
],
|
||
"nextCursor": 2113112213823905793,
|
||
"serverTime": 1792668986000
|
||
}
|
||
```
|
||
|
||
### 5.2 SSE 流(推荐)
|
||
|
||
`GET /api/v1/panels/{panelId}/events/stream?cursor=<上次最后事件id>`
|
||
|
||
- `Content-Type: text/event-stream`,`Accept: text/event-stream`。
|
||
- 连接建立即收到一行注释 `:connected`(确认链路 + 刷新代理计时)。
|
||
- 无事件时每 ~20s 发一行心跳注释 `:keepalive`(保活,防止代理/中间人掐断)。
|
||
- 连接 90s 超时后服务端正常结束;客户端重连即可(见 5.4)。
|
||
|
||
**事件信封(SSE 文本格式)**
|
||
|
||
```
|
||
id: 2113112213823905793
|
||
event: DEVICE_STATE
|
||
data: {"id":2113112213823905793,"eventType":"DEVICE_STATE","deviceId":"pd-xxxx","payload":{"propertyCode":"power","value":true,"stateEpoch":5,"sequence":3},"serverTime":1792668986000}
|
||
|
||
:keepalive
|
||
```
|
||
|
||
> `id` 为事件游标(= 事件 id),`event` 为事件类型,`data` 为 `EventView` 的 JSON。浏览器 `EventSource` 会自动把 `id` 暂存为 `Last-Event-ID`,重连时自动带上。
|
||
|
||
### 5.3 事件类型
|
||
|
||
| event(eventType) | payload 示例 | 说明 |
|
||
|-------------------|--------------|------|
|
||
| `DEVICE_STATE` | `{ "propertyCode":"power", "value":true, "stateEpoch":5, "sequence":3 }` | 设备属性状态变化(状态上报生效时产生) |
|
||
| `COMMAND_RESULT` | `{ "commandId":"cmd-xxx", "state":"CONFIRMED", "terminal":true, "result":{...} }` | 命令终态(广播给家庭内所有屏,用于界面即时刷新) |
|
||
|
||
> 屏端收到 `DEVICE_STATE` 更新对应设备卡片;收到 `COMMAND_RESULT` 刷新对应命令状态。`payload` 为 `Map`,字段以实际为准。
|
||
|
||
### 5.4 重连与游标
|
||
|
||
- **浏览器(EventSource)**:因每条事件带 `id`,断线重连时浏览器自动在请求头带 `Last-Event-ID`,服务端从该游标续传;无需屏端额外处理。
|
||
- **自定义客户端(OkHttp 等)**:自行记录收到的最后 `id`,重连时带 `?cursor=<最后id>`;若未带 `cursor`,则从头(`0`)拉取全部事件(仅调试/首连用)。
|
||
- 重连建议**指数退避**(1s→2s→4s…上限 30s)。重连后建议补一次 **家庭快照**(4.1)以对齐离线期间的整体状态(事件通道为增量,不保证全量)。
|
||
|
||
### 5.5 部署要求(nginx 等反向代理)
|
||
|
||
SSE 长连接须经反代透传:
|
||
|
||
- `/api/v1/panels` 路径需配置 `proxy_buffering off;`(关闭缓冲,保证事件实时到达)。
|
||
- `proxy_read_timeout` 建议 ≥ 60s(配合 20s 心跳足够;心跳会持续重置读超时)。
|
||
- 屏端若走 WSS/HTTPS 隧道同理保持长连接。
|
||
|
||
### 5.6 客户端接入示例
|
||
|
||
> 关键限制:`EventSource` **不支持自定义请求头**,无法直接带 `Authorization: Bearer`。因此:
|
||
> - 同源(Web 屏 + Cookie 鉴权)场景可用 `EventSource`(最省心,自动 `Last-Event-ID` 重连);
|
||
> - 需 Bearer 鉴权的场景(多数中控屏)请用 **`fetch` 流** 或 **OkHttp 流** 手动解析 `text/event-stream` 并自己维护游标与重连。
|
||
|
||
#### 5.6.1 Web(Bearer 鉴权,fetch 流)
|
||
|
||
```js
|
||
let lastCursor = 0;
|
||
let retryDelay = 1000;
|
||
|
||
async function connectPanelEvents(panelId, token) {
|
||
const url = `/api/v1/panels/${panelId}/events/stream?cursor=${lastCursor}`;
|
||
let resp;
|
||
try {
|
||
resp = await fetch(url, {
|
||
method: 'GET',
|
||
headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'text/event-stream' }
|
||
});
|
||
} catch (e) { return reconnect(panelId, token); }
|
||
if (!resp.ok) { // 401 等:刷新 token 后重连
|
||
await refreshToken();
|
||
return reconnect(panelId, token);
|
||
}
|
||
|
||
const reader = resp.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
let buf = '', id = null, event = null, data = '';
|
||
|
||
const flush = () => {
|
||
if (id !== null) lastCursor = Number(id); // 记录游标
|
||
if (data) onPanelEvent(event, JSON.parse(data)); // 投递业务
|
||
id = event = data = null;
|
||
};
|
||
|
||
try {
|
||
while (true) {
|
||
const { value, done } = await reader.read();
|
||
if (done) break;
|
||
buf += decoder.decode(value, { stream: true });
|
||
const lines = buf.split('\n');
|
||
buf = lines.pop(); // 保留未完成的行
|
||
for (const raw of lines) {
|
||
const line = raw.trimEnd();
|
||
if (line === '' || line.startsWith(':')) { flush(); continue; } // 空行=事件结束;:开头=注释/心跳
|
||
if (line.startsWith('id:')) id = line.slice(3).trim();
|
||
else if (line.startsWith('event:')) event = line.slice(6).trim();
|
||
else if (line.startsWith('data:')) data += (data ? '\n' : '') + line.slice(5).trim();
|
||
}
|
||
}
|
||
} finally {
|
||
flush();
|
||
reconnect(panelId, token); // 连接关闭→重连
|
||
}
|
||
}
|
||
|
||
function reconnect(panelId, token) {
|
||
setTimeout(() => connectPanelEvents(panelId, token), retryDelay);
|
||
retryDelay = Math.min(retryDelay * 2, 30000); // 指数退避,上限 30s
|
||
}
|
||
|
||
function onPanelEvent(eventType, view) {
|
||
if (eventType === 'DEVICE_STATE') updateDeviceCard(view.deviceId, view.payload);
|
||
else if (eventType === 'COMMAND_RESULT') refreshCommand(view.payload);
|
||
}
|
||
```
|
||
|
||
#### 5.6.2 Web(同源 Cookie 鉴权,EventSource)
|
||
|
||
```js
|
||
// 仅在后端对该端点放行 Cookie/Session 鉴权时使用;断线浏览器自动带 Last-Event-ID 续传
|
||
const es = new EventSource(`/api/v1/panels/${panelId}/events/stream?cursor=${lastCursor}`);
|
||
es.onmessage = (e) => { /* e.data 为 EventView JSON;e.lastEventId 即游标 */ };
|
||
es.addEventListener('DEVICE_STATE', (e) => updateDeviceCard(JSON.parse(e.data)));
|
||
es.onerror = () => { /* 浏览器会自动重连,无需手动处理 */ };
|
||
```
|
||
|
||
#### 5.6.3 Android(OkHttp,Bearer 鉴权)
|
||
|
||
```kotlin
|
||
private val client = OkHttpClient.Builder()
|
||
.readTimeout(0, TimeUnit.MILLISECONDS) // 长连接:禁用读超时
|
||
.build()
|
||
|
||
fun connect(panelId: Long, token: String, cursor: Long) {
|
||
val req = Request.Builder()
|
||
.url("$BASE/api/v1/panels/$panelId/events/stream?cursor=$cursor")
|
||
.header("Authorization", "Bearer $token")
|
||
.header("Accept", "text/event-stream")
|
||
.build()
|
||
client.newCall(req).enqueue(object : Callback {
|
||
override fun onFailure(call: Call, e: IOException) = scheduleReconnect(panelId, token)
|
||
override fun onResponse(call: Call, resp: Response) {
|
||
if (!resp.isSuccessful) { resp.close(); refreshToken(); scheduleReconnect(panelId, token); return }
|
||
var id: String? = null; var event: String? = null; var data = StringBuilder()
|
||
resp.body?.source()?.use { src ->
|
||
while (!src.exhausted()) {
|
||
val line = src.readUtf8Line() ?: break
|
||
when {
|
||
line.isEmpty() || line.startsWith(":") -> { // 事件结束 / 心跳
|
||
id?.let { lastCursor = it.toLong() }
|
||
if (data.isNotEmpty()) onEvent(event, data.toString())
|
||
id = null; event = null; data = StringBuilder()
|
||
}
|
||
line.startsWith("id:") -> id = line.substring(3).trim()
|
||
line.startsWith("event:") -> event = line.substring(6).trim()
|
||
line.startsWith("data:") -> data.append(line.substring(5).trim())
|
||
}
|
||
}
|
||
}
|
||
scheduleReconnect(panelId, token) // 连接关闭→重连
|
||
}
|
||
})
|
||
}
|
||
|
||
private fun scheduleReconnect(panelId: Long, token: String) {
|
||
handler.postDelayed({ connect(panelId, token, lastCursor) }, backoffMs.also { backoffMs = min(backoffMs * 2, 30000) })
|
||
}
|
||
```
|
||
|
||
#### 5.6.4 uni-app / 小程序 注意
|
||
|
||
- **小程序端**:`wx.request` 不支持流式响应,无法直接消费 SSE;如需实时,建议用 WebSocket(但本平台未提供 WS 端点)或退化为**长轮询**(5.1 的 `GET /panels/{id}/events`,每次请求-响应,适合小程序定时拉取)。
|
||
- **uni-app App 端(Android/iOS 原生)**:`uni.request` 同样非流式;App 端应通过**原生插件 / 原生模块**调用上述 OkHttp(Android)或 `URLSession` 流(iOS)消费 SSE,再用 bridging 回传业务层。
|
||
- 通用建议:屏端优先采用**长轮询端点(5.1)**做兜底,能力具备后再切 SSE 流以降低接入复杂度。
|
||
|
||
---
|
||
|
||
## 6. 管理端接口(管理员)
|
||
|
||
路径前缀 `/api/v1/admin/panels`,需管理员 JWT(`ROLE_ADMIN`)。
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| POST | `/api/v1/admin/panels` | 预置屏终端,返回一次性 `bootstrapSecret`(仅此一次,请妥善保存) |
|
||
| GET | `/api/v1/admin/panels` | 屏终端列表 |
|
||
| PUT | `/api/v1/admin/panels/{panelId}/status?status=` | 启用/禁用(`status=0` 禁用后该屏刷新凭证立即失效) |
|
||
| POST | `/api/v1/admin/panels/{panelId}/rotate` | 轮换初始化凭证(泄露时使用,旧凭证立即失效) |
|
||
| GET | `/api/v1/admin/panels/{panelId}/configuration` | 查看屏配置 |
|
||
| PUT | `/api/v1/admin/panels/{panelId}/configuration` | 保存屏配置(`configJson`),`version` 递增 |
|
||
| POST | `/api/v1/admin/panels/executor-reassign` | 多屏归属交接(见下) |
|
||
|
||
**预置屏终端 请求(CreateRequest)**:`serialNumber`(必填) / `homeId`(**可选**:留空=待绑定库存,由用户 APP 扫码自助绑定,见 3.5;填了则后台预置即绑定) / `memberId` / `role`(`GATEWAY`/`DISPLAY`) / `gatewayDeviceId` / `features` / `model`。
|
||
**响应(CreateResult)**:`panelId` / `serialNumber` / `bootstrapSecret`(仅返回一次)。
|
||
|
||
**多屏归属交接(executor-reassign)**
|
||
|
||
`POST /api/v1/admin/panels/executor-reassign`
|
||
|
||
```json
|
||
{ "deviceIds": ["pd-xxxx", "pd-yyyy"], "targetPanelId": 5 }
|
||
```
|
||
|
||
将一批设备从原执行屏改派到目标屏,递增 `routeRevision` 并作废在途命令(`ROUTE_CHANGED`),使旧屏停止执行。响应 `ReassignResult`:`reassigned`(实际交接数) / `expiredCommands`(作废在途命令数)。
|
||
|
||
---
|
||
|
||
## 7. 错误码
|
||
|
||
| code | message | 说明 / 处理建议 |
|
||
|------|---------|----------------|
|
||
| 200 | 操作成功 | — |
|
||
| 400 | 参数错误 | 检查请求体字段 |
|
||
| 401 | 未授权,请先登录 | Token 缺失/过期,用 refresh 刷新或重新建立会话 |
|
||
| 403 | 禁止访问 | panelId 与会话不一致 / 设备被禁用 / 权限不足 |
|
||
| 404 | 资源不存在 | 设备/场景/家庭不存在 |
|
||
| 500 | 操作失败 | 服务端异常,稍后重试 |
|
||
| 503 | 服务不可用 | 平台维护 |
|
||
| 10002 | 设备离线 | 设备不可达,无法控制 |
|
||
| 10004 | 指令发送失败 | 控制失败,重试或检查设备在线 |
|
||
| 10008 | 设备凭证无效 | SN / bootstrapSecret 错误或未绑定 |
|
||
| 10011 | Token 无效或已过期 | 刷新或重新建立会话 |
|
||
| 10013 | 权限不足 | 设备无权访问该资源 |
|
||
| 11003 | 第三方 API 调用失败 | 平台对接第三方失败,稍后重试 |
|
||
| 10100 | 设备未激活 | 需先完成 APP 扫码绑定 |
|
||
| 10101 | 初始化凭证无效或已使用 | 凭证错误/失效,需管理端重新签发(rotate) |
|
||
| 10102 | 设备已被禁用 | 管理员禁用该中控屏,联系管理员 |
|
||
|
||
**会员绑定相关(3.5)实际返回:**
|
||
- `10001`(DEVICE_NOT_FOUND):绑定/解绑时 `serialNumber` 未预置或不存在(提示「该屏未激活或不存在」)。
|
||
- `403`(FORBIDDEN):① 屏已绑定到其他账号(防抢绑);② 会员非目标家庭 `accepted` 成员;③ 解绑者非绑定本人。
|
||
|
||
> 错误码以服务端实际返回为准(统一 `ResultCode`);上表 10100/10101/10102 为中控屏登录/会话相关语义说明。
|
||
|
||
---
|
||
|
||
## 8. 安全与接入要求
|
||
|
||
1. **全链路 HTTPS**:禁止明文传输;SSE 亦走 HTTPS 长连接。
|
||
2. **证书固定(建议)**:中控屏出厂内置服务端证书指纹,防局域网中间人攻击。
|
||
3. **凭证保护**:SN + `bootstrapSecret` 为设备唯一身份,`bootstrapSecret` 一次性;设备仅安全存储 refreshToken,**不存储任何会员密码**。
|
||
4. **权限隔离**:中控屏仅可访问其绑定家庭(homeId 锁定),无法访问会员/管理员接口。
|
||
5. **限流**:建立会话、刷新、控制等敏感操作服务端限流,请勿高频重试(建议退避)。
|
||
6. **审计**:所有中控屏控制均记录来源可追溯到 `panelId`。
|
||
7. **远程禁用**:管理员可禁用某 SN,设备 Token 立即失效。
|
||
8. **多屏安全**:`routeRevision` 递增 + 在途命令作废,防止旧屏在交接后继续执行。
|
||
9. **重置策略**:设备恢复出厂需管理端重新签发 `bootstrapSecret` 后方可再次绑定。
|
||
|
||
---
|
||
|
||
## 9. 数据字典
|
||
|
||
- **device role(屏角色)**:`GATEWAY`(可本地执行设备命令)/ `DISPLAY`(仅展示与提交控制)。
|
||
- **executionSource(设备执行来源)**:`CLOUD`(云端执行)/ `LOCAL_PANEL`(由中控屏本地执行)。
|
||
- **completionMode(动作完成模式)**:`STATE`(需回读状态确认)/ `ACK_ONLY`(仅确认送达)。
|
||
- **online(设备在线)**:`ONLINE` / `OFFLINE` / `ERROR`。
|
||
- **命令 state**:`QUEUED` / `DELIVERED` / `EXECUTING` / `CONFIRMED` / `PARTIAL_FAILURE` / `FAILED` / `UNKNOWN`。
|
||
- **事件 eventType**:`DEVICE_STATE` / `COMMAND_RESULT`(通道可扩展)。
|
||
- **诊断 category**:`network` / `mqtt` / `ota` / `command` / `config`。
|
||
- **场景 status**:`pending` / `running` / `partial` / `success` / `failed`。
|
||
|
||
---
|
||
|
||
## 10. 版本与联系
|
||
|
||
- 本文档版本 **v2.0**,对应后端实现阶段 **P1(会话/设备/命令/快照/管理端)**、**P2(事件双通道/OTA/配置/物模型)**、**P3(场景/AI 二次确认/诊断/多屏交接)**。
|
||
- 接口路径/字段以本契约为准;后端实现严格遵循,如有调整将更新本文档并通知接入方。
|
||
- 联调问题请联系平台对接负责人(接口/环境/初始化凭证签发)。
|