Files
YwxAppThink/app/backend/controller/Console.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

336 lines
11 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-04-29 02:32:33
* @LastEditors: YwxApp <ywx@ywxapp.cn>
* @LastEditTime: 2026-07-21 22:29:51
* @Description:
* @FilePath: \ywxapp_dev\app\backend\controller\Console.php
* @CustomString: Copyright (c) 2026 YwxApp
*/
declare (strict_types = 1);
namespace app\backend\controller;
use app\backend\library\SystemInfo;
use think\Request;
use think\facade\View;
use think\facade\Db;
use ywxapp\controller\BackendBase;
/**
* Console 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Console extends BackendBase
{
/**
* Summary of needLogin
* @var array
*/
protected $noNeedLogin = [];
/**
* Summary of needRight
* @var array
*/
protected $noNeedVerify = [];
/**
* 控制器初始化 initialize
* @return void
*/
protected function initialize()
{}
/**
* 显示资源列表
*
* @param \think\Request $request
* @return \think\Response
*/
public function index()
{
// 控制台首页带一份系统概览(与「系统信息」页共用 SystemInfo,避免两套实现)
if ($this->request->isAjax()) {
$this->result->success(SystemInfo::collect());
}
View::assign('title', '控制台');
View::assign('sys', SystemInfo::collect());
// 控制台内的「框架升级」卡片用(与 Upgrade 页同源:config('ywxapp.version'))
View::assign('version', (string) config('ywxapp.version'));
// 首页下半区:真实统计(替换 layuiAdmin 模板自带的演示数据)
View::assign('stats', $this->collectStats());
return View::fetch();
}
/**
* 清理缓存:cache / temp / all(不动 log)
*
* 由原 System::clean() 迁来(/admin/system/clean → /admin/console/clean):
* 「系统信息」页已随 System 控制器一起下线,但首页顶部的三个清理按钮要留着,
* 所以能力搬来这里;清理实现仍在 SystemInfo::cleanRuntime(),权限节点同步改成 console:clean。
*
* @return void
*/
public function clean()
{
if (! $this->request->isPost()) {
$this->result->error('请求方式错误', 405);
}
$type = trim((string) $this->request->param('type', 'cache'));
try {
$files = SystemInfo::cleanRuntime($type);
} catch (\Throwable $e) {
$this->result->error($e->getMessage());
}
$this->result->success(['files' => $files], '已清理 ' . $files . ' 项缓存');
}
/* ====================== 首页统计(真实数据,替换演示数据) ====================== */
/**
* 采集首页下半区要用的统计
*
* 数据源只有三张核心表:会员 member_user / 后台日志 backend_log / 蜘蛛统计 common_spider_stat。
* 任意一张缺失或查询失败都退化为 0——首页是最容易被人访问的页面,
* 不能因为某张插件表没建就整体 500。
*
* @return array
*/
protected function collectStats(): array
{
$todayStart = strtotime('today');
$todayEnd = $todayStart + 86399;
$weekStart = strtotime('-6 days', $todayStart);
// 近 7 天先铺好 0 值骨架:某天没有数据也不会把曲线挤错位
$bucket = [];
$dates = [];
for ($i = 6; $i >= 0; $i--) {
$ts = strtotime("-{$i} days", $todayStart);
$key = date('Y-m-d', $ts);
$bucket[$key] = ['member' => 0, 'log' => 0];
$dates[] = date('m-d', $ts);
}
// 近 7 天:一次取回时间戳列,在 PHP 侧按天归桶(省掉 14 次 COUNT)
foreach ($this->safeColumn('member_user', 'create_at', $weekStart, $todayEnd) as $ts) {
$key = date('Y-m-d', (int) $ts);
if (isset($bucket[$key])) {
$bucket[$key]['member']++;
}
}
foreach ($this->safeColumn('backend_log', 'create_at', $weekStart, $todayEnd) as $ts) {
$key = date('Y-m-d', (int) $ts);
if (isset($bucket[$key])) {
$bucket[$key]['log']++;
}
}
$memberWeek = 0;
foreach ($bucket as $row) {
$memberWeek += $row['member'];
}
return [
'dates' => $dates,
'series' => [
'member' => array_column($bucket, 'member'),
'log' => array_column($bucket, 'log'),
],
'today' => [
'member' => SystemInfo::safeCount('member_user', function ($q) use ($todayStart, $todayEnd) {
$q->where('create_at', '>=', $todayStart)->where('create_at', '<=', $todayEnd);
}),
'log' => SystemInfo::safeCount('backend_log', function ($q) use ($todayStart, $todayEnd) {
$q->where('create_at', '>=', $todayStart)->where('create_at', '<=', $todayEnd);
}),
// 蜘蛛按日聚合表:今日那行的 count 就是当天抓取量
'spider' => $this->safeSum('common_spider_stat', 'count', function ($q) use ($todayStart) {
$q->where('stat_date', date('Y-m-d', $todayStart));
}),
],
'total' => [
'member' => SystemInfo::safeCount('member_user'),
'member_week' => $memberWeek,
'log' => SystemInfo::safeCount('backend_log'),
'spider' => $this->safeSum('common_spider_stat', 'count'),
],
'members' => $this->latestMembers(),
'logs' => $this->latestLogs(),
];
}
/**
* 最新注册的会员
*
* @return array
*/
protected function latestMembers(): array
{
try {
return Db::name('member_user')
->field('uid, account, nickname, create_at, update_at')
->order('uid', 'desc')
->limit(8)
->select()
->toArray();
} catch (\Throwable $e) {
return [];
}
}
/**
* 最近的后台操作日志(附带管理员昵称)
*
* @return array
*/
protected function latestLogs(): array
{
$admins = [];
try {
$admins = Db::name('backend_admin')->column('nickname', 'id');
} catch (\Throwable $e) {
$admins = [];
}
try {
$rows = Db::name('backend_log')
->field('id, aid, action, ip, create_at')
->order('id', 'desc')
->limit(8)
->select()
->toArray();
} catch (\Throwable $e) {
return [];
}
$list = [];
foreach ($rows as $row) {
$list[] = [
'id' => (int) $row['id'],
'admin' => $admins[$row['aid']] ?? ('#' . $row['aid']),
'action' => (string) $row['action'],
'ip' => (string) $row['ip'],
'time' => (int) $row['create_at'],
];
}
return $list;
}
/**
* 安全取单列:表缺失 / 查询异常时返回空数组
*
* @param string $table
* @param string $field
* @param int $since
* @param int|null $until
* @return array
*/
protected function safeColumn(string $table, string $field, int $since, int $until = null): array
{
try {
$q = Db::name($table)->where($field, '>=', $since);
if ($until !== null) {
$q->where($field, '<=', $until);
}
return $q->column($field);
} catch (\Throwable $e) {
return [];
}
}
/**
* 安全求和:表缺失 / 查询异常时返回 0
*
* @param string $table
* @param string $field
* @param callable|null $where
* @return int
*/
protected function safeSum(string $table, string $field, callable $where = null): int
{
try {
$q = Db::name($table);
if ($where) {
$where($q);
}
return (int) $q->sum($field);
} catch (\Throwable $e) {
return 0;
}
}
/**
* 热门统计(真实数据)
*
* @return \think\Response
*/
public function hotsearch()
{
$admin = Db::name('backend_admin')->count();
$user = Db::name('member_user')->count();
$article = Db::name('articles_article')->count();
$links = Db::name('common_links')->count();
$addon = Db::name('common_addon')->count();
$data = [
['keywords' => '管理员', 'frequency' => $admin, 'userNums' => $admin],
['keywords' => '会员', 'frequency' => $user, 'userNums' => $user],
['keywords' => '文章', 'frequency' => $article, 'userNums' => $article],
['keywords' => '友链', 'frequency' => $links, 'userNums' => $links],
['keywords' => '插件', 'frequency' => $addon, 'userNums' => $addon],
];
$this->result->success($data);
}
/**
* 热门内容(真实数据:最新文章)
*
* @return \think\Response
*/
public function hotTopic()
{
$list = Db::name('articles_article')
->field('id, title, author, cid, create_at')
->order('id', 'desc')
->limit(10)
->select()
->toArray();
$data = array_map(function ($item) {
return [
'id' => $item['id'],
'title' => $item['title'],
'username' => $item['author'] ?? '',
'channel' => $item['cid'] ?? '',
'href' => '',
'crt' => $item['create_at'] ?? 0,
];
}, $list);
$this->result->success($data);
}
/**
* 系统进度 / 环境信息(真实数据)
*
* @return \think\Response
*/
public function prograss()
{
$data = [
['prograss' => 'PHP 版本', 'time' => PHP_VERSION, 'complete' => '已完成'],
['prograss' => 'ThinkPHP', 'time' => \think\facade\App::version(), 'complete' => '已完成'],
['prograss' => '安装状态', 'time' => is_file(root_path() . 'install.lock') ? '已安装' : '未安装', 'complete' => '已完成'],
['prograss' => '运行环境', 'time' => php_sapi_name(), 'complete' => '进行中'],
];
$this->result->success($data);
}
}