# 设备操控动态(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. 统一事件模型 ```java 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 平台适配接口(后续对接统一入口) ```java public interface IPlatformEventAdapter { DevicePlatform platform(); /** 平台原始报文 → 统一事件 */ List normalize(Object rawEvent); /** 启动订阅/长连接,事件推给 sink */ void start(Consumer> 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)?