侧边栏壁纸
博主头像
云BLOG 博主等级

行动起来,活在当下

  • 累计撰写 318 篇文章
  • 累计创建 6 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

THINKPHP8 通用节流助手类

Administrator
2025-12-03 / 0 评论 / 0 点赞 / 3 阅读 / 0 字
<?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,则继续执行正常的业务代码。
这种实现方式简洁、通用,并且易于在不同的控制器方法中复用。它将节流逻辑与具体业务逻辑分离,提高了代码的可维护性。

0

评论区