学成线上-类慕课网 微服务教育网 第14天-讲义-媒资管理
1视讯处理
1.1需求分析原始视讯通常需要经过编码处理,生成m3u8和ts档案方可基于HLS协议播放视讯。通常使用者上传原始视讯,系统 自动处理成标准格式,系统对使用者上传的视讯自动编码、转换,最终生成m3u8档案和ts档案,处理流程如下:
1、使用者上传视讯成功
2、系统对上传成功的视讯自动开始编码处理
3、使用者检视视讯处理结果,没有处理成功的视讯使用者可在管理界面再次触发处理
4、视讯处理完成将视讯地址及处理结果储存到数据库视讯处理流程如下:

视讯处理程序的任务是接收视讯处理讯息进行视讯处理,业务流程如下:
1、监听MQ,接收视讯处理讯息。
2、进行视讯处理。
3、向数据库写入视讯处理结果。
视讯处理程序属于媒资管理系统的一部分,考虑提高系统的扩充套件性,将视讯处理单独定义视讯处理工程。
1.2视讯处理开发
1.2.1视讯处理工程建立
1、汇入“资料”下的视讯处理工程:xc-service-manage-media-processor

2、RabbitMQ配置
使用rabbitMQ的routing交换机模式,视讯处理程式监听视讯处理伫列,如下图:

RabbitMQ配置如下:
@Configuration
public class RabbitMQConfig {
public static final String EX_MEDIA_PROCESSTASK = "ex_media_processor";
//视讯处理伫列@Value("${xc‐service‐manage‐media.mq.queue‐media‐video‐processor}") public String queue_media_video_processtask;
//视讯处理路由@Value("${xc‐service‐manage‐media.mq.routingkey‐media‐video}") public String routingkey_media_video;
/**
*交换机配置
*@return the exchange
*/ @Bean(EX_MEDIA_PROCESSTASK)
public Exchange EX_MEDIA_VIDEOTASK() {
return ExchangeBuilder.directExchange(EX_MEDIA_PROCESSTASK).durable(true).build();
}
//宣告伫列@Bean("queue_media_video_processtask") public Queue QUEUE_PROCESSTASK() {
Queue queue = new Queue(queue_media_video_processtask,true,false,true); return queue;
}
/**
*系结伫列到交换机 .
*@param queue the queue
*@param exchange the exchange
*@return the binding
*/ @Bean
public Binding binding_queue_media_processtask(@Qualifier("queue_media_video_processtask") Queue queue, @Qualifier(EX_MEDIA_PROCESSTASK) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(routingkey_media_video).noargs();
}
}
在application.yml中配置伫列名称及routingkey
xc‐service‐manage‐media:
mq:
queue‐media‐video‐processor: queue_media_video_processor routingkey‐media‐video: routingkey_media_video
1.2.2视讯处理技术方案
如何通过程式进行视讯处理?
ffmpeg是一个可行的视讯处理程式,可以通过Java呼叫ffmpeg.exe完成视讯处理。
在java中可以使用Runtime类和Process Builder类两种方式来执行外部程式,工作中至少掌握一种。本专案使用Process Builder的方式来呼叫ffmpeg完成视讯处理。
关于Process Builder的测试如下:
@Test
public void testProcessBuilder(){
ProcessBuilder processBuilder = new ProcessBuilder();
//processBuilder.command("ping","127.0.0.1"); processBuilder.command("ipconfig");
//将标准输入流和错误输入流合并,通过标准输入流读取资讯
processBuilder.redirectErrorStream(true); try {
//启动程序
Process start = processBuilder.start();
//获取输入流
InputStream inputStream = start.getInputStream();
//转成字元输入流
InputStreamReader inputStreamReader = new InputStreamReader(inputStream,"gbk");
int len = ‐1;
char[] c = new char[1024];
StringBuffer outputString = new StringBuffer();
//读取程序输入流中的内容
while ((len= inputStreamReader.read(c))!=‐1) { String s = new String(c,0,len); outputString.append(s); System.out.print(s);
}
inputStream.close();
} catch (IOException e) { e.printStackTrace();
}
}
@Test
public void testFFmpeg(){
ProcessBuilder processBuilder = new ProcessBuilder();
//定义命令内容
List command = new ArrayList();
command.add("D:\Program Files\ffmpeg‐20180227‐fa0c9d6‐win64‐static\bin\ffmpeg.exe"); command.add("‐i");
command.add("E:\ffmpeg_test\1.avi"); command.add("‐y");//覆盖输出档案command.add("‐c:v");
command.add("libx264");
command.add("‐s"); command.add("1280x720"); command.add("‐pix_fmt"); command.add("yuv420p"); command.add("‐b:a");
command.add("63k");
command.add("‐b:v"); command.add("753k"); command.add("‐r");
command.add("18"); command.add("E:\ffmpeg_test\1.mp4"); processBuilder.command(command);
//将标准输入流和错误输入流合并,通过标准输入流读取资讯
processBuilder.redirectErrorStream(true); try {
//启动程序
Process start = processBuilder.start();
//获取输入流
InputStream inputStream = start.getInputStream();
//转成字元输入流
InputStreamReader inputStreamReader = new InputStreamReader(inputStream,"gbk"); int len = ‐1;
char[] c = new char[1024];
StringBuffer outputString = new StringBuffer();
//读取程序输入流中的内容
while ((len= inputStreamReader.read(c))!=‐1) { String s = new String(c,0,len); outputString.append(s); System.out.print(s);
}
inputStream.close();
} catch (IOException e) { e.printStackTrace();
}
}
上边的程式码已经封装成工具类,参见:

