chore: 添加缺失的静态资源文件

添加多个前端界面所需的图片、字体、音频等静态资源文件,包括:
- 管理员后台的图标、背景图、组件图片
- 移动端页面的商品图片、用户中心图片、活动图标
- 第三方库的示例图片和字体文件
- 浏览器兼容性配置文件
- 图标字体文件(eot、ttf、woff、woff2格式)

这些资源是前端界面正常显示所必需的,之前可能因版本控制忽略或遗漏而缺失。
This commit is contained in:
2026-03-06 20:15:52 +08:00
parent c9a98f342c
commit 0060b2798f
7739 changed files with 1314791 additions and 0 deletions

View File

@@ -0,0 +1,257 @@
<?php
declare(strict_types=1);
namespace think\middleware;
use Closure;
use Psr\SimpleCache\CacheInterface;
use think\Cache;
use think\Config;
use think\Container;
use think\exception\HttpResponseException;
use think\middleware\throttle\CounterFixed;
use think\middleware\throttle\ThrottleAbstract;
use think\Request;
use think\Response;
use function sprintf;
/**
* 访问频率限制中间件
* Class Throttle
* @package think\middleware
*/
class Throttle
{
/**
* 默认配置参数
* @var array
*/
public static $default_config = [
'prefix' => 'throttle_', // 缓存键前缀,防止键与其他应用冲突
'key' => true, // 节流规则 true为自动规则
'visit_method' => ['GET', 'HEAD'], // 要被限制的请求类型
'visit_rate' => null, // 节流频率 null 表示不限制 eg: 10/m 20/h 300/d
'visit_enable_show_rate_limit' => true, // 在响应体中设置速率限制的头部信息
'visit_fail_code' => 429, // 访问受限时返回的http状态码,当没有visit_fail_response时生效
'visit_fail_text' => 'Too Many Requests', // 访问受限时访问的文本信息,当没有visit_fail_response时生效
'visit_fail_response' => null, // 访问受限时的响应信息闭包回调
'driver_name' => CounterFixed::class, // 限流算法驱动
];
public static $duration = [
's' => 1,
'm' => 60,
'h' => 3600,
'd' => 86400,
];
/**
* 缓存对象
* @var CacheInterface
*/
protected $cache;
/**
* 配置参数
* @var array
*/
protected $config = [];
protected $key = null; // 解析后的标识
protected $wait_seconds = 0; // 下次合法请求还有多少秒
protected $now = 0; // 当前时间戳
protected $max_requests = 0; // 规定时间内允许的最大请求次数
protected $expire = 0; // 规定时间
protected $remaining = 0; // 规定时间内还能请求的次数
/**
* @var ThrottleAbstract|null
*/
protected $driver_class = null;
/**
* Throttle constructor.
* @param Cache $cache
* @param Config $config
*/
public function __construct(Cache $cache, Config $config)
{
$this->cache = $cache;
$this->config = array_merge(static::$default_config, $config->get('throttle', []));
}
/**
* 请求是否允许
* @param Request $request
* @return bool
*/
protected function allowRequest(Request $request): bool
{
// 若请求类型不在限制内
if (!in_array($request->method(), $this->config['visit_method'])) {
return true;
}
$key = $this->getCacheKey($request);
if (null === $key) {
return true;
}
[$max_requests, $duration] = $this->parseRate($this->config['visit_rate']);
$micronow = microtime(true);
$now = (int) $micronow;
$this->driver_class = Container::getInstance()->invokeClass($this->config['driver_name']);
if (!$this->driver_class instanceof ThrottleAbstract) {
throw new \TypeError('The throttle driver must extends ' . ThrottleAbstract::class);
}
$allow = $this->driver_class->allowRequest($key, $micronow, $max_requests, $duration, $this->cache);
if ($allow) {
// 允许访问
$this->now = $now;
$this->expire = $duration;
$this->max_requests = $max_requests;
$this->remaining = $max_requests - $this->driver_class->getCurRequests();
return true;
}
$this->wait_seconds = $this->driver_class->getWaitSeconds();
return false;
}
/**
* 处理限制访问
* @param Request $request
* @param Closure $next
* @param array $params
* @return Response
*/
public function handle(Request $request, Closure $next, array $params=[]): Response
{
if ($params) {
$this->config = array_merge($this->config, $params);
}
$allow = $this->allowRequest($request);
if (!$allow) {
// 访问受限
throw $this->buildLimitException($this->wait_seconds, $request);
}
$response = $next($request);
if (200 <= $response->getCode() && 300 > $response->getCode() && $this->config['visit_enable_show_rate_limit']) {
// 将速率限制 headers 添加到响应中
$response->header($this->getRateLimitHeaders());
}
return $response;
}
/**
* 生成缓存的 key
* @param Request $request
* @return null|string
*/
protected function getCacheKey(Request $request): ?string
{
$key = $this->config['key'];
if ($key instanceof \Closure) {
$key = Container::getInstance()->invokeFunction($key, [$this, $request]);
}
if ($key === null || $key === false || $this->config['visit_rate'] === null) {
// 关闭当前限制
return null;
}
if ($key === true) {
$key = $request->ip();
} elseif (false !== strpos($key, '__')) {
$key = str_replace(['__CONTROLLER__', '__ACTION__', '__IP__'], [$request->controller(), $request->action(), $request->ip()], $key);
}
return md5($this->config['prefix'] . $key . $this->config['driver_name']);
}
/**
* 解析频率配置项
* @param string $rate
* @return int[]
*/
protected function parseRate($rate): array
{
[$num, $period] = explode("/", $rate);
$max_requests = (int) $num;
$duration = static::$duration[$period] ?? (int) $period;
return [$max_requests, $duration];
}
/**
* 设置速率
* @param string $rate '10/m' '20/300'
* @return $this
*/
public function setRate(string $rate): self
{
$this->config['visit_rate'] = $rate;
return $this;
}
/**
* 设置缓存驱动
* @param CacheInterface $cache
* @return $this
*/
public function setCache(CacheInterface $cache): self
{
$this->cache = $cache;
return $this;
}
/**
* 设置限流算法类
* @param string $class_name
* @return $this
*/
public function setDriverClass(string $class_name): self
{
$this->config['driver_name'] = $class_name;
return $this;
}
/**
* 获取速率限制头
* @return array
*/
public function getRateLimitHeaders(): array
{
return [
'X-Rate-Limit-Limit' => $this->max_requests,
'X-Rate-Limit-Remaining' => $this->remaining < 0 ? 0 : $this->remaining,
'X-Rate-Limit-Reset' => $this->now + $this->expire,
];
}
/**
* 构建 Response Exception
* @param int $wait_seconds
* @param Request $request
* @return HttpResponseException
*/
public function buildLimitException(int $wait_seconds, Request $request): HttpResponseException {
$visitFail = $this->config['visit_fail_response'] ?? null;
if ($visitFail instanceof \Closure) {
$response = Container::getInstance()->invokeFunction($visitFail, [$this, $request, $wait_seconds]);
if (!$response instanceof Response) {
throw new \TypeError(sprintf('The closure must return %s instance', Response::class));
}
} else {
$content = str_replace('__WAIT__', (string) $wait_seconds, $this->config['visit_fail_text']);
$response = Response::create($content)->code($this->config['visit_fail_code']);
}
if ($this->config['visit_enable_show_rate_limit']) {
$response->header(['Retry-After' => $wait_seconds]);
}
return new HttpResponseException($response);
}
}

