修复(#4): 抽奖发奖修正 — 保底奖不参与随机 + 无订阅时按套餐自动新建订阅
- weighted_picker: Pick 排除 is_fallback 保底奖,避免真实奖被"谢谢参与"挤占 - vpn_duration handler: 无活跃订阅且奖品配置 subscribe_id 时,按该套餐在抽奖 事务内新建订阅并发放时长;未配置则沿用旧的安全跳过 - 补充单测:picker 排除保底奖、vpn_duration 自动新建订阅、draw 端到端链路 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,34 @@ func TestWeightedPicker_DistributionCloseToWeights(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeightedPicker_ExcludesFallback(t *testing.T) {
|
||||
p := NewWeightedPicker(3)
|
||||
pool := []Prize{
|
||||
{Id: 1, Weight: 100, IsFallback: true}, // 保底奖:即便权重很高也绝不被随机抽中
|
||||
{Id: 2, Weight: 5, Type: PrizeTypeVPNDuration}, // 唯一可抽真实奖品
|
||||
}
|
||||
for i := 0; i < 300; i++ {
|
||||
idx, err := p.Pick(pool)
|
||||
if err != nil {
|
||||
t.Fatalf("Pick err: %v", err)
|
||||
}
|
||||
if idx != 1 {
|
||||
t.Fatalf("fallback prize must never be picked; expected idx 1, got %d", idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeightedPicker_AllFallbackYieldsEmptyPool(t *testing.T) {
|
||||
p := NewWeightedPicker(9)
|
||||
pool := []Prize{
|
||||
{Id: 1, Weight: 10, IsFallback: true},
|
||||
{Id: 2, Weight: 20, IsFallback: true},
|
||||
}
|
||||
if _, err := p.Pick(pool); err != ErrEmptyPool {
|
||||
t.Fatalf("expected ErrEmptyPool when only fallback prizes exist, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeightedPicker_DeterministicWithFixedSeed(t *testing.T) {
|
||||
pool := []Prize{
|
||||
{Id: 1, Weight: 1},
|
||||
|
||||
Reference in New Issue
Block a user