1. 模块需求分析

1.1 模块介绍

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

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

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

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

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

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

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

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

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

image-20230531170817356

1.2 业务流程

1.2.1 上传图片

教学机构人员在课程信息编辑页面上传课程图片,课程图片统一记录在媒资管理系统。

下图是上传图片的界面:

img

1.2.2 上传视频

  1. 教学机构人员进入媒资管理列表查询自己上传的媒资文件。

点击“媒资管理”

img

进入媒资管理列表页面查询本机构上传的媒资文件。

img

  1. 教育机构用户在”媒资管理”页面中点击 “上传视频” 按钮。

img

点击“上传视频”打开上传页面

img

  1. 选择要上传的文件,自动执行文件上传,视频上传成功会自动处理。

img

1.2.3 处理视频

对需要转码处理的视频系统会自动对其处理,处理后生成视频的URL。

处理视频没有用户界面,完全是后台自动执行。

1.2.4 审核视频

审核媒资包括程序自动审核和人工审核,程序可以通过鉴黄接口(https://www.aliyun.com/product/lvwang?spm=5176.19720258.J_3207526240.51.e93976f4rSq796)审核视频,对有异议的视频由人工进行审核。

  1. 运营用户登入运营平台并进入媒资管理页面,查找待审核媒资

img

  1. 点击列表中媒资名称链接,可预览该媒资,若是视频,则播放视频。

img

  1. 点击列表中某媒资后的”审核” 按钮,既完成媒资的审批过程。

img

点击“审核”,选择审核结果,输入审核意见。

img

1.2.5 绑定媒资

课程计划创建好后需要绑定媒资文件,比如:如果课程计划绑定了视频文件,进入课程在线学习界面后点课程计划名称则在线播放视频。如下图:

img

如何将课程计划绑定媒资呢?

  1. 教育机构用户进入课程管理页面并编辑某一个课程,在”课程大纲”标签页的某一小节后可点击”添加视频“。

img

  1. 弹出添加视频对话框,可通过视频关键字搜索已审核通过的视频媒资。

img

  1. 选择视频媒资,点击提交按钮,完成课程计划绑定媒资流程。

img

课程计划关联视频后如下图:

img

2. 搭建模块环境

2.1 架构的问题分析

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

img

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

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

img

当系统上线后这里需要改成公网的域名,如果这种地址非常多则非常麻烦。

基于这个问题可以采用网关来解决,如下图:

img

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

img

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

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

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

img

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

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

img

流程如下:

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

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

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

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

  1. 服务发现中心。

    • 微服务将自身注册至Nacos,网关从Nacos获取微服务列表。
  2. 配置中心。

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

2.2 搭建 Nacos

2.2.1 服务发现中心

Spring Cloud :一套规范

  • Spring Cloud alibaba: nacos服务注册中心,配置中心

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

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

    • 在搭建Nacos服务发现中心之前需要搞清楚两个概念:namespace和group

    • namespace:用于区分环境、比如:开发环境、测试环境、生产环境。

    • group:用于区分项目,比如:xuecheng-plus项目、xuecheng2.0项目

  2. 首先在nacos配置namespace:

  3. 登录成功,点击左侧菜单“命名空间”进入命名空间管理界面,

首先完成各服务注册到Naocs,下边将内容管理服务注册到nacos中。

1) 在xuecheng-plus-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>
  1. 在内容管理模块的接口工程、系统管理模块的接口工程中添加如下依赖
1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  1. 配置nacos的地址
1
2
3
4
5
6
7
8
9
10
#微服务配置
spring:
application:
name: system-service
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery:
namespace: dev
group: xuecheng-plus-project

在内容管理的接口工程的配置文件中配置如下信息:

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
  1. 重启内容管理服务、系统管理服务。

待微服务启动成功,进入Nacos服务查看服务列表

image-20230602195622335

