jeecgboot vue 分片上传 minio

发布于:2025-03-28 ⋅ 阅读:(25) ⋅ 点赞:(0)

多视频分片上传组件

<template>
  <div class="img" style="margin-bottom: -16px">
    <a-upload
      name="file"
      listType="picture-card"
      :multiple="isMultiple"
      :accept="accept"
      :action="uploadAction"
      :headers="headers"
      :data="{biz:bizPath,watermark:watermark,bl:bl}"
      :fileList="fileList"
      :before-upload="beforeUpload"
      :disabled="disabled"
      :isMultiple="isMultiple"
      :remove="handleDelete"
      @preview="handlePreview"
      :show-upload-list={showRemoveIcon:this.showRemoveIcon,showPreviewIcon:true}
      :class="[(!isMultiple||!this.showRemoveIcon)?'image-upload-single-over':'']"
      >
      <div >
        <div class="iconp">
          <a-icon :type="uploadLoading ? 'loading' : 'plus'" />
          <div class="ant-upload-text">{{ text }}</div>
        </div>
      </div>
      <!-- 弹出视频显示组件    -->
      <a-modal :visible="previewVisible" :footer="null" @cancel="handleCancel()">
        <video v-if="previewVisible" width="100%" controls id="video"   >
          <source :src="previewImage" />
           <object :data="previewImage" width="100%">
            <embed :src="previewImage" width="100%"/>
           </object>
        </video>
      </a-modal>
    </a-upload>
    <a-progress v-if="show" :percent="uploadPercent" :status="statusFlag" />
    <j-image-open ref="imagePreview"></j-image-open>
  </div>
</template>

