topfans/backend/pkg/errors/errors.go
2026-06-15 16:28:35 +08:00

213 lines
9.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}