412 lines
12 KiB
Go
412 lines
12 KiB
Go
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
|
||
}
|