<?php
namespace app\common\helper;
use think\facade\Cache;
/**
* 通用节流助手类
* 提供多种方式的请求频率限制功能
*/
class RateLimitHelper
{
/**
* 通用节流函数
* @param array $options 配置选项
* [
* 'identifier' => '唯一标识符,如IP、用户ID等(必填)',
* 'time_window' => '时间窗口(秒),默认1秒',
* 'max_requests' => '最大请求数,默认1次',
* 'prefix' => '缓存键前缀,默认'rate_limit'',
* 'type' => '限制类型:'general', 'ip', 'user', 'api',默认'general''
* ]
* @return array ['allow' => true/false, 'msg' => '消息', 'data' => 其他信息]
*/
public static function submitData($options = [])
{
// 默认配置
$defaults = [
'identifier' => '', // 唯一标识符(必填)
'time_window' => 1, // 时间窗口(秒)
'max_requests' => 1, // 最大请求数
'prefix' => 'rate_limit', // 缓存键前缀
'type' => 'general' // 限制类型
];
// 合并配置
$config = array_merge($defaults, $options);
// 验证必要参数
if (empty($config['identifier'])) {
if ($config['type'] === 'ip') {
$config['identifier'] = request()->ip();
} elseif ($config['type'] === 'user') {
$config['identifier'] = session('user_id') ?: request()->ip();
} else {
return ['allow' => false, 'msg' => '缺少标识符参数'];
}
}
// 根据类型生成缓存键前缀
switch ($config['type']) {
case 'ip':
$config['prefix'] = 'rate_limit_ip';
if (empty($config['identifier'])) {
$config['identifier'] = request()->ip();
}
break;
case 'user':
$config['prefix'] = 'rate_limit_user';
if (empty($config['identifier'])) {
$config['identifier'] = session('user_id') ?: request()->ip();
}
break;
case 'api':
$config['prefix'] = 'rate_limit_api';
if (empty($config['identifier'])) {
$apiName = request()->controller() . '_' . request()->action();
$config['identifier'] = $apiName . '_' . request()->ip();
}
break;
default:
// 通用类型,使用传入的prefix
break;
}
// 生成缓存键
$cacheKey = $config['prefix'] . '_' . md5($config['identifier']);
// 获取当前计数
$current = Cache::get($cacheKey, 0);
// 检查是否超过限制
if ($current >= $config['max_requests']) {
return [
'allow' => false,
'msg' => '请求过于频繁,请稍后再试',
'data' => [
'current' => $current,
'max_requests' => $config['max_requests'],
'time_window' => $config['time_window'],
'identifier' => $config['identifier']
]
];
}
// 增加计数并设置过期时间
Cache::inc($cacheKey);
if ($current == 0) {
// 如果是第一次请求,设置过期时间
Cache::set($cacheKey, 1, $config['time_window']);
}
return [
'allow' => true,
'msg' => '允许请求',
'data' => [
'current' => $current + 1,
'max_requests' => $config['max_requests'],
'time_window' => $config['time_window'],
'identifier' => $config['identifier']
]
];
}
/**
* 按IP限制的快捷方法
* @param array $options 配置选项
* @return array
*/
public static function checkByIp($options = [])
{
$options['type'] = 'ip';
if (!isset($options['identifier'])) {
$options['identifier'] = request()->ip();
}
return self::submitData($options);
}
/**
* 按用户限制的快捷方法
* @param int|string $userId 用户ID
* @param array $options 配置选项
* @return array
*/
public static function checkByUser($userId = null, $options = [])
{
$options['type'] = 'user';
if ($userId !== null) {
$options['identifier'] = $userId;
} elseif (!isset($options['identifier'])) {
$options['identifier'] = session('user_id') ?: request()->ip();
}
return self::submitData($options);
}
/**
* 按API限制的快捷方法
* @param string $apiName API名称
* @param string|null $identifier 额外标识符
* @param array $options 配置选项
* @return array
*/
public static function checkByApi($apiName, $identifier = null, $options = [])
{
$options['type'] = 'api';
if ($identifier !== null) {
$options['identifier'] = $apiName . '_' . $identifier;
} elseif (!isset($options['identifier'])) {
$options['identifier'] = $apiName . '_' . request()->ip();
}
return self::submitData($options);
}
/**
* 重置节流计数
* @param string $identifier 标识符
* @param string $prefix 缓存键前缀
*/
public static function reset($identifier, $prefix = 'rate_limit')
{
$cacheKey = $prefix . '_' . md5($identifier);
Cache::delete($cacheKey);
}
}
// 使用示例:
/*
// 1. 基本使用(1秒内只处理一次)
$result = RateLimitHelper::submitData([
'identifier' => request()->ip(),
'time_window' => 1,
'max_requests' => 1
]);
if (!$result['allow']) {
return json(['code' => 429, 'msg' => $result['msg']]);
}
// 2. 按IP限制(快捷方法)
$result = RateLimitHelper::checkByIp(['time_window' => 1, 'max_requests' => 1]);
if (!$result['allow']) {
return json(['code' => 429, 'msg' => $result['msg']]);
}
// 3. 按用户限制
$result = RateLimitHelper::checkByUser($userId, ['time_window' => 1, 'max_requests' => 1]);
if (!$result['allow']) {
return json(['code' => 429, 'msg' => $result['msg']]);
}
// 4. 按API限制
$result = RateLimitHelper::checkByApi('submit_form', $userId, ['time_window' => 1, 'max_requests' => 1]);
if (!$result['allow']) {
return json(['code' => 429, 'msg' => $result['msg']]);
}
// 5. 自定义配置
$result = RateLimitHelper::submitData([
'identifier' => 'custom_key',
'time_window' => 5, // 5秒
'max_requests' => 3, // 最多3次
'prefix' => 'custom_rate_limit',
'type' => 'general'
]);
if (!$result['allow']) {
return json(['code' => 429, 'msg' => $result['msg']]);
}
*/
<?php
namespace app\controller;
use app\common\helper\RateLimitHelper;
use think\Controller;
/**
* 简单控制器示例
* 演示如何使用节流功能限制请求频率
*/
class SimpleController extends Controller
{
/**
* 简单的表单提交接口
* 限制:1秒内只能提交一次
*/
public function simpleSubmit()
{
// 节流检查:按IP地址限制,1秒内最多1次请求
$result = RateLimitHelper::submitData([
'identifier' => request()->ip(), // 使用客户端IP作为唯一标识符
'time_window' => 1, // 时间窗口:1秒
'max_requests' => 1 // 最大请求数:1次
]);
// 检查是否被节流限制
if (!$result['allow']) {
// 如果被限制,返回429状态码和错误信息
return json(['code' => 429, 'msg' => $result['msg']]);
}
// 节流检查通过,处理业务逻辑
$data = [
'name' => input('name'), // 获取前端传来的name参数
'email' => input('email'), // 获取前端传来的email参数
'time' => date('Y-m-d H:i:s') // 当前时间
];
// 验证必要参数
if (empty($data['name']) || empty($data['email'])) {
return json(['code' => 400, 'msg' => '姓名和邮箱不能为空']);
}
// 实际项目中,这里会保存数据到数据库
// Db::name('form_submissions')->insert($data);
// 返回成功响应
return json([
'code' => 200, // 成功状态码
'msg' => '提交成功', // 成功消息
'data' => $data // 返回提交的数据
]);
}
/**
* 使用快捷方法的提交接口
* 限制:1秒内只能提交一次
*/
public function quickSubmit()
{
// 使用快捷方法进行节流检查(按IP限制)
$result = RateLimitHelper::checkByIp([
'time_window' => 1, // 时间窗口:1秒
'max_requests' => 1 // 最大请求数:1次
]);
// 检查是否被节流限制
if (!$result['allow']) {
// 如果被限制,返回429状态码和错误信息
return json(['code' => 429, 'msg' => $result['msg']]);
}
// 节流检查通过,执行业务逻辑
$businessData = [
'timestamp' => date('Y-m-d H:i:s'), // 当前时间戳
'ip' => request()->ip() // 客户端IP
];
// 实际项目中,这里会执行具体的业务逻辑
// 例如:保存操作记录、更新用户状态等
// 返回成功响应
return json([
'code' => 200, // 成功状态码
'msg' => '处理成功', // 成功消息
'data' => $businessData // 返回业务数据
]);
}
/**
* 按用户限制的提交接口(需要登录)
* 限制:1秒内只能提交一次
*/
public function userSubmit()
{
// 获取当前登录用户ID
$userId = session('user_id');
if (!$userId) {
return json(['code' => 401, 'msg' => '请先登录']);
}
// 使用节流检查(按用户ID限制)
$result = RateLimitHelper::submitData([
'identifier' => $userId, // 使用用户ID作为标识符
'time_window' => 1, // 时间窗口:1秒
'max_requests' => 1, // 最大请求数:1次
'type' => 'user' // 指定为用户类型限制
]);
// 检查是否被节流限制
if (!$result['allow']) {
return json(['code' => 429, 'msg' => $result['msg']]);
}
// 节流检查通过,执行用户相关的业务逻辑
$userData = [
'user_id' => $userId,
'action' => 'user_submit',
'time' => date('Y-m-d H:i:s')
];
// 实际项目中,这里会处理用户相关操作
// 返回成功响应
return json([
'code' => 200,
'msg' => '用户操作成功',
'data' => $userData
]);
}
/**
* 按API接口限制的提交
* 限制:1秒内只能调用一次
*/
public function apiSubmit()
{
// 获取用户ID(可能未登录)
$userId = session('user_id') ?: 0;
// 使用API类型限制
$result = RateLimitHelper::checkByApi(
'api_submit', // API名称
$userId . '_' . request()->ip(), // 组合标识符(用户ID + IP)
['time_window' => 1, 'max_requests' => 1] // 配置参数
);
// 检查是否被节流限制
if (!$result['allow']) {
return json(['code' => 429, 'msg' => $result['msg']]);
}
// 节流检查通过,执行API业务逻辑
$apiData = [
'api_name' => 'api_submit',
'user_id' => $userId,
'ip' => request()->ip(),
'executed_at' => date('Y-m-d H:i:s')
];
// 实际项目中,这里会执行API特定的业务逻辑
// 返回成功响应
return json([
'code' => 200,
'msg' => 'API调用成功',
'data' => $apiData
]);
}
}
// 使用说明:
/*
前端调用示例(JavaScript):
// 1. 调用 simpleSubmit 接口
async function callSimpleSubmit() {
try {
const response = await fetch('/simple/simpleSubmit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: '张三',
email: 'zhangsan@example.com'
})
});
const result = await response.json();
if (result.code === 200) {
alert('提交成功!');
} else if (result.code === 429) {
alert('提交过于频繁,请1秒后再试!');
} else {
alert('提交失败:' + result.msg);
}
} catch (error) {
alert('网络错误!');
}
}
// 2. 快速连续调用测试(会触发节流)
async function testRateLimit() {
// 第一次调用
callSimpleSubmit();
// 立即第二次调用(会被节流限制)
setTimeout(() => {
callSimpleSubmit();
}, 100); // 100毫秒后调用
}
// layui 示例(注释掉)
/*
layui.use(['form', 'layer'], function(){
var form = layui.form;
var layer = layui.layer;
// 表单提交
form.on('submit(demo1)', function(data){
$.ajax({
url: '/simple/simpleSubmit',
type: 'POST',
data: data.field,
dataType: 'json',
success: function(res) {
if (res.code === 200) {
layer.msg('提交成功');
} else if (res.code === 429) {
layer.msg('提交过于频繁,请稍后再试');
} else {
layer.msg('提交失败:' + res.msg);
}
},
error: function() {
layer.msg('网络错误');
}
});
return false; // 阻止表单跳转
});
});
*/
ThinkPHP 8 的节流方法实现,用于限制同一 IP 在 2 秒内只能处理一次请求。
这个方法利用了 ThinkPHP 的缓存功能,将 IP 地址作为唯一标识符来存储请求的时间戳。它被设计为一个可复用的通用函数,方便在控制器中调用,符合您对代码整合性和复用性的要求。
<?php
// 假设此方法位于一个公共模型或服务类中,例如 app/service/ThrottleService.php
// 或者可以放在一个助手函数文件中,如 app/common.php
namespace app\service; // 请根据您的实际命名空间调整
use think\facade\Cache;
use think\facade\Request;
class ThrottleService
{
/**
* 请求节流方法,限制同一 IP 在指定时间间隔内只能处理一次请求
* @param string $key (可选) 节流的唯一标识符,默认为客户端 IP
* @param int $timeInterval 限制的时间间隔,单位为秒,默认为 2 秒
* @param string $message 被节流时返回的提示信息
* @return array 如果被节流则返回 ['success' => false, 'message' => ...],否则返回 ['success' => true]
*/
public static function checkThrottle(string $key = '', int $timeInterval = 2, string $message = '请求过于频繁,请稍后再试')
{
// 使用客户端 IP 作为默认的节流键
$ip = $key ?: Request::ip();
$cacheKey = "throttle_{$ip}"; // 构造缓存键
// 尝试获取上次请求的时间戳
$lastRequestTime = Cache::get($cacheKey);
if ($lastRequestTime !== null) {
// 如果当前时间与上次请求时间差小于时间间隔,则视为请求过于频繁
if (time() - $lastRequestTime < $timeInterval) {
return ['success' => false, 'message' => $message];
}
}
// 更新缓存,记录本次请求的时间戳,有效期为 $timeInterval 秒
Cache::set($cacheKey, time(), $timeInterval);
// 未被节流,允许继续处理
return ['success' => true];
}
}
// --- 在控制器中的使用示例 ---
// 文件路径: app/controller/YourController.php
namespace app\controller;
use app\BaseController;
use app\service\ThrottleService; // 引入服务类
use think\facade\Request;
use think\facade\Db;
class YourController extends BaseController
{
public function handlePostRequest()
{
if (Request::isPost()) {
// 调用节流方法,限制 2 秒内只能提交一次
$throttleResult = ThrottleService::checkThrottle('', 2, '操作过于频繁,请2秒后重试!');
if (!$throttleResult['success']) {
// 如果被节流,直接返回错误信息
return json(['code' => 429, 'msg' => $throttleResult['message']]);
}
// --- 业务逻辑开始 ---
// 节流通过,可以安全执行后续的业务代码
$data = Request::post(); // 获取 POST 数据
// ... 执行您的业务逻辑,例如数据库操作等 ...
// 示例:Db::name('some_table')->insert($data);
// --- 业务逻辑结束 ---
return json(['code' => 0, 'msg' => '请求处理成功']);
}
return json(['code' => 405, 'msg' => 'Method not allowed']);
}
// 您之前提到的 getStockBatchInfo 方法也可以应用此节流
public function getStockBatchInfo()
{
if (Request::isPost()) {
// 为库存查询也应用节流
$throttleResult = ThrottleService::checkThrottle('', 2, '查询过于频繁,请2秒后重试!');
if (!$throttleResult['success']) {
return json(['code' => 429, 'msg' => $throttleResult['message'], 'count' => 0, 'data' => []]);
}
$data = Request::post(['stockid','goodsname','batchno','page','limit']);
$stockid = $data['stockid'] ?? null;
$goodsname = $data['goodsname'] ?? null;
$batchno = $data['batchno'] ?? null;
$page = $data['page'] ?? 1;
$limit = $data['limit'] ?? 10;
// 之前的查询逻辑...
try {
$query = Db::name('glg_stock');
if (!empty($stockid)) { $query->where('stockid', $stockid); }
if (!empty($goodsname)) { $query->where('goodsname', $goodsname); }
if (!empty($batchno)) { $query->where('batchno', $batchno); }
$count = $query->count();
$result = $query->page($page, $limit)->select();
return json([
'code' => 0,
'msg' => '查询成功',
'count' => $count,
'data' => $result
]);
} catch (\Exception $e) {
return json([
'code' => 1,
'msg' => $e->getMessage(),
'count' => 0,
'data' => []
]);
}
}
// 如果不是 POST 请求...
return json(['code' => 405, 'msg' => 'Method not allowed']);
}
}
代码说明:
ThrottleService::checkThrottle: 这是核心的节流函数。
它接受一个可选的 $key(默认为当前 IP),一个 $timeInterval(默认 2 秒),和一个 $message(被节流时的提示)。
使用 Cache::get 检查是否存在该 IP 的请求记录。
如果存在记录 ($lastRequestTime),则比较当前时间与记录时间的差值。
如果差值小于 $timeInterval,说明请求过于频繁,返回 ['success' => false, ...]。
如果未被节流或没有历史记录,则使用 Cache::set 以 IP 为键,当前时间戳为值,设置一个 $timeInterval 秒后过期的缓存。
最后返回 ['success' => true] 表示可以通过。
在控制器中使用: 在处理 POST 请求的逻辑开始前,调用 ThrottleService::checkThrottle()。
检查返回结果,如果 success 为 false,则立即返回错误信息,阻止后续业务逻辑执行。
如果 success 为 true,则继续执行正常的业务代码。
这种实现方式简洁、通用,并且易于在不同的控制器方法中复用。它将节流逻辑与具体业务逻辑分离,提高了代码的可维护性。
评论区