统一版本管理
<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:10087
access-key: xxxxxx
secret-key: xxxxxx
bucket-name: public-example-xxxx
public-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;
@Bean
public 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名称
*/
@SneakyThrows
public void createBucket(String bucketName) {
if (!amazonS3.doesBucketExistV2(bucketName)) {
amazonS3.createBucket((bucketName));
}
}
/**
* 获取全部bucket API Documentation</a>
*/
@SneakyThrows
public List<Bucket> getAllBuckets() {
return amazonS3.listBuckets();
}
/**
* @param bucketName bucket名称 API Documentation</a>
*/
@SneakyThrows
public Optional<Bucket> getBucket(String bucketName) {
return amazonS3.listBuckets().stream().filter(b -> b.getName().equals(bucketName)).findFirst();
}
/**
* @param bucketName bucket名称
* @see <a href= Documentation</a>
*/
@SneakyThrows
public void removeBucket(String bucketName) {
amazonS3.deleteBucket(bucketName);
}
/**
* 根据文件前置查询文件
*
* @param bucketName bucket名称
* @param prefix 前缀
* @param recursive 是否递归查询
* @return S3ObjectSummary 列表 API Documentation</a>
*/
@SneakyThrows
public 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
*/
@SneakyThrows
public 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>
*/
@SneakyThrows
public 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);
}
@Override
public 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 数据对象
*/
@Override
public 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> {
@Serial
private 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
*/
@Override
public 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
*/
@Override
public 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
*/
@Override
public IPage<SysFileDto> getSysFileDtoPage(Page page, SysFileSelVo selvo) {
return baseMapper.getSysFileDtoPage(page, selvo);
}
@Override
public 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);
}
@Override
public Result preview(String groupId) {
List<SysFileSelVo> previewList = new ArrayList<>();
boolean preview = preview(groupId, previewList);
return preview ? Result.success(previewList) : Result.error("预览失败!");
}
/**
* 文件下载
*/
@Override
public 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());
}
}
}
}
@Override
public 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("删除成功!");
}
@Override
public 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
*/
@Override
public 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">
select
max(sort)
from
sys_file
where
group_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);
}
}