企业级微服务大项目实战《学成在线》【四】(媒资管理模块)

封面为啥要用苍穹外卖,想纪念下下以前的项目,不知道现在还跑得起来不哈哈哈哈~

上传图片

大部分都是源文档的东西,懒得写了~

流程:

课程图片上传至分布式文件系统,在课程信息中保存课程图片路径,如下流程:

image-20230309205459875

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

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

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

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

5、保存课程信息,在内容管理数据库保存图片地址。

image-20230309205533505

环境准备

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

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

image-20230309212926866

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

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

MinIO配置属性类

package com.xuecheng.media.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * @author woldier
 * @version 1.0
 * @description TODO
 * @date 2023/3/9 21:31
 **/
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinIOProperties {
}
 

Minio配置类

package com.xuecheng.media.config;

import io.minio.MinioClient;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author woldier
 * @version 1.0
 * @description TODO
 * @date 2023/3/9 21:33
 **/
@Configuration
@EnableConfigurationProperties({MinIOProperties.class})
@RequiredArgsConstructor
public class MinIOConfig {
    private final MinIOProperties minIOProperties;

    /**
    * @description 初始化MinIO客户端,并且交给spring管理
    *
    * @return io.minio.MinioClient
    * @author: woldier
    * @date: 2023/3/9 21:36
    */
    @Bean
    public MinioClient minioClient(){
        return MinioClient.builder()
                .endpoint("http://localhost:9000")
                .credentials("minioadmin", "minioadmin")
                .build();
    }
}
 

接口定义

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

首先分析接口:

请求地址:/media/upload/coursefile

请求参数:

Content-Type: multipart/form-data;boundary=.....

FormData: filedata=??

响应参数:文件信息,如下

{
  "id": "a16da7a132559daf9e1193166b3e7f52",
  "companyId": 1232141425,
  "companyName": null,
    "filename": "1.jpg",
  "fileType": "001001",
  "tags": "",
  "bucket": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg",
  "fileId": "a16da7a132559daf9e1193166b3e7f52",
  "url": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg",
  "timelength": null,
  "username": null,
  "createDate": "2022-09-12T21:57:18",
  "changeDate": null,
  "status": "1",
  "remark": "",
  "auditStatus": null,
  "auditMind": null,
  "fileSize": 248329

}
 

在media-model定义上传响应模型类:

package com.xuecheng.media.model.dto;

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

/**
 * @author woldier
 * @version 1.0
 * @description 上传文件响应结果类
 * @date 2023/3/9 21:55
 **/
public class UploadFileResultDto extends MediaFiles {
}
 

定义接口如下

