1.模块需求分析

1.1模块介绍

媒资管理系统是每个在线教育平台所必须具备的,查阅百度百科对它的定义如下:

媒体资源管理(Media Asset Management,MAM)系统是建立在多媒体、网络、数据库和数字存储等先进技术基础上的一个对各种媒体及内容(如视/音频资料、文本文件、图表等)进行数字化存储、管理以及应用的总体解决方案,包括数字媒体的采集、编目、管理、传输和编码转换等所有环节。其主要是满足媒体资源拥有者收集、保存、查找、编辑、发布各种信息的要求,为媒体资源的使用者提供访问内容的便捷方法,实现对媒体资源的高效管理,大幅度提高媒体资源的价值。

  • 每个教学机构都可以在媒资系统管理自己的教学资源,包括:视频、教案等文件。
  • 目前媒资管理的主要管理对象是视频、图片、文档等,包括:媒资文件的查询、文件上传、视频处理等。

  • 媒资查询:教学机构查询自己所拥有的媒资信息。

  • 文件上传:包括上传图片、上传文档、上传视频。
  • 视频处理:视频上传成功,系统自动对视频进行编码处理。
  • 文件删除:教学机构删除自己上传的媒资文件。

    1.2业务流程

  1. 上传图片
    • 教学机构人员在课程信息编辑页面上传课程图片,课程图片统一记录在媒资管理系统
  2. 上传视频
    • 教学机构人员进入媒资管理列表查询自己上传的媒资文件
    • 教育机构用户在媒资管理页面中点击上传视频按钮
    • 选择要上传的文件,自动执行文件上传
    • 视频上传成功会自动处理,处理完成后可以预览视频
  3. 处理视频
    • 对需要转码处理的视频,系统会自动对齐处理,处理后生成视频的URL
  4. 审核媒资
  5. 绑定媒资
    • 课程计划创建好后需要绑定媒资文件,比如:如果课程计划绑定了视频文件,进入课程在线学习界面后,点课程计划名称则在线播放视频
    • 如何将课程计划绑定媒资呢?
    • 教育机构用户进入课程管理页面编辑某一课程,在课程大纲编辑页的某一小节后,可以添加媒资信息
    • 点击添加视频,会弹出对话框,可通过输入视频关键字搜索已审核通过的视频媒资
    • 选择视频媒资,点击提交安努,完成课程计划绑定媒资流程

      1.3数据模型

      本模块媒资文件相关的数据表

      code)

      • 媒资文件表:存储文件信息,包括图片、视频、文档等。
      • media_process: 待处理视频表。
      • media_process_history: 视频处理历史表,记录已经处理成功的视频信息。
      媒资文件与课程计划绑定关系表

      code

      2.搭建模块环境

      2.1构建问题分析

  • 当前这种由前端直接请求微服务的方式存在弊端
  • 如果在前端对每个请求地址都配置绝对路径,非常不利于系统维护,比如下边代码中请求系统管理服务的地址使用的是localhost
    code
  • 基于这个问题可以采用网关来解决,如下图:
    code
  • 这样在前端的代码中只需要指定每个接口的相对路径,如下所示:
    code
  • 有了网关就可以对请求进行路由,路由到具体的微服务,减少外界对接微服务的成本,比如:400电话,路由的试可以根据请求路径进行路由、根据host地址进行路由等, 当微服务有多个实例时可以通过负载均衡算法进行路由
  • 另外,网关还可以实现权限控制、限流等功能。
  • 项目采用Spring Cloud Gateway作为网关,网关在请求路由时需要知道每个微服务实例的地址,项目使用Nacos作用服务发现中心和配置中心,整体的架构图如下:
    code

    2.2搭建Nacos

    2.2.1服务发现中心

    访问:http://192.168.101.65:8848/nacos/
    账号密码:nacos/nacos
  1. 创建命名空间
    code
  2. 在parent中添加依赖
    1
    2
    3
    4
    5
    6
    7
    <dependency>  
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-dependencies</artifactId>
        <version>${spring-cloud-alibaba.version}</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
  3. 在api模块中添加依赖(需要请求的模块都要添加)
    1
    2
    3
    4
    <dependency>  
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
  4. 修改yaml文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    spring:  
      application:
        name: content-api
      cloud:
        nacos:
          server-addr: 192.168.101.65:8848
          discovery:
            namespace: dev
            group: xuecheng-plus-project
  5. 重启查看
    code

    2.2.2配置中心

    配置api
    1. 添加依赖
      1
      2
      3
      4
      <dependency>  
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
      </dependency>
    2. nacos如何去定位一个具体的配置文件,即:namespace、group、dataid.
    • 以下配置的文件名为:content-api-dev.yaml
      code
    1. 添加配置
      code
    2. 扩展(依赖)配置文件和引用公共文件
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      nacos:  
            server-addr: 192.168.101.65:8848
            discovery:
              namespace: dev
              group: xuecheng-plus-project
            config:
              namespace: dev
              group: xuecheng-plus-project
              file-extension: yaml
              refresh-enabled: true
              extension-configs:
                - data-id: content-service-${spring.profiles.active}.yaml
                  group: xuecheng-plus-project
                  refresh: true
              shared-configs:
                - data-id: swagger-${spring.profiles.active}.yaml
                  group: xuecheng-plus-common
                  refresh: true
                - data-id: logging-${spring.profiles.active}.yaml
                  group: xuecheng-plus-common
                  refresh: true
    配置优先级

    code

    • 引入配置文件的形式有:
      1. 以项目应用名方式引入
      2. 以扩展配置文件方式引入
      3. 以共享配置文件 方式引入
      4. 本地配置文件
    • 项目应用名配置文件 > 扩展配置文件 > 共享配置文件 > 本地配置文件
    • 在项目nacos配置文件里面添加
      1
      2
      3
      4
      5
      配置本地优先  
      spring:
       cloud:
        config:
          override-none: true

