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:
yangwx committed 2026-10-22 00:53:39 +08:00
1 parent 1d49e6f5ee
commit 18101cf11a
636 files changed
+58517 -9519

No files matched your search

+192
View File
@@ -0,0 +1,192 @@
<?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');