安全(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(市场 / 开发者中心 / 审核 / 分润)
192 lines
12 KiB
PHP
192 lines
12 KiB
PHP
<?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'); |