fix:优化文件删除回收站功能

This commit is contained in:
2026-02-11 11:33:03 +08:00
parent e2471eb46b
commit a312da00af
3 changed files with 126 additions and 47 deletions

View File

@@ -8,6 +8,7 @@ import com.sdm.common.entity.resp.PageDataResp;
import com.sdm.common.entity.resp.data.BatchAddFileInfoResp; import com.sdm.common.entity.resp.data.BatchAddFileInfoResp;
import com.sdm.common.entity.resp.data.ChunkUploadMinioFileResp; import com.sdm.common.entity.resp.data.ChunkUploadMinioFileResp;
import com.sdm.common.entity.resp.data.FileMetadataInfoResp; import com.sdm.common.entity.resp.data.FileMetadataInfoResp;
import com.sdm.data.model.entity.FileMetadataInfo;
import com.sdm.data.model.req.*; import com.sdm.data.model.req.*;
import com.sdm.data.model.resp.KKFileViewURLFromMinioResp; import com.sdm.data.model.resp.KKFileViewURLFromMinioResp;
import com.sdm.data.model.resp.MinioDownloadUrlResp; import com.sdm.data.model.resp.MinioDownloadUrlResp;
@@ -28,6 +29,12 @@ import com.sdm.common.entity.resp.data.BatchCreateNormalDirResp;
@Service @Service
public interface IDataFileService { public interface IDataFileService {
/**
* 将文件或目录移入回收站(支持自动重命名释放路径)
* 内部方法,不校验权限
*/
default void moveFileToRecycleBin(FileMetadataInfo fileMetadataInfo) {}
/** /**
* 创建目录 * 创建目录
* @param req 创建目录请求参数 * @param req 创建目录请求参数

View File

@@ -68,6 +68,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartFile;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -185,6 +186,58 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
@Override
public void moveFileToRecycleBin(FileMetadataInfo fileMetadataInfo) {
if (fileMetadataInfo == null) return;
// 如果是文件夹,调用现有的目录移动逻辑
if (Objects.equals(DataTypeEnum.DIRECTORY.getValue(), fileMetadataInfo.getDataType())) {
moveDirectoryToRecycle(fileMetadataInfo.getId());
// 尝试刷新对象状态
FileMetadataInfo updated = fileMetadataInfoService.getById(fileMetadataInfo.getId());
if (updated != null) {
fileMetadataInfo.setObjectKey(updated.getObjectKey());
fileMetadataInfo.setDeletedAt(updated.getDeletedAt());
fileMetadataInfo.setRecycleExpireAt(updated.getRecycleExpireAt());
fileMetadataInfo.setUpdateTime(updated.getUpdateTime());
}
} else {
// 单文件逻辑
LocalDateTime now = LocalDateTime.now();
LocalDateTime expireAt = now.plusDays(recycleRetentionDays);
String oldKey = fileMetadataInfo.getObjectKey();
String suffix = "_del_" + System.currentTimeMillis();
String newKey;
int dotIndex = oldKey.lastIndexOf('.');
if (dotIndex > -1) {
newKey = oldKey.substring(0, dotIndex) + suffix + oldKey.substring(dotIndex);
} else {
newKey = oldKey + suffix;
}
String bucketName = fileMetadataInfo.getBucketName();
try {
// 复用 updatePathRecursively 处理单文件移动和 DB 更新
// updatePathRecursively 会处理 FileMetadataInfo 和 FileStorage
updatePathRecursively(oldKey, newKey, bucketName, now, expireAt, true);
// 更新传入的对象
fileMetadataInfo.setObjectKey(newKey);
fileMetadataInfo.setDeletedAt(now);
fileMetadataInfo.setRecycleExpireAt(expireAt);
fileMetadataInfo.setUpdateTime(now);
log.info("文件已移入回收站: id={}, oldKey={}, newKey={}", fileMetadataInfo.getId(), oldKey, newKey);
} catch (Exception e) {
log.error("移入回收站失败", e);
throw new RuntimeException("移入回收站失败: " + e.getMessage(), e);
}
}
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public SdmResponse createDir(CreateDirReq req) { public SdmResponse createDir(CreateDirReq req) {
@@ -1440,15 +1493,17 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
// 尝试回滚 // 尝试回滚
try { try {
updatePathRecursively(newDirMinioObjectKey, oldDirMinioObjectKey, bucketName, null, null, false); updatePathRecursively(newDirMinioObjectKey, oldDirMinioObjectKey, bucketName, null, null, false);
fileMetadataInfoService.lambdaUpdate() // 注意:这里不需要手动回滚 DB因为下面会 setRollbackOnly() 或抛出异常Spring 会自动回滚 DB。
.set(FileMetadataInfo::getObjectKey, oldDirMinioObjectKey) // 这里的 updatePathRecursively 虽然会执行 DB 更新,但最终都会被回滚。
.set(FileMetadataInfo::getOriginalName, oldName)
.eq(FileMetadataInfo::getId, dirMetadataInfo.getId())
.update();
} catch (Exception re) { } catch (Exception re) {
log.error("重命名失败后回滚失败", re); log.error("重命名失败后回滚失败", re);
// 回滚失败,抛出异常以确保 DB 回滚
throw new RuntimeException("重命名目录失败: " + e.getMessage(), e);
} }
throw new RuntimeException("重命名目录失败: " + e.getMessage(), e);
// 手动标记事务回滚,但不抛出异常,以便返回友好的错误信息
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return SdmResponse.failed("重命名目录失败: " + e.getMessage());
} }
} }
@@ -1476,7 +1531,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
* @param expireAt 过期时间(传 null 则不更新) * @param expireAt 过期时间(传 null 则不更新)
* @param updateStatus 是否更新删除状态 * @param updateStatus 是否更新删除状态
*/ */
private void updatePathRecursively(String oldPrefix, String newPrefix, String bucketName, LocalDateTime deletedAt, LocalDateTime expireAt, boolean updateStatus) { public void updatePathRecursively(String oldPrefix, String newPrefix, String bucketName, LocalDateTime deletedAt, LocalDateTime expireAt, boolean updateStatus) {
// 1. MinIO 移动 (如果路径不同) // 1. MinIO 移动 (如果路径不同)
if (!Objects.equals(oldPrefix, newPrefix)) { if (!Objects.equals(oldPrefix, newPrefix)) {
minioService.renameDirectoryRecursively(oldPrefix, newPrefix, bucketName); minioService.renameDirectoryRecursively(oldPrefix, newPrefix, bucketName);
@@ -4584,20 +4639,24 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
} }
// 1. 检查父目录状态 // 1. 检查父目录状态
FileMetadataInfo parent = null; String parentPath = "";
if (metadata.getParentId() != null) { if (metadata.getParentId() != null) {
parent = fileMetadataInfoService.getById(metadata.getParentId()); FileMetadataInfo parent = fileMetadataInfoService.getById(metadata.getParentId());
if (parent != null && parent.getDeletedAt() != null) { if (parent == null) {
return SdmResponse.failed("父文件夹不存在,无法还原");
}
if (parent.getDeletedAt() != null) {
return SdmResponse.failed("请先恢复父文件夹: " + parent.getOriginalName()); return SdmResponse.failed("请先恢复父文件夹: " + parent.getOriginalName());
} }
parentPath = parent.getObjectKey();
} }
String oldKey = metadata.getObjectKey(); String oldKey = metadata.getObjectKey();
String originalName = metadata.getOriginalName(); String originalName = metadata.getOriginalName();
String bucketName = metadata.getBucketName(); String bucketName = metadata.getBucketName();
String parentPath = parent != null ? parent.getObjectKey() : "";
// 2. 冲突检测与自动重命名 // 2. 冲突检测与自动重命名
// 循环检测 parentId 下是否存在同名文件(未删除的)
String restoreName = originalName; String restoreName = originalName;
String restoreKey; String restoreKey;

View File

@@ -8,8 +8,11 @@ import com.sdm.common.entity.enums.ApproveTypeEnum;
import com.sdm.common.entity.enums.DataTypeEnum; import com.sdm.common.entity.enums.DataTypeEnum;
import com.sdm.data.model.entity.*; import com.sdm.data.model.entity.*;
import com.sdm.data.service.*; import com.sdm.data.service.*;
import com.sdm.data.service.IDataFileService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -21,6 +24,11 @@ import java.util.stream.Collectors;
public class DeleteApproveStrategy implements ApproveStrategy { public class DeleteApproveStrategy implements ApproveStrategy {
@org.springframework.beans.factory.annotation.Value("${data.recycle.retention-days:7}") @org.springframework.beans.factory.annotation.Value("${data.recycle.retention-days:7}")
private Integer recycleRetentionDays; private Integer recycleRetentionDays;
@Autowired
@Lazy
private IDataFileService dataFileService;
@Override @Override
public boolean handle(ApproveContext context) { public boolean handle(ApproveContext context) {
FileMetadataInfo metadata = context.getApproveMetadataInfos().get(0); FileMetadataInfo metadata = context.getApproveMetadataInfos().get(0);
@@ -51,20 +59,23 @@ public class DeleteApproveStrategy implements ApproveStrategy {
private boolean handleFileDeletion(ApproveContext context, FileMetadataInfo metadata, int type) { private boolean handleFileDeletion(ApproveContext context, FileMetadataInfo metadata, int type) {
IFileMetadataInfoService service = context.getFileMetadataInfoService(); IFileMetadataInfoService service = context.getFileMetadataInfoService();
LocalDateTime now = LocalDateTime.now(); try {
LocalDateTime expireAt = now.plusDays(recycleRetentionDays); // 1. 移入回收站 (MinIO Rename + DB Path Update + DB DeleteStatus Update)
dataFileService.moveFileToRecycleBin(metadata);
// 更新审批状态 + 移入回收站 // 2. 更新审批状态
metadata.setTempMetadata(null); metadata.setTempMetadata(null);
metadata.setApprovalStatus(ApprovalFileDataStatusEnum.APPROVED.getKey()); metadata.setApprovalStatus(ApprovalFileDataStatusEnum.APPROVED.getKey());
metadata.setApproveType(ApproveFileDataTypeEnum.COMPLETED.getCode()); metadata.setApproveType(ApproveFileDataTypeEnum.COMPLETED.getCode());
metadata.setDeletedAt(now); metadata.setUpdateTime(LocalDateTime.now());
metadata.setRecycleExpireAt(expireAt); service.updateById(metadata);
metadata.setUpdateTime(now);
service.updateById(metadata);
log.info("审批通过,文件已移入回收站: id={}, objectKey={}, 过期时间={}", metadata.getId(), metadata.getObjectKey(), expireAt); log.info("审批通过,文件已移入回收站: id={}, objectKey={}", metadata.getId(), metadata.getObjectKey());
return true; return true;
} catch (Exception e) {
log.error("审批通过处理文件删除失败: id={}", metadata.getId(), e);
return false;
}
} }
/** /**
@@ -72,34 +83,36 @@ public class DeleteApproveStrategy implements ApproveStrategy {
*/ */
private boolean handleDirDeletion(ApproveContext context, FileMetadataInfo rootDirMetadata) { private boolean handleDirDeletion(ApproveContext context, FileMetadataInfo rootDirMetadata) {
IFileMetadataInfoService service = context.getFileMetadataInfoService(); IFileMetadataInfoService service = context.getFileMetadataInfoService();
Long rootDirId = rootDirMetadata.getId(); Long rootDirId = rootDirMetadata.getId();
// 递归收集所有待删除的 ID try {
Set<Long> allFileIds = new HashSet<>(); // 1. 移入回收站 (MinIO Rename + DB Path Update + DB DeleteStatus Update)
Set<Long> allDirIds = new HashSet<>(); dataFileService.moveFileToRecycleBin(rootDirMetadata);
collectRecursiveIds(service, rootDirId, allFileIds, allDirIds);
// 设置回收站时间 // 2. 递归收集所有 ID 用于更新审批状态
LocalDateTime now = LocalDateTime.now(); Set<Long> allFileIds = new HashSet<>();
LocalDateTime expireAt = now.plusDays(recycleRetentionDays); Set<Long> allDirIds = new HashSet<>();
collectRecursiveIds(service, rootDirId, allFileIds, allDirIds);
// 批量更新审批状态 + 回收站状态 // 3. 批量更新审批状态
if (CollectionUtils.isNotEmpty(allFileIds)) { if (CollectionUtils.isNotEmpty(allFileIds)) {
List<FileMetadataInfo> allMetadataList = service.listByIds(allFileIds); List<FileMetadataInfo> allMetadataList = service.listByIds(allFileIds);
allMetadataList.forEach(item -> { LocalDateTime now = LocalDateTime.now();
item.setTempMetadata(null); allMetadataList.forEach(item -> {
item.setApprovalStatus(ApprovalFileDataStatusEnum.APPROVED.getKey()); item.setTempMetadata(null);
item.setApproveType(ApproveFileDataTypeEnum.COMPLETED.getCode()); item.setApprovalStatus(ApprovalFileDataStatusEnum.APPROVED.getKey());
item.setDeletedAt(now); item.setApproveType(ApproveFileDataTypeEnum.COMPLETED.getCode());
item.setRecycleExpireAt(expireAt); item.setUpdateTime(now);
item.setUpdateTime(now); });
}); service.updateBatchById(allMetadataList);
service.updateBatchById(allMetadataList); }
log.info("审批通过,目录及所有子项已移入回收站: id={}", rootDirId);
return true;
} catch (Exception e) {
log.error("审批通过处理目录删除失败: id={}", rootDirId, e);
return false;
} }
log.info("审批通过,目录及所有子项已移入回收站: id={}, 过期时间={}", rootDirId, expireAt);
return true;
} }
/** /**