feat(quota): add downloadable traffic quota with local/OSS/share metering
This commit is contained in:
111
frontend/app.js
111
frontend/app.js
@@ -271,7 +271,11 @@ createApp({
|
||||
quota_unit: 'GB', // 本地配额单位:MB 或 GB
|
||||
oss_storage_quota_value: 1, // OSS配额数值(默认1GB)
|
||||
oss_quota_unit: 'GB', // OSS配额单位:MB / GB / TB
|
||||
oss_quota_unlimited: false // 兼容旧数据字段(当前固定为有限配额)
|
||||
oss_quota_unlimited: false, // 兼容旧数据字段(当前固定为有限配额)
|
||||
download_traffic_quota_value: 1, // 下载流量配额数值
|
||||
download_quota_unit: 'GB', // 下载流量单位:MB / GB / TB
|
||||
download_quota_unlimited: true, // 下载流量:true=不限
|
||||
download_traffic_used: 0 // 下载流量已使用(字节)
|
||||
},
|
||||
|
||||
// 服务器存储统计
|
||||
@@ -1454,36 +1458,13 @@ handleDragLeave(e) {
|
||||
// 构建文件路径
|
||||
const filePath = this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`;
|
||||
|
||||
// OSS 模式:使用签名 URL 直连下载(不经过后端)
|
||||
if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none') {
|
||||
this.downloadFromOSS(filePath);
|
||||
} else {
|
||||
// 本地存储模式:通过后端下载
|
||||
this.downloadFromLocal(filePath);
|
||||
}
|
||||
// 统一走后端下载接口,确保下载流量可精确计量
|
||||
this.downloadFromLocal(filePath);
|
||||
},
|
||||
|
||||
// OSS 直连下载(使用签名URL,不经过后端,节省后端带宽)
|
||||
async downloadFromOSS(filePath) {
|
||||
try {
|
||||
// 获取签名 URL
|
||||
const { data } = await axios.get(`${this.apiBase}/api/files/download-url`, {
|
||||
params: { path: filePath }
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
// 直连 OSS 下载(不经过后端,充分利用OSS带宽和CDN)
|
||||
window.open(data.downloadUrl, '_blank');
|
||||
} else {
|
||||
// 处理后端返回的错误
|
||||
console.error('获取下载链接失败:', data.message);
|
||||
this.showToast('error', '下载失败', data.message || '获取下载链接失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取下载链接失败:', error);
|
||||
const errorMsg = error.response?.data?.message || error.message || '获取下载链接失败';
|
||||
this.showToast('error', '下载失败', errorMsg);
|
||||
}
|
||||
// 保留方法名兼容旧调用,内部统一转发到后端下载
|
||||
downloadFromOSS(filePath) {
|
||||
this.downloadFromLocal(filePath);
|
||||
},
|
||||
|
||||
// 本地存储下载
|
||||
@@ -1785,16 +1766,19 @@ handleDragLeave(e) {
|
||||
? `/${file.name}`
|
||||
: `${this.currentPath}/${file.name}`;
|
||||
|
||||
// OSS 模式:返回签名 URL(用于媒体预览)
|
||||
if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none') {
|
||||
const hasDownloadTrafficLimit = Number(this.user?.download_traffic_quota || 0) > 0;
|
||||
|
||||
// OSS 模式且未启用下载流量限制时,返回签名 URL(用于媒体预览)
|
||||
if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none' && !hasDownloadTrafficLimit) {
|
||||
try {
|
||||
const { data } = await axios.get(`${this.apiBase}/api/files/download-url`, {
|
||||
params: { path: filePath }
|
||||
});
|
||||
return data.success ? data.downloadUrl : null;
|
||||
if (data.success) {
|
||||
return data.downloadUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取媒体URL失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2785,6 +2769,13 @@ handleDragLeave(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmMessage = type === 'local'
|
||||
? '切换到本地存储不会自动迁移 OSS 文件。切换后只会显示本地文件,确认继续?'
|
||||
: '切换到 OSS 存储不会自动迁移本地文件。切换后只会显示 OSS 文件,确认继续?';
|
||||
if (!window.confirm(confirmMessage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 不再弹出配置引导弹窗,直接尝试切换
|
||||
// 如果后端检测到没有OSS配置,会返回错误提示
|
||||
|
||||
@@ -2942,6 +2933,32 @@ handleDragLeave(e) {
|
||||
this.editStorageForm.oss_quota_unit = 'MB';
|
||||
}
|
||||
|
||||
// 下载流量配额(0 表示不限)
|
||||
const downloadQuotaBytes = Number(user.download_traffic_quota || 0);
|
||||
const effectiveDownloadQuotaBytes = Number.isFinite(downloadQuotaBytes) && downloadQuotaBytes > 0
|
||||
? downloadQuotaBytes
|
||||
: 0;
|
||||
const downloadUsedBytes = Number(user.download_traffic_used || 0);
|
||||
|
||||
this.editStorageForm.download_traffic_used = Number.isFinite(downloadUsedBytes) && downloadUsedBytes > 0
|
||||
? Math.floor(downloadUsedBytes)
|
||||
: 0;
|
||||
|
||||
this.editStorageForm.download_quota_unlimited = effectiveDownloadQuotaBytes <= 0;
|
||||
if (effectiveDownloadQuotaBytes <= 0) {
|
||||
this.editStorageForm.download_traffic_quota_value = 1;
|
||||
this.editStorageForm.download_quota_unit = 'GB';
|
||||
} else if (effectiveDownloadQuotaBytes >= tb && effectiveDownloadQuotaBytes % tb === 0) {
|
||||
this.editStorageForm.download_traffic_quota_value = effectiveDownloadQuotaBytes / tb;
|
||||
this.editStorageForm.download_quota_unit = 'TB';
|
||||
} else if (effectiveDownloadQuotaBytes >= gb && effectiveDownloadQuotaBytes % gb === 0) {
|
||||
this.editStorageForm.download_traffic_quota_value = effectiveDownloadQuotaBytes / gb;
|
||||
this.editStorageForm.download_quota_unit = 'GB';
|
||||
} else {
|
||||
this.editStorageForm.download_traffic_quota_value = Math.max(1, Math.round(effectiveDownloadQuotaBytes / mb));
|
||||
this.editStorageForm.download_quota_unit = 'MB';
|
||||
}
|
||||
|
||||
this.showEditStorageModal = true;
|
||||
},
|
||||
|
||||
@@ -2976,12 +2993,30 @@ 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,或选择不限流量');
|
||||
return;
|
||||
}
|
||||
|
||||
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 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
|
||||
oss_storage_quota: ossQuotaBytes,
|
||||
download_traffic_quota: downloadQuotaBytes
|
||||
},
|
||||
);
|
||||
|
||||
@@ -3022,6 +3057,14 @@ handleDragLeave(e) {
|
||||
return Math.min(100, Math.round((used / quota) * 100));
|
||||
},
|
||||
|
||||
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(used) || used <= 0) return 0;
|
||||
return Math.min(100, Math.round((used / quota) * 100));
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user