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 的奖品不参与随机;累计权重 // 为 0(如所有奖品 weight 都是 0)返回 ErrEmptyPool。 // // 算法:累计权重线性扫描一次,取 [0, total) 随机数落到哪个区间。稳定、 // 不需要预分配,并对小池(<20 项)足够快。 func (p *weightedPicker) Pick(candidates []Prize) (int, error) { if len(candidates) == 0 { return 0, ErrEmptyPool } var total int64 for _, c := range candidates { if c.Weight > 0 { 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 c.Weight <= 0 { continue } cum += int64(c.Weight) if roll < cum { return i, nil } } // 走到这里说明浮点/累加异常,回退到最后一个非 0 权重项。 for i := len(candidates) - 1; i >= 0; i-- { if candidates[i].Weight > 0 { return i, nil } } return 0, ErrEmptyPool }