4.3 媒资管理模块 - Minio系统上传图片与视频

news2024/9/29 11:35:39

文章目录

  • 一、上传图片
    • 1.1 需求分析
    • 1.2 数据模型
      • 1.2.1 media_files 媒资信息表
    • 1.3 准备Minio环境
      • 1.3.1 桶环境
      • 1.3.2 连接Minio参数
      • 1.3.3 Minio配置类
    • 1.4 接口定义
      • 1.4.1 上传图片接口请求参数
      • 1.4.2 上传图片接口返回值
      • 1.4.3 接口代码
    • 1.5 MediaFilesMapper
    • 1.6 MediaFileServiceImpl
    • 1.7 事物优化
    • 1.8 测试
  • 二、上传视频
    • 2.0 响应结果类
    • 2.1 断点续传
    • 2.2 测试Demo
      • 2.2.1 分块测试
      • 2.2.2 合并测试
    • 2.3 测试MinIo
      • 2.3.1 上传分块
      • 2.3.2 合并分块
    • 2.4 上传视频
      • 2.4.1 文件上传前校验
        • 2.4.1.1 BigFilesController
        • 2.4.1.2 MediaFileServiceImpl
      • 2.4.2 上传分块文件
        • 2.4.2.1 BigFilesController
        • 2.4.2.2 MediaFileServiceImpl
      • 2.4.3 合并分块文件
        • 2.4.3.1 BigFilesController
        • 2.4.3.2 MediaFileServiceImpl
      • 2.4.4 测试

一、上传图片

此模块涉及到修改/添加课程,之前已经写过接口了,在下面这篇文章中

3.2 内容管理模块 - 课程分类、新增课程、修改课程-CSDN博客

1.1 需求分析

我们的文件要存储在minio文件系统中

在新增课程界面上传课程图片,也可以修改课程图片

这一步上传图片是请求的媒资服务接口,并不是内容管理服务接口

image-20231222215414543

在课程基本信息中有一个“pic”字段,要存储图片的路径

image-20231222215700731

pic字段值类似“/mediafiles/2022/09/18/a16da7a132559daf9e1193166b3e7f52.jpg”,其中“mediafiles”就是桶,后面就是文件具体的对象名

在我们前端功能.env环境变量配置文件定义了图片服务器地址

如果想要展示图片,那就要将“http://192.168.101.65:9000”拼接上pic字段值

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为什么不将“http://192.168.101.65:9000”直接写在数据库里

因为如果上线之后,地址就不好修改了


为什么要单独设置一个媒资管理服务

①管理文件的信息,我们使用media_files表记录文件信息

不管是图片、视频、文档,全会记录在这个表中

image-20231222223702229

②把文件本身放在分布式文件系统(minio)中管理起来

方便管理,媒资服务就是一个统一的文件管理功能


详细流程

image-20231222223956542

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

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

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

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

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

1.2 数据模型

我们怎么区分文件是否已经上传

可以通过文件的MD5值,同一个文件的MD5值相同

前端上传的时候就会把文件MD5值传给我们后台,后台拿着MD5值去数据库查询,如果有这个MD5值了,说明文件已经上传成功了,不需要再上传了

1.2.1 media_files 媒资信息表

image-20231222224302847

file_path存储路径,相当于java代码中的对象名

url:可以通过http访问文件的路径

url = /bucket/+file_path

但是有例外,如果是上传了一个avi视频的话,那file_path里面存的就是avi结尾的视频地址,但是avi的视频最终是不能播放的,我们需要把这个视频进行转码生成MP4格式的视频,此时url就是存储的MP4的视频地址、

如果是图片的话url = /bucket/+file_path这个等式是一个样子的

/**
 * 媒资信息
 */
@Data
@TableName("media_files")
public class MediaFiles implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private String id;

    /**
     * 机构ID
     */
    private Long companyId;

    /**
     * 机构名称
     */
    private String companyName;

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

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

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

    /**
     * 存储目录
     */
    private String bucket;

    /**
     * 存储路径
     */
    private String filePath;


    /**
     * 文件标识
     */
    private String fileId;

    /**
     * 媒资文件访问地址
     */
    private String url;


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

    /**
     * 上传时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createDate;

    /**
     * 修改时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime changeDate;

    /**
     * 状态,1:未处理,视频处理完成更新为2
     */
    private String status;

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

    /**
     * 审核状态
     */
    private String auditStatus;

    /**
     * 审核意见
     */
    private String auditMind;

    /**
     * 文件大小
     */
    private Long fileSize;

}

除此之外还会涉及到课程基本信息表course_base,表中pic字段存储课程图片的路径

image-20231222215700731

1.3 准备Minio环境

1.3.1 桶环境

  • mediafiles桶

    视频以外的文件都存储在这个桶内

image-20231222230701829

  • video 桶

    存放视频

    image-20231222230711368

并且两个桶的Access Policy权限都要改成public

image-20231222230808165

1.3.2 连接Minio参数

在media-service工程中配置minio的相关信息,要配置在nacos中

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.101.65:3306/xcplus_media?serverTimezone=UTC&userUnicode=true&useSSL=false&
    username: root
    password: mysql
  cloud:
   config:
    #配置本地优先
    override-none: true  

minio:
  #地址
  endpoint: http://192.168.101.65:9000 
  #账号
  accessKey: minioadmin 
  #密码
  secretKey: minioadmin 
  #两个桶
  bucket:
    files: mediafiles 
    videofiles: video

本地bootstrap.yaml文件

spring:
  application:
    name: media-service
  cloud:
    nacos:
      server-addr: 192.168.101.65: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

1.3.3 Minio配置类

