refactor: MinIO 上传图片返回名称不带文件夹和全局异常支持自定义异常信息。

This commit is contained in:
Ray.Hao
2025-01-22 11:56:58 +08:00
parent 8bd38f6302
commit c2ab755cf3
6 changed files with 58 additions and 42 deletions

View File

@@ -20,6 +20,13 @@ public class BusinessException extends RuntimeException {
this.resultCode = errorCode; this.resultCode = errorCode;
} }
public BusinessException(IResultCode errorCode,String message) {
super(message);
this.resultCode = errorCode;
}
public BusinessException(String message, Throwable cause) { public BusinessException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }

View File

@@ -30,7 +30,6 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* 全局系统异常处理器 * 全局系统异常处理器
* <p> * <p>
@@ -219,9 +218,9 @@ public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class) @ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.BAD_REQUEST)
public <T> Result<T> handleBizException(BusinessException e) { public <T> Result<T> handleBizException(BusinessException e) {
log.error("biz exception: {}", e.getMessage()); log.error("biz exception", e);
if (e.getResultCode() != null) { if (e.getResultCode() != null) {
return Result.failed(e.getResultCode()); return Result.failed(e.getResultCode(), e.getMessage());
} }
return Result.failed(e.getMessage()); return Result.failed(e.getMessage());
} }
@@ -239,8 +238,7 @@ public class GlobalExceptionHandler {
|| e instanceof AuthenticationException) { || e instanceof AuthenticationException) {
throw e; throw e;
} }
log.error("unknown exception: {}", e.getMessage()); log.error("unknown exception", e);
e.printStackTrace();
return Result.failed(e.getLocalizedMessage()); return Result.failed(e.getLocalizedMessage());
} }

View File

