关键在于原子递增、标准化路由与IP防伪:必须用Cache::inc('api:rate:'.$ip.':'.$route, 1, 60)实现线程安全计数,$route需去除query/hash并转小写,$ip应取X-Real-IP头;返回前强制(int)转换count字段。

直接统计 AJAX 接口请求数据,关键不是“能不能”,而是「统计粒度是否一致」「缓存操作是否原子」「前端传参是否可控」——ThinkPHP 本身不自动记录 AJAX 请求频次,必须手动埋点,且默认的 throttle 中间件只拦截、不计数。
如何在中间件里安全递增请求计数
不能依赖 Cache::get + Cache::set 手动读改写,这在并发下会丢计数。必须用原子递增指令:
-
Cache::inc('api:rate:'.$ip.':'.$route, 1)是唯一可靠方式;Redis 的INCR天然原子,key 不存在时自动初始化为 1 - 不需要提前
Cache::set($key, 0, 60),反而可能覆盖已有值;TTL 统一设为 60 秒即可:Cache::set($key, 1, 60)不必要,inc自带过期控制(需配合缓存驱动支持) -
$route必须标准化:用str_replace(['?', '#'], '', $request->url(true))去掉 query 和 hash;再统一转小写,避免/api/user和/API/USER被算作两个接口 - IP 获取要防伪造:
$ip = $request->header('X-Real-IP', $request->ip()),别直接信$_SERVER['REMOTE_ADDR']
控制器里怎么取 AJAX 参数才不漏值
ThinkPHP 5/6 对 AJAX 请求没有特殊参数解析逻辑,所有数据仍走标准输入流,但容易踩坑:
- GET 请求参数:用
input('param_name')或$this->request->param('param_name'),注意它会自动过滤空格和 trim,如需原始值改用$this->request->get('param_name', null, 'htmlspecialchars') - POST 请求含 JSON:必须用
file_get_contents('php://input')读原始体,再json_decode($raw, true);input()无法解析 raw body - 表单提交(
application/x-www-form-urlencoded):input()正常可用,但注意 CSRF token 验证失败会导致整个请求被拦截,统计就断了 - 不要混用
$_POST和input(),后者已做过滤和类型转换,前者是裸数据,两者对同个字段可能返回不同结果
为什么前端轮询拿到的 count 总是字符串?
Redis 所有值都是 string 类型,Cache::get() 返回的 count 字段本质是字符串,ECharts 或 JS 计算时会出错:
立即学习“PHP免费学习笔记(深入)”;
- 后端返回前必须显式转整型:
'count' => (int) $cacheValue,别依赖 PHP 自动类型转换(尤其在 PHP 8.1+ 中行为更严格) - 前端也得兜底:
data.map(d => ({...d, count: Number(d.count)})),防止某条数据因网络截断或缓存污染变成空字符串 - 测试时用
curl -v http://your.app/api/rate?ip=192.168.1.100&route=/api/v1/order直接看原始响应,比浏览器调试更快定位类型问题 - 如果返回结构嵌套深,比如
data.items[0].count,务必逐层 cast,别只 cast 最外层
统计键设计错会导致数据完全对不上
键里硬塞时间戳(如 api:rate:2026081712:/order)是最常见错误,后果是:你查“最近 60 秒”,实际查的是“今天整点开始的那分钟”。
- 正确做法:key 不含时间,靠 TTL 控制生命周期,例如
api:rate:192.168.1.100:/api/order+Cache::inc($key, 1, 60) - 多维度聚合时用冒号分隔,但层级要固定:
api:rate:user_{$uid}:ip_{$ip}:{$route},别一会儿user:ip:route,一会儿ip:user:route - 别用
KEYS api:rate:*扫描全量 key —— 生产 Redis 会卡死;要用业务主键(如 IP + route)反查,或用SCAN游标分页拉取 - 真正难的不是写代码,而是确保每次
Cache::inc调用都发生在同一中间件位置、同一请求生命周期内,且没有被异常跳过



















