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 {

    @Resource
    private FileClientProperties fileClientProperties;
    @Resource
    private 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_EMPTY
        if (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.写在最后

本文很笼统的介绍了一下在附件与表单数据分开上传时自己遇到的一些问题,以及自己探索的解决方法,中间的描述有一些可能不是很清楚,也还有遗留问题,后续还会慢慢解决。看到这篇文章的你,如果有任何指教,欢迎私信探讨!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/259266.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

无约束优化问题求解笔记(1)

目录 1. 迭代求解的基本流程与停止准则1.1 迭代求解的基本流程1.2 停止准则1.3 收敛阶 2. 线搜索方法2.1 精确线搜索2.2 非精确搜索**Goldstein 准则****Wolfe 准则** 2.3 线搜索算法的收敛性 1. 迭代求解的基本流程与停止准则 1.1 迭代求解的基本流程 优化问题的解通常无法直…

[总线仲裁]

目录 一. 集中仲裁方式1.1 链式查询方式1.2 计数器查询方式1.3 独立请求方式 二. 分布式仲裁方式 总线仲裁是为了解决多个设备争用总线这个问题 \quad 一. 集中仲裁方式 \quad 集中仲裁方式: 就像是霸道总裁来决定谁先获得总线控制权 分布仲裁方式: 商量着谁先获得总线控制权 …

【六大排序详解】开篇 :插入排序 与 希尔排序

插入排序 与 希尔排序 六大排序之二 插入排序 与 希尔排序1 排序1.1排序的概念 2 插入排序2.1 插入排序原理2.2 排序步骤2.3 代码实现 3 希尔排序3.1 希尔排序原理3.2 排序步骤3.3 代码实现 4 时间复杂度分析 Thanks♪(&#xff65;ω&#xff65;)&#xff89;下一篇文章见&am…

基于ssm高校推免报名系统源码和论文

网络的广泛应用给生活带来了十分的便利。所以把高校推免报名管理与现在网络相结合&#xff0c;利用java技术建设高校推免报名管理系统&#xff0c;实现高校推免报名的信息化。则对于进一步提高高校推免报名管理发展&#xff0c;丰富高校推免报名管理经验能起到不少的促进作用。…

智能优化算法应用:基于蜜獾算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于蜜獾算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于蜜獾算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.蜜獾算法4.实验参数设定5.算法结果6.参考文献7.MA…

2023-12-20 二叉搜索树的最近公共祖先和二叉搜索树中的插入操作和删除二叉搜索树中的节点

235. 二叉搜索树的最近公共祖先 思想&#xff1a;和二叉树的公共最近祖先节点的思路基本一致的&#xff01;就是不用从下往上遍历处理&#xff01;可以利用的二叉搜索树的特点从上往下处理了&#xff01;而且最近公共节点肯定是第一个出现在【q&#xff0c;p】这个区间的内的&…

【已解决】vs2015操作创建声明定义由于以下原因无法完成

本博文解决这样的一个问题&#xff0c;就是vs2015下用qt&#xff0c;在快速创建槽函数时给笔者报了个错误&#xff0c;错误的完整说法是这样子的”操作创建声明/定义“由于下列原因无法完成&#xff0c;所选的文本不包含任何函数签名。第一次遇到这种花里胡哨的问题&#xff0c…

【数据结构】并查集的简单实现,合并,查找(C++)

文章目录 前言举例&#xff1a; 一、1.构造函数2.查找元素属于哪个集合FindRoot3.将两个集合归并成一个集合Union4.查找集合数量SetCount 二、源码 前言 需要将n个不同的元素划分成一些不相交的集合。开始时&#xff0c;每个元素自成一个单元素集合&#xff0c;然后按一定的规…

算法-滑动窗口类型

6666 滑动窗口 1、大小为K的最大和子数组 给定一个数组&#xff0c;找出该数组中所有大小为“K”的连续子数组的平均值。 让我们用实际输入来理解这个问题: Array: [1, 3, 2, 6, -1, 4, 1, 8, 2], K51、对于前5个数字(索引0-4的子数组)&#xff0c;平均值为:(1 3 2 6−…

贝蒂快扫雷~(C语言)

✨✨欢迎大家来到贝蒂大讲堂✨✨ ​​​​&#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;贝蒂的游戏 贝蒂的主页&#xff1a;Betty‘s blog 引言&#xff1a; 扫雷相信大家小时候到玩过吧&#xff0c;那…

Gin之GORM多表关联查询(多对多;自定义预加载SQL)

数据库三个,如下: 注意:配置中间表的时候,表设计层面最好和配置的其他两张表契合,例如其他两张表为fate内的master和slave;要整合其对应关系的话,设计中间表的结构为master_id和slave_id最好(不然会涉及重写外键的操作) 重写外键(介绍) 对于 many2many 关系,连接表…

DBdoctor,致力于解决数据库的一切性能问题

17(一起)&#xff0c;这是我的幸运数字&#xff0c;恰巧今年8月17日在DTCC大会上我们全网首次发布DBdoctor&#xff0c;今天同样是17日&#xff0c;在全网首发整四个月后我们发布重磅大版本V3.1。值此重大更新之际&#xff0c;想与各有识之士深度聊一下这款产品&#xff0c;以及…

【LeetCode刷题】--244.最短单词距离II

244.最短单词距离II 方法&#xff1a;哈希表双指针 class WordDistance {HashMap<String,List<Integer>> map new HashMap<>();public WordDistance(String[] wordsDict) {int len wordsDict.length;for(int i 0;i< len;i){String word wordsDict[i];…

Kafka基本原理及使用

目录 基本概念 单机版 环境准备 基本命令使用 集群版 消息模型 成员组成 1. Topic&#xff08;主题&#xff09;&#xff1a; 2. Partition&#xff08;分区&#xff09;&#xff1a; 3. Producer&#xff08;生产者&#xff09;&#xff1a; 4. Consumer&#xff08;…

2023年12月20日学习总结

今日to do list&#xff1a; 学习kaggle中store sales中的dart forcasting&#x1f3af; 大概搜集一个声纹识别的报告&#xff08;老师给的新项目&#x1f62d;&#xff09; 学习时不刷手机 okkkkkkkkkkkkkk 开始&#x1f44d; 1. 时间序列预测- a complete guide 总结一下这…

Vim:文本编辑的强大利器

Vim&#xff1a;文本编辑的强大利器 概述1. 工作模式1.1 普通模式1.2 插入模式1.3 可视模式 2. 代码示例2.1 移动光标2.2 复制和粘贴2.3 查找和替换 3. 应用场景结语 概述 Vim&#xff08;Vi Improved&#xff09;是一款强大的文本编辑器&#xff0c;广泛应用于Linux和Unix系统…

架构设计到底是什么?

文章目录 架构设计有哪些内容&#xff1f;架构原理与技术认知分布式技术原理与设计中间件常用组件的原理和设计问题数据库原理与设计问题分布式缓存原理与设计问题互联网高性能高可用设计问题 技术认知架构分析问题分析能力边界 架构设计&#xff0c;是中高级研发工程师逃不开的…

LabVIEW开发振动数据分析系统

LabVIEW开发振动数据分析系统 自动测试系统基于LabVIEW平台设计&#xff0c;采用了多种高级硬件设备。系统的硬件组成包括PCB振动加速度传感器&#xff0c;这是一种集成了传统压电加速度传感器和电荷放大器的先进设备&#xff0c;能够直接与采集仪器连接。此外&#xff0c;系统…

教师的职业素养有哪些

教师职业素养的重要性不言而喻。一个优秀的教师不仅需要具备专业知识&#xff0c;还需要具备一些基本的职业素养。 具备高尚的职业道德。作为教育工作者&#xff0c;教师应该以身作则&#xff0c;树立良好的榜样。他们应该尊重学生、关心学生、热爱学生&#xff0c;以自己的言行…

15 使用v-model绑定单选框

概述 使用v-model绑定单选框也比较常见&#xff0c;比如性别&#xff0c;要么是男&#xff0c;要么是女。比如单选题&#xff0c;给出多个选择&#xff0c;但是只能选择其中的一个。 在本节课中&#xff0c;我们演示一下这两种常见的用法。 基本用法 我们创建src/component…