errors.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. // Package errors 三层错误模型(业务错误 sentinel + 错误码分段)。
  2. //
  3. // 本包定义全后端统一的业务错误类型 AppError 与一组"可复用"的 sentinel 错误。
  4. // 分层职责(架构规格 §3.2):
  5. // - Repository:把 gorm.ErrRecordNotFound 等 ORM 原错转成 sentinel,不向上泄漏 ORM 原错;
  6. // - Service:只返回 sentinel(可附带 Cause 保留底层错误供日志),禁止把 gorm 原错直接上抛;
  7. // - Handler:用 *response.Fail 写出;非 *AppError 走 ErrInternal + 结构化日志。
  8. //
  9. // 错误码分段(新接口统一;迁移期旧接口保留原 code):
  10. // - 1xxx 认证/权限;2xxx 参数/校验;3xxx 数据;4xxx 依赖;5xxx 系统。
  11. //
  12. // 典型使用(Service 层):
  13. //
  14. // func (s *Service) Get(ctx context.Context, id int64) (*model.User, error) {
  15. // u, err := s.repo.Take(ctx, id)
  16. // if err != nil {
  17. // // Repository 已把 gorm.ErrRecordNotFound 转成 ErrNotFound;这里再组合 Cause 便于日志排障
  18. // return nil, errors.WithCause(errors.ErrNotFound, err)
  19. // }
  20. // return u, nil
  21. // }
  22. package errors
  23. import "fmt"
  24. // AppError 业务错误(可预期、可被 Handler 映射为统一响应码)。
  25. //
  26. // 字段:
  27. // - Code int:分段错误码(见包注释的 1xxx~5xxx 分段表)。
  28. // - Msg string:面向用户的错误文案(脱敏,不含 SQL/堆栈/哈希)。
  29. // - Cause error:底层原因(可选);用于日志排障,绝不进入响应体。
  30. // - base *AppError(非导出):WithCause 时的"源 sentinel"回引用——
  31. // 使 WithCause(ErrNotFound, gormErr) 仍能被 errors.Is(err, ErrNotFound) 命中
  32. // (见 Unwrap 说明)。New(code,msg) 创建的自定义错误 base 为 nil。
  33. //
  34. // 使用约束:
  35. // - 业务域通常直接复用本包提供的 sentinel(ErrNotFound 等);
  36. // - 需要自定义 code/msg 时用 New(code, msg);
  37. // - 需要保留底层原因时用 WithCause(sentinel, cause)。
  38. type AppError struct {
  39. Code int
  40. Msg string
  41. Cause error
  42. // base 指向"被 WithCause 附挂底层原因的源 sentinel"(如 ErrNotFound)。
  43. // 仅内部用于 Unwrap 的解包链,导出接口/JSON 均不感知。
  44. base *AppError
  45. }
  46. // Error 实现 error 接口。
  47. //
  48. // 入参:无(值接收在方法集上,调用者无需关心)。
  49. // 出参:
  50. // - string:Cause 非 nil 时为 "Msg: Cause.Error()"(便于日志串联);Cause 为 nil 时仅返回 Msg。
  51. //
  52. // 说明:Cause 参与字符串拼接仅服务于日志/调试;前端展示的文案是 Handler 单独取 Msg,不受 Cause 影响。
  53. func (e *AppError) Error() string {
  54. if e.Cause != nil {
  55. return fmt.Sprintf("%s: %s", e.Msg, e.Cause.Error())
  56. }
  57. return e.Msg
  58. }
  59. // Unwrap 暴露底层原因链,支持 errors.Is / errors.As 逐层解包(Go 1.20+ 多错误解包)。
  60. //
  61. // 入参:无(指针接收在方法集上,调用者无需关心)。
  62. // 出参:
  63. // - []error:解包链,按序含 Cause 与 base(源 sentinel);去重且过滤 nil 后返回。
  64. // 空切片表示"无底层可解包"(既无 Cause 也无 base,如 New 创建的自定义错误)。
  65. //
  66. // 关键设计(使 errors.Is 命中 WithCause 的源 sentinel):
  67. // WithCause(ErrNotFound, gormErr) 返回的 *AppError 是 ErrNotFound 的**副本**(Code/Msg 拷贝),
  68. // 与 ErrNotFound 指针不同。若 Unwrap 只回 Cause(gormErr),errors.Is(err, ErrNotFound) 会
  69. // 走 err→gormErr→nil,永远找不到 ErrNotFound。故这里把 base(源 sentinel 指针)一并解出:
  70. //
  71. // err := WithCause(ErrNotFound, fmt.Errorf("sql: no rows"))
  72. // err.Unwrap() // [gormErr, ErrNotFound]
  73. // errors.Is(err, ErrNotFound) // true(命中 base)
  74. func (e *AppError) Unwrap() []error {
  75. // 去重:若 base 与 Cause 恰好是同一 *AppError(调用方把 sentinel 本身当 cause 传入),只保留一份。
  76. var chain []error
  77. if e.Cause != nil {
  78. chain = append(chain, e.Cause)
  79. }
  80. if e.base != nil && e.base != e.Cause {
  81. chain = append(chain, e.base)
  82. }
  83. return chain
  84. }
  85. // New 创建一个新的 AppError(自定义 code 与 msg)。
  86. //
  87. // 入参:
  88. // - code int:分段错误码(遵循 1xxx~5xxx 分段;传 200 视为误用,成功请用 response.OK)。
  89. // - msg string:面向用户的错误文案(应已脱敏)。
  90. //
  91. // 出参:
  92. // - *AppError:新建的业务错误;Cause 为 nil。
  93. //
  94. // 异常场景:
  95. // - code 传 0 或负数:不校验(调用方责任),但建议单测断言 code 落在分段区间内。
  96. //
  97. // 样例:
  98. //
  99. // return &errors.AppError via errors.New(3002, "该手机号已被绑定")
  100. func New(code int, msg string) *AppError {
  101. return &AppError{Code: code, Msg: msg}
  102. }
  103. // WithCause 为已有 sentinel 附加底层原因,返回新的 *AppError。
  104. //
  105. // 入参:
  106. // - base *AppError:目标业务错误(如 ErrNotFound);为 nil 时返回 cause 包装成的 ErrInternal。
  107. // - cause error:底层原因(ORM 原错/第三方错误);为 nil 时直接返回 base 的副本。
  108. //
  109. // 出参:
  110. // - *AppError:Code/Msg 继承自 base,Cause 置为 cause,base 回引用指向入参 base
  111. // (不修改入参 base,避免共享污染;回引用使 errors.Is 能命中源 sentinel,见 Unwrap)。
  112. //
  113. // 异常场景:
  114. // - base 为 nil:返回 ErrInternal 的副本并挂上 cause(base 回引用指向 ErrInternal,防御调用方传错)。
  115. //
  116. // 样例:
  117. //
  118. // err := errors.WithCause(errors.ErrConflict, dbErr)
  119. // stderrors.Is(err, errors.ErrConflict) // true
  120. func WithCause(base *AppError, cause error) *AppError {
  121. if base == nil {
  122. return &AppError{Code: ErrInternal.Code, Msg: ErrInternal.Msg, Cause: cause, base: ErrInternal}
  123. }
  124. return &AppError{Code: base.Code, Msg: base.Msg, Cause: cause, base: base}
  125. }
  126. // 分段错误码常量(与 R.Code 对齐;新接口一律引用这些常量,禁止散落 magic number)。
  127. const (
  128. CodeOK = 200
  129. CodeUnauth = 1001 // 未登录
  130. CodeTokenExpired = 1002 // 登录态失效
  131. CodeForbidden = 1003 // 无权限
  132. CodeScopeViolation = 1004 // 作用域越界(跨机构/跨校区)
  133. CodeBadParam = 2001 // 参数缺失
  134. CodeBadFormat = 2002 // 参数格式错误
  135. CodeRuleConflict = 2003 // 业务规则冲突
  136. CodeNotFound = 3001 // 记录不存在
  137. CodeConflict = 3002 // 数据冲突
  138. CodeUnique = 3003 // 唯一约束
  139. CodeAgentDown = 4001 // Agent 不可达
  140. CodePayCallback = 4002 // 支付回调失败
  141. CodeThirdTimeout = 4003 // 第三方超时
  142. CodeInternal = 5001 // 内部错误
  143. CodeDBError = 5002 // 数据库异常
  144. CodeThrottled = 5003 // 限流
  145. )
  146. // 预置 sentinel 错误(业务域直接复用,无需各自 new)。
  147. //
  148. // 每个 sentinel 的语义:
  149. // - ErrUnauth:请求未携带有效登录态(缺 token / token 无效)。
  150. // - ErrTokenExpired:登录态已过期(access 超 30min 且 refresh 也失效)。
  151. // - ErrForbidden:已登录但无该操作权限(角色/权限点不匹配)。
  152. // - ErrUnscoped:作用域越界——试图访问非本机构/非本校区的资源(多租户防线)。
  153. // - ErrBadParam:入参缺失或格式非法(validator 校验失败的统一出口)。
  154. // - ErrNotFound:目标记录不存在(Repository 把 gorm.ErrRecordNotFound 转成本 sentinel)。
  155. // - ErrConflict:数据冲突(如唯一键撞车、状态机非法迁移)。
  156. // - ErrAgentDown:依赖的 Python Agent 不可达(fire-and-forget 语义下仅记日志,主链路不阻断时用本码告警)。
  157. // - ErrInternal:未分类内部错误(兜底,Handler 会把任何非 *AppError 降级到本码)。
  158. // - ErrDBError:数据库异常(连接/事务/SQL 层,非"记录不存在")。
  159. var (
  160. ErrUnauth = &AppError{Code: CodeUnauth, Msg: "未登录"}
  161. ErrTokenExpired = &AppError{Code: CodeTokenExpired, Msg: "登录态失效"}
  162. ErrForbidden = &AppError{Code: CodeForbidden, Msg: "无权限"}
  163. ErrUnscoped = &AppError{Code: CodeScopeViolation, Msg: "作用域越界"}
  164. ErrBadParam = &AppError{Code: CodeBadParam, Msg: "参数错误"}
  165. ErrNotFound = &AppError{Code: CodeNotFound, Msg: "记录不存在"}
  166. ErrConflict = &AppError{Code: CodeConflict, Msg: "数据冲突"}
  167. ErrAgentDown = &AppError{Code: CodeAgentDown, Msg: "AI 服务暂不可用"}
  168. ErrInternal = &AppError{Code: CodeInternal, Msg: "系统繁忙"}
  169. ErrDBError = &AppError{Code: CodeDBError, Msg: "数据服务异常"}
  170. )