在media-service工程中进行配置

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

    @Bean
    public MinioClient minioClient() {

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

1.4 接口定义

下面的内容其实是完成标红的地方

image-20231222235649629

做这一部分的时候可能发现course_base课程基本信息表和media_files表没有关联,那怎么将图片的url存放到course_base课程基本信息表中的pic字段呢?

当上传完图片之后,会点击保存,这个时候就会对course_base表中的各种信息进行操作了

image-20231223214409515

但是我感觉没有很好的方法让course_base表

1.4.1 上传图片接口请求参数

和media_file相关的文件上传的基本信息

/**
 * @description 上传普通文件请求参数
 */
@Data
public class UploadFileParamsDto {

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


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

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

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

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

}

1.4.2 上传图片接口返回值

这些信息其实是文件表中的信息

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

根据上面返回信息封装一个Dto

但是我们不会直接使用media_files的实体类,假如前端多需要几个参数的话,我们还需要修改

在media-model工程中添加如下实体类

@Data
public class UploadFileResultDto extends MediaFiles {

}

1.4.3 接口代码

    /**
     * 对于请求内容:Content-Type: multipart/form-data;
     * 前端向后端传输一个文件,那后端程序就属于一个消费者,我们指定一下类型
     * form-data; name="filedata"; filename="具体的文件名称"
     * <p>
     * 我们可以使用@RequestPart指定一下前端向后端传输文件的名称
     * 用MultipartFile类型接收前端向后端传输的文件
     */
    @ApiOperation("上传图片")
    @PostMapping(value = "/upload/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile filedata) throws IOException {
        //此时已经接收到文件了,目前作为临时文件存储在内存中
        //1.创建一个临时文件,前缀是"minio",后缀是“.temp”
        File tempFile = File.createTempFile("minio", ".temp");
        //2.将上传后的文件传输到临时文件中
        filedata.transferTo(tempFile);
        //3.取出临时文件的绝对路径
        String localFilePath = tempFile.getAbsolutePath();

        Long companyId = 1232141425L; //先写死,写认证授权系统时再进行
        //4.准备上传文件的信息
        UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
        //filedata.getOriginalFilename()获取原始文件名称
        uploadFileParamsDto.setFilename(filedata.getOriginalFilename());
        //文件大小
        uploadFileParamsDto.setFileSize(filedata.getSize());
        //文件类型 001001在数据字典中代表图片
        uploadFileParamsDto.setFileType("001001");
        //调用service上传图片
        return mediaFileService.uploadFile(companyId, uploadFileParamsDto, localFilePath);
    }

1.5 MediaFilesMapper

我们需要保存文件信息

/**
 * 媒资信息 Mapper 接口
 */
public interface MediaFilesMapper extends BaseMapper<MediaFiles> {

}

1.6 MediaFileServiceImpl

媒资管理业务类 - 下面的代码其实就做了上传图片与文件信息入库

    @Autowired
    private MinioClient minioClient;

    //除了视频文件以外的桶
    @Value("${minio.bucket.files}")
    private String bucket_medialFiles;

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

 /**
     * 上传文件
     *
     * @param companyId           机构id
     * @param uploadFileParamsDto 上传文件信息
     * @param localFilePath       文件磁盘路径
     * @return 文件信息
     */
    @Transactional
    @Override
    public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath) {
        //TODO 1.将文件上传到Minio

        //TODO 1.1 获取文件扩展名
        String filename = uploadFileParamsDto.getFilename();
        String extension = filename.substring(filename.lastIndexOf("."));

        //TODO 1.2 根据文件扩展名获取mimeType
        String mimeType = this.getMimeType(extension);

        //TODO 1.3 bucket,从nacos中读取
        //TODO 1.4 ObjectName约定在MinIo系统中存储的目录是年/月/日/图片文件
        //得到文件的路径defaultFolderPath
        String defaultFolderPath = this.getDefaultFolderPath();
        //最终存储的文件名是MD5值
        String fileMd5 = this.getFileMd5(new File(localFilePath));
        String ObjectName = defaultFolderPath + fileMd5 + extension;
        //TODO 1.5 上传文件到Minio
        boolean result = this.addMediaFilesToMinIO(localFilePath, bucket_medialFiles, ObjectName, mimeType);

        if (!result) {
            XueChengPlusException.cast("上传文件失败");
        }
        //TODO 2.将文件信息保存到数据库
        MediaFiles mediaFiles = this.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_medialFiles, ObjectName);
        if (mediaFiles == null){
            XueChengPlusException.cast("文件上传后保存信息失败");
        }
        UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
        BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
        return uploadFileResultDto;
    }

    /**
     * @param companyId           机构id
     * @param fileMd5             文件md5值
     * @param uploadFileParamsDto 上传文件的信息
     * @param bucket              桶
     * @param objectName          对象名称
     * @return com.xuecheng.media.model.po.MediaFiles
     * @description 将文件信息添加到文件表
     */

    public MediaFiles addMediaFilesToDb(Long companyId, String fileMd5, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName) {
        //根据文件MD5值向数据库查找文件信息
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        if (mediaFiles == null) {
            mediaFiles = new MediaFiles();
            BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);
            mediaFiles.setId(fileMd5);//文件信息的主键是文件的MD5值
            mediaFiles.setCompanyId(companyId);//机构ID
            mediaFiles.setBucket(bucket);//桶
            mediaFiles.setFilePath(objectName);//对象名
            mediaFiles.setFileId(fileMd5);//file_id字段
            mediaFiles.setUrl("/" + bucket + "/" + objectName);//url
            mediaFiles.setCreateDate(LocalDateTime.now());//上传时间
            mediaFiles.setStatus("1");//状态 1正常 0不展示
            mediaFiles.setAuditStatus("002003");//审核状态 002003审核通过
            int insert = mediaFilesMapper.insert(mediaFiles);
            if (insert <= 0) {
                log.debug("向数据库保存文件失败bucket:{},objectName:{}", bucket, objectName);
            }
        }
        return mediaFiles;
    }


    /**
     * 将文件上传到MinIo
     *
     * @param bucket        桶
     * @param localFilePath 文件在本地的路径
     * @param objectName    上传到MinIo系统中时的文件名称
     * @param mimeType      上传的文件类型
     */
    private boolean addMediaFilesToMinIO(String localFilePath, String bucket, String objectName, String mimeType) {
        try {
            UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                    //桶,也就是目录
                    .bucket(bucket)
                    //指定本地文件的路径
                    .filename(localFilePath)
                    //上传到minio中的对象名,上传的文件存储到哪个对象中
                    .object(objectName)
                    .contentType(mimeType)
                    //构建
                    .build();
            minioClient.uploadObject(uploadObjectArgs);
            log.debug("上传文件到minio成功,bucket:{},objectName:{}", bucket, objectName);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            log.info("上传文件出错,bucket:{},objectName:{},错误信息:{}", bucket, objectName, e.getMessage());
            return false;
        }
    }


    /**
     * 根据扩展名获取mimeType
     *
     * @param extension 扩展名
     */
    private String getMimeType(String extension) {
        if (extension == null) {
            //目的是防止空指针异常
            extension = "";
        }
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
        //通用mimeType,字节流
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        if (extensionMatch != null) {
            mimeType = extensionMatch.getMimeType();
        }
        return mimeType;
    }

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


    /**
     * 获取文件的md5
     *
     * @param file 文件
     * @return MD5值
     */
    private String getFileMd5(File file) {
        try (FileInputStream fileInputStream = new FileInputStream(file)) {
            return DigestUtils.md5Hex(fileInputStream);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

1.7 事物优化

在下面这篇文章中也有事物的方法,感兴趣的话可以查看一下1.6

Redis - 优惠券秒杀、库存超卖、分布式锁、Redisson

我在uploadFile上传文件的方法中加了@Transactional方法

也就是说在uploadFile方法执行之前就会开启事物

但是addMediaFilesToMinIO方法会通过网络访问Minio分布式文件系统上传文件,但是通过网络访问的话这个时间可长可短,假如说网络访问时间长的话,事物的时间就会比较长,占用数据库资源的时间就会长,极端情况下会导致数据库的连接不够用

所在当方法中有网络访问的话,千万不要使用数据库的@Transactional事物进行控制

所以此时我们在addMediaFilesToDb方法上添加@Transactional控制事物的方法就好了


public MediaFiles addMediaFilesToDb(Long companyId, String fileMd5, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName) {
....
}

但是现在也会出现一个问题,我们在uploadFile方法中调用addMediaFilesToDb方法的时候并不会触发addMediaFilesToDb方法上的@Transactional,这种情况也是事物失效的场景之一

分析一下这种情况出现的原因

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

image-20231223224851440

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

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

image-20231223224941165

在addMediaFilesToDb方法上添加@Transactional注解,也不会进行事务控制是因为并不是通过代理对象执行的addMediaFilesToDb方法

如下图所示,在addMediaFilesToDb方法中的this指代的并不是代理对象,uploadFile方法中this指代的也不是代理对象

我们在Controller使用@AutoWired的注解引入的MediaFileService对象是代理对象,怎么在调用的时候就不是了呢?

在MediaFileServiceImpl类中this指代的就是当前类,不是代理对象

代理对象其实是在原始对象上包了一层,然后调用原始对象上的方法而已

image-20231223225251724

为了解决这个问题,我们可以在uploadFile方法中使用代理的方式调用addMediaFilesToDb方法,使其能触发@Transactional注解

不同担心循环依赖的问题,spring有三级缓存

@Autowired
MediaFileService currentProxy;

将addMediaFilesToDb方法提成接口

/**
 * @description 将文件信息添加到文件表
 * @param companyId  机构id
 * @param fileMd5  文件md5值
 * @param uploadFileParamsDto  上传文件的信息
 * @param bucket  桶
 * @param objectName 对象名称
 * @return com.xuecheng.media.model.po.MediaFiles
 */

public MediaFiles addMediaFilesToDb(Long companyId, String fileMd5, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName);

调用addMediaFilesToDb方法的代码处改为如下

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

此时就是通过CGLB生成的代理对象,挺完美的

image-20231223231025237

1.8 测试

测试上传文件

image-20231223210201133

image-20231223210009088

二、上传视频

上传视频的位置如下所示

image-20231223231511151

image-20231223232117836

2.0 响应结果类

/**
 * 通用结果类
 */
@Data
@ToString
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;
    }

    /**
     * 错误信息的封装
     *
     * @param msg
     * @param <T>
     * @return
     */
    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;
    }


    /**
     * 添加正常响应数据(包含响应内容)
     *
     * @return RestResponse Rest服务封装相应数据
     */
    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;
    }

    /**
     * 添加正常响应数据(不包含响应内容)
     *
     * @return RestResponse Rest服务封装相应数据
     */
    public static <T> RestResponse<T> success() {
        return new RestResponse<T>();
    }


    public Boolean isSuccessful() {
        return this.code == 0;
    }
}