在 “开发环境” 命名空间下有两个服务这说明内容管理微服务和系统管理微服务在Nacos注册成功。

点击其它一个微服务的“详情”

2.2.2 配置中心

2.2.2.1 配置三要素

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

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

  1. 每个项目特有的配置

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

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

  2. 项目所公用的配置

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

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

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

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

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

      • content-service:第一部分,它是在application.yaml中配置的应用名,即spring.application.name的值。

      • dev:第二部分,它是环境名,通过spring.profiles.active指定,

      • Yaml: 第三部分,它是配置文件 的后缀,目前nacos支持properties、yaml等格式类型,本项目选择yaml格式类型。

2️⃣所以,如果我们要配置content-service工程的配置文件:

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

我们启动项目中传入spring.profiles.active的参数决定引用哪个环境的配置文件,例如:传入spring.profiles.active=dev表示使用dev环境的配置文件即content-service-dev.yaml。

2.2.2.2 配置 content-service

1️⃣下边以开发环境为例对content-service工程的配置文件进行配置,进入nacos,进入开发环境。

  • 点击加号,添加一个配置
  • 输入data id、group以及配置文件内容。

image-20230602200138421

2️⃣为什么没在nacos中配置下边的内容 ?

1
2
3
spring:
application:
name: content-service
  • 因为刚才说了dataid第一部分就是spring.application.name,nacos 客户端要根据此值确定配置文件 名称,
  • 所以spring.application.name不在nacos中配置,而是要在工程的本地进行配置。

3️⃣在content-service工程的test/resources 中添加bootstrap.yaml,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
application:
name: content-service
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery:
namespace: ty_xc_dev
group: xuecheng-plus-project
config:
namespace: ty_xc_dev
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true

#profiles默认为dev
profiles:
active: dev

4️⃣在内容管理模块的接口工程和service工程配置依赖:

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

配置完成,运行content-service工程 的单元测试文件,能否正常测试,跟踪单元测试方法可以正常读取数据库的数据,说明从nacos读取配置信息正常。

通过运行观察控制台打印出下边的信息,NacosRestTemplate.java通过Post方式与nacos服务端交互读取配置信息。

2.2.2.3 配置 content-api

以相同的方法配置content-api工程的配置文件,在nacos中的开发环境中配置content-api-dev.yaml,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
servlet:
context-path: /content
port: 63040

# 日志文件配置路径
logging:
config: classpath:log4j2-dev.xml

# swagger 文档配置
swagger:
title: "学成在线内容管理系统"
description: "内容系统管理系统对课程相关信息进行业务管理数据"
base-package: com.xuecheng.content
enabled: true
version: 1.0.0

在content-api工程 的本地配置bootstrap.yaml,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#微服务配置
spring:
application:
name: content-api
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery:
namespace: ty_xc_dev
group: xuecheng-plus-project
config:
namespace: ty_xc_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
profiles:
active: dev

注意:因为api接口工程依赖了service工程 的jar,所以这里使用extension-configs扩展配置文件 的方式引用service工程所用到的配置文件。

1
2
3
4
extension-configs:
- data-id: content-service-${spring.profiles.active}.yaml
group: xuecheng-plus-project
refresh: true

如果添加多个扩展文件,继续在下添加即可,如下:

1
2
3
4
5
6
7
extension-configs:
- data-id: content-service-${spring.profiles.active}.yaml
group: xuecheng-plus-project
refresh: true
- data-id: 填写文件 dataid
group: xuecheng-plus-project
refresh: true

启动content-api工程,查询控制台是否打印出了请求nacos的日志,如下:

并使用Httpclient测试课程查询接口是否可以正常查询。

2.2.3 公用配置

1️⃣还有一个优化的点是如何在nacos中配置项目的公用配置呢?

  • nacos提供了shared-configs可以引入公用配置。

  • 在content-api中配置了swagger,所有的接口工程 都需要配置swagger,

  • 这里就可以将swagger的配置定义为一个公用配置,哪个项目用引入即可。

