From 8ca5b1d53b27cdde0367e7dcd1eae1fa10f32309 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com>
Date: Wed, 2 Sep 2026 08:00:19 +0800
Subject: [PATCH] =?UTF-8?q?task-167:=20aiimage.http-client.*=20=E9=85=8D?=
=?UTF-8?q?=E7=BD=AE=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=EF=BC=88connect/r?=
=?UTF-8?q?ead/call=20=E8=B6=85=E6=97=B6=E4=B8=8E=E9=87=8D=E8=AF=95?=
=?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=80=BC=E5=90=8C=E7=8E=B0=E7=8A=B6=E3=80=81?=
=?UTF-8?q?=E9=9D=9E=E6=B3=95=E5=80=BC=E9=92=B3=E5=88=B6=E3=80=81env=20?=
=?UTF-8?q?=E8=A6=86=E7=9B=96=EF=BC=89+=208=20=E6=9D=A1=E6=B5=8B=E8=AF=95?=
=?UTF-8?q?=EF=BC=9Bsurefire=20=E5=8A=A0=20MaxMetaspaceSize=20=E4=BF=AE?=
=?UTF-8?q?=E5=A4=8D=20fork=20=E5=86=85=E5=AD=98=E5=B4=A9=E6=BA=83?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend-java/pom.xml | 2 +-
.../aiimage/config/HttpClientProperties.java | 59 +++++++++++++
.../src/main/resources/application.yml | 8 ++
.../config/HttpClientPropertiesTest.java | 85 +++++++++++++++++++
4 files changed, 153 insertions(+), 1 deletion(-)
create mode 100644 backend-java/src/main/java/com/nanri/aiimage/config/HttpClientProperties.java
create mode 100644 backend-java/src/test/java/com/nanri/aiimage/config/HttpClientPropertiesTest.java
diff --git a/backend-java/pom.xml b/backend-java/pom.xml
index a57f4908..a4e1ee38 100644
--- a/backend-java/pom.xml
+++ b/backend-java/pom.xml
@@ -183,7 +183,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- -XX:+EnableDynamicAgentLoading -Xshare:off -Xmx1536m
+ -XX:+EnableDynamicAgentLoading -Xshare:off -Xmx2g -XX:MaxMetaspaceSize=512m
diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/HttpClientProperties.java b/backend-java/src/main/java/com/nanri/aiimage/config/HttpClientProperties.java
new file mode 100644
index 00000000..7191068e
--- /dev/null
+++ b/backend-java/src/main/java/com/nanri/aiimage/config/HttpClientProperties.java
@@ -0,0 +1,59 @@
+package com.nanri.aiimage.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 统一 HTTP 客户端配置命名空间(task-167)。
+ *
+ * `aiimage.http-client.*`:connect/read/call 超时与重试默认值与现状一致
+ * (RustFS 客户端契约表基线);非法值钳制到合理范围;env 覆盖走宽松绑定。
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "aiimage.http-client")
+public class HttpClientProperties {
+
+ /** 连接超时(毫秒),默认 10000 与现状一致。 */
+ private long connectTimeoutMillis = 10_000;
+
+ /** 读取超时(毫秒),默认 60000 与现状一致。 */
+ private long readTimeoutMillis = 60_000;
+
+ /** 调用总超时(毫秒),默认 90000 与现状一致。 */
+ private long callTimeoutMillis = 90_000;
+
+ /** 最大重试次数(0-10),默认 3 与现状一致。 */
+ private int maxRetries = 3;
+
+ /** 重试基础延迟(毫秒),默认 500 与现状一致。 */
+ private long baseRetryDelayMillis = 500;
+
+ /** 钳制后的连接超时:1s-300s。 */
+ public long effectiveConnectTimeoutMillis() {
+ return clamp(connectTimeoutMillis, 1_000, 300_000);
+ }
+
+ /** 钳制后的读取超时:1s-3600s。 */
+ public long effectiveReadTimeoutMillis() {
+ return clamp(readTimeoutMillis, 1_000, 3_600_000);
+ }
+
+ /** 钳制后的调用总超时:1s-7200s。 */
+ public long effectiveCallTimeoutMillis() {
+ return clamp(callTimeoutMillis, 1_000, 7_200_000);
+ }
+
+ /** 钳制后的重试次数:0-10。 */
+ public int effectiveMaxRetries() {
+ return (int) clamp(maxRetries, 0, 10);
+ }
+
+ private static long clamp(long value, long min, long max) {
+ if (value < min) {
+ return min;
+ }
+ return Math.min(value, max);
+ }
+}
diff --git a/backend-java/src/main/resources/application.yml b/backend-java/src/main/resources/application.yml
index 2667c03f..5b105c93 100644
--- a/backend-java/src/main/resources/application.yml
+++ b/backend-java/src/main/resources/application.yml
@@ -81,6 +81,14 @@ knife4j:
language: zh_cn
aiimage:
+ # ===== HTTP 客户端统一配置命名空间(task-167)=====
+ # 默认值与现状一致(RustFS 客户端契约表基线);非法值由 HttpClientProperties 钳制。
+ http-client:
+ connect-timeout-millis: ${AIIMAGE_HTTP_CLIENT_CONNECT_TIMEOUT_MILLIS:10000}
+ read-timeout-millis: ${AIIMAGE_HTTP_CLIENT_READ_TIMEOUT_MILLIS:60000}
+ call-timeout-millis: ${AIIMAGE_HTTP_CLIENT_CALL_TIMEOUT_MILLIS:90000}
+ max-retries: ${AIIMAGE_HTTP_CLIENT_MAX_RETRIES:3}
+ base-retry-delay-millis: ${AIIMAGE_HTTP_CLIENT_BASE_RETRY_DELAY_MILLIS:500}
# Set a stable server-level ID in 1Panel/Docker with AIIMAGE_INSTANCE_ID.
# For IDEA/local startup, set -Daiimage.instance-id= or configure the AIIMAGE_INSTANCE_ID env var.
# Avoid relying on container hostname/container ID, otherwise task owner continuity may break after restart/redeploy.
diff --git a/backend-java/src/test/java/com/nanri/aiimage/config/HttpClientPropertiesTest.java b/backend-java/src/test/java/com/nanri/aiimage/config/HttpClientPropertiesTest.java
new file mode 100644
index 00000000..79efe7cd
--- /dev/null
+++ b/backend-java/src/test/java/com/nanri/aiimage/config/HttpClientPropertiesTest.java
@@ -0,0 +1,85 @@
+package com.nanri.aiimage.config;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * task-167:aiimage.http-client.* 配置命名空间契约(plan 10)。
+ * 默认值与现状一致;非法值钳制;配置类绑定字段齐全。
+ */
+class HttpClientPropertiesTest {
+
+ private HttpClientProperties properties;
+
+ @BeforeEach
+ void setUp() {
+ properties = new HttpClientProperties();
+ }
+
+ @Test
+ void namespaceDefaultsMatchCurrent() {
+ assertEquals(10_000L, properties.getConnectTimeoutMillis(), "connect 默认 10s(与现状一致)");
+ assertEquals(60_000L, properties.getReadTimeoutMillis(), "read 默认 60s");
+ assertEquals(90_000L, properties.getCallTimeoutMillis(), "call 默认 90s");
+ assertEquals(3, properties.getMaxRetries(), "重试默认 3");
+ assertEquals(500L, properties.getBaseRetryDelayMillis(), "退避基础延迟 500ms");
+ }
+
+ @Test
+ void connectTimeoutBounded() {
+ assertEquals(10_000L, properties.effectiveConnectTimeoutMillis());
+ properties.setConnectTimeoutMillis(1_000);
+ assertEquals(1_000L, properties.effectiveConnectTimeoutMillis());
+ }
+
+ @Test
+ void readTimeoutBounded() {
+ assertEquals(60_000L, properties.effectiveReadTimeoutMillis());
+ properties.setReadTimeoutMillis(3_600_000);
+ assertEquals(3_600_000L, properties.effectiveReadTimeoutMillis());
+ }
+
+ @Test
+ void retryCountBounded() {
+ assertEquals(3, properties.effectiveMaxRetries());
+ properties.setMaxRetries(0);
+ assertEquals(0, properties.effectiveMaxRetries());
+ }
+
+ @Test
+ void invalidValuesClamped() {
+ properties.setConnectTimeoutMillis(-5);
+ assertEquals(1_000L, properties.effectiveConnectTimeoutMillis(), "负数钳制到下限");
+ properties.setReadTimeoutMillis(999_999_999L);
+ assertEquals(3_600_000L, properties.effectiveReadTimeoutMillis(), "超上限钳制");
+ properties.setMaxRetries(99);
+ assertEquals(10, properties.effectiveMaxRetries(), "重试钳制到 10");
+ }
+
+ @Test
+ void callTimeoutBounded() {
+ properties.setCallTimeoutMillis(7_200_000);
+ assertEquals(7_200_000L, properties.effectiveCallTimeoutMillis());
+ properties.setCallTimeoutMillis(0);
+ assertEquals(1_000L, properties.effectiveCallTimeoutMillis());
+ }
+
+ @Test
+ void configClassBindingPrefix() throws Exception {
+ var annotation = HttpClientProperties.class
+ .getAnnotation(org.springframework.boot.context.properties.ConfigurationProperties.class);
+ assertEquals("aiimage.http-client", annotation.prefix(), "绑定 aiimage.http-client 前缀");
+ assertTrue(HttpClientProperties.class.isAnnotationPresent(
+ org.springframework.stereotype.Component.class), "配置类需注册为组件");
+ }
+
+ @Test
+ void envOverrideSemantics() {
+ // env 覆盖走宽松绑定:设置后 effective 反映新值
+ properties.setConnectTimeoutMillis(30_000);
+ assertEquals(30_000L, properties.effectiveConnectTimeoutMillis());
+ }
+}