上边的工具类中:
Mp4VideoUtil.java完成avi转mp4
HlsVideoUtil.java完成mp4转hls
分别测试每个工具类的使用方法。
public static void main(String[] args) throws IOException {
//ffmpeg的路径
String ffmpeg_path = "D:\Program Files\ffmpeg‐20180227‐fa0c9d6‐win64‐ static\bin\ffmpeg.exe";//ffmpeg的安装位置
//源avi视讯的路径
String video_path = "E:\ffmpeg_test\1.avi";
// 转 换 后 mp4 文 件 的 名 称 String mp4_name = "1.mp4";
//转换后mp4档案的路径
String mp4_path = "E:\ffmpeg_test";
//建立工具类物件
Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4_path);
//开始视讯转换,成功将返回success String s = videoUtil.generateMp4(); System.out.println(s);
}
1.2.3视讯处理实现
1.2.3.1确定讯息格式
MQ讯息统一采用json格式,视讯处理生产方会向MQ传送如下讯息,视讯处理消费方接收此讯息后进行视讯处 理:
{“mediaId”:XXX}
1.2.3.2处理流程
1)接收视讯处理讯息
2)判断媒体档案是否需要处理(本视讯处理程式目前只接收avi视讯的处理) 当前只有avi档案需要处理,其它档案需要更新处理状态为“无需处理”。
3)处理前初始化处理状态为“未处理”
4)处理失败需要在数据库记录处理日志,及处理状态为“处理失败”
5)处理成功记录处理状态为“处理成功”
1.2.3.3资料模型
在MediaFile类中新增mediaFileProcess_m3u8属性记录ts档案列表,程式码如下:
//处理状态
private String processStatus;
//hls处理
private MediaFileProcess_m3u8 mediaFileProcess_m3u8;
@Data @ToString
public class MediaFileProcess_m3u8 extends MediaFileProcess {
//ts列表
private List tslist;
}
1.2.3.4视讯处理生成Mp4
1、建立Dao
视讯处理结果需要储存到媒资数据库,建立dao如下:
public interface MediaFileRepository extends MongoRepository {
}
2、在application.yml中配置ffmpeg的位置及视讯目录的根目录:
xc‐service‐manage‐media:
video‐location: F:/develop/video/
ffmpeg‐path: D:/Program Files/ffmpeg‐20180227‐fa0c9d6‐win64‐static/bin/ffmpeg.exe
3、处理任务类
在mq包下建立MediaProcessTask类,此类负责监听视讯处理伫列,并进行视讯处理。
整个视讯处理内容较多,这里分两部分实现:生成Mp4和生成m3u8,下边程式码实现了生成mp4。
@Component
public class MediaProcessTask {
private static final Logger LOGGER = LoggerFactory.getLogger(MediaProcessTask.class);
//ffmpeg绝对路径@Value("${xc‐service‐manage‐media.ffmpeg‐path}") String ffmpeg_path;
//上传档案根目录@Value("${xc‐service‐manage‐media.upload‐location}")
String serverPath;
@Autowired
MediaFileRepository mediaFileRepository;
@RabbitListener(queues = "${xc‐service‐manage‐media.mq.queue‐media‐processtask}") public void receiveMediaProcessTask(String msg) throws IOException {
Map msgMap = JSON.parseObject(msg, Map.class); LOGGER.info("receive media process task msg :{} ",msgMap);
//解析讯息
//媒资档案id
String mediaId = (String) msgMap.get("mediaId");
//获取媒资档案资讯
Optional optional = mediaFileRepository.findById(fileMd5); if(!optional.isPresent()){
return ;
}
MediaFile mediaFile = optional.get();
//媒资档案型别
String fileType = mediaFile.getFileType();
if(fileType == null || !fileType.equals("avi")){//目前只处理avi档案mediaFile.setProcessStatus("303004");//处理状态为无需处理mediaFileRepository.save(mediaFile);
return ;
}else{
mediaFile.setProcessStatus("303001");//处理状态为未处理mediaFileRepository.save(mediaFile);
}
//生成mp4
String video_path = serverPath + mediaFile.getFilePath()+mediaFile.getFileName(); String mp4_name = mediaFile.getFileId()+".mp4";
String mp4folder_path = serverPath + mediaFile.getFilePath(); Mp4VideoUtil videoUtil = new
Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4folder_path); String result = videoUtil.generateMp4();
if(result == null || !result.equals("success")){
//操作失败写入处理日志mediaFile.setProcessStatus("303003");//处理状态为处理失败
MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8(); mediaFileProcess_m3u8.setErrormsg(result); mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8); mediaFileRepository.save(mediaFile);
return ;
}
//生成m3u8...
}
}
说明:
1、原始视讯转成mp4如何判断转换成功?
根据视讯时长来判断,取原视讯和转换成功视讯的时长(时分秒),如果相等则相同。
1.2.3.5视讯处理生成m3u8
下边是完整的视讯处理任务类程式码,包括了生成m3u8及生成mp4的程式码。
@Component
public class MediaProcessTask {
private static final Logger LOGGER = LoggerFactory.getLogger(MediaProcessTask.class);
//ffmpeg绝对路径@Value("${xc‐service‐manage‐media.ffmpeg‐path}") String ffmpeg_path;
//上传档案根目录@Value("${xc‐service‐manage‐media.upload‐location}") String serverPath;
@Autowired
MediaFileRepository mediaFileRepository;
@RabbitListener(queues = "${xc‐service‐manage‐media.mq.queue‐media‐processtask}") public void receiveMediaProcessTask(String msg) throws IOException {
Map msgMap = JSON.parseObject(msg, Map.class); LOGGER.info("receive media process task msg :{} ",msgMap);
//解析讯息
//媒资档案id
String mediaId = (String) msgMap.get("mediaId");
//获取媒资档案资讯
Optional optional = mediaFileRepository.findById(fileMd5); if(!optional.isPresent()){
return ;
}
MediaFile mediaFile = optional.get();
//媒资档案型别
String fileType = mediaFile.getFileType();
if(fileType == null || !fileType.equals("avi")){//目前只处理avi档案mediaFile.setProcessStatus("303004");//处理状态为无需处理mediaFileRepository.save(mediaFile);
return ;
}else{
mediaFile.setProcessStatus("303001");//处理状态为未处理mediaFileRepository.save(mediaFile);
}
//生成mp4
String video_path = serverPath + mediaFile.getFilePath()+mediaFile.getFileName(); String mp4_name = mediaFile.getFileId()+".mp4";
String mp4folder_path = serverPath + mediaFile.getFilePath(); Mp4VideoUtil videoUtil = new
Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4folder_path); String result = videoUtil.generateMp4();
if(result == null || !result.equals("success")){
//操作失败写入处理日志mediaFile.setProcessStatus("303003");//处理状态为处理失败
MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8(); mediaFileProcess_m3u8.setErrormsg(result); mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8); mediaFileRepository.save(mediaFile);
return ;
}
//生成m3u8
video_path = serverPath + mediaFile.getFilePath()+mp4_name;//此地址为mp4的地址
String m3u8_name = mediaFile.getFileId()+".m3u8";
String m3u8folder_path = serverPath + mediaFile.getFilePath()+"hls/"; HlsVideoUtil hlsVideoUtil = new
HlsVideoUtil(ffmpeg_path,video_path,m3u8_name,m3u8folder_path); result = hlsVideoUtil.generateM3u8();
if(result == null || !result.equals("success")){
//操作失败写入处理日志mediaFile.setProcessStatus("303003");//处理状态为处理失败
MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8(); mediaFileProcess_m3u8.setErrormsg(result); mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8); mediaFileRepository.save(mediaFile);
return ;
}
//获取m3u8列表
List ts_list = hlsVideoUtil.get_ts_list();
//更新处理状态为成功mediaFile.setProcessStatus("303002");//处理状态为处理成功
MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8(); mediaFileProcess_m3u8.setTslist(ts_list); mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);
//m3u8档案url
mediaFile.setFileUrl(mediaFile.getFilePath()+"hls/"+m3u8_name); mediaFileRepository.save(mediaFile);
}
}
说明:
mp4转成m3u8如何判断转换成功?
第一、根据视讯时长来判断,同mp4转换成功的判断方法。 第二、最后还要判断m3u8档案内容是否完整。
1.3传送视讯处理讯息
当视讯上传成功后向MQ 传送视讯 处理讯息。
修改媒资管理服务的档案上传程式码,当档案上传成功向MQ传送视讯处理讯息。
1.3.1RabbitMQ配置
1、将media-processor工程下的RabbitmqConfig配置类拷贝到media工程下。 2、在media工程下配置mq伫列等资讯
修改application.yml
xc‐service‐manage‐media:
mq:
queue‐media‐video‐processor: queue_media_video_processor routingkey‐media‐video: routingkey_media_video
1.3.2修改Service
在档案合并方法中新增向mq传送视讯处理讯息的程式码:
//向MQ传送视讯处理讯息
public ResponseResult sendProcessVideoMsg(String mediaId){ Optional optional = mediaFileRepository.findById(fileMd5);
if(!optional.isPresent()){
return new ResponseResult(CommonCode.FAIL);
}
MediaFile mediaFile = optional.get();
//传送视讯处理讯息
Map msgMap = new HashMap(); msgMap.put("mediaId",mediaId);
//传送的讯息
String msg = JSON.toJSONString(msgMap); try {
this.rabbitTemplate.convertAndSend(RabbitMQConfig.EX_MEDIA_PROCESSTASK,routingkey_media_video, msg);
LOGGER.info("send media process task msg:{}",msg);
}catch (Exception e){ e.printStackTrace();
LOGGER.info("send media process task error,msg is:{},error:{}",msg,e.getMessage()); return new ResponseResult(CommonCode.FAIL);
}
return new ResponseResult(CommonCode.SUCCESS);
}
在mergechunks方法最后呼叫sendProcessVideo方法。
//状态为上传成功
mediaFile.setFileStatus("301002"); mediaFileRepository.save(mediaFile); String mediaId = mediaFile.getFileId();
//向MQ传送视讯处理讯息
sendProcessVideoMsg(mediaId);
1.4视讯处理测试
测试流程:
1、上传avi档案
2、观察日志是否传送讯息
3、观察视讯处理程序是否接收到讯息进行处理
4、观察mp4档案是否生成
5、观察m3u8及 ts档案是否生成
1.5视讯处理并发设定
程式码中使用@RabbitListener注解指定消费方法,预设情况是单执行绪监听伫列,可以观察当伫列有多个任务时消费 端每次只消费一个讯息,单执行绪处理讯息容易引起讯息处理缓慢,讯息堆积,不能最大利用硬件资源。
可以配置mq的容器工厂引数,增加并发处理数量即可实现多执行绪处理监听伫列,实现多执行绪处理讯息。
1、在RabbitmqConfig.java中新增容器工厂配置:
//消费者并发数量
public static final int DEFAULT_CONCURRENT = 10;
@Bean("customContainerFactory")
public SimpleRabbitListenerContainerFactory containerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConcurrentConsumers(DEFAULT_CONCURRENT); factory.setMaxConcurrentConsumers(DEFAULT_CONCURRENT);
configurer.configure(factory, connectionFactory); return factory;
}
2、在@RabbitListener注解中指定容器工厂
//视讯处理方法
@RabbitListener(queues = {"${xc‐service‐manage‐media.mq.queue‐media‐video‐processor}"}, containerFactory="customContainerFactory")
再次测试当伫列有多个任务时消费端的并发处理能力。
2我的媒资
2.1需求分析通过我的媒资可以查询本教育机构拥有的媒资档案,进行档案处理、删除档案、修改档案资讯等操作,具体需求如 下:
1、分页查询我的媒资档案
2、删除媒资档案
3、处理媒资档案
4、修改媒资档案资讯

