大文件秒传-后端 Java 生产级实现代码

大文件秒传-后端 Java 生产级实现代码
大文件秒传-后端 Java 生产级实现代码本段为前端分片哈希 / 断点续算 / 秒传100% 配套后端实现包含预秒传校验接口、分片上传接口、分片合并接口、文件去重逻辑、断点分片查询、数据库核心逻辑。代码为 SpringBoot 生产级写法可直接部署使用。一、核心依赖pom.xml!-- 文件操作 MD5/SHA256 -- dependency groupIdcommons-codec/groupId artifactIdcommons-codec/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-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. 预秒传校验 DTOimport lombok.Data; Data public class PreUploadDTO { // 前端分片聚合算出的全局文件MD5 private String fileHash; // 文件完整大小 private Long fileSize; // 文件名 private String fileName; // 文件后缀 private String fileSuffix; }2. 分片上传 DTOimport 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; }四、后端核心接口实现Controller1. 预上传秒传校验接口核心秒传入口逻辑根据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 MapString,Object preUpload(RequestBody PreUploadDTO dto, Long userId) { MapString,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 MapString,Object chunkUpload(FileChunkDTO dto) { MapString,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 MapString,Object mergeFile(String taskId, String fileName, Long userId) { MapString,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全局文件资源Mapperimport 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用户文件关联Mapperimport 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分片任务Mapperimport 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) ListInteger 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) { ListInteger 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(); } // 按序号顺序读取分片合并 ListInteger 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 文件大小上传 → 后端双字段查重 → 命中直接绑定用户记录零传输完成秒传。防碰撞策略前端精准分片哈希 后端MD5SHA256双校验 文件大小强校验彻底杜绝采样哈希碰撞风险。断点续传兼容后端记录每一个分片任务刷新重传自动跳过已上传分片和前端断点续算逻辑双向打通。全局去重存储物理文件只存一份多用户共享资源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?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai username: root password: 123456 # 自定义文件上传路径 upload: chunk-temp-path: /data/upload/chunk/ full-path: /data/upload/full/十一、生产环境避坑总结路径适配Windows/Linux 环境需修改文件存储路径避免路径不存在报错权限配置服务器需开启文件夹读写权限防止文件保存/合并失败分片超时清理建议新增定时任务清理7天未完成的过期分片文件避免磁盘堆积并发防护文件查重、资源绑定接口增加分布式锁防止并发重复上传文件校验兜底合并后文件大小与前端传入大小不一致时直接判定文件异常拒绝入库秒传逻辑闭环前端分片聚合MD5 文件大小上传 → 后端双字段查重 → 命中直接绑定用户记录零传输完成秒传。防碰撞策略前端精准分片哈希 后端MD5SHA256双校验 文件大小强校验彻底杜绝采样哈希碰撞风险。断点续传兼容后端记录每一个分片任务刷新重传自动跳过已上传分片和前端断点续算逻辑双向打通。全局去重存储物理文件只存一份多用户共享资源ID极大节省磁盘空间是网盘秒传的底层架构。安全兜底后端不信任前端哈希文件合并后服务端重新全量哈希复核防止前端伪造指纹秒传恶意文件。

最新新闻

日新闻

周新闻

月新闻