From e77353b47836664ece73b775489361977a216ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Fri, 7 Aug 2026 09:23:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(minio):=20=E9=9B=86=E6=88=90=20MinIO=20?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=B1=BB=E5=B9=B6=E4=BC=98=E5=8C=96=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E5=AD=98=E5=82=A8=E5=AE=A2=E6=88=B7=E7=AB=AF=E6=9E=84?= =?UTF-8?q?=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 MinioUtil 工具类,统一封装 MinIO SDK 的常用操作 - 规范化 endpoint 处理,避免签名校验时因路径错误失败 - 统一使用 MinioUtil.buildClient 创建 MinIO 客户端,避免自动探测 region 导致错误 - 实现自动创建 bucket 及设置匿名只读策略,兼容裸桶场景 - 进程内缓存已准备好的 bucket,减少重复请求 - 修正 AliOssController 和 FileRecordServiceImpl 的 MinIO 客户端初始化逻辑 --- .../common/core/utils/MinioUtil.java | 151 ++++++++++++++++++ .../system/controller/AliOssController.java | 13 +- .../service/impl/FileRecordServiceImpl.java | 11 +- 3 files changed, 159 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/gxwebsoft/common/core/utils/MinioUtil.java diff --git a/src/main/java/com/gxwebsoft/common/core/utils/MinioUtil.java b/src/main/java/com/gxwebsoft/common/core/utils/MinioUtil.java new file mode 100644 index 0000000..eb3829a --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/core/utils/MinioUtil.java @@ -0,0 +1,151 @@ +package com.gxwebsoft.common.core.utils; + +import io.minio.BucketExistsArgs; +import io.minio.GetBucketPolicyArgs; +import io.minio.MakeBucketArgs; +import io.minio.MinioClient; +import io.minio.SetBucketPolicyArgs; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * MinIO 对象存储工具类 + * + *
说明:aliyun-sdk-oss 只实现了阿里云自家的 OSS 签名(OSSV1/V2/V4Signer),
+ * 不含 AWS4-HMAC-SHA256,因此无法对接只接受 AWS4 的 MinIO。
+ * 所有 MinIO 操作统一走官方 minio-java SDK。
+ *
+ * @author WebSoft
+ * @since 2026-08-07
+ */
+public class MinioUtil {
+
+ /**
+ * MinIO 默认 region。
+ * 必须显式指定,否则 minio-java 会先发 {@code GET /{bucket}?location=} 做 region 自动探测,
+ * 该探测请求在 MinIO + 反向代理场景下容易触发 SignatureDoesNotMatch。
+ */
+ public static final String DEFAULT_REGION = "us-east-1";
+
+ /**
+ * 已确保「存在 + 已配置匿名只读策略」的 bucket 缓存(endpoint|bucket)。
+ * 进程内缓存,避免每次上传都多发两次 HTTP 请求。
+ */
+ private static final Set 本方法不会抛异常:即使当前 AK 没有建桶/改策略权限,也只打日志,
+ * 后续的上传动作照常执行(真正失败会由上传自身报错)。
+ *
+ * @return true 表示 bucket 已就绪
+ */
+ public static boolean ensureBucketReady(MinioClient client, String endpoint, String bucketName) {
+ if (client == null || bucketName == null || bucketName.isEmpty()) {
+ return false;
+ }
+ String cacheKey = normalizeEndpoint(endpoint) + "|" + bucketName;
+ if (READY_BUCKETS.contains(cacheKey)) {
+ return true;
+ }
+ try {
+ boolean exists = client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
+ if (!exists) {
+ client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
+ client.setBucketPolicy(SetBucketPolicyArgs.builder()
+ .bucket(bucketName)
+ .config(buildPublicReadPolicy(bucketName))
+ .build());
+ System.out.println("[MinIO] 自动创建 bucket 并配置匿名只读策略: " + bucketName);
+ } else {
+ // minio-java 在无策略时返回空串而非抛错
+ String policy = client.getBucketPolicy(GetBucketPolicyArgs.builder().bucket(bucketName).build());
+ if (policy == null || policy.trim().isEmpty()) {
+ client.setBucketPolicy(SetBucketPolicyArgs.builder()
+ .bucket(bucketName)
+ .config(buildPublicReadPolicy(bucketName))
+ .build());
+ System.out.println("[MinIO] 已为存量 bucket 补充匿名只读策略: " + bucketName);
+ }
+ }
+ READY_BUCKETS.add(cacheKey);
+ return true;
+ } catch (Exception e) {
+ // 不阻断上传:权限不足或网络异常时仅告警
+ System.out.println("[MinIO] ensureBucketReady 失败(不影响上传), bucket=" + bucketName
+ + ", reason=" + e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 手动失效缓存(例如后台修改了存储配置后调用)
+ */
+ public static void evictBucketCache(String endpoint, String bucketName) {
+ READY_BUCKETS.remove(normalizeEndpoint(endpoint) + "|" + bucketName);
+ }
+
+ /**
+ * 清空全部缓存
+ */
+ public static void clearBucketCache() {
+ READY_BUCKETS.clear();
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/common/system/controller/AliOssController.java b/src/main/java/com/gxwebsoft/common/system/controller/AliOssController.java
index 832ccf3..d5d5c29 100644
--- a/src/main/java/com/gxwebsoft/common/system/controller/AliOssController.java
+++ b/src/main/java/com/gxwebsoft/common/system/controller/AliOssController.java
@@ -24,6 +24,7 @@ import com.aliyuncs.profile.IClientProfile;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.config.ConfigProperties;
import com.gxwebsoft.common.core.utils.FileServerUtil;
+import com.gxwebsoft.common.core.utils.MinioUtil;
import com.gxwebsoft.common.core.utils.RedisUtil;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
@@ -103,9 +104,7 @@ public class AliOssController extends BaseController {
String uploadMethod = settingInfo.getString("uploadMethod");
// 规范化 endpoint:去掉结尾斜杠,避免 minio-java 拼接出双斜杠导致签名不匹配
- if (StrUtil.isNotBlank(endpoint) && endpoint.endsWith("/")) {
- endpoint = endpoint.substring(0, endpoint.length() - 1);
- }
+ endpoint = MinioUtil.normalizeEndpoint(endpoint);
// 判断是否登录
String authorization = getAuthorization();
@@ -130,11 +129,9 @@ public class AliOssController extends BaseController {
if (isMinio) {
// 显式指定 region=us-east-1,跳过 minio-java 的 getBucketLocation 自动探测
// (该探测 GET 在 MinIO + 反代下会触发 SignatureDoesNotMatch)
- minioClient = MinioClient.builder()
- .endpoint(endpoint)
- .credentials(accessKeyId, accessKeySecret)
- .region("us-east-1")
- .build();
+ minioClient = MinioUtil.buildClient(endpoint, accessKeyId, accessKeySecret);
+ // bucket 不存在则自动创建,并套上匿名只读策略(存量裸桶也会自动补策略)
+ MinioUtil.ensureBucketReady(minioClient, endpoint, bucketName);
} else {
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
diff --git a/src/main/java/com/gxwebsoft/common/system/service/impl/FileRecordServiceImpl.java b/src/main/java/com/gxwebsoft/common/system/service/impl/FileRecordServiceImpl.java
index 1600daf..df0d087 100644
--- a/src/main/java/com/gxwebsoft/common/system/service/impl/FileRecordServiceImpl.java
+++ b/src/main/java/com/gxwebsoft/common/system/service/impl/FileRecordServiceImpl.java
@@ -13,6 +13,7 @@ import com.aliyun.oss.common.auth.DefaultCredentialProvider;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.core.utils.CommonUtil;
+import com.gxwebsoft.common.core.utils.MinioUtil;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.Company;
@@ -99,20 +100,14 @@ public class FileRecordServiceImpl extends ServiceImpl
+ *
+ * 结果会缓存在进程内,同一个 endpoint+bucket 只检查一次。
+ *
+ *