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>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
<dependencies>
|
<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 -->
|
<!-- 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>
|
||||||
|
|||||||
@@ -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 兼容 S3:V4 签名 + 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-style(endpoint/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对象。
|
// 上传文件到对象存储。
|
||||||
|
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);
|
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, path, upload);
|
||||||
// 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
|
ossClient.putObject(putObjectRequest);
|
||||||
// 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);
|
|
||||||
|
|
||||||
// 保存记录并返回
|
// 保存记录并返回
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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-style),aliyun OSS SDK 无法对接 MinIO
|
||||||
// MinIO 兼容 S3:V4 签名 + 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-style(endpoint/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 {
|
||||||
// 删除远程文件
|
// 删除远程文件
|
||||||
|
if (isMinio) {
|
||||||
|
minioClient.removeObject(RemoveObjectArgs.builder()
|
||||||
|
.bucket(bucketName)
|
||||||
|
.object(fileRecord.getPath())
|
||||||
|
.build());
|
||||||
|
} else {
|
||||||
ossClient.deleteObject(bucketName, fileRecord.getPath());
|
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());
|
||||||
|
|||||||
Reference in New Issue
Block a user