2026-02-03 17:44:02 +08:00

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),
)
}