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撰写、修改论文及工作报告,提供…...
大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...
STM32标准库-DMA直接存储器存取
文章目录 一、DMA1.1简介1.2存储器映像1.3DMA框图1.4DMA基本结构1.5DMA请求1.6数据宽度与对齐1.7数据转运DMA1.8ADC扫描模式DMA 二、数据转运DMA2.1接线图2.2代码2.3相关API 一、DMA 1.1简介 DMA(Direct Memory Access)直接存储器存取 DMA可以提供外设…...
现代密码学 | 椭圆曲线密码学—附py代码
Elliptic Curve Cryptography 椭圆曲线密码学(ECC)是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础,例如椭圆曲线数字签…...
【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术,它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton):由层级结构的骨头组成,类似于人体骨骼蒙皮 (Mesh Skinning):将模型网格顶点绑定到骨骼上,使骨骼移动…...
自然语言处理——Transformer
自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效,它能挖掘数据中的时序信息以及语义信息,但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN,但是…...
【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)
1.获取 authorizationCode: 2.利用 authorizationCode 获取 accessToken:文档中心 3.获取手机:文档中心 4.获取昵称头像:文档中心 首先创建 request 若要获取手机号,scope必填 phone,permissions 必填 …...
Hive 存储格式深度解析:从 TextFile 到 ORC,如何选对数据存储方案?
在大数据处理领域,Hive 作为 Hadoop 生态中重要的数据仓库工具,其存储格式的选择直接影响数据存储成本、查询效率和计算资源消耗。面对 TextFile、SequenceFile、Parquet、RCFile、ORC 等多种存储格式,很多开发者常常陷入选择困境。本文将从底…...
C语言中提供的第三方库之哈希表实现
一. 简介 前面一篇文章简单学习了C语言中第三方库(uthash库)提供对哈希表的操作,文章如下: C语言中提供的第三方库uthash常用接口-CSDN博客 本文简单学习一下第三方库 uthash库对哈希表的操作。 二. uthash库哈希表操作示例 u…...
Unity UGUI Button事件流程
场景结构 测试代码 public class TestBtn : MonoBehaviour {void Start(){var btn GetComponent<Button>();btn.onClick.AddListener(OnClick);}private void OnClick(){Debug.Log("666");}}当添加事件时 // 实例化一个ButtonClickedEvent的事件 [Formerl…...
django blank 与 null的区别
1.blank blank控制表单验证时是否允许字段为空 2.null null控制数据库层面是否为空 但是,要注意以下几点: Django的表单验证与null无关:null参数控制的是数据库层面字段是否可以为NULL,而blank参数控制的是Django表单验证时字…...
