当前位置: 代码迷 >> 综合 >> 微服务[学成在线] day14:媒资管理
  详细解决方案

微服务[学成在线] day14:媒资管理

热度:48   发布时间:2024-01-31 16:17:18.0

? 知识点概览

为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。

本章节为【学成在线】项目的 day14 的内容

  • 视频上传成功后通过 RabbitMQ 进行消息发送,再通过 视频处理服务 对视频进行格式转换,以及 m3u8 视频文件的生成。
  • 实现媒资信息的浏览
  • Vue 跨组件间的通讯实战,实现课程计划与已上传的媒资文件的关联

目录

知识点结合实战应用会更有意义,所以这里不再对单个知识点进行拆分成单个笔记,内容会比较多,可以根据目录进行按需查阅。

文章目录

  • ? 知识点概览
  • 目录
  • 一、视频处理
    • 0x01 需求分析
    • 0x02 视频处理开发
      • 视频处理工程创建
      • 视频处理技术方案
      • 视频处理实现
        • 1、确定消息格式
        • 2、处理流程
        • 3、数据模型
        • 4、视频处理生成 MP4
        • 5、视频处理生成 m3u8
    • 0x03 发送视频处理消息
      • 配置RabbitMQ
      • 配置Service
    • 0x04 视频处理测试
    • 0x05 视频处理并发设置
    • 0x06 完整代码
      • RabbitMQConfig
      • MediaProcessTask
      • MediaUploadServiceImpl
  • 二、我的媒资
    • 0x01 需求分析
    • 0x02 API
    • 0x03 服务端开发
      • Dao
      • Service
      • Controller
      • 接口测试
    • 0x04 前端开发
      • API方法
      • 页面
  • 三、媒资与课程计划关联
    • 0x01 需求分析
    • 0x02 选择视频
      • Vue 父子组件通信
      • 父组件:修改课程计划
      • 子组件:我的媒资查询
      • 页面效果
      • 全部代码
        • course_plan.vue
        • media_list.vue
    • 0x03 保存视频信息
      • 需求分析
      • 数据模型
      • API接口
      • 服务端开发
        • 1、Controller
        • 2、Dao
        • 3、Service
      • 前端开发
        • 1、API方法
        • 2、API调用
        • 3、测试
    • 0x04 查询视频信息
      • 需求分析
      • Dao
      • 页面查询视频
  • ? 认识作者

一、视频处理

0x01 需求分析

原始视频通常需要经过编码处理,生成 m3u8ts 文件方可基于 HLS 协议播放视频。通常用户上传原始视频,系统自动处理成标准格式,系统对用户上传的视频自动编码、转换,最终生成m3u8 文件和 ts 文件,处理流程如下:

1、用户上传视频成功

2、系统对上传成功的视频自动开始编码处理

3、用户查看视频处理结果,没有处理成功的视频用户可在管理界面再次触发处理

4、视频处理完成将视频地址及处理结果保存到数据库

视频处理流程如下:

视频处理进程的任务是接收视频处理消息进行视频处理,业务流程如下:

1、监听 MQ,接收视频处理消息。

2、进行视频处理。

3、向数据库写入视频处理结果。

视频处理进程属于媒资管理系统的一部分,考虑提高系统的扩展性,将视频处理单独定义视频处理工程。

0x02 视频处理开发

视频处理工程创建

1、导入“资料” 下的视频处理工程:xc-service-manage-media-processor

2、RabbitMQ 配置

使用 rabbitMQrouting 交换机模式,视频处理程序监听视频处理队列,如下图:

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 */@Beanpublic 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_processorroutingkey‐media‐video: routingkey_media_video

视频处理技术方案

如何通过程序进行视频处理?

ffmpeg 是一个可行的视频处理程序,可以通过 Java 调用 ffmpeg.exe 完成视频处理。

java 中可以使用 Runtime 类和 Process Builder 类两种方式来执行外部程序,工作中至少掌握一种。

本项目使用 Process Builder 的方式来调用 ffmpeg 完成视频处理。

关于 Process Builder 的测试如下:

//测试ping命令
@Test
public void testProcessBuilder() throws IOException {//创建ProcessBuilder对象ProcessBuilder processBuilder = new ProcessBuilder();//设置执行的第三方程序(命令)List<String> cmds = new ArrayList<>();cmds.add("ping");cmds.add("127.0.0.1");processBuilder.command(cmds);//合并标准输入流和错误输出processBuilder.redirectErrorStream(true);Process start = processBuilder.start();//获取输入流InputStream inputStream = start.getInputStream();//将输入流转换为字符输入流InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "gbk");//获取流的数据int len = -1;//数据缓冲区char[] cache = new char[1024];StringBuffer stringBuffer = new StringBuffer();while ((len = inputStreamReader.read(cache)) != -1) {//获取缓冲区内的数据String outStr = new String(cache, 0, len);System.out.println(outStr);stringBuffer.append(outStr);}inputStream.close();
}//测试使用工具类将avi转成mp4
@Test
public void testProcessMp4() throws IOException {ProcessBuilder processBuilder = new ProcessBuilder();//定义命令内容List<String> command = new ArrayList<>();command.add("D:/soft/ffmpeg-20200315-c467328-win64-static/bin/ffmpeg.exe");command.add("-i");command.add("E:/temp/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:/temp/1.mp4");processBuilder.command(command);//将标准输入流和错误输入流合并,通过标准输入流读取信息processBuilder.redirectErrorStream(true);Process start = processBuilder.start();InputStream inputStream = start.getInputStream();InputStreamReader streamReader = new InputStreamReader(inputStream, "gbk");//获取输入流数据int len = -1;//数据缓冲区char[] cache = new char[1024];StringBuffer stringBuffer = new StringBuffer();while ((len=streamReader.read(cache)) != -1){//从缓冲区获取数据String out = new String(cache, 0, len);System.out.println(out);stringBuffer.append(out);}inputStream.close();
}

上边的代码已经封装成工具类,参见:

上边的工具类中:

Mp4VideoUtil.java 完成 avimp4

HlsVideoUtil.java 完成 mp4hls

分别测试每个工具类的使用方法。

public static void main(String[] args) throws IOException {String ffmpeg_path = "D:/soft/ffmpeg-20200315-c467328-win64-static/bin/ffmpeg.exe";//ffmpeg的安装位置String video_path = "E:\\temp\\1.avi";String mp4_name = "2.mp4";String mp4_path = "E:\\temp\\";Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4_path);String s = videoUtil.generateMp4();System.out.println(s);
}

