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 }