| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- // Package errors 三层错误模型(业务错误 sentinel + 错误码分段)。
- //
- // 本包定义全后端统一的业务错误类型 AppError 与一组"可复用"的 sentinel 错误。
- // 分层职责(架构规格 §3.2):
- // - Repository:把 gorm.ErrRecordNotFound 等 ORM 原错转成 sentinel,不向上泄漏 ORM 原错;
- // - Service:只返回 sentinel(可附带 Cause 保留底层错误供日志),禁止把 gorm 原错直接上抛;
- // - Handler:用 *response.Fail 写出;非 *AppError 走 ErrInternal + 结构化日志。
- //
- // 错误码分段(新接口统一;迁移期旧接口保留原 code):
- // - 1xxx 认证/权限;2xxx 参数/校验;3xxx 数据;4xxx 依赖;5xxx 系统。
- //
- // 典型使用(Service 层):
- //
- // func (s *Service) Get(ctx context.Context, id int64) (*model.User, error) {
- // u, err := s.repo.Take(ctx, id)
- // if err != nil {
- // // Repository 已把 gorm.ErrRecordNotFound 转成 ErrNotFound;这里再组合 Cause 便于日志排障
- // return nil, errors.WithCause(errors.ErrNotFound, err)
- // }
- // return u, nil
- // }
- package errors
- import "fmt"
- // AppError 业务错误(可预期、可被 Handler 映射为统一响应码)。
- //
- // 字段:
- // - Code int:分段错误码(见包注释的 1xxx~5xxx 分段表)。
- // - Msg string:面向用户的错误文案(脱敏,不含 SQL/堆栈/哈希)。
- // - Cause error:底层原因(可选);用于日志排障,绝不进入响应体。
- // - base *AppError(非导出):WithCause 时的"源 sentinel"回引用——
- // 使 WithCause(ErrNotFound, gormErr) 仍能被 errors.Is(err, ErrNotFound) 命中
- // (见 Unwrap 说明)。New(code,msg) 创建的自定义错误 base 为 nil。
- //
- // 使用约束:
- // - 业务域通常直接复用本包提供的 sentinel(ErrNotFound 等);
- // - 需要自定义 code/msg 时用 New(code, msg);
- // - 需要保留底层原因时用 WithCause(sentinel, cause)。
- type AppError struct {
- Code int
- Msg string
- Cause error
- // base 指向"被 WithCause 附挂底层原因的源 sentinel"(如 ErrNotFound)。
- // 仅内部用于 Unwrap 的解包链,导出接口/JSON 均不感知。
- base *AppError
- }
- // Error 实现 error 接口。
- //
- // 入参:无(值接收在方法集上,调用者无需关心)。
- // 出参:
- // - string:Cause 非 nil 时为 "Msg: Cause.Error()"(便于日志串联);Cause 为 nil 时仅返回 Msg。
- //
- // 说明:Cause 参与字符串拼接仅服务于日志/调试;前端展示的文案是 Handler 单独取 Msg,不受 Cause 影响。
- func (e *AppError) Error() string {
- if e.Cause != nil {
- return fmt.Sprintf("%s: %s", e.Msg, e.Cause.Error())
- }
- return e.Msg
- }
- // Unwrap 暴露底层原因链,支持 errors.Is / errors.As 逐层解包(Go 1.20+ 多错误解包)。
- //
- // 入参:无(指针接收在方法集上,调用者无需关心)。
- // 出参:
- // - []error:解包链,按序含 Cause 与 base(源 sentinel);去重且过滤 nil 后返回。
- // 空切片表示"无底层可解包"(既无 Cause 也无 base,如 New 创建的自定义错误)。
- //
- // 关键设计(使 errors.Is 命中 WithCause 的源 sentinel):
- // WithCause(ErrNotFound, gormErr) 返回的 *AppError 是 ErrNotFound 的**副本**(Code/Msg 拷贝),
- // 与 ErrNotFound 指针不同。若 Unwrap 只回 Cause(gormErr),errors.Is(err, ErrNotFound) 会
- // 走 err→gormErr→nil,永远找不到 ErrNotFound。故这里把 base(源 sentinel 指针)一并解出:
- //
- // err := WithCause(ErrNotFound, fmt.Errorf("sql: no rows"))
- // err.Unwrap() // [gormErr, ErrNotFound]
- // errors.Is(err, ErrNotFound) // true(命中 base)
- func (e *AppError) Unwrap() []error {
- // 去重:若 base 与 Cause 恰好是同一 *AppError(调用方把 sentinel 本身当 cause 传入),只保留一份。
- var chain []error
- if e.Cause != nil {
- chain = append(chain, e.Cause)
- }
- if e.base != nil && e.base != e.Cause {
- chain = append(chain, e.base)
- }
- return chain
- }
- // New 创建一个新的 AppError(自定义 code 与 msg)。
- //
- // 入参:
- // - code int:分段错误码(遵循 1xxx~5xxx 分段;传 200 视为误用,成功请用 response.OK)。
- // - msg string:面向用户的错误文案(应已脱敏)。
- //
- // 出参:
- // - *AppError:新建的业务错误;Cause 为 nil。
- //
- // 异常场景:
- // - code 传 0 或负数:不校验(调用方责任),但建议单测断言 code 落在分段区间内。
- //
- // 样例:
- //
- // return &errors.AppError via errors.New(3002, "该手机号已被绑定")
- func New(code int, msg string) *AppError {
- return &AppError{Code: code, Msg: msg}
- }
- // WithCause 为已有 sentinel 附加底层原因,返回新的 *AppError。
- //
- // 入参:
- // - base *AppError:目标业务错误(如 ErrNotFound);为 nil 时返回 cause 包装成的 ErrInternal。
- // - cause error:底层原因(ORM 原错/第三方错误);为 nil 时直接返回 base 的副本。
- //
- // 出参:
- // - *AppError:Code/Msg 继承自 base,Cause 置为 cause,base 回引用指向入参 base
- // (不修改入参 base,避免共享污染;回引用使 errors.Is 能命中源 sentinel,见 Unwrap)。
- //
- // 异常场景:
- // - base 为 nil:返回 ErrInternal 的副本并挂上 cause(base 回引用指向 ErrInternal,防御调用方传错)。
- //
- // 样例:
- //
- // err := errors.WithCause(errors.ErrConflict, dbErr)
- // stderrors.Is(err, errors.ErrConflict) // true
- func WithCause(base *AppError, cause error) *AppError {
- if base == nil {
- return &AppError{Code: ErrInternal.Code, Msg: ErrInternal.Msg, Cause: cause, base: ErrInternal}
- }
- return &AppError{Code: base.Code, Msg: base.Msg, Cause: cause, base: base}
- }
- // 分段错误码常量(与 R.Code 对齐;新接口一律引用这些常量,禁止散落 magic number)。
- const (
- CodeOK = 200
- CodeUnauth = 1001 // 未登录
- CodeTokenExpired = 1002 // 登录态失效
- CodeForbidden = 1003 // 无权限
- CodeScopeViolation = 1004 // 作用域越界(跨机构/跨校区)
- CodeBadParam = 2001 // 参数缺失
- CodeBadFormat = 2002 // 参数格式错误
- CodeRuleConflict = 2003 // 业务规则冲突
- CodeNotFound = 3001 // 记录不存在
- CodeConflict = 3002 // 数据冲突
- CodeUnique = 3003 // 唯一约束
- CodeAgentDown = 4001 // Agent 不可达
- CodePayCallback = 4002 // 支付回调失败
- CodeThirdTimeout = 4003 // 第三方超时
- CodeInternal = 5001 // 内部错误
- CodeDBError = 5002 // 数据库异常
- CodeThrottled = 5003 // 限流
- )
- // 预置 sentinel 错误(业务域直接复用,无需各自 new)。
- //
- // 每个 sentinel 的语义:
- // - ErrUnauth:请求未携带有效登录态(缺 token / token 无效)。
- // - ErrTokenExpired:登录态已过期(access 超 30min 且 refresh 也失效)。
- // - ErrForbidden:已登录但无该操作权限(角色/权限点不匹配)。
- // - ErrUnscoped:作用域越界——试图访问非本机构/非本校区的资源(多租户防线)。
- // - ErrBadParam:入参缺失或格式非法(validator 校验失败的统一出口)。
- // - ErrNotFound:目标记录不存在(Repository 把 gorm.ErrRecordNotFound 转成本 sentinel)。
- // - ErrConflict:数据冲突(如唯一键撞车、状态机非法迁移)。
- // - ErrAgentDown:依赖的 Python Agent 不可达(fire-and-forget 语义下仅记日志,主链路不阻断时用本码告警)。
- // - ErrInternal:未分类内部错误(兜底,Handler 会把任何非 *AppError 降级到本码)。
- // - ErrDBError:数据库异常(连接/事务/SQL 层,非"记录不存在")。
- var (
- ErrUnauth = &AppError{Code: CodeUnauth, Msg: "未登录"}
- ErrTokenExpired = &AppError{Code: CodeTokenExpired, Msg: "登录态失效"}
- ErrForbidden = &AppError{Code: CodeForbidden, Msg: "无权限"}
- ErrUnscoped = &AppError{Code: CodeScopeViolation, Msg: "作用域越界"}
- ErrBadParam = &AppError{Code: CodeBadParam, Msg: "参数错误"}
- ErrNotFound = &AppError{Code: CodeNotFound, Msg: "记录不存在"}
- ErrConflict = &AppError{Code: CodeConflict, Msg: "数据冲突"}
- ErrAgentDown = &AppError{Code: CodeAgentDown, Msg: "AI 服务暂不可用"}
- ErrInternal = &AppError{Code: CodeInternal, Msg: "系统繁忙"}
- ErrDBError = &AppError{Code: CodeDBError, Msg: "数据服务异常"}
- )
|