2️⃣单独在xuecheng-plus-common分组下创建xuecheng-plus的公用配置,

  • 进入nacos的开发环境,添加swagger-dev.yaml公用配置

  • 再以相同 的方法配置日志的公用配置。

image-20230602200917855

3️⃣删除接口工程中对swagger的配置。

  • 项目使用shared-configs可以引入公用配置。在接口工程的本地配置文件 中引入公用配置,如下:
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
spring:
application:
name: content-api
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery:
namespace: ty_xc_dev
group: xuecheng-plus-project
config:
namespace: ty_xc_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
profiles:
active: dev

4️⃣配置完成,重启content-api接口工程,访问http://localhost:63040/content/swagger-ui.html 查看swagger接口文档是否可以正常访问,查看控制台log4j2日志输出是否正常。

2.2.4 配置优先级

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

img

引入配置文件的形式有:

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

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

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

  4. 本地配置文件

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

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

  • 我们想启动两个内容管理微服务,此时需要在本地指定不同的端口,通过VM Options参数,在IDEA配置启动参数

img

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

img

  • 启动ContentApplication2,发现端口仍然是63040,这说明本地的配置没有生效。

3️⃣这时我们想让本地最优先,可以在nacos配置文件 中配置如下即可实现:

1
2
3
4
5
#配置本地优先
spring:
cloud:
config:
override-none: true

4️⃣再次启动ContentApplication2,端口为63041。

2.2.5 导入配置文件

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

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

2.3 搭建 Gateway

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

1️⃣新建一个网关工程。

image-20230604202626045

2️⃣修改pom.xml,添加相关依赖

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
<?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>
<version>0.0.1-SNAPSHOT</version>
<name>xuecheng-plus-gateway</name>
<description>xuecheng-plus-gateway</description>

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

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

3️⃣配置网关的bootstrap.yaml配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#微服务配置
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery:
namespace: ty_xc_dev
group: xuecheng-plus-project
config:
namespace: ty_xc_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

4️⃣在nacos上配置网关路由策略:

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

5️⃣启动网关工程,通过网关工程访问微服务进行测试。

  • 在http-client-env.json中配置网关的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"dev": {
"access_token": "",
"gateway_host": "localhost:63010",
"content_host": "localhost:63040",
"system_host": "localhost:63110",
"media_host": "localhost:63050",
"search_host": "localhost:63080",
"auth_host": "localhost:63070",
"checkcode_host": "localhost:63075",
"learning_host": "localhost:63020"
}
}

6️⃣使用httpclient测试课程查询 接口,如下:

1
2
3
4
5
6
7
8
9
### 查询课程信息
POST {{gateway_host}}/content/course/list?pageNo=1&pageSize=2
Content-Type: application/json

{
"auditStatus": "202004",
"courseName": "java",
"publishStatus": ""
}

运行,观察是否可以正常访问接口 ,如下所示可以正常请求接口。

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

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

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

2.4 搭建媒资工程

至此网关、Nacos已经搭建完成,下边将媒资工程导入项目。

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

右键pom.xml转为maven工程。

image-20230604204151711

下边做如下配置:

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

  2. 修改nacos上的media-service-dev.yaml配置文件中的数据库链接信息

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

3. 分布式文件系统

3.1 什么是分布式文件系统

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

查阅百度百科:

img

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

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

img

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

2️⃣现在有个问题,一此短视频平台拥有大量的视频、图片,这些视频文件、图片文件该如何存储呢?

  • 如何存储可以满足互联网上海量用户的浏览。

  • 今天讲的分布式文件系统就是海量用户查阅海量文件的方案。

我们阅读百度百科去理解分布式文件系统的定义:

img

通过概念可以简单理解为:一个计算机无法存储海量的文件,通过网络将若干计算机组织起来共同去存储海量的文件,

  • 去接收海量用户的请求,这些组织起来的计算机通过网络进行通信,如下图:

