当前位置: 首页 > news >正文

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技术栈&#xff0c;参考ruoyi-vue-pro项目。 前端实现请看本篇文章el-form与el-upload结合上传带附件的表单数据&#xff08;前端篇&#xff09;-CSDN博客。 2.需求描述 在OA办公系统中&#xff0c;流程表单申请人…...

postMessage——不同源的网页直接通过localStorage/sessionStorage/Cookies——技能提升

最近遇到一个问题&#xff0c;就是不同源的两个网页之间进行localstorage或者cookie的共享。 上周其实遇到过一次&#xff0c;觉得麻烦就让后端换了种方式处理了&#xff0c;昨天又遇到了同样的问题。 使用场景 比如从网页A通过iframe跳转到网页B&#xff0c;而且这两个网页…...

上市公司-绿色投资者数据集(2000-2022)

上市公司-绿色投资者数据&#xff08;2000-2022年&#xff09;是一份涵盖了过去二十多年中国上市公司绿色投资情况的详细数据集。该数据集包括了各上市公司的股票代码、年份、会计年度、股票简称&#xff0c;以及STPT&#xff08;特殊处理股票的标识&#xff09;&#xff0c;行…...

3 pandas之dataframe

定义 DataFrame是一个二维数据结构&#xff0c;即数据以行和列的方式以表格形式对齐。 DataFrame特点&#xff1a; 存在不同类型的列大小可变带有标签的轴可对列和行进行算数运算 构造函数 pandas.DataFrame( data, index, columns, dtype, copy)参数解释&#xff1a; 序号…...

vue-内网,离线使用百度地图(地图瓦片图下载静态资源展示定位)

前言 最近发现很多小伙伴都在问内网怎么使用百度地图&#xff0c;或者是断网情况下能使用百度地图吗 后面经过一番研究&#xff0c;主要难点是&#xff0c;正常情况下我们是访问公网百度图片&#xff0c;数据&#xff0c;才能使用 内网时访问不了百度地图资源时就会使用不了&…...

OpenFeign 万字教程详解

OpenFeign 万字教程详解 目录 一、概述 1.1.OpenFeign是什么&#xff1f;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…...

全自动双轴晶圆划片机:半导体制造的关键利器

随着科技的飞速发展&#xff0c;半导体行业正以前所未有的速度向前迈进。在这个过程中&#xff0c;全自动双轴晶圆划片机作为一种重要的设备&#xff0c;在半导体晶圆、集成电路、QFN、发光二极管、miniLED、太阳能电池、电子基片等材料的划切过程中发挥着举足轻重的作用。 全自…...

Android Studio 安装和使用

前些天&#xff0c;打开了几年前的一个Android Studio app项目&#xff0c;使用安卓虚拟机仿真app崩溃&#xff0c;怀疑是不是中间升级过Android Studio导致异常的&#xff0c;马上脑子一热卸载了&#xff0c;结果上次踩过的坑&#xff0c;一个没少又踩一次&#xff0c;谨以此文…...

【已解决】Java中,判断:集合中是否包含指定元素(模糊匹配)比如权限中的user:list或者是user:*这种判断

背景描述 在工作中&#xff0c;有时候&#xff0c;我们需要对list中是否包含了指定元素进行判断&#xff0c;但是&#xff0c;有时候又需要支持模糊匹配&#xff0c;这个时候怎么办呢&#xff1f; 比如权限&#xff0c;我们知道&#xff0c;权限不仅可以配置完整的路径&#…...

【基于激光雷达的路沿检测用于自动驾驶的真值标注】

文章目录 概要主要贡献内容概述实验小结 概要 论文地址&#xff1a;https://arxiv.org/pdf/2312.00534.pdf 路沿检测在自动驾驶中扮演着重要的角色&#xff0c;因为它能够帮助车辆感知道可行驶区域和不可行驶区域。为了开发和验证自动驾驶功能&#xff0c;标注的数据是必不可…...

【Spring实战】配置多数据源

