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(市场 / 开发者中心 / 审核 / 分润)
This commit is contained in:
1 parent
1d49e6f5ee
commit
18101cf11a
636 files changed
+58517
-9519
No files matched your search
@@ -0,0 +1,547 @@
|
||||
# 智慧生活全流程设计(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` 模式)。
|
||||
Reference in new issue
Block a user