img

好处:

  1. 一台计算机的文件系统处理能力扩充到多台计算机同时处理。

  2. 一台计算机挂了还有另外副本计算机提供数据。

  3. 每台计算机可以放在不同的地域,这样用户就可以就近访问,提高访问速度。

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

  1. NFS

阅读百度百科:

img

img

特点:

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

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

  1. GFS

img

img

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

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

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

  1. HDFS

HDFS,是Hadoop Distributed File System的简称,是Hadoop抽象文件系统的一种实现。

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

下图是HDFS的架构图:

img

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

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

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

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

3.2.1 介绍

本项目采用MinIO构建分布式文件系统,

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

它一大特点就是轻量,使用简单,功能强大,支持各种平台,单个文件最大5TB,兼容 Amazon S3接口,提供了 Java、Python、GO等多版本SDK支持。

官网:https://min.io

中文:https://www.minio.org.cn/,http://docs.minio.org.cn/docs/

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

去中心化有什么好处?

在大数据领域,通常的设计理念都是无中心和分布式。

  • Minio分布式模式可以帮助你搭建一个高可用的对象存储服务,你可以使用这些存储设备,而不用考虑其真实物理位置。

它将分布在不同服务器上的多块硬盘组成一个对象存储服务。由于硬盘分布在不同的节点上,分布式Minio避免了单点故障。如下图:

img

Minio使用纠删码技术来保护数据,它是一种恢复丢失和损坏数据的数学算法,

  • 它将数据分块冗余的分散存储在各各节点的磁盘上,所有的可用磁盘组成一个集合,
  • 上图由8块硬盘组成一个集合,当上传一个文件时会通过纠删码算法计算对文件进行分块存储,除了将文件本身分成4个数据块,
  • 还会生成4个校验块,数据块和校验块会分散的存储在这8块硬盘上。

使用纠删码的好处是即便丢失一半数量(N/2)的硬盘,仍然可以恢复数据。

  • 比如上边集合中有4个以内的硬盘损害仍可保证数据恢复,不影响上传和下载,
  • 如果多于一半的硬盘坏了则无法恢复。

3.2.2 测试docker 环境

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

在下发的虚拟机中已安装了MinIO的镜像和容器,执行sh /data/soft /restart.sh启动Docker下的MinIO

启动完成登录MinIO查看是否正常。

访问http://192.168.101.65:9000

image-20230605195548163

本项目创建两个buckets:

  • mediafiles: 普通文件

  • video:视频文件

3.2.4 SDK

上传文件

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

地址:https://docs.min.io/docs/java-client-quickstart-guide.html

最低需求Java 1.8或更高版本:

maven依赖如下:

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>

在media-service工程添加此依赖。

参数说明:

需要三个参数才能连接到minio服务。

参数 说明
Endpoint 对象存储服务的URL
Access Key Access key就像用户ID,可以唯一标识你的账户。
Secret Key Secret key是你账户的密码。

官方的示例代码如下:

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
import io.minio.BucketExistsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.UploadObjectArgs;
import io.minio.errors.MinioException;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class FileUploader {
public static void main(String[] args)throws IOException, NoSuchAlgorithmException, InvalidKeyException {
try {
// Create a minioClient with the MinIO server playground, its access key and secret key.
MinioClient minioClient =
MinioClient.builder()
.endpoint("https://play.min.io")
.credentials("Q3AM3UQ867SPQQA43P2F", "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG")
.build();
// Make 'asiatrip' bucket if not exist.
boolean found =
minioClient.bucketExists(BucketExistsArgs.builder().bucket("asiatrip").build());
if (!found) {
// Make a new bucket called 'asiatrip'.
minioClient.makeBucket(MakeBucketArgs.builder().bucket("asiatrip").build());
} else {
System.out.println("Bucket 'asiatrip' already exists.");
}
// Upload '/home/user/Photos/asiaphotos.zip' as object name 'asiaphotos-2015.zip' to bucket
// 'asiatrip'.
minioClient.uploadObject(
UploadObjectArgs.builder()
.bucket("asiatrip")
.object("asiaphotos-2015.zip")
.filename("/home/user/Photos/asiaphotos.zip")
.build());
System.out.println(
"'/home/user/Photos/asiaphotos.zip' is successfully uploaded as "
+ "object 'asiaphotos-2015.zip' to bucket 'asiatrip'.");
} catch (MinioException e) {
System.out.println("Error occurred: " + e);
System.out.println("HTTP trace: " + e.httpTrace());
}
}
}

