diff --git a/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/controller/AttachmentController.java b/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/controller/AttachmentController.java index a6ee585..3f85504 100644 --- a/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/controller/AttachmentController.java +++ b/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/controller/AttachmentController.java @@ -1,6 +1,7 @@ package cn.meowrain.aioj.backend.fileservice.controller; import cn.meowrain.aioj.backend.fileservice.dao.entity.AttachmentDO; +import cn.meowrain.aioj.backend.fileservice.dto.resp.HashCheckRespDTO; import cn.meowrain.aioj.backend.fileservice.service.AttachmentService; import cn.meowrain.aioj.backend.framework.core.web.Result; import cn.meowrain.aioj.backend.framework.core.web.Results; @@ -29,8 +30,15 @@ public class AttachmentController { private final AttachmentService attachmentService; @Operation(summary = "通用文件上传组件") @PostMapping("/upload") - public Result uploadFile(@RequestParam("file")MultipartFile file) { - return Results.success(attachmentService.upload(file)); + public Result uploadFile(@RequestParam("file") MultipartFile file, + @RequestParam(value = "hash", required = false) String hash) { + return Results.success(attachmentService.upload(file, hash)); + } + + @Operation(summary = "哈希是否存在") + @GetMapping("/check") + public Result checkHash(@RequestParam("hash") String hash) { + return Results.success(attachmentService.checkHash(hash)); } /** diff --git a/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/dto/resp/HashCheckRespDTO.java b/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/dto/resp/HashCheckRespDTO.java new file mode 100644 index 0000000..f8caa76 --- /dev/null +++ b/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/dto/resp/HashCheckRespDTO.java @@ -0,0 +1,16 @@ +package cn.meowrain.aioj.backend.fileservice.dto.resp; + +import lombok.Data; + +@Data +public class HashCheckRespDTO { + Boolean exists; + Long fileId; + String url; + + @Data + static class FileInfo { + Long size; + String mime; + } +} diff --git a/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/service/AttachmentService.java b/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/service/AttachmentService.java index e279df8..5c12664 100644 --- a/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/service/AttachmentService.java +++ b/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/service/AttachmentService.java @@ -1,6 +1,7 @@ package cn.meowrain.aioj.backend.fileservice.service; import cn.meowrain.aioj.backend.fileservice.dao.entity.AttachmentDO; +import cn.meowrain.aioj.backend.fileservice.dto.resp.HashCheckRespDTO; import com.baomidou.mybatisplus.extension.service.IService; import org.springframework.web.multipart.MultipartFile; @@ -15,5 +16,13 @@ import org.springframework.web.multipart.MultipartFile; public interface AttachmentService extends IService { - AttachmentDO upload(MultipartFile file); + AttachmentDO upload(MultipartFile file, String hash); + + /** + * 检查文件哈希是否存在(用于秒传) + * + * @param hash 文件哈希值 + * @return 哈希检查结果 + */ + HashCheckRespDTO checkHash(String hash); } diff --git a/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/service/impl/AttachmentServiceImpl.java b/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/service/impl/AttachmentServiceImpl.java index 37e86a1..a82b38e 100644 --- a/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/service/impl/AttachmentServiceImpl.java +++ b/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/service/impl/AttachmentServiceImpl.java @@ -3,18 +3,16 @@ package cn.meowrain.aioj.backend.fileservice.service.impl; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; -import cn.hutool.crypto.digest.DigestUtil; import cn.meowrain.aioj.backend.fileservice.dao.entity.AttachmentDO; import cn.meowrain.aioj.backend.fileservice.dao.mapper.AttachmentMapper; +import cn.meowrain.aioj.backend.fileservice.dto.resp.HashCheckRespDTO; import cn.meowrain.aioj.backend.fileservice.service.AttachmentService; -import cn.meowrain.aioj.backend.fileservice.service.HashCalculationService; import cn.meowrain.aioj.backend.fileservice.storage.FileStorageStrategy; import cn.meowrain.aioj.backend.framework.core.enums.DelStatusEnum; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -35,58 +33,30 @@ import java.time.LocalDateTime; public class AttachmentServiceImpl extends ServiceImpl implements AttachmentService { private final FileStorageStrategy fileStorageStrategy; - private final HashCalculationService hashCalculationService; - - /** - * 是否启用文件去重 - */ - @Value("${file-config.deduplication-enabled:true}") - private boolean deduplicationEnabled; - - /** - * 同步计算哈希的文件大小阈值(字节) - * 超过此大小异步计算,默认 10MB - */ - @Value("${file-config.sync-hash-threshold:10485760}") - private long syncHashThreshold; @Override - public AttachmentDO upload(MultipartFile file) { + public AttachmentDO upload(MultipartFile file, String hash) { try { String originalFilename = file.getOriginalFilename(); String fileExtension = FileUtil.extName(originalFilename); - long fileSize = file.getSize(); - - // 判断文件大小,选择处理方式 - boolean isSmallFile = fileSize <= syncHashThreshold; - - if (deduplicationEnabled && isSmallFile) { - // ========== 小文件:同步计算哈希,立即去重 ========== - String fileHash = DigestUtil.sha256Hex(file.getInputStream()); + // 如果传入了hash,先检查是否已存在(秒传) + if (StrUtil.isNotBlank(hash)) { AttachmentDO existingFile = getOne(new LambdaQueryWrapper() - .eq(AttachmentDO::getFileHash, fileHash) + .eq(AttachmentDO::getFileHash, hash) .eq(AttachmentDO::getIsDeleted, DelStatusEnum.STATUS_NORMAL.code()) .last("LIMIT 1")); if (existingFile != null) { - log.info("小文件已存在,返回已有文件: fileName={}, fileHash={}, existingId={}", - originalFilename, fileHash, existingFile.getId()); + log.info("文件已存在,返回已有文件: fileName={}, hash={}, existingId={}", + originalFilename, hash, existingFile.getId()); return existingFile; } - - // 文件不存在,上传并保存哈希 - return uploadAndSave(file, originalFilename, fileExtension, fileHash); - } else { - // ========== 大文件:直接上传,异步计算哈希 ========== - AttachmentDO attachmentDO = uploadAndSave(file, originalFilename, fileExtension, null); - - // 异步计算哈希(只计算,不删除文件) - hashCalculationService.calculateHashAsync(attachmentDO.getId(), attachmentDO.getStoragePath(), attachmentDO.getStorageType()); - - return attachmentDO; } + // 文件不存在,上传并保存 + return uploadAndSave(file, originalFilename, fileExtension, hash); + } catch (IOException e) { log.error("文件上传失败: {}", file.getOriginalFilename(), e); throw new RuntimeException("文件上传失败: " + e.getMessage(), e); @@ -102,7 +72,7 @@ public class AttachmentServiceImpl extends ServiceImpl() + .eq(AttachmentDO::getFileHash, hash) + .eq(AttachmentDO::getIsDeleted, DelStatusEnum.STATUS_NORMAL.code()) + .last("LIMIT 1")); + + if (existingFile != null) { + respDTO.setExists(true); + respDTO.setFileId(existingFile.getId()); + respDTO.setUrl(fileStorageStrategy.getFileUrl(existingFile.getStoragePath())); + log.info("文件哈希已存在,支持秒传: hash={}, fileId={}", hash, existingFile.getId()); + } + + return respDTO; + } }