文章目录 1. 配置数据源信息2. 创建第一个数据源3. 创建第二个数据源4. 创建启动类及查询方法5. 启动服务6. 创建表及做数据7. 查询验证8. 详细代码总结 通过上一节的介绍&#xff0c;我们已经知道了如何使用 Spring 进行数据源的配置以及应用。在一些复杂的应用中&#xff0c;…...

DevOps系列文章 : 使用dpkg命令打deb包

创建一个打包的目录&#xff0c;类似rpmbuild&#xff0c;这里创建了目录deb_build mkdir deb_build目标 我有一个hello的二进制文件hello和源码hello.c, 准备安装到/opt/helloworld目录中 步骤 在deb_build目录创建一个文件夹用于存放我的安装文件 mkdir helloworld在he…...

linux sed命令操作大全

经常使用&#xff0c;但有些总记不全&#xff0c;有时候经常查找&#xff0c;这次全部捋清楚做备忘&#xff0c;有需要的小伙伴欢迎收藏起来哦&#xff01; 查、增、改、删一应俱全&#xff0c;非常详细&#xff01; 目录 一、查看 查看第2行 查看第2行到第3行 查看第1行、…...

Vue2+Vue3组件间通信方式汇总(3)------$bus

组件间通信方式是前端必不可少的知识点&#xff0c;前端开发经常会遇到组件间通信的情况&#xff0c;而且也是前端开发面试常问的知识点之一。接下来开始组件间通信方式第三弹------$bus,并讲讲分别在Vue2、Vue3中的表现。 Vue2Vue3组件间通信方式汇总&#xff08;1&#xff09…...

前端基础location的使用

概念 获取当前页面的地址信息&#xff0c;还可以修改某些属性&#xff0c;实现页面跳转和刷新等。 样例展示 window.location 含义.originURL 基础地址&#xff0c;包括协议名、域名和端口号.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字打拍法

前置内容&#xff1a; 文字版 https://note.youdao.com/s/6FSSvGBf &#xff08;顺序参考&#xff1a;下方的视频版里面目录顺序&#xff09; 视频版 【四川音乐学院作曲硕士】教你零基础自学乐理保姆级教学-学习视频教程-腾讯课堂 文字版还有下图红框中三个专栏里的内容&a…...

列表对象的时间进行中文格式化处理

在黑马的项目学习中&#xff0c;如何将前端页面时间显示成2023年12月21日 06:23:23中文形式。 如果你想使用中文格式化日期&#xff0c;你可以将 en-US 更改为 zh-CN&#xff0c;以使用中文语言环境。以下是修改后的代码&#xff1a; result.data.items.forEach(item > {//…...

vi和vim的区别

目录 一、前言 二、vi/vim 的介绍 三、Vi/Vim 常见指令 四、vi和vim的区别 一、前言 写这篇文章的目的&#xff0c;是为了告诉大家我们如果要在终端下对文本进行编辑和修改可以使用vim编辑器。 Ubuntu 自带了 VI 编辑器&#xff0c;但是 VI 编辑器对于习惯了 Windows 下进…...

【昆明*线上同步】最新ChatGPT/GPT4科研实践应用与AI绘图技术及论文高效写作

详情点击查看福利&#xff1a;【昆明*线上同步】最新ChatGPT/GPT4科研实践应用与AI绘图技术及论文高效写作 目标&#xff1a; 1、熟练掌握ChatGPT提示词技巧及各种应用方法&#xff0c;并成为工作中的助手。 2、通过案例掌握ChatGPT撰写、修改论文及工作报告&#xff0c;提供…...

移动开发终极指南:如何利用stb库在Android和iOS平台实现高性能图像处理

移动开发终极指南&#xff1a;如何利用stb库在Android和iOS平台实现高性能图像处理 【免费下载链接】stb stb single-file public domain libraries for C/C 项目地址: https://gitcode.com/GitHub_Trending/st/stb 在移动应用开发中&#xff0c;图像处理是提升用户体验…...

Avalonia11 Canvas拖拽与动态渲染保姆级教程:从MVVM绑定到事件处理完整流程