参考示例在media-service工程中 测试上传文件功能,

首先创建一个用于测试的bucket

  • 注意要设置权限为 public

在xuecheng-plus-media-service工程 的test下编写测试代码如下:

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
package com.xuecheng.media;

import io.minio.BucketExistsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.UploadObjectArgs;
import io.minio.errors.MinioException;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

/**
* @description 测试MinIO
* @author Mr.M
* @date 2022/9/11 21:24
* @version 1.0
*/
public class MinioTest {

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

//上传文件
@Test
public void upload() {
try {
UploadObjectArgs testbucket = UploadObjectArgs.builder()
.bucket("testbucket")
// .object("test001.mp4")
.object("001/test001.mp4")//添加子目录
.filename("D:\\develop\\upload\\1mp4.temp")
.contentType("video/mp4")//默认根据扩展名确定文件内容类型,也可以指定
.build();
minioClient.uploadObject(testbucket);
System.out.println("上传成功");
} catch (Exception e) {
e.printStackTrace();
System.out.println("上传失败");
}

}

}

执行upload方法,分别测试向桶的根目录上传文件以及子目录上传文件。

  • 上传成功,通过web控制台查看文件,并预览文件。

说明:

  • 设置contentType可以通过com.j256.simplemagic.ContentType枚举类查看常用的mimeType(媒体类型)

  • 通过扩展名得到mimeType,代码如下:

1
2
3
//根据扩展名取出mimeType
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(".mp4");
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用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
@Test
public void upload() {
//根据扩展名取出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("test001.mp4")
.object("001/test001.mp4")//添加子目录
.filename("D:\\develop\\upload\\1mp4.temp")
.contentType(mimeType)//默认根据扩展名确定文件内容类型,也可以指定
.build();
minioClient.uploadObject(testbucket);
System.out.println("上传成功");
} catch (Exception e) {
e.printStackTrace();
System.out.println("上传失败");
}

}

删除文件

下边测试删除文件

参考:https://docs.min.io/docs/java-client-api-reference#removeObject

1
2
3
4
5
6
7
8
9
10
11
@Test
public void delete(){
try {
minioClient.removeObject(
RemoveObjectArgs.builder().bucket("testbucket").object("001/test001.mp4").build());
System.out.println("删除成功");
} catch (Exception e) {
e.printStackTrace();
System.out.println("删除失败");
}
}

查询文件

通过查询文件查看文件是否存在minio中。

参考:https://docs.min.io/docs/java-client-api-reference#getObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Java
//查询文件
@Test
public void getFile() {
GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket("testbucket").object("test001.mp4").build();
try(
FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
FileOutputStream outputStream = new FileOutputStream(new File("D:\\develop\\upload\\1_2.mp4"));
) {
IOUtils.copy(inputStream,outputStream);
} catch (Exception e) {
e.printStackTrace();
}
}

校验文件的完整性,对文件计算出md5值,比较原始文件的md5和目标文件的md5,一致则说明完整

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

4. 上传图片

4.1 需求分析

4.1.1 业务流程

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

如下图:

img

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

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

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

详细流程如下:

img

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

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

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

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

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

4.1.2 数据模型

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

