目录

学成在线05-媒资管理-图片管理

[学成在线]05-媒资管理-图片管理

需求分析

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

每个教学机构都可以在媒资系统管理自己的教学资源,包括:视频、教案等文件。

目前媒资管理的主要管理对象是视频、图片、文档等,包括:媒资文件的查询、文件上传、视频处理等。

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

文件上传:包括上传图片、上传文档、上传视频。

视频处理:视频上传成功,系统自动对视频进行编码处理。

文件删除:教学机构删除自己上传的媒资文件。

下图是课程编辑与发布的整体流程,通过下图可以看到媒资管理在整体流程的位置:

https://i-blog.csdnimg.cn/img_convert/94f6e6a11ffc46c31f2c63324cb4143d.png

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

https://i-blog.csdnimg.cn/img_convert/fbdf83bf1218fb91f3da385c8dc747e4.png

  1. 媒资文件表:存储文件信息,包括图片、视频、文档等。
  2. media_process: 待处理视频表。
  3. media_process_history: 视频处理历史表,记录已经处理成功的视频信息。

媒资文件与课程计划绑定关系表如下:

https://i-blog.csdnimg.cn/img_convert/eafa4c76ff4efa8711047a4b0e3a0bf6.png

环境搭建

架构分析

当前要开发的是媒资管理服务,目前为止共三个微服务:内容管理、系统管理、媒资管理,如下图:

https://i-blog.csdnimg.cn/img_convert/97c5e33092658cff07b747f01acec466.png

后期还会添加更多的微服务,当前这种由前端直接请求微服务的方式存在弊端:

  1. 如果在前端对每个请求地址都配置绝对路径,非常不利于系统维护,比如下边代码中请求系统管理服务的地址使用的是localhost

https://i-blog.csdnimg.cn/img_convert/0318aa7854b53c7659ec84206f6bef6a.png

  1. 当系统上线后这里需要改成公网的域名,如果这种地址非常多则非常麻烦。
  2. 基于这个问题可以采用网关来解决,如下图:

https://i-blog.csdnimg.cn/img_convert/9f5a71bba204c638fc8b0a129540e96d.png

  1. 这样在前端的代码中只需要指定每个接口的相对路径,如下所示

https://i-blog.csdnimg.cn/img_convert/9233115d8c700071d5c9b533460430d2.png

  1. 在前端代码的一个固定的地方在接口地址前统一加网关的地址,每个请求统一到网关,由网关将请求转发到具体的微服务。

为什么所有的请求先到网关呢?

  1. 有了网关就可以对请求进行路由,路由到具体的微服务,减少外界对接微服务的成本,比如:400电话,路由的话可以根据请求路径进行路由、根据host地址进行路由等,当微服务有多个实例时可以通过负载均衡算法进行路由,如下:

https://i-blog.csdnimg.cn/img_convert/e365a9d5144cf39b25cffdcbfba15284.png

  1. 另外,网关还可以实现权限控制、限流等功能。

项目采用Spring Cloud Gateway作为网关,网关在请求路由时需要知道每个微服务实例的地址,项目使用Nacos作用服务发现中心和配置中心,整体的架构图如下:

https://i-blog.csdnimg.cn/img_convert/39001975d3cc8d6f1f5348d1e22704e4.png

流程如下:

1、微服务启动,将自己注册到Nacos,Nacos记录了各微服务实例的地址。

2、网关从Nacos读取服务列表,包括服务名称、服务地址等。

3、请求到达网关,网关将请求路由到具体的微服务。

要使用网关首先搭建Nacos,Nacos有两个作用:

1、服务发现中心。

微服务将自身注册至Nacos,网关从Nacos获取微服务列表。

2、配置中心。

微服务众多,它们的配置信息也非常复杂,为了提供系统的可维护性,微服务的配置信息统一在Nacos配置。

搭建Nacos

服务发现

根据上节讲解的网关的架构图,要使用网关首先搭建Nacos。

先明确概念

  1. Spring Cloud :是一套微服务的规范
  2. Spring Cloud alibaba: 阿里巴巴实现Spring Cloud的一套技术栈, 其中包括nacos技术, 用于服务注册中心,配置中心

首先搭建Nacos服务发现中心。

  1. 在搭建Nacos服务发现中心之前需要搞清楚两个概念:namespace和group
  • namespace(命名空间):用于区分环境、比如:开发环境、测试环境、生产环境。
  • group(分组):用于区分项目,比如:xuecheng-plus项目、xuecheng2.0项目
  1. 首先在nacos配置namespace:
  • 登录Centos,启动Naocs,使用sh /data/soft/restart.sh将自动启动Nacos。
  • 访问:http://192.168.101.65:8848/nacos/
  • 账号密码:nacos/nacos
  • 登录成功,点击左侧菜单“命名空间”进入命名空间管理界面,点击“新建命名空间”,填写命名空间的相关信息。如下图:

https://i-blog.csdnimg.cn/img_convert/ea294427b8c3ce8d55299fb67ea6505f.png

  • 使用相同的方法再创建“测试环境”、“生产环境"的命名空间。
  • 创建成功,如下图:

https://i-blog.csdnimg.cn/img_convert/423cdd8a2ab3a729c359bb07db63ae7b.png

  1. 完成各服务注册到Naocs,下边将内容管理服务注册到nacos中。
  • 在xuecheng-plus-parent中添加依赖管理
<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>
  • 在内容管理模块的接口工程、系统管理模块的接口工程中添加如下依赖
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  • 配置nacos的地址
// 在系统管理的接口工程的配置文件中配置如下信息:

spring:
  application:
    name: system-service
  cloud:
    nacos:
      server-addr: 192.168.101.65:8848
      discovery:
        namespace: dev
        group: xuecheng-plus-project
// 在内容管理的接口工程的配置文件中配置如下信息:

spring:
  application:
    name: content-api
  cloud:
    nacos:
      server-addr: 192.168.101.65:8848
      discovery:
        namespace: dev
        group: xuecheng-plus-project
  1. 重启内容管理服务、系统管理服务。待微服务启动成功,进入Nacos服务查看服务列表
  2. 在 “开发环境” 命名空间下有两个服务这说明内容管理微服务和系统管理微服务在Nacos注册成功

https://i-blog.csdnimg.cn/img_convert/6a9a4997c00ccee25021baf4a6920e6a.png

  1. 点击其它一个微服务的“详情, 以查看微服务实例的地址。

https://i-blog.csdnimg.cn/img_convert/07e581a040868b9cb26907458b5852fa.png

配置中心

搭建完成Nacos服务发现中心,下边搭建Nacos为配置中心,其目的就是通过Nacos去管理项目的所有配置。

前置梳理

先将项目中的配置文件分分类:

