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

934 lines
27 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"
"time"
appErrors "github.com/topfans/backend/pkg/errors"
"github.com/topfans/backend/pkg/logger"
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"
)
// UserService 用户信息Service接口
type UserService interface {
// GetUser 获取用户信息
GetUser(req *pb.GetUserRequest) (*pb.GetUserResponse, error)
// GetFanProfile 获取粉丝档案
GetFanProfile(req *pb.GetFanProfileRequest) (*pb.GetFanProfileResponse, error)
// GetMyProfile 获取个人信息页
GetMyProfile(req *pb.GetMyProfileRequest, userID, starID int64) (*pb.GetMyProfileResponse, error)
// CheckNickname 检查昵称是否已被注册
CheckNickname(req *pb.CheckNicknameRequest) (*pb.CheckNicknameResponse, error)
// CheckMobile 检查手机号是否已被注册
CheckMobile(req *pb.CheckMobileRequest) (*pb.CheckMobileResponse, error)
// UpdateNickname 修改昵称
UpdateNickname(req *pb.UpdateNicknameRequest, userID, starID int64) (*pb.UpdateNicknameResponse, error)
// UpdatePassword 修改密码
UpdatePassword(req *pb.UpdatePasswordRequest, userID int64) (*pb.UpdatePasswordResponse, error)
// UpdateFanProfileSocial 更新粉丝档案的好友数量内部RPC调用
UpdateFanProfileSocial(req *pb.UpdateFanProfileSocialRequest) (*pb.UpdateFanProfileSocialResponse, error)
// UpdateCrystalBalance 更新水晶余额内部RPC调用
UpdateCrystalBalance(req *pb.UpdateCrystalBalanceRequest) (*pb.UpdateCrystalBalanceResponse, error)
// UpdateAssetsCount 更新资产数量内部RPC调用
UpdateAssetsCount(req *pb.UpdateAssetsCountRequest) (*pb.UpdateAssetsCountResponse, error)
// AddExhibitionHours 增加用户累计上架时长内部RPC调用用于galleryService展品下架时
AddExhibitionHours(req *pb.AddExhibitionHoursRequest) (*pb.AddExhibitionHoursResponse, error)
// UpdateAvatar 更新用户头像
UpdateAvatar(req *pb.UpdateAvatarRequest, userID, starID int64) (*pb.UpdateAvatarResponse, error)
}
// userService 用户信息Service实现
type userService struct {
userRepo repository.UserRepository
fanProfileRepo repository.FanProfileRepository
db *gorm.DB
}
// NewUserService 创建用户信息Service实例
func NewUserService(
userRepo repository.UserRepository,
fanProfileRepo repository.FanProfileRepository,
db *gorm.DB,
) UserService {
return &userService{
userRepo: userRepo,
fanProfileRepo: fanProfileRepo,
db: db,
}
}
// GetUser 获取用户信息
func (s *userService) GetUser(req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
// 1. 参数验证
if !validator.ValidateUserID(req.UserId) {
logger.Logger.Warn("Invalid user_id",
zap.Int64("user_id", req.UserId),
)
return nil, fmt.Errorf("invalid user_id: %d", req.UserId)
}
// 2. 查询用户
user, err := s.userRepo.GetByID(req.UserId)
if err != nil {
if errors.Is(err, appErrors.ErrUserNotFound) {
logger.Logger.Warn("User not found",
zap.Int64("user_id", req.UserId),
)
return &pb.GetUserResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrUserNotFound),
}, nil // 返回响应对象,不返回 error
}
logger.Logger.Error("Failed to get user",
zap.Int64("user_id", req.UserId),
zap.Error(err),
)
return &pb.GetUserResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil // 返回响应对象,不返回 error
}
// 3. 构建响应
response := &pb.GetUserResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
User: &pb.User{
Id: user.ID,
Mobile: user.Mobile,
AvatarUrl: getStringValue(user.AvatarURL),
GlobalWalletAddress: getStringValue(user.GlobalWalletAddr),
IsActive: user.IsActive,
CreatedAt: user.CreatedAt,
},
}
logger.Logger.Debug("Get user successful",
zap.Int64("user_id", req.UserId),
)
return response, nil
}
// GetFanProfile 获取粉丝档案
func (s *userService) GetFanProfile(req *pb.GetFanProfileRequest) (*pb.GetFanProfileResponse, error) {
// 1. 参数验证
if !validator.ValidateUserID(req.UserId) {
logger.Logger.Warn("Invalid user_id",
zap.Int64("user_id", req.UserId),
)
return &pb.GetFanProfileResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidUserID),
}, nil
}
if !validator.ValidateStarID(req.StarId) {
logger.Logger.Warn("Invalid star_id",
zap.Int64("star_id", req.StarId),
)
return &pb.GetFanProfileResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidStarID),
}, nil
}
// 2. 查询粉丝档案
fanProfile, err := s.fanProfileRepo.GetByUserAndStar(req.UserId, req.StarId)
if err != nil {
if errors.Is(err, appErrors.ErrFanProfileNotFound) {
logger.Logger.Warn("Fan profile not found",
zap.Int64("user_id", req.UserId),
zap.Int64("star_id", req.StarId),
)
return &pb.GetFanProfileResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrFanProfileNotFound),
}, nil // 返回响应对象,不返回 error
}
logger.Logger.Error("Failed to get fan profile",
zap.Int64("user_id", req.UserId),
zap.Int64("star_id", req.StarId),
zap.Error(err),
)
return &pb.GetFanProfileResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil // 返回响应对象,不返回 error
}
// 4. 构建响应
response := &pb.GetFanProfileResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
Profile: ModelToProtoFanProfile(fanProfile),
}
logger.Logger.Debug("Get fan profile successful",
zap.Int64("user_id", req.UserId),
zap.Int64("star_id", req.StarId),
)
return response, nil
}
// GetMyProfile 获取个人信息页(聚合用户信息和粉丝档案)
func (s *userService) GetMyProfile(req *pb.GetMyProfileRequest, userID, starID int64) (*pb.GetMyProfileResponse, 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(starID) {
logger.Logger.Warn("Invalid star_id",
zap.Int64("star_id", starID),
)
return nil, appErrors.ErrInvalidStarID
}
// 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. 查询当前粉丝档案
currentFanProfile, err := s.fanProfileRepo.GetByUserAndStar(userID, starID)
if err != nil {
if errors.Is(err, appErrors.ErrFanProfileNotFound) {
logger.Logger.Warn("Fan profile not found",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
return nil, appErrors.ErrFanProfileNotFound
}
logger.Logger.Error("Failed to get fan profile",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to get fan profile: %w", err)
}
// 5. 查询用户的所有粉丝身份
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)
}
// 5. 构建响应
pbFanProfiles := make([]*pb.FanProfile, 0, len(fanProfiles))
for _, profile := range fanProfiles {
pbFanProfiles = append(pbFanProfiles, &pb.FanProfile{
Id: profile.ID,
UserId: profile.UserID,
StarId: profile.StarID,
Nickname: profile.Nickname,
Level: int32(profile.Level),
Times: int32(profile.Times),
Social: int32(profile.Social),
CoinBalance: profile.CoinBalance,
CrystalBalance: profile.CrystalBalance,
Tags: []string(profile.Tags),
CreatedAt: profile.CreatedAt,
})
}
response := &pb.GetMyProfileResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
User: &pb.User{
Id: user.ID,
Mobile: user.Mobile,
AvatarUrl: getStringValue(user.AvatarURL),
GlobalWalletAddress: getStringValue(user.GlobalWalletAddr),
IsActive: user.IsActive,
CreatedAt: user.CreatedAt,
},
FanProfile: ModelToProtoFanProfile(currentFanProfile),
FanProfiles: pbFanProfiles,
}
logger.Logger.Debug("Get my profile successful",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
return response, nil
}
// CheckNickname 检查昵称是否已被注册
func (s *userService) CheckNickname(req *pb.CheckNicknameRequest) (*pb.CheckNicknameResponse, error) {
// 1. 参数验证
if req.Nickname == "" {
logger.Logger.Warn("Nickname is empty")
return &pb.CheckNicknameResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidNickname),
}, nil
}
// 2. 验证昵称格式
valid, msg := validator.ValidateNickname(req.Nickname)
if !valid {
logger.Logger.Warn("Invalid nickname format",
zap.String("nickname", req.Nickname),
zap.String("error", msg),
)
return &pb.CheckNicknameResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidNickname),
}, nil
}
// 3. 查询昵称是否已存在
exists, err := s.fanProfileRepo.ExistsByNickname(req.Nickname)
if err != nil {
logger.Logger.Error("Failed to check nickname",
zap.String("nickname", req.Nickname),
zap.Error(err),
)
return &pb.CheckNicknameResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
logger.Logger.Info("Check nickname result",
zap.String("nickname", req.Nickname),
zap.Bool("exists", exists),
)
// 4. 构建响应
return &pb.CheckNicknameResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
Exists: exists,
}, nil
}
// CheckMobile 检查手机号是否已被注册
func (s *userService) CheckMobile(req *pb.CheckMobileRequest) (*pb.CheckMobileResponse, error) {
// 1. 参数验证
if req.Mobile == "" {
logger.Logger.Warn("Mobile is empty")
return &pb.CheckMobileResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidMobile),
}, nil
}
// 2. 验证手机号格式
if !validator.ValidateMobile(req.Mobile) {
logger.Logger.Warn("Invalid mobile format",
zap.String("mobile", req.Mobile),
)
return &pb.CheckMobileResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidMobile),
}, nil
}
// 3. 查询手机号是否已存在
exists, err := s.userRepo.ExistsByMobile(req.Mobile)
if err != nil {
logger.Logger.Error("Failed to check mobile",
zap.String("mobile", req.Mobile),
zap.Error(err),
)
return &pb.CheckMobileResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
logger.Logger.Info("Check mobile result",
zap.String("mobile", req.Mobile),
zap.Bool("exists", exists),
)
// 4. 构建响应
return &pb.CheckMobileResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
Exists: exists,
}, nil
}
// UpdateNickname 修改昵称
func (s *userService) UpdateNickname(req *pb.UpdateNicknameRequest, userID, starID int64) (*pb.UpdateNicknameResponse, 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(starID) {
logger.Logger.Warn("Invalid star_id",
zap.Int64("star_id", 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", starID),
zap.String("error", msg),
)
return nil, fmt.Errorf("invalid nickname: %s", msg)
}
// 2. 验证粉丝档案是否存在
_, err := s.fanProfileRepo.GetByUserAndStar(userID, starID)
if err != nil {
if errors.Is(err, appErrors.ErrFanProfileNotFound) {
logger.Logger.Warn("Fan profile not found",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
return nil, appErrors.ErrFanProfileNotFound
}
logger.Logger.Error("Failed to get fan profile",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to get fan profile: %w", err)
}
// 3. 更新昵称
if err := s.fanProfileRepo.UpdateNickname(userID, starID, req.Nickname); err != nil {
logger.Logger.Error("Failed to update nickname",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to update nickname: %w", err)
}
// 4. 查询更新后的粉丝档案
fanProfile, err := s.fanProfileRepo.GetByUserAndStar(userID, starID)
if err != nil {
logger.Logger.Error("Failed to get updated fan profile",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to get updated fan profile: %w", err)
}
// 5. 构建响应
response := &pb.UpdateNicknameResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
FanProfile: ModelToProtoFanProfile(fanProfile),
}
logger.Logger.Info("Update nickname successful",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.String("new_nickname", req.Nickname),
)
return response, nil
}
// UpdatePassword 修改密码
func (s *userService) UpdatePassword(req *pb.UpdatePasswordRequest, userID int64) (*pb.UpdatePasswordResponse, 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 req.OldPassword == "" {
logger.Logger.Warn("Old password is empty",
zap.Int64("user_id", userID),
)
return nil, appErrors.ErrInvalidPassword
}
valid, msg := validator.ValidatePassword(req.NewPassword)
if !valid {
logger.Logger.Warn("Invalid new password",
zap.Int64("user_id", userID),
zap.String("error", msg),
)
if msg == "password too short" {
return nil, appErrors.ErrPasswordTooShort
}
return nil, fmt.Errorf("invalid password: %s", msg)
}
// 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. 验证旧密码
if !s.userRepo.VerifyPassword(user, req.OldPassword) {
logger.Logger.Warn("Invalid old password",
zap.Int64("user_id", userID),
)
return nil, appErrors.ErrInvalidPassword
}
// 4. 加密新密码
newPasswordHash, err := repository.HashPassword(req.NewPassword)
if err != nil {
logger.Logger.Error("Failed to hash new password",
zap.Int64("user_id", userID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to hash password: %w", err)
}
// 5. 更新密码和updated_at清除Token使用事务
err = s.db.Transaction(func(tx *gorm.DB) error {
// 更新密码
user.PasswordHash = newPasswordHash
user.UpdatedAt = time.Now().UnixMilli()
// 清除Token设为nil
user.AccessToken = nil
user.TokenExpiresAt = nil
if err := tx.Model(user).Updates(map[string]interface{}{
"password_hash": user.PasswordHash,
"updated_at": user.UpdatedAt,
"access_token": nil,
"token_expires_at": nil,
}).Error; err != nil {
logger.Logger.Error("Failed to update password in transaction",
zap.Int64("user_id", userID),
zap.Error(err),
)
return fmt.Errorf("failed to update password: %w", err)
}
return nil
})
if err != nil {
return nil, err
}
logger.Logger.Info("Update password successful",
zap.Int64("user_id", userID),
)
return &pb.UpdatePasswordResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
// UpdateFanProfileSocial 更新粉丝档案的好友数量内部RPC调用
func (s *userService) UpdateFanProfileSocial(req *pb.UpdateFanProfileSocialRequest) (*pb.UpdateFanProfileSocialResponse, error) {
// 1. 参数验证
if !validator.ValidateUserID(req.UserId) {
logger.Logger.Warn("Invalid user_id",
zap.Int64("user_id", req.UserId),
)
return &pb.UpdateFanProfileSocialResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidUserID),
}, nil
}
if !validator.ValidateStarID(req.StarId) {
logger.Logger.Warn("Invalid star_id",
zap.Int64("star_id", req.StarId),
)
return &pb.UpdateFanProfileSocialResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidStarID),
}, nil
}
// 2. 更新 social 字段
newSocial, err := s.fanProfileRepo.UpdateSocial(req.UserId, req.StarId, req.Delta)
if err != nil {
logger.Logger.Error("Failed to update fan profile social",
zap.Int64("user_id", req.UserId),
zap.Int64("star_id", req.StarId),
zap.Int32("delta", req.Delta),
zap.Error(err),
)
// 判断错误类型
if errors.Is(err, appErrors.ErrFanProfileNotFound) {
return &pb.UpdateFanProfileSocialResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrFanProfileNotFound),
}, nil
}
return &pb.UpdateFanProfileSocialResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
logger.Logger.Info("Update fan profile social successful",
zap.Int64("user_id", req.UserId),
zap.Int64("star_id", req.StarId),
zap.Int32("delta", req.Delta),
zap.Int32("new_social", newSocial),
)
return &pb.UpdateFanProfileSocialResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
NewSocial: newSocial,
}, nil
}
// UpdateCrystalBalance 更新水晶余额内部RPC调用
func (s *userService) UpdateCrystalBalance(req *pb.UpdateCrystalBalanceRequest) (*pb.UpdateCrystalBalanceResponse, error) {
// 1. 参数验证
if !validator.ValidateUserID(req.UserId) {
logger.Logger.Warn("Invalid user_id",
zap.Int64("user_id", req.UserId),
)
return &pb.UpdateCrystalBalanceResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidUserID),
}, nil
}
if !validator.ValidateStarID(req.StarId) {
logger.Logger.Warn("Invalid star_id",
zap.Int64("star_id", req.StarId),
)
return &pb.UpdateCrystalBalanceResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidStarID),
}, nil
}
// 2. 更新水晶余额
newBalance, err := s.fanProfileRepo.UpdateCrystalBalance(req.UserId, req.StarId, req.Delta, req.ChangeType, req.SourceId, req.Description)
if err != nil {
logger.Logger.Error("Failed to update crystal balance",
zap.Int64("user_id", req.UserId),
zap.Int64("star_id", req.StarId),
zap.Int64("delta", req.Delta),
zap.Error(err),
)
// 判断错误类型
if errors.Is(err, appErrors.ErrFanProfileNotFound) {
return &pb.UpdateCrystalBalanceResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrFanProfileNotFound),
}, nil
}
return &pb.UpdateCrystalBalanceResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
logger.Logger.Info("Update crystal balance successful",
zap.Int64("user_id", req.UserId),
zap.Int64("star_id", req.StarId),
zap.Int64("delta", req.Delta),
zap.Int64("new_balance", newBalance),
)
return &pb.UpdateCrystalBalanceResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
NewBalance: newBalance,
}, nil
}
// UpdateAssetsCount 更新资产数量内部RPC调用
func (s *userService) UpdateAssetsCount(req *pb.UpdateAssetsCountRequest) (*pb.UpdateAssetsCountResponse, error) {
// 1. 参数验证
if !validator.ValidateUserID(req.UserId) {
logger.Logger.Warn("Invalid user_id",
zap.Int64("user_id", req.UserId),
)
return &pb.UpdateAssetsCountResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidUserID),
}, nil
}
if !validator.ValidateStarID(req.StarId) {
logger.Logger.Warn("Invalid star_id",
zap.Int64("star_id", req.StarId),
)
return &pb.UpdateAssetsCountResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidStarID),
}, nil
}
// 2. 根据 delta 的正负调用不同的方法
var err error
var newCount int32
if req.Delta > 0 {
// 增加资产数量
err = s.fanProfileRepo.IncrementAssetsCount(req.UserId, req.StarId, req.Delta)
if err == nil {
// 查询更新后的值
profile, getErr := s.fanProfileRepo.GetByUserAndStar(req.UserId, req.StarId)
if getErr == nil {
newCount = profile.AssetsCount
}
}
} else if req.Delta < 0 {
// 减少资产数量
err = s.fanProfileRepo.DecrementAssetsCount(req.UserId, req.StarId, -req.Delta)
if err == nil {
// 查询更新后的值
profile, getErr := s.fanProfileRepo.GetByUserAndStar(req.UserId, req.StarId)
if getErr == nil {
newCount = profile.AssetsCount
}
}
} else {
// delta == 0直接查询当前值
profile, getErr := s.fanProfileRepo.GetByUserAndStar(req.UserId, req.StarId)
if getErr != nil {
if errors.Is(getErr, appErrors.ErrFanProfileNotFound) {
return &pb.UpdateAssetsCountResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrFanProfileNotFound),
}, nil
}
return &pb.UpdateAssetsCountResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
newCount = profile.AssetsCount
}
if err != nil {
logger.Logger.Error("Failed to update assets count",
zap.Int64("user_id", req.UserId),
zap.Int64("star_id", req.StarId),
zap.Int32("delta", req.Delta),
zap.Error(err),
)
// 判断错误类型
if errors.Is(err, appErrors.ErrFanProfileNotFound) {
return &pb.UpdateAssetsCountResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrFanProfileNotFound),
}, nil
}
return &pb.UpdateAssetsCountResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
logger.Logger.Info("Update assets count successful",
zap.Int64("user_id", req.UserId),
zap.Int64("star_id", req.StarId),
zap.Int32("delta", req.Delta),
zap.Int32("new_count", newCount),
)
return &pb.UpdateAssetsCountResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
NewCount: newCount,
}, nil
}
// AddExhibitionHours 增加用户累计上架时长并触发升级检查内部RPC调用
func (s *userService) AddExhibitionHours(req *pb.AddExhibitionHoursRequest) (*pb.AddExhibitionHoursResponse, error) {
// 1. 参数验证
if !validator.ValidateUserID(req.UserId) {
logger.Logger.Warn("Invalid user_id",
zap.Int64("user_id", req.UserId),
)
return &pb.AddExhibitionHoursResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidUserID),
}, nil
}
if !validator.ValidateStarID(req.StarId) {
logger.Logger.Warn("Invalid star_id",
zap.Int64("star_id", req.StarId),
)
return &pb.AddExhibitionHoursResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidStarID),
}, nil
}
if req.ExhibitionHours <= 0 {
logger.Logger.Warn("Invalid exhibition_hours",
zap.Int64("exhibition_hours", req.ExhibitionHours),
)
return &pb.AddExhibitionHoursResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_BAD_REQUEST,
Message: "exhibition_hours must be positive",
},
}, nil
}
// 2. 增加累计上架时长并同步等级
newLevel, levelDelta, crystalReward, err := s.fanProfileRepo.AddExhibitionHours(req.UserId, req.StarId, req.ExhibitionHours, req.SourceId)
if err != nil {
logger.Logger.Error("AddExhibitionHours failed",
zap.Int64("user_id", req.UserId),
zap.Int64("star_id", req.StarId),
zap.Int64("exhibition_hours", req.ExhibitionHours),
zap.Error(err))
return &pb.AddExhibitionHoursResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, err
}
logger.Logger.Info("AddExhibitionHours successful",
zap.Int64("user_id", req.UserId),
zap.Int64("star_id", req.StarId),
zap.Int64("exhibition_hours", req.ExhibitionHours),
zap.Int32("new_level", newLevel),
zap.Int32("level_delta", levelDelta),
zap.Int64("crystal_reward", crystalReward))
return &pb.AddExhibitionHoursResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
NewLevel: newLevel,
LevelDelta: levelDelta,
CrystalReward: crystalReward,
}, nil
}
// UpdateAvatar 更新用户头像
func (s *userService) UpdateAvatar(req *pb.UpdateAvatarRequest, userID, starID int64) (*pb.UpdateAvatarResponse, 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(starID) {
logger.Logger.Warn("Invalid star_id",
zap.Int64("star_id", starID),
)
return nil, appErrors.ErrInvalidStarID
}
if req.AvatarUrl == "" {
logger.Logger.Warn("Avatar URL is empty",
zap.Int64("user_id", userID),
)
return nil, fmt.Errorf("avatar_url cannot be empty")
}
// 2. 验证URL格式简单验证
if len(req.AvatarUrl) > 500 {
logger.Logger.Warn("Avatar URL too long",
zap.Int64("user_id", userID),
zap.Int("length", len(req.AvatarUrl)),
)
return nil, fmt.Errorf("avatar_url too long, max length is 500")
}
// 3. 更新头像
if err := s.fanProfileRepo.UpdateAvatar(userID, starID, req.AvatarUrl); err != nil {
if errors.Is(err, appErrors.ErrFanProfileNotFound) {
logger.Logger.Warn("Fan profile not found",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
return nil, appErrors.ErrFanProfileNotFound
}
logger.Logger.Error("Failed to update avatar",
zap.Int64("user_id", userID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to update avatar: %w", err)
}
logger.Logger.Info("Update avatar successful",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.String("avatar_url", req.AvatarUrl),
)
// 4. 构建响应
return &pb.UpdateAvatarResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
AvatarUrl: req.AvatarUrl,
}, nil
}