本文目录导读:

在 PHP 项目中实现网盘容量统计与用户配额限制,通常需要从以下几个步骤入手:
- 数据库设计(存储配额与已用空间)
- 上传时实时统计(计算文件大小并累加)
- 配额校验逻辑(上传前检查是否超限)
- 删除时实时扣减(释放空间)
- 性能优化(避免每次查询都全表扫描)
下面是一套标准化的实现方案:
数据库表设计
需要在用户表中新增字段,或者创建独立的 user_quota 表。
方案 A:用户表直接扩展(推荐用户量 < 10万)
ALTER TABLE `users` ADD COLUMN `quota_total` BIGINT DEFAULT 1073741824 COMMENT '总配额,单位字节,默认1GB', ADD COLUMN `quota_used` BIGINT DEFAULT 0 COMMENT '已用空间,单位字节', ADD COLUMN `quota_files` INT DEFAULT 0 COMMENT '文件总数(可选限制)';
方案 B:独立配额表(适合 SaaS 或动态套餐)
CREATE TABLE `user_quotas` (
`user_id` INT PRIMARY KEY,
`total_bytes` BIGINT NOT NULL DEFAULT 1073741824,
`used_bytes` BIGINT NOT NULL DEFAULT 0,
`file_count` INT NOT NULL DEFAULT 0,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
上传文件前的配额校验(核心逻辑)
/**
* 检查用户配额是否充足
* @param int $userId
* @param int $fileSize 单位字节
* @param int $fileCount 本次上传文件数(通常为1)
* @return bool
*/
public function checkQuota(int $userId, int $fileSize, int $fileCount = 1): bool
{
// 从数据库或缓存获取用户配额信息
$user = $this->getUserQuota($userId);
// 检查总空间
if (($user['quota_used'] + $fileSize) > $user['quota_total']) {
throw new \Exception('存储空间不足');
}
// 检查文件数量(如果有限制)
if ($user['quota_files'] !== null) {
if (($user['file_count'] + $fileCount) > $user['quota_files']) {
throw new \Exception('文件数量已达上限');
}
}
return true;
}
何时调用:在 move_uploaded_file() 或写入云存储 SDK 之前。
上传成功后的空间更新
使用 悲观锁 或 原子操作 防止并发问题。
// 使用 MySQL UPDATE ... SET used_bytes = used_bytes + ? WHERE id = ? AND used_bytes + ? <= total_bytes
// 如果影响行数为0,说明配额超限,需要回滚已上传的文件
public function incrementQuota(int $userId, int $fileSize, int $fileCount = 1): bool
{
$sql = "UPDATE users
SET quota_used = quota_used + :size,
file_count = file_count + :count
WHERE id = :uid
AND (quota_used + :checkSize) <= quota_total";
// 如果使用了文件数量限制,还需要加 file_count 条件
$stmt = $this->db->prepare($sql);
$stmt->execute([
':size' => $fileSize,
':count' => $fileCount,
':uid' => $userId,
':checkSize' => $fileSize
]);
return $stmt->rowCount() > 0;
}
任务队列(T+1 修正)
由于网络波动或程序异常可能导致 quota_used 不准确,建议增加 定时校正任务:
// 每天凌晨执行
$sql = "UPDATE users u
LEFT JOIN (
SELECT user_id, COALESCE(SUM(file_size), 0) as real_used, COUNT(*) as real_count
FROM files
WHERE is_deleted = 0
GROUP BY user_id
) f ON u.id = f.user_id
SET u.quota_used = COALESCE(f.real_used, 0),
u.file_count = COALESCE(f.real_count, 0)";
多节点部署的并发安全
如果应用部署在多台服务器,推荐使用 Redis 分布式锁:
// 仅在配额增减时加锁
$lockKey = "quota_lock:{$userId}";
$lock = $redis->set($lockKey, 1, ['nx', 'ex' => 2]); // 2秒超时
if (!$lock) {
throw new \Exception('系统繁忙,请稍后重试');
}
try {
// 执行配额检查和更新
// 写入文件记录到数据库
} finally {
$redis->del($lockKey);
}
前端展示友好的容量信息
后端提供接口:
public function getQuotaInfo(int $userId): array
{
$user = $this->getUserQuota($userId);
return [
'total' => $user['quota_total'],
'used' => $user['quota_used'],
'percent' => $user['quota_total'] > 0
? round(($user['quota_used'] / $user['quota_total']) * 100, 2)
: 0,
// 格式化为人类可读
'total_formatted' => $this->formatBytes($user['quota_total']),
'used_formatted' => $this->formatBytes($user['quota_used']),
'free_formatted' => $this->formatBytes($user['quota_total'] - $user['quota_used']),
];
}
private function formatBytes(int $bytes, int $precision = 2): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, $precision) . ' ' . $units[$pow];
}
删除/回收站机制的特殊处理
如果支持回收站,建议:
- 移到回收站:不立即扣减配额,文件标记为
is_deleted = 1 - 永久删除/清空回收站:扣减配额
- 从回收站恢复:检查配额是否够用,如果不够提示用户清理
// 永久删除文件时
public function decrementQuota(int $userId, int $fileSize, int $fileCount = 1): void
{
$sql = "UPDATE users
SET quota_used = GREATEST(quota_used - :size, 0),
file_count = GREATEST(file_count - :count, 0)
WHERE id = :uid";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':size' => $fileSize,
':count' => $fileCount,
':uid' => $userId
]);
}
性能优化建议
- 缓存配额信息:将
quota_total和quota_used缓存到 Redis,减少数据库查询。 - 批量操作场景:Zip 下载、批量移动等操作,建议在操作完成后统一更新配额。
- 大文件场景:超过 100MB 的文件,使用 分片上传 ,每次上传分片时检查配额,避免最后合并时发现空间不够。
总结流程
用户上传请求
│
▼
检查配额 (数据库/缓存)
│ └── 不足 → 返回错误提示
▼
执行文件上传 (本地/OSS)
│ └── 失败 → 终止,不更新配额
▼
写入文件记录 (数据库)
│
▼
原子更新 quota_used += 文件大小
│ └── 更新失败(行锁) → 回滚文件,提示重试
▼
返回成功
这套方案在大多数 PHP 网盘项目(如 Nextcloud、自建企业网盘、SaaS 文件管理)中均适用。