依据 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 均通过。
15 KiB
15 KiB
模块理解确认书 — api-gateway
AI:ai01(Go 网关层) 阶段:阶段 1 交付物 日期:2026-07-09 关联:004 架构影响地图、AI 分配方案
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 与 Dockerfile 一致)
- 框架:Gin v1.12.0
- 核心依赖:
github.com/golang-jwt/jwt/v5v5.2.1(JWT 校验)github.com/sony/gobreaker/v2v2.1.0(熔断)github.com/google/uuidv1.6.0(请求 ID)go.opentelemetry.io/otelv1.44.0 +otelginv0.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)供本服务拉公钥
- P2 iam 服务须暴露 JWKS 端点(
6. 我需要对齐的黄金模板项(对照 classes 服务)
| 项 | classes(黄金模板) | api-gateway 现状 | 差距 |
|---|---|---|---|
| 权限装饰器 | @RequirePermission() |
N/A(Go 无装饰器;用中间件 AuthMiddleware 替代) |
✅ 等价实现 |
| 错误码前缀 | CLASSES_* |
无前缀(基础设施层) | ✅ 设计合理 |
| logger(pino) | 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/A(Go 无 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-10(P2-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 持有权限点常量造成耦合