用 Db::table() 直接写入访问日志最稳妥,需在控制器方法首行手动插入 api_log 表,字段含 url、ip、method、create_time(INT 类型),高并发时应异步处理,统计 UV 需过滤局域网 IP 并去重。

用 Db::table() 直接写入访问日志最稳妥
ThinkPHP 6/7 默认不带请求统计中间件,硬加全局钩子容易漏掉 CLI 或异常中断的请求。直接在关键接口开头手动记录,可控性最强。
- 在控制器方法里第一行调用
Db::table('api_log')->insert(['url' => $this->request->url(), 'ip' => $this->request->ip(), 'create_time' => time()]) - 别用
Log::write()记日志——它只存文件,没法做聚合查询 - 表结构至少包含
id、url、ip、method、create_time,create_time必须是 INT 类型,方便后续按天/小时分组 - 如果并发高,避免在主业务流程里同步写库,可改用
queue:work异步推送日志任务
统计昨日 UV 需注意 ip 去重逻辑
单纯 COUNT(DISTINCT ip) 在数据量大时会慢,且没考虑代理或内网 IP 重复问题。真实场景下要过滤掉常见局域网段和空值。
- SQL 示例:
SELECT COUNT(DISTINCT ip) FROM api_log WHERE create_time >= 1735689600 AND create_time -
1735689600是昨日 00:00 的时间戳,别用DATE_SUB(NOW(), INTERVAL 1 DAY)——MySQL 函数在索引字段上无法走索引 - 给
create_time加普通索引,(create_time, ip)联合索引效果更好 - TP6 中用
Db::table('api_log')->where('create_time', '>=', $start)->where('create_time', 'whereNotLike('ip', '10.%')->count('DISTINCT ip')写法更安全
group by date(from_unixtime(create_time)) 是按天统计的核心
ThinkPHP 不支持在 group 中直接用函数表达式,必须用原生 SQL 或 raw 包裹,否则会报错“Unknown column”。
- 正确写法:
Db::table('api_log')->field('COUNT(*) as cnt, DATE(FROM_UNIXTIME(create_time)) as day')->group('day')->select()->toArray() - 错误写法:
->group('DATE(FROM_UNIXTIME(create_time))')—— TP 解析器会把它当字段名查,找不到就崩 - 如果要用小时粒度,把
DATE(...)换成DATE_FORMAT(FROM_UNIXTIME(create_time), "%Y-%m-%d %H") - 注意时区:确保 PHP 的
date_default_timezone_set()和 MySQL 的time_zone一致,否则日期对不上
高频接口用 Redis 缓存计数能扛住突发流量
每秒几十次写 DB 对 api_log 表压力不小,尤其在秒杀类接口。用 Redis 的 INCR + 定时落库是更现实的方案。
- 记录时:
Cache::store('redis')->inc('api:count:' . md5($request->url())) - 定时任务(每天凌晨)执行:
Cache::store('redis')->get('api:count:xxx', 0),再批量写进 MySQL 并清零 - Key 命名别用原始 URL,含参数的 URL 会导致爆炸式 Key 数量,建议统一截取到
PATH_INFO,比如/api/v1/user/info - Redis 缓存失效时间设为 24 小时,避免缓存雪崩;但不要依赖过期自动清理,务必配合定时脚本兜底
Redis 的原子计数和 MySQL 的最终一致性之间有延迟,如果你需要实时看到“当前分钟请求数”,就得接受最多 60 秒的误差。这个 trade-off 很难绕开。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











