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
+4
View File
@@ -0,0 +1,4 @@
.git
.idea
*.md
logs
+67
View File
@@ -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' '<settings><profiles><profile><id>nexus-repo</id><repositories><repository><id>nexus-internal</id><url>http://nexus:8081/repository/maven-public/</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled><updatePolicy>always</updatePolicy></snapshots></repository></repositories></profile></profiles><activeProfiles><activeProfile>nexus-repo</activeProfile></activeProfiles></settings>' > /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"
+4
View File
@@ -0,0 +1,4 @@
target/
.idea/
*.iml
logs/
+9
View File
@@ -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`
+16
View File
@@ -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"]
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.yunhai.platform</groupId>
<artifactId>open-gateway</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>open-gateway</name>
<description>开放接口网关:容器网内 oapi 统一入口(路由/验签/转发/台账),后期对外 open 域名唯一鉴权面</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- 白名单(开放接口中心 T0.8):本清单外依赖不得引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- 复用 starter 的 SignKeyResolver/SignUtil(传递引入 data-redis);密钥链单一事实源 -->
<dependency>
<groupId>com.yunhai.platform</groupId>
<artifactId>yh-app-auth-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<finalName>app</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}