Files
hi-server/internal/model/lottery/weighted_picker_test.go
T
shanshanzhong147 9933d34bdd 新功能(#3): 抽奖活动 Stage 1 骨架(DB + 规则引擎 + Handler 抽象)
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 + 集成/并发/概率测试。
2026-07-08 20:05:05 -07:00

77 lines
1.8 KiB
Go

package lottery
import (
"testing"
)
func TestWeightedPicker_EmptyPool(t *testing.T) {
p := NewWeightedPicker(42)
if _, err := p.Pick(nil); err != ErrEmptyPool {
t.Fatalf("expected ErrEmptyPool for nil pool, got %v", err)
}
if _, err := p.Pick([]Prize{{Weight: 0}, {Weight: 0}}); err != ErrEmptyPool {
t.Fatalf("expected ErrEmptyPool when all weights are 0, got %v", err)
}
}
func TestWeightedPicker_SkipsZeroWeight(t *testing.T) {
p := NewWeightedPicker(1)
pool := []Prize{
{Id: 1, Weight: 0}, // 不参与
{Id: 2, Weight: 100}, // 独占权重
{Id: 3, Weight: 0}, // 不参与
}
for i := 0; i < 200; i++ {
idx, err := p.Pick(pool)
if err != nil {
t.Fatalf("Pick err: %v", err)
}
if idx != 1 {
t.Fatalf("expected idx 1 (only positive weight), got %d", idx)
}
}
}
func TestWeightedPicker_DistributionCloseToWeights(t *testing.T) {
p := NewWeightedPicker(2026)
pool := []Prize{
{Id: 10, Weight: 10}, // 10/60 ≈ 16.67%
{Id: 20, Weight: 20}, // 20/60 ≈ 33.33%
{Id: 30, Weight: 30}, // 30/60 = 50%
}
const trials = 60000
counts := make(map[int]int)
for i := 0; i < trials; i++ {
idx, err := p.Pick(pool)
if err != nil {
t.Fatalf("Pick err: %v", err)
}
counts[idx]++
}
// 允许 ±2% 偏差
expect := []float64{10.0 / 60, 20.0 / 60, 30.0 / 60}
for i, e := range expect {
got := float64(counts[i]) / float64(trials)
if got < e-0.02 || got > e+0.02 {
t.Fatalf("prize %d: expected %.4f (±0.02), got %.4f", i, e, got)
}
}
}
func TestWeightedPicker_DeterministicWithFixedSeed(t *testing.T) {
pool := []Prize{
{Id: 1, Weight: 1},
{Id: 2, Weight: 1},
{Id: 3, Weight: 1},
}
a := NewWeightedPicker(7)
b := NewWeightedPicker(7)
for i := 0; i < 20; i++ {
ai, _ := a.Pick(pool)
bi, _ := b.Pick(pool)
if ai != bi {
t.Fatalf("iter %d: seed 7 diverged: a=%d b=%d", i, ai, bi)
}
}
}