<script>
  import { httpAction, getAction } from '@/api/manage'
  const doc = require('@/assets/fileImage/doc.png')
  import Vue from 'vue'
  import { randomUUID } from '@/utils/util'
  import { ACCESS_TOKEN } from "@/store/mutation-types"
  import { getFileAccessHttpUrl } from '@/api/manage'
  import JImageOpen from './JImageOpen'

  const uidGenerator=()=>{
    return '-'+parseInt(Math.random()*10000+1,10);
  }
  const getFileName=(path)=>{
    if(path.lastIndexOf("\\")>=0){
      let reg=new RegExp("\\\\","g");
      path = path.replace(reg,"/");
    }
    return path.substring(path.lastIndexOf("/")+1);
  }
  export default {
    name: 'JVideoUpload',
    components: {
      JImageOpen
    },
    data(){
      return {
        uploadPercent:0,
        statusFlag:'active',
        show:false,
        tempThreads: 2,
        chunkSize: 10 * 1024 * 1024,
        uploadAction:window._CONFIG['domianURL']+"/sys/common/uploadPart",
        mergeAction:window._CONFIG['domianURL']+"/sys/common/merge",

        msg:"",
        successs:false,
        uploadLoading:false,
        headers:{},
        fileList: [],
        coverList: [],
        previewImage:"",
        previewVisible: false,
        coverSrc:'',
        uuId:'',
        a:true,
      }
    },
    props:{
      backgroundImage:{
        type:String,
        required:false,
        default:""
      },
      accept: {
        type: String,
        required:false,
        default: ""
      },
      imgList:{
        type:Array,
        required:false,
        default:function () { return [] }
      },
      text:{
        type:String,
        required:false,
        default:"视频"
      },
      /*这个属性用于控制文件上传的业务路径*/
      bizPath:{
        type:String,
        required:false,
        default:"temp"
      },
      showRemoveIcon:{
        type:Boolean,
        required:false,
        default:true
      },
      /*这个属性用于控制文件上传是否添加水印*/
      watermark:{
        type:String,
        required:false,
        default:"false"
      },
      /*这个属性用于控制图片上传宽高比例*/
      bl:{
        type:String,
        required:false,
        default:""
      },
      value:{
        type:[String,Array],
        required:false
      },
      disabled:{
        type:Boolean,
        required:false,
        default: false
      },
      isMultiple:{
        type:Boolean,
        required:false,
        default: true
      },
      number:{
        type:Number,
        required:false,
        default:0
      },
      index:{
        type:Number,
        required:false,
        default:0
      },
      classFlag:{
        type:String,
        required:false,
        default:''
      }
    },
    watch:{
      value: {
        handler(val,oldValue) {
          if (val instanceof Array) {
            this.initFileList(val.join(','),this.backgroundImage);
          } else {
            this.initFileList(val,this.backgroundImage);
          }
          if(!val || val.length==0){
            this.picUrl = false;
          }
        },
        //立刻执行handler
        immediate: true
      },

      backgroundImage:{
        handler(backgroundImage) {
          if(backgroundImage){
            this.initFileList(this.value,this.getFileUrl(backgroundImage))
          }
        },
      },


      previewVisible:function (val) {
        console.log('监听弹层是否打开,关闭视频',this.previewVisible)
        if(this.previewVisible === false){
          let my_video = document.getElementById("video")
          my_video.pause();
        }
      },
    },
    created(){
      const token = Vue.ls.get(ACCESS_TOKEN);
      this.headers = {"X-Access-Token":token}
      this.uuId = this.getUUID();

    },
    mounted(){
    },
    methods:{
      getUUID(){
        return randomUUID();
      },

      getFileUrl(url){
        return getFileAccessHttpUrl(url);
      },
      initFileList(paths,backgroundimg){
        if(!paths || paths.length==0){
          this.fileList = [];
          return;
        }
        if(this.fileList.length){
            return;
        }
        let fileList = [];
        let arr = paths.split(",")
        let arr2 = backgroundimg.split(",")
        for(var a=0;a<arr.length;a++){
          let url = this.getFileUrl(arr[a]);
          fileList.push({
            uid: uidGenerator(),
            message: this.getFileUrl(arr[a]),
            name: getFileName(arr[a]),
            status: 'done',
            url: url,
            thumbUrl:this.getFileUrl(arr2[a]),
            thumbUrl2:arr2[a],
            response:{
              status:"history",
              message:arr[a]
            }
          })
        }
        this.fileList = fileList
      },

      beforeUpload:function(file){
        //验证视频类型
         const fileType = file.name.split('.')[file.name.split('.').length - 1]
         if("mp4"!==fileType){
           this.$message.warn(file.name + "不是mp4格式!");
           return false
         }
        this.show = true;
        this.statusFlag = 'active';
        file.status = "uploading";//设置文件的状态为上传中
        let dt = new Date();
        let year = dt.getFullYear();
        let month = (dt.getMonth() + 1).toString().padStart(2,'0');
        let date = dt.getDate().toString().padStart(2,'0');
        let timestamp = year+month+date;
        const name =  timestamp+"_"+Math.floor(Math.random() * 900000);
        this.checkFile(file,name);
        return false;  //false 不调用组件的上传接口  true 直接调用组件的上传接口
      },

      async checkFile(file,name) {
        let aa =   await this.uploadChunks(file,name);
        const addFile = {
          uid: file.uid,
          name: getFileName(aa.message),
          thumbUrl: this.getFileUrl(aa.result.filePath),
          thumbUrl2: aa.result.filePath,
          status: 'done',
          response: {
            status: 'done',
            message: this.getFileUrl(aa.message),
          },
          url: this.getFileUrl(aa.message),
          message: aa.message,
        }
        this.fileList.push(addFile)
        this.$emit('change', this.fileList.map((item) => item.message).join(','))
        this.$emit('change2', this.fileList.map((item) => item.thumbUrl2).join(','))
      },

      //上传分片信息
      async uploadChunks(file,name) {
        //切割文件获取分片集合
        const fileChunkList = this.createFileChunk(file, this.chunkSize);
        file.chunkList = fileChunkList.map(({ file }, index) => ({
          index,
          source: file,
          size: file.size
        }));
        var chunkData = file.chunkList;
        return new Promise((resolve, reject) => {
          const requestDataList = chunkData.map(value => {
            const formData = new FormData();
            formData.append("chunk", value.index);
            formData.append("file", value.source);
            formData.append("bizPath", this.bizPath);
            formData.append("fileName", name+file.name+"_"+"part"+value.index);
            return { formData, index: value.index, file };
          });
          try {
            const ret = this.sendRequest(requestDataList);
            resolve(ret);
          } catch (error) {
            this.$message.error("上传失败,请重试");
            reject("sendRequest 失败", error);
          }
        }).then(async res => {
         let aa =  await this.mergeRequest(file,name,file.name,fileChunkList.length);
          return aa
        });
      },

      //切割分片
      createFileChunk(file, size) {
        const fileChunkList = [];
        let count = 0;
        while (count < file.size) {
          fileChunkList.push({
            file: file.slice(count, count + size)
          });
          count += size;
        }
        return fileChunkList;
      },


      // 并发,重试请求上传分片
      async sendRequest(list) {
        var finished = 0;
        const retryArr = []; // retryArr.length代表请求数,值代表重试次数
        var currentFileInfo;
        const total = list.length;
        // 所有请求都存放这个promise中
        return new Promise((resolve, reject) => {
          const handler = () => {
            if (list.length) {
              const formInfo = list.shift();
              const index = formInfo.index;
              const http = this.getUploadPart(formInfo.formData);
              // 分块不存在或不完整,重新发送该分块内容
              http.then(res => {
                console.log("分片上传完成回调数据!")
                console.log(res)
                if (res.success) {
                  const percent = Math.round((index / total) * 100);
                  this.uploadPercent = percent;
                  currentFileInfo = formInfo;
                }
                return res;
              })
                .then(res => {
                  if (res.success) {
                    finished++;
                    handler();
                  } else {
                    this.$message.error(res.msg);
                  }
                })
                .catch(e => {
                  console.log("失败了么")
                  if (typeof retryArr[index] !== "number") {
                    retryArr[index] = 1;
                  }
                  if (retryArr[index] >= this.chunkRetry) {
                    return reject("重试失败", retryArr);
                  }
                  // console.log(`${formInfo.file.name}--文件的第 ${index} 个分块,开始进行第 ${retryArr[index]} 次重试`);
                  retryArr[index]++; // 累加
                  this.tempThreads++; // 释放当前占用的通道
                  list.push(formInfo); // 将失败的重新加入队列
                  handler();
                });
            }
            if (finished >= total) {
              console.log("执行了几次==="+finished)
              resolve(currentFileInfo); // 输出当前完成上传的文件信息
            }
          };
          // 控制并发
          for (let i = 0; i < this.tempThreads; i++) {
            handler();
          }
        });
      },

      //执行分片上传
      getUploadPart(formData) {
        return new Promise((resolve, reject) => {
          httpAction(this.uploadAction,formData,"post").then((res)=>{
            if(res.success){
              resolve(res);
            }
          })
        })
      },

      //执行合并方法
      getMerge(mergeFormData) {
        return new Promise((resolve, reject) => {
          httpAction(this.mergeAction,mergeFormData,"post").then((res)=>{
            if(res.success){
              resolve(res);
            }
          })
        })
      },

      //合并分片
      async mergeRequest(file,path,fileName,chunkNum) {
        return new Promise((resolve, reject) => {
          const mergeFormData = new FormData();
          mergeFormData.append("path", path);
          mergeFormData.append("fileName", fileName);
          mergeFormData.append("chunkNum", chunkNum);
          mergeFormData.append("bizPath", this.bizPath);
          mergeFormData.append("uid", file.uid);
          const http = this.getMerge(mergeFormData);
          http.then(res => {
            if (res.success) {
              this.$message.success(fileName + " 文件上传成功!");
              if(this.uploadPercent<100){
                this.uploadPercent = 100;
              }
              this.statusFlag = 'success';
              resolve(res);
            } else {
              this.statusFlag = 'exception';
              this.$message.error(res.msg);
            }
          }).catch(err => {
              this.statusFlag = 'exception';
              reject("合并失败", err);
            });
        });
      },

      //不用
      handleChange(info) {
        let fileList = info.fileList
        if (this.number > 0 && this.isMultiple) {
          fileList = fileList.slice(-this.number);
        }
        if (info.file.status === 'done') {
          if (info.file.response.success) {
            fileList = fileList.map((file) => {
              if (file.response) {
                const data = JSON.parse(file.response.message);
                file.url = getFileAccessHttpUrl(data.newvideo);
                this.$emit("success", data)
              }
              return file;
            });
          }
        } else if (info.file.status === 'error') {
          this.$message.error(`${info.file.name} 上传失败.`);
        } else if (info.file.status === 'removed') {
          this.handleDelete(info.file)
        }
        this.fileList = fileList
        if (info.file.status === 'done' || info.file.status === 'removed') {
          if (info.file.status === 'removed') {
            this.msg = "";
            this.successs = false;
            this.handlePathChange()
          } else {
            let errorMsg = JSON.parse(info.file.response.message);
            this.successs = info.file.response.success;
            this.msg = errorMsg.uploadStatus;
            this.handlePathChange()
          }
        }
      },
      // 预览
      handlePreview (file) {
        this.previewImage = file.url || file.thumbUrl
        this.previewImage = this.previewImage+"?index="+this.getUUID();
        this.previewVisible = true
      },
      //不用
      coverOpen(file) {
        this.$refs.imagePreview.imageOpen(file);
      },
      //不用
      handlePathChange(){
        let uploadFiles = this.fileList
        let path = ''
        if(!uploadFiles || uploadFiles.length==0){
          path = ''
        }
        let arr = [];
        if(!this.isMultiple && uploadFiles.length>0){
          const data = JSON.parse(uploadFiles[uploadFiles.length-1].response.message);
          arr.push(data.newvideo)
        }else{
          for(let a=0;a<uploadFiles.length;a++){
            if(uploadFiles[a].status === 'done' ) {
              const data = JSON.parse(uploadFiles[a].response.message);
              arr.push(data.newvideo)
            }else{
              return;
            }
          }
        }
        this.$emit('change', arr.join(","));
      },

      /*删除逻辑
      * fileList 找到要删除的file的uid唯一表示去除
      * 上传进度条隐藏
      * 返回父类更新v-model的值
      * */
      handleDelete(file){
        this.fileList = this.fileList.filter((item) => item.uid!== file.uid)
        this.show = false;
        this.$emit('change', this.fileList.map((item) => item.message).join(','))
        this.$emit('change2', this.fileList.map((item) => item.thumbUrl2).join(','))
      },

      //视频预览关闭
      handleCancel() {
        this.previewVisible = false;
      },
    },
    model: {
      prop: 'value',
      event: 'change',
    }
  }
