Files
ulthon_admin/extend/base/tools/controller/timer/NginxLogCleanBase.php
augushong 0f9f12174c test(nginx-log): 权限节点扫描 + 端到端 QA 验证 + curd 路径修复
- 跑 admin:permission:nodes 扫描 138 个权限节点(17 个 nginx 相关节点)

- 17 个 nginx 节点 assign 给 role-id=1(写入 ul_system_auth_node 满足 plan 验收)

- curd 路径修复:菜单 href 由 system.nginx_access_log/index 改为 nginx.access_log/index(匹配 curd 生成的实际 URL,方案 B)

  - SystemMenu.php / v2.4.0.php / DB ul_system_menu 三处同步(含 id=303 log_position)

  - dashboard id=301 system.nginx_log_stat/index 保持不变(控制器确实在 system 模块)

- 端到端 QA 全通过:100 行日志 → import 74 行(静态过滤)→ aggregate stat_hour=1/url=6 → dashboard 数据完整

- 修复 T10 遗留 bug:cleanStatTables 用 Unix 时间戳当阈值,与 stat_date YYYYMMDD 不匹配导致误删

  - threshold 从 strtotime('today')-days*86400 改为 (int) date('Ymd', strtotime('today')-days*86400)

  - 验证:366 天前 stat 数据被删,今天 stat 数据保留

- 权限隔离:超管可访问,未登录被拦(code=40101 请先登录后台)

- Evidence: .omo/evidence/task-16-nginx-log-analytics-e2e/(15 个文件)
2026-07-28 06:47:24 +08:00

173 lines
5.7 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
declare(strict_types=1);
namespace base\tools\controller\timer;
use app\common\controller\TimerController;
use think\facade\Db;
use think\facade\Log;
/**
* nginx 日志清理定时任务Base 层).
*
* 清理范围:
* 1. raw 表 ul_nginx_access_log按 time_local 早于 N 天前分批删除(每批 5000 行)
* 2. 4 张 stat 表 ul_nginx_stat_{hour,url,referer,ua}:按 stat_date 早于 N 天前删除
*
* 保留:
* - position 表 ul_nginx_log_position 永久不删(增量位置基准,删了会全量重读)
*
* 控制参数sysconfig可后台界面动态调整0 = 永久跳过):
* - nginx_log.raw_retention_days 默认 7 raw 表保留天数
* - nginx_log.stat_retention_days 默认 365 stat 表保留天数
*
* 业务侧如需定制清理逻辑,重写 app/tools/controller/timer/NginxLogClean.php 对应方法即可拦截。
*/
class NginxLogCleanBase extends TimerController
{
/**
* 防刷间隔(秒).
* 仅做控制器侧防刷,不影响定时器侧 frequency 节流。
*/
protected $frequency = 600;
/**
* 并发上限 cap单分片id=0
*/
protected $concurrency = 1;
/**
* raw 表分批删除的批量大小(避免单条 DELETE 锁表太久)。
*/
protected const RAW_BATCH_SIZE = 5000;
/**
* 每批 DELETE 之间的微秒休眠(避免主从延迟、降低 DB 压力)。
*/
protected const RAW_BATCH_USLEEP = 100000;
/**
* 需要清理的 stat 表列表(不含表前缀,由 Db::name 自动补)。
* 各表独立按 stat_date 清理,不做级联。
*/
protected const STAT_TABLES = [
'nginx_stat_hour',
'nginx_stat_url',
'nginx_stat_referer',
'nginx_stat_ua',
];
public function do()
{
// 1. env 总开关校验(严格布尔判断,详见 notepad Task 2 #14
if (env('nginx_log.enable') !== true) {
Log::debug('nginx_log disabled, skip clean');
return 'disabled';
}
// 2. 读 sysconfigintval 强转DB 值是 varchar默认值是 int
$rawDays = (int) sysconfig('nginx_log', 'raw_retention_days', 7);
$statDays = (int) sysconfig('nginx_log', 'stat_retention_days', 365);
$rawDeleted = 0;
$statDeleted = 0;
// 3. raw 表分批清理rawDays > 0 才执行0 = 永久跳过)
if ($rawDays > 0) {
$rawDeleted = $this->cleanRawTable($rawDays);
}
// 4. stat 表清理statDays > 0 才执行0 = 永久跳过)
if ($statDays > 0) {
$statDeleted = $this->cleanStatTables($statDays);
}
return json_encode([
'raw_deleted' => $rawDeleted,
'stat_deleted' => $statDeleted,
], JSON_UNESCAPED_UNICODE);
}
/**
* 分批清理 raw 表 ul_nginx_access_log.
*
* 时间阈值基于 time_localnginx 记录的请求时间戳),不是 create_time入库时间
* 每批 5000 行 + usleep 100ms避免长事务锁表。
*
* @param int $rawDays 保留天数(>0
* @return int 累计删除行数
*/
protected function cleanRawTable(int $rawDays): int
{
$threshold = time() - ($rawDays * 86400);
$total = 0;
Log::info("nginx_log_clean: raw 阈值 time_local<" . date('Y-m-d H:i:s', $threshold) . " ({$rawDays} 天)");
// 循环分批 DELETE每批 limit 5000直到该阈值区间内无残留
while (true) {
// Db::name 自动加表前缀limit 保证单次 DELETE 行数可控
$deleted = Db::name('nginx_access_log')
->where('time_local', '<', $threshold)
->limit(self::RAW_BATCH_SIZE)
->delete();
if ($deleted <= 0) {
break;
}
$total += $deleted;
// 批次间休眠,降低 DB 压力(避免主从延迟)
if ($deleted === self::RAW_BATCH_SIZE) {
usleep(self::RAW_BATCH_USLEEP);
} else {
// 末批(< 5000说明已清完该阈值区间
break;
}
}
Log::info("nginx_log_clean: raw 删除 {$total}");
return $total;
}
/**
* 清理 4 张 stat 表(按 stat_date.
*
* stat_date 是 YYYYMMDD 整数(如 20260728不是 Unix 时间戳。
* - stat 表一行 = 一天的聚合stat_hour 按小时拆,但 stat_date 仍是当天 YYYYMMDD
* - 阈值用 (int) date('Ymd', strtotime('today') - statDays*86400)(同为 YYYYMMDD 格式)
* 保证整天边界,不会误删"今天聚合了 N 小时但今天整体还没过期"的数据
*
* 各表独立 DELETE不做级联如 stat_url 不依赖 stat_hour可单独保留
*
* @param int $statDays 保留天数(>0
* @return int 累计删除行数4 张表合计)
*/
protected function cleanStatTables(int $statDays): int
{
// strtotime('today') 返回当天 0 点时间戳;转 YYYYMMDD 整数与 stat_date 对齐
$threshold = (int) date('Ymd', strtotime('today') - ($statDays * 86400));
$total = 0;
Log::info("nginx_log_clean: stat 阈值 stat_date<{$threshold} ({$statDays} 天)");
foreach (self::STAT_TABLES as $table) {
$deleted = Db::name($table)
->where('stat_date', '<', $threshold)
->delete();
$total += $deleted;
if ($deleted > 0) {
Log::info("nginx_log_clean: {$table} 删除 {$deleted}");
}
}
Log::info("nginx_log_clean: stat 合计删除 {$total}");
return $total;
}
}