Files
Edu/services/api-gateway/docs/01-understanding.md
SpecialX 4307f6b73c feat(api-gateway): 实现 W1-W8 网关硬化与 P2-P5 路由扩展
依据 coord-final-decisions §3.8 W1-W8 裁决与
president-final-rulings §2.15/§2.16/§2.19 完整实现网关硬化:

- W1/W2: 错误码 GW_ 前缀 + ActionState 信封响应体
- W3: 全量替换为 log/slog 结构化日志
- W4: /readyz 并行 ping 9 下游 + 软失败规则
- W5: 7 个业务 Prometheus 指标 + /metrics 端点
- W6: tracer 资源属性补全(name/version/env/host)
- W7: DevMode=true && ENV=production panic 防护
- W8: 保持共享 downstream 熔断

P2 RS256 升级:接入 shared-go/jwks.Fetcher(TTL 5min)。
P2.7+P3-P5 路由扩展:student/parent/messages/dashboard。
文档同步:README/01/02/known-issues,arch.db 已更新。
质量校验:go vet + build + test 均通过。
2026-07-10 18:15:48 +08:00

148 lines
15 KiB
Markdown
Raw 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
> AIai01Go 网关层)
> 阶段:阶段 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 个反向代理目标
- BFFteacher-bff3003、student-bff待建、parent-bff待建
- 业务iam3002、core-edu3004、content3005、data-ana3006、msg3007、ai3008
- **通信方式**
- 入口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.22go.mod 与 Dockerfile 一致)
- **框架**Gin v1.12.0
- **核心依赖**
- `github.com/golang-jwt/jwt/v5` v5.2.1JWT 校验)
- `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`
- **构建**:多阶段 Dockerfilegolang: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 → RS256IAM 签发Gateway 公钥校验)
- **依赖上游阶段产出**
- P2 iam 服务须暴露 JWKS 端点(`/.well-known/jwks.json`)供本服务拉公钥
## 6. 我需要对齐的黄金模板项(对照 classes 服务)
| 项 | classes黄金模板 | api-gateway 现状 | 差距 |
| ----------------- | ------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- |
| 权限装饰器 | `@RequirePermission()` | N/AGo 无装饰器;用中间件 `AuthMiddleware` 替代) | ✅ 等价实现 |
| 错误码前缀 | `CLASSES_*` | 无前缀(基础设施层) | ✅ 设计合理 |
| loggerpino | `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/AGo 无 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-10P2-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 持有权限点常量造成耦合