2.2API
本节讲解我的媒资档案分页查询、处理媒资档案,其它功能请学员自行实现。
@Api(value = "媒体档案管理",description = "媒体档案管理界面",tags = {"媒体档案管理界面"})
public interface MediaFileControllerApi {
@ApiOperation("查询档案列表")
public QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest) ;
}
2.3服务端开发
2.3.1Dao
@Repository
public interface MediaFileDao extends MongoRepository {
}
2.3.2Service
定义findList方法实现媒资档案查询列表。
@Service
public class MediaFileService {
private static Logger logger = LoggerFactory.getLogger(MediaFileService.class); @Autowired
MediaFileRepository mediaFileRepository;
//档案列表分页查询
public QueryResponseResult findList(int page,int size,QueryMediaFileRequest queryMediaFileRequest){
//查询条件
MediaFile mediaFile = new MediaFile(); if(queryMediaFileRequest == null){
queryMediaFileRequest = new QueryMediaFileRequest();
模糊匹配
}
//查询条件匹配器
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("tag", ExampleMatcher.GenericPropertyMatchers.contains())//tag字段
.withMatcher("fileOriginalName",
ExampleMatcher.GenericPropertyMatchers.contains())//档案原始名称模糊匹配
.withMatcher("processStatus", ExampleMatcher.GenericPropertyMatchers.exact());//
处理状态精确匹配(预设)
//查询条件物件
if(StringUtils.isNotEmpty(queryMediaFileRequest.getTag())){
mediaFile.setTag(queryMediaFileRequest.getTag());
}
if(StringUtils.isNotEmpty(queryMediaFileRequest.getFileOriginalName())){ mediaFile.setFileOriginalName(queryMediaFileRequest.getFileOriginalName());
}
if(StringUtils.isNotEmpty(queryMediaFileRequest.getProcessStatus())){ mediaFile.setProcessStatus(queryMediaFileRequest.getProcessStatus());
}
//定义example例项
Example ex = Example.of(mediaFile, matcher);
page = page‐1;
//分页引数
Pageable pageable = new PageRequest(page, size);
//分页查询
Page all = mediaFileRepository.findAll(ex,pageable); QueryResult mediaFileQueryResult = new QueryResult(); mediaFileQueryResult.setList(all.getContent()); mediaFileQueryResult.setTotal(all.getTotalElements());
return new QueryResponseResult(CommonCode.SUCCESS,mediaFileQueryResult);
}
}
2.3.3Controller
@RestController
@RequestMapping("/media/file")
public class MediaFileController implements MediaFileControllerApi {
@Autowired
MediaFileService mediaFileService; @Autowired
MediaUploadService mediaUploadService;
@Override @GetMapping("/list/{page}/{size}")
public QueryResponseResult findList(@PathVariable("page") int page, @PathVariable("size") int size, QueryMediaFileRequest queryMediaFileRequest) {
//媒资档案查询
return mediaFileService.findList(page,size,queryMediaFileRequest);
}
}
2.4前端开发
2.4.1API方法
在media模组定义api方法如下:
import http from './../../../base/api/public' import querystring from 'querystring'
let sysConfig = require('@/../config/sysConfig') let apiUrl = sysConfig.xcApiUrlPre;
/*页面列表*/
export const media_list = (page,size,params) => {
//params为json格式
//使用querystring将json物件转成key/value串
let querys = querystring.stringify(params)
return http.requestQuickGet(apiUrl+'/media/file/list/'+page+'/'+size+'/?'+querys)
}
/*传送处理讯息*/
export const media_process = (id) => {
return http.requestPost(apiUrl+'/media/file/process/'+id)
}
2.4.2页面
在media模组建立media_list.vue,可参考cms系统的page_list.vue来编写此页面。
1、检视
标签:
‐input>
原始名称:
‐input>
处理状态:
v‐for="item in processStatusList"
:key="item.id"
:label="item.name"
:value="item.id">
‐option>
‐select>
查询‐button>
上传档案‐button>
‐link>
‐form>
‐table‐column>
‐table‐column>
‐table‐column>
‐table‐column>
‐table‐column>
‐table‐column>
:formatter="formatProcessStatus">
‐table‐column>
:formatter="formatCreatetime">
‐table‐column>
size="small" type="primary" plain @click="process(scope.row.fileId)">开始处理
‐button>
‐table‐column>
‐table>
:total="total" :current‐page="this.params.page" style="float:right;">
‐pagination>
‐col>
2、资料物件
import * as mediaApi from '../api/media' import utilApi from '@/common/utils'; export default{
data(){ return {
params:{ page:1,//页码
size:2,//每页显示个数
tag:'',//标签fileName:'',//档名称
processStatus:''//处理状态
},
listLoading:false, list:[],
total:0, processStatusList:[]
}
}
。。。
3、方法
methods:{
formatCreatetime(row, column){
var createTime = new Date(row.uploadTime); if (createTime) {
return utilApi.formatDate(createTime, 'yyyy‐MM‐dd hh:mm:ss');
}
},
formatProcessStatus(row,column){
var processStatus = row.processStatus; if (processStatus) {
if(processStatus == '303001'){ return "处理中";
}else if(processStatus == '303002'){ return "处理成功";
}else if(processStatus == '303003'){ return "处理失败";
}else if(processStatus == '303004'){ return "无需处理";
}
}
},
changePage(page){ this.params.page = page; this.query()
},
process (id) {
//console.log(id) mediaApi.media_process(id).then((res)=>{
console.log(res) if(res.success){
this.$message.success('开始处理,请稍后检视处理结果');
}else{
this.$message.error('操作失败,请重新整理页面重试');
}
})
},
query(){ mediaApi.media_list(this.params.page,this.params.size,this.params).then((res)=>{
console.log(res)
this.total = res.queryResult.total
this.list = res.queryResult.list
})
}
}
4、钩子方法
created(){
//预设第一页
this.params.page = Number.parseInt(this.$route.query.page||1);
},
mounted() {
//预设查询页面this.query()
//初始化处理状态
this.processStatusList = [
{
id:'',
name:'全部'
},
{
id:'303001',
name:'处理中'
},
{
id:'303002',
name:'处理成功'
},
{
id:'303003',
name:'处理失败'
},
{
id:'303004',
name:'无需处理'
}
]
}
}
3媒资与课程计划关联
3.1需求分析到目前为止,媒资管理已完成档案上传、视讯处理、我的媒资功能等基本功能。其它模组已可以使用媒资管理功 能,本节要讲解课程计划在编辑时如何选择媒资档案。
操作的业务流程如下:
1、进入课程计划修改页面
2、选择视讯
开启媒资档案查询视窗,找到该课程章节的视讯,选择此视讯。点选“选择媒资档案”开启媒资档案列表