</script>

<style scoped>
  /deep/ .imgupload .iconp{padding:20px;}
  /deep/ .image-upload-single-over .ant-upload-select{display: none}
</style>

引入

<a-row>
          <a-col :span="24">
            <a-form-model-item label="佐证视频" :labelCol="labelCol2" :wrapperCol="wrapperCol2" prop="filePath">
              <j-upload-part-video :accept="accept" @change2="videoSuccess" :backgroundImage="model.cover" v-model="model.filePath" placeholder="请输入佐证视频"  ></j-upload-part-video>
            </a-form-model-item>
          </a-col>
        </a-row>



 methods: {
     //成功后复制需要保存的字段
      videoSuccess(d) {
        this.$set(this.model, 'cover', d);
      },
}

后台

commonController

/**
     * 分片上传minio
     * @param request
     * @param response
     * @return
     */
    @PostMapping(value = "/uploadPart")
    public Result<?> uploadPart(HttpServletRequest request, HttpServletResponse response) {
        Result<?> upload = uploadUtil.uploadPart(request, response);
        return upload;
    }

    /**
     * 合并分片并删除分片
     * @param request
     * @param response
     * @return
     */
    @PostMapping(value = "/merge")
    public Result<?> merge(HttpServletRequest request, HttpServletResponse response) {
        Result<?> upload = uploadUtil.merge(request, response);
        return upload;
    }

