feat(storage): 支持 MinIO 存储并优化上传与删除逻辑

- 引入 MinIO 官方 Java SDK,替代 aliyun OSS SDK 处理与 MinIO 的交互
- 兼容 MinIO 的 AWS4-HMAC-SHA256 签名及 path-style 访问
- 区分上传和删除操作中 MinIO 与阿里云 OSS 的客户端实例创建及调用
- 统一文件路径处理,调整图片处理策略以兼容 MinIO 不支持的参数
- 增加对 MinIO 客户端异常的捕获与兼容处理
- 在 pom.xml 中添加对 okhttp 和 okio 依赖的版本锁定,解决版本冲突问题
This commit is contained in:
2026-08-07 03:46:49 +08:00
parent b9ec1fcdb9
commit 5ca3280ddc
3 changed files with 79 additions and 45 deletions

32
pom.xml
View File

@@ -26,7 +26,31 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties> </properties>
<dependencyManagement>
<dependencies>
<!-- 强制 okhttp/okio 版本,避免 minio-java(要求 okhttp>=4.11.0) 与微信/支付 SDK 引入的 okhttp3 旧版冲突 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>3.6.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies> <dependencies>
<!-- 强制 okhttp 4.12.0minio-java 要求 okhttp>=4.11.0,放在 dependencies 首位确保胜出,
避免运行时 NoSuchMethodErrorokhttp3.RequestBody.create 等) -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!-- spring-boot-devtools --> <!-- spring-boot-devtools -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@@ -297,6 +321,14 @@
<version>3.17.4</version> <version>3.17.4</version>
</dependency> </dependency>
<!-- MinIO 官方 Java SDK用于对接 MinIO 兼容 S3 存储。
注意 aliyun OSS SDK 无 AWS4-HMAC-SHA256 签名器,无法对接 MinIO故 minio 分支单独用此 SDK。 -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.7</version>
</dependency>
<!-- 阿里云 内容安全审核 --> <!-- 阿里云 内容安全审核 -->
<dependency> <dependency>
<groupId>com.aliyun</groupId> <groupId>com.aliyun</groupId>

View File