1、每个服务特有的配置

是指该配置只在有些项目中需要配置,或者该配置在每个项目中配置的值不同。

比如:spring.application.name每个项目都需要配置但值不一样,以及有些项目需要连接数据库而有些项目不需要,有些项目需要配置消息队列而有些项目不需要。

2、服务共用的配置

是指在若干项目中配置内容相同的配置。比如:redis的配置,很多项目用的同一套redis服务所以配置也一样。

配置三要素

需要知道nacos如何去定位一个具体的配置文件,即:namespace、group、dataid.

1、通过namespace、group找到具体的环境和具体的项目。

2、通过dataid找到具体的配置文件,dataid有三部分组成

3、比如:content-service-dev.yaml配置文件, 由(content-service)-(dev). (yaml)三部分组成

  • content-service:第一部分,它是在application.yaml中配置的应用名,即spring.application.name的值。
  • dev:第二部分,它是环境名,通过spring.profiles.active指定,
  • Yaml: 第三部分,它是配置文件的后缀,目前nacos支持properties、yaml等格式类型,本项目选择yaml格式类型。

4、大型项目, 服务的配置文件一般有三个, 根据需要使用不同的配置文件:

  • 在开发环境中配置content-service-dev.yaml
  • 在测试环境中配置content-service-test.yaml
  • 在生产环境中配置content-service-prod.yaml

5、补充: 我们启动项目时, 也可以通过传入spring.profiles.active的参数, 决定引用哪个环境的配置文件,6、例如:传入spring.profiles.active=dev表示使用dev环境的配置文件即content-service-dev.yaml

配置特有配置

把各微服务特有的配置提取到nacos配置中心

  1. 下边以开发环境为例, 把对content-service服务中的content-api模块的配置文件进行提取,进入nacos,进入开发环境。nacos服务地址: http://192.168.101.65:8848/nacos/

https://i-blog.csdnimg.cn/img_convert/6cc5bbc9280cec1ec073581e051b238a.png

  1. 添加一个配置: 输入data id、group以及配置文件内容

https://i-blog.csdnimg.cn/img_convert/f4d2d5242856bd8211dfdff618972891.png

#微服务配置
#Data id = 服务名 + 环境名 + 配置格式
#Group = group
#上面的对应关系不能出错, 不然项目中就不能正确的定位nacos中的配置文件
spring:
  application:
    name: content-api # 服务名
  cloud:
    nacos:
      server-addr: 192.168.101.65:8848
      discovery: #服务注册相关配置
        namespace: dev # 命名空间
        group: xuecheng-plus-project # 服务分组,通用指定项目名
  profiles:
    active: dev # 环境名,默认为dev
  1. 在本地的配置文件, 添加nacos配置文件的信息, 去掉重复配置的部分
#微服务配置
spring:
  application:
    name: content-api # 服务名
  cloud:
    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
  profiles:
    active: dev # 环境名,默认为dev
  1. 在该服务引入依赖
   <!-- nacos配置拉取 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
  1. 重启服务: NacosRestTemplate.java通过Post方式与nacos服务端交互读取配置信息。

https://i-blog.csdnimg.cn/img_convert/2e1de00943b7ad368d622df34160cacd.png

  1. 进到swagger文件, 测试接口, 结果正确返回, 说明数据库连接成功, 证明nacos配置文件生效

接口文档地址:

https://i-blog.csdnimg.cn/img_convert/dae881e3fd3750fc3a0dbd290a6988fa.png

继续把content-service服务中的content-service模块的配置文件进行提取

  1. 在content-service模块中引入依赖
  <!-- nacos配置拉取 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
  1. 在本地配置文件中配置nacos配置文件的信息
#微服务配置
spring:
  application:
    name: content-service
    datasource:
     driver-class-name: com.mysql.cj.jdbc.Driver
     url: jdbc:mysql://192.168.101.65:3306/xcplus_content?serverTimezone=UTC&userUnicode=true&useSSL=false&
     username: root
     password: mysql
  cloud:
    nacos:
      server-addr: 192.168.101.65:8848
      config: # 配置中心读取配置文件的相关配置(配置拉取)
        namespace: dev
        group: xuecheng-plus-project
        file-extension: yaml
        refresh-enabled: true
  profiles:
    active: dev # 环境名,默认为dev
# 日志文件配置路径
logging:
  config: classpath:log4j2-dev.xml
  1. 在nacos中创建配置文件

https://i-blog.csdnimg.cn/img_convert/5ec35d6671fa1448810d0906ce3b1314.png

https://i-blog.csdnimg.cn/img_convert/7f4cb471aa413f1d7ca5fe6911aa34cc.png

  1. 把本地配置文件的重复配置注释掉
#微服务配置
spring:
  application:
    name: content-service
#  datasource:
#    driver-class-name: com.mysql.cj.jdbc.Driver
#    url: jdbc:mysql://192.168.101.65:3306/xcplus_content?serverTimezone=UTC&userUnicode=true&useSSL=false&
#    username: root
#    password: mysql
  config: # 配置中心读取配置文件的相关配置(配置拉取)
    namespace: dev
    group: xuecheng-plus-project
    file-extension: yaml
    refresh-enabled: true
  profiles:
    active: dev # 环境名,默认为dev
# 日志文件配置路径
logging:
  config: classpath:log4j2-dev.xml
  1. 运行测试方法, 检查是否正确的拉取了nacos配置文件

https://i-blog.csdnimg.cn/img_convert/81c78a677741ff4f096bf65b2337b660.png

  1. 这样配置完成以后, content-api模块和content-service模块的本地配置就迁移到了nacos的配置文件
  2. 但是, 目前两个模块存在重复的配置, 就是datasource选项(数据库连接参数), api接口工程依赖了service工程的jar,这里使用extension-configs扩展配置文件的方式, 让api模块引用service模块的配置文件, 就不需要重复配置了。
#微服务配置
spring:
  application:
    name: content-api # 服务名
  cloud:
    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

https://i-blog.csdnimg.cn/img_convert/22017ef0df3f693755d5cf97759085b1.png

  1. 测试api模块是否正常工作

https://i-blog.csdnimg.cn/img_convert/2cc85aab6fde42472686240cdc77ba49.png

https://i-blog.csdnimg.cn/img_convert/a0fad23e4dc1744809e5773b795c1cf1.png

配置公用配置

把微服务公用的配置提取到nacos配置中心

把swagger和logging的配置提取到nacos中, 单独在xuecheng-plus-common分组下创建xuecheng-plus的公用配置, 哪个项目用引入即可。

  1. 在nacos中新建swagger的配置文件

https://i-blog.csdnimg.cn/img_convert/5ec35d6671fa1448810d0906ce3b1314.png

