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,7 +1,7 @@
package middleware
import (
"log"
"log/slog"
"net/http"
"runtime/debug"
@@ -9,20 +9,32 @@ import (
"github.com/google/uuid"
)
// Recovery 捕获 panic 并返回 500,记录堆栈日志
// 返回 JSON {"error":"internal_error","request_id":"<uuid>"}。
// request_id 使用 uuid.New() 生成Recovery 在 RequestID 之前注册,
// panic 发生时上下文中可能尚无 request_id故独立生成
// Recovery 捕获 panic 并返回 500ActionState 信封W1/W2 裁决)
//
// 响应体:{"success":false,"error":{"code":"GW_INTERNAL_ERROR","message":"..."},"request_id":"<uuid>"}。
// request_id 从 context 取RequestID 中间件注入),若 Recovery 在 RequestID 之前触发
// 导致 context 无 request_id则独立生成 uuid 保证可追溯。
func Recovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
stack := debug.Stack()
requestID := uuid.New().String()
log.Printf("[recovery] panic recovered, request_id=%s: %v\n%s", requestID, r, stack)
// 若已写入部分响应AbortWithStatusJSON 仍会设置状态并尝试写 JSON
// 优先从 context 取 request_idRequestID 中间件已注入)
requestID, exists := c.Get(requestIDContextKey)
if !exists {
requestID = uuid.New().String()
}
slog.Error("panic recovered",
"request_id", requestID,
"error", r,
"stack", string(stack),
)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": "internal_error",
"success": false,
"error": gin.H{
"code": "GW_INTERNAL_ERROR",
"message": "internal server error",
},
"request_id": requestID,
})
}