準備環境(必須)
配置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> <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> {
}
測試
先上傳文件
查看上傳後的文件