Files
YwxAppThink/docs/guide/smart_home_design.md
T
yangwx 18101cf11a feat: 上线前安全加固与发布工程化;appmall 由插件迁移为独立应用
安全(P0)
- 发布包签名:新增 scripts/package_keygen.php、scripts/package_verify.php、
  ywxapp\service\PackageVerifier;FrameworkService::apply() 解压前强制验签
  (ywxapp.framework_package_verify,默认开启)
- JWT 收紧:config/jwt.php 移除公开默认密钥,JwtService::assertStrongSecret()
  校验非空 / 长度≥32 / 已知弱值;.env.production.example 键名由 JWT_SECRET_KEY 修正为 JWT_SECRET
- /api/v1/Article:免登录白名单收窄为只读接口,写操作字段白名单 + assertOwner() 越权校验
- 开发者令牌:查找接口不再回显明文、统一失败文案 + 限流
- TemplateInstaller:文件名白名单 + realpath 归一化 + 目录内断言,修复 Zip Slip;补 use ZipArchive
- 后台登录:接入验证码、失败锁定与登录限流(config/ratelimit.php)
- 缓存 / Session 驱动可由 .env 覆盖(CACHE_DRIVER / SESSION_TYPE),集群部署改 redis
- paycenter 支付日志开关改为 PAY_LOG,不再跟随 APP_DEBUG

工程化 / 交付
- 新增 CHANGELOG.md 与 .env.production.example;composer.lock 入库锁定依赖版本
- 插件离线安装 CLI 增加 --uid,后台上传安装透传 uid,付费插件授权校验可通过
- 授权拒绝文案区分「未携带 uid」与「未找到有效授权」,便于排障
- .gitignore 放行 .env.production.example / .env.addon.example / .gitattributes / public/.htaccess

结构
- appmall 由 addon/appmall 迁移为独立应用 app/appmall(市场 / 开发者中心 / 审核 / 分润)
2026-10-22 00:53:39 +08:00

548 lines
26 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.
# 智慧生活全流程设计(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` 模式)。