统一版本管理
<properties><minio.version>8.5.10</minio.version><aws.version>1.12.737</aws.version><hutool.version>5.8.28</hutool.version>
</properties>
<!--minio -->
<dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>${minio.version}</version>
</dependency>
<!--aws-s3-->
<dependency><groupId>com.amazonaws</groupId><artifactId>aws-java-sdk-s3</artifactId><version>${aws.version}</version>
</dependency>
<!--hutool -->
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>${hutool.version}</version>
</dependency>
项目配置 application-dev.yml
# 文件系统
minio: #内部地址,可以访问到内网地址endpoint: http://172.16.11.110:10087access-key: xxxxxxsecret-key: xxxxxxbucket-name: public-example-xxxxpublic-bucket-name: public-example-xxx#外网,互联网地址preview-domain: http://116.201.11.xxx:30087
创建 MinioConfig
package com.example.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.minio.MinioClient;
import lombok.AllArgsConstructor;/*** aws-s3 通用存储操作 支持所有兼容s3协议的云存储: 阿里云OSS、腾讯云COS、华为云、七牛云、,京东云、minio * @author weimeilayer@gmail.com* @date 2021年2月3日*/
@Configuration
@AllArgsConstructor
public class MinioConfig {private final MinioProperties minioProperties;@Beanpublic MinioClient minioClient() {MinioClient minioClient = MinioClient.builder().endpoint(minioProperties.getEndpoint()).credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey()).build();return minioClient;}
}
创建 MinioProperties
package com.example.config;import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;/*** aws 配置信息bucket 设置公共读权限* @author weimeilayer@gmail.com* @date 💓💕2021年4月1日🐬🐇 💓💕*/
@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {/*** 对象存储服务的URL*/@Schema(description = "对象存储服务的URL")private String endpoint;/*** 自定义域名*/@Schema(description = "自定义域名")private String customDomain;/*** 反向代理和S3默认支持*/@Schema(description = "反向代理和S3默认支持")private Boolean pathStyleAccess = true;/*** 应用ID*/@Schema(description = "应用ID")private String appId;/*** 区域*/@Schema(description = "区域")private String region;/*** 预览地址*/@Schema(description = "预览地址")private String previewDomain;/*** Access key就像用户ID,可以唯一标识你的账户*/@Schema(description = "Access key就像用户ID,可以唯一标识你的账户")private String accessKey;/*** Secret key是你账户的密码*/@Schema(description = "Secret key是你账户的密码")private String secretKey;/*** 默认的存储桶名称*/@Schema(description = "默认的存储桶名称")private String bucketName;/*** 公开桶名*/@Schema(description = "公开桶名")private String publicBucketName;/*** 物理删除文件*/@Schema(description = "物理删除文件")private boolean physicsDelete;/*** 最大线程数,默认: 100*/@Schema(description = "最大线程数,默认: 100")private Integer maxConnections = 100;
}
创建 MinioTemplate
package com.example.config;import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Optional;import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Configuration;import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.Bucket;
import com.amazonaws.services.s3.model.ObjectListing;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectResult;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import com.amazonaws.util.IOUtils;import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;/*** aws-s3 通用存储操作 支持所有兼容s3协议的云存储: {阿里云OSS,腾讯云COS,七牛云,京东云,minio 等}* @author weimeilayer@gmail.com ✨* @date 💓💕2024年3月7日🐬🐇 💓💕*/
@Configuration
@RequiredArgsConstructor
public class MinioTemplate implements InitializingBean {private final MinioProperties ossProperties;private AmazonS3 amazonS3;/*** 创建bucket* * @param bucketName bucket名称*/@SneakyThrowspublic void createBucket(String bucketName) {if (!amazonS3.doesBucketExistV2(bucketName)) {amazonS3.createBucket((bucketName));}}/*** 获取全部bucket API Documentation</a>*/@SneakyThrowspublic List<Bucket> getAllBuckets() {return amazonS3.listBuckets();}/*** @param bucketName bucket名称 API Documentation</a>*/@SneakyThrowspublic Optional<Bucket> getBucket(String bucketName) {return amazonS3.listBuckets().stream().filter(b -> b.getName().equals(bucketName)).findFirst();}/*** @param bucketName bucket名称* @see <a href= Documentation</a>*/@SneakyThrowspublic void removeBucket(String bucketName) {amazonS3.deleteBucket(bucketName);}/*** 根据文件前置查询文件* * @param bucketName bucket名称* @param prefix 前缀* @param recursive 是否递归查询* @return S3ObjectSummary 列表 API Documentation</a>*/@SneakyThrowspublic List<S3ObjectSummary> getAllObjectsByPrefix(String bucketName, String prefix, boolean recursive) {ObjectListing objectListing = amazonS3.listObjects(bucketName, prefix);return new ArrayList<>(objectListing.getObjectSummaries());}/*** 获取文件外链* * @param bucketName bucket名称* @param objectName 文件名称* @param expires 过期时间 <=7* @return url*/@SneakyThrowspublic String getObjectURL(String bucketName, String objectName, Integer expires) {Date date = new Date();Calendar calendar = new GregorianCalendar();calendar.setTime(date);calendar.add(Calendar.DAY_OF_MONTH, expires);URL url = amazonS3.generatePresignedUrl(bucketName, objectName, calendar.getTime());return url.toString();}/*** 获取文件* * @param bucketName bucket名称* @param objectName 文件名称* @return 二进制流 API Documentation</a>*/@SneakyThrowspublic S3Object getObject(String bucketName, String objectName) {return amazonS3.getObject(bucketName, objectName);}/*** 上传文件* * @param bucketName bucket名称* @param objectName 文件名称* @param stream 文件流* @throws Exception*/public void putObject(String bucketName, String objectName, InputStream stream) throws Exception {putObject(bucketName, objectName, stream, (long) stream.available(), "application/octet-stream");}/*** 上传文件* * @param bucketName bucket名称* @param objectName 文件名称* @param stream 文件流* @param size 大小* @param contextType 类型* @throws Exception*/public PutObjectResult putObject(String bucketName, String objectName, InputStream stream, long size,String contextType) throws Exception {byte[] bytes = IOUtils.toByteArray(stream);ObjectMetadata objectMetadata = new ObjectMetadata();objectMetadata.setContentLength(size);objectMetadata.setContentType(contextType);ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);// 上传return amazonS3.putObject(bucketName, objectName, byteArrayInputStream, objectMetadata);}/*** 获取文件信息* * @param bucketName bucket名称* @param objectName 文件名称* @throws Exception API Documentation</a>*/public S3Object getObjectInfo(String bucketName, String objectName) throws Exception {return amazonS3.getObject(bucketName, objectName);}/*** 删除文件* * @param bucketName bucket名称* @param objectName 文件名称* @throws Exception*/public void removeObject(String bucketName, String objectName) throws Exception {amazonS3.deleteObject(bucketName, objectName);}@Overridepublic void afterPropertiesSet() {ClientConfiguration clientConfiguration = new ClientConfiguration();clientConfiguration.setMaxConnections(ossProperties.getMaxConnections());AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder.EndpointConfiguration(ossProperties.getEndpoint(), ossProperties.getRegion());AWSCredentials awsCredentials = new BasicAWSCredentials(ossProperties.getAccessKey(),ossProperties.getSecretKey());AWSCredentialsProvider awsCredentialsProvider = new AWSStaticCredentialsProvider(awsCredentials);this.amazonS3 = AmazonS3Client.builder().withEndpointConfiguration(endpointConfiguration).withClientConfiguration(clientConfiguration).withCredentials(awsCredentialsProvider).disableChunkedEncoding().withPathStyleAccessEnabled(ossProperties.getPathStyleAccess()).build();}
}
创建Result
package com.example.utils;import java.util.HashMap;/*** 响应信息主体* @author weimeilayer@gmail.com ✨* @date 💓💕2021年6月28日 🐬🐇 💓💕*/
public class Result extends HashMap<String, Object> {private static final long serialVersionUID = 1L;/** 状态码 */public static final String CODE_TAG = "code";/** 返回内容 */public static final String MSG_TAG = "msg";/** 数据对象 */public static final String DATA_TAG = "data";/*** 初始化一个新创建的 Result 对象,使其表示一个空消息。*/public Result() {}/*** 初始化一个新创建的 Result 对象** @param code 状态码* @param msg 返回内容*/public Result(int code, String msg) {super.put(CODE_TAG, code);super.put(MSG_TAG, msg);}/*** 初始化一个新创建的 Result 对象** @param code 状态码* @param msg 返回内容* @param data 数据对象*/public Result(int code, String msg, Object data) {super.put(CODE_TAG, code);super.put(MSG_TAG, msg);if (data != null) {super.put(DATA_TAG, data);}}/*** 返回成功消息** @return 成功消息*/public static Result success() {return Result.success("操作成功");}/*** 返回成功数据** @return 成功消息*/public static Result success(Object data) {return Result.success("操作成功", data);}/*** 返回成功消息** @param msg 返回内容* @return 成功消息*/public static Result success(String msg) {return Result.success(msg, null);}/*** 返回成功消息** @param msg 返回内容* @param data 数据对象* @return 成功消息*/public static Result success(String msg, Object data) {return new Result(HttpStatus.SUCCESS, msg, data);}/*** 返回警告消息** @param msg 返回内容* @return 警告消息*/public static Result warn(String msg) {return Result.warn(msg, null);}/*** 返回警告消息** @param msg 返回内容* @param data 数据对象* @return 警告消息*/public static Result warn(String msg, Object data) {return new Result(HttpStatus.WARN, msg, data);}/*** 返回错误消息** @return 错误消息*/public static Result error() {return Result.error("操作失败");}/*** 返回错误消息** @param msg 返回内容* @return 错误消息*/public static Result error(String msg) {return Result.error(msg, null);}/*** 返回错误消息** @param msg 返回内容* @param data 数据对象* @return 错误消息*/public static Result error(String msg, Object data) {return new Result(HttpStatus.ERROR, msg, data);}/*** 返回错误消息** @param code 状态码* @param msg 返回内容* @return 错误消息*/public static Result error(int code, String msg) {return new Result(code, msg, null);}/*** 方便链式调用** @param key 键* @param value 值* @return 数据对象*/@Overridepublic Result put(String key, Object value) {super.put(key, value);return this;}
}
创建 HttpStatus
package com.example.utils;/*** http请求状态* @author weimeilayer@gmail.com ✨* @date 💓💕2024年6月28日 🐬🐇 💓💕*/
public class HttpStatus {/*** 操作成功*/public static final int SUCCESS = 200;/*** 对象创建成功*/public static final int CREATED = 201;/*** 请求已经被接受*/public static final int ACCEPTED = 202;/*** 操作已经执行成功,但是没有返回数据*/public static final int NO_CONTENT = 204;/*** 资源已被移除*/public static final int MOVED_PERM = 301;/*** 重定向*/public static final int SEE_OTHER = 303;/*** 资源没有被修改*/public static final int NOT_MODIFIED = 304;/*** 参数列表错误(缺少,格式不匹配)*/public static final int BAD_REQUEST = 400;/*** 未授权*/public static final int UNAUTHORIZED = 401;/*** 访问受限,授权过期*/public static final int FORBIDDEN = 403;/*** 资源,服务未找到*/public static final int NOT_FOUND = 404;/*** 不允许的http方法*/public static final int BAD_METHOD = 405;/*** 资源冲突,或者资源被锁*/public static final int CONFLICT = 409;/*** 不支持的数据,媒体类型*/public static final int UNSUPPORTED_TYPE = 415;/*** 系统内部错误*/public static final int ERROR = 500;/*** 接口未实现*/public static final int NOT_IMPLEMENTED = 501;/*** 系统警告消息*/public static final int WARN = 601;
}
创建 Constants
package com.example.utils;/*** 通用常量信息* @author weimeilayer@gmail.com ✨* @date 💓💕2024年6月28日 🐬🐇 💓💕*/
public class Constants {/*** UTF-8 字符集*/public static final String UTF8 = "UTF-8";/*** GBK 字符集*/public static final String GBK = "GBK";/*** www主域*/public static final String WWW = "www.";/*** http请求*/public static final String HTTP = "http://";/*** https请求*/public static final String HTTPS = "https://";/*** 通用成功标识*/public static final String SUCCESS = "0";/*** 通用失败标识*/public static final String FAIL = "1";/*** 登录成功*/public static final String LOGIN_SUCCESS = "Success";/*** 注销*/public static final String LOGOUT = "Logout";/*** 注册*/public static final String REGISTER = "Register";/*** 登录失败*/public static final String LOGIN_FAIL = "Error";/*** 验证码有效期(分钟)*/public static final Integer CAPTCHA_EXPIRATION = 2;/*** 令牌*/public static final String TOKEN = "token";/*** 令牌前缀*/public static final String TOKEN_PREFIX = "Bearer ";/*** 令牌前缀*/public static final String LOGIN_USER_KEY = "login_user_key";/*** 用户头像*/public static final String JWT_AVATAR = "avatar";/*** 创建时间*/public static final String JWT_CREATED = "created";/*** 用户权限*/public static final String JWT_AUTHORITIES = "authorities";/*** 资源映射路径 前缀*/public static final String RESOURCE_PREFIX = "/profile";/*** RMI 远程方法调用*/public static final String LOOKUP_RMI = "rmi:";/*** LDAP 远程方法调用*/public static final String LOOKUP_LDAP = "ldap:";/*** LDAPS 远程方法调用*/public static final String LOOKUP_LDAPS = "ldaps:";
}
数据库表
CREATE TABLE `sys_file` (`id` varchar(32) NOT NULL COMMENT '主键',`name` varchar(200) DEFAULT NULL COMMENT '原文件名',`group_id` varchar(32) DEFAULT NULL COMMENT '分组编号,对应多文件',`file_type` varchar(200) DEFAULT NULL COMMENT '文件类型',`suffix` varchar(200) DEFAULT NULL COMMENT '文件后缀',`size` int(11) DEFAULT NULL COMMENT '文件大小,单位字节',`preview_url` varchar(1000) DEFAULT NULL COMMENT '预览地址',`storage_type` varchar(200) DEFAULT NULL COMMENT '存储类型',`storage_url` varchar(200) DEFAULT NULL COMMENT '存储地址',`bucket_name` varchar(200) DEFAULT NULL COMMENT '桶名',`object_name` varchar(200) DEFAULT NULL COMMENT '桶内文件名',`visit_count` int(11) DEFAULT NULL COMMENT '访问次数',`sort` int(11) DEFAULT '0' COMMENT '排序值',`remarks` varchar(200) DEFAULT NULL COMMENT '备注',`gmt_create` timestamp NULL DEFAULT NULL COMMENT '创建时间',`gmt_modified` timestamp NULL DEFAULT NULL COMMENT '更新时间',`create_by` varchar(32) DEFAULT NULL COMMENT '创建人ID',`update_by` varchar(32) DEFAULT NULL COMMENT '修改人ID',`del_flag` varchar(32) DEFAULT '0' COMMENT '逻辑删除(0:未删除;null:已删除)',`tenant_id` int(11) DEFAULT NULL COMMENT '所属租户',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统基本信息--文件管理信息';
实体类 SysFile
package com.example.entity;import java.io.Serial;
import java.time.LocalDateTime;import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;/*** 系统基础信息--文件管理表* @author weimeilayer@gmail.com ✨* @date 💓💕2021年2月28日 🐬🐇 💓💕*/
@Data
@TableName("sys_file")
@EqualsAndHashCode(callSuper = false)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(description = "系统基础信息--文件管理表")
public class SysFile extends Model<SysFile> {@Serialprivate static final long serialVersionUID = 1L;/*** 主键*/@TableId(value = "id", type = IdType.ASSIGN_ID)@Schema(description = "主键ID")private String id;/*** 原文件名*/@Schema(description = "原文件名")private String name;/*** 存储桶名称*/@Schema(description = "原始文件名")private String original;/*** 分组编号,用于对应多文件*/@Schema(description = "分组编号,用于对应多文件")private String groupId;/*** 文件类型*/@Schema(description = "文件类型")private String fileType;/*** 文件后缀*/@Schema(description = "文件后缀")private String suffix;/*** 文件大小,单位字节*/@Schema(description = "文件大小,单位字节")private Integer size;/*** 预览地址*/@Schema(description = "预览地址")private String previewUrl;/*** 存储类型*/@Schema(description = "存储类型")private String storageType;/*** 存储地址*/@Schema(description = "存储地址")private String storageUrl;/*** 桶名*/@Schema(description = "桶名")private String bucketName;/*** 桶内文件名*/@Schema(description = "桶内文件名")private String objectName;/*** 访问次数*/@Schema(description = "访问次数")private Integer visitCount;/*** 排序*/@Schema(description = "排序")private Integer sort;/*** 备注*/@Schema(description = "备注")private String remarks;/*** 逻辑删除(0:未删除;null:已删除)*/@TableLogic@Schema(description = "逻辑删除(0:未删除;null:已删除)")@TableField(fill = FieldFill.INSERT)private String delFlag;/*** 创建人*/@Schema(description = "创建人")@TableField(fill = FieldFill.INSERT)private String createBy;/*** 编辑人*/@Schema(description = "编辑人")@TableField(fill = FieldFill.UPDATE)private String updateBy;/*** 创建时间*/@TableField(fill = FieldFill.INSERT)@Schema(description = "创建时间")private LocalDateTime gmtCreate;/*** 编辑时间*/@Schema(description = "编辑时间")@TableField(fill = FieldFill.UPDATE)private LocalDateTime gmtModified;/*** 所属租户*/@Schema(description = "所属租户")private String tenantId;
}
创建接口类 SysFileService
package com.example.service;
import java.util.List;
import org.springframework.web.multipart.MultipartFile;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.dto.SysFileDto;
import com.example.entity.SysFile;
import com.example.utils.Result;
import com.example.vo.SysFileSelVo;
import com.example.vo.SysFileSortVo;
import jakarta.servlet.http.HttpServletResponse;
/**
-
系统基础信息–文件管理服务类
-
@author weimeilayer@gmail.com ✨
-
@date 💓💕 2023年5月20日 🐬🐇 💓💕
/
public interface SysFileService extends IService {
/*- 上传文件
- @param files
- @param groupId
- @param isPreview
- @param isPublic
- @param sort
- @return
/
Result uploadFile(MultipartFile[] files, String groupId, Boolean isPreview, Boolean isPublic, Integer sort);
/* - 预览
- @param groupId
- @return
/
Result preview(String groupId);
/* - 分组预览
- @param groupId
- @param previewList
- @return
/
boolean preview(String groupId, List previewList);
/* - 下载
- @param response
- @param id
/
void download(HttpServletResponse response, String id);
/* - 删除文件
- @param id
- @return
/
Result delete(String id);
/* - 排序
- @param vo
- @return
*/
Result sort(SysFileSortVo vo);
/**
- 分页查询SysFile
- @param selvo 查询参数
- @return
/
public IPage getSysFileDtoPage(Page page,SysFileSelVo selvo);
/* - 上传文件
- @param file
- @return
*/
public Result uploadFile(MultipartFile file);
/**
- 读取文件
- @param bucket 桶名称
- @param fileName 文件名称
- @param response 输出流
*/
public void getFile(String bucket, String fileName, HttpServletResponse response);
/**
- 删除文件
- @param id
- @return
*/
public Boolean deleteFile(String id);
}
实现类 SysFileServiceImpl
package com.example.service.impl;import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;import com.amazonaws.services.s3.model.S3Object;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.config.MinioProperties;
import com.example.config.MinioTemplate;
import com.example.dto.SysFileDto;
import com.example.dto.SysFileSelDto;
import com.example.entity.SysFile;
import com.example.mapper.SysFileMapper;
import com.example.service.SysFileService;
import com.example.utils.Result;
import com.example.vo.SysFileSelVo;
import com.example.vo.SysFileSortVo;import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Console;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import io.minio.GetObjectArgs;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import io.minio.StatObjectArgs;
import io.minio.StatObjectResponse;
import io.minio.http.Method;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
/*** 系统基础信息--文件管理服务实现类** @author weimeilayer@gmail.com ✨* @date 💓💕 2023年5月20日 🐬🐇 💓💕*/
@Service
@AllArgsConstructor
public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> implements SysFileService {private final MinioClient minioClient;private final MinioTemplate minioTemplate;private final MinioProperties minioProperties;/*** 上传文件** @param file* @return*/@Overridepublic Result uploadFile(MultipartFile file) {String fileId = IdUtil.simpleUUID();String originalFilename = new String(Objects.requireNonNull(file.getOriginalFilename()).getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);String fileName = IdUtil.simpleUUID() + StrUtil.DOT + FileUtil.extName(originalFilename);Map<String, String> resultMap = new HashMap<>(4);resultMap.put("bucketName", minioProperties.getBucketName());resultMap.put("fileName", fileName);resultMap.put("originalFilename", originalFilename);resultMap.put("fileId", fileId);resultMap.put("url", String.format("/sysfile/%s/%s", minioProperties.getBucketName(), fileName));try (InputStream inputStream = file.getInputStream()) {minioTemplate.putObject(minioProperties.getBucketName(), fileName, inputStream, file.getSize(), file.getContentType());// 文件管理数据记录,收集管理追踪文件fileLog(file, fileName, fileId);} catch (Exception e) {log.error("上传失败", e);return Result.error(e.getLocalizedMessage());}return Result.success(resultMap);}/*** 读取文件** @param bucket* @param fileName* @param response*/@Overridepublic void getFile(String bucket, String fileName, HttpServletResponse response) {try (S3Object s3Object = minioTemplate.getObject(bucket, fileName)) {response.setContentType("application/octet-stream; charset=UTF-8");IoUtil.copy(s3Object.getObjectContent(), response.getOutputStream());} catch (Exception e) {Console.log("文件读取异常: {}", e.getLocalizedMessage());}}/*** 删除文件** @param id* @return*/@Override@SneakyThrows@Transactional(rollbackFor = Exception.class)public Boolean deleteFile(String id) {SysFile file = this.getById(id);minioTemplate.removeObject(minioProperties.getBucketName(), file.getName());return file.updateById();}/*** 文件管理数据记录,收集管理追踪文件** @param file 上传文件格式* @param fileName 文件名*/private void fileLog(MultipartFile file, String fileName, String fileId) {String originalFilename = new String(Objects.requireNonNull(file.getOriginalFilename()).getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);SysFile sysFile = new SysFile();sysFile.setId(fileId);sysFile.setName(fileName);sysFile.setOriginal(originalFilename);sysFile.setSize((int) file.getSize());sysFile.setFileType(FileUtil.extName(file.getOriginalFilename()));sysFile.setBucketName(minioProperties.getBucketName());this.save(sysFile);}/*** 分页查询SysFile* @param page* @param selvo 查询参数* @return*/@Overridepublic IPage<SysFileDto> getSysFileDtoPage(Page page, SysFileSelVo selvo) {return baseMapper.getSysFileDtoPage(page, selvo);}@Overridepublic Result uploadFile(MultipartFile[] files, String groupId, Boolean isPreview, Boolean isPublic, Integer sort) {if (files == null || files.length == 0) {return Result.error("上传文件不能为空!");}// 是否公开isPublic = isPublic != null && isPublic;// 是否预览isPreview = isPreview != null && isPreview;// 桶名String bucketName = isPublic ? minioProperties.getPublicBucketName() : minioProperties.getBucketName();// 文件目录String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd/"));// 预览列表List<SysFileSelDto> previewList = new ArrayList<>();// 分组编号,用于对应多文件if (StringUtils.hasText(groupId)) {// 排序if (sort == null) {sort = baseMapper.getMaxSort(groupId);if (sort != null) {sort++;} else {sort = 0;}}} else {groupId = IdUtil.simpleUUID();sort = 0;}for (int i = 0; i < files.length; i++) {MultipartFile file = files[i];InputStream in = null;try {// 原文件名String oriFileName = new String(file.getOriginalFilename().getBytes("ISO-8859-1"), "UTF-8");// 后缀String suffix = "";if (StringUtils.hasText(oriFileName)) {int index = oriFileName.lastIndexOf(StrPool.DOT);if (index != -1) {suffix = oriFileName.substring(index + 1);}}// minio文件名String objectName = dir + IdUtil.simpleUUID() + StrPool.DOT + suffix;in = file.getInputStream();// 上传文件minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(in, file.getSize(), -1).contentType(file.getContentType()).build());long size = file.getSize();String id = IdUtil.simpleUUID();String previewUrl = null;if (isPreview) {// 返回预览地址previewUrl = getPreviewUrl(bucketName, objectName);if (!StringUtils.hasText(previewUrl)) {continue;}// 去掉后缀if (isPublic) {previewUrl = previewUrl.substring(0, previewUrl.indexOf("?"));}previewList.add(new SysFileSelDto(id, oriFileName, suffix, formatFileSize(size), previewUrl, i));}// minio文件信息插入数据库minioInsertToDb(id, oriFileName, groupId, file.getContentType(), suffix, (int) size, bucketName, objectName, previewUrl, i + sort);} catch (Exception e) {log.error(e.getMessage());return Result.error("上传失败!");} finally {if (in != null) {try {in.close();} catch (IOException e) {log.error(e.getMessage());}}}}return Result.success("上传成功!",isPreview ? previewList : groupId);}@Overridepublic Result preview(String groupId) {List<SysFileSelVo> previewList = new ArrayList<>();boolean preview = preview(groupId, previewList);return preview ? Result.success(previewList) : Result.error("预览失败!");}/*** 文件下载*/@Overridepublic void download(HttpServletResponse response, String id) {SysFile sysFile = baseMapper.selectOne(Wrappers.<SysFile>lambdaQuery().select(SysFile::getBucketName, SysFile::getObjectName, SysFile::getName).eq(SysFile::getDelFlag, 0).eq(SysFile::getId, id));if (sysFile == null) {return;}String objectName = sysFile.getObjectName();if (CharSequenceUtil.isBlank(objectName)) {return;}InputStream in = null;try {String bucketName = sysFile.getBucketName();// 获取对象信息StatObjectResponse stat = minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());response.setContentType(stat.contentType());response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(sysFile.getName(), "UTF-8"));// 文件下载in = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());IoUtil.copy(in, response.getOutputStream());} catch (Exception e) {log.error(e.getMessage());} finally {if (in != null) {try {in.close();} catch (IOException e) {log.error(e.getMessage());}}}}@Overridepublic Result delete(String id) {SysFile sysFile = baseMapper.selectOne(Wrappers.<SysFile>lambdaQuery().select(SysFile::getId, SysFile::getBucketName, SysFile::getObjectName).eq(SysFile::getDelFlag, 0).eq(SysFile::getId, id));if (sysFile == null) {return Result.error("未找到文件!");}String objectName = sysFile.getObjectName();if (CharSequenceUtil.isBlank(objectName)) {return Result.error("未找到文件!");}// 数据库删除文件int update = baseMapper.update(null, Wrappers.<SysFile>lambdaUpdate().set(SysFile::getDelFlag, null).set(SysFile::getGmtModified, LocalDateTime.now()).set(SysFile::getUpdateBy, sysFile.getId()).eq(SysFile::getId, id));if (update == 0) {Result.error("删除失败!");}// 是否物理删除minio上文件if (minioProperties.isPhysicsDelete()) {try {minioClient.removeObject(RemoveObjectArgs.builder().bucket(sysFile.getBucketName()).object(objectName).build());// minio文件信息数据库逻辑删除minioDeleteToDb(objectName);} catch (Exception e) {log.error(e.getMessage());return Result.error("删除失败!");}}return Result.success("删除成功!");}@Overridepublic Result sort(SysFileSortVo vo) {String id = vo.getId();Integer sort = vo.getSort();if (!StringUtils.hasText(id) || sort == null) {return Result.error("参数错误!");}SysFile sysFile = new SysFile();sysFile.setId(id);sysFile.setSort(sort);sysFile.updateById();return Result.success("编辑成功!");}/*** 文件大小处理** @param fileSize 文件大小,单位B* @param fileSize* @return*/private String formatFileSize(long fileSize) {DecimalFormat df = new DecimalFormat("#.00");String fileSizeizeString;String wrongSize = "0B";if (fileSize == 0) {return wrongSize;}if (fileSize < 1024) {fileSizeizeString = df.format((double) fileSize) + " B";} else if (fileSize < 1048576) {fileSizeizeString = df.format((double) fileSize / 1024) + " KB";} else if (fileSize < 1073741824) {fileSizeizeString = df.format((double) fileSize / 1048576) + " MB";} else {fileSizeizeString = df.format((double) fileSize / 1073741824) + " GB";}return fileSizeizeString;}/*** 获取预览地址路径** @param bucketName 桶名* @param objectName minio文件名*/private String getPreviewUrl(String bucketName, String objectName) {String previewUrl = null;try {// 预览地址previewUrl = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.GET).bucket(bucketName).object(objectName)// 24小时,默认7天.expiry(60 * 60 * 24).expiry(15).build());if (StrUtil.isNotBlank(minioProperties.getPreviewDomain())) {int count = 0;int index = -1;for (int i = 0; i < previewUrl.length(); i++) {if (previewUrl.charAt(i) == '/') {count++;if (count == 3) {index = i;break;}}}if (index != -1) {previewUrl = minioProperties.getPreviewDomain() + previewUrl.substring(index);}}} catch (Exception e) {Console.log(e.getMessage());}return previewUrl;}/*** minio文件信息插入数据库** @param id 主键* @param name 原文件名* @param groupId 分组编号,用于对应多文件* @param fileType fileType* @param suffix suffix* @param size 文件大小,单位字节* @param objectName 桶内文件名*/private void minioInsertToDb(String id, String name, String groupId, String fileType, String suffix, Integer size, String bucketName, String objectName, String previewUrl, int sort) {SysFile sysFile = new SysFile();sysFile.setId(id);sysFile.setName(name);sysFile.setGroupId(groupId);sysFile.setFileType(fileType);sysFile.setSuffix(suffix);sysFile.setSize(size);sysFile.setStorageType("minio");sysFile.setBucketName(bucketName);sysFile.setObjectName(objectName);sysFile.setVisitCount(0);sysFile.setPreviewUrl(previewUrl);sysFile.setSort(sort);baseMapper.insert(sysFile);}/*** minio文件信息数据库逻辑删除** @param objectName 桶内文件名*/private void minioDeleteToDb(String objectName) {SysFile sysFile = baseMapper.selectOne(Wrappers.<SysFile>lambdaQuery().select(SysFile::getId).eq(SysFile::getObjectName, objectName).eq(SysFile::getDelFlag, 0));if (sysFile != null) {baseMapper.update(null, Wrappers.<SysFile>lambdaUpdate().set(SysFile::getDelFlag, null).set(SysFile::getGmtModified, LocalDateTime.now()).eq(SysFile::getId, sysFile.getDelFlag()));}}/*** 预览** @param groupId 分组id*/@Overridepublic boolean preview(String groupId, List<SysFileSelVo> previewList) {List<SysFile> sysFiles = baseMapper.selectList(Wrappers.<SysFile>lambdaQuery().select(SysFile::getId, SysFile::getName, SysFile::getBucketName, SysFile::getObjectName, SysFile::getSuffix, SysFile::getSize, SysFile::getSort).eq(SysFile::getDelFlag, 0).eq(SysFile::getGroupId, groupId).orderByAsc(SysFile::getSort));if (CollUtil.isEmpty(sysFiles)) {return false;}for (SysFile sysFile : sysFiles) {try {// 预览地址String previewUrl = getPreviewUrl(sysFile.getBucketName(), sysFile.getObjectName());// 文件大小并格式化String size = formatFileSize(sysFile.getSize());previewList.add(new SysFileSelVo(sysFile.getId(), sysFile.getName(), sysFile.getSuffix(), size, previewUrl, sysFile.getSort()));} catch (Exception e) {Console.log(e.getMessage());}}return true;}
}
创建SysFileMapper
package com.example.mapper;import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cqcloud.platform.common.data.datascope.DynamicBaseMapper;
import com.cqcloud.platform.dto.SysFileDto;
import com.cqcloud.platform.entity.SysFile;
import com.cqcloud.platform.vo.SysFileSelVo;/*** 系统基础信息--文件管理信息 Mapper 接口* @author weimeilayer@gmail.com ✨* @date 💓💕 2021年5月20日 🐬🐇 💓💕*/
@Mapper
public interface SysFileMapper extends BaseMapper<SysFile> {/*** 排序* @param groupId* @return*/public Integer getMaxSort(@Param("groupId") String groupId);/*** 分页查询SysFile* @param selvo 查询参数* @return*/public IPage<SysFileDto> getSysFileDtoPage(@Param("page")Page page,@Param("query")SysFileSelVo selvo);
}
创建 SysFileMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cqcloud.platform.mapper.SysFileMapper"><resultMap id="sysFileMap" type="com.example.dto.SysFileDto" ><result property="id" column="id"/><result property="name" column="name"/><result property="groupId" column="group_id"/><result property="fileType" column="file_type"/><result property="suffix" column="suffix"/><result property="size" column="size"/><result property="previewUrl" column="preview_url"/><result property="storageType" column="storage_type"/><result property="storageUrl" column="storage_url"/><result property="bucketName" column="bucket_name"/><result property="objectName" column="object_name"/><result property="visitCount" column="visit_count"/><result property="sort" column="sort"/><result property="remarks" column="remarks"/><result property="gmtCreate" column="gmt_create"/><result property="gmtModified" column="gmt_modified"/><result property="createBy" column="create_by"/><result property="updateBy" column="update_by"/><result property="delFlag" column="del_flag"/><result property="tenantId" column="tenant_id"/></resultMap><sql id="sysFileSql">
t.id,t.name,t.group_id,t.file_type,t.suffix,t.size,t.preview_url,t.storage_type,t.storage_url,t.bucket_name,t.object_name,t.visit_count,t.sort,t.remarks,t.gmt_create,t.gmt_modified,t.create_by,t.update_by,t.del_flag,t.tenant_id</sql><select id="getSysFileDtoPage" resultMap="sysFileMap">select <include refid="sysFileSql" />from sys_file t<where>t.del_flag='0'<if test="query.name !=null and query.name !=''">and t.name LIKE '%' || #{name} || '%'</if></where>order by t.gmt_create desc</select><select id="getMaxSort" resultType="java.lang.Integer">selectmax(sort)fromsys_filewheregroup_id = #{groupId}and del_flag = '0'</select>
</mapper>
创建 SysFileController
package com.example.controller;import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cqcloud.platform.common.log.annotation.SysLog;
import com.cqcloud.platform.service.SysFileService;
import com.cqcloud.platform.utils.Result;
import com.cqcloud.platform.vo.SysFileSelVo;
import com.cqcloud.platform.vo.SysFileSortVo;import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;/*** 系统基础信息--文件管理模块* @author weimeilayer@gmail.com* @date 2021-12-13 16:28:32*/
@RestController
@AllArgsConstructor
@RequestMapping("/sysfile")
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class SysFileController {private final SysFileService sysFileService;/*** 上传文件 文件名采用uuid,避免原始文件名中带"-"符号导致下载的时候解析出现异常* @param file 资源* @return R(/bucketName/filename)*/@PostMapping("/uploadOnToken")public Result upload(@RequestParam("file") MultipartFile file) {if (file.isEmpty()) {return Result.error("文件上传失败");}return sysFileService.uploadFile(file);}/*** 获取文件* * @param bucket 桶名称* @param fileName 文件空间/名称* @param response* @return*/@GetMapping("/{bucket}/{fileName}")public void file(@PathVariable String bucket, @PathVariable String fileName, HttpServletResponse response) {sysFileService.getFile(bucket, fileName, response);}/*** 分页查询文件信息列表* @param page* @return*/@GetMapping("/pagelist")public Result getSysFileDtoPage(@ParameterObject Page page,@ParameterObject SysFileSelVo selvo) {return Result.success(sysFileService.getSysFileDtoPage(page, selvo));}/*** 上传文件* @param file 多文件* @param groupId 分组id,用于文件追加*/@PostMapping("/upload")@Parameters({@Parameter(name = "groupId", description = "分组编号,用于对应多文件",example = "1"),@Parameter(name = "isPreview", description = "是否预览", required = true,example = "1"),@Parameter(name = "isPublic", description = "是否公开", required = true,example = "1"),@Parameter(name = "sort", description = "排序", required = true,example = "1")})public Result upload(@RequestParam MultipartFile[] file, String groupId, Boolean isPreview, Boolean isPublic,Integer sort) {return sysFileService.uploadFile(file, groupId, isPreview, isPublic, sort);}/*** 批量预览文件* @param groupId 文件名*/@GetMapping("/preview/{groupId}")public Result preview(@PathVariable("groupId") String groupId) {return sysFileService.preview(groupId);}/*** 下载文件* @param id 主键*/@GetMapping("/download/{id}")public void download(HttpServletResponse response, @PathVariable("id") String id) {sysFileService.download(response, id);}/*** 删除文件* @param id 主键*/@DeleteMapping("/delete/{id}")public Result delete(@PathVariable("id") String id) {return sysFileService.delete(id);}/*** 文件排序* @param vo 排序封装*/@PostMapping("/sort")public Result sort(@RequestBody SysFileSortVo vo) {return sysFileService.sort(vo);}
}