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撰写、修改论文及工作报告,提供…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...
STM32F4基本定时器使用和原理详解
STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...
SpringBoot+uniapp 的 Champion 俱乐部微信小程序设计与实现,论文初版实现
摘要 本论文旨在设计并实现基于 SpringBoot 和 uniapp 的 Champion 俱乐部微信小程序,以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务,提供稳定高效的数据处理与业务逻辑支持;利用 uniapp 实现跨平台前…...
【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...
智能分布式爬虫的数据处理流水线优化:基于深度强化学习的数据质量控制
在数字化浪潮席卷全球的今天,数据已成为企业和研究机构的核心资产。智能分布式爬虫作为高效的数据采集工具,在大规模数据获取中发挥着关键作用。然而,传统的数据处理流水线在面对复杂多变的网络环境和海量异构数据时,常出现数据质…...
以光量子为例,详解量子获取方式
光量子技术获取量子比特可在室温下进行。该方式有望通过与名为硅光子学(silicon photonics)的光波导(optical waveguide)芯片制造技术和光纤等光通信技术相结合来实现量子计算机。量子力学中,光既是波又是粒子。光子本…...
【MATLAB代码】基于最大相关熵准则(MCC)的三维鲁棒卡尔曼滤波算法(MCC-KF),附源代码|订阅专栏后可直接查看
文章所述的代码实现了基于最大相关熵准则(MCC)的三维鲁棒卡尔曼滤波算法(MCC-KF),针对传感器观测数据中存在的脉冲型异常噪声问题,通过非线性加权机制提升滤波器的抗干扰能力。代码通过对比传统KF与MCC-KF在含异常值场景下的表现,验证了后者在状态估计鲁棒性方面的显著优…...
【C++】纯虚函数类外可以写实现吗?
1. 答案 先说答案,可以。 2.代码测试 .h头文件 #include <iostream> #include <string>// 抽象基类 class AbstractBase { public:AbstractBase() default;virtual ~AbstractBase() default; // 默认析构函数public:virtual int PureVirtualFunct…...
Neko虚拟浏览器远程协作方案:Docker+内网穿透技术部署实践
前言:本文将向开发者介绍一款创新性协作工具——Neko虚拟浏览器。在数字化协作场景中,跨地域的团队常需面对实时共享屏幕、协同编辑文档等需求。通过本指南,你将掌握在Ubuntu系统中使用容器化技术部署该工具的具体方案,并结合内网…...