视频处理实现

1、确定消息格式

MQ 消息统一采用 json 格式,视频处理生产方会向 MQ 发送如下消息,视频处理消费方接收此消息后进行视频处
理:

{"mediaId":XXX}

2、处理流程

1)接收视频处理消息

2)判断媒体文件是否需要处理(本视频处理程序目前只接收avi 视频的处理)当前只有 avi 文件需要处理,其它文件需要更新处理状态为 “无需处理”。

3)处理前初始化处理状态为 “未处理

4)处理失败需要在数据库记录处理日志,及处理状态为 “处理失败

5)处理成功记录处理状态为 “处理成功

3、数据模型

MediaFile 类中添加 mediaFileProcess_m3u8 属性记录 ts 文件列表,代码如下 :

//处理状态
private String processStatus;
//hls处理
private MediaFileProcess_m3u8 mediaFileProcess_m3u8;

MediaFileProcess_m3u8 如下

@Data
@ToString
public class MediaFileProcess_m3u8 extends MediaFileProcess {
//ts列表
private List<String> tslist;
}

4、视频处理生成 MP4

1)创建 dao

视频处理结果需要保存到媒资数据库,创建 dao 如下:

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

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.video-location}")String serverPath;@AutowiredMediaFileRepository mediaFileRepository;@RabbitListener(queues = "${xc-service-manage-media.mq.queue-media-video-processor}")public void receiveMediaProcessTask(String msg){//将接收到的消息转换为json数据Map msgMap = JSON.parseObject(msg);LOGGER.info("receive media process task msg :{} ",msgMap);//解析消息//媒资文件idString mediaId = (String) msgMap.get("mediaId");//获取媒资文件信息Optional<MediaFile> byId = mediaFileRepository.findById(mediaId);if(!byId.isPresent()){return;}MediaFile mediaFile = byId.get();//媒资文件类型String fileType = mediaFile.getFileType();//目前只处理avi文件if(fileType == null || !fileType.equals("avi")){mediaFile.setProcessStatus("303004"); // 处理状态为无需处理mediaFileRepository.save(mediaFile);}else{mediaFile.setProcessStatus("303001"); //处理状态为未处理}//生成MP4String videoPath = serverPath + mediaFile.getFilePath() + mediaFile.getFileName();String mp4Name = mediaFile.getFileId() + ".mp4";String mp4FloderPath = serverPath  + mediaFile.getFilePath();Mp4VideoUtil mp4VideoUtil = new Mp4VideoUtil(ffmpeg_path, videoPath, mp4Name, mp4FloderPath);String result = mp4VideoUtil.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 如何判断转换成功?

根据视频时长来判断,取原视频和转换成功视频的时长(时分秒),如果相等则相同。

5、视频处理生成 m3u8

下边是完整的视频处理任务类代码,包括了生成 m3u8 及生成 mp4 的代码

package com.xuecheng.manage_media_process.mq;import com.alibaba.fastjson.JSON;
import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.MediaFileProcess_m3u8;
import com.xuecheng.framework.utils.HlsVideoUtil;
import com.xuecheng.framework.utils.Mp4VideoUtil;
import com.xuecheng.manage_media_process.dao.MediaFileRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import java.util.List;
import java.util.Map;
import java.util.Optional;@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.video-location}")String serverPath;@AutowiredMediaFileRepository mediaFileRepository;@RabbitListener(queues = "${xc-service-manage-media.mq.queue-media-video-processor}")public void receiveMediaProcessTask(String msg){//将接收到的消息转换为json数据Map msgMap = JSON.parseObject(msg);LOGGER.info("receive media process task msg :{} ",msgMap);//解析消息//媒资文件idString mediaId = (String) msgMap.get("mediaId");//获取媒资文件信息Optional<MediaFile> byId = mediaFileRepository.findById(mediaId);if(!byId.isPresent()){return;}MediaFile mediaFile = byId.get();//媒资文件类型String fileType = mediaFile.getFileType();//目前只处理avi文件if(fileType == null || !fileType.equals("avi")){mediaFile.setProcessStatus("303004"); // 处理状态为无需处理mediaFileRepository.save(mediaFile);}else{mediaFile.setProcessStatus("303001"); //处理状态为未处理}//生成MP4String videoPath = serverPath + mediaFile.getFilePath() + mediaFile.getFileName();String mp4Name = mediaFile.getFileId() + ".mp4";String mp4FloderPath = serverPath  + mediaFile.getFilePath();Mp4VideoUtil mp4VideoUtil = new Mp4VideoUtil(ffmpeg_path, videoPath, mp4Name, mp4FloderPath);String result = mp4VideoUtil.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列表//生成m3u8String mp4VideoPath = serverPath + mediaFile.getFilePath()+ mp4Name;//此地址为mp4的地址String m3u8Name = mediaFile.getFileId()+".m3u8";String m3u8FolderPath = serverPath + mediaFile.getFilePath()+"hls/";//调用工具类进行生成m3u8HlsVideoUtil hlsVideoUtil = new HlsVideoUtil(ffmpeg_path, mp4VideoPath, m3u8Name, m3u8FolderPath);String m3u8Result = hlsVideoUtil.generateM3u8();if(m3u8Result==null || !m3u8Result.equals("success")){//操作失败写入处理日志mediaFile.setProcessStatus("303003");//处理状态为处理失败MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();mediaFileProcess_m3u8.setErrormsg(m3u8Result);mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);mediaFileRepository.save(mediaFile);return ;}//获取m3u8列表List<String> 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文件urlmediaFile.setFileUrl(mediaFile.getFilePath()+"hls/"+m3u8Name);mediaFileRepository.save(mediaFile);}
}

0x03 发送视频处理消息

当视频上传成功后向 MQ 发送视频 处理消息。

修改媒资管理服务的文件上传代码,当文件上传成功向 MQ 发送视频处理消息。

配置RabbitMQ

1、将media-processor 工程下的 RabbitmqConfig 配置类拷贝到 media 工程下。

2、在 media 工程下配置 mq 队列等信息

修改 application.yml

xc-service-manage-media:mq:queue-media-video-processor: queue_media_video_processorroutingkey-media-video: routingkey_media_video

配置Service

在文件合并方法中添加向 mq 发送视频处理消息的代码:

//视频处理路由
@Value("${xc-service-manage-media.mq.routingkey-media-video}")
public String routingkey_media_video;@Autowired
RabbitTemplate rabbitTemplate;//向MQ发送视频处理消息
private ResponseResult sendProcessVideoMsg(String mediaId){Optional<MediaFile> optional = mediaFileRepository.findById(mediaId);if(!optional.isPresent()){return new ResponseResult(CommonCode.FAIL);}MediaFile mediaFile = optional.get();//发送视频处理消息Map<String,String> 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);
......

0x04 视频处理测试

测试流程:

1、上传avi文件

2、观察日志是否发送消息

3、观察视频处理进程是否接收到消息进行处理

4、观察 mp4 文件是否生成

5、观察 m3u8ts 文件是否生成

0x05 视频处理并发设置

代码中使用 @RabbitListener 注解指定消费方法,默认情况是单线程监听队列,可以观察当队列有多个任务时消费端每次只消费一个消息,单线程处理消息容易引起消息处理缓慢,消息堆积,不能最大利用硬件资源。

可以配置 mq 的容器工厂参数,增加并发处理数量即可实现多线程处理监听队列,实现多线程处理消息。

1、在 RabbitmqConfig.java 中添加容器工厂配置:

/*** 多线程处理消息* @param configurer* @param connectionFactory* @return*/
@Bean("customContainerFactory")
public SimpleRabbitListenerContainerFactory containerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactoryconnectionFactory) {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")

再次测试当队列有多个任务时消费端的并发处理能力。

0x06 完整代码

RabbitMQConfig

package com.xuecheng.manage_media_process.config;import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@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;//消费者并发数量public static final int DEFAULT_CONCURRENT = 10;/*** 交换机配置* @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*/@Beanpublic 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();}/*** 多线程处理消息* @param configurer* @param connectionFactory* @return*/@Bean("customContainerFactory")public SimpleRabbitListenerContainerFactory containerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactoryconnectionFactory) {SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();factory.setConcurrentConsumers(DEFAULT_CONCURRENT);factory.setMaxConcurrentConsumers(DEFAULT_CONCURRENT);configurer.configure(factory,connectionFactory);return factory;}
}

MediaProcessTask

package com.xuecheng.manage_media_process.mq;import com.alibaba.fastjson.JSON;
import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.MediaFileProcess_m3u8;
import com.xuecheng.framework.utils.HlsVideoUtil;
import com.xuecheng.framework.utils.Mp4VideoUtil;
import com.xuecheng.manage_media_process.dao.MediaFileRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import java.util.List;
import java.util.Map;
import java.util.Optional;@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.video-location}")String serverPath;@AutowiredMediaFileRepository mediaFileRepository;@RabbitListener(queues = "${xc-service-manage-media.mq.queue-media-video-processor}" , containerFactory="customContainerFactory")public void receiveMediaProcessTask(String msg){//将接收到的消息转换为json数据Map msgMap = JSON.parseObject(msg);LOGGER.info("receive media process task msg :{} ",msgMap);//解析消息//媒资文件idString mediaId = (String) msgMap.get("mediaId");//获取媒资文件信息Optional<MediaFile> byId = mediaFileRepository.findById(mediaId);if(!byId.isPresent()){return;}MediaFile mediaFile = byId.get();//媒资文件类型String fileType = mediaFile.getFileType();//目前只处理avi文件if(fileType == null || !fileType.equals("avi")){mediaFile.setProcessStatus("303004"); // 处理状态为无需处理mediaFileRepository.save(mediaFile);}else{mediaFile.setProcessStatus("303001"); //处理状态为未处理}//生成MP4String videoPath = serverPath + mediaFile.getFilePath() + mediaFile.getFileName();String mp4Name = mediaFile.getFileId() + ".mp4";String mp4FloderPath = serverPath  + mediaFile.getFilePath();Mp4VideoUtil mp4VideoUtil = new Mp4VideoUtil(ffmpeg_path, videoPath, mp4Name, mp4FloderPath);String result = mp4VideoUtil.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列表//生成m3u8String mp4VideoPath = serverPath + mediaFile.getFilePath()+ mp4Name;//此地址为mp4的地址String m3u8Name = mediaFile.getFileId()+".m3u8";String m3u8FolderPath = serverPath + mediaFile.getFilePath()+"hls/";//调用工具类进行生成m3u8HlsVideoUtil hlsVideoUtil = new HlsVideoUtil(ffmpeg_path, mp4VideoPath, m3u8Name, m3u8FolderPath);String m3u8Result = hlsVideoUtil.generateM3u8();if(m3u8Result==null || !m3u8Result.equals("success")){//操作失败写入处理日志mediaFile.setProcessStatus("303003");//处理状态为处理失败MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();mediaFileProcess_m3u8.setErrormsg(m3u8Result);mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);mediaFileRepository.save(mediaFile);return ;}//获取m3u8列表List<String> 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文件urlmediaFile.setFileUrl(mediaFile.getFilePath()+"hls/"+m3u8Name);mediaFileRepository.save(mediaFile);}
}

MediaUploadServiceImpl