https://i-blog.csdnimg.cn/img_convert/94481c2df5979ba5912157ff36a7ecf4.png

  1. 在nacos中新建logging的配置文件

https://i-blog.csdnimg.cn/img_convert/dac10964e08dccf210d72772e8cc53e1.png

  1. nacos提供了shared-configs可以引入公用配置。
微服务配置
spring:
  application:
    name: content-api # 服务名
  cloud:
    nacos:
      server-addr: 192.168.101.65:8848
      config: # 配置中心读取配置文件的相关配置(配置拉取)
        namespace: dev
        group: xuecheng-plus-project
        file-extension: yaml
        refresh-enabled: 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
  profiles:
    active: dev # 环境名,默认为dev
  1. 重启服务, 访问接口文档, 正常展示说明配置成功了

https://i-blog.csdnimg.cn/img_convert/708602d1f758185d70ab1dee7e2e9503.png

配置的优先级

到目前为止已将所有微服务的配置统一在nacos进行配置,用到的配置文件有本地的配置文件 bootstrap.yaml和nacos上的配置文件,SpringBoot读取配置文件的顺序如下:

https://i-blog.csdnimg.cn/img_convert/eb9167a713e8f6284216f6da45fda7ac.png

目前项目中引入配置文件的形式有:

https://i-blog.csdnimg.cn/img_convert/7de2c77c763c29256d16f7b41f9740ff.png

1、以项目应用名方式引入

2、以扩展配置文件方式引入

3、以共享配置文件方式引入

4、本地配置文件

各配置文件的优先级:项目应用名配置文件 > 扩展配置文件 > 共享配置文件 > 本地配置文件。

有时候我们在测试程序时直接在本地加一个配置进行测试,比如下边的例子:

  1. 我们想启动两个内容管理微服务,此时需要在本地指定不同的端口,

https://i-blog.csdnimg.cn/img_convert/93fec0b8cf266a7780272956d552c699.png

  1. 通过VM Options参数,在IDEA配置启动参数

https://i-blog.csdnimg.cn/img_convert/87df4820417a61a7bfecedff8cbb6d41.png

  1. 通过-D指定参数名和参数值,参数名即在bootstrap.yml中配置的server.port。

https://i-blog.csdnimg.cn/img_convert/ff2e704d6c7d1e6ae9c0876abbd4f26f.png

  1. 由于本地配置文件的优先级最低, 所以这样的配置不会生效, 想让本地最优先,可以在nacos配置文件中配置如下即可实现
#配置本地优先
spring:
 cloud:
  config:
    override-none: true

https://i-blog.csdnimg.cn/img_convert/b5fa4915333ea94d7875838351f1184b.png

  1. 启动ContentApplication3,发现端口仍然是63043,本地的配置生效了。

https://i-blog.csdnimg.cn/img_convert/fc813a6c85f4ca3f48f5fae1e0020500.png

  1. VM Options参数的作用和配置文件的作用一样的

https://i-blog.csdnimg.cn/img_convert/795bea349c410321a75218d6fb402242.png

https://i-blog.csdnimg.cn/img_convert/0990a72b2cb49a1b0715f8109224f2bf.png

导入配置文件

课程资料中提供了系统用的所有配置文件nacos_config_export.zip,下边将nacos_config_export.zip导入nacos。

  1. 进入具体的命名空间,点击“导入配置”

https://i-blog.csdnimg.cn/img_convert/454646045b3eb33b218fc0062b515233.png

  1. 打开导入窗口, 相同的配置选择跳过配置。

https://i-blog.csdnimg.cn/img_convert/01b0fd9975acbb3ee4ded6f1a9c01e39.png

  1. 点击“上传文件”选择资料中的nacos_config_export.zip开始导入

搭建网关

本项目使用Spring Cloud Gateway作为网关,下边创建网关工程。

  1. 新建一个网关工程。

https://i-blog.csdnimg.cn/img_convert/4f411d7920abdd0940f3d6517fe4dc18.png

  1. 整理工程结构

https://i-blog.csdnimg.cn/img_convert/2d73a8333a72a3d34f24384cdee71031.png

  1. 添加依赖
<?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>
  1. 配置网关的bootstrap.yaml配置文件

https://i-blog.csdnimg.cn/img_convert/1aa68f0a644e40ec778968333694eb5e.png

#微服务配置
spring:
  application:
    name: gateway
  cloud:
    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
        shared-configs:
          - data-id: logging-${spring.profiles.active}.yaml
            group: xuecheng-plus-common
            refresh: true


  profiles:
    active: dev
  1. 在nacos上配置网关路由策略(已经导入, 这里确认一下就行):
server:
  port: 63010 # 网关端口
spring:
  cloud:
    gateway:
#      filter:
#        strip-prefix:
#          enabled: true
      routes: # 网关路由配置
        - id: content-api # 路由id,自定义,只要唯一即可
          # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
          uri: lb://content-api # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/content/** # 这个是按照路径匹配,只要以/content/开头就符合要求
