本文目录导读:

- 核心思路
- 方案一:基于数据库 + 用户表(适合中小项目)
- 方案二:使用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方案,能极大减轻服务器负载和带宽成本。