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撰写、修改论文及工作报告,提供…...
rknn优化教程(二)
文章目录 1. 前述2. 三方库的封装2.1 xrepo中的库2.2 xrepo之外的库2.2.1 opencv2.2.2 rknnrt2.2.3 spdlog 3. rknn_engine库 1. 前述 OK,开始写第二篇的内容了。这篇博客主要能写一下: 如何给一些三方库按照xmake方式进行封装,供调用如何按…...
Appium+python自动化(十六)- ADB命令
简介 Android 调试桥(adb)是多种用途的工具,该工具可以帮助你你管理设备或模拟器 的状态。 adb ( Android Debug Bridge)是一个通用命令行工具,其允许您与模拟器实例或连接的 Android 设备进行通信。它可为各种设备操作提供便利,如安装和调试…...
Spring Boot 实现流式响应(兼容 2.7.x)
在实际开发中,我们可能会遇到一些流式数据处理的场景,比如接收来自上游接口的 Server-Sent Events(SSE) 或 流式 JSON 内容,并将其原样中转给前端页面或客户端。这种情况下,传统的 RestTemplate 缓存机制会…...
前端倒计时误差!
提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...
使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装
以下是基于 vant-ui(适配 Vue2 版本 )实现截图中照片上传预览、删除功能,并封装成可复用组件的完整代码,包含样式和逻辑实现,可直接在 Vue2 项目中使用: 1. 封装的图片上传组件 ImageUploader.vue <te…...
Qt Http Server模块功能及架构
Qt Http Server 是 Qt 6.0 中引入的一个新模块,它提供了一个轻量级的 HTTP 服务器实现,主要用于构建基于 HTTP 的应用程序和服务。 功能介绍: 主要功能 HTTP服务器功能: 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...
Go 并发编程基础:通道(Channel)的使用
在 Go 中,Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式,用于在多个 Goroutine 之间传递数据,从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...
深度学习水论文:mamba+图像增强
🧀当前视觉领域对高效长序列建模需求激增,对Mamba图像增强这方向的研究自然也逐渐火热。原因在于其高效长程建模,以及动态计算优势,在图像质量提升和细节恢复方面有难以替代的作用。 🧀因此短时间内,就有不…...
腾讯云V3签名
想要接入腾讯云的Api,必然先按其文档计算出所要求的签名。 之前也调用过腾讯云的接口,但总是卡在签名这一步,最后放弃选择SDK,这次终于自己代码实现。 可能腾讯云翻新了接口文档,现在阅读起来,清晰了很多&…...
论文阅读笔记——Muffin: Testing Deep Learning Libraries via Neural Architecture Fuzzing
Muffin 论文 现有方法 CRADLE 和 LEMON,依赖模型推理阶段输出进行差分测试,但在训练阶段是不可行的,因为训练阶段直到最后才有固定输出,中间过程是不断变化的。API 库覆盖低,因为各个 API 都是在各种具体场景下使用。…...