2.3搭建网关

  1. 创建xuecheng-plus-gateway模块
  2. 导入依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    <?xml version="1.0" encoding="UTF-8"?>  
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
    <groupId>com.xuecheng</groupId>
    <artifactId>xuecheng-plus-parent</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <relativePath>../xuecheng-plus-parent</relativePath>
    </parent>
    <artifactId>xuecheng-plus-gateway</artifactId>

    <dependencies>

    <!--网关-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

    <!--服务发现中心-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    </dependency>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    </dependency>
    <!-- 排除 Spring Boot 依赖的日志包冲突 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <exclusions>
    <exclusion>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-logging</artifactId>
    </exclusion>
    </exclusions>
    </dependency>

    <!-- Spring Boot 集成 log4j2 --> <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>
    </dependencies>

    </project>
  3. 把api里面的resources文件夹拷贝过来
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #微服务配置  
    spring:
    application:
    name: gateway
    cloud:
    nacos:
    server-addr: 192.168.101.65:8848
    discovery:
    namespace: dev001
    group: xuecheng-plus-project
    config:
    namespace: dev001
    group: xuecheng-plus-project
    file-extension: yaml
    refresh-enabled: true
    shared-configs:
    - data-id: logging-${spring.profiles.active}.yaml
    group: xuecheng-plus-common
    refresh: true
    profiles:
    active: dev
  4. 运行启动类,并在nacos中查看是否运行成功
  5. 在测试接口中测试
    1
    POST {{gateway_host}}/content/course/list?pageNo=1&pageSize=2

    2.4搭建媒资工程

    直接在导入后设置bootstrap

    3.分布式文件系统

    3.1介绍

  • 文件系统是负责管理和存储文件的系统软件,操作系统通过文件系统提供的接口去存取文件,用户通过操作系统访问磁盘上的文件。
    code
  • 通过概念可以简单理解为:一个计算机无法存储海量的文件,通过网络将若干计算机组织起来共同去存储海量的文件,去接收海量用户的请求,这些组织起来的计算机通过网络进行通信,如下图:
    code
  • 好处:
    1. 一台计算机的文件系统处理能力扩充到多台计算机同时处理。
    2. 一台计算机挂了还有另外副本计算机提供数据。
    3. 每台计算机可以放在不同的地域,这样用户就可以就近访问,提高访问速度。
  • 市面上有哪些分布式文件系统的产品呢?
  1. NFS
    • 阅读百度百科:

    • 特点:
      • 在客户端上映射NFS服务器的驱动器。
      • 客户端通过网络访问NFS服务器的硬盘完全透明。
  2. GFS

    • GFS采用主从结构,一个GFS集群由一个master和大量的chunkserver组成。
    • master存储了数据文件的元数据,一个文件被分成了若干块存储在多个chunkserver中。
    • 用户从master中获取数据元信息,向chunkserver存储数据。
  3. HDFS

    • HDFS,是Hadoop Distributed File System的简称,是Hadoop抽象文件系统的一种实现。HDFS是一个高度容错性的系统,适合部署在廉价的机器上。HDFS能提供高吞吐量的数据访问,非常适合大规模数据集上的应用。 HDFS的文件分布在集群机器上,同时提供副本进行容错及可靠性保证。例如客户端写入读取文件的直接操作都是分布在集群各个机器上的,没有单点性能压力。
    • 下图是HDFS的架构图:
      • HDFS采用主从结构,一个HDFS集群由一个名称结点和若干数据结点组成。
      • 名称结点存储数据的元信息,一个完整的数据文件分成若干块存储在数据结点。
      • 客户端从名称结点获取数据的元信息及数据分块的信息,得到信息客户端即可从数据块来存取数据。
  4. 云计算厂家

    • 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。其数据设计持久性不低于 99.9999999999%(12 个 9),服务设计可用性(或业务连续性)不低于 99.995%。
    • 官方网站:https://www.aliyun.com/product/oss
    • 百度对象存储BOS提供稳定、安全、高效、高可扩展的云存储服务。您可以将任意数量和形式的非结构化数据存入BOS,并对数据进行管理和处理。BOS支持标准、低频、冷和归档存储等多种存储类型,满足多场景的存储需求。
    • 官方网站:https://cloud.baidu.com/product/bos.html

