安全(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(市场 / 开发者中心 / 审核 / 分润)
150 lines
5.4 KiB
PHP
150 lines
5.4 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 app\backend\controller;
|
||
|
||
use think\facade\Db;
|
||
use ywxapp\controller\BackendBase;
|
||
use ywxapp\library\AdminLogger;
|
||
|
||
/**
|
||
* 管理员操作日志
|
||
*
|
||
* 日志来源:
|
||
* - 后台所有写操作:BackendBase 的 create / edit / del / destroy / restore / multi 成功后自动留痕;
|
||
* - 登录与退出:BackendBase::login() / logout();
|
||
* - 业务代码可直接 AdminLogger::write('动作', '摘要') 补充关键操作。
|
||
*
|
||
* 权限节点(首次访问自动补录):adminlog:index / adminlog:delete / adminlog:clean
|
||
*
|
||
* @author ywxapp <admin@ywxapp.cn>
|
||
*/
|
||
class Adminlog extends BackendBase
|
||
{
|
||
use EnsuresPower;
|
||
|
||
protected $noNeedLogin = [];
|
||
protected $noNeedVerify = [];
|
||
|
||
/**
|
||
* 进程内只补录一次权限
|
||
* @var bool
|
||
*/
|
||
protected static $powerChecked = false;
|
||
|
||
protected function initialize()
|
||
{
|
||
if (!self::$powerChecked) {
|
||
self::$powerChecked = true;
|
||
$this->ensureMenus();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 列表
|
||
*/
|
||
public function index()
|
||
{
|
||
if ($this->request->isAjax()) {
|
||
$page = $this->request->param('page/d', 1);
|
||
$limit = $this->request->param('limit/d', 20);
|
||
$kw = trim((string) $this->request->param('keyword', ''));
|
||
$action = trim((string) $this->request->param('action', ''));
|
||
$start = trim((string) $this->request->param('start', ''));
|
||
$end = trim((string) $this->request->param('end', ''));
|
||
|
||
$query = Db::name('backend_log')->alias('l')
|
||
->leftJoin('backend_admin a', 'a.id = l.aid')
|
||
->field('l.*, a.account, a.nickname');
|
||
|
||
if ($kw !== '') {
|
||
// 注意:本 ORM 版本的 whereLike 对带表别名字段会抛
|
||
// 「where express error」,统一用 where(field, 'like', ...)
|
||
$query->where(function ($q) use ($kw) {
|
||
$q->where('l.remark', 'like', '%' . $kw . '%')
|
||
->whereOr('l.ip', 'like', '%' . $kw . '%')
|
||
->whereOr('a.account', 'like', '%' . $kw . '%')
|
||
->whereOr('a.nickname', 'like', '%' . $kw . '%');
|
||
});
|
||
}
|
||
if ($action !== '') {
|
||
$query->where('l.action', 'like', '%' . $action . '%');
|
||
}
|
||
if ($start !== '') {
|
||
$query->where('l.create_at', '>=', strtotime($start . ' 00:00:00'));
|
||
}
|
||
if ($end !== '') {
|
||
$query->where('l.create_at', '<=', strtotime($end . ' 23:59:59'));
|
||
}
|
||
|
||
$total = (int) $query->count();
|
||
$list = $query->order('l.id', 'desc')
|
||
->page($page, $limit)
|
||
->select()
|
||
->toArray();
|
||
|
||
foreach ($list as &$row) {
|
||
$row['create_at_text'] = $row['create_at'] ? date('Y-m-d H:i:s', (int) $row['create_at']) : '-';
|
||
$row['operator'] = $row['account'] ?: ($row['aid'] ? ('#' . $row['aid']) : '系统');
|
||
}
|
||
$this->result->setCount($total)->success($list);
|
||
}
|
||
|
||
// 动作类型下拉:取表里出现过的动作,便于筛选
|
||
$actions = Db::name('backend_log')
|
||
->field('action, COUNT(*) AS total')
|
||
->group('action')
|
||
->order('total', 'desc')
|
||
->limit(50)
|
||
->select()
|
||
->toArray();
|
||
$this->assign('actions', $actions);
|
||
|
||
return $this->fetch();
|
||
}
|
||
|
||
/**
|
||
* 删除(支持逗号分隔批量)
|
||
*/
|
||
public function delete()
|
||
{
|
||
if (! $this->request->isPost()) {
|
||
$this->result->error('请求方式错误', 405);
|
||
}
|
||
$ids = $this->request->param('ids', '');
|
||
$ids = is_array($ids) ? $ids : array_filter(explode(',', (string) $ids));
|
||
if (empty($ids)) {
|
||
$this->result->error('请选择要删除的日志');
|
||
}
|
||
$count = Db::name('backend_log')->whereIn('id', $ids)->delete();
|
||
AdminLogger::write('adminlog/delete', '删除日志 ' . $count . ' 条');
|
||
$this->result->success(['count' => $count], '已删除 ' . $count . ' 条');
|
||
}
|
||
|
||
/**
|
||
* 清理:按保留天数删除,days=0 表示清空
|
||
*/
|
||
public function clean()
|
||
{
|
||
if (! $this->request->isPost()) {
|
||
$this->result->error('请求方式错误', 405);
|
||
}
|
||
$days = $this->request->param('days/d', 30);
|
||
if ($days < 0 || $days > 3650) {
|
||
$this->result->error('保留天数应在 0 ~ 3650 之间(0=清空)');
|
||
}
|
||
$count = AdminLogger::clean($days);
|
||
AdminLogger::write('adminlog/clean', ($days > 0 ? ('清理 ' . $days . ' 天前日志') : '清空日志') . ',共 ' . $count . ' 条');
|
||
$this->result->success(['count' => $count], $days > 0
|
||
? ('已清理 ' . $count . ' 条(保留最近 ' . $days . ' 天)')
|
||
: ('已清空 ' . $count . ' 条'));
|
||
}
|
||
}
|