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

11 KiB
Raw Blame History

设备操控动态(Activity)统一方案

目标:以 APP 自己的控制 + 监听到的信息为第一数据源形成操控动态,再逐步对接各平台(涂鸦/米家/华为/Matter)的云端动态。 编写日期:2026-09-20 | 状态:方案待评审


1. 现状盘点

1.1 已有资产

资产 位置 说明
device_activity_log 表 iot-device 动态存储,字段见 1.2
DeviceActivityService iot-device/service record() 异步写入、hasRecentActivity() 查重、buildDpsDesc() 描述生成
DeviceActivityController iot-device/controller GET /devices/activities/recent(首页)、GET /devices/activities(分页)
物模型 / 标准控制模型 product + product_property + ControlSchemaService 可把原始 DP 翻译成人类可读语义(P3 已落地)
平台抽象层 PlatformServiceManager / IPlatformService 多平台统一控制入口

1.2 当前表结构

id, home_id, owner_id, device_id, device_name,
action, action_desc, source, operator_member_id, detail, create_time

1.3 现有记录点(4 个)

# 位置 触发 source
1 TuyaController.sendCommand 经后端下发的指令(云端通道) app / voice
2 TuyaController.syncDeviceStatus(/tuya/devices/status) APP 上报 DP 变化 panel
3 TuyaMessageSubscribeService 涂鸦云端推送(Pulsar) cloud_push
4 DeviceActivityController.report(/devices/{id}/activity,本期新增) APP 走 SDK 直连后主动上报 app

1.4 已暴露的问题

  1. APP 走 SDK 直连,后端天然感知不到:控制不经过后端 /commands,动态全靠"上报"补(记录点 4 是补丁,不是终态)。
  2. 语义弱:buildDpsDesc() 只识别 bool,输出打开/关闭 设备名;不懂物模型,亮度/模式/枚举都只能输出"状态更新(N 项)"。
  3. 来源语义重叠,易双记或漏记:一次真实操作常同时触发 记录点 2 和 3,只能靠 8 秒查重粗略兜底。
  4. 平台耦合:涂鸦专属逻辑(Pulsar 解析、/tuya/... 接口)散落在 TuyaController、TuyaMessageSubscribeService,接米家/华为/Matter 要重来一遍。
  5. 休眠设备天然无动态:蓝牙 Mesh 子设备(JD6BTSA / 门磁)休眠不上报,云端与本地都可能感知不到,属设备物理特性。
  6. 历史数据为空:device_activity_log 长期 0 条(本期已修复涂鸦推送解析 bug:bizCode/bizData 在 data 内)。

2. 设计目标

  • 平台无关:动态模型不绑定任何平台,平台差异只体现在"采集适配"层。
  • 两类来源统一:主动控制(Command)与被动监听(Observe)归一到同一张表、同一套语义。
  • 语义可读:结合物模型把 {"1":true} 翻译成"打开 客厅灯"。
  • 去重可控:控制与其回执只记一条;短窗口内同设备同功能点合并。
  • 渐进对接:先把"自己的"做扎实,再按统一接口接各平台。

3. 总体架构

┌─────────────── 采集层 Collector(三类,平台相关) ───────────────┐
│ ① CommandCollector   我们自己的控制入口                          │
│    APP: PlatformServiceManager.controlDevice 成功回调            │
│    后端: /commands(云端下发)、语音、场景执行                    │
│                                                                  │
│ ② ObserveCollector   APP 本地监听(SDK 直连,最实时)            │
│    DP 变化 / 上下线(涂鸦 Mesh、蓝牙、局域网)                    │
│                                                                  │
│ ③ PlatformEventCollector  各平台云端事件(后续对接)              │
│    涂鸦 Pulsar(已有)→ 米家 / 华为 / Matter(后续)              │
└──────────────────────────┬───────────────────────────────────────┘
                           ↓
              归一化层 Normalizer → DeviceEvent(平台无关)
              { deviceId, platform, type, dpCode, value, ts, raw }
                           ↓
              语义层 Enricher(查 product + product_property + ControlSchema)
              dpId → code → name / type / enum → "打开 客厅灯"、"亮度调到 60%"
                           ↓
              去重层 Dedupe(commandId 配对 + 短窗口合并)
                           ↓
              存储 device_activity_log(扩展字段,见第 5 节)
                           ↓
              消费:首页近期动态 / 动态详情 / 规则引擎 / 中控屏 / WebSocket 推送

关键:采集层可插拔,下面三层完全平台无关。 新增平台 = 新增一个 Collector/Adapter。


4. 统一事件模型

public class DeviceEvent {
    private String deviceId;      // 平台设备ID
    private String platform;      // tuya / mi / huawei / matter / local
    private EventType type;       // CONTROL / STATE / ONLINE / OFFLINE / ALARM
    private String dpCode;        // 物模型功能点编码(平台无关语义 key,如 switch_1)
    private String dpId;          // 平台原始 DP 点(如涂鸦 "1")
    private Object value;         // 归一化后的值
    private Long homeId;
    private Long operatorMemberId;// 操作人(主动控制有,被动上报为 null)
    private String commandId;     // 控制回执配对 ID
    private Long ts;
    private Object raw;           // 原始报文(审计/排查用)
}

EventType:

  • CONTROL:我方发起并确认成功的操作(最可信,优先级最高)
  • STATE:状态变化(DP 上报/云端推送)
  • ONLINE / OFFLINE:上下线
  • ALARM:告警类(门磁触发、烟雾报警等,后续)

