Files
Edu/services/api-gateway/docs/01-understanding.md
SpecialX 834e2c61fd docs(api-gateway): ai01 阶段1+2 模块理解确认书与架构设计文档
阶段1交付:services/api-gateway/docs/01-understanding.md
- 8节内容:架构位置/限界上下文/契约/技术栈/阶段归属/黄金模板对齐审计
- 审计13项差距(3高:缺/metrics、/readyz stub、auth.go死代码;4中:log/slog缺失、go.mod版本不匹配、HS256待升RS256、DevMode风险;6低)

阶段2交付:services/api-gateway/docs/02-architecture-design.md
- 9节内容:内部分层图/路由表矩阵9下游/限流策略表/熔断阈值表/JWT RS256流程含JWKS缓存/CORS白名单/请求ID注入/metrics 7项指标/P0-P3实施优先级

同步更新 docs/troubleshooting/known-issues.md 工作经验日志(追加ai01条目)

AI Agent: ai01 (api-gateway/push-gateway)
Branch: main
Coordinator: coord
2026-07-09 17:21:03 +08:00

146 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.22+go.mod 声明 1.25.0,需与 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` | ⚠️ 待补 `log/slog` 结构化日志 |
| metrics | `shared/observability/metrics.ts` 暴露 `/metrics` | ❌ 无 `/metrics` 端点 | ⚠️ 待补 prom-client |
| tracer | `shared/observability/tracer.ts` OTel SDK | ✅ `internal/observability/tracer.go` | ✅ 对齐 |
| `/healthz` | ✅ | ✅ | ✅ 对齐 |
| `/readyz` | ✅ 检查 DB `SELECT 1` | ❌ stub 直接返回 200 | ⚠️ 待补下游服务健康检查 |
| 优雅关闭 | 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 | ⚠️ 中间件替代 | ✅ 无前缀合理 | ❌ 标准 log | ❌ 无 | ✅ OTel | ✅ | ⚠️ stub | ✅ 5s 超时 | ~25% | ✅ 多阶段 |
### 7.1 详细问题清单(按严重度排序)
| # | 严重度 | 文件 | 问题 | 修复建议 |
| --- | ------ | -------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| 1 | 高 | `internal/observability/` 缺失 | 无 `/metrics` 端点Prometheus 404 | 新增 `metrics.go`,注册 `http_requests_total`/`http_request_duration_seconds`/`circuit_breaker_state`,在 main.go 暴露 `/metrics` |
| 2 | 高 | `internal/health/health.go` | `/readyz` 直接返回 200未检查下游 | 改为并行 ping 9 个下游 `/healthz`,任一不可达返回 503超时 2s |
| 3 | 高 | `internal/middleware/auth.go` L124-139 | 死代码 `RequestIDMiddleware()` + `generateUUID()` 重复 requestid.go 且未使用;`uuid` 包未导入 | 删除 L124-139已由 `requestid.go` 实现) |
| 4 | 高 | 全文件 | 用 `log.Printf`,不符合 coding-standards §3.8 `log/slog` 结构化日志要求 | 引入 `slog.New(slog.NewJSONHandler(os.Stdout))`,所有日志带 `request_id`/`trace_id` |
| 5 | 中 | `go.mod` L3 vs `Dockerfile` L1 | go.mod 声明 `go 1.25.0`Dockerfile 用 `golang:1.22-alpine` | 统一为 `go 1.22`(与 Dockerfile 一致),或升级 Dockerfile 到 `golang:1.25-alpine` |
| 6 | 中 | `internal/middleware/auth.go` | P2 待升级 HS256 → RS256 | 新增 `JWKSFetcher` 缓存 IAM 公钥TTL 1h`jwt.Parse``jwt.WithKeySet(jwks)` |
| 7 | 中 | `internal/middleware/cors.go` L21 | `CORS_ORIGINS` 直接 `os.Getenv`,未纳入 Config 结构 | 移入 `config.Config.CORSOrigins`,与其他配置统一 |
| 8 | 中 | `internal/middleware/ratelimit.go` | 单实例内存令牌桶,水平扩展后限流失效 | P6 引入 Redis 令牌桶(`redis_rate`)或保留单实例但文档标注 |
| 9 | 中 | `internal/middleware/auth.go` L68 | DevMode 注入固定 `teacher,admin` 角色,生产风险 | 启动时若 `DevMode=true && ENV=production` 则 panic 拒绝启动 |
| 10 | 低 | `README.md` L38 | 提到 `GET /health` 兼容端点,但代码未注册 | 删除 README 描述或补注册 |
| 11 | 低 | `internal/proxy/proxy.go` L24 | 连续两次 `TrimPrefix``/api/v1` 后再 `/api`)逻辑冗余 | 第二次 `TrimPrefix("/api")` 实际无效果(首字符已是 `/`),可删 |
| 12 | 低 | `Dockerfile` L18 | 构建命令 `./main.go` 而非 `./` | 改为 `go build -ldflags="-s -w" -o /app/bin/api-gateway .` 更规范 |
| 13 | 低 | 测试 | auth/cors/security/recovery/requestid/proxy 无测试 | 补 `*_test.go`,目标覆盖率 ≥ 80% |
## 8. 风险与假设
- **假设**iam 服务在 P2 会暴露 JWKS 端点,否则 RS256 升级阻塞
- **假设**:所有下游服务在 P1 都已实现 `/healthz`(用于 `/readyz` 真实检查)
- **风险**单实例限流在多副本部署后失效P6 需迁 Redis
- **风险**DevMode 旁路若误开到生产,可绕过鉴权注入 admin 角色
- **未决**:是否在 Gateway 层做权限校验(当前仅透传角色,由下游服务自校验)?建议保持现状,避免 Gateway 持有权限点常量造成耦合