feat: enhance download traffic quota lifecycle controls

This commit is contained in:
2026-02-17 17:19:25 +08:00
parent 2629237f9e
commit 7687397954
5 changed files with 635 additions and 53 deletions

View File

@@ -275,7 +275,13 @@ createApp({
download_traffic_quota_value: 1, // 下载流量配额数值
download_quota_unit: 'GB', // 下载流量单位MB / GB / TB
download_quota_unlimited: true, // 下载流量true=不限
download_traffic_used: 0 // 下载流量已使用(字节)
download_traffic_used: 0, // 下载流量已使用(字节)
download_quota_operation: 'set', // set/increase/decrease
download_quota_adjust_value: 1, // 增减额度数值
download_quota_adjust_unit: 'GB', // 增减额度单位
download_quota_expires_at: '', // 到期时间datetime-local
download_quota_reset_cycle: 'none', // none/daily/weekly/monthly
reset_download_used_now: false // 保存时立即重置已用流量
},
// 服务器存储统计
@@ -2943,6 +2949,12 @@ handleDragLeave(e) {
this.editStorageForm.download_traffic_used = Number.isFinite(downloadUsedBytes) && downloadUsedBytes > 0
? Math.floor(downloadUsedBytes)
: 0;
this.editStorageForm.download_quota_operation = 'set';
this.editStorageForm.download_quota_adjust_value = 1;
this.editStorageForm.download_quota_adjust_unit = 'GB';
this.editStorageForm.download_quota_reset_cycle = user.download_traffic_reset_cycle || 'none';
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) {
@@ -2993,31 +3005,65 @@ handleDragLeave(e) {
ossQuotaBytes = this.editStorageForm.oss_storage_quota_value * 1024 * 1024;
}
// 计算下载流量配额字节0表示不限
let 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或选择不限流量');
const toBytes = (value, unit) => {
if (unit === 'TB') return value * 1024 * 1024 * 1024 * 1024;
if (unit === 'GB') return value * 1024 * 1024 * 1024;
return value * 1024 * 1024;
};
// 下载流量:支持直接设置 / 增加 / 删减
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或选择不限流量');
return;
}
downloadQuotaBytes = toBytes(
this.editStorageForm.download_traffic_quota_value,
this.editStorageForm.download_quota_unit
);
}
} else {
if (!this.editStorageForm.download_quota_adjust_value || this.editStorageForm.download_quota_adjust_value < 1) {
this.showToast('error', '参数错误', '下载流量增减值必须大于 0');
return;
}
const adjustBytes = toBytes(
this.editStorageForm.download_quota_adjust_value,
this.editStorageForm.download_quota_adjust_unit
);
downloadTrafficDelta = this.editStorageForm.download_quota_operation === 'increase'
? adjustBytes
: -adjustBytes;
}
if (this.editStorageForm.download_quota_unit === 'TB') {
downloadQuotaBytes = this.editStorageForm.download_traffic_quota_value * 1024 * 1024 * 1024 * 1024;
} else if (this.editStorageForm.download_quota_unit === 'GB') {
downloadQuotaBytes = this.editStorageForm.download_traffic_quota_value * 1024 * 1024 * 1024;
} else {
downloadQuotaBytes = this.editStorageForm.download_traffic_quota_value * 1024 * 1024;
}
const downloadQuotaExpiresAt = this.normalizeDateTimeLocalToApi(this.editStorageForm.download_quota_expires_at);
if (this.editStorageForm.download_quota_expires_at && !downloadQuotaExpiresAt) {
this.showToast('error', '参数错误', '下载流量到期时间格式无效');
return;
}
const payload = {
storage_permission: this.editStorageForm.storage_permission,
local_storage_quota: localQuotaBytes,
oss_storage_quota: ossQuotaBytes,
download_traffic_quota_expires_at: downloadQuotaExpiresAt,
download_traffic_reset_cycle: this.editStorageForm.download_quota_reset_cycle || 'none',
reset_download_traffic_used: !!this.editStorageForm.reset_download_used_now
};
if (downloadQuotaBytes !== null) {
payload.download_traffic_quota = downloadQuotaBytes;
}
if (downloadTrafficDelta !== null) {
payload.download_traffic_delta = downloadTrafficDelta;
}
const response = await axios.post(
`${this.apiBase}/api/admin/users/${this.editStorageForm.userId}/storage-permission`,
{
storage_permission: this.editStorageForm.storage_permission,
local_storage_quota: localQuotaBytes,
oss_storage_quota: ossQuotaBytes,
download_traffic_quota: downloadQuotaBytes
},
payload,
);
if (response.data.success) {
@@ -3065,6 +3111,39 @@ handleDragLeave(e) {
return Math.min(100, Math.round((used / quota) * 100));
},
getDownloadResetCycleText(cycle) {
if (cycle === 'daily') return '每日重置';
if (cycle === 'weekly') return '每周重置';
if (cycle === 'monthly') return '每月重置';
return '不自动重置';
},
toDateTimeLocalInput(dateString) {
if (!dateString) return '';
const normalized = String(dateString).trim().replace(' ', 'T');
const match = normalized.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})/);
if (match) return match[1];
const parsed = new Date(normalized);
if (Number.isNaN(parsed.getTime())) return '';
const year = parsed.getFullYear();
const month = String(parsed.getMonth() + 1).padStart(2, '0');
const day = String(parsed.getDate()).padStart(2, '0');
const hours = String(parsed.getHours()).padStart(2, '0');
const minutes = String(parsed.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
},
normalizeDateTimeLocalToApi(localValue) {
if (!localValue) return null;
const normalized = String(localValue).trim();
if (!normalized) return null;
const fullMatch = normalized.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})(?::(\d{2}))?$/);
if (!fullMatch) return null;
const seconds = fullMatch[3] || '00';
return `${fullMatch[1]} ${fullMatch[2]}:${seconds}`;
},
formatDate(dateString) {
if (!dateString) return '-';