修复(#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:
@@ -23,8 +23,12 @@ func NewWeightedPicker(seed int64) WeightedPicker {
|
||||
}
|
||||
}
|
||||
|
||||
// Pick 从 candidates 中返回一个索引。权重为 0 的奖品不参与随机;累计权重
|
||||
// 为 0(如所有奖品 weight 都是 0)返回 ErrEmptyPool。
|
||||
// Pick 从 candidates 中返回一个索引。以下奖品不参与随机:
|
||||
// - 权重 <= 0
|
||||
// - is_fallback=true 的保底奖(只在限量奖售罄时兜底发放,绝不能被随机抽中,
|
||||
// 否则真实奖品会被“谢谢参与”类保底项挤占)
|
||||
//
|
||||
// 累计权重为 0(无任何可抽奖品)返回 ErrEmptyPool。
|
||||
//
|
||||
// 算法:累计权重线性扫描一次,取 [0, total) 随机数落到哪个区间。稳定、
|
||||
// 不需要预分配,并对小池(<20 项)足够快。
|
||||
@@ -32,9 +36,12 @@ 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 c.Weight > 0 {
|
||||
if eligible(c) {
|
||||
total += int64(c.Weight)
|
||||
}
|
||||
}
|
||||
@@ -48,7 +55,7 @@ func (p *weightedPicker) Pick(candidates []Prize) (int, error) {
|
||||
|
||||
var cum int64
|
||||
for i, c := range candidates {
|
||||
if c.Weight <= 0 {
|
||||
if !eligible(c) {
|
||||
continue
|
||||
}
|
||||
cum += int64(c.Weight)
|
||||
@@ -56,9 +63,9 @@ func (p *weightedPicker) Pick(candidates []Prize) (int, error) {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
// 走到这里说明浮点/累加异常,回退到最后一个非 0 权重项。
|
||||
// 走到这里说明浮点/累加异常,回退到最后一个参与随机的项。
|
||||
for i := len(candidates) - 1; i >= 0; i-- {
|
||||
if candidates[i].Weight > 0 {
|
||||
if eligible(candidates[i]) {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user