package xerr import ( "errors" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) const errorDomain = "hyapp" // GRPCCode 把稳定业务 reason 映射为 gRPC 标准 code。 // gateway 后续只需要读取 gRPC code 和 ErrorInfo.reason 即可生成 HTTP envelope。 func GRPCCode(code Code) codes.Code { switch code { case InvalidArgument, DisplayUserIDInvalid: return codes.InvalidArgument case NotFound, DisplayUserIDNotFound, CountryNotFound, RegionNotFound: return codes.NotFound case Conflict, RegionCountryConflict, RegionDisabled: return codes.FailedPrecondition case Unauthorized, AuthFailed, SessionExpired, SessionRevoked: return codes.Unauthenticated case PermissionDenied, ProfileRequired, UserDisabled: return codes.PermissionDenied case Unavailable: return codes.Unavailable case PasswordAlreadySet, DisplayUserIDExists, DuplicateBillingCommand, EventAlreadyConsumed: return codes.AlreadyExists case DisplayUserIDCooldown, DisplayUserIDPrettyNotAvailable, DisplayUserIDPrettyActive, DisplayUserIDLeaseExpired, DisplayUserIDPaymentRequired, CountryChangeCooldown, InsufficientBalance, RuleNotActive, RewardPending: return codes.FailedPrecondition case DisplayUserIDAllocateFailed: return codes.Internal case LedgerConflict: return codes.Aborted default: return codes.Internal } } // ToGRPCError 只在 transport/grpc 层调用。 // 如果传入的错误已经是 gRPC status,会原样返回,避免重复包裹健康检查等基础错误。 func ToGRPCError(err error) error { if err == nil { return nil } if _, ok := status.FromError(err); ok { return err } domainErr, ok := As(err) if !ok { domainErr = &Error{Code: Internal, Message: "internal error"} } st := status.New(GRPCCode(domainErr.Code), domainErr.Message) detail := &errdetails.ErrorInfo{ Reason: string(domainErr.Code), Domain: errorDomain, } withDetails, detailErr := st.WithDetails(detail) if detailErr != nil { return st.Err() } return withDetails.Err() } // ReasonFromGRPC 供 gateway 或测试从 gRPC error detail 中恢复稳定 reason。 func ReasonFromGRPC(err error) Code { if err == nil { return "" } st, ok := status.FromError(err) if !ok { return Internal } for _, detail := range st.Details() { info, ok := detail.(*errdetails.ErrorInfo) if ok && info.GetReason() != "" { return Code(info.GetReason()) } } return codeFromGRPC(st.Code()) } // codeFromGRPC 是没有 ErrorInfo detail 时的降级映射。 func codeFromGRPC(code codes.Code) Code { switch code { case codes.InvalidArgument: return InvalidArgument case codes.NotFound: return NotFound case codes.Unauthenticated: return Unauthorized case codes.PermissionDenied: return PermissionDenied case codes.Unavailable: return Unavailable case codes.AlreadyExists, codes.FailedPrecondition, codes.Aborted: return Conflict default: return Internal } } // IsCode 帮助测试或 transport 分支判断错误 reason。 func IsCode(err error, code Code) bool { if err == nil { return false } if typed, ok := As(err); ok { return typed.Code == code } if _, ok := status.FromError(err); ok { return ReasonFromGRPC(err) == code } return errors.Is(err, &Error{Code: code}) }