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

@@ -2,15 +2,16 @@ package middleware
import (
"errors"
"log"
"log/slog"
"net/http"
"time"
"github.com/edu-cloud/api-gateway/internal/observability"
"github.com/gin-gonic/gin"
"github.com/sony/gobreaker/v2"
)
// CircuitBreaker 返回针对指定服务的熔断中间件。
// CircuitBreaker 返回针对指定服务的熔断中间件W8 裁决:保持共享 downstream
//
// 基于 gobreaker v2 实现:
// - Interval=5sCLOSED 状态下的统计窗口
@@ -19,7 +20,9 @@ import (
// - MaxRequests=1HALF_OPEN 仅允许 1 个探测请求
//
// 仅当下游返回 5xx 视为失败4xx 与 2xx 不计入熔断。
// 熔断打开或半开探测名额已满时返回 503 + JSON {"error":"circuit_open","retry_after":30}。
// 熔断打开或半开探测名额已满时返回 503 + ActionState 信封W1/W2 裁决):
//
// {"success":false,"error":{"code":"GW_CIRCUIT_OPEN","message":"..."},"retry_after":30}
func CircuitBreaker(serviceName string) gin.HandlerFunc {
cb := gobreaker.NewCircuitBreaker[struct{}](gobreaker.Settings{
Name: serviceName,
@@ -35,7 +38,13 @@ func CircuitBreaker(serviceName string) gin.HandlerFunc {
return counts.TotalFailures*2 > counts.Requests
},
OnStateChange: func(name string, from, to gobreaker.State) {
log.Printf("[circuit-breaker] service=%s state: %s -> %s", name, from, to)
observability.SetCircuitBreakerState(name, from.String(), 0)
observability.SetCircuitBreakerState(name, to.String(), 1)
slog.Info("circuit breaker state changed",
"service", name,
"from", from.String(),
"to", to.String(),
)
},
})
@@ -52,8 +61,13 @@ func CircuitBreaker(serviceName string) gin.HandlerFunc {
if err != nil {
// 熔断打开或半开探测名额已满:返回 503
if errors.Is(err, gobreaker.ErrOpenState) || errors.Is(err, gobreaker.ErrTooManyRequests) {
c.Header("Retry-After", "30")
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{
"error": "circuit_open",
"success": false,
"error": gin.H{
"code": "GW_CIRCUIT_OPEN",
"message": "upstream circuit breaker open",
},
"retry_after": 30,
})
return