/**
     * @param upload     表单数据
     * @param folder     文件夹名-非必须
     * @param objectName 文件名-非必须
     * @return com.xuecheng.media.model.dto.UploadFileResultDto
     * @description 文件上传
     * @author: woldier
     * @date: 2023/3/9 22:09
     */
    @ApiOperation("文件上传")
    @RequestMapping(value = "/upload/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    // 定义请求url与可消费的文件操作content-type类型
    public UploadFileResultDto upload(
            @RequestPart("filedata") MultipartFile upload,
            @RequestParam(value = "folder", required = false) String folder,
            @RequestParam(value = "objectName", required = false) String objectName) {
        return null;
    }

环境准备

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

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

image-20230309212926866

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

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

MinIO配置属性类

package com.xuecheng.media.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * @author woldier
 * @version 1.0
 * @description TODO
 * @date 2023/3/9 21:31
 **/
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinIOProperties {
}
 

Minio配置类

package com.xuecheng.media.config;

import io.minio.MinioClient;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author woldier
 * @version 1.0
 * @description TODO
 * @date 2023/3/9 21:33
 **/
@Configuration
@EnableConfigurationProperties({MinIOProperties.class})
@RequiredArgsConstructor
public class MinIOConfig {
    private final MinIOProperties minIOProperties;

    /**
    * @description 初始化MinIO客户端,并且交给spring管理
    *
    * @return io.minio.MinioClient
    * @author: woldier
    * @date: 2023/3/9 21:36
    */
    @Bean
    public MinioClient minioClient(){
        return MinioClient.builder()
                .endpoint("http://localhost:9000")
                .credentials("minioadmin", "minioadmin")
                .build();
    }
}
 

接口定义

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

首先分析接口:

请求地址:/media/upload/coursefile

请求参数:

Content-Type: multipart/form-data;boundary=.....

FormData: filedata=??

响应参数:文件信息,如下

{
  "id": "a16da7a132559daf9e1193166b3e7f52",
  "companyId": 1232141425,
  "companyName": null,
    "filename": "1.jpg",
  "fileType": "001001",
  "tags": "",
  "bucket": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg",
  "fileId": "a16da7a132559daf9e1193166b3e7f52",
  "url": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg",
  "timelength": null,
  "username": null,
  "createDate": "2022-09-12T21:57:18",
  "changeDate": null,
  "status": "1",
  "remark": "",
  "auditStatus": null,
  "auditMind": null,
  "fileSize": 248329

}
 

在media-model定义上传响应模型类:

package com.xuecheng.media.model.dto;

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

/**
 * @author woldier
 * @version 1.0
 * @description 上传文件响应结果类
 * @date 2023/3/9 21:55
 **/
public class UploadFileResultDto extends MediaFiles {
}
 

定义接口如下

/**
     * @param upload     表单数据
     * @param folder     文件夹名-非必须
     * @param objectName 文件名-非必须
     * @return com.xuecheng.media.model.dto.UploadFileResultDto
     * @description 文件上传
     * @author: woldier
     * @date: 2023/3/9 22:09
     */
    @ApiOperation("文件上传")
    @RequestMapping(value = "/upload/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    // 定义请求url与可消费的文件操作content-type类型
    public UploadFileResultDto upload(
            @RequestPart("filedata") MultipartFile upload,
            @RequestParam(value = "folder", required = false) String folder,
            @RequestParam(value = "objectName", required = false) String objectName) {
        return null;
    }

controller层中我们需要将文件暂存在本地,让后将临时文件的地址放进服务层方法参数中

接口开发

DAO开发

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

Service开发

为了使代码更具有可读性,我们创建了两个枚举工具类,用于区分数据库字段值

image-20230310165611318

可以看到我们操作mediaFile表时需要用到这两种字段,因此我们使用枚举简化

在meida-model的dto包下创建如下两个枚举类

package com.xuecheng.media.model.dto;

/**
 * @author woldier
 * @version 1.0
 * @description 媒体资源类型枚举
 * [{"code":"001001","desc":"图片"},{"code":"001002","desc":"视频"},{"code":"001003","desc":"其它"}]
 * @date 2023/3/10 15:45
 **/
public enum MediaResourceType {
    IMAGE("001001","图片"),
    VIDEO("001002","视频"),
    OTHER("001003","其它")
    ;

    private String code;
    private String description;

    MediaResourceType(String code, String description) {
        this.code = code;
        this.description = description;
    }

    public String getCode() {
        return code;
    }
}
 
package com.xuecheng.media.model.dto;

/**
 * @author woldier
 * @version 1.0
 * @description 审核状态
 * [{"code":"002001","desc":"审核未通过"},
 * {"code":"002002","desc":"未审核"},
 * {"code":"002003","desc":"审核通过"}]
 * @date 2023/3/10 14:55
 **/
public enum MediaAuditStatus {

    NOT_Approved("002001","审核未通过"),

    Not_Audited("002002","未审核"),
    Approved("002003","审核通过");

    private String code;
    private String description;

     MediaAuditStatus(String code, String description){
        this.code = code;
        this.description = description;

    }

    public String getCode() {
        return code;
    }
}
 

除此之外由于我们操作的数据表有公共字段,updateTime,createTime。因此我们可以加入一个mp的自动填充功能。

在media-service的config包下创建如下类

package com.xuecheng.media.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * <P>
 * 		Mybatis-Plus 配置
 * </p>
 */
@Configuration
@MapperScan("com.xuecheng.media.mapper")
public class MybatisPlusConfig {
	/**
	 * 新的分页插件
	 * 需要设置 MybatisConfiguration#useDeprecatedExecutor = false
	 * 避免缓存出现问题(该属性会在旧插件移除后一同移除)
	 */
	@Bean
	public MybatisPlusInterceptor mybatisPlusInterceptor() {
		MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
		interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
		return interceptor;
	}


}
 

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

定义请求参数类:

package com.xuecheng.media.model.dto;

import lombok.Data;

/**
 * @author woldier
 * @version 1.0
 * @description 文件上传通用参数dto,这里的大部分都来自与 MediaFiles
 * @date 2023/3/9 22:14
 **/
@Data
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;

}
 

定义service方法:

com.xuecheng.media.service.MediaFileService

/**
     * @param companyId           公司ID
     * @param uploadFileParamsDto 上传文件参数类
     * @param LocalFilePath       要上传的文件其本地路径
     * @return com.xuecheng.media.model.dto.UploadFileResultDto
     * @description 上传文件
     * @author: woldier
     * @date: 2023/3/10 13:36
     */
    UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String LocalFilePath) throws XueChengPlusException;
    
    /**
    * @description 将上传的文件插入数据库 
    * @param companyId 
     * @param uploadFileParamsDto 
     * @param md5 
     * @param bucket 
     * @param objectName  
    * @return com.xuecheng.media.model.po.MediaFiles 
    * @author: woldier 
    * @date: 2023/3/10 16:42
    */
     MediaFiles insertMediaFile2DB(Long companyId, UploadFileParamsDto uploadFileParamsDto, String md5, String bucket,String objectName);
 

