# 模块架构设计文档 — api-gateway > AI:ai01(Go 网关层) > 阶段:阶段 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
panic 兜底] M2[2. OTelgin
HTTP span] M3[3. RequestID
X-Request-Id] M4[4. CORS
Origin 白名单] M5[5. SecurityHeaders
安全响应头] M6[6. RequestBodyLimit
10MB] M7[7. RateLimit
令牌桶] M8[8. CircuitBreaker
gobreaker v2] M9[9. Auth
JWT RS256] M10[10. Metrics
prom-client] end subgraph Routes["路由"] H[/healthz /readyz /metrics/] V1[/api/v1/* 代理路由/] end subgraph Proxy["反向代理"] P[httputil.ReverseProxy
去 /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 密钥(P1,P2 弃用) | | 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` | classes(P3 起合并入 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) - P6:Redis 令牌桶(`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 GW->>GW: 解析 token header(kid) 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- // 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 // 全局使用 slog(main.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} // 并行 ping(sync.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-10(P2-P5 实施后复核) | 优先级 | 任务 | 状态 | 阶段 | | ------ | --------------------------------------------- | --------- | ------------- | | P0 | 删除 auth.go 死代码 | ✅ 已完成 | P2.0 | | P0 | 修复 go.mod 与 Dockerfile Go 版本不一致 | ✅ 已完成 | P2.0 | | P0 | 新增 `/metrics` 端点 + 7 个业务指标 | ✅ 已完成 | P2.4(W5) | | P0 | 引入 `log/slog` 替换标准 log | ✅ 已完成 | P2.4(W3) | | P1 | `/readyz` 真实健康检查 | ✅ 已完成 | P2.5(W4) | | P1 | JWT RS256 升级 + JWKS 缓存 | ✅ 已完成 | P2.2 | | P1 | 错误码 GW_ 前缀 + ActionState 信封 | ✅ 已完成 | P2.3(W1/W2) | | P1 | DevMode 生产防护 | ✅ 已完成 | P2.6(W7) | | P1 | tracer 资源属性补全 | ✅ 已完成 | P2.4(W6) | | 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 |