Skip to content

一次请求的流转

全局中间件链

所有进程(standalone 与四个 modular)注册顺序完全一致:

Recover → CORS → TraceID → RepeatableBody → AccessLog → XSS → I18n
#中间件职责关键非显而易见的行为
1Recover兜底 panic 与 handler 登记的 c.Error,统一渲染响应。只在 !c.Writer.Written() 时渲染错误——handler 抢先写字节会让后续错误被静默吞掉。panic 一律 HTTP 200 + response.Fail;客户端断连只记日志不渲染。*errs.ServiceError 的 Detail 只进日志不回前端;Code==0 回落 500;非业务错误生成 8 位「错误编号」便于前端对账。
2CORS跨域:来源 / 方法 / 头校验,回显响应头。必须注册在 Auth 之前。Vary 头在判断是否跨域之前就加。origin 用 * 通配。不合规直接 403。
3TraceID取或生成 32 位十六进制链路 id,写进 context 与响应头。header 名 X-Request-Id(与 cors.exposedHeaders 配套)。trustInbound=true 时沿用上游 id 并白名单校验。无条件回写(c.Next() 之前)。
4RepeatableBody把 JSON 请求体缓存进 gin.BodyBytesKey,允许后续重复读。只缓存 contentTypes 命中的请求(默认 application/json勿加 multipart/form-data,会把上传文件读进内存)。ContentLength > maxSize 直接拒。
5AccessLog请求开始 / 结束日志。必须在 RepeatableBody 之后(要读缓存 body)。用 URL.Path 而非 RequestURI,避免把 ?password=xxx 写进日志。skipPaths 精确匹配跳过。
6XSS剔掉请求里的 HTML 标签,保留标签内文字。必须在 RepeatableBody 之后、所有读参数的中间件 / handler 之前。按 method(GET/DELETE)与路径(excludeUrls)跳过。清洗 query / form / JSON body(json.Decoder.UseNumber 不能省,否则 19 位雪花 id 被 float64 抹尾数)。
7I18n解析语言写进 context 与响应头。必须在 Auth 之前。header 名 content-language。解析失败用默认 zh-cn,不拒绝请求。

路由级中间件

全局链之后进入路由组级中间件:

请求
 → [全局中间件链 1-7]
 → plugin.TokenInterceptor()        // sa-token-go 解析 token,写登录态进 gin.Context
 → loginhelper.AuditContext()       // 取登录用户,把 userId/deptId 挂进 request.Context
 → 路由级中间件(按需组合,顺序固定):
     鉴权 → 解密 → 日志 → 防重 → handler
 → handler → service → repository → response.R

AuditContext 为何要 ctx 接力

AuditContext 须排在 TokenInterceptor 之后:它取的登录态依赖后者解析出的 token。

之所以要这道中间件:GORM 回调只拿得到 *gorm.DB 取不到 *gin.Contextpkg/repository 也不能反向 import loginhelper (成环),故登录态只能经 context.Context 接力(Go 无全局 thread-local 等价物)。未登录时不写,回调按 -1 兜底。

路由级中间件顺序:鉴权 → 解密 → 日志 → 防重 → handler

顺序不是风格问题,改了行为就变

  • 鉴权最前:未授权请求不该白占一个防重锁。
  • 日志在防重之前:被防重挡掉的请求 handler 没执行,但仍记一条失败日志。
  • repeatsubmit 须在 encrypt.ApiEncrypt() 之后:指纹要用解密后的明文,否则密文每次随机密钥、同样入参算出不同指纹,防重直接失效。

推送端点的特殊链

internal/resource/router.go:RegisterPushRoutes

push.NormalizeQueryToken() → plugin.TokenInterceptor() → sagin.CheckLogin() → push.Handler()

NormalizeQueryToken 须排在 TokenInterceptor 之前:EventSource / WebSocket 不能自定义请求头,token 只能走 query( ?Authorization=Bearer xxx),而 sa-token-go 的 query 分支不剥 Bearer 前缀,不规范化会一律 401。

进程入口与初始化顺序

所有 main.go 调用顺序一致(monitor 略有调整)。以 cmd/standalone/main.go 为基准:

config.Load → jsonx.Init → database.Init → redis.Init
→ satoken.Init → encrypt.Init → snowflake.Init → captcha.Init
→ ratelimiter.Init → repeatsubmit.Init → mail.Init → sms.Init
→ social.Init → oplog.Init(recorder) → push.Init → (standalone/resource) OSS 配置预热
调用依赖 / 为什么
jsonx.Init必须在首个 c.JSON / 参数绑定之前接管 gin codec,否则 19 位雪花 id 被 float64 抹尾数。
database.InitMySQL 连接池。New 内强制调 repository.RegisterAuditCallbacks
redis.Initcaptcha / ratelimiter / repeatsubmit / social / push 都依赖它。
snowflake.Init主键发号器,各业务表主键无 auto_increment,插入前必须就绪。
oplog.Init(recorder)操作日志落库实现反向注册pkg/oplog(pkg 不依赖 internal/service,避免成环);依赖 database + snowflake。

各模块差异:

  • standalone:全量初始化 + OSS 配置预热。
  • auth:无 encrypt.Init、无 push.Init;有 captcha/ratelimiter/social。
  • system:无 captcha/ratelimiter/social/mail/sms;有 encrypt/repeatsubmit/oplog/push。
  • resource:无 social;有 mail/sms/ratelimiter/repeatsubmit/oplog/push + OSS 预热。
  • monitor:redis 先于 database;无 encrypt/captcha/ratelimiter/social/mail/sms/push;有 database/snowflake/oplog/repeatsubmit。

探针

部署探针路径响应
standaloneGET /system/ping{"module":"system","message":"pong"}
modularGET /ping{"module":"<模块名>","message":"pong"}

均免鉴权、不碰 DB/Redis。

基于 MIT 协议开源