From e9ed3967943149839c061b0821f7274899ed5e98 Mon Sep 17 00:00:00 2001
From: zhaowenyu <35589726@qq.com>
Date: Wed, 04 Feb 2026 16:44:00 +0800
Subject: [PATCH] 2026-02-04 优化minio上传,兼容minio所有接口的转发

---
 ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioGeneralProxyController.java |  118 +++++++++++++++++++
 /dev/null                                                                                    |  180 ------------------------------
 ard-sample/src/main/java/com/ard/sample/config/RestTemplateConfig.java                       |   22 +++
 3 files changed, 140 insertions(+), 180 deletions(-)

diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioGeneralProxyController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioGeneralProxyController.java
new file mode 100644
index 0000000..00715f5
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioGeneralProxyController.java
@@ -0,0 +1,118 @@
+package com.ard.web.controller.sample.media;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.util.StreamUtils;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestTemplate;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Enumeration;
+
+@Slf4j
+@Api(tags = "机场上传minio")
+@RestController
+@RequestMapping("/minioTransfer")
+public class MinioGeneralProxyController {
+
+    @Value("${oss.endpoint}")
+    private String minioServerEndpoint;
+
+    private final RestTemplate restTemplate;
+    private static final String BASE_PREFIX = "/minioTransfer";
+
+    public MinioGeneralProxyController(RestTemplate restTemplate) {
+        this.restTemplate = restTemplate;
+    }
+
+    @ApiOperation("MinIO原生接口代理(无感中转)")
+    @RequestMapping(value = "/**")
+    public ResponseEntity<byte[]> proxyAllRequest(HttpServletRequest request) {
+        try {
+            // 1. 解析请求基础信息
+            String methodStr = request.getMethod();
+            String rawUri = URLDecoder.decode(request.getRequestURI(), StandardCharsets.UTF_8);
+            String queryString = request.getQueryString();
+
+            log.info("转发请求 | 方法:{} | URI:{} | 参数:{}", methodStr, rawUri, queryString);
+
+            // 2. 构建MinIO目标地址
+            String targetPath = rawUri.substring(BASE_PREFIX.length());
+            StringBuilder targetUrl = new StringBuilder(minioServerEndpoint);
+            targetUrl.append(targetPath);
+            if (queryString != null && !queryString.isBlank()) {
+                targetUrl.append("?").append(queryString);
+            }
+            URI uri = URI.create(targetUrl.toString());
+
+            // 3. 转换为Spring标准HttpMethod(核心修复点)
+            HttpMethod httpMethod = HttpMethod.valueOf(methodStr);
+
+            // 4. 复制请求头+请求体
+            HttpHeaders headers = copyRequestHeaders(request);
+            byte[] requestBody = extractRequestBody(request);
+            HttpEntity<byte[]> requestEntity = new HttpEntity<>(requestBody, headers);
+
+            // 5. 修复exchange方法调用,匹配Spring官方重载方法
+            ResponseEntity<byte[]> response = restTemplate.exchange(
+                    uri,
+                    httpMethod,
+                    requestEntity,
+                    byte[].class
+            );
+
+            log.info("转发成功 | 目标地址:{} | 状态码:{}", targetUrl, response.getStatusCode());
+            // 原样回传所有响应信息
+            return new ResponseEntity<>(
+                    response.getBody(),
+                    response.getHeaders(),
+                    response.getStatusCode()
+            );
+
+        } catch (Exception e) {
+            log.error("代理转发执行异常", e);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+                    .body(("代理服务异常:" + e.getMessage()).getBytes(StandardCharsets.UTF_8));
+        }
+    }
+
+    /**
+     * 透传所有请求头
+     */
+    private HttpHeaders copyRequestHeaders(HttpServletRequest request) {
+        HttpHeaders headers = new HttpHeaders();
+        Enumeration<String> headerNames = request.getHeaderNames();
+        if (headerNames == null) {
+            return headers;
+        }
+        while (headerNames.hasMoreElements()) {
+            String name = headerNames.nextElement();
+            Enumeration<String> values = request.getHeaders(name);
+            headers.addAll(name, Collections.list(values));
+        }
+        // 移除Host头,避免转发地址冲突
+        headers.remove(HttpHeaders.HOST);
+        return headers;
+    }
+
+    /**
+     * 读取请求体数据
+     */
+    private byte[] extractRequestBody(HttpServletRequest request) {
+        try {
+            return StreamUtils.copyToByteArray(request.getInputStream());
+        } catch (IOException e) {
+            log.warn("读取请求体失败", e);
+            return new byte[0];
+        }
+    }
+}
\ No newline at end of file
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioProxyUploadController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioProxyUploadController.java
deleted file mode 100644
index 97a59b7..0000000
--- a/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioProxyUploadController.java
+++ /dev/null
@@ -1,180 +0,0 @@
-package com.ard.web.controller.sample.media;
-
-import com.ard.common.config.OssConfiguration;
-import com.ard.oss.model.CredentialsDTO;
-import com.ard.oss.service.impl.OssServiceContext;
-import com.ard.sample.media.Response.S3ErrorResponse;
-import com.ard.sample.media.Response.S3PutObjectResponse;
-import io.minio.BucketExistsArgs;
-import io.minio.MinioClient;
-import io.minio.PutObjectArgs;
-import io.minio.credentials.StaticProvider;
-import io.minio.errors.ErrorResponseException;
-import io.minio.errors.MinioException;
-import io.minio.messages.ErrorResponse;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.codec.digest.DigestUtils;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
-
-import javax.servlet.http.HttpServletRequest;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.net.URLDecoder;
-import java.nio.charset.StandardCharsets;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.time.OffsetDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * 适配 MinIO SDK 8.3.7 + 服务器 RELEASE.2024-08-03
- * 承接大疆 PUT 上传请求,中转上传到 MinIO HTTPS 接口
- */
-@Api(tags = "机场上传minio")
-@Slf4j
-@RestController
-@RequestMapping("/minioTransfer") // 根路径,匹配 Nginx 转发的 /dock/、/kmz/ 路径
-public class MinioProxyUploadController {
-
-    // 注入你的凭证服务(保持原有注入方式不变)
-    @Autowired
-    private OssServiceContext ossService;
-
-    // S3标准错误码 -> HTTP状态码 映射表(行业通用标准)
-    private static final Map<String, HttpStatus> S3_ERROR_CODE_MAP;
-
-    // 初始化常见错误映射
-    static {
-        S3_ERROR_CODE_MAP = new HashMap<>();
-        S3_ERROR_CODE_MAP.put("NoSuchBucket", HttpStatus.NOT_FOUND);
-        S3_ERROR_CODE_MAP.put("NoSuchKey", HttpStatus.NOT_FOUND);
-        S3_ERROR_CODE_MAP.put("AccessDenied", HttpStatus.FORBIDDEN);
-        S3_ERROR_CODE_MAP.put("InvalidCredentials", HttpStatus.FORBIDDEN);
-        S3_ERROR_CODE_MAP.put("BucketAlreadyExists", HttpStatus.CONFLICT);
-        S3_ERROR_CODE_MAP.put("InvalidRequest", HttpStatus.BAD_REQUEST);
-    }
-
-    /**
-     * 完全复刻 MinIO 原生 PUT 上传接口
-     * 通配符匹配所有路径,保留原始URI格式
-     */
-    @ApiOperation("MinIO原生PUT接口代理(无感中转)")
-    @PutMapping(value = "/{bucketName}/**", consumes = MediaType.ALL_VALUE)
-    public ResponseEntity<?> proxyMinioPutObject(
-            @PathVariable String bucketName,
-            // 透传大疆设备原始请求体,不修改、不封装
-            @RequestBody(required = false) byte[] rawRequestBody,
-            HttpServletRequest request
-    ) {
-        log.info("接收参数bucketName:{}",bucketName);
-        // ===================== 核心修复:安全路径解析,替换替代截取,杜绝索引错误 =====================
-        // String contextPath = request.getContextPath();
-        String rawUri = URLDecoder.decode(request.getRequestURI(), StandardCharsets.UTF_8);
-        // 剔除上下文路径,得到纯业务路径
-        String purePath = rawUri.substring("/minioTransfer".length());
-        String bucketPrefix = "/" + bucketName;
-
-        log.info("中转MinIO 路径解析开始 | 原始URI:{} | 纯业务路径:{} | 桶前缀:{}", rawUri, purePath, bucketPrefix);
-
-        // 校验路径是否以桶前缀开头
-        if (!purePath.startsWith(bucketPrefix)) {
-            return buildNativeS3Error("InvalidRequest", "请求路径格式不匹配", rawUri, HttpStatus.BAD_REQUEST);
-        }
-
-        // 修复点:replace替换前缀,替代 substring 长度截取,彻底解决索引错位问题
-        String objectKey = purePath.replaceFirst("^" + bucketPrefix, "");
-        // 兜底处理空路径
-        if (objectKey.isEmpty()) {
-            objectKey = "/";
-        }
-
-        log.info("中转MinIO 路径解析成功 | 纯业务路径:{} | 桶名:{} | 最终对象路径:{}",
-                purePath, bucketName, objectKey);
-
-        // byte[] requestData = rawRequestBody == null ? new byte[0] : rawRequestBody;
-
-        try {
-            // ===================== 2. 复用原有逻辑:获取临时凭证 =====================
-            var credentials = ossService.getCredentials();
-            if (credentials == null || credentials.getAccessKeyId() == null || credentials.getSecurityToken() == null) {
-                log.error("临时凭证获取失败");
-                return buildNativeS3Error("InvalidCredentials", "临时凭证无效", rawUri, HttpStatus.FORBIDDEN);
-            }
-
-            // ===================== 3. 复用原有逻辑:构建MinIO客户端 =====================
-            MinioClient minioClient = MinioClient.builder()
-                    .endpoint(OssConfiguration.endpoint)
-                    .credentialsProvider(new StaticProvider(
-                            credentials.getAccessKeyId(),
-                            credentials.getAccessKeySecret(),
-                            credentials.getSecurityToken()
-                    ))
-                    .build();
-
-            // 校验桶存在性(兼容原生接口行为,桶不存在返回404)
-            boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
-            if (!bucketExists) {
-                return buildNativeS3Error("NoSuchBucket", "指定存储桶不存在", bucketName, HttpStatus.NOT_FOUND);
-            }
-
-            // ===================== 4. 透传请求:原样转发大疆原始数据到MinIO,无业务篡改 =====================
-            // 空请求体=大疆原生创建目录请求,直接透传;带请求体=上传文件,原生处理
-            byte[] requestBody = rawRequestBody == null ? new byte[0] : rawRequestBody;
-            try (ByteArrayInputStream inputStream = new ByteArrayInputStream(requestBody)) {
-                minioClient.putObject(
-                        PutObjectArgs.builder()
-                                .bucket(bucketName)
-                                .object(objectKey)
-                                // 透传原始流、原始长度,完全复刻原生请求
-                                .stream(inputStream, requestBody.length, -1)
-                                // 透传原生Content-Type,无修改
-                                .contentType(request.getContentType() == null ? "application/octet-stream" : request.getContentType())
-                                .build()
-                );
-            }
-
-            // ===================== 5. 原生接口成功响应:200 OK + 空响应体(核心对齐!)=====================
-            log.info("中转上传成功 | 路径:{}", objectKey);
-            return ResponseEntity.ok()
-                    // 无任何响应体,与MinIO原生行为完全一致
-                    .body(null);
-
-        } catch (ErrorResponseException e) {
-            // ===================== 最终修正:无任何无效方法调用 =====================
-            ErrorResponse nativeError = e.errorResponse();
-            String errorCode = nativeError.code();
-            // 映射状态码,无匹配则默认500
-            HttpStatus status = S3_ERROR_CODE_MAP.getOrDefault(errorCode, HttpStatus.INTERNAL_SERVER_ERROR);
-
-            log.error("MinIO服务错误 | 错误码:{} | 信息:{}", errorCode, nativeError.message());
-            // 透传MinIO原生XML错误体,大疆设备完美兼容
-            return ResponseEntity.status(status)
-                    .contentType(MediaType.APPLICATION_XML)
-                    .body(nativeError);
-        } catch (MinioException e) {
-            log.error("MinIO客户端异常:{}", e.getMessage());
-            return buildNativeS3Error("ClientError", e.getMessage(), rawUri, HttpStatus.INTERNAL_SERVER_ERROR);
-        } catch (Exception e) {
-            log.error("代理中转通用异常:{}", e.getMessage(), e);
-            return buildNativeS3Error("InternalError", "服务器代理异常", rawUri, HttpStatus.INTERNAL_SERVER_ERROR);
-        }
-    }
-
-    /**
-     * 兜底构造S3标准XML错误响应,兼容大疆设备解析
-     */
-    private ResponseEntity<S3ErrorResponse> buildNativeS3Error(String code, String message, String resource, HttpStatus status) {
-        S3ErrorResponse error = new S3ErrorResponse(code, message, resource);
-        return ResponseEntity.status(status)
-                .contentType(MediaType.APPLICATION_XML)
-                .body(error);
-    }
-}
\ No newline at end of file
diff --git a/ard-sample/src/main/java/com/ard/sample/config/RestTemplateConfig.java b/ard-sample/src/main/java/com/ard/sample/config/RestTemplateConfig.java
new file mode 100644
index 0000000..c56f2a5
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/config/RestTemplateConfig.java
@@ -0,0 +1,22 @@
+package com.ard.sample.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class RestTemplateConfig {
+
+    @Bean
+    public RestTemplate restTemplate() {
+        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+        // 连接超时:10s
+        factory.setConnectTimeout(10000);
+        // 读取超时:5min(适配大文件上传/下载)
+        factory.setReadTimeout(300000);
+        // 开启流式请求,避免大文件加载到内存
+        factory.setBufferRequestBody(false);
+        return new RestTemplate(factory);
+    }
+}

--
Gitblit v1.9.3