From 97f14824977cd9b66693a414bc654896f0724a0e Mon Sep 17 00:00:00 2001 From: gaoxq <376340421@qq.com> Date: Fri, 3 Apr 2026 22:27:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AE=9A=E6=97=B6=E6=B8=85?= =?UTF-8?q?=E7=90=86=E5=9B=9E=E6=94=B6=E7=AB=99=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/compiler.xml | 2 + .idea/inspectionProfiles/Project_Default.xml | 13 +++++ .idea/misc.xml | 2 +- .../com/filesystem/FileSystemApplication.java | 2 + .../com/filesystem/config/SecurityConfig.java | 1 + .../filesystem/controller/AuthController.java | 49 +++++++++---------- .../filesystem/controller/UserController.java | 18 ++++++- .../com/filesystem/service/FileService.java | 45 +++++++++++++++++ .../com/filesystem/service/UserService.java | 12 +++-- .../com/filesystem/task/TrashCleanupTask.java | 27 ++++++++++ src/main/resources/application.yml | 20 +++++--- web-vue/package-lock.json | 9 +--- web-vue/src/api/request.js | 2 +- web-vue/src/api/user.js | 2 + web-vue/src/components/UploadDialog.vue | 14 +++++- web-vue/src/store/user.js | 21 ++++++-- web-vue/src/views/files/index.vue | 13 +++-- web-vue/src/views/login/index.vue | 27 +++++++++- 18 files changed, 220 insertions(+), 59 deletions(-) create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 src/main/java/com/filesystem/task/TrashCleanupTask.java diff --git a/.idea/compiler.xml b/.idea/compiler.xml index a891a69..e2a5e51 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -7,6 +7,7 @@ + @@ -14,6 +15,7 @@ + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..002363c --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 1f93b88..1e86ad3 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,5 +8,5 @@ - + \ No newline at end of file diff --git a/src/main/java/com/filesystem/FileSystemApplication.java b/src/main/java/com/filesystem/FileSystemApplication.java index a1820b6..46671c3 100644 --- a/src/main/java/com/filesystem/FileSystemApplication.java +++ b/src/main/java/com/filesystem/FileSystemApplication.java @@ -3,9 +3,11 @@ package com.filesystem; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @MapperScan("com.filesystem.mapper") +@EnableScheduling public class FileSystemApplication { public static void main(String[] args) { diff --git a/src/main/java/com/filesystem/config/SecurityConfig.java b/src/main/java/com/filesystem/config/SecurityConfig.java index fd1aed8..d287582 100644 --- a/src/main/java/com/filesystem/config/SecurityConfig.java +++ b/src/main/java/com/filesystem/config/SecurityConfig.java @@ -47,6 +47,7 @@ public class SecurityConfig { // API 公开接口 .requestMatchers("/api/auth/login", "/api/auth/register", "/api/auth/logout").permitAll() .requestMatchers("/api/files/test").permitAll() + .requestMatchers("/api/users/config").permitAll() // WebSocket .requestMatchers("/ws/**").permitAll() // 静态资源 diff --git a/src/main/java/com/filesystem/controller/AuthController.java b/src/main/java/com/filesystem/controller/AuthController.java index 7dffca2..c11b166 100644 --- a/src/main/java/com/filesystem/controller/AuthController.java +++ b/src/main/java/com/filesystem/controller/AuthController.java @@ -14,23 +14,23 @@ import java.util.Map; @RestController @RequestMapping("/api/auth") public class AuthController { - + @Resource private UserService userService; - + @PostMapping("/login") public ResponseEntity> login(@RequestBody Map request) { String username = request.get("username"); String password = request.get("password"); - + try { String token = userService.login(username, password); User user = userService.findByUsername(username); - + // 精确重算存储空间 long storageUsed = userService.recalculateStorage(user.getId()); - long storageLimit = user.getStorageLimit() != null ? user.getStorageLimit() : 20L * 1024 * 1024 * 1024; - + long storageLimit = user.getStorageLimit(); + Map userData = new HashMap<>(); userData.put("id", user.getId()); userData.put("username", user.getUsername()); @@ -41,35 +41,35 @@ public class AuthController { userData.put("email", user.getEmail() != null ? user.getEmail() : ""); userData.put("storageUsed", storageUsed); userData.put("storageLimit", storageLimit); - + Map result = new HashMap<>(); result.put("token", token); result.put("user", userData); - + Map body = new HashMap<>(); body.put("data", result); body.put("message", "登录成功"); - + return ResponseEntity.ok(body); } catch (Exception e) { return ResponseEntity.badRequest().body(Map.of("message", e.getMessage())); } } - + @PostMapping("/register") public ResponseEntity> register(@RequestBody Map request) { String username = request.get("username"); String password = request.get("password"); String nickname = request.get("nickname"); - + if (userService.findByUsername(username) != null) { return ResponseEntity.badRequest().body(Map.of("message", "用户名已存在")); } - + User user = userService.createUser(username, password, nickname != null ? nickname : username); return ResponseEntity.ok(Map.of("message", "注册成功", "data", Map.of("id", user.getId()))); } - + @PostMapping("/logout") public ResponseEntity> logout(@AuthenticationPrincipal UserPrincipal principal) { if (principal != null) { @@ -77,36 +77,35 @@ public class AuthController { } return ResponseEntity.ok(Map.of("message", "退出成功")); } - + @GetMapping("/info") public ResponseEntity> getUserInfo(@AuthenticationPrincipal UserPrincipal principal) { if (principal == null) { return ResponseEntity.status(401).body(Map.of("message", "未登录")); } - + User user = userService.findById(principal.getUserId()); if (user == null) { return ResponseEntity.status(401).body(Map.of("message", "用户不存在")); } - + // 精确重算存储空间 long storageUsed = userService.recalculateStorage(user.getId()); - long storageLimit = user.getStorageLimit() != null ? user.getStorageLimit() : 20L * 1024 * 1024 * 1024; - + long storageLimit = user.getStorageLimit(); + Map userData = new HashMap<>(); userData.put("id", user.getId()); userData.put("username", user.getUsername()); - userData.put("nickname", user.getNickname() != null ? user.getNickname() : ""); - userData.put("signature", user.getSignature() != null ? user.getSignature() : ""); - userData.put("avatar", user.getAvatar() != null ? user.getAvatar() : ""); - userData.put("email", user.getEmail() != null ? user.getEmail() : ""); - userData.put("phone", user.getPhone() != null ? user.getPhone() : ""); + userData.put("nickname", user.getNickname()); + userData.put("signature", user.getSignature()); + userData.put("avatar", user.getAvatar()); + userData.put("email", user.getEmail()); + userData.put("phone", user.getPhone()); userData.put("storageUsed", storageUsed); userData.put("storageLimit", storageLimit); - Map body = new HashMap<>(); body.put("data", userData); - + return ResponseEntity.ok(body); } } diff --git a/src/main/java/com/filesystem/controller/UserController.java b/src/main/java/com/filesystem/controller/UserController.java index 1cbafff..790283b 100644 --- a/src/main/java/com/filesystem/controller/UserController.java +++ b/src/main/java/com/filesystem/controller/UserController.java @@ -29,6 +29,22 @@ public class UserController { @Value("${file.storage.path:./uploads}") private String storagePath; + @Value("${file.user.storage-limit-gb:50}") + private int storageLimitGb; + + /** + * 获取系统配置(存储配额等) + */ + @GetMapping("/config") + public ResponseEntity> getSystemConfig() { + return ResponseEntity.ok(Map.of( + "data", Map.of( + "storageLimitGb", storageLimitGb, + "storageLimitBytes", (long) storageLimitGb * 1024 * 1024 * 1024 + ) + )); + } + /** * 获取所有可用用户(用于文件共享等场景) */ @@ -41,7 +57,7 @@ public class UserController { Map m = new java.util.HashMap<>(); m.put("id", u.getId()); m.put("username", u.getUsername()); - m.put("nickname", u.getNickname() != null ? u.getNickname() : u.getUsername()); + m.put("nickname", u.getNickname()); m.put("avatar", u.getAvatar()); m.put("signature", u.getSignature()); return m; diff --git a/src/main/java/com/filesystem/service/FileService.java b/src/main/java/com/filesystem/service/FileService.java index 30ee59c..ad08385 100644 --- a/src/main/java/com/filesystem/service/FileService.java +++ b/src/main/java/com/filesystem/service/FileService.java @@ -714,4 +714,49 @@ public class FileService { } return tree; } + + /** + * 删除回收站中超过指定天数的文件 + * @param retentionDays 保留天数 + * @return 删除的文件数量 + */ + @Transactional + public int deleteExpiredTrashFiles(int retentionDays) { + LocalDateTime cutoffDate = LocalDateTime.now().minusDays(retentionDays); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + // 查询所有在回收站且超过保留天数的文件 + List expiredFiles = fileMapper.selectList( + new LambdaQueryWrapper() + .eq(FileEntity::getIsDeleted, 1) + .isNotNull(FileEntity::getDeletedAt) + ); + + int deletedCount = 0; + for (FileEntity file : expiredFiles) { + try { + LocalDateTime deletedAt = LocalDateTime.parse(file.getDeletedAt(), formatter); + if (deletedAt.isBefore(cutoffDate)) { + // 彻底删除 + if (file.getPath() != null && !file.getPath().isEmpty()) { + try { + Path filePath = Paths.get(storagePath).toAbsolutePath().resolve("files").resolve(file.getPath()); + Files.deleteIfExists(filePath); + } catch (IOException e) { + // ignore + } + if (file.getSize() != null && file.getSize() > 0) { + userService.decreaseStorage(file.getUserId(), file.getSize()); + } + } + fileMapper.deleteById(file.getId()); + deletedCount++; + } + } catch (Exception e) { + // 日期解析失败,跳过 + } + } + + return deletedCount; + } } diff --git a/src/main/java/com/filesystem/service/UserService.java b/src/main/java/com/filesystem/service/UserService.java index adbdfc4..79f5a93 100644 --- a/src/main/java/com/filesystem/service/UserService.java +++ b/src/main/java/com/filesystem/service/UserService.java @@ -7,6 +7,7 @@ import com.filesystem.mapper.FileMapper; import com.filesystem.mapper.UserMapper; import com.filesystem.security.JwtUtil; import jakarta.annotation.Resource; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @@ -27,6 +28,9 @@ public class UserService { @Resource private BCryptPasswordEncoder passwordEncoder; + @Value("${file.user.storage-limit-gb:50}") + private int storageLimitGb; + public User findByUsername(String username) { return userMapper.selectOne( new LambdaQueryWrapper().eq(User::getUsername, username) @@ -59,7 +63,7 @@ public class UserService { user.setNickname(nickname); user.setStatus(1); user.setStorageUsed(0L); - user.setStorageLimit(20L * 1024 * 1024 * 1024); // 20GB + user.setStorageLimit((long) storageLimitGb * 1024 * 1024 * 1024); userMapper.insert(user); return user; } @@ -84,9 +88,9 @@ public class UserService { User user = userMapper.selectById(userId); if (user != null) { user.setStorageUsed(total); - // 同步存储上限为 20GB(兼容老用户) - if (user.getStorageLimit() == null || user.getStorageLimit() < 20L * 1024 * 1024 * 1024) { - user.setStorageLimit(20L * 1024 * 1024 * 1024); + // 同步存储上限(兼容老用户) + if (user.getStorageLimit() == null || user.getStorageLimit() < (long) storageLimitGb * 1024 * 1024 * 1024) { + user.setStorageLimit((long) storageLimitGb * 1024 * 1024 * 1024); } userMapper.updateById(user); } diff --git a/src/main/java/com/filesystem/task/TrashCleanupTask.java b/src/main/java/com/filesystem/task/TrashCleanupTask.java new file mode 100644 index 0000000..d917bf0 --- /dev/null +++ b/src/main/java/com/filesystem/task/TrashCleanupTask.java @@ -0,0 +1,27 @@ +package com.filesystem.task; + +import com.filesystem.service.FileService; +import jakarta.annotation.Resource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +public class TrashCleanupTask { + + @Resource + private FileService fileService; + + @Value("${file.trash.retention-days:30}") + private int retentionDays; + + /** + * 每天凌晨2点执行清理回收站任务 + */ + @Scheduled(cron = "0 0 2 * * ?") + public void cleanupExpiredTrashFiles() { + System.out.println("[TrashCleanupTask] 开始清理回收站过期文件,保留天数: " + retentionDays); + int deletedCount = fileService.deleteExpiredTrashFiles(retentionDays); + System.out.println("[TrashCleanupTask] 清理完成,共删除 " + deletedCount + " 个文件"); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e858e6f..bbadbc8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,24 +3,24 @@ server: spring: datasource: - url: jdbc:mysql://127.0.0.1:13306/chat_app?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&createDatabaseIfNotExist=true - username: root - password: root_dream + url: jdbc:mysql://192.168.31.182:13306/system?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&createDatabaseIfNotExist=true + username: dream + password: info_dream driver-class-name: com.mysql.cj.jdbc.Driver data: redis: - host: 127.0.0.1 - port: 6379 - database: 0 + host: 192.168.31.194 + port: 16379 + database: 9 timeout: 10000ms password: admin servlet: multipart: enabled: true - max-file-size: 1024MB - max-request-size: 2048MB + max-file-size: 512MB + max-request-size: 1024MB file-size-threshold: 0 mybatis-plus: @@ -36,6 +36,10 @@ mybatis-plus: file: storage: path: /ogsapp/uploads + user: + storage-limit-gb: 200 + trash: + retention-days: 30 jwt: secret: mySecretKeyForJWTTokenGenerationThatIsLongEnough256BitsForHS256Algorithm diff --git a/web-vue/package-lock.json b/web-vue/package-lock.json index 5c9b6fa..a21e891 100644 --- a/web-vue/package-lock.json +++ b/web-vue/package-lock.json @@ -886,7 +886,6 @@ "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/lodash": "*" } @@ -1422,15 +1421,13 @@ "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash-es": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash-unified": { "version": "1.0.3", @@ -1637,7 +1634,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -1697,7 +1693,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz", "integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/compiler-sfc": "3.5.31", diff --git a/web-vue/src/api/request.js b/web-vue/src/api/request.js index e3d55ac..bd5a6ca 100644 --- a/web-vue/src/api/request.js +++ b/web-vue/src/api/request.js @@ -2,7 +2,7 @@ const request = axios.create({ baseURL: '/api', - timeout: 300000 + timeout: 0 // 不限制超时,大文件上传需要 }) request.interceptors.request.use( diff --git a/web-vue/src/api/user.js b/web-vue/src/api/user.js index 0c1f45e..937b21a 100644 --- a/web-vue/src/api/user.js +++ b/web-vue/src/api/user.js @@ -1,3 +1,5 @@ import request from './request' export const getUsers = () => request.get('/users') + +export const getSystemConfig = () => request.get('/users/config') diff --git a/web-vue/src/components/UploadDialog.vue b/web-vue/src/components/UploadDialog.vue index 36fa4c5..faa9143 100644 --- a/web-vue/src/components/UploadDialog.vue +++ b/web-vue/src/components/UploadDialog.vue @@ -44,6 +44,7 @@ @@ -52,7 +53,7 @@ 取消 - + 开始上传 @@ -90,6 +91,12 @@ const isOverLimit = computed(() => { return totalSize.value > props.remainingStorage }) +// 是否超过500MB限制 +const MAX_UPLOAD_SIZE = 500 * 1024 * 1024 // 500MB +const isOverSizeLimit = computed(() => { + return totalSize.value > MAX_UPLOAD_SIZE +}) + const handleChange = (file, list) => { if (list.length > 10) { ElMessage.warning('最多一次上传10个文件') @@ -124,6 +131,11 @@ const handleUpload = () => { return } + if (isOverSizeLimit.value) { + ElMessage.warning('单次上传总大小不能超过500MB') + return + } + emit('upload', fileList.value.map(f => f.raw)) } diff --git a/web-vue/src/store/user.js b/web-vue/src/store/user.js index 18074fe..90c07f7 100644 --- a/web-vue/src/store/user.js +++ b/web-vue/src/store/user.js @@ -1,5 +1,6 @@ import { defineStore } from 'pinia' import { ref, computed } from 'vue' +import { getSystemConfig } from '@/api/user' export const useUserStore = defineStore('user', () => { const token = ref(localStorage.getItem('token') || '') @@ -11,7 +12,7 @@ export const useUserStore = defineStore('user', () => { const phone = ref(localStorage.getItem('phone') || '') const email = ref(localStorage.getItem('email') || '') const storageUsed = ref(Number(localStorage.getItem('storageUsed')) || 0) - const storageLimit = ref(Number(localStorage.getItem('storageLimit')) || 20 * 1024 * 1024 * 1024) + const storageLimit = ref(Number(localStorage.getItem('storageLimit')) || 0) const isLoggedIn = computed(() => !!token.value) @@ -54,11 +55,23 @@ export const useUserStore = defineStore('user', () => { localStorage.setItem('storageUsed', storageUsed.value) } if (user.storageLimit !== undefined) { - storageLimit.value = Number(user.storageLimit) || 20 * 1024 * 1024 * 1024 + storageLimit.value = Number(user.storageLimit) || 0 localStorage.setItem('storageLimit', storageLimit.value) } } + const fetchStorageLimit = async () => { + try { + const res = await getSystemConfig() + if (res.data?.storageLimitBytes) { + storageLimit.value = res.data.storageLimitBytes + localStorage.setItem('storageLimit', storageLimit.value) + } + } catch (e) { + console.error('获取存储配额失败', e) + } + } + const logout = () => { token.value = '' username.value = '' @@ -69,7 +82,7 @@ export const useUserStore = defineStore('user', () => { phone.value = '' email.value = '' storageUsed.value = 0 - storageLimit.value = 20 * 1024 * 1024 * 1024 + storageLimit.value = 0 localStorage.removeItem('token') localStorage.removeItem('username') localStorage.removeItem('userId') @@ -82,5 +95,5 @@ export const useUserStore = defineStore('user', () => { localStorage.removeItem('storageLimit') } - return { token, username, userId, nickname, signature, avatar, phone, email, storageUsed, storageLimit, isLoggedIn, setToken, setUser, logout } + return { token, username, userId, nickname, signature, avatar, phone, email, storageUsed, storageLimit, isLoggedIn, setToken, setUser, fetchStorageLimit, logout } }) diff --git a/web-vue/src/views/files/index.vue b/web-vue/src/views/files/index.vue index 2241ed5..3f1b4c3 100644 --- a/web-vue/src/views/files/index.vue +++ b/web-vue/src/views/files/index.vue @@ -225,7 +225,7 @@ const movableFolders = ref([]) // 存储 —— 真实数据 const storagePercent = computed(() => { - const limit = userStore.storageLimit || 20 * 1024 * 1024 * 1024 + const limit = userStore.storageLimit || 0 const used = userStore.storageUsed || 0 if (limit <= 0) return 0 return Math.min(Math.round((used / limit) * 100), 100) @@ -237,13 +237,14 @@ const usedStorage = computed(() => { : (used / (1024 * 1024)).toFixed(2) + ' MB' }) const totalStorage = computed(() => { - const limit = userStore.storageLimit || 20 * 1024 * 1024 * 1024 + const limit = userStore.storageLimit || 0 + if (limit <= 0) return '—' return (limit / (1024 * 1024 * 1024)).toFixed(0) + ' GB' }) // 剩余存储空间(字节) const remainingStorage = computed(() => { - const limit = userStore.storageLimit || 20 * 1024 * 1024 * 1024 + const limit = userStore.storageLimit || 0 const used = userStore.storageUsed || 0 return Math.max(0, limit - used) }) @@ -251,12 +252,14 @@ const remainingStorage = computed(() => { // 刷新存储数据(从后端精确重算) const refreshStorage = async () => { try { + // 先获取系统配置(存储配额) + await userStore.fetchStorageLimit() + // 再获取用户当前用量 const res = await getCurrentUser() const data = res.data if (data) { userStore.setUser({ - storageUsed: data.storageUsed ?? 0, - storageLimit: data.storageLimit ?? 20 * 1024 * 1024 * 1024 + storageUsed: data.storageUsed ?? 0 }) } } catch (e) { diff --git a/web-vue/src/views/login/index.vue b/web-vue/src/views/login/index.vue index 38f6f4f..0223ae8 100644 --- a/web-vue/src/views/login/index.vue +++ b/web-vue/src/views/login/index.vue @@ -17,7 +17,7 @@ - 20GB 超大存储空间 + {{ storageLimitText }} 存储空间 @@ -58,17 +58,40 @@