# 智慧生活全流程设计(iot 插件扩展) > 版本:v0.1(设计稿) > 日期:2026-08-21 > 目标:以米家/华为智慧生活的核心模型,在现有 `iot` 插件上落地「设备接入 → 云中枢 → App 控制 → 场景自动化 → 告警/OTA」完整闭环。 --- ## 一、现状盘点(已有能力) | 模块 | 现状 | 说明 | |---|---|---| | MQTT Broker | ✅ `Broker.php` | 单进程内存态(worker=1),支持 retain / 离线队列 / 遗嘱 / QoS2 | | 认证 | ✅ `Auth.php` | bcrypt 密码、匿名开关、框架会员直通(`allow_member_login`) | | ACL | ✅ `Auth.php` | 按 user/client 匹配,`%u`/`%c` 占位符,自动生成设备/会员规则 | | 影子 | ✅ `Store` + `DeviceService::shadow()` | 设备 `state` 主题 retain=1,离线可查最后状态 | | 离线/命令 | ✅ `Store::enqueueManual()` | 后台/API 发布经 outbox 出栈,Broker 消费后内部路由,零外部依赖 | | 设备绑定 | ✅ `DeviceService` | 扫码 SN → 激活 uuid/secret → 绑定会员 → 自动 ACL | | 事件钩子 | ✅ `Broker` 内 `event('mqtt_message_published')` | 场景引擎的天然接入点 | | 转发规则 | ⚠️ `RuleEngine.php` | 仅「源主题→外部 Broker」桥接,**无条件判断/动作执行** | | 场景/自动化 | ❌ 无 | 本次设计核心 | | 设备分组 | ❌ 无(仅有 `room` 文本字段) | 本次新增 | | 告警/通知 | ❌ 无 | 本次新增 | | OTA 升级 | ❌ 无 | 本次新增 | | 设备日志 | ⚠️ 仅有全量 `message` 表 | 需新增设备维度日志 | **关键架构事实**: - Broker 为单进程内存态 → 场景规则可做进程内内存缓存(TTL 刷新,同 `Auth::loadAclRules` 模式)。 - 场景动作不阻塞主循环 → 统一走 `Store::enqueueManual()` 异步投递。 - 所有新增表遵循框架约定:`__PREFIX__iot_*`、时间字段 `create_at/update_at/delete_at`、字段命名禁裸 `name`(用 `title`)。 --- ## 二、总体架构 ``` ┌─ 设备端 ────────────────────────────────────────────────┐ │ 扫码SN → 激活(uuid+secret) → MQTT连云 │ │ publish /devices/{uuid}/state(retain) ← 影子 │ │ publish /devices/{uuid}/event ← 事件/告警条件 │ │ publish /devices/{uuid}/ota/report ← OTA进度 │ │ subscribe /devices/{uuid}/cmd ← 控制命令 │ │ subscribe /devices/{uuid}/ota ← 升级指令 │ └───────────────────────────────────────────────────────────┘ │ MQTT ┌─ 云中枢(iot 插件)───────────────────────────────┐ │ Broker: 认证/ACL/retain/离线/遗嘱/事件钩子 │ │ 影子服务: retain 持久化 + shadow() 查询 │ │ 命令路由: outbox 出站队列 → Broker 消费 → 内部路由 │ │ ★ SceneService: 场景/自动化引擎(新增) │ │ 订阅 mqtt_message_published → 条件匹配 → 动作入 outbox │ │ 定时触发器(Workerman Timer 轮询 cron) │ │ ★ AlertService: 告警记录 + /notify/{account}/# 通知(新增) │ │ ★ OtaService: 固件/版本管理 + 升级指令(新增) │ │ ★ GroupService: 设备分组(新增) │ └───────────────────────────────────────────────────────────┘ │ HTTPS REST │ MQTT(实时) ┌─ App 端 ─────────────────┼────────────────────┼──────────┐ │ 会员JWT │ │ │ │ 登录/注册(/api/v1/user) │ │ │ │ 设备绑定/列表/控制 │ │ │ │ 分组管理 / 场景管理 │ │ │ │ 告警列表 / OTA 查询 │ │ │ │ 实时状态订阅(/devices/+/state) │ │ 实时通知订阅(/notify/{account}/#) │ └───────────────────────────────────────────────────────────┘ ``` --- ## 三、Topic 规范(完整) 沿用现有 `/devices/{uuid}/...` 体系,扩展事件/OTA/通知: | Topic | 方向 | 用途 | |---|---|---| | `/devices/{uuid}/state` | 设备→云 | 状态上报(retain=1=影子),**已有** | | `/devices/{uuid}/cmd` | 云→设备 | 控制命令下发,**已有** | | `/devices/{uuid}/event` | 设备→云 | 事件上报(非状态,如"门被打开"),新增 | | `/devices/{uuid}/ota` | 云→设备 | OTA 升级指令,新增 | | `/devices/{uuid}/ota/report` | 设备→云 | OTA 进度/结果上报,新增 | | `/notify/{account}/#` | 云→App | 会员通知(告警/场景执行结果),新增 | ACL 同步扩展: - 设备规则 `auto:device:{uuid}` 已含 `/devices/%u/#`(覆盖新增 event/ota 子主题,无需改)。 - 会员规则 `auto:member:{uuid}:*` 需新增 `/devices/%s/state` 与 `event` 只读订阅(命令仍走 REST 下发,避免复杂化)。 - 新增 `auto:notify:{account}`:`/notify/{account}/#` 只读,绑定设备时一并生成。 --- ## 四、数据库表设计(新增) > 前缀约定 `__PREFIX__` = 实际 prefix(如 `wxapp_`);时间字段 int 时间戳。 ### 1. `__PREFIX__iot_group` —— 设备分组(房间/自定义) ```sql CREATE TABLE IF NOT EXISTS `__PREFIX__iot_group` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `member_id` int unsigned NOT NULL DEFAULT '0' COMMENT '归属会员(0=后台全局分组)', `title` varchar(64) NOT NULL DEFAULT '' COMMENT '分组名', `type` varchar(16) NOT NULL DEFAULT 'room' COMMENT 'room=房间 custom=自定义分组', `sort` int NOT NULL DEFAULT '0', `create_at` int DEFAULT NULL, `update_at` int DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_member` (`member_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备分组'; ``` ### 2. `__PREFIX__iot_group_device` —— 分组-设备关联 ```sql CREATE TABLE IF NOT EXISTS `__PREFIX__iot_group_device` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `group_id` int unsigned NOT NULL DEFAULT '0', `device_id` int unsigned NOT NULL DEFAULT '0' COMMENT 'iot_device.id', `create_at` int DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_group_device` (`group_id`,`device_id`), KEY `idx_device` (`device_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分组设备关联'; ``` ### 3. `__PREFIX__iot_scene` —— 场景 ```sql CREATE TABLE IF NOT EXISTS `__PREFIX__iot_scene` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `member_id` int unsigned NOT NULL DEFAULT '0' COMMENT '归属会员(0=后台全局场景)', `title` varchar(64) NOT NULL DEFAULT '' COMMENT '场景名', `icon` varchar(32) NOT NULL DEFAULT '' COMMENT '图标标识', `enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1启用 0停用', `remark` varchar(255) DEFAULT NULL, `create_at` int DEFAULT NULL, `update_at` int DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_member` (`member_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='场景'; ``` ### 4. `__PREFIX__iot_scene_trigger` —— 场景触发器 ```sql CREATE TABLE IF NOT EXISTS `__PREFIX__iot_scene_trigger` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `scene_id` int unsigned NOT NULL DEFAULT '0', `type` varchar(16) NOT NULL DEFAULT 'device_state' COMMENT 'device_state=设备状态/事件 schedule=定时 manual=手动', `topic` varchar(255) NOT NULL DEFAULT '' COMMENT 'device_state: 主题过滤器(默认 /devices/+/state)', `condition_json` text COMMENT '条件 JSON(见下)', `cron` varchar(64) NOT NULL DEFAULT '' COMMENT 'schedule 用: 5段cron,如 0 8 * * *', `enabled` tinyint(1) NOT NULL DEFAULT '1', `create_at` int DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_scene` (`scene_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='场景触发器'; ``` `condition_json` 约定(device_state 触发时按顺序全部满足才触发): ```json [ { "field": "temperature", "op": "gt", "value": 60 }, { "field": "status", "op": "eq", "value": "on", "ref": "/devices/dev_abc123/state" } ] ``` - `field`:payload 内 JSON 字段路径(点分,如 `pos.0.t`),缺省按整条 payload 求值。 - `op`:`eq/neq/gt/gte/lt/lte/exists/contains`。 - `value`:比较值。 - `ref`(可选):引用另一设备的影子(`/devices/{uuid}/state`),不写则取**当前触发消息**的 payload。 > **state 报文改过三轮键名,路径请按 v1.7.0 词汇写**: > `pos[下标]`(**下标即位置序号,已没有 `index`**)+ `t` / `df` / `pr[[身份, 温度]]`。 > 已配置的旧路径由 `SceneService::dig()` 在 payload 是新格式时自动改写,无需手工迁移: > > | 旧(≤1.4.0) | 旧(v1.5.0 / v1.6.0) | 现在(v1.7.0) | > |---|---|---| > | `data.0.t` | `positions.0.t` | `pos.0.t` | > | `data.0.df` | `positions.0.df` | `pos.0.df` | > | `data.0.vs.1` | `positions.0.probes.1.1` | `pos.0.pr.1.1` | > | — | `positions.0.probes.1.0` | `pos.0.pr.1.0` | > | 顶层 `iv` | `interval` | `iv` | > > `pr`(v1.6.0 起叫 `probes`)从「对象数组」变成了「二元组数组」,所以 `.N` 后面那一段从 > **字段名**变成了**分量下标**(`0`=身份、`1`=温度)—— 这是旧路径唯一需要"猜"的地方, > 改写规则就是按这个映射。 > > 两个**迁不了**的(路径取不到值 → 条件恒不成立,必须人工改): > - `v`(旧的有效性标志):v1.5.0 起字段已删,等价于「`t !== null`」→ > 改用对 `pos.N.t` 判 `exists` / `gt` > - `i` / `index`(位置号):v1.6.0 起位置序号就是数组下标,报文里不再有这个字段 > > ⚠️ 场景路径会"跟着线上词汇走",而本项目已经改过三轮(`data`→`positions`→`pos`、 > `iv`→`interval`→`iv`)。**每次改报文键名都必须同步 `SceneService::upgradeStatePath()` 的 > `$map`**,否则库里已存的场景条件会取不到值 → **静默**恒不成立(不报错,最难查)。 > > 例:`{"field":"pos.0.t","op":"gt","value":60}` = 位置 0 平均温度超 60℃。 ### 5. `__PREFIX__iot_scene_action` —— 场景动作 ```sql CREATE TABLE IF NOT EXISTS `__PREFIX__iot_scene_action` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `scene_id` int unsigned NOT NULL DEFAULT '0', `sort` int NOT NULL DEFAULT '0', `type` varchar(16) NOT NULL DEFAULT 'send_cmd' COMMENT 'send_cmd=下发命令 http=HTTP回调 notify=站内告警', `device_uuid` varchar(64) NOT NULL DEFAULT '' COMMENT 'send_cmd: 目标设备', `payload_json` text COMMENT 'send_cmd: 命令payload(JSON) / http: 请求体', `topic` varchar(255) NOT NULL DEFAULT '' COMMENT 'http: 回调URL 或 notify: 通知标题', `create_at` int DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_scene` (`scene_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='场景动作'; ``` 动作语义: - `send_cmd`:`Store::enqueueManual('/devices/{uuid}/cmd', payload_json)` 下发命令(在线即达,离线入队)。 - `http`:`topic` 为回调 URL,`payload_json` 为请求体,POST 触发(可对接第三方 webhook/小程序推送)。 - `notify`:写 `iot_alert`(站内告警),并 retain 到 `/notify/{account}/notice`。 ### 6. `__PREFIX__iot_scene_log` —— 场景执行日志 ```sql CREATE TABLE IF NOT EXISTS `__PREFIX__iot_scene_log` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `scene_id` int unsigned NOT NULL DEFAULT '0', `trigger_type` varchar(16) NOT NULL DEFAULT '', `matched` tinyint(1) NOT NULL DEFAULT '0' COMMENT '1命中 0未命中', `message` text COMMENT '命中摘要/失败原因', `create_at` int DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_scene` (`scene_id`), KEY `idx_create_at` (`create_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='场景执行日志'; ``` ### 7. `__PREFIX__iot_ota` —— 固件版本 ```sql CREATE TABLE IF NOT EXISTS `__PREFIX__iot_ota` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `model` varchar(64) NOT NULL DEFAULT '' COMMENT '适用设备型号', `version` varchar(32) NOT NULL DEFAULT '' COMMENT '版本号', `filename` varchar(128) NOT NULL DEFAULT '' COMMENT '固件文件名', `file_hash` varchar(64) NOT NULL DEFAULT '' COMMENT 'SHA256', `size` int unsigned NOT NULL DEFAULT '0' COMMENT '字节数', `url` varchar(255) NOT NULL DEFAULT '' COMMENT '固件下载地址', `remark` varchar(255) DEFAULT NULL, `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1可下发 0停用', `create_at` int DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_model` (`model`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OTA固件版本'; ``` ### 8. `__PREFIX__iot_device_log` —— 设备日志(设备维度) ```sql CREATE TABLE IF NOT EXISTS `__PREFIX__iot_device_log` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `uuid` varchar(64) NOT NULL DEFAULT '' COMMENT '设备uuid', `topic` varchar(255) NOT NULL DEFAULT '', `payload` longtext, `direction` varchar(8) NOT NULL DEFAULT 'in' COMMENT 'in=设备上报 out=下发', `create_at` int DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_uuid` (`uuid`), KEY `idx_create_at` (`create_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备日志'; ``` ### 9. `__PREFIX__iot_alert` —— 告警记录 ```sql CREATE TABLE IF NOT EXISTS `__PREFIX__iot_alert` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `member_id` int unsigned NOT NULL DEFAULT '0' COMMENT '归属会员', `uuid` varchar(64) NOT NULL DEFAULT '' COMMENT '关联设备(可空)', `type` varchar(32) NOT NULL DEFAULT '' COMMENT 'offline/scene/condition/custom', `level` tinyint(1) NOT NULL DEFAULT '2' COMMENT '1提示 2警告 3严重', `title` varchar(128) NOT NULL DEFAULT '', `content` varchar(512) NOT NULL DEFAULT '', `is_read` tinyint(1) NOT NULL DEFAULT '0', `create_at` int DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_member` (`member_id`), KEY `idx_create_at` (`create_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='告警记录'; ``` ### 自愈建表 所有新表由 `Auth::ensureTables()`(或独立 `SchemaService`)追加 `CREATE TABLE IF NOT EXISTS`,遵循现有自愈模式,老库无需重装即可用。 --- ## 五、服务层模块设计(新增) ### 1. `SceneService`(场景/自动化引擎)—— 核心 ``` 事件流: Broker::handlePublish ├─ 持久化 retain / message 日志 ├─ 写 iot_device_log(uuid 维度,若 topic 属于 /devices/{uuid}/#) └─ event('mqtt_message_published', [topic, payload, client_id]) └─ SceneService::evaluate($topic, $payload, $clientId) // 同步评估,动作异步投递 ``` ```php class SceneService { /** 规则内存缓存(TTL 30s,同 Auth ACL 模式;Broker 单进程,进程内安全) */ protected static array $sceneCache = []; protected static int $loadedAt = 0; /** Broker 事件钩子入口 */ public static function onMessagePublished(string $topic, string $payload, string $clientId): void; /** 评估:遍历启用场景的 device_state 触发器 */ protected static function matchTrigger(array $scene, string $topic, string $payload): ?array; /** 条件解析:condition_json + 可选 ref 影子读取 */ protected static function matchCondition(array $conds, string $payload, string $topic): bool; /** 执行动作(全部入 outbox 异步投递,不阻塞 Broker) */ protected static function runActions(array $scene, string $triggerType): void; /** 定时触发:Workerman Timer 每分钟扫描 cron 命中场景(单独挂载) */ public static function onTick(): void; /** 手动触发(App/后台一键执行场景) */ public static function runScene(int $sceneId): array; } ``` **设计要点**: - 评估在 Broker 进程内同步进行,但**只做内存判断**;动作一律 `Store::enqueueManual()` 异步投递,DB 写操作轻量(日志/告警),不影响消息分发主循环。 - 引用其他设备影子时读 `iot_retain`(或 Broker 进程内 retained 缓存),避免实时依赖。 - `cron` 解析:5 段(分 时 日 月 周),实现一个最小解析器(约 40 行),不引入第三方依赖。 ### 2. `GroupService`(设备分组) ```php class GroupService { public static function ensureTable(): bool; public static function create(int $memberId, string $title, string $type = 'room'): array; public static function rename(int $groupId, int $memberId, string $title): void; public static function remove(int $groupId, int $memberId): void; public static function groups(int $memberId): array; // 含每组的设备数/在线数 public static function addDevice(int $groupId, int $deviceId): void; public static function removeDevice(int $groupId, int $deviceId): void; public static function devicesByGroup(int $groupId): array; // 复用 DeviceService::myDevices 增强字段 } ``` ### 3. `AlertService`(告警/通知) ```php class AlertService { public static function ensureTable(): bool; public static function add(int $memberId, string $type, int $level, string $title, string $content, string $uuid = ''): int; public static function push(int $memberId, string $topic, string $payload): void; // retain 到 /notify/{account}/{topic} public static function list(int $memberId, int $page = 1, int $size = 20): array; public static function markRead(int $memberId, array $ids = []): void; public static function onDeviceOffline(string $uuid): void; // 可选:断线遗嘱触发离线告警 } ``` ### 4. `OtaService`(固件升级) ```php class OtaService { public static function ensureTable(): bool; public static function upload(string $model, string $version, string $file, string $url): array; // 计算SHA256/大小 public static function list(string $model = ''): array; public static function publish(int $otaId, array $deviceUuids): void; // 逐个 enqueueManual 到 /devices/{uuid}/ota public static function onReport(string $uuid, string $payload): void; // 解析 /ota/report 进度写日志/告警 } ``` --- ## 六、App 端 API 接口清单 > 统一前缀 `/iot/api/client/`,需会员 JWT。响应 `{code, msg, data, timestamp}`,code=0 成功。 ### 分组(GroupService) | 方法 | 接口 | 参数 | 说明 | |---|---|---|---| | GET | `groupList` | — | 我的分组(含设备数/在线数) | | POST | `saveGroup` | `title`, `type`, `id?` | 新建/编辑分组 | | POST | `deleteGroup` | `id` | 删除分组 | | POST | `groupAddDevice` | `group_id`, `device_id` | 设备加入分组 | | POST | `groupRemoveDevice` | `group_id`, `device_id` | 移出分组 | | GET | `groupDevices` | `group_id` | 组内设备(含影子/在线) | ### 场景(SceneService) | 方法 | 接口 | 参数 | 说明 | |---|---|---|---| | GET | `sceneList` | — | 我的场景列表 | | GET | `sceneDetail` | `id` | 场景详情(触发器+动作) | | POST | `saveScene` | `title`, `icon`, `triggers[]`, `actions[]`, `id?` | 新建/编辑(triggers/actions 为 JSON 数组字符串) | | POST | `deleteScene` | `id` | 删除 | | POST | `toggleScene` | `id`, `enabled` | 启用/停用 | | POST | `runScene` | `id` | 手动执行(触发 manual 型动作) | | GET | `sceneLogs` | `id`, `page` | 场景执行日志 | ### 告警(AlertService) | 方法 | 接口 | 参数 | 说明 | |---|---|---|---| | GET | `alertList` | `page`, `size`, `is_read?` | 我的告警 | | POST | `readAlert` | `ids[]` | 标记已读 | | GET | `alertUnread` | — | 未读数(App 角标轮询) | ### 设备日志 / OTA | 方法 | 接口 | 参数 | 说明 | |---|---|---|---| | GET | `deviceLogs` | `uuid`, `page` | 设备日志(in/out) | | GET | `otaList` | `model` | 可用固件列表 | | POST | `publishOta` | `ota_id`, `uuids[]` | App 触发升级(校验设备归属) | ### 实时通道(MQTT 直连,需 `allow_member_login`) | 用途 | 订阅/发布 | 说明 | |---|---|---| | 设备实时状态 | 订阅 `/devices/{uuid}/state` | ACL `auto:member:{uuid}:state` 已授权 | | 事件流 | 订阅 `/devices/{uuid}/event` | 新增只读授权 | | 通知 | 订阅 `/notify/{account}/#` | 新增 `auto:notify:{account}` 只读授权 | | 控制 | 发布 `/devices/{uuid}/cmd` | ACL 已授权(或走 REST `sendCommand`) | --- ## 七、后台管理接口清单 > 前缀 `/iot/backend/...`,AdminAuth 鉴权。 | 方法 | 接口 | 说明 | |---|---|---| | GET | `group` | 分组管理页 | | POST | `saveGroup` | 新建/编辑(全局分组 member_id=0) | | POST | `deleteGroup` | 删除 | | GET | `scene` | 场景管理页(全局场景) | | POST | `saveScene` / `deleteScene` | 场景维护 | | GET | `sceneLogs` | 场景日志页 | | GET | `ota` | 固件管理页 | | POST | `uploadOta` / `deleteOta` / `publishOta` | 固件维护/下发 | | GET | `alerts` | 告警列表页 | | POST | `readAlert` / `deleteAlert` | 告警处理 | | GET | `deviceLogs` | 设备日志页(按 uuid 过滤) | 菜单 `menu.json` 新增子项:智能设备(已有)→ 分组管理 / 场景联动 / 固件升级 / 告警中心(sort 7.x)。 --- ## 八、场景引擎触发时序 ``` 设备 publish /devices/dev_x/state payload:{"temperature":65,"status":"on"} │ ▼ Broker::handlePublish ├─ retain 持久化(影子更新) ├─ message 日志 + device_log(uuid 维度) ├─ 转发给在线订阅者(App 实时) └─ event('mqtt_message_published') │ ▼ SceneService::onMessagePublished ├─ 加载启用场景(内存缓存 TTL30s) ├─ 逐个匹配 device_state 触发器: │ ├─ topic 匹配(/devices/+/state ✓) │ ├─ 条件全部满足?(temperature>60 ✓) │ └─ 命中 → 该场景进入执行 └─ runActions(scene) ├─ 写 scene_log(matched=1) ├─ action[send_cmd] → Store::enqueueManual('/devices/dev_y/cmd', {"power":"on"}) ├─ action[http] → HTTP POST webhook(异步) └─ action[notify] → AlertService::add + push('/notify/{account}/notice') │ ▼ Broker 消费 outbox → 下发到设备 /devices/dev_y/cmd ▼ App 收到 /notify/{account}/notice(在线即推 / retain 兜底) ``` **定时触发器**:Workerman Timer(每分钟)→ `SceneService::onTick()` → 匹配 `cron` 命中的 schedule 型场景 → `runActions`。手动触发:App `runScene` → 直接执行对应场景动作。 --- ## 九、OTA 流程 ``` 后台上传固件(uploadOta) → 计算SHA256/大小 → 存 iot_ota │ ▼ 选中设备 publishOta Store::enqueueManual('/devices/{uuid}/ota', {"version":"1.2.0","url":"https://...","hash":"...","size":12345}) │ ▼ 设备收到 → 下载固件 → 校验哈希 → 升级 设备上报 /devices/{uuid}/ota/report {"state":"downloading","progress":42} │ ▼ Broker → OtaService::onReport() ├─ 写 device_log ├─ 进度变更 → /notify/{account}/ota 通知 └─ state=done/fail → 告警/提示 ``` --- ## 十、告警触发源汇总 | 触发源 | 方式 | 说明 | |---|---|---| | 设备离线 | Broker 遗嘱消息 → `AlertService::onDeviceOffline` | 优雅/非优雅断开均可达(可选开关) | | 场景条件命中 | `runActions` 内 `notify` 动作 | 与场景引擎天然复用 | | 设备主动上报 | 设备 publish 到 `/devices/{uuid}/event` | payload 含 level/title/content 即转告警 | | OTA 失败 | `OtaService::onReport` 异常分支 | 升级失败告警 | --- ## 十一、实施路线图(分阶段) | 阶段 | 内容 | 交付物 | 依赖 | |---|---|---|---| | **P0** | 前置收敛:确认清理 `runtime/route_list.php` 使现有 `/iot/api/client/*` 路由生效 | 路由联调 | — | | **P1** | 基础增强:`device_log` + `alert` 表、Broker 钩子写日志、遗嘱离线告警 | `DeviceLogService`/`AlertService` | — | | **P2** | 场景引擎:`scene/trigger/action/log` 表 + `SceneService` + cron 定时器 + 后台管理页 + App API | 场景自动化核心闭环 | P1 | | **P3** | 设备分组:`group` 表 + `GroupService` + 后台 + App API + `/notify` 主题与 ACL | 分组与通知 | P2 | | **P4** | OTA:`ota` 表 + `OtaService` + 后台上传/下发 + 设备上报处理 | 固件升级闭环 | P3 | | **P5** | App 实时推送文档 + 端到端联调验证脚本 | 验证报告 | P2-P4 | **每阶段验收**:后台可用 + App API 可用 + `runtime/_devtest.php` 自动化断言通过(沿用现有测试脚本模式)。 --- ## 十二、风险与注意 1. **Broker 单进程**:场景评估是同步的,条件/动作必须轻量;动作入 outbox 异步投递,禁止在评估中同步发送 MQTT。 2. **cron 解析**:实现最小 5 段解析器即可(不支持秒/年,避免依赖)。 3. **路由缓存**:新增后台/API 路由后必须清 `runtime/route_list.php`(沿用 `Remove-Item runtime/route_list.php`)。 4. **自愈**:所有新表走 `ensureTables()` 自愈建表,老库升级无需重装插件。 5. **命名**:新表/字段遵守框架铁律(禁裸 `name`、`__PREFIX__`、`create_at/update_at/delete_at`)。 6. **ACL 安全**:`/notify/{account}/#` 只能放行对应账号,绑定/解绑设备时同步生成/清理 `auto:notify:{account}` 规则(同 `syncMemberAcl`/`clearMemberAcl` 模式)。