package lottery import ( "math/rand" "sync" "time" ) // weightedPicker 是 WeightedPicker 的默认实现。使用累计权重 O(log n) 二分选取。 // 注入的 rand.Source 允许测试固定种子。 type weightedPicker struct { mu sync.Mutex rng *rand.Rand } // NewWeightedPicker 返回默认加权选取器。传 seed=0 使用当前时间纳秒。 func NewWeightedPicker(seed int64) WeightedPicker { if seed == 0 { seed = time.Now().UnixNano() } return &weightedPicker{ rng: rand.New(rand.NewSource(seed)), } } // Pick 从 candidates 中返回一个索引。以下奖品不参与随机: // - 权重 <= 0 // - is_fallback=true 的保底奖(只在限量奖售罄时兜底发放,绝不能被随机抽中, // 否则真实奖品会被“谢谢参与”类保底项挤占) // // 累计权重为 0(无任何可抽奖品)返回 ErrEmptyPool。 // // 算法:累计权重线性扫描一次,取 [0, total) 随机数落到哪个区间。稳定、 // 不需要预分配,并对小池(<20 项)足够快。 func (p *weightedPicker) Pick(candidates []Prize) (int, error) { if len(candidates) == 0 { return 0, ErrEmptyPool } // eligible 判定:非保底 && 权重为正,才计入随机池。 eligible := func(c Prize) bool { return !c.IsFallback && c.Weight > 0 } var total int64 for _, c := range candidates { if eligible(c) { total += int64(c.Weight) } } if total == 0 { return 0, ErrEmptyPool } p.mu.Lock() roll := p.rng.Int63n(total) p.mu.Unlock() var cum int64 for i, c := range candidates { if !eligible(c) { continue } cum += int64(c.Weight) if roll < cum { return i, nil } } // 走到这里说明浮点/累加异常,回退到最后一个参与随机的项。 for i := len(candidates) - 1; i >= 0; i-- { if eligible(candidates[i]) { return i, nil } } return 0, ErrEmptyPool }