com.xuecheng.media.service.impl.MediaFileServiceImpl

/**
     * @param companyId           公司ID
     * @param uploadFileParamsDto 上传文件参数类
     * @param localFilePath       要上传的文件其本地路径
     * @return com.xuecheng.media.model.dto.UploadFileResultDto
     * @description 上传文件
     * @author: woldier
     * @date: 2023/3/10 13:36
     */
    @Override
    public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath) throws XueChengPlusException {
        /*
         * 1.上传文件到minio ,文件路径为 /{桶名}/{年}/{月}/{日}/
         * 2.插入数据库
         * */
        /*通过扩展名获取媒体资源类型*/
        String mimeType = getMimeType(uploadFileParamsDto.getFilename());
        /*组装文件基路径*/
        String basePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd/"));
        /*由于Minio中同一天的数据保存位置都在同一个文件夹下,为了防止重名现象,不能用原文件名,应该用md5值加后缀*/
        int index = uploadFileParamsDto.getFilename().lastIndexOf(".");
        //文件后缀
        String fileSuffix = uploadFileParamsDto.getFilename().substring(index);
        //文件md5值
        String md5 = getMd5(localFilePath);
        //拼接得到Minio存储路径
        String objectName = basePath + md5 + fileSuffix;
        //上传到minio
        boolean minIOUpload = minIOUpload(localFilePath, mimeType, fileBucket, objectName);
        if (!minIOUpload) XueChengPlusException.cast("MinIO上传出错");
        //上传到数据库
        MediaFiles files = insertMediaFile2DB(companyId, uploadFileParamsDto, md5, fileBucket, objectName);
//        MediaFileService proxy = (MediaFileService)AopContext.currentProxy();
//        MediaFiles files = proxy.insertMediaFile2DB(companyId, uploadFileParamsDto, md5, fileBucket,objectName);
        //结果为空表示上传失败
        if (files == null) XueChengPlusException.cast("文件上传后保存信息到数据库失败");
        UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
        BeanUtils.copyProperties(files, uploadFileResultDto);

        return uploadFileResultDto;
    }

    /**
     * @param companyId           公司id
     * @param uploadFileParamsDto 上传参数信息
     * @param md5                 md5
     * @param bucket              桶
     * @param objectName          对象名
     * @return com.xuecheng.media.model.po.MediaFiles
     * @description 插入数据库
     * @author: woldier
     * @date: 2023/3/10 15:21
     */
    @Transactional
    public MediaFiles insertMediaFile2DB(Long companyId, UploadFileParamsDto uploadFileParamsDto, String md5, String bucket, String objectName) {
        /*添加数据库之前,根据md5查询该文件是否已经存在*/
        MediaFiles files = mediaFilesMapper.selectById(md5);
        if (files == null) {
            /*生成数据库entity*/
            MediaFiles mediaFiles = new MediaFiles();
            BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);
            //设置uploadFileParamsDto中不存在的部分
            //设置id
            mediaFiles.setId(md5);
            //机构id
            mediaFiles.setCompanyId(companyId);
            //bucket
            mediaFiles.setBucket(bucket);
            //存储路径
            mediaFiles.setFilePath(objectName);
            //file_id
            mediaFiles.setFileId(md5);
            //url
            mediaFiles.setUrl("/" + bucket + "/" + objectName);
            //上传时间,更新时间自动设置
            //文件状态
            mediaFiles.setStatus("1");
            //审核状态
            mediaFiles.setAuditStatus(MediaAuditStatus.Approved.getCode());

            int insert = mediaFilesMapper.insert(mediaFiles);

            if (insert <= 0) {
                log.debug("向数据库保存文件失败,bucket:{},objectName{}", fileBucket, objectName);
                return null;
            }
            return mediaFiles;
        }
        return files;
    }

    @NotNull
    private static String getMd5(String localFilePath) throws XueChengPlusException {
        String md5 = null;
        try {
            md5 = DigestUtils.md5Hex(Files.newInputStream(new File(localFilePath).toPath()));
        } catch (IOException e) {
            XueChengPlusException.cast("md5计算时出错");
        }
        return md5;
    }

    /**
     * @param fileName 带后缀文件名
     * @return java.lang.String
     * @description 根据文件后缀名获取MimeType
     * @author: woldier
     * @date: 2023/3/10 13:55
     */
    private String getMimeType(String fileName) {
        if (fileName == null) fileName = "";
        ContentInfo contentInfo = ContentInfoUtil.findExtensionMatch(fileName);
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        if (contentInfo != null) mimeType = contentInfo.getMimeType();
        return mimeType;
    }

    /**
     * @param localFilePath 本地文件路径
     * @param fileType      文件类型
     * @param bucket        桶名称
     * @return boolean
     * @description 上传文件到MinIO的方法
     * @author: woldier
     * @date: 2023/3/10 13:33
     */
    private boolean minIOUpload(String localFilePath, String fileType, String bucket, String objectName) {

        /*上传*/
        try {
            minioClient.uploadObject(
                    UploadObjectArgs.builder()
                            .bucket(bucket)  //桶
                            .object(objectName) // 对象名,在桶下存储的文件
                            .filename(localFilePath)  //指定本地文件路径
                            .contentType(fileType) //设置媒体文件类型
                            .build()
            );
        } catch (Exception e) {
            log.error("文件上传到MinIO出错,buckcet:{},path:{},error:{}", bucket, objectName, e.getMessage());
            e.printStackTrace();
            return false;
        }
        return true;

    }
 
