diff --git a/.idea/misc.xml b/.idea/misc.xml index 1e86ad3..1f93b88 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/controller/FileController.java b/src/main/java/com/filesystem/controller/FileController.java index 0600417..eedcc36 100644 --- a/src/main/java/com/filesystem/controller/FileController.java +++ b/src/main/java/com/filesystem/controller/FileController.java @@ -307,4 +307,52 @@ public class FileController { List folders = fileService.getMovableFolders(principal.getUserId(), excludeIds, currentFolderId); return ResponseEntity.ok(Map.of("data", folders)); } + + @PostMapping("/batchCancelShare") + public ResponseEntity batchCancelShare( + @AuthenticationPrincipal UserPrincipal principal, + @RequestBody Map request) { + Object idsObj = request.get("ids"); + if (idsObj == null) { + return ResponseEntity.badRequest().body(Map.of("message", "请选择要取消共享的文件")); + } + List ids = new ArrayList<>(); + if (idsObj instanceof List) { + for (Object id : (List) idsObj) { + if (id instanceof Number) { + ids.add(((Number) id).longValue()); + } else if (id instanceof String) { + ids.add(Long.parseLong((String) id)); + } + } + } + for (Long fileId : ids) { + fileService.cancelShare(fileId, principal.getUserId()); + } + return ResponseEntity.ok(Map.of("message", "批量取消共享成功")); + } + + @PostMapping("/batchRestore") + public ResponseEntity batchRestore( + @AuthenticationPrincipal UserPrincipal principal, + @RequestBody Map request) { + Object idsObj = request.get("ids"); + if (idsObj == null) { + return ResponseEntity.badRequest().body(Map.of("message", "请选择要还原的文件")); + } + List ids = new ArrayList<>(); + if (idsObj instanceof List) { + for (Object id : (List) idsObj) { + if (id instanceof Number) { + ids.add(((Number) id).longValue()); + } else if (id instanceof String) { + ids.add(Long.parseLong((String) id)); + } + } + } + for (Long fileId : ids) { + fileService.restoreFile(fileId, principal.getUserId()); + } + return ResponseEntity.ok(Map.of("message", "批量还原成功")); + } } diff --git a/src/main/java/com/filesystem/entity/FileEntity.java b/src/main/java/com/filesystem/entity/FileEntity.java index 05f22a1..a432405 100644 --- a/src/main/java/com/filesystem/entity/FileEntity.java +++ b/src/main/java/com/filesystem/entity/FileEntity.java @@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import lombok.EqualsAndHashCode; +import java.util.List; + @Data @EqualsAndHashCode(callSuper = true) @TableName(value = "sys_file", autoResultMap = true) @@ -31,4 +33,10 @@ public class FileEntity extends BaseEntity { @TableField("deleted_at") private String deletedAt; + + /** + * 子文件夹列表(非数据库字段,用于树形结构) + */ + @TableField(exist = false) + private List children; } diff --git a/src/main/java/com/filesystem/service/FileService.java b/src/main/java/com/filesystem/service/FileService.java index 3c20107..ed405f9 100644 --- a/src/main/java/com/filesystem/service/FileService.java +++ b/src/main/java/com/filesystem/service/FileService.java @@ -67,18 +67,8 @@ public class FileService { public List getTrashFiles(Long userId, Long folderId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(FileEntity::getUserId, userId) - .eq(FileEntity::getIsDeleted, 1); - - if (folderId != null) { - // 查询指定文件夹下的已删除文件 - wrapper.eq(FileEntity::getFolderId, folderId); - } else { - // 查询根目录下已删除的文件和文件夹 - wrapper.isNull(FileEntity::getFolderId) - .or(w -> w.eq(FileEntity::getFolderId, 0)); - } - - wrapper.orderByDesc(FileEntity::getDeletedAt); + .eq(FileEntity::getIsDeleted, 1) + .orderByDesc(FileEntity::getDeletedAt); return fileMapper.selectList(wrapper); } @@ -90,6 +80,19 @@ public class FileService { public void moveToTrash(Long id, Long userId) { FileEntity file = fileMapper.selectById(id); if (file != null && file.getUserId().equals(userId)) { + // 如果是文件夹,检查是否有子文件 + if (file.getIsFolder() != null && file.getIsFolder() == 1) { + Long childCount = fileMapper.selectCount( + new LambdaQueryWrapper() + .eq(FileEntity::getFolderId, id) + .eq(FileEntity::getIsDeleted, 0) + .eq(FileEntity::getUserId, userId) + ); + if (childCount != null && childCount > 0) { + throw new RuntimeException("请删除该目录下文件后重试"); + } + } + // 使用 LambdaUpdateWrapper 明确指定要更新的字段 LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); wrapper.eq(FileEntity::getId, id) @@ -509,18 +512,30 @@ public class FileService { java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(baos); + // 用于跟踪已使用的文件名,避免重复 + java.util.Set usedNames = new java.util.HashSet<>(); + for (Long fileId : fileIds) { FileEntity file = fileMapper.selectById(fileId); - if (file == null || !file.getUserId().equals(userId) || file.getIsDeleted() == 1) { + if (file == null || file.getIsDeleted() == 1) { continue; } + // 检查权限:文件所有者 或 文件被共享给当前用户(包括文件夹内的子文件) + boolean hasPermission = file.getUserId().equals(userId) || canAccessFile(fileId, userId); + if (!hasPermission) { + continue; + } + + // 确定文件夹内容的查询用户ID(如果是共享文件,用文件所有者的ID查询子文件) + Long ownerUserId = file.getUserId(); + if (file.getIsFolder() == 1) { // 递归添加文件夹内容 - addFolderToZip(zos, file, "", userId); + addFolderToZip(zos, file, "", ownerUserId, usedNames); } else { // 添加单个文件 - addFileToZip(zos, file, ""); + addFileToZip(zos, file, "", usedNames); } } @@ -528,7 +543,44 @@ public class FileService { return baos.toByteArray(); } - private void addFolderToZip(java.util.zip.ZipOutputStream zos, FileEntity folder, String parentPath, Long userId) throws IOException { + /** + * 检查用户是否有权限访问文件(包括共享文件夹内的子文件) + */ + private boolean canAccessFile(Long fileId, Long userId) { + FileEntity file = fileMapper.selectById(fileId); + if (file == null) return false; + + // 直接共享给用户 + if (isFileSharedToUser(fileId, userId)) { + return true; + } + + // 检查父文件夹是否被共享 + Long parentId = file.getFolderId(); + while (parentId != null) { + if (isFileSharedToUser(parentId, userId)) { + return true; + } + FileEntity parent = fileMapper.selectById(parentId); + parentId = parent != null ? parent.getFolderId() : null; + } + + return false; + } + + /** + * 检查文件是否被共享给指定用户 + */ + private boolean isFileSharedToUser(Long fileId, Long userId) { + Long count = fileShareMapper.selectCount( + new LambdaQueryWrapper() + .eq(FileShare::getFileId, fileId) + .eq(FileShare::getShareToUserId, userId) + ); + return count != null && count > 0; + } + + private void addFolderToZip(java.util.zip.ZipOutputStream zos, FileEntity folder, String parentPath, Long userId, java.util.Set usedNames) throws IOException { String folderPath = parentPath + folder.getName() + "/"; // 获取文件夹下的所有内容 @@ -541,21 +593,41 @@ public class FileService { for (FileEntity child : children) { if (child.getIsFolder() == 1) { - addFolderToZip(zos, child, folderPath, userId); + addFolderToZip(zos, child, folderPath, userId, usedNames); } else { - addFileToZip(zos, child, folderPath); + addFileToZip(zos, child, folderPath, usedNames); } } } - private void addFileToZip(java.util.zip.ZipOutputStream zos, FileEntity file, String parentPath) throws IOException { + private void addFileToZip(java.util.zip.ZipOutputStream zos, FileEntity file, String parentPath, java.util.Set usedNames) throws IOException { if (file.getPath() == null) return; Path filePath = Paths.get(storagePath).toAbsolutePath().resolve("files").resolve(file.getPath()); if (!Files.exists(filePath)) return; + // 生成唯一的 ZIP 条目名称 String zipEntryName = parentPath + file.getName(); - java.util.zip.ZipEntry zipEntry = new java.util.zip.ZipEntry(zipEntryName); + String finalName = zipEntryName; + int counter = 1; + + // 如果名称已存在,添加序号 + while (usedNames.contains(finalName)) { + // 处理文件名和扩展名 + String baseName = zipEntryName; + String extension = ""; + int dotIndex = zipEntryName.lastIndexOf("."); + if (dotIndex > 0 && dotIndex > parentPath.length()) { + baseName = zipEntryName.substring(0, dotIndex); + extension = zipEntryName.substring(dotIndex); + } + finalName = baseName + " (" + counter + ")" + extension; + counter++; + } + + usedNames.add(finalName); + + java.util.zip.ZipEntry zipEntry = new java.util.zip.ZipEntry(finalName); zos.putNextEntry(zipEntry); byte[] fileBytes = Files.readAllBytes(filePath); @@ -569,17 +641,35 @@ public class FileService { .eq(FileEntity::getIsDeleted, 0) .eq(FileEntity::getIsFolder, 1); - // 只查询当前目录下的文件夹(同级) - if (currentFolderId == null) { - wrapper.isNull(FileEntity::getFolderId); - } else { - wrapper.eq(FileEntity::getFolderId, currentFolderId); - } - if (excludeIds != null && !excludeIds.isEmpty()) { wrapper.notIn(FileEntity::getId, excludeIds); } - return fileMapper.selectList(wrapper); + // 查询用户所有文件夹 + List allFolders = fileMapper.selectList(wrapper); + + // 构建树形结构 + return buildFolderTree(allFolders, null, excludeIds); + } + + /** + * 构建文件夹树形结构 + */ + private List buildFolderTree(List allFolders, Long parentId, List excludeIds) { + List tree = new ArrayList<>(); + for (FileEntity folder : allFolders) { + // 匹配父级关系 + boolean isChildOfParent = (parentId == null && folder.getFolderId() == null) + || (parentId != null && parentId.equals(folder.getFolderId())); + + if (isChildOfParent) { + // 递归构建子文件夹 + List children = buildFolderTree(allFolders, folder.getId(), excludeIds); + // 设置子文件夹列表(通过 transient 字段) + folder.setChildren(children.isEmpty() ? null : children); + tree.add(folder); + } + } + return tree; } } diff --git a/web-vue/package-lock.json b/web-vue/package-lock.json index a21e891..5c9b6fa 100644 --- a/web-vue/package-lock.json +++ b/web-vue/package-lock.json @@ -886,6 +886,7 @@ "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": "*" } @@ -1421,13 +1422,15 @@ "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "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" + "license": "MIT", + "peer": true }, "node_modules/lodash-unified": { "version": "1.0.3", @@ -1634,6 +1637,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -1693,6 +1697,7 @@ "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/App.vue b/web-vue/src/App.vue index a3cf66c..59e6246 100644 --- a/web-vue/src/App.vue +++ b/web-vue/src/App.vue @@ -1,4 +1,4 @@ -