在Ruoyi框架中,虽然也提供了基于fileinput的文件上传示例,加入企业在真实业务中有大文件的上传,比如上GB的文件,那使用fileinput的用户体验不怎么友好,因而在大容量文件上传处理时,就有必要进行切片,断点续传,重复文件判断等。因此本文将使用百度开源的WebUploader上传组件,对文件上传业务提供统一的封装和扩展,可以满足所有业务场景的覆盖。
本文将重点说明ruoyi使用的基础技术,简单介绍webuploader,webuploader如何在Ruoyi中进行集成。Ruoyi的示例例子采用的是Ruoyi的单体集成框架,不是前后端分离版,不过技术的思路是类似的,可以作为参考。
ruoyi的前端是依赖于fileinput来实现的,其官方的文档手册地址可以参见:bootstrap-fileinput
实现的效果大致是这样的:
Ruoyi使用了最简单的文件接收方式,没有文件切片,这样设计的目的,个人猜测是因为不考虑大文件的这种场景,当然在互联网里,确实遇到大文件的情况也不多,使用这样的方案也可以应对。Ruoyi的后台处理类代码如下:
package com.hngtghy.project.common;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
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.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import com.hngtghy.common.constant.Constants;
import com.hngtghy.common.utils.StringUtils;
import com.hngtghy.common.utils.file.FileUploadUtils;
import com.hngtghy.common.utils.file.FileUtils;
import com.hngtghy.framework.config.HngtghyConfig;
import com.hngtghy.framework.config.ServerConfig;
import com.hngtghy.framework.web.domain.AjaxResult;
/**
* 通用请求处理
*
* @author wuzuhu
*/
@Controller
@RequestMapping("/common")
public class CommonController
{
private static final Logger log = LoggerFactory.getLogger(CommonController.class);
@Autowired
private ServerConfig serverConfig;
private static final String FILE_DELIMETER = ",";
/**
* 通用上传请求(单个)
*/
@PostMapping("/upload")
@ResponseBody
public AjaxResult uploadFile(MultipartFile file) throws Exception
{
try
{
// 上传文件路径
String filePath = HngtghyConfig.getUploadPath();
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
String url = serverConfig.getUrl() + fileName;
AjaxResult ajax = AjaxResult.success();
ajax.put("url", url);
ajax.put("fileName", fileName);
ajax.put("newFileName", FileUtils.getName(fileName));
ajax.put("originalFilename", file.getOriginalFilename());
return ajax;
}
catch (Exception e)
{
return AjaxResult.error(e.getMessage());
}
}
/**
* 通用上传请求(多个)
*/
@PostMapping("/uploads")
@ResponseBody
public AjaxResult uploadFiles(List files) throws Exception
{
try
{
// 上传文件路径
String filePath = HngtghyConfig.getUploadPath();
List urls = new ArrayList();
List fileNames = new ArrayList();
List newFileNames = new ArrayList();
List originalFilenames = new ArrayList();
for (MultipartFile file : files)
{
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
String url = serverConfig.getUrl() + fileName;
urls.add(url);
fileNames.add(fileName);
newFileNames.add(FileUtils.getName(fileName));
originalFilenames.add(file.getOriginalFilename());
}
AjaxResult ajax = AjaxResult.success();
ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));
ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER));
ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER));
return ajax;
}
catch (Exception e)
{
return AjaxResult.error(e.getMessage());
}
}
}
正是由于Ruoyi天生的大文件处理能力比较差的,经过对开源组件的比较,我们选定了百度开源的百度Webuploader,它具有以下的能力:
在官网上下载最新的webuploader资源包后,将相应的资源文件拷贝到Ruoyi的工程目录中,
在这里,我们设计了统一的文件存储服务,因此,将文件的查询、上传、编辑、删除功能都封装在一个界面中,对外提供单个文件上传功能,也提供批量管理功能。所以,有必要对文件进行统一封装。下面是基于Thymeleaf的一个简单封装:
这里我们放在百度网盘的样子对WebUpload的样式进行改造,同时需要将文件上传的列表展示出来,同时可以对文件进行上传、暂停、删除等操作,因此需要对webuploader进行定制化开发。相关代码如下:
function initUploader(){
bindFileListeners();
var fileNumLimit = [[${fileNumLimit}]];//文件数量限制
var acceptType = [[${acceptType}]];//支持文件类型
var auto = [[${autoUpload}]];//是否自动上传0否1是
var multipleMode = [[${multipleMode}]];//多选模式 add by wuzuhu on 2022-07-18
uploader = WebUploader.create({
auto: auto==0 ? false : true,
swf: [[@{/uploader/Uploader.swf}]],
server: [[@{/uploadfile/bigUploader}]],
pick: {id:'#filePicker_'+[[${temp_b_id}]],multiple: multipleMode == "single" ? false : true},
dnd: '#filePicker_'+[[${temp_b_id}]],
method:'POST',
resize: false ,
chunked : true,
chunkRetry:false,
formData : {
fid : '',
name : '',
size : 0,
md5code : '',
tablename : tablename,
temp_b_id : temp_b_id,
bizType : [[${bizType}]],
bid : b_id
},
compress : false,
duplicate:true,
prepareNextFile: true,
disableGlobalDnd:true,
});
uploader.on('beforeFileQueued', function(file) {
if(file.size == 0){
var error = "文件不能为空!";
parent.layer.msg(error);
return false;
}
if (fileNumLimit != null && fileNumLimit <= fileArray.length) {
message.info("文件数量不能超过" + fileNumLimit);
return false;
}
var file_name = file.name;
var file_type = file_name.substring(file_name.lastIndexOf(".") + 1);
if (acceptType != null && acceptType.length !== 0) {
if (acceptType.indexOf(file_type) == -1) {
message.info("文件类型只能是" + acceptType.toString());
return false;
}
}
return true;
});
uploader.on('fileQueued', function(file) {
/* for(i in fileArray){
var obj = fileArray[i];
if(obj.name == file.name){
if(obj.status == '上传失败'){
modal.removeFile(obj.f_id);
}
}
}
var uuid = WebUploader.Base.guid('');
var file_upload = new FileObj(file.id,uuid,file.name,file.size,'','等待上传','','');
fileArray.push(file_upload);
openProcessModalFile(file); */
});
uploader.on('filesQueued', function(files) {
for(j in files){
var file = files[j];
for(i in fileArray){
var obj = fileArray[i];
if(obj.name == file.name){
if(obj.status == '上传失败' && modal != null){
modal.removeFile(obj.f_id);
}
}
}
var uuid = WebUploader.Base.guid('');
var file_upload = new FileObj(file.id,uuid,file.name,file.size,'','等待上传','','');
fileArray.push(file_upload);
}
openProcessModalFiles(files);
});
uploader.on( 'uploadProgress', function( file, percentage ) {
var obj = getFileObjById(file.id);
if(obj.status == '暂停'){
return;
}
if (obj.id === file.id) {
if (percentage === 1) {
if (obj.status === '99.99%') {
return;
}
if (file.size > block_size) {
obj.status = '99.99%';
if(modal){
modal.updateStatus(obj.f_id,'99.99%')
}
}
} else {
percentage = (percentage * 100).toFixed(2);
if (percentage + "%" === obj.status) {
return;
}
obj.status = percentage + "%";
if(modal){
modal.updateStatus(obj.f_id,percentage + "%")
}
}
}
});
uploader.on( 'uploadBeforeSend', function( block,data,headers ) {
var obj = getFileObjById(block.file.id);
data.md5code = obj.md5code;
data.fid = obj.f_id;
data.name = obj.f_name;
data.size = obj.f_size;
data.chunk = block.chunk;
data.chunkSize = block.end-block.start;
});
}
function addFiles(files){
for(i in files){
var file = files[i];
addFile(file);
}
}
由于篇幅有限,这里不把所有的代码都列出来,仅将部分代码列出来。
这里讲解了Webuploader与Ruoyi的简单集成,这是第一个部分,如果需要详细了解的,可以深入交流,这里有涉及数据分片的具体实现,还有后端的服务端支持等等,关于后端的设计和业务表的设计,打算在后续再进行说明。
Webuploader与Ruoyi的集成效果图如下图所示:
通过观察网络请求可以看到,前端往服务端提交数据时,数据是已经进行了分片: