zoukankan      html  css  js  c++  java
  • Day14_媒资管理

    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配置如下:

    package com.xuecheng.manage_media_process.config;
    
    import org.springframework.amqp.core.*;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * @author Administrator
     * @version 1.0
     * @create 2018-07-12 9:04
     **/
    @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
         */
        @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

    server:
      port: 31450
    spring:
      application:
        name: xc-service-manage-media-processor
      data:
        mongodb:
          uri:  mongodb://localhost:27017
          database: xc_media
    #rabbitmq配置
      rabbitmq:
        host: 127.0.0.1
        port: 5672
        username: guest
        password: guest
        virtual-host: /
    xc-service-manage-media:
      mq:
        queue-media-video-processor: queue_media_video_processor
        routingkey-media-video: routingkey_media_video
      video-location: /Users/XinxingWang/Development/Java/video
      ffmpeg-path: /usr/local/Cellar/ffmpeg/4.3.1/bin/ffmpeg
    

    1.2.2 视频处理技术方案

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

    ffmpeg是一个可行的视频处理程序,可以通过Java调用ffmpeg.exe完成视频处理。
    在java中可以使用Runtime类和Process Builder类两种方式来执行外部程序,工作中至少掌握一种。
    

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

    关于Process Builder的测试如下:

    package com.xuecheng.manage_media_process;
    
    import com.xuecheng.framework.utils.Mp4VideoUtil;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    
    /**
     * @author Administrator
     * @version 1.0
     * @create 2018-07-12 9:11
     **/
    @SpringBootTest
    @RunWith(SpringRunner.class)
    public class TestProcessBuilder {
    
        @Test
        public void testProcessBuilder() throws IOException {
    
            //创建ProcessBuilder对象
            ProcessBuilder processBuilder =new ProcessBuilder();
            //设置执行的第三方程序(命令)
    //        processBuilder.command("ping","127.0.0.1");
            processBuilder.command("ifconfig");
    //        processBuilder.command("java","-jar","f:/xc-service-manage-course.jar");
            //将标准输入流和错误输入流合并,通过标准输入流读取信息就可以拿到第三方程序输出的错误信息、正常信息
            processBuilder.redirectErrorStream(true);
    
            //启动一个进程
            Process process = processBuilder.start();
            //由于前边将错误和正常信息合并在输入流,只读取输入流
            InputStream inputStream = process.getInputStream();
            //将字节流转成字符流
            InputStreamReader reader = new InputStreamReader(inputStream,"gbk");
           //字符缓冲区
            char[] chars = new char[1024];
            int len = -1;
            while((len = reader.read(chars))!=-1){
                String string = new String(chars,0,len);
                System.out.println(string);
            }
    
            inputStream.close();
            reader.close();
    
        }
    
        //测试使用工具类将avi转成mp4
        @Test
        public void testProcessMp4(){
            //String ffmpeg_path, String video_path, String mp4_name, String mp4folder_path
            //ffmpeg的路径
            String ffmpeg_path = "/usr/local/Cellar/ffmpeg/4.3.1/bin/ffmpeg";
            //video_path视频地址
            String video_path = "/Users/XinxingWang/Development/Java/video/solr.avi";
            //mp4_name mp4文件名称
            String mp4_name  ="1.mp4";
            //mp4folder_path mp4文件目录路径
            String mp4folder_path="/Users/XinxingWang/Development/Java/video/upload/";
            Mp4VideoUtil mp4VideoUtil = new Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4folder_path);
            //开始编码,如果成功返回success,否则返回输出的日志
            String result = mp4VideoUtil.generateMp4();
            System.out.println(result);
        }
    
    }
    

    转换视频工具类,参见:

    上边的工具类中:

    Mp4VideoUtil.java完成avi转mp4

    HlsVideoUtil.java完成mp4转hls

    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文件列表,代码如下:

    package com.xuecheng.framework.domain.media;
    
    import lombok.Data;
    import lombok.ToString;
    import org.springframework.data.annotation.Id;
    import org.springframework.data.mongodb.core.mapping.Document;
    
    import java.util.Date;
    
    /**
     * @Author: mrt.
     * @Description:
     * @Date:Created in 2018/1/24 10:04.
     * @Modified By:
     */
    @Data
    @ToString
    @Document(collection = "media_file")
    public class MediaFile {
        /*
        文件id、名称、大小、文件类型、文件状态(未上传、上传完成、上传失败)、上传时间、视频处理方式、视频处理状态、hls_m3u8,hls_ts_list、课程视频信息(课程id、章节id)
         */
        @Id
        //文件id
        private String fileId;
        //文件名称
        private String fileName;
        //文件原始名称
        private String fileOriginalName;
        //文件路径
        private String filePath;
        //文件url
        private String fileUrl;
        //文件类型
        private String fileType;
        //mimetype
        private String mimeType;
        //文件大小
        private Long fileSize;
        //文件状态
        private String fileStatus;
        //上传时间
        private Date uploadTime;
        //处理状态
        private String processStatus;
        //hls处理
        private MediaFileProcess_m3u8 mediaFileProcess_m3u8;
    
        //tag标签用于查询
        private String tag;
    }
    
    package com.xuecheng.framework.domain.media;
    
    import lombok.Data;
    import lombok.ToString;
    
    import java.util.List;
    
    /**
     * @Author: mrt.
     * @Description:
     * @Date:Created in 2018/1/24 10:04.
     * @Modified By:
     */
    @Data
    @ToString
    public class MediaFileProcess_m3u8 extends MediaFileProcess {
    
        //ts列表
        private List<String> tslist;
    
    }
    
    1.2.3.4 视频处理生成Mp4

    1、创建Dao

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

    package com.xuecheng.manage_media_process.dao;
    
    import com.xuecheng.framework.domain.media.MediaFile;
    import org.springframework.data.mongodb.repository.MongoRepository;
    
    public interface MediaFileRepository extends MongoRepository<MediaFile,String> {
    }
    

    2、在application.yml中配置ffmpeg的位置及视频目录的根目录:

    server:
      port: 31450
    spring:
      application:
        name: xc-service-manage-media-processor
      data:
        mongodb:
          uri:  mongodb://localhost:27017
          database: xc_media
    #rabbitmq配置
      rabbitmq:
        host: 127.0.0.1
        port: 5672
        username: guest
        password: guest
        virtual-host: /
    xc-service-manage-media:
      mq:
        queue-media-video-processor: queue_media_video_processor
        routingkey-media-video: routingkey_media_video
      video-location: /Users/XinxingWang/Development/Java/video
      ffmpeg-path: /usr/local/Cellar/ffmpeg/4.3.1/bin/ffmpeg
    

    3、处理任务类

    在mq包下创建MediaProcessTask类,此类负责监听视频处理队列,并进行视频处理。

    整个视频处理内容较多,这里分两部分实现:生成Mp4和生成m3u8。

    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.io.IOException;
    import java.util.List;
    import java.util.Map;
    import java.util.Optional;
    
    /**
     * @author HackerStar
     * @create 2020-08-29 15:29
     */
    @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;
    
        @Autowired
        MediaFileRepository mediaFileRepository;
    
        @RabbitListener(queues="${xc-service-manage-media.mq.queue-media-video-processor}")
        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<MediaFile> optional = mediaFileRepository.findById(mediaId);
            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");//处理状态为处理失败
                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<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文件url
            mediaFile.setFileUrl(mediaFile.getFilePath() + "hls/" + m3u8_name);
            mediaFileRepository.save(mediaFile);
        }
    }
    

    说明:

    mp4转成m3u8如何判断转换成功?

    第一、根据视频时长来判断,同mp4转换成功的判断方法。

    第二、最后还要判断m3u8文件内容是否完整。

    1.3 发送视频处理消息

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

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

    1.3.1 RabbitMQ配置

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

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

    修改application.yml

    server:
      port: 31400
    spring:
      application:
        name: xc-service-manage-media
      data:
        mongodb:
          uri:  mongodb://localhost:27017
          database: xc_media
    eureka:
      client:
        registerWithEureka: true #服务注册开关
        fetchRegistry: true #服务发现开关
        serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址,多个中间用逗号分隔
          defaultZone: ${EUREKA_SERVER:http://localhost:50101/eureka/}
      instance:
        prefer-ip-address:  true  #将自己的ip地址注册到Eureka服务中
        ip-address: ${IP_ADDRESS:127.0.0.1}
        instance-id: ${spring.application.name}:${server.port} #指定实例id
    ribbon:
      MaxAutoRetries: 2 #最大重试次数,当Eureka中可以找到服务,但是服务连不上时将会重试,如果eureka中找不到服务则直接走断路器
      MaxAutoRetriesNextServer: 3 #切换实例的重试次数
      OkToRetryOnAllOperations: false  #对所有操作请求都进行重试,如果是get则可以,如果是post,put等操作没有实现幂等的情况下是很危险的,所以设置为false
      ConnectTimeout: 5000  #请求连接的超时时间
      ReadTimeout: 6000 #请求处理的超时时间
    xc-service-manage-media:
      upload-location: /Users/XinxingWang/Development/Java/video/upload
      mq:
        queue‐media‐video‐processor: queue_media_video_processor
        routingkey‐media‐video: routingkey_media_video
    

    1.3.2 修改Service

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

    在mergechunks方法最后调用sendProcessVideo方法。

    		//向MQ发送视频处理消息
        public 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);
        }
    
    ......
    //状态为上传成功 
    mediaFile.setFileStatus("301002"); 
    mediaFileRepository.save(mediaFile); 
    String mediaId = mediaFile.getFileId(); 
    //向MQ发送视频处理消息
    sendProcessVideoMsg(mediaId);
    ......
    

    完整代码:

    package com.xuecheng.manage_media.service;
    
    import com.alibaba.fastjson.JSON;
    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 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.multipart.MultipartFile;
    
    import java.io.*;
    import java.util.*;
    
    /**
     * @author HackerStar
     * @create 2020-08-28 15:04
     */
    @Service
    public class MediaUploadService {
        private final static Logger LOGGER = LoggerFactory.getLogger(MediaUploadController.class);
    
        @Autowired
        MediaFileRepository mediaFileRepository;
    
        @Autowired
        RabbitTemplate rabbitTemplate;
    
        //上传文件根目录
        @Value("${xc-service-manage-media.upload-location}")
        String uploadPath;
    
        @Value("${xc-service-manage-media.mq.routingkey‐media‐video}")
        String routingkey_media_video;
    
        /**
         * 根据文件md5得到文件路径
         * * 规则:
         * * 一级目录:md5的第一个字符
         * * 二级目录:md5的第二个字符
         * * 三级目录:md5
         * * 文件名:md5+文件扩展名
         * * @param fileMd5 文件md5值
         * * @param fileExt 文件扩展名
         * * @return 文件路径
         */
        private String getFilePath(String fileMd5, String fileExt) {
            String filePath = uploadPath + "/" + fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5 + "." + fileExt;
            return filePath;
        }
    
        //得到文件目录相对路径,路径中去掉根目录
        private String getFileFolderRelativePath(String fileMd5, String fileExt) {
            String filePath = fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/";
            return filePath;
        }
    
        //得到文件所在目录
        private String getFileFolderPath(String fileMd5) {
            String fileFolderPath = uploadPath + "/" + fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/";
            return fileFolderPath;
        }
    
        //创建文件目录
        private boolean createFileFold(String fileMd5) {
            //创建上传文件目录
            String fileFolderPath = getFileFolderPath(fileMd5);
            File fileFolder = new File(fileFolderPath);
            if (!fileFolder.exists()) {
                //创建文件夹
                boolean mkdirs = fileFolder.mkdirs();
                return mkdirs;
            }
            return true;
        }
    
        //文件上传注册
        public ResponseResult register(String fileMd5, String fileName, String fileSize, String mimetype, String fileExt) {
            //检查文件是否上传
            //1、得到文件的路径
            String filePath = getFilePath(fileMd5, fileExt);
            File file = new File(filePath);
    
            //2、查询数据库文件是否存在
            Optional<MediaFile> optional = mediaFileRepository.findById(fileMd5);
            //文件存在直接返回
            if (file.exists() && optional.isPresent()) {
                ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST);
            }
            boolean fileFold = createFileFold(fileMd5);
            if (!fileFold) {
                //上传文件目录创建失败
                ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_CREATEFOLDER_FAIL);
            }
            return new ResponseResult(CommonCode.SUCCESS);
        }
    
        //得到块文件所在目录
        private String getChunkFileFolderPath(String fileMd5) {
            String fileChunkFolderPath = getFileFolderPath(fileMd5) + "/" + "chunks" + "/";
            return fileChunkFolderPath;
        }
    
        //检查块文件
        public CheckChunkResult checkchunk(String fileMd5, String chunk, String chunkSize) {
            //得到块文件所在路径
            String chunkfileFolderPath = getChunkFileFolderPath(fileMd5);
            //块文件的文件名称以1,2,3..序号命名,没有扩展名
            File chunkFile = new File(chunkfileFolderPath + chunk);
            if (chunkFile.exists()) {
                return new CheckChunkResult(MediaCode.CHUNK_FILE_EXIST_CHECK, true);
            } else {
                return new CheckChunkResult(MediaCode.CHUNK_FILE_EXIST_CHECK, false);
            }
        }
    
        //块文件上传
        public ResponseResult uploadchunk(MultipartFile file, String fileMd5, String chunk) {
            if (file == null) {
                ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_ISNULL);
            }
            //创建块文件目录
            boolean fileFold = createChunkFileFolder(fileMd5);
            //块文件
            File chunkfile = new File(getChunkFileFolderPath(fileMd5) + chunk);
            //上传的块文件
            InputStream inputStream = null;
            FileOutputStream outputStream = null;
            try {
                inputStream = file.getInputStream();
                outputStream = new FileOutputStream(chunkfile);
                IOUtils.copy(inputStream, outputStream);
            } catch (Exception e) {
                e.printStackTrace();
                LOGGER.error("upload chunk file fail:{}", e.getMessage());
                ExceptionCast.cast(MediaCode.CHUNK_FILE_UPLOAD_FAIL);
            } finally {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return new ResponseResult(CommonCode.SUCCESS);
        }
    
        //创建块文件目录
        private boolean createChunkFileFolder(String fileMd5) {
            //创建上传文件目录
            String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
            File chunkFileFolder = new File(chunkFileFolderPath);
            if (!chunkFileFolder.exists()) {
                //创建文件夹
                boolean mkdirs = chunkFileFolder.mkdirs();
                return mkdirs;
            }
            return true;
        }
    
        //合并块文件
        public ResponseResult mergechunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
            //获取块文件的路径
            String chunkfileFolderPath = getChunkFileFolderPath(fileMd5);
            File chunkfileFolder = new File(chunkfileFolderPath);
            if (!chunkfileFolder.exists()) {
                chunkfileFolder.mkdirs();
            }
            //合并文件路径
            File mergeFile = new File(getFilePath(fileMd5, fileExt));
            //创建合并文件
            //合并文件存在先删除再创建
            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 = getChunkFiles(chunkfileFolder);
            //合并文件
            mergeFile = 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);
            //文件路径保存相对路径
            mediaFile.setFilePath(getFileFolderRelativePath(fileMd5, fileExt));
            mediaFile.setFileSize(fileSize);
            mediaFile.setUploadTime(new Date());
            mediaFile.setMimeType(mimetype);
            mediaFile.setFileType(fileExt);
            //状态为上传成功
            mediaFile.setFileStatus("301002");
            MediaFile save = mediaFileRepository.save(mediaFile);
    
            this.sendProcessVideoMsg(fileMd5);
    
            return new ResponseResult(CommonCode.SUCCESS);
        }
    
        //校验文件的md5值
        private boolean checkFileMd5(File mergeFile, String md5) {
            if (mergeFile == null || StringUtils.isEmpty(md5)) {
                return false;
            }
            //进行md5校验
            FileInputStream mergeFileInputstream = null;
            try {
                mergeFileInputstream = new FileInputStream(mergeFile);
                //得到文件的md5
                String mergeFileMd5 = DigestUtils.md5Hex(mergeFileInputstream);
                //比较md5
                if (md5.equalsIgnoreCase(mergeFileMd5)) {
                    return true;
                }
            } catch (Exception e) {
                e.printStackTrace();
                LOGGER.error("checkFileMd5 error,file is:{},md5 is: {}", mergeFile.getAbsoluteFile(), md5);
            } finally {
                try {
                    mergeFileInputstream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
            return false;
        }
    
        //获取所有块文件
        private List<File> getChunkFiles(File chunkfileFolder) {
            //获取路径下的所有块文件
            File[] chunkFiles = chunkfileFolder.listFiles();
            //将文件数组转成list,并排序
            List<File> chunkFileList = new ArrayList<File>();
            chunkFileList.addAll(Arrays.asList(chunkFiles));
            //排序
            Collections.sort(chunkFileList, new Comparator<File>() {
                @Override
                public int compare(File o1, File o2) {
                    if (Integer.parseInt(o1.getName()) > Integer.parseInt(o2.getName())) {
                        return 1;
                    }
                    return -1;
                }
            });
            return chunkFileList;
        }
    
        //合并文件
        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;
        }
    
        //向MQ发送视频处理消息
        public 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);
        }
    }
    

    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.2 API

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

    package com.xuecheng.api.media;
    
    import com.xuecheng.framework.domain.media.MediaFile;
    import com.xuecheng.framework.domain.media.request.QueryMediaFileRequest;
    import com.xuecheng.framework.model.response.QueryResponseResult;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    
    /**
     * Created by Administrator.
     */
    @Api(value = "媒体文件管理",description = "媒体文件管理接口",tags = {"媒体文件管理接口"})
    public interface MediaFileControllerApi {
    
        @ApiOperation("我的媒资文件查询列表")
        public QueryResponseResult<MediaFile> findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest);
    
    }
    

    2.3 服务端开发

    2.3.1 Dao

    package com.xuecheng.manage_media_process.dao;
    
    import com.xuecheng.framework.domain.media.MediaFile;
    import org.springframework.data.mongodb.repository.MongoRepository;
    
    public interface MediaFileRepository extends MongoRepository<MediaFile,String> {
    }
    

    2.3.2 Service

    package com.xuecheng.manage_media.service;
    
    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 org.apache.commons.lang3.StringUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.domain.*;
    
    /**
     * @author HackerStar
     * @create 2020-08-31 09:03
     */
    @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<MediaFile> ex = Example.of(mediaFile, matcher);
            page = page - 1;
            //分页参数
            Pageable pageable = new PageRequest(page, size);
            //分页查询
            Page<MediaFile> all = mediaFileRepository.findAll(ex, pageable);
    
            QueryResult<MediaFile> mediaFileQueryResult = new QueryResult<MediaFile>();
    
            mediaFileQueryResult.setList(all.getContent());
            mediaFileQueryResult.setTotal(all.getTotalElements());
            return new QueryResponseResult(CommonCode.SUCCESS, mediaFileQueryResult);
        }
    }
    

    2.3.3 Controller

    package com.xuecheng.manage_media.controller;
    
    import com.xuecheng.api.media.MediaFileControllerApi;
    import com.xuecheng.framework.domain.media.request.QueryMediaFileRequest;
    import com.xuecheng.framework.model.response.QueryResponseResult;
    import com.xuecheng.manage_media.service.MediaFileService;
    import com.xuecheng.manage_media.service.MediaUploadService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    
    /**
     * @author HackerStar
     * @create 2020-08-31 09:09
     */
    @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.1 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)
    }
    

    2.4.2 页面

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

    <template>
      <div>
        <!--查询表单-->
        <el-form :model="params">
          标签:
          <el-input v-model="params.tag" style="160px"></el-input>
          原始名称:
          <el-input v-model="params.fileOriginalName" style="160px"></el-input>
          处理状态:
          <el-select v-model="params.processStatus" placeholder="请选择处理状态">
            <el-option
              v-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=" 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="100" v-if="ischoose != true">
            <template slot-scope="scope">
              <el-button
                size="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-button
              size="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.total
              this.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>
    

    3 媒资与课程计划关联

    3.1 需求分析

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

    操作的业务流程如下:

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

    2、选择视频

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

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

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

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

    3.2 选择视频

    3.2.1 Vue父子组件通信

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

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

    问题1:

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

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

    问题2:

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

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

    3.2.2 父组件(修改课程计划)

    本节实现功能:在课程计划页面打开我的媒资页面。(course/page/course_manage/course_summary.vue)

    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){
            //保存视频到课程计划表中
            let teachplanMedia ={}
            teachplanMedia.mediaId =mediaId;
            teachplanMedia.mediaFileOriginalName =fileOriginalName;
            teachplanMedia.mediaUrl =mediaUrl;
            teachplanMedia.courseId =this.courseid;
            //课程计划
            teachplanMedia.teachplanId=this.teachplanId
    
            courseApi.savemedia(teachplanMedia).then(res=>{
                if(res.success){
                    this.$message.success("选择视频成功")
                  //查询课程计划
                  this.findTeachplan()
                }else{
                  this.$message.error(res.message)
                }
            })
          },
    

    4、打开子组件窗口

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

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

    效果如下:

    1. 打开子组件窗口方法

    定义querymedia方法:

    			//选择视频,打开窗口
          choosevideo(data){
              //得到当前的课程计划
              this.teachplanId = data.id
    //        alert(this.teachplanId)
              this.mediaFormVisible = true;//打开窗口
          },
    

    3.2.3 子组件(我的媒资查询)

    1、定义ischoose变量,接收父组件传入的ischoose(media_list.vue)

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

    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-button
                size="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,mediaFile.fileUrl);
          },
    

    3.3 保存视频信息

    3.3.1 需求分析

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

    用户操作流程:

    1、进入课程计划,点击”选择视频“,打开我的媒资查询页面
    2、为课程计划选择对应的视频,选择“选择”
    3、前端请求课程管理服务保存课程计划与视频信息。
    

    3.3.2 数据模型

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

    创建teachplanMedia 模型类:

    package com.xuecheng.framework.domain.course;
    
    import lombok.Data;
    import lombok.ToString;
    import org.hibernate.annotations.GenericGenerator;
    
    import javax.persistence.*;
    import java.io.Serializable;
    
    /**
     * Created by admin on 2018/2/7.
     */
    @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;
        private String courseId;
    
    }
    

    3.3.3 API接口

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

    在课程管理服务增加接口:

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

    3.3.4 服务端开发

    3.3.3.1 DAO

    创建 TeachplanMediaRepository用于对TeachplanMedia的操作。

    package com.xuecheng.manage_course.dao;
    
    import com.xuecheng.framework.domain.course.TeachplanMedia;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    /**
     * @author HackerStar
     * @create 2020-08-31 11:34
     */
    public interface TeachplanMediaRepository extends JpaRepository<TeachplanMedia, String> {
    }
    

    3.3.3.2 Service
    		//保存媒资信息
        public ResponseResult savemedia(@RequestBody 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);
        }
    
    3.3.3.3 Controller
    		@Override
        @PostMapping("/savemedia")
        public ResponseResult savemedia(TeachplanMedia teachplanMedia) {
            return courseService.savemedia(teachplanMedia);
        }
    

    3.3.4 前端开发

    定义api方法,调用课程管理服务保存媒资信息接口

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

    3.3.4.2 API调用

    在课程视频方法中调用api:

    			//保存选择的视频
          choosemedia(mediaId,fileOriginalName,mediaUrl){
    				this.mediaFormVisible = false;
            //保存视频到课程计划表中
            let teachplanMedia ={}
    				teachplanMedia.teachplanId = this.teachplanId;
            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、向非叶子结点课程计划保存媒资信息

    操作结果:保存失败

    如果报数据库的错,将数据库列mediaid改为media_id

    3.4 查询视频信息

    3.4.1 需求分析

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

    解决方案:

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

    3.4.2 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;
    
    /**
     * Created by admin on 2018/2/7.
     */
    @Data
    @ToString
    public class TeachplanNode extends Teachplan {
    
        List<TeachplanNode> children;
    
        //媒资信息
        private String mediaId;
        private String mediaFileOriginalName;
    
    }
    

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

    添加mediaId、mediaFileOriginalName

    <?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 column="one_id" property="id"></id>-->
    <!--        <result column="one_pname" property="pname"></result>-->
    <!--        <collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode">-->
    <!--            <id column="two_id" property="id"></id>-->
    <!--            <result column="two_pname" property="pname"></result>-->
    <!--            <collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode">-->
    <!--                <id column="three_id" property="id"></id>-->
    <!--                <result column="three_pname" property="pname"></result>-->
    <!--            </collection>-->
    <!--        </collection>-->
    <!--    </resultMap>-->
    
    <!--    <select id="selectList" parameterType="java.lang.String" resultMap="teachplanMap">-->
    <!--        SELECT-->
    <!--        a.id one_id,-->
    <!--        a.pname one_pname,-->
    <!--        b.id two_id,-->
    <!--        b.pname two_pname,-->
    <!--        c.id three_id,-->
    <!--        c.pname three_pname-->
    <!--        FROM-->
    <!--        teachplan a-->
    <!--        LEFT JOIN teachplan b-->
    <!--        ON b.parentid = a.id-->
    <!--        LEFT JOIN teachplan c-->
    <!--        ON c.parentid = b.id-->
    <!--        WHERE a.parentid = '0'-->
    <!--        <if test="_parameter !=null and _parameter!=''">-->
    <!--            AND a.courseid = #{courseId}-->
    <!--        </if>-->
    <!--        ORDER BY a.orderby,-->
    <!--        b.orderby,-->
    <!--        c.orderby-->
    <!--    </select>-->
    
        <resultMap type="com.xuecheng.framework.domain.course.ext.TeachplanNode" id="teachplanMap">
            <id property="id" column="one_id"/>
            <result property="pname" column="one_name"/>
            <result property="grade" column="one_grade"/>
            <collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode">
                <id property="id" column="two_id"/>
                <result property="pname" column="two_name"/>
                <result property="grade" column="two_grade"/>
                <collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode">
                    <id property="id" column="three_id"/>
                    <result property="pname" column="three_name"/>
                    <result property="grade" column="three_grade"/>
                    <result property="mediaId" column="mediaId"/>
                    <result property="mediaFileOriginalName" column="mediaFileOriginalName"/>
                </collection>
            </collection>
        </resultMap>
    
        <select id="selectList" resultMap="teachplanMap" parameterType="java.lang.String">
            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'
            <if test="_parameter!=null and _parameter!=''">
                and a.courseid=#{courseId}
            </if>
            ORDER BY a.orderby, b.orderby, c.orderby
        </select>
    </mapper>
    

    3.4.3 页面查询视频

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

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

    效果如下:

    选择视频后立即刷新课程计划树,在提交成功后,添加查询课程计划代码:this.findTeachplan(),完整代码如下:

    			//保存选择的视频
          choosemedia(mediaId,fileOriginalName,mediaUrl){
            //保存视频到课程计划表中
            let teachplanMedia ={}
            teachplanMedia.mediaId =mediaId;
            teachplanMedia.mediaFileOriginalName =fileOriginalName;
            teachplanMedia.mediaUrl =mediaUrl;
            teachplanMedia.courseId =this.courseid;
            //课程计划
            teachplanMedia.teachplanId=this.teachplanId
    
            courseApi.savemedia(teachplanMedia).then(res=>{
                if(res.success){
                    this.$message.success("选择视频成功")
                  //查询课程计划
                  this.findTeachplan()
                }else{
                  this.$message.error(res.message)
                }
            })
          },
    
  • 相关阅读:
    实例讲解虚拟机3种网络模式(桥接、nat、Host-only)
    期刊搜索问题——SCI、EI、IEEE和中文期刊
    面向对象分析与设计(C++)课堂笔记
    windows与VMware ubuntu虚拟机实现文件共享
    GDI+在绘制验证码中的使用
    【转】.net中快捷键的使用
    MD5加密“破解”在.NET平台实现最基本的理解
    UE4 Persona 骨架网格物体动画
    从零开始做3D地图编辑器 基于QT与OGRE
    TBB(Intel Threading Building Blocks)学习笔记
  • 原文地址:https://www.cnblogs.com/artwalker/p/13591813.html
Copyright © 2011-2022 走看看