# 模块架构设计文档 — 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<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. Auth<br/>JWT RS256]
        M9[9. CircuitBreaker<br/>gobreaker v2]
        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 第三（后续中间件日志可引用）
- Auth 在 CircuitBreaker 之前（避免未鉴权请求消耗下游配额）
- Metrics 在 CircuitBreaker 之后（统计通过鉴权且未被熔断的请求）
- 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 1h）
    end

    rect rgb(240, 248, 255)
        Note over U,GW: 请求鉴权
        U->>GW: GET /api/v1/classes + Authorization: Bearer <RS256 token>
        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 缓存策略**：

- TTL 1h，到期后台异步刷新（不阻塞请求）
- 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 错误码清单

| 错误码              | HTTP | 触发条件              | 响应体                                 |
| ------------------- | ---- | --------------------- | -------------------------------------- |
| `UNAUTHORIZED`      | 401  | 缺失 Authorization 头 | `{success:false,error:{code,message}}` |
| `INVALID_TOKEN`     | 401  | JWT 签名/格式错误     | 同上                                   |
| `INVALID_CLAIMS`    | 401  | JWT claims 解析失败   | 同上                                   |
| `RATE_LIMITED`      | 429  | 超出令牌桶限流        | 同上 + `retry_after: 60`               |
| `CIRCUIT_OPEN`      | 503  | 下游熔断打开          | 同上 + `retry_after: 30`               |
| `REQUEST_TOO_LARGE` | 413  | 请求体超 10MB         | 同上                                   |
| `INTERNAL_ERROR`    | 500  | panic 兜底            | 同上 + `request_id`                    |

### 6.3 Logger 初始化

```go
// internal/observability/logger.go（待新增）
import "log/slog"

var Logger *slog.Logger

func InitLogger(level string) {
    var lv slog.Level
    _ = lv.UnmarshalText([]byte(level))
    Logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: lv}))
    slog.SetDefault(Logger)
}
```

**日志字段规范**：

- `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 传播。

**待补**：

- 启动时记录 `service.name` / `service.version` / `deployment.environment` 资源属性
- 关键业务 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 检查逻辑（待重构）

```go
func Readyz(downstreams []string) gin.HandlerFunc {
    return func(c *gin.Context) {
        ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
        defer cancel()
        var wg errgroup.Group
        unhealthy := make([]string, 0)
        var mu sync.Mutex
        for _, url := range downstreams {
            url := url
            wg.Go(func() error {
                req, _ := http.NewRequestWithContext(ctx, "GET", url+"/healthz", nil)
                resp, err := http.DefaultClient.Do(req)
                if err != nil || resp.StatusCode != 200 {
                    mu.Lock()
                    unhealthy = append(unhealthy, url)
                    mu.Unlock()
                }
                if resp != nil { resp.Body.Close() }
                return nil
            })
        }
        _ = wg.Wait()
        if len(unhealthy) > 0 {
            c.JSON(503, gin.H{"status":"error","unhealthy":unhealthy})
            return
        }
        c.JSON(200, gin.H{"status":"ok"})
    }
}
```

**下游清单**：iam / classes / teacher-bff / core-edu / content / msg / ai / data-ana 的 `/healthz`

### 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. 实施优先级

| 优先级 | 任务                                    | 阶段           |
| ------ | --------------------------------------- | -------------- |
| P0     | 删除 auth.go 死代码（L124-139）         | 立即           |
| P0     | 修复 go.mod 与 Dockerfile Go 版本不一致 | 立即           |
| P0     | 新增 `/metrics` 端点 + prom-client      | P2             |
| P0     | 引入 `log/slog` 替换标准 log            | P2             |
| P1     | `/readyz` 真实健康检查                  | P2             |
| P1     | JWT RS256 升级 + JWKS 缓存              | P2（依赖 iam） |
| P2     | 限流策略表细化（per-路由）              | P6             |
| P2     | 熔断 per-服务实例                       | P6             |
| P3     | 补全测试覆盖率到 80%                    | P6             |
| P3     | DevMode 生产防护                        | P2             |
