update workline to detail full P2-P6 tasks with clear milestones and dependencies revise contract to correct route prefix, JWKS method and dependency descriptions clean up outdated issue content
211 lines
16 KiB
Markdown
211 lines
16 KiB
Markdown
# 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 |
|