T0.8: open-gateway 建仓——Spring Boot 2.7.18/JDK8 骨架 + EnvelopeWriter(裸码/信封两形态,信封与 SDK OpenApiEnvelope 字段对齐)+ CI/Dockerfile/.dockerignore
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
.git
|
||||
.idea
|
||||
*.md
|
||||
logs
|
||||
@@ -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"
|
||||
@@ -0,0 +1,4 @@
|
||||
target/
|
||||
.idea/
|
||||
*.iml
|
||||
logs/
|
||||
@@ -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`
|
||||
@@ -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"]
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user