feat: 添加文件哈希检查功能,支持秒传
- 新增 HashCheckRespDTO 用于哈希检查响应 - 文件上传接口支持可选的 hash 参数,用于秒传 - 新增 /check 接口用于检查文件哈希是否存在 - 简化上传逻辑,移除同步/异步哈希计算配置 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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<AttachmentDO> uploadFile(@RequestParam("file")MultipartFile file) {
|
||||
return Results.success(attachmentService.upload(file));
|
||||
public Result<AttachmentDO> 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<HashCheckRespDTO> checkHash(@RequestParam("hash") String hash) {
|
||||
return Results.success(attachmentService.checkHash(hash));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
|
||||
AttachmentDO upload(MultipartFile file);
|
||||
AttachmentDO upload(MultipartFile file, String hash);
|
||||
|
||||
/**
|
||||
* 检查文件哈希是否存在(用于秒传)
|
||||
*
|
||||
* @param hash 文件哈希值
|
||||
* @return 哈希检查结果
|
||||
*/
|
||||
HashCheckRespDTO checkHash(String hash);
|
||||
}
|
||||
|
||||
@@ -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<AttachmentMapper, AttachmentDO> 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<AttachmentDO>()
|
||||
.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<AttachmentMapper, Attachm
|
||||
String datePath = LocalDateTime.now().toLocalDate().toString().replace("-", "/");
|
||||
String relativePath = datePath + "/" + fileName;
|
||||
|
||||
String fileUrl = fileStorageStrategy.upload(file, relativePath);
|
||||
fileStorageStrategy.upload(file, relativePath);
|
||||
|
||||
AttachmentDO attachmentDO = new AttachmentDO();
|
||||
attachmentDO.setId(IdUtil.getSnowflakeNextId());
|
||||
@@ -124,4 +94,28 @@ public class AttachmentServiceImpl extends ServiceImpl<AttachmentMapper, Attachm
|
||||
|
||||
return attachmentDO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashCheckRespDTO checkHash(String hash) {
|
||||
HashCheckRespDTO respDTO = new HashCheckRespDTO();
|
||||
respDTO.setExists(false);
|
||||
|
||||
if (StrUtil.isBlank(hash)) {
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
AttachmentDO existingFile = getOne(new LambdaQueryWrapper<AttachmentDO>()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user