學成在線筆記九:在線學習

準備環境(必須)

配置hosts文件

# xcEdu
127.0.0.1 localhost
127.0.0.1 eureka01
127.0.0.1 eureka02
127.0.0.1 www.xuecheng.com
127.0.0.1 ucenter.xuecheng.com
127.0.0.1 video.xuecheng.com
192.168.136.110 img.xuecheng.com

配置nginx

新增配置如下:

    #學成網媒體服務
    server {
        listen       90;
        server_name  localhost;
     
        #視頻目錄
        location /video/ {
            alias   E:/nginx/xcEdu/video/;
        }
    }
    #學成網用戶中心
    server {
        listen       80;
        server_name ucenter.xuecheng.com;
        
        #個人中心
        location / {  
            proxy_pass http://ucenter_server_pool;  
        } 
    }
    #前端ucenter
    upstream ucenter_server_pool{
      #server 127.0.0.1:7081 weight=10;
      server 127.0.0.1:13000 weight=10;
    }
    #學成網媒體服務代理
    map $http_origin $origin_list{
        default http://www.xuecheng.com;
        "~http://www.xuecheng.com" http://www.xuecheng.com;
        "~http://ucenter.xuecheng.com" http://ucenter.xuecheng.com;
    }

    #學成網媒體服務代理
    server {
        listen       80;
        server_name video.xuecheng.com;
        
        location /video {  
            proxy_pass http://video_server_pool;  
            add_header Access-Control-Allow-Origin $origin_list;
            #add_header Access-Control-Allow-Origin *;
            add_header Access-Control-Allow-Credentials true;  
            add_header Access-Control-Allow-Methods GET;
        } 
        
    }
    #媒體服務
    upstream video_server_pool{
        server 127.0.0.1:90 weight=10;
    }       

前端工程

工程導入(省略)

測試Video.js

可以正常播放視頻

媒資管理

需求分析

每個教學機構都可以在媒資系統管理自己的教學資源,包括:視頻、教案等文件。

目前媒資管理的主要管理對象是課程錄播視頻,包括:媒資文件的查詢、視頻上傳、視頻刪除、視頻處理等。

媒資查詢:教學機構查詢自己所擁有的媒體文件。

視頻上傳:將用戶線下錄製的教學視頻上傳到媒資系統。

視頻處理:視頻上傳成功,系統自動對視頻進行編碼處理。

視頻刪除:如果該視頻已不再使用,可以從媒資系統刪除。

文件上傳前端

upload.vue

<template>
<div><br/>
  操作步驟:<br/>
  1、點擊“選擇文件”,選擇要上傳的文件<br/>
  2、點擊“開始上傳”,開始上傳文件<br/>
  3、如需重新上傳請重複上邊的步驟。<br/><br/>

  <div id="uploader" class="wu-example">
    <div class="btns" style="float:left;padding-right: 20px">
      <div id="picker">選擇文件</div>
    </div>
    <div id="ctlBtn" class="webuploader-pick" @click="upload()">開始上傳</div>

  </div>
  <!--用來存放文件信息-->
  <div id="thelist" class="uploader-list" >
    <div v-if="uploadFile.id" :id='uploadFile.id'><span>{{uploadFile.name}}</span>&nbsp;<span class='percentage'>{{percentage}}%</span></div>

  </div>
