spring-boot中实现分片上传文件

一、上传文件基本实现

  • 1、前端效果图展示,这里使用element-ui plus来展示样式效果
  • 2、基础代码如下

    vue 复制代码
    <template>
      <div
        >
        <el-upload
          ref="uploadRef"
          class="upload-demo"
          :limit="1"
          :on-change="handleExceed"
          :auto-upload="false"
        >
          <template #trigger>
            <el-button type="primary">选择文件</el-button>
          </template>
          <el-button style="margin-left: 30px" type="success" @click="submitUpload"> 上传 </el-button>
        </el-upload>
      </div>
    </template>
    
    <script setup>
      import { ref } from 'vue';
    
      import axios from 'axios';
    
      const fileRef = ref(null);
    
      const handleExceed = (files) => {
        console.log(files);
        fileRef.value = files;
      };
    
      const submitUpload = () => {
        console.log('开始上传文件', JSON.stringify(fileRef.value));
        const formData = new FormData();
        formData.append('file', fileRef.value.raw);
        axios.post('http://localhost:9002/file/upload', formData).then((res) => {
          console.log(fileRef.value, '??');
          console.log('上传成功');
        });
      };
    </script>
    
    <style lang="scss" scoped></style>
  • 3、定义后端接口,并且处理好跨域(关于跨域处理,自己百度处理)

    java 复制代码
    package com.course.file.controller;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.multipart.MultipartFile;
    
    @RestController
    public class UploadController {
        private static final Logger LOG = LoggerFactory.getLogger(UploadController.class);
        @PostMapping("/upload")
        public String uploadApi(@RequestParam MultipartFile file) {
            this.LOG.info("上传文件开始");
            this.LOG.info(file.getOriginalFilename());
            this.LOG.info(String.valueOf(file.getSize()));
            return "上传成功";
        }
    }
  • 4、保存文件到本地文件

    java 复制代码
    package com.course.file.controller;
    
    import com.course.file.utils.UuidUtil;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.multipart.MultipartFile;
    
    import java.io.File;
    import java.io.IOException;
    
    @RestController
    public class UploadController {
        private static final Logger LOG = LoggerFactory.getLogger(UploadController.class);
    
        @PostMapping("/upload")
        public String uploadApi(@RequestParam MultipartFile file) throws IOException {
            this.LOG.info("上传文件开始");
            this.LOG.info(file.getOriginalFilename());
            this.LOG.info(String.valueOf(file.getSize()));
            // 保存文件到本地
            String fileName = file.getOriginalFilename();
            String key = UuidUtil.getShortUuid();
            // 需要先本地创建一个file文件夹
            String fullPath = "E:/file/" + key + "-" + fileName;
            File dest = new File(fullPath);
            file.transferTo(dest);
            return "上传成功";
        }
    }