2.1 断点续传

假如说用户上传了一个大文件,但是用户的网不是很好,上传了一部分网断了,这个时候就需要一个断点续传的需求了

通常视频文件都比较大,所以对于媒资系统上传文件的需求要满足大文件的上传要求。

http协议本身对上传文件大小没有限制,但是客户的网络环境质量、电脑硬件环境等参差不齐,如果一个大文件快上传完了网断了没有上传完成,需要客户重新上传,用户体验非常差,所以对于大文件上传的要求最基本的是断点续传

断点续传:在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载,断点续传可以提高节省操作时间,提高用户体验性

image-20231224012021229

流程如下

1、前端对文件进行分块

2、前端上传分块文件前请求媒资服务检查文件是否存在,如果已经存在则不再上传

3、如果分块文件不存在则前端开始上传

4、前端请求媒资服务上传分块

5、媒资服务将分块上传至MinIO

6、前端将分块上传完毕请求媒资服务合并分块

7、媒资服务判断分块上传完成则请求MinIO合并文件

8、合并完成校验合并后的文件是否完整,如果不完整则删除文件

2.2 测试Demo

2.2.1 分块测试

/**
 * 测试分块
 */
@Test
public void testChunk() throws Exception {
    //TODO 1.获取源文件
    File sourceFile = new File("E:\\歌.mp4");

    //TODO 2.定义基本参数
    //2.1 分块文件存储路径
    String chunkFilePath = "E:\\chunk\\";
    //2.2 分块文件的大小 1024*1024*1 代表1M,5M的话乘5即可(也就是最小单位是字节byte 1024个byte是1k)
    int chunkSize = 1024 * 1024 * 1;
    //2.3 分块文件大小
    //Math.ceil表示向上取整
    //sourceFile.length()是获取文件的大小是多少byte字节
    int chunkNum = (int) Math.ceil((sourceFile.length() * 1.0) / chunkSize);

    //TODO 3.从源文件中读数据,向分块文件中写数据
    //RandomAccessFile流既可以读又可以写
    //参数一:File类型  参数二:是读(“r”)还是写"rw"
    RandomAccessFile raf_r = new RandomAccessFile(sourceFile, "r");
    //缓存区,1k
    byte[] bytes = new byte[1024];
    //TODO 3.1 创建分块文件
    for (int i = 0; i < chunkNum; i++) {
        File chunkFile = new File(chunkFilePath + i);
        //分块文件写入流
        RandomAccessFile raf_rw = new RandomAccessFile(chunkFile, "rw");
        int len = -1;
        //将数据读取到缓冲区中raf_r.read(bytes)
        while ((len = raf_r.read(bytes)) != -1) {
            //向临时文件中进行写入
            raf_rw.write(bytes, 0, len);
            //如果分块文件chunkFile的大小大于等于我们规定的分块文件的chunkSize大小,就不要再继续了
            if (chunkFile.length() >= chunkSize) {
                break;
            }
        }
        raf_rw.close();
    }
    raf_r.close();
}

