SpringBoot整合minio实现断点续传、分片上传(附源码)
作者:mmseoamin日期:2023-12-14

前言

在Web开发中,大文件上传是必不可少的功能之一。本文将介绍如何使用SpringBoot整合minio实现一个简单的大文件上传网站。

一、项目介绍

项目下载

gitee:https://gitee.com/wusupweilgy/springboot-vue.git

1.项目运行 SpringBoot整合minio实现断点续传、分片上传(附源码),第1张

2.技术栈

前端:vue2、element-ui组件、axios

后端:springboot、minio、mybatis-plus、redis

3.功能

  • 断点续传
  • 分片上传
  • 前端显示上传进度条

    4.流程图

    基本分成3步完成上传,检查是否上传、获取分片上传的url列表和分片上传合并,所以这里我把文件上传分成了三张流程图,就是为了详细点,其实是一个流程。

    检查是否上传

    SpringBoot整合minio实现断点续传、分片上传(附源码),第2张

    获取上传url 

    SpringBoot整合minio实现断点续传、分片上传(附源码),第3张

     分片上传合并SpringBoot整合minio实现断点续传、分片上传(附源码),第4张

     redis主要作用是缓存上传任务,因为如果是断点续传,需要查询上次上传的信息去查询已上传的分片信息,只要上传没有上传的分片即可。

    mysql主要作用是存储已完成上传的文件信息。如果下次上传的是同一个文件,数据库中存在就不用上传,直接返回数据库中的文件信息。

    二、前端实现

    1.spark-md5 主要用来计算文件MD5,安装命令:

    npm install spark-md5 --S

    2.FileView.vue

    
    
    

    3.FileUtil.js(判断文件类型)

    /**
     * @param: fileName - 文件名称
     * @param: 数据返回 1) 无后缀匹配 - false
     * @param: 数据返回 2) 匹配图片 - image
     * @param: 数据返回 3) 匹配 txt - txt
     * @param: 数据返回 4) 匹配 excel - excel
     * @param: 数据返回 5) 匹配 word - word
     * @param: 数据返回 6) 匹配 pdf - pdf
     * @param: 数据返回 7) 匹配 ppt - ppt
     * @param: 数据返回 8) 匹配 视频 - video
     * @param: 数据返回 9) 匹配 音频 - radio
     * @param: 数据返回 10) 其他匹配项 - other
     * @author: ljw
     **/
    export function fileSuffixTypeUtil(fileName){
        // 后缀获取
        var suffix = "";
        // 获取类型结果
        var result = "";
        try {
            var flieArr = fileName.split(".");
            suffix = flieArr[flieArr.length - 1];
        } catch (err) {
            suffix = "";
        }
        // fileName无后缀返回 false
        if (!suffix) {
            result = false;
            return result;
        }
        // 图片格式
        var imglist = ["png", "jpg", "jpeg", "bmp", "gif"];
        // 进行图片匹配
        result = imglist.some(function (item) {
            return item == suffix;
        });
        if (result) {
            result = "image";
            return result;
        }
        // 匹配txt
        var txtlist = ["txt"];
        result = txtlist.some(function (item) {
            return item == suffix;
        });
        if (result) {
            result = "txt";
            return result;
        }
        // 匹配 excel
        var excelist = ["xls", "xlsx"];
        result = excelist.some(function (item) {
            return item == suffix;
        });
        if (result) {
            result = "excel";
            return result;
        }
        // 匹配 word
        var wordlist = ["doc", "docx"];
        result = wordlist.some(function (item) {
            return item == suffix;
        });
        if (result) {
            result = "word";
            return result;
        }
        // 匹配 pdf
        var pdflist = ["pdf"];
        result = pdflist.some(function (item) {
            return item == suffix;
        });
        if (result) {
            result = "pdf";
            return result;
        }
        // 匹配 ppt
        var pptlist = ["ppt"];
        result = pptlist.some(function (item) {
            return item == suffix;
        });
        if (result) {
            result = "ppt";
            return result;
        }
        // 匹配 视频
        var videolist = ["mp4", "m2v", "mkv","ogg", "flv", "avi", "wmv", "rmvb"];
        result = videolist.some(function (item) {
            return item == suffix;
        });
        if (result) {
            result = "video";
            return result;
        }
        // 匹配 音频
        var radiolist = ["mp3", "wav", "wmv"];
        result = radiolist.some(function (item) {
            return item == suffix;
        });
        if (result) {
            result = "radio";
            return result;
        }
        // 其他 文件类型
        result = "other";
        return result;
    };
    

    4.upload.js(请求后端方法)

    import request from '@/utils/request'
    //上传信息
    export function uploadScreenshot(data){
        return request({
            url:'upload/multipart/uploadScreenshot',
            method:'post',
            data
        })
    }
    //上传信息
    export function uploadFileInfo(data){
        return request({
            url:'upload/multipart/uploadFileInfo',
            method:'post',
            data
        })
    }
    // 上传校验
    export function checkUpload(MD5) {
        return request({
            url: `upload/multipart/check?md5=${MD5}`,
            method: 'get',
        })
    };
    // 初始化上传
    export function initUpload(data) {
        return request({
            url: `upload/multipart/init`,
            method: 'post',
            data
        })
    };
    // 初始化上传
    export function mergeUpload(data) {
        return request({
            url: `upload/multipart/merge`,
            method: 'post',
            data
        })
    };
    

    5.request.js

    import axios from 'axios'
    import router from "@/router";
    import ElementUI from "element-ui";
    const request = axios.create({
        baseURL: `http://localhost:9090`,
        timeout: 30000
    })
    // request 拦截器
    // 可以自请求发送前对请求做一些处理
    // 比如统一加token,对请求参数统一加密
    request.interceptors.request.use(config => {
        config.headers['Content-Type'] = 'application/json;charset=utf-8';
        return config
    }, error => {
        return Promise.reject(error)
    });
    // response 拦截器
    // 可以在接口响应后统一处理结果
    request.interceptors.response.use(
        response => {
            let res = response.data;
            // 如果是返回的文件
            if (response.headers === 'blob') {
                return res
            }
            // 兼容服务端返回的字符串数据
            if (typeof res === 'string') {
                res = res ? JSON.parse(res) : res
                console.log(res)
            }
           
            return res;
        },
        error => {
            console.log('err' + error) // for debug
            return Promise.reject(error)
        }
    )
    export default request
    

    三、后端实现

    1.数据库

    SpringBoot整合minio实现断点续传、分片上传(附源码),第5张

    /*
     Navicat Premium Data Transfer
     Source Server         : local
     Source Server Type    : MySQL
     Source Server Version : 80028
     Source Host           : localhost:3306
     Source Schema         : shiro_jwt_vue_file
     Target Server Type    : MySQL
     Target Server Version : 80028
     File Encoding         : 65001
     Date: 01/05/2023 13:52:06
    */
    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    -- ----------------------------
    -- Table structure for files
    -- ----------------------------
    DROP TABLE IF EXISTS `files`;
    CREATE TABLE `files`  (
      `id` int(0) NOT NULL AUTO_INCREMENT COMMENT 'id',
      `upload_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '分片上传uploadId',
      `file_md5` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件md5',
      `url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '下载链接',
      `file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件名称',
      `bucket_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '桶名',
      `file_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件类型',
      `file_size` bigint(0) NULL DEFAULT NULL COMMENT '文件大小(byte)',
      `chunk_size` bigint(0) NULL DEFAULT NULL COMMENT '每个分片的大小(byte)',
      `chunk_num` int(0) NULL DEFAULT NULL COMMENT '分片数量',
      `is_delete` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除',
      `enable` tinyint(1) NULL DEFAULT 1 COMMENT '是否禁用链接',
      `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
      `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 262 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
    SET FOREIGN_KEY_CHECKS = 1;
    

    2.依赖pom.xml

    
            
                org.springframework.boot
                spring-boot-starter-web
            
            
                org.projectlombok
                lombok
                true
            
            
                com.alibaba
                fastjson
                1.2.50
            
            
                com.baomidou
                mybatis-plus-boot-starter
                3.5.2
            
            
                mysql
                mysql-connector-java
                8.0.30
            
            
                io.minio
                minio
                8.3.1
            
            
                com.squareup.okhttp3
                okhttp
                4.9.2
            
            
                org.springframework.boot
                spring-boot-starter-data-redis
            
            
                cn.hutool
                hutool-all
                5.7.20
            
            
            
                org.springframework.boot
                spring-boot-starter-validation
                
            
            
                org.apache.commons
                commons-lang3
            
            
                org.springframework.boot
                spring-boot-starter-test
                test
            
        
        
            
                
                    org.springframework.boot
                    spring-boot-dependencies
                    ${spring-boot.version}
                    pom
                    import
                
            

    3.application.yml

    server:
      port: 9090
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/shiro_jwt_vue_file?serverTimezone=Asia/Shanghai&userUnicode=true&useSSL=false
        username: root
        password: mysql
      redis:
        # Redis服务器地址
        host: 127.0.0.1
        # Redis服务器端口号
        port: 6379
        # 使用的数据库索引,默认是0
        database: 0
        # 连接超时时间
        timeout: 1800000
        # 设置密码
        #password: "123456"
        lettuce:
          pool:
            # 最大阻塞等待时间,负数表示没有限制
            max-wait: -1
            # 连接池中的最大空闲连接
            max-idle: 5
            # 连接池中的最小空闲连接
            min-idle: 0
            # 连接池中最大连接数,负数表示没有限制
            max-active: 20
      jackson:
        date-format: yyyy-MM-dd HH:mm:ss
        time-zone: GMT+8
    mybatis-plus:
      mapper-locations: classpath:mapper/*.xml
      configuration:
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    minio:
      endpoint: http://localhost:9000
      accesskey: minioadmin
      secretkey: minioadmin
      expiry: 1  #分片对象过期时间 单位(天)
      breakpoint-time: 1 #断点续传有效时间,在redis存储任务的时间 单位(天)

    4.controller文件上传

    package com.wusuowei.miniouploadfile.controller;
    import com.wusuowei.miniouploadfile.model.vo.FileUploadInfo;
    import com.wusuowei.miniouploadfile.service.UploadService;
    import com.wusuowei.miniouploadfile.utils.MinioUtils;
    import com.wusuowei.miniouploadfile.utils.R;
    import com.wusuowei.miniouploadfile.utils.RedisUtil;
    import com.wusuowei.miniouploadfile.utils.RespEnum;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.web.bind.annotation.*;
    import org.springframework.web.multipart.MultipartFile;
    import javax.annotation.Resource;
    /**
     * minio上传流程
     *
     * 1.检查数据库中是否存在上传文件
     *
     * 2.根据文件信息初始化,获取分片预签名url地址,前端根据url地址上传文件
     *
     * 3.上传完成后,将分片上传的文件进行合并
     *
     * 4.保存文件信息到数据库
     */
    @RestController
    @Slf4j
    @RequestMapping("upload")
    public class FileMinioController {
        @Resource
        private UploadService uploadService;
        @Resource
        private RedisUtil redisUtil;
        @Resource
        private MinioUtils minioUtils;
        /**
         * @description 获取上传文件
         * @param fileMD5 文件md5
         * @return {@link R }
         * @author LGY
         * @date 2023/04/26 16:00
         */
        @GetMapping("/getUploadingFile/{fileMD5}")
        public R getUploadingFile(@PathVariable String fileMD5) {
            if (StringUtils.isBlank(fileMD5)) {
                return R.error();
            }
            FileUploadInfo fileUploadInfo = (FileUploadInfo) redisUtil.get(fileMD5);
            if (fileUploadInfo != null) {
                // 查询上传后的分片数据
                fileUploadInfo.setChunkUploadedList(minioUtils.getChunkByFileMD5(fileUploadInfo.getFileName(), fileUploadInfo.getUploadId(), fileUploadInfo.getFileType()));
                return R.ok().setData(fileUploadInfo);
            }
            return R.error();
        }
        /**
         * 校验文件是否存在
         *
         * @param md5 String
         * @return ResponseResult
         */
        @GetMapping("/multipart/check")
        public R checkFileUploadedByMd5(@RequestParam("md5") String md5) {
            log.info("REST: 通过查询 <{}> 文件是否存在、是否进行断点续传", md5);
            if (StringUtils.isEmpty(md5)) {
                log.error("查询文件是否存在、入参无效");
                return R.error(RespEnum.ACCESS_PARAMETER_INVALID);
            }
            return uploadService.getByFileMD5(md5);
        }
        /**
         * 分片初始化
         *
         * @param fileUploadInfo 文件信息
         * @return ResponseResult
         */
        @PostMapping("/multipart/init")
        public R initMultiPartUpload(@RequestBody FileUploadInfo fileUploadInfo) {
            log.info("REST: 通过 <{}> 初始化上传任务", fileUploadInfo);
            return R.ok().setData(uploadService.initMultiPartUpload(fileUploadInfo));
        }
        /**
         * 完成上传
         *
         * @param fileUploadInfo  文件信息
         * @return ResponseResult
         */
        @PostMapping("/multipart/merge")
        public R completeMultiPartUpload(@RequestBody FileUploadInfo fileUploadInfo) {
            log.info("REST: 通过 {} 合并上传任务", fileUploadInfo);
            //合并文件
            String url = uploadService.mergeMultipartUpload(fileUploadInfo);
            //获取上传文件地址
            if (StringUtils.isNotBlank(url)) {
                return R.ok().setData(url);
            }
            return R.error();
        }
        @PostMapping("/multipart/uploadScreenshot")
        public R uploaduploadScreenshot(@RequestPart("photos") MultipartFile[] photos,
                                        @RequestParam("buckName") String buckName) {
            log.info("REST: 上传文件信息 <{}> ", photos);
            for (MultipartFile photo : photos) {
                if (!photo.isEmpty()) {
                    uploadService.upload(photo, buckName);
                }
            }
            return R.ok();
        }
        @RequestMapping("/createBucket")
        public void createBucket(@RequestParam("bucketName") String bucketName) {
            String bucket = minioUtils.createBucket(bucketName);
        }
    }
     
    

    5.UploadService

    package com.wusuowei.miniouploadfile.service;
    import com.wusuowei.miniouploadfile.model.vo.FileUploadInfo;
    import com.wusuowei.miniouploadfile.utils.R;
    import org.springframework.web.multipart.MultipartFile;
    import java.util.Map;
    public interface UploadService {
        /**
         * 分片上传初始化
         *
         * @param fileUploadInfo
         * @return Map
         */
        Map initMultiPartUpload(FileUploadInfo fileUploadInfo);
        /**
         * 完成分片上传
         *
         * @param  fileUploadInfo
         * @return String
         */
        String mergeMultipartUpload(FileUploadInfo fileUploadInfo);
        /**
         *  通过 md5 获取已上传的数据
         * @param md5 String
         * @return Mono>
         */
        R getByFileMD5(String md5);
        /**
         *  获取文件地址
         * @param bucketName
         * @param fileName
         *
         */
        String getFliePath(String bucketName, String fileName);
        /**
         * 单文件上传
         * @param file
         * @param bucketName
         * @return
         */
        String upload(MultipartFile file, String bucketName);
    }

    6.UploadServiceImpl

    package com.wusuowei.miniouploadfile.service.impl;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.wusuowei.miniouploadfile.mapper.FilesMapper;
    import com.wusuowei.miniouploadfile.model.vo.FileUploadInfo;
    import com.wusuowei.miniouploadfile.model.po.Files;
    import com.wusuowei.miniouploadfile.service.UploadService;
    import com.wusuowei.miniouploadfile.utils.MinioUtils;
    import com.wusuowei.miniouploadfile.utils.R;
    import com.wusuowei.miniouploadfile.utils.RedisUtil;
    import com.wusuowei.miniouploadfile.utils.RespEnum;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.BeanUtils;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    import org.springframework.web.multipart.MultipartFile;
    import javax.annotation.Resource;
    import java.util.List;
    import java.util.Map;
    @Slf4j
    @Service
    public class UploadServiceImpl implements UploadService {
        @Resource
        private FilesMapper filesMapper;
        @Resource
        private MinioUtils minioUtils;
        @Resource
        private RedisUtil redisUtil;
        @Value("${minio.breakpoint-time}")
        private Integer breakpointTime;
        /**
         * 通过 md5 获取已上传的数据(断点续传)
         *
         * @param md5 String
         * @return Mono>
         */
        @Override
        public R getByFileMD5(String md5) {
            log.info("tip message: 通过 <{}> 查询redis是否存在", md5);
            // 从redis获取文件名称和id
            FileUploadInfo fileUploadInfo = (FileUploadInfo) redisUtil.get(md5);
            if (fileUploadInfo != null) {
                // 正在上传,查询上传后的分片数据
                List chunkList = minioUtils.getChunkByFileMD5(fileUploadInfo.getFileName(), fileUploadInfo.getUploadId(), fileUploadInfo.getFileType());
                fileUploadInfo.setChunkUploadedList(chunkList);
                return R.ok(RespEnum.UPLOADING).setData(fileUploadInfo);
            }
            log.info("tip message: 通过 <{}> 查询mysql是否存在", md5);
            // 查询数据库是否上传成功
            Files one = filesMapper.selectOne(new LambdaQueryWrapper().eq(Files::getFileMd5, md5));
            if (one != null) {
                FileUploadInfo mysqlsFileUploadInfo = new FileUploadInfo();
                BeanUtils.copyProperties(one, mysqlsFileUploadInfo);
                return R.ok(RespEnum.UPLOADSUCCESSFUL).setData(mysqlsFileUploadInfo);
            }
            return R.ok(RespEnum.NOT_UPLOADED);
        }
        /**
         * 文件分片上传
         *
         * @param fileUploadInfo
         * @return Mono>
         */
        @Override
        public Map initMultiPartUpload(FileUploadInfo fileUploadInfo) {
            FileUploadInfo redisFileUploadInfo = (FileUploadInfo) redisUtil.get(fileUploadInfo.getFileMd5());
            if (redisFileUploadInfo != null) {
                fileUploadInfo = redisFileUploadInfo;
            }
            log.info("tip message: 通过 <{}> 开始初始化<分片上传>任务", fileUploadInfo);
            // 获取桶
            String bucketName = minioUtils.getBucketName(fileUploadInfo.getFileType());
            // 单文件上传
            if (fileUploadInfo.getChunkNum() == 1) {
                log.info("tip message: 当前分片数量 <{}> 进行单文件上传", fileUploadInfo.getChunkNum());
                saveFileToDB(fileUploadInfo);
                return minioUtils.getUploadObjectUrl(fileUploadInfo.getFileName(), bucketName);
            }
            // 分片上传
            else {
                log.info("tip message: 当前分片数量 <{}> 进行分片上传", fileUploadInfo.getChunkNum());
                Map map = minioUtils.initMultiPartUpload(fileUploadInfo, fileUploadInfo.getFileName(), fileUploadInfo.getChunkNum(), fileUploadInfo.getContentType(), bucketName);
                String uploadId = (String) map.get("uploadId");
                fileUploadInfo.setUploadId(uploadId);
                redisUtil.set(fileUploadInfo.getFileMd5(),fileUploadInfo,breakpointTime*60*60*24);
                return map;
            }
        }
        /**
         * 文件合并
         *
         * @param
         * @return String
         */
        @Override
        public String mergeMultipartUpload(FileUploadInfo fileUploadInfo) {
            log.info("tip message: 通过 <{}> 开始合并<分片上传>任务", fileUploadInfo);
            FileUploadInfo redisFileUploadInfo = (FileUploadInfo) redisUtil.get(fileUploadInfo.getFileMd5());
            if(redisFileUploadInfo!=null){
                fileUploadInfo.setFileName(redisFileUploadInfo.getFileName());
            }
            boolean result = minioUtils.mergeMultipartUpload(fileUploadInfo.getFileName(), fileUploadInfo.getUploadId(), fileUploadInfo.getFileType());
            //合并成功
            if (result) {
                //存入数据库
                Files files = saveFileToDB(fileUploadInfo);
                redisUtil.del(fileUploadInfo.getFileMd5());
                return files.getUrl();
            }
            return null;
        }
        @Override
        public String getFliePath(String bucketName, String fileName) {
            return minioUtils.getFliePath(bucketName, fileName);
        }
        @Override
        public String upload(MultipartFile file, String bucketName) {
            minioUtils.upload(file, bucketName);
            return getFliePath(bucketName, file.getName());
        }
        private Files saveFileToDB(FileUploadInfo fileUploadInfo) {
            String url = this.getFliePath(fileUploadInfo.getFileType().toLowerCase(), fileUploadInfo.getFileName());
            //存入数据库
            Files files = new Files();
            BeanUtils.copyProperties(fileUploadInfo, files);
            files.setBucketName(fileUploadInfo.getFileType());
            files.setUrl(url);
            filesMapper.insert(files);
            return files;
        }
    }

    7.FilesMapper

    package com.wusuowei.miniouploadfile.mapper;
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.wusuowei.miniouploadfile.model.po.Files;
    /**
     * 

    * Mapper 接口 *

    * * @author LGY */ public interface FilesMapper extends BaseMapper { }

    8.Files、FileUploadInfo实体类

    package com.wusuowei.miniouploadfile.model.po;
    import com.baomidou.mybatisplus.annotation.*;
    import lombok.Data;
    import java.io.Serializable;
    import java.time.LocalDateTime;
    /**
     * 

    * *

    * * @author LGY */ @Data @TableName("files") public class Files implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Integer id; @TableField("upload_id") private String uploadId; @TableField("file_md5") private String fileMd5; @TableField("url") private String url; @TableField("file_name") private String fileName; @TableField("bucket_name") private String bucketName; @TableField("file_type") private String fileType; @TableField("file_size") private Long fileSize; @TableField("chunk_size") private Long chunkSize; @TableField("chunk_num") private Integer chunkNum; @TableField("is_delete") @TableLogic(value = "0",delval = "1") private Boolean isDelete; @TableField("enable") private Boolean enable; @TableField(value = "create_time", fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }
    package com.wusuowei.miniouploadfile.model.vo;
    import lombok.Data;
    import lombok.experimental.Accessors;
    import javax.validation.constraints.NotBlank;
    import javax.validation.constraints.NotNull;
    import java.util.List;
    @Data
    @Accessors(chain = true)
    public class FileUploadInfo {
        @NotBlank(message = "文件名不能为空")
        private String fileName;
        @NotNull(message = "文件大小不能为空")
        private Long fileSize;
        @NotBlank(message = "Content-Type不能为空")
        private String contentType;
        @NotNull(message = "分片数量不能为空")
        private Integer chunkNum;
        @NotBlank(message = "uploadId 不能为空")
        private String uploadId;
        private Long chunkSize;
        // 桶名称
        //private String bucketName;
        //md5
        private String fileMd5;
        //文件类型
        private String fileType;
        //已上传的分片索引+1
        private List chunkUploadedList;
    }
    

    9.MinioUtils

    package com.wusuowei.miniouploadfile.utils;
    import cn.hutool.core.text.CharSequenceUtil;
    import cn.hutool.core.util.StrUtil;
    import com.google.common.collect.HashMultimap;
    import com.wusuowei.miniouploadfile.config.CustomMinioClient;
    import com.wusuowei.miniouploadfile.model.vo.FileUploadInfo;
    import io.minio.*;
    import io.minio.errors.*;
    import io.minio.http.Method;
    import io.minio.messages.Part;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    import org.springframework.web.multipart.MultipartFile;
    import javax.annotation.PostConstruct;
    import java.io.IOException;
    import java.io.InputStream;
    import java.security.InvalidKeyException;
    import java.security.NoSuchAlgorithmException;
    import java.util.*;
    import java.util.concurrent.TimeUnit;
    import java.util.stream.Collectors;
    @Slf4j
    @Component
    public class MinioUtils {
        @Value(value = "${minio.endpoint}")
        private String endpoint;
        @Value(value = "${minio.accesskey}")
        private String accesskey;
        @Value(value = "${minio.secretkey}")
        private String secretkey;
        @Value(value = "${minio.expiry}")
        private Integer expiry;
        private CustomMinioClient customMinioClient;
        /**
         * 用spring的自动注入会注入失败
         */
        @PostConstruct
        public void init() {
            MinioClient minioClient = MinioClient.builder()
                    .endpoint(endpoint)
                    .credentials(accesskey, secretkey)
                    .build();
            customMinioClient = new CustomMinioClient(minioClient);
        }
        /**
         * 单文件签名上传
         *
         * @param objectName 文件全路径名称
         * @param bucketName 桶名称
         * @return /
         */
        public Map getUploadObjectUrl(String objectName, String bucketName) {
            try {
                log.info("tip message: 通过 <{}-{}> 开始单文件上传", objectName, bucketName);
                Map resMap = new HashMap();
                List partList = new ArrayList<>();
                String url = customMinioClient.getPresignedObjectUrl(
                        GetPresignedObjectUrlArgs.builder()
                                .method(Method.PUT)
                                .bucket(bucketName)
                                .object(objectName)
                                .expiry(expiry, TimeUnit.DAYS)
                                .build());
                log.info("tip message: 单个文件上传、成功");
                partList.add(url);
                resMap.put("uploadId", "SingleFileUpload");
                resMap.put("urlList", partList);
                return resMap;
            } catch (Exception e) {
                log.error("error message: 单个文件上传失败、原因:", e);
                // 返回 文件上传失败
                return null;
            }
        }
        /**
         * 初始化分片上传
         *
         * @param fileUploadInfo
         * @param objectName     文件全路径名称
         * @param chunkNum      分片数量
         * @param contentType    类型,如果类型使用默认流会导致无法预览
         * @param bucketName     桶名称
         * @return Mono>
         */
        public Map initMultiPartUpload(FileUploadInfo fileUploadInfo, String objectName, int chunkNum, String contentType, String bucketName) {
            log.info("tip message: 通过 <{}-{}-{}-{}> 开始初始化<分片上传>数据", objectName, chunkNum, contentType, bucketName);
            Map resMap = new HashMap<>();
            try {
                if (CharSequenceUtil.isBlank(contentType)) {
                    contentType = "application/octet-stream";
                }
                HashMultimap headers = HashMultimap.create();
                headers.put("Content-Type", contentType);
                //获取uploadId
                String uploadId = null;
                if(StringUtils.isBlank(fileUploadInfo.getUploadId())){
                    uploadId = customMinioClient.initMultiPartUpload(bucketName, null, objectName, headers, null);
                }else{
                    uploadId = fileUploadInfo.getUploadId();
                }
                resMap.put("uploadId", uploadId);
                fileUploadInfo.setUploadId(uploadId);
                fileUploadInfo.setChunkNum(chunkNum);
                List partList = new ArrayList<>();
                Map reqParams = new HashMap<>();
                reqParams.put("uploadId", uploadId);
                for (int i = 1; i <= chunkNum; i++) {
                    reqParams.put("partNumber", String.valueOf(i));
                    String uploadUrl = customMinioClient.getPresignedObjectUrl(
                            GetPresignedObjectUrlArgs.builder()
                                    .method(Method.PUT)
                                    .bucket(bucketName)
                                    .object(objectName)
                                    .expiry(1, TimeUnit.DAYS)
                                    .extraQueryParams(reqParams)
                                    .build());
                    partList.add(uploadUrl);
                }
                log.info("tip message: 文件初始化<分片上传>、成功");
                resMap.put("urlList", partList);
                return resMap;
            } catch (Exception e) {
                log.error("error message: 初始化分片上传失败、原因:", e);
                // 返回 文件上传失败
                return R.error(RespEnum.UPLOAD_FILE_FAILED);
            }
        }
        /**
         * 分片上传完后合并
         *
         * @param objectName 文件全路径名称
         * @param uploadId   返回的uploadId
         * @param bucketName 桶名称
         * @return boolean
         */
        public boolean mergeMultipartUpload(String objectName, String uploadId, String bucketName) {
            try {
                log.info("tip message: 通过 <{}-{}-{}> 合并<分片上传>数据", objectName, uploadId, bucketName);
                //目前仅做了最大1000分片
                Part[] parts = new Part[1000];
                // 查询上传后的分片数据
                ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);
                int partNumber = 1;
                for (Part part : partResult.result().partList()) {
                    parts[partNumber - 1] = new Part(partNumber, part.etag());
                    partNumber++;
                }
                // 合并分片
                customMinioClient.mergeMultipartUpload(bucketName, null,objectName, uploadId, parts, null, null);
            } catch (Exception e) {
                log.error("error message: 合并失败、原因:", e);
                //TODO删除redis的数据
                return false;
            }
            return true;
        }
        /**
         * 通过 sha256 获取上传中的分片信息
         *
         * @param objectName 文件全路径名称
         * @param uploadId   返回的uploadId
         * @param bucketName 桶名称
         * @return Mono>
         */
        public List getChunkByFileMD5(String objectName, String uploadId, String bucketName) {
            log.info("通过 <{}-{}-{}> 查询上传分片数据", objectName, uploadId, bucketName);
            try {
                // 查询上传后的分片数据
                ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);
                return partResult.result().partList().stream().map(Part::partNumber).collect(Collectors.toList());
            } catch (Exception e) {
                log.error("error message: 查询上传后的分片信息失败、原因:", e);
                return null;
            }
        }
        /**
         * 获取文件下载地址
         *
         * @param bucketName 桶名称
         * @param fileName   文件名
         * @return
         */
        public String getFliePath(String bucketName, String fileName) {
            return StrUtil.format("{}/{}/{}", endpoint, bucketName, fileName);//文件访问路径
        }
        /**
         * 创建一个桶
         *
         * @return
         */
        public String createBucket(String bucketName) {
            try {
                BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build();
                //如果桶存在
                if (customMinioClient.bucketExists(bucketExistsArgs)) {
                    return bucketName;
                }
                MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucketName).build();
                customMinioClient.makeBucket(makeBucketArgs);
                return bucketName;
            } catch (Exception e) {
                log.error("创建桶失败:{}", e.getMessage());
                throw new RuntimeException(e);
            }
        }
        /**
         * 根据文件类型获取minio桶名称
         *
         * @param fileType
         * @return
         */
        public String getBucketName(String fileType) {
            try {
                //String bucketName = getProperty(fileType.toLowerCase());
                if (fileType != null && !fileType.equals("")) {
                    //判断桶是否存在
                    String bucketName2 = createBucket(fileType.toLowerCase());
                    if (bucketName2 != null && !bucketName2.equals("")) {
                        return bucketName2;
                    }else{
                        return  fileType;
                    }
                }
            } catch (Exception e) {
                log.error("Error reading bucket name ");
            }
            return fileType;
        }
        /**
         * 读取配置文件
         *
         * @param fileType
         * @return
         * @throws IOException
         */
        private String getProperty(String fileType) throws IOException {
            Properties SysLocalPropObject = new Properties();
            //判断桶关系配置文件是否为空
            if (SysLocalPropObject.isEmpty()) {
                InputStream is = getClass().getResourceAsStream("/BucketRelation.properties");
                SysLocalPropObject.load(is);
                is.close();
            }
            return SysLocalPropObject.getProperty("bucket." + fileType);
        }
        /**
         * 文件上传
         *
         * @param file 文件
         * @return Boolean
         */
        public String upload(MultipartFile file, String bucketName) {
            String originalFilename = file.getOriginalFilename();
            if (StringUtils.isBlank(originalFilename)) {
                throw new RuntimeException();
            }
            String objectName = file.getName();
            try {
                PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(bucketName).object(objectName)
                        .stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build();
                //文件名称相同会覆盖
                customMinioClient.putObject(objectArgs);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
            // 查看文件地址
            GetPresignedObjectUrlArgs build = new GetPresignedObjectUrlArgs().builder().bucket(bucketName).object(objectName).method(Method.GET).build();
            String url = null;
            try {
                url = customMinioClient.getPresignedObjectUrl(build);
            } catch (ErrorResponseException e) {
                e.printStackTrace();
            } catch (InsufficientDataException e) {
                e.printStackTrace();
            } catch (InternalException e) {
                e.printStackTrace();
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            } catch (InvalidResponseException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (XmlParserException e) {
                e.printStackTrace();
            } catch (ServerException e) {
                e.printStackTrace();
            }
            return url;
        }
    //    /**
    //     * 写入配置文件
    //     */
    //    public void setProperty(String bucketName) {
    //        String tempPath = Objects.requireNonNull(getClass().getResource("/BucketRelation.properties")).getPath();
    //        OutputStream os;
    //        try {
    //            os = new FileOutputStream(tempPath);
    //            SysLocalPropObject.setProperty(bucketName, bucketName);
    //            SysLocalPropObject.store(os, "Update " + bucketName + " " + bucketName);
    //            os.close();
    //        } catch (IOException e) {
    //        }
    //    }
    }
    //    @Autowired
    //    private MinioProp minioProp;
    //
    //
    //    @Autowired
    //    private MinioClient minioClient;
    //
    //
    //
    //    /**
    //     * 列出所有的桶
    //     */
    //    public List listBuckets() throws Exception {
    //        List list = minioClient.listBuckets();
    //        List names = new ArrayList<>();
    //        list.forEach(b -> {
    //            names.add(b.name());
    //        });
    //        return names;
    //    }
    //
    //    /**
    //     * 列出一个桶中的所有文件和目录
    //     */
    //    public List listFiles(String bucket) throws Exception {
    //        Iterable> results = minioClient.listObjects(
    //                ListObjectsArgs.builder().bucket(bucket).recursive(true).build());
    //
    //        List infos = new ArrayList<>();
    //        results.forEach(r->{
    //            Fileinfo info = new Fileinfo();
    //            try {
    //                Item item = r.get();
    //                info.setFilename(item.objectName());
    //                info.setDirectory(item.isDir());
    //                infos.add(info);
    //            } catch (Exception e) {
    //                e.printStackTrace();
    //            }
    //        });
    //        return infos;
    //    }
    //
    //    /**
    //     * 下载一个文件
    //     */
    //    public InputStream download(String bucket, String objectName) throws Exception {
    //        InputStream stream = minioClient.getObject(
    //                GetObjectArgs.builder().bucket(bucket).object(objectName).build());
    //        return stream;
    //    }
    //
    //    /**
    //     * 删除一个桶
    //     */
    //    public void deleteBucket(String bucket) throws Exception {
    //        minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucket).build());
    //    }
    //
    //    /**
    //     * 删除一个对象
    //     */
    //    public void deleteObject(String bucket, String objectName) throws Exception {
    //        minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(objectName).build());
    //    }
    //
    //
    //    /**
    //     * 创建一个桶
    //     */
    //    public void createBucket(String bucketName) {
    //        BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build();
    //        MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucketName).build();
    //        try {
    //            if (minioClient.bucketExists(bucketExistsArgs))
    //                return;
    //            minioClient.makeBucket(makeBucketArgs);
    //        } catch (Exception e) {
    //            log.error("创建桶失败:{}", e.getMessage());
    //            throw new RuntimeException(e);
    //        }
    //    }
    //
    //    /**
    //     * 上传一个文件
    //     * @param file       文件
    //     * @param bucketName 存储桶
    //     * @return
    //     */
    //    public JSONObject uploadFile(MultipartFile file, String bucketName) throws Exception {
    //        JSONObject res = new JSONObject();
    //        res.put("code", 0);
    //        // 判断上传文件是否为空
    //        if (null == file || 0 == file.getSize()) {
    //            res.put("msg", "上传文件不能为空");
    //            return res;
    //        }
    //        // 判断存储桶是否存在
    //        createBucket(bucketName);
    //        // 文件名
    //        String originalFilename = file.getOriginalFilename();
    //        // 新的文件名 = 存储桶名称_时间戳.后缀名
    //        String fileName = bucketName + "_" + System.currentTimeMillis() +                              									originalFilename.substring(originalFilename.lastIndexOf("."));
    //        // 开始上传
    //        InputStream inputStream = file.getInputStream();
    //        PutObjectArgs args = PutObjectArgs.builder().bucket(bucketName).object(fileName)
    //                .stream(inputStream,inputStream.available(),-1).build();
    //        minioClient.putObject(args);
    //        res.put("code", 1);
    //        res.put("msg", minioProp.getEndpoint() + "/" + bucketName + "/" + fileName);
    //        return res;
    //    }

    10.RedisUtil

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    import org.springframework.util.CollectionUtils;
    import java.util.Collection;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import java.util.concurrent.TimeUnit;
    @Component
    public class RedisUtil {
        @Autowired
        private RedisTemplate redisTemplate;
        /**
         * 指定缓存失效时间
         * @param key 键
         * @param time 时间(秒)
         * @return
         */
        public boolean expire(String key, long time) {
            try {
                if (time > 0) {
                    redisTemplate.expire(key, time, TimeUnit.SECONDS);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 根据key 获取过期时间
         * @param key 键 不能为null
         * @return 时间(秒) 返回0代表为永久有效
         */
        public long getExpire(String key) {
            return redisTemplate.getExpire(key, TimeUnit.SECONDS);
        }
        /**
         * 判断key是否存在
         * @param key 键
         * @return true 存在 false不存在
         */
        public boolean hasKey(String key) {
            try {
                return redisTemplate.hasKey(key);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 删除缓存
         * @param key 可以传一个值 或多个
         */
        @SuppressWarnings("unchecked")
        public void del(String... key) {
            if (key != null && key.length > 0) {
                if (key.length == 1) {
                    redisTemplate.delete(key[0]);
                } else {
                    redisTemplate.delete((Collection) CollectionUtils.arrayToList(key));
                }
            }
        }
        // ============================String=============================
        /**
         * 普通缓存获取
         * @param key 键
         * @return 值
         */
        public Object get(String key) {
            return key == null ? null : redisTemplate.opsForValue().get(key);
        }
        /**
         * 普通缓存放入
         * @param key 键
         * @param value 值
         * @return true成功 false失败
         */
        public boolean set(String key, Object value) {
            try {
                redisTemplate.opsForValue().set(key, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 普通缓存放入并设置时间
         * @param key 键
         * @param value 值
         * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
         * @return true成功 false 失败
         */
        public boolean set(String key, Object value, long time) {
            try {
                if (time > 0) {
                    redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
                } else {
                    set(key, value);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 递增
         * @param key 键
         * @param delta 要增加几(大于0)
         * @return
         */
        public long incr(String key, long delta) {
            if (delta < 0) {
                throw new RuntimeException("递增因子必须大于0");
            }
            return redisTemplate.opsForValue().increment(key, delta);
        }
        /**
         * 递减
         * @param key 键
         * @param delta 要减少几(小于0)
         * @return
         */
        public long decr(String key, long delta) {
            if (delta < 0) {
                throw new RuntimeException("递减因子必须大于0");
            }
            return redisTemplate.opsForValue().increment(key, -delta);
        }
        // ================================Map=================================
        /**
         * 获取list缓存的长度
         * @param key 键
         * @return
         */
        public long hGetMapSize(String key) {
            try {
                return redisTemplate.opsForHash().size(key);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
        /**
         * HashGet
         * @param key 键 不能为null
         * @param item 项 不能为null
         * @return 值
         */
        public Object hget(String key, String item) {
            return redisTemplate.opsForHash().get(key, item);
        }
        /**
         * 获取hashKey对应的所有键值
         * @param key 键
         * @return 对应的多个键值
         */
        public Map hmget(String key) {
            return redisTemplate.opsForHash().entries(key);
        }
        /**
         * HashSet
         * @param key 键
         * @param map 对应多个键值
         * @return true 成功 false 失败
         */
        public boolean hmset(String key, Map map) {
            try {
                redisTemplate.opsForHash().putAll(key, map);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * HashSet 并设置时间
         * @param key 键
         * @param map 对应多个键值
         * @param time 时间(秒)
         * @return true成功 false失败
         */
        public boolean hmset(String key, Map map, long time) {
            try {
                redisTemplate.opsForHash().putAll(key, map);
                if (time > 0) {
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 向一张hash表中放入数据,如果不存在将创建
         * @param key 键
         * @param item 项
         * @param value 值
         * @return true 成功 false失败
         */
        public boolean hset(String key, String item, Object value) {
            try {
                redisTemplate.opsForHash().put(key, item, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 向一张hash表中放入数据,如果不存在将创建
         * @param key 键
         * @param item 项
         * @param value 值
         * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
         * @return true 成功 false失败
         */
        public boolean hset(String key, String item, Object value, long time) {
            try {
                redisTemplate.opsForHash().put(key, item, value);
                if (time > 0) {
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 删除hash表中的值
         * @param key 键 不能为null
         * @param item 项 可以使多个 不能为null
         */
        public void hdel(String key, Object... item) {
            redisTemplate.opsForHash().delete(key, item);
        }
        /**
         * 判断hash表中是否有该项的值
         * @param key 键 不能为null
         * @param item 项 不能为null
         * @return true 存在 false不存在
         */
        public boolean hHasKey(String key, String item) {
            return redisTemplate.opsForHash().hasKey(key, item);
        }
        /**
         * hash递增 如果不存在,就会创建一个 并把新增后的值返回
         * @param key 键
         * @param item 项
         * @param by 要增加几(大于0)
         * @return
         */
        public double hincr(String key, String item, double by) {
            return redisTemplate.opsForHash().increment(key, item, by);
        }
        /**
         * hash递减
         * @param key 键
         * @param item 项
         * @param by 要减少记(小于0)
         * @return
         */
        public double hdecr(String key, String item, double by) {
            return redisTemplate.opsForHash().increment(key, item, -by);
        }
        // ============================set=============================
        /**
         * 根据key获取Set中的所有值
         * @param key 键
         * @return
         */
        public Set sGet(String key) {
            try {
                return redisTemplate.opsForSet().members(key);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
        /**
         * 根据value从一个set中查询,是否存在
         * @param key 键
         * @param value 值
         * @return true 存在 false不存在
         */
        public boolean sHasKey(String key, Object value) {
            try {
                return redisTemplate.opsForSet().isMember(key, value);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 将数据放入set缓存
         * @param key 键
         * @param values 值 可以是多个
         * @return 成功个数
         */
        public long sSet(String key, Object... values) {
            try {
                return redisTemplate.opsForSet().add(key, values);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
        /**
         * 将set数据放入缓存
         * @param key 键
         * @param time 时间(秒)
         * @param values 值 可以是多个
         * @return 成功个数
         */
        public long sSetAndTime(String key, long time, Object... values) {
            try {
                Long count = redisTemplate.opsForSet().add(key, values);
                if (time > 0) {
                    expire(key, time);
                }
                return count;
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
        /**
         * 获取set缓存的长度
         * @param key 键
         * @return
         */
        public long sGetSetSize(String key) {
            try {
                return redisTemplate.opsForSet().size(key);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
        /**
         * 移除值为value的
         * @param key 键
         * @param values 值 可以是多个
         * @return 移除的个数
         */
        public long setRemove(String key, Object... values) {
            try {
                Long count = redisTemplate.opsForSet().remove(key, values);
                return count;
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
        // ===============================list=================================
        /**
         * 获取list缓存的内容
         * @param key 键
         * @param start 开始
         * @param end 结束 0 到 -1代表所有值
         * @return
         */
        public List lGet(String key, long start, long end) {
            try {
                return redisTemplate.opsForList().range(key, start, end);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
        /**
         * 获取list缓存的长度
         * @param key 键
         * @return
         */
        public long lGetListSize(String key) {
            try {
                return redisTemplate.opsForList().size(key);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
        /**
         * 通过索引 获取list中的值
         * @param key 键
         * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
         * @return
         */
        public Object lGetIndex(String key, long index) {
            try {
                return redisTemplate.opsForList().index(key, index);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
        /**
         * 将list放入缓存
         * @param key 键
         * @param value 值
         * @return
         */
        public boolean lSet(String key, Object value) {
            try {
                redisTemplate.opsForList().rightPush(key, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 将list放入缓存
         * @param key 键
         * @param value 值
         * @param time 时间(秒)
         * @return
         */
        public boolean lSet(String key, Object value, long time) {
            try {
                redisTemplate.opsForList().rightPush(key, value);
                if (time > 0) {
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 将list放入缓存
         * @param key 键
         * @param value 值
         * @return
         */
        public boolean lSet(String key, List value) {
            try {
                redisTemplate.opsForList().rightPushAll(key, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 将list放入缓存
         *
         * @param key 键
         * @param value 值
         * @param time 时间(秒)
         * @return
         */
        public boolean lSet(String key, List value, long time) {
            try {
                redisTemplate.opsForList().rightPushAll(key, value);
                if (time > 0) {
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 根据索引修改list中的某条数据
         * @param key 键
         * @param index 索引
         * @param value 值
         * @return
         */
        public boolean lUpdateIndex(String key, long index, Object value) {
            try {
                redisTemplate.opsForList().set(key, index, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 移除N个值为value
         * @param key 键
         * @param count 移除多少个
         * @param value 值
         * @return 移除的个数
         */
        public long lRemove(String key, long count, Object value) {
            try {
                Long remove = redisTemplate.opsForList().remove(key, count, value);
                return remove;
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    }
     
    

    11.CustomMinioClient

    import com.google.common.collect.Multimap;
    import io.minio.CreateMultipartUploadResponse;
    import io.minio.ListPartsResponse;
    import io.minio.MinioClient;
    import io.minio.ObjectWriteResponse;
    import io.minio.errors.*;
    import io.minio.messages.Part;
    import java.io.IOException;
    import java.security.InvalidKeyException;
    import java.security.NoSuchAlgorithmException;
    public class CustomMinioClient extends MinioClient {
        /**
         * 继承父类
         * @param client
         */
        public CustomMinioClient(MinioClient client) {
            super(client);
        }
        /**
         * 初始化分片上传、获取 uploadId
         *
         * @param bucket           String  存储桶名称
         * @param region           String
         * @param object           String   文件名称
         * @param headers          Multimap 请求头
         * @param extraQueryParams Multimap
         * @return String
         */
        public String initMultiPartUpload(String bucket, String region, String object, Multimap headers, Multimap extraQueryParams) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException {
            CreateMultipartUploadResponse response = this.createMultipartUpload(bucket, region, object, headers, extraQueryParams);
            return response.result().uploadId();
        }
        /**
         * 合并分片
         *
         * @param bucketName       String   桶名称
         * @param region           String
         * @param objectName       String   文件名称
         * @param uploadId         String   上传的 uploadId
         * @param parts            Part[]   分片集合
         * @param extraHeaders     Multimap
         * @param extraQueryParams Multimap
         * @return ObjectWriteResponse
         */
        public ObjectWriteResponse mergeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap extraHeaders, Multimap extraQueryParams) throws IOException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException, ServerException, InvalidKeyException {
            return this.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
        }
        /**
         * 查询当前上传后的分片信息
         *
         * @param bucketName       String   桶名称
         * @param region           String
         * @param objectName       String   文件名称
         * @param maxParts         Integer  分片数量
         * @param partNumberMarker Integer  分片起始值
         * @param uploadId         String   上传的 uploadId
         * @param extraHeaders     Multimap
         * @param extraQueryParams Multimap
         * @return ListPartsResponse
         */
        public ListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap extraHeaders, Multimap extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
            return this.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
        }
    }

    12.WebAppConfigurer

    import com.alibaba.fastjson.serializer.SerializerFeature;
    import com.alibaba.fastjson.support.config.FastJsonConfig;
    import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
    import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.converter.HttpMessageConverter;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    /**
     * @description Web应用程序配置
     * @author LGY
     * @date 2023/03/14 20:09
     * @version 1.0.0
     */
    @Configuration
    public class WebAppConfigurer implements WebMvcConfigurer {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                    .allowCredentials(true)
                    .allowedOriginPatterns("*")
                    .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
                    .allowedHeaders("*");
        }
        //实体类属性为空时不进行序列化返回给前端
        @Bean
        public HttpMessageConverters fastJsonHttpMessageConverters() {
            FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
            FastJsonConfig fastJsonConfig = new FastJsonConfig();
            fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
            fastConverter.setFastJsonConfig(fastJsonConfig);
            HttpMessageConverter converter = fastConverter;
            return new HttpMessageConverters(converter);
        }
    }
    

    13.MybatisPlusConfig

    import com.baomidou.mybatisplus.annotation.DbType;
    import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
    import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.ibatis.reflection.MetaObject;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import java.time.LocalDateTime;
    /**
     * MybatisPlus配置类
     *
     */
    @Slf4j
    @Configuration
    @MapperScan("com.wusuowei.miniouploadfile.mapper")
    public class MybatisPlusConfig implements MetaObjectHandler {
        /**
         * 新的分页插件
         * 需要设置 MybatisConfiguration#useDeprecatedExecutor = false
         * 避免缓存出现问题(该属性会在旧插件移除后一同移除)
         */
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
            return interceptor;
        }
        @Override
        public void insertFill(MetaObject metaObject) {
            log.info("start insert fill ....");
            this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
            this.fillStrategy(metaObject, "createTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug请升级到之后的版本如`3.3.1.8-SNAPSHOT`)
            /* 上面选其一使用,下面的已过时(注意 strictInsertFill 有多个方法,详细查看源码) */
        }
        @Override
        public void updateFill(MetaObject metaObject) {
            log.info("start update fill ....");
            this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
            this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug请升级到之后的版本如`3.3.1.8-SNAPSHOT`)
            /* 上面选其一使用,下面的已过时(注意 strictUpdateFill 有多个方法,详细查看源码) */
        }
    

    14.R统一结果返回

    package com.wusuowei.miniouploadfile.utils;
    import com.alibaba.fastjson.JSON;
    import java.util.HashMap;
    import java.util.Map;
    /**
     * 返回数据
     *
     * @author Mark sunlightcs@gmail.com
     */
    public class R extends HashMap {
        private static final long serialVersionUID = 1L;
        public R setData(Object data) {
            put("data",data);
            return this;
        }
        //利用fastjson进行反序列化
        public  T getData(Class typeReference) {
            Object data = get("data");	//默认是map
            String jsonString = JSON.toJSONString(data);
            T t = JSON.parseObject(jsonString, typeReference);
            return t;
        }
        public R() {
            put("code", 200);
            put("msg", "success");
        }
        public static R error() {
            return error(5000, "未知异常,请联系管理员");
        }
        public static R error(String msg) {
            return error(500, msg);
        }
        public static R error(RespEnum respEnum) {
            return error(respEnum.getCode(), respEnum.getMessage());
        }
        public static R error(int code, String msg) {
            R r = new R();
            r.put("code", code);
            r.put("msg", msg);
            return r;
        }
        public static R ok(RespEnum respEnum) {
            return ok(respEnum.getCode(), respEnum.getMessage());
        }
        public static R ok(String msg) {
            R r = new R();
            r.put("msg", msg);
            return r;
        }
        public static R ok(int code, String msg) {
            R r = new R();
            r.put("code", code);
            r.put("msg", msg);
            return r;
        }
        public static R ok(Map map) {
            R r = new R();
            r.putAll(map);
            return r;
        }
        public static R ok() {
            return new R();
        }
        public R put(String key, Object value) {
            super.put(key, value);
            return this;
        }
        public  Integer getCode() {
            return (Integer) this.get("code");
        }
    }
    

    15.RespEnum

    package com.wusuowei.miniouploadfile.utils;
    public enum RespEnum {
        UPLOADSUCCESSFUL(1, "上传成功"),
        UPLOADING(2, "上传中"),
        NOT_UPLOADED(3, "未上传"),
        ACCESS_PARAMETER_INVALID(1001,"访问参数无效"),
        UPLOAD_FILE_FAILED(1002,"文件上传失败"),
        DATA_NOT_EXISTS(1003,"数据不存在"),
        ;
        private final Integer code;
        private final String message;
        public Integer getCode() {
            return code;
        }
        public String getMessage() {
            return message;
        }
        RespEnum(Integer code, String message) {
            this.code = code;
            this.message = message;
        }
    }
    

    小结

    文章贴的代码有点多了,大家可以去我的gitee上下载下来运行。项目的功能我测试优化了很多次,如果代码有何异常或者不完整欢迎在评论区留言。如果这篇文章有幸帮助到你,希望读者大大们可以给作者点个赞呀😶‍🌫️😶‍🌫️😶‍🌫️