二、配置静态目录

  • 1、在FileApplication.java旁边添加一个SpringMvcConfig.java的文件

    java 复制代码
    package com.course.file;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class SpringMvcConfig implements WebMvcConfigurer {
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/f/**").addResourceLocations("file:E:/file/");
        }
        // http://localhost:9002/file/f/FdXMQJdF-xx.png
    }
  • 2、直接浏览器上直接访问上面的地址

  • 3、上传地址返回给前端

    java 复制代码
    @RestController
    public class UploadController {
        private static final Logger LOG = LoggerFactory.getLogger(UploadController.class);
    
        @PostMapping("/upload")
        public String uploadApi(@RequestParam MultipartFile file) throws IOException {
            this.LOG.info("上传文件开始");
            this.LOG.info(file.getOriginalFilename());
            this.LOG.info(String.valueOf(file.getSize()));
            // 保存文件到本地
            String fileName = file.getOriginalFilename();
            String key = UuidUtil.getShortUuid();
            // 需要先本地创建一个file文件夹
            String fullPath = "E:/file/" + key + "-" + fileName;
            File dest = new File(fullPath);
            file.transferTo(dest);
            return "http://localhost:9002/file/f/" + key + "-" + fileName;
        }
    }
  • 4、将上面几个固定的配置写到配置文件中

    properties 复制代码
    file.path=E:/file/
    file.domain=http://localhost:9002/file/
    # 修改上传文件大小(不设置大文件可能上传失败)
    spring.servlet.multipart.max-file-size=50MB
    spring.servlet.multipart.max-request-size=50MB
  • 5、在控制器中使用

    java 复制代码
    public class UploadController {
        private static final Logger LOG = LoggerFactory.getLogger(UploadController.class);
        @Value("${file.path}")
        private String FILE_PATH;
    
        @Value("${file.domain}")
        private String FILE_DOMAIN;
    }

三、断点续传

  • 1、主要原理

    • 前端将大文件根据文件大小来切割成小片段,使用递归的方式调用后端接口,将文件上传到服务器端
    • 服务器端接收到前端上传片段,存储到服务器上
    • 等前端最后一个上传完成后,将全部的文件合并成一个文件
    • 合并完成后,返回一个url地址,将之前的分片上传的文件删除
  • 2、手动演示前端分段上传

    javascript 复制代码
    const submitUpload = () => {
        console.log('开始上传文件', JSON.stringify(fileRef.value));
        const formData = new FormData();
        const file = fileRef.value.raw;
        // 文件分配
        let shardSize = 5 * 1024 * 1024; // 以5MB为一个分片
        let shardIndex = 0; // 分片索引
        let start = shardIndex * shardSize; // 开始位置
        let end = Math.min(file.size, start + shardSize); // 结束位置
        let fileShard = file.slice(start, end); // 每次上传的分片数据
        formData.append('file', fileShard);
        axios.post('http://localhost:9002/file/upload', formData).then((res) => {
          console.log(fileRef.value, '??');
          console.log('上传成功');
        });
      };
  • 3、第二次的时候将shardIndex改为1

  • 4、查看本地文件夹下的文件

  • 5、手动创建一个接口来尝试合并文件

    java 复制代码
    @GetMapping("merge")
        public String merge() throws FileNotFoundException {
            // 最终合成后的视频文件名称
            File newFile = new File(FILE_PATH + "test.mp4");
            FileOutputStream outputStream = new FileOutputStream(newFile, true);
            FileInputStream fileInputStream = null;
            byte[] bytes = new byte[5 * 1024 * 1024];
            int len;
            try {
                // 读取第一段
                fileInputStream = new FileInputStream(new File(FILE_PATH + "/pN0EoOny-blob"));
                while ((len = fileInputStream.read(bytes)) != -1) {
                    outputStream.write(bytes, 0, len);
                }
                // 读取第二段
                fileInputStream = new FileInputStream(new File(FILE_PATH + "/f5oeIEDW-blob"));
                while ((len = fileInputStream.read(bytes)) != -1) {
                    outputStream.write(bytes, 0, len);
                }
                // 读取第三段
                fileInputStream = new FileInputStream(new File(FILE_PATH + "/qsm8n03q-blob"));
                while ((len = fileInputStream.read(bytes)) != -1) {
                    outputStream.write(bytes, 0, len);
                }
            } catch (IOException e) {
                LOG.error("合并分片失败", e);
            } finally {
                try {
                    if (fileInputStream != null) {
                        fileInputStream.close();
                    }
                    outputStream.close();
                    LOG.info("IO流关闭");
                } catch (IOException e) {
                    LOG.error("IO流关闭", e);
                }
            }
            return "合并视频成功";
        }

四、使用数据库来实现分片上传

  • 1、数据表字段

    在数据库中涉及key只跟文件有关,跟上传多少片没关系的,当已经上传的分片数和分片总数一样的时候就合并文件

    sql 复制代码
    drop table if exists `file`;
    create table `file` (
      `id` int(11) not null PRIMARY key auto_increment comment '主键id',
      `path` varchar(100) not null comment '相对路径',
      `name` varchar(100) comment '文件名',
      `suffix` varchar(10) comment '后缀',
      `size` int(11) comment '大小|字节B',
    	`shard_index` int(11) DEFAULT 0 COMMENT '已上传分片',
    	`shard_size` int(11) DEFAULT 0 COMMENT '分片大小',
    	`shard_total` int(11) DEFAULT 0 COMMENT '分片总数',
    	`key` VARCHAR(100) DEFAULT NULL COMMENT '文件标识',
      `created_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间',
      `updated_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '更新时间',
      `deleted_at` timestamp(6) NULL DEFAULT NULL COMMENT '软删除时间'
    ) engine=innodb default charset=utf8mb4 comment='文件';
  • 2、在pom.xml文件中添加mybatis-plus的依赖包

    xml 复制代码
    <!--    配置连接到数据库    -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.26</version>
    </dependency>
    <!--   mybatis plus 依赖包  -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.2.0</version>
    </dependency>
    
    <!--    mybatis 代码生成器    -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-generator</artifactId>
        <version>3.2.0</version>
    </dependency>
  • 3、application.properties添加配置

    properties 复制代码
    # mysql数据库链接
    spring.datasource.url=jdbc:mysql://localhost:3306/beego?characterEncoding=utf-8&serverTimezone=GMT%2B8
    spring.datasource.username=root
    spring.datasource.password=123456
    
    # 配置mybatis-plus
    # 开启下划线转驼峰
    mybatis-plus.configuration.map-underscore-to-camel-case=true 
    mybatis-plus.configuration.auto-mapping-behavior=full
    mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
    # mapping的路径
    mybatis-plus.mapper-locations=classpath*:mapper/**/*Mapper.xml
    #主键类型  AUTO:"数据库ID自增", INPUT:"用户输入ID", ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
    mybatis-plus.global-config.db-config.id-type=AUTO
    # 逻辑删除(软删除)
    mybatis-plus.global-config.db-config.logic-delete-value=NOW()
    mybatis-plus.global-config.db-config.logic-not-delete-value=NULL
  • 4、使用模板生成器生成代码

  • 5、前端使用递归的方式来分片上传文件

    vue 复制代码
    <script setup>
      import { ref } from 'vue';
      import { genFileId } from 'element-plus';
      import axios from 'axios';
      import md5 from 'js-md5';
    
      const fileRef = ref(null);
    
      const handleExceed = (files) => {
        console.log(files);
        fileRef.value = files;
      };
    
      const updateFile = (shardIndex) => {
        const formData = new FormData();
        const file = fileRef.value.raw;
        // 文件分配
        let shardSize = 5 * 1024 * 1024; // 以5MB为一个分片
        // let shardIndex = 0; // 分片索引
        let start = (shardIndex - 1) * shardSize; // 开始位置
        let end = Math.min(file.size, start + shardSize); // 结束位置
        let fileShard = file.slice(start, end); // 每次上传的分片数据
        // 前端多上传参数
        let size = file.size;
        let shardTotal = Math.ceil(size / shardSize); // 总片数
        const suffix = file.name.substring(file.name.lastIndexOf('.') + 1);
        formData.append('shard', fileShard);
        formData.append('shardIndex', shardIndex);
        formData.append('shardSize', shardSize);
        formData.append('shardTotal', shardTotal);
        formData.append('name', file.name);
        formData.append('size', size);
        formData.append('suffix', suffix); 
        formData.append('key', md5(`${file.name}_${file.size}_${file.type}`));
    
        axios.post('http://localhost:9002/file/upload1', formData).then((res) => {
          // 判断如果当前的shardIndex < shardTotal的时候递归上传
          if (shardIndex < shardTotal) {
            updateFile(++shardIndex);
          } else {
            console.log('上传成功');
          }
        });
      };
      const submitUpload = () => {
        console.log('开始上传文件', JSON.stringify(fileRef.value));
        // 开始上传文件
        updateFile(1);
      };
    </script>
  • 6、后端对文件上传处理,存储到本地和入库操作

    java 复制代码
    package com.course.file.controller;
    
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.course.file.model.FileEntity;
    import com.course.file.service.IFileService;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.multipart.MultipartFile;
    
    import java.io.File;
    import java.io.IOException;
    
    @RestController
    public class FileController {
        @Autowired
        private IFileService fileService;
        private static final Logger LOG = LoggerFactory.getLogger(FileController.class);
        @Value("${file.path}")
        private String FILE_PATH;
    
        @Value("${file.domain}")
        private String FILE_DOMAIN;
    
        @PostMapping("upload1")
        public String upload1(
                @RequestParam MultipartFile shard,
                Integer shardIndex,
                Integer shardSize,
                Integer shardTotal,
                String name,
                String suffix,
                Integer size,
                String key
        ) throws IOException {
            this.LOG.info("开始上传文件");
            System.out.println("当前分片:" + shardIndex);
            System.out.println("当前分片大小:" + shardSize);
            System.out.println("当前分片总数:" + shardTotal);
            System.out.println("文件名称:" + name);
            System.out.println("文件后缀名:" + suffix);
            System.out.println("文件大小:" + size);
            System.out.println("文件唯一的key:" + key);
            // 文件保存到本地目录下
            String localPath = this.FILE_PATH + key + "." + suffix + "." + shardIndex;
            File dest = new File(localPath);
            shard.transferTo(dest);
            LOG.info(dest.getAbsolutePath());
            // 数据入库操作
            LambdaQueryWrapper<FileEntity> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(FileEntity::getFileKey, key);
            FileEntity fileEntity = new FileEntity();
            if (this.fileService.getOne(queryWrapper) != null) {
                // 说明不是第一次上传,需要更新当前上传的分片数量
                fileEntity.setShardIndex(shardIndex);
                if (this.fileService.update(fileEntity, queryWrapper)) {
                    return "上传成功";
                } else {
                    return "上传失败";
                }
            } else {
                // 第一次上传创建
                String path = this.FILE_PATH + key + "." + suffix;
                fileEntity.setFileKey(key);
                fileEntity.setName(name);
                fileEntity.setPath(path);
                fileEntity.setShardIndex(shardIndex);
                fileEntity.setSuffix(suffix);
                fileEntity.setShardSize(shardSize);
                fileEntity.setShardTotal(shardTotal);
                fileEntity.setSize(size);
                if (this.fileService.save(fileEntity)) {
                    return "上传成功";
                } else {
                    return "上传失败";
                }
            }
        }
    }
  • 7、查看本地目录是否生成分片文件

  • 8、对本地分片文件合并操作,当shardIndex=shardTotal的时候进行合并操作

    java 复制代码
    @RestController
    public class FileController {
        @Autowired
        private IFileService fileService;
        private static final Logger LOG = LoggerFactory.getLogger(FileController.class);
        @Value("${file.path}")
        private String FILE_PATH;
    
        @Value("${file.domain}")
        private String FILE_DOMAIN;
    
        @PostMapping("upload1")
        public String upload1(
                @RequestParam MultipartFile shard,
                Integer shardIndex,
                Integer shardSize,
                Integer shardTotal,
                String name,
                String suffix,
                Integer size,
                String key
        ) throws IOException {
            this.LOG.info("开始上传文件");
            System.out.println("当前分片:" + shardIndex);
            System.out.println("当前分片大小:" + shardSize);
            System.out.println("当前分片总数:" + shardTotal);
            System.out.println("文件名称:" + name);
            System.out.println("文件后缀名:" + suffix);
            System.out.println("文件大小:" + size);
            System.out.println("文件唯一的key:" + key);
            // 文件保存到本地目录下
            String localPath = this.FILE_PATH + key + "." + suffix + "." + shardIndex;
            File dest = new File(localPath);
            shard.transferTo(dest);
            LOG.info(dest.getAbsolutePath());
            // 合并文件操作
            if (Objects.equals(shardIndex, shardTotal)) {
                this.merge(key, suffix, shardTotal);
            }
    
            // 数据入库操作
            LambdaQueryWrapper<FileEntity> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(FileEntity::getFileKey, key);
            FileEntity fileEntity = new FileEntity();
            if (this.fileService.getOne(queryWrapper) != null) {
                // 说明不是第一次上传,需要更新当前上传的分片数量
                fileEntity.setShardIndex(shardIndex);
    
                if (this.fileService.update(fileEntity, queryWrapper)) {
                    return "上传成功";
                } else {
                    return "上传失败";
                }
            } else {
                // 第一次上传创建
                String path = this.FILE_PATH + key + "." + suffix;
                fileEntity.setFileKey(key);
                fileEntity.setName(name);
                fileEntity.setPath(path);
                fileEntity.setShardIndex(shardIndex);
                fileEntity.setSuffix(suffix);
                fileEntity.setShardSize(shardSize);
                fileEntity.setShardTotal(shardTotal);
                fileEntity.setSize(size);
                if (this.fileService.save(fileEntity)) {
                    return "上传成功";
                } else {
                    return "上传失败";
                }
            }
        }
    	/**
         * 对上传的文件片段合并操作
         * @param key
         * @param suffix
         * @param shardTotal
         * @throws FileNotFoundException
         */
        private void merge(String key, String suffix, Integer shardTotal) throws FileNotFoundException {
            this.LOG.info("=====开始合并切片操作=====");
            String path = this.FILE_PATH + key + "." + suffix;
            File newFile = new File(path);
            FileOutputStream outputStream = new FileOutputStream(newFile, true);//文件追加写入
            FileInputStream fileInputStream = null;//分片文件
            byte[] byt = new byte[10 * 1024 * 1024];
            int len;
            try {
                for (int i = 0; i < shardTotal; i++) {
                    fileInputStream = new FileInputStream(new File(this.FILE_PATH + key + "." + suffix + "." + (i + 1)));
                    while ((len = fileInputStream.read(byt)) != -1) {
                        outputStream.write(byt, 0, len);
                    }
                }
            } catch (Exception e) {
                this.LOG.error("分片合并异常", e);
            } finally {
                try {
                    if (fileInputStream != null) {
                        fileInputStream.close();
                    }
                    outputStream.close();
                    LOG.info("IO流关闭");
                } catch (Exception e) {
                    LOG.error("IO流关闭", e);
                }
            }
            this.LOG.info("=====结束合并切片操作=====");
        }
    }
  • 9、当合并后可以对之前的分片进行删除操作,避免占用更的磁盘空间

    java 复制代码
    private void merge(String key, String suffix, Integer shardTotal) throws FileNotFoundException, InterruptedException {
        // ...
        this.LOG.info("=====结束合并切片操作=====");
        System.gc();
        Thread.sleep(1000);
    
        // 删除分片
        LOG.info("删除分片开始");
        for (int i = 0; i < shardTotal; i++) {
            String filePath = path + "." + (i + 1);
            File file = new File(filePath);
            boolean result = file.delete();
            this.LOG.info("删除{},{}", filePath, result ? "成功" : "失败");
        }
        LOG.info("删除分片结束");
    }

五、前端使用BS64提交数据

  • 1、使用bs64提交后端可以直接定义一个字符串接收数据,将接收到的数据转换为图片

  • 2、前端将上传的文件转换为bs64

    javascript 复制代码
    // 使用BS64上传
      const bs64UploadHandler = () => {
        const file = fileRef.value.raw;
        let fileReader = new FileReader();
        fileReader.onload = function (e) {
          const base64 = e.target.result;
          console.log('base64', base64);
          const suffix = file.name.substring(file.name.lastIndexOf('.') + 1);
          axios
            .post('http://localhost:9002/file/bs64Upload', {
              fileName: md5(`${file.name}_${file.size}_${file.type}`) + '.' + suffix,
              fileBs64: base64,
            })
            .then((res) => {
              console.log(res);
            });
        };
        fileReader.readAsDataURL(file);
      };
  • 3、后端定义一个方法,将字符串转换为MultipartFile数据类型

    java 复制代码
    package com.course.file.utils;
    
    
    import org.springframework.web.multipart.MultipartFile;
    import sun.misc.BASE64Decoder;
    
    import java.io.*;
    
    public class Base64ToMultipartFile implements MultipartFile {
    
        private final byte[] imgContent;
        private final String header;
    
        public Base64ToMultipartFile(byte[] imgContent, String header) {
            this.imgContent = imgContent;
            this.header = header.split(";")[0];
        }
    
        @Override
        public String getName() {
            // TODO - implementation depends on your requirements
            return System.currentTimeMillis() + Math.random() + "." + header.split("/")[1];
        }
    
        @Override
        public String getOriginalFilename() {
            // TODO - implementation depends on your requirements
            return System.currentTimeMillis() + (int) Math.random() * 10000 + "." + header.split("/")[1];
        }
    
        @Override
        public String getContentType() {
            // TODO - implementation depends on your requirements
            return header.split(":")[1];
        }
    
        @Override
        public boolean isEmpty() {
            return imgContent == null || imgContent.length == 0;
        }
    
        @Override
        public long getSize() {
            return imgContent.length;
        }
    
        @Override
        public byte[] getBytes() throws IOException {
            return imgContent;
        }
    
        @Override
        public InputStream getInputStream() throws IOException {
            return new ByteArrayInputStream(imgContent);
        }
    
        @Override
        public void transferTo(File dest) throws IOException, IllegalStateException {
            new FileOutputStream(dest).write(imgContent);
        }
    
        public static MultipartFile base64ToMultipart(String base64) {
            try {
                String[] baseStrs = base64.split(",");
    
                BASE64Decoder decoder = new BASE64Decoder();
                byte[] b = new byte[0];
                b = decoder.decodeBuffer(baseStrs[1]);
    
                for(int i = 0; i < b.length; ++i) {
                    if (b[i] < 0) {
                        b[i] += 256;
                    }
                }
    
                return new Base64ToMultipartFile(b, baseStrs[0]);
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        }
    }
  • 4、直接保存文件,这里就不做分片上传

    java 复制代码
    @PostMapping("bs64Upload")
    public String bs64Upload(@RequestBody FileDTO req) throws IOException {
        // 1.将上传的bs64转为图片
        String bs64 = req.getFileBs64();
        MultipartFile shard = Base64ToMultipartFile.base64ToMultipart(bs64);
        // 图片保存到本地
        String localPath = this.FILE_PATH + req.getFileName() ;
        File dest = new File(localPath);
        shard.transferTo(dest);
        LOG.info(dest.getAbsolutePath());
        return this.FILE_DOMAIN + req.getFileName();
    }

六、断点续传

  • 1、断点续传主要原谅,前端在点击上传按钮的时候先调用后端一个接口,判断之前是否有上传过记录,如果有就返回之前上传的分片shardIndex,前端就继续以这个分片来上传,如果没有就返回0表示从0开始上传

  • 2、后端定义一个接口根据key来查询数据库是否已经有上传过记录

    java 复制代码
    @GetMapping("{key}")
    public Integer getShardIndexByKeyApi(@PathVariable String key) {
        LambdaQueryWrapper<FileEntity> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(FileEntity::getFileKey, key).select(FileEntity::getShardIndex);
        FileEntity fileEntity = this.fileService.getOne(queryWrapper);
        return fileEntity.getShardIndex();
    }
  • 3、前端在使用上传前先调用上面的接口

    javascript 复制代码
     const submitUpload = () => {
        console.log('开始上传文件', JSON.stringify(fileRef.value));
        const file = fileRef.value.raw;
        const key = md5(`${file.name}_${file.size}_${file.type}`);
        axios.get(`http://localhost:9002/file/${key}`).then((response) => {
          if (response.data > 0) {
            // 历史上传
            updateFile(response.data + 1);
          } else {
            // 首次上传
            updateFile(1);
          }
        });
     };

七、秒传功能

  • 1、当前端使用MD5加密后提交的名字在数据库已经存在,则直接拼接url返回就可以,不需要再次上传

八、上传到阿里OSS

  • 1、配置依赖包

    xml 复制代码
    <!--    阿里云oss存储    -->
    <dependency>
        <groupId>com.aliyun.oss</groupId>
        <artifactId>aliyun-sdk-oss</artifactId>
        <version>3.10.2</version>
    </dependency>
  • 2、封装方法使用

    java 复制代码
    import com.aliyun.oss.OSS;
    import com.aliyun.oss.OSSClientBuilder;
    import com.aliyun.oss.model.ObjectMetadata;
    import com.aliyun.oss.model.PutObjectResult;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    import org.springframework.web.multipart.MultipartFile;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.URL;
    import java.util.Date;
    import java.util.List;
    import java.util.UUID;
    
    /**
     * 阿里云上传文件
     */
    @Component
    public class OssUtil {
    
        @Value("${aliyun.oss.endpoint}")
        private String endpoint;
        @Value("${aliyun.oss.accessKeyId}")
        private String accessKeyId;
        @Value("${aliyun.oss.accessKeySecret}")
        private String accessKeySecret;
        @Value("${aliyun.oss.bucketName}")
        private String bucketName;
    
        //文件存储目录(自定义阿里云的)
        private String fileDir = "clouFile/";
    
        /**
         * 单个文件上传
         *
         * @param file
         * @return
         */
        public String uploadFile(MultipartFile file) {
            // 调用封装好的上传文件方法
            String fileUrl = uploadImg2Oss(file);
            // 返回完整的路径
            String str = getFileUrl(fileUrl);
            return str.trim();
        }
    
        /**
         * 单个文件上传(指定文件名需要后缀名)
         *
         * @param file
         * @param fileName
         * @return
         */
        public String uploadFile(MultipartFile file, String fileName) {
            try {
                InputStream inputStream = file.getInputStream();
                this.uploadFile2OSS(inputStream, fileName);
                return fileName;
            } catch (Exception e) {
                return "上传失败";
            }
        }
    
        /**
         * 多个文件的上传,返回路径用,分割
         *
         * @param fileList
         * @return
         */
        public String uploadFile(List<MultipartFile> fileList) {
            String fileUrl = "";
            String str = "";
            String photoUrl = "";
            for (int i = 0; i < fileList.size(); i++) {
                fileUrl = uploadImg2Oss(fileList.get(i));
                str = getFileUrl(fileUrl);
                if (i == 0) {
                    photoUrl = str;
                } else {
                    photoUrl += "," + str;
                }
            }
            return photoUrl.trim();
        }
    
        /**
         * 获取完整的路径名
         *
         * @param fileUrl
         * @return
         */
        private String getFileUrl(String fileUrl) {
            if (fileUrl != null && fileUrl.length() > 0) {
                String[] split = fileUrl.split("/");
                String url = this.getUrl(this.fileDir + split[split.length - 1]);
                return url;
            }
            return null;
        }
    
        /**
         * 获取去掉参数的完整路径
         *
         * @param url
         * @return
         */
        private String getShortUrl(String url) {
            String[] imgUrls = url.split("\\?");
            return imgUrls[0].trim();
        }
    
        /**
         * 获取url地址
         *
         * @param key
         * @return
         */
        private String getUrl(String key) {
            // 设置URL过期时间为20年  3600l* 1000*24*365*20
            Date expiration = new Date(new Date().getTime() + 3600l * 1000 * 24 * 365 * 20);
            // 生成URL
            OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
            URL url = ossClient.generatePresignedUrl(bucketName, key, expiration);
            if (url != null) {
                return getShortUrl(url.toString());
            }
            return null;
        }
    
        private String uploadImg2Oss(MultipartFile file) {
            //1、限制最大文件为20M
            if (file.getSize() > 1024 * 1024 * 20) {
                return "图片太大";
            }
            //2、重命名文件
            String fileName = file.getOriginalFilename();
            String suffix = fileName.substring(fileName.lastIndexOf(".")).toLowerCase(); //文件后缀
            String uuid = UUID.randomUUID().toString();
            String name = uuid + suffix;
    
            try {
                InputStream inputStream = file.getInputStream();
                this.uploadFile2OSS(inputStream, name);
                return name;
            } catch (Exception e) {
                return "上传失败";
            }
        }
    
        /**
         * 使用阿里云上传文件
         *
         * @param inStream 输入流
         * @param fileName 文件名称
         * @return
         */
        private String uploadFile2OSS(InputStream inStream, String fileName) {
            String ret = "";
            try {
                //创建上传Object的Metadata
                ObjectMetadata objectMetadata = new ObjectMetadata();
                objectMetadata.setContentLength(inStream.available());
                objectMetadata.setCacheControl("no-cache");
                objectMetadata.setHeader("Pragma", "no-cache");
                objectMetadata.setContentType(getContentType(fileName.substring(fileName.lastIndexOf("."))));
                objectMetadata.setContentDisposition("inline;filename=" + fileName);
                //上传文件
                OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
                PutObjectResult putResult = ossClient.putObject(bucketName, fileDir + fileName, inStream, objectMetadata);
                ret = putResult.getETag();
            } catch (IOException e) {
                System.out.println(e.getMessage());
            } finally {
                try {
                    if (inStream != null) {
                        inStream.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return ret;
        }
    
        /**
         * 获取文件类型
         *
         * @param FilenameExtension
         * @return
         */
        private static String getContentType(String FilenameExtension) {
            if (FilenameExtension.equalsIgnoreCase(".bmp")) {
                return "image/bmp";
            }
            if (FilenameExtension.equalsIgnoreCase(".gif")) {
                return "image/gif";
            }
            if (FilenameExtension.equalsIgnoreCase(".jpeg") ||
                    FilenameExtension.equalsIgnoreCase(".jpg") ||
                    FilenameExtension.equalsIgnoreCase(".png")) {
                return "image/jpeg";
            }
            if (FilenameExtension.equalsIgnoreCase(".html")) {
                return "text/html";
            }
            if (FilenameExtension.equalsIgnoreCase(".txt")) {
                return "text/plain";
            }
            if (FilenameExtension.equalsIgnoreCase(".vsd")) {
                return "application/vnd.visio";
            }
            if (FilenameExtension.equalsIgnoreCase(".pptx") ||
                    FilenameExtension.equalsIgnoreCase(".ppt")) {
                return "application/vnd.ms-powerpoint";
            }
            if (FilenameExtension.equalsIgnoreCase(".docx") ||
                    FilenameExtension.equalsIgnoreCase(".doc")) {
                return "application/msword";
            }
            if (FilenameExtension.equalsIgnoreCase(".xml")) {
                return "text/xml";
            }
            //PDF
            if (FilenameExtension.equalsIgnoreCase(".pdf")) {
                return "application/pdf";
            }
            return "image/jpeg";
        }
    }
相关推荐
PanZonghui4 分钟前
Centos项目部署之运行SpringBoot打包后的jar文件
linux·spring boot
沉着的码农35 分钟前
【设计模式】基于责任链模式的参数校验
java·spring boot·分布式
zyxzyx66639 分钟前
Flyway 介绍以及与 Spring Boot 集成指南
spring boot·笔记
一头生产的驴2 小时前
java整合itext pdf实现自定义PDF文件格式导出
java·spring boot·pdf·itextpdf
程序员张35 小时前
SpringBoot计时一次请求耗时
java·spring boot·后端
麦兜*11 小时前
Spring Boot启动优化7板斧(延迟初始化、组件扫描精准打击、JVM参数调优):砍掉70%启动时间的魔鬼实践
java·jvm·spring boot·后端·spring·spring cloud·系统架构
KK溜了溜了12 小时前
JAVA-springboot 整合Redis
java·spring boot·redis
大只鹅12 小时前
解决 Spring Boot 对 Elasticsearch 字段没有小驼峰映射的问题
spring boot·后端·elasticsearch
天河归来12 小时前
使用idea创建springboot单体项目
java·spring boot·intellij-idea
IT_102412 小时前
Spring Boot项目开发实战销售管理系统——数据库设计!
java·开发语言·数据库·spring boot·后端·oracle