193 lines
5.2 KiB
Go
193 lines
5.2 KiB
Go
package livestream
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"fmt"
|
|
"math/big"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"bindbox-game/internal/repository/mysql/model"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// ActivityDiscreteState 活动随机离散状态
|
|
type ActivityDiscreteState struct {
|
|
ActivityID int64
|
|
TotalWeight int32
|
|
PrizePosMap map[int64][]int32 // prize_id -> sorted random positions
|
|
LastDrawIndex int32 // 已抽奖序号,用于恢复或验证
|
|
GeneratedAt time.Time
|
|
}
|
|
|
|
// ActivityDiscreteCache 活动随机离散缓存
|
|
type ActivityDiscreteCache struct {
|
|
mu sync.RWMutex
|
|
cache map[int64]*ActivityDiscreteState
|
|
}
|
|
|
|
// NewActivityDiscreteCache 创建新的缓存实例
|
|
func NewActivityDiscreteCache() *ActivityDiscreteCache {
|
|
return &ActivityDiscreteCache{
|
|
cache: make(map[int64]*ActivityDiscreteState),
|
|
}
|
|
}
|
|
|
|
// Get 获取活动的离散状态
|
|
func (c *ActivityDiscreteCache) Get(activityID int64) (*ActivityDiscreteState, bool) {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
state, ok := c.cache[activityID]
|
|
return state, ok
|
|
}
|
|
|
|
// Set 设置活动的离散状态
|
|
func (c *ActivityDiscreteCache) Set(state *ActivityDiscreteState) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.cache[state.ActivityID] = state
|
|
}
|
|
|
|
// Delete 删除活动的离散状态
|
|
func (c *ActivityDiscreteCache) Delete(activityID int64) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
delete(c.cache, activityID)
|
|
}
|
|
|
|
// GenerateDiscretePositions 为活动生成随机离散位置
|
|
// 调用时机:活动奖品配置完成后调用一次
|
|
func GenerateDiscretePositions(activityID int64, prizes []*model.LivestreamPrizes) (*ActivityDiscreteState, error) {
|
|
if len(prizes) == 0 {
|
|
return nil, fmt.Errorf("没有奖品")
|
|
}
|
|
|
|
// 计算总权重
|
|
var totalWeight int32
|
|
for _, p := range prizes {
|
|
totalWeight += p.Weight
|
|
}
|
|
|
|
if totalWeight == 0 {
|
|
return nil, fmt.Errorf("总权重为0")
|
|
}
|
|
|
|
// 创建位置池 [0, totalWeight-1]
|
|
positions := make([]int32, totalWeight)
|
|
for i := int32(0); i < totalWeight; i++ {
|
|
positions[i] = i
|
|
}
|
|
|
|
// Fisher-Yates 洗牌(使用 crypto/rand 保证密码学安全)
|
|
for i := totalWeight - 1; i > 0; i-- {
|
|
randBig, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("随机数生成失败: %w", err)
|
|
}
|
|
j := int32(randBig.Int64())
|
|
positions[i], positions[j] = positions[j], positions[i]
|
|
}
|
|
|
|
// 分配位置给每个奖品
|
|
prizePosMap := make(map[int64][]int32)
|
|
posIdx := int32(0)
|
|
|
|
for _, p := range prizes {
|
|
prizePositions := make([]int32, p.Weight)
|
|
for i := int32(0); i < p.Weight; i++ {
|
|
prizePositions[i] = positions[posIdx]
|
|
posIdx++
|
|
}
|
|
|
|
// 必须排序,用于后续二分查找
|
|
sort.Slice(prizePositions, func(i, j int) bool {
|
|
return prizePositions[i] < prizePositions[j]
|
|
})
|
|
|
|
prizePosMap[p.ID] = prizePositions
|
|
}
|
|
|
|
state := &ActivityDiscreteState{
|
|
ActivityID: activityID,
|
|
TotalWeight: totalWeight,
|
|
PrizePosMap: prizePosMap,
|
|
LastDrawIndex: 0,
|
|
GeneratedAt: time.Now(),
|
|
}
|
|
|
|
return state, nil
|
|
}
|
|
|
|
// SelectPrizeByDiscrete 使用随机离散位置选择奖品
|
|
func SelectPrizeByDiscrete(state *ActivityDiscreteState, randValue int32) (*model.LivestreamPrizes, int32, error) {
|
|
if randValue >= state.TotalWeight {
|
|
return nil, 0, fmt.Errorf("随机值超出范围")
|
|
}
|
|
|
|
// 二分查找随机值属于哪个奖品的区间
|
|
var selectedPrizeID int64
|
|
|
|
for prizeID, positions := range state.PrizePosMap {
|
|
// 二分查找
|
|
idx := sort.Search(len(positions), func(i int) bool {
|
|
return positions[i] >= randValue
|
|
})
|
|
|
|
if idx < len(positions) && positions[idx] == randValue {
|
|
selectedPrizeID = prizeID
|
|
break
|
|
}
|
|
}
|
|
|
|
if selectedPrizeID == 0 {
|
|
// 如果没找到,这是不应该发生的
|
|
return nil, randValue, fmt.Errorf("未找到匹配的奖品")
|
|
}
|
|
|
|
// 找到对应的奖品信息
|
|
// 注意:这里需要从原始奖品列表中查找
|
|
return nil, randValue, fmt.Errorf("需要传递奖品列表进行查找")
|
|
}
|
|
|
|
// SelectPrizeByDiscreteWithList 使用随机离散位置选择奖品(带完整奖品列表)
|
|
func SelectPrizeByDiscreteWithList(state *ActivityDiscreteState, prizes []*model.LivestreamPrizes, randValue int32) (*model.LivestreamPrizes, int32, error) {
|
|
if randValue >= state.TotalWeight {
|
|
return nil, 0, fmt.Errorf("随机值超出范围")
|
|
}
|
|
|
|
// 创建奖品ID到对象的映射
|
|
prizeMap := make(map[int64]*model.LivestreamPrizes)
|
|
for _, p := range prizes {
|
|
prizeMap[p.ID] = p
|
|
}
|
|
|
|
// 查找随机值对应的奖品
|
|
for prizeID, positions := range state.PrizePosMap {
|
|
idx := sort.Search(len(positions), func(i int) bool {
|
|
return positions[i] >= randValue
|
|
})
|
|
|
|
if idx < len(positions) && positions[idx] == randValue {
|
|
if prize, ok := prizeMap[prizeID]; ok {
|
|
return prize, randValue, nil
|
|
}
|
|
return nil, randValue, fmt.Errorf("奖品ID %d 不在当前列表中", prizeID)
|
|
}
|
|
}
|
|
|
|
return nil, randValue, fmt.Errorf("随机值 %d 未匹配任何奖品", randValue)
|
|
}
|
|
|
|
// LogDrawEvent 记录抽奖事件(用于调试和分析)
|
|
func LogDrawEvent(s *service, activityID int64, drawIndex int32, prizeID int64, prizeName string, randValue int32, method string) {
|
|
s.logger.Debug("随机离散抽奖",
|
|
zap.Int64("activity_id", activityID),
|
|
zap.Int32("draw_index", drawIndex),
|
|
zap.Int64("prize_id", prizeID),
|
|
zap.String("prize_name", prizeName),
|
|
zap.Int32("rand_value", randValue),
|
|
zap.String("method", method),
|
|
)
|
|
}
|