uploadUtil

/**
     * 分片上传minio
     * @param request
     * @param response
     * @return
     */
    public Result<?> uploadPart(HttpServletRequest request, HttpServletResponse response) {
        String fileName = request.getParameter("fileName");//分片文件名称
        //String chunkNum = request.getParameter("chunk");//上传的是第几片
        String bizPath = request.getParameter("bizPath");//mino存放文件夹
        MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
        MultipartFile file = multipartRequest.getFile("file");// 获取上传文件对象 blob 对象
        if (oConvertUtils.isEmpty(bizPath)) {
            bizPath = "temp";
        }
        Result<ResultVo> result = new Result<>();
        String minioPartPath = CommonUtils.uploadPart(file, bizPath, fileName);
        if (oConvertUtils.isNotEmpty(minioPartPath)) {
            result.setMessage(minioPartPath);
            result.setSuccess(true);
            //添加文件上传记录日志
            //String originalFilename = file.getOriginalFilename();
            //fileDataService.save(FileDataApply.NO,bizPath,uploadType,originalFilename,savePath);
        } else {
            result.setMessage("上传失败!");
            result.setSuccess(false);
        }
        return result;
    }

    /**
     * 合并分片
     * @param request
     * @param response
     * @return
     */
    public Result<?> merge(HttpServletRequest request, HttpServletResponse response) {
        String path = request.getParameter("path");
        String chunkNum = request.getParameter("chunkNum");
        String fileName = request.getParameter("fileName");
        String bizPath = request.getParameter("bizPath");
        String uid = request.getParameter("uid");
        if (oConvertUtils.isEmpty(bizPath)) {
            bizPath = "temp";
        }
        String merge = CommonUtils.merge(path, fileName, chunkNum,bizPath);
        //获取文件后缀
        String suffix = merge.substring(merge.lastIndexOf(".")+1);
        String name = merge.substring(0,merge.lastIndexOf("."));
        Result<ResultVo> result = new Result<>();
        if("mp4".equals(suffix)){
            //获取视频封面
            String coverImage = videoToCover.coverImageToMinio(merge, name+ "_" + DateUtil.format(new Date(), "yyyyMMdd")+"_"+ RandomUtil.randomNumbers(6));
            ResultVo vo = new ResultVo();
            vo.setFilePath(coverImage);
            vo.setUid(uid);
            result.setResult(vo);
        }
        result.setMessage(merge);
        result.setCode(200);
        result.setSuccess(true);
        return result;
    }

