errors.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  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. //
  31. // 使用约束:
  32. // - 业务域通常直接复用本包提供的 sentinel(ErrNotFound 等);
  33. // - 需要自定义 code/msg 时用 New(code, msg);
  34. // - 需要保留底层原因时用 WithCause(sentinel, cause)。
  35. type AppError struct {
  36. Code int
  37. Msg string
  38. Cause error
  39. }
  40. // Error 实现 error 接口。
  41. //
  42. // 入参:无(值接收在方法集上,调用者无需关心)。
  43. // 出参:
  44. // - string:Cause 非 nil 时为 "Msg: Cause.Error()"(便于日志串联);Cause 为 nil 时仅返回 Msg。
  45. //
  46. // 说明:Cause 参与字符串拼接仅服务于日志/调试;前端展示的文案是 Handler 单独取 Msg,不受 Cause 影响。
  47. func (e *AppError) Error() string {
  48. if e.Cause != nil {
  49. return fmt.Sprintf("%s: %s", e.Msg, e.Cause.Error())
  50. }
  51. return e.Msg
  52. }
  53. // Unwrap 暴露底层原因,支持 errors.Is / errors.As 逐层解包。
  54. //
  55. // 入参:无。
  56. // 出参:
  57. // - error:Cause(可能为 nil)。
  58. //
  59. // 样例:
  60. //
  61. // err := WithCause(ErrNotFound, fmt.Errorf("sql: no rows in result set"))
  62. // errors.Is(err, ErrNotFound) // true
  63. func (e *AppError) Unwrap() error {
  64. return e.Cause
  65. }
  66. // New 创建一个新的 AppError(自定义 code 与 msg)。
  67. //
  68. // 入参:
  69. // - code int:分段错误码(遵循 1xxx~5xxx 分段;传 200 视为误用,成功请用 response.OK)。
  70. // - msg string:面向用户的错误文案(应已脱敏)。
  71. //
  72. // 出参:
  73. // - *AppError:新建的业务错误;Cause 为 nil。
  74. //
  75. // 异常场景:
  76. // - code 传 0 或负数:不校验(调用方责任),但建议单测断言 code 落在分段区间内。
  77. //
  78. // 样例:
  79. //
  80. // return &errors.AppError via errors.New(3002, "该手机号已被绑定")
  81. func New(code int, msg string) *AppError {
  82. return &AppError{Code: code, Msg: msg}
  83. }
  84. // WithCause 为已有 sentinel 附加底层原因,返回新的 *AppError。
  85. //
  86. // 入参:
  87. // - base *AppError:目标业务错误(如 ErrNotFound);为 nil 时返回 cause 包装成的 ErrInternal。
  88. // - cause error:底层原因(ORM 原错/第三方错误);为 nil 时直接返回 base 的副本。
  89. //
  90. // 出参:
  91. // - *AppError:Code/Msg 继承自 base,Cause 置为 cause(不修改入参 base,避免共享污染)。
  92. //
  93. // 异常场景:
  94. // - base 为 nil:返回 ErrInternal 的副本并挂上 cause(防御调用方传错)。
  95. //
  96. // 样例:
  97. //
  98. // return errors.WithCause(errors.ErrConflict, dbErr)
  99. func WithCause(base *AppError, cause error) *AppError {
  100. if base == nil {
  101. return &AppError{Code: ErrInternal.Code, Msg: ErrInternal.Msg, Cause: cause}
  102. }
  103. return &AppError{Code: base.Code, Msg: base.Msg, Cause: cause}
  104. }
  105. // 分段错误码常量(与 R.Code 对齐;新接口一律引用这些常量,禁止散落 magic number)。
  106. const (
  107. CodeOK = 200
  108. CodeUnauth = 1001 // 未登录
  109. CodeTokenExpired = 1002 // 登录态失效
  110. CodeForbidden = 1003 // 无权限
  111. CodeScopeViolation = 1004 // 作用域越界(跨机构/跨校区)
  112. CodeBadParam = 2001 // 参数缺失
  113. CodeBadFormat = 2002 // 参数格式错误
  114. CodeRuleConflict = 2003 // 业务规则冲突
  115. CodeNotFound = 3001 // 记录不存在
  116. CodeConflict = 3002 // 数据冲突
  117. CodeUnique = 3003 // 唯一约束
  118. CodeAgentDown = 4001 // Agent 不可达
  119. CodePayCallback = 4002 // 支付回调失败
  120. CodeThirdTimeout = 4003 // 第三方超时
  121. CodeInternal = 5001 // 内部错误
  122. CodeDBError = 5002 // 数据库异常
  123. CodeThrottled = 5003 // 限流
  124. )
  125. // 预置 sentinel 错误(业务域直接复用,无需各自 new)。
  126. //
  127. // 每个 sentinel 的语义:
  128. // - ErrUnauth:请求未携带有效登录态(缺 token / token 无效)。
  129. // - ErrTokenExpired:登录态已过期(access 超 30min 且 refresh 也失效)。
  130. // - ErrForbidden:已登录但无该操作权限(角色/权限点不匹配)。
  131. // - ErrUnscoped:作用域越界——试图访问非本机构/非本校区的资源(多租户防线)。
  132. // - ErrBadParam:入参缺失或格式非法(validator 校验失败的统一出口)。
  133. // - ErrNotFound:目标记录不存在(Repository 把 gorm.ErrRecordNotFound 转成本 sentinel)。
  134. // - ErrConflict:数据冲突(如唯一键撞车、状态机非法迁移)。
  135. // - ErrAgentDown:依赖的 Python Agent 不可达(fire-and-forget 语义下仅记日志,主链路不阻断时用本码告警)。
  136. // - ErrInternal:未分类内部错误(兜底,Handler 会把任何非 *AppError 降级到本码)。
  137. // - ErrDBError:数据库异常(连接/事务/SQL 层,非"记录不存在")。
  138. var (
  139. ErrUnauth = &AppError{Code: CodeUnauth, Msg: "未登录"}
  140. ErrTokenExpired = &AppError{Code: CodeTokenExpired, Msg: "登录态失效"}
  141. ErrForbidden = &AppError{Code: CodeForbidden, Msg: "无权限"}
  142. ErrUnscoped = &AppError{Code: CodeScopeViolation, Msg: "作用域越界"}
  143. ErrBadParam = &AppError{Code: CodeBadParam, Msg: "参数错误"}
  144. ErrNotFound = &AppError{Code: CodeNotFound, Msg: "记录不存在"}
  145. ErrConflict = &AppError{Code: CodeConflict, Msg: "数据冲突"}
  146. ErrAgentDown = &AppError{Code: CodeAgentDown, Msg: "AI 服务暂不可用"}
  147. ErrInternal = &AppError{Code: CodeInternal, Msg: "系统繁忙"}
  148. ErrDBError = &AppError{Code: CodeDBError, Msg: "数据服务异常"}
  149. )