学成在线笔记九:在线学习

准备环境(必须)

配置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> {
}

测试

先上传文件

查看上传后的文件

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