package com.xuecheng.manage_media.service.impl;import com.alibaba.fastjson.JSON;
import com.netflix.discovery.converters.Auto;
import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.response.CheckChunkResult;
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.model.response.ResponseResult;
import com.xuecheng.manage_media.config.RabbitMQConfig;
import com.xuecheng.manage_media.controller.MediaUploadController;
import com.xuecheng.manage_media.dao.MediaFileRepository;
import com.xuecheng.manage_media.service.MediaUploadService;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;import javax.jws.Oneway;
import java.io.*;
import java.util.*;@Service
class MediaUploadServiceImpl implements MediaUploadService {private final static Logger LOGGER = LoggerFactory.getLogger(MediaUploadController.class);@AutowiredMediaFileRepository mediaFileRepository;//上传文件根目录@Value("${xc-service-manage-media.upload-location}")String uploadPath;/*** 检查文件信息是否已经存在本地以及mongodb内,其中一者不存在则重新注册* @param fileMd5 文件md5值* @param fileExt 文件扩展名* @return 文件路径*/@Overridepublic ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {//1.检查文件在磁盘上是否存在//2.检查文件信息在mongodb上是否存在//获取文件所属目录以及文件路径String fileFloderPath = this.getFileFloderPath(fileMd5);String filePath = this.getFileFullPath(fileMd5, fileExt);File file = new File(filePath);boolean exists = file.exists();//查询mongodb上的文件信息Optional<MediaFile> optional = mediaFileRepository.findById(fileMd5);if(exists && optional.isPresent()){ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST);}//其中一者不存在则重新注册文件信息File fileFloder = new File(fileFloderPath);if(!fileFloder.exists()){//创建文件目录fileFloder.mkdirs();}return new ResponseResult(CommonCode.SUCCESS);}/*** 检查文件块是否存在* @param fileMd5 文件md5* @param chunk 块编号* @param chunkSize 块大小* @return CheckChunkResult*/@Overridepublic CheckChunkResult checkChunk(String fileMd5, Integer chunk, Integer chunkSize) {//获取文件块路径String chunkFloder = this.getChunkFloderPath(fileMd5);File chunkFile = new File(chunkFloder + chunk);if(chunkFile.exists()){return new CheckChunkResult(CommonCode.SUCCESS, true);}return new CheckChunkResult(CommonCode.SUCCESS, false);}/*** 上传分块文件* @param file 上传的文件* @param chunk 分块号* @param fileMd5 文件MD5* @return*/@Overridepublic ResponseResult uploadChunk(MultipartFile file, Integer chunk, String fileMd5) {//获取分块文件所属目录String chunkFloder = this.getChunkFloderPath(fileMd5);InputStream inputStream = null;FileOutputStream fileOutputStream = null;try {inputStream = file.getInputStream();fileOutputStream = new FileOutputStream(chunkFloder + chunk);IOUtils.copy(inputStream,fileOutputStream);} catch (IOException e) {//文件保存失败e.printStackTrace();LOGGER.error("upload chunk file fail:{}",e.getMessage());ExceptionCast.cast(MediaCode.CHUNK_FILE_UPLOAD_FAIL);}return new ResponseResult(CommonCode.SUCCESS);}/*** 合并文件块信息* @param fileMd5 文件MD5* @param fileName 文件名称* @param fileSize 文件大小* @param mimetype 文件类型* @param fileExt 文件拓展名* @return ResponseResult*/@Overridepublic ResponseResult mergeChunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {//获取文件块路径String chunkFloderPath = getChunkFloderPath(fileMd5);//合并文件路径String fileFullPath = this.getFileFullPath(fileMd5, fileExt);File mergeFile = new File(fileFullPath);//创建合并文件,如果存在则先删除再创建if(mergeFile.exists()){mergeFile.delete();}boolean newFile = false;try {newFile = mergeFile.createNewFile();} catch (IOException e) {e.printStackTrace();LOGGER.error("mergechunks..create mergeFile fail:{}",e.getMessage());}if(!newFile){//文件创建失败ExceptionCast.cast(MediaCode.MERGE_FILE_CREATEFAIL);}//获取块文件列表,此列表是已经排序好的List<File> chunkFiles = this.getChunkFiles(chunkFloderPath);//合并文件mergeFile = this.mergeFile(mergeFile, chunkFiles);if(mergeFile == null){ExceptionCast.cast(MediaCode.MERGE_FILE_FAIL);}//校验文件boolean checkResult = this.checkFileMd5(mergeFile, fileMd5);if(!checkResult){ExceptionCast.cast(MediaCode.MERGE_FILE_CHECKFAIL);}//将文件信息保存到数据库MediaFile mediaFile = new MediaFile();mediaFile.setFileId(fileMd5);mediaFile.setFileName(fileMd5+"."+fileExt);mediaFile.setFileOriginalName(fileName);//文件路径保存相对路径String filePath = this.getFilePath(fileMd5,fileExt);mediaFile.setFilePath(this.getFilePath(fileMd5,fileExt));mediaFile.setFileUrl(filePath + fileName + "." + fileExt);mediaFile.setFileSize(fileSize);mediaFile.setUploadTime(new Date());mediaFile.setMimeType(mimetype);mediaFile.setFileType(fileExt);//状态为上传成功mediaFile.setFileStatus("301002");MediaFile save = mediaFileRepository.save(mediaFile);//向MQ发送视频处理消息this.sendProcessVideoMsg(fileMd5);return new ResponseResult(CommonCode.SUCCESS);}//视频处理路由@Value("${xc-service-manage-media.mq.routingkey-media-video}")public String routingkey_media_video;@AutowiredRabbitTemplate rabbitTemplate;//向MQ发送视频处理消息private ResponseResult sendProcessVideoMsg(String mediaId){Optional<MediaFile> optional = mediaFileRepository.findById(mediaId);if(!optional.isPresent()){return new ResponseResult(CommonCode.FAIL);}MediaFile mediaFile = optional.get();//发送视频处理消息Map<String,String> 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);}//校验文件MD5private boolean checkFileMd5(File mergeFile, String fileMd5) {if(mergeFile == null || StringUtils.isEmpty(fileMd5)){return false;}//进行md5校验try {FileInputStream fileInputStream = new FileInputStream(mergeFile);//得到文件的MD5String md5Hex = DigestUtils.md5Hex(fileInputStream);//比较两个MD5值if(md5Hex.equalsIgnoreCase(fileMd5)){return true;}} catch (FileNotFoundException e) {e.printStackTrace();LOGGER.error("未找到该文件 {}",e.getMessage());} catch (IOException e) {e.printStackTrace();}return false;}//合并文件private File mergeFile(File mergeFile, List<File> chunkFiles) {try {//创建写文件对象RandomAccessFile raf_write = new RandomAccessFile(mergeFile,"rw");//遍历分块文件开始合并//读取文件缓冲区byte[] b = new byte[1024];for(File chunkFile:chunkFiles){RandomAccessFile raf_read = new RandomAccessFile(chunkFile,"r");int len = -1;//读取分块文件while((len = raf_read.read(b))!= -1){//向合并文件中写数据raf_write.write(b,0,len);} raf_read.close();} raf_write.close();} catch (Exception e) {e.printStackTrace();LOGGER.error("merge file error:{}",e.getMessage());return null;} return mergeFile;}//获取块文件列表private List<File> getChunkFiles(String chunkFloderPath) {//块文件目录File chunkFolder = new File(chunkFloderPath);//分块文件列表File[] fileArray = chunkFolder.listFiles();//将分块列表转为集合,便于排序ArrayList<File> fileList = new ArrayList<>(Arrays.asList(fileArray));//从小到大排序,按名称升序Collections.sort(fileList, new Comparator<File>() {@Overridepublic int compare(File o1, File o2) {//比较两个文件的名称if (Integer.parseInt(o1.getName()) < Integer.parseInt(o2.getName())) {return -1;}return 1;}});return fileList;}//获取文件块路径private String getChunkFloderPath(String fileMd5) {//获取分块文件所属目录String fileFloderPath = this.getFileFloderPath(fileMd5);String chunkFloder = fileFloderPath + "chunk/";File fileChunkFloder = new File(chunkFloder);//如果分块所属目录不存在则创建if(!fileChunkFloder.exists()){fileChunkFloder.mkdirs();}return chunkFloder;}/*** 根据文件md5得到文件的所属目录* 规则:* 一级目录:md5的第一个字符* 二级目录:md5的第二个字符* 三级目录:md5*/private String getFileFloderPath(String fileMd5){String floderPath = uploadPath + "/" + fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" +fileMd5  + "/";return floderPath;}/*** 获取全文件路径* 文件名:md5+文件扩展名*/private String getFileFullPath(String fileMd5, String fileExt){String floderPath = this.getFileFloderPath(fileMd5);String filePath = floderPath + fileMd5 + "." + fileExt;return filePath;}/*** 获取文件路径* 文件名:md5+文件扩展名*/private String getFilePath(String fileMd5, String fileExt){String filePath = "/" + fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/";return filePath;}
}

