feat: 添加 OSS 存储配额功能
- 数据库:添加 oss_storage_quota 字段(0 表示无限制) - 后端:登录/用户信息返回 OSS 配额 - 后端:管理员可设置用户 OSS 配额 - 后端:上传时检查 OSS 配额限制 - 前端:管理员编辑用户增加 OSS 配额设置 - 前端:用户文件页面显示 OSS 使用量和配额 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1247,6 +1247,23 @@ function migrateToOss() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 数据库版本迁移 - 添加 OSS 配额字段
|
||||||
|
function migrateAddOssQuota() {
|
||||||
|
try {
|
||||||
|
const columns = db.prepare("PRAGMA table_info(users)").all();
|
||||||
|
const hasOssQuota = columns.some(col => col.name === 'oss_storage_quota');
|
||||||
|
|
||||||
|
if (!hasOssQuota) {
|
||||||
|
console.log('[数据库迁移] 添加 OSS 配额字段...');
|
||||||
|
db.exec();
|
||||||
|
console.log('[数据库迁移] ✅ OSS 配额字段已添加 (默认 0 表示无限制)');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[数据库迁移] OSS 配额迁移失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 系统日志操作
|
// 系统日志操作
|
||||||
const SystemLogDB = {
|
const SystemLogDB = {
|
||||||
// 日志级别常量
|
// 日志级别常量
|
||||||
@@ -1432,6 +1449,7 @@ initDefaultSettings();
|
|||||||
migrateToV2(); // 执行数据库迁移
|
migrateToV2(); // 执行数据库迁移
|
||||||
migrateThemePreference(); // 主题偏好迁移
|
migrateThemePreference(); // 主题偏好迁移
|
||||||
migrateToOss(); // SFTP → OSS 迁移
|
migrateToOss(); // SFTP → OSS 迁移
|
||||||
|
migrateAddOssQuota(); // 添加 OSS 配额字段
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
db,
|
db,
|
||||||
|
|||||||
@@ -1897,6 +1897,7 @@ app.post('/api/login',
|
|||||||
current_storage_type: user.current_storage_type || 'oss',
|
current_storage_type: user.current_storage_type || 'oss',
|
||||||
local_storage_quota: user.local_storage_quota || 1073741824,
|
local_storage_quota: user.local_storage_quota || 1073741824,
|
||||||
local_storage_used: user.local_storage_used || 0,
|
local_storage_used: user.local_storage_used || 0,
|
||||||
|
oss_storage_quota: user.oss_storage_quota || 0, // 0 表示无限制
|
||||||
// OSS配置来源(重要:用于前端判断是否使用OSS直连上传)
|
// OSS配置来源(重要:用于前端判断是否使用OSS直连上传)
|
||||||
oss_config_source: SettingsDB.hasUnifiedOssConfig() ? 'unified' : (user.has_oss_config ? 'personal' : 'none')
|
oss_config_source: SettingsDB.hasUnifiedOssConfig() ? 'unified' : (user.has_oss_config ? 'personal' : 'none')
|
||||||
}
|
}
|
||||||
@@ -2986,6 +2987,7 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => {
|
|||||||
const filename = req.query.filename;
|
const filename = req.query.filename;
|
||||||
const uploadPath = req.query.path || '/'; // 上传目标路径
|
const uploadPath = req.query.path || '/'; // 上传目标路径
|
||||||
const contentType = req.query.contentType || 'application/octet-stream';
|
const contentType = req.query.contentType || 'application/octet-stream';
|
||||||
|
const fileSize = parseInt(req.query.fileSize, 10) || 0;
|
||||||
|
|
||||||
if (!filename) {
|
if (!filename) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -3036,6 +3038,25 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OSS 配额检查(0 表示无限制)
|
||||||
|
const ossQuota = req.user.oss_storage_quota || 0;
|
||||||
|
if (ossQuota > 0 && fileSize > 0) {
|
||||||
|
// 获取当前 OSS 使用量
|
||||||
|
const { getOssUsage } = require('./storage');
|
||||||
|
try {
|
||||||
|
const ossUsage = await getOssUsage(req.user);
|
||||||
|
const currentUsed = ossUsage?.totalSize || 0;
|
||||||
|
if (currentUsed + fileSize > ossQuota) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (usageErr) {
|
||||||
|
console.warn('[OSS配额检查] 获取使用量失败,跳过检查:', usageErr.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
|
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
|
||||||
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
||||||
@@ -3169,6 +3190,25 @@ app.get('/api/files/download-url', authMiddleware, async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OSS 配额检查(0 表示无限制)
|
||||||
|
const ossQuota = req.user.oss_storage_quota || 0;
|
||||||
|
if (ossQuota > 0 && fileSize > 0) {
|
||||||
|
// 获取当前 OSS 使用量
|
||||||
|
const { getOssUsage } = require('./storage');
|
||||||
|
try {
|
||||||
|
const ossUsage = await getOssUsage(req.user);
|
||||||
|
const currentUsed = ossUsage?.totalSize || 0;
|
||||||
|
if (currentUsed + fileSize > ossQuota) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (usageErr) {
|
||||||
|
console.warn('[OSS配额检查] 获取使用量失败,跳过检查:', usageErr.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
|
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
|
||||||
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
||||||
@@ -5251,7 +5291,8 @@ app.get('/api/admin/users', authMiddleware, adminMiddleware, (req, res) => {
|
|||||||
storage_permission: u.storage_permission || 'oss_only',
|
storage_permission: u.storage_permission || 'oss_only',
|
||||||
current_storage_type: u.current_storage_type || 'oss',
|
current_storage_type: u.current_storage_type || 'oss',
|
||||||
local_storage_quota: u.local_storage_quota || 1073741824,
|
local_storage_quota: u.local_storage_quota || 1073741824,
|
||||||
local_storage_used: u.local_storage_used || 0
|
local_storage_used: u.local_storage_used || 0,
|
||||||
|
oss_storage_quota: u.oss_storage_quota || 0
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -5847,7 +5888,7 @@ app.post('/api/admin/users/:id/storage-permission',
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { storage_permission, local_storage_quota } = req.body;
|
const { storage_permission, local_storage_quota, oss_storage_quota } = req.body;
|
||||||
|
|
||||||
// 参数验证:验证 ID 格式
|
// 参数验证:验证 ID 格式
|
||||||
const userId = parseInt(id, 10);
|
const userId = parseInt(id, 10);
|
||||||
@@ -5860,11 +5901,16 @@ app.post('/api/admin/users/:id/storage-permission',
|
|||||||
|
|
||||||
const updates = { storage_permission };
|
const updates = { storage_permission };
|
||||||
|
|
||||||
// 如果提供了配额,更新配额(单位:字节)
|
// 如果提供了本地配额,更新本地配额(单位:字节)
|
||||||
if (local_storage_quota !== undefined) {
|
if (local_storage_quota !== undefined) {
|
||||||
updates.local_storage_quota = parseInt(local_storage_quota, 10);
|
updates.local_storage_quota = parseInt(local_storage_quota, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果提供了 OSS 配额,更新 OSS 配额(单位:字节,0 表示无限制)
|
||||||
|
if (oss_storage_quota !== undefined) {
|
||||||
|
updates.oss_storage_quota = parseInt(oss_storage_quota, 10);
|
||||||
|
}
|
||||||
|
|
||||||
// 根据权限设置自动调整存储类型
|
// 根据权限设置自动调整存储类型
|
||||||
const user = UserDB.findById(userId);
|
const user = UserDB.findById(userId);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
14
ecosystem.config.js
Normal file
14
ecosystem.config.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps: [{
|
||||||
|
name: 'vue-driven-cloud',
|
||||||
|
script: './backend/server.js',
|
||||||
|
cwd: '/www/wwwroot/vue-driven-cloud-storage',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
PORT: 40001,
|
||||||
|
TRUST_PROXY: '1',
|
||||||
|
ENFORCE_HTTPS: 'true',
|
||||||
|
COOKIE_SECURE: 'true'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
};
|
||||||
@@ -1809,7 +1809,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else-if="ossUsage" style="font-size: 13px; font-weight: 600; color: #4b5fc9;">
|
<div v-else-if="ossUsage" style="font-size: 13px; font-weight: 600; color: #4b5fc9;">
|
||||||
{{ ossUsage.totalSizeFormatted }}
|
{{ ossUsage.totalSizeFormatted }}
|
||||||
<span style="font-weight: 400; color: var(--text-muted); font-size: 12px;">({{ ossUsage.fileCount }} 文件)</span>
|
<span v-if="ossQuota > 0" style="font-weight: 400; color: var(--text-muted);">/ {{ formatBytes(ossQuota) }}</span>
|
||||||
|
<span v-else style="font-weight: 400; color: var(--text-muted);">/ 无限制</span>
|
||||||
|
<div style="font-weight: 400; color: var(--text-muted); font-size: 12px; margin-top: 4px;">{{ ossUsage.fileCount }} 文件</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else style="font-size: 12px; color: var(--text-muted);">点击刷新查看</div>
|
<div v-else style="font-size: 12px; color: var(--text-muted);">点击刷新查看</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3109,7 +3111,19 @@
|
|||||||
• 默认配额: 1GB<br>
|
• 默认配额: 1GB<br>
|
||||||
• 当前配额: {{ editStorageForm.local_storage_quota_value }} {{ editStorageForm.quota_unit }}
|
• 当前配额: {{ editStorageForm.local_storage_quota_value }} {{ editStorageForm.quota_unit }}
|
||||||
({{ editStorageForm.quota_unit === 'GB' ? (editStorageForm.local_storage_quota_value * 1024).toFixed(0) : editStorageForm.local_storage_quota_value }} MB)<br>
|
({{ editStorageForm.quota_unit === 'GB' ? (editStorageForm.local_storage_quota_value * 1024).toFixed(0) : editStorageForm.local_storage_quota_value }} MB)<br>
|
||||||
• 配额仅影响本地存储,OSS存储不受此限制
|
• 本地配额默认: 1GB | OSS配额默认: 无限制
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- OSS 存储配额 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">OSS 存储配额 <span style="color: var(--text-muted); font-weight: normal;">(0 = 无限制)</span></label>
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<input type="number" class="form-input" v-model.number="editStorageForm.oss_storage_quota_value" min="0" max="102400" step="1" style="flex: 1;">
|
||||||
|
<select class="form-input" v-model="editStorageForm.oss_quota_unit" style="width: 100px;">
|
||||||
|
<option value="GB">GB</option>
|
||||||
|
<option value="MB">MB</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ createApp({
|
|||||||
storagePermission: 'oss_only', // 存储权限
|
storagePermission: 'oss_only', // 存储权限
|
||||||
localQuota: 0, // 本地存储配额(字节)
|
localQuota: 0, // 本地存储配额(字节)
|
||||||
localUsed: 0, // 本地存储已使用(字节)
|
localUsed: 0, // 本地存储已使用(字节)
|
||||||
|
ossQuota: 0, // OSS 存储配额(字节,0表示无限制)
|
||||||
|
|
||||||
|
|
||||||
// 右键菜单
|
// 右键菜单
|
||||||
@@ -653,6 +654,7 @@ handleDragLeave(e) {
|
|||||||
this.storageType = this.user.current_storage_type || 'oss';
|
this.storageType = this.user.current_storage_type || 'oss';
|
||||||
this.localQuota = this.user.local_storage_quota || 0;
|
this.localQuota = this.user.local_storage_quota || 0;
|
||||||
this.localUsed = this.user.local_storage_used || 0;
|
this.localUsed = this.user.local_storage_used || 0;
|
||||||
|
this.ossQuota = this.user.oss_storage_quota || 0;
|
||||||
|
|
||||||
console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType, 'OSS配置:', this.user.oss_config_source);
|
console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType, 'OSS配置:', this.user.oss_config_source);
|
||||||
|
|
||||||
@@ -1157,6 +1159,7 @@ handleDragLeave(e) {
|
|||||||
this.storageType = this.user.current_storage_type || 'oss';
|
this.storageType = this.user.current_storage_type || 'oss';
|
||||||
this.localQuota = this.user.local_storage_quota || 0;
|
this.localQuota = this.user.local_storage_quota || 0;
|
||||||
this.localUsed = this.user.local_storage_used || 0;
|
this.localUsed = this.user.local_storage_used || 0;
|
||||||
|
this.ossQuota = this.user.oss_storage_quota || 0;
|
||||||
|
|
||||||
console.log('[页面加载] Cookie验证成功,存储权限:', this.storagePermission, '存储类型:', this.storageType);
|
console.log('[页面加载] Cookie验证成功,存储权限:', this.storagePermission, '存储类型:', this.storageType);
|
||||||
|
|
||||||
@@ -1912,7 +1915,8 @@ handleDragLeave(e) {
|
|||||||
params: {
|
params: {
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
path: this.currentPath,
|
path: this.currentPath,
|
||||||
contentType: file.type || 'application/octet-stream'
|
contentType: file.type || 'application/octet-stream',
|
||||||
|
fileSize: file.size
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2655,6 +2659,23 @@ handleDragLeave(e) {
|
|||||||
this.editStorageForm.quota_unit = 'MB';
|
this.editStorageForm.quota_unit = 'MB';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OSS 配额初始化
|
||||||
|
const ossQuotaBytes = user.oss_storage_quota || 0;
|
||||||
|
if (ossQuotaBytes === 0) {
|
||||||
|
this.editStorageForm.oss_storage_quota_value = 0;
|
||||||
|
this.editStorageForm.oss_quota_unit = "GB";
|
||||||
|
} else {
|
||||||
|
const ossQuotaMB = ossQuotaBytes / 1024 / 1024;
|
||||||
|
const ossQuotaGB = ossQuotaMB / 1024;
|
||||||
|
if (ossQuotaMB >= 1024 && ossQuotaMB % 1024 === 0) {
|
||||||
|
this.editStorageForm.oss_storage_quota_value = ossQuotaGB;
|
||||||
|
this.editStorageForm.oss_quota_unit = "GB";
|
||||||
|
} else {
|
||||||
|
this.editStorageForm.oss_storage_quota_value = Math.round(ossQuotaMB);
|
||||||
|
this.editStorageForm.oss_quota_unit = "MB";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.showEditStorageModal = true;
|
this.showEditStorageModal = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -2669,11 +2690,22 @@ handleDragLeave(e) {
|
|||||||
quotaBytes = this.editStorageForm.local_storage_quota_value * 1024 * 1024;
|
quotaBytes = this.editStorageForm.local_storage_quota_value * 1024 * 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算 OSS 配额字节数
|
||||||
|
let ossQuotaBytes = 0;
|
||||||
|
if (this.editStorageForm.oss_storage_quota_value > 0) {
|
||||||
|
if (this.editStorageForm.oss_quota_unit === "GB") {
|
||||||
|
ossQuotaBytes = this.editStorageForm.oss_storage_quota_value * 1024 * 1024 * 1024;
|
||||||
|
} else {
|
||||||
|
ossQuotaBytes = this.editStorageForm.oss_storage_quota_value * 1024 * 1024;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${this.apiBase}/api/admin/users/${this.editStorageForm.userId}/storage-permission`,
|
`${this.apiBase}/api/admin/users/${this.editStorageForm.userId}/storage-permission`,
|
||||||
{
|
{
|
||||||
storage_permission: this.editStorageForm.storage_permission,
|
storage_permission: this.editStorageForm.storage_permission,
|
||||||
local_storage_quota: quotaBytes
|
local_storage_quota: quotaBytes,
|
||||||
|
oss_storage_quota: ossQuotaBytes
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "vue-driven-cloud-storage",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user