一次请求的流转
全局中间件链
所有进程(standalone 与四个 modular)注册顺序完全一致:
Recover → CORS → TraceID → RepeatableBody → AccessLog → XSS → I18n| # | 中间件 | 职责 | 关键非显而易见的行为 |
|---|---|---|---|
| 1 | Recover | 兜底 panic 与 handler 登记的 c.Error,统一渲染响应。 | 只在 !c.Writer.Written() 时渲染错误——handler 抢先写字节会让后续错误被静默吞掉。panic 一律 HTTP 200 + response.Fail;客户端断连只记日志不渲染。*errs.ServiceError 的 Detail 只进日志不回前端;Code==0 回落 500;非业务错误生成 8 位「错误编号」便于前端对账。 |
| 2 | CORS | 跨域:来源 / 方法 / 头校验,回显响应头。 | 必须注册在 Auth 之前。Vary 头在判断是否跨域之前就加。origin 用 * 通配。不合规直接 403。 |
| 3 | TraceID | 取或生成 32 位十六进制链路 id,写进 context 与响应头。 | header 名 X-Request-Id(与 cors.exposedHeaders 配套)。trustInbound=true 时沿用上游 id 并白名单校验。无条件回写(c.Next() 之前)。 |
| 4 | RepeatableBody | 把 JSON 请求体缓存进 gin.BodyBytesKey,允许后续重复读。 | 只缓存 contentTypes 命中的请求(默认 application/json;勿加 multipart/form-data,会把上传文件读进内存)。ContentLength > maxSize 直接拒。 |
| 5 | AccessLog | 请求开始 / 结束日志。 | 必须在 RepeatableBody 之后(要读缓存 body)。用 URL.Path 而非 RequestURI,避免把 ?password=xxx 写进日志。skipPaths 精确匹配跳过。 |
| 6 | XSS | 剔掉请求里的 HTML 标签,保留标签内文字。 | 必须在 RepeatableBody 之后、所有读参数的中间件 / handler 之前。按 method(GET/DELETE)与路径(excludeUrls)跳过。清洗 query / form / JSON body(json.Decoder.UseNumber 不能省,否则 19 位雪花 id 被 float64 抹尾数)。 |
| 7 | I18n | 解析语言写进 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.RAuditContext 为何要 ctx 接力
AuditContext 须排在 TokenInterceptor 之后:它取的登录态依赖后者解析出的 token。
之所以要这道中间件:GORM 回调只拿得到 *gorm.DB 取不到 *gin.Context,pkg/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.Init | MySQL 连接池。New 内强制调 repository.RegisterAuditCallbacks。 |
redis.Init | captcha / 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。
探针
| 部署 | 探针路径 | 响应 |
|---|---|---|
| standalone | GET /system/ping | {"module":"system","message":"pong"} |
| modular | GET /ping | {"module":"<模块名>","message":"pong"} |
均免鉴权、不碰 DB/Redis。