二、我的媒资

0x01 需求分析

通过我的媒资可以查询本教育机构拥有的媒资文件,进行文件处理、删除文件、修改文件信息等操作,具体需求如
下:

1、分页查询我的媒资文件

2、删除媒资文件

3、处理媒资文件

4、修改媒资文件信息

0x02 API

本节讲解我的媒资文件分页查询、处理媒资文件,其它功能请学员自行实现

@Api(value = "媒体文件管理",description = "媒体文件管理接口",tags = {"媒体文件管理接口"})
public interface MediaFileControllerApi {@ApiOperation("查询文件列表")public QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest);
}

0x03 服务端开发

Dao

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

Service

定义 findList 方法实现媒资文件查询列表

package com.xuecheng.manage_media.service;import com.xuecheng.framework.domain.media.request.QueryMediaFileRequest;
import com.xuecheng.framework.model.response.QueryResponseResult;public interface MediaFileService {/*** 查询媒体问价内信息* @param page 页码* @param size 每页数量* @param queryMediaFileRequest 查询条件* @return QueryResponseResult*/public QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest);
}

实现

package com.xuecheng.manage_media.service.impl;import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.request.QueryMediaFileRequest;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.QueryResponseResult;
import com.xuecheng.framework.model.response.QueryResult;
import com.xuecheng.manage_media.dao.MediaFileRepository;
import com.xuecheng.manage_media.service.MediaFileService;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;@Service
public class MediaFileServiceImpl implements MediaFileService {private static Logger logger = LoggerFactory.getLogger(MediaFileService.class);@AutowiredMediaFileRepository mediaFileRepository;/*** 分页查询文件信息* @param page 页码* @param size 每页数量* @param queryMediaFileRequest 查询条件* @return QueryResponseResult*/@Overridepublic QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest) {MediaFile mediaFile = new MediaFile();//查询条件if(queryMediaFileRequest == null){queryMediaFileRequest = new QueryMediaFileRequest();}//查询条件匹配器ExampleMatcher exampleMatcher = ExampleMatcher.matching().withMatcher("tag", ExampleMatcher.GenericPropertyMatchers.contains()) //模糊匹配.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<MediaFile> example = Example.of(mediaFile, exampleMatcher);//校验page和size参数的合法性,并设置默认值if(page <=0){page = 0;}else{page = page -1;}if(size <=0){size = 10;}//分页对象PageRequest pageRequest = new PageRequest(page, size);//分页查询Page<MediaFile> all = mediaFileRepository.findAll(example, pageRequest);//设置响应对象属性QueryResult<MediaFile> mediaFileQueryResult = new QueryResult<MediaFile>();mediaFileQueryResult.setList(all.getContent());mediaFileQueryResult.setTotal(all.getTotalElements());return new QueryResponseResult(CommonCode.SUCCESS,mediaFileQueryResult);}
}

Controller

@RestController
@RequestMapping("/media/file")
public class MediaFileController implements MediaFileControllerApi {@AutowiredMediaFileService mediaFileService;@GetMapping("/list/{page}/{size}")@Overridepublic QueryResponseResult findList(@PathVariable("page") int page,@PathVariable("size") int size, QueryMediaFileRequest queryMediaFileRequest) {//媒资文件信息查询return mediaFileService.findList(page,size,queryMediaFileRequest);}
}

接口测试

使用 swagger 进行接口测试

0x04 前端开发

API方法

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)
}

页面

media 模块创建 media_list.vue,可参考 cms系统的 page_list.vue 来编写此页面。

1、视图

<template><div><!--查询表单--><el-form :model="params">标签:<el-input v-model="params.tag" style="width:160px"></el-input>原始名称:<el-input v-model="params.fileOriginalName" style="width:160px"></el-input>处理状态:<el-select v-model="params.processStatus" placeholder="请选择处理状态"><el-optionv-for="item in processStatusList":key="item.id":label="item.name":value="item.id"></el-option></el-select><br/><el-button type="primary" v-on:click="query" size="small">查询</el-button><router-link class="mui-tab-item" :to="{path:'/upload'}"><el-button  type="primary" size="small" v-if="ischoose != true">上传文件</el-button></router-link></el-form><!--列表--><el-table :data="list" highlight-current-row v-loading="listLoading" style="width: 100%;"><el-table-column type="index" width="30"></el-table-column><el-table-column prop="fileOriginalName" label="原始文件名称" width="220"></el-table-column><el-table-column prop="fileName" label="文件名称" width="220"></el-table-column><el-table-column prop="fileUrl" label="访问url" width="260"></el-table-column><el-table-column prop="tag" label="标签" width="100"></el-table-column><el-table-column prop="fileSize" label="文件大小" width="120"></el-table-column><el-table-column prop="processStatus" label="处理状态" width="100" :formatter="formatProcessStatus"></el-table-column><el-table-column prop="uploadTime" label="创建时间" width="110" :formatter="formatCreatetime"></el-table-column><el-table-column label="开始处理" width="" v-if="ischoose != true"><template slot-scope="scope"><el-buttonsize="small" type="primary" plain @click="process(scope.row.fileId)">开始处理</el-button></template></el-table-column><el-table-column label="选择" width="80" v-if="ischoose == true"><template slot-scope="scope"><el-buttonsize="small" type="primary" plain @click="choose(scope.row)">选择</el-button></template></el-table-column></el-table><!--分页--><el-col :span="24" class="toolbar"><el-pagination background layout="prev, pager, next" @current-change="changePage" :page-size="this.params.size":total="total" :current-page="this.params.page"style="float:right;"></el-pagination></el-col></div>
</template>

