topfans/backend/services/userService/service/identity_service.go
2026-05-16 02:42:32 +08:00

412 lines
12 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 service
import (
"errors"
"fmt"
"strings"
"time"
appErrors "github.com/topfans/backend/pkg/errors"
"github.com/topfans/backend/pkg/database"
"github.com/topfans/backend/pkg/jwt"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/models"
pbCommon "github.com/topfans/backend/pkg/proto/common"
pb "github.com/topfans/backend/pkg/proto/user"
"github.com/topfans/backend/pkg/validator"
"github.com/topfans/backend/services/userService/repository"
"go.uber.org/zap"
"gorm.io/gorm"
)
// GetMaxIdentities 获取最大身份数量限制(从数据库配置)
func GetMaxIdentities() int {
db := database.GetDB()
if db == nil {
logger.Logger.Warn("Database not initialized, using default MaxIdentities=2")
return 2
}
var config models.SystemConfig
err := db.Where("config_key = ? AND is_active = ?", "max_star_identities", true).First(&config).Error
if err != nil {
logger.Logger.Warn("Failed to get max_star_identities config, using default=2",
zap.Error(err))
return 2
}
return int(config.ConfigValue)
}
// IdentityService 粉丝身份Service接口
type IdentityService interface {
// GetFanIdentities 获取可选粉丝身份列表(所有可用明星)
GetFanIdentities(req *pb.GetFanIdentitiesRequest) (*pb.GetFanIdentitiesResponse, error)
// GetMyFanIdentities 获取我的粉丝身份列表(当前用户拥有的粉丝身份)
GetMyFanIdentities(userID int64, currentStarID int64) (*pb.GetMyFanIdentitiesResponse, error)
// AddIdentity 新增粉丝身份
AddIdentity(req *pb.AddIdentityRequest, userID int64) (*pb.AddIdentityResponse, error)
// SwitchIdentity 切换粉丝身份
SwitchIdentity(req *pb.SwitchIdentityRequest, userID int64, currentStarID int64) (*pb.SwitchIdentityResponse, error)
}
// identityService 粉丝身份Service实现
type identityService struct {
fanProfileRepo repository.FanProfileRepository
starRepo repository.StarRepository
userRepo repository.UserRepository
db *gorm.DB
}
// NewIdentityService 创建粉丝身份Service实例
func NewIdentityService(
fanProfileRepo repository.FanProfileRepository,
starRepo repository.StarRepository,
userRepo repository.UserRepository,
db *gorm.DB,
) IdentityService {
return &identityService{
fanProfileRepo: fanProfileRepo,
starRepo: starRepo,
userRepo: userRepo,
db: db,
}
}
// GetFanIdentities 获取可选粉丝身份列表(支持搜索)
func (s *identityService) GetFanIdentities(req *pb.GetFanIdentitiesRequest) (*pb.GetFanIdentitiesResponse, error) {
var stars []*models.Star
var err error
// 如果有关键词,进行搜索;否则返回所有可用明星
if req.Keyword != "" {
stars, err = s.starRepo.Search(req.Keyword)
} else {
stars, err = s.starRepo.GetAllActive()
}
if err != nil {
logger.Logger.Error("Failed to get stars",
zap.String("keyword", req.Keyword),
zap.Error(err),
)
return nil, fmt.Errorf("failed to get stars: %w", err)
}
// 转换为proto类型使用统一转换函数
pbStars := make([]*pb.Star, 0, len(stars))
for _, star := range stars {
pbStars = append(pbStars, ModelToProtoStar(star))
}
logger.Logger.Debug("Get fan identities successful",
zap.String("keyword", req.Keyword),
zap.Int("count", len(pbStars)),
)
return &pb.GetFanIdentitiesResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
Stars: pbStars,
}, nil
}
// GetMyFanIdentities 获取我的粉丝身份列表
func (s *identityService) GetMyFanIdentities(userID int64, currentStarID int64) (*pb.GetMyFanIdentitiesResponse, error) {
// 1. 参数验证
if !validator.ValidateUserID(userID) {
logger.Logger.Warn("Invalid user_id",
zap.Int64("user_id", userID),
)
return nil, fmt.Errorf("invalid user_id: %d", userID)
}
// 2. 查询用户的所有粉丝身份
fanProfiles, err := s.fanProfileRepo.GetByUserID(userID)
if err != nil {
logger.Logger.Error("Failed to get fan profiles",
zap.Int64("user_id", userID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to get fan profiles: %w", err)
}
// 3. 查询对应的明星信息并组合
items := make([]*pb.MyFanIdentityItem, 0, len(fanProfiles))
for _, profile := range fanProfiles {
// 查询明星信息
star, err := s.starRepo.GetByID(profile.StarID)
if err != nil {
logger.Logger.Error("Failed to get star info",
zap.Int64("star_id", profile.StarID),
zap.Error(err),
)
// 如果查询明星失败,使用空的明星信息
star = &models.Star{
StarID: profile.StarID,
Name: "",
NameEn: nil,
Tag: nil,
IdentityID: "",
}
}
// 组合粉丝档案和明星信息
items = append(items, &pb.MyFanIdentityItem{
FanProfile: ModelToProtoFanProfile(profile),
Star: ModelToProtoStar(star),
})
}
logger.Logger.Debug("Get my fan identities successful",
zap.Int64("user_id", userID),
zap.Int64("current_star_id", currentStarID),
zap.Int("count", len(items)),
)
return &pb.GetMyFanIdentitiesResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
Items: items,
CurrentStarId: currentStarID,
}, nil
}
// AddIdentity 新增粉丝身份最多2个
func (s *identityService) AddIdentity(req *pb.AddIdentityRequest, userID int64) (*pb.AddIdentityResponse, error) {
// 1. 参数验证
if !validator.ValidateUserID(userID) {
logger.Logger.Warn("Invalid user_id",
zap.Int64("user_id", userID),
)
return nil, fmt.Errorf("invalid user_id: %d", userID)
}
if !validator.ValidateStarID(req.StarId) {
logger.Logger.Warn("Invalid star_id",
zap.Int64("star_id", req.StarId),
)
return nil, appErrors.ErrInvalidStarID
}
valid, msg := validator.ValidateNickname(req.Nickname)
if !valid {
logger.Logger.Warn("Invalid nickname",
zap.Int64("user_id", userID),
zap.Int64("star_id", req.StarId),
zap.String("error", msg),
)
return nil, fmt.Errorf("invalid nickname: %s", msg)
}
// 2. 检查用户已有的粉丝身份数量最多2个
count, err := s.fanProfileRepo.CountByUserID(userID)
if err != nil {
logger.Logger.Error("Failed to count fan profiles",
zap.Int64("user_id", userID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to count fan profiles: %w", err)
}
if count >= int64(GetMaxIdentities()) {
logger.Logger.Warn("Maximum identities reached",
zap.Int64("user_id", userID),
zap.Int64("current_count", count),
zap.Int("max_identities", GetMaxIdentities()),
)
return nil, appErrors.ErrMaxIdentitiesReached
}
// 3. 验证明星是否存在
_, err = s.starRepo.GetByID(req.StarId)
if err != nil {
logger.Logger.Error("Failed to get star",
zap.Int64("star_id", req.StarId),
zap.Error(err),
)
if errors.Is(err, appErrors.ErrStarNotFound) {
return nil, appErrors.ErrStarNotFound
}
return nil, fmt.Errorf("failed to get star: %w", err)
}
// 4. 检查该用户是否已有该明星的身份
existingProfile, err := s.fanProfileRepo.GetByUserAndStar(userID, req.StarId)
if err == nil && existingProfile != nil {
logger.Logger.Warn("Fan profile already exists",
zap.Int64("user_id", userID),
zap.Int64("star_id", req.StarId),
)
return nil, fmt.Errorf("fan profile already exists for this user and star")
}
// 如果返回ErrFanProfileNotFound说明不存在可以继续创建
if err != nil && !errors.Is(err, appErrors.ErrFanProfileNotFound) {
logger.Logger.Error("Failed to check existing fan profile",
zap.Int64("user_id", userID),
zap.Int64("star_id", req.StarId),
zap.Error(err),
)
return nil, fmt.Errorf("failed to check existing fan profile: %w", err)
}
// 5. 创建新的粉丝档案
now := time.Now().UnixMilli()
fanProfile := &models.FanProfile{
UserID: userID,
StarID: req.StarId,
Nickname: req.Nickname,
Level: 1,
Times: 1,
Social: 0,
CoinBalance: 0,
CrystalBalance: 0,
Tags: models.StringArray{},
IsActive: true,
CreatedAt: now,
UpdatedAt: now,
}
if err := s.fanProfileRepo.Create(fanProfile); err != nil {
logger.Logger.Error("Failed to create fan profile",
zap.Int64("user_id", userID),
zap.Int64("star_id", req.StarId),
zap.Error(err),
)
// 直接返回 Repository 层定义的业务错误
if errors.Is(err, appErrors.ErrNicknameAlreadyExists) {
return nil, appErrors.ErrNicknameAlreadyExists
}
if errors.Is(err, appErrors.ErrFanProfileAlreadyExists) {
return nil, appErrors.ErrFanProfileAlreadyExists
}
return nil, fmt.Errorf("failed to create fan profile: %w", err)
}
logger.Logger.Info("Add identity successful",
zap.Int64("user_id", userID),
zap.Int64("star_id", req.StarId),
zap.String("nickname", req.Nickname),
)
return &pb.AddIdentityResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
FanProfile: ModelToProtoFanProfile(fanProfile),
}, nil
}
// contains 检查字符串是否包含子串(不区分大小写)
func contains(s, substr string) bool {
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
}
// SwitchIdentity 切换粉丝身份生成新Token
func (s *identityService) SwitchIdentity(req *pb.SwitchIdentityRequest, userID int64, currentStarID int64) (*pb.SwitchIdentityResponse, error) {
// 1. 参数验证
if !validator.ValidateUserID(userID) {
logger.Logger.Warn("Invalid user_id",
zap.Int64("user_id", userID),
)
return nil, fmt.Errorf("invalid user_id: %d", userID)
}
if !validator.ValidateStarID(req.NewStarId) {
logger.Logger.Warn("Invalid new_star_id",
zap.Int64("new_star_id", req.NewStarId),
)
return nil, appErrors.ErrInvalidStarID
}
// 检查是否切换到相同的身份
if req.NewStarId == currentStarID {
logger.Logger.Warn("Switching to same identity",
zap.Int64("user_id", userID),
zap.Int64("star_id", currentStarID),
)
return nil, fmt.Errorf("already using this identity")
}
// 2. 查询用户
user, err := s.userRepo.GetByID(userID)
if err != nil {
if errors.Is(err, appErrors.ErrUserNotFound) {
logger.Logger.Warn("User not found",
zap.Int64("user_id", userID),
)
return nil, appErrors.ErrUserNotFound
}
logger.Logger.Error("Failed to get user",
zap.Int64("user_id", userID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to get user: %w", err)
}
// 3. 验证新身份是否存在
fanProfile, err := s.fanProfileRepo.GetByUserAndStar(userID, req.NewStarId)
if err != nil {
if errors.Is(err, appErrors.ErrFanProfileNotFound) {
logger.Logger.Warn("Fan profile not found for new star",
zap.Int64("user_id", userID),
zap.Int64("new_star_id", req.NewStarId),
)
return nil, appErrors.ErrFanProfileNotFound
}
logger.Logger.Error("Failed to get fan profile",
zap.Int64("user_id", userID),
zap.Int64("new_star_id", req.NewStarId),
zap.Error(err),
)
return nil, fmt.Errorf("failed to get fan profile: %w", err)
}
// 4. 生成新Token包含新的star_id
newToken, err := jwt.GenerateToken(user.ID, req.NewStarId, user.UpdatedAt)
if err != nil {
logger.Logger.Error("Failed to generate token",
zap.Int64("user_id", userID),
zap.Int64("new_star_id", req.NewStarId),
zap.Error(err),
)
return nil, fmt.Errorf("failed to generate token: %w", err)
}
// 5. 更新用户Token
tokenExpiresAt := jwt.GetExpiresAt()
if err := s.userRepo.UpdateToken(user.ID, newToken, tokenExpiresAt); err != nil {
logger.Logger.Error("Failed to update token",
zap.Int64("user_id", userID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to update token: %w", err)
}
logger.Logger.Info("Switch identity successful",
zap.Int64("user_id", userID),
zap.Int64("old_star_id", currentStarID),
zap.Int64("new_star_id", req.NewStarId),
)
return &pb.SwitchIdentityResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
AccessToken: newToken,
ExpiresIn: jwt.GetExpiresIn(),
FanProfile: ModelToProtoFanProfile(fanProfile),
}, nil
}