</div>
</template>
<script>
  import $ from '../../../../static/plugins/jquery/dist/jquery.js'
  import webuploader from '../../../../static/plugins/webuploader/dist/webuploader.js'
  import '../../../../static/css/webuploader/webuploader.css'
  export default{
    data(){
      return{
        uploader:{},
        uploadFile:{},
        percentage:0,
        fileMd5:''
      }
    },
    methods:{
      //開始上傳
      upload(){
        if(this.uploadFile && this.uploadFile.id){
          this.uploader.upload(this.uploadFile.id);
        }else{
          alert("請選擇文件");
        }
      }
    },
    mounted(){
//      var fileMd5;
//      var uploadFile;
      WebUploader.Uploader.register({
          "before-send-file":"beforeSendFile",
          "before-send":"beforeSend",
          "after-send-file":"afterSendFile"
        },{
          beforeSendFile:function(file) {
            // 創建一個deffered,用於通知是否完成操作
            var deferred = WebUploader.Deferred();
            // 計算文件的唯一標識,用於斷點續傳
            (new WebUploader.Uploader()).md5File(file, 0, 100*1024*1024)
              .then(function(val) {

                this.fileMd5 = val;
                this.uploadFile = file;
//                alert(this.fileMd5 )
                //向服務端請求註冊上傳文件
                $.ajax(
                  {
                    type:"POST",
                    url:"/api/media/upload/register",
                    data:{
                      // 文件唯一表示
                      fileMd5:this.fileMd5,
                      fileName: file.name,
                      fileSize:file.size,
                      mimetype:file.type,
                      fileExt:file.ext
                    },
                    dataType:"json",
                    success:function(response) {
                      if(response.success) {
                        //alert('上傳文件註冊成功開始上傳');
                        deferred.resolve();
                      } else {
                        alert(response.message);
                        deferred.reject();
                      }
                    }
                  }
                );
              }.bind(this));

            return deferred.promise();
          }.bind(this),
          beforeSend:function(block) {
            var deferred = WebUploader.Deferred();
            // 每次上傳分塊前校驗分塊,如果已存在分塊則不再上傳,達到斷點續傳的目的
            $.ajax(
              {
                type:"POST",
                url:"/api/media/upload/checkchunk",
                data:{
                  // 文件唯一表示
                  fileMd5:this.fileMd5,
                  // 當前分塊下標
                  chunk:block.chunk,
                  // 當前分塊大小
                  chunkSize:block.end-block.start
                },
                dataType:"json",
                success:function(response) {
                  if(response.fileExist) {
                    // 分塊存在,跳過該分塊
                    deferred.reject();
                  } else {
                    // 分塊不存在或不完整,重新發送
                    deferred.resolve();
                  }
                }
              }
            );
            //構建fileMd5參數,上傳分塊時帶上fileMd5
            this.uploader.options.formData.fileMd5 = this.fileMd5;
            this.uploader.options.formData.chunk = block.chunk;
            return deferred.promise();
          }.bind(this),
          afterSendFile:function(file) {
            // 合併分塊
            $.ajax(
              {
                type:"POST",
                url:"/api/media/upload/mergechunks",
                data:{
                  fileMd5:this.fileMd5,
                  fileName: file.name,
                  fileSize:file.size,
                  mimetype:file.type,
                  fileExt:file.ext
                },
                success:function(response){
                  //在這裏解析合併成功結果
                  if(response && response.success){
                      alert("上傳成功")
                  }else{
                      alert("上傳失敗")
                  }
                }
              }
            );
          }.bind(this)
        }
      );
      // 創建uploader對象,配置參數
      this.uploader = WebUploader.create(
        {
          swf:"/static/plugins/webuploader/dist/Uploader.swf",//上傳文件的flash文件,瀏覽器不支持h5時啓動flash
          server:"/api/media/upload/uploadchunk",//上傳分塊的服務端地址,注意跨域問題
          fileVal:"file",//文件上傳域的name
          pick:"#picker",//指定選擇文件的按鈕容器
          auto:false,//手動觸發上傳
          disableGlobalDnd:true,//禁掉整個頁面的拖拽功能
          chunked:true,// 是否分塊上傳
          chunkSize:1*1024*1024, // 分塊大小(默認5M)
          threads:3, // 開啓多個線程(默認3個)
          prepareNextFile:true// 允許在文件傳輸時提前把下一個文件準備好
        }
      );

      // 將文件添加到隊列
      this.uploader.on("fileQueued", function(file) {
          this.uploadFile = file;
          this.percentage = 0;

        }.bind(this)
      );
      //選擇文件後觸發
      this.uploader.on("beforeFileQueued", function(file) {
//     this.uploader.removeFile(file)
        //重置uploader
        this.uploader.reset()
        this.percentage = 0;
      }.bind(this));

      // 監控上傳進度
      // percentage:代表上傳文件的百分比
      this.uploader.on("uploadProgress", function(file, percentage) {
          this.percentage = Math.ceil(percentage * 100);
      }.bind(this));
      //上傳失敗觸發
      this.uploader.on("uploadError", function(file,reason) {
        console.log(reason)
        alert("上傳文件失敗");
      });
      //上傳成功觸發
      this.uploader.on("uploadSuccess", function(file,response ) {
        console.log(response)
//        alert("上傳文件成功!");
      });
      //每個分塊上傳請求後觸發
      this.uploader.on( 'uploadAccept', function( file, response ) {
          if(!(response && response.success)){//分塊上傳失敗,返回false
              return false;
          }
      });

    }
  }

