package errors import ( "errors" "fmt" "strings" "time" "google.golang.org/grpc/codes" pb "github.com/topfans/backend/pkg/proto/common" ) // 业务错误类型 var ( ErrUserNotFound = errors.New("user not found") ErrUserAlreadyExists = errors.New("user already exists") ErrInvalidPassword = errors.New("invalid password") ErrInvalidOldPassword = errors.New("old password is incorrect") ErrSameAsOldPassword = errors.New("new password is same as old") ErrInvalidVerifyToken = errors.New("invalid verify token") ErrInvalidToken = errors.New("invalid token") ErrTokenExpired = errors.New("token expired") ErrTokenMismatch = errors.New("token mismatch") ErrUserInactive = errors.New("user is inactive") ErrFanProfileNotFound = errors.New("fan profile not found") ErrFanProfileAlreadyExists = errors.New("fan profile already exists") ErrNicknameAlreadyExists = errors.New("该昵称已被注册") ErrInvalidNickname = errors.New("invalid nickname format") ErrStarNotFound = errors.New("star not found") ErrInvalidMobile = errors.New("invalid mobile format") ErrPasswordTooShort = errors.New("password too short") ErrInvalidStarID = errors.New("invalid star_id") ErrInvalidUserID = errors.New("invalid user_id") ErrMaxIdentitiesReached = errors.New("maximum number of identities reached") ErrInternalServer = errors.New("internal server error") // 社交服务相关错误 ErrCannotAddSelf = errors.New("不能添加自己为好友") ErrCannotSearchSelf = errors.New("不能查找自己") ErrNotFanOfStar = errors.New("对方不是该明星的粉丝") ErrAlreadyFriends = errors.New("你们已经是好友了") ErrRequestAlreadyPending = errors.New("已有待处理的好友请求") ErrRequestInCooldown = errors.New("请求已被拒绝,请稍后再试") ErrInvalidFriendUserID = errors.New("friend_user_id不能为空") ErrFriendRequestNotFound = errors.New("好友请求不存在") ErrCannotProcessOwnRequest = errors.New("不能处理自己发送的请求") ErrRequestAlreadyProcessed = errors.New("该请求已被处理") ErrRequestExpired = errors.New("好友请求已过期") ErrInvalidAction = errors.New("无效的操作") ErrNotFriends = errors.New("你们不是好友") ErrMaxFriendsReached = errors.New("已达到最大好友数量限制") // 资产服务相关错误 ErrAssetNotFound = errors.New("资产不存在") ErrMintOrderNotFound = errors.New("铸造订单不存在") ErrInsufficientCrystal = errors.New("水晶余额不足") ErrInsufficientMintTimes = errors.New("铸造次数不足") ErrAssetAccessDenied = errors.New("无权访问该资产") ErrMintOrderAccessDenied = errors.New("无权访问该订单") ErrInvalidAssetStatus = errors.New("资产状态无效") ErrInvalidMintOrderStatus = errors.New("订单状态无效") // 账号状态相关错误 ErrAccountFrozen = errors.New("账号已被冻结") ErrAccountBanned = errors.New("账号已被封禁") // 活动服务相关错误 ErrActivityNotFound = errors.New("活动不存在") ErrActivityItemNotFound = errors.New("活动道具不存在") // 星册服务相关错误 ErrCollectionAssetNotFound = errors.New("典藏藏品不存在") ErrActivityAssetNotFound = errors.New("活动藏品不存在") ErrAssetRegistryNotFound = errors.New("资产索引不存在") ErrInvalidAssetType = errors.New("无效的资产类型") ) // ToGRPCCode 将错误转换为 google.rpc.Code 数字 // 用 errors.Is 而非 == 比较,以便识别 fmt.Errorf("%w: ...", ErrXxx, ...) 包装后的错误 // // 映射表: // codes.OK (0) - 成功 // codes.NotFound (5) - ErrUserNotFound / ErrFanProfileNotFound / ErrStarNotFound / ErrFriendRequestNotFound / ErrAssetNotFound / ErrMintOrderNotFound / ErrActivityNotFound / ErrActivityItemNotFound / ErrCollectionAssetNotFound / ErrActivityAssetNotFound / ErrAssetRegistryNotFound // codes.InvalidArgument (3) - 参数/数据校验错 // codes.Unauthenticated (16) - ErrInvalidPassword / ErrInvalidToken / ErrTokenExpired / ErrTokenMismatch // codes.PermissionDenied (7) - ErrAccountFrozen / ErrAccountBanned / ErrUserInactive / ErrAssetAccessDenied / ErrMintOrderAccessDenied // codes.ResourceExhausted (8) - ErrRequestInCooldown // codes.Internal (13) - 未识别的 error func ToGRPCCode(err error) codes.Code { if err == nil { return codes.OK } switch { case errors.Is(err, ErrUserNotFound), errors.Is(err, ErrFanProfileNotFound), errors.Is(err, ErrStarNotFound): return codes.NotFound case errors.Is(err, ErrUserAlreadyExists), errors.Is(err, ErrFanProfileAlreadyExists), errors.Is(err, ErrNicknameAlreadyExists): return codes.InvalidArgument case errors.Is(err, ErrInvalidPassword), errors.Is(err, ErrInvalidToken), errors.Is(err, ErrTokenExpired), errors.Is(err, ErrTokenMismatch): return codes.Unauthenticated case errors.Is(err, ErrAccountFrozen), errors.Is(err, ErrAccountBanned), errors.Is(err, ErrUserInactive): return codes.PermissionDenied case errors.Is(err, ErrInvalidMobile), errors.Is(err, ErrPasswordTooShort), errors.Is(err, ErrInvalidOldPassword), errors.Is(err, ErrSameAsOldPassword), errors.Is(err, ErrInvalidVerifyToken), errors.Is(err, ErrInvalidStarID), errors.Is(err, ErrInvalidUserID), errors.Is(err, ErrMaxIdentitiesReached), errors.Is(err, ErrInvalidNickname): return codes.InvalidArgument case errors.Is(err, ErrCannotAddSelf), errors.Is(err, ErrCannotSearchSelf), errors.Is(err, ErrNotFanOfStar), errors.Is(err, ErrAlreadyFriends), errors.Is(err, ErrRequestAlreadyPending), errors.Is(err, ErrInvalidFriendUserID), errors.Is(err, ErrCannotProcessOwnRequest), errors.Is(err, ErrRequestAlreadyProcessed), errors.Is(err, ErrRequestExpired), errors.Is(err, ErrInvalidAction), errors.Is(err, ErrNotFriends): return codes.InvalidArgument case errors.Is(err, ErrRequestInCooldown): return codes.ResourceExhausted case errors.Is(err, ErrFriendRequestNotFound), errors.Is(err, ErrAssetNotFound), errors.Is(err, ErrMintOrderNotFound): return codes.NotFound case errors.Is(err, ErrInsufficientCrystal), errors.Is(err, ErrInsufficientMintTimes), errors.Is(err, ErrInvalidAssetStatus), errors.Is(err, ErrInvalidMintOrderStatus): return codes.InvalidArgument case errors.Is(err, ErrAssetAccessDenied), errors.Is(err, ErrMintOrderAccessDenied): return codes.PermissionDenied case errors.Is(err, ErrActivityNotFound), errors.Is(err, ErrActivityItemNotFound), errors.Is(err, ErrCollectionAssetNotFound), errors.Is(err, ErrActivityAssetNotFound), errors.Is(err, ErrAssetRegistryNotFound): return codes.NotFound case errors.Is(err, ErrInvalidAssetType): return codes.InvalidArgument default: return codes.Internal } } // FormatErrorResponse 格式化错误响应 // code 字段填 google.rpc.Code 数字(uint32) func FormatErrorResponse(err error) *pb.BaseResponse { if err == nil { return &pb.BaseResponse{ Code: uint32(codes.OK), Message: "", Timestamp: getCurrentTimestamp(), } } return &pb.BaseResponse{ Code: uint32(ToGRPCCode(err)), Message: err.Error(), Timestamp: getCurrentTimestamp(), } } // FormatSuccessResponse 格式化成功响应 func FormatSuccessResponse() *pb.BaseResponse { return &pb.BaseResponse{ Code: uint32(codes.OK), Message: "", Timestamp: getCurrentTimestamp(), } } // BuildBaseResponse 构建基础响应(FormatErrorResponse的别名) func BuildBaseResponse(err error) *pb.BaseResponse { return FormatErrorResponse(err) } // NewError 创建新的业务错误,带 google.rpc.Code // 推荐用法:appErrors.NewError(codes.Unauthenticated, "token 过期") func NewError(code codes.Code, message string) error { return fmt.Errorf("[%s] %s", code.String(), message) } // NewRequestInCooldownError 创建冷静期错误,包含剩余天数 // 用 fmt.Errorf("%w: ...") 保留 wrap 关系,让 errors.Is(err, ErrRequestInCooldown) 仍能识别 func NewRequestInCooldownError(days int) error { return fmt.Errorf("%w: %d 天后再试", ErrRequestInCooldown, days) } // BuildBaseResponseWithMessage 构建带有自定义消息的响应 // code 是 google.rpc.Code 数字 func BuildBaseResponseWithMessage(code codes.Code, message string) *pb.BaseResponse { return &pb.BaseResponse{ Code: uint32(code), Message: message, Timestamp: getCurrentTimestamp(), } } // getCurrentTimestamp 获取当前时间戳(毫秒) func getCurrentTimestamp() int64 { return time.Now().UnixMilli() } // NewAccountBannedError 返回 ErrAccountBanned,可选地把 reason 拼到 message 里 // 用 fmt.Errorf("%w: ...") 保留 wrap 关系,让 errors.Is(err, ErrAccountBanned) 仍能识别 func NewAccountBannedError(reason string) error { if reason != "" { return fmt.Errorf("%w: %s", ErrAccountBanned, reason) } return ErrAccountBanned } // NewAccountFrozenError 返回 ErrAccountFrozen,把 reason / frozenUntil 拼到 message 里 // frozenUntil 为 nil 表示永久冻结 func NewAccountFrozenError(reason string, frozenUntil *int64) error { parts := []string{} if reason != "" { parts = append(parts, "原因:"+reason) } if frozenUntil != nil { parts = append(parts, "解封时间:"+time.UnixMilli(*frozenUntil).Format("2006-01-02 15:04:05")) } if len(parts) > 0 { return fmt.Errorf("%w: %s", ErrAccountFrozen, strings.Join(parts, ", ")) } return ErrAccountFrozen }