#          filters:
#            - StripPrefix=1
        - id: system-api
          # uri: http://127.0.0.1:8081
          uri: lb://system-api
          predicates:
            - Path=/system/**
#          filters:
#            - StripPrefix=1
        - id: media-api
          # uri: http://127.0.0.1:8081
          uri: lb://media-api
          predicates:
            - Path=/media/**
#          filters:
#            - StripPrefix=1
        - id: search-service
          # uri: http://127.0.0.1:8081
          uri: lb://search
          predicates:
            - Path=/search/**
#          filters:
#            - StripPrefix=1
        - id: auth-service
          # uri: http://127.0.0.1:8081
          uri: lb://auth-service
          predicates:
            - Path=/auth/**
#          filters:
#            - StripPrefix=1
        - id: checkcode
          # uri: http://127.0.0.1:8081
          uri: lb://checkcode
          predicates:
            - Path=/checkcode/**
#          filters:
#            - StripPrefix=1
        - id: learning-api
          # uri: http://127.0.0.1:8081
          uri: lb://learning-api
          predicates:
            - Path=/learning/**
#          filters:
#            - StripPrefix=1
        - id: orders-api
          # uri: http://127.0.0.1:8081
          uri: lb://orders-api
          predicates:
            - Path=/orders/**
#          filters:
#            - StripPrefix=1
  1. 启动网关工程,查看网关是否注册到nacos中

https://i-blog.csdnimg.cn/img_convert/89157594fdec01580c137c9d7f6acc4f.png

  1. 通过网关工程访问微服务进行测试。

https://i-blog.csdnimg.cn/img_convert/fb5e8c96bb9f1f08fd750544b59db8ff.png

  1. 网关工程搭建完成即可将前端工程中的接口地址改为网关的地址

https://i-blog.csdnimg.cn/img_convert/4dae2d3b78faa6430997fe271644ae3f.png

  1. 启动前端工程,测试之前开发内容管理模块的功能。

https://i-blog.csdnimg.cn/img_convert/6b272765f0cd25475e18ec86e16d68b0.png

  1. 观察网关控制台,通过网关转发课程查询的日志如下:

https://i-blog.csdnimg.cn/img_convert/7836736a7bc6f3c392b33a32cdc11acf.png

搭建媒资工程

从课程资料中获取媒资工程 xuecheng-plus-media,拷贝到项目工程根目录。

右键pom.xml转为maven工程。

https://i-blog.csdnimg.cn/img_convert/a7903ea3cd644e2c93eb0bc3b3c512af.png

创建媒资数据库xc_media,并导入资料目录中的xcplus_media.sql

https://i-blog.csdnimg.cn/img_convert/36207d80a125508b2bdef03af933525e.png

检查本地的配置文件和nacos上的media-service-dev.yaml配置文件中的配置信息

https://i-blog.csdnimg.cn/img_convert/2bf1ef254c845fefa8647d981127193a.png

https://i-blog.csdnimg.cn/img_convert/873c87eef29c74e9be38b90d2b888ca2.png

重启media-api工程只要能正常启动成功即可,访问接口文档, 稍后根据需求写接口。

https://i-blog.csdnimg.cn/img_convert/7ca90f98c49aaa16360b188f4361382c.png

分布式文件系统

概念介绍

要理解分布式文件系统首先了解什么是文件系统。

文件系统是负责管理和存储文件的系统软件,操作系统通过文件系统提供的接口去存取文件,用户通过操作系统访问磁盘上的文件。

下图指示了文件系统所处的位置:

https://i-blog.csdnimg.cn/img_convert/d657ec9ee0e13fdf33e473de1e6d10de.png

常见的文件系统:FAT16/FAT32、NTFS、HFS、UFS、APFS、XFS、Ext4等。

现在有个问题,一此短视频平台拥有大量的视频、图片,这些视频文件、图片文件该如何存储呢?如何存储可以满足互联网上海量用户的浏览。

  1. 今天讲的分布式文件系统就是海量用户查阅海量文件的方案。
  2. 一个计算机无法存储海量的文件,通过网络将若干计算机组织起来共同去存储海量的文件,去接收海量用户的请求,这些组织起来的计算机通过网络进行通信,如下图:

https://i-blog.csdnimg.cn/img_convert/15cabecede0f24afdc0788113c8006bb.png

  1. 好处:
  • 一台计算机的文件系统处理能力扩充到多台计算机同时处理。
  • 一台计算机挂了还有另外副本计算机提供数据。
  • 每台计算机可以放在不同的地域,这样用户就可以就近访问,提高访问速度。

市面上有哪些分布式文件系统的产品呢?

  1. NFS

网络文件系统(NFS)是文件系统之上的一个网络抽象,来允许远程客户端以与本地文件系统类似的方式,来通过网络进行访问。虽然 NFS 不是第一个此类系统,但是它已经发展并演变成 UNIX系统中最强大最广泛使用的网络文件系统。NFS 允许在多个用户之间共享公共文件系统,并提供数据集中的优势,来最小化所需的存储空间。

https://i-blog.csdnimg.cn/img_convert/24f0736369e53dc05544e11a8a8c95d2.png

特点:

1)在客户端上映射NFS服务器的驱动器。

2)客户端通过网络访问NFS服务器的硬盘完全透明。

  1. GFS

GFS是一个可扩展的分布式文件系统,用于大型的、分布式的、对大量数据进行访问的应用。它运行于廉价的苦通硬件上,可以提供容错功能,它可以以给大量的用户提供总体性能较高的服务。

https://i-blog.csdnimg.cn/img_convert/5944f1a3aeb4d6703a223d9c678bb119.png

1)GFS采用主从结构,一个GFS集群由一个master和大量的chunkserver组成。

2)master存储了数据文件的元数据,一个文件被分成了若干块存储在多个chunkserver中。

3)用户从master中获取数据元信息,向chunkserver存储数据。

  1. HDFS

HDFS,是Hadoop Distributed File System的简称,是Hadoop抽象文件系统的一种实现。HDFS是一个高度容错性的系统,适合部署在廉价的机器上。HDFS能提供高吞吐量的数据访问,非常适合大规模数据集上的应用。 HDFS的文件分布在集群机器上,同时提供副本进行容错及可靠性保证。例如客户端写入读取文件的直接操作都是分布在集群各个机器上的,没有单点性能压力。

https://i-blog.csdnimg.cn/img_convert/287769ab9b70cc82120867c1c67ca208.png

1)HDFS采用主从结构,一个HDFS集群由一个名称结点和若干数据结点组成。

  1. 名称结点存储数据的元信息,一个完整的数据文件分成若干块存储在数据结点。

3)客户端从名称结点获取数据的元信息及数据分块的信息,得到信息客户端即可从数据块来存取数据。

  1. 云计算厂家

阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。其数据设计持久性不低于 99.9999999999%(12 个 9),服务设计可用性(或业务连续性)不低于 99.995%。

官方网站:

百度对象存储BOS提供稳定、安全、高效、高可扩展的云存储服务。您可以将任意数量和形式的非结构化数据存入BOS,并对数据进行管理和处理。BOS支持标准、低频、冷和归档存储等多种存储类型,满足多场景的存储需求。

官方网站:

MinIO介绍

项目采用MinIO构建分布式文件系统,MinIO 是一个非常轻量的服务,可以很简单的和其他应用的结合使用,它兼容亚马逊 S3 云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等。

  1. 它一大特点就是轻量,使用简单,功能强大,支持各种平台,单个文件最大5TB,兼容 Amazon S3接口,提供了 Java、Python、GO等多版本SDK支持。
  2. 官网:https://min.io
  3. 中文:https://www.minio.org.cn/,http://docs.minio.org.cn/docs/

MinIO集群采用去中心化共享架构,每个结点是对等关系,通过Nginx可对MinIO进行负载均衡访问。

  1. 去中心化有什么好处?
  2. 在大数据领域,通常的设计理念都是无中心和分布式。Minio分布式模式可以帮助你搭建一个高可用的对象存储服务,你可以使用这些存储设备,而不用考虑其真实物理位置。
  3. 它将分布在不同服务器上的多块硬盘组成一个对象存储服务。由于硬盘分布在不同的节点上,分布式Minio避免了单点故障。如下图:

https://i-blog.csdnimg.cn/img_convert/662ee80879c0ef9fc0b628e77a1c12f8.png

  1. Minio使用纠删码技术来保护数据,它是一种恢复丢失和损坏数据的数学算法,它将数据分块冗余的分散存储在各各节点的磁盘上,所有的可用磁盘组成一个集合,上图由8块硬盘组成一个集合,当上传一个文件时会通过纠删码算法计算对文件进行分块存储,除了将文件本身分成4个数据块,还会生成4个校验块,数据块和校验块会分散的存储在这8块硬盘上。
  2. 使用纠删码的好处是即便丢失一半数量(N/2)的硬盘,仍然可以恢复数据。比如上边集合中有4个以内的硬盘损害仍可保证数据恢复,不影响上传和下载,如果多于一半的硬盘坏了则无法恢复。

数据恢复演示

下边在本机演示MinIO恢复数据的过程,在本地创建4个目录表示4个硬盘。

https://i-blog.csdnimg.cn/img_convert/96a74654ae9f781a5d633158a15cac45.png

下载minio,下载地址在https://dl.min.io/server/minio/release/,可从课程资料找到MinIO的安装文件minio.zip解压即可使用

CMD进入有minio.exe的目录,运行下边的命令:

minio.exe server D:\UserDatas\Note\16Project\xue-cheng-zai-xian\minio\data1 D:\UserDatas\Note\16Project\xue-cheng-zai-xian\minio\data2 D:\UserDatas\Note\16Project\xue-cheng-zai-xian\minio\data3 D:\UserDatas\Note\16Project\xue-cheng-zai-xian\minio\data4

启动结果如下:

https://i-blog.csdnimg.cn/img_convert/c55a71a3df10d91191fea0340f16f7cf.png

说明如下:

WARNING: MINIO_ACCESS_KEY and MINIO_SECRET_KEY are deprecated.
         Please use MINIO_ROOT_USER and MINIO_ROOT_PASSWORD
Formatting 1st pool, 1 set(s), 4 drives per set.
WARNING: Host local has more than 2 drives of set. A host failure will result in data becoming unavailable.
WARNING: Detected default credentials 'minioadmin:minioadmin', we recommend that you change these values with 'MINIO_ROOT_USER' and 'MINIO_ROOT_PASSWORD' environment variables

1)老版本使用的MINIO_ACCESS_KEY 和 MINIO_SECRET_KEY不推荐使用,推荐使用MINIO_ROOT_USER 和MINIO_ROOT_PASSWORD设置账号和密码。

2)pool即minio节点组成的池子,当前有一个pool和4个硬盘组成的set集合

3)因为集合是4个硬盘,大于2的硬盘损坏数据将无法恢复。

4)账号和密码默认为minioadmin、minioadmin,可以在环境变量中设置通过’MINIO_ROOT_USER’ and ‘MINIO_ROOT_PASSWORD’ 进行设置。

  1. 下边输入http://localhost:9000进行登录,账号和密码为:minioadmin/minioadmin

https://i-blog.csdnimg.cn/img_convert/edf8a12593c4adcf27d187e107557ee3.png

  1. 下一步创建bucket,桶,它相当于存储文件的目录,可以创建若干的桶。

https://i-blog.csdnimg.cn/img_convert/ee844a7123995800e3777f0caa39f1a1.png

  1. 点击“upload”上传文件。

https://i-blog.csdnimg.cn/img_convert/e94d9155a184a549b1035fd09d26f1e4.png

  1. 下边去四个目录观察文件的存储情况, 我们发现上传的文件存储在了四个目录,即四个硬盘上。

https://i-blog.csdnimg.cn/img_convert/3e11848906d6323686e9c9afee6a2d61.png

  1. 下边测试minio的数据恢复过程:
  1. 首先删除一个目录。
  • 删除目录后仍然可以在web控制台上传文件和下载文件。
  • 稍等片刻删除的目录自动恢复。
  1. 删除两个目录。
  • 删除两个目录也会自动恢复。
  1. 删除三个目录。
  • 由于集合中共有4块硬盘,有大于一半的硬盘损坏数据无法恢复。
  • 此时报错:We encountered an internal error, please try again. (Read failed. Insufficient number of drives online)在线驱动器数量不足。

测试Docker环境

开发阶段和生产阶段统一使用Docker下的MINIO。

  1. 在下发的虚拟机中已安装了MinIO的镜像和容器,执行sh /data/soft /restart.sh启动Docker下的MinIO
  2. 启动完成登录MinIO查看是否正常。访问http://192.168.101.65:9000

https://i-blog.csdnimg.cn/img_convert/b0b0cad44985f9feedb67f01b398147d.png

  1. 本项目创建两个buckets:
  • mediafiles:普通文件
  • video:视频文件

测试SDK

MinIO提供多个语言版本SDK的支持,下边找到java版本的文档:

  1. 地址:https://docs.min.io/docs/java-client-quickstart-guide.html
  2. 最低需求Java 1.8或更高版本:
  3. maven依赖如下:在media-service工程添加此依赖。
<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>
  1. 参数说明:需要三个参数才能连接到minio服务。

https://i-blog.csdnimg.cn/img_convert/132dd5ff51804cf677f7068c515498b9.png

  1. 首先创建一个用于测试的bucket, 并设置访问权限

https://i-blog.csdnimg.cn/img_convert/d09f33928fc64abaf1f15baa8638bdd1.png

https://i-blog.csdnimg.cn/img_convert/ad9b3023d6b286383a5bb27ec57b4956.png

https://i-blog.csdnimg.cn/img_convert/0b341b995cf3e90b124fbeab95e5bdbc.png

https://i-blog.csdnimg.cn/img_convert/e96452261d420de84ba244c677456f03.png

  1. 在xuecheng-plus-media-service工程的test下编写测试代码如下:
  2. 测试文件上传

https://i-blog.csdnimg.cn/img_convert/69082482c848e3c5e2b2e37da246c331.png

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

    //上传文件1
    @Test
    public void upload() {
        try {
            // 上传文件的参数
            UploadObjectArgs testbucket = UploadObjectArgs.builder()
                    .bucket("testbucket") // 桶名
                    .filename("D:\\UserDatas\\Note\\16Project\\xue-cheng-zai-xian\\minio\\test.png") // 文件地址
                    //.object("test001.png") // 上传至根目录下
                    .object("001/test001.png") //上传到子目录下
                    .contentType("image/png") //指定文件类型
                    .build();
            // 上传文件
            minioClient.uploadObject(testbucket);
            System.out.println("上传成功");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("上传失败");
        }

    }

}

https://i-blog.csdnimg.cn/img_convert/bda7990165b83d53396fe06d4bd84094.png

  1. 根据文件扩展名获得媒体类型
  • 引入j256依赖
 <!--根据扩展名取mimetype-->
        <dependency>
            <groupId>com.j256.simplemagic</groupId>
            <artifactId>simplemagic</artifactId>
            <version>1.17</version>
        </dependency>
  • 通过com.j256.simplemagic.ContentType枚举类查看常用的mimeType(媒体类型)

https://i-blog.csdnimg.cn/img_convert/349f39cfd3c5d5b9e4e2017632b8a71f.png

  • 使用j256提供的ContentInfoUtil工具类, 通过文件扩展名得到mimeType,代码如下:
/**
 * @description 测试MinIO
 */
