安全(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(市场 / 开发者中心 / 审核 / 分润)
136 lines
5.8 KiB
PHP
136 lines
5.8 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||
// +----------------------------------------------------------------------
|
||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||
// +----------------------------------------------------------------------
|
||
// | Author: ywxapp<admin@ywxapp.cn>
|
||
// +----------------------------------------------------------------------
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace addon\iot\controller;
|
||
|
||
use think\facade\Db;
|
||
use ywxapp\controller\FrontendBase;
|
||
|
||
/**
|
||
* IoT 插件前台展示页(免登录门面,对外介绍功能/能力/优势)
|
||
*
|
||
* GET /iot/index/index
|
||
*
|
||
* 遵循插件前台开发约定(与 articles/Index 同构):
|
||
* - 继承 FrontendBase:视图根自动切到 addon/iot/view/frontend/,
|
||
* noNeedLogin 默认 ['*'] 游客放行,fetch 叠加皮肤覆盖层;
|
||
* - 控制器放 controller/ 根(非 api/backend 子目录),
|
||
* 路由用字符串目标 'addon\iot\controller\Index@index'。
|
||
* 数据侧只读聚合指标,全部 try/catch 兜底 —— 任何一张表缺失/异常都
|
||
* 降级为 0,绝不 500(门面页的可用性 > 数据完整性)。
|
||
*/
|
||
class Index extends FrontendBase
|
||
{
|
||
/**
|
||
* 展示页
|
||
*/
|
||
public function index()
|
||
{
|
||
$this->assign('stats', $this->liveStats());
|
||
$this->assign('apiGroups', $this->apiCatalog());
|
||
$this->assign('now', date('Y-m-d H:i'));
|
||
return $this->fetch('index/index');
|
||
}
|
||
|
||
/**
|
||
* 实时聚合指标(每项独立兜底,缺表/异常返回 0)
|
||
*/
|
||
protected function liveStats(): array
|
||
{
|
||
$count = function (string $table, array $where = []): int {
|
||
try {
|
||
return (int) Db::name($table)->where($where)->count();
|
||
} catch (\Throwable $e) {
|
||
return 0;
|
||
}
|
||
};
|
||
// Broker 快照(iot_stats id=1,Broker 进程每 2s 写入)
|
||
$broker = ['uptime' => 0, 'clients_online' => 0, 'messages_received' => 0, 'retained' => 0];
|
||
try {
|
||
$row = Db::name('iot_stats')->where('id', 1)->find();
|
||
if ($row) {
|
||
$broker = array_merge($broker, array_intersect_key($row, $broker));
|
||
}
|
||
} catch (\Throwable $e) {
|
||
}
|
||
return [
|
||
'uptime' => $this->fmtUptime((int) $broker['uptime']),
|
||
'online' => (int) $broker['clients_online'],
|
||
'recv' => (int) $broker['messages_received'],
|
||
'retained' => (int) $broker['retained'],
|
||
'devices' => $count('iot_device'),
|
||
'points' => $count('iot_sensor_point'),
|
||
'alerts' => $count('iot_alert'),
|
||
'scenes' => $count('iot_scene'),
|
||
'shares' => $count('iot_share', ['status' => 1]),
|
||
'ota_pkgs' => $count('iot_ota'),
|
||
];
|
||
}
|
||
|
||
/** 供模板格式化运行时长(时:分:秒) */
|
||
protected function fmtUptime(int $s): string
|
||
{
|
||
return sprintf('%d:%02d:%02d', intdiv($s, 3600), intdiv($s % 3600, 60), $s % 60);
|
||
}
|
||
|
||
/**
|
||
* App API 能力域统计:从 route/app.php 自动解析(路由唯一真源)。
|
||
* 宣传页只展示功能域划分与接口数量——不输出任何端点路径 / 主题字符串
|
||
* (公开页面不做攻击者的侦察地图;详细契约见 docs/guide/iot_api_reference.md)。
|
||
*
|
||
* 解析约定:只匹配数组目标规则(本插件全部 App API 均为 [Xxx::class,'action'] 书写;
|
||
* backend/* 与前台页是字符串目标,天然不会被误收)。
|
||
* 同一 controller+action 只计首次出现:新域前缀 URL 注册在前、文件末尾的
|
||
* 旧 client/* 兼容别名在后,去重避免兼容别名把数量翻倍。
|
||
*/
|
||
protected function apiCatalog(): array
|
||
{
|
||
try {
|
||
$file = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'route' . DIRECTORY_SEPARATOR . 'app.php';
|
||
if (!is_file($file) || !preg_match_all(
|
||
"#Route::(get|post)\(\s*'([^']+)'\s*,\s*\[(\w+)::class\s*,\s*'(\w+)'\]#",
|
||
(string) @file_get_contents($file),
|
||
$ms,
|
||
PREG_SET_ORDER
|
||
)) {
|
||
return [];
|
||
}
|
||
} catch (\Throwable $e) {
|
||
return [];
|
||
}
|
||
$domains = [
|
||
'mqtt' => ['ico' => '📡', 'label' => 'Broker 运维', 'blurb' => '运行状态、指标快照与消息直发'],
|
||
'client' => ['ico' => '📱', 'label' => '设备与会员', 'blurb' => '绑定认领、共享授权、短时票据与命令下发'],
|
||
'monitor' => ['ico' => '📈', 'label' => '实时监测', 'blurb' => '历史曲线、分钟级聚合、告警与设备日志'],
|
||
'scene' => ['ico' => '⚡', 'label' => '场景联动', 'blurb' => '规则引擎、设备分组与批量控制'],
|
||
'ota' => ['ico' => '🚀', 'label' => '固件升级', 'blurb' => '升级包管理与分批推送进度跟踪'],
|
||
'_root' => ['ico' => '🔑', 'label' => '设备入云激活', 'blurb' => '出厂预置令牌换领一机一密凭证'],
|
||
];
|
||
$groups = [];
|
||
$seen = []; // "controller/action" => true:兼容别名去重
|
||
foreach ($ms as $m) {
|
||
[, , $rule, $ctrl, $action] = $m;
|
||
$key = $ctrl . '/' . $action;
|
||
if (isset($seen[$key])) {
|
||
continue;
|
||
}
|
||
$seen[$key] = true;
|
||
$domain = strpos($rule, '/') === false ? '_root' : strtok($rule, '/');
|
||
if (!isset($domains[$domain])) {
|
||
continue; // 未知前缀不入目录
|
||
}
|
||
$groups[$domain] = $groups[$domain] ?? ($domains[$domain] + ['count' => 0]);
|
||
$groups[$domain]['count']++;
|
||
}
|
||
return array_values($groups);
|
||
}
|
||
}
|