Avalonia11 Canvas拖拽与动态渲染实战&#xff1a;构建高性能迷你地图导航系统 在复杂的图形界面应用中&#xff0c;迷你地图导航已经成为提升用户体验的标准配置。想象一下&#xff0c;当你在处理一张超大的设计图纸或地图时&#xff0c;如何快速定位到感兴趣的局部区域&#…...

5分钟完成Windows平台Poppler PDF处理工具完整部署指南

5分钟完成Windows平台Poppler PDF处理工具完整部署指南 【免费下载链接】poppler-windows Download Poppler binaries packaged for Windows with dependencies 项目地址: https://gitcode.com/gh_mirrors/po/poppler-windows 在Windows系统上快速部署专业的PDF文档处理…...

创龙T113-i开发板:从SDK解压到镜像打包,一个完整Linux系统构建实录(含80分钟编译避坑)

创龙T113-i开发板实战&#xff1a;从零构建嵌入式Linux系统的完整指南 1. 开发环境准备与SDK解压 第一次接触全志T113-i开发板时&#xff0c;最令人头疼的莫过于搭建开发环境。与常见的树莓派或BeagleBone开发板不同&#xff0c;工业级嵌入式设备往往需要更专业的工具链支持。我…...

Windows 7如何突破Python版本限制?企业级兼容性解决方案指南

Windows 7如何突破Python版本限制&#xff1f;企业级兼容性解决方案指南 【免费下载链接】PythonVista Python 3.9 installers that support Windows 7 SP1 and Windows Server 2008 R2 项目地址: https://gitcode.com/gh_mirrors/py/PythonVista 一、痛点分析&#xff…...

Realistic Vision V5.1 性能调优:针对STM32嵌入式设备图像生成的优化思路探讨

Realistic Vision V5.1 性能调优&#xff1a;针对STM32嵌入式设备图像生成的优化思路探讨 最近在捣鼓一个挺有意思的项目&#xff0c;想把一些前沿的AI图像生成能力&#xff0c;塞进像STM32F103C8T6这种资源极其有限的嵌入式设备里。你可能要问了&#xff0c;这怎么可能&#…...

视频生成技术新范式:Wan2.2如何重新定义AI创作边界

视频生成技术新范式&#xff1a;Wan2.2如何重新定义AI创作边界 【免费下载链接】Wan2.2-Animate-14B 项目地址: https://ai.gitcode.com/hf_mirrors/Wan-AI/Wan2.2-Animate-14B 在数字内容创作领域&#xff0c;视频生成技术正经历着从实验性探索到产业化应用的关键转型…...

利用Timeshift在Linux系统中实现高效系统快照与灾难恢复

1. 为什么你需要Timeshift来保护你的Linux系统 作为一个用了十几年Linux的老用户&#xff0c;我见过太多因为系统崩溃而抓狂的场景。记得有一次在更新内核时突然断电&#xff0c;结果系统直接罢工&#xff0c;那天我花了整整8小时才把环境重新配置好。如果你也遇到过类似情况&a…...

如何通过有效方法提升儿童专注力障碍的注意力集中度?

提升儿童专注力的有效策略与技巧解析 在帮助儿童提高注意力集中度的过程中&#xff0c;首先需要建立一个适合学习的环境。创造一个安静、整洁的学习空间&#xff0c;减少杂音和干扰&#xff0c;有助于孩子更好地专注。此外&#xff0c;开展一些分段学习的小技巧也是非常有效的方…...

别再重复造轮子了!手把手教你用Vue3 + v3layer组件库,5分钟搞定PC端复杂弹窗需求

Vue3弹窗开发实战&#xff1a;从零封装到v3layer高效解决方案 在PC端Web应用开发中&#xff0c;弹窗组件几乎是每个项目都无法绕开的基础功能。从简单的提示框到复杂的交互式面板&#xff0c;弹窗承载着信息展示、用户反馈、表单提交等多样化需求。传统开发模式下&#xff0c;前…...