@@ -37,6 +37,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.minio.MinioClient;
import io.minio.UploadObjectArgs;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@@ -115,26 +117,18 @@ public class AliOssController extends BaseController {
// 上传文件结果 // 上传文件结果
FileRecord result; FileRecord result;
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret); boolean isMinio = "minio".equals(uploadMethod);
// 创建OSSClient实例。 // MinIO 用官方 minio-java SDK原生 AWS4-HMAC-SHA256 + path-style
// MinIO 兼容 S3 协议,需开启 PathStyle 访问endpoint/bucket/key并使用 V4 签名 // aliyun OSS SDK 无 AWS4 签名器,对接不了 MinIO因此 minio 分支独立使用 MinioClient。
OSS ossClient; OSS ossClient = null;
if ("minio".equals(uploadMethod)) { MinioClient minioClient = null;
ClientBuilderConfiguration minioConfig = new ClientBuilderConfiguration(); if (isMinio) {
// MinIO 兼容 S3V4 签名 + path-style。自定义域名无法从 endpoint 推导 region minioClient = MinioClient.builder()
// 必须显式指定MinIO 默认 us-east-1否则 OSSV4Signer.getRegion() 为 null 触发 NPE。
minioConfig.setSignatureVersion(SignVersion.V4);
minioConfig.setExtractSettingFromEndpoint(true);
// 关闭 CNAME默认 true 会让 SDK 把自定义域名当 CNAME走 bucket.endpoint 虚拟主机风格,
// 触发 *.minio-api.websoft.top 证书不匹配。关闭后强制 path-styleendpoint/bucket/key
minioConfig.setSupportCname(false);
ossClient = OSSClientBuilder.create()
.endpoint(endpoint) .endpoint(endpoint)
.credentialsProvider(credentialsProvider) .credentials(accessKeyId, accessKeySecret)
.clientConfiguration(minioConfig)
.region("us-east-1")
.build(); .build();
} else { } else {
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider); ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
} }
@@ -145,16 +139,18 @@ public class AliOssController extends BaseController {
String path = upload.getAbsolutePath().replace("\\", "/").substring(dir.length()); String path = upload.getAbsolutePath().replace("\\", "/").substring(dir.length());
String originalName = file.getOriginalFilename(); String originalName = file.getOriginalFilename();
// 创建PutObjectRequest对象 // 上传文件到对象存储
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, path, upload); if (isMinio) {
// 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。 // minio-javapath 此时无前导 "/",作为对象名(如 2026/08/07/xxx.png默认 region=us-east-1
// ObjectMetadata metadata = new ObjectMetadata(); minioClient.uploadObject(UploadObjectArgs.builder()
// metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString()); .bucket(bucketName)
// metadata.setObjectAcl(CannedAccessControlList.Private); .object(path)
// putObjectRequest.setMetadata(metadata); .filename(upload.getAbsolutePath())
.build());
// 上传文件。 } else {
PutObjectResult ossResult = ossClient.putObject(putObjectRequest); PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, path, upload);
ossClient.putObject(putObjectRequest);
}
// 保存记录并返回 // 保存记录并返回
result = new FileRecord(); result = new FileRecord();
@@ -185,7 +181,7 @@ public class AliOssController extends BaseController {
// 根据文件类型设置缩略图和预览图 // 根据文件类型设置缩略图和预览图
// MinIO 不支持阿里云 x-oss-process 图片处理参数,统一返回原图 URL // MinIO 不支持阿里云 x-oss-process 图片处理参数,统一返回原图 URL
boolean isMinio = "minio".equals(uploadMethod); isMinio = "minio".equals(uploadMethod);
if (FileServerUtil.isImage(contentType)) { if (FileServerUtil.isImage(contentType)) {
if (isMinio) { if (isMinio) {
result.setThumbnail(bucketDomain + path); result.setThumbnail(bucketDomain + path);
@@ -236,10 +232,15 @@ public class AliOssController extends BaseController {
+ "a serious internal problem while trying to communicate with OSS, " + "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network."); + "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage()); System.out.println("Error Message:" + ce.getMessage());
} catch (Exception e) {
// 兼容 minio-java 抛出的异常MinioException / IOException 等)
System.out.println("Caught an upload Exception: " + e.getMessage());
e.printStackTrace();
} finally { } finally {
if (ossClient != null) { if (ossClient != null) {
ossClient.shutdown(); ossClient.shutdown();
} }
// MinioClient 内部复用 HttpClient 连接池,无需显式关闭
} }
return fail("上传失败", null); return fail("上传失败", null);
} }

View File

@@ -22,6 +22,8 @@ import com.gxwebsoft.common.system.param.FileRecordParam;
import com.gxwebsoft.common.system.service.CompanyService; import com.gxwebsoft.common.system.service.CompanyService;
import com.gxwebsoft.common.system.service.FileRecordService; import com.gxwebsoft.common.system.service.FileRecordService;
import com.gxwebsoft.common.system.service.SettingService; import com.gxwebsoft.common.system.service.SettingService;
import io.minio.MinioClient;
import io.minio.RemoveObjectArgs;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -96,32 +98,31 @@ public class FileRecordServiceImpl extends ServiceImpl<FileRecordMapper, FileRec
String accessKeyId = uploadConfig.getString("accessKeyId"); String accessKeyId = uploadConfig.getString("accessKeyId");
String accessKeySecret = uploadConfig.getString("accessKeySecret"); String accessKeySecret = uploadConfig.getString("accessKeySecret");
String uploadMethod = uploadConfig.getString("uploadMethod"); String uploadMethod = uploadConfig.getString("uploadMethod");
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret); boolean isMinio = "minio".equals(uploadMethod);
// MinIO 兼容 S3 协议,需开启 PathStyle 访问 + V4 签名 OSS ossClient = null;
OSS ossClient; MinioClient minioClient = null;
if ("minio".equals(uploadMethod)) { if (isMinio) {
ClientBuilderConfiguration minioConfig = new ClientBuilderConfiguration(); // MinIO 用官方 minio-java SDK原生 AWS4 + path-stylealiyun OSS SDK 无法对接 MinIO
// MinIO 兼容 S3V4 签名 + path-style。自定义域名无法从 endpoint 推导 region minioClient = MinioClient.builder()
// 必须显式指定MinIO 默认 us-east-1否则 OSSV4Signer.getRegion() 为 null 触发 NPE。
minioConfig.setSignatureVersion(SignVersion.V4);
minioConfig.setExtractSettingFromEndpoint(true);
// 关闭 CNAME默认 true 会让 SDK 把自定义域名当 CNAME走 bucket.endpoint 虚拟主机风格,
// 触发 *.minio-api.websoft.top 证书不匹配。关闭后强制 path-styleendpoint/bucket/key
minioConfig.setSupportCname(false);
ossClient = OSSClientBuilder.create()
.endpoint(endpoint) .endpoint(endpoint)
.credentialsProvider(credentialsProvider) .credentials(accessKeyId, accessKeySecret)
.clientConfiguration(minioConfig)
.region("us-east-1")
.build(); .build();
} else { } else {
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider); ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
} }
for (FileRecord fileRecord : fileRecords) { for (FileRecord fileRecord : fileRecords) {
fileRecord.setPath(StrUtil.replace(fileRecord.getPath(), bucketDomain.concat("/"), "")); fileRecord.setPath(StrUtil.replace(fileRecord.getPath(), bucketDomain.concat("/"), ""));
try { try {
// 删除远程文件 // 删除远程文件
ossClient.deleteObject(bucketName, fileRecord.getPath()); if (isMinio) {
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(bucketName)
.object(fileRecord.getPath())
.build());
} else {
ossClient.deleteObject(bucketName, fileRecord.getPath());
}
// 释放空间大小 // 释放空间大小
if (fileRecord.getCompanyId() > 0) { if (fileRecord.getCompanyId() > 0) {
Company company = companyService.getById(fileRecord.getCompanyId()); Company company = companyService.getById(fileRecord.getCompanyId());