View File

@@ -0,0 +1,33 @@
<?php
// +----------------------------------------------------------------------
// | 节流设置
// +----------------------------------------------------------------------
use think\middleware\Throttle;
use think\middleware\throttle\CounterFixed;
use think\Request;
use think\Response;
return [
// 缓存键前缀,防止键值与其他应用冲突
'prefix' => 'throttle_',
// 缓存的键,true 表示使用来源ip
'key' => true,
// 要被限制的请求类型, eg: GET POST PUT DELETE HEAD 等
'visit_method' => ['GET', 'HEAD'],
// 设置访问频率,例如 '10/m' 指的是允许每分钟请求10次;'10/60'指允许每60秒请求10次。值 null 表示不限制, eg: null 10/m 20/h 300/d 200/300
'visit_rate' => '100/m',
/*
* 设置节流算法,组件提供了四种算法:
* - CounterFixed :计数固定窗口
* - CounterSlider: 滑动窗口
* - TokenBucket : 令牌桶算法
* - LeakyBucket : 漏桶限流算法
*/
'driver_name' => CounterFixed::class,
// 响应体中设置速率限制的头部信息
'visit_enable_show_rate_limit' => true,
// 访问受限时返回的响应
'visit_fail_response' => function (Throttle $throttle, Request $request, int $wait_seconds) {
return Response::create('Too many requests, try again after ' . $wait_seconds . ' seconds.')->code(429);
},
];

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace think\middleware\throttle;
use Psr\SimpleCache\CacheInterface;
/**
* 计数器固定窗口算法
* Class CounterFixed
* @package think\middleware\throttle
*/
class CounterFixed extends ThrottleAbstract
{
public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, CacheInterface $cache): bool
{
$cur_requests = (int) $cache->get($key, 0);
$now = (int) $micronow;
$wait_reset_seconds = $duration - $now % $duration; // 距离下次重置还有n秒时间
$this->wait_seconds = $wait_reset_seconds % $duration + 1;
$this->cur_requests = $cur_requests;
if ($cur_requests < $max_requests) { // 允许访问
$cache->set($key, $this->cur_requests + 1, $wait_reset_seconds);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace think\middleware\throttle;
use Psr\SimpleCache\CacheInterface;
/**
* 计数器滑动窗口算法
* Class CouterSlider
* @package think\middleware\throttle
*/
class CounterSlider extends ThrottleAbstract
{
public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, CacheInterface $cache): bool
{
$history = $cache->get($key, []);
$now = (int) $micronow;
// 移除过期的请求的记录
$history = array_values(array_filter($history, function ($val) use ($now, $duration) {
return $val >= $now - $duration;
}));
$this->cur_requests = count($history);
if ($this->cur_requests < $max_requests) {
// 允许访问
$history[] = $now;
$cache->set($key, $history, $duration);
return true;
}
if ($history) {
$wait_seconds = $duration - ($now - $history[0]) + 1;
$this->wait_seconds = $wait_seconds > 0 ? $wait_seconds : 0;
}
return false;
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace think\middleware\throttle;
use Psr\SimpleCache\CacheInterface;
/**
* 漏桶算法
* Class LeakyBucket
* @package think\middleware\throttle
*/
class LeakyBucket extends ThrottleAbstract
{
public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, CacheInterface $cache): bool
{
if ($max_requests <= 0) return false;
$last_time = (float) $cache->get($key, 0); // 最近一次请求
$rate = (float) $duration / $max_requests; // 平均 n 秒一个请求
if ($micronow - $last_time < $rate) {
$this->cur_requests = 1;
$this->wait_seconds = ceil($rate - ($micronow - $last_time));
return false;
}
$cache->set($key, $micronow, $duration);
return true;
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace think\middleware\throttle;
use Psr\SimpleCache\CacheInterface;
abstract class ThrottleAbstract
{
/** @var int */
protected $cur_requests = 0; // 当前已有的请求数
/** @var int */
protected $wait_seconds = 0; // 距离下次合法请求还有多少秒
/**
* 是否允许访问
* @param string $key 缓存键
* @param float $micronow 当前时间戳,可含毫秒
* @param int $max_requests 允许最大请求数
* @param int $duration 限流时长
* @param CacheInterface $cache 缓存对象
* @return bool
*/
abstract public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, CacheInterface $cache): bool;
/**
* 计算距离下次合法请求还有多少秒
* @return int
*/
public function getWaitSeconds(): int
{
return (int) $this->wait_seconds;
}
/**
* 当前已有的请求数
* @return int
*/
public function getCurRequests(): int
{
return (int) $this->cur_requests;
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace think\middleware\throttle;
use Psr\SimpleCache\CacheInterface;
/**
* 令牌桶算法
* Class TokenBucket
* @package think\middleware\throttle
*/
class TokenBucket extends ThrottleAbstract
{
public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, CacheInterface $cache): bool
{
if ($max_requests <= 0 || $duration <= 0) return false;
$assist_key = $key . 'store_num'; // 辅助缓存
$rate = (float) $max_requests / $duration; // 平均一秒生成 n 个 token
$last_time = $cache->get($key, null);
$store_num = $cache->get($assist_key, null);
if ($last_time === null || $store_num === null) { // 首次访问
$cache->set($key, $micronow, $duration);
$cache->set($assist_key, $max_requests - 1, $duration);
return true;
}
$create_num = floor(($micronow - $last_time) * $rate); // 推算生成的 token 数
$token_left = (int) min($max_requests, $store_num + $create_num); //当前剩余 tokens 数量
if ($token_left < 1) {
$tmp = (int) ceil($duration / $max_requests);
$this->wait_seconds = $tmp - ($micronow - $last_time) % $tmp;
return false;
}
$this->cur_requests = $max_requests - $token_left;
$cache->set($key, $micronow, $duration);
$cache->set($assist_key, $token_left - 1, $duration);
return true;
}
}