springboot 后端大文件分片上传(断点上传)
目录简介1. 数据库表创建2. 编写文件记录标识接口3. 编写分片是否存在判断接口4. 编写文件分片上传接口5. 编写文件分片合并接口简介什么是分片上传?分片上传就是把一个大的文件分成N个部分然后一部分一部分的进行上传。分片上传有什么好处分片上传可以实现断网、关浏览器、传输错误等情况发生后再次进行此文件的上传的时候已上传的部分无需继续上传减少上传文件的流量消耗与用户的等待时间。后端如何实现分片上传后端传统上传直接开发一个接口接受文件即可但是分片上传如何实现呢按照分片的解读理解咱们可以将上传接口进行拆分①. 首先咱们需要一个接口来记录标识一个文件这样才能区分不同文件②. 然后咱们还需要一个接口用来判断某个分片是否已经上传完成③. 再然后还需要一个接口用来进行分片文件的上传其实就是一个简单的上传功能将文件分片存放并且记录到文件分片上传记录中④. 最后还需要一个最重要的接口那当然就是分片的合并接口了调用此接口按照顺序将某个文件的所有分片合并成真实的对应的文件。那么废话不多说下面咱们就开始编码有问题可留言评论断点上传不是断点续传断点续传是特指下载这又是另外的知识了注ResponseVo为自定义统一返回格式自行定义接口中用到jackjson包;接口中用到CommonException为自定义异常自行定义或其他方式处理接口中用到FileExistsResponse为判断文件分片是否存在的返回数据结构自行定义接口中用到的filemapper等数据库层的内容自行实现数据库涉及到一张表file_uploadFileUpload为文件上传表file_upload的实体1. 数据库表创建create table file_upload ( id NUMBER not null, index NUMBER, parent NUMBER, name VARCHAR2(100 char) not null, path VARCHAR2(255 char), md5 VARCHAR2(200 char) not null, size NUMBER not null constraint PK_FILE_UPLOAD primary key (id) );2. 编写文件记录标识接口此接口每个文件调用一次调用完成后需要返回文件记录的信息主要是返回ID以便前端拿到后作为该文件所有分片的parent。接口入参public class FileNewRequest { private String index; private String parent; private String name; private String path; private String md5; private String size; public String getIndex() { return index; } public void setIndex(String index) { this.index index; } public String getParent() { return parent; } public void setParent(String parent) { this.parent parent; } public String getName() { return name; } public void setName(String name) { this.name name; } public String getPath() { return path; } public void setPath(String path) { this.path path; } public String getMd5() { return md5; } public void setMd5(String md5) { this.md5 md5; } public String getSize() { return size; } public void setSize(String size) { this.size size; } }接口/** * 新创建一个文件标识记录 * author kevin * param request : * return com.liu.services.system.entity.FileUpload * date 2021/1/8 */ ApiOperation(value 文件标识记录, notes /file/new) PostMapping(/file/new) public ResponseVo uploadFile(Valid RequestBody FileNewRequest request) { String fileName request.getName(); String md5 request.getMd5(); try { //写入文件标识记录 FileUpload file (FileUpload) JSONObject.toBean(JSONObject.fromObject(request), FileUpload.class); //如果文件标识记录不存在则插入;如果存在则更新文件大小 FileUpload fileUpload fileMapper.findByMd5AndName(md5, fileName); if (ObjectUtils.isNotEmpty(fileUpload)) { fileMapper.updateSize(file); } else { fileMapper.insert(file); } fileUpload fileMapper.findByMd5AndName(md5, fileName); return new ResponseVo.Builder().ok().data(fileUpload).build(); }catch (Exception e){ return new ResponseVo.Builder().error().message(String.format(文件%1$s 上传错误, fileName)).build(); } }3. 编写分片是否存在判断接口此接口前端每个分片处理都需要调用判断跟第4点中的接口配合使用此处parent为第2点中接口返回的ID接口入参public class FileExistsRequest { private Long parent; private String md5; private Long size; public Long getParent() { return parent; } public void setParent(Long parent) { this.parent parent; } public String getMd5() { return md5; } public void setMd5(String md5) { this.md5 md5; } public Long getSize() { return size; } public void setSize(Long size) { this.size size; } }接口/** * 文件分片上传判断文件记录或者文件分片是否已经存在 * * param request : * return com.liu.base.ResponseVo * author kevin * date 2021/1/9 */ PostMapping(/file/exists) public ResponseVo fileExists(Valid RequestBody FileExistsRequest request) { FileUpload file; if(null ! request.getParent()){ file fileMapper.findByParentAndMd5(String.valueOf(request.getParent()), request.getMd5()); }else{ ListFileUpload files fileMapper.findByMd5(request.getMd5()); //查找父文件ID不为空的记录即文件分片记录 files files.stream().filter(item - item.getParent() ! null).collect(Collectors.toList()); int count files.size(); if(count 1){ throw new CommonException(找到了不止一个分片/文件记录); }else if (count 1){ file null; }else { file files.get(0); } } //文件标识或文件片不存在 if(file null) { return new ResponseVo.Builder().ok().data(FileExistsResponse.nonExistent()).build(); } //无父文件ID则表示记录为文件标识记录判断文件是否存在 if(null file.getParent() || 0 file.getParent()){ ListFileUpload patchs fileMapper.findByParent(file.getId()); Long total patchs.stream().mapToLong(FileUpload::getSize).sum(); if(total.equals(request.getSize())) { return new ResponseVo.Builder().ok().data(FileExistsResponse.exists(file.getId())).build(); }else if(total 0) { return new ResponseVo.Builder().ok().data(FileExistsResponse.nonExistent()).build(); }else { return new ResponseVo.Builder().ok().data( FileExistsResponse.partExistent(file.getId(), fileMapper.findPatchIndexByParent(file.getId())) ).build(); } } //父文件ID存在需要判断的内容为文件片 //文件片存在且大小为正确的大小表示文件片已存在 if(file.getFileSize().equals(request.getFileSize())) { return new ResponseVo.Builder().ok().data(FileExistsResponse.exists(file.getId())).build(); } //文件片大小为0表示文件片还未上传 if(file.getFileSize() 0) { return new ResponseVo.Builder().ok().data(FileExistsResponse.nonExistent()).build(); } //其他文件片大小不为0又不等于需要上传的文件片大小表示文件片存在部分 return new ResponseVo.Builder().ok().data( FileExistsResponse.partExistent(file.getId(), fileMapper.findPatchIndexByParent(file.getId())) ).build(); }4. 编写文件分片上传接口此接口在前端调用文件分片是否存在判断分片不存在或只存在部分后调用此处parent为第2点中接口返回的ID接口入参public class FilePatchUploadRequest { private String name; private Long index; private Long parent; private String md5; private Long size; public String getName() { return name; } public void setName(String name) { this.name name; } public Long getIndex() { return index; } public void setIndex(Long index) { this.index index; } public Long getParent() { return parent; } public void setParent(Long parent) { this.parent parent; } public String getMd5() { return md5; } public void setMd5(String md5) { this.md5 md5; } public Long getSize() { return size; } public void setSize(Long size) { this.size size; } }接口/** * 文件分片上传 * * param request : * param patch : * return com.liu.base.ResponseVo * author kevin * date 2021/1/7 */ PostMapping(/file/patch/upload) public ResponseVo filePatchUpload(Valid FilePatchUploadRequest request, RequestParam(patch) MultipartFile patch){ Long index request.getIndex(); //判断分片文件是否存在 if(patch.isEmpty()){ return new ResponseVo.Builder().error().message(第:%1$s 个分片未获取到分片文件).build(); } Long parent request.getParent(); Long size request.getSize(); String md5 request.getMd5(); String name request.getName(); //开始分片保存 FileUpload file fileMapper.findByParentAndMd5(String.valueOf(parent), md5); try { //判断数据库如果分片不存在或者分片大小与上传的分片的大小不一致则上传保存分片 if (file null || !file.getFileSize().equals(size)) { Optional.ofNullable(file).ifPresent(e - fileMapper.deleteByPk(e.getId())); //保存分片文件到服务器构建文件分片记录信息 FileUpload fileUpload new FileUpload(index, parent, name, saveFile(patch, size, d:/), md5, size); return new ResponseVo.Builder().ok().build(); } }catch (IOException e){ log.error(SystemConst.PATCH_UPLOAD_FILE_ERROR_LOG, name, index, e); throw new CommonException(ResponseState.REQUEST_ERROR.getCode(), String.format(SystemConst.UPLOAD_FILE_PATCH_ERROR, name, index)); } return new ResponseVo.Builder().ok().build() ; } /** * 保存文件块(此方法可提取为一个工具类中的工具方法) * author kevin * param source : 文件块 * param size : 文件块大小 * return java.lang.String * date 2021/1/7 */ private String saveFile(MultipartFile source, Long size, String path) throws IOException { String now LocalDateTime.now().format(DateTimeFormatter.ofPattern(SystemConst.DATE_ONLY_FORMATTER)); if(null source || source.isEmpty()) { throw new CommonException(未获取到需要保存的分片文件); } if(source.getSize() ! size) { throw new CommonException(分片文件大小与传入的分片大小不一致); } path path File.separator now; File file new File(path); if(!file.exists() !file.mkdirs()) { throw new CommonException(创建文件夹失败); } //根据文件名获取文件类型 String fileType (null fileName || !fileName.contains(.)) ? unknown : fileName.substring(fileName.lastIndexOf(.)); //保存文件块到磁盘 while(true) { String saveFileName UUID.randomUUID() fileType.toLowerCase() .tmp; File saveFilePath new File(path, saveFileName); //磁盘上块文件名如果已经存在则重新生成文件名 if(saveFilePath.exists()) { continue; } source.transferTo(saveFilePath); return saveFilePath.getAbsolutePath(); } }5. 编写文件分片合并接口此接口在前端确认所有分片已完成上传后调用。此处parent为第2点中接口返回的ID接口入参public class FileMergeRequest { private Long parent; private Long fileSize; public Long getParent() { return parent; } public void setParent(Long parent) { this.parent parent; } public Long getFileSize() { return fileSize; } public void setFileSize(Long fileSize) { this.fileSize fileSize; } }接口/** * 文件分片合并 * author kevin * param request : * return com.liu.base.ResponseVo * date 2021/1/8 */ ApiOperation(value 文件分片合并, notes /file/merge) PostMapping(/file/merge) public ResponseVo filePatchMerge(Valid RequestBody FileMergeRequest request) { Long parent request.getParent(); Long size request.getFileSize(); //查询当前文件 FileUpload fileInfo fileMapper.findByPk(parent); String fileName null fileInfo ? : fileInfo.getName(); String md5 null fileInfo ? : fileInfo.getMd5(); try { //查询当前文件所有分块 ListFileUpload patchs fileMapper.findByParentPatchIndex(parent); Long total patchs.stream().mapToLong(FileUpload::getFileSize).sum(); if (fileInfo null || CollectionUtils.isEmpty(patchs) || !total.equals(size)) { fileMapper.deleteByParent(parent); log.error(String.format(文件合并失败大小不正确); return new ResponseVo.Builder().error().message(文件合并失败大小不正确).build(); } //合并文件 String path mergeFile(fileName, patchs.stream().map(FileUpload::getPath) .collect(Collectors.toList()), filePath, userName); fileMapper.updateByIdSetPathAndSize(String.valueOf(parent), path, String.valueOf(total)); fileMapper.deleteByParent(parent); }catch (Exception e){ fileMapper.deleteByParent(parent); e.printStackTrace(); return new ResponseVo.Builder().error().message(文件合并失败e.getMessage()).build(); } return new ResponseVo.Builder().ok().build(); } /** * 合并文件块(此方法可提取到工具类) * author kevin * param fileName : 文件名称 * param filePaths : 文件分片路径 * param path : 文件存放路径 * param directory : 文件存放目录 * return java.lang.String * date 2021/1/8 */ private String mergeFile(String fileName, ListString filePaths, String path, String directory) throws IOException { String now LocalDateTime.now().format(DateTimeFormatter.ofPattern(SystemConst.DATE_ONLY_FORMATTER)); path path File.separator (StringUtils.isEmpty(directory) ? : (directory File.separator)) now; File file new File(path); if(!file.exists() !file.mkdirs()) { throw new CommonException(创建文件夹失败); } //生成文件名 File saveFilePath new File(path, fileName); while (saveFilePath.exists()){ //生成文件名 Files.delete(saveFilePath.toPath()); } //创建文件输出流 try (FileOutputStream fos new FileOutputStream(saveFilePath)){ FileChannel out fos.getChannel(); //循环每一个文件块对应的文件 for (String filename : filePaths) { File patch new File(filename); //将每一个文件块的内容都追加到新文件中 try (FileChannel in new FileInputStream(patch).getChannel()) { in.transferTo(0, in.size(), out); } //追加完成后删除文件块 Files.delete(patch.toPath()); } out.close(); } return saveFilePath.getAbsolutePath(); }

相关新闻

Decnupaz(pivekimab sunirine-pvzy)中文说明书【海得康】

Decnupaz(pivekimab sunirine-pvzy)中文说明书【海得康】

药品基本信息通用名称‌:pivekimab sunirine-pvzy商品名称‌:Decnupaz药品类型‌:靶向CD123的抗体药物偶联物(ADC)适应症本品为处方药,适用于治疗成年母细胞性浆细胞样树突状细胞肿瘤(BPDCN&…

2026/10/16 18:42:18 阅读更多 →
终极Steam游戏数据提取指南:轻松获取DLC、成就和文件信息

终极Steam游戏数据提取指南:轻松获取DLC、成就和文件信息

终极Steam游戏数据提取指南:轻松获取DLC、成就和文件信息 【免费下载链接】GetDataFromSteam-SteamDB 项目地址: https://gitcode.com/gh_mirrors/ge/GetDataFromSteam-SteamDB 你是否曾经想要从Steam游戏中提取详细的DLC信息、成就数据或文件哈希值&#x…

2026/10/16 18:56:25 阅读更多 →
HarmonyOS应用实战-启示散页-57-题库颜色迁移别让旧数据失色:给 colorKey 做兼容映射

HarmonyOS应用实战-启示散页-57-题库颜色迁移别让旧数据失色:给 colorKey 做兼容映射

HarmonyOS 应用实战 57:题库颜色迁移别让旧数据失色,给 colorKey 做兼容映射 题库颜色看起来只是一个视觉字段,但它一旦写进 Preferences,就变成了需要迁移的持久数据。旧版本如果保存的是 #8AA7FF 或已经废弃的颜色名&#xff0c…

2026/10/16 22:56:45 阅读更多 →

最新新闻

SPC开发包源代码实战:控制图与过程能力计算落地指南

SPC开发包源代码实战:控制图与过程能力计算落地指南

简介:这份SPC开发包源代码面向工业自动化与现场总线方向的嵌入式开发者,聚焦Profibus-DP从站通信的底层实现,适合已具备C语言基础、希望深入理解协议栈细节的中高级工程师。压缩包共4个文件,包含3个c源文件与1个h头文件&#xff0…

2026/10/17 17:12:18 阅读更多 →
人工智能导论期末冲刺:知识表示、归结原理与可信度计算全攻略

人工智能导论期末冲刺:知识表示、归结原理与可信度计算全攻略

简介:这是一份山东大学《人工智能导论》课程概念汇总PDF,适合正在备考期末、考研复试或系统梳理AI基础概念的本科生与自学读者。内容以章节为主线,从智能、知识、智能特征出发,梳理人工智能定义与发展阶段;随后系统整理…

2026/10/17 17:12:18 阅读更多 →
Serv-U 14.0白金版实战:老文件服务器安全升级与多协议配置指南

Serv-U 14.0白金版实战:老文件服务器安全升级与多协议配置指南

简介:Serv-U 14.0 白金版是一款面向专业用户与企业级场景的 FTP 服务器软件,主打高效、安全的文件传输与稳定的服务运营。该资源包聚焦其界面与传输历史记录相关的前端素材,适合正在部署或维护 FTP 服务、需要了解历史记录模块实现方式的运维…

2026/10/17 17:12:17 阅读更多 →
claude-code-templates 不是 CLI:本地代码知识库的正确打开方式

claude-code-templates 不是 CLI:本地代码知识库的正确打开方式

1. 这不是又一个 CLI 工具:Claude-Code-Templates 的真实定位与误用陷阱“claude-code-templates”——光看这个名字,绝大多数人第一反应是:“哦,Anthropic 官方出的 Claude 代码生成 CLI?”或者“是不是类似 Copilot …

2026/10/17 17:12:17 阅读更多 →
光伏功率预测新框架:VMD-RIME-LSTM原理与Matlab实战

光伏功率预测新框架:VMD-RIME-LSTM原理与Matlab实战

现在到处都能看到光伏功率预测的论文,组合模型一个比一个花哨,但很多同学卡在同一个地方:方法选了一堆,真正能跑出干净结果、还能把论文故事讲完整的没几个。最近被问得最多的就是"变分模态分解霜冰算法优化LSTM"这条路…

2026/10/17 17:12:17 阅读更多 →
Unity虚拟装配中的机械运动仿真:从可视化到工程验证

Unity虚拟装配中的机械运动仿真:从可视化到工程验证

1. 这不是“转个齿轮”那么简单:虚拟装配仿真里藏着的机械逻辑陷阱很多人第一次在Unity里拖进一个齿轮模型,加个Rigidbody和HingeJoint,再写几行代码让电机轴转起来,看到齿轮咬合转动,就以为“机械运动仿真”这事搞定了…

2026/10/17 17:11:17 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →