# api-gateway 对接契约

> 负责人：ai01
> 关联：[matrix.md](./matrix.md)、[iam.proto](../../../packages/shared-proto/proto/iam.proto)、[objections/api-gateway_issue.md](../objections/api-gateway_issue.md)、[worklines/api-gateway_workline.md](../worklines/api-gateway_workline.md)、[02-architecture-design.md](../../../services/api-gateway/docs/02-architecture-design.md)
> 依据：[coord-final-decisions.md](../../coord-final-decisions.md) §3.8 W1-W8、[president-final-rulings.md](../../president-final-rulings.md) §2.15/§2.16/§2.19
> 版本：v2（2026-07-10 修正：路径前缀 /api→/api/v1、JWKS 拉取方式 gRPC→HTTP、删除 GetEffectiveAccess、错误码加 GW_ 前缀）

---

## §1 我提供什么（对外接口）

### 1.1 gRPC 接口

无。api-gateway 是 HTTP 入口，不对外提供 gRPC（依据 [president-final-rulings.md](../../president-final-rulings.md) §2.16 裁决：gateway 保持 HTTP 透传，不改为 gRPC 客户端）。

### 1.2 HTTP 端点

#### 1.2.1 健康检查与指标端点（公开，无鉴权）

| Method | Path       | 用途                                        | 认证 | 阶段 |
| ------ | ---------- | ------------------------------------------- | ---- | ---- |
| GET    | /healthz   | 网关存活探针（liveness）                    | 公开 | P1   |
| GET    | /readyz    | 网关就绪探针（readiness，并行 ping 下游 /healthz）| 公开 | P2   |
| GET    | /metrics   | Prometheus 指标端点（7 个业务指标 + Go runtime）| 公开（内网）| P2   |

#### 1.2.2 反向代理路由（JWT 必需，除白名单外）

> **路径前缀**：所有业务路由统一 `/api/v1/*` 前缀（与 [main.go](../../../services/api-gateway/main.go) L59 `r.Group("/api/v1")` 一致）。
>
> **API 版本化**：按 [president-final-rulings.md](../../president-final-rulings.md) §2.15 裁决，各服务 Controller 内加 `/v1` 前缀。Gateway 透传时不改路径，前端调用 `/api/v1/<service>/v1/*`（ISSUE-003 待 coord 仲裁最终方案，本表暂列方案 A）。