3.2minio文件系统

3.2.1介绍

官网:https://min.io
中文:https://www.minio.org.cn/
http://docs.minio.org.cn/docs/

  • MinIO 是一个非常轻量的服务,可以很简单的和其他应用的结合使用,它兼容亚马逊 S3 云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等。
  • 它一大特点就是轻量,使用简单,功能强大,支持各种平台,单个文件最大5TB,兼容 Amazon S3接口,提供了 Java、Python、GO等多版本SDK支持。
  • MinIO集群采用去中心化共享架构,每个结点是对等关系,通过Nginx可对MinIO进行负载均衡访问。
    code
  • Minio使用纠删码技术来保护数据,它是一种恢复丢失和损坏数据的数学算法,它将数据分块冗余的分散存储在各各节点的磁盘上,所有的可用磁盘组成一个集合,上图由8块硬盘组成一个集合,当上传一个文件时会通过纠删码算法计算对文件进行分块存储,除了将文件本身分成4个数据块,还会生成4个校验块,数据块和校验块会分散的存储在这8块硬盘上。
  • 使用纠删码的好处是即便丢失一半数量(N/2)的硬盘,仍然可以恢复数据。 比如上边集合中有4个以内的硬盘损害仍可保证数据恢复,不影响上传和下载,如果多于一半的硬盘坏了则无法恢复。