CommonUtils

 /**
     * 分片上传minio
     * @param file  分片文件  blob
     * @param bizPath  保存minio文件夹
     * @param fileName 分片文件名称
     * @return
     */
    public static String uploadPart(MultipartFile file, String bizPath,String fileName) {
        String fileName1 = getFileName(fileName);
        return MinioUtil.uploadPart(file,bizPath,fileName1);
    }

    /**
     * 组装需要合并的分片
     * @param filePath 自定义文件路径
     * @param fileName 原始文件名称
     * @param chunkNum 总分片
     * @return
     */
    public static String merge(String filePath,String fileName,String chunkNum,String bizPath) {
        String fileName3 = getFileName(fileName);
        bizPath= StrAttackFilter.filter(bizPath);
        String minioWjj =  filePath.substring(0,filePath.indexOf("_"));
        List<String> partList = new ArrayList<>();
        //组装全部分片
        for (int i = 0; i <Integer.parseInt(chunkNum) ; i++) {
            partList.add(bizPath+"/"+ minioWjj+"/"+filePath+fileName3+"_part"+i);
        }
        //获取后缀
        String suffix = fileName3.substring(fileName3.lastIndexOf("."));
        String random = fileName3.substring(0,fileName3.lastIndexOf("."));
        random = random+"_"+System.currentTimeMillis();
        String fileName1 = bizPath+"/"+ minioWjj+"/"+random+suffix;
        String merge = MinioUtil.merge(partList, fileName1);
        return merge;
    }

minioUtils

 /**
     * 分片上传minio
     * @param file  分片文件
     * @param bizPath
     * @param fileName
     * @return
     */
    public static String uploadPart(MultipartFile file, String bizPath,String fileName) {

        String file_url = "";
        bizPath=StrAttackFilter.filter(bizPath);
        try {
            initMinio(minioUrl, minioName,minioPass);
            // 检查存储桶是否已经存在
            if(minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
                log.info("Bucket already exists.");
            } else {
                // 创建一个名为ota的存储桶
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
                log.info("create a new bucket.");
            }
            InputStream stream = file.getInputStream();
            String minioWjj =  fileName.substring(0,fileName.indexOf("_"));
            String objectName = bizPath+"/"+minioWjj+"/"+fileName;
            if(objectName.startsWith("/")){
                objectName = objectName.substring(1);
            }
            //分片上传
            PutObjectArgs objectArgs = PutObjectArgs.builder().object(objectName)
                    .bucket(bucketName)
                    .contentType(ContentType.contentType(""))
                    .stream(stream,stream.available(),-1).build();
            minioClient.putObject(objectArgs);
            stream.close();
            file_url = bucketName+"/"+objectName;
        }catch (Exception e){
            log.error(e.getMessage(), e);
        }
        return file_url;
    }

    public static String merge(List<String> fileList,String objectName){
        //获取需要合并的分片组装成ComposeSource
        List<ComposeSource> sourceObjectList = new ArrayList<>(fileList.size());
        for (String chunk : fileList){
            sourceObjectList.add(
                    ComposeSource.builder()
                            .bucket(bucketName)
                            .object(chunk)
                            .build()
            );
        }
        //合并分片
        ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
                .bucket(bucketName)
                //合并后的文件的objectname
                .object(objectName)
                //指定源文件
                .sources(sourceObjectList)
                .build();
        try {
            minioClient.composeObject(composeObjectArgs);
        } 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 (ServerException e) {
            e.printStackTrace();
        } catch (XmlParserException e) {
            e.printStackTrace();
        }
        List<DeleteObject> collect = fileList.stream().map(DeleteObject::new).collect(Collectors.toList());
        //执行删除
        RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder()
                .bucket(bucketName)
                .objects(collect)
                .build();
        Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
        //如果没有下面try的代码,文件史删除不了的,加上下面的代码就可以删除了
        try{
            for (Result<DeleteError> result : results){
                DeleteError deleteError = result.get();
                System.out.println("error in deleteing object"+deleteError.objectName()+";"+deleteError.message());
            }
        }catch (Exception e){
            System.out.println("minio删除文件失败");
            e.printStackTrace();
        }
        String file_url = bucketName+"/"+objectName;
        return file_url;
    }

主要的实现就是这些,其他辅助的工具类需要的留言