vue-simple-uploader+springboot大文件上传

    最近遇到一个vue大文件上传的问题,需要实现断点和分片上传。在网上查询许久,发现vue-simple-uploader这个插件可以满足新需求。由于本人项目用的是vue+springboot前后端分离技术,因此在此分前后端记录一下vue-simple-uploader如何使用。

SpringBoot后端

一、创建文件分片和文件实体类

Chunk.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* @author xiongzhigang
* @date 2020-05-19 10:07
* @description 文件块
*/
public class Chunk implements Serializable {
private Integer id;
/**
* 当前文件块,从1开始
*/
private Integer chunkNumber;
/**
* 分块大小
*/
private Integer chunkSize;
/**
* 当前分块大小
*/
private Integer currentChunkSize;
/**
* 总大小
*/
private Integer totalSize;
/**
* 文件标识
*/
private String identifier;
/**
* 文件名
*/
private String filename;
/**
* 相对路径
*/
private String relativePath;
/**
* 总块数
*/
private Integer totalChunks;
/**
* 文件类型
*/
private String type;
private MultipartFile file;

//setter、getter
}

FileInfo.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author xiongzhigang
* @date 2020-05-19 10:04
* @description 文件
*/
public class FileInfo implements Serializable {
private Integer id;
private String filename;
private String identifier;
private Integer totalSize;
private String type;
private String location;
private Integer status;
private Integer game_id;
}

二、编写业务逻辑
文件分片相关逻辑,主要由两部分:入库和分片检验,用于vue-simple-uploader每次上传分片时都会检验该分片是否上传,用到controller里的@PostMapping(“chunk”)和@GetMapping(“chunk”),这两个接口稍后再说。
ChunkServiceImpl.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* @author xiongzhigang
* @date 2020-05-19 10:12
* @description 文件块操作
*/
@Service
public class ChunkServiceImpl implements ChunkService {
@Resource(name = "kfUserDao")
private KFUserDao kfUserDao;

/**
* 保存分片
* @param chunk
*/
@Override
public void saveChunk(Chunk chunk) throws Exception{
kfUserDao.addOrUpdate(saveChunk, new Object[]{
chunk.getChunkNumber(), chunk.getChunkSize(), chunk.getCurrentChunkSize(),
chunk.getTotalSize(), chunk.getIdentifier(), chunk.getFilename(),
chunk.getRelativePath(), chunk.getTotalChunks(), chunk.getType()});
}

/**
* 检查分片
* @param identifier
* @param chunkNumber
* @return
*/
@Override
public boolean checkChunk(String identifier, Integer chunkNumber) throws Exception{
StringBuilder sql = new StringBuilder(checkChunkSql);
sql.append(" where identifier = ? and chunkNumber = ? and rownum = 1");

int res = kfUserDao.queryForInt(sql.toString(), new Object[]{identifier,chunkNumber});
return res == -1;
}
}

文件相关逻辑主要是入库和处理文件相关操作
FileInfoServiceImpl.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @author xiongzhigang
* @date 2020-05-19 11:31
* @description 文件操作
*/
@Service
public class FileInfoServiceImpl implements FileInfoService {
/**
* 添加文件信息
*
* @param fileInfo
* @return
*/
@Override
public int addFileInfo(FileInfo fileInfo) throws Exception {
int res = kfUserDao.addOrUpdate(addFileInfoSql, new Object[]{
fileInfo.getFilename(), fileInfo.getIdentifier(),
fileInfo.getTotalSize(), fileInfo.getType(), fileInfo.getLocation(),
fileInfo.getStatus(), fileInfo.getGame_id()});
return res;
}

// 其他逻辑...
}

