在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的一个简单封装:
- DOCTYPE html>
- <html lang="zh" xmlns:th="http://www.thymeleaf.org">
- <head th:include="include :: webupload">
- <body class="no-skin">
- <div class="main-container ace-save-state" id="main-container">
- <script type="text/javascript">
- try{ace.settings.loadState('main-container')}catch(e){}
- script>
- <div class="main-content">
- <div class="main-content-inner">
- <div class="page-content" style="padding: 8px 10px 0px;">
- <div class="widget-box">
- <div>
- <form class="form-search">
- <div class="row">
- <div class="col-xs-12 col-sm-12">
- <table class="table" style="margin-bottom: 0px;">
- <tbody>
- <tr>
- <td style="border: 0px;vertical-align: middle;width:10%;"><p class="text-right">文件名称p>td>
- <td style="border: 0px;vertical-align: middle;width:50%;">
- <input type="text" name="name" class="form-control" placeholder="请输入文件名称"/>
- td>
- <td style="border: 0px;vertical-align: middle;width:30%;" >
- <button type="button" class="btn btn-primary btn-xs" id="btn-search">
- <span class="fa fa-search ">span>
- 查询
- button>
- <a href="#" class="btn btn-success btn-xs filepicker_btn" th:id="'filePicker_'+${temp_b_id}">
- <i class="ace-icon fa fa-upload">i>
- 选择
- a>
- <div th:include="include-upload-js :: header">div>
- td>
- tr>
- tbody>
- table>
- div>
- div>
- form>
- div>
- div>
-
- <div class="table-responsive">
- <table id="dataTable" lay-filter="dataTable" cellspacing="0" >
- table>
- div>
- div>
- div>
- div>
- div>
- <script th:inline="javascript">
- var prefix = [[@{/uploadfile}]];
-
- var fileUploadIndex = 0;
- $(document).ready(function() {
- $("#btn-search").on("click",doSearch);
- initTable();
- });
- function doSearch(){
- table.reload('dataTable',{
- where : {
- name :$("input[name='name']").val(),
- }
- });
- }
- var uploadSuccessCallback = function(file,fileArray){
- var allFinished = true;
- for(i in fileArray){
- var obj = fileArray[i];
- if(obj.status != '上传失败' && obj.status != '上传成功'){
- allFinished = false;
- break;
-
- }
- }
- if(allFinished){
- doSearch();
- parent.layer.close(fileUploadIndex);
- modal = null;
- }
-
- }
- function initTable(){
- var bid = [[${bid}]];
- var temp_b_id = [[${temp_b_id}]];
- var b_ids = bid == null ? temp_b_id : bid;
- var tablename = [[${tablename}]];
- var bizType = [[${bizType}]];
- var multipleMode = [[${multipleMode}]];
- layui.use('table', function(){
- table = layui.table;
- table.render({
- elem: '#dataTable',
- height: "full",
- url: prefix + "/list?b_id="+b_ids,
- method : "post",
- page: true,
- //toolbar:"#toolbar",
- defaultToolbar:[],
- where:{orderByColumn:'createTime',isAsc:'desc',tablename:tablename,bizType:bizType},
- done: function(res, curr, count){
- if(multipleMode == "single"){//单选模式下需要进行设置 add by wuzuhu on 2022-07-18
- if(count > 0){
- $(".filepicker_btn").hide();
- }else{
- $(".filepicker_btn").show();
- }
- }
- },
- cols: [[//表头
- {type: 'checkbox',fixed: 'left'},
- {field: 'name', title: '文件名',sort: true, fixed: 'left'},
- {field: 'createTime', title: '创建时间',sort: true,width:170,align: 'center'},
- {field: 'size', title: '文件大小',sort: true,width:110,align: 'center',templet: function(data){
- return WebUploader.Base.formatSize(data.size);
- }},
- {field:'title', title: '操作',width:120,templet: function(data){
- var actions = [];
- return actions.join('');
- }
- }
- ]] });
-
- //监听工具条
- table.on('toolbar(dataTable)', function(obj){
- var layEvent = obj.event;
- if(layEvent == 'create'){
- }
- if(layEvent == "del"){
- }
- });
-
- //监听排序事件
- table.on('sort(dataTable)',function(obj){
- table.reload('dataTable',{
- initSort: obj,
- where:{orderByColumn:obj.field,isAsc:obj.type}
- });
- });
- });
- }
-
- function deleteFile(fid){
- $.ajax({
- type:"POST",
- url:[[@{/uploadfile/deleteByFid}]],
- data:{
- fid : fid,
- },
- dataType:"json",
- success:function(response){
- doSearch();
- parent.layer.msg("操作成功",{time:1500,icon:6});
- },
- error:function(){
- }
- });
- }
-
- function downloadFile(fid){
- window.location.href=[[@{/uploadfile/download}]]+"?fid="+ fid;
- }
-
- script>
- body>
- html>
这里我们放在百度网盘的样子对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的集成效果图如下图所示:

通过观察网络请求可以看到,前端往服务端提交数据时,数据是已经进行了分片:

