fix:优化回收站全量删除

This commit is contained in:
2026-02-12 15:56:53 +08:00
parent a3f3373582
commit 911967e64b

View File

@@ -178,7 +178,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
@Autowired
private LocalFileService localFileService;
@Autowired
private DictTagHelper dictTagHelper;
@@ -209,31 +209,31 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
// 单文件逻辑
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);
@@ -689,7 +689,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
LocalDateTime now = LocalDateTime.now();
LocalDateTime expireAt = now.plusDays(recycleRetentionDays);
String oldKey = rootDir.getObjectKey();
String suffix = "_del_" + System.currentTimeMillis();
// 目录 key 规范处理
@@ -785,10 +785,16 @@ 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 newKey;
int dotIndex = oldKey.lastIndexOf('.');
if (dotIndex > -1) {
newKey = oldKey.substring(0, dotIndex) + suffix + oldKey.substring(dotIndex);
} else {
newKey = oldKey + suffix;
}
String bucketName = deleteFileMetadataInfo.getBucketName();
// 1. MinIO 重命名
@@ -1505,7 +1511,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
// 回滚失败,抛出异常以确保 DB 回滚
throw new RuntimeException("重命名目录失败: " + e.getMessage(), e);
}
// 手动标记事务回滚,但不抛出异常,以便返回友好的错误信息
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return SdmResponse.failed("重命名目录失败: " + e.getMessage());
@@ -1539,7 +1545,12 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
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);
// 根据路径特征决定是递归目录还是单文件重命名
if (oldPrefix.endsWith("/")) {
minioService.renameDirectoryRecursively(oldPrefix, newPrefix, bucketName);
} else {
minioService.renameFile(oldPrefix, newPrefix, bucketName);
}
}
// 2. 数据库批量更新 Metadata
@@ -1556,7 +1567,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
if (currentKey.startsWith(oldPrefix)) {
String suffix = currentKey.substring(oldPrefix.length());
String newKey = newPrefix + suffix;
child.setObjectKey(newKey);
if (updateStatus) {
child.setDeletedAt(deletedAt);
@@ -1566,9 +1577,17 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
updates.add(child);
}
}
if (CollectionUtils.isNotEmpty(updates)) {
fileMetadataInfoService.updateBatchById(updates);
for (FileMetadataInfo child : updates) {
fileMetadataInfoService.lambdaUpdate()
.eq(FileMetadataInfo::getId, child.getId())
.set(FileMetadataInfo::getObjectKey, child.getObjectKey())
.set(updateStatus, FileMetadataInfo::getDeletedAt, deletedAt)
.set(updateStatus, FileMetadataInfo::getRecycleExpireAt, expireAt)
.set(FileMetadataInfo::getUpdateTime, child.getUpdateTime())
.update();
}
}
}
}
@@ -2148,7 +2167,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
/**
* 更新文件标签
* 删除旧标签,插入新标签
*
*
* @param req 更新文件请求
* @param fileMetadataInfo 文件元数据
* @param dirMetadataInfo 目录元数据
@@ -2158,10 +2177,10 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
Long creatorId = ThreadLocalContext.getUserId();
Long fileId = fileMetadataInfo.getId();
Long dirId = dirMetadataInfo != null ? dirMetadataInfo.getId() : null;
// 1. 收集当前目录和所有祖先目录
List<Long> ancestorDirIds = dirId != null ? collectAncestorDirIds(dirId) : new ArrayList<>();
// 2. 删除文件在所有目录下的旧标签关系
if (!ancestorDirIds.isEmpty()) {
fileTagRelService.lambdaUpdate()
@@ -2170,7 +2189,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
.in(FileTagRel::getDirId, ancestorDirIds)
.remove();
}
// 3. 从缓存获取字典标签ID已由AOP切面自动填充
Map<String, Map<String, Integer>> dictIdMap = req.getDictTagIdsCache();
if (dictIdMap == null || dictIdMap.isEmpty()) {
@@ -2181,19 +2200,19 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
return;
}
}
// 4. 构建新的标签关系
List<FileTagRel> newTagRelList = new ArrayList<>();
long fileSize = fileMetadataInfo.getFileSize() != null ? fileMetadataInfo.getFileSize() : 0L;
for (Map.Entry<String, Map<String, Integer>> classEntry : dictIdMap.entrySet()) {
Map<String, Integer> valueMap = classEntry.getValue();
for (Integer dictId : valueMap.values()) {
if (dictId == null) {
continue;
}
// 为所有目录(当前目录 + 祖先目录)创建标签关系
for (Long dirIdItem : ancestorDirIds) {
FileTagRel tagRel = new FileTagRel();
@@ -2207,7 +2226,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
}
}
}
// 5. 批量插入新标签关系
if (CollectionUtils.isNotEmpty(newTagRelList)) {
fileTagRelService.saveBatch(newTagRelList);
@@ -2242,7 +2261,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
// 遍历查询结果,构造文件标签关系
for (Map.Entry<String, Map<String, Integer>> classEntry : dictIdMap.entrySet()) {
Map<String, Integer> valueMap = classEntry.getValue();
// 遍历该dictClass下的所有dictValue
for (Integer dictId : valueMap.values()) {
if (dictId == null) {
@@ -2496,7 +2515,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
tempFileMetadataInfo.setCreateTime(fileMetadataInfo.getCreateTime());
tempFileMetadataInfo.setUpdaterId(ThreadLocalContext.getUserId());
tempFileMetadataInfo.setUpdateTime(LocalDateTime.now());
// 保存标签缓存到 tempFileMetadataInfo如果有
if (CollectionUtils.isNotEmpty(req.getDictTags())) {
Map<String, Map<String, Integer>> dictIdMap = req.getDictTagIdsCache();
@@ -2565,7 +2584,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
}
}
}
// 更新标签(如果有)
if (CollectionUtils.isNotEmpty(req.getDictTags())) {
updateFileTags(req, fileMetadataInfo, dirMetadataInfo);
@@ -3371,7 +3390,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
TaskBaseInfo::getFileId,
Collectors.groupingBy(TaskBaseInfo::getPoolId)
));
// 按 fileId 和 poolId 分组收集 simulationPoolTaskId
Map<Long, Map<Integer, List<String>>> filePoolTaskIdsMap = fileSimulationMappingByFileId.stream()
.collect(Collectors.groupingBy(
@@ -3381,20 +3400,20 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
Collectors.mapping(FileSimulationMappingResp::getSimulationPoolTaskId, Collectors.toList())
)
));
// 为每个文件创建 PoolInfo 列表
for (FileMetadataInfo fileMetadataInfo : list) {
Long fileId = fileMetadataInfo.getId();
if (fileTaskPoolMap.containsKey(fileId)) {
Map<Integer, List<TaskBaseInfo>> poolMap = fileTaskPoolMap.get(fileId);
Map<Integer, List<String>> poolTaskIdsMap = filePoolTaskIdsMap.getOrDefault(fileId, new HashMap<>());
List<PoolInfo> poolInfos = new ArrayList<>();
for (Map.Entry<Integer, List<TaskBaseInfo>> poolEntry : poolMap.entrySet()) {
Integer poolId = poolEntry.getKey();
List<TaskBaseInfo> taskList = poolEntry.getValue();
List<String> taskIds = poolTaskIdsMap.getOrDefault(poolId, new ArrayList<>());
PoolInfo poolInfo = new PoolInfo();
poolInfo.setSimulationPoolId(poolId);
poolInfo.setSimulationPoolName("");
@@ -3405,10 +3424,10 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
poolInfo.setSimulationPoolName(taskList.get(0).getPoolName());
poolInfo.setSimulationPoolVersion(taskList.get(0).getVersion());
}
poolInfos.add(poolInfo);
}
fileMetadataInfo.setPoolInfos(poolInfos);
}
}
@@ -4084,7 +4103,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
@Override
@Transactional(rollbackFor = Exception.class)
public SdmResponse<List<Long>> batchCreateDir(BatchCreateDirReq req) {
log.info("开始执行批量创建目录dirType: {}, items数量: {}", req.getDirType(),
log.info("开始执行批量创建目录dirType: {}, items数量: {}", req.getDirType(),
req.getItems() == null ? 0 : req.getItems().size());
long startTime = System.currentTimeMillis();
@@ -4139,7 +4158,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
return SdmResponse.success(ctx.createdDirIds);
} catch (Exception e) {
log.error("批量创建目录异常已创建MinIO Key数量: {}, 错误信息: {}",
log.error("批量创建目录异常已创建MinIO Key数量: {}, 错误信息: {}",
ctx.createdMinioKeys.size(), e.getMessage(), e);
compensateDeleteMinioKeys(ctx.createdMinioKeys);
throw new RuntimeException("批量创建目录失败: " + e.getMessage(), e);
@@ -4206,7 +4225,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
.filter(element -> element.getParentUuId() == null)
.collect(Collectors.toList());
ctx.setFirstLevelNodes(firstLevelNodes);
// 提取第一层节点UUID集合
ctx.setFirstLevelParentUuIds(firstLevelNodes.stream()
.map(DirNodeInfo::getUuId)
@@ -4315,7 +4334,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
// 5. 批量创建权限记录
createBatchPermissions(entities, ctx.tenantId);
log.info("第{}层处理完成,创建了{}个目录,总耗时: {}ms",
log.info("第{}层处理完成,创建了{}个目录,总耗时: {}ms",
levelIndex + 1, entities.size(), System.currentTimeMillis() - levelStartTime);
}
@@ -4389,12 +4408,12 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
List<DirNodeInfo> firstLevelNodes) {
List<List<DirNodeInfo>> levelNodes = new ArrayList<>();
// 先添加第一层节点
if (CollectionUtils.isNotEmpty(firstLevelNodes)) {
levelNodes.add(new ArrayList<>(firstLevelNodes));
}
// 提取第一层节点的UUID作为初始父节点集合
Set<String> currentLevelParents = firstLevelNodes.stream()
.map(DirNodeInfo::getUuId)
@@ -4436,14 +4455,14 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
log.info("开始执行批量创建普通文件夹父目录ID: {}, 父目录UUID: {}, folderItems数量: {}, 跳过权限校验: {}",
req.getParentId(), req.getParentUUId(), req.getFolderItems() == null ? 0 : req.getFolderItems().size(), req.getSkipPermissionCheck());
long startTime = System.currentTimeMillis();
// 1. 参数校验
SdmResponse<Void> validateResult = validateBatchCreateNormalDirRequest(req);
if (!validateResult.isSuccess()) {
log.error("批量创建普通文件夹参数校验失败: {}", validateResult.getMessage());
return SdmResponse.failed(validateResult.getMessage());
}
// 2. 父目录校验与权限检查
SdmResponse<FileMetadataInfo> parentDirResult = validateParentDirAndPermission(
req.getParentId(), req.getParentUUId(), req.getSkipPermissionCheck());
@@ -4452,16 +4471,16 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
return SdmResponse.failed(parentDirResult.getMessage());
}
FileMetadataInfo parentDir = parentDirResult.getData();
BatchCreateNormalDirResp resp = new BatchCreateNormalDirResp();
// 3. 过滤并验证文件夹项
List<FolderItemReq> validFolderItems = filterAndValidateFolderItems(req.getFolderItems());
if (validFolderItems.isEmpty()) {
log.error("有效文件夹项为空");
return SdmResponse.failed("有效文件夹项为空");
}
// 4. 检查已存在的文件夹,分离出需要创建的项
List<FolderItemReq> toCreateItems = checkExistingFoldersAndFilter(
validFolderItems, parentDir, resp);
@@ -4469,40 +4488,40 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
log.info("所有文件夹均已存在,无需创建");
return SdmResponse.success(resp);
}
// 5. 准备创建数据(元数据对象 + MinIO Key
List<FileMetadataInfo> newDirs = new ArrayList<>();
List<String> minioKeys = new ArrayList<>();
prepareBatchCreateData(toCreateItems, parentDir, newDirs, minioKeys);
try {
// 6. 批量MinIO创建
minioService.batchCreateDirectories(minioKeys, minioService.getCurrentTenantBucketName());
log.info("批量MinIO创建完成数量: {}", minioKeys.size());
// 7. 数据库批量插入
fileMetadataInfoService.saveBatch(newDirs, 500);
log.info("数据库批量插入完成,数量: {}", newDirs.size());
// 8. 批量添加权限
createBatchPermissions(newDirs, ThreadLocalContext.getTenantId());
// 9. 填充成功列表
fillSuccessResponse(newDirs, resp);
long duration = System.currentTimeMillis() - startTime;
log.info("批量创建普通文件夹成功,耗时: {}ms, 共创建{}个目录", duration, newDirs.size());
} catch (Exception e) {
log.error("批量创建普通文件夹失败", e);
// 补偿删除MinIO目录
compensateDeleteMinioKeys(minioKeys);
throw new RuntimeException("批量创建失败: " + e.getMessage(), e);
}
return SdmResponse.success(resp);
}
/**
* 校验批量创建普通目录请求参数
*/
@@ -4518,13 +4537,13 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
}
return SdmResponse.success(null);
}
/**
* 验证父目录并检查权限
*/
private SdmResponse<FileMetadataInfo> validateParentDirAndPermission(Long parentId, String parentUuid, boolean skipPermissionCheck) {
FileMetadataInfo parentDir;
// 优先使用 parentId 查找,如果为空则使用 parentUuid
if (parentId != null) {
parentDir = fileMetadataInfoService.getById(parentId);
@@ -4535,11 +4554,11 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
} else {
return SdmResponse.failed("父文件夹ID和UUID不能同时为空");
}
if (parentDir == null) {
return SdmResponse.failed("父文件夹不存在");
}
// 如果不跳过权限校验,则检查权限
if (!skipPermissionCheck) {
// 权限检查(需要写入权限)
@@ -4549,10 +4568,10 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
return SdmResponse.failed("没有写入权限");
}
}
return SdmResponse.success(parentDir);
}
/**
* 过滤并验证文件夹项:去重、过滤空名
*/
@@ -4567,7 +4586,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
}
return new ArrayList<>(uniqueMap.values());
}
/**
* 检查已存在的文件夹,过滤出需要创建的项
*/
@@ -4575,12 +4594,12 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
List<FolderItemReq> validFolderItems,
FileMetadataInfo parentDir,
BatchCreateNormalDirResp resp) {
// 提取所有文件夹名称
List<String> folderNames = validFolderItems.stream()
.map(FolderItemReq::getFolderName)
.collect(Collectors.toList());
// 查询数据库中是否已存在同名目录
List<FileMetadataInfo> existingFiles = fileMetadataInfoService.lambdaQuery()
.eq(FileMetadataInfo::getParentId, parentDir.getId())
@@ -4588,12 +4607,12 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
.eq(FileMetadataInfo::getDataType, DataTypeEnum.DIRECTORY.getValue())
.in(FileMetadataInfo::getOriginalName, folderNames)
.list();
// 构建已存在的文件夹名称集合
Set<String> existingNames = existingFiles.stream()
.map(FileMetadataInfo::getOriginalName)
.collect(Collectors.toSet());
// 过滤出需要创建的项
List<FolderItemReq> toCreateItems = new ArrayList<>();
for (FolderItemReq item : validFolderItems) {
@@ -4603,10 +4622,10 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
toCreateItems.add(item);
}
}
return toCreateItems;
}
/**
* 准备批量创建数据构建元数据对象和MinIO Key
*/
@@ -4615,17 +4634,17 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
FileMetadataInfo parentDir,
List<FileMetadataInfo> newDirs,
List<String> minioKeys) {
String parentObjectKey = parentDir.getObjectKey();
// 确保 parentObjectKey 以 / 结尾
if (!parentObjectKey.endsWith("/")) {
parentObjectKey += "/";
}
for (FolderItemReq item : toCreateItems) {
// 构造对象Key
String objectKey = getDirMinioObjectKey(parentObjectKey + item.getFolderName());
// 创建元数据对象(复用现有的 createDirectoryMetadata 方法)
// 传入 folderUuid 作为 relatedResourceUuid
FileMetadataInfo dirInfo = createDirectoryMetadata(
@@ -4636,12 +4655,12 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
item.getFolderUuid(), // 保存 UUID
null,
parentDir.getDirType());
newDirs.add(dirInfo);
minioKeys.add(objectKey);
}
}
/**
* 填充成功响应结果
*/
@@ -4656,7 +4675,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
public SdmResponse listRecycleBin(ListRecycleBinReq req) {
Long tenantId = ThreadLocalContext.getTenantId();
PageHelper.startPage(req.getCurrent(), req.getSize());
List<FileMetadataInfo> list = fileMetadataInfoService.lambdaQuery()
.eq(FileMetadataInfo::getTenantId, tenantId)
.isNotNull(FileMetadataInfo::getDeletedAt)
@@ -4664,16 +4683,16 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
.eq(ObjectUtils.isNotEmpty(req.getDirType()), FileMetadataInfo::getDirType, req.getDirType())
.eq(ObjectUtils.isNotEmpty(req.getDataType()), FileMetadataInfo::getDataType, req.getDataType())
// 仅显示顶层删除项:即其父级未被删除(或者无父级)
.apply("(parentId IS NULL OR NOT EXISTS (SELECT 1 FROM file_metadata_info p WHERE p.id = parentId AND p.deletedAt IS NOT NULL))")
.apply("(parentId IS NULL OR EXISTS (SELECT 1 FROM file_metadata_info p WHERE p.id = file_metadata_info.parentId AND p.deletedAt IS NULL))")
.orderByDesc(FileMetadataInfo::getDeletedAt)
.list();
setCreatorNames(list);
setCidInfos(list);
setProjectName(list);
setAnalysisDirectionName(list);
setSimulationPoolAndTaskInfo(list);
PageInfo<FileMetadataInfo> page = new PageInfo<>(list);
List<FileMetadataInfoResp> dtoList = list.stream().map(entity -> {
FileMetadataInfoResp dto = new FileMetadataInfoResp();
@@ -4681,7 +4700,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
dto.setPermissionValue(fileUserPermissionService.getMergedPermission(entity.getId(), ThreadLocalContext.getUserId()));
return dto;
}).collect(Collectors.toList());
PageInfo<FileMetadataInfoResp> page1 = new PageInfo<>(dtoList);
page1.setTotal(page.getTotal());
return PageUtils.getJsonObjectSdmResponse(dtoList, page1);
@@ -4716,7 +4735,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
.eq(FileMetadataInfo::getId, id)
.isNotNull(FileMetadataInfo::getDeletedAt)
.one();
if (ObjectUtils.isEmpty(metadata)) {
return SdmResponse.failed("文件/目录不存在或不在回收站中");
}
@@ -4733,60 +4752,40 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
}
parentPath = parent.getObjectKey();
}
String oldKey = metadata.getObjectKey();
String originalName = metadata.getOriginalName();
String bucketName = metadata.getBucketName();
// 2. 冲突检测与自动重命名
// 循环检测 parentId 下是否存在同名文件(未删除的)
// 2. 计算还原用的 objectKey从回收站 key 移除 "_del_时间戳" 后缀(文件在扩展名前插入,目录在末尾插入)
boolean isDirectory = Objects.equals(metadata.getDataType(), DataTypeEnum.DIRECTORY.getValue());
String restoreName = originalName;
String restoreKey;
String restoreKey = removeRecycleDeleteSuffix(oldKey, isDirectory);
// 3. 同名冲突originalName不带版本号改名为 xxx(1).pngobjectKey 保持版本号不变,改成 xxx(1)_V1.png
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 (!exists) {
break;
}
// 自动重命名: 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 + ")";
}
}
while (fileMetadataInfoService.lambdaQuery()
.eq(FileMetadataInfo::getParentId, metadata.getParentId())
.eq(FileMetadataInfo::getOriginalName, restoreName)
.eq(FileMetadataInfo::getTenantId, ThreadLocalContext.getTenantId())
.isNull(FileMetadataInfo::getDeletedAt)
.exists()) {
restoreName = buildOriginalNameWithCounter(originalName, isDirectory, counter);
restoreKey = buildObjectKeyWithCounterKeepingVersion(restoreKey, isDirectory, counter);
counter++;
}
// 3. 构建新的 ObjectKey
if (Objects.equals(metadata.getDataType(), DataTypeEnum.DIRECTORY.getValue())) {
restoreKey = getDirMinioObjectKey(parentPath + restoreName);
} else {
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);
@@ -4794,7 +4793,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
metadata.setRecycleExpireAt(null);
metadata.setUpdateTime(LocalDateTime.now());
fileMetadataInfoService.updateById(metadata);
// 6. 如果是文件,更新 fileStorage
if (Objects.equals(metadata.getDataType(), DataTypeEnum.FILE.getValue())) {
fileStorageService.lambdaUpdate()
@@ -4805,7 +4804,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
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);
@@ -4816,7 +4815,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
@Transactional(rollbackFor = Exception.class)
public SdmResponse permanentDeleteFromRecycle(PermanentDeleteFromRecycleReq req) {
List<Long> ids = req.getIds();
if (ObjectUtils.isEmpty(ids)) {
if (CollectionUtils.isEmpty(ids)) {
Long userId = ThreadLocalContext.getUserId();
// 查询回收站中的顶层节点(避免对子节点重复触发递归删除)
List<FileMetadataInfo> list = fileMetadataInfoService.lambdaQuery()
@@ -4839,16 +4838,66 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
return SdmResponse.success("彻底删除成功");
}
private static String removeRecycleDeleteSuffix(String objectKey, boolean isDirectory) {
if (!StringUtils.hasText(objectKey)) return objectKey;
// 移除 _del_123456789 后缀
int delIndex = objectKey.lastIndexOf("_del_");
if (delIndex == -1) return objectKey;
if (isDirectory) {
// 目录:.../folder_del_timestamp/ -> .../folder/
return objectKey.substring(0, delIndex) + "/";
} else {
// 文件:.../name_V1_del_timestamp.png -> .../name_V1.png
int dotIndex = objectKey.lastIndexOf('.');
if (dotIndex > delIndex) {
return objectKey.substring(0, delIndex) + objectKey.substring(dotIndex);
}
return objectKey.substring(0, delIndex);
}
}
private static String buildOriginalNameWithCounter(String originalName, boolean isDirectory, int counter) {
if (isDirectory) return originalName + "(" + counter + ")";
int dotIndex = originalName.lastIndexOf('.');
if (dotIndex > -1) {
return originalName.substring(0, dotIndex) + "(" + counter + ")" + originalName.substring(dotIndex);
}
return originalName + "(" + counter + ")";
}
private static String buildObjectKeyWithCounterKeepingVersion(String restoreKey, boolean isDirectory, int counter) {
if (isDirectory) {
String key = restoreKey.endsWith("/") ? restoreKey.substring(0, restoreKey.length() - 1) : restoreKey;
return key + "(" + counter + ")/";
}
// 文件处理:在 _Vn 之前插入 (n)
// restoreKey 此时已移除 _del_格式为 .../name_V1.png
int versionIdx = restoreKey.lastIndexOf("_V");
if (versionIdx > -1) {
return restoreKey.substring(0, versionIdx) + "(" + counter + ")" + restoreKey.substring(versionIdx);
}
// 兜底(如果不带 _V按扩展名插
int dotIndex = restoreKey.lastIndexOf('.');
if (dotIndex > -1) {
return restoreKey.substring(0, dotIndex) + "(" + counter + ")" + restoreKey.substring(dotIndex);
}
return restoreKey + "(" + counter + ")";
}
private SdmResponse permanentDeleteSingleFile(Long id) {
FileMetadataInfo metadata = fileMetadataInfoService.lambdaQuery()
.eq(FileMetadataInfo::getId, id)
.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());
@@ -4873,22 +4922,22 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
if (ObjectUtils.isEmpty(req.getIds())) {
return SdmResponse.failed("未选择要更新的文件");
}
List<FileMetadataInfo> metadataList = fileMetadataInfoService.listByIds(req.getIds());
if (CollectionUtils.isEmpty(metadataList)) {
return SdmResponse.failed("文件不存在");
}
int successCount = 0;
for (FileMetadataInfo metadata : metadataList) {
// 仅处理已删除的文件
if (metadata.getDeletedAt() == null) {
continue;
}
// 计算新的过期时间:基于 deletedAt + days
LocalDateTime newExpireAt = metadata.getDeletedAt().plusDays(req.getDays());
// 如果是目录,递归更新子项
if (Objects.equals(DataTypeEnum.DIRECTORY.getValue(), metadata.getDataType())) {
updateDirectoryRecycleExpire(metadata, newExpireAt);
@@ -4900,7 +4949,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
}
successCount++;
}
return SdmResponse.success("成功更新 " + successCount + " 个文件的回收站过期时间");
}
@@ -4909,14 +4958,14 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
rootDir.setRecycleExpireAt(newExpireAt);
rootDir.setUpdateTime(LocalDateTime.now());
fileMetadataInfoService.updateById(rootDir);
// 2. 批量更新子项(包括子目录和文件)
// 只要是该目录下的objectKey以目录key为前缀且已删除的都更新
String dirKey = rootDir.getObjectKey();
if (!dirKey.endsWith("/")) {
dirKey += "/";
}
// 使用 LambdaUpdateWrapper 批量更新,效率更高
fileMetadataInfoService.lambdaUpdate()
.likeRight(FileMetadataInfo::getObjectKey, dirKey) // objectKey like 'dirKey%'