Files
Edu/services/api-gateway/docs/02-architecture-design.md
SpecialX 4307f6b73c feat(api-gateway): 实现 W1-W8 网关硬化与 P2-P5 路由扩展
依据 coord-final-decisions §3.8 W1-W8 裁决与
president-final-rulings §2.15/§2.16/§2.19 完整实现网关硬化:

- W1/W2: 错误码 GW_ 前缀 + ActionState 信封响应体
- W3: 全量替换为 log/slog 结构化日志
- W4: /readyz 并行 ping 9 下游 + 软失败规则
- W5: 7 个业务 Prometheus 指标 + /metrics 端点
- W6: tracer 资源属性补全(name/version/env/host)
- W7: DevMode=true && ENV=production panic 防护
- W8: 保持共享 downstream 熔断

P2 RS256 升级:接入 shared-go/jwks.Fetcher(TTL 5min)。
P2.7+P3-P5 路由扩展:student/parent/messages/dashboard。
文档同步:README/01/02/known-issues,arch.db 已更新。
质量校验:go vet + build + test 均通过。
2026-07-10 18:15:48 +08:00

446 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 模块架构设计文档 — api-gateway
> AIai01Go 网关层)
> 阶段:阶段 2 交付物
> 日期2026-07-09
> 关联:[01 理解确认书](./01-understanding.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)
>
> 本文档覆盖 ai-allocation §5 设计重点路由表矩阵、限流策略表、熔断阈值、JWT RS256 流程、CORS 白名单、请求 ID 注入。
---
## 1. 模块内部分层图
```mermaid
graph TB
subgraph Client["客户端"]
B[浏览器/移动端]
end
subgraph MW["中间件链(按注册顺序)"]
M1[1. Recovery<br/>panic 兜底]
M2[2. OTelgin<br/>HTTP span]
M3[3. RequestID<br/>X-Request-Id]
M4[4. CORS<br/>Origin 白名单]
M5[5. SecurityHeaders<br/>安全响应头]
M6[6. RequestBodyLimit<br/>10MB]
M7[7. RateLimit<br/>令牌桶]
M8[8. CircuitBreaker<br/>gobreaker v2]
M9[9. Auth<br/>JWT RS256]
M10[10. Metrics<br/>prom-client]
end
subgraph Routes["路由"]
H[/healthz /readyz /metrics/]
V1[/api/v1/* 代理路由/]
end
subgraph Proxy["反向代理"]
P[httputil.ReverseProxy<br/>去 /api/v1 前缀]
end
subgraph Downstream["下游服务"]
D1[iam :3002]
D2[classes/core-edu :3001/:3004]
D3[teacher-bff :3003]
D4[content :3005]
D5[msg :3007]
D6[ai :3008]
D7[data-ana :3006]
end
B --> M1 --> M2 --> M3 --> M4 --> M5 --> M6 --> M7
M7 --> H
M7 --> M8 --> M9 --> M10 --> V1 --> P
P --> D1
P --> D2
P --> D3
P --> D4
P --> D5
P --> D6
P --> D7
```
**拦截点说明**
- Recovery 必须最外层(捕获后续所有 panic
- OTelgin 第二(覆盖全链路 span
- RequestID 第三(后续中间件日志可引用)
- CircuitBreaker 在 Auth 之前(未鉴权请求先经熔断器,避免对已熔断下游发起鉴权开销)
- Auth 在 Metrics 之前(仅统计通过鉴权的请求)
- Health/Metrics 端点旁路 Auth在 Auth 之前注册)
## 2. 领域模型
**无领域模型**。api-gateway 是基础设施层,不持有业务聚合/实体/值对象。
仅有的值对象:
| 值对象 | 字段 | 用途 |
| ----------------------------- | -------------------------------------- | ------------------ |
| `Config` | 见 §3 配置项 | 启动时加载,不可变 |
| `bucket`(限流) | `tokens float64`, `lastTime time.Time` | per-IP 令牌桶 |
| `CircuitBreaker`gobreaker | 内部状态机 | per-服务熔断状态 |
## 3. 数据模型
**无数据库**。所有状态在内存:
- `rateLimiter.buckets sync.Map[string]*bucket`IP → 令牌桶
- `CircuitBreaker` 实例(每个下游服务一个,当前共享一个 "downstream"
**P6 演进**
- 限流迁 Redis`redis_rate` 包),支持多副本一致
- 熔断状态保持本地(每个副本独立判断下游健康)
### 3.1 配置项Config 结构)
| 字段 | 环境变量 | 默认值 | 说明 |
| --------------- | ----------------------------- | --------------------------------------- | ------------------------- |
| Port | `API_GATEWAY_PORT` | 8080 | 监听端口 |
| JWTSecret | `JWT_SECRET` | (必填) | HS256 密钥P1P2 弃用) |
| JWKSURL | `IAM_JWKS_URL` | `http://iam:3002/.well-known/jwks.json` | RS256 公钥端点P2 |
| JWTIssuer | `JWT_ISSUER` | next-edu-cloud | JWT iss 校验 |
| JWTAudience | `JWT_AUDIENCE` | next-edu-cloud | JWT aud 校验 |
| CORSOrigins | `CORS_ORIGINS` | * | 逗号分隔白名单 |
| DevMode | `DEV_MODE` | false | 开发旁路(生产禁用) |
| RateLimitRPS | `RATE_LIMIT_RPS` | 100 | 全局默认 RPS |
| RateLimitBurst | `RATE_LIMIT_BURST` | 20 | 突发容量 |
| BodyLimitBytes | `BODY_LIMIT_BYTES` | 10485760 | 请求体上限10MB |
| OTLPEndpoint | `OTEL_EXPORTER_OTLP_ENDPOINT` | http://localhost:4318 | OTLP trace 端点 |
| LogLevel | `LOG_LEVEL` | info | slog 级别 |
| ServicesURL | `*_SERVICE_URL` | 见路由表 | 9 个下游服务地址 |
| ShutdownTimeout | `SHUTDOWN_TIMEOUT` | 5s | 优雅关闭超时 |
## 4. API 设计
### 4.1 路由表矩阵(核心交付物)
| 路径前缀 | 目标服务 | 默认端口 | 鉴权 | 公开子路径 | 阶段 |
| --------------------------------------------------------------------------- | ------------------------------- | ----------- | ---- | ------------------------------------------- | ---- |
| `/api/v1/iam` + `/*path` | iam | 3002 | JWT | `/iam/register` `/iam/login` `/iam/refresh` | P2 |
| `/api/v1/classes` + `/*path` | classesP3 起合并入 core-edu | 3001 → 3004 | JWT | — | P1 |
| `/api/v1/teacher` + `/*path` | teacher-bff | 3003 | JWT | — | P2 |
| `/api/v1/student` + `/*path` | student-bff | 3009 | JWT | — | P3 |
| `/api/v1/parent` + `/*path` | parent-bff | 3010 | JWT | — | P4 |
| `/api/v1/exams` `/homework` `/grades` + `/*path` | core-edu | 3004 | JWT | — | P3 |
| `/api/v1/textbooks` `/chapters` `/knowledge-points` `/questions` + `/*path` | content | 3005 | JWT | — | P4 |
| `/api/v1/notifications` `/messages` + `/*path` | msg | 3007 | JWT | — | P5 |
| `/api/v1/ai` + `/*path` | ai | 3008 | JWT | — | P5 |
| `/api/v1/analytics` `/dashboard` + `/*path` | data-ana | 3006 | JWT | — | P4 |
| `/healthz` | 本服务 | 8080 | 无 | — | P1 |
| `/readyz` | 本服务 | 8080 | 无 | — | P1 |
| `/metrics` | 本服务 | 8080 | 无 | — | P6 |
**路由注册规则**(强制):
- 每个前缀同时注册无尾斜杠与通配符两条路由(`/classes` + `/classes/*path`
- `r.RedirectTrailingSlash = false`(避免 Next.js rewrites 代理循环,见 known-issues
- 新增服务时按本表追加,禁止改其他服务路由
### 4.2 限流策略表
| 路由类别 | RPS | Burst | 备注 |
| ------------------------------- | --- | ----- | ---------------------------------- |
| `/api/v1/iam/login` | 5 | 5 | 登录接口额外加用户级限流(防爆破) |
| `/api/v1/iam/register` | 10 | 10 | 注册接口 |
| `/api/v1/iam/refresh` | 20 | 20 | 刷新 token |
| `/api/v1/ai/*` | 20 | 10 | AI 接口成本高,单独限流 |
| `/api/v1/analytics/*` | 30 | 20 | 分析查询较重 |
| 其他 `/api/v1/*` | 100 | 20 | 默认全局限流 |
| `/healthz` `/readyz` `/metrics` | ∞ | ∞ | 不限流(探针高频访问) |
**实现方式**
- P1内存令牌桶`sync.Map` per-IP
- P6Redis 令牌桶(`redis_rate`),支持多副本一致 + 用户级限流
### 4.3 熔断阈值配置
| 服务 | Interval | Timeout | MaxRequests | ReadyToTrip | 备注 |
| ---------------- | -------- | ------- | ----------- | ------------ | -------------------- |
| iam | 5s | 30s | 1 | 错误率 > 50% | 鉴权失败影响全链路 |
| classes/core-edu | 5s | 30s | 1 | 错误率 > 50% | 默认 |
| teacher-bff | 5s | 30s | 1 | 错误率 > 50% | 默认 |
| content | 10s | 60s | 1 | 错误率 > 30% | ES 慢查询容忍度低 |
| msg | 5s | 30s | 1 | 错误率 > 50% | 默认 |
| ai | 10s | 60s | 1 | 错误率 > 30% | LLM 延迟高,容忍度低 |
| data-ana | 10s | 60s | 1 | 错误率 > 30% | ClickHouse 查询较重 |
**熔断失败定义**:下游返回 5xx 视为失败4xx/2xx 不计入。
**熔断打开响应**HTTP 503 `{ success: false, error: { code: "CIRCUIT_OPEN", message: "downstream unhealthy", retry_after: 30 } }`
### 4.4 JWT RS256 校验流程P2 升级)
```mermaid
sequenceDiagram
participant U as 用户
participant GW as api-gateway
participant IAM as iam 服务
participant JWKS as JWKS 缓存
rect rgb(255, 250, 240)
Note over GW,JWKS: 启动时与定期刷新
GW->>IAM: GET /.well-known/jwks.json
IAM-->>GW: { keys: [...] }
GW->>JWKS: 缓存公钥集TTL 5min
end
rect rgb(240, 248, 255)
Note over U,GW: 请求鉴权
U->>GW: GET /api/v1/classes + Authorization: Bearer <RS256 token>
GW->>GW: 解析 token headerkid
GW->>JWKS: 查 kid 对应公钥
alt 缓存命中
JWKS-->>GW: 公钥
else 缓存未命中
GW->>IAM: GET /.well-known/jwks.json强制刷新
IAM-->>GW: 新公钥集
GW->>JWKS: 更新缓存
end
GW->>GW: jwt.Verify(token,公钥,iss,aud,exp)
alt 校验通过
GW->>GW: 提取 sub/roles/dataScope
GW->>GW: 注入 x-user-id / x-user-roles / x-data-scope 头
GW-->>U: 代理转发到 classes 服务
else 校验失败
GW-->>U: 401 INVALID_TOKEN
end
end
```
**JWKS 缓存策略**(由 `shared-go/jwks.Fetcher` 实现TTL 5min
- TTL 5min到期后台异步刷新不阻塞请求
- kid 未命中时强制同步刷新一次
- 刷新失败保留旧公钥集继续服务fail-open 1 次后 fail-close
- 启动时同步拉取一次,失败则 panic 拒绝启动
### 4.5 CORS 白名单
```go
// 默认白名单(生产环境通过 CORS_ORIGINS 覆盖)
allowedOrigins = []string{
"https://teacher.edu.example.com",
"https://student.edu.example.com",
"https://parent.edu.example.com",
"https://admin.edu.example.com",
"http://localhost:3000", // 开发
}
```
**规则**
- 未配置 `CORS_ORIGINS` 时默认 `*`(仅开发环境)
- 生产环境必须显式配置白名单,禁止 `*`
- 允许方法GET POST PUT DELETE OPTIONS PATCH
- 允许头Authorization Content-Type X-Request-Id X-Trace-Id
- 暴露头X-Request-Id X-Trace-Id
- 预检缓存12 小时(`Access-Control-Max-Age: 43200`
### 4.6 请求 ID 注入
```go
// 规则:
// 1. 优先透传客户端 X-Request-Id 头
// 2. 缺失则生成 req-<uuid-v4>
// 3. 写入 context + 响应头 X-Request-Id
// 4. 注入到下游请求头ReverseProxy.Director 中保留)
// 5. 日志/metrics/trace 全部引用此 request_id
```
**W3C Trace Context 透传**
- `traceparent` / `tracestate` 头透传OTel SDK 自动处理)
- `X-Request-Id` 是业务层 ID`traceparent` 是 OTel 层 ID两者并存
- 日志同时记录两者,便于 Loki → Tempo 关联查询
## 5. 事件设计
**无**。api-gateway 是纯同步代理,不发布/消费任何 Kafka 事件。
## 6. 横切关注点对齐清单
### 6.1 权限装饰器(等价实现)
| 端点 | 鉴权方式 | 备注 |
| ------------------------------------------ | ---------------- | ----------------------- |
| `/healthz` `/readyz` `/metrics` | 无 | 探针端点 |
| `/api/v1/iam/register` `/login` `/refresh` | 白名单 | 公开接口 |
| 其他 `/api/v1/*` | `AuthMiddleware` | JWT RS256 校验 + 头注入 |
> Go 无装饰器用中间件链等价实现。Gateway 不做权限点校验(不持有 Permissions 常量),仅透传 `x-user-id`/`x-user-roles`/`x-data-scope` 头,由下游业务服务 Controller 自校验。
### 6.2 错误码清单
> 依据 W1/W2 裁决:错误码统一 `GW_` 前缀,响应体统一 ActionState 信封 `{success,error:{code,message}}`。
| 错误码 | HTTP | 触发条件 | 响应体 |
| ---------------------- | ---- | --------------------- | -------------------------------------------------------------- |
| `GW_UNAUTHORIZED` | 401 | 缺失 Authorization 头 | `{success:false,error:{code:"GW_UNAUTHORIZED",message:"..."}}` |
| `GW_INVALID_TOKEN` | 401 | JWT 签名/格式错误 | 同上 |
| `GW_INVALID_CLAIMS` | 401 | JWT claims 解析失败 | 同上 |
| `GW_RATE_LIMITED` | 429 | 超出令牌桶限流 | 同上 + `retry_after: 60` |
| `GW_CIRCUIT_OPEN` | 503 | 下游熔断打开 | 同上 + `retry_after: 30` |
| `GW_REQUEST_TOO_LARGE` | 413 | 请求体超 10MB | 同上 |
| `GW_INTERNAL_ERROR` | 500 | panic 兜底 | 同上 + `request_id` |
### 6.3 Logger 初始化
> 已实现:直接使用标准库 `log/slog`W3 裁决),未接入 `shared-go/logger`zap待后续评估。当前 `slog.SetDefault` 由 `InitTracer` 侧初始化时隐式使用默认 logger。
```go
// 全局使用 slogmain.go + 各 middleware 直接调用 slog.Info/slog.Error
import "log/slog"
// 日志级别由 LOG_LEVEL 控制(当前默认 info未单独封装 InitLogger
```
**日志字段规范**
- `timestamp` ISO8601
- `level` INFO/WARN/ERROR
- `service` "api-gateway"
- `request_id` 从 context 取
- `trace_id` 从 OTel span 取
- `user_id` 从注入头取
- `method` `path` `status` `latency_ms`
### 6.4 Metrics 指标清单
| 指标名 | 类型 | 标签 | 描述 |
| --------------------------------------------- | --------- | ---------------------- | ----------------------------------------- |
| `api_gateway_http_requests_total` | Counter | method,endpoint,status | 请求总数 |
| `api_gateway_http_request_duration_seconds` | Histogram | method,endpoint | 请求延迟 |
| `api_gateway_circuit_breaker_state` | Gauge | service,state | 熔断器状态0=CLOSED 1=OPEN 2=HALF_OPEN |
| `api_gateway_rate_limited_total` | Counter | ip | 被限流请求数 |
| `api_gateway_proxy_upstream_duration_seconds` | Histogram | upstream | 下游响应延迟 |
| `api_gateway_jwks_refresh_total` | Counter | result | JWKS 刷新次数 |
| `go_*` | — | — | prom-client 默认 Go runtime 指标 |
**暴露端点**`GET /metrics`prom-client 默认 handler
### 6.5 Tracer 初始化
已实现:`internal/observability/tracer.go`OTLP HTTP exporter + W3C TraceContext 传播。
**资源属性**W6 完整):
- `service.name` = "api-gateway"
- `service.version` = 编译时注入的 version 变量
- `deployment.environment` = cfg.Env
- `host.name` = os.Hostname()
**签名**
```go
func InitTracer(serviceName, endpoint, env, version, hostName string) (shutdown func(context.Context) error, err error)
```
**业务 span 命名规范**`HTTP GET /api/v1/classes`otelgin 自动注入)。
### 6.6 /healthz 检查逻辑
```go
func Healthz(c *gin.Context) {
c.JSON(200, gin.H{
"status": "ok",
"service": "api-gateway",
"version": Version, // 编译时注入
"timestamp": time.Now().UTC().Format(time.RFC3339),
})
}
```
### 6.7 /readyz 检查逻辑
> 已实现W4`Readyz(cfg *config.Config)`,并行 ping 9 个下游 `/healthz`2s 超时,软失败规则。
```go
func Readyz(cfg *config.Config) gin.HandlerFunc {
checks := []downstreamCheck{
{name: "iam", url: cfg.IamServiceURL + "/healthz", required: true},
{name: "teacher-bff", url: cfg.TeacherBffURL + "/healthz", required: true},
{name: "core-edu", url: cfg.CoreEduServiceURL + "/healthz", required: false},
{name: "content", url: cfg.ContentServiceURL + "/healthz", required: false},
{name: "data-ana", url: cfg.DataAnaServiceURL + "/healthz", required: false},
{name: "msg", url: cfg.MsgServiceURL + "/healthz", required: false},
{name: "ai", url: cfg.AiServiceURL + "/healthz", required: false},
{name: "student-bff", url: cfg.StudentBffURL + "/healthz", required: false},
{name: "parent-bff", url: cfg.ParentBffURL + "/healthz", required: false},
}
client := &http.Client{Timeout: 2 * time.Second}
// 并行 pingsync.WaitGroup + sync.Mutex
// 软失败规则:
// - required 下游不可达 → 503
// - optional 下游不可达 → 200 + degraded 列表
}
```
**下游清单与必需性**W4 软失败规则):
| 下游 | required | 不可达时 |
| ----------- | -------- | -------------- |
| iam | ✅ | 503 |
| teacher-bff | ✅ | 503 |
| core-edu | ❌ | 200 + degraded |
| content | ❌ | 200 + degraded |
| data-ana | ❌ | 200 + degraded |
| msg | ❌ | 200 + degraded |
| ai | ❌ | 200 + degraded |
| student-bff | ❌ | 200 + degraded |
| parent-bff | ❌ | 200 + degraded |
### 6.8 优雅关闭顺序
```go
// 1. 收到 SIGTERM
// 2. srv.Shutdown(ctx) 停止接受新请求等待在途请求完成5s 超时)
// 3. tracerShutdown() flush 待发送 span
// 4. log.Println("exited")
// 5. os.Exit(0)
```
## 7. 与其他模块的交互点(契约清单)
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 |
| ------ | ---------- | ---- | -------------------------------------------- | ------------------- |
| 调用 | iam | HTTP | `GET /.well-known/jwks.json` | 拉 RS256 公钥P2 |
| 透传 | 所有下游 | HTTP | `x-user-id` `x-user-roles` `x-data-scope` 头 | 用户身份传递 |
| 透传 | 所有下游 | HTTP | `X-Request-Id` `traceparent` 头 | 链路追踪 |
| 调用 | 所有下游 | HTTP | `GET /healthz` | /readyz 健康检查 |
| 被调用 | 微前端 | HTTP | `/api/v1/*` | 业务请求 |
| 被调用 | Prometheus | HTTP | `GET /metrics` | 指标采集 |
| 被调用 | K8s/Docker | HTTP | `GET /healthz` `GET /readyz` | 探针 |
## 8. 风险与假设
- **假设**iam P2 暴露 JWKS 端点若延期RS256 升级阻塞,临时保留 HS256
- **假设**:所有下游服务实现 `/healthz`;若某服务未实现,`/readyz` 误报
- **风险**单实例限流在多副本部署后失效P6 迁 Redis 解决)
- **风险**JWKS 缓存过期时若 iam 不可达fail-open 1 次后 fail-close可能导致全量 401
- **风险**DevMode 旁路误开到生产 → 启动时强制校验 `DevMode=true && ENV=production` panic
- **未决**:是否在 Gateway 层做 IP 黑名单WAF建议 P6 在 Istio 层做,本服务不介入
## 9. 实施优先级
> 更新日期2026-07-10P2-P5 实施后复核)
| 优先级 | 任务 | 状态 | 阶段 |
| ------ | --------------------------------------------- | --------- | ------------- |
| P0 | 删除 auth.go 死代码 | ✅ 已完成 | P2.0 |
| P0 | 修复 go.mod 与 Dockerfile Go 版本不一致 | ✅ 已完成 | P2.0 |
| P0 | 新增 `/metrics` 端点 + 7 个业务指标 | ✅ 已完成 | P2.4W5 |
| P0 | 引入 `log/slog` 替换标准 log | ✅ 已完成 | P2.4W3 |
| P1 | `/readyz` 真实健康检查 | ✅ 已完成 | P2.5W4 |
| P1 | JWT RS256 升级 + JWKS 缓存 | ✅ 已完成 | P2.2 |
| P1 | 错误码 GW_ 前缀 + ActionState 信封 | ✅ 已完成 | P2.3W1/W2 |
| P1 | DevMode 生产防护 | ✅ 已完成 | P2.6W7 |
| P1 | tracer 资源属性补全 | ✅ 已完成 | P2.4W6 |
| P1 | CORS 纳入 Config | ✅ 已完成 | P2.4 |
| P1 | 路由扩展student/parent/messages/dashboard | ✅ 已完成 | P2.7 + P3-P5 |
| P2 | 限流策略表细化per-路由) | ⏳ P6 | P6 |
| P2 | 熔断 per-服务实例 | ⏳ P6 | P6 |
| P3 | 补全测试覆盖率到 80% | ⏳ P6 | P6 |