From 978ae545e1e34c409a301c4d8579eca41518e6c3 Mon Sep 17 00:00:00 2001 From: yuyx <237899745@qq.com> Date: Tue, 17 Feb 2026 19:25:39 +0800 Subject: [PATCH] feat: make zero download quota block downloads and use -1 for unlimited --- backend/database.js | 22 ++++++++++++++++------ backend/server.js | 28 +++++++++++++++++----------- frontend/app.html | 12 ++++++------ frontend/app.js | 30 +++++++++++++++++++----------- 4 files changed, 58 insertions(+), 34 deletions(-) diff --git a/backend/database.js b/backend/database.js index d86d113..010c049 100644 --- a/backend/database.js +++ b/backend/database.js @@ -1426,17 +1426,27 @@ function migrateDownloadTrafficFields() { console.log('[数据库迁移] ✓ download_traffic_last_reset_at 字段已添加'); } - // 统一策略:download_traffic_quota <= 0 表示不限流量 + // 统一策略:download_traffic_quota 为空时回填为 0(0 表示禁止下载,-1 表示不限流量) const quotaBackfillResult = db.prepare(` UPDATE users SET download_traffic_quota = 0 - WHERE download_traffic_quota IS NULL OR download_traffic_quota < 0 + WHERE download_traffic_quota IS NULL `).run(); if (quotaBackfillResult.changes > 0) { console.log(`[数据库迁移] ✓ 下载流量配额默认值已回填: ${quotaBackfillResult.changes} 条记录`); } + const quotaUnlimitedNormalizeResult = db.prepare(` + UPDATE users + SET download_traffic_quota = -1 + WHERE download_traffic_quota < 0 + `).run(); + + if (quotaUnlimitedNormalizeResult.changes > 0) { + console.log(`[数据库迁移] ✓ 不限流量配额已标准化为 -1: ${quotaUnlimitedNormalizeResult.changes} 条记录`); + } + const usedBackfillResult = db.prepare(` UPDATE users SET download_traffic_used = 0 @@ -1450,7 +1460,7 @@ function migrateDownloadTrafficFields() { const usedCapResult = db.prepare(` UPDATE users SET download_traffic_used = download_traffic_quota - WHERE download_traffic_quota > 0 AND download_traffic_used > download_traffic_quota + WHERE download_traffic_quota >= 0 AND download_traffic_used > download_traffic_quota `).run(); if (usedCapResult.changes > 0) { @@ -1468,14 +1478,14 @@ function migrateDownloadTrafficFields() { console.log(`[数据库迁移] ✓ 下载流量重置周期已回填: ${resetCycleBackfillResult.changes} 条记录`); } - const clearExpiryForUnlimitedResult = db.prepare(` + const clearExpiryForNonPositiveQuotaResult = db.prepare(` UPDATE users SET download_traffic_quota_expires_at = NULL WHERE download_traffic_quota <= 0 `).run(); - if (clearExpiryForUnlimitedResult.changes > 0) { - console.log(`[数据库迁移] ✓ 不限流量用户已清理到期时间: ${clearExpiryForUnlimitedResult.changes} 条记录`); + if (clearExpiryForNonPositiveQuotaResult.changes > 0) { + console.log(`[数据库迁移] ✓ 非正下载配额用户已清理到期时间: ${clearExpiryForNonPositiveQuotaResult.changes} 条记录`); } } catch (error) { console.error('[数据库迁移] 下载流量字段迁移失败:', error); diff --git a/backend/server.js b/backend/server.js index e182c50..bca76b1 100644 --- a/backend/server.js +++ b/backend/server.js @@ -650,8 +650,11 @@ function normalizeOssQuota(rawQuota) { function normalizeDownloadTrafficQuota(rawQuota) { const parsedQuota = Number(rawQuota); - if (!Number.isFinite(parsedQuota) || parsedQuota <= 0) { - return 0; // 0 表示不限流量 + if (!Number.isFinite(parsedQuota)) { + return 0; // 0 表示禁止下载 + } + if (parsedQuota < 0) { + return -1; // -1 表示不限流量 } return Math.min(MAX_DOWNLOAD_TRAFFIC_BYTES, Math.floor(parsedQuota)); } @@ -661,7 +664,7 @@ function normalizeDownloadTrafficUsed(rawUsed, quota = 0) { const normalizedUsed = Number.isFinite(parsedUsed) && parsedUsed > 0 ? Math.floor(parsedUsed) : 0; - if (quota > 0) { + if (quota >= 0) { return Math.min(normalizedUsed, quota); } return normalizedUsed; @@ -670,11 +673,12 @@ function normalizeDownloadTrafficUsed(rawUsed, quota = 0) { function getDownloadTrafficState(user) { const quota = normalizeDownloadTrafficQuota(user?.download_traffic_quota); const used = normalizeDownloadTrafficUsed(user?.download_traffic_used, quota); + const isUnlimited = quota < 0; return { quota, used, - isUnlimited: quota <= 0, - remaining: quota > 0 ? Math.max(0, quota - used) : Number.POSITIVE_INFINITY + isUnlimited, + remaining: isUnlimited ? Number.POSITIVE_INFINITY : Math.max(0, quota - used) }; } @@ -3077,11 +3081,13 @@ app.get('/api/user/download-traffic-report', authMiddleware, (req, res) => { return peak; }, null); - const safeQuota = trafficState.quota > 0 ? trafficState.quota : 0; - const remainingBytes = safeQuota > 0 ? Math.max(0, safeQuota - trafficState.used) : null; - const usagePercentage = safeQuota > 0 - ? Math.min(100, Math.round((trafficState.used / safeQuota) * 100)) - : null; + const safeQuota = trafficState.isUnlimited ? 0 : Math.max(0, trafficState.quota); + const remainingBytes = trafficState.isUnlimited ? null : Math.max(0, safeQuota - trafficState.used); + const usagePercentage = trafficState.isUnlimited + ? null + : (safeQuota > 0 + ? Math.min(100, Math.round((trafficState.used / safeQuota) * 100)) + : 100); res.json({ success: true, @@ -7602,7 +7608,7 @@ app.post('/api/admin/users/:id/storage-permission', body('storage_permission').isIn(['local_only', 'oss_only', 'user_choice']).withMessage('无效的存储权限'), body('local_storage_quota').optional({ nullable: true }).isInt({ min: 1048576, max: 10995116277760 }).withMessage('本地配额必须在 1MB 到 10TB 之间'), body('oss_storage_quota').optional({ nullable: true }).isInt({ min: 1048576, max: 10995116277760 }).withMessage('OSS配额必须在 1MB 到 10TB 之间'), - body('download_traffic_quota').optional({ nullable: true }).isInt({ min: 0, max: MAX_DOWNLOAD_TRAFFIC_BYTES }).withMessage('下载流量配额必须在 0 到 10TB 之间(0表示不限)'), + body('download_traffic_quota').optional({ nullable: true }).isInt({ min: -1, max: MAX_DOWNLOAD_TRAFFIC_BYTES }).withMessage('下载流量配额必须在 -1 到 10TB 之间(-1表示不限,0表示禁止下载)'), body('download_traffic_delta').optional({ nullable: true }).isInt({ min: -MAX_DOWNLOAD_TRAFFIC_BYTES, max: MAX_DOWNLOAD_TRAFFIC_BYTES }).withMessage('下载流量增减值必须在 -10TB 到 10TB 之间'), body('download_traffic_reset_cycle').optional({ nullable: true }).isIn(['none', 'daily', 'weekly', 'monthly']).withMessage('下载流量重置周期无效'), body('download_traffic_quota_expires_at').optional({ nullable: true }).custom((value) => { diff --git a/frontend/app.html b/frontend/app.html index 766afb0..cd2992e 100644 --- a/frontend/app.html +++ b/frontend/app.html @@ -3335,10 +3335,10 @@ -
+
{{ formatBytes(u.download_traffic_used || 0) }} / {{ formatBytes(u.download_traffic_quota) }}
- {{ getAdminUserDownloadQuotaPercentage(u) }}% + {{ u.download_traffic_quota === 0 ? '已禁用下载' : (getAdminUserDownloadQuotaPercentage(u) + '%') }}
@@ -3709,7 +3709,7 @@
@@ -3717,7 +3717,7 @@ type="number" class="form-input" v-model.number="editStorageForm.download_traffic_quota_value" - min="1" + min="0" max="10240" step="1" style="flex: 1;"> @@ -3746,7 +3746,7 @@
- 下载流量支持直接设置、增减操作,范围: 不限 或 1MB - 10TB + 下载流量支持直接设置、增减操作,范围: 0B - 10TB;勾选“不限流量(-1)”表示不限制
@@ -3754,7 +3754,7 @@ - 到期后自动恢复为不限流量并清零已用流量;留空表示永不过期 + 到期后自动恢复为 0 并清零已用流量;留空表示永不过期
diff --git a/frontend/app.js b/frontend/app.js index 80b1500..eef63bb 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -274,7 +274,7 @@ createApp({ oss_quota_unlimited: false, // 兼容旧数据字段(当前固定为有限配额) download_traffic_quota_value: 1, // 下载流量配额数值 download_quota_unit: 'GB', // 下载流量单位:MB / GB / TB - download_quota_unlimited: true, // 下载流量:true=不限 + download_quota_unlimited: false, // 下载流量:true=不限(后端值为 -1) download_traffic_used: 0, // 下载流量已使用(字节) download_quota_operation: 'set', // set/increase/decrease download_quota_adjust_value: 1, // 增减额度数值 @@ -407,7 +407,8 @@ createApp({ if (this.downloadTrafficReport?.quota?.is_unlimited === true) { return true; } - return this.downloadTrafficQuotaBytes <= 0; + const userQuota = Number(this.user?.download_traffic_quota); + return Number.isFinite(userQuota) && userQuota < 0; }, downloadTrafficRemainingBytes() { @@ -434,7 +435,7 @@ createApp({ } if (this.downloadTrafficQuotaBytes <= 0) { - return 0; + return 100; } return Math.min(100, Math.round((this.downloadTrafficUsedBytes / this.downloadTrafficQuotaBytes) * 100)); @@ -3120,8 +3121,9 @@ handleDragLeave(e) { this.editStorageForm.oss_quota_unit = 'MB'; } - // 下载流量配额(0 表示不限) + // 下载流量配额(-1 表示不限,0 表示禁止下载) const downloadQuotaBytes = Number(user.download_traffic_quota || 0); + const isDownloadUnlimited = Number.isFinite(downloadQuotaBytes) && downloadQuotaBytes < 0; const effectiveDownloadQuotaBytes = Number.isFinite(downloadQuotaBytes) && downloadQuotaBytes > 0 ? downloadQuotaBytes : 0; @@ -3137,10 +3139,13 @@ handleDragLeave(e) { this.editStorageForm.download_quota_expires_at = this.toDateTimeLocalInput(user.download_traffic_quota_expires_at); this.editStorageForm.reset_download_used_now = false; - this.editStorageForm.download_quota_unlimited = effectiveDownloadQuotaBytes <= 0; - if (effectiveDownloadQuotaBytes <= 0) { + this.editStorageForm.download_quota_unlimited = isDownloadUnlimited; + if (isDownloadUnlimited) { this.editStorageForm.download_traffic_quota_value = 1; this.editStorageForm.download_quota_unit = 'GB'; + } else if (effectiveDownloadQuotaBytes <= 0) { + this.editStorageForm.download_traffic_quota_value = 0; + this.editStorageForm.download_quota_unit = 'MB'; } else if (effectiveDownloadQuotaBytes >= tb && effectiveDownloadQuotaBytes % tb === 0) { this.editStorageForm.download_traffic_quota_value = effectiveDownloadQuotaBytes / tb; this.editStorageForm.download_quota_unit = 'TB'; @@ -3196,10 +3201,11 @@ handleDragLeave(e) { let downloadQuotaBytes = null; let downloadTrafficDelta = null; if (this.editStorageForm.download_quota_operation === 'set') { - downloadQuotaBytes = 0; - if (!this.editStorageForm.download_quota_unlimited) { - if (!this.editStorageForm.download_traffic_quota_value || this.editStorageForm.download_traffic_quota_value < 1) { - this.showToast('error', '参数错误', '下载流量配额必须大于 0,或选择不限流量'); + if (this.editStorageForm.download_quota_unlimited) { + downloadQuotaBytes = -1; + } else { + if (this.editStorageForm.download_traffic_quota_value === null || this.editStorageForm.download_traffic_quota_value === undefined || this.editStorageForm.download_traffic_quota_value < 0) { + this.showToast('error', '参数错误', '下载流量配额必须大于等于 0,或选择不限流量'); return; } downloadQuotaBytes = toBytes( @@ -3287,7 +3293,9 @@ handleDragLeave(e) { getAdminUserDownloadQuotaPercentage(user) { const quota = Number(user?.download_traffic_quota || 0); const used = Number(user?.download_traffic_used || 0); - if (!Number.isFinite(quota) || quota <= 0) return 0; + if (!Number.isFinite(quota)) return 0; + if (quota < 0) return 0; + if (quota === 0) return 100; if (!Number.isFinite(used) || used <= 0) return 0; return Math.min(100, Math.round((used / quota) * 100)); },