bindbox-game/internal/service/livestream/draw_integration_test.go
2026-02-03 17:44:02 +08:00

242 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package livestream
import (
"testing"
"bindbox-game/internal/repository/mysql/model"
)
// TestDrawWithDiscreteIntegration 集成测试:完整的随机离散抽奖流程
func TestDrawWithDiscreteIntegration(t *testing.T) {
// 创建service实例简化测试不依赖真实数据库
s := &service{
discreteCache: NewActivityDiscreteCache(),
}
// 1. 配置奖品
prizes := []*model.LivestreamPrizes{
{ID: 58, Name: "Nu高达", Weight: 100, Remaining: -1},
{ID: 59, Name: "NT高达", Weight: 100, Remaining: -1},
{ID: 60, Name: "魔礼青", Weight: 100, Remaining: -1},
{ID: 61, Name: "维达尔", Weight: 100, Remaining: -1},
{ID: 62, Name: "玉衡星6号", Weight: 400, Remaining: -1},
{ID: 63, Name: "马克兔", Weight: 600, Remaining: -1},
{ID: 64, Name: "00高达", Weight: 700, Remaining: -1},
{ID: 65, Name: "SD随机款", Weight: 1100, Remaining: -1},
{ID: 66, Name: "兰博基尼", Weight: 3000, Remaining: -1},
{ID: 67, Name: "高达徽章", Weight: 28000, Remaining: -1},
{ID: 68, Name: "冰箱贴", Weight: 65800, Remaining: -1},
}
activityID := int64(1)
// 2. 生成随机离散位置
state, err := GenerateDiscretePositions(activityID, prizes)
if err != nil {
t.Fatalf("生成随机离散位置失败: %v", err)
}
// 3. 设置到缓存
s.discreteCache.Set(state)
t.Logf("总权重: %d", state.TotalWeight)
// 4. 模拟多次抽奖
simulateCount := 1000
results := make(map[int64]int)
for i := 0; i < simulateCount; i++ {
// 模拟随机值生成实际用crypto/rand
randValue := int32(i % int(state.TotalWeight))
// 查找对应的奖品
prizeMap := make(map[int64]*model.LivestreamPrizes)
for _, p := range prizes {
prizeMap[p.ID] = p
}
// 二分查找
found := false
for prizeID, positions := range state.PrizePosMap {
for _, pos := range positions {
if pos == randValue {
results[prizeID]++
found = true
break
}
}
if found {
break
}
}
}
// 5. 统计结果
t.Log("\n========== 随机离散抽奖统计结果 ==========")
t.Logf("模拟次数: %d", simulateCount)
t.Log("")
totalWeight := int32(0)
for _, p := range prizes {
totalWeight += p.Weight
}
totalCount := 0
for _, p := range prizes {
count := results[p.ID]
totalCount += count
theoryProb := float64(p.Weight) / float64(totalWeight) * 100
actualProb := float64(count) / float64(simulateCount) * 100
diff := actualProb - theoryProb
t.Logf("%-20s 权重:%6d 理论:%6.2f%% 实际:%6.2f%% 偏差:%+6.2f%% 次数:%5d",
p.Name, p.Weight, theoryProb, actualProb, diff, count)
}
t.Logf("\n总计中奖: %d", totalCount)
// 6. 验证概率偏差允许的误差范围±2%
t.Run("概率偏差检查", func(t *testing.T) {
for _, p := range prizes {
count := results[p.ID]
theoryProb := float64(p.Weight) / float64(totalWeight) * 100
actualProb := float64(count) / float64(simulateCount) * 100
diff := actualProb - theoryProb
// 大奖权重100允许更大偏差
threshold := 2.0
if p.Weight <= 100 {
threshold = 5.0 // 大奖允许5%偏差
}
if diff > threshold || diff < -threshold {
t.Errorf("%s 概率偏差过大: 理论%.2f%% 实际%.2f%% 偏差%.2f%% (阈值±%.1f%%)",
p.Name, theoryProb, actualProb, diff, threshold)
}
}
})
}
// TestDrawVsOriginal 对比新旧方案
func TestDrawVsOriginal(t *testing.T) {
prizes := []*model.LivestreamPrizes{
{ID: 1, Name: "大奖", Weight: 100, Remaining: -1},
{ID: 2, Name: "中奖", Weight: 3000, Remaining: -1},
{ID: 3, Name: "小奖", Weight: 3000, Remaining: -1},
{ID: 4, Name: "冰箱贴", Weight: 65800, Remaining: -1},
}
activityID := int64(999)
// 原始连续区间方案
t.Log("\n========== 方案对比 ==========")
t.Log("【原始连续区间方案】")
t.Log("大奖区间: [0, 99] (0.15%)")
t.Log("中奖区间: [100, 3099] (4.48%)")
t.Log("小奖区间: [3100, 6099] (4.48%)")
t.Log("冰箱贴区间: [6100, 71899] (97.02%)")
t.Log("")
// 随机离散方案
state, err := GenerateDiscretePositions(activityID, prizes)
if err != nil {
t.Fatalf("生成失败: %v", err)
}
t.Log("【随机离散方案】")
for _, p := range prizes {
positions := state.PrizePosMap[p.ID]
t.Logf("%s: %d个随机位置", p.Name, len(positions))
}
// 位置分布示例
t.Log("\n【位置分布示例】")
if pos, ok := state.PrizePosMap[1]; ok && len(pos) > 0 {
sample := make([]int32, 0, 10)
for i := 0; i < 10 && i < len(pos); i++ {
sample = append(sample, pos[i])
}
t.Logf("大奖前10个位置: %v", sample)
}
if pos, ok := state.PrizePosMap[4]; ok && len(pos) > 0 {
sample := make([]int32, 0, 10)
for i := 0; i < 10 && i < len(pos); i++ {
sample = append(sample, pos[i])
}
t.Logf("冰箱贴前10个位置: %v", sample)
}
}
// TestCacheOperations 测试缓存操作
func TestCacheOperations(t *testing.T) {
cache := NewActivityDiscreteCache()
// 创建测试状态
state := &ActivityDiscreteState{
ActivityID: 1,
TotalWeight: 1000,
PrizePosMap: map[int64][]int32{
1: {0, 10, 20, 30, 40},
2: {5, 15, 25, 35, 45},
},
}
// 测试Set
cache.Set(state)
// 测试Get
got, ok := cache.Get(1)
if !ok {
t.Fatal("应该能找到缓存")
}
if got.ActivityID != 1 {
t.Errorf("ActivityID不匹配: 期望1, 实际%d", got.ActivityID)
}
// 测试Delete
cache.Delete(1)
_, ok = cache.Get(1)
if ok {
t.Error("删除后应该找不到")
}
t.Log("缓存操作测试通过")
}
// TestSelectPrizeByDiscreteWithList 测试带奖品列表的选择
func TestSelectPrizeByDiscreteWithList(t *testing.T) {
prizes := []*model.LivestreamPrizes{
{ID: 1, Name: "大奖", Weight: 100, Remaining: -1},
{ID: 2, Name: "小奖", Weight: 3000, Remaining: -1},
}
state, err := GenerateDiscretePositions(1, prizes)
if err != nil {
t.Fatalf("生成失败: %v", err)
}
// 测试有效随机值
for randValue := int32(0); randValue < state.TotalWeight; randValue++ {
prize, _, err := SelectPrizeByDiscreteWithList(state, prizes, randValue)
if err != nil {
t.Errorf("随机值%d查找失败: %v", randValue, err)
continue
}
if prize == nil {
t.Errorf("随机值%d返回空奖品", randValue)
continue
}
}
// 测试边界值
t.Run("边界值测试", func(t *testing.T) {
// 超出范围
_, _, err := SelectPrizeByDiscreteWithList(state, prizes, state.TotalWeight)
if err == nil {
t.Error("超出范围应该返回错误")
}
})
t.Logf("成功测试了 %d 个随机值", state.TotalWeight)
}