T0.8: open-gateway 建仓——Spring Boot 2.7.18/JDK8 骨架 + EnvelopeWriter(裸码/信封两形态,信封与 SDK OpenApiEnvelope 字段对齐)+ CI/Dockerfile/.dockerignore

This commit is contained in:
chenxing
2026-09-21 00:17:49 +08:00
commit f10343a4c1
10 changed files with 365 additions and 0 deletions
@@ -0,0 +1,18 @@
package com.yunhai.platform.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 开放接口网关入口。
* <p>
* 环境值(datasource/redis/routes/observe)一律走平台配置层(application-platform.yml),
* jar 内 application.yml 只留占位注释——环境值打进 jar 曾造成全站事故(§0 规则 8)。
*/
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
@@ -0,0 +1,53 @@
package com.yunhai.platform.gateway.envelope;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 网关统一错误写出组件(开放接口中心 T0.8)。
* <p>
* 两形态(§1.1/D3):
* <ul>
* <li><b>裸码</b>:网关自产错误一律裸 HTTP 状态码、无响应体——401 验签失败/缺失、403 可见性拒绝、
* 404 target/路由未知、400 URI 非法、413 body 超限、429 限流(RAW)、503 熔断(RAW)、
* 502 提供方连接失败、504 提供方超时(F5:鉴权错 = 裸 HTTP,不裹信封);</li>
* <li><b>信封</b>:HTTP 200 + {@code {code, msg, data:null}}——字段结构与 SDK 侧
* {@code OpenApiEnvelope}(yh-openapi-client core,自 yh-api-std 提炼)的解析形态逐字段对齐:
* unwrap 读 {@code code}(非 0 抛业务异常)与 {@code msg},网关治理信封写同名同义字段,
* 老 SDK 无需升级即可按 DException 收到 code/msg。</li>
* </ul>
* 提供方响应<b>原样透传</b>,不经本组件。
*/
public final class EnvelopeWriter {
/** 治理错误码(D3 预留;error-style 开关 T3.1 才消费,本阶段只预留常量) */
public static final int RATE_LIMITED = 7;
public static final int CIRCUIT_OPEN = 8;
private static final ObjectMapper MAPPER = new ObjectMapper();
private EnvelopeWriter() {
}
/** 网关自产错误:裸 HTTP 状态码,无响应体(不裹信封) */
public static void writeBare(HttpServletResponse response, int status) throws IOException {
response.reset();
response.setStatus(status);
response.setContentLength(0);
}
/** 业务信封:HTTP 200 + {code, msg, data:null}(与 SDK OpenApiEnvelope 解析形态对齐) */
public static void writeEnvelope(HttpServletResponse response, int code, String msg) throws IOException {
response.reset();
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json;charset=UTF-8");
ObjectNode node = MAPPER.createObjectNode();
node.put("code", code);
node.put("msg", msg);
node.putNull("data");
response.getWriter().write(MAPPER.writeValueAsString(node));
}
}
+52
View File
@@ -0,0 +1,52 @@
server:
# 网关默认端口(projman 注册档案:port=10010;compose 模板端口注入以盘点结论为准)
port: 10010
spring:
application:
name: open-gateway
datasource:
# 注册表/台账库(open_gateway)——连接串由平台配置层注入,jar 内不留环境值
url: # 例(平台配置层):jdbc:mysql://<host>:<port>/open_gateway?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: #
password: #
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
# 表结构由 docs/sql/01_schema.sql 交付,应用不自动建表
ddl-auto: none
redis:
# 密钥链(sign:app:{appKey})走共享 Redis db0——由平台配置层注入
host: #
port: #
main:
allow-bean-definition-overriding: true
management:
endpoints:
web:
exposure:
include: health
endpoint:
health:
probes:
enabled: true
yh:
app:
auth:
# starter 验签配置(T0.11 起 GatewayKeyService 复用 SignKeyResolver)
# include/exclude 路径策略由平台配置层下发;本文件不配任何真实值
enabled: # 默认随 starter
gateway:
# 路由表(T0.12):target -> base-url,值 = 容器网内服务直连 URL,走平台配置层
routes: #
# userbase: http://userbase-service:8080
# userclass: http://userclass-service:8080
# cms: http://cms-service:8080
# svc-hall: http://svc-hall-service:8080
# flow-center: http://flow-center-service:8080
# usermsg: http://usermsg-service:8080
# notice: http://notice-service:8080
# 观察模式(T1.6):true = 全判定只记不拦(R4:P1 只观察);生产初期保持 true
observe: true
@@ -0,0 +1,49 @@
package com.yunhai.platform.gateway.envelope;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletResponse;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
/**
* EnvelopeWriter 两形态写出契约(T0.8 TDD):
* 裸码无信封 JSON;信封 HTTP 200 + code/msg/data 与 SDK OpenApiEnvelope 解析形态逐字段对齐。
*/
class EnvelopeWriterTest {
@Test
void bare_status_written() throws IOException {
MockHttpServletResponse res = new MockHttpServletResponse();
EnvelopeWriter.writeBare(res, 401);
assertThat(res.getStatus()).isEqualTo(401);
assertThat(res.getContentAsString()).as("裸码无信封 JSON").isEmpty();
MockHttpServletResponse res404 = new MockHttpServletResponse();
EnvelopeWriter.writeBare(res404, 404);
assertThat(res404.getStatus()).isEqualTo(404);
assertThat(res404.getContentAsString()).isEmpty();
MockHttpServletResponse res502 = new MockHttpServletResponse();
EnvelopeWriter.writeBare(res502, 502);
assertThat(res502.getStatus()).isEqualTo(502);
assertThat(res502.getContentAsString()).isEmpty();
}
@Test
void envelope_written_with_code_and_message() throws IOException {
MockHttpServletResponse res = new MockHttpServletResponse();
EnvelopeWriter.writeEnvelope(res, 7, "rate limited");
assertThat(res.getStatus()).as("信封恒走 HTTP 200").isEqualTo(200);
assertThat(res.getContentType()).contains("application/json");
assertThat(res.getContentAsString())
.isEqualTo("{\"code\":7,\"msg\":\"rate limited\",\"data\":null}");
}
@Test
void rate_limit_constants_reserved() {
assertThat(EnvelopeWriter.RATE_LIMITED).isEqualTo(7);
assertThat(EnvelopeWriter.CIRCUIT_OPEN).isEqualTo(8);
}
}