feat: make zero download quota block downloads and use -1 for unlimited
This commit is contained in:
@@ -1426,17 +1426,27 @@ function migrateDownloadTrafficFields() {
|
|||||||
console.log('[数据库迁移] ✓ download_traffic_last_reset_at 字段已添加');
|
console.log('[数据库迁移] ✓ download_traffic_last_reset_at 字段已添加');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统一策略:download_traffic_quota <= 0 表示不限流量
|
// 统一策略:download_traffic_quota 为空时回填为 0(0 表示禁止下载,-1 表示不限流量)
|
||||||
const quotaBackfillResult = db.prepare(`
|
const quotaBackfillResult = db.prepare(`
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET download_traffic_quota = 0
|
SET download_traffic_quota = 0
|
||||||
WHERE download_traffic_quota IS NULL OR download_traffic_quota < 0
|
WHERE download_traffic_quota IS NULL
|
||||||
`).run();
|
`).run();
|
||||||
|
|
||||||
if (quotaBackfillResult.changes > 0) {
|
if (quotaBackfillResult.changes > 0) {
|
||||||
console.log(`[数据库迁移] ✓ 下载流量配额默认值已回填: ${quotaBackfillResult.changes} 条记录`);
|
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(`
|
const usedBackfillResult = db.prepare(`
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET download_traffic_used = 0
|
SET download_traffic_used = 0
|
||||||
@@ -1450,7 +1460,7 @@ function migrateDownloadTrafficFields() {
|
|||||||
const usedCapResult = db.prepare(`
|
const usedCapResult = db.prepare(`
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET download_traffic_used = download_traffic_quota
|
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();
|
`).run();
|
||||||
|
|
||||||
if (usedCapResult.changes > 0) {
|
if (usedCapResult.changes > 0) {
|
||||||
@@ -1468,14 +1478,14 @@ function migrateDownloadTrafficFields() {
|
|||||||
console.log(`[数据库迁移] ✓ 下载流量重置周期已回填: ${resetCycleBackfillResult.changes} 条记录`);
|
console.log(`[数据库迁移] ✓ 下载流量重置周期已回填: ${resetCycleBackfillResult.changes} 条记录`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearExpiryForUnlimitedResult = db.prepare(`
|
const clearExpiryForNonPositiveQuotaResult = db.prepare(`
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET download_traffic_quota_expires_at = NULL
|
SET download_traffic_quota_expires_at = NULL
|
||||||
WHERE download_traffic_quota <= 0
|
WHERE download_traffic_quota <= 0
|
||||||
`).run();
|
`).run();
|
||||||
|
|
||||||
if (clearExpiryForUnlimitedResult.changes > 0) {
|
if (clearExpiryForNonPositiveQuotaResult.changes > 0) {
|
||||||
console.log(`[数据库迁移] ✓ 不限流量用户已清理到期时间: ${clearExpiryForUnlimitedResult.changes} 条记录`);
|
console.log(`[数据库迁移] ✓ 非正下载配额用户已清理到期时间: ${clearExpiryForNonPositiveQuotaResult.changes} 条记录`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[数据库迁移] 下载流量字段迁移失败:', error);
|
console.error('[数据库迁移] 下载流量字段迁移失败:', error);
|
||||||
|
|||||||
@@ -650,8 +650,11 @@ function normalizeOssQuota(rawQuota) {
|
|||||||
|
|
||||||
function normalizeDownloadTrafficQuota(rawQuota) {
|
function normalizeDownloadTrafficQuota(rawQuota) {
|
||||||
const parsedQuota = Number(rawQuota);
|
const parsedQuota = Number(rawQuota);
|
||||||
if (!Number.isFinite(parsedQuota) || parsedQuota <= 0) {
|
if (!Number.isFinite(parsedQuota)) {
|
||||||
return 0; // 0 表示不限流量
|
return 0; // 0 表示禁止下载
|
||||||
|
}
|
||||||
|
if (parsedQuota < 0) {
|
||||||
|
return -1; // -1 表示不限流量
|
||||||
}
|
}
|
||||||
return Math.min(MAX_DOWNLOAD_TRAFFIC_BYTES, Math.floor(parsedQuota));
|
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
|
const normalizedUsed = Number.isFinite(parsedUsed) && parsedUsed > 0
|
||||||
? Math.floor(parsedUsed)
|
? Math.floor(parsedUsed)
|
||||||
: 0;
|
: 0;
|
||||||
if (quota > 0) {
|
if (quota >= 0) {
|
||||||
return Math.min(normalizedUsed, quota);
|
return Math.min(normalizedUsed, quota);
|
||||||
}
|
}
|
||||||
return normalizedUsed;
|
return normalizedUsed;
|
||||||
@@ -670,11 +673,12 @@ function normalizeDownloadTrafficUsed(rawUsed, quota = 0) {
|
|||||||
function getDownloadTrafficState(user) {
|
function getDownloadTrafficState(user) {
|
||||||
const quota = normalizeDownloadTrafficQuota(user?.download_traffic_quota);
|
const quota = normalizeDownloadTrafficQuota(user?.download_traffic_quota);
|
||||||
const used = normalizeDownloadTrafficUsed(user?.download_traffic_used, quota);
|
const used = normalizeDownloadTrafficUsed(user?.download_traffic_used, quota);
|
||||||
|
const isUnlimited = quota < 0;
|
||||||
return {
|
return {
|
||||||
quota,
|
quota,
|
||||||
used,
|
used,
|
||||||
isUnlimited: quota <= 0,
|
isUnlimited,
|
||||||
remaining: quota > 0 ? Math.max(0, quota - used) : Number.POSITIVE_INFINITY
|
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;
|
return peak;
|
||||||
}, null);
|
}, null);
|
||||||
|
|
||||||
const safeQuota = trafficState.quota > 0 ? trafficState.quota : 0;
|
const safeQuota = trafficState.isUnlimited ? 0 : Math.max(0, trafficState.quota);
|
||||||
const remainingBytes = safeQuota > 0 ? Math.max(0, safeQuota - trafficState.used) : null;
|
const remainingBytes = trafficState.isUnlimited ? null : Math.max(0, safeQuota - trafficState.used);
|
||||||
const usagePercentage = safeQuota > 0
|
const usagePercentage = trafficState.isUnlimited
|
||||||
? Math.min(100, Math.round((trafficState.used / safeQuota) * 100))
|
? null
|
||||||
: null;
|
: (safeQuota > 0
|
||||||
|
? Math.min(100, Math.round((trafficState.used / safeQuota) * 100))
|
||||||
|
: 100);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
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('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('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('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_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_reset_cycle').optional({ nullable: true }).isIn(['none', 'daily', 'weekly', 'monthly']).withMessage('下载流量重置周期无效'),
|
||||||
body('download_traffic_quota_expires_at').optional({ nullable: true }).custom((value) => {
|
body('download_traffic_quota_expires_at').optional({ nullable: true }).custom((value) => {
|
||||||
|
|||||||
@@ -3335,10 +3335,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
||||||
<div v-if="u.download_traffic_quota > 0">
|
<div v-if="u.download_traffic_quota >= 0">
|
||||||
<div>{{ formatBytes(u.download_traffic_used || 0) }} / {{ formatBytes(u.download_traffic_quota) }}</div>
|
<div>{{ formatBytes(u.download_traffic_used || 0) }} / {{ formatBytes(u.download_traffic_quota) }}</div>
|
||||||
<div style="font-size: 11px; color: var(--text-muted);">
|
<div style="font-size: 11px; color: var(--text-muted);">
|
||||||
{{ getAdminUserDownloadQuotaPercentage(u) }}%
|
{{ u.download_traffic_quota === 0 ? '已禁用下载' : (getAdminUserDownloadQuotaPercentage(u) + '%') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
@@ -3709,7 +3709,7 @@
|
|||||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
|
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
|
||||||
<label style="display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-secondary);">
|
<label style="display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-secondary);">
|
||||||
<input type="checkbox" v-model="editStorageForm.download_quota_unlimited">
|
<input type="checkbox" v-model="editStorageForm.download_quota_unlimited">
|
||||||
不限流量
|
不限流量(-1)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!editStorageForm.download_quota_unlimited" style="display: flex; gap: 10px;">
|
<div v-if="!editStorageForm.download_quota_unlimited" style="display: flex; gap: 10px;">
|
||||||
@@ -3717,7 +3717,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
v-model.number="editStorageForm.download_traffic_quota_value"
|
v-model.number="editStorageForm.download_traffic_quota_value"
|
||||||
min="1"
|
min="0"
|
||||||
max="10240"
|
max="10240"
|
||||||
step="1"
|
step="1"
|
||||||
style="flex: 1;">
|
style="flex: 1;">
|
||||||
@@ -3746,7 +3746,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
|
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
|
||||||
下载流量支持直接设置、增减操作,范围: 不限 或 1MB - 10TB
|
下载流量支持直接设置、增减操作,范围: 0B - 10TB;勾选“不限流量(-1)”表示不限制
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -3754,7 +3754,7 @@
|
|||||||
<label class="form-label">下载流量到期时间(可选)</label>
|
<label class="form-label">下载流量到期时间(可选)</label>
|
||||||
<input type="datetime-local" class="form-input" v-model="editStorageForm.download_quota_expires_at">
|
<input type="datetime-local" class="form-input" v-model="editStorageForm.download_quota_expires_at">
|
||||||
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
|
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
|
||||||
到期后自动恢复为不限流量并清零已用流量;留空表示永不过期
|
到期后自动恢复为 0 并清零已用流量;留空表示永不过期
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ createApp({
|
|||||||
oss_quota_unlimited: false, // 兼容旧数据字段(当前固定为有限配额)
|
oss_quota_unlimited: false, // 兼容旧数据字段(当前固定为有限配额)
|
||||||
download_traffic_quota_value: 1, // 下载流量配额数值
|
download_traffic_quota_value: 1, // 下载流量配额数值
|
||||||
download_quota_unit: 'GB', // 下载流量单位:MB / GB / TB
|
download_quota_unit: 'GB', // 下载流量单位:MB / GB / TB
|
||||||
download_quota_unlimited: true, // 下载流量:true=不限
|
download_quota_unlimited: false, // 下载流量:true=不限(后端值为 -1)
|
||||||
download_traffic_used: 0, // 下载流量已使用(字节)
|
download_traffic_used: 0, // 下载流量已使用(字节)
|
||||||
download_quota_operation: 'set', // set/increase/decrease
|
download_quota_operation: 'set', // set/increase/decrease
|
||||||
download_quota_adjust_value: 1, // 增减额度数值
|
download_quota_adjust_value: 1, // 增减额度数值
|
||||||
@@ -407,7 +407,8 @@ createApp({
|
|||||||
if (this.downloadTrafficReport?.quota?.is_unlimited === true) {
|
if (this.downloadTrafficReport?.quota?.is_unlimited === true) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return this.downloadTrafficQuotaBytes <= 0;
|
const userQuota = Number(this.user?.download_traffic_quota);
|
||||||
|
return Number.isFinite(userQuota) && userQuota < 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
downloadTrafficRemainingBytes() {
|
downloadTrafficRemainingBytes() {
|
||||||
@@ -434,7 +435,7 @@ createApp({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.downloadTrafficQuotaBytes <= 0) {
|
if (this.downloadTrafficQuotaBytes <= 0) {
|
||||||
return 0;
|
return 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.min(100, Math.round((this.downloadTrafficUsedBytes / this.downloadTrafficQuotaBytes) * 100));
|
return Math.min(100, Math.round((this.downloadTrafficUsedBytes / this.downloadTrafficQuotaBytes) * 100));
|
||||||
@@ -3120,8 +3121,9 @@ handleDragLeave(e) {
|
|||||||
this.editStorageForm.oss_quota_unit = 'MB';
|
this.editStorageForm.oss_quota_unit = 'MB';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载流量配额(0 表示不限)
|
// 下载流量配额(-1 表示不限,0 表示禁止下载)
|
||||||
const downloadQuotaBytes = Number(user.download_traffic_quota || 0);
|
const downloadQuotaBytes = Number(user.download_traffic_quota || 0);
|
||||||
|
const isDownloadUnlimited = Number.isFinite(downloadQuotaBytes) && downloadQuotaBytes < 0;
|
||||||
const effectiveDownloadQuotaBytes = Number.isFinite(downloadQuotaBytes) && downloadQuotaBytes > 0
|
const effectiveDownloadQuotaBytes = Number.isFinite(downloadQuotaBytes) && downloadQuotaBytes > 0
|
||||||
? downloadQuotaBytes
|
? downloadQuotaBytes
|
||||||
: 0;
|
: 0;
|
||||||
@@ -3137,10 +3139,13 @@ handleDragLeave(e) {
|
|||||||
this.editStorageForm.download_quota_expires_at = this.toDateTimeLocalInput(user.download_traffic_quota_expires_at);
|
this.editStorageForm.download_quota_expires_at = this.toDateTimeLocalInput(user.download_traffic_quota_expires_at);
|
||||||
this.editStorageForm.reset_download_used_now = false;
|
this.editStorageForm.reset_download_used_now = false;
|
||||||
|
|
||||||
this.editStorageForm.download_quota_unlimited = effectiveDownloadQuotaBytes <= 0;
|
this.editStorageForm.download_quota_unlimited = isDownloadUnlimited;
|
||||||
if (effectiveDownloadQuotaBytes <= 0) {
|
if (isDownloadUnlimited) {
|
||||||
this.editStorageForm.download_traffic_quota_value = 1;
|
this.editStorageForm.download_traffic_quota_value = 1;
|
||||||
this.editStorageForm.download_quota_unit = 'GB';
|
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) {
|
} else if (effectiveDownloadQuotaBytes >= tb && effectiveDownloadQuotaBytes % tb === 0) {
|
||||||
this.editStorageForm.download_traffic_quota_value = effectiveDownloadQuotaBytes / tb;
|
this.editStorageForm.download_traffic_quota_value = effectiveDownloadQuotaBytes / tb;
|
||||||
this.editStorageForm.download_quota_unit = 'TB';
|
this.editStorageForm.download_quota_unit = 'TB';
|
||||||
@@ -3196,10 +3201,11 @@ handleDragLeave(e) {
|
|||||||
let downloadQuotaBytes = null;
|
let downloadQuotaBytes = null;
|
||||||
let downloadTrafficDelta = null;
|
let downloadTrafficDelta = null;
|
||||||
if (this.editStorageForm.download_quota_operation === 'set') {
|
if (this.editStorageForm.download_quota_operation === 'set') {
|
||||||
downloadQuotaBytes = 0;
|
if (this.editStorageForm.download_quota_unlimited) {
|
||||||
if (!this.editStorageForm.download_quota_unlimited) {
|
downloadQuotaBytes = -1;
|
||||||
if (!this.editStorageForm.download_traffic_quota_value || this.editStorageForm.download_traffic_quota_value < 1) {
|
} else {
|
||||||
this.showToast('error', '参数错误', '下载流量配额必须大于 0,或选择不限流量');
|
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;
|
return;
|
||||||
}
|
}
|
||||||
downloadQuotaBytes = toBytes(
|
downloadQuotaBytes = toBytes(
|
||||||
@@ -3287,7 +3293,9 @@ handleDragLeave(e) {
|
|||||||
getAdminUserDownloadQuotaPercentage(user) {
|
getAdminUserDownloadQuotaPercentage(user) {
|
||||||
const quota = Number(user?.download_traffic_quota || 0);
|
const quota = Number(user?.download_traffic_quota || 0);
|
||||||
const used = Number(user?.download_traffic_used || 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;
|
if (!Number.isFinite(used) || used <= 0) return 0;
|
||||||
return Math.min(100, Math.round((used / quota) * 100));
|
return Math.min(100, Math.round((used / quota) * 100));
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user