安全(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(市场 / 开发者中心 / 审核 / 分润)
26 KiB
智慧生活全流程设计(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 —— 设备分组(房间/自定义)
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 —— 分组-设备关联
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 —— 场景
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 —— 场景触发器
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 触发时按顺序全部满足才触发):
[
{ "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.tpositions.0.tpos.0.tdata.0.dfpositions.0.dfpos.0.dfdata.0.vs.1positions.0.probes.1.1pos.0.pr.1.1— positions.0.probes.1.0pos.0.pr.1.0顶层 ivintervaliv
pr(v1.6.0 起叫probes)从「对象数组」变成了「二元组数组」,所以.N后面那一段从 字段名变成了分量下标(0=身份、1=温度)—— 这是旧路径唯一需要"猜"的地方, 改写规则就是按这个映射。两个迁不了的(路径取不到值 → 条件恒不成立,必须人工改):
v(旧的有效性标志):v1.5.0 起字段已删,等价于「t !== null」→ 改用对pos.N.t判exists/gti/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 —— 场景动作
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 —— 场景执行日志
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 —— 固件版本
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 —— 设备日志(设备维度)
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 —— 告警记录
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) // 同步评估,动作异步投递
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(设备分组)
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(告警/通知)
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(固件升级)
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 自动化断言通过(沿用现有测试脚本模式)。
十二、风险与注意
- Broker 单进程:场景评估是同步的,条件/动作必须轻量;动作入 outbox 异步投递,禁止在评估中同步发送 MQTT。
- cron 解析:实现最小 5 段解析器即可(不支持秒/年,避免依赖)。
- 路由缓存:新增后台/API 路由后必须清
runtime/route_list.php(沿用Remove-Item runtime/route_list.php)。 - 自愈:所有新表走
ensureTables()自愈建表,老库升级无需重装插件。 - 命名:新表/字段遵守框架铁律(禁裸
name、__PREFIX__、create_at/update_at/delete_at)。 - ACL 安全:
/notify/{account}/#只能放行对应账号,绑定/解绑设备时同步生成/清理auto:notify:{account}规则(同syncMemberAcl/clearMemberAcl模式)。