Files
Edu/docs/architecture/issues/contracts/api-gateway_contract.md

211 lines
16 KiB
Markdown
Raw Permalink 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 对接契约
> 负责人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
> 版本v22026-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 | PathGateway 外部路径) | 目标服务 | 端口 | 鉴权 | 公开子路径 | 阶段 |
| ------ | --------------------------------------------------- | ------------------------- | ----- | ---- | --------------------------------------- | ---- |
| 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-bffGraphQL | 3003 | JWT | — | P2 |
| ANY | /api/v1/student/v1/*path + /api/v1/student/v1 | student-bffGraphQL | 3009 | JWT | — | P3 |
| ANY | /api/v1/parent/v1/*path + /api/v1/parent/v1 | parent-bffGraphQL | 3010 | JWT | — | P4 |
| ANY | /api/v1/classes/v1/*path + /api/v1/classes/v1 | core-educlasses 合并) | 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 验签,不解析 GraphQLGraphQL 由 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 公钥集JWKSTTL 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 FetcherHTTP 拉 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/pushWebSocket 协作评估)| 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` 返回固定 JWTmock 签发)+ UserInfo
- `/api/v1/teacher/v1/*` `/api/v1/student/v1/*` `/api/v1/parent/v1/*` 直接返回各 BFF 的 mock GraphQL 响应
- `/healthz` `/readyz` 返回 200
- **JWT mock**:前端开发期使用固定 mock JWTapi-gateway 就绪前不走真实验签)
### 4.2 我消费的 mock在真实上游就绪前
在真实上游就绪前api-gateway 使用以下 mock
- **iam JWKS 公钥**:使用本地固定 mock RS256 公钥(与 mock 私钥配对),验签 mock JWTDevMode 下生效)
- **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 |