From 996e69dde30fae9efd68e0ed8f3e0d48934ab045 Mon Sep 17 00:00:00 2001 From: gulongcheng <474084054@qq.com> Date: Thu, 5 Feb 2026 14:53:39 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E6=96=87=E4=BB=B6=E5=9B=9E=E6=94=B6?= =?UTF-8?q?=E7=AB=99=E5=8A=9F=E8=83=BD=EF=BC=9A=E5=88=A0=E9=99=A4=E5=88=B0?= =?UTF-8?q?=E5=9B=9E=E6=94=B6=E7=AB=99=EF=BC=8C7=E5=A4=A9(=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E6=97=B6=E9=97=B4)=E5=90=8E=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=88=A0=E9=99=A4=EF=BC=9B=E5=9B=9E=E6=94=B6=E7=AB=99=E4=B8=AD?= =?UTF-8?q?=E4=B9=9F=E5=8F=AF=E4=BB=A5=E6=89=8B=E5=8A=A8=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/file_metadata_info_recycle.sql | 12 ++ .../entity/req/data/ListRecycleBinReq.java | 19 ++ .../data/PermanentDeleteFromRecycleReq.java | 13 ++ .../req/data/RestoreFromRecycleReq.java | 13 ++ .../data/controller/DataFileController.java | 40 +++- .../data/model/entity/FileMetadataInfo.java | 10 + .../schedule/RecycleBinCleanupSchedule.java | 117 +++++++++++ .../sdm/data/service/IDataFileService.java | 27 +++ .../impl/MinioFileIDataFileServiceImpl.java | 198 +++++++++++++++--- .../dataFileHandle/DeleteApproveStrategy.java | 73 +++---- data/src/main/resources/application-test.yml | 6 + .../mapper/FileMetadataInfoMapper.xml | 3 + .../resources/mapper/FileStorageMapper.xml | 1 + 13 files changed, 450 insertions(+), 82 deletions(-) create mode 100644 1-sql/2026-02-05/data/file_metadata_info_recycle.sql create mode 100644 common/src/main/java/com/sdm/common/entity/req/data/ListRecycleBinReq.java create mode 100644 common/src/main/java/com/sdm/common/entity/req/data/PermanentDeleteFromRecycleReq.java create mode 100644 common/src/main/java/com/sdm/common/entity/req/data/RestoreFromRecycleReq.java create mode 100644 data/src/main/java/com/sdm/data/schedule/RecycleBinCleanupSchedule.java diff --git a/1-sql/2026-02-05/data/file_metadata_info_recycle.sql b/1-sql/2026-02-05/data/file_metadata_info_recycle.sql new file mode 100644 index 00000000..ddcb4c16 --- /dev/null +++ b/1-sql/2026-02-05/data/file_metadata_info_recycle.sql @@ -0,0 +1,12 @@ +-- 为 file_metadata_info 表添加回收站相关字段 +-- 添加时间: 2026-02-05 +-- 说明: 支持文件回收站功能,删除的文件先移到回收站,7天后自动删除 + +ALTER TABLE `file_metadata_info` +ADD COLUMN `deletedAt` datetime DEFAULT NULL COMMENT '删除时间(移入回收站的时间),NULL表示未删除', +ADD COLUMN `recycleExpireAt` datetime DEFAULT NULL COMMENT '回收站过期时间(超过此时间将自动物理删除),NULL表示未删除'; + +-- 添加索引以优化回收站查询和定时清理任务 +ALTER TABLE `file_metadata_info` +ADD INDEX `idx_deleted_at` (`deletedAt`) COMMENT '回收站查询索引', +ADD INDEX `idx_recycle_expire_at` (`recycleExpireAt`) COMMENT '回收站过期清理索引'; diff --git a/common/src/main/java/com/sdm/common/entity/req/data/ListRecycleBinReq.java b/common/src/main/java/com/sdm/common/entity/req/data/ListRecycleBinReq.java new file mode 100644 index 00000000..0dd9e4d9 --- /dev/null +++ b/common/src/main/java/com/sdm/common/entity/req/data/ListRecycleBinReq.java @@ -0,0 +1,19 @@ +package com.sdm.common.entity.req.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "回收站列表查询请求") +public class ListRecycleBinReq extends BaseReq { + @Schema(description = "文件/目录名称(模糊查询)") + private String fileName; + + @Schema(description = "目录类型(1 知识库文件夹,2 项目节点文件夹,3 头像库文件夹,4 仿真参数库文件夹,5 训练模型文件夹)") + private Integer dirType; + + @Schema(description = "数据类型:1-文件夹,2-文件") + private Integer dataType; +} diff --git a/common/src/main/java/com/sdm/common/entity/req/data/PermanentDeleteFromRecycleReq.java b/common/src/main/java/com/sdm/common/entity/req/data/PermanentDeleteFromRecycleReq.java new file mode 100644 index 00000000..51f11965 --- /dev/null +++ b/common/src/main/java/com/sdm/common/entity/req/data/PermanentDeleteFromRecycleReq.java @@ -0,0 +1,13 @@ +package com.sdm.common.entity.req.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +@Schema(description = "从回收站彻底删除请求") +public class PermanentDeleteFromRecycleReq { + @Schema(description = "要彻底删除的文件/目录ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "文件/目录ID不能为空") + private Long id; +} diff --git a/common/src/main/java/com/sdm/common/entity/req/data/RestoreFromRecycleReq.java b/common/src/main/java/com/sdm/common/entity/req/data/RestoreFromRecycleReq.java new file mode 100644 index 00000000..0e21bb2c --- /dev/null +++ b/common/src/main/java/com/sdm/common/entity/req/data/RestoreFromRecycleReq.java @@ -0,0 +1,13 @@ +package com.sdm.common.entity.req.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +@Schema(description = "从回收站还原请求") +public class RestoreFromRecycleReq { + @Schema(description = "要还原的文件/目录ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "文件/目录ID不能为空") + private Long id; +} diff --git a/data/src/main/java/com/sdm/data/controller/DataFileController.java b/data/src/main/java/com/sdm/data/controller/DataFileController.java index 73142ca1..b4431c42 100644 --- a/data/src/main/java/com/sdm/data/controller/DataFileController.java +++ b/data/src/main/java/com/sdm/data/controller/DataFileController.java @@ -128,11 +128,49 @@ public class DataFileController implements IDataFeignClient { */ @SysLog("删除文件") @PostMapping("/delFile") - @Operation(summary = "删除文件", description = "根据请求参数删除指定的文件") + @Operation(summary = "删除文件", description = "根据请求参数删除指定的文件(移入回收站)") public SdmResponse delFile(@RequestBody @Validated DelFileReq req) { return IDataFileService.delFile(req); } + /** + * 查询回收站列表 + * + * @param req + * @return + */ + @PostMapping("/listRecycleBin") + @Operation(summary = "查询回收站列表", description = "查询当前租户的回收站文件/目录列表") + public SdmResponse listRecycleBin(@RequestBody @Validated ListRecycleBinReq req) { + return IDataFileService.listRecycleBin(req); + } + + /** + * 从回收站还原 + * + * @param req + * @return + */ + @SysLog("从回收站还原") + @PostMapping("/restoreFromRecycle") + @Operation(summary = "从回收站还原", description = "将文件/目录从回收站还原到原位置") + public SdmResponse restoreFromRecycle(@RequestBody @Validated RestoreFromRecycleReq req) { + return IDataFileService.restoreFromRecycle(req); + } + + /** + * 从回收站彻底删除 + * + * @param req + * @return + */ + @SysLog("从回收站彻底删除") + @PostMapping("/permanentDeleteFromRecycle") + @Operation(summary = "从回收站彻底删除", description = "从回收站彻底删除文件/目录(物理删除,不可恢复)") + public SdmResponse permanentDeleteFromRecycle(@RequestBody @Validated PermanentDeleteFromRecycleReq req) { + return IDataFileService.permanentDeleteFromRecycle(req); + } + /** * 搜索文件 * diff --git a/data/src/main/java/com/sdm/data/model/entity/FileMetadataInfo.java b/data/src/main/java/com/sdm/data/model/entity/FileMetadataInfo.java index 85deec4b..a5805af8 100644 --- a/data/src/main/java/com/sdm/data/model/entity/FileMetadataInfo.java +++ b/data/src/main/java/com/sdm/data/model/entity/FileMetadataInfo.java @@ -206,6 +206,16 @@ public class FileMetadataInfo implements Serializable { @TableField(value = "templateId") private String templateId; + @Schema(description= "删除时间(移入回收站的时间),NULL表示未删除") + @TableField("deletedAt") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime deletedAt; + + @Schema(description= "回收站过期时间(超过此时间将自动物理删除),NULL表示未删除") + @TableField("recycleExpireAt") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime recycleExpireAt; + @Schema(description= "cidFlowReviewer:cid审核电子流程里面的评审人,只有列表展示使用") @TableField(value = "cidFlowReviewer", insertStrategy = FieldStrategy.NEVER,select = false,updateStrategy = FieldStrategy.NEVER) private String cidFlowReviewer; diff --git a/data/src/main/java/com/sdm/data/schedule/RecycleBinCleanupSchedule.java b/data/src/main/java/com/sdm/data/schedule/RecycleBinCleanupSchedule.java new file mode 100644 index 00000000..c0bab95d --- /dev/null +++ b/data/src/main/java/com/sdm/data/schedule/RecycleBinCleanupSchedule.java @@ -0,0 +1,117 @@ +package com.sdm.data.schedule; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.sdm.data.model.entity.FileMetadataExtension; +import com.sdm.data.model.entity.FileMetadataInfo; +import com.sdm.data.model.entity.FileSimulationMapping; +import com.sdm.data.model.entity.FileStorage; +import com.sdm.data.model.entity.FileTagRel; +import com.sdm.data.model.entity.FileUserPermission; +import com.sdm.data.service.IFileMetadataExtensionService; +import com.sdm.data.service.IFileMetadataInfoService; +import com.sdm.data.service.IFileStorageService; +import com.sdm.data.service.IFileTagRelService; +import com.sdm.data.service.IFileUserPermissionService; +import com.sdm.data.service.IMinioService; +import com.sdm.data.service.IFileSimulationMappingService; +import com.sdm.data.service.impl.MinioFileIDataFileServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 回收站自动清理定时任务 + * 定期清理回收站中已过期的文件/目录 + */ +@Component +@Slf4j +public class RecycleBinCleanupSchedule { + + @Autowired + private IFileMetadataInfoService fileMetadataInfoService; + + @Autowired + private IMinioService minioService; + + @Autowired + private IFileStorageService fileStorageService; + + @Autowired + private IFileMetadataExtensionService fileMetadataExtensionService; + + @Autowired + private IFileUserPermissionService fileUserPermissionService; + + @Autowired + private IFileTagRelService fileTagRelService; + + @Autowired + private IFileSimulationMappingService fileSimulationMappingService; + + @Autowired + private MinioFileIDataFileServiceImpl minioFileIDataFileService; + + /** + * 定时清理回收站中已过期的文件/目录 + * 默认每天凌晨2点执行 + */ + @Scheduled(cron = "${data.recycle.cleanup-cron:0 0 2 * * ?}") + public void cleanupExpiredRecycleBin() { + log.info("开始执行回收站自动清理任务"); + try { + LocalDateTime now = LocalDateTime.now(); + + // 查询所有已过期的回收站项 + List expiredItems = fileMetadataInfoService.lambdaQuery() + .isNotNull(FileMetadataInfo::getDeletedAt) + .le(FileMetadataInfo::getRecycleExpireAt, now) + .list(); + + if (CollectionUtils.isEmpty(expiredItems)) { + log.info("回收站中没有已过期的文件/目录"); + return; + } + + log.info("发现 {} 个已过期的回收站项,开始物理删除", expiredItems.size()); + + int successCount = 0; + int failCount = 0; + + for (FileMetadataInfo item : expiredItems) { + try { + // 如果是目录,需要递归删除所有子项 + if (item.getDataType() != null && item.getDataType() == 1) { // 1表示目录 + minioFileIDataFileService.executeDirectoryDeletion( + item.getId(), + item.getObjectKey(), + item.getBucketName() + ); + } else { + // 单文件物理删除 + minioService.deleteFile(item.getObjectKey(), item.getBucketName()); + fileMetadataInfoService.removeById(item.getId()); + // 删除关联数据 + fileStorageService.remove(new LambdaQueryWrapper().eq(FileStorage::getFileId, item.getId())); + fileMetadataExtensionService.remove(new LambdaQueryWrapper().eq(FileMetadataExtension::getTFilemetaId, item.getId())); + fileUserPermissionService.remove(new LambdaQueryWrapper().eq(FileUserPermission::getTFilemetaId, item.getId())); + fileSimulationMappingService.remove(new LambdaQueryWrapper().eq(FileSimulationMapping::getFileId, item.getId())); + fileTagRelService.remove(new LambdaQueryWrapper().eq(FileTagRel::getFileId, item.getId())); + } + successCount++; + } catch (Exception e) { + log.error("删除回收站过期项失败: id={}, objectKey={}", item.getId(), item.getObjectKey(), e); + failCount++; + } + } + + log.info("回收站自动清理任务执行完成: 成功={}, 失败={}", successCount, failCount); + } catch (Exception e) { + log.error("回收站自动清理任务执行失败", e); + } + } +} diff --git a/data/src/main/java/com/sdm/data/service/IDataFileService.java b/data/src/main/java/com/sdm/data/service/IDataFileService.java index a55207c6..b0170ab3 100644 --- a/data/src/main/java/com/sdm/data/service/IDataFileService.java +++ b/data/src/main/java/com/sdm/data/service/IDataFileService.java @@ -128,6 +128,33 @@ public interface IDataFileService { */ SdmResponse fileSearch(FileSearchReq req); + /** + * 查询回收站列表 + * @param req 回收站列表查询请求参数 + * @return 回收站列表响应 + */ + default SdmResponse listRecycleBin(com.sdm.common.entity.req.data.ListRecycleBinReq req) { + return null; + } + + /** + * 从回收站还原文件/目录 + * @param req 还原请求参数 + * @return 还原结果响应 + */ + default SdmResponse restoreFromRecycle(com.sdm.common.entity.req.data.RestoreFromRecycleReq req) { + return null; + } + + /** + * 从回收站彻底删除文件/目录 + * @param req 彻底删除请求参数 + * @return 删除结果响应 + */ + default SdmResponse permanentDeleteFromRecycle(com.sdm.common.entity.req.data.PermanentDeleteFromRecycleReq req) { + return null; + } + /** * 重命名文件 * @param req 重命名文件请求参数 diff --git a/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java b/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java index d6e1e46b..92686327 100644 --- a/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java +++ b/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java @@ -62,6 +62,7 @@ import org.jetbrains.annotations.NotNull; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; @@ -102,6 +103,9 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { private static final String FLOWABLE_SIMULATION_BASEDIR = "/home/simulation/"; + @Value("${data.recycle.retention-days:7}") + private Integer recycleRetentionDays; + @Autowired private IFileMetadataInfoService fileMetadataInfoService; @@ -529,8 +533,6 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { } Long rootDirId = deleteDirMetadataInfo.getId(); - String rootDirObjectKey = deleteDirMetadataInfo.getObjectKey(); - String bucketName = deleteDirMetadataInfo.getBucketName(); Integer dirType = deleteDirMetadataInfo.getDirType(); // 1. 权限校验(仅校验根目录删除权限) @@ -546,18 +548,15 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { .eq(FileMetadataInfo::getParentId, rootDirId) .exists(); - // 3. 知识库且非空目录判定:需要走审批流程 + // 3. 知识库且非空目录:走审批流程,审批通过后由 DeleteApproveStrategy 移入回收站 boolean isKnowledgeDir = Objects.equals(DirTypeEnum.KNOWLEDGE_BASE_DIR.getValue(), dirType); if (isKnowledgeDir && hasChildren) { - // 递归收集所有待删除的子项 Set allFileIds = new HashSet<>(); Set allDirIds = new HashSet<>(); collectRecursiveIds(rootDirId, allFileIds, allDirIds); - - // 发起审批(仅传父目录元数据,但 fileIds 包含所有子项) return launchKnowledgeBaseDeletionApproval( - List.of(deleteDirMetadataInfo), // 仅父目录 - Sets.union(allFileIds,allDirIds), // allFileIds+allDirIds 所有受影响的 ID(用于批量状态更新) + List.of(deleteDirMetadataInfo), + Sets.union(allFileIds, allDirIds), deleteDirMetadataInfo, "知识库目录删除", req.getTemplateId(), @@ -565,8 +564,8 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { ); } - // 4. 非知识库目录 或 空知识库目录:直接删除 - return executeDirectoryDeletion(rootDirId, rootDirObjectKey, bucketName); + // 4. 非知识库目录 或 空知识库目录:直接移入回收站 + return moveDirectoryToRecycle(rootDirId); } catch (Exception e) { log.error("删除目录失败", e); @@ -624,9 +623,34 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { } /** - * 执行目录的物理删除(数据库 + MinIO) + * 将目录及其子项移入回收站(设置 deletedAt、recycleExpireAt) */ - private SdmResponse executeDirectoryDeletion(Long rootDirId, String rootDirObjectKey, String bucketName) { + private SdmResponse moveDirectoryToRecycle(Long rootDirId) { + Set allFileIds = new HashSet<>(); + Set allDirIds = new HashSet<>(); + collectRecursiveIds(rootDirId, allFileIds, allDirIds); + + LocalDateTime now = LocalDateTime.now(); + LocalDateTime expireAt = now.plusDays(recycleRetentionDays); + + if (CollectionUtils.isNotEmpty(allFileIds)) { + List allMetadataList = fileMetadataInfoService.listByIds(allFileIds); + allMetadataList.forEach(item -> { + item.setDeletedAt(now); + item.setRecycleExpireAt(expireAt); + item.setUpdateTime(now); + }); + fileMetadataInfoService.updateBatchById(allMetadataList); + } + + log.info("目录及子项已移入回收站: id={}, 过期时间={}", rootDirId, expireAt); + return SdmResponse.success("已移入回收站,将在" + recycleRetentionDays + "天后自动删除"); + } + + /** + * 执行目录的物理删除(数据库 + MinIO)- 仅用于回收站过期或手动彻底删除 + */ + public SdmResponse executeDirectoryDeletion(Long rootDirId, String rootDirObjectKey, String bucketName) { // 递归收集所有待删除的 ID Set allFileIds = new HashSet<>(); Set allDirIds = new HashSet<>(); @@ -642,6 +666,8 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { fileUserPermissionService.lambdaUpdate().in(FileUserPermission::getTFilemetaId, allFileIds).remove(); // 删除仿真映射 fileSimulationMappingService.lambdaUpdate().in(FileSimulationMapping::getFileId, allFileIds).remove(); + // 删除文件标签关系 + fileTagRelService.lambdaUpdate().in(FileTagRel::getFileId, allFileIds).or().in(FileTagRel::getDirId, allFileIds).remove(); // 删除存储统计(包含 fileId 关联和 dirId 关联) fileStorageService.lambdaUpdate() .in(FileStorage::getFileId, allFileIds) @@ -673,18 +699,14 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { return SdmResponse.failed("文件不存在"); } - // 文件夹 - FileMetadataInfo dirMetadataInfo = fileMetadataInfoService.lambdaQuery().eq(FileMetadataInfo::getId, deleteFileMetadataInfo.getParentId()).eq(FileMetadataInfo::getDataType, DataTypeEnum.DIRECTORY.getValue()).one(); + // 所属文件夹(用于判断是否知识库) + FileMetadataInfo dirMetadataInfo = fileMetadataInfoService.lambdaQuery() + .eq(FileMetadataInfo::getId, deleteFileMetadataInfo.getParentId()) + .eq(FileMetadataInfo::getDataType, DataTypeEnum.DIRECTORY.getValue()) + .one(); - - /*boolean hasDeletePermission = fileUserPermissionService.hasFilePermission(deleteFileMetadataInfo.getId(), ThreadLocalContext.getUserId(), FilePermissionEnum.DELETE); - if (!hasDeletePermission) { - return SdmResponse.failed("没有删除权限"); - }*/ - - // 知识库 - if(dirMetadataInfo!=null&&Objects.equals(DirTypeEnum.KNOWLEDGE_BASE_DIR.getValue(), dirMetadataInfo.getDirType())){ - // 发起审批 + // 知识库文件:走审批流程,审批通过后由 DeleteApproveStrategy 移入回收站 + if (dirMetadataInfo != null && Objects.equals(DirTypeEnum.KNOWLEDGE_BASE_DIR.getValue(), dirMetadataInfo.getDirType())) { return launchKnowledgeBaseDeletionApproval( List.of(deleteFileMetadataInfo), Set.of(delFileId), @@ -693,17 +715,16 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { req.getTemplateId(), req.getTemplateName() ); - }else{ - // 删除MinIO文件 - minioService.deleteFile(deleteFileMetadataInfo.getObjectKey(), deleteFileMetadataInfo.getBucketName()); - // 删除数据库记录 - fileMetadataInfoService.removeById(deleteFileMetadataInfo.getId()); - fileStorageService.remove(new LambdaQueryWrapper().eq(FileStorage::getFileId, deleteFileMetadataInfo.getId())); - fileMetadataExtensionService.remove(new LambdaQueryWrapper().eq(FileMetadataExtension::getTFilemetaId, deleteFileMetadataInfo.getId())); - fileUserPermissionService.remove(new LambdaQueryWrapper().eq(FileUserPermission::getTFilemetaId, deleteFileMetadataInfo.getId())); - fileTagRelService.remove(new LambdaQueryWrapper().eq(FileTagRel::getFileId, deleteFileMetadataInfo.getId())); - return SdmResponse.success("操作成功"); } + + // 非知识库文件:直接移入回收站 + LocalDateTime now = LocalDateTime.now(); + LocalDateTime expireAt = now.plusDays(recycleRetentionDays); + deleteFileMetadataInfo.setDeletedAt(now); + deleteFileMetadataInfo.setRecycleExpireAt(expireAt); + deleteFileMetadataInfo.setUpdateTime(now); + fileMetadataInfoService.updateById(deleteFileMetadataInfo); + return SdmResponse.success("已移入回收站,将在" + recycleRetentionDays + "天后自动删除"); } catch (Exception e) { log.error("删除文件失败", e); throw new RuntimeException("删除文件失败: " + e.getMessage(), e); @@ -873,6 +894,8 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { .eq(ObjectUtils.isNotEmpty(req.getQueryTarget()), FileMetadataInfo::getDataType, req.getQueryTarget()) .like(ObjectUtils.isNotEmpty(req.getFileName()), FileMetadataInfo::getOriginalName, req.getFileName()) .eq(FileMetadataInfo::getIsLatest, FileIsLastEnum.YES.getValue()) + // 排除已删除(回收站中的文件) + .isNull(FileMetadataInfo::getDeletedAt) // 审核完成 ,元数据修改审核中,文件修改审核中,删除文件审核中 .in(FileMetadataInfo::getApproveType,fileDatdList) // 文件夹在前(dataType=1),文件在后(dataType=2),同类型内按名称升序 @@ -4454,4 +4477,113 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { } } + @Override + @Transactional(rollbackFor = Exception.class) + public SdmResponse listRecycleBin(com.sdm.common.entity.req.data.ListRecycleBinReq req) { + Long tenantId = ThreadLocalContext.getTenantId(); + PageHelper.startPage(req.getCurrent(), req.getSize()); + + List list = fileMetadataInfoService.lambdaQuery() + .eq(FileMetadataInfo::getTenantId, tenantId) + .isNotNull(FileMetadataInfo::getDeletedAt) + .like(ObjectUtils.isNotEmpty(req.getFileName()), FileMetadataInfo::getOriginalName, req.getFileName()) + .eq(ObjectUtils.isNotEmpty(req.getDirType()), FileMetadataInfo::getDirType, req.getDirType()) + .eq(ObjectUtils.isNotEmpty(req.getDataType()), FileMetadataInfo::getDataType, req.getDataType()) + .orderByDesc(FileMetadataInfo::getDeletedAt) + .list(); + + setCreatorNames(list); + setCidInfos(list); + setProjectName(list); + setAnalysisDirectionName(list); + setSimulationPoolAndTaskInfo(list); + + PageInfo page = new PageInfo<>(list); + List dtoList = list.stream().map(entity -> { + FileMetadataInfoResp dto = new FileMetadataInfoResp(); + BeanUtils.copyProperties(entity, dto); + dto.setPermissionValue(fileUserPermissionService.getMergedPermission(entity.getId(), ThreadLocalContext.getUserId())); + return dto; + }).collect(Collectors.toList()); + + PageInfo page1 = new PageInfo<>(dtoList); + page1.setTotal(page.getTotal()); + return PageUtils.getJsonObjectSdmResponse(dtoList, page1); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public SdmResponse restoreFromRecycle(com.sdm.common.entity.req.data.RestoreFromRecycleReq req) { + FileMetadataInfo metadata = fileMetadataInfoService.lambdaQuery() + .eq(FileMetadataInfo::getId, req.getId()) + .isNotNull(FileMetadataInfo::getDeletedAt) + .one(); + + if (ObjectUtils.isEmpty(metadata)) { + return SdmResponse.failed("文件/目录不存在或不在回收站中"); + } + + // 如果是目录,需要递归还原所有子项 + if (Objects.equals(DataTypeEnum.DIRECTORY.getValue(), metadata.getDataType())) { + Set allFileIds = new HashSet<>(); + Set allDirIds = new HashSet<>(); + collectRecursiveIds(metadata.getId(), allFileIds, allDirIds); + + // 批量还原 + if (CollectionUtils.isNotEmpty(allFileIds)) { + List allMetadataList = fileMetadataInfoService.listByIds(allFileIds); + LocalDateTime now = LocalDateTime.now(); + allMetadataList.forEach(item -> { + item.setDeletedAt(null); + item.setRecycleExpireAt(null); + item.setUpdateTime(now); + }); + fileMetadataInfoService.updateBatchById(allMetadataList); + } + + log.info("成功从回收站还原目录及所有子项: id={}", metadata.getId()); + return SdmResponse.success("还原成功"); + } else { + // 单文件还原 + LocalDateTime now = LocalDateTime.now(); + metadata.setDeletedAt(null); + metadata.setRecycleExpireAt(null); + metadata.setUpdateTime(now); + fileMetadataInfoService.updateById(metadata); + + log.info("成功从回收站还原文件: id={}", metadata.getId()); + return SdmResponse.success("还原成功"); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public SdmResponse permanentDeleteFromRecycle(com.sdm.common.entity.req.data.PermanentDeleteFromRecycleReq req) { + FileMetadataInfo metadata = fileMetadataInfoService.lambdaQuery() + .eq(FileMetadataInfo::getId, req.getId()) + .isNotNull(FileMetadataInfo::getDeletedAt) + .one(); + + if (ObjectUtils.isEmpty(metadata)) { + return SdmResponse.failed("文件/目录不存在或不在回收站中"); + } + + // 如果是目录,需要递归物理删除所有子项 + if (Objects.equals(DataTypeEnum.DIRECTORY.getValue(), metadata.getDataType())) { + return executeDirectoryDeletion(metadata.getId(), metadata.getObjectKey(), metadata.getBucketName()); + } else { + // 单文件物理删除 + minioService.deleteFile(metadata.getObjectKey(), metadata.getBucketName()); + fileMetadataInfoService.removeById(metadata.getId()); + fileStorageService.remove(new LambdaQueryWrapper().eq(FileStorage::getFileId, metadata.getId())); + fileMetadataExtensionService.remove(new LambdaQueryWrapper().eq(FileMetadataExtension::getTFilemetaId, metadata.getId())); + fileUserPermissionService.remove(new LambdaQueryWrapper().eq(FileUserPermission::getTFilemetaId, metadata.getId())); + fileSimulationMappingService.remove(new LambdaQueryWrapper().eq(FileSimulationMapping::getFileId, metadata.getId())); + fileTagRelService.remove(new LambdaQueryWrapper().eq(FileTagRel::getFileId, metadata.getId())); + + log.info("成功从回收站彻底删除文件: id={}", metadata.getId()); + return SdmResponse.success("彻底删除成功"); + } + } + } \ No newline at end of file diff --git a/data/src/main/java/com/sdm/data/service/impl/dataFileHandle/DeleteApproveStrategy.java b/data/src/main/java/com/sdm/data/service/impl/dataFileHandle/DeleteApproveStrategy.java index e72f8e23..8026399d 100644 --- a/data/src/main/java/com/sdm/data/service/impl/dataFileHandle/DeleteApproveStrategy.java +++ b/data/src/main/java/com/sdm/data/service/impl/dataFileHandle/DeleteApproveStrategy.java @@ -19,6 +19,8 @@ import java.util.stream.Collectors; @Service @Slf4j public class DeleteApproveStrategy implements ApproveStrategy { + @org.springframework.beans.factory.annotation.Value("${data.recycle.retention-days:7}") + private Integer recycleRetentionDays; @Override public boolean handle(ApproveContext context) { FileMetadataInfo metadata = context.getApproveMetadataInfos().get(0); @@ -44,77 +46,52 @@ public class DeleteApproveStrategy implements ApproveStrategy { } /** - * 处理文件删除审批通过 + * 处理文件删除审批通过 - 移入回收站 */ private boolean handleFileDeletion(ApproveContext context, FileMetadataInfo metadata, int type) { IFileMetadataInfoService service = context.getFileMetadataInfoService(); - IMinioService minioService = context.getMinioService(); - IFileMetadataExtensionService fileMetadataExtensionService = context.getFileMetadataExtensionService(); - IFileUserPermissionService fileUserPermissionService = context.getFileUserPermissionService(); - IFileStorageService fileStorageService = context.getFileStorageService(); - ISimulationParameterLibraryCategoryObjectService paramObjectService = context.getParamObjectService(); - IFileSimulationMappingService fileSimulationMappingService = context.getFileSimulationMappingService(); - IFileTagRelService fileTagRelService = context.getFileTagRelService(); - // 删除MinIO文件 - minioService.deleteFile(metadata.getObjectKey(), metadata.getBucketName()); + // 移入回收站 + LocalDateTime now = LocalDateTime.now(); + LocalDateTime expireAt = now.plusDays(recycleRetentionDays); + metadata.setDeletedAt(now); + metadata.setRecycleExpireAt(expireAt); + metadata.setUpdateTime(now); + service.updateById(metadata); - // 删除数据库记录 - service.removeById(metadata.getId()); - fileStorageService.remove(new LambdaQueryWrapper().eq(FileStorage::getFileId, metadata.getId())); - fileMetadataExtensionService.remove(new LambdaQueryWrapper().eq(FileMetadataExtension::getTFilemetaId, metadata.getId())); - fileUserPermissionService.remove(new LambdaQueryWrapper().eq(FileUserPermission::getTFilemetaId, metadata.getId())); - fileSimulationMappingService.remove(new LambdaQueryWrapper().eq(FileSimulationMapping::getFileId, metadata.getId())); - fileTagRelService.remove(new LambdaQueryWrapper().in(FileTagRel::getFileId, metadata.getId())); - - // 如果是参数库审批 删除参数库对象 - if (ApproveTypeEnum.PARAM_APPROVE.getCode() == type) { - paramObjectService.remove(new LambdaQueryWrapper().eq(SimulationParameterLibraryCategoryObject::getFileId, metadata.getId())); - } - - log.info("审批通过,成功删除文件: id={}, objectKey={}", metadata.getId(), metadata.getObjectKey()); + log.info("审批通过,文件已移入回收站: id={}, objectKey={}, 过期时间={}", metadata.getId(), metadata.getObjectKey(), expireAt); return true; } /** - * 处理目录删除审批通过 + * 处理目录删除审批通过 - 移入回收站 */ private boolean handleDirDeletion(ApproveContext context, FileMetadataInfo rootDirMetadata) { IFileMetadataInfoService service = context.getFileMetadataInfoService(); - IMinioService minioService = context.getMinioService(); - IFileMetadataExtensionService fileMetadataExtensionService = context.getFileMetadataExtensionService(); - IFileUserPermissionService fileUserPermissionService = context.getFileUserPermissionService(); - IFileStorageService fileStorageService = context.getFileStorageService(); - IFileSimulationMappingService fileSimulationMappingService = context.getFileSimulationMappingService(); - IFileTagRelService fileTagRelService = context.getFileTagRelService(); Long rootDirId = rootDirMetadata.getId(); - String rootDirObjectKey = rootDirMetadata.getObjectKey(); - String bucketName = rootDirMetadata.getBucketName(); // 递归收集所有待删除的 ID Set allFileIds = new HashSet<>(); Set allDirIds = new HashSet<>(); collectRecursiveIds(service, rootDirId, allFileIds, allDirIds); - // 批量删除数据库元数据 + // 设置回收站时间 + LocalDateTime now = LocalDateTime.now(); + LocalDateTime expireAt = now.plusDays(recycleRetentionDays); + + // 批量更新为回收站状态 if (CollectionUtils.isNotEmpty(allFileIds)) { - service.removeByIds(allFileIds); - fileMetadataExtensionService.lambdaUpdate().in(FileMetadataExtension::getTFilemetaId, allFileIds).remove(); - fileUserPermissionService.lambdaUpdate().in(FileUserPermission::getTFilemetaId, allFileIds).remove(); - fileSimulationMappingService.lambdaUpdate().in(FileSimulationMapping::getFileId, allFileIds).remove(); - fileStorageService.lambdaUpdate() - .in(FileStorage::getFileId, allFileIds) - .or() - .in(FileStorage::getDirId, allDirIds) - .remove(); - fileTagRelService.lambdaUpdate().in(FileTagRel::getDirId, allFileIds).remove(); + List allMetadataList = service.listByIds(allFileIds); + allMetadataList.forEach(item -> { + item.setDeletedAt(now); + item.setRecycleExpireAt(expireAt); + item.setUpdateTime(now); + }); + service.updateBatchById(allMetadataList); } - // MinIO 递归删除 - minioService.deleteDirectoryRecursively(rootDirObjectKey, bucketName); - - log.info("审批通过,成功删除目录及所有子项: id={}, objectKey={}", rootDirId, rootDirObjectKey); + log.info("审批通过,目录及所有子项已移入回收站: id={}, 过期时间={}", rootDirId, expireAt); return true; } diff --git a/data/src/main/resources/application-test.yml b/data/src/main/resources/application-test.yml index 76115db6..b0ef95d4 100644 --- a/data/src/main/resources/application-test.yml +++ b/data/src/main/resources/application-test.yml @@ -139,6 +139,12 @@ fileSystem: system: system chose: minio # 这里选择minio或者system +# 回收站配置 +data: + recycle: + retention-days: 7 # 回收站保留天数,默认7天 + cleanup-cron: "0 0 2 * * ?" # 定时清理任务,每天凌晨2点执行 + #地址: https://play.min.io #凭据: #账号: minioadmin / 密码: minioadmin diff --git a/data/src/main/resources/mapper/FileMetadataInfoMapper.xml b/data/src/main/resources/mapper/FileMetadataInfoMapper.xml index 05a984cf..cda5eec2 100644 --- a/data/src/main/resources/mapper/FileMetadataInfoMapper.xml +++ b/data/src/main/resources/mapper/FileMetadataInfoMapper.xml @@ -15,6 +15,7 @@ #{dirId} ) + AND file_metadata_info.deletedAt IS NULL AND file_storage.fileId IS NOT NULL @@ -35,6 +36,7 @@ and tenantId = #{tenantId} AND dataType = 2 AND isLatest = true + AND deletedAt IS NULL @@ -54,6 +56,7 @@ #{fileId} ) + AND file_metadata_info.deletedAt IS NULL AND file_storage.fileId IS NOT NULL diff --git a/data/src/main/resources/mapper/FileStorageMapper.xml b/data/src/main/resources/mapper/FileStorageMapper.xml index e8d4fab9..37dc288a 100644 --- a/data/src/main/resources/mapper/FileStorageMapper.xml +++ b/data/src/main/resources/mapper/FileStorageMapper.xml @@ -166,6 +166,7 @@ and file_metadata_info.isLatest = #{queryBigFileReq.isLatest} and file_metadata_info.tenantId = #{tenantId} and file_storage.tenantId = #{tenantId} + and file_metadata_info.deletedAt IS NULL AND file_metadata_info.approveType IN