image-20231224003333071

2.2.2 合并测试

/**
 * 测试合并
 */
@Test
public void testMerge() throws Exception {
    //TODO 1.基本参数
    //分块文件目录
    File chunkFolder = new File("E:\\chunk\\");
    //源文件
    File sourceFile = new File("E:\\歌.mp4");
    //合并后的文件在哪里
    File mergeFile = new File("E:\\chunk\\歌Copy.mp4");

    //TODO 2.取出所有分块文件,此时的顺序可能是无序的
    File[] files = chunkFolder.listFiles();
    //将数组转换成List
    List<File> fileList = Arrays.asList(files);
    //利用Comparator进行排序
    Collections.sort(fileList, new Comparator<File>() {
        @Override
        public int compare(File o1, File o2) {
            //升序
            return Integer.parseInt(o1.getName()) - Integer.parseInt(o2.getName());
        }
    });

    //TODO 3.合并分块文件
    //缓存区,1k
    byte[] bytes = new byte[1024];

    //向合并分块的流
    RandomAccessFile raf_rw = new RandomAccessFile(mergeFile, "rw");
    for (File file : fileList) {
        //向读取分块文件
        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();

    //TODO 校验是否合并成功
    //合并文件完成后比对合并的文件MD5值域源文件MD5值
    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("合并文件失败");
    }
}

image-20231224010242972

image-20231224010056328

2.3 测试MinIo

image-20231224012021229

MinioClient minioClient =
        MinioClient.builder()
                //这个地方是运行minio后展示的地址
                .endpoint("http://192.168.101.65:9000")
                //账号和密码
                .credentials("minioadmin", "minioadmin")
                .build();

2.3.1 上传分块

将分块文件上传至MinIo

/**
 * 将分块文件上传到minio
 */
@Test
public void uploadChunk() throws Exception {
    //获取所有的分块文件
    File file = new File("E:\\chunk\\");
    File[] files = file.listFiles();
    for (File chunkFile : files) {
        minioClient.uploadObject(
                UploadObjectArgs.builder()
                        //桶,也就是目录
                        .bucket("testbucket")
                        //指定本地文件的路径
                        .filename(chunkFile.getAbsolutePath())
                        //对象名
                        .object("chunk/"+chunkFile.getName())
                        //构建
                        .build());
        System.out.println("上传分块"+chunkFile.getName()+"成功");
    }
}

image-20231224015333814

2.3.2 合并分块

分块已经在Minio了,我们调用Minio提供的SDK即可合并分块

/**
 * 调用minio接口合并分块
 */
@Test
public void merge() throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
    //分块文件集合
    List<ComposeSource> sources = new ArrayList<>();
    sources.add(ComposeSource.builder()
            //分块文件所在桶
            .bucket("testbucket")
            //分块文件名称
            .object("chunk/0")
            .build());
    sources.add(ComposeSource.builder()
            //分块文件所在桶
            .bucket("testbucket")
            //分块文件名称
            .object("chunk/1")
            .build());

    ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
            //指定合并后文件在哪个桶里
            .bucket("testbucket")
            //最终合并后的文件路径及名称
            .object("merge/merge01.mp4")
            //指定分块源文件
            .sources(sources)
            .build();
    //合并分块
    minioClient.composeObject(composeObjectArgs);
}

出现这个文件说明我们分块的文件太小了,要大于5242880byte,也就是5M,我们上传的文件才1048576byte,也就是1M

Minio默认的分块文件大小是5M

image-20231224021335475