public class MinioTest {
    static MinioClient minioClient =
            MinioClient.builder()
                    .endpoint("http://192.168.101.65:9000")
                    .credentials("minioadmin", "minioadmin")
                    .build();

    //上传文件2
    @Test
    public void upload2() {
        // 根据扩展名取出mimeType
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(".mp4");
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType,字节流
        if(extensionMatch!=null){
            mimeType = extensionMatch.getMimeType();
        }
        try {
            UploadObjectArgs testbucket = UploadObjectArgs.builder()
                    .bucket("testbucket") // 桶名
                    .object("001/test001.png") //保存到子目录下
                    .filename("D:\\UserDatas\\Note\\16Project\\xue-cheng-zai-xian\\minio\\test.png") // 文件地址
                    .contentType(mimeType)//默认根据扩展名确定文件内容类型,也可以指定
                    .build();
            minioClient.uploadObject(testbucket);
            System.out.println("上传成功");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("上传失败");
        }


    }

}
  1. 测试删除文件
/**
 * @description 测试MinIO
 */
public class MinioTest {
    static MinioClient minioClient =
            MinioClient.builder()
                    .endpoint("http://192.168.101.65:9000")
                    .credentials("minioadmin", "minioadmin")
                    .build();

    //删除文件
    @Test
    public void delete(){
        try {
            minioClient.removeObject(
                    RemoveObjectArgs.builder().bucket("testbucket").object("001/test001.png").build());
            System.out.println("删除成功");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("删除失败");
        }
    }


}
  1. 测试查询文件 (下载文件)
