fix:优化文件删除回收站功能
This commit is contained in:
@@ -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 创建目录请求参数
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
try {
|
||||
// 1. 移入回收站 (MinIO Rename + DB Path Update + DB DeleteStatus Update)
|
||||
dataFileService.moveFileToRecycleBin(metadata);
|
||||
|
||||
// 更新审批状态 + 移入回收站
|
||||
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);
|
||||
// 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(), expireAt);
|
||||
return true;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user