169 lines
4.6 KiB
Markdown
169 lines
4.6 KiB
Markdown
# 直播间抽奖系统 - 随机离散方案
|
||
|
||
## 概述
|
||
|
||
此方案将原来的"连续区间权重"改为"随机离散位置",解决大奖连续出现或长时间不出的问题。
|
||
|
||
## 核心变化
|
||
|
||
### 原方案(连续区间)
|
||
```
|
||
总权重: 99000
|
||
|
||
冰箱贴 (65800): [0 - 65799] ← 占据大片连续区间
|
||
高达徽章 (28000): [65800 - 93799]
|
||
兰博基尼 (3000): [93800 - 96799]
|
||
...
|
||
大奖 (100): [98900 - 98999] ← 仅占100个连续数字
|
||
|
||
问题:如果随机数生成有偏,整个区间都会受影响
|
||
```
|
||
|
||
### 新方案(随机离散)
|
||
```
|
||
1. 创建位置池 [0 - 98999]
|
||
2. 使用Fisher-Yates算法打乱位置顺序
|
||
3. 按权重分配位置给每个奖品
|
||
|
||
冰箱贴获得65800个**分散**的位置
|
||
大奖获得100个**分散**的位置
|
||
|
||
优势:随机数生成即使有偏,也不会导致某个奖品连续出现
|
||
```
|
||
|
||
## 实现原理
|
||
|
||
### 1. 随机位置生成
|
||
|
||
```go
|
||
// Fisher-Yates洗牌算法
|
||
positions := [0, 1, 2, 3, ..., 98999]
|
||
for i := len(positions)-1; i > 0; i-- {
|
||
j = random(0, i)
|
||
swap(positions[i], positions[j])
|
||
}
|
||
```
|
||
|
||
### 2. 位置分配
|
||
|
||
```go
|
||
prizePosMap = {
|
||
冰箱贴ID: [打乱后的位置...], // 65800个
|
||
徽章ID: [打乱后的位置...], // 28000个
|
||
...
|
||
}
|
||
```
|
||
|
||
### 3. 抽奖查找
|
||
|
||
```go
|
||
randValue = crypto/rand(0, totalWeight)
|
||
|
||
// 二分查找
|
||
for each prize in prizePosMap:
|
||
if randValue in prize.positions:
|
||
return prize
|
||
```
|
||
|
||
## 代码结构
|
||
|
||
```
|
||
internal/service/livestream/
|
||
├── livestream.go # 主服务文件(已集成随机离散)
|
||
├── discrete_random.go # 随机离散核心逻辑
|
||
├── discrete_random_test.go # 单元测试
|
||
└── draw_integration_test.go # 集成测试
|
||
```
|
||
|
||
## 使用方法
|
||
|
||
### 1. 配置活动奖品
|
||
|
||
```go
|
||
// 原有接口,无需改动
|
||
s.CreatePrizes(ctx, activityID, prizes)
|
||
|
||
// 系统会自动生成随机离散位置
|
||
```
|
||
|
||
### 2. 执行抽奖
|
||
|
||
```go
|
||
// Draw() 方法已自动使用随机离散
|
||
result, err := s.Draw(ctx, input)
|
||
```
|
||
|
||
### 3. 调试日志
|
||
|
||
```
|
||
随机离散位置已生成
|
||
activity_id: 1
|
||
total_weight: 99000
|
||
prize_count: 11
|
||
```
|
||
|
||
## 测试验证
|
||
|
||
### 运行测试
|
||
|
||
```bash
|
||
cd /Users/win/aicode/bindbox/bindbox_game
|
||
|
||
# 单元测试
|
||
go test -v ./internal/service/livestream/ -run TestDiscreteRandomDistribution
|
||
|
||
# 集成测试
|
||
go test -v ./internal/service/livestream/ -run TestDrawWithDiscreteIntegration
|
||
|
||
# 性能测试
|
||
go test -bench=BenchmarkDiscreteGeneration ./internal/service/livestream/
|
||
```
|
||
|
||
### 预期输出
|
||
|
||
```
|
||
========== 随机离散抽奖统计结果 ==========
|
||
模拟次数: 1000
|
||
|
||
Nu高达 权重: 100 理论: 0.10% 实际: 0.10% 偏差: +0.00% 次数: 1
|
||
NT高达 权重: 100 理论: 0.10% 实际: 0.10% 偏差: +0.00% 次数: 1
|
||
魔礼青 权重: 100 理论: 0.10% 实际: 0.10% 偏差: +0.00% 次数: 1
|
||
维达尔 权重: 100 理论: 0.10% 实际: 0.20% 偏差: +0.10% 次数: 2
|
||
玉衡星6号 权重: 400 理论: 0.40% 实际: 0.40% 偏差: +0.00% 次数: 4
|
||
马克兔 权重: 600 理论: 0.61% 实际: 0.70% 偏差: +0.09% 次数: 7
|
||
00高达 权重: 700 理论: 0.71% 实际: 0.60% 偏差: -0.11% 次数: 6
|
||
SD随机款 权重: 1100 理论: 1.11% 实际: 1.10% 偏差: -0.01% 次数: 11
|
||
兰博基尼 权重: 3000 理论: 3.03% 实际: 2.90% 偏差: -0.13% 次数: 29
|
||
高达徽章 权重: 28000 理论: 28.28% 实际: 28.20% 偏差: -0.08% 次数: 282
|
||
冰箱贴 权重: 65800 理论: 66.47% 实际: 66.50% 偏差: +0.03% 次数: 665
|
||
```
|
||
|
||
## 性能分析
|
||
|
||
| 操作 | 时间复杂度 | 说明 |
|
||
|------|-----------|------|
|
||
| 生成位置 | O(n) | 一次生成,缓存复用 |
|
||
| 查找奖品 | O(m × log(k)) | m=奖品数量, k=平均位置数 |
|
||
| 内存占用 | O(totalWeight × 4字节) | 99000个位置≈390KB |
|
||
|
||
## 注意事项
|
||
|
||
1. **缓存失效**:奖品配置变更后,缓存会自动重新生成
|
||
2. **并发安全**:使用RWMutex保护缓存读写
|
||
3. **位置唯一**:Fisher-Yates算法保证每个位置只属于一个奖品
|
||
4. **密码学安全**:使用crypto/rand保证随机性
|
||
|
||
## 对比分析
|
||
|
||
| 特性 | 连续区间 | 随机离散 |
|
||
|------|----------|----------|
|
||
| 实现复杂度 | ⭐ 简单 | ⭐⭐⭐ 复杂 |
|
||
| 概率精度 | ✅ 准确 | ✅ 准确 |
|
||
| 分布均匀性 | ⚠️ 可能偏斜 | ✅ 均匀 |
|
||
| 防连续中奖 | ❌ 无 | ✅ 天然防聚集 |
|
||
| 缓存需求 | 无 | 需要 |
|
||
| 内存占用 | 低 | 中等(~400KB) |
|
||
|
||
## 结论
|
||
|
||
随机离散方案更适合需要**分布均匀、防连续中奖**的场景,虽然实现稍复杂,但能显著提升用户体验。 |