🐛 MinIO 客户端支持 /oss 路径前缀并固定 region,修复合并分片失败

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-14 00:50:27 +08:00
parent 64b4ad99fc
commit fe78152515
6 changed files with 197 additions and 46 deletions
@@ -6,6 +6,9 @@ package org.springblade.transport.config;
import io.minio.MinioClient;
import lombok.extern.slf4j.Slf4j;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.beans.factory.annotation.Value;
@@ -14,34 +17,49 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.net.URI;
import java.util.concurrent.TimeUnit;
/**
* 凭证图片 MinIO 客户端配置。
* <p>
* 连接参数由 Nacos 的 file.storage.minio 配置提供。
* <ul>
* <li>{@code endpoint}:内网地址,供服务端 put/get,不能带 path</li>
* <li>{@code transform-endpoint}:外网地址(可带 /oss),仅用于浏览器预签名</li>
* <li>{@code endpoint}:建议内网地址;若误配为 https://host/oss,会自动剥离 path 并用拦截器补回</li>
* <li>{@code transform-endpoint}:外网地址(可带 /oss),用于浏览器预签名</li>
* </ul>
* 未配置 {@code file.storage.minio.endpoint} 时不注册该客户端,
* 以免凭证上传功能缺失配置导致整个 blade-transport 服务无法启动。
*/
@Slf4j
@Configuration
@ConditionalOnProperty(prefix = "file.storage.minio", name = "endpoint")
public class VoucherMinioConfig {
private static final String DEFAULT_REGION = "us-east-1";
@Bean
public MinioClient voucherMinioClient(
@Value("${file.storage.minio.endpoint:${minio.endpoint:}}") String endpoint,
@Value("${file.storage.minio.access-key-id:${minio.access-key:}}") String accessKey,
@Value("${file.storage.minio.access-key-secret:${minio.secret-key:}}") String secretKey) {
String clientEndpoint = resolveClientEndpoint(endpoint);
if (!clientEndpoint.equals(endpoint == null ? null : endpoint.trim())) {
log.warn("MinIO endpoint 含 path,已剥离为 {}(原值={})。服务端读写请配置内网 endpoint,外网请用 transform-endpoint",
clientEndpoint, endpoint);
String origin = resolveClientEndpoint(endpoint);
String pathPrefix = resolvePathPrefix(endpoint);
if (StringUtil.isNotBlank(pathPrefix)) {
log.warn("MinIO endpoint 含 path,将按 origin={} pathPrefix={} 访问;建议 endpoint 使用内网地址",
origin, pathPrefix);
}
return MinioClient.builder().endpoint(clientEndpoint).credentials(accessKey, secretKey).build();
return create(endpoint, accessKey, secretKey);
}
public static MinioClient create(String endpoint, String accessKey, String secretKey) {
String origin = resolveClientEndpoint(endpoint);
String pathPrefix = resolvePathPrefix(endpoint);
MinioClient.Builder builder = MinioClient.builder()
.endpoint(origin)
.credentials(accessKey, secretKey)
.region(DEFAULT_REGION);
if (StringUtil.isNotBlank(pathPrefix)) {
builder.httpClient(buildHttpClient(pathPrefix), true);
}
return builder.build();
}
/**
@@ -62,4 +80,34 @@ public class VoucherMinioConfig {
}
return origin.toString();
}
private static String resolvePathPrefix(String endpoint) {
URI uri = URI.create(endpoint.trim());
String path = uri.getPath();
if (StringUtil.isBlank(path) || "/".equals(path)) {
return "";
}
String normalized = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
return normalized.startsWith("/") ? normalized : "/" + normalized;
}
private static OkHttpClient buildHttpClient(String pathPrefix) {
return new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(chain -> {
Request original = chain.request();
HttpUrl originalUrl = original.url();
String encodedPath = originalUrl.encodedPath();
if (encodedPath.startsWith(pathPrefix + "/") || encodedPath.equals(pathPrefix)) {
return chain.proceed(original);
}
HttpUrl newUrl = originalUrl.newBuilder()
.encodedPath(pathPrefix + encodedPath)
.build();
return chain.proceed(original.newBuilder().url(newUrl).build());
})
.build();
}
}
@@ -1525,11 +1525,8 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
}
synchronized (this) {
if (transformPresignClient == null) {
transformPresignClient = MinioClient.builder()
.endpoint(org.springblade.transport.config.VoucherMinioConfig.resolveClientEndpoint(minioTransformEndpoint))
.credentials(minioAccessKey, minioSecretKey)
.region("us-east-1")
.build();
transformPresignClient = org.springblade.transport.config.VoucherMinioConfig.create(
minioTransformEndpoint, minioAccessKey, minioSecretKey);
}
return transformPresignClient;
}