214 lines
6.6 KiB
PHP
214 lines
6.6 KiB
PHP
<?php
|
||
/**
|
||
* 排队控制器
|
||
* 创建时间: 2026-06-25
|
||
* 更新时间: 2026-06-25
|
||
* 作用: 处理排队页面的验证码验证和队列状态查询
|
||
*/
|
||
|
||
namespace app\index\controller;
|
||
|
||
use think\Config;
|
||
use think\Cookie;
|
||
use think\Request;
|
||
use think\Controller;
|
||
|
||
class Queue extends Controller
|
||
{
|
||
/**
|
||
* 检查队列状态
|
||
* GET /queue/check
|
||
*/
|
||
public function check()
|
||
{
|
||
$config = Config::get('rate_limit');
|
||
if (!$config || !is_array($config)) {
|
||
$config = [];
|
||
}
|
||
$queueFile = isset($config['queue_counter_file']) ? $config['queue_counter_file'] : RUNTIME_PATH . 'rate_limit/queue_count.txt';
|
||
|
||
$queueCount = 0;
|
||
|
||
if (file_exists($queueFile)) {
|
||
$content = @file_get_contents($queueFile);
|
||
if ($content !== false && $content !== '') {
|
||
$data = json_decode($content, true);
|
||
if (!$data || !is_array($data)) {
|
||
$data = array('count' => 0, 'updated' => 0);
|
||
}
|
||
} else {
|
||
$data = array('count' => 0, 'updated' => 0);
|
||
}
|
||
|
||
// 如果超过10秒未更新,认为队列已清空
|
||
if (time() - $data['updated'] > 10) {
|
||
$queueCount = 0;
|
||
} else {
|
||
$queueCount = isset($data['count']) ? intval($data['count']) : 0;
|
||
}
|
||
}
|
||
|
||
// 计算预计等待时间(每人约0.5秒)
|
||
$waitSeconds = max(1, ceil($queueCount * 0.5));
|
||
$maxQueries = isset($config['max_queries_per_second']) ? intval($config['max_queries_per_second']) : 10;
|
||
|
||
return json(array(
|
||
'code' => 1,
|
||
'msg' => 'ok',
|
||
'data' => array(
|
||
'queue_count' => $queueCount,
|
||
'wait_seconds' => $waitSeconds,
|
||
'max_queries_per_second' => $maxQueries
|
||
)
|
||
));
|
||
}
|
||
|
||
/**
|
||
* 生成验证码
|
||
* GET /queue/captcha
|
||
*/
|
||
public function captcha()
|
||
{
|
||
// 生成数学题
|
||
$num1 = rand(1, 20);
|
||
$num2 = rand(1, 20);
|
||
$answer = $num1 + $num2;
|
||
|
||
// 生成签名
|
||
$timestamp = time();
|
||
$dbConfig = Config::get('database');
|
||
$dbName = isset($dbConfig['database']) ? $dbConfig['database'] : '';
|
||
$sign = md5($answer . '_' . $timestamp . '_' . $dbName);
|
||
|
||
// 存储验证码答案到临时文件
|
||
$captchaDir = RUNTIME_PATH . 'rate_limit/';
|
||
if (!is_dir($captchaDir)) {
|
||
@mkdir($captchaDir, 0755, true);
|
||
}
|
||
|
||
// 使用 sign 本身作为文件名(不再二次 md5)
|
||
$captchaFile = $captchaDir . 'captcha_' . $sign . '.txt';
|
||
@file_put_contents($captchaFile, json_encode(array(
|
||
'answer' => $answer,
|
||
'created' => $timestamp
|
||
)));
|
||
|
||
return json(array(
|
||
'code' => 1,
|
||
'msg' => 'ok',
|
||
'data' => array(
|
||
'question' => $num1 . ' + ' . $num2 . ' = ?',
|
||
'sign' => $sign,
|
||
'expire' => 300
|
||
)
|
||
));
|
||
}
|
||
|
||
/**
|
||
* 验证验证码
|
||
* POST /queue/verify
|
||
*/
|
||
public function verify()
|
||
{
|
||
$request = Request::instance();
|
||
$answer = $request->post('answer');
|
||
$sign = $request->post('sign');
|
||
|
||
if (!$answer || !$sign) {
|
||
return json(array('code' => 0, 'msg' => 'param error'));
|
||
}
|
||
|
||
// 验证签名格式
|
||
if (!preg_match('/^[a-f0-9]{32}$/', $sign)) {
|
||
return json(array('code' => 0, 'msg' => 'sign format error'));
|
||
}
|
||
|
||
// 读取验证码文件(使用 sign 本身作为文件名)
|
||
$captchaFile = RUNTIME_PATH . 'rate_limit/captcha_' . $sign . '.txt';
|
||
|
||
if (!file_exists($captchaFile)) {
|
||
return json(array('code' => 0, 'msg' => 'captcha expired'));
|
||
}
|
||
|
||
$content = @file_get_contents($captchaFile);
|
||
$data = json_decode($content, true);
|
||
|
||
if (!$data || !isset($data['answer']) || !isset($data['created'])) {
|
||
@unlink($captchaFile);
|
||
return json(array('code' => 0, 'msg' => 'captcha data error'));
|
||
}
|
||
|
||
// 检查是否过期(5分钟)
|
||
if (time() - $data['created'] > 300) {
|
||
@unlink($captchaFile);
|
||
return json(array('code' => 0, 'msg' => 'captcha expired'));
|
||
}
|
||
|
||
// 验证答案
|
||
if (intval($answer) !== intval($data['answer'])) {
|
||
return json(array('code' => 0, 'msg' => 'wrong answer'));
|
||
}
|
||
|
||
// 验证通过,删除验证码文件
|
||
@unlink($captchaFile);
|
||
|
||
// 设置免排队Cookie(30分钟有效)
|
||
$config = Config::get('rate_limit');
|
||
if (!$config || !is_array($config)) {
|
||
$config = array();
|
||
}
|
||
$cookieName = isset($config['bypass_cookie_name']) ? $config['bypass_cookie_name'] : 'queue_bypass';
|
||
$validDuration = isset($config['captcha_valid_duration']) ? intval($config['captcha_valid_duration']) : 1800;
|
||
|
||
$timestamp = time();
|
||
$dbConfig = Config::get('database');
|
||
$dbName = isset($dbConfig['database']) ? $dbConfig['database'] : '';
|
||
$cookieSign = md5($timestamp . '_' . $dbName);
|
||
$cookieValue = $timestamp . '_' . $cookieSign;
|
||
|
||
Cookie::set($cookieName, $cookieValue, $validDuration);
|
||
|
||
return json(array(
|
||
'code' => 1,
|
||
'msg' => 'ok',
|
||
'data' => array(
|
||
'bypass_duration' => $validDuration
|
||
)
|
||
));
|
||
}
|
||
|
||
/**
|
||
* 队列状态
|
||
* GET /queue/status
|
||
*/
|
||
public function status()
|
||
{
|
||
$config = Config::get('rate_limit');
|
||
if (!$config || !is_array($config)) {
|
||
$config = array();
|
||
}
|
||
$counterPath = isset($config['counter_path']) ? $config['counter_path'] : RUNTIME_PATH . 'rate_limit/';
|
||
$counterDir = rtrim($counterPath, '/');
|
||
$timeWindow = isset($config['time_window']) ? intval($config['time_window']) : 1;
|
||
$currentSecond = floor(time() / $timeWindow);
|
||
$counterFile = $counterDir . '/counter_' . $currentSecond . '.txt';
|
||
|
||
$queryCount = 0;
|
||
if (file_exists($counterFile)) {
|
||
$queryCount = intval(@file_get_contents($counterFile));
|
||
}
|
||
|
||
$maxQueries = isset($config['max_queries_per_second']) ? intval($config['max_queries_per_second']) : 10;
|
||
|
||
return json(array(
|
||
'code' => 1,
|
||
'msg' => 'ok',
|
||
'data' => array(
|
||
'current_queries' => $queryCount,
|
||
'max_queries_per_second' => $maxQueries,
|
||
'is_limited' => $queryCount >= $maxQueries
|
||
)
|
||
));
|
||
}
|
||
}
|