</script>
<style scoped>


</style>

文件上傳後端

API定義

package com.xuecheng.api.media;

import com.xuecheng.framework.domain.media.response.CheckChunkResult;
import com.xuecheng.framework.model.response.ResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.multipart.MultipartFile; 

@Api(value="媒資管理接口",description="媒資管理接口,提供文件上傳,文件處理等接口")
public interface MediaUploadControllerApi {


    @ApiOperation("文件上傳註冊")
    ResponseResult register(String fileMd5, String fileName,
                            Long fileSize, String mimetype, String fileExt);

    @ApiOperation("分塊檢查")
    CheckChunkResult checkchunk(String fileMd5, Integer chunk, Integer chunkSize);

    @ApiOperation("上傳分塊")
    ResponseResult uploadchunk(MultipartFile file, Integer chunk, String fileMd5);

    @ApiOperation("合併文件")
    ResponseResult mergechunks(String fileMd5, String fileName, 
                               Long fileSize, String mimetype, String fileExt);

}

MediaServiceProperties

package com.xuecheng.manage_media.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "xc-service-manage-media")
public class MediaServiceProperties {
    private String uploadLocation;
}

MediaUploadController

package com.xuecheng.manage_media.controller;

import com.xuecheng.api.media.MediaUploadControllerApi;
import com.xuecheng.framework.domain.media.response.CheckChunkResult;
import com.xuecheng.framework.domain.media.response.MediaCode;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.manage_media.service.MediaUploadService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("media/upload")
public class MediaUploadController implements MediaUploadControllerApi {

    @Autowired
    private MediaUploadService mediaUploadService;


    /**
     * 文件上傳準備
     *
     * @param fileMd5  文件md5碼
     * @param fileName 文件名稱
     * @param fileSize 文件大小
     * @param mimetype 文件mimetype
     * @param fileExt  文件擴展名
     * @return ResponseResult
     */
    @Override
    @PostMapping("register")
    public ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
        mediaUploadService.register(fileMd5, fileName, fileSize, mimetype, fileExt);
        return new ResponseResult(CommonCode.SUCCESS);
    }

    /**
     * 檢查分塊文件
     *
     * @param fileMd5   文件md5碼
     * @param chunk     當前分塊編號
     * @param chunkSize 分塊大小
     * @return CheckChunkResult
     */
    @Override
    @PostMapping("checkchunk")
    public CheckChunkResult checkchunk(String fileMd5, Integer chunk, Integer chunkSize) {
        boolean checkChunk = mediaUploadService.checkChunk(fileMd5, chunk, chunkSize);
        return new CheckChunkResult(MediaCode.CHUNK_FILE_EXIST_CHECK, checkChunk);
    }

    /**
     * 上傳分塊
     *
     * @param file    分塊文件
     * @param chunk   當前分塊編號
     * @param fileMd5 文件md5碼
     * @return ResponseResult
     */
    @Override
    @PostMapping("uploadchunk")
    public ResponseResult uploadchunk(MultipartFile file, Integer chunk, String fileMd5) {
        mediaUploadService.uploadChunk(file, chunk, fileMd5);
        return new ResponseResult(CommonCode.SUCCESS);
    }

    /**
     * 合併分塊並保存數據庫
     *
     * @param fileMd5  文件md5碼
     * @param fileName 文件名稱
     * @param fileSize 文件大小
     * @param mimetype 文件mimetype
     * @param fileExt  文件擴展名
     * @return ResponseResult
     */
    @Override
    @PostMapping("mergechunks")
    public ResponseResult mergechunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
        mediaUploadService.mergeChunks(fileMd5, fileName, fileSize, mimetype, fileExt);
        return new ResponseResult(CommonCode.SUCCESS);
    }
}

MediaUploadService

package com.xuecheng.manage_media.service;

import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.response.MediaCode;
import com.xuecheng.framework.exception.ExceptionCast;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.service.BaseService;
import com.xuecheng.manage_media.config.MediaServiceProperties;
import com.xuecheng.manage_media.dao.MediaFileRepository;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
public class MediaUploadService extends BaseService {

    @Autowired
    private MediaServiceProperties mediaServiceProperties;