三、控制器
这部分最为关键,主要分为@PostMapping(“chunk”)、@GetMapping(“chunk”)和@PostMapping(“merge”),第一个Post请求的chunk是分片写入服务器和分片信息入库的,第二个Get请求的chunk是检验分片是否上传过(可以用于断点或跨浏览器上传),第三个merge负责在文件分片完成后进行合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**
* @author xiongzhigang
* @date 2020-05-19 11:34
* @description 文件操作
*/
@RestController
@RequestMapping("/uploader")
public class UploadController {
@Value("${upload.folder}")
private String uploadFolder;
@Resource
private FileInfoService fileInfoService;
@Resource
private ChunkService chunkService;

/**
* 分片上传
*
* @param chunk
* @return
* @throws Exception
*/
@PostMapping("chunk")
public String uploadChunk(Chunk chunk) throws Exception {
MultipartFile file = chunk.getFile();
LogInfo.WEB_LOG.info("file originName: {}, chunkNumber: {}", file.getOriginalFilename(), chunk.getChunkNumber());

byte[] bytes = file.getBytes();
Path path = Paths.get(generatePath(uploadFolder, chunk));
//文件写入指定路径
Files.write(path, bytes);
LogInfo.WEB_LOG.info("文件 {} 写入成功, uuid:{}", chunk.getFilename(), chunk.getIdentifier());
chunkService.saveChunk(chunk);

return "文件上传成功";
}

/**
* 断点和快传
*
* @param chunk
* @param response
* @return
*/
@GetMapping("chunk")
public Object checkChunk(Chunk chunk, HttpServletResponse response) throws Exception {
if (chunkService.checkChunk(chunk.getIdentifier(), chunk.getChunkNumber())) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}

return chunk;
}

/**
* 合并分片
*
* @param fileInfo
* @return
*/
@PostMapping("/mergeFile")
public String mergeFile( @RequestBody FileInfo fileInfo) throws Exception {
String filename = fileInfo.getFilename();
String file = uploadFolder + "/" + fileInfo.getIdentifier() + "/" + filename;
String folder = uploadFolder + "/" + fileInfo.getIdentifier();
merge(file, folder, filename);

fileInfo.setLocation(file);
fileInfo.setStatus(0);
fileInfoService.addFileInfo(fileInfo);
// 异步读取excel且批量insert overwrite impala
fileInfoService.addGameItemBatch(fileInfo);

return "合并成功";
}

/**
* 检查上传是否完毕
* @param game
* @param request
* @return
* @throws Exception
*/
@PostMapping("checkUploadComplete")
public Boolean checkUploadComplete() throws Exception {
...
}
}

Vue前端

    首先安装vue-simple-uploader,安装命令如下:

1
cnpm install --save vue-simple-uploader

新建一个组件便于引用,里面主要包括:uploader、uploader-btn、uploader-list和uploader-file,其中详细的解释见:simple-uploader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<template>

<!-- 上传 -->
<uploader
ref="uploader"
:options="options"
:file-status-text="statusText"
@file-added="onFileAdded"
@file-success="onFileSuccess"
@file-complete="fileComplete"
@file-error="onFileError"
@file-removed="fileRemoved"
class="uploader-app">
<uploader-unsupport></uploader-unsupport>

<el-tooltip class="item" effect="dark" :content="uploadTip" placement="right">
<uploader-btn class="global-uploader-btn" :attrs="attrs" ref="uploadBtn">选择文件</uploader-btn>
</el-tooltip>

<uploader-list v-show="panelShow" class="global-uploader-list">
<upload-file></upload-file>
</uploader-list>

</uploader>

</template>

<script>

export default {
name: "upload",
data() {
return {
options: {
target: serverURL + '/uploader/chunk',
chunkSize: 10 * 1024 * 1024,
maxChunkRetries: 3,
testChunks: true,
headers: {},
query() {
},
single: true
},
attrs: {
accept: ['.xlsx', '.xls']
},
statusText: {
success: '成功了',
error: '出错了',
uploading: '上传中',
paused: '暂停中',
waiting: '等待中'
}
}
},
computed: {
//Uploader实例
uploader() {
return this.$refs.uploader.uploader;
}
},
methods: {
onFileAdded(file) {
// 选择文件后触发,可以检验文件
},
onFileSuccess(rootFile, file, response, chunk) {
let res = JSON.parse(response);

// 服务器自定义的错误(即虽返回200,但是是错误的情况),这种错误是Uploader无法拦截的
},
onFileError(rootFile, file, response, chunk) {
// 上传失败
},
fileComplete() {
// 上传完成
},
fileRemoved(file) {
// 移除上传文件
}
}
}
</script>

×

纯属好玩

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. SpringBoot后端
  2. 2. Vue前端
,