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/7/30 16:55:37阅读更多 →
终极Steam游戏数据提取指南:轻松获取DLC、成就和文件信息

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

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

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

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

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

2026/7/30 16:55:37阅读更多 →
AI营收拐点识别:用时间序列分解+因果推断,在营收下滑前72小时精准干预

AI营收拐点识别:用时间序列分解+因果推断,在营收下滑前72小时精准干预

更多请点击: https://codechina.net 第一章:AI营收趋势分析 全球人工智能产业正经历从技术投入期向商业化兑现期的关键跃迁。据IDC最新预测,2024年全球AI软件市场规模达约638亿美元,年同比增长34.2%,其中企业级AI应用…

2026/7/30 18:14:14阅读更多 →
【设计模式】观察者模式

【设计模式】观察者模式

参考文章 一文讲清楚观察者模式 代码-图 #include <stdio.h>// 前置声明 typedef struct _subject_t subject_t; typedef struct _observer_t observer_t;void subject_attach(subject_t *sub, observer_t *obs); void subject_detach(subject_t *sub, observer_t *obs…

2026/7/30 18:14:14阅读更多 →
终极指南:快速掌握TileMapDual插件的7个高效技巧

终极指南:快速掌握TileMapDual插件的7个高效技巧

终极指南&#xff1a;快速掌握TileMapDual插件的7个高效技巧 【免费下载链接】TileMapDual An automatic, real-time dual-grid tileset system for Godot 项目地址: https://gitcode.com/gh_mirrors/ti/TileMapDual 在Godot引擎中创建复杂的瓦片地图&#xff0c;尤其是…

2026/7/30 18:14:14阅读更多 →
Power BI 主题模板:告别单调报表,3分钟打造专业数据可视化

Power BI 主题模板:告别单调报表,3分钟打造专业数据可视化

Power BI 主题模板&#xff1a;告别单调报表&#xff0c;3分钟打造专业数据可视化 【免费下载链接】PowerBI-ThemeTemplates Snippets for assembling Power BI Themes 项目地址: https://gitcode.com/gh_mirrors/po/PowerBI-ThemeTemplates 还在为PowerBI报表的千篇一律…

2026/7/30 18:14:14阅读更多 →
OpenWork高级技巧:自定义插件开发与工作区配置教程

OpenWork高级技巧:自定义插件开发与工作区配置教程

OpenWork高级技巧&#xff1a;自定义插件开发与工作区配置教程 【免费下载链接】openwork The open-source alternative to Claude Cowork (powered by opencode) 项目地址: https://gitcode.com/GitHub_Trending/ope/openwork OpenWork作为开源的Claude Cowork替代方案…

2026/7/30 18:14:14阅读更多 →
第六届全国高校教师教学创新大赛获奖:哈尔滨华德学院历史性突破

第六届全国高校教师教学创新大赛获奖:哈尔滨华德学院历史性突破

2026年7月23日&#xff0c;第六届全国高校教师教学创新大赛全国现场赛在南京圆满落幕。本届大赛由教育部高等教育司指导、中国高等教育学会主办&#xff0c;是全国高校教师教学竞赛中覆盖面最广、参与教师最多、影响力最大的赛事之一。大赛围绕“推动教学创新、培养一流人才”目…

2026/7/30 18:12:14阅读更多 →
覆盖国产 + 海外 + 开源模型,OpenClaw 2.7.9 Windows/Mac 双端部署详解

覆盖国产 + 海外 + 开源模型,OpenClaw 2.7.9 Windows/Mac 双端部署详解

&#x1f539; 工具基础介绍 OpenClaw 是开源生态中一款实用性较强的本地智能工具&#xff0c;凭借本地离线运行、可视化图形操作和任务自动化三大核心特性&#xff0c;赢得了众多用户的青睐。与普通在线对话AI工具不同&#xff0c;它属于能够直接操控本机软硬件的智能数字员工…

2026/7/30 15:03:16阅读更多 →
伺服阀焊完微漏毁整机?精密激光焊接三关锁住高压

伺服阀焊完微漏毁整机?精密激光焊接三关锁住高压

所谓液压伺服阀体的精密激光焊接&#xff0c;是用激光束对阀座壳体&#xff08;通常为不锈钢或铝合金&#xff09;进行密封焊接&#xff0c;使阀体在21-35MPa的高压液压油或压缩气体中长期运行而不发生介质泄漏。液压伺服阀是高端液压系统的"大脑"。从航空航天飞行控…

2026/7/30 12:22:27阅读更多 →
D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南

D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南

D2DX&#xff1a;三步实现《暗黑破坏神2》高清宽屏体验的终极指南 【免费下载链接】d2dx D2DX is a complete solution to make Diablo II run well on modern PCs, with high fps and better resolutions. 项目地址: https://gitcode.com/gh_mirrors/d2/d2dx 你是否还在…

2026/7/30 15:13:02阅读更多 →
3分钟解锁iOS应用自由:TrollInstallerX让你的iPhone摆脱安装限制 [特殊字符]

3分钟解锁iOS应用自由:TrollInstallerX让你的iPhone摆脱安装限制 [特殊字符]

3分钟解锁iOS应用自由&#xff1a;TrollInstallerX让你的iPhone摆脱安装限制 &#x1f680; 【免费下载链接】TrollInstallerX A TrollStore installer for iOS 14.0 - 16.6.1 项目地址: https://gitcode.com/gh_mirrors/tr/TrollInstallerX 你是否曾经因为iOS系统的严格…

2026/7/30 0:00:58阅读更多 →
[GESP202606 四级] 扫雷

[GESP202606 四级] 扫雷

B4557 [GESP202606 四级] 扫雷 https://www.luogu.com.cn/problem/B4557 中国计算机学会&#xff08;CCF&#xff09;2026年6月C四级讲解——扫雷 https://www.bilibili.com/video/BV1MCMg6AEXR/ B4557 [GESP202606 四级] 扫雷 https://www.bilibili.com/video/BV1ZKTj6ZEVh/ 2…

2026/7/30 0:00:58阅读更多 →
Windows驱动存储终极清理工具:DriverStoreExplorer完全指南

Windows驱动存储终极清理工具:DriverStoreExplorer完全指南

Windows驱动存储终极清理工具&#xff1a;DriverStoreExplorer完全指南 【免费下载链接】DriverStoreExplorer Driver Store Explorer 项目地址: https://gitcode.com/gh_mirrors/dr/DriverStoreExplorer 您是否曾因Windows系统盘空间不足而烦恼&#xff1f;是否遇到过设…

2026/7/30 0:00:58阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时&#xff0c;发现推理速度只有可怜的 1-2 FPS&#xff0c;而别人的演示视频却能跑到 30 FPS 以上&#xff0c;那么问题很可能不在模型本身&#xff0c;而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后&#xff0c;会直接使用官方示例…

2026/7/30 0:27:26阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一&#xff1a;为什么你需要了解 Coze 和 Dify&#xff1f;如果你对 AI 应用开发感兴趣&#xff0c;但一看到“大模型”、“智能体”、“工作流”这些词就头疼&#xff0c;觉得门槛太高&#xff0c;那这篇文章就是为你准备的。很多开发者&#xff0c;包括我自己&#…

2026/7/30 4:47:18阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会&#xff1a;配图一直是个让人头疼的问题。2026年&#xff0c;AI生图工具已经非常成熟了&#xff0c;但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1&#xff1a;速度之王2026年6月11日&#xff0c…

2026/7/30 15:43:46阅读更多 →