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


    背景

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

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

    一、Ruoyi的实现

    1、ruoyi的前端实现

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

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

    2、Ruoyi后端实现 

            Ruoyi使用了最简单的文件接收方式,没有文件切片,这样设计的目的,个人猜测是因为不考虑大文件的这种场景,当然在互联网里,确实遇到大文件的情况也不多,使用这样的方案也可以应对。Ruoyi的后台处理类代码如下:

    1. package com.hngtghy.project.common;
    2. import java.util.ArrayList;
    3. import java.util.List;
    4. import javax.servlet.http.HttpServletRequest;
    5. import javax.servlet.http.HttpServletResponse;
    6. import org.slf4j.Logger;
    7. import org.slf4j.LoggerFactory;
    8. import org.springframework.beans.factory.annotation.Autowired;
    9. import org.springframework.http.MediaType;
    10. import org.springframework.stereotype.Controller;
    11. import org.springframework.web.bind.annotation.GetMapping;
    12. import org.springframework.web.bind.annotation.PostMapping;
    13. import org.springframework.web.bind.annotation.RequestMapping;
    14. import org.springframework.web.bind.annotation.ResponseBody;
    15. import org.springframework.web.multipart.MultipartFile;
    16. import com.hngtghy.common.constant.Constants;
    17. import com.hngtghy.common.utils.StringUtils;
    18. import com.hngtghy.common.utils.file.FileUploadUtils;
    19. import com.hngtghy.common.utils.file.FileUtils;
    20. import com.hngtghy.framework.config.HngtghyConfig;
    21. import com.hngtghy.framework.config.ServerConfig;
    22. import com.hngtghy.framework.web.domain.AjaxResult;
    23. /**
    24. * 通用请求处理
    25. *
    26. * @author wuzuhu
    27. */
    28. @Controller
    29. @RequestMapping("/common")
    30. public class CommonController
    31. {
    32. private static final Logger log = LoggerFactory.getLogger(CommonController.class);
    33. @Autowired
    34. private ServerConfig serverConfig;
    35. private static final String FILE_DELIMETER = ",";
    36. /**
    37. * 通用上传请求(单个)
    38. */
    39. @PostMapping("/upload")
    40. @ResponseBody
    41. public AjaxResult uploadFile(MultipartFile file) throws Exception
    42. {
    43. try
    44. {
    45. // 上传文件路径
    46. String filePath = HngtghyConfig.getUploadPath();
    47. // 上传并返回新文件名称
    48. String fileName = FileUploadUtils.upload(filePath, file);
    49. String url = serverConfig.getUrl() + fileName;
    50. AjaxResult ajax = AjaxResult.success();
    51. ajax.put("url", url);
    52. ajax.put("fileName", fileName);
    53. ajax.put("newFileName", FileUtils.getName(fileName));
    54. ajax.put("originalFilename", file.getOriginalFilename());
    55. return ajax;
    56. }
    57. catch (Exception e)
    58. {
    59. return AjaxResult.error(e.getMessage());
    60. }
    61. }
    62. /**
    63. * 通用上传请求(多个)
    64. */
    65. @PostMapping("/uploads")
    66. @ResponseBody
    67. public AjaxResult uploadFiles(List files) throws Exception
    68. {
    69. try
    70. {
    71. // 上传文件路径
    72. String filePath = HngtghyConfig.getUploadPath();
    73. List urls = new ArrayList();
    74. List fileNames = new ArrayList();
    75. List newFileNames = new ArrayList();
    76. List originalFilenames = new ArrayList();
    77. for (MultipartFile file : files)
    78. {
    79. // 上传并返回新文件名称
    80. String fileName = FileUploadUtils.upload(filePath, file);
    81. String url = serverConfig.getUrl() + fileName;
    82. urls.add(url);
    83. fileNames.add(fileName);
    84. newFileNames.add(FileUtils.getName(fileName));
    85. originalFilenames.add(file.getOriginalFilename());
    86. }
    87. AjaxResult ajax = AjaxResult.success();
    88. ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
    89. ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));
    90. ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER));
    91. ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER));
    92. return ajax;
    93. }
    94. catch (Exception e)
    95. {
    96. return AjaxResult.error(e.getMessage());
    97. }
    98. }
    99. }

    二、基于WebUploader的切片处理

    1、关于webuploader

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

    2、Webuploader集成

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

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

           在这里,我们设计了统一的文件存储服务,因此,将文件的查询、上传、编辑、删除功能都封装在一个界面中,对外提供单个文件上传功能,也提供批量管理功能。所以,有必要对文件进行统一封装。下面是基于Thymeleaf的一个简单封装:

    1. DOCTYPE html>
    2. <html lang="zh" xmlns:th="http://www.thymeleaf.org">
    3. <head th:include="include :: webupload">
    4. <body class="no-skin">
    5. <div class="main-container ace-save-state" id="main-container">
    6. <script type="text/javascript">
    7. try{ace.settings.loadState('main-container')}catch(e){}
    8. script>
    9. <div class="main-content">
    10. <div class="main-content-inner">
    11. <div class="page-content" style="padding: 8px 10px 0px;">
    12. <div class="widget-box">
    13. <div>
    14. <form class="form-search">
    15. <div class="row">
    16. <div class="col-xs-12 col-sm-12">
    17. <table class="table" style="margin-bottom: 0px;">
    18. <tbody>
    19. <tr>
    20. <td style="border: 0px;vertical-align: middle;width:10%;"><p class="text-right">文件名称p>td>
    21. <td style="border: 0px;vertical-align: middle;width:50%;">
    22. <input type="text" name="name" class="form-control" placeholder="请输入文件名称"/>
    23. td>
    24. <td style="border: 0px;vertical-align: middle;width:30%;" >
    25. <button type="button" class="btn btn-primary btn-xs" id="btn-search">
    26. <span class="fa fa-search ">span>
    27. 查询
    28. button>
    29. <a href="#" class="btn btn-success btn-xs filepicker_btn" th:id="'filePicker_'+${temp_b_id}">
    30. <i class="ace-icon fa fa-upload">i>
    31. 选择
    32. a>
    33. <div th:include="include-upload-js :: header">div>
    34. td>
    35. tr>
    36. tbody>
    37. table>
    38. div>
    39. div>
    40. form>
    41. div>
    42. div>
    43. <div class="table-responsive">
    44. <table id="dataTable" lay-filter="dataTable" cellspacing="0" >
    45. table>
    46. div>
    47. div>
    48. div>
    49. div>
    50. div>
    51. <script th:inline="javascript">
    52. var prefix = [[@{/uploadfile}]];
    53. var fileUploadIndex = 0;
    54. $(document).ready(function() {
    55. $("#btn-search").on("click",doSearch);
    56. initTable();
    57. });
    58. function doSearch(){
    59. table.reload('dataTable',{
    60. where : {
    61. name :$("input[name='name']").val(),
    62. }
    63. });
    64. }
    65. var uploadSuccessCallback = function(file,fileArray){
    66. var allFinished = true;
    67. for(i in fileArray){
    68. var obj = fileArray[i];
    69. if(obj.status != '上传失败' && obj.status != '上传成功'){
    70. allFinished = false;
    71. break;
    72. }
    73. }
    74. if(allFinished){
    75. doSearch();
    76. parent.layer.close(fileUploadIndex);
    77. modal = null;
    78. }
    79. }
    80. function initTable(){
    81. var bid = [[${bid}]];
    82. var temp_b_id = [[${temp_b_id}]];
    83. var b_ids = bid == null ? temp_b_id : bid;
    84. var tablename = [[${tablename}]];
    85. var bizType = [[${bizType}]];
    86. var multipleMode = [[${multipleMode}]];
    87. layui.use('table', function(){
    88. table = layui.table;
    89. table.render({
    90. elem: '#dataTable',
    91. height: "full",
    92. url: prefix + "/list?b_id="+b_ids,
    93. method : "post",
    94. page: true,
    95. //toolbar:"#toolbar",
    96. defaultToolbar:[],
    97. where:{orderByColumn:'createTime',isAsc:'desc',tablename:tablename,bizType:bizType},
    98. done: function(res, curr, count){
    99. if(multipleMode == "single"){//单选模式下需要进行设置 add by wuzuhu on 2022-07-18
    100. if(count > 0){
    101. $(".filepicker_btn").hide();
    102. }else{
    103. $(".filepicker_btn").show();
    104. }
    105. }
    106. },
    107. cols: [[//表头
    108. {type: 'checkbox',fixed: 'left'},
    109. {field: 'name', title: '文件名',sort: true, fixed: 'left'},
    110. {field: 'createTime', title: '创建时间',sort: true,width:170,align: 'center'},
    111. {field: 'size', title: '文件大小',sort: true,width:110,align: 'center',templet: function(data){
    112. return WebUploader.Base.formatSize(data.size);
    113. }},
    114. {field:'title', title: '操作',width:120,templet: function(data){
    115. var actions = [];
    116. actions.push('fid + '\')">下载 ');
    117. actions.push('fid + '\')">删除 ');
    118. return actions.join('');
    119. }
    120. }
    121. ]] });
    122. //监听工具条
    123. table.on('toolbar(dataTable)', function(obj){
    124. var layEvent = obj.event;
    125. if(layEvent == 'create'){
    126. }
    127. if(layEvent == "del"){
    128. }
    129. });
    130. //监听排序事件
    131. table.on('sort(dataTable)',function(obj){
    132. table.reload('dataTable',{
    133. initSort: obj,
    134. where:{orderByColumn:obj.field,isAsc:obj.type}
    135. });
    136. });
    137. });
    138. }
    139. function deleteFile(fid){
    140. $.ajax({
    141. type:"POST",
    142. url:[[@{/uploadfile/deleteByFid}]],
    143. data:{
    144. fid : fid,
    145. },
    146. dataType:"json",
    147. success:function(response){
    148. doSearch();
    149. parent.layer.msg("操作成功",{time:1500,icon:6});
    150. },
    151. error:function(){
    152. }
    153. });
    154. }
    155. function downloadFile(fid){
    156. window.location.href=[[@{/uploadfile/download}]]+"?fid="+ fid;
    157. }
    158. script>
    159. body>
    160. html>

     4、Webuploader功能定制

           这里我们放在百度网盘的样子对WebUpload的样式进行改造,同时需要将文件上传的列表展示出来,同时可以对文件进行上传、暂停、删除等操作,因此需要对webuploader进行定制化开发。相关代码如下:

    1. function initUploader(){
    2. bindFileListeners();
    3. var fileNumLimit = [[${fileNumLimit}]];//文件数量限制
    4. var acceptType = [[${acceptType}]];//支持文件类型
    5. var auto = [[${autoUpload}]];//是否自动上传0否1是
    6. var multipleMode = [[${multipleMode}]];//多选模式 add by wuzuhu on 2022-07-18
    7. uploader = WebUploader.create({
    8. auto: auto==0 ? false : true,
    9. swf: [[@{/uploader/Uploader.swf}]],
    10. server: [[@{/uploadfile/bigUploader}]],
    11. pick: {id:'#filePicker_'+[[${temp_b_id}]],multiple: multipleMode == "single" ? false : true},
    12. dnd: '#filePicker_'+[[${temp_b_id}]],
    13. method:'POST',
    14. resize: false ,
    15. chunked : true,
    16. chunkRetry:false,
    17. formData : {
    18. fid : '',
    19. name : '',
    20. size : 0,
    21. md5code : '',
    22. tablename : tablename,
    23. temp_b_id : temp_b_id,
    24. bizType : [[${bizType}]],
    25. bid : b_id
    26. },
    27. compress : false,
    28. duplicate:true,
    29. prepareNextFile: true,
    30. disableGlobalDnd:true,
    31. });
    32. uploader.on('beforeFileQueued', function(file) {
    33. if(file.size == 0){
    34. var error = "文件不能为空!";
    35. parent.layer.msg(error);
    36. return false;
    37. }
    38. if (fileNumLimit != null && fileNumLimit <= fileArray.length) {
    39. message.info("文件数量不能超过" + fileNumLimit);
    40. return false;
    41. }
    42. var file_name = file.name;
    43. var file_type = file_name.substring(file_name.lastIndexOf(".") + 1);
    44. if (acceptType != null && acceptType.length !== 0) {
    45. if (acceptType.indexOf(file_type) == -1) {
    46. message.info("文件类型只能是" + acceptType.toString());
    47. return false;
    48. }
    49. }
    50. return true;
    51. });
    52. uploader.on('fileQueued', function(file) {
    53. /* for(i in fileArray){
    54. var obj = fileArray[i];
    55. if(obj.name == file.name){
    56. if(obj.status == '上传失败'){
    57. modal.removeFile(obj.f_id);
    58. }
    59. }
    60. }
    61. var uuid = WebUploader.Base.guid('');
    62. var file_upload = new FileObj(file.id,uuid,file.name,file.size,'','等待上传','','');
    63. fileArray.push(file_upload);
    64. openProcessModalFile(file); */
    65. });
    66. uploader.on('filesQueued', function(files) {
    67. for(j in files){
    68. var file = files[j];
    69. for(i in fileArray){
    70. var obj = fileArray[i];
    71. if(obj.name == file.name){
    72. if(obj.status == '上传失败' && modal != null){
    73. modal.removeFile(obj.f_id);
    74. }
    75. }
    76. }
    77. var uuid = WebUploader.Base.guid('');
    78. var file_upload = new FileObj(file.id,uuid,file.name,file.size,'','等待上传','','');
    79. fileArray.push(file_upload);
    80. }
    81. openProcessModalFiles(files);
    82. });
    83. uploader.on( 'uploadProgress', function( file, percentage ) {
    84. var obj = getFileObjById(file.id);
    85. if(obj.status == '暂停'){
    86. return;
    87. }
    88. if (obj.id === file.id) {
    89. if (percentage === 1) {
    90. if (obj.status === '99.99%') {
    91. return;
    92. }
    93. if (file.size > block_size) {
    94. obj.status = '99.99%';
    95. if(modal){
    96. modal.updateStatus(obj.f_id,'99.99%')
    97. }
    98. }
    99. } else {
    100. percentage = (percentage * 100).toFixed(2);
    101. if (percentage + "%" === obj.status) {
    102. return;
    103. }
    104. obj.status = percentage + "%";
    105. if(modal){
    106. modal.updateStatus(obj.f_id,percentage + "%")
    107. }
    108. }
    109. }
    110. });
    111. uploader.on( 'uploadBeforeSend', function( block,data,headers ) {
    112. var obj = getFileObjById(block.file.id);
    113. data.md5code = obj.md5code;
    114. data.fid = obj.f_id;
    115. data.name = obj.f_name;
    116. data.size = obj.f_size;
    117. data.chunk = block.chunk;
    118. data.chunkSize = block.end-block.start;
    119. });
    120. }
    121. function addFiles(files){
    122. for(i in files){
    123. var file = files[i];
    124. addFile(file);
    125. }
    126. }

             由于篇幅有限,这里不把所有的代码都列出来,仅将部分代码列出来。

    三、WebUploader与Ruoyi集成总结

           这里讲解了Webuploader与Ruoyi的简单集成,这是第一个部分,如果需要详细了解的,可以深入交流,这里有涉及数据分片的具体实现,还有后端的服务端支持等等,关于后端的设计和业务表的设计,打算在后续再进行说明。

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

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

     

  • 相关阅读:
    Flutter——最详细(Scaffold)使用教程
    国考省考行测:问题型材料主旨分析,有问题有对策,主旨是对策,有问题无对策,要合理引申对策
    THREE--demo10(地球坐标)
    behave结果转化为cucumber结果,主要用于将behave.json转化为cucumber.json
    企业架构LNMP学习笔记31
    【Python爬虫项目实战四】Chatgpt国内接口分享第一期
    C++ string类常用函数
    中国地质大学许少辉著《乡村振兴战略下传统村落文化旅游设计》图书馆荐购辉少许
    DolphinDB & 浙商银行 | 第二期现场培训圆满结束
    如何在 Vue.js 中引入原子设计?
  • 原文地址:https://blog.csdn.net/yelangkingwuzuhu/article/details/127069179