基于Ruoyi和WebUploader的统一附件管理扩展(上)

发布于:2022-12-06 ⋅ 阅读:(231) ⋅ 点赞:(0)

背景

        在Ruoyi框架中,虽然也提供了基于fileinput的文件上传示例,加入企业在真实业务中有大文件的上传,比如上GB的文件,那使用fileinput的用户体验不怎么友好,因而在大容量文件上传处理时,就有必要进行切片,断点续传,重复文件判断等。因此本文将使用百度开源的WebUploader上传组件,对文件上传业务提供统一的封装和扩展,可以满足所有业务场景的覆盖。

       本文将重点说明ruoyi使用的基础技术,简单介绍webuploader,webuploader如何在Ruoyi中进行集成。Ruoyi的示例例子采用的是Ruoyi的单体集成框架,不是前后端分离版,不过技术的思路是类似的,可以作为参考。

一、Ruoyi的实现

1、ruoyi的前端实现

         ruoyi的前端是依赖于fileinput来实现的,其官方的文档手册地址可以参见:bootstrap-fileinput

      实现的效果大致是这样的:

2、Ruoyi后端实现 

        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<MultipartFile> files) throws Exception
    {
        try
        {
            // 上传文件路径
            String filePath = HngtghyConfig.getUploadPath();
            List<String> urls = new ArrayList<String>();
            List<String> fileNames = new ArrayList<String>();
            List<String> newFileNames = new ArrayList<String>();
            List<String> originalFilenames = new ArrayList<String>();
            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());
        }
    }

}

二、基于WebUploader的切片处理

1、关于webuploader

        正是由于Ruoyi天生的大文件处理能力比较差的,经过对开源组件的比较,我们选定了百度开源的百度Webuploader,它具有以下的能力:

2、Webuploader集成

        在官网上下载最新的webuploader资源包后,将相应的资源文件拷贝到Ruoyi的工程目录中,

 3、在上传页面中引用webuploader.js

       在这里,我们设计了统一的文件存储服务,因此,将文件的查询、上传、编辑、删除功能都封装在一个界面中,对外提供单个文件上传功能,也提供批量管理功能。所以,有必要对文件进行统一封装。下面是基于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 = [];
						        actions.push('<a class="btn btn-purple btn-xs" href="#" onclick="downloadFile(\'' + data.fid + '\')"><i class="fa fa-download">下载</i></a> ');
						        actions.push('<a class="btn btn-danger btn-xs" href="#" onclick="deleteFile(\'' + data.fid + '\')"><i class="fa fa-remove">删除</i></a> ');
						        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>

 4、Webuploader功能定制

       这里我们放在百度网盘的样子对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的简单集成,这是第一个部分,如果需要详细了解的,可以深入交流,这里有涉及数据分片的具体实现,还有后端的服务端支持等等,关于后端的设计和业务表的设计,打算在后续再进行说明。

       Webuploader与Ruoyi的集成效果图如下图所示:

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