本文目录导读:

- 核心思路
- 方案一:基于数据库 + 用户表(适合中小项目)
- 方案二:使用Redis + 计数器(适合高并发)
- 方案三:使用令牌桶算法(限流 + 额度)
- 方案四:云存储/CDN预签名URL(推荐生产环境)
- 方案五:限制用户IP + 未登录用户(防刷)
- 额度检查的执行时机
- 建议选择
针对PHP项目的流量统计和用户下载额度限制,通常需要结合用户认证、流量计数和中间件拦截来实现,以下是几种常见的实现方案,从简单到复杂,可以根据项目规模选择。
核心思路
- 记录用户下载行为:用户每次下载时,记录下载流量(文件大小)或下载次数。
- 设置额度规则:如“每日1GB”、“每月100次下载”等。
- 在下载前拦截检查:若超出额度,拒绝下载并提示。
基于数据库 + 用户表(适合中小项目)
数据库设计
-- 用户表增加额度字段(按需选择) ALTER TABLE users ADD COLUMN daily_download_limit BIGINT DEFAULT 1073741824; -- 1GB 字节 ALTER TABLE users ADD COLUMN monthly_download_limit BIGINT DEFAULT 10737418240; -- 10GB ALTER TABLE users ADD COLUMN today_downloaded BIGINT DEFAULT 0; ALTER TABLE users ADD COLUMN month_downloaded BIGINT DEFAULT 0; ALTER TABLE users ADD COLUMN last_download_date DATE NULL;
下载流量记录中间件(PHP)
<?php
// download.php
session_start();
require_once 'db.php';
$userId = $_SESSION['user_id'];
$fileSize = $_GET['file_size']; // 实际应通过真实文件获取
$filePath = $_GET['file_path'];
// 1. 获取用户当前额度状态
$stmt = $pdo->prepare("SELECT daily_download_limit, monthly_download_limit, today_downloaded, month_downloaded, last_download_date FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch();
// 2. 检查日期是否变更,重置日/月统计
$today = date('Y-m-d');
$currentMonth = date('Y-m');
if ($user['last_download_date'] !== $today) {
// 重置日统计
$pdo->prepare("UPDATE users SET today_downloaded = 0, last_download_date = ? WHERE id = ?")->execute([$today, $userId]);
$user['today_downloaded'] = 0;
}
// 3. 检查额度
if (($user['today_downloaded'] + $fileSize) > $user['daily_download_limit']) {
die('今日下载流量已用完,请明日再试');
}
if (($user['month_downloaded'] + $fileSize) > $user['monthly_download_limit']) {
die('本月下载流量已用完');
}
// 4. 扣除额度(事务保护)
$pdo->beginTransaction();
try {
$pdo->prepare("UPDATE users SET today_downloaded = today_downloaded + ?, month_downloaded = month_downloaded + ? WHERE id = ?")
->execute([$fileSize, $fileSize, $userId]);
// 5. 发送文件(伪代码)
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($filePath) . '"');
header('Content-Length: ' . $fileSize);
readfile($filePath);
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
die('下载失败');
}
优缺点
- 优点:实现简单,流量控制精确到字节。
- 缺点:高并发下数据库压力大,且未能防止用户绕过(直接URL访问)。
使用Redis + 计数器(适合高并发)
利用Redis的原子计数和过期时间特性,实现更高效的流量限制。
Redis键设计
用户每日下载量:user:daily:{userId}:2025-04-01
用户月下载量:user:monthly:{userId}:2025-04
PHP代码示例
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$userId = 123;
$fileSize = 50000000; // 50MB 字节
$dailyLimit = 1073741824; // 1GB
$monthlyLimit = 10737418240; // 10GB
// 日流量键(自动过期)
$dailyKey = "user:daily:{$userId}:" . date('Y-m-d');
$redis->setNx($dailyKey, 0);
$redis->expire($dailyKey, 86400); // 24小时
// 月流量键
$monthlyKey = "user:monthly:{$userId}:" . date('Y-m');
$redis->setNx($monthlyKey, 0);
$redis->expire($monthlyKey, 86400 * 30);
// 原子检查 + 扣费
$dailyUsed = $redis->get($dailyKey);
$monthlyUsed = $redis->get($monthlyKey);
if (($dailyUsed + $fileSize) > $dailyLimit) {
die('每日下载超限');
}
if (($monthlyUsed + $fileSize) > $monthlyLimit) {
die('每月下载超限');
}
// 原子递增
$redis->incrBy($dailyKey, $fileSize);
$redis->incrBy($monthlyKey, $fileSize);
// 实际下载文件...
优化点
- 使用
incrBy与Lua脚本确保检查+扣费的原子性。 - 避免用户在短时间内绕过检查(可配合令牌桶算法)。
使用令牌桶算法(限流 + 额度)
适合需要控制“瞬时下载速度”的场景,用户可以下载总量1GB,但每分钟只能下载50MB。
PHP实现(基于Redis)
<?php
class TokenBucket {
private $redis;
private $key;
private $capacity; // 桶容量(总字节)
private $rate; // 速率(每秒字节)
public function __construct($redis, $userId, $capacity, $rate) {
$this->redis = $redis;
$this->key = "bucket:{$userId}";
$this->capacity = $capacity;
$this->rate = $rate;
}
public function consume($tokens) {
$lua = <<<SCRIPT
local key = KEYS[1]
local now = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local rate = tonumber(ARGV[3])
local tokens = tonumber(ARGV[4])
-- 获取上次更新时间与当前令牌数
local last_time = redis.call('HGET', key, 'last_time') or 0
local current_tokens = redis.call('HGET', key, 'tokens') or capacity
-- 计算需补充的令牌
local elapsed = now - last_time
current_tokens = math.min(capacity, current_tokens + elapsed * rate)
-- 检查是否足够
if current_tokens >= tokens then
current_tokens = current_tokens - tokens
redis.call('HMSET', key, 'tokens', current_tokens, 'last_time', now)
return 1
else
return 0
end
SCRIPT;
return $this->redis->eval($lua, [$this->key, time(), $this->capacity, $this->rate, $tokens], 1);
}
}
// 使用
$bucket = new TokenBucket($redis, $userId, 1073741824, 1048576); // 总1GB,每秒补充1MB
if (!$bucket->consume($fileSize)) {
die('流量不足,请稍后重试');
}
云存储/CDN预签名URL(推荐生产环境)
使用阿里云OSS、腾讯云COS等云存储服务,通过生成预签名URL并限制URL的有效时间和下载次数。
步骤:
- 上传文件到OSS,生成私有访问权限。
- PHP生成预签名URL,并在URL中嵌入有效期和最大下载次数(部分服务支持)。
- 前端/用户点击下载时,获取该URL,直接跳转到云存储下载。
示例(阿里云OSS):
<?php use OSS\OssClient; $ossClient = new OssClient($accessKeyId, $accessKeySecret, $endpoint); $expireTime = 3600; // 1小时有效期 // 生成签名URL(支持通过Callback回调记录下载次数) $url = $ossClient->signUrl($bucket, $object, $expireTime, 'GET'); // 在下载前检查用户额度 // 下载完成后,OSS可通过回调接口通知你的服务器扣除额度
优点:
- 服务端不直接传输文件,大幅降低服务器带宽压力。
- 额度控制与文件传输分离,更安全、易扩展。
- 支持CDN加速,提升下载速度。
限制用户IP + 未登录用户(防刷)
对于匿名用户,可以通过IP + UserAgent + Session组合限制。
<?php
$ip = $_SERVER['REMOTE_ADDR'];
$ua = $_SERVER['HTTP_USER_AGENT'];
$key = "download_limit:ip:{$ip}:" . date('Y-m-d');
$dailyDownload = $redis->incr($key);
$redis->expire($key, 86400);
if ($dailyDownload > 10) { // 每天最多10次
die('下载次数过多,请登录后继续');
}
额度检查的执行时机
最好在下载请求的入口处(如中间件、路由拦截)统一执行,而非每个文件下载脚本中重复编写,可以封装成以下结构:
// middleware/checkDownloadQuota.php
function checkDownloadQuota($userId, $fileSize) {
// 执行检查逻辑,失败抛出异常
}
// 在 download.php 或控制器中
try {
checkDownloadQuota($userId, $fileSize);
// 执行下载...
} catch (QuotaExceededException $e) {
http_response_code(429);
echo json_encode(['error' => $e->getMessage()]);
}
建议选择
| 方案 | 适用场景 | 复杂度 |
|---|---|---|
| 数据库记录 | 个人项目、低并发 | 低 |
| Redis计数 | 中等及以上并发 | 中 |
| 令牌桶 | 需控制下载速率 | 中高 |
| 云存储预签名URL | 生产环境、大文件、防滥用 | 高(但最可靠) |
- 核心:下载前先检查额度,原子扣减,防止并发超卖。
- 持久化:Redis方案要定期同步到数据库,避免重启丢失。
- 安全性:预签名URL + 文件权限设置(不直接暴露真实下载路径)。
- 用户体验:超出额度时返回友好提示,并提供“升级套餐”或“明日可用”等信息。
如果项目使用云服务,强烈推荐云存储预签名URL方案,能极大减轻服务器负载和带宽成本。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播