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 均通过。
This commit is contained in:
SpecialX
2026-07-10 18:15:48 +08:00
parent 9e767b4e95
commit 4307f6b73c
20 changed files with 797 additions and 382 deletions

View File

@@ -195,21 +195,30 @@
### 2.1 api-gatewayGo
| 场景 | 技术/规则 |
| ----------------- | ------------------------------------------------------------------------------------------------------------- |
| P1 鉴权 | Gateway 内置 HS256 JWT`jwt.ParseWithClaims` + `SigningMethodHMAC` 校验 |
| P2 鉴权升级 | 改 RS256IAM 私钥签发Gateway 公钥校验,无需调 IAM |
| 路由转发 | `gin.Group("/api/v1")` + `httputil.NewSingleHostReverseProxy` |
| 路径重写 | 去掉 `/api/v1` 前缀后转发到下游服务 |
| 用户上下文注入 | `c.Request.Header.Set("x-user-id", claims.UserID)` 传递给下游 |
| 请求 ID | Gateway 生成或透传 `X-Request-ID``c.Set("request_id", ...)` + `c.Header(...)` |
| P1 不做 | 限流/熔断/灰度P6 硬化阶段实现) |
| 健康检查 | `GET /health` 无需鉴权 |
| 尾斜杠重定向循环 | `r.RedirectTrailingSlash=false` + 同时注册 `Any("/classes")` 与 `Any("/classes/*path")` |
| 开发模式鉴权旁路 | `DEV_MODE=true` 时接受 `Bearer dev-token`,注入固定身份;生产必须 `false` |
| 路由注册位置 | 真实路由在 `main.go` 的 `api.Group` 内注册,`internal/routing/routing.go` 若未被 main 引用即为死代码 |
| 多服务路由扩展 | 新增服务代理时在 main.go 注册两组路由:`Any("/x")` + `Any("/x/*path")`,与 classes 一致 |
| DEV_MODE 环境变量 | Go 不自动加载 .env`DEV_MODE` 必须在启动前 export 或写入系统环境变量,否则 DevMode=false 导致 dev-token 被拒 |
| 场景 | 技术/规则 |
| ---------------------- | ------------------------------------------------------------------------------------------------------------- |
| P2 鉴权RS256 | `shared-go/jwks.Fetcher` 拉 IAM `/.well-known/jwks.json` 公钥TTL 5min 缓存,`jwt.Parse` 用公钥验签 |
| 路由转发 | `gin.Group("/api/v1")` + `httputil.NewSingleHostReverseProxy` |
| 路径重写 | 去掉 `/api/v1` 前缀后转发到下游服务(方案 BGateway 不改路径,各服务 Controller 内加 `/v1` |
| 用户上下文注入 | `c.Request.Header.Set("x-user-id/x-user-roles/x-data-scope", ...)` 传递给下游 |
| 请求 ID | Gateway 生成或透传 `X-Request-Id``req-<uuid-v4>` 格式),`c.Set("request_id", ...)` + `c.Header(...)` |
| 限流 | per-IP 令牌桶(`sync.Map`),超限 429 `GW_RATE_LIMITED`P6 迁 Redis 支持多副本 |
| 熔断 | `gobreaker v2` 共享 downstream 实例5xx > 50% 触发 OPEN返回 503 `GW_CIRCUIT_OPEN` + Retry-After |
| 错误码 | 统一 `GW_` 前缀W1响应体 ActionState 信封 `{success,error:{code,message}}`W2 |
| 结构化日志 | 标准库 `log/slog`W3`slog.Info`/`slog.Error` 带 key-value未接入 shared-go/loggerzap待评估 |
| 健康检查 | `GET /healthz`liveness无鉴权+ `GET /readyz`readiness并行 ping 9 下游 /healthz软失败规则W4 |
| Metrics | 7 个业务指标promauto`GET /metrics` 暴露W5requests/duration/circuit/ratelimit/proxy/jwks/authfail |
| Tracer 资源属性 | `resource.New` 补 service.name/version/deployment.environment/host.nameW64 项完整) |
| DevMode 生产防护 | 启动时 `DevMode=true && ENV=production` panic 拒绝启动W7config.go Load 校验) |
| 尾斜杠重定向循环 | `r.RedirectTrailingSlash=false` + 同时注册 `Any("/classes")` 与 `Any("/classes/*path")` |
| 开发模式鉴权旁路 | `DEV_MODE=true` 时接受 `Bearer dev-token`,注入固定身份;生产必须 `false` |
| 路由注册位置 | 真实路由在 `main.go` 的 `api.Group` 内注册,`internal/routing/routing.go` 若未被 main 引用即为死代码 |
| 多服务路由扩展 | 新增服务代理时用 `registerProxy(api, prefix, targetURL)` helper 注册两组路由,统一管理 |
| DEV_MODE 环境变量 | Go 不自动加载 .env`DEV_MODE` 必须在启动前 export 或写入系统环境变量,否则 DevMode=false 导致 dev-token 被拒 |
| go.work workspace 解析 | `go.work` 列 `use ./packages/shared-go`,本地解析 shared-go 依赖,无需 `go mod tidy` 拉远程 |
| go.work 版本冲突 | go.work 的 `go` 版本须 ≥ workspace 内所有模块的最高版本push-gateway 要求 1.25.0,故 go.work 用 1.25.0 |
| CORS 配置 | `CORS(cfg *config.Config)` 从 Config 读白名单未配置时用开发默认白名单localhost:3000,3001 |
| 请求体限制 | `RequestBodyLimit` 先检 Content-Length 再用 `http.MaxBytesReader`,超限 413 `GW_REQUEST_TOO_LARGE` 信封 |
### 2.2 classesTS/NestJSP1 黄金模板)