@@ -3,8 +3,8 @@ package com.youlai.boot.common.result;
/** /**
* 响应码接口 * 响应码接口
* *
* @author Ray * @author Ray.Hao
* @since 2022/2/18 * @since 1.0.0
**/ **/
public interface IResultCode { public interface IResultCode {

View File

@@ -42,7 +42,7 @@ public enum ResultCode implements IResultCode, Serializable {
VOICE_VERIFICATION_CODE_INPUT_ERROR("A0133", "语音校验码输入错误"), VOICE_VERIFICATION_CODE_INPUT_ERROR("A0133", "语音校验码输入错误"),
USER_CERTIFICATE_EXCEPTION("A0140", "用户证件异常"), USER_CERTIFICATE_EXCEPTION("A0140", "用户证件异常"),
USER_CERTIFICATE_TYPE_NOT_SELECTED("A0141", "用户证<EFBFBD><EFBFBD>类型未选择"), USER_CERTIFICATE_TYPE_NOT_SELECTED("A0141", "用户证类型未选择"),
MAINLAND_ID_NUMBER_VERIFICATION_ILLEGAL("A0142", "大陆身份证编号校验非法"), MAINLAND_ID_NUMBER_VERIFICATION_ILLEGAL("A0142", "大陆身份证编号校验非法"),
USER_BASIC_INFORMATION_VERIFICATION_FAILED("A0150", "用户基本信息校验失败"), USER_BASIC_INFORMATION_VERIFICATION_FAILED("A0150", "用户基本信息校验失败"),
@@ -127,12 +127,14 @@ public enum ResultCode implements IResultCode, Serializable {
USER_RESOURCE_NOT_FOUND("A0606", "用户资源不存在"), USER_RESOURCE_NOT_FOUND("A0606", "用户资源不存在"),
/** 二级宏观错误码 */ /** 二级宏观错误码 */
USER_UPLOAD_FILE_EXCEPTION("A0700", "用户上传文件异常"), UPLOAD_FILE_EXCEPTION("A0700", "上传文件异常"),
USER_UPLOAD_FILE_TYPE_MISMATCH("A0701", "用户上传文件类型不匹配"), UPLOAD_FILE_TYPE_MISMATCH("A0701", "上传文件类型不匹配"),
USER_UPLOAD_FILE_TOO_LARGE("A0702", "用户上传文件太大"), UPLOAD_FILE_TOO_LARGE("A0702", "上传文件太大"),
USER_UPLOAD_IMAGE_TOO_LARGE("A0703", "用户上传图片太大"), UPLOAD_IMAGE_TOO_LARGE("A0703", "上传图片太大"),
USER_UPLOAD_VIDEO_TOO_LARGE("A0704", "用户上传视频太大"), UPLOAD_VIDEO_TOO_LARGE("A0704", "上传视频太大"),
USER_UPLOAD_COMPRESSED_FILE_TOO_LARGE("A0705", "用户上传压缩文件太大"), UPLOAD_COMPRESSED_FILE_TOO_LARGE("A0705", "上传压缩文件太大"),
DELETE_FILE_EXCEPTION("A0710", "删除文件异常"),
/** 二级宏观错误码 */ /** 二级宏观错误码 */
USER_CURRENT_VERSION_EXCEPTION("A0800", "用户当前版本异常"), USER_CURRENT_VERSION_EXCEPTION("A0800", "用户当前版本异常"),

View File

@@ -3,6 +3,13 @@ package com.youlai.boot.shared.file.model;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
/**
* 文件信息对象
*
* @author Ray.Hao
* @since 1.0.0
*/
@Schema(description = "文件对象") @Schema(description = "文件对象")
@Data @Data
public class FileInfo { public class FileInfo {

View File

@@ -5,30 +5,30 @@ import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.shared.file.service.FileService; import com.youlai.boot.shared.file.service.FileService;
import com.youlai.boot.shared.file.model.FileInfo; import com.youlai.boot.shared.file.model.FileInfo;
import io.minio.*; import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method; import io.minio.http.Method;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import lombok.Data; import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException; import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/** /**
* MinIO 文件上传服务类 * MinIO 文件上传服务类
* *
* @author haoxr * @author Ray.Hao
* @since 2023/6/2 * @since 2023/6/2
*/ */
@Component @Component
@@ -36,6 +36,7 @@ import java.time.LocalDateTime;
@ConfigurationProperties(prefix = "oss.minio") @ConfigurationProperties(prefix = "oss.minio")
@RequiredArgsConstructor @RequiredArgsConstructor
@Data @Data
@Slf4j
public class MinioFileService implements FileService { public class MinioFileService implements FileService {
/** /**
@@ -77,7 +78,7 @@ public class MinioFileService implements FileService {
* 上传文件 * 上传文件
* *
* @param file 表单文件对象 * @param file 表单文件对象
* @return * @return 文件信息
*/ */
@Override @Override
public FileInfo uploadFile(MultipartFile file) { public FileInfo uploadFile(MultipartFile file) {
@@ -85,16 +86,19 @@ public class MinioFileService implements FileService {
// 创建存储桶(存储桶不存在)如果有搭建好的minio服务建议放在init方法中 // 创建存储桶(存储桶不存在)如果有搭建好的minio服务建议放在init方法中
createBucketIfAbsent(bucketName); createBucketIfAbsent(bucketName);
// 生成文件名(日期文件夹) // 文件后缀
String suffix = FileUtil.getSuffix(file.getOriginalFilename()); String suffix = FileUtil.getSuffix(file.getOriginalFilename());
String uuid = IdUtil.simpleUUID(); // 文件夹名称
String fileName = DateUtil.format(LocalDateTime.now(), "yyyyMMdd") + "/" + uuid + "." + suffix; String dateFolder = DateUtil.format(LocalDateTime.now(), "yyyyMMdd");
// 文件名称
String fileName = IdUtil.simpleUUID() + "." + suffix;
// try-with-resource 语法糖自动释放流 // try-with-resource 语法糖自动释放流
try (InputStream inputStream = file.getInputStream()) { try (InputStream inputStream = file.getInputStream()) {
// 文件上传 // 文件上传
PutObjectArgs putObjectArgs = PutObjectArgs.builder() PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.bucket(bucketName) .bucket(bucketName)
.object(fileName) .object(dateFolder + "/"+ fileName)
.contentType(file.getContentType()) .contentType(file.getContentType())
.stream(inputStream, inputStream.available(), -1) .stream(inputStream, inputStream.available(), -1)
.build(); .build();
@@ -104,15 +108,18 @@ public class MinioFileService implements FileService {
String fileUrl; String fileUrl;
// 未配置自定义域名 // 未配置自定义域名
if (StrUtil.isBlank(customDomain)) { if (StrUtil.isBlank(customDomain)) {
// 获取文件URL
GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder() GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder()
.bucket(bucketName).object(fileName) .bucket(bucketName)
.object(dateFolder + "/"+ fileName)
.method(Method.GET) .method(Method.GET)
.build(); .build();
fileUrl = minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs); fileUrl = minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs);
fileUrl = fileUrl.substring(0, fileUrl.indexOf("?")); fileUrl = fileUrl.substring(0, fileUrl.indexOf("?"));
} else { // 配置自定义文件路径域名 } else {
fileUrl = customDomain + '/' + bucketName + "/" + fileName; // 配置自定义文件路径域名
fileUrl = customDomain + "/"+ bucketName + "/"+ dateFolder + "/"+ fileName;
} }
FileInfo fileInfo = new FileInfo(); FileInfo fileInfo = new FileInfo();
@@ -120,7 +127,8 @@ public class MinioFileService implements FileService {
fileInfo.setUrl(fileUrl); fileInfo.setUrl(fileUrl);
return fileInfo; return fileInfo;
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("文件上传失败"); log.error("上传文件失败", e);
throw new BusinessException(ResultCode.UPLOAD_FILE_EXCEPTION, e.getMessage());
} }
} }
@@ -128,9 +136,8 @@ public class MinioFileService implements FileService {
/** /**
* 删除文件 * 删除文件
* *
* @param filePath 文件路径 http://localhost:9000/default/20221120/test.jpg * @param filePath 文件完整路径
* * @return 是否删除成功
* @return
*/ */
@Override @Override
public boolean deleteFile(String filePath) { public boolean deleteFile(String filePath) {
@@ -152,7 +159,8 @@ public class MinioFileService implements FileService {
minioClient.removeObject(removeObjectArgs); minioClient.removeObject(removeObjectArgs);
return true; return true;
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("文件删除失败", e); log.error("删除文件失败", e);
throw new BusinessException(ResultCode.DELETE_FILE_EXCEPTION, e.getMessage());
} }
} }
@@ -161,17 +169,11 @@ public class MinioFileService implements FileService {
* PUBLIC桶策略 * PUBLIC桶策略
* 如果不配置则新建的存储桶默认是PRIVATE则存储桶文件会拒绝访问 Access Denied * 如果不配置则新建的存储桶默认是PRIVATE则存储桶文件会拒绝访问 Access Denied
* *
* @param bucketName * @param bucketName 存储桶名称
* @return * @return 存储桶策略
*/ */
private static String publicBucketPolicy(String bucketName) { private static String publicBucketPolicy(String bucketName) {
/** // AWS的S3存储桶策略 JSON 格式 https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/userguide/example-bucket-policies.html
* AWS的S3存储桶策略
* Principal: 生效用户对象
* Resource: 指定存储桶
* Action: 操作行为
*/
return "{\"Version\":\"2012-10-17\"," return "{\"Version\":\"2012-10-17\","
+ "\"Statement\":[{\"Effect\":\"Allow\"," + "\"Statement\":[{\"Effect\":\"Allow\","
+ "\"Principal\":{\"AWS\":[\"*\"]}," + "\"Principal\":{\"AWS\":[\"*\"]},"
@@ -185,7 +187,7 @@ public class MinioFileService implements FileService {
/** /**
* 创建存储桶(存储桶不存在) * 创建存储桶(存储桶不存在)
* *
* @param bucketName * @param bucketName 存储桶名称
*/ */
@SneakyThrows @SneakyThrows
private void createBucketIfAbsent(String bucketName) { private void createBucketIfAbsent(String bucketName) {