From 949653ac00bb3dd1289c2a50e8578505009e35d0 Mon Sep 17 00:00:00 2001 From: 237899745 <237899745@qq.com> Date: Thu, 22 Jan 2026 19:25:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20OSS=20=E5=AD=98?= =?UTF-8?q?=E5=82=A8=E9=85=8D=E9=A2=9D=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 数据库:添加 oss_storage_quota 字段(0 表示无限制) - 后端:登录/用户信息返回 OSS 配额 - 后端:管理员可设置用户 OSS 配额 - 后端:上传时检查 OSS 配额限制 - 前端:管理员编辑用户增加 OSS 配额设置 - 前端:用户文件页面显示 OSS 使用量和配额 Co-Authored-By: Claude Opus 4.5 --- backend/database.js | 18 ++++++++++++++++ backend/server.js | 52 ++++++++++++++++++++++++++++++++++++++++++--- ecosystem.config.js | 14 ++++++++++++ frontend/app.html | 20 ++++++++++++++--- frontend/app.js | 36 +++++++++++++++++++++++++++++-- package-lock.json | 6 ++++++ 6 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 ecosystem.config.js create mode 100644 package-lock.json diff --git a/backend/database.js b/backend/database.js index 1348b54..0ee2606 100644 --- a/backend/database.js +++ b/backend/database.js @@ -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 = { // 日志级别常量 @@ -1432,6 +1449,7 @@ initDefaultSettings(); migrateToV2(); // 执行数据库迁移 migrateThemePreference(); // 主题偏好迁移 migrateToOss(); // SFTP → OSS 迁移 +migrateAddOssQuota(); // 添加 OSS 配额字段 module.exports = { db, diff --git a/backend/server.js b/backend/server.js index 2b3ecab..9e44035 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1897,6 +1897,7 @@ app.post('/api/login', current_storage_type: user.current_storage_type || 'oss', local_storage_quota: user.local_storage_quota || 1073741824, local_storage_used: user.local_storage_used || 0, + oss_storage_quota: user.oss_storage_quota || 0, // 0 表示无限制 // OSS配置来源(重要:用于前端判断是否使用OSS直连上传) 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 uploadPath = req.query.path || '/'; // 上传目标路径 const contentType = req.query.contentType || 'application/octet-stream'; + const fileSize = parseInt(req.query.fileSize, 10) || 0; if (!filename) { 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 { const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'); 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 { const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3'); 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', current_storage_type: u.current_storage_type || 'oss', 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) { @@ -5847,7 +5888,7 @@ app.post('/api/admin/users/:id/storage-permission', try { const { id } = req.params; - const { storage_permission, local_storage_quota } = req.body; + const { storage_permission, local_storage_quota, oss_storage_quota } = req.body; // 参数验证:验证 ID 格式 const userId = parseInt(id, 10); @@ -5860,10 +5901,15 @@ app.post('/api/admin/users/:id/storage-permission', const updates = { storage_permission }; - // 如果提供了配额,更新配额(单位:字节) + // 如果提供了本地配额,更新本地配额(单位:字节) if (local_storage_quota !== undefined) { 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); diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..0977c91 --- /dev/null +++ b/ecosystem.config.js @@ -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' + } + }] +}; diff --git a/frontend/app.html b/frontend/app.html index 1d6b4e7..01c8051 100644 --- a/frontend/app.html +++ b/frontend/app.html @@ -1808,8 +1808,10 @@ {{ ossUsageError }}
- {{ ossUsage.totalSizeFormatted }} - ({{ ossUsage.fileCount }} 文件) + {{ ossUsage.totalSizeFormatted }} + / {{ formatBytes(ossQuota) }} + / 无限制 +
{{ ossUsage.fileCount }} 文件
点击刷新查看
@@ -3109,7 +3111,19 @@ • 默认配额: 1GB
• 当前配额: {{ 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)
- • 配额仅影响本地存储,OSS存储不受此限制 + • 本地配额默认: 1GB | OSS配额默认: 无限制 + + + + +
+ +
+ +
diff --git a/frontend/app.js b/frontend/app.js index 7152be3..86ffa30 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -239,6 +239,7 @@ createApp({ storagePermission: 'oss_only', // 存储权限 localQuota: 0, // 本地存储配额(字节) localUsed: 0, // 本地存储已使用(字节) + ossQuota: 0, // OSS 存储配额(字节,0表示无限制) // 右键菜单 @@ -653,6 +654,7 @@ handleDragLeave(e) { this.storageType = this.user.current_storage_type || 'oss'; this.localQuota = this.user.local_storage_quota || 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); @@ -1157,6 +1159,7 @@ handleDragLeave(e) { this.storageType = this.user.current_storage_type || 'oss'; this.localQuota = this.user.local_storage_quota || 0; this.localUsed = this.user.local_storage_used || 0; + this.ossQuota = this.user.oss_storage_quota || 0; console.log('[页面加载] Cookie验证成功,存储权限:', this.storagePermission, '存储类型:', this.storageType); @@ -1912,7 +1915,8 @@ handleDragLeave(e) { params: { filename: file.name, 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'; } + // 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; }, @@ -2669,11 +2690,22 @@ handleDragLeave(e) { 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( `${this.apiBase}/api/admin/users/${this.editStorageForm.userId}/storage-permission`, { storage_permission: this.editStorageForm.storage_permission, - local_storage_quota: quotaBytes + local_storage_quota: quotaBytes, + oss_storage_quota: ossQuotaBytes }, ); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5d3a710 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "vue-driven-cloud-storage", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}