2.4 上传视频

  • 前端上传视频之前,需要先检查文件是否已经上传过,我们只需要拿着文件的MD5值去数据库查询即可,如果查到了就说明之前已经上传完成了

  • 假如说文件没有上传过,前端开始对视频进行分块然后将分块传输给后端,但是后端不会立即将分块上传到Minio系统,而是先检查此分块是否从前上传到在Minio系统

我们知道文件的MD5值及分块的序号,就可以知道这个分块在不在Minio系统

因为我们是根据文件的MD5值及分块的序号在Minio系统中存储的

  • 等所有的分块文件都上传完毕后,前端就可以请求后端接口进行合并分块

我们某个文件的分块都是在一个目录下的,也就是说一个分块是在一个目录,不用担心分块文件会重复

2.4.1 文件上传前校验

分为“文件上传前检查文件”与“分块文件上传前的检测”两个接口

2.4.1.1 BigFilesController
/**
 * 1.检查文件数据库有没有
 * 2.如果数据库有了再检查minio系统当用有没有(可能存在脏数据,数据库中有但是minio没有那也要传输)
 */
@ApiOperation(value = "文件上传前检查文件")
@PostMapping("/upload/checkfile")
public RestResponse<Boolean> checkfile(@RequestParam("fileMd5") String fileMd5) {
    return mediaFileService.checkFile(fileMd5);
}

/**
 * 分块在数据库中是不存储的,但是可以向minio中查询分块是否存在
 * minio中有了就不再传了,若没有的话再传
 */
@ApiOperation(value = "分块文件上传前的检测")
@PostMapping("/upload/checkchunk")
public RestResponse<Boolean> checkChunk(@RequestParam("fileMd5") String fileMd5,
                                        @RequestParam("chunk") int chunk) throws Exception {
    return mediaFileService.checkChunk(fileMd5,chunk);
}
2.4.1.2 MediaFileServiceImpl
/**
 * 1.首先查询数据库,如果文件不在数据库中表明文件不在
 * 2.如果文件在数据库中再查询minio系统,
 *
 * @param fileMd5 文件的md5
 * @return com.xuecheng.base.model.RestResponse<java.lang.Boolean> false不存在,true存在
 * @description 检查文件是否存在
 */