/**
 * @description 测试MinIO
 */
public class MinioTest {
    static MinioClient minioClient =
            MinioClient.builder()
                    .endpoint("http://192.168.101.65:9000")
                    .credentials("minioadmin", "minioadmin")
                    .build();

    //查询文件 (从minio中下载)
    @Test
    public void getFile() {
        // 构建文件参数
        GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket("testbucket").object("001/test001.png").build();
        try(
                // 查询minio服务获取一个流对象
                FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
                // 指定输出流
                FileOutputStream outputStream = new FileOutputStream(new File("D:\\UserDatas\\Note\\16Project\\xue-cheng-zai-xian\\minio\\test_copy.png"));
        ) {
            // 把输入流拷贝到输出流, 完成文件的下载
            // org.apache.commons.compress.utils.IOUtils;
            IOUtils.copy(inputStream,outputStream);

            // 校验文件的完整性:
            // 思路: 把原始文件和下载的文件对两个文件的MD5加密值进行比较
            // 1.获取原始文件的输入流
            FileInputStream fileInputStream1 = new FileInputStream(new File("D:\\UserDatas\\Note\\16Project\\xue-cheng-zai-xian\\minio\\test.png"));
            // 2.获取下载文件的输入流
            FileInputStream fileInputStream2 = new FileInputStream(new File("D:\\UserDatas\\Note\\16Project\\xue-cheng-zai-xian\\minio\\test_copy.png"));
            // 3.把两个文件流进行md5加密
            // org.apache.commons.codec.digest.DigestUtils;
            String source_md5 = DigestUtils.md5Hex(fileInputStream1);
            String local_md5 = DigestUtils.md5Hex(fileInputStream2);
            if(source_md5.equals(local_md5)) {
                System.out.println("文件下载成功");
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }


}

https://i-blog.csdnimg.cn/img_convert/75cf38472cafe18f328e8e255d0d1c0f.png

https://i-blog.csdnimg.cn/img_convert/b85ec7d9fd20133fc96ed405aaed2d56.png

上传图片

需求分析

课程图片是宣传课程非常重要的信息,在新增课程界面上传课程图片,也可以修改课程图片。

https://i-blog.csdnimg.cn/img_convert/df4acfb5a6227517ae50bb8f7678f22e.png

上传课程图片总体上包括两部分:

1、上传课程图片前端请求媒资管理服务将文件上传至分布式文件系统,并且在媒资管理数据库保存文件信息。

2、上传图片成功保存图片地址到课程基本信息表中。

https://i-blog.csdnimg.cn/img_convert/8411ed7f7e70d5472c284f66e4813dc6.png

1、前端进入上传图片界面

2、上传图片,请求媒资管理服务。

3、媒资管理服务将图片文件存储在MinIO。

4、媒资管理记录文件信息到数据库。

5、前端请求内容管理服务保存课程信息,在内容管理数据库保存图片地址。

涉及到的数据表有:

课程信息表中的图片字段、媒资数据库的文件表,下边主要看媒资数据库的文件表。

https://i-blog.csdnimg.cn/img_convert/7f98bdc79ef0c457e35853822f565d91.png

https://i-blog.csdnimg.cn/img_convert/1d02ee58e125425d08e42929a30fb12e.png

环境准备

video这个桶用于存放课程视频, 其他的资源放在mediafiles这个桶

https://i-blog.csdnimg.cn/img_convert/de058768cd8a57017396f0f5abc6c603.png

在nacos中配置minio的相关信息,进入media-service-dev.yaml:

https://i-blog.csdnimg.cn/img_convert/76bd76008c14f9df6f7a653aa326c493.png

minio:
  endpoint: http://192.168.101.65:9000
  accessKey: minioadmin
  secretKey: minioadmin
  bucket:
    files: mediafiles
    videofiles: video

在media-service工程编写minio的配置类:

https://i-blog.csdnimg.cn/img_convert/e89c7c0f634323a1c49b49176bb7e739.png

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;

/**
 * @description minio配置
 */
@Configuration
public class MinioConfig {
    @Value("${minio.endpoint}")
    private String endpoint;
    @Value("${minio.accessKey}")
    private String accessKey;
    @Value("${minio.secretKey}")
    private String secretKey;

    @Bean
    public MinioClient minioClient() {

        MinioClient minioClient =
                MinioClient.builder()
                        .endpoint(endpoint)
                        .credentials(accessKey, secretKey)
                        .build();
        return minioClient;
    }

}

代码解释

  • 这段代码定义了一个Spring配置类MinioConfig,用于创建并配置MinioClient对象。
  • 通过@Value注解从配置文件中读取MinIO的endpoint、accessKey和secretKey。
  • 使用MinioClient.builder()方法构建MinioClient实例,并设置endpoint和凭据。
  • 使用@Bean注解, 将构建好的MinioClient实例作为Spring的Bean返回。
  • 使用的时候, 直接注入该配置类, 就可以得到minioClient对象

接口设计

根据需求分析,下边进行接口定义,此接口定义为一个通用的上传文件接口,可以上传图片或其它文件。

/**
 * @author Mr.M
 * @version 1.0
 * @description 媒资文件管理接口
 * @date 2022/9/6 11:29
 */
@Api(value = "媒资文件管理接口", tags = "媒资文件管理接口")
@RestController
public class MediaFilesController {

    @Autowired
    MediaFileService mediaFileService;

    @ApiOperation("上传文件")
    @RequestMapping(value = "/upload/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile upload) throws IOException {

        return null;
    }

}
  • 请求地址:/media/upload/coursefile
  • 请求格式为 multipart/form-data
  • 接收一个MultipartFile类型的文件数据
  • 与前端约定的文件名称是filedata

响应参数:文件上传成功后, 返回文件信息给前端,如下

{
  "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
}

定义上传响应模型类:PO对应的是数据库中表的信息, 直接把数据封装到PO类中返回给前端也可以, 但是扩展性不强, 后续前端可能需要我们返回更多的数据, 所以我们定义一个DTO类

https://i-blog.csdnimg.cn/img_convert/77dc9148594d06bae4d2dfca73fb1158.png

package com.xuecheng.media.model.dto;

import com.xuecheng.media.model.po.MediaFiles;
import lombok.Data;

/**
 * 上传普通文件成功响应结果
 */
@Data
public class UploadFileResultDto extends MediaFiles {

}

service开发

实现目标: 向media_files表插入文件信息, 向minio系统上传文件

https://i-blog.csdnimg.cn/img_convert/cb03f52acf74ac359f0858e528a4e23e.png

定义文件信息传输对象, 用于封装文件信息

https://i-blog.csdnimg.cn/img_convert/5a83be5a2c170665919bdf9f59ba7de8.png

package com.xuecheng.media.model.dto;

import lombok.Data;

/**
 * 文件信息
 */
@Data
public class UploadFileParamsDto {
    /**
     * 文件名称
     */
    private String filename;


    /**
     * 文件类型(文档,音频,视频)
     */
    private String fileType;
    /**
     * 文件大小
     */
    private Long fileSize;

    /**
     * 标签
     */
    private String tags;

    /**
     * 上传人
     */
    private String username;

    /**
     * 备注
     */
    private String remark;

}

定义接口和实现类

https://i-blog.csdnimg.cn/img_convert/2b0bbe38c220fb8fc8ee1122c444d787.png

/**
 * @author Mr.M
 * @version 1.0
 * @description 媒资文件管理业务类
 * @date 2022/9/10 8:55
 */
public interface MediaFileService {

    /**
     * 上传文件
     *
     * @param companyId           机构id
     * @param uploadFileParamsDto 上传文件信息
     * @param localFilePath       文件磁盘路径
     * @return 文件信息
     */
    public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath);

}
package com.xuecheng.media.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.xuecheng.base.execption.XueChengPlusException;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.media.mapper.MediaFilesMapper;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.service.MediaFileService;
import io.minio.MinioClient;
import io.minio.UploadObjectArgs;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.File;
import java.io.FileInputStream;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;

/**
 * @author Mr.M
 * @version 1.0
 * @description TODO
 * @date 2022/9/10 8:58
 */
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {

    @Autowired
    MediaFilesMapper mediaFilesMapper;

    @Autowired
    MinioClient minioClient;

    //普通文件桶
    @Value("${minio.bucket.files}")
    private String bucket_mediafiles;

    //视频文件桶
    @Value("${minio.bucket.videofiles}")
    private String bucket_video;


    /**
     * 文件上传
     *
     * @param companyId           机构id
     * @param uploadFileParamsDto 上传文件信息
     * @param localFilePath       文件磁盘路径
     * @return
     */
    @Transactional
    @Override
    public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath) {
        // 1.上传文件
        // 1.1获取mimeType
        String filename = uploadFileParamsDto.getFilename();
        String extension = filename.substring(filename.lastIndexOf("."));
        String mimeType = getMimeType(extension);
        // 1.2获取对象名称(根据日期生成子目录, 使用MD5值作为文件名)
        String defaultFolderPath = getDefaultFolderPath();
        String fileMd5 = getFileMd5(new File(localFilePath));
        String objectName = defaultFolderPath + fileMd5 + extension;
        // 1.3将文件上传到minio中
        boolean result = addMediaFilesToMinIO(localFilePath, mimeType, bucket_mediafiles, objectName);
        if(!result) {
            XueChengPlusException.cast("上传文件失败");
        }

        // 2.保存文件信息到数据库
        MediaFiles mediaFiles = addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_mediafiles, objectName);
        if(mediaFiles == null) {
            XueChengPlusException.cast("文件上传后保存信息失败");
        }

