摘要:
断点续传 就是下载文件时,不必重头开始下载,而是从指定的位置继续下载,这样的功能就叫做断点续传。 断点续传的理解可以分为两部分:一部分是断点,一部分是续传。断点的由来是... 断点续传 就是下载文件时,不必重头开始下载,而是从指定的位置继续下载,这样的功能就叫做断点续传。 断点续传的理解可以分为两部分:一部分是断点,一部分是续传。断点的由来是在下载过程中,将一个下载文件分成了多个部分,同时进行多个部分一起的下载,当某个时间点,任务被暂停了,此时下载暂停的位置就是断点了。续传就是当一个未完成的下载任务再次开始时,会从上次的断点继续传送。
以前文件无法分割,但随着html5新特性的引入,类似普通字符串、数组的分割,我们可以可以使用slice方法来分割文件。所以断点续传的最基本实现也就是:前端通过FileList对象获取到相应的文件,按照指定的分割方式将大文件分段,然后一段一段地传给后端,后端再按顺序一段段将文件进行拼接。
而我们需要对FileList对象进行修改再提交,在之前的文章中知晓了这种提交的一些注意点,因为FileList对象不能直接更改,所以不能直接通过表单的.submit()方法上传提交,需要结合FormData对象生成一个新的数据,通过Ajax进行上传操作。
设计思路
1. 分片上传(Chunk Upload)
核心思想:将一个大文件(比如1GB)切割成多个固定大小的小分片(比如10MB/片),分别上传,服务端接收所有分片后再合并成完整文件。
解决问题:避免单次上传大文件时网络中断导致全部重传、请求超时、服务器内存溢出等问题。
2. 断点 续传(Resumable Upload)
核心思想:上传前先校验文件的唯一标识(如MD5/文件大小+修改时间),服务端查询该文件已上传的分片,只上传未完成的分片。
关键依赖:需要一个“分片记录器”(数据库/缓存),记录每个文件的分片上传状态(已上传分片索引、总分片数、文件唯一标识等)。
3. 多文件上传
本质是对单文件上传逻辑的复用,前端传递文件列表,后端循环处理每个文件的分片/断点续传逻辑即可。
整体实现流程
整个流程分为前端预处理和后端核心逻辑两部分,以下是标准化的实现步骤:
步骤1:文件唯一标识生成(前端+后端)
为了区分不同文件、校验分片归属,首先生成文件的唯一标识(推荐MD5):
前端:读取文件内容,计算文件的MD5(可使用SparkMD5等库),同时指定分片大小(如10MB),计算总分片数。
后端:接收MD5后,可作为文件的唯一ID,用于查询分片上传状态。
步骤2:断点续传校验(前端请求,后端响应)
前端上传前先请求后端:“文件MD5为某某的文件,哪些分片已经上传完成”
后端:查询分片记录(数据库/缓存),返回已上传的分片索引列表。
前端:根据返回结果,只上传未完成的分片(实现断点续传)。
步骤3:分片上传(核心)
前端将文件切割成分片,逐个/并发上传分片,每个分片携带:
1.文件MD5(文件唯一标识)
2.分片索引(第几个分片,从0开始)
3.总分片数
4.分片内容
后端接收分片后,保存到临时目录,并更新分片上传状态。
步骤4:合并分片(所有分片上传完成后)
前端确认所有分片上传完成后,请求后端合并分片:
后端:按分片索引顺序读取所有临时分片文件,写入最终文件路径,删除临时分片,更新文件上传状态为“完成”。
基于这个思路,以下博主分享代码
html部分
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="http://localhost/jquery-4.0.0.min.js"></script>
<!-- 使用SparkMD5库来获取文件的MD5哈希值 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
</head>
<body>
<h1>html5大文件断点切割上传</h1>
<input type="file" id="fileInput" />
<button onclick="calculateMD5()">上传文件</button>
<div id="progress">0%</div>
<script>
function calculateMD5() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert('请选择一个文件');
return;
}
const reader = new FileReader();
const spark = new SparkMD5.ArrayBuffer();
let fileLoaded = false;
reader.onload = function(e) {
// 文件加载成功
fileLoaded = true;
spark.append(e.target.result); // 读取文件完成之后放进内存缓冲区
const hash = spark.end(); // 获取hash文件哈希值
console.log(hash,"曲中")
uploadFile(file, hash);
}
reader.onerror = function() {
console.log('Spark读取文件中出现意外错误');
};
reader.readAsArrayBuffer(file);
}
function uploadFile(file, fileMD5) {
const CHUNK_SIZE = 1024 * 1024 * 5; // 5MB 每个分片大小
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
checkFile(fileMD5, totalChunks);
/**
* offset 在那个分片索引标识开始上传
* fileMD5 文件唯一标识
* currentChunk 当前已经上传的分片数量
* uploadedBytes 当前已完成上传的文件大小
* */
function uploadChunk(fileMD5, currentChunk) {
offset = currentChunk * CHUNK_SIZE;
let uploadedBytes = offset;
const blob = file.slice(offset, offset + CHUNK_SIZE);
const formData = new FormData();
formData.append('chunkFile', blob);
formData.append('chunkIndex', currentChunk);
formData.append('totalChunks', totalChunks);
formData.append('fileName', file.name);
formData.append('fileSize', file.size);
formData.append('chunkSize', CHUNK_SIZE);
formData.append('fileMd5', fileMD5);
$.ajax({
url: 'http://localhost:60000/upload/chunk',
type: 'POST',
data: formData,
processData: false,
contentType: false,
xhr: function() { // Custom XMLHttpRequest to handle progress updates
var xhr = new XMLHttpRequest(); // XMLHttpRequest object to monitor progress events
xhr.upload.addEventListener("progress", function(evt) {
if (evt.lengthComputable) {
var percentComplete = Math.round((evt.loaded / evt.total) * 100);
uploadedBytes += evt.loaded; // Update uploaded bytes counter
$('#progress').text(percentComplete + '%'); // Update progress bar or text display on client side if needed
}
}, false);
return xhr; // Return the XHR object for jQuery to use it for the request
},
success: function(data) { // On successful upload of this chunk, upload the next one (recursive call)
if (data.code == 200) {
currentChunk++;
if (currentChunk < totalChunks) {
uploadChunk(fileMD5, currentChunk); // 继续上传下一个分片
} else {
// 所有分片上传完毕,发起文件合并请求
mergeFile(fileMD5);
}
} else {
alert(data.msg);
}
}
});
}
// 在执行上传该文件时,向后端提交检查该文件有没有中断上传的记录,如果有,则从上传失败的分片索引处开始继续上传
function checkFile(fileMD5, chunksTotalNum){
const formData = new FormData();
formData.append('fileMd5', fileMD5);
$.ajax({
url: 'http://localhost:60000/upload/check',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(data) {
if (data.code == 200) {
let arr = data.data;
if(arr.finishChunk.length == 0) uploadChunk(fileMD5, 0);
else if(chunksTotalNum <= arr.finishChunk.length){
if(arr.isMerge == 0) mergeFile(fileMD5);
else return alert('文件早已上传')
}
else{
// 继续上传未尽的分片
uploadChunk(fileMD5, arr.finishChunk[arr.finishChunk.length - 1]); // 继续上传下一个分片
}
} else {
alert(data.msg);
}
}
});
}
// 在分片上传全部完成后,向后端提交合并请求
function mergeFile(fileMD5){
const formData = new FormData();
formData.append('fileMd5', fileMD5);
$.ajax({
url: 'http://localhost:60000/upload/merge',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(data) { // On successful upload of this chunk, upload the next one (recursive call)
if (data.code == 200) {
alert('文件上传完成');
} else {
alert('文件上传失败');
}
}
});
}
}
</script>
</body>
</html>后端部分(采用了springBoot+Mysql)
数据表
CREATE TABLE `file_upload_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `file_md5` varchar(64) NOT NULL COMMENT '文件唯一标识(MD5)', `file_name` varchar(255) NOT NULL COMMENT '文件名', `file_size` bigint(20) NOT NULL COMMENT '文件总大小(字节)', `chunk_size` bigint(20) NOT NULL COMMENT '分片大小(字节)', `total_chunks` int(11) NOT NULL COMMENT '总分片数', `uploaded_chunks` varchar(1000) DEFAULT '' COMMENT '已上传分片索引(逗号分隔,如0,1,2)', `file_path` varchar(500) DEFAULT NULL COMMENT '最终文件保存路径', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0-未完成 1-已完成', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_file_md5` (`file_md5`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='文件上传记录表';
// maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- mysql驱动包 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mybatis的相关依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<!-- 日志输出依赖包 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<!-- myBatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- 文件操作工具 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.0</version>
</dependency>
<!-- 校验MD5 -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>// 实体类
package com.durkblue.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Date;
@TableName("file_upload_record")
public class FileUploadRecord {
@TableId(type = IdType.AUTO)
private Long id;
// 文件MD5
@TableField("file_md5")
private String fileMd5;
// 文件名
@TableField("file_name")
private String fileName;
// 文件总大小(字节)
@TableField("file_size")
private Long fileSize;
// 分片大小(字节)
@TableField("chunk_size")
private Long chunkSize;
// 总分片数
@TableField("total_chunks")
private Integer totalChunks;
// 已上传分片索引(逗号分隔)
@TableField("uploaded_chunks")
private String uploadedChunks;
// 最终文件路径
@TableField("file_path")
private String filePath;
// 上传状态:0-未完成 1-已完成
@TableField("status")
private Integer status;
@TableField("create_time")
// 记录时间
private Date createTime;
@TableField("update_time")
// 上传时间
private Date updateTime;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFileMd5() {
return fileMd5;
}
public void setFileMd5(String fileMd5) {
this.fileMd5 = fileMd5;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public Long getFileSize() {
return fileSize;
}
public void setFileSize(Long fileSize) {
this.fileSize = fileSize;
}
public Long getChunkSize() {
return chunkSize;
}
public void setChunkSize(Long chunkSize) {
this.chunkSize = chunkSize;
}
public Integer getTotalChunks() {
return totalChunks;
}
public void setTotalChunks(Integer totalChunks) {
this.totalChunks = totalChunks;
}
public String getUploadedChunks() {
return uploadedChunks;
}
public void setUploadedChunks(String uploadedChunks) {
this.uploadedChunks = uploadedChunks;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
}
// mapper层
package com.durkblue.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.durkblue.domain.FileUploadRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface FileUploadRecordMapper extends BaseMapper<FileUploadRecord> {
}
// service层接口
package com.durkblue.service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
public interface IFileUploadService {
/**
* 断点续传校验,返回已上传的分片索引
*
* @param fileMd5 文件分片标识
* @return 分片索引
*/
Map<String, Object> checkUploadedChunks(String fileMd5);
/**
* 上传总文件当中单个分片
* @param fileMd5 总文件唯一标识
* @param fileName 总文件名称
* @param fileSize 总文件大小
* @param chunkSize 分片文件大小
* @param chunkIndex 分片索引
* @param totalChunks 总分片数
* @param chunkFile 分片文件
* @return 是否上传该分片成功
* @throws IOException 异常
*/
boolean uploadChunk(String fileMd5, String fileName, Long fileSize, Long chunkSize,
Integer chunkIndex, Integer totalChunks, MultipartFile chunkFile) throws IOException;
/**
* 合并所有文件分片
* @param fileMd5 总文件唯一标识
* @return 是否合并成功
* @throws IOException 异常
*/
boolean mergeChunks(String fileMd5) throws IOException;
}// service层实现类
package com.durkblue.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.durkblue.domain.FileUploadRecord;
import com.durkblue.mapper.FileUploadRecordMapper;
import com.durkblue.service.IFileUploadService;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.FileUtils;
import org.apache.ibatis.util.MapUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class FileUploadServiceImpl implements IFileUploadService {
// 当分片上传全部完成后最终文件存储目录
private final String finalPath = "D:\\file\\upload\\";
// 临时文件分片存储目录
private String tempPath = "D:\\file\\tmp\\";
@Autowired
private FileUploadRecordMapper fileUploadRecordMapper;
@Override
public Map<String, Object> checkUploadedChunks(String fileMd5) {
Map<String, Object> dataMap = new HashMap<>();
LambdaQueryWrapper<FileUploadRecord> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FileUploadRecord::getFileMd5, fileMd5);
FileUploadRecord record = fileUploadRecordMapper.selectOne(queryWrapper);
// 无记录:返回空集合(所有分片都未上传)
if (Objects.isNull(record)) {
dataMap.put("finishChunk", new HashSet<>());// 当前已经上传的分片索引
dataMap.put("isMerge", 0);// 当前文件是否需要文件合并操作0=否1=是
}else{
// 已完成:返回所有分片索引(前端无需再上传)
if (record.getStatus() == 1) {
Set<Integer> allChunks = new HashSet<>();
for (int i = 0; i < record.getTotalChunks(); i++) {
allChunks.add(i);
}
dataMap.put("finishChunk", allChunks);
dataMap.put("isMerge", record.getStatus());
}
else{
// 未完成:解析已上传分片索引
String uploadedChunks = record.getUploadedChunks();
if (uploadedChunks == null || uploadedChunks.isEmpty()) {
dataMap.put("finishChunk", new HashSet<>());
dataMap.put("isMerge", record.getStatus());
}else{
Set<Integer> chunkSet = Arrays.stream(uploadedChunks.split(","))
.map(Integer::parseInt)
.collect(Collectors.toSet());
dataMap.put("finishChunk", chunkSet);
dataMap.put("isMerge", record.getStatus());
}
}
}
return dataMap;
}
@Override
public boolean uploadChunk(String fileMd5, String fileName, Long fileSize, Long chunkSize, Integer chunkIndex, Integer totalChunks, MultipartFile chunkFile) throws IOException {
// 1. 校验分片文件
if (chunkFile.isEmpty()) {
throw new RuntimeException("分片文件为空");
}
// 2. 创建临时目录(按文件MD5分目录,避免分片冲突)
File tempDir = new File(tempPath + File.separator + fileMd5);
if (!tempDir.exists()) {
FileUtils.forceMkdir(tempDir);
}
// 3. 保存分片文件(命名规则:fileMd5 + "_" + chunkIndex)
File chunkFileObj = new File(tempDir, fileMd5 + "_" + chunkIndex);
FileUtils.copyInputStreamToFile(chunkFile.getInputStream(), chunkFileObj);
// 4. 更新分片上传记录
LambdaQueryWrapper<FileUploadRecord> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FileUploadRecord::getFileMd5, fileMd5);
FileUploadRecord record = fileUploadRecordMapper.selectOne(queryWrapper);
if (Objects.isNull(record)) {
// 首次上传:新增记录
record = new FileUploadRecord();
record.setFileMd5(fileMd5);
record.setFileName(fileName);
record.setFileSize(fileSize);
record.setChunkSize(chunkSize);
record.setTotalChunks(totalChunks);
record.setUploadedChunks(chunkIndex.toString());
record.setStatus(0);
fileUploadRecordMapper.insert(record);
} else {
// 非首次上传:追加分片索引
Map<String, Object> uploadMap = checkUploadedChunks(fileMd5);
Set<Integer> uploadedSet = (Set<Integer>) MapUtils.getObject(uploadMap, "finishChunk");
uploadedSet.add(chunkIndex);
String newUploadedChunks = uploadedSet.stream()
.sorted()
.map(String::valueOf)
.collect(Collectors.joining(","));
record.setUploadedChunks(newUploadedChunks);
fileUploadRecordMapper.updateById(record);
// 判断是否最后一个分片上传
if(uploadedSet.size() == record.getTotalChunks()){
// 最后一个分片上传,执行文件合并
mergeChunks(fileMd5);
}
}
return true;
}
@Override
public boolean mergeChunks(String fileMd5) throws IOException {
// 1. 查询文件记录
LambdaQueryWrapper<FileUploadRecord> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FileUploadRecord::getFileMd5, fileMd5);
FileUploadRecord record = fileUploadRecordMapper.selectOne(queryWrapper);
if (record == null || record.getStatus() == 1) {
return true; // 无记录或已合并,直接返回成功
}
// 2. 校验所有分片是否上传完成
Map<String, Object> uploadMap = checkUploadedChunks(fileMd5);
Set<Integer> uploadedChunks = (Set<Integer>) MapUtils.getObject(uploadMap, "finishChunk");
if (uploadedChunks.size() != record.getTotalChunks()) {
throw new RuntimeException("分片未全部上传,无法合并");
}
// 3. 创建最终文件目录
File finalDir = new File(finalPath);
if (!finalDir.exists()) {
FileUtils.forceMkdir(finalDir);
}
// 4. 合并分片到最终文件
File finalFile = new File(finalDir, record.getFileName());
try (FileOutputStream fos = new FileOutputStream(finalFile, true);
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
// 按分片索引顺序读取并写入
for (int i = 0; i < record.getTotalChunks(); i++) {
File chunkFile = new File(tempPath + File.separator + fileMd5, fileMd5 + "_" + i);
if (!chunkFile.exists()) {
throw new RuntimeException("分片" + i + "不存在");
}
// 写入分片内容
FileUtils.copyFile(chunkFile, bos);
// 删除临时分片文件
FileUtils.delete(chunkFile);
}
}
// 5. 更新记录:标记为已完成,保存最终文件路径
record.setFilePath(finalFile.getAbsolutePath());
record.setStatus(1);
fileUploadRecordMapper.updateById(record);
// 6. 删除临时目录
File tempDir = new File(tempPath + File.separator + fileMd5);
FileUtils.deleteDirectory(tempDir);
return true;
}
}
// 工具类
com.durkblue.utiljava.util.HashMapjava.util.ObjectsAjaxResult HashMap<StringObject> {
= String = String = String = String = ()
{
}
(codeString msg)
{
.put(code).put(msg)}
(Object dataInteger total){
.put(data).put(total).put().put()}
(codeString msgObject data)
{
.put(code).put(msg)(!Objects.(data))
{
.put(data)}
}
AjaxResult ()
{
AjaxResult.()}
AjaxResult (Object data)
{
AjaxResult.(data)}
AjaxResult (String msg)
{
AjaxResult.(msg)}
AjaxResult (String msgObject data)
{
AjaxResult(msgdata)}
AjaxResult ()
{
AjaxResult.()}
AjaxResult (String msg)
{
AjaxResult.(msg)}
AjaxResult (String msgObject data)
{
AjaxResult(msgdata)}
AjaxResult (codeString msg)
{
AjaxResult(codemsg)}
}// controller层
package com.durkblue.controller;
import com.durkblue.service.IFileUploadService;
import com.durkblue.util.AjaxResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
@RestController
@RequestMapping("/upload")
public class FileUploadController {
@Autowired
private IFileUploadService fileUploadService;
/**
* 断点续传校验接口
*/
@PostMapping("/check")
public AjaxResult checkUpload(String fileMd5) {
Map<String, Object> uploadedChunks = fileUploadService.checkUploadedChunks(fileMd5);
return AjaxResult.success(uploadedChunks);
}
/**
* 分片上传接口(单文件分片)
*/
@PostMapping("/chunk")
public AjaxResult uploadChunk(String fileMd5, String fileName, Long fileSize, Long chunkSize, Integer chunkIndex,
Integer totalChunks, MultipartFile chunkFile) {
try {
fileUploadService.uploadChunk(fileMd5, fileName, fileSize,
chunkSize, chunkIndex, totalChunks, chunkFile);
return AjaxResult.success("分片上传成功");
} catch (IOException e) {
return AjaxResult.error("分片上传失败:" + e.getMessage());
}
}
/**
* 分片合并接口
*/
@PostMapping("/merge")
public AjaxResult mergeChunks(String fileMd5) {
try {
fileUploadService.mergeChunks(fileMd5);
return AjaxResult.success("文件合并成功");
} catch (IOException e) {
return AjaxResult.error("文件合并失败:" + e.getMessage());
}
}
}
// 解决跨域问题
package com.durkblue.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/upload/**") // 调整为你的映射路径
.allowedOrigins("http://localhost") // 替换为你的前端应用的 URL
.allowedMethods("POST", "GET", "PUT", "DELETE") // 根据需要添加更多方法
.allowCredentials(true)
.maxAge(3600);
}
}
// springboot配置文件
: : : file-resume-from-breakpoint : : jdbc:mysql://localhost:3306/ruoyi-wvp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai : ....: root : root : : : : : : : : : : 50MB : 100MB : : classpath:/mapping/*Mapper.xml : com.durkblue.domain : : : :