2、数据对象、方法、钩子函数

<script>import * as mediaApi from '../api/media'import utilApi from '@/common/utils';export default{props: ['ischoose'],// 页面数据data(){return {params:{page:1,//页码size:10,//每页显示个数tag:'',//标签fileName:'',//文件名称processStatus:''//处理状态},listLoading:false,list:[],total:0,processStatusList:[]}},//方法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 "无需处理";}}},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,mediaFile.fileUrl);},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.totalthis.list = res.queryResult.list})}},//页面初始化完成前钩子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:'无需处理'}]}}
</script>

三、媒资与课程计划关联

0x01 需求分析

到目前为止,媒资管理已完成文件上传、视频处理、我的媒资功能等基本功能。其它模块已可以使用媒资管理功
能,本节要讲解课程计划在编辑时如何选择媒资文件。

操作的业务流程如下:

1、进入课程计划修改页面

2、选择视频

打开媒资文件查询窗口,找到该课程章节的视频,选择此视频。

点击 “选择媒资文件” 打开媒资文件列表

3、 选择成功后,将在课程管理数据库保存课程计划对应在的课程视频地址。

在课程管理数据库创建表 teachplan_media 存储课程计划与媒资关联信息,表结构如下:

0x02 选择视频

Vue 父子组件通信

上一章已实现了我的媒资页面,所以媒资查询窗口页面不需要再开发,将 “我的媒资页面” 作为一个组件在修改课程
计划页面中引用,如下图:

修改课程计划页面为父组件,我的媒资查询页面为子组件。

问题1:

我的媒资页面在选择媒资文件时不允许显示,比如 视频处理 按钮,该如何控制?

这时就需要父组件(修改课程计划页面)向子组件(我的媒资页面)传入一个变量,使用此变量来控制当前是否进入选择媒资文件业务,从而控制哪些元素不显示,如下图:

问题2:

在我的媒资页面选择了媒资文件,如何将选择的媒资文件信息传到父组件?

这时就需要子组件调用父组件的方法来解决此问题,如下图:

父组件:修改课程计划

本节实现功能:在课程计划页面打开我的媒资页面。

1、引入子组件

import mediaList from '@/module/media/page/media_list.vue';
export default {components:{mediaList},data() {....

2、使用子组件

在父组件的视图中使用子组件,同时传入变量 ischoose,并指定父组件的方法名为choosemedia

这里使用 el-dialog 实现弹出窗口。

<el‐dialog title="选择媒资文件" :visible.sync="mediaFormVisible"><media‐list v‐bind:ischoose="true" @choosemedia="choosemedia"></media‐list>
</el‐dialog>

3、choosemedia 方法

在父组件中定义 choosemedia 方法,接收子组件调用,参数包括:媒资文件 id、媒资文件的原始名称、媒资文件 url

choosemedia(mediaId,fileOriginalName,mediaUrl){
}

4、打开子组件窗口

1)打开子组件窗口按钮定义

<el‐button style="font‐size: 12px;" type="text" on‐click={ () => this.choosevideo(data.id) }>选择视频</el‐button>

效果如下:

2)打开子组件窗口方法

定义 querymedia 方法:

methods: {//打开查询媒资文件窗口,传入课程计划idchoosevideo(teachplanId){this.activeTeachplanId = teachplanId;this.mediaFormVisible = true;},...
}

子组件:我的媒资查询

1、定义 ischoose 变量,接收父组件传入的 ischoose

export default{props: ['ischoose'],data(){

2、父组件传的 ischoose 变量为 true 时表示当前是选择媒资文件业务,需要控制页面元素是否显示

1)ischoose=true,选择按钮显示

<el‐table‐column label="选择" width="80" v‐if="ischoose == true"><template slot‐scope="scope"><el‐button size="small" type="primary" plain @click="choose(scope.row)">选择</el‐button></template>
</el‐table‐column>

2)ischoose=false,视频处理按钮显示

<el‐table‐column label="开始处理" width="100" v‐if="ischoose != true"><template slot‐scope="scope"><el‐buttonsize="small" type="primary" plain @click="process(scope.row.fileId)">开始处理</el‐button></template>
</el‐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);
}

页面效果

全部代码

course_plan.vue

