139 lines
3.9 KiB
Go
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
|
|
}
|