image-20230607195217426

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
create table media_files
(
id varchar(32) not null comment '文件id,md5值'
primary key,
company_id bigint null comment '机构ID',
company_name varchar(255) null comment '机构名称',
filename varchar(255) not null comment '文件名称',
file_type varchar(12) null comment '文件类型(图片、文档,视频)',
tags varchar(120) null comment '标签',
bucket varchar(128) null comment '存储目录',
file_path varchar(512) null comment '存储路径',
file_id varchar(32) not null comment '文件id',
url varchar(1024) null comment '媒资文件访问地址',
username varchar(60) null comment '上传人',
create_date datetime null comment '上传时间',
change_date datetime null comment '修改时间',
status varchar(12) default '1' null comment '状态,1:正常,0:不展示',
remark varchar(32) null comment '备注',
audit_status varchar(12) null comment '审核状态',
audit_mind varchar(255) null comment '审核意见',
file_size bigint null comment '文件大小',
constraint unique_fileid
unique (file_id) comment '文件id唯一索引 '
)
comment '媒资信息' charset = utf8
row_format = DYNAMIC;

4.2 准备环境

1️⃣首先在minio配置bucket,bucket名称为:mediafiles,并设置bucket的权限为公开。

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

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
27
28
29
30
31
32
33
34
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;

/**
* @author TaoYaopc
* @version v1.0
* @description TODO
* @date 2023/6/7 19:59
*/
@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;
}
}

4.3 接口定义

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

1️⃣首先分析接口:

  • 请求地址:/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
}

2️⃣定义上传响应模型类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.xuecheng.media.model.dto;

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

/**
* @author TaoYaopc
* @version v1.0
* @description TODO
* @date 2023/6/7 20:06
*/
@Data
public class UploadFileResultDto extends MediaFiles {

}

3️⃣定义接口如下:

1
2
3
4
5
6
@ApiOperation("上传图片")
@RequestMapping(value = "/upload/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public UploadFileResultDto upload(@RequestPart("filedata")MultipartFile filedata) throws IOException {

return null;
}

4️⃣接口定义好后可以用httpclient工具测试一下

  • 使用httpclient测试
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:/develop/upload/1.jpg

4.4 接口开发

4.4.1 DAO 开发

根据需求分析DAO层实现向media_files表插入一条记录,使用media_files表生成的mapper即可。

4.4.2 Service 开发

Service方法需要提供一个更加通用的保存文件的方法。

1️⃣定义请求参数类:

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
package com.xuecheng.media.model.dto;

import lombok.Data;

/**
* @author TaoYaopc
* @version v1.0
* @description 文件信息
* @date 2023/6/7 20:09
*/
@Data
public class UploadFileParamsDto {

/**
* 文件名称
*/
private String filename;

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

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

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

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

2️⃣定义service方法:

1
2
3
4
5
6
7
8
/**
*
* @param companyId 机构id
* @param uploadFileParamsDto 文件信息
* @param localFilePath 文件本地路径
* @return UploadFileResultDto
*/
UploadFileResultDto upload(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath);

3️⃣实现方法如下:

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
package com.xuecheng.media.service.impl;

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

@Resource
private MediaFilesMapper mediaFilesMapper;

@Resource
private MinioClient minioClient;

@Resource
private MediaFileService currentProxy;


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

// 存储视频
@Value("${minio.bucket.videofiles}")
private String bucket_video;

@Override
public PageResult<MediaFiles> queryMediaFiels(Long companyId, PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {

// 构建查询条件对象
LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();

// 分页对象
Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
// 查询数据内容获得结果
Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
// 获取数据列表
List<MediaFiles> list = pageResult.getRecords();
// 获取数据总数
long total = pageResult.getTotal();
// 构建结果集
PageResult<MediaFiles> mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
return mediaListResult;

}

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

// 文件名
String filename = uploadFileParamsDto.getFilename();
// 拓展名
String extension = filename.substring(filename.lastIndexOf("."));

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("上传文件失败");
}

// TODO: 校验文件上传是否完整

// 将文件信息保存到数据库
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;
}

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