<template><div><el-button type="primary" @click="teachplayFormVisible = true">添加课程计划</el-button><el-tree:data="teachplanList":props="defaultProps"node-key="id"default-expand-all:expand-on-click-node="false":render-content="renderContent"></el-tree><el-dialog title="添加课程计划" :visible.sync="teachplayFormVisible" ><el-form ref="teachplanForm" :model="teachplanActive" label-width="140px" style="width:600px;" :rules="teachplanRules" ><el-form-item label="上级结点" ><el-select v-model="teachplanActive.parentid" placeholder="不填表示根结点"><el-optionv-for="item in teachplanList":key="item.id":label="item.pname":value="item.id"></el-option></el-select></el-form-item><el-form-item label="章节/课时名称" prop="pname"><el-input v-model="teachplanActive.pname" auto-complete="off"></el-input></el-form-item><el-form-item label="课程类型" ><el-radio-group v-model="teachplanActive.ptype"><el-radio class="radio" label='1'>视频</el-radio><el-radio class="radio" label='2'>文档</el-radio></el-radio-group></el-form-item><el-form-item label="学习时长(分钟) 请输入数字" ><el-input type="number" v-model="teachplanActive.timelength" auto-complete="off" ></el-input></el-form-item><el-form-item label="排序字段" ><el-input v-model="teachplanActive.orderby" auto-complete="off" ></el-input></el-form-item><el-form-item label="章节/课时介绍" prop="description"><el-input type="textarea" v-model="teachplanActive.description" ></el-input></el-form-item><el-form-item label="状态" prop="status"><el-radio-group v-model="teachplanActive.status" ><el-radio class="radio" label="0" >未发布</el-radio><el-radio class="radio" label='1'>已发布</el-radio></el-radio-group></el-form-item><el-form-item ><el-button type="primary" v-on:click="addTeachplan">提交</el-button><el-button type="primary" v-on:click="resetForm">重置</el-button></el-form-item></el-form></el-dialog><el-dialog title="选择媒资文件" :visible.sync="mediaFormVisible"><media-list v-bind:ischoose="true" @choosemedia="choosemedia"></media-list></el-dialog></div>
</template>
<script>let id = 1000;import * as courseApi from '../../api/course';import utilApi from '../../../../common/utils';import * as systemApi from '../../../../base/api/system';import mediaList from '@/module/media/page/media_list.vue';export default {components:{mediaList},data() {return {mediaFormVisible:false,teachplayFormVisible:false,//控制添加窗口是否显示teachplanList : [{id: 1,pname: '一级 1',children: [{id: 4,pname: '二级 1-1',children: [{id: 9,pname: '三级 1-1-1'}, {id: 10,pname: '三级 1-1-2'}]}]}],defaultProps:{children: 'children',label: 'pname'},teachplanRules: {pname: [{required: true, message: '请输入课程计划名称', trigger: 'blur'}],status: [{required: true, message: '请选择状态', trigger: 'blur'}]},teachplanActive:{},teachplanId:''}},methods: {//选择视频,打开窗口choosevideo(data){//得到当前的课程计划this.teachplanId = data.id // alert(this.teachplanId)this.mediaFormVisible = true;//打开窗口},//保存选择的视频choosemedia(mediaId,fileOriginalName,mediaUrl){//保存视频到课程计划表中let teachplanMedia ={}teachplanMedia.mediaId =mediaId;teachplanMedia.mediaFileOriginalName =fileOriginalName;teachplanMedia.mediaUrl =mediaUrl;teachplanMedia.courseId =this.courseid;//课程计划teachplanMedia.teachplanId=this.teachplanIdcourseApi.savemedia(teachplanMedia).then(res=>{if(res.success){this.$message.success("选择视频成功")//查询课程计划this.findTeachplan()}else{this.$message.error(res.message)}})},//提交课程计划addTeachplan(){//校验表单this.$refs.teachplanForm.validate((valid) => {if (valid) {//调用api方法//将课程id设置到teachplanActivethis.teachplanActive.courseid = this.courseidcourseApi.addTeachplan(this.teachplanActive).then(res=>{if(res.success){this.$message.success("添加成功")//刷新树this.findTeachplan()}else{this.$message.error(res.message)}})}})},//重置表单resetForm(){this.teachplanActive = {}},append(data) {const newChild = { id: id++, label: 'testtest', children: [] };if (!data.children) {this.$set(data, 'children', []);}data.children.push(newChild);},edit(data){//alert(data.id);},remove(node, data) {const parent = node.parent;const children = parent.data.children || parent.data;const index = children.findIndex(d => d.id === data.id);children.splice(index, 1);},renderContent(h, { node, data, store }) {return (<span style="flex: 1; display: flex; align-items: center; justify-content: space-between; font-size: 14px; padding-right: 8px;"><span><span>{node.label}</span></span><span><el-button style="font-size: 12px;" type="text" on-click={ () => this.choosevideo(data) }>{data.mediaFileOriginalName}&nbsp;&nbsp;&nbsp;&nbsp; 选择视频</el-button><el-button style="font-size: 12px;" type="text" on-click={ () => this.edit(data) }>修改</el-button><el-button style="font-size: 12px;" type="text" on-click={ () => this.remove(node, data) }>删除</el-button></span></span>);},findTeachplan(){this.teachplanList = []//查询课程计划courseApi.findTeachplanList(this.courseid).then(res=>{if(res && res.children){this.teachplanList = res.children;}else {this.$message.error("课程计划查询失败")console.log(res)}})}},mounted(){//课程idthis.courseid = this.$route.params.courseid;//查询课程计划this.findTeachplan()}} </script>
<style></style>

media_list.vue

<template><div><!--查询表单--><el-form :model="params">标签:<el-input v-model="params.tag" style="width:160px"></el-input>原始名称:<el-input v-model="params.fileOriginalName" style="width:160px"></el-input>处理状态:<el-select v-model="params.processStatus" placeholder="请选择处理状态"><el-optionv-for="item in processStatusList":key="item.id":label="item.name":value="item.id"></el-option></el-select><br/><el-button type="primary" v-on:click="query" size="small">查询</el-button><router-link class="mui-tab-item" :to="{path:'/upload'}"><el-button type="primary" size="small" v-if="ischoose != true">上传文件</el-button></router-link></el-form><!--列表--><el-table :data="list" highlight-current-row v-loading="listLoading" style="width: 100%;"><el-table-column type="index" width="30"></el-table-column><el-table-column prop="fileOriginalName" label="原始文件名称" width="220"></el-table-column><el-table-column prop="fileName" label="文件名称" width="220"></el-table-column><el-table-column prop="fileUrl" label="访问url" width="260"></el-table-column><el-table-column prop="tag" label="标签" width="100"></el-table-column><el-table-column prop="fileSize" label="文件大小" width="120"></el-table-column><el-table-column prop="processStatus" label="处理状态" width="100" :formatter="formatProcessStatus"></el-table-column><el-table-column prop="uploadTime" label="创建时间" width="110" :formatter="formatCreatetime"></el-table-column><el-table-column label="开始处理" width="" v-if="ischoose != true"><template slot-scope="scope"><el-buttonsize="small" type="primary" plain @click="process(scope.row.fileId)">开始处理</el-button></template></el-table-column><el-table-column label="选择" width="80" v-if="ischoose == true"><template slot-scope="scope"><el-buttonsize="small" type="primary" plain @click="choose(scope.row)">选择</el-button></template></el-table-column></el-table><!--分页--><el-col :span="24" class="toolbar"><el-pagination background layout="prev, pager, next" @current-change="changePage" :page-size="this.params.size":total="total" :current-page="this.params.page"style="float:right;"></el-pagination></el-col></div>
</template>
<script>import * as mediaApi from '../api/media'import utilApi from '@/common/utils';export default{props: ['ischoose'],// 页面数据data(){return {params:{page:1,//页码size:10,//每页显示个数tag:'',//标签fileName:'',//文件名称processStatus:''//处理状态},listLoading:false,list:[],total:0,processStatusList:[]}},//方法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 "无需处理";}}},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,mediaFile.fileUrl);},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.totalthis.list = res.queryResult.list})}},//页面初始化完成前钩子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:'无需处理'}]}} </script>
<style></style>

0x03 保存视频信息

需求分析

用户进入课程计划页面,选择视频,将课程计划与视频信息保存在课程管理数据库中。

用户操作流程:

1、进入课程计划,点击”选择视频“,打开我的媒资查询页面

2、为课程计划选择对应的视频,选择“选择”

3、前端请求课程管理服务保存课程计划与视频信息

数据模型

在课程管理数据库创建表 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;
}

API接口

此接口作为前端请求课程管理服务保存课程计划与视频信息的接口:

TeachplanControllerApi 增加接口:

@ApiOperation("保存媒资信息")
public ResponseResult saveTeachplanMedia(TeachplanMedia teachplanMedia);

服务端开发

1、Controller

TeachplanController 下添加该方法

@Override
@PostMapping("/savemedia")
public ResponseResult saveTeachplanMedia(@RequestBody TeachplanMedia teachplanMedia) {return teachplanService.saveTeachplanMedia(teachplanMedia);
}

2、Dao

创建 TeachplanMediaRepository 用于对 TeachplanMedia 的操作。

public interface TeachplanMediaRepository extends JpaRepository<TeachplanMedia, String> {}

3、Service

//保存媒资信息
public ResponseResult saveTeachplanMedia(TeachplanMedia teachplanMedia) {if(teachplanMedia == null){ExceptionCast.cast(CommonCode.INVALIDPARAM);} //课程计划String teachplanId = teachplanMedia.getTeachplanId();//查询课程计划Optional<Teachplan> 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<TeachplanMedia> 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);
}

前端开发

1、API方法

/*保存媒资信息*/
export const savemedia = teachplanMedia => {return http.requestPost(apiUrl+'/course/savemedia',teachplanMedia);
}

2、API调用

在课程视频方法中调用 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、测试

1、向叶子结点课程计划保存媒资信息

操作结果:保存成功

2、向非叶子结点课程计划保存媒资信息

操作结果:保存失败

0x04 查询视频信息

需求分析

课程计划的视频信息保存后在页面无法查看,本节解决课程计划页面显示相关联的媒资信息。

解决方案:

在获取课程计划树结点信息时将关联的媒资信息一并查询,并在前端显示,下图说明了课程计划显示的区域。

Dao

修改课程计划查询的 Dao:

1、修改模型

在课程计划结果信息中添加媒资信息

package com.xuecheng.framework.domain.course.ext;import com.xuecheng.framework.domain.course.Teachplan;
import lombok.Data;
import lombok.ToString;import java.util.List;@Data
@ToString
public class TeachplanNode extends Teachplan {List<TeachplanNode> children;//媒资信息private String media_id;private String media_fileoriginalname;
}

2、修改sql 语句,添加关联查询媒资信息

添加 mediaIdmediaFileOriginalName

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.xuecheng.manage_course.dao.TeachplanMapper"><resultMap id="teachplanMap" type="com.xuecheng.framework.domain.course.ext.TeachplanNode"><!--一级节点--><id property="id" column="one_id"/><result property="pname" column="one_pname"/><collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode"><!--二级节点--><id property="id" column="two_id"/><result property="pname" column="two_pname"/><collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode"><!--三级节点--><id property="id" column="three_id"/><result property="pname" column="three_pname"/><result property="media_id" column="media_id"/><result property="media_fileoriginalname" column="media_fileoriginalname"/></collection></collection></resultMap><!--三级菜单查询--><select id="selectList" resultMap="teachplanMap" parameterType="java.lang.String">SELECTa.id one_id,a.pname one_pname,a.courseid one_course,b.id two_id,b.pname two_pname,c.id three_id,c.pname three_pname,media.media_id media_id,media.media_fileoriginalname media_fileoriginalnameFROMteachplan aLEFT JOIN teachplan bON b.parentid = a.idLEFT JOIN teachplan cON c.parentid = b.idLEFT JOIN teachplan_media mediaON c.id = media.teachplan_idWHEREa.parentid = '0'<!--判断参数不为空时才进行参数的匹配--><if test="_parameter!=null and _parameter!=''">and a.courseid = #{courseId}</if>ORDER BY a.orderby,b.orderby,c.orderby</select>
</mapper>

这里的核心代码是使用 LEFT JOIN 关联 teachplan_media 表中的数据,再获取该课程计划下的 mediaIdmediaFileOriginalName 代码如下

LEFT JOIN teachplan_media media ON c.id = media.teachplan_id
WHERE
<!--三级节点-->
<id property="id" column="three_id"/>
<result property="pname" column="three_pname"/>
<result property="media_id" column="media_id"/>
<result property="media_fileoriginalname" 

使用swagger进行接口测试

从结果中成功的查询到了课程计划所关联的媒资信息。

页面查询视频

课程计划结点信息已包括媒资信息,可在页面获取信息后显示。

通过 data.media_fileoriginalname 获取媒资视频的原始名称

<el‐button style="font‐size: 12px;" type="text" on‐click={ () => this.querymedia(data.id) }>
{data.media_fileoriginalname}&nbsp;&nbsp;&nbsp;&nbsp;选择视频</el‐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)}})
},

? 认识作者

作者:? LCyee ,全干型代码?

自建博客:https://www.codeyee.com

记录学习以及项目开发过程中的笔记与心得,记录认知迭代的过程,分享想法与观点。

CSDN 博客:https://blog.csdn.net/codeyee

记录和分享一些开发过程中遇到的问题以及解决的思路。

欢迎加入微服务练习生的队伍,一起交流项目学习过程中的一些问题、分享学习心得等,不定期组织一起刷题、刷项目,共同见证成长。

  相关解决方案