3、 选择成功后,将在课程管理数据库储存课程计划对应在的课程视讯地址。
在课程管理数据库建立表 teachplan_media 储存课程计划与媒资关联资讯,如下:

3.2选择视讯
3.2.1Vue父子元件通讯
上一章已实现了我的媒资页面,所以媒资查询视窗页面不需要再开发,将“我的媒资页面”作为一个元件在修改课程 计划页面中引用,如下图:

修改课程计划页面为父元件,我的媒资查询页面为子元件。
问题1:
我的媒资页面在选择媒资档案时不允许显示,比如“视讯处理”按钮,该如何控制?
这时就需要父元件(修改课程计划页面)向子元件(我的媒资页面)传入一个变数,使用此变数来控制当前是否进 入选择媒资档案业务,从而控制哪些元素不显示,如下图:

问题2:
在我的媒资页面选择了媒资档案,如何将选择的媒资档案资讯传到父元件? 这时就需要子元件呼叫父元件的方法来解决此问题,如下图:

3.2.2父元件(修改课程计划)
本节实现功能:在课程计划页面开启我的媒资页面。
1、引入子元件
import mediaList from '@/module/media/page/media_list.vue'; export default {
components:{ mediaList
},
data() {
....
2、使用子元件
在父元件的检视中使用子元件,同时传入变数ischoose,并指定父元件的方法名为choosemedia 这里使用el-dialog 实现弹出视窗。
‐list>
‐dialog>
3、choosemedia方法
在父元件中定义choosemedia方法,接收子元件呼叫,引数包括:媒资档案id、媒资档案的原始名称、媒资档案
url
choosemedia(mediaId,fileOriginalName,mediaUrl){
}
4、开启子元件视窗
1)开启子元件视窗按钮定义
新增“选择视讯”按钮:
this.querymedia(data.id) }>选择视讯‐button>
效果如下:

