213 lines
9.4 KiB
Go
213 lines
9.4 KiB
Go
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
|
||
}
|