        // 3.准备返回的对象
        UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
        BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);

        return uploadFileResultDto;
    }

    /**
     * @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);
            //补充其他信息
            mediaFiles.setId(fileMd5);
            mediaFiles.setFileId(fileMd5);
            mediaFiles.setCompanyId(companyId);
            mediaFiles.setUrl("/" + bucket + "/" + objectName);
            mediaFiles.setBucket(bucket);
            mediaFiles.setFilePath(objectName);
            mediaFiles.setCreateDate(LocalDateTime.now());
            mediaFiles.setAuditStatus("002003");
            mediaFiles.setStatus("1");
            //保存文件信息到文件表
            int insert = mediaFilesMapper.insert(mediaFiles);
            if (insert < 0) {
                log.error("保存文件信息到数据库失败,{}",mediaFiles.toString());
                XueChengPlusException.cast("保存文件信息失败");
            }
            log.debug("保存文件信息到数据库成功,{}",mediaFiles.toString());

        }
        return mediaFiles;

    }


    /**
     * @param localFilePath 文件地址
     * @param bucket        桶
     * @param objectName    对象名称
     * @return void
     * @description 将文件写入minIO
     * @author Mr.M
     * @date 2022/10/12 21:22
     */
    public boolean addMediaFilesToMinIO(String localFilePath, String mimeType, String bucket, String objectName) {
        try {
            // 构建文件参数
            UploadObjectArgs testbucket = UploadObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .filename(localFilePath)
                    .contentType(mimeType)
                    .build();
            // 执行上传操作
            minioClient.uploadObject(testbucket);
            log.debug("上传文件到minio成功,bucket:{},objectName:{}", bucket, objectName);
            System.out.println("上传成功");
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("上传文件到minio出错,bucket:{},objectName:{},错误原因:{}", bucket, objectName, e.getMessage(), e);
            XueChengPlusException.cast("上传文件到文件系统失败");
        }
        return false;
    }


    //获取文件默认存储目录路径 年/月/日
    private String getDefaultFolderPath() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String folder = sdf.format(new Date()).replace("-", "/") + "/";
        return folder;
    }

    //获取文件的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;
        }
    }

    // 根据文件扩展名取出mimeType
    private String getMimeType(String extension) {
        if (extension == null)
            extension = "";
        //根据扩展名取出mimeType
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
        //通用mimeType,字节流
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        if (extensionMatch != null) {
            mimeType = extensionMatch.getMimeType();
        }
        return mimeType;
    }

}