2、开启子元件视窗方法定义querymedia方法:
methods: {
//开启查询媒资档案视窗,传入课程计划id querymedia(teachplanId){
this.activeTeachplanId = teachplanId;
this.mediaFormVisible = true;
},
3.2.3子元件(我的媒资查询)
1、定义ischoose变数,接收父元件传入的ischoose
export default{
props: ['ischoose'], data(){
2、父元件传的ischoose变数为 true时表示当前是选择媒资档案业务,需要控制页面元素是否显示
1)ischoose=true,选择按钮显示
size="small" type="primary" plain @click="choose(scope.row)">选择‐button>
‐table‐column>
2)ischoose=false,视讯处理按钮显示
size="small" type="primary" plain @click="process(scope.row.fileId)">开始处理
‐button>
‐table‐column>
3、选择媒资档案方法
使用者点选“选择”按钮将向父元件传递媒资档案资讯
choose(mediaFile){
if(mediaFile.processStatus !='303002' && mediaFile.processStatus !='303004'){ this.$message.error('该档案未处理,不允许选择');
return ;
}
if(!mediaFile.fileUrl){
this.$message.error('该档案的访问url为空,不允许选择'); return ;
}
//呼叫父元件的choosemedia方法this.$emit('choosemedia',mediaFile.fileId,mediaFile.fileOriginalName);
}
3.3储存视讯资讯
3.3.1需求分析
使用者进入课程计划页面,选择视讯,将课程计划与视讯资讯储存在课程管理数据库中。 使用者操作流程:
1、进入课程计划,点选”选择视讯“,开启我的媒资查询页面
2、为课程计划选择对应的视讯,选择“选择”
3、前端请求课程管理服务储存课程计划与视讯资讯。
3.3.2资料模型
在课程管理数据库建立表 teachplan_media 储存课程计划与媒资关联资讯,如下:

