完整版请移步至我的个人博客查看:https://cyborg2077.github.io/
学成在线–项目环境搭建
学成在线–内容管理模块
学成在线–媒资管理模块
学成在线–课程发布模块
学成在线–认证授权模块
学成在线–选课学习模块
学成在线–项目优化
Git仓库:https://github.com/Cyborg2077/xuecheng-plus
媒体资源管理(Media Asset Management,MAM)系统是建立在多媒体、网络、数据库和数字存储等先进技术基础上的一个对各种媒体及内容(如视/音频资料、文本文件、图表等)进行数字化存储、管理以及应用的总体解决方案,包括数字媒体的采集、编目、管理、传输和编码转换等所有环节。其主要是满足媒体资源拥有者收集、保存、查找、编辑、发布各种信息的要求,为媒体资源的使用者提供访问内容的便捷方法,实现对媒体资源的高效管理,大幅度提高媒体资源的价值。
媒资管理
页面中点击上传视频
按钮审核按钮
,即完成媒资的审批过程课程大纲
编辑页的某一小节后,可以添加媒资信息
添加视频
,会弹出对话框,可通过输入视频关键字搜索已审核通过的视频媒资// 列表
export async function dictionaryAll(params: any = undefined, body: any = undefined): Promise {
//const { data } = await createAPI('/system/dictionary/all', 'get', params, body)
const { data } = await createAPI('http://localhost:53110/system/dictionary/all', 'get', params, body)
return data
}
// 列表
export async function dictionaryAll(params: any = undefined, body: any = undefined): Promise {
//const { data } = await createAPI('/system/dictionary/all', 'get', params, body)
const { data } = await createAPI('/system/dictionary/all', 'get', params, body)
return data
}
docker pull nacos/nacos-server:1.4.1
docker run --name nacos -e MODE=standalone -p 8849:8848 -d nacos/nacos-server:1.4.1
虚拟机ip:8848/nacos
登录,默认的账号密码均为nacos
2.2.6.RELEASE
com.alibaba.cloud
spring-cloud-alibaba-dependencies
${spring-cloud-alibaba.version}
pom
import
com.alibaba.cloud
spring-cloud-alibaba-nacos-discovery
spring:
application:
name: content-service
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
namespace: dev
group: xuecheng-plus-project
spring:
application:
name: system-service
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
namespace: dev
group: xuecheng-plus-project
content-service-dev.yml
,由content-service
、dev
、yml
三部分组成
content-service
:它是在application.yml中配置的应用名,即spring.application.name
的值dev
:它是环境名,由spring.profile.active指定yml
:它是配置文件的后缀spring:
application:
name: content-service
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
作为文件id,来读取配置。spring:
application:
name: content-service
cloud:
nacos:
server-addr: 127.0.0.1:8848
config:
namespace: ${spring.profiles.active}
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true
profiles:
active: dev
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
@Test
void contextQueryCourseTest() {
PageResult result = courseBaseInfoService.queryCourseBaseList(new PageParams(1L, 10L), new QueryCourseParamDto());
log.info("查询到数据:{}", result);
}
content-api-dev.yaml
content-api-dev.yaml
管理server:
servlet:
context-path: /content
port: 53040
spring:
application:
name: content-api
# 日志文件配置路径
logging:
config: classpath:log4j2-dev.xml
# swagger 文档配置
swagger:
title: "学成在线内容管理系统"
description: "内容系统管理系统对课程相关信息进行业务管理数据"
base-package: com.xuecheng.content
enabled: true
version: 1.0.0
content-api-dev.yaml
的内容如下server:
servlet:
context-path: /content
port: 53040
# 日志文件配置路径
logging:
config: classpath:log4j2-dev.xml
# swagger 文档配置
swagger:
title: "学成在线内容管理系统"
description: "内容系统管理系统对课程相关信息进行业务管理数据"
base-package: com.xuecheng.content
enabled: true
version: 1.0.0
{% endtabs %}
content-api
的bootstrap.yml
#微服务配置
spring:
application:
name: content-api
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
namespace: ${spring.profiles.active}
group: xuecheng-plus-project
config:
namespace: ${spring.profiles.active}
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
{% note warning no-icon %}
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
{% endnote %}
swagger-dev.yaml
公用配置,这里的group可以设置为xuecheng-plus-common
,该组下的内容都作为xuecheng-plus
的公用配置content-api-dev.yaml
中的swagger
配置,在content-api
的bootstrap.yml
中使用shared-config
添加公用配置 server:
servlet:
context-path: /content
port: 53040
# 日志文件配置路径
logging:
config: classpath:log4j2-dev.xml
# swagger 文档配置
- swagger:
- title: "学成在线内容管理系统"
- description: "内容系统管理系统对课程相关信息进行业务管理数据"
- base-package: com.xuecheng.content
- enabled: true
- version: 1.0.0
#微服务配置
spring:
application:
name: content-api
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
namespace: ${spring.profiles.active}
group: xuecheng-plus-project
config:
namespace: ${spring.profiles.active}
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
profiles:
active: dev
{% endtabs %}
content-api-dev.yaml
中的logging
配置,在content-api
的bootstrap.yml
中使用shared-config
添加公用配置 server:
servlet:
context-path: /content
port: 53040
- # 日志文件配置路径
- logging:
- config: classpath:log4j2-dev.xml
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
{% endtabs %}
system-service-dev.yaml
配置spring:
application:
name: system-service
cloud:
nacos:
server-addr: 127.0.0.1:8848
config:
namespace: ${spring.profiles.active}
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
#微服务配置
spring:
application:
name: system-api
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
namespace: ${spring.profiles.active}
group: xuecheng-plus-project
config:
namespace: ${spring.profiles.active}
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true
extension-configs:
- data-id: system-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
spring:
cloud:
config:
override-none: true
4.0.0
com.xuecheng
xuecheng-plus-parent
0.0.1-SNAPSHOT
../xuecheng-plus-parent
xuecheng-plus-gateway
0.0.1-SNAPSHOT
xuecheng-plus-gateway
xuecheng-plus-gateway
1.8
org.springframework.cloud
spring-cloud-starter-gateway
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-logging
org.springframework.boot
spring-boot-starter-log4j2
#微服务配置
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
namespace: ${spring.profiles.active}
group: xuecheng-plus-project
config:
namespace: ${spring.profiles.active}
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
gateway-dev.yaml
配置server:
port: 53010 # 网关端口
spring:
cloud:
gateway:
routes:
- id: content-api # 路由id,自定义,只要唯一即可
uri: lb://content-api # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/content/** # 这个是按照路径匹配,只要以/content/开头就符合要求
- id: system-api
uri: lb://system-api
predicates:
- Path=/system/**
{
"dev": {
"host": "localhost:53010",
"content_host": "localhost:53040",
"system_host": "localhost:53110",
"media_host": "localhost:53050",
"cache_host": "localhost:53035",
+ "gateway_host": "localhost:53010"
}
}
### 课程查询列表
POST {{gateway_host}}/content/course/list?pageNo=1&pageSize=2
Content-Type: application/json
{
"auditStatus": "",
"courseName": "",
"publishStatus": ""
}
POST http://localhost:53010/content/course/list?pageNo=1&pageSize=2
HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: application/json
Date: Tue, 14 Feb 2023 10:25:35 GMT
{
"items": [
{
"id": 1,
"companyId": 22,
"companyName": null,
"name": "JAVA8/9/10新特性讲解啊",
···
}
media-api-dev.yaml
和media-service-dev.yaml
server:
servlet:
context-path: /media
port: 53050
spring:
datasource:
druid:
stat-view-servlet:
enabled: true
loginUsername: admin
loginPassword: 123456
dynamic:
primary: content #设置默认的数据源或者数据源组,默认值即为master
strict: true #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候回抛出异常,不启动会使用默认数据源.
druid:
initial-size: 3
max-active: 5
min-idle: 5
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
stat-view-servlet:
enabled: true
url-pattern: /druid/*
#login-username: admin
#login-password: admin
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
datasource:
content:
url: jdbc:mysql://localhost:3306/xc_content?serverTimezone=UTC&allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
username: root
password: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
media:
url: jdbc:mysql://localhost:3306/xc_media?serverTimezone=UTC&allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
username: root
password: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
{% endtabs %}
文件系统是操作系统用于明确存储设备(常见的是磁盘,也有基于NAND Flash的固态硬盘)或分区上的文件的方法和数据结构;即在存储设备上组织文件的方法。操作系统中负责管理和存储文件信息的软件机构称为文件管理系统,简称文件系统。文件系统由三部分组成:文件系统的接口,对对象操纵和管理的软件集合,对象及属性。从系统角度来看,文件系统是对文件存储设备的空间进行组织和分配,负责文件存储并对存入的文件进行保护和检索的系统。具体地说,它负责为用户建立文件,存入、读出、修改、转储文件,控制文件的存取,当用户不再使用时撤销文件等。
分布式文件系统(Distributed File System,DFS)是指文件系统管理的物理存储资源不一定直接连接在本地节点上,而是通过计算机网络与节点(可简单的理解为一台计算机)相连;或是若干不同的逻辑磁盘分区或卷标组合在一起而形成的完整的有层次的文件系统。DFS为分布在网络上任意位置的资源提供一个逻辑上的树形文件系统结构,从而使用户访问分布在网络上的共享文件更加简便。单独的 DFS共享文件夹的作用是相对于通过网络上的其他共享文件夹的访问点
可以简单的理解为:一个计算机无法存储海量的文件,通过网络将若干计算机组织起来共同去存储海量的文件,去接收海量用户的请求,这些组织起来的计算机通过计算机网络通信
这样做的好处
市面上有哪些分布式文件系统的产品呢?
NFS是基于UDP/IP协议的应用,其实现主要是采用远程过程调用RPC机制,RPC提供了一组与机器、操作系统以及低层传送协议无关的存取远程文件的操作。RPC采用了XDR的支持。XDR是一种与机器无关的数据描述编码的协议,他以独立与任意机器体系结构的格式对网上传送的数据进行编码和解码,支持在异构系统之间数据的传送。
GFS是一个可扩展的分布式文件系统,用于大型的、分布式的、对大量数据进行访问的应用。它运行于廉价的普通硬件上,并提供容错功能。它可以给大量的用户提供总体性能较高的服务。
Hadoop分布式文件系统(HDFS)是指被设计成适合运行在通用硬件(commodity hardware)上的分布式文件系统(Distributed File System)。它和现有的分布式文件系统有很多共同点。但同时,它和其他的分布式文件系统的区别也是很明显的。HDFS是一个高度容错性的系统,适合部署在廉价的机器上。HDFS能提供高吞吐量的数据访问,非常适合大规模数据集上的应用。HDFS放宽了一部分POSIX约束,来实现流式读取文件系统数据的目的。HDFS在最开始是作为Apache Nutch搜索引擎项目的基础架构而开发的。HDFS是Apache Hadoop Core项目的一部分。
HDFS有着高容错性(fault-tolerant)的特点,并且设计用来部署在低廉的(low-cost)硬件上。而且它提供高吞吐量(high throughput)来访问应用程序的数据,适合那些有着超大数据集(large data set)的应用程序。HDFS放宽了(relax)POSIX的要求(requirements)这样可以实现流的形式访问(streaming access)文件系统中的数据。
[外链图片转存中…(img-X4y2doSR-1679735939077)]
minio.exe server D:\develop\minio_data\data1 D:\develop\minio_data\data2 D:\develop\minio_data\data3 D:\develop\minio_data\data4
minio.exe server d:\minio_data
mediafiles
:普通文件video
:视频文件
io.minio
minio
8.4.3
com.squareup.okhttp3
okhttp
4.8.1
Parameters | Description |
---|---|
Endpoint | URL to S3 service. |
Access Key | Access key (aka user ID) of an account in the S3 service. |
Secret Key | Secret key (aka password) of an account in the S3 service. |
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());
}
}
}
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.
// 创建MinIO客户端,连接参数就是上述表格中的三个参数,127.0.0.1:9000、minioadmin、minioadmin
MinioClient minioClient =
MinioClient.builder()
.endpoint("https://play.min.io")
.credentials("Q3AM3UQ867SPQQA43P2F", "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG")
.build();
// Make 'asiatrip' bucket if not exist.
// 由于backet我们已经手动创建了,所以这段代码可以删掉
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'.
// 将 '/home/user/Photos/asiaphotos.zip' 文件命名为 'asiaphotos-2015.zip'
// 并上传到 'asiatrip' 里(示例代码创建的bucket)
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());
}
}
}
@SpringBootTest
public class MinIOTest {
// 创建MinioClient对象
static MinioClient minioClient =
MinioClient.builder()
.endpoint("http://127.0.0.1:9000")
.credentials("minioadmin", "minioadmin")
.build();
/**
* 上传测试方法
*/
@Test
public void uploadTest() {
try {
minioClient.uploadObject(
UploadObjectArgs.builder()
.bucket("testbucket")
.object("pic01.png") // 同一个桶内对象名不能重复
.filename("D:\\Picture\\background\\01.png")
.build()
);
System.out.println("上传成功");
} catch (Exception e) {
System.out.println("上传失败");
}
}
}
{% endtabs %}
@Test
public void deleteTest() {
try {
minioClient.removeObject(RemoveObjectArgs
.builder()
.bucket("testbucket")
.object("pic01.png")
.build());
System.out.println("删除成功");
} catch (Exception e) {
System.out.println("删除失败");
}
}
@Test
public void getFileTest() {
try {
InputStream inputStream = minioClient.getObject(GetObjectArgs.builder()
.bucket("testbucket")
.object("pic01.png")
.build());
FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\15863\\Desktop\\tmp.png");
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer,0,len);
}
inputStream.close();
fileOutputStream.close();
System.out.println("下载成功");
} catch (Exception e) {
System.out.println("下载失败");
}
}
@Test
public void getFileTest() {
try {
InputStream inputStream = minioClient.getObject(GetObjectArgs.builder()
.bucket("testbucket")
.object("pic01.png")
.build());
FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\15863\\Desktop\\tmp.png");
IOUtils.copy(inputStream,fileOutputStream);
System.out.println("下载成功");
} catch (Exception e) {
System.out.println("下载失败");
}
}
media-service-dev.yaml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/xc_media?serverTimezone=UTC&userUnicode=true&useSSL=false
username: root
password: A10ne,tillde@th.
cloud:
config:
override-none: true
minio:
endpoint: http://127.0.0.1:9000
accessKey: minioadmin
secretKey: minioadmin
bucket:
files: mediafiles
videofiles: video
spring:
application:
name: media-service
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
namespace: ${spring.profiles.active}
group: xuecheng-plus-project
config:
namespace: ${spring.profiles.active}
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默认为dev
profiles:
active: dev
@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() {
return MinioClient.builder().
endpoint(endpoint).
credentials(accessKey, secretKey).
build();
}
}
{
"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
}
@Data
public class UploadFileResultDto extends MediaFiles {
}
@ApiOperation("上传文件")
@RequestMapping(value = "/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile upload,
@RequestParam(value = "folder", required = false) String folder,
@RequestParam(value = "objectName", required = false) String objectName) {
return null;
}
@Data
@ToString
public class UploadFileParamsDto {
/**
* 文件名称
*/
private String filename;
/**
* 文件content-type
*/
private String contentType;
/**
* 文件类型(文档,图片,视频)
*/
private String fileType;
/**
* 文件大小
*/
private Long fileSize;
/**
* 标签
*/
private String tags;
/**
* 上传人
*/
private String username;
/**
* 备注
*/
private String remark;
}
/**
* @description 上传文件的通用接口
* @param companyId 机构id
* @param uploadFileParamsDto 文件信息
* @param bytes 文件字节数组
* @param folder 桶下边的子目录
* @param objectName 对象名称
* @return com.xuecheng.media.model.dto.UploadFileResultDto
*/
UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName);
{% tabs asdasd %}
@Autowired
MinioClient minioClient;
// 从配置文件获取bucket
@Value("${minio.bucket.files}")
private String bucket_files;
/**
*
* @param companyId 机构id
* @param uploadFileParamsDto 文件信息
* @param bytes 文件字节数组
* @param folder 桶下边的子目录
* @param objectName 对象名称
* @return
*/
@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
if (StringUtils.isEmpty(folder)) {
// 如果目录不存在,则自动生成一个目录
folder = getFileFolder(true, true, true);
} else if (!folder.endsWith("/")) {
// 如果目录末尾没有 / ,替他加一个
folder = folder + "/";
}
if (StringUtils.isEmpty(objectName)) {
// 如果文件名为空,则设置其默认文件名为文件的md5码 + 文件后缀名
String filename = uploadFileParamsDto.getFilename();
objectName = DigestUtils.md5DigestAsHex(bytes) + filename.substring(filename.lastIndexOf("."));
}
objectName = folder + objectName;
try {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucket_files)
.object(objectName)
.stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
.contentType(uploadFileParamsDto.getContentType())
.build());
} catch (Exception e) {
}
return null;
}
/**
* 自动生成目录
* @param year 是否包含年
* @param month 是否包含月
* @param day 是否包含日
* @return
*/
private String getFileFolder(boolean year, boolean month, boolean day) {
StringBuffer stringBuffer = new StringBuffer();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String dateString = dateFormat.format(new Date());
String[] split = dateString.split("-");
if (year) {
stringBuffer.append(split[0]).append("/");
}
if (month) {
stringBuffer.append(split[1]).append("/");
}
if (day) {
stringBuffer.append(split[2]).append("/");
}
return stringBuffer.toString();
}
// 保存到数据库
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.setBucket(bucket_files);
mediaFiles.setCreateDate(LocalDateTime.now());
mediaFiles.setStatus("1");
mediaFiles.setFilePath(objectName);
mediaFiles.setUrl("/" + bucket_files + "/" + objectName);
// 查阅数据字典,002003表示审核通过
mediaFiles.setAuditStatus("002003");
}
int insert = mediaFilesMapper.insert(mediaFiles);
if (insert <= 0) {
XueChengPlusException.cast("保存文件信息失败");
}
UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
return uploadFileResultDto;
@Autowired
MediaFilesMapper mediaFilesMapper;
@Autowired
MinioClient minioClient;
@Value("${minio.bucket.files}")
private String bucket_files;
/**
* @param companyId 机构id
* @param uploadFileParamsDto 文件信息
* @param bytes 文件字节数组
* @param folder 桶下边的子目录
* @param objectName 对象名称
* @return
*/
@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
String fileMD5 = DigestUtils.md5DigestAsHex(bytes);
if (StringUtils.isEmpty(folder)) {
// 如果目录不存在,则自动生成一个目录
folder = getFileFolder(true, true, true);
} else if (!folder.endsWith("/")) {
// 如果目录末尾没有 / ,替他加一个
folder = folder + "/";
}
if (StringUtils.isEmpty(objectName)) {
// 如果文件名为空,则设置其默认文件名为文件的md5码 + 文件后缀名
String filename = uploadFileParamsDto.getFilename();
objectName = fileMD5 + filename.substring(filename.lastIndexOf("."));
}
objectName = folder + objectName;
try {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
// 上传到minio
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucket_files)
.object(objectName)
.stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
.contentType(uploadFileParamsDto.getContentType())
.build());
// 保存到数据库
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.setBucket(bucket_files);
mediaFiles.setCreateDate(LocalDateTime.now());
mediaFiles.setStatus("1");
mediaFiles.setFilePath(objectName);
mediaFiles.setUrl("/" + bucket_files + "/" + objectName);
// 查阅数据字典,002003表示审核通过
mediaFiles.setAuditStatus("002003");
}
int insert = mediaFilesMapper.insert(mediaFiles);
if (insert <= 0) {
XueChengPlusException.cast("保存文件信息失败");
}
UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
return uploadFileResultDto;
} catch (Exception e) {
XueChengPlusException.cast("上传过程中出错");
}
return null;
}
/**
* 自动生成目录
* @param year 是否包含年
* @param month 是否包含月
* @param day 是否包含日
* @return
*/
private String getFileFolder(boolean year, boolean month, boolean day) {
StringBuffer stringBuffer = new StringBuffer();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String dateString = dateFormat.format(new Date());
String[] split = dateString.split("-");
if (year) {
stringBuffer.append(split[0]).append("/");
}
if (month) {
stringBuffer.append(split[1]).append("/");
}
if (day) {
stringBuffer.append(split[2]).append("/");
}
return stringBuffer.toString();
}
private String getFileFolder(boolean year, boolean month, boolean day) {
StringBuffer stringBuffer = new StringBuffer();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String dateString = dateFormat.format(new Date());
String[] split = dateString.split("-");
if (year) {
stringBuffer.append(split[0]).append("/");
}
if (month) {
stringBuffer.append(split[1]).append("/");
}
if (day) {
stringBuffer.append(split[2]).append("/");
}
return stringBuffer.toString();
}
{% endtabs %}
@ApiOperation("上传文件")
@RequestMapping(value = "/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile upload,
@RequestParam(value = "folder", required = false) String folder,
@RequestParam(value = "objectName", required = false) String objectName) {
UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
uploadFileParamsDto.setFileSize(upload.getSize());
String contentType = upload.getContentType();
if (contentType.contains("image")) {
// 图片
uploadFileParamsDto.setFileType("001001");
} else {
// 其他
uploadFileParamsDto.setFileType("001003");
}
uploadFileParamsDto.setFilename(upload.getOriginalFilename());
uploadFileParamsDto.setContentType(contentType);
Long companyId = 1232141425L;
try {
UploadFileResultDto uploadFileResultDto = mediaFileService.uploadFile(companyId, uploadFileParamsDto, upload.getBytes(), folder, objectName);
return uploadFileResultDto;
} catch (IOException e) {
XueChengPlusException.cast("上传文件过程出错");
}
return null;
}
### 上传文件
POST {{media_host}}/media/upload/coursefile
Content-Type: multipart/form-data; boundary=WebAppBoundary
--WebAppBoundary
Content-Disposition: form-data; name="filedata"; filename="test01.jpg"
Content-Type: application/octet-stream
< C:\Users\kyle\Desktop\Picture\photo\bg01.jpg
# 响应结果如下
POST http://localhost:53050/media/upload/coursefile
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 16 Feb 2023 09:57:48 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"id": "632fb34166d91865da576032b9330ced",
"companyId": 1232141425,
"companyName": null,
"filename": "test01.jpg",
"fileType": "001003",
"tags": null,
"bucket": "mediafiles",
"filePath": "2023/57/16/632fb34166d91865da576032b9330ced.jpg",
"fileId": "632fb34166d91865da576032b9330ced",
"url": "/mediafiles/2023/57/16/632fb34166d91865da576032b9330ced.jpg",
"username": null,
"createDate": "2023-02-16 17:57:48",
"changeDate": null,
"status": "1",
"remark": "",
"auditStatus": "002003",
"auditMind": null,
"fileSize": 22543
}
响应文件已保存。
> 2023-02-16T175748.200.json
com.j256.simplemagic
simplemagic
1.17
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(扩展名);
String contentType = extensionMatch.getMimeType();
/**
* @param companyId 机构id
* @param uploadFileParamsDto 文件信息
* @param bytes 文件字节数组
* @param folder 桶下边的子目录
* @param objectName 对象名称
* @return
*/
@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
String fileMD5 = DigestUtils.md5DigestAsHex(bytes);
if (StringUtils.isEmpty(folder)) {
// 如果目录不存在,则自动生成一个目录
folder = getFileFolder(true, true, true);
} else if (!folder.endsWith("/")) {
// 如果目录末尾没有 / ,替他加一个
folder = folder + "/";
}
if (StringUtils.isEmpty(objectName)) {
// 如果文件名为空,则设置其默认文件名为文件的md5码 + 文件后缀名
String filename = uploadFileParamsDto.getFilename();
objectName = fileMD5 + filename.substring(filename.lastIndexOf("."));
}
objectName = folder + objectName;
try {
addMediaFilesToMinIO(bytes, bucket_files, objectName);
MediaFiles mediaFiles = addMediaFilesToDB(companyId, uploadFileParamsDto, objectName, fileMD5, bucket_files);
UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
return uploadFileResultDto;
} catch (Exception e) {
XueChengPlusException.cast("上传过程中出错");
}
return null;
}
/**
* 将文件信息添加到文件表
* @param companyId 机构id
* @param uploadFileParamsDto 上传文件的信息
* @param objectName 对象名称
* @param fileMD5 文件的md5码
* @param bucket 桶
* @return
*/
private MediaFiles addMediaFilesToDB(Long companyId, UploadFileParamsDto uploadFileParamsDto, String objectName, String fileMD5, String bucket) {
// 保存到数据库
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.setBucket(bucket);
mediaFiles.setCreateDate(LocalDateTime.now());
mediaFiles.setStatus("1");
mediaFiles.setFilePath(objectName);
mediaFiles.setUrl("/" + bucket + "/" + objectName);
// 查阅数据字典,002003表示审核通过
mediaFiles.setAuditStatus("002003");
}
int insert = mediaFilesMapper.insert(mediaFiles);
if (insert <= 0) {
XueChengPlusException.cast("保存文件信息失败");
}
return mediaFiles;
}
/**
* @param bytes 文件字节数组
* @param bucket 桶
* @param objectName 对象名称 23/02/15/porn.mp4
* @throws ErrorResponseException
* @throws InsufficientDataException
* @throws InternalException
* @throws InvalidKeyException
* @throws InvalidResponseException
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws ServerException
* @throws XmlParserException
*/
private void addMediaFilesToMinIO(byte[] bytes, String bucket, String objectName) {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE; // 默认content-type为未知二进制流
if (objectName.indexOf(".") >= 0) { // 判断对象名是否包含 .
// 有 . 则划分出扩展名
String extension = objectName.substring(objectName.lastIndexOf("."));
// 根据扩展名得到content-type,如果为未知扩展名,例如 .abc之类的东西,则会返回null
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
// 如果得到了正常的content-type,则重新赋值,覆盖默认类型
if (extensionMatch != null) {
contentType = extensionMatch.getMimeType();
}
}
try {
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
.contentType(contentType)
.build());
} catch (Exception e) {
log.debug("上传到文件系统出错:{}", e.getMessage());
throw new XueChengPlusException("上传到文件系统出错");
}
}
private static String getContentType(String objectName) {
String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE; // 默认content-type为未知二进制流
if (objectName.indexOf(".") >= 0) { // 判断对象名是否包含 .
// 有 . 则划分出扩展名
String extension = objectName.substring(objectName.lastIndexOf("."));
// 根据扩展名得到content-type,如果为未知扩展名,例如 .abc之类的东西,则会返回null
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
// 如果得到了正常的content-type,则重新赋值,覆盖默认类型
if (extensionMatch != null) {
contentType = extensionMatch.getMimeType();
}
}
return contentType;
}
@Transactional
,当调用updateFile方法前会开启数据库事务,如果上传文件过程时间较长(例如用户在上传超大视频文件),那么数据库的食物持续时间也会变长(因为在updateFile方法中,我们即要将文件上传到minio,又要将文件信息写入数据库),这样数据库连接释放就慢,最终导致数据库链接不够用addMediaFilesToDB
方法上添加事务控制即可,同时将uploadFile方法上的@Transactional
注解去掉@Transactional
注解 @Autowired
MediaFileService currentProxy;
/**
* 将文件信息添加到文件表
*
* @param companyId 机构id
* @param uploadFileParamsDto 上传文件的信息
* @param objectName 对象名称
* @param fileMD5 文件md5码
* @param bucket 桶
* @return
*/
MediaFiles addMediaFilesToDB(Long companyId, UploadFileParamsDto uploadFileParamsDto, String objectName, String fileMD5, String bucket);
MediaFiles mediaFiles = currentProxy.addMediaFilesToDB(companyId, uploadFileParamsDto, objectName, fileMD5, bucket_files);
# 图片服务器地址
VUE_APP_SERVER_PICSERVER_URL=http://127.0.0.1:9000
@Override
public PageResult queryMediaFiles(Long companyId, PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {
//构建查询条件对象
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
+ queryWrapper.like(!StringUtils.isEmpty(queryMediaParamsDto.getFilename()), MediaFiles::getFilename, queryMediaParamsDto.getFilename());
+ queryWrapper.eq(!StringUtils.isEmpty(queryMediaParamsDto.getFileType()), MediaFiles::getFileType, queryMediaParamsDto.getFileType());
//分页对象
Page page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
// 查询数据内容获得结果
Page pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
// 获取数据列表
List list = pageResult.getRecords();
// 获取数据总数
long total = pageResult.getTotal();
// 构建结果集
PageResult mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
return mediaListResult;
}
媒资管理
页面中点击上传视频
按钮,打开上传界面 @Test
public void testChunk() throws IOException {
// 源文件
File sourceFile = new File("D:\\BaiduNetdiskDownload\\星际牛仔1998\\星际牛仔1.mp4");
// 块文件路径
String chunkPath = "D:\\BaiduNetdiskDownload\\星际牛仔1998\\chunk\\";
File chunkFolder = new File(chunkPath);
if (!chunkFolder.exists()) {
chunkFolder.mkdirs();
}
// 分块大小 1M
long chunkSize = 1024 * 1024 * 1;
// 计算块数,向上取整
long chunkNum = (long) Math.ceil(sourceFile.length() * 1.0 / chunkSize);
// 缓冲区大小
byte[] buffer = new byte[1024];
// 使用RandomAccessFile访问文件
RandomAccessFile raf_read = new RandomAccessFile(sourceFile, "r");
// 遍历分块,依次向每一个分块写入数据
for (int i = 0; i < chunkNum; i++) {
// 创建分块文件,默认文件名 path + i,例如chunk\1 chunk\2
File file = new File(chunkPath + i);
if (file.exists()){
file.delete();
}
boolean newFile = file.createNewFile();
if (newFile) {
int len;
RandomAccessFile raf_write = new RandomAccessFile(file, "rw");
// 向分块文件写入数据
while ((len = raf_read.read(buffer)) != -1) {
raf_write.write(buffer, 0, len);
// 写满就停
if (file.length() >= chunkSize)
break;
}
raf_write.close();
}
}
raf_read.close();
System.out.println("写入分块完毕");
}
@Test
public void testMerge() throws IOException {
// 块文件目录
File chunkFolder = new File("D:\\BaiduNetdiskDownload\\星际牛仔1998\\chunk\\");
// 源文件
File sourceFile = new File("D:\\BaiduNetdiskDownload\\星际牛仔1998\\星际牛仔1.mp4");
// 合并文件
File mergeFile = new File("D:\\BaiduNetdiskDownload\\星际牛仔1998\\星际牛仔1-1.mp4");
mergeFile.createNewFile();
// 用于写文件
RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");
// 缓冲区
byte[] buffer = new byte[1024];
// 文件名升序排序
File[] files = chunkFolder.listFiles();
List fileList = Arrays.asList(files);
Collections.sort(fileList, Comparator.comparingInt(o -> Integer.parseInt(o.getName())));
// 合并文件
for (File chunkFile : fileList) {
RandomAccessFile raf_read = new RandomAccessFile(chunkFile, "r");
int len;
while ((len = raf_read.read(buffer)) != -1) {
raf_write.write(buffer, 0, len);
}
raf_read.close();
}
raf_write.close();
// 判断合并后的文件是否与源文件相同
FileInputStream fileInputStream = new FileInputStream(sourceFile);
FileInputStream mergeFileStream = new FileInputStream(mergeFile);
//取出原始文件的md5
String originalMd5 = DigestUtils.md5Hex(fileInputStream);
//取出合并文件的md5进行比较
String mergeFileMd5 = DigestUtils.md5Hex(mergeFileStream);
if (originalMd5.equals(mergeFileMd5)) {
System.out.println("合并文件成功");
} else {
System.out.println("合并文件失败");
}
}
{
"code": 0
}
{
"code": 1
}
@Data
public class RestResponse {
/**
* 相应编码 0为正常 -1为错误
*/
private int code;
/**
* 响应提示信息
*/
private String msg;
/**
* 响应内容
*/
private T result;
public RestResponse(int code, String msg) {
this.code = code;
this.msg = msg;
}
public RestResponse() {
this(0, "success");
}
/**
* 错误信息的封装
*/
public static RestResponse validfail() {
RestResponse response = new RestResponse<>();
response.setCode(-1);
return response;
}
public static RestResponse validfail(String msg) {
RestResponse response = new RestResponse<>();
response.setCode(-1);
response.setMsg(msg);
return response;
}
public static RestResponse validfail(String msg, T result) {
RestResponse response = new RestResponse<>();
response.setCode(-1);
response.setMsg(msg);
response.setResult(result);
return response;
}
/**
* 正常信息的封装
*/
public static RestResponse success() {
return new RestResponse<>();
}
public static RestResponse success(T result) {
RestResponse response = new RestResponse<>();
response.setResult(result);
return response;
}
public static RestResponse success(String msg, T result) {
RestResponse response = new RestResponse<>();
response.setMsg(msg);
response.setResult(result);
return response;
}
}
@Api(value = "大文件上传接口", tags = "大文件上传接口")
@RestController
public class BigFilesController {
@ApiOperation(value = "文件上传前检查文件")
@PostMapping("/upload/checkfile")
public RestResponse checkFile(@RequestParam("fileMd5") String fileMd5) {
return null;
}
@ApiOperation(value = "分块文件上传前检查分块")
@PostMapping("/upload/checkchunk")
public RestResponse checkChunk(@RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") int chunk) {
return null;
}
@ApiOperation(value = "上传分块文件")
@PostMapping("/upload/uploadchunk")
public RestResponse uploadChunk(@RequestParam("file") MultipartFile file, @RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") int chunk) {
return null;
}
@ApiOperation(value = "合并分块文件")
@PostMapping("/upload/mergechunks")
public RestResponse mergeChunks(@RequestParam("fileMd5") String fileMd5, @RequestParam("fileName") String fileName, @RequestParam("chunkTotal") int chunkTotal) {
return null;
}
}
/**
* 检查文件是否存在
*
* @param fileMd5 文件的md5
* @return
*/
boolean checkFile(String fileMd5);
/**
* 检查分块是否存在
* @param fileMd5 文件的MD5
* @param chunkIndex 分块序号
* @return
*/
boolean checkChunk(String fileMd5, int chunkIndex);
@Override
public RestResponse 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);
}
@Override
public RestResponse 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" + "/";
}
/**
* 上传分块
* @param fileMd5 文件MD5
* @param chunk 分块序号
* @param bytes 文件字节
* @return
*/
RestResponse uploadChunk(String fileMd5,int chunk,byte[] bytes);
@Override
public RestResponse uploadChunk(String fileMd5, int chunk, byte[] bytes) {
// 分块文件路径
String chunkFilePath = getChunkFileFolderPath(fileMd5) + chunk;
try {
addMediaFilesToMinIO(bytes, video_files, chunkFilePath);
return RestResponse.success(true);
} catch (Exception e) {
log.debug("上传分块文件:{}失败:{}", chunkFilePath, e.getMessage());
}
return RestResponse.validfail("上传文件失败", false);
}
@ApiOperation(value = "文件上传前检查文件")
@PostMapping("/upload/checkfile")
public RestResponse checkFile(@RequestParam("fileMd5") String fileMd5) {
return mediaFileService.checkFile(fileMd5);
}
@ApiOperation(value = "分块文件上传前检查分块")
@PostMapping("/upload/checkchunk")
public RestResponse checkChunk(@RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") int chunk) {
return mediaFileService.checkChunk(fileMd5, chunk);
}
@ApiOperation(value = "上传分块文件")
@PostMapping("/upload/uploadchunk")
public RestResponse uploadChunk(@RequestParam("file") MultipartFile file, @RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") int chunk) throws Exception {
return mediaFileService.uploadChunk(fileMd5, chunk, file.getBytes());
}
/**
* 下载分块文件
* @param fileMd5 文件的MD5
* @param chunkTotal 总块数
* @return 分块文件数组
*/
private File[] checkChunkStatus(String fileMd5, int chunkTotal) {
// 作为结果返回
File[] files = new File[chunkTotal];
// 获取分块文件目录
String chunkFileFolder = getChunkFileFolderPath(fileMd5);
for (int i = 0; i < chunkTotal; i++) {
// 获取分块文件路径
String chunkFilePath = chunkFileFolder + i;
File chunkFile = null;
try {
// 创建临时的分块文件
chunkFile = File.createTempFile("chunk" + i, null);
} catch (Exception e) {
XueChengPlusException.cast("创建临时分块文件出错:" + e.getMessage());
}
// 下载分块文件
chunkFile = downloadFileFromMinio(chunkFile, video_files, chunkFilePath);
// 组成结果
files[i] = chunkFile;
}
return files;
}
/**
* 从Minio中下载文件
* @param file 目标文件
* @param bucket 桶
* @param objectName 桶内文件路径
* @return
*/
private File downloadFileFromMinio(File file, String bucket, String objectName) {
try (FileOutputStream fileOutputStream = new FileOutputStream(file);
InputStream inputStream = minioClient.getObject(GetObjectArgs
.builder()
.bucket(bucket)
.object(objectName)
.build())) {
IOUtils.copy(inputStream, fileOutputStream);
return file;
} catch (Exception e) {
XueChengPlusException.cast("查询文件分块出错");
}
return null;
}
@Override
public RestResponse mergeChunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) throws IOException {
// 下载分块文件
File[] chunkFiles = checkChunkStatus(fileMd5, chunkTotal);
// 获取源文件名
String fileName = uploadFileParamsDto.getFilename();
// 获取源文件扩展名
String extension = fileName.substring(fileName.lastIndexOf("."));
// 创建出临时文件,准备合并
File mergeFile = File.createTempFile(fileName, extension);
// 缓冲区
byte[] buffer = new byte[1024];
// 写入流,向临时文件写入
RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");
// 遍历分块文件数组
for (File chunkFile : chunkFiles) {
// 读取流,读分块文件
RandomAccessFile raf_read = new RandomAccessFile(chunkFile, "r");
int len;
while ((len = raf_read.read(buffer)) != -1) {
raf_write.write(buffer, 0, len);
}
}
uploadFileParamsDto.setFileSize(mergeFile.length());
// 对文件进行校验,通过MD5值比较
FileInputStream mergeInputStream = new FileInputStream(mergeFile);
String mergeMd5 = DigestUtils.md5DigestAsHex(mergeInputStream);
if (!fileMd5.equals(mergeMd5)) {
XueChengPlusException.cast("合并文件校验失败");
}
// 拼接合并文件路径
String mergeFilePath = getFilePathByMd5(fileMd5, extension);
// 将本地合并好的文件,上传到minio中,这里重载了一个方法
addMediaFilesToMinIO(mergeFile.getAbsolutePath(), video_files, mergeFilePath);
// 将文件信息写入数据库
MediaFiles mediaFiles = addMediaFilesToDB(companyId, uploadFileParamsDto, mergeFilePath, mergeMd5, video_files);
if (mediaFiles == null) {
XueChengPlusException.cast("媒资文件入库出错");
}
return RestResponse.success();
}
/**
* 将本地文件上传到minio
* @param filePath 本地文件路径
* @param bucket 桶
* @param objectName 对象名称
*/
private void addMediaFilesToMinIO(String filePath, String bucket, String objectName) {
String contentType = getContentType(objectName);
try {
minioClient.uploadObject(UploadObjectArgs
.builder()
.bucket(bucket)
.object(objectName)
.filename(filePath)
.contentType(contentType)
.build());
} catch (Exception e) {
XueChengPlusException.cast("上传到文件系统出错");
}
}
/**
* 根据MD5和文件扩展名,生成文件路径,例 /2/f/2f6451sdg/2f6451sdg.mp4
* @param fileMd5 文件MD5
* @param extension 文件扩展名
* @return
*/
private String getFilePathByMd5(String fileMd5, String extension) {
return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5 + extension;
}
@Override
public RestResponse mergeChunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
// 下载分块文件
File[] chunkFiles = checkChunkStatus(fileMd5, chunkTotal);
// 获取源文件名
String fileName = uploadFileParamsDto.getFilename();
// 获取源文件扩展名
String extension = fileName.substring(fileName.lastIndexOf("."));
// 创建出临时文件,准备合并
File mergeFile = null;
try {
mergeFile = File.createTempFile(fileName, extension);
} catch (IOException e) {
XueChengPlusException.cast("创建合并临时文件出错");
}
try {
// 缓冲区
byte[] buffer = new byte[1024];
// 写入流,向临时文件写入
try (RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw")) {
// 遍历分块文件数组
for (File chunkFile : chunkFiles) {
// 读取流,读分块文件
try (RandomAccessFile raf_read = new RandomAccessFile(chunkFile, "r")) {
int len;
while ((len = raf_read.read(buffer)) != -1) {
raf_write.write(buffer, 0, len);
}
}
}
} catch (Exception e) {
XueChengPlusException.cast("合并文件过程中出错");
}
uploadFileParamsDto.setFileSize(mergeFile.length());
// 对文件进行校验,通过MD5值比较
try (FileInputStream mergeInputStream = new FileInputStream(mergeFile)) {
String mergeMd5 = org.apache.commons.codec.digest.DigestUtils.md5Hex(mergeInputStream);
if (!fileMd5.equals(mergeMd5)) {
XueChengPlusException.cast("合并文件校验失败");
}
log.debug("合并文件校验通过:{}", mergeFile.getAbsolutePath());
} catch (Exception e) {
XueChengPlusException.cast("合并文件校验异常");
}
String mergeFilePath = getFilePathByMd5(fileMd5, extension);
// 将本地合并好的文件,上传到minio中,这里重载了一个方法
addMediaFilesToMinIO(mergeFile.getAbsolutePath(), video_files, mergeFilePath);
log.debug("合并文件上传至MinIO完成{}", mergeFile.getAbsolutePath());
// 将文件信息写入数据库
MediaFiles mediaFiles = addMediaFilesToDB(companyId, uploadFileParamsDto, mergeFilePath, fileMd5, video_files);
if (mediaFiles == null) {
XueChengPlusException.cast("媒资文件入库出错");
}
log.debug("媒资文件入库完成");
return RestResponse.success();
} finally {
for (File chunkFile : chunkFiles) {
try {
chunkFile.delete();
} catch (Exception e) {
log.debug("临时分块文件删除错误:{}", e.getMessage());
}
}
try {
mergeFile.delete();
} catch (Exception e) {
log.debug("临时合并文件删除错误:{}", e.getMessage());
}
}
}
@Api(value = "大文件上传接口", tags = "大文件上传接口")
@RestController
public class BigFilesController {
@Autowired
private MediaFileService mediaFileService;
@ApiOperation(value = "文件上传前检查文件")
@PostMapping("/upload/checkfile")
public RestResponse checkFile(@RequestParam("fileMd5") String fileMd5) {
return mediaFileService.checkFile(fileMd5);
}
@ApiOperation(value = "分块文件上传前检查分块")
@PostMapping("/upload/checkchunk")
public RestResponse checkChunk(@RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") int chunk) {
return mediaFileService.checkChunk(fileMd5, chunk);
}
@ApiOperation(value = "上传分块文件")
@PostMapping("/upload/uploadchunk")
public RestResponse uploadChunk(@RequestParam("file") MultipartFile file, @RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") int chunk) throws Exception {
return mediaFileService.uploadChunk(fileMd5, chunk, file.getBytes());
}
@ApiOperation(value = "合并分块文件")
@PostMapping("/upload/mergechunks")
public RestResponse mergeChunks(@RequestParam("fileMd5") String fileMd5, @RequestParam("fileName") String fileName, @RequestParam("chunkTotal") int chunkTotal) throws IOException {
Long companyId = 1232141425L;
UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
uploadFileParamsDto.setFileType("001002");
uploadFileParamsDto.setTags("课程视频");
uploadFileParamsDto.setRemark("");
uploadFileParamsDto.setFilename(fileName);
return mediaFileService.mergeChunks(companyId, fileMd5, chunkTotal, uploadFileParamsDto);
}
}
@ApiOperation(value = "预览文件")
@GetMapping("/preview/{mediaId}")
public RestResponse getPlayUrlByMediaId(@PathVariable String mediaId) {
return null;
}
/**
* 将文件信息添加到文件表
*
* @param companyId 机构id
* @param uploadFileParamsDto 上传文件的信息
* @param objectName 对象名称
* @param fileMD5 文件的md5码
* @param bucket 桶
*/
@Transactional
public MediaFiles addMediaFilesToDB(Long companyId, UploadFileParamsDto uploadFileParamsDto, String objectName, String fileMD5, String bucket) {
// 保存到数据库
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.setBucket(bucket);
mediaFiles.setCreateDate(LocalDateTime.now());
mediaFiles.setStatus("1");
mediaFiles.setFilePath(objectName);
+ // 获取源文件名的contentType
+ String contentType = getContentType(objectName);
+ // 如果是图片格式或者mp4格式,则设置URL属性,否则不设置
+ if (contentType.contains("image") || contentType.contains("mp4")) {
+ mediaFiles.setUrl("/" + bucket + "/" + objectName);
+ }
// 查阅数据字典,002003表示审核通过
mediaFiles.setAuditStatus("002003");
}
int insert = mediaFilesMapper.insert(mediaFiles);
if (insert <= 0) {
XueChengPlusException.cast("保存文件信息失败");
}
return mediaFiles;
}