From f10343a4c152d5d50b93613c2c80a4b4cce6a0d1 Mon Sep 17 00:00:00 2001 From: chenxing Date: Mon, 21 Sep 2026 00:17:49 +0800 Subject: [PATCH] =?UTF-8?q?T0.8:=20open-gateway=20=E5=BB=BA=E4=BB=93?= =?UTF-8?q?=E2=80=94=E2=80=94Spring=20Boot=202.7.18/JDK8=20=E9=AA=A8?= =?UTF-8?q?=E6=9E=B6=20+=20EnvelopeWriter=EF=BC=88=E8=A3=B8=E7=A0=81/?= =?UTF-8?q?=E4=BF=A1=E5=B0=81=E4=B8=A4=E5=BD=A2=E6=80=81=EF=BC=8C=E4=BF=A1?= =?UTF-8?q?=E5=B0=81=E4=B8=8E=20SDK=20OpenApiEnvelope=20=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=EF=BC=89+=20CI/Dockerfile/.dockerignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 4 + .gitea/workflows/build.yml | 67 +++++++++++++ .gitignore | 4 + README.md | 9 ++ docker/Dockerfile | 16 ++++ pom.xml | 93 +++++++++++++++++++ .../platform/gateway/GatewayApplication.java | 18 ++++ .../gateway/envelope/EnvelopeWriter.java | 53 +++++++++++ src/main/resources/application.yml | 52 +++++++++++ .../gateway/envelope/EnvelopeWriterTest.java | 49 ++++++++++ 10 files changed, 365 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/build.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docker/Dockerfile create mode 100644 pom.xml create mode 100644 src/main/java/com/yunhai/platform/gateway/GatewayApplication.java create mode 100644 src/main/java/com/yunhai/platform/gateway/envelope/EnvelopeWriter.java create mode 100644 src/main/resources/application.yml create mode 100644 src/test/java/com/yunhai/platform/gateway/envelope/EnvelopeWriterTest.java diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a2c11e5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +.idea +*.md +logs diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..067e2c9 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,67 @@ +name: build + +# open-gateway 开放接口网关 CI(仿 usermsg/service build.yml 平台链,不含部署) +# 触发:push 到 main +# 链路:clone 源码 -> maven 容器构建 jar(复用宿主 m2 缓存)-> cp 固定名 app.jar +# -> docker build -f docker/Dockerfile -> push Nexus docker-hosted +# Nexus 凭据从宿主 prod.env 读入 job env(同机内网 CI,安全边界等同,勿回显) +# 部署:projman 注册应用(open-gateway,空 ctx,端口 10010)后走发布链 Apply 拉起 + +on: + push: + branches: [ main ] + workflow_dispatch: + +jobs: + build-push: + runs-on: ubuntu-latest + container: + image: docker:24 + options: --network infra-infra-net + steps: + - name: 安装 git / curl(alpine) + run: apk add --no-cache git curl + + - name: 拉取源码(内网无 GitHub,手动 clone 本仓库) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git clone --depth 1 --branch main "http://x-access-token:${GITHUB_TOKEN}@gitea.yunhaixingtong.com/${{ github.repository }}.git" /tmp/src + cp -a /tmp/src/. . + rm -rf /tmp/src + + - name: 读取 Nexus 凭据(同机 prod.env,不回显) + run: | + set -e + docker run --rm -v /opt/docker/projman-deploy:/pe:ro alpine sh -c \ + 'grep -E "^PROJMAN_NEXUS_USER=|^PROJMAN_NEXUS_PASSWORD=" /pe/prod.env' > /tmp/nx.env + # prod.env 无 NEXUS_USER 时默认 admin + grep -q '^PROJMAN_NEXUS_USER=' /tmp/nx.env || echo 'PROJMAN_NEXUS_USER=admin' >> /tmp/nx.env + echo "NX_USER=$(grep '^PROJMAN_NEXUS_USER=' /tmp/nx.env | head -1 | cut -d= -f2-)" >> "$GITHUB_ENV" + echo "NX_PASS=$(grep '^PROJMAN_NEXUS_PASSWORD=' /tmp/nx.env | head -1 | cut -d= -f2-)" >> "$GITHUB_ENV" + rm -f /tmp/nx.env + + - name: 构建 jar(maven 容器,复用宿主 m2 缓存) + run: | + set -e + CTR="mavenbuild-open-gateway-$(date +%s)" + trap 'docker rm -f "$CTR" >/dev/null 2>&1 || true' EXIT + docker run -d --name "$CTR" \ + --network infra-infra-net \ + -v /opt/docker/projman-m2:/root/.m2 \ + -w /build maven:3.6.3-jdk-8 sleep 3600 + docker cp "${PWD}/." "$CTR":/build/ + docker exec "$CTR" sh -c "printf '%s' 'nexus-reponexus-internalhttp://nexus:8081/repository/maven-public/truetruealwaysnexus-repo' > /root/.m2/settings.xml" + docker exec "$CTR" sh -c 'cd /build && mvn -B -DskipTests clean package' + docker exec "$CTR" sh -c 'cd /build/target && cp *.jar app.jar' + mkdir -p "${PWD}/target" + docker cp "$CTR":/build/target/app.jar "${PWD}/target/app.jar" + + - name: 构建并推送镜像 + run: | + set -e + TAG=$(echo "$GITHUB_SHA" | cut -c1-8) + echo "${NX_PASS}" | docker login nexus:8083 -u "${NX_USER}" --password-stdin + docker build -t "nexus:8083/docker-hosted/open-gateway-service:${TAG}" -f docker/Dockerfile . + docker push "nexus:8083/docker-hosted/open-gateway-service:${TAG}" + echo "open-gateway-service:${TAG} pushed" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02ac4c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +target/ +.idea/ +*.iml +logs/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..795b335 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# open-gateway 开放接口网关 + +容器网内 oapi 统一入口(路由 / 验签 / 转发 / 台账),后期对外 `open.yunhaixingtong.com` 唯一鉴权面。 +实施依据:`开放接口中心实施方案1.0.md`(云海 workspace 根目录)。 + +- 端口:10010(projman 注册:空 contextPath、needDb=true、infraServices=["redis"]) +- 库表:`docs/sql/01_schema.sql`(注册表/台账/日统计;T1.7 导入) +- 环境值(datasource/redis/routes/observe)一律走平台配置层,jar 内 yml 只留占位注释 +- CI:push main 自动构建 app.jar 并推 `nexus:8083/docker-hosted/open-gateway-service` diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..8d7f78c --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,16 @@ +# 平台标准运行镜像(对齐 projman 注入模板):SPRING_PROFILES_ACTIVE 由容器 env 注入 +# (platform,{bucket},命令行不写死——写死会以更高优先级压掉平台公共配置层) +# CI 已先 mvn package 并 cp 成固定名 target/app.jar;构建上下文 = 仓库根 +FROM eclipse-temurin:8-jre + +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl tzdata \ + && ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ + && echo "Asia/Shanghai" > /etc/timezone \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY target/app.jar /app/app.jar + +EXPOSE 10010 +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS:--Xms512m -Xmx1024m} -Dfile.encoding=UTF-8 -jar /app/app.jar"] diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e5e44c4 --- /dev/null +++ b/pom.xml @@ -0,0 +1,93 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + + com.yunhai.platform + open-gateway + 1.0.0-SNAPSHOT + jar + open-gateway + 开放接口网关:容器网内 oapi 统一入口(路由/验签/转发/台账),后期对外 open 域名唯一鉴权面 + + + 1.8 + UTF-8 + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-actuator + + + com.github.ben-manes.caffeine + caffeine + + + org.apache.httpcomponents + httpclient + + + + com.yunhai.platform + yh-app-auth-spring-boot-starter + 1.0.0-SNAPSHOT + + + com.mysql + mysql-connector-j + runtime + + + com.h2database + h2 + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + true + + + + + app + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + diff --git a/src/main/java/com/yunhai/platform/gateway/GatewayApplication.java b/src/main/java/com/yunhai/platform/gateway/GatewayApplication.java new file mode 100644 index 0000000..bc1bbbe --- /dev/null +++ b/src/main/java/com/yunhai/platform/gateway/GatewayApplication.java @@ -0,0 +1,18 @@ +package com.yunhai.platform.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 开放接口网关入口。 + *