建立teachplanMedia 模型类:
@Data
@ToString @Entity
@Table(name="teachplan_media")
@GenericGenerator(name = "jpa‐assigned", strategy = "assigned") public class TeachplanMedia implements Serializable {
private static final long serialVersionUID = ‐916357110051689485L; @Id
@GeneratedValue(generator = "jpa‐assigned") @Column(name="teachplan_id")
private String teachplanId;
@Column(name="media_id") private String mediaId;
@Column(name="media_fileoriginalname") private String mediaFileOriginalName;
@Column(name="media_url") private String mediaUrl;
@Column(name="courseid")
private String courseId;
}
3.3.3API界面
此界面作为前端请求课程管理服务储存课程计划与视讯资讯的界面: 在课程管理服务增加界面:
@ApiOperation("储存媒资资讯")
public ResponseResult savemedia(TeachplanMedia teachplanMedia);
3.3.4服务端开发
3.3.3.1DAO
建立TeachplanMediaRepository用于对TeachplanMedia的操作。
public interface TeachplanMediaRepository extends JpaRepository {
}
3.3.3.2Service
//储存媒资资讯
public ResponseResult savemedia(TeachplanMedia teachplanMedia) { if(teachplanMedia == null){
ExceptionCast.cast(CommonCode.INVALIDPARAM);
}
//课程计划
String teachplanId = teachplanMedia.getTeachplanId();
//查询课程计划
Optional optional = teachplanRepository.findById(teachplanId); if(!optional.isPresent()){
ExceptionCast.cast(CourseCode.COURSE_MEDIA_TEACHPLAN_ISNULL);
}
Teachplan teachplan = optional.get();
//只允许为叶子结点课程计划选择视讯String grade = teachplan.getGrade();
if(StringUtils.isEmpty(grade) || !grade.equals("3")){ ExceptionCast.cast(CourseCode.COURSE_MEDIA_TEACHPLAN_GRADEERROR);
}
TeachplanMedia one = null; Optional teachplanMediaOptional =
teachplanMediaRepository.findById(teachplanId); if(!teachplanMediaOptional.isPresent()){
one = new TeachplanMedia();
}else{
one = teachplanMediaOptional.get();
}
//储存媒资资讯与课程计划资讯one.setTeachplanId(teachplanId); one.setCourseId(teachplanMedia.getCourseId());
one.setMediaFileOriginalName(teachplanMedia.getMediaFileOriginalName()); one.setMediaId(teachplanMedia.getMediaId()); one.setMediaUrl(teachplanMedia.getMediaUrl()); teachplanMediaRepository.save(one);
return new ResponseResult(CommonCode.SUCCESS);
}
3.3.3.3Controller
@Override @PostMapping("/savemedia")
public ResponseResult savemedia(@RequestBody TeachplanMedia teachplanMedia) { return courseService.savemedia(teachplanMedia);
}
3.3.4前端开发
3.3.4.1API方法
定义api方法,呼叫课程管理服务储存媒资资讯界面
/*储存媒资资讯*/
export const savemedia = teachplanMedia => {
return http.requestPost(apiUrl+'/course/savemedia',teachplanMedia);
}
3.3.4.2API呼叫
在课程视讯方法中呼叫api:
choosemedia(mediaId,fileOriginalName,mediaUrl){ this.mediaFormVisible = false;
//储存课程计划与视讯对应关系
let teachplanMedia = {};
teachplanMedia.teachplanId = this.activeTeachplanId; teachplanMedia.mediaId = mediaId; teachplanMedia.mediaFileOriginalName = fileOriginalName; teachplanMedia.mediaUrl = mediaUrl; teachplanMedia.courseId = this.courseid;
//储存媒资资讯到课程数据库
courseApi.savemedia(teachplanMedia).then(res=>{ if(res.success){
this.$message.success("选择视讯成功")
}else{
this.$message.error(res.message)
}
})
},
3.3.4测试
1、向叶子结点课程计划储存媒资资讯操作结果:储存成功
2、向非叶子结点课程计划储存媒资资讯操作结果:储存失败
3.4查询视讯资讯
3.4.1需求分析
课程计划的视讯资讯储存后在页面无法检视,本节解决课程计划页面显示相关联的媒资资讯。 解决方案:
在获取课程计划树结点资讯时将关联的媒资资讯一并查询,并在前端显示,下图说明了课程计划显示的区域。