// 获取当前时间并转换为 2023/03/03/
private String getDefaultFolderPath() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String folder = sdf.format(new Date()).replace("-", "/") + "/";
return folder;
}

/**
* 讲文件上传到 miniIO
* @param localFilePath 文件本地路径
* @param mimeType 媒体类型
* @param bucket 桶
* @param objectName 对象名
* @return boolean
*/
private 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;
}

private String getMimeType(String extension) {
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
if(extensionMatch != null) {
mimeType = extensionMatch.getMimeType();
}
return mimeType;
}
}

4.4.3 完善接口层

完善接口层代码

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) throws IOException {
// 机构id
Long companyId = 1232141425L;

// 准备上传文件的信息
UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
// 原始文件名称
uploadFileParamsDto.setFilename(filedata.getOriginalFilename());
// 文件大小
uploadFileParamsDto.setFileSize(filedata.getSize());
// 文件类型
uploadFileParamsDto.setFileType("001001");

// 接收到文件了
File tempFile = File.createTempFile("minio", ".temp");
filedata.transferTo(tempFile);

// 文件路径
String localFilePath = tempFile.getAbsolutePath();

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

return uploadFileResultDto;
}

4.4.4 接口测试

1️⃣首先使用httpclient测试

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:/develop/upload/1.jpg

2️⃣再进行前后端联调测试

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

image-20230607220122805

  • 上图图片完成后,进入媒资管理,查看文件列表中是否有刚刚上传的图片信息。

4.4.5 Service 事务优化

上边的service方法优化后并测试通过,现在思考==关于uploadFile方法的是否应该开启事务==。

1️⃣目前是在uploadFile方法上添加@Transactional,当调用uploadFile方法前会开启数据库事务,

  • 如果上传文件过程时间较长那么数据库的事务持续时间就会变长,
  • 这样数据库链接释放就慢,最终导致数据库链接不够用。

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

  • 优化后如下:
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
@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;
}

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

  • 测试是否事务控制。很遗憾,事务控制失败。

方法上已经添加了@Transactional注解为什么该方法不能被事务控制呢?

  • 如果是在uploadFile方法上添加@Transactional注解就可以控制事务,去掉则不行。

现在的问题其实是一个非事务方法调同类一个事务方法,事务无法控制,这是为什么?

3️⃣下边分析原因:

  • 如果在uploadFile方法上添加@Transactional注解,代理对象执行此方法前会开启事务,如下图:

img

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

img

  • 所以判断该方法是否可以事务控制必须保证是==通过代理对象调用此方法,且此方法上添加了@Transactional注解==。

现在在addMediaFilesToDb方法上添加@Transactional注解,

  • 也不会进行事务控制是因为并不是通过代理对象执行的addMediaFilesToDb方法。
  • 为了判断在uploadFile方法中去调用addMediaFilesToDb方法是否是通过代理对象去调用,我们可以打断点跟踪。

img

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

4️⃣如何解决呢?通过代理对象去调用addMediaFilesToDb方法即可解决。

  1. 在MediaFileService的实现类中注入MediaFileService的代理对象,如下:
1
2
@Resource
private MediaFileService currentProxy;
  1. 将addMediaFilesToDb方法提成接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @description 将文件信息添加到文件表
* @param companyId 机构id
* @param fileMd5 文件md5值
* @param uploadFileParamsDto 上传文件的信息
* @param bucket 桶
* @param objectName 对象名称
* @return com.xuecheng.media.model.po.MediaFiles
* @author TaoYaopc
* @date 2023/06/07 21:22
*/

MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName);
  1. 调用addMediaFilesToDb方法的代码处改为如下:
1
2
3
4
.....
//写入文件表
MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_files, objectName);
....

5️⃣再次测试事务是否可以正常控制。