diff --git a/src/main/java/com/filesystem/service/FileService.java b/src/main/java/com/filesystem/service/FileService.java index ad08385..6aeff5e 100644 --- a/src/main/java/com/filesystem/service/FileService.java +++ b/src/main/java/com/filesystem/service/FileService.java @@ -50,19 +50,18 @@ public class FileService { boolean hasKeyword = keyword != null && !keyword.isEmpty(); + // 根目录:folderId == null 或 0 都视为根目录,统一用 0 查询 + Long actualFolderId = (folderId == null || folderId == 0L) ? 0L : folderId; + if (hasKeyword) { // 有搜索关键词:根目录搜索查所有,子目录搜索限当前目录 - if (folderId != null) { + if (!actualFolderId.equals(0L)) { wrapper.eq(FileEntity::getFolderId, folderId); } wrapper.like(FileEntity::getName, keyword); } else { // 无搜索关键词:正常浏览当前目录 - if (folderId != null) { - wrapper.eq(FileEntity::getFolderId, folderId); - } else { - wrapper.isNull(FileEntity::getFolderId); - } + wrapper.eq(FileEntity::getFolderId, actualFolderId); } wrapper.orderByDesc(FileEntity::getIsFolder) @@ -77,10 +76,13 @@ public class FileService { .eq(FileEntity::getIsDeleted, 1) .orderByDesc(FileEntity::getDeletedAt); - if (folderId != null) { - wrapper.eq(FileEntity::getFolderId, folderId); + // 根目录:folderId == null 或 0 都视为根目录 + Long actualFolderId = (folderId == null || folderId == 0L) ? 0L : folderId; + // 根目录在数据库中兼容 null 和 0 两种值 + if (actualFolderId.equals(0L)) { + wrapper.and(w -> w.eq(FileEntity::getFolderId, 0L).or().isNull(FileEntity::getFolderId)); } else { - wrapper.isNull(FileEntity::getFolderId); + wrapper.eq(FileEntity::getFolderId, actualFolderId); } return fileMapper.selectList(wrapper); @@ -93,26 +95,45 @@ public class FileService { @Transactional 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 明确指定要更新的字段 + if (file == null || !file.getUserId().equals(userId)) return; + + if (file.getIsFolder() != null && file.getIsFolder() == 1) { + // 文件夹:递归将所有子文件标记删除,移动到回收站根目录 + moveChildrenToTrash(id, userId); + } + + // 当前文件/文件夹:移动到回收站根目录 + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(FileEntity::getId, id) + .set(FileEntity::getFolderId, 0L) + .set(FileEntity::getIsDeleted, 1) + .set(FileEntity::getDeletedAt, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + fileMapper.update(null, wrapper); + } + + /** + * 递归将文件夹下所有子文件/子文件夹标记为删除(folderId 保持不变) + */ + private void moveChildrenToTrash(Long parentFolderId, Long userId) { + List children = fileMapper.selectList( + new LambdaQueryWrapper() + .eq(FileEntity::getFolderId, parentFolderId) + .eq(FileEntity::getIsDeleted, 0) + .eq(FileEntity::getUserId, userId) + ); + + for (FileEntity child : children) { + // 子文件/子文件夹只标删除,folderId 保持原值不变 LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); - wrapper.eq(FileEntity::getId, id) + wrapper.eq(FileEntity::getId, child.getId()) .set(FileEntity::getIsDeleted, 1) .set(FileEntity::getDeletedAt, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); fileMapper.update(null, wrapper); + + if (child.getIsFolder() != null && child.getIsFolder() == 1) { + // 递归处理子文件夹 + moveChildrenToTrash(child.getId(), userId); + } } } @@ -121,21 +142,29 @@ public class FileService { FileEntity file = fileMapper.selectById(id); if (file == null || !file.getUserId().equals(userId)) return; - // 检查父文件夹是否在回收站,如果是,将文件移到根目录 - Long folderId = file.getFolderId(); - if (folderId != null) { - FileEntity parentFolder = fileMapper.selectById(folderId); - if (parentFolder != null && parentFolder.getIsDeleted() == 1) { - // 父文件夹在回收站,将文件移到根目录 - folderId = null; + // 检查父文件夹状态,决定还原目标 + // - folderId == 0 或 null:根目录,还原到根目录(folderId=0) + // - 父文件夹存在且未删除:还原到原父文件夹 + // - 父文件夹不存在或已删除:还原到根目录(folderId=0) + Long originalFolderId = file.getFolderId(); + Long targetFolderId = originalFolderId; + + if (targetFolderId != null && !targetFolderId.equals(0L)) { + FileEntity parentFolder = fileMapper.selectById(targetFolderId); + if (parentFolder == null || parentFolder.getIsDeleted() == 1) { + // 父文件夹不在了,还原到根目录 + targetFolderId = 0L; } + } else if (targetFolderId == null || targetFolderId.equals(0L)) { + // 本身就是根目录 + targetFolderId = 0L; } LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); wrapper.eq(FileEntity::getId, id) .set(FileEntity::getIsDeleted, 0) .set(FileEntity::getDeletedAt, null) - .set(FileEntity::getFolderId, folderId); + .set(FileEntity::getFolderId, targetFolderId); fileMapper.update(null, wrapper); // 如果是文件夹,递归还原所有子文件 @@ -169,28 +198,9 @@ public class FileService { FileEntity file = fileMapper.selectById(id); if (file == null || !file.getUserId().equals(userId)) return; - // 如果是文件夹,检查是否有未删除的子文件 if (file.getIsFolder() != null && file.getIsFolder() == 1) { - Long undeletedCount = fileMapper.selectCount( - new LambdaQueryWrapper() - .eq(FileEntity::getFolderId, id) - .eq(FileEntity::getIsDeleted, 0) - .eq(FileEntity::getUserId, userId) - ); - if (undeletedCount != null && undeletedCount > 0) { - throw new RuntimeException("请先处理该目录下的子文件后重试"); - } - - // 检查是否有已删除的子文件(在回收站里的) - Long deletedChildrenCount = fileMapper.selectCount( - new LambdaQueryWrapper() - .eq(FileEntity::getFolderId, id) - .eq(FileEntity::getIsDeleted, 1) - .eq(FileEntity::getUserId, userId) - ); - if (deletedChildrenCount != null && deletedChildrenCount > 0) { - throw new RuntimeException("请先处理该目录下的子文件后重试"); - } + // 递归彻底删除所有子文件/子文件夹 + deleteChildrenPermanently(id, userId); } // 删除当前文件的物理文件并扣减存储 @@ -208,11 +218,58 @@ public class FileService { fileMapper.deleteById(id); } + /** + * 递归彻底删除文件夹下所有子文件/子文件夹 + */ + private void deleteChildrenPermanently(Long parentFolderId, Long userId) { + List children = fileMapper.selectList( + new LambdaQueryWrapper() + .eq(FileEntity::getFolderId, parentFolderId) + .eq(FileEntity::getIsDeleted, 1) + .eq(FileEntity::getUserId, userId) + ); + + for (FileEntity child : children) { + if (child.getIsFolder() != null && child.getIsFolder() == 1) { + deleteChildrenPermanently(child.getId(), userId); + } + + // 删除物理文件并扣减存储 + if (child.getPath() != null && !child.getPath().isEmpty()) { + try { + Path filePath = Paths.get(storagePath).toAbsolutePath().resolve("files").resolve(child.getPath()); + Files.deleteIfExists(filePath); + } catch (IOException e) { + // ignore + } + if (child.getSize() != null && child.getSize() > 0) { + userService.decreaseStorage(userId, child.getSize()); + } + } + fileMapper.deleteById(child.getId()); + } + } + @Transactional public void emptyTrash(Long userId) { List trashFiles = getTrashFiles(userId, null); + + // 找出回收站里所有文件夹的 ID + java.util.Set folderIds = trashFiles.stream() + .filter(f -> f.getIsFolder() != null && f.getIsFolder() == 1) + .map(FileEntity::getId) + .collect(java.util.stream.Collectors.toSet()); + for (FileEntity file : trashFiles) { - deletePermanently(file.getId(), userId); + // 跳过子文件(folderId 指向的父文件夹也在回收站中,会被级联删除) + if (folderIds.contains(file.getFolderId())) { + continue; + } + try { + deletePermanently(file.getId(), userId); + } catch (Exception e) { + // ignore,级联删除时子文件可能已不存在 + } } } @@ -223,7 +280,8 @@ public class FileService { folder.setType("folder"); folder.setIsFolder(1); folder.setUserId(userId); - folder.setFolderId(parentId); + // 根目录 parentId = null 或 0 → 统一用 0 + folder.setFolderId((parentId == null || parentId == 0L) ? 0L : parentId); folder.setSize(0L); folder.setIsShared(0); folder.setIsDeleted(0); @@ -498,8 +556,13 @@ public class FileService { throw new RuntimeException("无法移动回收站中的文件"); } - // 检查目标文件夹是否存在(如果不是根目录) - if (targetFolderId != null) { + // 规范化 targetFolderId:null → 0L(根目录) + if (targetFolderId == null) { + targetFolderId = 0L; + } + + // 检查目标文件夹是否存在(如果不是根目录,folderId=0 视为根目录) + if (!targetFolderId.equals(0L)) { FileEntity targetFolder = fileMapper.selectById(targetFolderId); if (targetFolder == null) { throw new RuntimeException("目标文件夹不存在"); @@ -529,20 +592,22 @@ public class FileService { } // 检查目标位置是否已有同名文件 - FileEntity existing = fileMapper.selectOne( - new LambdaQueryWrapper() - .eq(FileEntity::getUserId, userId) - .eq(FileEntity::getFolderId, targetFolderId) - .eq(FileEntity::getName, file.getName()) - .eq(FileEntity::getIsDeleted, 0) - .ne(FileEntity::getId, fileId) - ); + LambdaQueryWrapper dupWrapper = new LambdaQueryWrapper() + .eq(FileEntity::getUserId, userId) + .eq(FileEntity::getName, file.getName()) + .eq(FileEntity::getIsDeleted, 0) + .ne(FileEntity::getId, fileId); + + // targetFolderId 为 null 或 0 → 根目录,统一用 eq(0L) + Long actualTarget = (targetFolderId == null || targetFolderId == 0L) ? 0L : targetFolderId; + dupWrapper.eq(FileEntity::getFolderId, actualTarget); + + FileEntity existing = fileMapper.selectOne(dupWrapper); if (existing != null) { throw new RuntimeException("目标位置已存在同名文件"); } - file.setFolderId(targetFolderId); - // 使用直接更新确保 null 值也能被设置 + // 使用直接更新,folderId=0 表示根目录,null 表示"我的文档"原始状态(已在 moveToTrash 时处理) fileMapper.update(null, new LambdaUpdateWrapper() .eq(FileEntity::getId, fileId) @@ -700,9 +765,13 @@ public class FileService { 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())); + // 匹配父级关系:parentId == null 或 0 都视为根目录,匹配 folderId == 0 的文件夹 + boolean isChildOfParent; + if (parentId == null || parentId == 0L) { + isChildOfParent = (folder.getFolderId() != null && folder.getFolderId() == 0L); + } else { + isChildOfParent = parentId.equals(folder.getFolderId()); + } if (isChildOfParent) { // 递归构建子文件夹 diff --git a/web-vue/src/components/BatchMoveDialog.vue b/web-vue/src/components/BatchMoveDialog.vue index 13b5ece..61d3c6b 100644 --- a/web-vue/src/components/BatchMoveDialog.vue +++ b/web-vue/src/components/BatchMoveDialog.vue @@ -50,7 +50,7 @@ const visible = computed({ set: (val) => emit('update:modelValue', val) }) -const targetFolderId = ref(null) +const targetFolderId = ref(0) // 树形配置 const treeProps = { @@ -64,7 +64,7 @@ const folderTreeData = computed(() => { // 根节点 const rootNodes = [ { - id: 'root', + id: 0, name: '根目录', children: [] } @@ -90,20 +90,21 @@ const folderTreeData = computed(() => { }) const handleConfirm = () => { + // targetFolderId = 0 表示根目录,null 表示未选择 emit('confirm', targetFolderId.value) visible.value = false - targetFolderId.value = null + targetFolderId.value = 0 } const open = () => { - targetFolderId.value = null + targetFolderId.value = 0 visible.value = true } // 重置选中状态 watch(visible, (val) => { if (!val) { - targetFolderId.value = null + targetFolderId.value = 0 } }) diff --git a/web-vue/src/views/files/index.vue b/web-vue/src/views/files/index.vue index 33da2e4..e284def 100644 --- a/web-vue/src/views/files/index.vue +++ b/web-vue/src/views/files/index.vue @@ -623,14 +623,8 @@ const handleBatchMove = async () => { } const handleConfirmBatchMove = async (targetFolderId) => { - // Handle target folder ID - let finalFolderId = null - - if (targetFolderId === 'root' || targetFolderId === '' || targetFolderId === null || targetFolderId === undefined) { - finalFolderId = null - } else { - finalFolderId = targetFolderId - } + // targetFolderId = 0 表示根目录,null 表示未选择(此时用 0 作为默认值) + let finalFolderId = (targetFolderId === null || targetFolderId === undefined) ? 0 : targetFolderId let successCount = 0 let failCount = 0