diff --git a/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java b/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java index e1c1a078..62b33b12 100644 --- a/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java +++ b/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java @@ -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 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> dictIdMap = req.getDictTagIdsCache(); if (dictIdMap == null || dictIdMap.isEmpty()) { @@ -2181,19 +2200,19 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { return; } } - + // 4. 构建新的标签关系 List newTagRelList = new ArrayList<>(); long fileSize = fileMetadataInfo.getFileSize() != null ? fileMetadataInfo.getFileSize() : 0L; - + for (Map.Entry> classEntry : dictIdMap.entrySet()) { Map 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> classEntry : dictIdMap.entrySet()) { Map 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> 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>> 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> poolMap = fileTaskPoolMap.get(fileId); Map> poolTaskIdsMap = filePoolTaskIdsMap.getOrDefault(fileId, new HashMap<>()); - + List poolInfos = new ArrayList<>(); for (Map.Entry> poolEntry : poolMap.entrySet()) { Integer poolId = poolEntry.getKey(); List taskList = poolEntry.getValue(); List 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> 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 firstLevelNodes) { List> levelNodes = new ArrayList<>(); - + // 先添加第一层节点 if (CollectionUtils.isNotEmpty(firstLevelNodes)) { levelNodes.add(new ArrayList<>(firstLevelNodes)); } - + // 提取第一层节点的UUID作为初始父节点集合 Set 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 validateResult = validateBatchCreateNormalDirRequest(req); if (!validateResult.isSuccess()) { log.error("批量创建普通文件夹参数校验失败: {}", validateResult.getMessage()); return SdmResponse.failed(validateResult.getMessage()); } - + // 2. 父目录校验与权限检查 SdmResponse 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 validFolderItems = filterAndValidateFolderItems(req.getFolderItems()); if (validFolderItems.isEmpty()) { log.error("有效文件夹项为空"); return SdmResponse.failed("有效文件夹项为空"); } - + // 4. 检查已存在的文件夹,分离出需要创建的项 List toCreateItems = checkExistingFoldersAndFilter( validFolderItems, parentDir, resp); @@ -4469,40 +4488,40 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { log.info("所有文件夹均已存在,无需创建"); return SdmResponse.success(resp); } - + // 5. 准备创建数据(元数据对象 + MinIO Key) List newDirs = new ArrayList<>(); List 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 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 validFolderItems, FileMetadataInfo parentDir, BatchCreateNormalDirResp resp) { - + // 提取所有文件夹名称 List folderNames = validFolderItems.stream() .map(FolderItemReq::getFolderName) .collect(Collectors.toList()); - + // 查询数据库中是否已存在同名目录 List 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 existingNames = existingFiles.stream() .map(FileMetadataInfo::getOriginalName) .collect(Collectors.toSet()); - + // 过滤出需要创建的项 List 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 newDirs, List 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 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 page = new PageInfo<>(list); List 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 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).png;objectKey 保持版本号不变,改成 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 ids = req.getIds(); - if (ObjectUtils.isEmpty(ids)) { + if (CollectionUtils.isEmpty(ids)) { Long userId = ThreadLocalContext.getUserId(); // 查询回收站中的顶层节点(避免对子节点重复触发递归删除) List 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 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%'