fix:优化文件删除回收站功能
This commit is contained in:
@@ -51,9 +51,13 @@ public class FileSimulationMappingServiceImpl extends ServiceImpl<FileSimulation
|
||||
fileSimulationMapping.setSimulationPoolTaskId(fileSimulationMappingReq.getSimulationPoolTaskId());
|
||||
fileSimulationMappings.add(fileSimulationMapping);
|
||||
});
|
||||
// 先删除原先所有的文件绑定关系
|
||||
this.lambdaUpdate()
|
||||
.in(FileSimulationMapping::getFileId, fileIds)
|
||||
.in(FileSimulationMapping::getSimulationPoolId, fileSimulationMappingReq.getSimulationPoolId())
|
||||
.in(FileSimulationMapping::getSimulationPoolVersion, fileSimulationMappingReq.getSimulationPoolVersion())
|
||||
.remove();
|
||||
}
|
||||
// 先删除原先所有的文件绑定关系
|
||||
this.lambdaUpdate().in(FileSimulationMapping::getFileId, fileIds).remove();
|
||||
this.saveBatch(fileSimulationMappings);
|
||||
return SdmResponse.success();
|
||||
}
|
||||
|
||||
@@ -623,28 +623,35 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 将目录及其子项移入回收站(设置 deletedAt、recycleExpireAt)
|
||||
* 将目录及其子项移入回收站(设置 deletedAt、recycleExpireAt,并修改 MinIO objectKey)
|
||||
*/
|
||||
private SdmResponse moveDirectoryToRecycle(Long rootDirId) {
|
||||
Set<Long> allFileIds = new HashSet<>();
|
||||
Set<Long> allDirIds = new HashSet<>();
|
||||
collectRecursiveIds(rootDirId, allFileIds, allDirIds);
|
||||
FileMetadataInfo rootDir = fileMetadataInfoService.getById(rootDirId);
|
||||
if (rootDir == null) return SdmResponse.failed("目录不存在");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime expireAt = now.plusDays(recycleRetentionDays);
|
||||
|
||||
if (CollectionUtils.isNotEmpty(allFileIds)) {
|
||||
List<FileMetadataInfo> allMetadataList = fileMetadataInfoService.listByIds(allFileIds);
|
||||
allMetadataList.forEach(item -> {
|
||||
item.setDeletedAt(now);
|
||||
item.setRecycleExpireAt(expireAt);
|
||||
item.setUpdateTime(now);
|
||||
});
|
||||
fileMetadataInfoService.updateBatchById(allMetadataList);
|
||||
|
||||
String oldKey = rootDir.getObjectKey();
|
||||
String suffix = "_del_" + System.currentTimeMillis();
|
||||
// 目录 key 规范处理
|
||||
String newKey;
|
||||
if (oldKey.endsWith("/")) {
|
||||
newKey = oldKey.substring(0, oldKey.length() - 1) + suffix + "/";
|
||||
} else {
|
||||
newKey = oldKey + suffix + "/";
|
||||
}
|
||||
String bucketName = rootDir.getBucketName();
|
||||
|
||||
log.info("目录及子项已移入回收站: id={}, 过期时间={}", rootDirId, expireAt);
|
||||
return SdmResponse.success("已移入回收站,将在" + recycleRetentionDays + "天后自动删除");
|
||||
try {
|
||||
// 递归移动并更新状态
|
||||
updatePathRecursively(oldKey, newKey, bucketName, now, expireAt, true);
|
||||
log.info("目录及子项已移入回收站: id={}, objectKey={}, newKey={}", rootDirId, oldKey, newKey);
|
||||
return SdmResponse.success("已移入回收站,将在" + recycleRetentionDays + "天后自动删除");
|
||||
} catch (Exception e) {
|
||||
log.error("移入回收站失败", e);
|
||||
throw new RuntimeException("移入回收站失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -717,11 +724,22 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
|
||||
);
|
||||
}
|
||||
|
||||
// 非知识库文件:直接移入回收站
|
||||
// 非知识库文件:直接移入回收站 (Rename + Soft Delete)
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime expireAt = now.plusDays(recycleRetentionDays);
|
||||
|
||||
String oldKey = deleteFileMetadataInfo.getObjectKey();
|
||||
String suffix = "_del_" + System.currentTimeMillis();
|
||||
String newKey = oldKey + suffix;
|
||||
String bucketName = deleteFileMetadataInfo.getBucketName();
|
||||
|
||||
// 1. MinIO 重命名
|
||||
minioService.renameFile(oldKey, newKey, bucketName);
|
||||
|
||||
// 2. DB 更新
|
||||
deleteFileMetadataInfo.setDeletedAt(now);
|
||||
deleteFileMetadataInfo.setRecycleExpireAt(expireAt);
|
||||
deleteFileMetadataInfo.setObjectKey(newKey);
|
||||
deleteFileMetadataInfo.setUpdateTime(now);
|
||||
fileMetadataInfoService.updateById(deleteFileMetadataInfo);
|
||||
return SdmResponse.success("已移入回收站,将在" + recycleRetentionDays + "天后自动删除");
|
||||
@@ -1381,16 +1399,17 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
|
||||
String newName = req.getNewName();
|
||||
String oldName = dirMetadataInfo.getOriginalName();
|
||||
|
||||
// 3. 名称变化校验:若名称未改变,直接返回,避免 MinIO 原地拷贝报错
|
||||
// 3. 名称变化校验:若名称未改变,直接返回
|
||||
if (Objects.equals(newName, oldName)) {
|
||||
return SdmResponse.success("名称未改变");
|
||||
}
|
||||
|
||||
// 4. 同名冲突校验:检查同级目录下是否存在同名文件夹
|
||||
// 4. 同名冲突校验:检查同级目录下是否存在同名文件夹 (排除已删除的)
|
||||
boolean exists = fileMetadataInfoService.lambdaQuery()
|
||||
.eq(FileMetadataInfo::getParentId, dirMetadataInfo.getParentId())
|
||||
.eq(FileMetadataInfo::getOriginalName, newName)
|
||||
.eq(FileMetadataInfo::getTenantId, ThreadLocalContext.getTenantId())
|
||||
.isNull(FileMetadataInfo::getDeletedAt)
|
||||
.exists();
|
||||
if (exists) {
|
||||
return SdmResponse.failed("当前目录下已存在同名文件夹");
|
||||
@@ -1403,13 +1422,10 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
|
||||
String newDirMinioObjectKey = getDirMinioObjectKey(parentPath + newName);
|
||||
|
||||
try {
|
||||
// 5. MinIO 递归重命名:将目录下所有对象从旧前缀迁移到新前缀
|
||||
minioService.renameDirectoryRecursively(oldDirMinioObjectKey, newDirMinioObjectKey, bucketName);
|
||||
// 5. 统一更新路径(MinIO + DB递归)
|
||||
updatePathRecursively(oldDirMinioObjectKey, newDirMinioObjectKey, bucketName, null, null, false);
|
||||
|
||||
// 6. 数据库递归更新:更新所有子文件/子目录的 objectKey
|
||||
updateChildrenObjectKeys(oldDirMinioObjectKey, newDirMinioObjectKey, bucketName);
|
||||
|
||||
// 7. 更新当前目录记录
|
||||
// 6. 更新当前目录记录
|
||||
fileMetadataInfoService.lambdaUpdate()
|
||||
.set(FileMetadataInfo::getObjectKey, newDirMinioObjectKey)
|
||||
.set(FileMetadataInfo::getOriginalName, newName)
|
||||
@@ -1418,20 +1434,19 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
|
||||
.eq(FileMetadataInfo::getId, dirMetadataInfo.getId())
|
||||
.update();
|
||||
|
||||
// 8. 更新 file_storage 表中当前目录的 fileName
|
||||
fileStorageService.lambdaUpdate()
|
||||
.set(FileStorage::getFileName, newName)
|
||||
.eq(FileStorage::getDirId, dirMetadataInfo.getId())
|
||||
.update();
|
||||
|
||||
return SdmResponse.success("重命名目录成功");
|
||||
} catch (Exception e) {
|
||||
log.error("重命名目录失败, oldKey={}, newKey={}", oldDirMinioObjectKey, newDirMinioObjectKey, e);
|
||||
// 尝试回滚 MinIO(注:若部分成功部分失败,回滚可能不完整,需人工介入)
|
||||
// 尝试回滚
|
||||
try {
|
||||
minioService.renameDirectoryRecursively(newDirMinioObjectKey, oldDirMinioObjectKey, bucketName);
|
||||
updatePathRecursively(newDirMinioObjectKey, oldDirMinioObjectKey, bucketName, null, null, false);
|
||||
fileMetadataInfoService.lambdaUpdate()
|
||||
.set(FileMetadataInfo::getObjectKey, oldDirMinioObjectKey)
|
||||
.set(FileMetadataInfo::getOriginalName, oldName)
|
||||
.eq(FileMetadataInfo::getId, dirMetadataInfo.getId())
|
||||
.update();
|
||||
} catch (Exception re) {
|
||||
log.error("重命名失败后回滚 MinIO 路径失败", re);
|
||||
log.error("重命名失败后回滚失败", re);
|
||||
}
|
||||
throw new RuntimeException("重命名目录失败: " + e.getMessage(), e);
|
||||
}
|
||||
@@ -1452,29 +1467,50 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归更新数据库中所有子项的 objectKey
|
||||
* 递归更新路径和状态 (核心复用方法)
|
||||
*
|
||||
* @param oldPrefix 旧的 objectKey 前缀
|
||||
* @param newPrefix 新的 objectKey 前缀
|
||||
* @param oldPrefix 旧路径前缀
|
||||
* @param newPrefix 新路径前缀
|
||||
* @param bucketName 桶名称
|
||||
* @param deletedAt 删除时间(传 null 则不更新)
|
||||
* @param expireAt 过期时间(传 null 则不更新)
|
||||
* @param updateStatus 是否更新删除状态
|
||||
*/
|
||||
private void updateChildrenObjectKeys(String oldPrefix, String newPrefix, String bucketName) {
|
||||
// 使用 SQL 的 REPLACE 函数批量更新所有以 oldPrefix 开头的 objectKey
|
||||
fileMetadataInfoService.lambdaQuery()
|
||||
private 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);
|
||||
}
|
||||
|
||||
// 2. 数据库批量更新 Metadata
|
||||
List<FileMetadataInfo> children = fileMetadataInfoService.lambdaQuery()
|
||||
.likeRight(FileMetadataInfo::getObjectKey, oldPrefix)
|
||||
.eq(FileMetadataInfo::getBucketName, bucketName)
|
||||
.eq(FileMetadataInfo::getTenantId, ThreadLocalContext.getTenantId())
|
||||
.list()
|
||||
.forEach(child -> {
|
||||
String oldKey = child.getObjectKey();
|
||||
String newKey = newPrefix + oldKey.substring(oldPrefix.length());
|
||||
fileMetadataInfoService.lambdaUpdate()
|
||||
.set(FileMetadataInfo::getObjectKey, newKey)
|
||||
.set(FileMetadataInfo::getUpdateTime, LocalDateTime.now())
|
||||
.set(FileMetadataInfo::getUpdaterId, ThreadLocalContext.getUserId())
|
||||
.eq(FileMetadataInfo::getId, child.getId())
|
||||
.update();
|
||||
});
|
||||
.list();
|
||||
|
||||
if (CollectionUtils.isNotEmpty(children)) {
|
||||
List<FileMetadataInfo> updates = new ArrayList<>();
|
||||
for (FileMetadataInfo child : children) {
|
||||
String currentKey = child.getObjectKey();
|
||||
if (currentKey.startsWith(oldPrefix)) {
|
||||
String suffix = currentKey.substring(oldPrefix.length());
|
||||
String newKey = newPrefix + suffix;
|
||||
|
||||
child.setObjectKey(newKey);
|
||||
if (updateStatus) {
|
||||
child.setDeletedAt(deletedAt);
|
||||
child.setRecycleExpireAt(expireAt);
|
||||
}
|
||||
child.setUpdateTime(LocalDateTime.now());
|
||||
updates.add(child);
|
||||
}
|
||||
}
|
||||
|
||||
if (CollectionUtils.isNotEmpty(updates)) {
|
||||
fileMetadataInfoService.updateBatchById(updates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -4501,7 +4537,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SdmResponse listRecycleBin(com.sdm.common.entity.req.data.ListRecycleBinReq req) {
|
||||
public SdmResponse listRecycleBin(ListRecycleBinReq req) {
|
||||
Long tenantId = ThreadLocalContext.getTenantId();
|
||||
PageHelper.startPage(req.getCurrent(), req.getSize());
|
||||
|
||||
@@ -4511,6 +4547,8 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
|
||||
.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())
|
||||
// 仅显示顶层删除项:即其父级未被删除(或者无父级)
|
||||
.apply("(parent_id IS NULL OR NOT EXISTS (SELECT 1 FROM file_metadata_info p WHERE p.id = parent_id AND p.deleted_at IS NOT NULL))")
|
||||
.orderByDesc(FileMetadataInfo::getDeletedAt)
|
||||
.list();
|
||||
|
||||
@@ -4535,7 +4573,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SdmResponse restoreFromRecycle(com.sdm.common.entity.req.data.RestoreFromRecycleReq req) {
|
||||
public SdmResponse restoreFromRecycle(RestoreFromRecycleReq req) {
|
||||
FileMetadataInfo metadata = fileMetadataInfoService.lambdaQuery()
|
||||
.eq(FileMetadataInfo::getId, req.getId())
|
||||
.isNotNull(FileMetadataInfo::getDeletedAt)
|
||||
@@ -4544,43 +4582,97 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
|
||||
if (ObjectUtils.isEmpty(metadata)) {
|
||||
return SdmResponse.failed("文件/目录不存在或不在回收站中");
|
||||
}
|
||||
|
||||
// 1. 检查父目录状态
|
||||
FileMetadataInfo parent = null;
|
||||
if (metadata.getParentId() != null) {
|
||||
parent = fileMetadataInfoService.getById(metadata.getParentId());
|
||||
if (parent != null && parent.getDeletedAt() != null) {
|
||||
return SdmResponse.failed("请先恢复父文件夹: " + parent.getOriginalName());
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是目录,需要递归还原所有子项
|
||||
if (Objects.equals(DataTypeEnum.DIRECTORY.getValue(), metadata.getDataType())) {
|
||||
Set<Long> allFileIds = new HashSet<>();
|
||||
Set<Long> allDirIds = new HashSet<>();
|
||||
collectRecursiveIds(metadata.getId(), allFileIds, allDirIds);
|
||||
String oldKey = metadata.getObjectKey();
|
||||
String originalName = metadata.getOriginalName();
|
||||
String bucketName = metadata.getBucketName();
|
||||
String parentPath = parent != null ? parent.getObjectKey() : "";
|
||||
|
||||
// 2. 冲突检测与自动重命名
|
||||
String restoreName = originalName;
|
||||
String restoreKey;
|
||||
|
||||
int counter = 1;
|
||||
while (true) {
|
||||
boolean exists = fileMetadataInfoService.lambdaQuery()
|
||||
.eq(FileMetadataInfo::getParentId, metadata.getParentId())
|
||||
.eq(FileMetadataInfo::getOriginalName, restoreName)
|
||||
.eq(FileMetadataInfo::getTenantId, ThreadLocalContext.getTenantId())
|
||||
.isNull(FileMetadataInfo::getDeletedAt)
|
||||
.exists();
|
||||
|
||||
// 批量还原
|
||||
if (CollectionUtils.isNotEmpty(allFileIds)) {
|
||||
List<FileMetadataInfo> allMetadataList = fileMetadataInfoService.listByIds(allFileIds);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
allMetadataList.forEach(item -> {
|
||||
item.setDeletedAt(null);
|
||||
item.setRecycleExpireAt(null);
|
||||
item.setUpdateTime(now);
|
||||
});
|
||||
fileMetadataInfoService.updateBatchById(allMetadataList);
|
||||
if (!exists) {
|
||||
break;
|
||||
}
|
||||
|
||||
log.info("成功从回收站还原目录及所有子项: id={}", metadata.getId());
|
||||
return SdmResponse.success("还原成功");
|
||||
// 自动重命名: name(1).txt 或 folder(1)
|
||||
if (Objects.equals(metadata.getDataType(), DataTypeEnum.DIRECTORY.getValue())) {
|
||||
restoreName = originalName + "(" + counter + ")";
|
||||
} else {
|
||||
int dotIndex = originalName.lastIndexOf('.');
|
||||
if (dotIndex > -1) {
|
||||
restoreName = originalName.substring(0, dotIndex) + "(" + counter + ")" + originalName.substring(dotIndex);
|
||||
} else {
|
||||
restoreName = originalName + "(" + counter + ")";
|
||||
}
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
|
||||
// 3. 构建新的 ObjectKey
|
||||
if (Objects.equals(metadata.getDataType(), DataTypeEnum.DIRECTORY.getValue())) {
|
||||
restoreKey = getDirMinioObjectKey(parentPath + restoreName);
|
||||
} else {
|
||||
// 单文件还原
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
restoreKey = getFileMinioObjectKey(parentPath + restoreName);
|
||||
}
|
||||
|
||||
try {
|
||||
// 4. 执行恢复(MinIO Rename + DB Recursive Update + Status Update)
|
||||
// 传递 null 给 deletedAt 和 expireAt 表示清除删除状态
|
||||
updatePathRecursively(oldKey, restoreKey, bucketName, null, null, true);
|
||||
|
||||
// 5. 如果名称发生了变化,更新当前记录的 originalName
|
||||
if (!Objects.equals(restoreName, originalName)) {
|
||||
metadata.setOriginalName(restoreName);
|
||||
}
|
||||
|
||||
// updatePathRecursively 内部已经批量更新了子项和当前项的 key/status
|
||||
// 但 metadata 对象是旧的,手动 update 确保一致性(尤其是 originalName)
|
||||
metadata.setObjectKey(restoreKey);
|
||||
metadata.setDeletedAt(null);
|
||||
metadata.setRecycleExpireAt(null);
|
||||
metadata.setUpdateTime(now);
|
||||
metadata.setUpdateTime(LocalDateTime.now());
|
||||
fileMetadataInfoService.updateById(metadata);
|
||||
|
||||
log.info("成功从回收站还原文件: id={}", metadata.getId());
|
||||
return SdmResponse.success("还原成功");
|
||||
// 6. 如果是文件,更新 Storage fullPath
|
||||
if (Objects.equals(metadata.getDataType(), DataTypeEnum.FILE.getValue())) {
|
||||
fileStorageService.lambdaUpdate()
|
||||
.eq(FileStorage::getFileId, metadata.getId())
|
||||
.set(FileStorage::getFileName, restoreName)
|
||||
.update();
|
||||
}
|
||||
|
||||
log.info("成功从回收站还原: id={}, oldKey={}, newKey={}, newName={}", metadata.getId(), oldKey, restoreKey, restoreName);
|
||||
return SdmResponse.success(Objects.equals(restoreName, originalName) ? "还原成功" : "还原成功,原路径已存在,已重命名为: " + restoreName);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("还原失败", e);
|
||||
throw new RuntimeException("还原失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SdmResponse permanentDeleteFromRecycle(com.sdm.common.entity.req.data.PermanentDeleteFromRecycleReq req) {
|
||||
public SdmResponse permanentDeleteFromRecycle(PermanentDeleteFromRecycleReq req) {
|
||||
FileMetadataInfo metadata = fileMetadataInfoService.lambdaQuery()
|
||||
.eq(FileMetadataInfo::getId, req.getId())
|
||||
.isNotNull(FileMetadataInfo::getDeletedAt)
|
||||
|
||||
Reference in New Issue
Block a user