el-form与el-upload结合上传带附件的表单数据(后端篇)
1.写在之前
本文采用Spring Boot + MinIO + MySQL+Mybatis Plus技术栈,参考ruoyi-vue-pro项目。
前端实现请看本篇文章el-form与el-upload结合上传带附件的表单数据(前端篇)-CSDN博客。
2.需求描述
在OA办公系统中,流程表单申请人填写表单数据,上传所需附件,供流程后续审核人员下载查看。如下图所示,生产单位经办人填写表单数据,保存后,提交流程审批任务到下一节点,下一节点人员审核时下载查看初始节点人员上传的附件。
图注:表单数据填写页面
图注:流程节点审批信息页面
3.设计思路
文件存储放到MinIO中,封装一个MinIO客户端,给出上传,下载,删除文件方法,代码如下所示。
@Configuration
public class MinIOFileClient {@Resourceprivate FileClientProperties fileClientProperties;@Resourceprivate MinioClient client;public String upload(byte[] content, String name, String bucket, String type) throws Exception {// 执行上传client.putObject(PutObjectArgs.builder().bucket(bucket) // bucket 必须传递.contentType(type).object(name) // 相对路径作为 key.stream(new ByteArrayInputStream(content), content.length, -1) // 文件内容.build());// 拼接返回路径return String.format("%s/%s/%s", fileClientProperties.getUrl(), bucket, name);}public void delete(String name, String bucket) throws Exception {client.removeObject(RemoveObjectArgs.builder().bucket(bucket) // bucket 必须传递.object(name) // 相对路径作为 key.build());}public byte[] getContent(String name, String bucket) throws Exception {GetObjectResponse response = client.getObject(GetObjectArgs.builder().bucket(bucket) // bucket 必须传递.object(name) // 相对路径作为 key.build());return IoUtil.readBytes(response);}}
文件上传到MinIO服务器上,返回一个文件下载的URL,在具体的表单业务中,存储一个List转换的String,每个list包含文件的名称,文件的唯一编码,文件查看的URL。
@Data
public class FileObject implements Serializable {/*** 文件名*/private String name;/*** 文件的唯一标识编码 因为要和前端el-upload联合使用 所以使用response这个字段*/private String response;/*** 文件存储的地址*/private String url;
}
初期设计思路为表单数据与文件数据一同传输给后端,后端存储表单数据进入数据库之前单独调用文件传输接口上传文件,获取文件的URL,利用MyBatis Plus的type handler把List转为String存储进入业务表单数据。这样设计的好处在于文件上传下载的时候相当于与表单数据的存储合为一个事务,能保证附件的上传成功率,表单附件修改尤其删除已经存储的附件很方便。坏处就是表单数据是使用JSON数据格式传输的,想要传输文件附件有一点不好实现。经过网上的探索,最终这种方案被放弃,来自一个网上的评论)用JSON没办法使用流上传,使用base64还会增加文件大小,转换消耗浏览器资源,图片一大或者变成了视频、大文件之类的,这个就用不了了,加上服务器需要对请求体大小专门放开,不能这样惯着后端,不然到时候代码会成祖传。最后这种方法被否了。选择了另外一种方案。
另外一种方案为文件上传删除与表单数据上传分开,即开启el-upload的自动上传,通过el-upload组件的on-success回调函数,获取文件上传的信息(FileObject),获取到对应的信息后,通过Vue的组件通信,返回给表单对应的数据,表单数据保存时,直接传入对应的文件信息数组,后端直接转换的String存储到业务数据表中。这种方案的问题在于,
第一,在填写表单时,点击了附件也上传成功后,表单没有点击保存,此时重新填写表单时,上传的附件不会出现在表单中,重新上传附件,会导致文件重复存储进入磁盘,造成资源的浪费;
第二,在表单附件删除时,已经点击了附件删除,但此时表单没有保存,重新刷新打开表单时,此时业务表单存储的附件信息没有改变,但对应的有的文件已经被删除,本来应该存在的文件不存在了。
4.文件的上传
为了解决第三节上传遇到的问题,存储文件的信息引入一个数据库表,存储信息如下
public class FileDO extends BaseDO {/*** 编号,数据库自增*/private Long id;/*** 原文件名*/private String name;/*** 存储的bucket*/private String bucket;/*** 访问地址*/private String url;/*** 文件的 MIME 类型,例如 "application/octet-stream"*/private String type;/*** 文件大小*/private Integer size;/*** 文件的唯一编码 因为要和前端el-upload联合使用 所以使用response这个字段*/private String response;}
在每次存储文件时,根据文件内容与文件名生成唯一的文件编码,也既是存储进入MinIO服务磁盘的文件名,这样保证相同的文件会被替换,减少空间的浪费。用此唯一编码查看FileDo所对应数据数据库是否有本数据,如果有本数据,删除本数据,重新生成新的数据并插入。数据上传成功后,返回对应的文件信息,供前端使用。
public FileObject createFile(String name, String bucket, byte[] content) {// 计算默认的 path 名String type = FileTypeUtils.getMineType(content, name);// 文件名称不能为空 FILE_NAME_IS_EMPTYif (StrUtil.isEmpty(name)) {throw exception(FILE_NAME_IS_EMPTY);}//生成唯一id存储 防止名称相同的文件被顶替String response = FileUtils.generateCode(content, name);if (Boolean.TRUE.equals(validateFileExists(response, bucket))){// 说明该文件已经存储过 minIO存储的文件会被覆盖 不需要做任何事情// FileDO 删除 然后新保存fileMapper.deleteByResponseAndBucket(response, bucket);}String url = minIOFileClient.upload(content, response, bucket, type);// 保存到数据库FileDO file = new FileDO();file.setName(name);file.setResponse(response);file.setUrl(url);file.setType(type);file.setSize(content.length);file.setBucket(bucket);fileMapper.insert(file);// 构造返回数据FileObject fileObject = new FileObject();fileObject.setName(name);fileObject.setResponse(response);fileObject.setUrl(url);return fileObject;}
[{"name":"1.jpg","response":"5eb1dfe0f288445a49260074041508d932f6ad190a898ff0500e052d8ecf5a88.jpg","url":"http://192.168.16.58:9000/operation/5eb1dfe0f288445a49260074041508d932f6ad190a898ff0500e052d8ecf5a88.jpg"},{"name":"2.jpg","response":"7d85e8fb46db1259089b025e44c09c9fa1f696db05437f21879d035b6f04e331.jpg","url":"http://192.168.16.58:9000/operation/7d85e8fb46db1259089b025e44c09c9fa1f696db05437f21879d035b6f04e331.jpg"}]
上面代码为业务数据表存储的具体数据。下图为FileDO对应的数据存储。
5.文件的删除
为解决第三节第二个删除的问题。在删除时,el-upload组件删除文件不调用后端删除文件接口,只做一个假删除,真正的删除在表单修改点击保存调用后端的update接口时,由后端做删除操作。
删除的逻辑为,接口调用到达后端时,比较数据库中已经存储的的附件数据data1与本次上传的附件数据data2,计算单差集,即只在data1中有而在data2中没有的附件信息data3,data3即本次需要删除的附件信息。
fileApi.deleteFile(bidMapper.selectById(bidDO.getId()).getFiles(), bidDO.getFiles());
public void deleteFile(String response, String bucket){try{// 校验存在if (Boolean.FALSE.equals(validateFileExists(response, bucket))){throw exception(FILE_NOT_EXISTS);}//删除数据minIOFileClient.delete(response, bucket);fileMapper.deleteByResponseAndBucket(response, bucket);}catch (Exception e){log.error(e.getMessage());throw exception(FILE_DELETE_FAILED);}}public void deleteFile(List<FileObject> oldFileList, List<FileObject> newFileList){//计算集合的单差集,即只返回【集合1】中有,但是【集合2】中没有的元素,例如:// subtract([1,2,3,4],[2,3,4,5]) -》 [1]List<FileObject> subtract = CollUtil.subtractToList(JSON.parseArray(String.valueOf(oldFileList), FileObject.class), newFileList);subtract.forEach( s -> {String response = s.getResponse();FileDO fileDO = fileMapper.selectOne("response", response);this.deleteFile(response, fileDO.getBucket());});}
6.文件的下载
文件的下载没有什么特殊的情况,直接上代码就行。
public byte[] getFileContent(String response) {try{FileDO fileDO = fileMapper.selectOne("response", response);return minIOFileClient.getContent(response, fileDO.getBucket());}catch (Exception e){log.error(e.getMessage());throw exception(FILE_DOWNLOAD_FAILED);}}public void downloadFile(HttpServletRequest request, HttpServletResponse response, String code) {try{byte[] file = getFileContent(code);FileDO fileDO = fileMapper.selectOne("response", code);ServletUtils.writeAttachment(response, fileDO.getName(), file);}catch (Exception e){log.error(e.getMessage());throw exception(FILE_DOWNLOAD_FAILED);}}
7.没有解决的问题
有这样一种情况,在初始填写表单时,上传了5个附件,都上传成功了,但发现上传错误,删除了其中的两个附件,此时点击保存表单,表单中只存储了3个附件的信息,被删除的两个附件不会再表单中体现,也不会在磁盘上被删除(因为前端没有调用实际的删除接口,后端在差集比较时,存储的数据为空),造成了资源的浪费。
8.写在最后
本文很笼统的介绍了一下在附件与表单数据分开上传时自己遇到的一些问题,以及自己探索的解决方法,中间的描述有一些可能不是很清楚,也还有遗留问题,后续还会慢慢解决。看到这篇文章的你,如果有任何指教,欢迎私信探讨!
相关文章:
el-form与el-upload结合上传带附件的表单数据(后端篇)
1.写在之前 本文采用Spring Boot MinIO MySQLMybatis Plus技术栈,参考ruoyi-vue-pro项目。 前端实现请看本篇文章el-form与el-upload结合上传带附件的表单数据(前端篇)-CSDN博客。 2.需求描述 在OA办公系统中,流程表单申请人…...
postMessage——不同源的网页直接通过localStorage/sessionStorage/Cookies——技能提升
最近遇到一个问题,就是不同源的两个网页之间进行localstorage或者cookie的共享。 上周其实遇到过一次,觉得麻烦就让后端换了种方式处理了,昨天又遇到了同样的问题。 使用场景 比如从网页A通过iframe跳转到网页B,而且这两个网页…...
上市公司-绿色投资者数据集(2000-2022)
上市公司-绿色投资者数据(2000-2022年)是一份涵盖了过去二十多年中国上市公司绿色投资情况的详细数据集。该数据集包括了各上市公司的股票代码、年份、会计年度、股票简称,以及STPT(特殊处理股票的标识),行…...
3 pandas之dataframe
定义 DataFrame是一个二维数据结构,即数据以行和列的方式以表格形式对齐。 DataFrame特点: 存在不同类型的列大小可变带有标签的轴可对列和行进行算数运算 构造函数 pandas.DataFrame( data, index, columns, dtype, copy)参数解释: 序号…...
vue-内网,离线使用百度地图(地图瓦片图下载静态资源展示定位)
前言 最近发现很多小伙伴都在问内网怎么使用百度地图,或者是断网情况下能使用百度地图吗 后面经过一番研究,主要难点是,正常情况下我们是访问公网百度图片,数据,才能使用 内网时访问不了百度地图资源时就会使用不了&…...
OpenFeign 万字教程详解
OpenFeign 万字教程详解 目录 一、概述 1.1.OpenFeign是什么?1.2.OpenFeign能干什么1.3.OpenFeign和Feign的区别1.4.FeignClient 二、OpenFeign使用 2.1.OpenFeign 常规远程调用2.2.OpenFeign 微服务使用步骤2.3.OpenFeign 超时控制2.4.OpenFeign 日志打印2.5.O…...
全自动双轴晶圆划片机:半导体制造的关键利器
随着科技的飞速发展,半导体行业正以前所未有的速度向前迈进。在这个过程中,全自动双轴晶圆划片机作为一种重要的设备,在半导体晶圆、集成电路、QFN、发光二极管、miniLED、太阳能电池、电子基片等材料的划切过程中发挥着举足轻重的作用。 全自…...
Android Studio 安装和使用
前些天,打开了几年前的一个Android Studio app项目,使用安卓虚拟机仿真app崩溃,怀疑是不是中间升级过Android Studio导致异常的,马上脑子一热卸载了,结果上次踩过的坑,一个没少又踩一次,谨以此文…...
【已解决】Java中,判断:集合中是否包含指定元素(模糊匹配)比如权限中的user:list或者是user:*这种判断
背景描述 在工作中,有时候,我们需要对list中是否包含了指定元素进行判断,但是,有时候又需要支持模糊匹配,这个时候怎么办呢? 比如权限,我们知道,权限不仅可以配置完整的路径&#…...
【基于激光雷达的路沿检测用于自动驾驶的真值标注】
文章目录 概要主要贡献内容概述实验小结 概要 论文地址:https://arxiv.org/pdf/2312.00534.pdf 路沿检测在自动驾驶中扮演着重要的角色,因为它能够帮助车辆感知道可行驶区域和不可行驶区域。为了开发和验证自动驾驶功能,标注的数据是必不可…...
【Spring实战】配置多数据源
文章目录 1. 配置数据源信息2. 创建第一个数据源3. 创建第二个数据源4. 创建启动类及查询方法5. 启动服务6. 创建表及做数据7. 查询验证8. 详细代码总结 通过上一节的介绍,我们已经知道了如何使用 Spring 进行数据源的配置以及应用。在一些复杂的应用中,…...
DevOps系列文章 : 使用dpkg命令打deb包
创建一个打包的目录,类似rpmbuild,这里创建了目录deb_build mkdir deb_build目标 我有一个hello的二进制文件hello和源码hello.c, 准备安装到/opt/helloworld目录中 步骤 在deb_build目录创建一个文件夹用于存放我的安装文件 mkdir helloworld在he…...
linux sed命令操作大全
经常使用,但有些总记不全,有时候经常查找,这次全部捋清楚做备忘,有需要的小伙伴欢迎收藏起来哦! 查、增、改、删一应俱全,非常详细! 目录 一、查看 查看第2行 查看第2行到第3行 查看第1行、…...
Vue2+Vue3组件间通信方式汇总(3)------$bus
组件间通信方式是前端必不可少的知识点,前端开发经常会遇到组件间通信的情况,而且也是前端开发面试常问的知识点之一。接下来开始组件间通信方式第三弹------$bus,并讲讲分别在Vue2、Vue3中的表现。 Vue2Vue3组件间通信方式汇总(1)…...
前端基础location的使用
概念 获取当前页面的地址信息,还可以修改某些属性,实现页面跳转和刷新等。 样例展示 window.location 含义.originURL 基础地址,包括协议名、域名和端口号.protocol协议 (http: 或 https:).host域名端口号.hostname域名.port端口号.pathname路…...
Android JNI入门到基础
一、JNI项目创建 AS创建项目时选择NativeC 会创建一个基本的JNI项目 MainActivity中写java层的native方法 具体实现在cpp文件中 native-lib.cpp #include <jni.h> #include <string>extern "C" JNIEXPORT jstring JNICALL Java_com_cn_techvision_j…...
60.乐理基础-打拍子-V字打拍法
前置内容: 文字版 https://note.youdao.com/s/6FSSvGBf (顺序参考:下方的视频版里面目录顺序) 视频版 【四川音乐学院作曲硕士】教你零基础自学乐理保姆级教学-学习视频教程-腾讯课堂 文字版还有下图红框中三个专栏里的内容&a…...
列表对象的时间进行中文格式化处理
在黑马的项目学习中,如何将前端页面时间显示成2023年12月21日 06:23:23中文形式。 如果你想使用中文格式化日期,你可以将 en-US 更改为 zh-CN,以使用中文语言环境。以下是修改后的代码: result.data.items.forEach(item > {//…...
vi和vim的区别
目录 一、前言 二、vi/vim 的介绍 三、Vi/Vim 常见指令 四、vi和vim的区别 一、前言 写这篇文章的目的,是为了告诉大家我们如果要在终端下对文本进行编辑和修改可以使用vim编辑器。 Ubuntu 自带了 VI 编辑器,但是 VI 编辑器对于习惯了 Windows 下进…...
【昆明*线上同步】最新ChatGPT/GPT4科研实践应用与AI绘图技术及论文高效写作
详情点击查看福利:【昆明*线上同步】最新ChatGPT/GPT4科研实践应用与AI绘图技术及论文高效写作 目标: 1、熟练掌握ChatGPT提示词技巧及各种应用方法,并成为工作中的助手。 2、通过案例掌握ChatGPT撰写、修改论文及工作报告,提供…...
【解决Typora图片不是显示问题】PicGo+Github+Typora+ onedrive/坚果云 实现笔记同步
【解决Typora图片不是显示问题】PicGo、Github、Typora实现笔记同步 写在前面: typora笔记软件使用记录typora图片上传问题:原因分析:解决方案:PicGoGithubTypora 坚果云/onedrive 实现笔记同步第一步. 设置上传模式:u…...
使用Guava轻松创建和管理不可变集合
第1章:引言 大家好,我是小黑。今天,我们来聊聊一个在Java编程里超有用的话题:使用Guava创建和管理不可变集合。首先,咱们得明白,什么是不可变集合。简单来说,不可变集合就是一旦创建就不能被修…...
深入了解 Android 中的应用程序签名
深入了解 Android 中的应用程序签名 一、应用程序签名介绍1.1 应用程序签名1.2 应用程序签名的意义1.3 应用程序签名的流程1.4 应用程序签名的方案1.5 签名的重要性和应用场景 二、AOSP 的应用签名2.1 AOSP的应用签名文件路径2.2 应用程序指定签名文件 三、Android Studio 的应…...
说说 style gan 中的感知路径长度(Perceptual Path Length)
我在之前的博库中介绍了 style gan 的基本原理,原文中有提出感知路径长度(Perceptual Path Length)的概念。这是一种评价生成器质量的方式。 PPL基本思想:给出两个随机噪声 z 1 , z 2 ,为求得两点的感知路径长度PPL…...
基于JAVA的厦门旅游电子商务预订系统 开源项目
目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 景点类型模块2.2 景点档案模块2.3 酒店管理模块2.4 美食管理模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 学生表3.2.2 学生表3.2.3 学生表3.2.4 学生表 四、系统展示五、核心代码5.1 新增景点类型5.2 查询推荐的…...
uniapp中使用封装步骤条组件
针对步骤条封装完终于清清楚楚啦 先看效果: 附上代码:使用可直接复用:数据是写在了当前组件中,如有必须,可以使用其中的props传值stepInfos传递相应的数据,根据steps步数就可以控制走到哪一步啦 <temp…...
【MySQL】sum 函数和 count 函数的相同作用
力扣题 1、题目地址 1174. 即时食物配送 II 2、模拟表 配送表:Delivery Column NameTypedelivery_idintcustomer_idintorder_datedatecustomer_pref_delivery_datedate delivery_id 是该表中具有唯一值的列。该表保存着顾客的食物配送信息,顾客在某…...
在QT Creator下用CMake编译GEOS库
最近,想要在C下编一个可用GDAL模块的地图管理系统,找来找去,找到了GEOS。GEOS(Geometry Engine-Open Source)开源几何引擎 是一个用于计算几何的JTS库的 C/C实现,专注于地理信息系统 (GIS&#…...
【Qt之Quick模块】4. QML语法格式及命名规范
概述 QML(Qt Meta-Object Language)是一种声明式语言,用于设计用户界面。它是由Qt框架提供的一种描述界面组件的语言,可以与C代码结合使用,用于创建跨平台的应用程序。 QML具有以下特点: 声明式ÿ…...
Python内置类属性__class__属性的使用教程
概要 Python作为一种高级编程语言,提供了丰富的功能和灵活性,使得开发人员能够更加方便地处理各种任务。其中一个强大的功能是内置类属性__class__属性。本文将详细介绍__class__属性的用法,帮助读者更好地理解和利用这一功能。 第一部分&am…...
网络建站怎么做/视频号视频下载助手app
A. Three Pairwise Maximums 题意:xmax(a,b);ymax(a,c),zmax(b,c),输出满足的a,b,c; 思路:直接看分三种情况a最大,b最大,c最大。某一项最大了,剩下的那俩就输出小的那个就可以啦&…...
想做一个自己设计公司的网站怎么做的/排名优化关键词
在Excel和Word文档中录入信息时,要说F1-F12的快捷键使用,Sara随口而出的肯定是“F4”,毕竟在Excel表格中的“绝对引用”绕不开它呀,O(∩_∩)O哈哈~这不,今天饶有兴趣的来说说这12个快捷键的简单使用,能记一…...
免费网站申请/百度手机助手app下载并安装
国战按计划进行,插7灵壁并在此集合,国战开始后很快就拿下寿春,并改7寿春雕像.这次好多了,绝大部分人都执行了命令,但还是有个别团友没有改7.吴攻寿春,被击退;龙攻许昌,也被击退.这种情况对我们很有利,许昌寿春两边刷,兄弟们想不上榜也难.可惜,人算不如天算,龙出重兵攻许昌,于是…...
手工做皮具国外的网站/windows7优化大师
作业模拟实现词法分析器,记录一下。 题目: 一、待分析的C语言子集的词法 关键字 main if else int return void while (都是小写) 专用符号 — * / < < < > ! ; : ,{ } [ ] …...
惠阳有做公司网站的吗/青岛网站seo公司
(一) 拦截器配置 对于Web应用而言,URL就表示资源,URL就表示一个操作,Spring MVC也正是对URL的匹配进行相关操作,但是有些时候WEBUI的js、图片以及HTML等静态资源对应的URL不是一种操作,而仅仅是…...
浙江省城乡与住房建设厅网站/企业网站营销的实现方式
公粽号:黒掌 一个专注于分享渗透测试、黑客圈热点、黑客工具技术区博主! 前言 今天介绍一种走私技术,HTML Smuggling,这不是一种特别新的攻击方法,但是它确实在越来越多的攻击场景中出现,其中就包括网络钓…...