3.2.2window使用

  1. 创建4个目录表示4块硬盘
  2. CMD进入有minio.exe的目录
    1
    minio.exe server D:\develop\minio_data\data1  D:\develop\minio_data\data2  D:\develop\minio_data\data3  D:\develop\minio_data\data4
    code
  3. 按上面给出的链接登录(账号密码默认为minioadmin)
  4. 虚拟机链接:http://192.168.101.65:9000

    3.2.3本地测试文件和视频方法

  • 导入依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <dependency>  
        <groupId>io.minio</groupId>
        <artifactId>minio</artifactId>
        <version>8.4.3</version>
    </dependency>
    <dependency>
        <groupId>com.squareup.okhttp3</groupId>
        <artifactId>okhttp</artifactId>
        <version>4.8.1</version>
    </dependency>

    3.2.3.1minio上传/删除/下载文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    package com.xuecheng.media;  
    import com.j256.simplemagic.ContentInfo;
    import com.j256.simplemagic.ContentInfoUtil;
    import io.minio.*;
    import org.apache.commons.codec.digest.DigestUtils;
    import org.apache.commons.compress.utils.IOUtils;
    import org.junit.jupiter.api.Test;
    import org.springframework.http.MediaType;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.FilterInputStream;


    public class MinioTest {
    static MinioClient minioClient =
    MinioClient.builder()
    .endpoint("http://192.168.101.65:9000")
    .credentials("minioadmin", "minioadmin")
    .build();

    @Test
    public void test_upload() throws Exception {
    //通过扩展名得到媒体资源类型 mineType
    ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(".mp4");
    String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
    //通用mimeType,字节流
    if (extensionMatch != null) {
    mimeType = extensionMatch.getMimeType();
    }
    //上传文件参数信息
    UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
    .bucket("testbucket")//桶
    .filename("D:\\1.mp4")//本地文件路径
    // .object("1.mp4")//对象名
    .object("test/1/1.pm4") //设置多层目录
    .contentType(mimeType) //设置媒体文件类型
    .build();
    //上传文件
    minioClient.uploadObject(uploadObjectArgs);
    }

    @Test
    public void test_delete() throws Exception {

    //文件参数信息
    RemoveObjectArgs deleteObject = RemoveObjectArgs.builder()
    .bucket("testbucket")
    .object("test/1/1.pm4")
    .build();

    //删除文件
    minioClient.removeObject(deleteObject);
    }

    //查询文件 从minio中下载
    @Test
    public void test_getFile() throws Exception {

    GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket("testbucket").object("test/1/1.pm4").build();
    //查询远程服务获取到一个流对象
    FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
    //指定输出流
    FileOutputStream outputStream = new FileOutputStream(new File("D:\\1a.mp4"));
    IOUtils.copy(inputStream, outputStream);

    //校验文件的完整性对文件的内容进行md5
    FileInputStream fileInputStream1 = new FileInputStream(new File("D:\\1.mp4"));
    String source_md5 = DigestUtils.md5Hex(fileInputStream1);
    FileInputStream fileInputStream = new FileInputStream(new File("D:\\1a.mp4"));
    String local_md5 = DigestUtils.md5Hex(fileInputStream);
    if (source_md5.equals(local_md5)) {
    System.out.println("下载成功");
    }
    }
    }

    3.2.3.2本地拆分/合并大文件方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    public class BigFileTest {  
    //分块测试
    @Test
    public void testChunk() throws Exception {
    //源文件
    File sourceFile = new File("D:\\1视频\\下载 (1).mp4");
    //分块文件存储路径
    String chunkFilePath = "D:\\1视频\\chunk\\";
    //分块文件大小
    int chunkSize = 1024 * 1024 *5;
    //分块文件个数
    int chunkNum = (int) Math.ceil(sourceFile.length() * 1.0 / chunkSize);
    //使用流从源文件读数据,向分块文件中写数据
    RandomAccessFile raf_r = new RandomAccessFile(sourceFile, "r");
    //缓存区
    byte[] bytes = new byte[1024];
    for (int i = 0;i < chunkNum;i++){
    File chunkfile = new File(chunkFilePath + i);
    //分块文件写入流
    RandomAccessFile raf_rw = new RandomAccessFile(chunkfile, "rw");
    int len = -1;
    while ((len=raf_r.read(bytes)) != -1){
    raf_rw.write(bytes,0,len);
    if (chunkfile.length() >= chunkSize){
    break;
    }
    }
    raf_rw.close();
    }
    raf_r.close();
    }

    //将分块合并
    @Test
    public void testMerge() throws Exception {
    //块文件目录
    File chunkFolder = new File("D:\\1视频\\chunk");
    //源文件
    File sourceFile = new File("D:\\1视频\\下载 (1).mp4");
    //合并后的文件路径
    File mergeFile = new File("D:\\1视频\\下载 (1)_2.mp4");

    //取出所有分块文件
    File[] files = chunkFolder.listFiles();
    //将数组转成list
    List<File> filesList = Arrays.asList(files);

    //排序
    Collections.sort(filesList, new Comparator<File>() {
    @Override
    public int compare(File o1, File o2) {
    return Integer.parseInt(o1.getName())-Integer.parseInt(o2.getName());
    }
    });

    //向合并文件写的流
    RandomAccessFile raf_rw = new RandomAccessFile(mergeFile, "rw");
    //缓存区
    byte[] bytes = new byte[1024];
    //遍历分块文件,向合并的文件写
    for (File file : filesList) {
    RandomAccessFile raf_r = new RandomAccessFile(file, "r");
    int len = -1;
    while ((len=raf_r.read(bytes)) != -1){
    raf_rw.write(bytes,0,len);
    }
    raf_r.close();
    }
    raf_rw.close();
    //合并文件完成后校验
    FileInputStream fileInputStream_merge = new FileInputStream(mergeFile);
    FileInputStream fileInputStream_source = new FileInputStream(sourceFile);
    String md5_merge = DigestUtils.md5Hex(fileInputStream_merge);
    String md5_source = DigestUtils.md5Hex(fileInputStream_source);
    if (md5_merge.equals(md5_source)){
    System.out.println("文件合并完成");
    }
    }
    }

    3.2.3.3minio拆分合并大文件方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    public class MinioTest {  

    MinioClient minioClient =
    MinioClient.builder()
    .endpoint("http://192.168.101.65:9000")
    .credentials("minioadmin", "minioadmin")
    .build();

    //查询文件 从minio中下载
    @Test
    public void test_getFile() throws Exception {

    GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket("testbucket").object("test/1/1.pm4").build();
    //查询远程服务获取到一个流对象
    FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
    //指定输出流
    FileOutputStream outputStream = new FileOutputStream(new File("D:\\1a.mp4"));
    IOUtils.copy(inputStream, outputStream);

    //校验文件的完整性对文件的内容进行md5
    FileInputStream fileInputStream1 = new FileInputStream(new File("D:\\1.mp4"));
    String source_md5 = DigestUtils.md5Hex(fileInputStream1);
    FileInputStream fileInputStream = new FileInputStream(new File("D:\\1a.mp4"));
    String local_md5 = DigestUtils.md5Hex(fileInputStream);
    if (source_md5.equals(local_md5)) {
    System.out.println("下载成功");
    }
    }

    //将分块文件上传到minio
    @Test
    public void uploadChunk() throws Exception {

    for (int i = 0; i < 9; i++) {
    //上传文件参数信息
    UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
    .bucket("testbucket")//桶
    .filename("D:\\1视频\\chunk\\"+i)//本地文件路径
    .object("chunk/"+i) //设置多层目录
    .build();
    //上传文件
    minioClient.uploadObject(uploadObjectArgs);
    System.out.println("上传分块"+i+"成功");
    }
    }

    //调用minio接口合并分块
    @Test
    public void testMerge() throws Exception{

    List<ComposeSource> sources = new ArrayList<>();
    for (int i = 0; i < 9; i++) {
    //指定分块文件的信息
    ComposeSource composeSource = ComposeSource
    .builder()
    .bucket("testbucket")
    .object("chunk/" + i)
    .build();
    sources.add(composeSource);
    }

    //指定合并后的objectName等信息
    ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
    .bucket("testbucket")
    .object("merge01.mp4")
    .sources(sources) //指定源文件
    .build();
    //合并文件
    minioClient.composeObject(composeObjectArgs);
    }
    //批量清理分块文件
    }

    4.上传文件

    4.1需求分析

  • 媒资管理服务的作用:对文件进行统一管理,文件本身放在minio,文件信息放在media_files表中
  • course_base表中的pic地址为桶的地址路径,前端会把minio的地址进行拼接
    code
    code
  • 数据模型
    code

    4.2准备工作

  1. 把minio里面的桶改为public
  2. 在nacos配置中minio的相关信息
    1
    2
    3
    4
    5
    6
    7
    minio:  
      endpoint: http://192.168.101.65:9000
      accessKey: minioadmin
      secretKey: minioadmin
      bucket:
        files: mediafiles
        videofiles: video
  3. 在media-service工程编写minio的配置类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    package com.xuecheng.media.config;  
    import io.minio.MinioClient;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    @Configuration
    public class MinioConfig {
    @Value("${minio.endpoint}")
    private String endpoint;
    @Value("${minio.accessKey}")
    private String admin;
    @Value("${minio.secretKey}")
    private String passwork;

    @Bean
    public MinioClient minioClient() {

    MinioClient minioClient =
    MinioClient.builder()
    .endpoint(endpoint)
    .credentials(admin, passwork)
    .build();
    return minioClient;
    }
    }

    4.3接口定义

  • 首先分析接口:
  • 请求地址:/media/upload/coursefile
  • 请求内容:Content-Type: multipart/form-data;
  • form-data; name=”filedata”; filename=”具体的文件名称”
  • 响应参数:文件信息,如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    {  
      "id": "a16da7a132559daf9e1193166b3e7f52",
      "companyId": 1232141425,
      "companyName": null,
      "filename": "1.jpg",
      "fileType": "001001",
      "tags": "",
      "bucket": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg",
      "fileId": "a16da7a132559daf9e1193166b3e7f52",
      "url": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg",
      "timelength": null,
      "username": null,
      "createDate": "2022-09-12T21:57:18",
      "changeDate": null,
      "status": "1",
      "remark": "",
      "auditStatus": null,
      "auditMind": null,
      "fileSize": 248329
    }
  • 创建响应实体类(防止前端要加数据,导致修改数据库表)
    1
    2
    3
    4
    5
    package com.xuecheng.media.model.dto;  
    import com.xuecheng.media.model.po.MediaFiles;
    @Data
    public class UploadFileResultDto extends MediaFiles {
    }
  • MediaFilesController接口
    1
    2
    3
    4
    5
    6
    7
    8
    @ApiOperation("上传图片")  
    @RequestMapping(value = "/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public UploadFileResultDto uploadFileResultDto(@RequestPart("filedata") MultipartFile filedata){

    //调用service上传图片

    return null;
    }

    4.4接口实现

  • MediaFileService接口
    1
    2
    3
    4
    5
    6
    //上传文件  
    //参数:1.机构id 2.请求参数 3.本地路径
    public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath);

    //将文件上传到minio
    public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName);
  • MediaFileServiceImpl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    public class MediaFileServiceImpl implements MediaFileService {  
    @Autowired
    MediaFilesMapper mediaFilesMapper;
    @Autowired
    MinioClient minioClient;
    @Autowired
    MediaFileService currentProxy;
    //存储普通文件
    @Value("${minio.bucket.files}")
    private String bucket_mediafiles;
    //存储视频
    @Value("${minio.bucket.videofiles}")
    private String bucket_video;

    @Override
    public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath) {

    //文件名
    String filename = uploadFileParamsDto.getFilename();
    //先得到扩展名
    String extension = filename.substring(filename.lastIndexOf("."));

    //得到mimeType
    String mimeType = getMimeType(extension);

    //子目录
    String defaultFolderPath = getDefaultFolderPath();
    //文件的md5值
    String fileMd5 = getFileMd5(new File(localFilePath));
    String objectName = defaultFolderPath+fileMd5+extension;
    //上传文件到minio
    boolean result = addMediaFilesToMinIO(localFilePath, mimeType, bucket_mediafiles, objectName);
    if(!result){
    XueChengPlusException.cast("上传文件失败");
    }
    //入库文件信息
    MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_mediafiles, objectName);
    if(mediaFiles==null){
    XueChengPlusException.cast("文件上传后保存信息失败");
    }
    //准备返回的对象
    UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
    BeanUtils.copyProperties(mediaFiles,uploadFileResultDto);
    return uploadFileResultDto;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //根据扩展名获取mimeType  
    private String getMimeType(String extension){
    if(extension == null){
    extension = "";
    }
    //根据扩展名取出mimeType
    ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
    String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType,字节流
    if(extensionMatch!=null){
    mimeType = extensionMatch.getMimeType();
    }
    return mimeType;
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    /**  
    * 将文件上传到minio
    * @param localFilePath 文件本地路径
    * @param mimeType 媒体类型
    * @param bucket 桶
    * @param objectName 对象名
    * @return
    */
    public boolean addMediaFilesToMinIO(String localFilePath,String mimeType,String bucket, String objectName){
    try {
    UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
    .bucket(bucket)//桶
    .filename(localFilePath) //指定本地文件路径
    .object(objectName)//对象名 放在子目录下
    .contentType(mimeType)//设置媒体文件类型
    .build();
    //上传文件
    minioClient.uploadObject(uploadObjectArgs);
    log.debug("上传文件到minio成功,bucket:{},objectName:{},错误信息:{}",bucket,objectName);
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    log.error("上传文件出错,bucket:{},objectName:{},错误信息:{}",bucket,objectName,e.getMessage());
    }
    return false;
    }
    1
    2
    3
    4
    5
    6
    //获取文件默认存储目录路径 年/月/日  
    private String getDefaultFolderPath() {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    String folder = sdf.format(new Date()).replace("-", "/")+"/";
    return folder;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //获取文件的md5  
    private String getFileMd5(File file) {
    try (FileInputStream fileInputStream = new FileInputStream(file)) {
    String fileMd5 = DigestUtils.md5Hex(fileInputStream);
    return fileMd5;
    } catch (Exception e) {
    e.printStackTrace();
    return null;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    /**  
    * @description 将文件信息添加到文件表
    * @param companyId 机构id
    * @param fileMd5 文件md5值
    * @param uploadFileParamsDto 上传文件的信息
    * @param bucket 桶
    * @param objectName 对象名称
    * @return com.xuecheng.media.model.po.MediaFiles
    * @author Mr.M * @date 2022/10/12 21:22 */@Transactional
    public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName){
    //将文件信息保存到数据库
    MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
    if(mediaFiles == null){
    mediaFiles = new MediaFiles();
    BeanUtils.copyProperties(uploadFileParamsDto,mediaFiles);
    //文件id
    mediaFiles.setId(fileMd5);
    //机构id
    mediaFiles.setCompanyId(companyId);
    //桶
    mediaFiles.setBucket(bucket);
    //file_path
    mediaFiles.setFilePath(objectName);
    //file_id
    mediaFiles.setFileId(fileMd5);
    //url
    mediaFiles.setUrl("/"+bucket+"/"+objectName);
    //上传时间
    mediaFiles.setCreateDate(LocalDateTime.now());
    //状态
    mediaFiles.setStatus("1");
    //审核状态
    mediaFiles.setAuditStatus("002003");
    //插入数据库
    int insert = mediaFilesMapper.insert(mediaFiles);
    if(insert<=0){
    log.debug("向数据库保存文件失败,bucket:{},objectName:{}",bucket,objectName);
    return null;
    }
    return mediaFiles;

    }
    return mediaFiles;
    }
  • 完善controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    @ApiOperation("上传图片")  
    @RequestMapping(value = "/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile filedata,
    @RequestParam(value = "folder",required=false) String folder,
    @RequestParam(value = "objectName",required=false) String objectName)
    throws IOException {

    //准备上传文件的信息
    UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
    //原始文件名称
    uploadFileParamsDto.setFilename(filedata.getOriginalFilename());
    //文件大小
    uploadFileParamsDto.setFileSize(filedata.getSize());
    //文件类型
    uploadFileParamsDto.setFileType("001001");
    //创建一个临时文件
    File tempFile = File.createTempFile("minio", ".temp");
    filedata.transferTo(tempFile);
    Long companyId = 1232141425L;
    //文件路径
    String localFilePath = tempFile.getAbsolutePath();

    //调用service上传图片
    UploadFileResultDto uploadFileResultDto = mediaFileService.uploadFile(companyId, uploadFileParamsDto, localFilePath);

    return uploadFileResultDto;
    }

    4.5测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ### 上传文件  
    POST {{media_host}}/media/upload/coursefile
    Content-Type: multipart/form-data; boundary=WebAppBoundary

    --WebAppBoundary
    Content-Disposition: form-data; name="filedata"; filename="1.jpg"
    Content-Type: application/octet-stream

    < d:/6.jpg

    5.上传视频

    5.1需求分析

    code

    5.2断点续传

  • 断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载,断点续传可以提高节省操作时间,提高用户体验性。
    code
    code
  1. 前端对文件进行分块。
  2. 前端上传分块文件前请求媒资服务检查文件是否存在,如果已经存在则不再上传。s
  3. 如果分块文件不存在则前端开始上传
  4. 前端请求媒资服务上传分块。
  5. 媒资服务将分块上传至MinIO。
  6. 前端将分块上传完毕请求媒资服务合并分块。
  7. 媒资服务判断分块上传完成则请求MinIO合并文件。
  8. 合并完成校验合并后的文件是否完整,如果不完整则删除文件。

    5.3接口定义

  • 与前端的约定是操作成功返回{code:0}否则返回{code:-1}
  • 在base工程下的model包下定义通用返回的类型RestResponse
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    package com.xuecheng.base.model;
    import lombok.Data;
    import lombok.ToString;

    //@description 通用结果类型
    @Data
    @ToString
    public class RestResponse<T> {

    //响应编码,0为正常,-1错误
    private int code;
    //响应提示信息
    private String msg;
    //响应内容
    private T result;

    public RestResponse() {
    this(0, "success");
    }
    public RestResponse(int code, String msg) {
    this.code = code;
    this.msg = msg;
    }

    //错误信息的封装
    public static <T> RestResponse<T> validfail(String msg) {
    RestResponse<T> response = new RestResponse<T>();
    response.setCode(-1);
    response.setMsg(msg);
    return response;
    }
    public static <T> RestResponse<T> validfail(T result,String msg) {
    RestResponse<T> response = new RestResponse<T>();
    response.setCode(-1);
    response.setResult(result);
    response.setMsg(msg);
    return response;
    }



    //添加正常响应数据(包含响应内容)
    public static <T> RestResponse<T> success(T result) {
    RestResponse<T> response = new RestResponse<T>();
    response.setResult(result);
    return response;
    }
    public static <T> RestResponse<T> success(T result,String msg) {
    RestResponse<T> response = new RestResponse<T>();
    response.setResult(result);
    response.setMsg(msg);
    return response;
    }

    //添加正常响应数据(不包含响应内容)
    public static <T> RestResponse<T> success() {
    return new RestResponse<T>();
    }

    public Boolean isSuccessful() {
    return this.code == 0;
    }

    }
  • 创建BigFilesController
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    @Api(value = "大文件上传接口", tags = "大文件上传接口")
    @RestController
    public class BigFilesController {
    @ApiOperation(value = "文件上传前检查文件")
    @PostMapping("/upload/checkfile")
    public RestResponse<Boolean> checkFile(@RequestParam("fileMd5") String fileMd5) {
    return null;
    }

    @ApiOperation(value = "分块文件上传前检查分块")
    @PostMapping("/upload/checkchunk")
    public RestResponse<Boolean> checkChunk(@RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") int chunk) {
    return null;
    }

    @ApiOperation(value = "上传分块文件")
    @PostMapping("/upload/uploadchunk")
    public RestResponse uploadChunk(@RequestParam("file") MultipartFile file, @RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") int chunk) {
    return null;
    }

    @ApiOperation(value = "合并分块文件")
    @PostMapping("/upload/mergechunks")
    public RestResponse mergeChunks(@RequestParam("fileMd5") String fileMd5, @RequestParam("fileName") String fileName, @RequestParam("chunkTotal") int chunkTotal) {
    return null;
    }
    }

    5.4接口定义

    5.4.1检查文件和分块

  • 定义MediaFileService接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
    * 检查文件是否存在
    *
    * @param fileMd5 文件的md5
    * @return
    */
    boolean checkFile(String fileMd5);

    /**
    * 检查分块是否存在
    * @param fileMd5 文件的MD5
    * @param chunkIndex 分块序号
    * @return
    */
    boolean checkChunk(String fileMd5, int chunkIndex);
  • 判断文件是否存在
    • 首先判断数据库中是否存在该文件
    • 其次判断minio的bucket中是否存在该文件
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      @Override
      public RestResponse<Boolean> checkFile(String fileMd5) {
      MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
      // 数据库中不存在,则直接返回false 表示不存在
      if (mediaFiles == null) {
      return RestResponse.success(false);
      }
      // 若数据库中存在,根据数据库中的文件信息,则继续判断bucket中是否存在
      try {
      InputStream inputStream = minioClient.getObject(GetObjectArgs
      .builder()
      .bucket(mediaFiles.getBucket())
      .object(mediaFiles.getFilePath())
      .build());
      if (inputStream == null) {
      return RestResponse.success(false);
      }
      } catch (Exception e) {
      return RestResponse.success(false);
      }
      return RestResponse.success(true);
      }
  • 判断分块是否存在
    • 分块是否存在,只需要判断minio对应的目录下是否存在分块文件
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      @Override
      public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
      // 获取分块目录
      String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
      String chunkFilePath = chunkFileFolderPath + chunkIndex;
      try {
      // 判断分块是否存在
      InputStream inputStream = minioClient.getObject(GetObjectArgs
      .builder()
      .bucket(video_files)
      .object(chunkFilePath)
      .build());
      // 不存在返回false
      if (inputStream == null) {
      return RestResponse.success(false);
      }
      } catch (Exception e) {
      // 出异常也返回false
      return RestResponse.success(false);
      }
      // 否则返回true
      return RestResponse.success();
      }

      private String getChunkFileFolderPath(String fileMd5) {
      return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
      }

      5.4.2上传分块

  • 定义接口
    1
    2
    3
    4
    5
    6
    7
    8
    /**
    * 上传分块
    * @param fileMd5 文件MD5
    * @param chunk 分块序号
    * @param bytes 文件字节
    * @return
    */
    RestResponse uploadChunk(String fileMd5,int chunk,byte[] bytes);
  • 接口实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Override
    public RestResponse uploadChunk(String fileMd5, int chunk, byte[] bytes) {
    // 分块文件路径
    String chunkFilePath = getChunkFileFolderPath(fileMd5) + chunk;
    try {
    addMediaFilesToMinIO(bytes, video_files, chunkFilePath);
    return RestResponse.success(true);
    } catch (Exception e) {
    log.debug("上传分块文件:{}失败:{}", chunkFilePath, e.getMessage());
    }
    return RestResponse.validfail("上传文件失败", false);
    }

    5.4.3上传分块测试

  • 完善controller
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @ApiOperation(value = "文件上传前检查文件")
    @PostMapping("/upload/checkfile")
    public RestResponse<Boolean> checkFile(@RequestParam("fileMd5") String fileMd5) {
    return mediaFileService.checkFile(fileMd5);
    }

    @ApiOperation(value = "分块文件上传前检查分块")
    @PostMapping("/upload/checkchunk")
    public RestResponse<Boolean> checkChunk(@RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") int chunk) {
    return mediaFileService.checkChunk(fileMd5, chunk);
    }

    @ApiOperation(value = "上传分块文件")
    @PostMapping("/upload/uploadchunk")
    public RestResponse uploadChunk(@RequestParam("file") MultipartFile file, @RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") int chunk) throws Exception {
    return mediaFileService.uploadChunk(fileMd5, chunk, file.getBytes());
    }