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