package router import ( "context" "errors" "fmt" "net/http" "runtime/debug" "sort" "strings" "sync" "sync/atomic" "time" "github.com/EthanCodeCraft/xlgo-core/logger" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" ) var routerWarnf atomic.Value // stores func(string, ...any) func warnf(format string, args ...any) { if fn, ok := routerWarnf.Load().(func(string, ...any)); ok && fn != nil { fn(format, args...) return } logger.Warnf(format, args...) } func isDuplicateRoutePanic(msg, path string) bool { if strings.Contains(msg, "already registered") { return true } if !strings.Contains(msg, "conflicts with existing wildcard") { return false } return strings.Contains(msg, fmt.Sprintf("new path '%s'", path)) && strings.Contains(msg, fmt.Sprintf("existing prefix '%s'", path)) } // HealthCheck 健康检查项 type HealthCheck struct { Name string Check func(context.Context) error Disabled bool Timeout time.Duration } // DefaultHealthCheckTimeout bounds each dependency check so /health and /readyz // do not hang behind a stuck database/client call. const DefaultHealthCheckTimeout = 2 * time.Second type healthCheckRunner struct { check HealthCheck running chan struct{} } func newHealthCheckRunners(checks []HealthCheck) []healthCheckRunner { runners := make([]healthCheckRunner, len(checks)) for i, check := range checks { runners[i] = healthCheckRunner{ check: check, running: make(chan struct{}, 1), } } return runners } func healthCheckTimeout(check HealthCheck) time.Duration { if check.Timeout > 0 { return check.Timeout } return DefaultHealthCheckTimeout } func healthCheckStatusFromContext(err error) string { if errors.Is(err, context.DeadlineExceeded) { return "timeout" } return "error" } func (r healthCheckRunner) run(ctx context.Context) (string, error) { check := r.check checkCtx, cancel := context.WithTimeout(ctx, healthCheckTimeout(check)) defer cancel() select { case r.running <- struct{}{}: case <-checkCtx.Done(): return healthCheckStatusFromContext(checkCtx.Err()), checkCtx.Err() } errCh := make(chan error, 1) go func() { defer func() { <-r.running }() defer func() { if rec := recover(); rec != nil { errCh <- fmt.Errorf("health check %q panic recovered: %v\n%s", check.Name, rec, debug.Stack()) } }() errCh <- check.Check(checkCtx) }() select { case err := <-errCh: if err == nil { return "ok", nil } return healthCheckStatusFromContext(err), err case <-checkCtx.Done(): return healthCheckStatusFromContext(checkCtx.Err()), checkCtx.Err() } } // registerGETOnce 幂等注册 GET 路由(H8d 收尾:消除 defaultModule 与 Register* 系列 // 并存时 Gin 重复路由 panic 的 footgun)。若 (GET, path) 已存在则静默跳过——首次注册胜出。 // // 实现分两条路径: // - *gin.Engine:经 Routes() 精确预检(method+path),命中即跳过;未命中则直接注册, // 不吞 panic——真正不同的路由冲突仍按 gin 原语义 panic,不被掩盖。 // - *gin.RouterGroup(gin 未暴露其 engine,无法预检):用 recover 兜底,仅吞 gin 的 // 重复路由 panic("already registered" / "conflicts with existing wildcard", // 两者覆盖 gin 对重复注册的两类 panic),其余 panic 原样抛出。defaultModule 的 // /swagger/*any 与 /health 重复注册即走此路径被吞。最坏情况(gin 改动文本)退化为 // 当前行为(panic),不引入新风险。 // // 注册期单线程调用,无并发问题。 func registerGETOnce(r gin.IRoutes, path string, h gin.HandlerFunc) { if r == nil { warnf("router: skip GET %q because routes is nil", path) return } if h == nil { warnf("router: skip GET %q because handler is nil", path) return } switch route := r.(type) { case *gin.Engine: eng := route if eng == nil { warnf("router: skip GET %q because engine is nil", path) return } for _, ri := range eng.Routes() { if ri.Method == http.MethodGet && ri.Path == path { warnf("router: duplicate GET %q skipped", path) return } } eng.GET(path, h) return case *gin.RouterGroup: if route == nil { warnf("router: skip GET %q because router group is nil", path) return } } defer func() { if rec := recover(); rec != nil { msg := fmt.Sprint(rec) if isDuplicateRoutePanic(msg, path) { warnf("router: duplicate GET %q skipped: %v", path, rec) return // 重复路由,静默跳过 } panic(rec) // 非重复路由 panic,原样抛出 } }() r.GET(path, h) } // runHealthChecks 执行所有检查项,返回总体状态、HTTP code 与逐项结果。 // 无检查项时视为健康(用于 /livez 与无依赖场景)。 func runHealthChecks(ctx context.Context, runners []healthCheckRunner) (string, int, map[string]string) { if len(runners) == 0 { return "ok", http.StatusOK, nil } status := "ok" code := http.StatusOK result := make(map[string]string, len(runners)) for _, runner := range runners { check := runner.check if check.Name == "" { continue } if check.Disabled || check.Check == nil { result[check.Name] = "disabled" continue } checkStatus, err := runner.run(ctx) result[check.Name] = checkStatus if err != nil { status = "error" code = http.StatusServiceUnavailable continue } } return status, code, result } // healthHandler 返回统一的 /health 风格响应(H8d 收敛)。 // 无检查项时视为健康(200 + {"status":"ok"});有检查项时任一失败返回 503 // 并附逐项结果。RegisterHealthRoute / RegisterReadinessRoute / defaultModule // 均委托此实现,避免三个 /health 行为与响应体不一致。 func healthHandler(checks []HealthCheck) gin.HandlerFunc { runners := newHealthCheckRunners(checks) return func(c *gin.Context) { status, code, result := runHealthChecks(c.Request.Context(), runners) if result == nil { c.JSON(http.StatusOK, gin.H{"status": status}) return } c.JSON(code, gin.H{"status": status, "checks": result}) } } // RegisterHealthRoute 注册健康检查路由(兼容端点,等价于 readiness)。 // 幂等:若 /health 已注册则跳过(首次注册胜出),避免与 defaultModule 等并存时重复路由 panic。 func RegisterHealthRoute(r *gin.Engine, checks ...HealthCheck) { registerGETOnce(r, "/health", healthHandler(checks)) } // RegisterLivenessRoute 注册存活性探针(#17)。 // GET /livez 永不依赖外部,仅表示进程存活,始终返回 200。 // 供 K8s livenessProbe 使用:失败由进程崩溃体现,而非端点返回 503。 // 幂等:重复注册跳过。 func RegisterLivenessRoute(r *gin.Engine) { registerGETOnce(r, "/livez", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) } // RegisterReadinessRoute 注册就绪性探针(#17)。 // GET /readyz 复用 HealthCheck 检查依赖(mysql/redis...),任一失败返回 503。 // 供 K8s readinessProbe 使用:未就绪时不接流量。 // 幂等:重复注册跳过。 func RegisterReadinessRoute(r *gin.Engine, checks ...HealthCheck) { registerGETOnce(r, "/readyz", healthHandler(checks)) } // RegisterSwaggerRoutes 注册 Swagger 文档路由。幂等:重复注册跳过。 func RegisterSwaggerRoutes(r *gin.Engine) { registerGETOnce(r, "/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) } // RegisterDefaultRoutes 注册框架默认路由(健康检查、Swagger) // 用户可以选择使用或不使用这些默认路由 func RegisterDefaultRoutes(r *gin.Engine, checks ...HealthCheck) { RegisterSwaggerRoutes(r) RegisterHealthRoute(r, checks...) } // DefaultModule 默认路由模块(可用于 WithModules) var DefaultModule = &defaultModule{} type defaultModule struct{} func (m *defaultModule) Name() string { return "default" } func (m *defaultModule) Register(r *gin.RouterGroup) { // 作为模块注册时,路由在根路径。经 registerGETOnce 幂等注册(H8d 收尾): // 若用户已通过 RegisterSwaggerRoutes / RegisterHealthRoute 注册过同名路由, // 此处静默跳过,避免 Gin 重复路由 panic。首次注册胜出。 registerGETOnce(r, "/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) registerGETOnce(r, "/health", healthHandler(nil)) } // Module 路由模块接口 // 用户实现此接口来注册业务路由 type Module interface { // Name 模块名称(用于日志和调试) Name() string // Register 注册路由到指定组 Register(r *gin.RouterGroup) } // ModuleFunc 函数式模块(简化单文件模块注册) type ModuleFunc func(r *gin.RouterGroup) // Register 实现 Module 接口 func (f ModuleFunc) Register(r *gin.RouterGroup) { f(r) } // Name 实现 Module 接口(函数式模块默认名称) func (f ModuleFunc) Name() string { return "func-module" } // VersionedAPI 版本化 API 配置 type VersionedAPI struct { Version string // 版本标识,如 "v1", "v2" BasePath string // 基础路径,如 "/api/v1" Modules []Module // 该版本的模块列表 Middlewares []gin.HandlerFunc // 该版本的公共中间件 } // MiddlewareGroup 中间件分组 type MiddlewareGroup struct { Name string Middlewares []gin.HandlerFunc } // Registry 路由注册中心 type Registry struct { mu sync.Mutex engine *gin.Engine modules []Module versions map[string]*VersionedAPI middlewareGroups map[string]*MiddlewareGroup globalMiddlewares []gin.HandlerFunc // metricsMiddleware 在 Apply 时作为首个全局中间件装入(H8c), // 使所有经注册中心注册的路由都被采集,不依赖 RegisterMetricsRoute 的调用顺序。 // 为 nil 时不装入。/metrics 端点本身与 /health 等基础路由直接挂在 engine 上, // 不经此中间件,故不被自采集。 metricsMiddleware gin.HandlerFunc // applyOnce 保证 Apply 仅执行一次(H8b + P1 #13)。 // 原实现用裸 bool 无同步,并发 Apply 会竞态并可能重复 engine.Use/重复注册致 gin panic; // 改用 sync.Once,二次/并发 Apply 均安全幂等。 applyOnce sync.Once applied atomic.Bool } // NewRegistry 创建路由注册中心 func NewRegistry(engine *gin.Engine) *Registry { return &Registry{ engine: engine, modules: make([]Module, 0), versions: make(map[string]*VersionedAPI), middlewareGroups: make(map[string]*MiddlewareGroup), } } func (r *Registry) lockForMutation(action string) (func(), bool) { if r == nil { warnf("router: skip %s because registry is nil", action) return nil, false } r.mu.Lock() if r.applied.Load() { r.mu.Unlock() warnf("router: %s called after Apply; registration will not take effect", action) return nil, false } return r.mu.Unlock, true } func filterNilHandlers(action string, handlers []gin.HandlerFunc) []gin.HandlerFunc { out := make([]gin.HandlerFunc, 0, len(handlers)) for _, h := range handlers { if h == nil { warnf("router: nil middleware skipped in %s", action) continue } out = append(out, h) } return out } // Use 注册全局中间件 func (r *Registry) Use(middlewares ...gin.HandlerFunc) *Registry { unlock, ok := r.lockForMutation("Use") if !ok { return r } defer unlock() r.globalMiddlewares = append(r.globalMiddlewares, filterNilHandlers("Use", middlewares)...) return r } // RegisterModule 注册模块(无版本) func (r *Registry) RegisterModule(module Module) *Registry { unlock, ok := r.lockForMutation("RegisterModule") if !ok { return r } defer unlock() if module == nil { warnf("router: nil module skipped") return r } r.modules = append(r.modules, module) return r } // RegisterModuleFunc 注册函数式模块 func (r *Registry) RegisterModuleFunc(name string, fn func(r *gin.RouterGroup)) *Registry { if fn == nil { warnf("router: module func %q skipped because function is nil", name) return r } return r.RegisterModule(&namedModule{name: name, fn: fn}) } // namedModule 命名模块包装(内部类型) type namedModule struct { name string fn func(r *gin.RouterGroup) } func (m *namedModule) Name() string { return m.name } func (m *namedModule) Register(r *gin.RouterGroup) { m.fn(r) } // RegisterVersion 注册版本化 API func (r *Registry) RegisterVersion(version *VersionedAPI) *Registry { unlock, ok := r.lockForMutation("RegisterVersion") if !ok { return r } defer unlock() if version == nil { warnf("router: nil version skipped") return r } version.Middlewares = filterNilHandlers("RegisterVersion "+version.Version, version.Middlewares) if _, exists := r.versions[version.Version]; exists { warnf("router: duplicate version %q overwritten", version.Version) } r.versions[version.Version] = version return r } // RegisterMiddlewareGroup 注册中间件分组 func (r *Registry) RegisterMiddlewareGroup(group *MiddlewareGroup) *Registry { unlock, ok := r.lockForMutation("RegisterMiddlewareGroup") if !ok { return r } defer unlock() if group == nil { warnf("router: nil middleware group skipped") return r } group.Middlewares = filterNilHandlers("RegisterMiddlewareGroup "+group.Name, group.Middlewares) if _, exists := r.middlewareGroups[group.Name]; exists { warnf("router: duplicate middleware group %q overwritten", group.Name) } r.middlewareGroups[group.Name] = group return r } // GetMiddlewareGroup 获取中间件分组 func (r *Registry) GetMiddlewareGroup(name string) []gin.HandlerFunc { if r == nil { warnf("router: GetMiddlewareGroup(%q) on nil registry", name) return nil } r.mu.Lock() defer r.mu.Unlock() if group, ok := r.middlewareGroups[name]; ok { return append([]gin.HandlerFunc(nil), group.Middlewares...) } return nil } // SetMetricsMiddleware 设置指标采集中间件(H8c)。Apply 时它会作为首个全局中间件 // 装入 engine,使所有经注册中心注册的路由都被采集,不再依赖注册顺序。 // 传入 nil 清除。须在 Apply 之前调用。 func (r *Registry) SetMetricsMiddleware(mw gin.HandlerFunc) { unlock, ok := r.lockForMutation("SetMetricsMiddleware") if !ok { return } defer unlock() r.metricsMiddleware = mw } type versionApplySnapshot struct { isNil bool version string basePath string middlewares []gin.HandlerFunc modules []Module } // Apply 应用所有路由注册。 // // 幂等(H8b):二次调用直接返回,避免重复 engine.Use 与 Gin 重复路由 panic。 // 装入顺序:metrics 中间件(若有)→ 用户全局中间件 → 模块/版本路由。 // metrics 置于首位保证全量业务路由被采集,且不依赖 RegisterMetricsRoute 调用顺序。 func (r *Registry) Apply() { if r == nil { warnf("router: Apply called on nil registry") return } if r.engine == nil { warnf("router: Apply skipped because engine is nil") return } r.applyOnce.Do(func() { r.mu.Lock() r.applied.Store(true) metricsMiddleware := r.metricsMiddleware globalMiddlewares := append([]gin.HandlerFunc(nil), r.globalMiddlewares...) modules := append([]Module(nil), r.modules...) versions := make([]versionApplySnapshot, 0, len(r.versions)) versionKeys := make([]string, 0, len(r.versions)) for k := range r.versions { versionKeys = append(versionKeys, k) } sort.Strings(versionKeys) for _, k := range versionKeys { v := r.versions[k] if v == nil { versions = append(versions, versionApplySnapshot{isNil: true, version: k}) continue } versions = append(versions, versionApplySnapshot{ version: v.Version, basePath: v.BasePath, middlewares: append([]gin.HandlerFunc(nil), v.Middlewares...), modules: append([]Module(nil), v.Modules...), }) } r.mu.Unlock() // 指标采集中间件首个装入,统计所有经注册中心注册的业务路由 if metricsMiddleware != nil { r.engine.Use(metricsMiddleware) } // 应用全局中间件 r.engine.Use(globalMiddlewares...) // 注册无版本模块 for _, module := range modules { if module == nil { warnf("router: nil module skipped during Apply") continue } module.Register(r.engine.Group("")) } // 注册版本化 API。P1 #13:按 version 键排序遍历,使跨版本注册顺序确定, // 避免 map 随机序导致重叠路径"谁先胜出"(配合 registerGETOnce)每次运行不一致。 for _, v := range versions { if v.isNil { warnf("router: nil version %q skipped during Apply", v.version) continue } group := r.engine.Group(v.basePath) if len(v.middlewares) > 0 { group.Use(v.middlewares...) } for _, module := range v.modules { if module == nil { warnf("router: nil module skipped in version %q", v.version) continue } module.Register(group) } } }) } // ===== 全局注册中心 ===== // globalRegistry 包级全局注册中心。用 atomic.Pointer 保护读写(H8a): // Init 写入、各全局 helper 读取,避免裸指针与请求 goroutine 的无锁竞争。 var globalRegistry atomic.Pointer[Registry] // Init 初始化全局注册中心。须在使用任何全局 helper(Use/RegisterModule/Apply…)之前调用。 func Init(engine *gin.Engine) *Registry { r := NewRegistry(engine) globalRegistry.Store(r) return r } // GetRegistry 获取全局注册中心,未初始化时返回 nil。 func GetRegistry() *Registry { return globalRegistry.Load() } // ensureRegistry 取全局注册中心,未初始化时以明确信息 panic(H8a)。 // 把晦涩的 nil 解引用 panic 转成可定位的初始化顺序错误。 func ensureRegistry() *Registry { r := globalRegistry.Load() if r == nil { panic("router: 全局注册中心未初始化,请先调用 router.Init(engine) 再使用全局 helper") } return r } // Use 注册全局中间件(全局方式) func Use(middlewares ...gin.HandlerFunc) *Registry { return ensureRegistry().Use(middlewares...) } // RegisterModule 注册模块(全局方式) func RegisterModule(module Module) *Registry { return ensureRegistry().RegisterModule(module) } // RegisterModuleFunc 注册函数式模块(全局方式) func RegisterModuleFunc(name string, fn func(r *gin.RouterGroup)) *Registry { return ensureRegistry().RegisterModuleFunc(name, fn) } // RegisterVersion 注册版本化 API(全局方式) func RegisterVersion(version *VersionedAPI) *Registry { return ensureRegistry().RegisterVersion(version) } // Apply 应用路由注册(全局方式) func Apply() { ensureRegistry().Apply() } // ===== 快捷构建函数 ===== // NewVersion 创建版本化 API func NewVersion(version, basePath string, middlewares ...gin.HandlerFunc) *VersionedAPI { return &VersionedAPI{ Version: version, BasePath: basePath, Middlewares: filterNilHandlers("NewVersion "+version, middlewares), Modules: make([]Module, 0), } } // AddModule 为版本添加模块 func (v *VersionedAPI) AddModule(module Module) *VersionedAPI { if v == nil { warnf("router: AddModule called on nil version") return v } if module == nil { warnf("router: nil module skipped in version %q", v.Version) return v } v.Modules = append(v.Modules, module) return v } // AddModuleFunc 为版本添加函数式模块 func (v *VersionedAPI) AddModuleFunc(name string, fn func(r *gin.RouterGroup)) *VersionedAPI { if fn == nil { warnf("router: module func %q skipped because function is nil", name) return v } return v.AddModule(&namedModule{name: name, fn: fn}) } // NewMiddlewareGroup 创建中间件分组 func NewMiddlewareGroup(name string, middlewares ...gin.HandlerFunc) *MiddlewareGroup { return &MiddlewareGroup{ Name: name, Middlewares: filterNilHandlers("NewMiddlewareGroup "+name, middlewares), } } // ===== 路由组辅助 ===== // Group 创建路由组(带中间件分组) func Group(engine *gin.Engine, path string, middlewares ...gin.HandlerFunc) *gin.RouterGroup { if engine == nil { warnf("router: Group(%q) skipped because engine is nil", path) return nil } return engine.Group(path, filterNilHandlers("Group "+path, middlewares)...) } // GroupWithMiddlewareGroup 使用中间件分组创建路由组。 // // H-B 修复:改走 ensureRegistry()(与 Use/RegisterModule/Apply 等所有全局 helper 一致), // 把"未初始化"从 nil 解引用 panic 转成可定位的明确 panic(H8a 目标)。原实现用 GetRegistry() // 可返回 nil,nil.GetMiddlewareGroup 即 panic,错误信息晦涩。 func (r *Registry) GroupWithMiddlewareGroup(engine *gin.Engine, path string, groupName string) *gin.RouterGroup { if engine == nil { warnf("router: GroupWithMiddlewareGroup(%q) skipped because engine is nil", path) return nil } return engine.Group(path, r.GetMiddlewareGroup(groupName)...) } // GroupWithMiddlewareGroup 使用中间件分组创建路由组(全局方式,代理到默认 Registry)。 func GroupWithMiddlewareGroup(engine *gin.Engine, path string, groupName string) *gin.RouterGroup { return ensureRegistry().GroupWithMiddlewareGroup(engine, path, groupName) } // RESTfulRoute RESTful 路由快捷注册 type RESTfulRoute struct { Group *gin.RouterGroup Path string } // NewRESTful 创建 RESTful 路由 func NewRESTful(group *gin.RouterGroup, path string) *RESTfulRoute { return &RESTfulRoute{Group: group, Path: path} } func (r *RESTfulRoute) prepareHandlers(method string, handlers []gin.HandlerFunc) []gin.HandlerFunc { if r == nil || r.Group == nil { warnf("router: RESTful %s skipped because route/group is nil", method) return nil } filtered := filterNilHandlers("RESTful "+method+" "+r.Path, handlers) if len(filtered) == 0 { warnf("router: RESTful %s %q skipped because handlers are empty", method, r.Path) return nil } return filtered } // GET 注册 GET 路由 func (r *RESTfulRoute) GET(handlers ...gin.HandlerFunc) { handlers = r.prepareHandlers(http.MethodGet, handlers) if handlers == nil { return } r.Group.GET(r.Path, handlers...) } // POST 注册 POST 路由 func (r *RESTfulRoute) POST(handlers ...gin.HandlerFunc) { handlers = r.prepareHandlers(http.MethodPost, handlers) if handlers == nil { return } r.Group.POST(r.Path, handlers...) } // PUT 注册 PUT 路由 func (r *RESTfulRoute) PUT(handlers ...gin.HandlerFunc) { handlers = r.prepareHandlers(http.MethodPut, handlers) if handlers == nil { return } r.Group.PUT(r.Path, handlers...) } // DELETE 注册 DELETE 路由 func (r *RESTfulRoute) DELETE(handlers ...gin.HandlerFunc) { handlers = r.prepareHandlers(http.MethodDelete, handlers) if handlers == nil { return } r.Group.DELETE(r.Path, handlers...) } // PATCH 注册 PATCH 路由 func (r *RESTfulRoute) PATCH(handlers ...gin.HandlerFunc) { handlers = r.prepareHandlers(http.MethodPatch, handlers) if handlers == nil { return } r.Group.PATCH(r.Path, handlers...) } // CRUD 注册标准 CRUD 路由 // GET /path - 列表 // GET /path/:id - 详情 // POST /path - 创建 // PUT /path/:id - 更新 // DELETE /path/:id - 删除 func (r *RESTfulRoute) CRUD(list, detail, create, update, delete gin.HandlerFunc) { if r == nil || r.Group == nil { warnf("router: RESTful CRUD skipped because route/group is nil") return } if list != nil { r.Group.GET(r.Path, list) } if detail != nil { r.Group.GET(r.Path+"/:id", detail) } if create != nil { r.Group.POST(r.Path, create) } if update != nil { r.Group.PUT(r.Path+"/:id", update) } if delete != nil { r.Group.DELETE(r.Path+"/:id", delete) } }