准备环境(必须)
配置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> {
}
测试
先上传文件
查看上传后的文件