Loading

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="width:160px"></el-input>
      原始名称:
      <el-input v-model="params.fileOriginalName" style="width: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="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="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)
            }
        })
      },
posted @ 2020-08-31 20:42  Artwalker  阅读(245)  评论(0编辑  收藏  举报
Live2D