9933d34bdd
Closes HIF-3 Stage 1 骨架:7 张表迁移 + 门槛规则引擎 + 加权选奖 + 次数入账/消耗(幂等)+ 发奖 handler 抽象与注册表。 - 单测覆盖 75.5%(未覆盖行 = stub handler ErrNotImplemented,合理) - CI 全绿(构建/Vet/测试 + golangci-lint) - 骨架不接入真实业务,vpn_duration/commission handler 在 Dispatch 中返回 ErrNotImplemented;合并后线上零变更 架构师 review 通过,4 项决策已在 issue 上给出: 1. 邀请转化语义 = 首次付款激活 2. 佣金日志类型 = 新增 CommissionTypeLottery=339 3. 家庭组归属 = 穿透到 owner 4. 管理端 IP 白名单 = 不做(推到 nginx/ingress 层) 后续 PR B/C 补真实业务对接 + 用户 API + 后台 CRUD + 集成/并发/概率测试。
67 lines
1.5 KiB
Go
67 lines
1.5 KiB
Go
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
|
|
}
|