5. 数据模型演进(V15 迁移草案)

在现有 device_activity_log 上新增列(保留原字段,保证读兼容):

新增列 类型 说明
platform varchar(32) 平台标识
event_type varchar(16) control / state / online / offline / alarm
dp_code varchar(64) 物模型功能点编码(语义 key)
value_text varchar(128) 归一化值("true" / "60%" / "cold")
command_id varchar(64) 控制回执配对,用于去重
dedupe_key varchar(128) 去重键,唯一索引
raw json 原始平台报文(detail 保留兼容,新数据同时写 raw)

索引:idx_home_time(home_id, create_time)、idx_device_time(device_id, create_time)、uniq(dedupe_key)

为什么加 dedupe_key 而不是继续用 8 秒查重:查重是"启发式",去重键是"确定性"——由 commandId 或 deviceId + dpCode + 时间桶 生成,重复写入直接被唯一索引拦截,不依赖时序。


6. 关键机制

6.1 控制—回执配对(解决"一次操作记两条")

  1. 发起控制时生成 commandId(客户端 UUID 或后端生成)
  2. 控制成功 → 写 CONTROL 记录(带 commandId)
  3. 随后到达的 STATE(同 commandId,或 同设备+同 dpCode+10s 内)→ 只更新该 CONTROL 记录,不新增
  4. 超窗仍未配对 → CONTROL 记录保持("指令已下发"),后续 STATE 独立成条

6.2 去重规则

场景 策略
控制 + 其回执 commandId 配对,合并为 1 条
本地监听 + 云端推送(同一变化) deviceId + dpCode + 5s 时间桶 去重键
上下线抖动 同设备 60s 内同状态只记 1 条
场景批量执行 按场景维度聚合 1 条("执行场景:回家模式"),不逐设备刷屏

去重窗口可配置(iot.device.activity.dedupe.*)。

6.3 语义化(重点,复用 P3 物模型)

映射链:平台 dpId → product_property.code → name / type / enum / unit

原始: device=6c...  dps={"1":true}
TSL : dpId "1" → code "switch_1" → name "开关" → type "bool"
输出: "打开 客厅灯"

原始: dps={"22":500}
TSL : dpId "22" → code "bright_value" → name "亮度" → min 0 / max 1000 / unit "%"
输出: "客厅灯 亮度调到 50%"

原始: dps={"1":"cold"}
TSL : dpId "1" → code "mode" → enum ["auto","cold","hot"]
输出: "空调 模式切换为 制冷"

降级策略:产品无物模型时,回退现有 buildDpsDesc()(bool→开/关,其余→"状态更新"),保证老产品不空白。

6.4 平台适配接口(后续对接统一入口)

public interface IPlatformEventAdapter {
    DevicePlatform platform();
    /** 平台原始报文 → 统一事件 */
    List<DeviceEvent> normalize(Object rawEvent);
    /** 启动订阅/长连接,事件推给 sink */
    void start(Consumer<List<DeviceEvent>> sink);
    void stop();
}
  • 涂鸦:TuyaPulsarEventAdapter(把现有 TuyaMessageSubscribeService 的解析逻辑迁入,含新版 bizCode/bizData 处理)
  • 米家 / 华为 / Matter:按同一接口实现,业务层零改动

7. 与现有模块的关系

  • 复用 Product / ProductProperty / ControlSchemaService(P3 成果)做语义翻译
  • 复用 DeviceEventPublisher(RabbitMQ)把动态分发给规则引擎、中控屏 WebSocket
  • 复用 PlatformServiceManager 作为所有控制的统一收口(已在成功回调埋点)
  • 中控屏与 APP 共用同一份动态数据(/api/v1/panel/... 命名空间未来直接复用)

8. 分阶段计划

阶段 内容 产出
P0 表结构扩展(platform/event_type/dp_code/value_text/command_id/dedupe_key/raw)+ 去重键唯一索引 V15 迁移
P1 DeviceEvent 模型 + ActivityPipeline(归一化→语义→去重→存储);commandId 贯穿控制链路(APP 与后端) 一次操作只记一条、语义可读
P2 语义层接入物模型(dpId→code→name/enum/unit);无物模型降级 "亮度调到 50%" 级别描述
P3 采集层重构:CommandCollector / ObserveCollector / PlatformEventAdapter;涂鸦 Pulsar 迁入 Adapter 平台解耦
P4 对接米家 / 华为 / Matter;场景聚合、告警类动态、保留策略与清理任务 多平台统一动态

9. 风险与边界

  1. 休眠设备天然无动态:蓝牙 Mesh 子设备(JD6BTSA / 门磁)不上报,任何方案都记不到。UI 应对此类设备明示"休眠设备,可能无法记录全部动态",避免用户误解为 bug。
  2. 去重过激会漏记:窗口需可配置,且保留 raw 原始报文便于回溯。
  3. 数据增长:建议保留策略(如 90 天滚动清理或按月归档),写入全异步不阻塞控制主链路。
  4. 语义依赖物模型:物模型未录入的产品只能降级输出,需引导在后台补齐(P3 已有录入界面)。

10. 待确认

  1. commandId 由客户端生成还是后端下发?(建议客户端 UUID,随控制请求带给后端)
  2. 场景/AI 触发的操作,operator_member_id 记触发人还是留空并记场景 ID?
  3. 动态保留期限与归档策略?
  4. 是否需要"动态订阅/推送"(设备动态实时推到 APP/中控屏 WebSocket)?