学成在线-媒资管理模块
1.模块需求分析
1.1模块介绍
媒资管理系统是每个在线教育平台所必须具备的,查阅百度百科对它的定义如下:
媒体资源管理(Media Asset Management,MAM)系统是建立在多媒体、网络、数据库和数字存储等先进技术基础上的一个对各种媒体及内容(如视/音频资料、文本文件、图表等)进行数字化存储、管理以及应用的总体解决方案,包括数字媒体的采集、编目、管理、传输和编码转换等所有环节。其主要是满足媒体资源拥有者收集、保存、查找、编辑、发布各种信息的要求,为媒体资源的使用者提供访问内容的便捷方法,实现对媒体资源的高效管理,大幅度提高媒体资源的价值。
- 每个教学机构都可以在媒资系统管理自己的教学资源,包括:视频、教案等文件。
目前媒资管理的主要管理对象是视频、图片、文档等,包括:媒资文件的查询、文件上传、视频处理等。
媒资查询:教学机构查询自己所拥有的媒资信息。
- 文件上传:包括上传图片、上传文档、上传视频。
- 视频处理:视频上传成功,系统自动对视频进行编码处理。
- 文件删除:教学机构删除自己上传的媒资文件。
1.2业务流程
- 上传图片
- 教学机构人员在课程信息编辑页面上传课程图片,课程图片统一记录在媒资管理系统
- 上传视频
- 教学机构人员进入媒资管理列表查询自己上传的媒资文件
- 教育机构用户在媒资管理页面中点击上传视频按钮
- 选择要上传的文件,自动执行文件上传
- 视频上传成功会自动处理,处理完成后可以预览视频
- 处理视频
- 对需要转码处理的视频,系统会自动对齐处理,处理后生成视频的URL
- 审核媒资
- 运营用户登入运营平台,并进入媒资管理界面,查找待审核媒资
- 点击列表中媒资名称链接,可以预览该媒资,若是视频,则播放视频
- 点击列表中某媒资后的审核按钮,即完成媒资的审批过程
- 审核媒资包括程序自动审核和人工审核,程序可以通过鉴黄接口(https://www.aliyun.com/product/lvwang?spm=5176.19720258.J_3207526240.51.e93976f4rSq796)审核视频,对有异议的视频由人工进行审核。
- 绑定媒资
- 课程计划创建好后需要绑定媒资文件,比如:如果课程计划绑定了视频文件,进入课程在线学习界面后,点课程计划名称则在线播放视频
- 如何将课程计划绑定媒资呢?
- 教育机构用户进入课程管理页面编辑某一课程,在课程大纲编辑页的某一小节后,可以添加媒资信息
- 点击添加视频,会弹出对话框,可通过输入视频关键字搜索已审核通过的视频媒资
- 选择视频媒资,点击提交安努,完成课程计划绑定媒资流程
1.3数据模型
本模块媒资文件相关的数据表
)
- 媒资文件表:存储文件信息,包括图片、视频、文档等。
- media_process: 待处理视频表。
- media_process_history: 视频处理历史表,记录已经处理成功的视频信息。
媒资文件与课程计划绑定关系表
2.搭建模块环境
2.1构建问题分析
- 当前这种由前端直接请求微服务的方式存在弊端:
- 如果在前端对每个请求地址都配置绝对路径,非常不利于系统维护,比如下边代码中请求系统管理服务的地址使用的是localhost
- 基于这个问题可以采用网关来解决,如下图:
- 这样在前端的代码中只需要指定每个接口的相对路径,如下所示:
- 有了网关就可以对请求进行路由,路由到具体的微服务,减少外界对接微服务的成本,比如:400电话,路由的试可以根据请求路径进行路由、根据host地址进行路由等, 当微服务有多个实例时可以通过负载均衡算法进行路由
- 另外,网关还可以实现权限控制、限流等功能。
- 项目采用Spring Cloud Gateway作为网关,网关在请求路由时需要知道每个微服务实例的地址,项目使用Nacos作用服务发现中心和配置中心,整体的架构图如下:
2.2搭建Nacos
2.2.1服务发现中心
访问:http://192.168.101.65:8848/nacos/
账号密码:nacos/nacos
- 创建命名空间
- 在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> - 在api模块中添加依赖(需要请求的模块都要添加)
1
2
3
4<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency> - 修改yaml文件
1
2
3
4
5
6
7
8
9spring:
application:
name: content-api
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery:
namespace: dev
group: xuecheng-plus-project - 重启查看
2.2.2配置中心
配置api
- 添加依赖
1
2
3
4<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency> - nacos如何去
定位一个具体的配置文件
,即:namespace、group、dataid.
- 以下配置的文件名为:content-api-dev.yaml
- 添加配置
- 扩展(依赖)配置文件和引用公共文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21nacos:
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
配置优先级
- 引入配置文件的形式有:
- 以项目应用名方式引入
- 以扩展配置文件方式引入
- 以共享配置文件 方式引入
- 本地配置文件
- 项目应用名配置文件 > 扩展配置文件 > 共享配置文件 > 本地配置文件
- 在项目nacos配置文件里面添加
1
2
3
4
5配置本地优先
spring:
cloud:
config:
override-none: true
- 添加依赖
2.3搭建网关
- 创建xuecheng-plus-gateway模块
- 导入依赖
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
<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> - 把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 - 运行启动类,并在nacos中查看是否运行成功
- 在测试接口中测试
1
POST {{gateway_host}}/content/course/list?pageNo=1&pageSize=2
2.4搭建媒资工程
直接在导入后设置bootstrap3.分布式文件系统
3.1介绍
- 文件系统是负责管理和存储文件的系统软件,操作系统通过文件系统提供的接口去存取文件,用户通过操作系统访问磁盘上的文件。
- 通过概念可以简单理解为:一个计算机无法存储海量的文件,通过网络将若干计算机组织起来
共同去存储
海量的文件,去接收海量用户的请求,这些组织起来的计算机通过网络进行通信,如下图: - 好处:
- 一台计算机的文件系统处理能力扩充到多台计算机同时处理。
- 一台计算机挂了还有另外副本计算机提供数据。
- 每台计算机可以放在不同的地域,这样用户就可以就近访问,提高访问速度。
- 市面上有哪些分布式文件系统的产品呢?
- NFS
- 阅读百度百科:
- 特点:
- 在客户端上映射NFS服务器的驱动器。
- 客户端通过网络访问NFS服务器的硬盘完全透明。
- 阅读百度百科:
GFS
- GFS采用主从结构,一个GFS集群由一个master和大量的chunkserver组成。
- master存储了数据文件的元数据,一个文件被分成了若干块存储在多个chunkserver中。
- 用户从master中获取数据元信息,向chunkserver存储数据。
HDFS
- HDFS,是Hadoop Distributed File System的简称,是Hadoop抽象文件系统的一种实现。HDFS是一个高度容错性的系统,适合部署在廉价的机器上。HDFS能提供高吞吐量的数据访问,非常适合大规模数据集上的应用。 HDFS的文件分布在集群机器上,同时提供副本进行容错及可靠性保证。例如客户端写入读取文件的直接操作都是分布在集群各个机器上的,没有单点性能压力。
- 下图是HDFS的架构图:
- HDFS采用主从结构,一个HDFS集群由一个名称结点和若干数据结点组成。
- 名称结点存储数据的元信息,一个完整的数据文件分成若干块存储在数据结点。
- 客户端从名称结点获取数据的元信息及数据分块的信息,得到信息客户端即可从数据块来存取数据。
云计算厂家
- 阿里云对象存储服务(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进行负载均衡访问。
- Minio使用纠删码技术来保护数据,它是一种恢复丢失和损坏数据的数学算法,它将数据分块冗余的分散存储在各各节点的磁盘上,所有的可用磁盘组成一个集合,上图由8块硬盘组成一个集合,当上传一个文件时会通过纠删码算法计算对文件进行分块存储,除了将文件本身分成4个数据块,还会生成4个校验块,数据块和校验块会分散的存储在这8块硬盘上。
- 使用纠删码的好处是即便丢失一半数量(N/2)的硬盘,仍然可以恢复数据。 比如上边集合中有4个以内的硬盘损害仍可保证数据恢复,不影响上传和下载,如果多于一半的硬盘坏了则无法恢复。
3.2.2window使用
- 创建4个目录表示4块硬盘
- 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
- 按上面给出的链接登录(账号密码默认为minioadmin)
- 虚拟机链接: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
76package 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();
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);
}
public void test_delete() throws Exception {
//文件参数信息
RemoveObjectArgs deleteObject = RemoveObjectArgs.builder()
.bucket("testbucket")
.object("test/1/1.pm4")
.build();
//删除文件
minioClient.removeObject(deleteObject);
}
//查询文件 从minio中下载
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
79public class BigFileTest {
//分块测试
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();
}
//将分块合并
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>() {
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
72public class MinioTest {
MinioClient minioClient =
MinioClient.builder()
.endpoint("http://192.168.101.65:9000")
.credentials("minioadmin", "minioadmin")
.build();
//查询文件 从minio中下载
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
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接口合并分块
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的地址进行拼接
- 数据模型
4.2准备工作
- 把minio里面的桶改为public
- 在nacos配置中minio的相关信息
1
2
3
4
5
6
7minio:
endpoint: http://192.168.101.65:9000
accessKey: minioadmin
secretKey: minioadmin
bucket:
files: mediafiles
videofiles: video - 在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
26package 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;
public class MinioConfig {
private String endpoint;
private String admin;
private String passwork;
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
5package com.xuecheng.media.model.dto;
import com.xuecheng.media.model.po.MediaFiles;
public class UploadFileResultDto extends MediaFiles {
} - MediaFilesController接口
1
2
3
4
5
6
7
8
public UploadFileResultDto uploadFileResultDto({ 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
46public class MediaFileServiceImpl implements MediaFileService {
MediaFilesMapper mediaFilesMapper;
MinioClient minioClient;
MediaFileService currentProxy;
//存储普通文件
private String bucket_mediafiles;
//存储视频
private String bucket_video;
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 */
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
public UploadFileResultDto upload( MultipartFile filedata,
String folder,
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.jpg5.上传视频
5.1需求分析
5.2断点续传
- 断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载,断点续传可以提高节省操作时间,提高用户体验性。
- 前端对文件进行分块。
- 前端上传分块文件前请求媒资服务检查文件是否存在,如果已经存在则不再上传。s
- 如果分块文件不存在则前端开始上传
- 前端请求媒资服务上传分块。
- 媒资服务将分块上传至MinIO。
- 前端将分块上传完毕请求媒资服务合并分块。
- 媒资服务判断分块上传完成则请求MinIO合并文件。
- 合并完成校验合并后的文件是否完整,如果不完整则删除文件。
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
64package com.xuecheng.base.model;
import lombok.Data;
import lombok.ToString;
//@description 通用结果类型
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
public class BigFilesController {
public RestResponse<Boolean> checkFile( { String fileMd5)
return null;
}
public RestResponse<Boolean> checkChunk(int chunk) { String fileMd5,
return null;
}
public RestResponse uploadChunk(int chunk) { MultipartFile file, String fileMd5,
return null;
}
public RestResponse mergeChunks(int chunkTotal) { String fileMd5, String fileName,
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
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
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上传分块
- 分块是否存在,只需要判断minio对应的目录下是否存在分块文件
- 定义接口
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
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
public RestResponse<Boolean> checkFile( { String fileMd5)
return mediaFileService.checkFile(fileMd5);
}
public RestResponse<Boolean> checkChunk(int chunk) { String fileMd5,
return mediaFileService.checkChunk(fileMd5, chunk);
}
public RestResponse uploadChunk(int chunk) MultipartFile file, String fileMd5, throws Exception {
return mediaFileService.uploadChunk(fileMd5, chunk, file.getBytes());
}
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Lemon的博客!