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

139 lines
3.9 KiB
Go

package service
import (
"errors"
"fmt"
"time"
"github.com/topfans/backend/pkg/models"
pb "github.com/topfans/backend/pkg/proto/gallery"
"github.com/topfans/backend/services/galleryService/client"
"github.com/topfans/backend/services/galleryService/config"
"github.com/topfans/backend/services/galleryService/repository"
)
// SlotService 展位服务接口
type SlotService interface {
UnlockSlot(userID, starID int64) (*pb.UnlockSlotData, error)
}
// slotService 展位服务实现
type slotService struct {
repo repository.GalleryRepository
userClient client.UserRPCClient
}
// NewSlotService 创建展位服务实例
func NewSlotService(repo repository.GalleryRepository, userClient client.UserRPCClient) SlotService {
return &slotService{
repo: repo,
userClient: userClient,
}
}
// UnlockSlot 解锁/购买新展位(优先等级解锁)
func (s *slotService) UnlockSlot(userID, starID int64) (*pb.UnlockSlotData, error) {
// 1. 获取粉丝档案(等级、水晶余额)
if s.userClient == nil {
return nil, errors.New("user client not initialized")
}
profile, err := s.userClient.GetFanProfile(userID, starID)
if err != nil {
return nil, err
}
// 2. 查询当前展位数
currentSlotCount, err := s.repo.GetSlotCount(userID, starID)
if err != nil {
return nil, err
}
nextSlotIndex := int(currentSlotCount) + 1
// 3. 检查是否已达到最大展位数
if nextSlotIndex > config.GalleryRules.MaxSlotCount {
return nil, errors.New("已达到最大展位数")
}
// 4. 获取解锁规则
requiredLevel, hasLevel := config.GalleryRules.UnlockLevelBySlot[nextSlotIndex]
requiredCrystal, hasCrystal := config.GalleryRules.UnlockCrystalBySlot[nextSlotIndex]
// 如果没有配置解锁规则,返回错误
if !hasLevel && !hasCrystal {
return nil, errors.New("该展位无法解锁")
}
// 5. 优先检查等级解锁
if hasLevel && profile.Level >= int32(requiredLevel) {
// 等级足够,直接解锁(不消耗水晶)
_, err := s.createUnlockedSlot(userID, starID, nextSlotIndex, "level", requiredLevel)
if err != nil {
return nil, err
}
return &pb.UnlockSlotData{
SlotTotal: int32(nextSlotIndex),
CrystalBalance: profile.CrystalBalance, // 水晶余额不变
}, nil
}
// 6. 等级不够,检查水晶购买
if hasCrystal && profile.CrystalBalance >= int64(requiredCrystal) {
// 扣除水晶
newBalance, err := s.userClient.UpdateCrystalBalance(userID, starID, -int64(requiredCrystal),
"slot_purchase", "", fmt.Sprintf("购买展位 #%d", nextSlotIndex))
if err != nil {
return nil, err
}
// 创建展位
_, err = s.createUnlockedSlot(userID, starID, nextSlotIndex, "crystal", requiredCrystal)
if err != nil {
// 如果创建失败,需要回滚水晶扣除
// TODO: 考虑使用分布式事务或补偿机制
return nil, err
}
return &pb.UnlockSlotData{
SlotTotal: int32(nextSlotIndex),
CrystalBalance: newBalance, // 返回扣除后的水晶余额
}, nil
}
// 7. 都不满足,返回错误
return nil, errors.New("等级和水晶余额都不足,无法解锁")
}
// createUnlockedSlot 创建已解锁的展位
func (s *slotService) createUnlockedSlot(userID, starID int64, slotIndex int, unlockType string, unlockValue int) (*models.BoothSlot, error) {
now := time.Now().UnixMilli()
// 获取真实的 fan_profile ID
profile, err := s.userClient.GetFanProfile(userID, starID)
if err != nil {
return nil, fmt.Errorf("failed to get fan profile: %w", err)
}
slot := &models.BoothSlot{
HostProfileID: profile.ID,
UserID: userID,
StarID: starID,
SlotIndex: slotIndex,
Visibility: "public", // 新解锁的展位默认为公有
IsEnabled: true,
UnlockType: unlockType,
UnlockValue: unlockValue,
CreatedAt: now,
UpdatedAt: now,
}
err = s.repo.CreateSlot(slot)
if err != nil {
return nil, err
}
return slot, nil
}