完善接口

在接口层调用逻辑层, 完成文件的上传

/**
 * @author Mr.M
 * @version 1.0
 * @description 媒资文件管理接口
 * @date 2022/9/6 11:29
 */
@Api(value = "媒资文件管理接口", tags = "媒资文件管理接口")
@RestController
public class MediaFilesController {


    @Autowired
    MediaFileService mediaFileService;

    @ApiOperation("上传文件")
    @RequestMapping(value = "/upload/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile filedata) throws IOException {
        Long companyId = 1232141425L;
        UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
        //文件大小
        uploadFileParamsDto.setFileSize(filedata.getSize());
        //图片
        uploadFileParamsDto.setFileType("001001");
        //文件名称
        uploadFileParamsDto.setFilename(filedata.getOriginalFilename());//文件名称
        //文件大小
        long fileSize = filedata.getSize();
        uploadFileParamsDto.setFileSize(fileSize);
        //创建临时文件
        File tempFile = File.createTempFile("minio", "temp");
        //上传的文件拷贝到临时文件
        filedata.transferTo(tempFile);
        //文件路径
        String absolutePath = tempFile.getAbsolutePath();
        //上传文件
        UploadFileResultDto uploadFileResultDto = mediaFileService.uploadFile(companyId, uploadFileParamsDto, absolutePath);

        return uploadFileResultDto;
    }

}

接口测试

启动媒资服务, 首先使用httpclient测试

https://i-blog.csdnimg.cn/img_convert/e66d6dd350aea15ef991e2a74f3bd19c.png

### 上传文件
POST {{media_host}}/media/upload/coursefile
Content-Type: multipart/form-data; boundary=WebAppBoundary

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

< C:\Users\Lenovo\Desktop\1.png

https://i-blog.csdnimg.cn/img_convert/58296b42d857cbf10817b943902e3ff8.png

再进行前后端联调测试

  1. 在新增课程、编辑课程界面上传图,保存课程信息后再次进入编辑课程界面,查看是否可以正常保存课程图片信息。

https://i-blog.csdnimg.cn/img_convert/8a9b8a0521bce107af634d9b0b99160a.png

  1. 上图图片完成后,进入minIO管理系统,查看文件列表中是否有刚刚上传的图片信息。

https://i-blog.csdnimg.cn/img_convert/509d4e3c5521a7a680ff31fdd5f54c33.png

事务优化

由于uploadFile方法中, 要把文件上传到minio中, 还要把文件信息保存到数据库中, 为了保持数据一致性, 要进行事务控制

目前的事务处理是在uploadFile方法上添加@Transactional注解,uploadFile方法调用前就会开启数据库事务, 方法调用结束后会提交事务, 方法调用报错后会回滚事务

   /**
     * 文件上传
     */
    @Transactional
    @Override
    public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath) {
        // ... ...
        // 1.将文件上传到minio中
        boolean result = addMediaFilesToMinIO(localFilePath, mimeType, bucket_mediafiles, objectName);

        // 2.保存文件信息到数据库
        MediaFiles mediaFiles = addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_mediafiles, objectName);
        ... ...
        return uploadFileResultDto;
    }

虽然实现了需求, 但是并不合理, 因为上传文件是网络操作, 执行时间可能会很长,那么数据库的事务持续时间就会变长, 导致数据库链接释放很慢,最终导致数据库链接不够用。

解决方案

我们只将addMediaFilesToDb方法添加事务控制即可,uploadFile方法上的@Transactional注解去掉。

   /**
     * @description 将文件信息添加到文件表
     */
    @Transactional
    public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName){
        //从数据库查询文件
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        //文件不存在才需要添加到数据库
        if (mediaFiles == null) {
             ... ...
            //保存文件信息到文件表
            int insert = mediaFilesMapper.insert(mediaFiles);
        }
        return mediaFiles;

    }

代码测试

我们人为在 int insert = mediaFilesMapper.insert(mediaFiles); 下边添加一个异常代码int a=1/0;

  1. 测试是否事务控制。很遗憾,事务控制失败。
  2. 方法上已经添加了@Transactional注解为什么该方法不能被事务控制呢?
  3. 下边分析原因:
  4. 如果在uploadFile方法上添加@Transactional注解,代理对象执行此方法前会开启事务,如下图:

https://i-blog.csdnimg.cn/img_convert/105bc48f6570a14e63c04ff95ca3d992.png

  1. 如果在uploadFile方法上没有@Transactional注解,代理对象执行此方法前不进行事务控制

https://i-blog.csdnimg.cn/img_convert/590f0b3c80adc7043dfa85d7641e0429.png

  1. 所以判断该方法是否可以事务控制, 必须保证是通过代理对象调用此方法,且此方法上添加了@Transactional注解。
  2. 现在在addMediaFilesToDb方法上添加@Transactional注解,也不会进行事务控制是因为并不是通过代理对象执行的addMediaFilesToDb方法。
  3. 为了判断在uploadFile方法中去调用addMediaFilesToDb方法是否是通过代理对象去调用,我们可以打断点跟踪。

https://i-blog.csdnimg.cn/img_convert/0465b914d1b6a3191e07dcbbb9a8ab0c.png

  1. 我们发现在uploadFile方法中去调用addMediaFilesToDb方法不是通过代理对象去调用。

如何解决呢?

通过代理对象去调用addMediaFilesToDb方法即可解决。

  1. 在MediaFileService的实现类中注入MediaFileService的代理对象
/**
 * @author Mr.M
 * @version 1.0
 * @description TODO
 * @date 2022/9/10 8:58
 */
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {
    
    @Autowired
    MediaFileService currentProxy;
    
}
  1. 将addMediaFilesToDb方法提成接口
/**
 * @author Mr.M
 * @version 1.0
 * @description 媒资文件管理业务类
 * @date 2022/9/10 8:55
 */
public interface MediaFileService {

    /**
     * @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
     */
    public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName);

}
  1. 使用代理对象调用addMediaFilesToDb方法

https://i-blog.csdnimg.cn/img_convert/0732d12fb383bc8b4314c4b21f759c48.png

  1. 再次测试, 事务可以正常控制。