PHP项目流量统计如何限制用户下载额度

wen PHP项目 30

本文目录导读:

PHP项目流量统计如何限制用户下载额度

  1. 核心思路
  2. 方案一:基于数据库 + 用户表(适合中小项目)
  3. 方案二:使用Redis + 计数器(适合高并发)
  4. 方案三:使用令牌桶算法(限流 + 额度)
  5. 方案四:云存储/CDN预签名URL(推荐生产环境)
  6. 方案五:限制用户IP + 未登录用户(防刷)
  7. 额度检查的执行时机
  8. 建议选择

针对PHP项目的流量统计和用户下载额度限制,通常需要结合用户认证流量计数中间件拦截来实现,以下是几种常见的实现方案,从简单到复杂,可以根据项目规模选择。


核心思路

  1. 记录用户下载行为:用户每次下载时,记录下载流量(文件大小)或下载次数。
  2. 设置额度规则:如“每日1GB”、“每月100次下载”等。
  3. 在下载前拦截检查:若超出额度,拒绝下载并提示。

基于数据库 + 用户表(适合中小项目)

数据库设计

-- 用户表增加额度字段(按需选择)
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);
// 实际下载文件...

优化点

  • 使用incrByLua脚本确保检查+扣费的原子性。
  • 避免用户在短时间内绕过检查(可配合令牌桶算法)。

使用令牌桶算法(限流 + 额度)

适合需要控制“瞬时下载速度”的场景,用户可以下载总量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的有效时间下载次数

步骤:

  1. 上传文件到OSS,生成私有访问权限。
  2. PHP生成预签名URL,并在URL中嵌入有效期最大下载次数(部分服务支持)。
  3. 前端/用户点击下载时,获取该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方案,能极大减轻服务器负载和带宽成本。

抱歉,评论功能暂时关闭!