diff --git a/docs/sql/03_target.sql b/docs/sql/03_target.sql new file mode 100644 index 0000000..aff4cbb --- /dev/null +++ b/docs/sql/03_target.sql @@ -0,0 +1,18 @@ +-- ===================================================================== +-- 开放接口中心 target 地址表(E11,T-D1a) +-- 语义:target_code → 提供方 base-url(容器网内直连)。地址解析优先级 DB > yml(yh.gateway.routes); +-- DB 无行 / enabled=0 / base_url 空白 三者一律回落 yml,故本表可整表为空、可逐行灰度。 +-- 写入方:projman Apply 派生(E12)+ 存量回填(E13);网关运行时只读, +-- 改行后 POST /api/internal/routes/refresh 重载即生效(不重启)。 +-- 可重复执行:CREATE TABLE IF NOT EXISTS(不 DROP——生产回填数据不可丢) +-- ===================================================================== + +CREATE TABLE IF NOT EXISTS open_api_target ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + target_code VARCHAR(32) NOT NULL COMMENT '目标应用 ctx(剥掉首斜杠,如 userbase/notice/flowcenter)', + base_url VARCHAR(256) NOT NULL COMMENT '提供方 base-url(如 http://notice-service:8080,不含路径)', + enabled TINYINT NOT NULL DEFAULT 1, + gmt_create DATETIME NOT NULL, + gmt_modified DATETIME NOT NULL, + UNIQUE KEY uk_target (target_code) +); diff --git a/src/main/java/com/yunhai/platform/gateway/entity/OpenApiTarget.java b/src/main/java/com/yunhai/platform/gateway/entity/OpenApiTarget.java new file mode 100644 index 0000000..6b51cd3 --- /dev/null +++ b/src/main/java/com/yunhai/platform/gateway/entity/OpenApiTarget.java @@ -0,0 +1,66 @@ +package com.yunhai.platform.gateway.entity; + +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.PrePersist; +import javax.persistence.PreUpdate; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import java.time.LocalDateTime; + +/** + * target 地址表(E11 T-D1a;DDL 见 docs/sql/03_target.sql,字段与列逐一对齐)。 + *
+ * 与 yml {@code yh.gateway.routes} 同语义(target → base-url),解析优先级 DB > yml:
+ * 无行 / enabled=0 / base_url 空白一律回落 yml,故派生写入(projman Apply)可逐行灰度。
+ * 网关运行时只读,改行后走 {@code /api/internal/routes/refresh} 重载。
+ */
+@Entity
+@Table(name = "open_api_target", uniqueConstraints = @UniqueConstraint(
+ name = "uk_target", columnNames = {"target_code"}))
+@Getter
+@Setter
+public class OpenApiTarget {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "target_code", nullable = false, length = 32)
+ private String targetCode;
+
+ /** 提供方 base-url,不含路径(转发 = base-url + 完整原始 URI,换主机不换路径) */
+ @Column(name = "base_url", nullable = false, length = 256)
+ private String baseUrl;
+
+ @Column(name = "enabled", nullable = false)
+ private boolean enabled = true;
+
+ @Column(name = "gmt_create", nullable = false)
+ private LocalDateTime gmtCreate;
+
+ @Column(name = "gmt_modified", nullable = false)
+ private LocalDateTime gmtModified;
+
+ @PrePersist
+ void preCreate() {
+ LocalDateTime now = LocalDateTime.now();
+ if (gmtCreate == null) {
+ gmtCreate = now;
+ }
+ if (gmtModified == null) {
+ gmtModified = now;
+ }
+ }
+
+ @PreUpdate
+ void preUpdate() {
+ gmtModified = LocalDateTime.now();
+ }
+}
diff --git a/src/main/java/com/yunhai/platform/gateway/repository/OpenApiTargetRepository.java b/src/main/java/com/yunhai/platform/gateway/repository/OpenApiTargetRepository.java
new file mode 100644
index 0000000..be6be95
--- /dev/null
+++ b/src/main/java/com/yunhai/platform/gateway/repository/OpenApiTargetRepository.java
@@ -0,0 +1,11 @@
+package com.yunhai.platform.gateway.repository;
+
+import com.yunhai.platform.gateway.entity.OpenApiTarget;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface OpenApiTargetRepository extends JpaRepository
+ * E11 追加 DB>yml 四例:DB 有行覆盖 yml、DB 无行回落 yml、enabled=0 回落 yml、
+ * refresh 后新增行生效。target 快照经真实 {@link RouteRegistry}(mock repository)走,
+ * 不 stub 解析逻辑本身。
*/
class TargetRouterTest {
private TargetRouter router;
+ private RouteRegistry registry;
+ private List
* 网关只做 oapi:未知 target / 单段 URI / 第二段非 oapi(如 /{target}/api/x)一律返回 null
- * = 裸 404({@code /api} 与页面不属于网关职责)。
+ * = 裸 404({@code /api} 与页面不属于网关职责)。
+ * base-url 解析优先级 DB(open_api_target)> yml(yh.gateway.routes)(E11):
+ * DB 无行、禁用行、空白地址行都按「无行」处理,回落 yml,故 yml 恒为兜底不被绕过。
*/
@Component
public class TargetRouter {
@@ -33,11 +35,20 @@ public class TargetRouter {
@Autowired
private GatewayProperties properties;
+ /** target 地址 DB 快照持有者(E11);解析优先级 DB > yml */
+ @Autowired
+ private RouteRegistry routeRegistry;
+
/** 测试/显式装配入口 */
void setProperties(GatewayProperties properties) {
this.properties = properties;
}
+ /** 测试/显式装配入口 */
+ void setRouteRegistry(RouteRegistry routeRegistry) {
+ this.routeRegistry = routeRegistry;
+ }
+
/**
* @param requestUri 原始请求 URI(未解码,如 /userbase/oapi/x)
* @param queryString 原始 query(无 '?' 前缀;null = 无)
@@ -64,7 +75,10 @@ public class TargetRouter {
if (!rest.equals("/oapi") && !rest.startsWith("/oapi/")) {
return null;
}
- String baseUrl = properties.getRoutes().get(target);
+ String baseUrl = routeRegistry.targetBaseUrl(target);
+ if (baseUrl == null) {
+ baseUrl = properties.getRoutes().get(target); // DB 无行(含禁用/空白行)→ yml 兜底
+ }
if (baseUrl == null || baseUrl.trim().isEmpty()) {
return null; // 未知 target = 裸 404
}
diff --git a/src/test/java/com/yunhai/platform/gateway/route/RouteRegistryTest.java b/src/test/java/com/yunhai/platform/gateway/route/RouteRegistryTest.java
index 88a465c..8fdd957 100644
--- a/src/test/java/com/yunhai/platform/gateway/route/RouteRegistryTest.java
+++ b/src/test/java/com/yunhai/platform/gateway/route/RouteRegistryTest.java
@@ -1,7 +1,9 @@
package com.yunhai.platform.gateway.route;
import com.yunhai.platform.gateway.entity.OpenApiRoute;
+import com.yunhai.platform.gateway.entity.OpenApiTarget;
import com.yunhai.platform.gateway.repository.OpenApiRouteRepository;
+import com.yunhai.platform.gateway.repository.OpenApiTargetRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@@ -109,4 +111,27 @@ class RouteRegistryTest {
assertThat(spec.authMode).as("refresh 后新增条目生效").isEqualTo("ANON");
assertThat(spec.cacheTtlSeconds).isEqualTo(120);
}
+
+ @Test
+ void reloadTargets_装载数只计有效行_空白地址不入快照() {
+ // E11:target 地址快照与路由条目同库不同表,装载口径一致(enabled 行 + 非空地址)
+ OpenApiTargetRepository targetRepo = Mockito.mock(OpenApiTargetRepository.class);
+ when(targetRepo.findByEnabledTrue()).thenReturn(Arrays.asList(
+ targetRow("userbase", "http://userbase-service:8080"),
+ targetRow("notice", " ")));
+ RouteRegistry reg = new RouteRegistry();
+ ReflectionTestUtils.setField(reg, "targetRepo", targetRepo);
+ assertThat(reg.reloadTargets()).as("空白地址行不装载").isEqualTo(1);
+ assertThat(reg.targetBaseUrl("userbase")).isEqualTo("http://userbase-service:8080");
+ assertThat(reg.targetBaseUrl("notice")).as("空白地址 = 无 DB 行语义,由 TargetRouter 回落 yml").isNull();
+ assertThat(reg.targetBaseUrl("nobody")).as("DB 无此 target").isNull();
+ }
+
+ private static OpenApiTarget targetRow(String code, String baseUrl) {
+ OpenApiTarget t = new OpenApiTarget();
+ t.setTargetCode(code);
+ t.setBaseUrl(baseUrl);
+ t.setEnabled(true);
+ return t;
+ }
}
diff --git a/src/test/java/com/yunhai/platform/gateway/route/TargetRouterTest.java b/src/test/java/com/yunhai/platform/gateway/route/TargetRouterTest.java
index cad395f..ce178d1 100644
--- a/src/test/java/com/yunhai/platform/gateway/route/TargetRouterTest.java
+++ b/src/test/java/com/yunhai/platform/gateway/route/TargetRouterTest.java
@@ -1,20 +1,33 @@
package com.yunhai.platform.gateway.route;
+import com.yunhai.platform.gateway.entity.OpenApiTarget;
+import com.yunhai.platform.gateway.repository.OpenApiTargetRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.test.util.ReflectionTestUtils;
+import java.util.ArrayList;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
/**
* TargetRouter 用例(T0.12 TDD):已知 target forwardUrl 逐字符断言(换主机不换路径)、
* 未知 target / 单段 / 非 oapi 第二段一律裸 404(null)、base-url 尾斜杠归一化。
+ *