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

@@ -1,20 +1,26 @@
package config
import (
"log"
"log/slog"
"os"
"strconv"
)
// Config 持有 api-gateway 运行时配置
// Config 持有 api-gateway 运行时配置
// P2 起 JWT 验签改 RS256IAM 签发Gateway 用 JWKS 公钥校验),
// JWTSecret 仅在 DevMode 下作为 mock 密钥保留。
type Config struct {
Port string
JWTSecret string
JWKSURL string // RS256 公钥端点IAM GET /.well-known/jwks.json
JWTSecret string // DevMode 下 mock 用(生产不再使用 HS256
JWTIssuer string
JWTAudience string
CORSOrigins string
ClassesServiceURL string
IamServiceURL string
TeacherBffURL string
StudentBffURL string
ParentBffURL string
CoreEduServiceURL string
ContentServiceURL string
DataAnaServiceURL string
@@ -23,33 +29,57 @@ type Config struct {
OTLPEndpoint string
LogLevel string
DevMode bool
Env string // 部署环境标识production / developmentW7 防护用)
}
// devJWTSecret 是 DevMode 下的默认 JWT 密钥(仅用于本地联调,生产必须配置 JWT_SECRET
// devJWTSecret 是 DevMode 下的默认 JWT 密钥(仅用于本地联调,生产必须关闭 DevMode
const devJWTSecret = "p1-dev-secret-change-in-production"
// Load 从环境变量加载配置。
// 非 DevMode 下若 JWT_SECRET 未配置则 fatal 退出DevMode 下使用默认密钥并打印 warning。
//
// W7 生产防护DevMode=true 且 ENV=production 时 panic 拒绝启动,
// 避免 dev-token 旁路鉴权被误开到生产环境。
// P2 起 RS256非 DevMode 下要求 IAM_JWKS_URLJWKS 公钥端点),
// JWT_SECRET 不再是生产必填(仅 DevMode mock 用)。
func Load() *Config {
devMode := getEnvBool("DEV_MODE", false)
jwtSecret := getEnv("JWT_SECRET", "")
if jwtSecret == "" {
if devMode {
log.Println("warning: JWT_SECRET not set, using dev default (DEV_MODE=true)")
jwtSecret = devJWTSecret
} else {
log.Fatal("JWT_SECRET must be set in non-dev mode")
}
env := getEnv("ENV", "development")
// W7 生产防护DevMode 旁路仅允许非生产环境
if devMode && env == "production" {
panic("DEV_MODE=true is not allowed in production (ENV=production)")
}
jwtSecret := getEnv("JWT_SECRET", "")
if jwtSecret == "" && devMode {
slog.Warn("JWT_SECRET not set, using dev default (DEV_MODE=true)")
jwtSecret = devJWTSecret
}
// 非 DevMode 下要求 JWKS URLRS256 验签)
jwksURL := getEnv("IAM_JWKS_URL", "http://localhost:3002/.well-known/jwks.json")
if !devMode && jwksURL == "" {
panic("IAM_JWKS_URL must be set in non-dev mode (RS256 JWT verification)")
}
slog.Info("config loaded",
"env", env,
"dev_mode", devMode,
"jwks_url", jwksURL,
)
return &Config{
Port: getEnv("API_GATEWAY_PORT", "8080"),
JWKSURL: jwksURL,
JWTSecret: jwtSecret,
JWTIssuer: getEnv("JWT_ISSUER", "next-edu-cloud"),
JWTAudience: getEnv("JWT_AUDIENCE", "next-edu-cloud"),
CORSOrigins: getEnv("CORS_ORIGINS", ""),
ClassesServiceURL: getEnv("CLASSES_SERVICE_URL", "http://localhost:3001"),
IamServiceURL: getEnv("IAM_SERVICE_URL", "http://localhost:3002"),
TeacherBffURL: getEnv("TEACHER_BFF_URL", "http://localhost:3003"),
StudentBffURL: getEnv("STUDENT_BFF_URL", "http://localhost:3009"),
ParentBffURL: getEnv("PARENT_BFF_URL", "http://localhost:3010"),
CoreEduServiceURL: getEnv("CORE_EDU_SERVICE_URL", "http://localhost:3004"),
ContentServiceURL: getEnv("CONTENT_SERVICE_URL", "http://localhost:3005"),
DataAnaServiceURL: getEnv("DATA_ANA_SERVICE_URL", "http://localhost:3006"),
@@ -58,10 +88,11 @@ func Load() *Config {
OTLPEndpoint: getEnv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318"),
LogLevel: getEnv("LOG_LEVEL", "info"),
DevMode: devMode,
Env: env,
}
}
// getEnv 读取环境变量,缺失时返回 fallback
// getEnv 读取环境变量,缺失或空字符串时返回 fallback
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v