# 模块理解确认书 — api-gateway

> AI：ai01（Go 网关层）
> 阶段：阶段 1 交付物
> 日期：2026-07-09
> 关联：[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[AI 分配方案](../../../docs/architecture/ai-allocation.md)

---

## 1. 我在架构中的位置

- **层级**：L3 网关层（004 §3.1 六层架构）
- **上游**：4 个微前端（teacher-portal / student-portal / parent-portal / admin-portal），通过浏览器/移动端 HTTP 请求
- **下游**：3 个 BFF + 6 个业务服务，共 9 个反向代理目标
  - BFF：teacher-bff（3003）、student-bff（待建）、parent-bff（待建）
  - 业务：iam（3002）、core-edu（3004）、content（3005）、data-ana（3006）、msg（3007）、ai（3008）
- **通信方式**：
  - 入口：HTTP/REST（含 WebSocket 升级透传到 push-gateway，不在本服务）
  - 出口：HTTP 反向代理（`httputil.ReverseProxy`），P3 起部分链路改 gRPC
- **不持有业务状态**：仅做路由/鉴权/限流/熔断/可观测，无 DB

## 2. 我的限界上下文

- **我负责**：所有外部请求的统一入口、JWT 校验、用户身份注入、限流、熔断、CORS、安全头、请求 ID 注入、反向代理
- **聚合/实体**：无（网关无领域模型）
- **业务领域**：不属于 D1-D6 任一业务领域，属于基础设施层
- **我不负责**：
  - WebSocket 长连接管理（push-gateway 负责）
  - 用户身份认证逻辑（iam 负责，本服务只校验 JWT 签名）
  - 权限点解析（业务服务 Controller 通过 `@RequirePermission` 自行校验，本服务只透传 `x-user-id`/`x-user-roles` 头）
  - 业务数据持久化（无 DB）

## 3. 我与外部的契约

### 3.1 我消费的 proto message

| proto     | message                  | 用途                                                                    |
| --------- | ------------------------ | ----------------------------------------------------------------------- |
| iam.proto | `UserInfo` / `TokenPair` | P2 起 RS256 公钥校验时通过 IAM `/auth/jwks` 端点拉公钥（HTTP，非 gRPC） |

> P1 阶段用 HS256 共享密钥，**不消费任何 proto**。P2 起 RS256 通过 IAM 暴露的 JWKS 端点（`/.well-known/jwks.json`）拉取公钥，仍是 HTTP，无需 gRPC 客户端。

### 3.2 我暴露的 API 端点

| 方法 | 路径                                                                        | 鉴权                                    | 说明                                      |
| ---- | --------------------------------------------------------------------------- | --------------------------------------- | ----------------------------------------- |
| GET  | `/healthz`                                                                  | 无                                      | liveness 探针                             |
| GET  | `/readyz`                                                                   | 无                                      | readiness 探针（当前 stub，需补真实检查） |
| ANY  | `/api/v1/classes` + `/api/v1/classes/*path`                                 | JWT                                     | 代理到 classes/core-edu 服务              |
| ANY  | `/api/v1/iam` + `/api/v1/iam/*path`                                         | JWT（除 register/login/refresh 白名单） | 代理到 iam 服务                           |
| ANY  | `/api/v1/teacher` + `/api/v1/teacher/*path`                                 | JWT                                     | 代理到 teacher-bff                        |
| ANY  | `/api/v1/exams` `/homework` `/grades` + `/*path`                            | JWT                                     | 代理到 core-edu                           |
| ANY  | `/api/v1/textbooks` `/chapters` `/knowledge-points` `/questions` + `/*path` | JWT                                     | 代理到 content                            |
| ANY  | `/api/v1/notifications` + `/*path`                                          | JWT                                     | 代理到 msg                                |
| ANY  | `/api/v1/ai` + `/*path`                                                     | JWT                                     | 代理到 ai                                 |
| ANY  | `/api/v1/analytics` + `/*path`                                              | JWT                                     | 代理到 data-ana                           |
| GET  | `/metrics`                                                                  | 无（待实现）                            | Prometheus 指标端点                       |

### 3.3 我发布/消费的 Kafka 事件

**无**。api-gateway 不接入 Kafka，是纯同步 HTTP 反向代理。

### 3.4 错误码前缀

| 错误码              | HTTP | 触发条件                                                            |
| ------------------- | ---- | ------------------------------------------------------------------- |
| `UNAUTHORIZED`      | 401  | 缺失 Authorization 头                                               |
| `INVALID_TOKEN`     | 401  | JWT 签名/格式错误                                                   |
| `INVALID_CLAIMS`    | 401  | JWT claims 解析失败                                                 |
| `RATE_LIMITED`      | 429  | 超出令牌桶限流（响应体当前写 `error: rate_limited`，需统一加 code） |
| `CIRCUIT_OPEN`      | 503  | 下游熔断打开（响应体当前写 `error: circuit_open`）                  |
| `INTERNAL_ERROR`    | 500  | panic 兜底（Recovery 中间件）                                       |
| `REQUEST_TOO_LARGE` | 413  | 请求体超 10MB（由 MaxBytesReader 自动触发，但响应非 JSON 信封）     |

> **错误码统一约定**：本服务无业务错误码前缀（无业务），仅上述 7 个 HTTP 语义错误。响应体需统一为 `{ success: false, error: { code, message } }` 信封，与 classes 黄金模板 `ApplicationError` 对齐。

## 4. 我的技术栈

- **语言**：Go 1.22（go.mod 与 Dockerfile 一致）
- **框架**：Gin v1.12.0
- **核心依赖**：
  - `github.com/golang-jwt/jwt/v5` v5.2.1（JWT 校验）
  - `github.com/sony/gobreaker/v2` v2.1.0（熔断）
  - `github.com/google/uuid` v1.6.0（请求 ID）
  - `go.opentelemetry.io/otel` v1.44.0 + `otelgin` v0.69.0（链路追踪）
- **存储**：无 DB，无 Redis（限流用内存令牌桶 `sync.Map`）
- **构建**：多阶段 Dockerfile（golang:1.22-alpine → alpine:3.20，非 root 用户）

## 5. 我的阶段归属

- **阶段**：P1（已实现）+ P2 升级（RS256）+ P6 硬化（限流策略表、熔断阈值细化）
- **当前阶段目标**：
  - P1 退出标准已达成：classes 域 CRUD 端到端跑通（见 known-issues 工作日志 2026-07-09 00:24）
  - P2 待升级：JWT HS256 → RS256（IAM 签发，Gateway 公钥校验）
- **依赖上游阶段产出**：
  - P2 iam 服务须暴露 JWKS 端点（`/.well-known/jwks.json`）供本服务拉公钥

## 6. 我需要对齐的黄金模板项（对照 classes 服务）

| 项                | classes（黄金模板）                               | api-gateway 现状                                         | 差距                                                     |
| ----------------- | ------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- |
| 权限装饰器        | `@RequirePermission()`                            | N/A（Go 无装饰器；用中间件 `AuthMiddleware` 替代）       | ✅ 等价实现                                              |
| 错误码前缀        | `CLASSES_*`                                       | 无前缀（基础设施层）                                     | ✅ 设计合理                                              |
| logger（pino）    | `shared/observability/logger.ts`                  | ✅ `log/slog` 结构化 JSON                                | ✅ 对齐                                                  |
| metrics           | `shared/observability/metrics.ts` 暴露 `/metrics` | ✅ 7 个业务指标（promauto）                              | ✅ 对齐                                                  |
| tracer            | `shared/observability/tracer.ts` OTel SDK         | ✅ `internal/observability/tracer.go`（W6 资源属性完整） | ✅ 对齐                                                  |
| `/healthz`        | ✅                                                | ✅                                                       | ✅ 对齐                                                  |
| `/readyz`         | ✅ 检查 DB `SELECT 1`                             | ✅ 并行 ping 下游 /healthz（软失败规则）                 | ✅ 对齐                                                  |
| 优雅关闭          | SIGTERM → app.close()                             | ✅ `srv.Shutdown(ctx)` 5s 超时                           | ✅ 对齐                                                  |
| 测试覆盖率        | ≥ 80%                                             | ~25%（仅 circuit-breaker + ratelimit）                   | ⚠️ 待补 auth/cors/security/recovery/requestid/proxy 测试 |
| Dockerfile        | 多阶段 + 非 root + healthcheck                    | ✅                                                       | ✅ 对齐                                                  |
| Zod 输入验证      | `schema.safeParse(body)`                          | N/A（Go 无 Zod；用 `ShouldBindJSON`）                    | ✅ 等价实现                                              |
| GlobalErrorFilter | `GlobalErrorFilter`                               | ✅ Recovery 中间件兜底                                   | ✅ 等价实现                                              |

## 7. 服务审计表（按 ai-allocation §10 模板）

| 服务        | 权限装饰器    | 错误码前缀    | logger  | metrics   | tracer  | /healthz | /readyz      | 优雅关闭   | 测试覆盖率 | Dockerfile |
| ----------- | ------------- | ------------- | ------- | --------- | ------- | -------- | ------------ | ---------- | ---------- | ---------- |
| api-gateway | ⚠️ 中间件替代 | ✅ `GW_` 前缀 | ✅ slog | ✅ 7 指标 | ✅ OTel | ✅       | ✅ 并行 ping | ✅ 5s 超时 | ~25%       | ✅ 多阶段  |

### 7.1 详细问题清单（按严重度排序）

> 更新日期：2026-07-10（P2-P5 实施后复核）

| #   | 严重度 | 文件                               | 问题                                                                         | 状态      | 修复说明                                                                                                     |
| --- | ------ | ---------------------------------- | ---------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------ |
| 1   | 高     | `internal/observability/`          | 无 `/metrics` 端点，Prometheus 404                                           | ✅ 已修复 | 新增 `metrics.go`，注册 7 个业务指标，main.go 暴露 `/metrics`                                                |
| 2   | 高     | `internal/health/health.go`        | `/readyz` 直接返回 200，未检查下游                                           | ✅ 已修复 | 改为并行 ping 9 个下游 `/healthz`，软失败规则（iam/teacher-bff required，其余 optional）                     |
| 3   | 高     | `internal/middleware/auth.go`      | 死代码 `RequestIDMiddleware()` + `generateUUID()` 重复 requestid.go 且未使用 | ✅ 已修复 | 死代码已删除（P2.0）                                                                                         |
| 4   | 高     | 全文件                             | 用 `log.Printf`，不符合 coding-standards §3.8 `log/slog` 结构化日志要求      | ✅ 已修复 | 全部 `log.Printf`/`log.Fatal` 替换为 `slog.Info`/`slog.Error`（W3）                                          |
| 5   | 中     | `go.mod` L3 vs `Dockerfile` L1     | go.mod 声明 `go 1.25.0`，Dockerfile 用 `golang:1.22-alpine`                  | ✅ 已修复 | go.mod 统一为 `go 1.22`，go.work 因 push-gateway 要求升级为 `go 1.25.0`（workspace 兼容更低版本模块）        |
| 6   | 中     | `internal/middleware/auth.go`      | P2 待升级 HS256 → RS256                                                      | ✅ 已修复 | 接入 `shared-go/jwks.Fetcher`，RS256 公钥校验 + kid 路由（W6 资源属性完整）                                  |
| 7   | 中     | `internal/middleware/cors.go` L21  | `CORS_ORIGINS` 直接 `os.Getenv`，未纳入 Config 结构                          | ✅ 已修复 | `CORS()` 改为 `CORS(cfg *config.Config)`，从 Config 读取白名单                                               |
| 8   | 中     | `internal/middleware/ratelimit.go` | 单实例内存令牌桶，水平扩展后限流失效                                         | ⏳ P6     | P6 引入 Redis 令牌桶（`redis_rate`），支持多副本一致                                                         |
| 9   | 中     | `internal/middleware/auth.go` L68  | DevMode 注入固定 `teacher,admin` 角色，生产风险                              | ✅ 已修复 | 启动时 `DevMode=true && ENV=production` panic 拒绝启动（W7 防护，config.go）                                 |
| 10  | 低     | `README.md` L38                    | 提到 `GET /health` 兼容端点，但代码未注册                                    | ✅ 已修复 | README 重写，删除 `/health` 描述                                                                             |
| 11  | 低     | `internal/proxy/proxy.go` L24      | 连续两次 `TrimPrefix`（`/api/v1` 后再 `/api`）逻辑冗余                       | ✅ 已修复 | 冗余 `TrimPrefix` 已删除（P2.0）                                                                             |
| 12  | 低     | `Dockerfile` L18                   | 构建命令 `./main.go` 而非 `./`                                               | ⚠️ 保留   | `./main.go` 单文件构建可正常工作，`-ldflags="-s -w"` 已添加；改为 `.` 需评估是否有其他 main 包文件（当前无） |
| 13  | 低     | 测试                               | auth/cors/security/recovery/requestid/proxy 无测试                           | ⏳ P6     | P6 补 `*_test.go`，目标覆盖率 ≥ 80%                                                                          |

## 8. 风险与假设

- **假设**：iam 服务在 P2 会暴露 JWKS 端点，否则 RS256 升级阻塞
- **假设**：所有下游服务在 P1 都已实现 `/healthz`（用于 `/readyz` 真实检查）
- **风险**：单实例限流在多副本部署后失效，P6 需迁 Redis
- **风险**：DevMode 旁路若误开到生产，可绕过鉴权注入 admin 角色
- **未决**：是否在 Gateway 层做权限校验（当前仅透传角色，由下游服务自校验）？建议保持现状，避免 Gateway 持有权限点常量造成耦合
