diff --git a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/controller/FileController.java b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/controller/FileController.java index 4fb3afeaeb993ec5904f1d06d82779d88d1dbe3e..a74a7415ef1adcd9e36e548ede0ce6fc4109feda 100644 --- a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/controller/FileController.java +++ b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/controller/FileController.java @@ -9,6 +9,7 @@ import com.xddcodec.fs.file.domain.vo.FileDetailVO; import com.xddcodec.fs.file.domain.vo.FileRecycleVO; import com.xddcodec.fs.file.domain.vo.FileVO; import com.xddcodec.fs.file.service.FileInfoService; +import com.xddcodec.fs.file.service.FileRecycleService; import com.xddcodec.fs.file.service.FileUserFavoritesService; import com.xddcodec.fs.framework.common.domain.Result; import com.xddcodec.fs.framework.common.exception.StorageOperationException; @@ -47,6 +48,9 @@ public class FileController { @Autowired private FileInfoService fileInfoService; + @Autowired + private FileRecycleService fileRecycleService; + @Autowired private FileUserFavoritesService fileUserFavoritesService; @@ -116,7 +120,7 @@ public class FileController { @DeleteMapping() @Operation(summary = "移到回收站", description = "将文件移动到回收站") public Result deleteFiles(@RequestBody List fileIds) { - fileInfoService.deleteFiles(fileIds); + fileInfoService.moveFilesToRecycleBin(fileIds); return Result.ok(); } @@ -152,28 +156,28 @@ public class FileController { @GetMapping("/recycles") @Operation(summary = "获取回收站列表", description = "获取回收站列表") public Result getRecycles(String keyword) { - List list = fileInfoService.getRecycles(keyword); + List list = fileRecycleService.getRecycles(keyword); return Result.ok(list); } @PutMapping("/recycles") @Operation(summary = "恢复文件", description = "从回收站批量恢复文件") public Result restoreFile(@RequestBody List fileIds) { - fileInfoService.restoreFiles(fileIds); + fileRecycleService.restoreFiles(fileIds); return Result.ok(); } @DeleteMapping("/recycles") @Operation(summary = "永久删除文件", description = "永久删除文件,不可恢复") public Result permanentlyDeleteFiles(@RequestBody List fileIds) { - fileInfoService.permanentlyDeleteFiles(fileIds); + fileRecycleService.permanentlyDeleteFiles(fileIds); return Result.ok(); } @DeleteMapping("/recycles/clear") @Operation(summary = "清空回收站", description = "清空回收站,永久删除所有文件") public Result clearRecycles() { - fileInfoService.clearRecycles(); + fileRecycleService.clearRecycles(); return Result.ok(); } diff --git a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/controller/FileHomeController.java b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/controller/FileHomeController.java new file mode 100644 index 0000000000000000000000000000000000000000..a4d78c1f8c8b8243734100ba3895564dd66510e4 --- /dev/null +++ b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/controller/FileHomeController.java @@ -0,0 +1,32 @@ +package com.xddcodec.fs.file.controller; + +import com.xddcodec.fs.file.domain.vo.FileHomeVO; +import com.xddcodec.fs.file.service.FileHomeService; +import com.xddcodec.fs.framework.common.domain.Result; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Validated +@Slf4j +@RestController +@RequestMapping("/apis/home") +@Tag(name = "文件首页", description = "首页") +public class FileHomeController { + + @Autowired + private FileHomeService fileHomeService; + + @GetMapping("/info") + @Operation(summary = "查询首页信息", description = "查询首页信息") + public Result getHomes() { + FileHomeVO homeVO = fileHomeService.getFileHomes(); + return Result.ok(homeVO); + } + +} diff --git a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/domain/vo/FileHomeVO.java b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/domain/vo/FileHomeVO.java new file mode 100644 index 0000000000000000000000000000000000000000..b9b16666b2eb0e17085c99a84b5cf1ccdcec7eff --- /dev/null +++ b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/domain/vo/FileHomeVO.java @@ -0,0 +1,21 @@ +package com.xddcodec.fs.file.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +@Data +public class FileHomeVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long usedStorage; + private Long fileCount; + private Long directoryCount; + private Long favoriteCount; + private Long shareCount; + private List recentFiles; +} diff --git a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/schedule/RecycleBinCleanupTask.java b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/schedule/RecycleBinCleanupTask.java index fd29036068181bc1c26c727a37c613267f9930a8..65bf9f43f0b7de37fc706a39f3c9890161d54f6c 100644 --- a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/schedule/RecycleBinCleanupTask.java +++ b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/schedule/RecycleBinCleanupTask.java @@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil; import com.mybatisflex.core.query.QueryWrapper; import com.xddcodec.fs.file.domain.FileInfo; import com.xddcodec.fs.file.service.FileInfoService; +import com.xddcodec.fs.file.service.FileRecycleService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; @@ -29,6 +30,7 @@ import static com.xddcodec.fs.file.domain.table.FileInfoTableDef.FILE_INFO; public class RecycleBinCleanupTask { private final FileInfoService fileInfoService; + private final FileRecycleService recycleService; /** * 定时清理回收站 @@ -50,7 +52,7 @@ public class RecycleBinCleanupTask { } List fileIds = expiredFiles.stream().map(FileInfo::getId).toList(); log.info("查询到 {} 个过期文件,开始清理", fileIds.size()); - fileInfoService.permanentlyDeleteFiles(fileIds); + recycleService.permanentlyDeleteFiles(fileIds); stopWatch.stop(); log.info("回收站清理结束, 总计: {} 个, 耗时: {} ms", fileIds.size(), stopWatch.getTotalTimeMillis()); } catch (Exception e) { diff --git a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/FileHomeService.java b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/FileHomeService.java new file mode 100644 index 0000000000000000000000000000000000000000..ab6979b1fd49c8902609c6112a5926f7ddfecc23 --- /dev/null +++ b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/FileHomeService.java @@ -0,0 +1,13 @@ +package com.xddcodec.fs.file.service; + +import com.xddcodec.fs.file.domain.vo.FileHomeVO; + +public interface FileHomeService { + + /** + * 获取文件仪表盘信息 + * + * @return + */ + FileHomeVO getFileHomes(); +} diff --git a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/FileInfoService.java b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/FileInfoService.java index fc5cb9c3c3c918f4a6850e6a98000de5098f128d..f302f5096a14c175b625dba39bd11aca1437b0b4 100644 --- a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/FileInfoService.java +++ b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/FileInfoService.java @@ -7,7 +7,6 @@ import com.xddcodec.fs.file.domain.dto.RenameFileCmd; import com.xddcodec.fs.file.domain.qry.FileQry; import com.mybatisflex.core.service.IService; import com.xddcodec.fs.file.domain.vo.FileDetailVO; -import com.xddcodec.fs.file.domain.vo.FileRecycleVO; import com.xddcodec.fs.file.domain.vo.FileVO; import java.io.InputStream; @@ -39,12 +38,12 @@ public interface FileInfoService extends IService { String getFileUrl(String fileId, Integer expireSeconds); /** - * 删除文件 + * 放入回收站 * * @param fileIds 文件ID集合 * @return 是否删除成功 */ - void deleteFiles(List fileIds); + void moveFilesToRecycleBin(List fileIds); /** * 创建目录 @@ -103,6 +102,13 @@ public interface FileInfoService extends IService { */ List getList(FileQry qry); + /** + * 计算已使用的存储空间 + * + * @return + */ + Long calculateUsedStorage(); + /** * 查询文件详情 * @@ -126,32 +132,4 @@ public interface FileInfoService extends IService { * @return */ List getByFileIds(List fileIds); - - /** - * 查询回收站文件列表 - * - * @return - */ - List getRecycles(String keyword); - - /** - * 恢复已删除的文件 - * - * @param fileIds 文件ID集合 - * @return - */ - void restoreFiles(List fileIds); - - /** - * 永久删除文件 - * - * @param fileIds 文件ID集合 - * @return - */ - void permanentlyDeleteFiles(List fileIds); - - /** - * 清空回收站 - */ - void clearRecycles(); } \ No newline at end of file diff --git a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/FileRecycleService.java b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/FileRecycleService.java new file mode 100644 index 0000000000000000000000000000000000000000..4fe637f7b84b08355cb05ed58a35f89e7e8be06d --- /dev/null +++ b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/FileRecycleService.java @@ -0,0 +1,42 @@ +package com.xddcodec.fs.file.service; + +import com.xddcodec.fs.file.domain.vo.FileRecycleVO; + +import java.util.List; + +/** + * 文件回收站服务接口 + * + * @Author: xddcode + * @Date: 2025/5/8 9:35 + */ +public interface FileRecycleService { + + /** + * 查询回收站文件列表 + * + * @return + */ + List getRecycles(String keyword); + + /** + * 恢复已删除的文件 + * + * @param fileIds 文件ID集合 + * @return + */ + void restoreFiles(List fileIds); + + /** + * 永久删除文件 + * + * @param fileIds 文件ID集合 + * @return + */ + void permanentlyDeleteFiles(List fileIds); + + /** + * 清空回收站 + */ + void clearRecycles(); +} diff --git a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/FileUserFavoritesService.java b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/FileUserFavoritesService.java index f3df7d482e80dd99b7a4576f8165b0839c0bdf8a..b171218445e27b8514ec7995eaaa1db4e141138c 100644 --- a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/FileUserFavoritesService.java +++ b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/FileUserFavoritesService.java @@ -3,6 +3,7 @@ package com.xddcodec.fs.file.service; import com.xddcodec.fs.file.domain.FileUserFavorites; import com.mybatisflex.core.service.IService; +import java.util.Collection; import java.util.List; /** @@ -28,4 +29,19 @@ public interface FileUserFavoritesService extends IService { * @return 是否取消收藏成功 */ void unFavoritesFile(List fileIds); + + /** + * 根据文件ID批量删除收藏记录 + * + * @param fileIds 文件ID列表 + * @param userId 用户ID + */ + void removeByFileIds(Collection fileIds, String userId); + + /** + * 获取用户收藏文件数量 + * + * @return + */ + Long getFavoritesCount(); } diff --git a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/impl/FileHomeServiceImpl.java b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/impl/FileHomeServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..36c3343d5d474841d36ca254680ea6e6fee9492d --- /dev/null +++ b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/impl/FileHomeServiceImpl.java @@ -0,0 +1,69 @@ +package com.xddcodec.fs.file.service.impl; + +import cn.dev33.satoken.stp.StpUtil; +import com.mybatisflex.core.query.QueryWrapper; +import com.xddcodec.fs.file.domain.FileInfo; +import com.xddcodec.fs.file.domain.qry.FileQry; +import com.xddcodec.fs.file.domain.vo.FileHomeVO; +import com.xddcodec.fs.file.domain.vo.FileVO; +import com.xddcodec.fs.file.service.FileHomeService; +import com.xddcodec.fs.file.service.FileInfoService; +import com.xddcodec.fs.file.service.FileShareService; +import com.xddcodec.fs.file.service.FileUserFavoritesService; +import com.xddcodec.fs.storage.plugin.core.context.StoragePlatformContextHolder; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static com.xddcodec.fs.file.domain.table.FileInfoTableDef.FILE_INFO; +import static com.xddcodec.fs.file.domain.table.FileShareTableDef.FILE_SHARE; + +@Service +@RequiredArgsConstructor +public class FileHomeServiceImpl implements FileHomeService { + + private final FileInfoService fileInfoService; + + private final FileShareService fileShareService; + + private final FileUserFavoritesService fileUserFavoritesService; + + @Override + public FileHomeVO getFileHomes() { + String userId = StpUtil.getLoginIdAsString(); + String storagePlatformSettingId = StoragePlatformContextHolder.getConfigId(); + FileHomeVO fileHomeVO = new FileHomeVO(); + List fileInfoList = fileInfoService.list(new QueryWrapper() + .where(FILE_INFO.USER_ID.eq(userId) + .and(FILE_INFO.STORAGE_PLATFORM_SETTING_ID.eq(storagePlatformSettingId)) + .and(FILE_INFO.IS_DELETED.eq(false)) + )); + long directoryCount = fileInfoList.stream() + .filter(FileInfo::getIsDir) + .count(); + long fileCount = fileInfoList.size() - directoryCount; + fileHomeVO.setFileCount(fileCount); + fileHomeVO.setDirectoryCount(directoryCount); + + // 统计已使用容量 + Long usedStorage = fileInfoService.calculateUsedStorage(); + fileHomeVO.setUsedStorage(usedStorage); + + //查询已分享数量 + Long shareCount = fileShareService.count( + new QueryWrapper().where(FILE_SHARE.USER_ID.eq(userId)) + ); + fileHomeVO.setShareCount(shareCount); + //查询收藏文件数量 + Long favoriteCount = fileUserFavoritesService.getFavoritesCount(); + fileHomeVO.setFavoriteCount(favoriteCount); + + //查询用户最近使用的文件 + FileQry fileQry = new FileQry(); + fileQry.setIsRecents(Boolean.TRUE); + List recentFiles = fileInfoService.getList(fileQry); + fileHomeVO.setRecentFiles(recentFiles); + return fileHomeVO; + } +} diff --git a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/impl/FileInfoServiceImpl.java b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/impl/FileInfoServiceImpl.java index d8892d6fdafa161dd4303e8f3dfdd3f031f94920..f733e7dbc7ba036c9ab93131b070364ef9fa61b2 100644 --- a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/impl/FileInfoServiceImpl.java +++ b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/impl/FileInfoServiceImpl.java @@ -4,7 +4,6 @@ import cn.dev33.satoken.stp.StpUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; -import com.mybatisflex.core.update.UpdateChain; import com.mybatisflex.core.util.UpdateEntity; import com.xddcodec.fs.file.domain.FileInfo; import com.xddcodec.fs.file.domain.dto.CreateDirectoryCmd; @@ -12,7 +11,6 @@ import com.xddcodec.fs.file.domain.dto.MoveFileCmd; import com.xddcodec.fs.file.domain.dto.RenameFileCmd; import com.xddcodec.fs.file.domain.qry.FileQry; import com.xddcodec.fs.file.domain.vo.FileDetailVO; -import com.xddcodec.fs.file.domain.vo.FileRecycleVO; import com.xddcodec.fs.file.domain.vo.FileVO; import com.xddcodec.fs.file.mapper.FileInfoMapper; import com.xddcodec.fs.file.service.FileInfoService; @@ -30,13 +28,10 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.TransactionSynchronization; -import org.springframework.transaction.support.TransactionSynchronizationManager; import java.io.InputStream; import java.time.LocalDateTime; import java.util.*; -import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -44,7 +39,6 @@ import java.util.stream.Collectors; import static com.xddcodec.fs.file.domain.table.FileInfoTableDef.FILE_INFO; import static com.xddcodec.fs.file.domain.table.FileUserFavoritesTableDef.FILE_USER_FAVORITES; - /** * 文件资源服务实现类 * @@ -110,7 +104,7 @@ public class FileInfoServiceImpl extends ServiceImpl i @Override @Transactional(rollbackFor = Exception.class) - public void deleteFiles(List fileIds) { + public void moveFilesToRecycleBin(List fileIds) { if (fileIds == null || fileIds.isEmpty()) { return; } @@ -408,113 +402,6 @@ public class FileInfoServiceImpl extends ServiceImpl i return pathList; } - - @Override - @Transactional(rollbackFor = Exception.class) - public void restoreFiles(List fileIds) { - if (CollUtil.isEmpty(fileIds)) { - return; - } - - String userId = StpUtil.getLoginIdAsString(); - - Set allFileIds = collectFileIdsRecursively( - fileIds, - userId, - wrapper -> wrapper.and(FILE_INFO.IS_DELETED.eq(true)) // 只收集已删除的 - ); - - if (CollUtil.isEmpty(allFileIds)) { - throw new BusinessException("未找到要恢复的文件或文件夹"); - } - - // 批量恢复 - UpdateChain.of(FileInfo.class) - .set(FileInfo::getIsDeleted, false) - .set(FileInfo::getDeletedTime, null) - .where(FILE_INFO.ID.in(allFileIds)) - .and(FILE_INFO.USER_ID.eq(userId)) - .update(); - - log.info("用户 {} 恢复文件/文件夹,共 {} 项", userId, allFileIds.size()); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void permanentlyDeleteFiles(List fileIds) { - if (CollUtil.isEmpty(fileIds)) { - return; - } - String userId = StpUtil.getLoginIdAsString(); - Set allFileIds = collectFileIdsRecursively( - fileIds, - userId, - wrapper -> wrapper.and(FILE_INFO.IS_DELETED.eq(true)) // 只能删除回收站中的 - ); - if (CollUtil.isEmpty(allFileIds)) { - throw new BusinessException("未找到要删除的文件或文件夹"); - } - List allFiles = listByIds(allFileIds); - // 找出需要删除物理文件的(没有其他引用的) - List physicalFilesToDelete = new ArrayList<>(); - for (FileInfo file : allFiles) { - if (StrUtil.isBlank(file.getObjectKey())) { - continue; - } - - // 查询除了本次要删除的文件外,还有没有其他文件引用这个objectKey - long count = this.count(new QueryWrapper() - .where(FILE_INFO.OBJECT_KEY.eq(file.getObjectKey()) - .and(FILE_INFO.ID.notIn(allFileIds)))); - - if (count == 0) { - physicalFilesToDelete.add(file); - } - } - - removeByIds(allFileIds); - - TransactionSynchronizationManager.registerSynchronization( - new TransactionSynchronization() { - @Override - public void afterCommit() { - for (FileInfo file : physicalFilesToDelete) { - try { - deletePhysicalFile(file); - } catch (Exception e) { - log.error("删除物理文件失败: {}", file.getObjectKey(), e); - } - } - } - } - ); - } - - @Transactional(rollbackFor = Exception.class) - @Override - public void clearRecycles() { - String userId = StpUtil.getLoginIdAsString(); - String storagePlatformSettingId = StoragePlatformContextHolder.getConfigId(); - List deletedFiles = this.list(new QueryWrapper() - .where(FILE_INFO.USER_ID.eq(userId) - .and(FILE_INFO.IS_DELETED.eq(true) - .and(FILE_INFO.STORAGE_PLATFORM_SETTING_ID.eq(storagePlatformSettingId)) - )) - ); - List deletedFileIds = deletedFiles.stream().map(FileInfo::getId).toList(); - this.permanentlyDeleteFiles(deletedFileIds); - } - - /** - * 删除物理文件 - * - * @param file 文件信息 - */ - private void deletePhysicalFile(FileInfo file) { - IStorageOperationService storageService = storageServiceFacade.getStorageService(file.getStoragePlatformSettingId()); - storageService.deleteFile(file.getObjectKey()); - } - @Override public List getList(FileQry qry) { String userId = StpUtil.getLoginIdAsString(); @@ -574,6 +461,27 @@ public class FileInfoServiceImpl extends ServiceImpl i return this.listAs(wrapper, FileVO.class); } + @Override + public Long calculateUsedStorage() { + String userId = StpUtil.getLoginIdAsString(); + String storagePlatformSettingId = StoragePlatformContextHolder.getConfigId(); + + // 查询所有未删除的文件 + List fileInfoList = this.list(new QueryWrapper() + .where(FILE_INFO.USER_ID.eq(userId) + .and(FILE_INFO.STORAGE_PLATFORM_SETTING_ID.eq(storagePlatformSettingId)) + .and(FILE_INFO.IS_DELETED.eq(false)) + .and(FILE_INFO.IS_DIR.eq(false)) // 只统计文件,不统计文件夹 + )); + + // 统计总大小 + return fileInfoList.stream() + .map(FileInfo::getSize) + .filter(Objects::nonNull) + .mapToLong(Long::longValue) + .sum(); + } + @Override public FileDetailVO getFileDetails(String fileId) { return null; @@ -653,7 +561,6 @@ public class FileInfoServiceImpl extends ServiceImpl i } } - /** * 按具体类型筛选 */ @@ -675,100 +582,4 @@ public class FileInfoServiceImpl extends ServiceImpl i } } } - - @Override - public List getRecycles(String keyword) { - String userId = StpUtil.getLoginIdAsString(); - String storagePlatformSettingId = StoragePlatformContextHolder.getConfigId(); - QueryWrapper wrapper = new QueryWrapper(); - wrapper.where(FILE_INFO.USER_ID.eq(userId)); - wrapper.and(FILE_INFO.IS_DELETED.eq(true)); - wrapper.and(FILE_INFO.STORAGE_PLATFORM_SETTING_ID.eq(storagePlatformSettingId)); - if (StrUtil.isNotBlank(keyword)) { - keyword = "%" + keyword.trim() + "%"; - wrapper.and( - FILE_INFO.ORIGINAL_NAME.like(keyword) - .or(FILE_INFO.DISPLAY_NAME.like(keyword)) - ); - } - List fileInfos = this.list(wrapper); - return converter.convert(fileInfos, FileRecycleVO.class); - } - - /** - * 递归收集文件ID(通用方法) - * - * @param fileIds 初始文件ID列表 - * @param userId 用户ID - * @param filter 过滤条件(可选) - * @return 收集到的所有文件ID集合 - */ - private Set collectFileIdsRecursively( - List fileIds, - String userId, - Consumer filter) { - - // 查询初始文件列表 - QueryWrapper initialWrapper = new QueryWrapper() - .where(FILE_INFO.ID.in(fileIds)) - .and(FILE_INFO.USER_ID.eq(userId)); - - // 应用额外过滤条件 - if (filter != null) { - filter.accept(initialWrapper); - } - - List files = list(initialWrapper); - - if (CollUtil.isEmpty(files)) { - return Collections.emptySet(); - } - - // 递归收集 - Set allFileIds = new HashSet<>(); - files.forEach(file -> { - collectFileIdsRecursive(file, allFileIds, userId, filter); - }); - - return allFileIds; - } - - /** - * 递归收集单个文件及其子文件的ID - * - * @param file 文件信息 - * @param allFileIds 收集的文件ID集合 - * @param userId 用户ID - * @param filter 过滤条件(可选) - */ - private void collectFileIdsRecursive( - FileInfo file, - Set allFileIds, - String userId, - Consumer filter) { - - // 添加当前文件ID - allFileIds.add(file.getId()); - - // 如果是文件夹,递归处理子项 - if (file.getIsDir()) { - log.debug("收集文件夹 {} 的子项", file.getDisplayName()); - - // 构建查询条件 - QueryWrapper wrapper = new QueryWrapper() - .where(FILE_INFO.PARENT_ID.eq(file.getId())) - .and(FILE_INFO.USER_ID.eq(userId)); - - // 应用额外过滤条件 - if (filter != null) { - filter.accept(wrapper); - } - - // 查询所有子文件 - List children = list(wrapper); - - // 递归收集子项ID - children.forEach(child -> collectFileIdsRecursive(child, allFileIds, userId, filter)); - } - } } diff --git a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/impl/FileRecycleServiceImpl.java b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/impl/FileRecycleServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..fd37ee1a26d359f89b08e60e203602d3ae77fc48 --- /dev/null +++ b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/impl/FileRecycleServiceImpl.java @@ -0,0 +1,254 @@ +package com.xddcodec.fs.file.service.impl; + +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.core.update.UpdateChain; +import com.xddcodec.fs.file.domain.FileInfo; +import com.xddcodec.fs.file.domain.vo.FileRecycleVO; +import com.xddcodec.fs.file.service.FileInfoService; +import com.xddcodec.fs.file.service.FileRecycleService; +import com.xddcodec.fs.file.service.FileUserFavoritesService; +import com.xddcodec.fs.framework.common.exception.BusinessException; +import com.xddcodec.fs.storage.facade.StorageServiceFacade; +import com.xddcodec.fs.storage.plugin.core.IStorageOperationService; +import com.xddcodec.fs.storage.plugin.core.context.StoragePlatformContextHolder; +import io.github.linpeilie.Converter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.util.*; +import java.util.function.Consumer; + +import static com.xddcodec.fs.file.domain.table.FileInfoTableDef.FILE_INFO; + +/** + * 回收站服务接口实现 + * + * @Author: xddcode + * @Date: 2025/5/8 9:35 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class FileRecycleServiceImpl implements FileRecycleService { + + private final Converter converter; + + private final FileInfoService fileInfoService; + + private final FileUserFavoritesService fileUserFavoritesService; + + private final StorageServiceFacade storageServiceFacade; + + @Override + public List getRecycles(String keyword) { + String userId = StpUtil.getLoginIdAsString(); + String storagePlatformSettingId = StoragePlatformContextHolder.getConfigId(); + QueryWrapper wrapper = new QueryWrapper(); + wrapper.where(FILE_INFO.USER_ID.eq(userId)); + wrapper.and(FILE_INFO.IS_DELETED.eq(true)); + wrapper.and(FILE_INFO.STORAGE_PLATFORM_SETTING_ID.eq(storagePlatformSettingId)); + if (StrUtil.isNotBlank(keyword)) { + keyword = "%" + keyword.trim() + "%"; + wrapper.and( + FILE_INFO.ORIGINAL_NAME.like(keyword) + .or(FILE_INFO.DISPLAY_NAME.like(keyword)) + ); + } + List fileInfos = fileInfoService.list(wrapper); + return converter.convert(fileInfos, FileRecycleVO.class); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void restoreFiles(List fileIds) { + if (CollUtil.isEmpty(fileIds)) { + return; + } + + String userId = StpUtil.getLoginIdAsString(); + + Set allFileIds = collectFileIdsRecursively( + fileIds, + userId, + wrapper -> wrapper.and(FILE_INFO.IS_DELETED.eq(true)) // 只收集已删除的 + ); + + if (CollUtil.isEmpty(allFileIds)) { + throw new BusinessException("未找到要恢复的文件或文件夹"); + } + + // 批量恢复 + UpdateChain.of(FileInfo.class) + .set(FileInfo::getIsDeleted, false) + .set(FileInfo::getDeletedTime, null) + .where(FILE_INFO.ID.in(allFileIds)) + .and(FILE_INFO.USER_ID.eq(userId)) + .update(); + + log.info("用户 {} 恢复文件/文件夹,共 {} 项", userId, allFileIds.size()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void permanentlyDeleteFiles(List fileIds) { + if (CollUtil.isEmpty(fileIds)) { + return; + } + String userId = StpUtil.getLoginIdAsString(); + Set allFileIds = collectFileIdsRecursively( + fileIds, + userId, + wrapper -> wrapper.and(FILE_INFO.IS_DELETED.eq(true)) // 只能删除回收站中的 + ); + if (CollUtil.isEmpty(allFileIds)) { + throw new BusinessException("未找到要删除的文件或文件夹"); + } + List allFiles = fileInfoService.listByIds(allFileIds); + // 找出需要删除物理文件的(没有其他引用的) + List physicalFilesToDelete = new ArrayList<>(); + for (FileInfo file : allFiles) { + if (StrUtil.isBlank(file.getObjectKey())) { + continue; + } + + // 查询除了本次要删除的文件外,还有没有其他文件引用这个objectKey + long count = fileInfoService.count(new QueryWrapper() + .where(FILE_INFO.OBJECT_KEY.eq(file.getObjectKey()) + .and(FILE_INFO.ID.notIn(allFileIds)))); + + if (count == 0) { + physicalFilesToDelete.add(file); + } + } + + // 删除文件信息记录 + fileInfoService.removeByIds(allFileIds); + + // 删除用户收藏记录 + fileUserFavoritesService.removeByFileIds(allFileIds, userId); + + TransactionSynchronizationManager.registerSynchronization( + new TransactionSynchronization() { + @Override + public void afterCommit() { + for (FileInfo file : physicalFilesToDelete) { + try { + deletePhysicalFile(file); + } catch (Exception e) { + log.error("删除物理文件失败: {}", file.getObjectKey(), e); + } + } + } + } + ); + } + + /** + * 删除物理文件 + * + * @param file 文件信息 + */ + private void deletePhysicalFile(FileInfo file) { + IStorageOperationService storageService = storageServiceFacade.getStorageService(file.getStoragePlatformSettingId()); + storageService.deleteFile(file.getObjectKey()); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void clearRecycles() { + String userId = StpUtil.getLoginIdAsString(); + String storagePlatformSettingId = StoragePlatformContextHolder.getConfigId(); + List deletedFiles = fileInfoService.list(new QueryWrapper() + .where(FILE_INFO.USER_ID.eq(userId) + .and(FILE_INFO.IS_DELETED.eq(true) + .and(FILE_INFO.STORAGE_PLATFORM_SETTING_ID.eq(storagePlatformSettingId)) + )) + ); + List deletedFileIds = deletedFiles.stream().map(FileInfo::getId).toList(); + this.permanentlyDeleteFiles(deletedFileIds); + } + + /** + * 递归收集文件ID(通用方法) + * + * @param fileIds 初始文件ID列表 + * @param userId 用户ID + * @param filter 过滤条件(可选) + * @return 收集到的所有文件ID集合 + */ + private Set collectFileIdsRecursively( + List fileIds, + String userId, + Consumer filter) { + + // 查询初始文件列表 + QueryWrapper initialWrapper = new QueryWrapper() + .where(FILE_INFO.ID.in(fileIds)) + .and(FILE_INFO.USER_ID.eq(userId)); + + // 应用额外过滤条件 + if (filter != null) { + filter.accept(initialWrapper); + } + + List files = fileInfoService.list(initialWrapper); + + if (CollUtil.isEmpty(files)) { + return Collections.emptySet(); + } + + // 递归收集 + Set allFileIds = new HashSet<>(); + files.forEach(file -> { + collectFileIdsRecursive(file, allFileIds, userId, filter); + }); + + return allFileIds; + } + + /** + * 递归收集单个文件及其子文件的ID + * + * @param file 文件信息 + * @param allFileIds 收集的文件ID集合 + * @param userId 用户ID + * @param filter 过滤条件(可选) + */ + private void collectFileIdsRecursive( + FileInfo file, + Set allFileIds, + String userId, + Consumer filter) { + + // 添加当前文件ID + allFileIds.add(file.getId()); + + // 如果是文件夹,递归处理子项 + if (file.getIsDir()) { + log.debug("收集文件夹 {} 的子项", file.getDisplayName()); + + // 构建查询条件 + QueryWrapper wrapper = new QueryWrapper() + .where(FILE_INFO.PARENT_ID.eq(file.getId())) + .and(FILE_INFO.USER_ID.eq(userId)); + + // 应用额外过滤条件 + if (filter != null) { + filter.accept(wrapper); + } + + // 查询所有子文件 + List children = fileInfoService.list(wrapper); + + // 递归收集子项ID + children.forEach(child -> collectFileIdsRecursive(child, allFileIds, userId, filter)); + } + } +} diff --git a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/impl/FileUserFavoritesServiceImpl.java b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/impl/FileUserFavoritesServiceImpl.java index b6b58d873b87f9de6252ed9bdbb1e34875b59026..ea6ef5413cd650edb8944416ab0e17148ab0dc3f 100644 --- a/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/impl/FileUserFavoritesServiceImpl.java +++ b/fs-modules/fs-file/src/main/java/com/xddcodec/fs/file/service/impl/FileUserFavoritesServiceImpl.java @@ -9,12 +9,14 @@ import com.xddcodec.fs.file.service.FileUserFavoritesService; import com.xddcodec.fs.framework.common.exception.BusinessException; import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.spring.service.impl.ServiceImpl; +import com.xddcodec.fs.storage.plugin.core.context.StoragePlatformContextHolder; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.Collection; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -121,4 +123,27 @@ public class FileUserFavoritesServiceImpl extends ServiceImpl fileIds, String userId) { + if (CollUtil.isEmpty(fileIds)) { + return; + } + this.remove(new QueryWrapper() + .where(FILE_USER_FAVORITES.FILE_ID.in(fileIds)) + .and(FILE_USER_FAVORITES.USER_ID.eq(userId))); + } + + @Override + public Long getFavoritesCount() { + String userId = StpUtil.getLoginIdAsString(); + String storagePlatformSettingId = StoragePlatformContextHolder.getConfigId(); + return this.count(new QueryWrapper() + .from(FILE_USER_FAVORITES) + .leftJoin(FILE_INFO).on(FILE_USER_FAVORITES.FILE_ID.eq(FILE_INFO.ID)) + .where(FILE_USER_FAVORITES.USER_ID.eq(userId)) + .and(FILE_INFO.STORAGE_PLATFORM_SETTING_ID.eq(storagePlatformSettingId)) + .and(FILE_INFO.IS_DELETED.eq(false)) // 只统计未删除的文件 + ); + } }