@Override
public RestResponse<Boolean> checkFile(String fileMd5) {
    //TODO 1.查询数据库
    MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
    //TODO 2.如果数据库存在再查询minio
    if (mediaFiles != null) {
        GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                //mediaFiles会有记录
                .bucket(mediaFiles.getBucket())
                .object(mediaFiles.getFilePath())
                .build();
        try {
            FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
            if (inputStream != null) {
                //文件已经存在
                return RestResponse.success(true);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //文件不存在
    return RestResponse.success(false);
}

private String getChunkFileFolderPath(String fileMd5) {
    return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/chunk/";
}

检查分块是否存在

对于Service的checkChunk分块文件校验方法我觉得还有另外一个见解思路

课程中对分块文件的校验思路只是检查一下minio系统中有没有对应的minio文件,如果没有的话就上传分块

但是我觉得这个地方可以改一改

有化成根据fileMd5去数据库查一下是否存在这个文件,然后得到这个文件的getFilePath,查看一下是否有完整的视频文件,如果有的话直接不用检查Minio分块文件是否存在了,假如说Minio没有完整的文件,那再上传对应的分块文件

可能有的人会问,我们明明已经在checkFile中校验了,为什么还要校验?

因为我在学习这个课程的时候我发现一个bug,就是我无法重复上传同一个文件,显然是不合理的

我说的重复上传的意思是毫无征兆的上传不上,前端也不会给什么提示信息,一直卡在百分之九十多的位置不动,为了优化这个地方我才采用了我这个思路

而且我不明白课程中checkFile明明已经校验为true文件存在了,还要进行checkChunk检查分块、合并分块的逻辑

如果是我做前端的话,我就当checkFile返回true时也不进行checkchunk检查分块,更不会合并分块

image-20231224223747158

image-20231224223730483

下面的TODO 1便是我新添加上的,只需要加载Todo2之前即可

2.4.3 合并分块文件模块也要加

//TODO 1.判断fileMd5对应的文件已经合并成一个完整的文件了,如果有了的话,那也不需要检查分块了
//判断Minio系统中是否有已经有合并的文件了,如果有的话没有分块所在路径也无所谓
RestResponse<Boolean> booleanRestResponse = this.checkFile(fileMd5);
if (booleanRestResponse.getResult()){
    //文件已经存在
    return RestResponse.success(true);
}
/**
 * 分块不会存在于数据库,直接查询minio系统即可
 *
 * @param fileMd5    文件的md5
 * @param chunkIndex 分块序号
 * @return com.xuecheng.base.model.RestResponse<java.lang.Boolean> false不存在,true存在
 * @description 检查分块是否存在
 */
@Override
public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
    //TODO 2.根据MD5得到分块文件的目录路径
    //分块存储路径:md5前两位为两个目录,MD5值也是一层目录,chunk目录存储分块文件
    String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);

    GetObjectArgs getObjectArgs = GetObjectArgs.builder()
            //视频的桶
            .bucket(bucket_video)
            //文件名是目录路径+分块序号
            .object(chunkFileFolderPath+chunkIndex)
            .build();
    try {
        FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
        if (inputStream != null) {
            //文件已经存在
            return RestResponse.success(true);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return RestResponse.success(false);
}

2.4.2 上传分块文件

2.4.2.1 BigFilesController
@ApiOperation(value = "上传分块文件")
@PostMapping("/upload/uploadchunk")
public RestResponse uploadChunk(@RequestParam("file") MultipartFile file,
                                @RequestParam("fileMd5") String fileMd5,
                                @RequestParam("chunk") int chunk) throws Exception {
    //1.创建一个临时文件,前缀是"minio",后缀是“.temp”
    File tempFile = File.createTempFile("minio", ".temp");
    file.transferTo(tempFile);
    return mediaFileService.uploadChunk(fileMd5,chunk,tempFile.getAbsolutePath());
}
2.4.2.2 MediaFileServiceImpl
/**
 * @param fileMd5            文件md5
 * @param chunk              分块序号
 * @param localChunkFilePath 本地文件路径
 * @return com.xuecheng.base.model.RestResponse
 * @description 上传分块
 */
@Override
public RestResponse uploadChunk(String fileMd5, int chunk, String localChunkFilePath) {
    //TODO 将分块文件上传到minio
    //传空默认返回类型MediaType.APPLICATION_OCTET_STREAM_VALUE application/octet-stream未知流类型
    String mimeType = getMimeType(null);
    //获取分块文件的目录
    String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
    boolean b = this.addMediaFilesToMinIO(localChunkFilePath, bucket_video, chunkFileFolderPath + chunk, mimeType);
    if (!b) {
        //false
        return RestResponse.validfail(false, "上传分块文件{" + fileMd5 + "/" + chunk + "}失败");
    }
    //上传分块文件成功
    return RestResponse.success(true);
}

    /**
     * 将文件上传到MinIo
     *
     * @param bucket        桶
     * @param localFilePath 文件在本地的路径
     * @param objectName    上传到MinIo系统中时的文件名称
     * @param mimeType      上传的文件类型
     */
    private boolean addMediaFilesToMinIO(String localFilePath, String bucket, String objectName, String mimeType) {
        try {
            UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                    //桶,也就是目录
                    .bucket(bucket)
                    //指定本地文件的路径
                    .filename(localFilePath)
                    //上传到minio中的对象名,上传的文件存储到哪个对象中
                    .object(objectName)
                    .contentType(mimeType)
                    //构建
                    .build();
            minioClient.uploadObject(uploadObjectArgs);
            log.debug("上传文件到minio成功,bucket:{},objectName:{}", bucket, objectName);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            log.info("上传文件出错,bucket:{},objectName:{},错误信息:{}", bucket, objectName, e.getMessage());
            return false;
        }
    }

2.4.3 合并分块文件

  • 找到分块文件

  • 调用Minio的SDK,将分块文件进行合并

  • 校验合并后的文件与原文件是否一致

    合并后的文件来源于分块文件,如果上传分块的过程中有问题导致数据丢失,那合并后的文件肯定是不完整的

    合并后的文件与原文件一致,才可以说视频是上传成功的

  • 将文件信息入库

  • 最后清理Minio的分块文件

2.4.3.1 BigFilesController
/**
 * @param fileMd5    文件md5值
 * @param fileName   合并分块之后要入库,fileName原始文件名要写在数据库
 * @param chunkTotal 总共分块数
 */
@ApiOperation(value = "合并文件")
@PostMapping("/upload/mergechunks")
public RestResponse mergeChunks(@RequestParam("fileMd5") String fileMd5,
                                @RequestParam("fileName") String fileName,
                                @RequestParam("chunkTotal") int chunkTotal) throws Exception {
    Long companyId = 1232141425L;
    //文件信息对象
    UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
    uploadFileParamsDto.setFilename(fileName);
    uploadFileParamsDto.setTags("视图文件");
    uploadFileParamsDto.setFileType("001002");//数据字典代码 - 001002代表视频
    return mediaFileService.mergeChunks(companyId, fileMd5, chunkTotal, uploadFileParamsDto);
}
2.4.3.2 MediaFileServiceImpl

其实和2.4.1模块同理

可以在mergeChunks放大TODO1之前下面添加这一段代码

假如说合并后的文件已经存在了,就不需要进行合并了

//TODO 0.如果已经有了合并分块后对应的文件的话,就不用再合并了
//判断Minio系统中是否有已经有合并的文件了,如果有的话没有分块所在路径也无所谓
RestResponse<Boolean> booleanRestResponse = this.checkFile(fileMd5);
if (booleanRestResponse.getResult()){
    //文件已经存在
    return RestResponse.success(true);
}
/**
 * 为什么又companyId 机构ID?
 * 分布式文件系统空间不是随便使用的,比如某个机构传输的课程很多很多,那我们就可以收费了(比如超过1Tb便开始收费)
 * 知道了companyId我们就知道是谁传的,也知道这些机构用了多少GB
 *
 * @param companyId           机构id
 * @param fileMd5             文件md5
 * @param chunkTotal          分块总和
 * @param uploadFileParamsDto 文件信息(要入库)
 * @return com.xuecheng.base.model.RestResponse
 * @description 合并分块
 */
@Override
public RestResponse mergeChunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
    //TODO 1.获取所有分块文件
    List<ComposeSource> sources = new ArrayList<>();
    //1.1 分块文件路径
    String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
    for (int i = 0; i < chunkTotal; i++) {
        sources.add(ComposeSource.builder()
                //分块文件所在桶
                .bucket(bucket_video)
                //分块文件名称
                .object(chunkFileFolderPath + i).build());
    }
    //1.2 指定合并后文件存储在哪里
    String filename = uploadFileParamsDto.getFilename();
    String fileExt = filename.substring(filename.lastIndexOf("."));
    //1.3 获取对象存储名
    String filePathByMD5 = this.getFilePathByMD5(fileMd5, fileExt);
    ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
            //指定合并后文件在哪个桶里
            .bucket(bucket_video)
            //最终合并后的文件路径及名称
            .object(filePathByMD5)
            //指定分块源文件
            .sources(sources).build();
    //TODO 2.合并分块
    try {
        minioClient.composeObject(composeObjectArgs);
    } catch (Exception e) {
        e.printStackTrace();
        log.error("合并文件出错:bucket:{},objectName:{},错误信息:{}", bucket_video, filePathByMD5, e.getMessage());
        return RestResponse.validfail(false, "合并文件出错");
    }

    //TODO 3.校验合并后的文件与原文件是否一致
    //3.1校验时先要把文件下载下来
    File tempFile = this.downloadFileFromMinIO(bucket_video, filePathByMD5);
    //3.2 比较原文件与临时文件的MD5值
    //将FileInputStream放在括号里,当try..catch执行结束后会自动关闭流,不用加finally了
    try (FileInputStream fis = new FileInputStream(tempFile)) {
        String mergeFile_md5 = DigestUtils.md5Hex(fis);
        if (!fileMd5.equals(mergeFile_md5)) {
            log.error("校验合并文件md5值不一致,原始文件{},合并文件{}", fileMd5, mergeFile_md5);
            return RestResponse.validfail(false, "文件校验失败");
        }

        //保存一下文件信息 - 文件大小
        uploadFileParamsDto.setFileSize(tempFile.length());

    } catch (IOException e) {
        e.printStackTrace();
        return RestResponse.validfail(false, "文件校验失败");
    }

    //TODO 4.文件信息
    MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_video, filePathByMD5);
    if (mediaFiles == null) {
        return RestResponse.validfail(false, "文件入库失败");
    }

    //TODO 5.清理分块文件
    //5.1获取分块文件路径
    //this.getChunkFileFolderPath(fileMd5);
    this.clearChunkFiles(chunkFileFolderPath, chunkTotal);

    return RestResponse.success(true);
}
       /**
     * 清除分块文件
     *
     * @param chunkFileFolderPath 分块文件路径
     * @param chunkTotal          分块文件总数
     */
    private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal) {
        //需要参数removeObjectsArgs

        //Iterable<DeleteObject> objects =
        List<DeleteObject> objects = Stream.iterate(0, i -> ++i)
                .limit(chunkTotal)
                //String.concat函数用于拼接字符串
                .map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
                .collect(Collectors.toList());

        RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder()
                //指定要清理的分块文件的桶
                .bucket(bucket_video)
                //需要一个Iterable<DeleteObject>迭代器
                .objects(objects)
                .build();
        //执行了这段方法并没有真正的删除,还需要遍历一下
        Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
        results.forEach(f->{
            try {
                //get方法执行之后才是真正的删除了
                DeleteError deleteError = f.get();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
//        或者是下面这种遍历方式,都是可以的
//        for (Result<DeleteError> deleteError:results){
//            DeleteError error = deleteError.get();
//        }

    }

最终合并后的路径是在getFilePathByMD5中

/**
 * @param fileMd5 文件md5值
 * @param fileExt 文件扩展名
 */
private String getFilePathByMD5(String fileMd5, String fileExt) {
    return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5 + fileExt;
}

2.4.4 测试

  • 首先修改Springboot-web默认上传文件大小

前端对文件分块的大小为5MB,SpringBoot web默认上传文件的大小限制为1MB,这里需要在media-api工程修改配置如下:

spring:
  servlet:
    multipart:
      max-file-size: 50MB
      max-request-size: 50MB
  • max-file-size

    指定单个文件上传的最大大小限制,单个文件的大小如果超过了这个配置的值,将会导致文件上传失败

  • max-request-size

    指定整个 HTTP 请求的最大大小限制,包括所有上传文件和其他请求数据,请求的总大小超过了这个配置的值,将会导致整个请求失败

两个配置项的值可以使用标准的大小单位,比如 KB(千字节)、MB(兆字节)等。在你的例子中,50MB 表示最大文件大小和最大请求大小都被限制为 50 兆字节

  • 点击“上传视频”按钮

image-20231224165736643

  • 点击“添加文件”按钮

image-20231224165809867

  • 上传文件

image-20231224172907840

  • 查看Minio系统

    分块上传挺成功的

image-20231224172946156

  • 合并分块之后

    所有的分块都被清理了,然后合并成一个文件

  • 经过我的2.4.3与2.4.1优化之后,上传挺正常了

image-20231224230839650

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1333637.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解决log4j多个日志都写到一个文件

之前客户端程序由于Websockt包依赖的log4j&#xff0c;就用log4j写日志了&#xff0c;Web用的log4j2没毛病。用log4j的多个logger的日志都写到一个文件里了&#xff0c;查了很多资料都没解决。今天闲了解决一下。 最后好使的配置 # 设置日志根 log4j.rootLogger INFO,Except…

uni-app之HelloWorld实现

锋哥原创的uni-app视频教程&#xff1a; 2023版uniapp从入门到上天视频教程(Java后端无废话版)&#xff0c;火爆更新中..._哔哩哔哩_bilibili2023版uniapp从入门到上天视频教程(Java后端无废话版)&#xff0c;火爆更新中...共计23条视频&#xff0c;包括&#xff1a;第1讲 uni…

选择排序(java)

选择排序 选择排序是默认前面都是已经排序好的&#xff0c;然后从后面 选择最小的放在前面排序好的的后面&#xff0c;首先第一轮循环的时候默认的排序好的为空&#xff0c;然后从后面选择最小的放到数组的第一个位置&#xff0c;第二轮循环的时候默认第个元素是已经 排序好的…

深度学习中的Dropout

1 Dropout概述 1.1 什么是Dropout 在2012年&#xff0c;Hinton在其论文《Improving neural networks by preventing co-adaptation of feature detectors》中提出Dropout。当一个复杂的前馈神经网络被训练在小的数据集时&#xff0c;容易造成过拟合。为了防止过拟合&#xff…

光耦继电器

光耦继电器(光电继电器) AQW282SX 282SZ 280SX 280SZ 284SX 284SZ 212S 212SX 21 2SZ 文章目录 光耦继电器(光电继电器)前言一、光耦继电器是什么二、光耦继电器的类型三、光电耦合器的应用总结前言 光耦继电器在工业控制、通讯、医疗设备、家电及汽车电子等领域得到广泛应…

基于鸢尾花数据集的逻辑回归分类实践

基于鸢尾花数据集的逻辑回归分类实践 重要知识点 逻辑回归 原理简介&#xff1a; Logistic回归虽然名字里带“回归”&#xff0c;但是它实际上是一种分类方法&#xff0c;主要用于两分类问题&#xff08;即输出只有两种&#xff0c;分别代表两个类别&#xff09;&#xff0c…

Flutter windows 环境配置

Flutter windows 环境配置 从零开始&#xff0c;演示flutter环境配置到启动项目&#xff0c;同时支持 vscode 和 android studio 目录 Flutter windows 环境配置一、环境配置1. Flutter SDK2. Android Studio3. JDK4. 拓展安装5. Visual Studio 2022二、项目创建和启动1. vsco…

Spark集群部署与架构

在大数据时代&#xff0c;处理海量数据需要分布式计算框架。Apache Spark作为一种强大的大数据处理工具&#xff0c;可以在集群中高效运行&#xff0c;处理数十TB甚至PB级别的数据。本文将介绍如何构建和管理Spark集群&#xff0c;以满足大规模数据处理的需求。 Spark集群架构…

小狐狸ChatGPT付费创作系统小程序端开发工具提示打开显示无法打开页面解决办法

最新版2.6.7版下载&#xff1a;https://download.csdn.net/download/mo3408/88656497 很多会员在上传小程序前端时经常出现首页无法打开的情况&#xff0c;错误提示无法打开该页面&#xff0c;不支持打开&#xff0c;这种问题其实就是权限问题&#xff0c;页面是通过调用web-v…

vue3(七)-基础入门之事件总线与动态组件

一、事件总线 事件总线使用场景&#xff1a; 两个兄弟组件之间的传参&#xff0c;或者两个没有关联的组件之间的传参 html &#xff1a;引入 publicmsg 与 acceptmsg 自定义组件 (自定义组件名称必须小写) <body><div id"app"><publicmsg></…

Linux---命令行参数+环境变量

一、命令行参数 int main(int argc,char*argv[]) {//...return 0; } 不知道有没有人见过这样的主函数&#xff0c;它带了两个参数&#xff0c;argv接收的参数就叫做命令行参数&#xff0c;因为它的参数是从命令行来的&#xff0c;给大家演示一下&#xff0c;大家就懂了 命令行…

YOLOv8改进 | 2023注意力篇 | MSDA多尺度空洞注意力(附多位置添加教程)

一、本文介绍 本文给大家带来的改进机制是MSDA&#xff08;多尺度空洞注意力&#xff09;发表于今年的中科院一区(算是国内计算机领域的最高期刊了)&#xff0c;其全称是"DilateFormer: Multi-Scale Dilated Transformer for Visual Recognition"。MSDA的主要思想是…

北海 - Rust与面向对象(三)

策略模式 上节说到&#xff0c;模板方法变化一下就能成策略模式&#xff0c;怎么变化的&#xff1f;且看策略模式典型案例&#xff1a; pub trait Fly {fn fly(&self); }pub trait Quack {fn quack($self); }/// 先以静多态的方式实现 /// 似 trait Fly Quack就是Duck&…

java调用GDAL实现栅格数据的重采样的一种方法

目录 1.关于重采样 1.1概念 1.2用途 1.3常见算法 2.关于GDAL 2.1GDAL中的重采样算法 3.实现重采样 3.1思路 3.2完整代码 3.3使用QGIS验证效果 1.关于重采样 1.1概念 重采样是以原始图像的像元值或者导出的值填充到新的图像的每个像元的的过程。 1.2用途 在地理信…

【MySQL学习笔记007】约束

1、概述 概念&#xff1a;约束是作用于表中字段上的规则&#xff0c;用于限制存储在表中的数据。 目的&#xff1a;保证数据库中数据的正确、有效性和完整性。 分类 注意&#xff1a;约束是作用于表中字段上的&#xff0c;可以在创建表/修改表的时候添加约束。 2、约束…

文章标题(备注)

现在也裁员了吗&#xff1f;怎么感觉越来越垃圾 这个又是什么&#xff1f;真搞笑&#xff0c;我也没开隐私呀

生物神经网络衍生出的算法

一个生物神经网络的基本结构&#xff1a; 生物神经网络由大量神经元组成&#xff0c;这些神经元之间通过突触相互连接。神经元可以接收来自其他神经元的信号&#xff0c;并根据信号的强度和类型来调整自己的输出信号。这种神经元之间的相互连接和信号传递形成了生物神经网络的基…

记pbcms网站被攻击,很多标题被篡改(1)

记得定期打开网站看看哦! 被攻击后的网站异常表现:网页内容缺失或变更,页面布局破坏,按钮点击无效,...... 接着查看HTML、CSS、JS文件,发现嵌入了未知代码! 攻击1:index.html 或其他html模板页面的标题、关键词、描述被篡改(俗称,被挂马...),如下: 攻击2:在ht…

[原创][R语言]股票分析实战[4]:周级别涨幅趋势的相关性

[简介] 常用网名: 猪头三 出生日期: 1981.XX.XX QQ联系: 643439947 个人网站: 80x86汇编小站 https://www.x86asm.org 编程生涯: 2001年~至今[共22年] 职业生涯: 20年 开发语言: C/C、80x86ASM、PHP、Perl、Objective-C、Object Pascal、C#、Python 开发工具: Visual Studio、D…

<JavaEE> 协议格式 -- 传输层协议 UDP

目录 一、UDP协议格式长啥样&#xff1f; 二、端口号和IP地址 1&#xff09;UDP协议中包含哪两个端口号&#xff1f; 2&#xff09;有没有包含IP地址&#xff1f; 三、UDP报文长度 1&#xff09;UDP报文长度最长多长&#xff1f; 2&#xff09;UDP报文的组成&#xff1f…