thinkphp 6 中按天统计请求量需用 date(create_time) 分组并加 limit(30) 防卡顿,推荐中间件异步记录日志,导出报表用流式 fputcsv 避免内存溢出,统一时区(建议全存 utc)防止统计偏差。

ThinkPHP 6 中用 Db 查询请求量按天分组统计
直接查日志表或访问记录表,按日期聚合是最常见需求。ThinkPHP 6 的 Db 支持原生函数,DATE_FORMAT 或 DATE 都能用,但要注意 MySQL 版本兼容性:DATE 更稳妥,5.7+ 都支持;DATE_FORMAT 在严格模式下可能报错。
假设你有个 request_log 表,含 create_time(datetime 类型)字段:
Db::name('request_log')
->field('DATE(create_time) as date, count(*) as total')
->group('DATE(create_time)')
->order('date', 'desc')
->limit(30)
->select();
- 必须用
DATE(create_time)而不是create_time做group,否则会按秒级分组 -
field中别名用as date,后续 PHP 处理时可直接取$row['date'] -
limit(30)很关键——不加的话,上万条日志一查就卡死,前端渲染也容易崩
ThinkPHP 6 日志中间件自动记录请求并支持统计
手动写 INSERT 容易漏、难维护。推荐用中间件 + Log::write 或自定义表写入。但注意:不要在中间件里直接调 Db::insert(),尤其高并发时可能引发连接池耗尽。
更稳的做法是异步写入或队列延迟处理。如果只是内部小系统,可简化:
// app/middleware/RequestLogger.php
public function handle($request, \Closure $next)
{
$startTime = microtime(true);
$response = $next($request);
$cost = round((microtime(true) - $startTime) * 1000);
<pre class="brush:php;toolbar:false;">// 只记录非静态资源、且状态码为 2xx 的请求
if (!in_array($request->ext(), ['js', 'css', 'png', 'jpg', 'ico'])
&& $response->getCode() >= 200 && $response->getCode() insert([
'url' => $request->url(true),
'method' => $request->method(),
'status_code' => $response->getCode(),
'cost_time' => $cost,
'create_time' => date('Y-m-d H:i:s'),
]);
}
return $response;}
-
$request->ext()判断后缀,避免把 favicon.ico、logo.png 算进 PV - 务必检查
$response->getCode(),否则 404、500 请求也会被计入“成功请求” - 生产环境建议关掉该中间件,或改用 Swoole task 投递到队列
导出 Excel 报表时内存溢出怎么绕过
用 PhpSpreadsheet 直接 foreach 30 天的统计数据通常没问题,但若某天有上万次请求,再拼成大表格就容易 OOM。ThinkPHP 自带的 excel 导出扩展(如 topthink/think-excel)底层仍是 PhpSpreadsheet,没本质区别。
真正有效的解法是流式导出——边查边写,不全量加载:
$fp = fopen('php://output', 'w');
fputcsv($fp, ['日期', '请求数', '平均响应时间(ms)']);
<p>$data = Db::name('request_log')
->field('DATE(create_time) as date, count(*) as total, avg(cost_time) as avg_cost')
->group('DATE(create_time)')
->order('date', 'desc')
->limit(30)
->select();</p><p>foreach ($data as $row) {
fputcsv($fp, [
$row['date'],
$row['total'],
round($row['avg_cost'], 2),
]);
}
fclose($fp);</p>
- 用
fopen('php://output', 'w')绕过内存缓冲,适合中小数据量报表 - 别用
ob_start()+header手动设 Content-Type,ThinkPHP 的Response类会干扰输出流 - 如果真要导出超大数据(比如半年),得换
spout库,它原生支持无内存导出
统计口径不一致导致前后端数字对不上
最常踩的坑:前端看到的“今日请求数”是 UTC 时间 00:00–23:59,而后端数据库存的是本地时间(如东八区),DATE(create_time) 按服务器时区解析,结果差 8 小时。
- 统一用
CONVERT_TZ强制转时区:DATE(CONVERT_TZ(create_time, '+00:00', '+08:00')) - 或者更简单:所有日志写入时,
create_time全部存 UTC 时间(date('Y-m-d H:i:s', time())改成date('Y-m-d H:i:s', time() - 8 * 3600)) - 检查
php.ini的date.timezone和 MySQL 的time_zone是否一致,不一致必出错
时区问题不会报错,只会让数据每天偏移,等发现时往往已积压多日,修复成本远高于初期对齐。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











