Files
YwxAppThink/addon/iot/route/app.php
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

192 lines
12 KiB
PHP
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.
<?php
/*
* @Author: YwxApp <ywx@ywxapp.cn>
* @Date: 2026-07-10 17:48:22
* @LastEditors: YwxApp <ywx@ywxapp.cn>
* @LastEditTime: 2026-09-18 02:09:05
* @Description:
* @FilePath: \ywxapp_dev\addon\iot\route\app.php
* @CustomString: Copyright (c) 2026 YwxApp
*/
// +----------------------------------------------------------------------
// | YwxApp IoT MQTT 路由
// +----------------------------------------------------------------------
// | 说明:
// | 1) /iot/backend/* 后台管理(由框架 boot 阶段 loadAddonRoutes 嵌套注册,并追加 iot 前缀)
// | 2) /iot/api/* App 端接口(插件作为 addon 应用独立挂载时,本文件会被“顶层”再引入一次,
// | 因此会出现 嵌套(iot/...) 与 顶层(api/...) 双份注册)
// |
// | ⚠️ 关键坑:think 字符串路由目标会自动注册同名“路由名”,且 RuleGroup::checkUrl() 在
// | url_route_must=false 时:若请求 URL 恰等于某已注册路由名、且同名第一条 rule 与 URL 不同,
// | 会直接拦截(“定义过路由地址的 不支持访问”)。本插件 client/* 规则在双份注册下
// | 嵌套版(rule=iot/api/client/x)先注册 → 顶层 URL=api/client/x 与其同名 → 被拦 → 404。
// | 修复:client/* 采用 [类名, 方法] 数组目标(自动名为空,不进入路由名索引)。
// | 单段规则字符串目标不与 URL 全等,保持原样即可。
// +----------------------------------------------------------------------
use think\facade\Route;
use addon\iot\controller\api\Client;
use addon\iot\controller\api\Monitor;
use addon\iot\controller\api\Scene;
use addon\iot\controller\api\Ota;
use addon\iot\controller\api\Mqtt;
// ===================== 前台展示页(免登录门面) =====================
// GET /iot/index/index —— 插件功能/能力/优势介绍 + 实时指标快照
// 与 articles 插件同构:控制器在 controller/ 根、继承 FrontendBase(视图根自动切
// view/frontend/、游客放行),字符串路由目标,模板 view/frontend/index/index.html
Route::get('index/index', 'addon\iot\controller\Index@index');
// ===================== 后台(Backend) =====================
Route::group('backend', function () {
Route::get('index', 'backend/Overview/index');
Route::get('statsData', 'backend/Overview/statsData');
Route::get('connections', 'backend/Connections/connections');
Route::get('connectionsTable', 'backend/Connections/connectionsTable');
Route::get('messages', 'backend/Messages/messages');
Route::get('messagesTable', 'backend/Messages/messagesTable');
Route::get('publish', 'backend/Publish/publish');
Route::post('publish', 'backend/Publish/doPublish');
Route::get('setting', 'backend/Setting/setting');
Route::post('setting', 'backend/Setting/saveSetting');
Route::get('auth', 'backend/Auth/auth');
Route::get('authTable', 'backend/Auth/authTable');
Route::post('saveAuth', 'backend/Auth/saveAuth');
Route::post('deleteAuth', 'backend/Auth/deleteAuth');
Route::get('acl', 'backend/Acl/acl');
Route::get('aclTable', 'backend/Acl/aclTable');
Route::post('saveAcl', 'backend/Acl/saveAcl');
Route::post('deleteAcl', 'backend/Acl/deleteAcl');
Route::get('device', 'backend/Devices/device');
Route::get('deviceTable', 'backend/Devices/deviceTable');
Route::post('saveDevice', 'backend/Devices/saveDevice');
Route::post('resetDeviceSecret', 'backend/Devices/resetDeviceSecret');
Route::post('resetActivateToken', 'backend/Devices/resetActivateToken');
Route::post('deleteDevice', 'backend/Devices/deleteDevice');
Route::get('rule', 'backend/Rules/rule');
Route::get('ruleTable', 'backend/Rules/ruleTable');
Route::post('saveRule', 'backend/Rules/saveRule');
Route::post('deleteRule', 'backend/Rules/deleteRule');
Route::get('scene', 'backend/Scenes/scene');
Route::get('sceneDetail', 'backend/Scenes/sceneDetail');
Route::post('saveScene', 'backend/Scenes/saveScene');
Route::post('deleteScene', 'backend/Scenes/deleteScene');
Route::post('toggleScene', 'backend/Scenes/toggleScene');
Route::post('runScene', 'backend/Scenes/runScene');
Route::get('sceneLogs', 'backend/Scenes/sceneLogs');
Route::get('group', 'backend/Groups/group');
Route::post('saveGroup', 'backend/Groups/saveGroup');
Route::post('deleteGroup', 'backend/Groups/deleteGroup');
Route::get('deviceLogs', 'backend/Devices/deviceLogs');
// 传感器位置配置(配置真源:后台编辑 → retained 下发设备 → App 共用同一份)
Route::get('deviceConfig', 'backend/Devices/deviceConfig');
Route::post('saveDeviceConfig', 'backend/Devices/saveDeviceConfig');
Route::get('alertsPage', 'backend/Alerts/alertsPage');
Route::get('alerts', 'backend/Alerts/alerts');
Route::post('readAlertAll', 'backend/Alerts/readAlertAll');
Route::get('ota', 'backend/Ota/ota');
Route::get('otaList', 'backend/Ota/otaList');
Route::get('otaTable', 'backend/Ota/otaTable');
Route::post('saveOta', 'backend/Ota/saveOta');
Route::post('deleteOta', 'backend/Ota/deleteOta');
Route::post('toggleOta', 'backend/Ota/toggleOta');
Route::post('publishOta', 'backend/Ota/publishOta');
});
// ===================== App API =====================
// URL 按控制器域分组,一眼可辨:
// api/mqtt/* Broker 状态/直发(Mqtt 控制器)
// api/client/* 会员与设备管理(Client 控制器:绑定/共享/票据/命令)
// api/monitor/* 监测域(Monitor 控制器:传感器配置/曲线/日志/告警)
// api/scene/* 联动域(Scene 控制器:场景 + 分组)
// api/ota/* 固件升级(Ota 控制器)
// deviceActivate 保持顶层(设备固件侧入网,不带域)
//
// 文件末尾保留一段旧 client/* 兼容别名(URL 迁移期),App 全量切到新 URL 后可整段删除。
Route::group('api', function () {
// ---------- Broker(Mqtt 控制器) ----------
Route::get('mqtt/status', [Mqtt::class, 'status']);
Route::post('mqtt/publish', [Mqtt::class, 'publish']);
// 设备激活(公开,设备侧入云换 uuid/secret)
Route::post('deviceActivate', [Client::class, 'deviceActivate']);
// ---------- 会员/设备域(Client 控制器) ----------
Route::get('client/devices', [Client::class, 'devices']);
Route::get('client/subscriptions', [Client::class, 'subscriptions']);
Route::post('client/publish', [Client::class, 'publish']);
Route::post('client/bindDevice', [Client::class, 'bindDevice']);
Route::get('client/myDevices', [Client::class, 'myDevices']);
Route::post('client/unbindDevice', [Client::class, 'unbindDevice']);
Route::post('client/resetSecret', [Client::class, 'resetSecret']);
Route::post('client/mqttTicket', [Client::class, 'mqttTicket']);
Route::post('client/sendCommand', [Client::class, 'sendCommand']);
Route::get('client/deviceState', [Client::class, 'deviceState']);
Route::post('client/shareDevice', [Client::class, 'shareDevice']);
Route::post('client/revokeShare', [Client::class, 'revokeShare']);
Route::get('client/shareList', [Client::class, 'shareList']);
// ---------- 监测域(Monitor 控制器:传感器配置/曲线/日志/告警) ----------
// 传感器位置配置(配置真源:后台编辑 → retained 下发设备 → App 共用同一份)
Route::get('monitor/deviceConfig', [Monitor::class, 'deviceConfig']);
Route::post('monitor/saveDeviceConfig', [Monitor::class, 'saveDeviceConfig']);
// 传感器历史曲线(服务端结构化时序 iot_sensor_point,稀疏点 + interval 供前向填充)
Route::get('monitor/sensorSeries', [Monitor::class, 'sensorSeries']);
// 传感器分钟级聚合曲线(时间加权平均,step/window 可控:最近30/60分钟每分钟一点等)
Route::get('monitor/sensorAvg', [Monitor::class, 'sensorAvg']);
// 最近 N 分钟窗口平均(按位置汇总,时间加权;window=分钟数,返回单条 avg/min/max/samples)
Route::get('monitor/recentAvg', [Monitor::class, 'recentAvg']);
Route::get('monitor/deviceLogs', [Monitor::class, 'deviceLogs']);
Route::get('monitor/alertList', [Monitor::class, 'alertList']);
Route::post('monitor/readAlert', [Monitor::class, 'readAlert']);
Route::get('monitor/alertUnread', [Monitor::class, 'alertUnread']);
// ---------- 联动域(Scene 控制器:场景 + 分组) ----------
Route::get('scene/sceneList', [Scene::class, 'sceneList']);
Route::get('scene/sceneDetail', [Scene::class, 'sceneDetail']);
Route::post('scene/saveScene', [Scene::class, 'saveScene']);
Route::post('scene/deleteScene', [Scene::class, 'deleteScene']);
Route::post('scene/toggleScene', [Scene::class, 'toggleScene']);
Route::post('scene/runScene', [Scene::class, 'runScene']);
Route::get('scene/sceneLogs', [Scene::class, 'sceneLogs']);
Route::get('scene/groupList', [Scene::class, 'groupList']);
Route::post('scene/saveGroup', [Scene::class, 'saveGroup']);
Route::post('scene/deleteGroup', [Scene::class, 'deleteGroup']);
Route::post('scene/groupAddDevice', [Scene::class, 'groupAddDevice']);
Route::post('scene/groupRemoveDevice', [Scene::class, 'groupRemoveDevice']);
Route::get('scene/groupDevices', [Scene::class, 'groupDevices']);
// ---------- OTA 域(Ota 控制器) ----------
Route::get('ota/otaList', [Ota::class, 'otaList']);
Route::post('ota/publishOta', [Ota::class, 'publishOta']);
// ================ 旧 client/* 兼容别名(迁移期保留,App 切完新 URL 后整段删除) ================
// 全部数组目标:不注册路由名,规避双份注册同名拦截(见文件头 ⚠️)
Route::get('client/deviceConfig', [Monitor::class, 'deviceConfig']);
Route::post('client/saveDeviceConfig', [Monitor::class, 'saveDeviceConfig']);
Route::get('client/sensorSeries', [Monitor::class, 'sensorSeries']);
Route::get('client/sensorAvg', [Monitor::class, 'sensorAvg']);
Route::get('client/recentAvg', [Monitor::class, 'recentAvg']);
Route::get('client/deviceLogs', [Monitor::class, 'deviceLogs']);
Route::get('client/alertList', [Monitor::class, 'alertList']);
Route::post('client/readAlert', [Monitor::class, 'readAlert']);
Route::get('client/alertUnread', [Monitor::class, 'alertUnread']);
Route::get('client/sceneList', [Scene::class, 'sceneList']);
Route::get('client/sceneDetail', [Scene::class, 'sceneDetail']);
Route::post('client/saveScene', [Scene::class, 'saveScene']);
Route::post('client/deleteScene', [Scene::class, 'deleteScene']);
Route::post('client/toggleScene', [Scene::class, 'toggleScene']);
Route::post('client/runScene', [Scene::class, 'runScene']);
Route::get('client/sceneLogs', [Scene::class, 'sceneLogs']);
Route::get('client/groupList', [Scene::class, 'groupList']);
Route::post('client/saveGroup', [Scene::class, 'saveGroup']);
Route::post('client/deleteGroup', [Scene::class, 'deleteGroup']);
Route::post('client/groupAddDevice', [Scene::class, 'groupAddDevice']);
Route::post('client/groupRemoveDevice', [Scene::class, 'groupRemoveDevice']);
Route::get('client/groupDevices', [Scene::class, 'groupDevices']);
Route::get('client/otaList', [Ota::class, 'otaList']);
Route::post('client/publishOta', [Ota::class, 'publishOta']);
// Broker 旧顶层路径(/iot/api/status、/iot/api/publish)
Route::get('status', [Mqtt::class, 'status']);
Route::post('publish', [Mqtt::class, 'publish']);
})->namespace('addon\iot\controller\api');
Route::get('/', 'Index/index');