大文件秒传-后端 Java 生产级实现代码
大文件秒传-后端 Java 生产级实现代码
本段为前端分片哈希 / 断点续算 / 秒传 100% 配套后端实现,包含:预秒传校验接口、分片上传接口、分片合并接口、文件去重逻辑、断点分片查询、数据库核心逻辑。代码为 SpringBoot 生产级写法,可直接部署使用。
一、核心依赖(pom.xml)
<!-- 文件操作 + MD5/SHA256 -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
二、数据库核心表 SQL(秒传去重核心)
1. 全局文件资源表(唯一物理文件)
CREATE TABLE file_resource (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
file_md5 VARCHAR(64) NOT NULL COMMENT '文件全局MD5指纹(前端分片聚合哈希)',
file_sha256 VARCHAR(128) NOT NULL COMMENT '二次校验SHA256',
file_size BIGINT NOT NULL COMMENT '文件字节大小',
file_suffix VARCHAR(20),
storage_path VARCHAR(255) NOT NULL COMMENT '物理磁盘路径',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_md5_size (file_md5,file_size)
) COMMENT '全局文件资源表(单实例存储,秒传核心)';
2. 用户文件关联表(多用户共享同一物理文件)
CREATE TABLE user_file (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
resource_id BIGINT NOT NULL,
file_name VARCHAR(255) NOT NULL,
folder_id BIGINT DEFAULT 0,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) COMMENT '用户文件视图表';
3. 分片临时记录表(断点续传核心)
CREATE TABLE file_chunk_task (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
task_id VARCHAR(64) NOT NULL COMMENT '上传唯一任务ID',
file_md5 VARCHAR(64) NOT NULL,
chunk_index INT NOT NULL COMMENT '分片序号',
chunk_size BIGINT NOT NULL,
chunk_md5 VARCHAR(64) NOT NULL COMMENT '单分片哈希',
total_chunks INT NOT NULL,
file_size BIGINT NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_task_chunk (task_id,chunk_index)
) COMMENT '文件分片上传临时任务表';
三、核心DTO 参数类
1. 预秒传校验 DTO
import lombok.Data;
@Data
public class PreUploadDTO {
// 前端分片聚合算出的全局文件MD5
private String fileHash;
// 文件完整大小
private Long fileSize;
// 文件名
private String fileName;
// 文件后缀
private String fileSuffix;
}
2. 分片上传 DTO
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Data
public class FileChunkDTO {
private String taskId;
private String fileHash;
private Integer chunkIndex;
private Integer totalChunks;
private Long chunkSize;
private Long fileSize;
private String chunkMd5;
private MultipartFile chunkFile;
}
四、后端核心接口实现(Controller)
1. 预上传秒传校验接口(核心秒传入口)
逻辑:根据 fileHash + fileSize 精准查重,命中直接秒传,未命中返回分片上传参数。
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/upload")
public class FileUploadController {
@Resource
private FileResourceService fileResourceService;
@Resource
private UserFileService userFileService;
/**
* 预上传秒传校验接口
*/
@PostMapping("/pre-upload")
public Map<String,Object> preUpload(@RequestBody PreUploadDTO dto, Long userId) {
Map<String,Object> result = new HashMap<>();
// 1. 双重校验:MD5 + 文件大小(杜绝采样哈希极低碰撞)
Long resourceId = fileResourceService.getResourceIdByHashAndSize(dto.getFileHash(), dto.getFileSize());
// 2. 命中资源:直接秒传,绑定用户记录
if (resourceId != null) {
// 新建用户文件关联记录
userFileService.bindUserFile(userId, resourceId, dto.getFileName());
result.put("fastUpload", true);
result.put("msg", "秒传成功");
return result;
}
// 3. 未命中:返回分片上传参数,进入正常分片上传
result.put("fastUpload", false);
result.put("taskId", UUID.randomUUID().toString().replace("-", ""));
// 沿用前端行业标准4MB分片
result.put("chunkSize", 4 * 1024 * 1024);
result.put("msg", "需要分片上传");
return result;
}
}
2. 分片上传接口(支持断点续传、去重)
@PostMapping("/chunk-upload")
public Map<String,Object> chunkUpload(FileChunkDTO dto) {
Map<String,Object> result = new HashMap<>();
// 1. 判断当前分片是否已上传(断点续传核心)
boolean chunkExists = fileChunkService.checkChunkExists(dto.getTaskId(), dto.getChunkIndex());
if (chunkExists) {
result.put("success", true);
result.put("msg", "分片已存在,跳过");
return result;
}
// 2. 保存分片到临时目录
String tempPath = fileChunkService.saveChunkFile(dto);
// 3. 写入分片任务记录
fileChunkService.saveChunkTask(dto, tempPath);
result.put("success", true);
result.put("msg", "分片上传成功");
return result;
}
3. 分片合并接口(文件落地、入库、支持后续秒传)
@PostMapping("/merge")
public Map<String,Object> mergeFile(String taskId, String fileName, Long userId) {
Map<String,Object> result = new HashMap<>();
// 1. 校验所有分片完整性、序号连续性
boolean allComplete = fileChunkService.checkAllChunkComplete(taskId);
if (!allComplete) {
result.put("success", false);
result.put("msg", "分片未上传完成");
return result;
}
// 2. 合并分片为完整文件
String fullFilePath = fileChunkService.mergeChunkFile(taskId);
// 3. 服务端二次计算全局MD5/SHA256(防篡改、防前端造假)
String serverMd5 = FileMD5Util.getFileMD5(fullFilePath);
String serverSha256 = FileMD5Util.getFileSHA256(fullFilePath);
// 4. 再次查重,防止并发重复上传
Long resourceId = fileResourceService.getResourceIdByHashAndSize(serverMd5, new File(fullFilePath).length());
if (resourceId == null) {
// 全新文件:录入全局资源库,供全网秒传
resourceId = fileResourceService.saveFileResource(serverMd5,serverSha256,fullFilePath,fileName);
}
// 5. 绑定用户文件记录
userFileService.bindUserFile(userId, resourceId, fileName);
// 6. 清理无用分片临时文件
fileChunkService.clearChunkTemp(taskId);
result.put("success", true);
result.put("msg", "上传完成,已纳入秒传资源库");
return result;
}
五、文件哈希工具类(后端二次校验核心)
用于服务端复核文件指纹,防止前端篡改、伪造哈希,生产必须配置。
import org.apache.commons.codec.digest.DigestUtils;
import java.io.File;
import java.io.FileInputStream;
public class FileMD5Util {
public static String getFileMD5(File file) {
try (FileInputStream fis = new FileInputStream(file)) {
return DigestUtils.md5Hex(fis);
} catch (Exception e) {
throw new RuntimeException("文件MD5计算失败");
}
}
public static String getFileMD5(String filePath) {
return getFileMD5(new File(filePath));
}
public static String getFileSHA256(String filePath) {
try (FileInputStream fis = new FileInputStream(filePath)) {
return DigestUtils.sha256Hex(fis);
} catch (Exception e) {
throw new RuntimeException("文件SHA256计算失败");
}
}
}
六、完整 Mapper 数据持久层代码
6.1 FileResourceMapper(全局文件资源Mapper)
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
public interface FileResourceMapper {
/**
* 根据MD5+文件大小查询资源ID(秒传核心查重)
*/
@Select("select id from file_resource where file_md5 = #{fileMd5} and file_size = #{fileSize} limit 1")
Long getResourceId(@Param("fileMd5") String fileMd5, @Param("fileSize") Long fileSize);
/**
* 新增全局文件资源记录
*/
@Insert("insert into file_resource(file_md5,file_sha256,file_size,file_suffix,storage_path) " +
"values(#{fileMd5},#{fileSha256},#{fileSize},#{fileSuffix},#{storagePath})")
int insertResource(@Param("fileMd5") String fileMd5,
@Param("fileSha256") String fileSha256,
@Param("fileSize") Long fileSize,
@Param("fileSuffix") String fileSuffix,
@Param("storagePath") String storagePath);
}
6.2 UserFileMapper(用户文件关联Mapper)
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
public interface UserFileMapper {
/**
* 绑定用户与全局文件资源关联关系
*/
@Insert("insert into user_file(user_id,resource_id,file_name) values(#{userId},#{resourceId},#{fileName})")
int bindUserFile(@Param("userId") Long userId,
@Param("resourceId") Long resourceId,
@Param("fileName") String fileName);
}
6.3 FileChunkTaskMapper(分片任务Mapper)
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
public interface FileChunkTaskMapper {
/**
* 查询分片是否已上传
*/
@Select("select count(1) from file_chunk_task where task_id = #{taskId} and chunk_index = #{chunkIndex}")
int countChunkExist(@Param("taskId") String taskId, @Param("chunkIndex") Integer chunkIndex);
/**
* 新增分片任务记录
*/
@Insert("insert into file_chunk_task(task_id,file_md5,chunk_index,chunk_size,chunk_md5,total_chunks,file_size) " +
"values(#{taskId},#{fileMd5},#{chunkIndex},#{chunkSize},#{chunkMd5},#{totalChunks},#{fileSize})")
int insertChunkTask(@Param("taskId") String taskId,
@Param("fileMd5") String fileMd5,
@Param("chunkIndex") Integer chunkIndex,
@Param("chunkSize") Long chunkSize,
@Param("chunkMd5") String chunkMd5,
@Param("totalChunks") Integer totalChunks,
@Param("fileSize") Long fileSize);
/**
* 查询当前任务已上传的所有分片序号
*/
@Select("select chunk_index from file_chunk_task where task_id = #{taskId} order by chunk_index")
List<Integer> getUploadedChunkIndex(@Param("taskId") String taskId);
}
七、完整 Service 业务层代码
7.1 FileResourceService(全局文件资源业务)
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class FileResourceService {
@Resource
private FileResourceMapper fileResourceMapper;
/**
* 查重获取文件资源ID
*/
public Long getResourceIdByHashAndSize(String fileMd5, Long fileSize) {
return fileResourceMapper.getResourceId(fileMd5, fileSize);
}
/**
* 保存全新文件资源,返回资源ID
*/
public Long saveFileResource(String fileMd5, String fileSha256, String filePath, String fileName) {
// 截取文件后缀
String suffix = fileName.lastIndexOf(".") == -1 ? "" : fileName.substring(fileName.lastIndexOf("."));
long fileSize = new java.io.File(filePath).length();
fileResourceMapper.insertResource(fileMd5, fileSha256, fileSize, suffix, filePath);
// 回查自增主键ID
return getResourceIdByHashAndSize(fileMd5, fileSize);
}
}
7.2 UserFileService(用户文件关联业务)
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class UserFileService {
@Resource
private UserFileMapper userFileMapper;
/**
* 绑定用户文件记录(秒传核心:无需存文件,仅关联映射)
*/
public void bindUserFile(Long userId, Long resourceId, String fileName) {
userFileMapper.bindUserFile(userId, resourceId, fileName);
}
}
7.3 FileChunkService(分片上传核心业务)
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
@Service
public class FileChunkService {
// 本地临时分片存储目录,可配置到yml
private static final String CHUNK_TEMP_PATH = "/data/upload/chunk/";
@Resource
private FileChunkTaskMapper fileChunkTaskMapper;
/**
* 判断分片是否已上传
*/
public boolean checkChunkExists(String taskId, Integer chunkIndex) {
return fileChunkTaskMapper.countChunkExist(taskId, chunkIndex) > 0;
}
/**
* 保存分片文件到临时目录
*/
public String saveChunkFile(FileChunkDTO dto) {
// 创建任务专属临时文件夹
String taskPath = CHUNK_TEMP_PATH + dto.getTaskId() + "/";
File taskDir = new File(taskPath);
if (!taskDir.exists()) {
taskDir.mkdirs();
}
// 分片文件命名:序号-分片MD5
String chunkFilePath = taskPath + dto.getChunkIndex() + "-" + dto.getChunkMd5();
File chunkFile = new File(chunkFilePath);
try (FileOutputStream fos = new FileOutputStream(chunkFile)) {
MultipartFile chunkFileData = dto.getChunkFile();
fos.write(chunkFileData.getBytes());
} catch (IOException e) {
throw new RuntimeException("分片保存失败");
}
return chunkFilePath;
}
/**
* 保存分片任务记录
*/
public void saveChunkTask(FileChunkDTO dto, String tempPath) {
fileChunkTaskMapper.insertChunkTask(
dto.getTaskId(),
dto.getFileHash(),
dto.getChunkIndex(),
dto.getChunkSize(),
dto.getChunkMd5(),
dto.getTotalChunks(),
dto.getFileSize()
);
}
/**
* 校验所有分片是否全部上传完成
*/
public boolean checkAllChunkComplete(String taskId) {
List<Integer> uploadedIndexList = fileChunkTaskMapper.getUploadedChunkIndex(taskId);
if (uploadedIndexList == null || uploadedIndexList.isEmpty()) {
return false;
}
// 简单完整性校验:可根据业务强化序号连续性
int maxIndex = uploadedIndexList.stream().max(Integer::compareTo).get();
return uploadedIndexList.size() == (maxIndex + 1);
}
/**
* 合并所有分片为完整文件
*/
public String mergeChunkFile(String taskId) {
String taskPath = CHUNK_TEMP_PATH + taskId + "/";
File taskDir = new File(taskPath);
if (!taskDir.exists()) {
throw new RuntimeException("分片文件不存在");
}
// 合并后完整文件路径
String fullFilePath = "/data/upload/full/" + taskId;
File fullFile = new File(fullFilePath);
// 创建父目录
if (!fullFile.getParentFile().exists()) {
fullFile.getParentFile().mkdirs();
}
// 按序号顺序读取分片合并
List<Integer> indexList = fileChunkTaskMapper.getUploadedChunkIndex(taskId);
try (FileOutputStream fos = new FileOutputStream(fullFile)) {
for (Integer index : indexList) {
File chunkFile = new File(taskPath + index + "-*");
File[] files = chunkFile.getParentFile().listFiles((dir, name) -> name.startsWith(index + "-"));
if (files != null && files.length > 0) {
// 写入分片数据
java.nio.file.Files.copy(files[0].toPath(), fos);
}
}
} catch (IOException e) {
throw new RuntimeException("文件合并失败");
}
return fullFilePath;
}
/**
* 清理分片临时文件与目录
*/
public void clearChunkTemp(String taskId) {
String taskPath = CHUNK_TEMP_PATH + taskId + "/";
File taskDir = new File(taskPath);
if (taskDir.exists()) {
// 删除所有分片文件
File[] files = taskDir.listFiles();
if (files != null) {
for (File file : files) {
file.delete();
}
}
taskDir.delete();
}
}
}
八、生产级核心逻辑说明(与前端完全对齐)
-
秒传逻辑闭环:前端分片聚合MD5 + 文件大小上传 → 后端双字段查重 → 命中直接绑定用户记录,零传输完成秒传。
-
防碰撞策略:前端精准分片哈希 + 后端MD5+SHA256双校验 + 文件大小强校验,彻底杜绝采样哈希碰撞风险。
-
断点续传兼容:后端记录每一个分片任务,刷新重传自动跳过已上传分片,和前端断点续算逻辑双向打通。
-
全局去重存储:物理文件只存一份,多用户共享资源ID,极大节省磁盘空间,是网盘秒传的底层架构。
-
安全兜底:后端不信任前端哈希,文件合并后服务端重新全量哈希复核,防止前端伪造指纹秒传恶意文件。
九、前后端完整调用链路(最终闭环)
文件选择 → 前端WebWorker分片增量哈希计算 → 预上传请求查重 → 【命中:秒传成功】 / 【未命中:分片并行上传】 → 分片完整后后端合并文件 → 后端二次哈希校验 → 入库纳入全局秒传资源池。
十、项目落地补充配置(yml生产配置)
spring:
servlet:
multipart:
# 适配大文件分片上传
max-file-size: 100GB
max-request-size: 100GB
datasource:
# 数据库配置自行替换
url: jdbc:mysql://127.0.0.1:3306/upload_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
# 自定义文件上传路径
upload:
chunk-temp-path: /data/upload/chunk/
full-path: /data/upload/full/
十一、生产环境避坑总结
-
路径适配:Windows/Linux 环境需修改文件存储路径,避免路径不存在报错
-
权限配置:服务器需开启文件夹读写权限,防止文件保存/合并失败
-
分片超时清理:建议新增定时任务,清理7天未完成的过期分片文件,避免磁盘堆积
-
并发防护:文件查重、资源绑定接口增加分布式锁,防止并发重复上传
-
文件校验兜底:合并后文件大小与前端传入大小不一致时,直接判定文件异常,拒绝入库
-
秒传逻辑闭环:前端分片聚合MD5 + 文件大小上传 → 后端双字段查重 → 命中直接绑定用户记录,零传输完成秒传。
-
防碰撞策略:前端精准分片哈希 + 后端MD5+SHA256双校验 + 文件大小强校验,彻底杜绝采样哈希碰撞风险。
-
断点续传兼容:后端记录每一个分片任务,刷新重传自动跳过已上传分片,和前端断点续算逻辑双向打通。
-
全局去重存储:物理文件只存一份,多用户共享资源ID,极大节省磁盘空间,是网盘秒传的底层架构。
-
安全兜底:后端不信任前端哈希,文件合并后服务端重新全量哈希复核,防止前端伪造指纹秒传恶意文件。
更多推荐


所有评论(0)