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:
32
pom.xml
32
pom.xml
@@ -26,7 +26,31 @@
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
</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>
|
||||
<!-- 强制 okhttp 4.12.0:minio-java 要求 okhttp>=4.11.0,放在 dependencies 首位确保胜出,
|
||||
避免运行时 NoSuchMethodError(okhttp3.RequestBody.create 等) -->
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>4.12.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- spring-boot-devtools -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -297,6 +321,14 @@
|
||||
<version>3.17.4</version>
|
||||
</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>
|
||||
<groupId>com.aliyun</groupId>
|
||||
|
||||
@@ -37,6 +37,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import io.minio.MinioClient;
|
||||
import io.minio.UploadObjectArgs;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -115,26 +117,18 @@ public class AliOssController extends BaseController {
|
||||
|
||||
// 上传文件结果
|
||||
FileRecord result;
|
||||
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
|
||||
// 创建OSSClient实例。
|
||||
// MinIO 兼容 S3 协议,需开启 PathStyle 访问(endpoint/bucket/key)并使用 V4 签名
|
||||
OSS ossClient;
|
||||
if ("minio".equals(uploadMethod)) {
|
||||
ClientBuilderConfiguration minioConfig = new ClientBuilderConfiguration();
|
||||
// MinIO 兼容 S3:V4 签名 + path-style。自定义域名无法从 endpoint 推导 region,
|
||||
// 必须显式指定(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-style(endpoint/bucket/key)。
|
||||
minioConfig.setSupportCname(false);
|
||||
ossClient = OSSClientBuilder.create()
|
||||
boolean isMinio = "minio".equals(uploadMethod);
|
||||
// MinIO 用官方 minio-java SDK(原生 AWS4-HMAC-SHA256 + path-style);
|
||||
// aliyun OSS SDK 无 AWS4 签名器,对接不了 MinIO,因此 minio 分支独立使用 MinioClient。
|
||||
OSS ossClient = null;
|
||||
MinioClient minioClient = null;
|
||||
if (isMinio) {
|
||||
minioClient = MinioClient.builder()
|
||||
.endpoint(endpoint)
|
||||
.credentialsProvider(credentialsProvider)
|
||||
.clientConfiguration(minioConfig)
|
||||
.region("us-east-1")
|
||||
.credentials(accessKeyId, accessKeySecret)
|
||||
.build();
|
||||
} else {
|
||||
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
|
||||
ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
|
||||
}
|
||||
|
||||
@@ -145,16 +139,18 @@ public class AliOssController extends BaseController {
|
||||
String path = upload.getAbsolutePath().replace("\\", "/").substring(dir.length());
|
||||
String originalName = file.getOriginalFilename();
|
||||
|
||||
// 创建PutObjectRequest对象。
|
||||
// 上传文件到对象存储。
|
||||
if (isMinio) {
|
||||
// minio-java:path 此时无前导 "/",作为对象名(如 2026/08/07/xxx.png),默认 region=us-east-1
|
||||
minioClient.uploadObject(UploadObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.object(path)
|
||||
.filename(upload.getAbsolutePath())
|
||||
.build());
|
||||
} else {
|
||||
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, path, upload);
|
||||
// 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
|
||||
// ObjectMetadata metadata = new ObjectMetadata();
|
||||
// metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
|
||||
// metadata.setObjectAcl(CannedAccessControlList.Private);
|
||||
// putObjectRequest.setMetadata(metadata);
|
||||
|
||||
// 上传文件。
|
||||
PutObjectResult ossResult = ossClient.putObject(putObjectRequest);
|
||||
ossClient.putObject(putObjectRequest);
|
||||
}
|
||||
|
||||
// 保存记录并返回
|
||||
result = new FileRecord();
|
||||
@@ -185,7 +181,7 @@ public class AliOssController extends BaseController {
|
||||
|
||||
// 根据文件类型设置缩略图和预览图
|
||||
// MinIO 不支持阿里云 x-oss-process 图片处理参数,统一返回原图 URL
|
||||
boolean isMinio = "minio".equals(uploadMethod);
|
||||
isMinio = "minio".equals(uploadMethod);
|
||||
if (FileServerUtil.isImage(contentType)) {
|
||||
if (isMinio) {
|
||||
result.setThumbnail(bucketDomain + path);
|
||||
@@ -236,10 +232,15 @@ public class AliOssController extends BaseController {
|
||||
+ "a serious internal problem while trying to communicate with OSS, "
|
||||
+ "such as not being able to access the network.");
|
||||
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 {
|
||||
if (ossClient != null) {
|
||||
ossClient.shutdown();
|
||||
}
|
||||
// MinioClient 内部复用 HttpClient 连接池,无需显式关闭
|
||||
}
|
||||
return fail("上传失败", null);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import com.gxwebsoft.common.system.param.FileRecordParam;
|
||||
import com.gxwebsoft.common.system.service.CompanyService;
|
||||
import com.gxwebsoft.common.system.service.FileRecordService;
|
||||
import com.gxwebsoft.common.system.service.SettingService;
|
||||
import io.minio.MinioClient;
|
||||
import io.minio.RemoveObjectArgs;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -96,32 +98,31 @@ public class FileRecordServiceImpl extends ServiceImpl<FileRecordMapper, FileRec
|
||||
String accessKeyId = uploadConfig.getString("accessKeyId");
|
||||
String accessKeySecret = uploadConfig.getString("accessKeySecret");
|
||||
String uploadMethod = uploadConfig.getString("uploadMethod");
|
||||
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
|
||||
// MinIO 兼容 S3 协议,需开启 PathStyle 访问 + V4 签名
|
||||
OSS ossClient;
|
||||
if ("minio".equals(uploadMethod)) {
|
||||
ClientBuilderConfiguration minioConfig = new ClientBuilderConfiguration();
|
||||
// MinIO 兼容 S3:V4 签名 + path-style。自定义域名无法从 endpoint 推导 region,
|
||||
// 必须显式指定(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-style(endpoint/bucket/key)。
|
||||
minioConfig.setSupportCname(false);
|
||||
ossClient = OSSClientBuilder.create()
|
||||
boolean isMinio = "minio".equals(uploadMethod);
|
||||
OSS ossClient = null;
|
||||
MinioClient minioClient = null;
|
||||
if (isMinio) {
|
||||
// MinIO 用官方 minio-java SDK(原生 AWS4 + path-style),aliyun OSS SDK 无法对接 MinIO
|
||||
minioClient = MinioClient.builder()
|
||||
.endpoint(endpoint)
|
||||
.credentialsProvider(credentialsProvider)
|
||||
.clientConfiguration(minioConfig)
|
||||
.region("us-east-1")
|
||||
.credentials(accessKeyId, accessKeySecret)
|
||||
.build();
|
||||
} else {
|
||||
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
|
||||
ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
|
||||
}
|
||||
for (FileRecord fileRecord : fileRecords) {
|
||||
fileRecord.setPath(StrUtil.replace(fileRecord.getPath(), bucketDomain.concat("/"), ""));
|
||||
try {
|
||||
// 删除远程文件
|
||||
if (isMinio) {
|
||||
minioClient.removeObject(RemoveObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.object(fileRecord.getPath())
|
||||
.build());
|
||||
} else {
|
||||
ossClient.deleteObject(bucketName, fileRecord.getPath());
|
||||
}
|
||||
// 释放空间大小
|
||||
if (fileRecord.getCompanyId() > 0) {
|
||||
Company company = companyService.getById(fileRecord.getCompanyId());
|
||||
|
||||
Reference in New Issue
Block a user