Files

117 lines
4.6 KiB
PHP
Raw Permalink 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 ywxapp\middleware;
use think\facade\Db;
use ywxapp\model\BaseModel;
use ywxapp\library\SpiderDetect;
/**
* 全站搜索蜘蛛统计中间件(全局注册于 app/middleware.php
*
* 设计要点:
* - 仅命中蜘蛛 UA 才落库,普通用户请求零额外查询;
* - 落库在响应生成之后($next 之后),不阻塞蜘蛛抓取响应;
* - 明细写 spider_log,按日聚合 upsert 到 spider_stat(报表免全表扫描);
* - 任何数据库异常静默吞掉(统计绝不能影响业务),缺表时经 BaseModel 自愈引擎 自愈一次;
* - install.sql 为唯一事实源,此处 DDL 仅为老库运行时兜底。
*
* @author ywxapp <admin@ywxapp.cn>
*/
class SpiderStat
{
public function handle($request, \Closure $next)
{
$response = $next($request);
try {
$ua = (string) $request->header('user-agent', '');
$spider = SpiderDetect::detect($ua);
if ($spider !== null) {
$this->record($request, $response, $spider, $ua);
}
} catch (\Throwable $e) {
// 统计失败绝不影响正常响应
}
return $response;
}
/**
* 落库:明细 + 按日聚合
*/
protected function record($request, $response, string $spider, string $ua): void
{
$prefix = config('database.connections.mysql.prefix', 'wxapp_');
$logTable = $prefix . 'spider_log';
$statTable = $prefix . 'spider_stat';
$data = [
'spider' => $spider,
'url' => mb_substr((string) $request->url(), 0, 500),
'ip' => mb_substr((string) $request->ip(), 0, 45),
'app' => mb_substr((string) (app('http')->getName() ?: ''), 0, 20),
'user_agent' => mb_substr($ua, 0, 500),
'http_code' => method_exists($response, 'getCode') ? (int) $response->getCode() : 200,
'create_at' => time(),
];
try {
$this->insert($logTable, $statTable, $data, $spider);
} catch (\Throwable $e) {
// 表可能不存在(老库未升级):自愈一次后重试
$this->ensureTables($logTable, $statTable);
$this->insert($logTable, $statTable, $data, $spider);
}
}
/**
* 写明细 + 聚合 upsert
*/
protected function insert(string $logTable, string $statTable, array $data, string $spider): void
{
Db::table($logTable)->insert($data);
// 按日聚合:主键(stat_date, spider),存在则计数+1
Db::execute(
"INSERT INTO `{$statTable}` (`stat_date`, `spider`, `count`) VALUES (?, ?, 1) "
. "ON DUPLICATE KEY UPDATE `count` = `count` + 1",
[date('Y-m-d'), $spider]
);
}
/**
* 缺表自愈(DDL 与 public/install/install.sql 保持一致)
*/
protected function ensureTables(string $logTable, string $statTable): void
{
BaseModel::ensureTable($logTable, "CREATE TABLE IF NOT EXISTS `{$logTable}` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`spider` varchar(20) NOT NULL DEFAULT '' COMMENT '蜘蛛标识',
`url` varchar(500) NOT NULL DEFAULT '' COMMENT '抓取URL',
`ip` varchar(45) NOT NULL DEFAULT '' COMMENT '来源IP',
`app` varchar(20) NOT NULL DEFAULT '' COMMENT '应用名',
`user_agent` varchar(500) NOT NULL DEFAULT '' COMMENT 'User-Agent',
`http_code` smallint unsigned NOT NULL DEFAULT '200' COMMENT '响应状态码',
`create_at` int NOT NULL DEFAULT '0' COMMENT '抓取时间',
PRIMARY KEY (`id`),
KEY `idx_spider_time` (`spider`,`create_at`),
KEY `idx_create_at` (`create_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='搜索蜘蛛抓取日志'");
BaseModel::ensureTable($statTable, "CREATE TABLE IF NOT EXISTS `{$statTable}` (
`stat_date` date NOT NULL COMMENT '统计日期',
`spider` varchar(20) NOT NULL DEFAULT '' COMMENT '蜘蛛标识',
`count` int unsigned NOT NULL DEFAULT '0' COMMENT '抓取次数',
PRIMARY KEY (`stat_date`,`spider`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='搜索蜘蛛按日统计'");
}
}