Files
YwxAppThink/addon/iot/controller/Index.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

136 lines
5.8 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
// +----------------------------------------------------------------------
// | 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);
}
}