3.4.2Dao
修改课程计划查询的Dao:
1、修改模型
在课程计划结果资讯中新增媒资资讯
@Data @ToString
public class TeachplanNode extends Teachplan { List children;
//媒资资讯
private String mediaId;
private String mediaFileOriginalName;
}
2、修改sql语句,新增关联查询媒资资讯新增mediaId、mediaFileOriginalName
SELECT
a.id one_id, a.pname one_name, a.grade one_grade,
a.orderby one_orderby, b.id two_id,
b.pname two_name, b.grade two_grade, b.orderby two_orderby, c.id three_id,
c.pname three_name, c.grade three_grade, c.orderby three_orderby, media.media_id mediaId,
media.media_fileoriginalname mediaFileOriginalName FROM
teachplan a LEFT JOIN teachplan b ON a.id = b.parentid
LEFT JOIN teachplan c ON b.id = c.parentid
LEFT JOIN teachplan_media media ON c.id = media.teachplan_id WHERE a.parentid = '0'
and a.courseid=#{courseId}
ORDER BY a.orderby, b.orderby, c.orderby
3.4.3页面查询视讯
课程计划结点资讯已包括媒资资讯,可在页面获取资讯后显示:
this.querymedia(data.id) }>
{data.mediaFileOriginalName} 选择视讯‐button>
效果如下:

选择视讯后立即重新整理课程计划树,在提交成功后,新增查询课程计划程式码:this.findTeachplan(),完整程式码如下:
choosemedia(mediaId,fileOriginalName,mediaUrl){ this.mediaFormVisible = false;
//储存课程计划与视讯对应关系
let teachplanMedia = {};
teachplanMedia.teachplanId = this.activeTeachplanId; teachplanMedia.mediaId = mediaId; teachplanMedia.mediaFileOriginalName = fileOriginalName; teachplanMedia.mediaUrl = mediaUrl; teachplanMedia.courseId = this.courseid;
//储存媒资资讯到课程数据库
courseApi.savemedia(teachplanMedia).then(res=>{ if(res.success){
this.$message.success("选择视讯成功")
//查询课程计划
this.findTeachplan()
}else{
this.$message.error(res.message)
}
})
},