    @Autowired
    private MediaFileRepository mediaFileRepository;


    /**
     * 文件上傳準備
     * 1. 檢查文件是否已存在
     * 2. 創建文件存放目錄
     *
     * @param fileMd5  文件md5碼
     * @param fileName 文件名稱
     * @param fileSize 文件大小
     * @param mimetype 文件mimetype
     * @param fileExt  文件擴展名
     */
    public void register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
        // 獲取文件路徑
        String filePath = getFilePath(fileMd5, fileExt);
        File file = new File(filePath);

        // 獲取數據庫記錄
        Optional<MediaFile> mediaFile = mediaFileRepository.findById(fileMd5);

        if (file.exists() && mediaFile.isPresent()) {// 文件已存在
            ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST);
        }

        // 創建目錄
        if (!createFileFolder(fileMd5)) {// 創建目錄失敗
            ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_FAIL);
        }

    }

    /**
     * 檢查分塊文件
     *
     * @param fileMd5   文件md5碼
     * @param chunk     當前分塊編號
     * @param chunkSize 分塊大小
     * @return boolean
     */
    public boolean checkChunk(String fileMd5, Integer chunk, Integer chunkSize) {
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        File file = new File(chunkFileFolderPath + chunk);
        return file.exists();
    }

    /**
     * 上傳分塊
     *
     * @param file    分塊文件
     * @param chunk   當前分塊編號
     * @param fileMd5 文件md5碼
     */
    public void uploadChunk(MultipartFile file, Integer chunk, String fileMd5) {
        isNullOrEmpty(file, CommonCode.PARAMS_ERROR);
        // 創建分塊文件目錄
        createChunkFileFolder(fileMd5);
        File chunkFile = new File(getChunkFileFolderPath(fileMd5) + chunk);
        // 保存文件
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            inputStream = file.getInputStream();
            outputStream = new FileOutputStream(chunkFile);
            IOUtils.copy(inputStream, outputStream);
        } catch (IOException e) {
            // 記錄日誌
            log.error("[上傳分塊文件] 保存分塊文件失敗, e = ", e);
            ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_FAIL);
        } finally {
            // 關閉資源
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException ignored) {
                }
            }
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException ignored) {
                }
            }
        }
    }

    /**
     * 合併分塊並保存數據庫
     * 1. 合併文件
     * 2. 校驗md5碼
     * 3. 保存至數據庫
     *
     * @param fileMd5  文件md5碼
     * @param fileName 文件名稱
     * @param fileSize 文件大小
     * @param mimetype 文件mimetype
     * @param fileExt  文件擴展名
     */
    public void mergeChunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
        // 合併文件
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        File file = new File(chunkFileFolderPath);
        if (!file.exists()) {
            file.mkdirs();
        }
        // 創建合併文件
        File mergeFile = new File(getFilePath(fileMd5, fileExt));
        if (mergeFile.exists()) {// 刪除原有文件
            mergeFile.delete();
        } else {
            try {
                mergeFile.createNewFile();
            } catch (IOException e) {
                log.error("[合併文件分塊] 合併文件分塊時,創建合併文件失敗, e = ", e);
                ExceptionCast.cast(MediaCode.MERGE_FILE_FAIL);
            }
        }
        List<File> chunkFiles = getChunkFiles(file);
        mergeFile = mergeFile(mergeFile, chunkFiles);

        // 校驗md5碼
        boolean b = checkMd5(mergeFile, fileMd5);
        if (!b) {
            ExceptionCast.cast(MediaCode.MERGE_FILE_CHECKFAIL);
        }

        // 保存數據庫
        MediaFile mediaFile = new MediaFile();
        mediaFile.setFileId(fileMd5);
        mediaFile.setFileName(fileMd5 + "." + fileExt);
        mediaFile.setFileOriginalName(fileName);
        // 文件路徑保存相對路徑
        mediaFile.setFilePath(getFileFolderRelativePath(fileMd5, fileExt));
        mediaFile.setFileSize(fileSize);
        mediaFile.setUploadTime(new Date());
        mediaFile.setMimeType(mimetype);
        mediaFile.setFileType(fileExt);
        // 狀態爲上傳成功
        mediaFile.setFileStatus("301002");
        mediaFileRepository.save(mediaFile);
    }


    /**
     * 得到指定md5碼所在文件目錄
     *
     * @param fileMd5 文件md5碼
     * @return String 文件目錄
     */
    private String getFileFolderPath(String fileMd5) {
        return mediaServiceProperties.getUploadLocation() + fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/";

    }

    /**
     * 根據指定的md5碼和文件擴展名獲取文件路徑
     *
     * @param fileMd5 文件md5碼
     * @param fileExt 文件擴展名
     * @return String 文件路徑
     */
    private String getFilePath(String fileMd5, String fileExt) {
        return getFileFolderPath(fileMd5) + fileMd5 + "." + fileExt;
    }

    /**
     * 獲取文件相對路徑
     *
     * @param fileMd5 文件md5碼
     * @param fileExt 文件擴展名
     * @return String 文件相對路徑
     */
    private String getFileFolderRelativePath(String fileMd5, String fileExt) {
        return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/";
    }

    /**
     * 創建文件存放目錄
     *
     * @param fileMd5 文件md5碼
     * @return boolean
     */
    private boolean createFileFolder(String fileMd5) {
        String fileFolderPath = getFileFolderPath(fileMd5);
        File file = new File(fileFolderPath);
        if (!file.exists()) {
            // 創建
            return file.mkdirs();
        }
        return true;
    }

    /**
     * 獲取存放文件分塊目錄路徑
     *
     * @param fileMd5 文件md5碼
     * @return String 分塊目錄路徑
     */
    private String getChunkFileFolderPath(String fileMd5) {
        return getFileFolderPath(fileMd5) + "/chunks/";
    }

    /**
     * 創建存放分塊文件的目錄
     *
     * @param fileMd5 文件md5碼
     * @return boolean
     */
    private boolean createChunkFileFolder(String fileMd5) {
        //創建上傳文件目錄
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        File chunkFileFolder = new File(chunkFileFolderPath);
        if (!chunkFileFolder.exists()) {
            //創建文件夾
            return chunkFileFolder.mkdirs();
        }
        return true;
    }

    /**
     * 獲取分塊文件列表
     *
     * @param file 分塊文件目錄
     * @return List<File> 排好序的分塊文件列表
     */
    private List<File> getChunkFiles(File file) {
        // 獲取分塊文件集合
        File[] files = file.listFiles();
        assert files != null;
        List<File> fileList = Arrays.asList(files);
        return fileList.stream().sorted(Comparator.comparing(f -> Integer.valueOf(f.getName()))).collect(Collectors.toList());
    }

    /**
     * 合併文件
     *
     * @param mergeFile  合併後的文件
     * @param chunkFiles 分塊文件列表
     * @return File
     */
    private File mergeFile(File mergeFile, List<File> chunkFiles) {
        try {
            // 寫入流
            RandomAccessFile write = new RandomAccessFile(mergeFile, "rw");
            // 開始寫入
            byte[] b = new byte[1024];
            for (File chunkFile : chunkFiles) {
                RandomAccessFile read = new RandomAccessFile(chunkFile, "r");
                int len = -1;
                while ((len = read.read(b)) != -1) {
                    // 寫入數據
                    write.write(b, 0, len);
                }
                read.close();
            }
            write.close();

            return mergeFile;
        } catch (IOException e) {
            // 記錄日誌
            log.error("[合併文件分塊] 執行文件合併時發生異常, e = ", e);
            return null;
        }
    }

    /**
     * 校驗文件md5碼
     *
     * @param file    待校驗文件
     * @param fileMd5 正確的md5碼
     * @return boolean
     */
    private boolean checkMd5(File file, String fileMd5) {
        if (file == null || StringUtils.isBlank(fileMd5)) {
            return false;
        }
        // 校驗
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream(file);
            String md5Hex = DigestUtils.md5Hex(fileInputStream);
            if (fileMd5.equalsIgnoreCase(md5Hex)) {
                return true;
            }
        } catch (IOException e) {
            // 記錄日誌
            log.error("[合併文件分塊] 校驗文件md5碼發生異常, e = ", e);
        } finally {
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException ignored) {
                }
            }
        }
        return false;
    }


}

MediaFileRepository

package com.xuecheng.manage_media.dao;

import com.xuecheng.framework.domain.media.MediaFile;
import org.springframework.data.mongodb.repository.MongoRepository;


public interface MediaFileRepository extends MongoRepository<MediaFile, String> {
}

測試

先上傳文件

查看上傳後的文件

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章