+ * 环境值(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); + } +} diff --git a/src/main/java/com/yunhai/platform/gateway/envelope/EnvelopeWriter.java b/src/main/java/com/yunhai/platform/gateway/envelope/EnvelopeWriter.java new file mode 100644 index 0000000..bda1368 --- /dev/null +++ b/src/main/java/com/yunhai/platform/gateway/envelope/EnvelopeWriter.java @@ -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)。 + *

+ * 两形态(§1.1/D3): + *

+ * 提供方响应原样透传,不经本组件。 + */ +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)); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..ffb912a --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,52 @@ +server: + # 网关默认端口(projman 注册档案:port=10010;compose 模板端口注入以盘点结论为准) + port: 10010 + +spring: + application: + name: open-gateway + datasource: + # 注册表/台账库(open_gateway)——连接串由平台配置层注入,jar 内不留环境值 + url: # 例(平台配置层):jdbc:mysql://:/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 diff --git a/src/test/java/com/yunhai/platform/gateway/envelope/EnvelopeWriterTest.java b/src/test/java/com/yunhai/platform/gateway/envelope/EnvelopeWriterTest.java new file mode 100644 index 0000000..0508528 --- /dev/null +++ b/src/test/java/com/yunhai/platform/gateway/envelope/EnvelopeWriterTest.java @@ -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); + } +}