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

26 KiB
Raw Blame History

智慧生活全流程设计(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.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 —— 场景动作

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 自动化断言通过(沿用现有测试脚本模式)。


十二、风险与注意

  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 模式)。