| Method | Path（Gateway 外部路径）                            | 目标服务                  | 端口  | 鉴权 | 公开子路径                              | 阶段 |
| ------ | --------------------------------------------------- | ------------------------- | ----- | ---- | --------------------------------------- | ---- |
| ANY     | /api/v1/iam/v1/*path + /api/v1/iam/v1              | iam                       | 3002  | JWT  | `/iam/v1/register` `/login` `/refresh`  | P2   |
| ANY     | /api/v1/teacher/v1/*path + /api/v1/teacher/v1      | teacher-bff（GraphQL）    | 3003  | JWT  | —                                       | P2   |
| ANY     | /api/v1/student/v1/*path + /api/v1/student/v1      | student-bff（GraphQL）    | 3009  | JWT  | —                                       | P3   |
| ANY     | /api/v1/parent/v1/*path + /api/v1/parent/v1        | parent-bff（GraphQL）     | 3010  | JWT  | —                                       | P4   |
| ANY     | /api/v1/classes/v1/*path + /api/v1/classes/v1      | core-edu（classes 合并）  | 3004  | JWT  | —                                       | P3   |
| ANY     | /api/v1/exams/v1/*path + /api/v1/homework/v1/*path + /api/v1/grades/v1/*path | core-edu | 3004 | JWT | — | P3 |
| ANY     | /api/v1/textbooks/v1/*path + /api/v1/chapters/v1/*path + /api/v1/knowledge-points/v1/*path + /api/v1/questions/v1/*path | content | 3005 | JWT | — | P4 |
| ANY     | /api/v1/analytics/v1/*path + /api/v1/dashboard/v1/*path | data-ana            | 3006  | JWT  | —                                       | P4   |
| ANY     | /api/v1/notifications/v1/*path + /api/v1/messages/v1/*path | msg                 | 3007  | JWT  | —                                       | P5   |
| ANY     | /api/v1/ai/v1/*path + /api/v1/ai/v1               | ai                        | 3008  | JWT  | —                                       | P5   |
| ANY     | /api/v1/admin/v1/*path + /api/v1/admin/v1          | teacher-bff admin namespace | 3003 | JWT + admin 角色 | —                          | P6   |

> **注 1**：每个前缀同时注册无尾斜杠与通配符两条路由（`/iam/v1` + `/iam/v1/*path`），因为 `r.RedirectTrailingSlash = false`（[main.go](../../../services/api-gateway/main.go) L34）。
>
> **注 2**：admin-portal P6 阶段复用 teacher-bff + admin schema 命名空间（依据 [coord.md](../coord.md) §1.3 ARB-001 裁决）。
>
> **注 3**：当前 P1 代码暂未加 `/v1` 前缀（如 `/api/v1/iam/*path`），待 ISSUE-003 仲裁后 P2.7 任务统一迁移。

#### 1.2.3 透传请求头（Gateway 注入，下游读取）

| 头名           | 来源                          | 用途                          | 下游消费方 |
| -------------- | ----------------------------- | ----------------------------- | ---------- |
| `x-user-id`    | JWT `sub` claim               | 用户身份传递                  | 所有下游   |
| `x-user-roles` | JWT `roles` claim（逗号分隔）| 角色传递                      | 所有下游   |
| `x-data-scope` | JWT `data_scope` claim        | 数据范围传递（P2 新增）       | 所有下游   |
| `X-Request-Id` | 客户端透传或 Gateway 生成 UUID | 请求 ID（全链路追踪）         | 所有下游   |
| `traceparent`  | OTel SDK 自动处理             | W3C Trace Context（链路追踪）| 所有下游   |
| `tracestate`   | OTel SDK 自动处理             | W3C Trace Context 扩展        | 所有下游   |

### 1.3 GraphQL schema

不适用。api-gateway 仅做 HTTP 反向代理 + JWT 验签，不解析 GraphQL（GraphQL 由 BFF 层处理）。

### 1.4 Kafka 事件发布

无。api-gateway 不发布 Kafka 事件（纯同步 HTTP 反向代理）。

### 1.5 错误码前缀

`GW_`（依据 [coord-final-decisions.md](../../coord-final-decisions.md) W1 / G14 裁决）

### 1.6 错误码清单

| 错误码                   | HTTP | 触发条件              | 响应体（ActionState 信封）                                              |
| ------------------------ | ---- | --------------------- | ----------------------------------------------------------------------- |
| `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  | 超出令牌桶限流        | `{success:false,error:{code:"GW_RATE_LIMITED",message:"...",retry_after:60}}` |
| `GW_CIRCUIT_OPEN`        | 503  | 下游熔断打开          | `{success:false,error:{code:"GW_CIRCUIT_OPEN",message:"...",retry_after:30}}`  |
| `GW_REQUEST_TOO_LARGE`   | 413  | 请求体超 10MB         | `{success:false,error:{code:"GW_REQUEST_TOO_LARGE",message:"..."}}`     |
| `GW_INTERNAL_ERROR`      | 500  | panic 兜底            | `{success:false,error:{code:"GW_INTERNAL_ERROR",message:"...",request_id:"..."}}` |

> 依据 W1 / W2 裁决：错误码统一 `GW_` 前缀，响应体统一 ActionState 信封 `{success,error:{code,message}}`。

---

## §2 我消费什么（依赖上游）

### 2.1 gRPC 调用（同步）

**无**。依据 [president-final-rulings.md](../../president-final-rulings.md) §2.16 裁决"gateway 保持 HTTP 透传，不改为 gRPC 客户端"，api-gateway 不消费任何 gRPC 接口。

> **勘误**（v1 版本曾错误声明消费 `IamService.GetPublicKey` 和 `IamService.GetEffectiveAccess`，v2 已删除）：
> - JWT 公钥拉取走 HTTP JWKS 端点，非 gRPC（见 §2.3）
> - Gateway 不做权限点校验，不消费 `GetEffectiveAccess`（权限由下游服务 Controller `@RequirePermission` 自校验，见 [01-understanding.md](../../../services/api-gateway/docs/01-understanding.md) §2 + B3 裁决）
> - [matrix.md](./matrix.md) §2 gRPC 接口提供方矩阵中"iam 消费方"应移除 api-gateway（已提请 ISSUE-001）

### 2.2 Kafka 事件订阅

无。api-gateway 不订阅 Kafka 事件。

### 2.3 HTTP 调用（同步）

| 被调用方           | Method.Path                         | 用途                                       | mock 策略                                                       | 阶段 |
| ------------------ | ----------------------------------- | ------------------------------------------ | --------------------------------------------------------------- | ---- |
| iam (ai06)         | `GET /.well-known/jwks.json`        | 拉 RS256 公钥集（JWKS），TTL 5min 缓存      | iam 就绪前使用本地固定 mock RS256 公钥（与 mock 私钥配对签发 mock JWT）| P2   |
| iam (ai06)         | `GET /healthz`                      | /readyz 下游健康检查                       | iam 就绪前 /readyz 软失败（返回 200 + degraded）               | P2   |
| teacher-bff (ai03) | `POST /graphql`（反向代理）         | 代理教师端 GraphQL 请求                    | teacher-bff 就绪前返回 503 + Retry-After，前端降级到本地 mock   | P2   |
| student-bff (ai04) | `POST /graphql`（反向代理）         | 代理学生端 GraphQL 请求                    | student-bff 就绪前返回 503                                     | P3   |
| parent-bff (ai05)  | `POST /graphql`（反向代理）         | 代理家长端 GraphQL 请求                    | parent-bff 就绪前返回 503                                      | P4   |
| core-edu (ai08)    | `GET /healthz` + 业务 REST（反向代理）| /readyz 检查 + 业务请求代理               | core-edu 就绪前 /readyz 软失败                                 | P3   |
| content (ai09)     | `GET /healthz` + 业务 REST          | /readyz 检查 + 业务请求代理                | content 就绪前 /readyz 软失败                                  | P4   |
| data-ana (ai11)    | `GET /healthz` + 业务 REST          | /readyz 检查 + 业务请求代理                | data-ana 就绪前 /readyz 软失败                                 | P4   |
| msg (ai10)         | `GET /healthz` + 业务 REST          | /readyz 检查 + 业务请求代理                | msg 就绪前 /readyz 软失败                                      | P5   |
| ai (ai12)          | `GET /healthz` + 业务 REST          | /readyz 检查 + 业务请求代理                | ai 就绪前 /readyz 软失败                                       | P5   |

> **JWKS 缓存策略**（[packages/shared-go/jwks/jwks.go](../../../packages/shared-go/jwks/jwks.go)）：
> - TTL 5min，到期后台异步刷新（不阻塞请求）
> - kid 未命中时强制同步刷新一次
> - 刷新失败保留旧公钥集继续服务（fail-open 1 次后 fail-close）
> - 启动时同步拉取一次，失败则 panic 拒绝启动

### 2.4 shared-go 包依赖

依据 [president-final-rulings.md](../../president-final-rulings.md) §2.19 裁决，ai01 直接 import `packages/shared-go`：

| 模块              | 用途                                       | 接入阶段 |
| ----------------- | ------------------------------------------ | -------- |
| `shared-go/jwks`  | JWKS Fetcher（HTTP 拉 RS256 公钥 + 缓存）  | P2.2     |
| `shared-go/logger`| 结构化日志（zap 或 slog，待 ISSUE-004 仲裁）| P2.1     |
| `shared-go/tracer`| OTel tracer 初始化（评估接入，若接口兼容） | P2.1     |
| `shared-go/env`   | 环境变量加载（评估接入，若接口兼容）       | P2.1     |

---

## §3 就绪信号

### 3.1 我依赖的上游就绪标志

| 上游 | 就绪标志 | 阶段 | 状态 |
| ---- | -------- | ---- | ---- |
| coord | shared-go 包骨架（tracer/logger/jwks/env 4 模块）| 批次 0 | ✅ 已完成 |
| coord | ISSUE-001 仲裁（JWKS HTTP 确认）| P2 启动前 | ⏳ 待仲裁 |
| coord | ISSUE-002 仲裁（shared-go 接入）| P2 启动前 | ⏳ 待仲裁 |
| coord | ISSUE-003 仲裁（API 版本化路由方案）| P2.7 前 | ⏳ 待仲裁 |
| coord | ISSUE-004 仲裁（zap vs slog）| P2.1 前 | ⏳ 待仲裁 |
| iam (ai06) | `GET /.well-known/jwks.json` HTTP 端点 + RS256 JWT 签发 | P2 | ⏳ |
| iam (ai06) | `GET /healthz` 端点 | P2 | ⏳ |
| teacher-bff (ai03) | `POST /graphql` :3003 启用 + `GET /healthz` | P2 | ⏳ |
| student-bff (ai04) | `POST /graphql` :3009 启用 + `GET /healthz` | P3 | ⏳ |
| core-edu (ai08) | `GET /healthz` + classes 合并 | P3 | ⏳ |
| parent-bff (ai05) | `POST /graphql` :3010 启用 + `GET /healthz` | P4 | ⏳ |
| content (ai09) | `GET /healthz` + REST 端点 | P4 | ⏳ |
| data-ana (ai11) | `GET /healthz` + REST 端点 | P4 | ⏳ |
| msg (ai10) | `GET /healthz` + REST 端点 | P5 | ⏳ |
| ai (ai12) | `GET /healthz` + REST 端点 | P5 | ⏳ |
| push-gateway (ai02) | :8081 启用 + /internal/push（WebSocket 协作评估）| P5 | ⏳ |

### 3.2 我的就绪标志（供下游消费）

| 阶段 | 就绪标志 | 状态 |
| ---- | -------- | ---- |
| P2 | api-gateway HTTP :8080 启用（/healthz 返回 200）| ⏳ |
| P2 | /readyz 返回 200（含 iam + teacher-bff 连通性检查通过）| ⏳ |
| P2 | JWT RS256 验签链路打通（使用 iam JWKS 公钥校验 access_token）| ⏳ |
| P2 | /api/v1/iam/v1/* 代理到 iam 认证链路可用 | ⏳ |
| P2 | /api/v1/teacher/v1/* 反向代理到 teacher-bff GraphQL 可用 | ⏳ |
| P2 | 限流（IP 级令牌桶）+ 熔断（共享 downstream）+ CORS 白名单生效 | ⏳ |
| P2 | 7 个业务指标暴露在 /metrics | ⏳ |
| P2 | 错误响应统一 ActionState 信封 + GW_ 前缀 | ⏳ |
| P3 | /api/v1/student/v1/* + /api/v1/exams/v1/* 等路由可用 | ⏳ |
| P4 | /api/v1/parent/v1/* + /api/v1/textbooks/v1/* + /api/v1/analytics/v1/* 路由可用 | ⏳ |
| P5 | /api/v1/notifications/v1/* + /api/v1/ai/v1/* 路由可用 | ⏳ |
| P6 | 限流迁 Redis + 测试覆盖率 ≥ 80% | ⏳ |

---

## §4 Mock 策略

### 4.1 我提供的 mock（供下游各前端 portal 消费）

在 api-gateway 真实就绪前，为下游（teacher-portal / student-portal / parent-portal / admin-portal）提供以下 mock：

- **HTTP mock**（由各前端 portal 自行用 MSW 拦截，api-gateway 不提供 mock 服务）：
  - `/api/v1/iam/v1/login` 返回固定 JWT（mock 签发）+ UserInfo
  - `/api/v1/teacher/v1/*` `/api/v1/student/v1/*` `/api/v1/parent/v1/*` 直接返回各 BFF 的 mock GraphQL 响应
  - `/healthz` `/readyz` 返回 200
- **JWT mock**：前端开发期使用固定 mock JWT（api-gateway 就绪前不走真实验签）

### 4.2 我消费的 mock（在真实上游就绪前）

在真实上游就绪前，api-gateway 使用以下 mock：

- **iam JWKS 公钥**：使用本地固定 mock RS256 公钥（与 mock 私钥配对），验签 mock JWT（DevMode 下生效）
- **iam /readyz 检查**：iam 就绪前软失败，返回 200 + `degraded: true`
- **各 BFF 代理**：BFF 就绪前返回 503 + `Retry-After`，前端降级到本地 mock 数据
- **下游 /healthz 检查**：未就绪服务软失败（返回 200 + degraded），已就绪服务硬失败（返回 503）

---

## §5 变更记录

| 版本 | 日期       | 变更内容                                                                 | 变更者 |
| ---- | ---------- | ------------------------------------------------------------------------ | ------ |
| v1   | 2026-07-09 | 初始创建（含错误：/api/* 路径、gRPC GetPublicKey、GetEffectiveAccess）   | ai01   |
| v2   | 2026-07-10 | 修正：路径 /api→/api/v1、JWKS 拉取 gRPC→HTTP、删除 GetEffectiveAccess、错误码加 GW_ 前缀、对齐 ActionState 信封、补充 shared-go 依赖、补充 admin-portal P6 路由 | ai01   |
