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

View File

@@ -68,6 +68,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
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
@Transactional(rollbackFor = Exception.class)
public SdmResponse createDir(CreateDirReq req) {
@@ -1440,15 +1493,17 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
// 尝试回滚
try {
updatePathRecursively(newDirMinioObjectKey, oldDirMinioObjectKey, bucketName, null, null, false);
fileMetadataInfoService.lambdaUpdate()
.set(FileMetadataInfo::getObjectKey, oldDirMinioObjectKey)
.set(FileMetadataInfo::getOriginalName, oldName)
.eq(FileMetadataInfo::getId, dirMetadataInfo.getId())
.update();
// 注意:这里不需要手动回滚 DB因为下面会 setRollbackOnly() 或抛出异常Spring 会自动回滚 DB。
// 这里的 updatePathRecursively 虽然会执行 DB 更新,但最终都会被回滚。
} catch (Exception 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 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 移动 (如果路径不同)
if (!Objects.equals(oldPrefix, newPrefix)) {
minioService.renameDirectoryRecursively(oldPrefix, newPrefix, bucketName);
@@ -4584,20 +4639,24 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
}
// 1. 检查父目录状态
FileMetadataInfo parent = null;
String parentPath = "";
if (metadata.getParentId() != null) {
parent = fileMetadataInfoService.getById(metadata.getParentId());
if (parent != null && parent.getDeletedAt() != null) {
FileMetadataInfo parent = fileMetadataInfoService.getById(metadata.getParentId());
if (parent == null) {
return SdmResponse.failed("父文件夹不存在,无法还原");
}
if (parent.getDeletedAt() != null) {
return SdmResponse.failed("请先恢复父文件夹: " + parent.getOriginalName());
}
parentPath = parent.getObjectKey();
}
String oldKey = metadata.getObjectKey();
String originalName = metadata.getOriginalName();
String bucketName = metadata.getBucketName();
String parentPath = parent != null ? parent.getObjectKey() : "";
// 2. 冲突检测与自动重命名
// 循环检测 parentId 下是否存在同名文件(未删除的)
String restoreName = originalName;
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.data.model.entity.*;
import com.sdm.data.service.*;
import com.sdm.data.service.IDataFileService;
import lombok.extern.slf4j.Slf4j;
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 java.time.LocalDateTime;
@@ -21,6 +24,11 @@ import java.util.stream.Collectors;
public class DeleteApproveStrategy implements ApproveStrategy {
@org.springframework.beans.factory.annotation.Value("${data.recycle.retention-days:7}")
private Integer recycleRetentionDays;
@Autowired
@Lazy
private IDataFileService dataFileService;
@Override
public boolean handle(ApproveContext context) {
FileMetadataInfo metadata = context.getApproveMetadataInfos().get(0);
@@ -51,20 +59,23 @@ public class DeleteApproveStrategy implements ApproveStrategy {
private boolean handleFileDeletion(ApproveContext context, FileMetadataInfo metadata, int type) {
IFileMetadataInfoService service = context.getFileMetadataInfoService();
LocalDateTime now = LocalDateTime.now();
LocalDateTime expireAt = now.plusDays(recycleRetentionDays);
// 更新审批状态 + 移入回收站
metadata.setTempMetadata(null);
metadata.setApprovalStatus(ApprovalFileDataStatusEnum.APPROVED.getKey());
metadata.setApproveType(ApproveFileDataTypeEnum.COMPLETED.getCode());
metadata.setDeletedAt(now);
metadata.setRecycleExpireAt(expireAt);
metadata.setUpdateTime(now);
service.updateById(metadata);
try {
// 1. 移入回收站 (MinIO Rename + DB Path Update + DB DeleteStatus Update)
dataFileService.moveFileToRecycleBin(metadata);
log.info("审批通过,文件已移入回收站: id={}, objectKey={}, 过期时间={}", metadata.getId(), metadata.getObjectKey(), expireAt);
return true;
// 2. 更新审批状态
metadata.setTempMetadata(null);
metadata.setApprovalStatus(ApprovalFileDataStatusEnum.APPROVED.getKey());
metadata.setApproveType(ApproveFileDataTypeEnum.COMPLETED.getCode());
metadata.setUpdateTime(LocalDateTime.now());
service.updateById(metadata);
log.info("审批通过,文件已移入回收站: id={}, objectKey={}", metadata.getId(), metadata.getObjectKey());
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) {
IFileMetadataInfoService service = context.getFileMetadataInfoService();
Long rootDirId = rootDirMetadata.getId();
// 递归收集所有待删除的 ID
Set<Long> allFileIds = new HashSet<>();
Set<Long> allDirIds = new HashSet<>();
collectRecursiveIds(service, rootDirId, allFileIds, allDirIds);
try {
// 1. 移入回收站 (MinIO Rename + DB Path Update + DB DeleteStatus Update)
dataFileService.moveFileToRecycleBin(rootDirMetadata);
// 设置回收站时间
LocalDateTime now = LocalDateTime.now();
LocalDateTime expireAt = now.plusDays(recycleRetentionDays);
// 2. 递归收集所有 ID 用于更新审批状态
Set<Long> allFileIds = new HashSet<>();
Set<Long> allDirIds = new HashSet<>();
collectRecursiveIds(service, rootDirId, allFileIds, allDirIds);
// 批量更新审批状态 + 回收站状态
if (CollectionUtils.isNotEmpty(allFileIds)) {
List<FileMetadataInfo> allMetadataList = service.listByIds(allFileIds);
allMetadataList.forEach(item -> {
item.setTempMetadata(null);
item.setApprovalStatus(ApprovalFileDataStatusEnum.APPROVED.getKey());
item.setApproveType(ApproveFileDataTypeEnum.COMPLETED.getCode());
item.setDeletedAt(now);
item.setRecycleExpireAt(expireAt);
item.setUpdateTime(now);
});
service.updateBatchById(allMetadataList);
// 3. 批量更新审批状态
if (CollectionUtils.isNotEmpty(allFileIds)) {
List<FileMetadataInfo> allMetadataList = service.listByIds(allFileIds);
LocalDateTime now = LocalDateTime.now();
allMetadataList.forEach(item -> {
item.setTempMetadata(null);
item.setApprovalStatus(ApprovalFileDataStatusEnum.APPROVED.getKey());
item.setApproveType(ApproveFileDataTypeEnum.COMPLETED.getCode());
item.setUpdateTime(now);
});
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;
}
/**