接口代码完善
 /**
     * @param upload     表单数据
     * @param folder     文件夹名-非必须
     * @param objectName 文件名-非必须
     * @return com.xuecheng.media.model.dto.UploadFileResultDto
     * @description 文件上传
     * @author: woldier
     * @date: 2023/3/9 22:09
     */
    @ApiOperation("文件上传")
    @RequestMapping(value = "/upload/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    // 定义请求url与可消费的文件操作content-type类型
    public UploadFileResultDto upload(
            @RequestPart("filedata") MultipartFile upload,
            @RequestParam(value = "folder", required = false) String folder,
            @RequestParam(value = "objectName", required = false) String objectName) throws IOException, XueChengPlusException {
        /*
        *对接受到的文件进行处理
         */
        /*产生一个临时文件*/
        File tempFile = File.createTempFile("minio", "temp");
        /*将请求中的表单数据拷贝到临时文件中*/
        upload.transferTo(tempFile);
        /*获取绝对路径*/
        String absolutePath = tempFile.getAbsolutePath();

        /*
        *对上传参数进行处理
         */

        //上传文件参数类
        UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
        //原始文件名称
        uploadFileParamsDto.setFilename(upload.getOriginalFilename());
        //文件大小
        uploadFileParamsDto.setFileSize(upload.getSize());
        //文件类型
        uploadFileParamsDto.setFileType(MediaResourceType.IMAGE.getCode());
        

        /*
        * 公司id获取
        * */
        //TODO 硬编码公司id
        Long companyId = 123456789L;

        UploadFileResultDto uploadFileResultDto = mediaFileService.uploadFile(companyId, uploadFileParamsDto, absolutePath);

        /*删除临时文件*/
        tempFile.deleteOnExit();
        return uploadFileResultDto;
 

 service事务代码优化

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

目前是在uploadFile方法上添加@Transactional,当调用uploadFile方法前会开启数据库事务,如果上传文件过程时间较长那么数据库的事务持续时间就会变长,这样数据库链接释放就慢,最终导致数据库链接不够用。

我们只将addMediaFilesToDb方法添加事务控制即可,uploadFile方法上的@Transactional注解去掉。(上小节代码已经时这样做的)

但是现在的问题是,controller调用的service方法upload没用加入事务注解,相当于在service中一个没有事务的方法调用了另一个事务方法,事务不生效

下边分析原因:

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

image-20230310170730673

如果在uploadFile方法上没有@Transactional注解,代理对象执行此方法前

不进行事务控制,如下图:

image-20230310170810020

现在在addMediaFilesToDb方法上添加@Transactional注解,也不会进行事务是因为并不是通过代理对象执行的addMediaFilesToDb方法。为了判断在uploadFile方法中去调用addMediaFilesToDb方法是否是通过代理对象去调用,我们可以打断点跟踪。

image-20230310170826985

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

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

我们先获取代理对象,然后调用代理对象的insertMediaFile2DB方法

MediaFileService proxy = (MediaFileService)AopContext.currentProxy();
MediaFiles files = proxy.insertMediaFile2DB(companyId, uploadFileParamsDto, md5, fileBucket,objectName);
 

但是只修改这个代码启动会报错

Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.
 

我们需要在启动类下加入注解并且设置exposeProxy属性,除此之外可能的报错原因是没有加入包

@EnableAspectJAutoProxy(exposeProxy = true)
 
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>
 

我们可以在代码中插入数据库后除0模拟错误,看一下是否进行了数据库回归,经过测试可以发现进行了回滚

image-20230310172853142

 接口测试

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

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

< d:/123.jpg
 
POST http://localhost:63010/media//upload/coursefile

HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: application/json
Date: Fri, 10 Mar 2023 08:33:46 GMT

{
  "id": "8a58662af30ace3e83f629a10ddd8662",
  "companyId": 123456789,
  "companyName": null,
  "filename": "1.jpg",
  "fileType": "001001",
  "tags": null,
  "bucket": "mediafiles",
  "filePath": "2023/03/10/8a58662af30ace3e83f629a10ddd8662.jpg",
  "fileId": "8a58662af30ace3e83f629a10ddd8662",
  "url": "/mediafiles/2023/03/10/8a58662af30ace3e83f629a10ddd8662.jpg",
  "username": null,
  "createDate": null,
  "changeDate": null,
  "status": "1",
  "remark": null,
  "auditStatus": "002003",
  "auditMind": null,
  "fileSize": 9778
}

上传视频

 

posted @ 2024-02-03 02:51  何平安  阅读(53)  评论(0编辑  收藏  举报
浏览器标题切换end