Files
ulthon_admin/extend/base/common/service/NginxLogAggregatorServiceBase.php
augushong f1a2e8fd5d fix(nginx-log): service 文件加 Service 后缀(命名规范合规)
F2 Code quality review 发现:3 个新增 service 未遵守
.agents/rules/ulthon-naming-convention.md 第 19 行规定
(service 模块文件名需带 Service 后缀)。

变更:
- 重命名 6 个文件(3 Base + 3 App):
  NginxLogParser(Base)→NginxLogParserService(Base)
  NginxLogReader(Base)→NginxLogReaderService(Base)
  NginxLogAggregator(Base)→NginxLogAggregatorService(Base)
- 同步类名、use、类型 hint、new 引用
- NginxLogReaderServiceBase 异常消息前缀同步加 Service
- controller Base 引用更新(NginxLogImportBase / NginxLogStatAggregateBase)

验证:
- php -l 全部 8 个文件语法通过
- grep 残留旧名 = 0 结果(24 处引用全部带 Service 后缀)
2026-07-28 06:57:13 +08:00

230 lines
10 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
namespace base\common\service;
use app\admin\model\NginxAccessLog;
use app\admin\model\NginxStatHour;
use app\admin\model\NginxStatReferer;
use app\admin\model\NginxStatUa;
use app\admin\model\NginxStatUrl;
use think\facade\Db;
/**
* Nginx 访问日志聚合统计 ServiceBase 内核).
*
* 设计要点:
* - 幂等delete-then-insert 模式(不用 INSERT ON DUPLICATE KEY UPDATE避免翻倍
* - 事务Db::startTrans / commit / rollback 包裹所有 DELETE + INSERT
* - top N 截断后不保留长尾(明确接受,不在 sysconfig 控制"是否截断"
* - 依赖倒置:内部 use app 入口 modelNginxStatHour 等),不 use Base model
* - 只读 raw 表ul_nginx_access_log不做 nginx log line 解析(那是 T5 Parser 的事)
*/
class NginxLogAggregatorServiceBase
{
/**
* 聚合指定日期的指定小时.
*
* 流程:
* 1. stat_hourDELETE WHERE stat_date=:d AND stat_hour=:h → INSERT 1 行(该小时聚合)
* 2. stat_url / stat_referer / stat_uaDELETE WHERE stat_date=:d整天重跑→ INSERT top N 行
*
* @param int $statDate 日期 YYYYMMDD如 20260728
* @param int $statHour 小时 0-23
* @return array {stat_hour_rows:int, stat_url_rows:int, stat_referer_rows:int, stat_ua_rows:int}
*
* @throws \InvalidArgumentException 日期/小时非法
* @throws \Throwable 事务内任何错误(已 rollback 后 rethrow
*/
public function aggregateHour(int $statDate, int $statHour): array
{
if ($statHour < 0 || $statHour > 23) {
throw new \InvalidArgumentException("stat_hour must be 0-23, got: {$statHour}");
}
// 时间窗口
$dateStr = $this->formatDateStr($statDate);
$dayStart = strtotime("{$dateStr} 00:00:00");
if ($dayStart === false) {
throw new \InvalidArgumentException("invalid stat_date: {$statDate}");
}
$dayEnd = $dayStart + 86400;
$hourStart = $dayStart + $statHour * 3600;
$hourEnd = $hourStart + 3600;
// topN 配置topn=0 时不限制)
$topn = (int) sysconfig('nginx_log', 'topn_default', 10);
// 表名(带前缀)。框架默认连接是 main不是 mysql按 default 取连接 prefix
$conn = (string) config('database.default', 'main');
$prefix = (string) config("database.connections.{$conn}.prefix", 'ul_');
$rawTable = $prefix . 'nginx_access_log';
$hourTable = $prefix . 'nginx_stat_hour';
$urlTable = $prefix . 'nginx_stat_url';
$refererTable = $prefix . 'nginx_stat_referer';
$uaTable = $prefix . 'nginx_stat_ua';
Db::startTrans();
try {
// 1. stat_hourDELETE + INSERT 1 行
NginxStatHour::where('stat_date', $statDate)
->where('stat_hour', $statHour)
->delete();
$hourSql = "INSERT INTO `{$hourTable}`
(stat_date, stat_hour, pv, uv, total_bytes, status_2xx, status_3xx, status_4xx, status_5xx, status_other, avg_request_time)
SELECT :stat_date, :stat_hour,
COUNT(*) AS pv,
COUNT(DISTINCT remote_addr) AS uv,
COALESCE(SUM(body_bytes_sent), 0) AS total_bytes,
COALESCE(SUM(CASE WHEN status BETWEEN 200 AND 299 THEN 1 ELSE 0 END), 0) AS status_2xx,
COALESCE(SUM(CASE WHEN status BETWEEN 300 AND 399 THEN 1 ELSE 0 END), 0) AS status_3xx,
COALESCE(SUM(CASE WHEN status BETWEEN 400 AND 499 THEN 1 ELSE 0 END), 0) AS status_4xx,
COALESCE(SUM(CASE WHEN status BETWEEN 500 AND 599 THEN 1 ELSE 0 END), 0) AS status_5xx,
COALESCE(SUM(CASE WHEN status < 200 OR status > 599 THEN 1 ELSE 0 END), 0) AS status_other,
COALESCE(AVG(request_time), 0) AS avg_request_time
FROM `{$rawTable}`
WHERE time_local >= :hour_start AND time_local < :hour_end
HAVING COUNT(*) > 0";
$statHourRows = (int) Db::execute($hourSql, [
'stat_date' => $statDate,
'stat_hour' => $statHour,
'hour_start' => $hourStart,
'hour_end' => $hourEnd,
]);
// 2. stat_urlDELETE整天+ INSERT topN
NginxStatUrl::where('stat_date', $statDate)->delete();
$urlLimit = $topn > 0 ? 'LIMIT ' . $topn : '';
$urlSql = "INSERT INTO `{$urlTable}`
(stat_date, uri, pv, uv, total_bytes, avg_request_time)
SELECT :stat_date, uri,
COUNT(*) AS pv,
COUNT(DISTINCT remote_addr) AS uv,
COALESCE(SUM(body_bytes_sent), 0) AS total_bytes,
COALESCE(AVG(request_time), 0) AS avg_request_time
FROM `{$rawTable}`
WHERE time_local >= :day_start AND time_local < :day_end
GROUP BY uri
ORDER BY pv DESC
{$urlLimit}";
$statUrlRows = (int) Db::execute($urlSql, [
'stat_date' => $statDate,
'day_start' => $dayStart,
'day_end' => $dayEnd,
]);
// 3. stat_refererDELETE + INSERT topN从 http_referer 提取 domain
NginxStatReferer::where('stat_date', $statDate)->delete();
$refererLimit = $topn > 0 ? 'LIMIT ' . $topn : '';
$refererSql = "INSERT INTO `{$refererTable}`
(stat_date, referer_domain, pv, uv)
SELECT :stat_date,
CASE
WHEN http_referer IS NULL OR http_referer = '' OR http_referer = '-' THEN '-'
ELSE SUBSTRING_INDEX(
SUBSTRING_INDEX(
REPLACE(REPLACE(http_referer, 'https://', ''), 'http://', ''),
'/', 1
),
'?', 1
)
END AS referer_domain,
COUNT(*) AS pv,
COUNT(DISTINCT remote_addr) AS uv
FROM `{$rawTable}`
WHERE time_local >= :day_start AND time_local < :day_end
GROUP BY referer_domain
ORDER BY pv DESC
{$refererLimit}";
$statRefererRows = (int) Db::execute($refererSql, [
'stat_date' => $statDate,
'day_start' => $dayStart,
'day_end' => $dayEnd,
]);
// 4. stat_uaDELETE + INSERT topN从 http_user_agent 分类)
NginxStatUa::where('stat_date', $statDate)->delete();
$uaLimit = $topn > 0 ? 'LIMIT ' . $topn : '';
$uaSql = "INSERT INTO `{$uaTable}`
(stat_date, ua_type, ua_name, pv, uv)
SELECT :stat_date,
CASE
WHEN http_user_agent LIKE '%bot%'
OR http_user_agent LIKE '%spider%'
OR http_user_agent LIKE '%crawl%'
OR http_user_agent LIKE '%slurp%'
OR http_user_agent LIKE '%bingpreview%'
OR http_user_agent LIKE '%facebookexternalhit%'
OR http_user_agent LIKE '%twitterbot%' THEN 'spider'
WHEN http_user_agent IS NULL OR http_user_agent = '' OR http_user_agent = '-' THEN 'unknown'
WHEN http_user_agent LIKE '%Mozilla%'
OR http_user_agent LIKE '%Chrome%'
OR http_user_agent LIKE '%Safari%'
OR http_user_agent LIKE '%Firefox%'
OR http_user_agent LIKE '%Edg%'
OR http_user_agent LIKE '%Opera%'
OR http_user_agent LIKE '%MSIE%'
OR http_user_agent LIKE '%Trident%' THEN 'browser'
ELSE 'unknown'
END AS ua_type,
CASE
WHEN http_user_agent LIKE '%Googlebot%' THEN 'Googlebot'
WHEN http_user_agent LIKE '%Baiduspider%' THEN 'Baiduspider'
WHEN http_user_agent LIKE '%bingbot%' THEN 'Bingbot'
WHEN http_user_agent LIKE '%DuckDuckBot%' THEN 'DuckDuckBot'
WHEN http_user_agent LIKE '%YandexBot%' THEN 'YandexBot'
WHEN http_user_agent LIKE '%Edg/%' THEN 'Microsoft Edge'
WHEN http_user_agent LIKE '%OPR/%' OR http_user_agent LIKE '%Opera%' THEN 'Opera'
WHEN http_user_agent LIKE '%Firefox/%' THEN 'Firefox'
WHEN http_user_agent LIKE '%Chrome/%' THEN 'Chrome'
WHEN http_user_agent LIKE '%Safari/%' THEN 'Safari'
WHEN http_user_agent LIKE '%MSIE%' OR http_user_agent LIKE '%Trident%' THEN 'Internet Explorer'
WHEN http_user_agent IS NULL OR http_user_agent = '' OR http_user_agent = '-' THEN 'Unknown'
ELSE 'Other'
END AS ua_name,
COUNT(*) AS pv,
COUNT(DISTINCT remote_addr) AS uv
FROM `{$rawTable}`
WHERE time_local >= :day_start AND time_local < :day_end
GROUP BY ua_type, ua_name
ORDER BY pv DESC
{$uaLimit}";
$statUaRows = (int) Db::execute($uaSql, [
'stat_date' => $statDate,
'day_start' => $dayStart,
'day_end' => $dayEnd,
]);
Db::commit();
return [
'stat_hour_rows' => $statHourRows,
'stat_url_rows' => $statUrlRows,
'stat_referer_rows' => $statRefererRows,
'stat_ua_rows' => $statUaRows,
];
} catch (\Throwable $e) {
Db::rollback();
throw $e;
}
}
/**
* 将 int 日期 YYYYMMDD 格式化为 "YYYY-MM-DD".
*/
protected function formatDateStr(int $statDate): string
{
$y = intdiv($statDate, 10000);
$m = intdiv($statDate % 10000, 100);
$d = $statDate % 100;
return sprintf('%04d-%02d-%02d', $y, $m, $d);
}
}