阶段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
146 lines
15 KiB
Markdown
146 lines
15 KiB
Markdown
# 模块理解确认书 — api-gateway
|
||
|
||
> AI:ai01(Go 网关层)
|
||
> 阶段:阶段 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 个反向代理目标
|
||
- BFF:teacher-bff(3003)、student-bff(待建)、parent-bff(待建)
|
||
- 业务:iam(3002)、core-edu(3004)、content(3005)、data-ana(3006)、msg(3007)、ai(3008)
|
||
- **通信方式**:
|
||
- 入口: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.1(JWT 校验)
|
||
- `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`)
|
||
- **构建**:多阶段 Dockerfile(golang: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 → RS256(IAM 签发,Gateway 公钥校验)
|
||
- **依赖上游阶段产出**:
|
||
- P2 iam 服务须暴露 JWKS 端点(`/.well-known/jwks.json`)供本服务拉公钥
|
||
|
||
## 6. 我需要对齐的黄金模板项(对照 classes 服务)
|
||
|
||
| 项 | classes(黄金模板) | api-gateway 现状 | 差距 |
|
||
| ----------------- | ------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------------- |
|
||
| 权限装饰器 | `@RequirePermission()` | N/A(Go 无装饰器;用中间件 `AuthMiddleware` 替代) | ✅ 等价实现 |
|
||
| 错误码前缀 | `CLASSES_*` | 无前缀(基础设施层) | ✅ 设计合理 |
|
||
| logger(pino) | `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/A(Go 无 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 持有权限点常量造成耦合
|