2026-02-18 23:23:34 +08:00

273 lines
8.0 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 main
import (
"fmt"
"os"
_ "github.com/go-sql-driver/mysql"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type LivestreamPrize struct {
ID int64 `gorm:"column:id"`
Name string `gorm:"column:name"`
Weight int32 `gorm:"column:weight"`
Level int32 `gorm:"column:level"`
}
type LivestreamDrawLog struct {
ID int64 `gorm:"column:id"`
ActivityID int64 `gorm:"column:activity_id"`
PrizeID int64 `gorm:"column:prize_id"`
PrizeName string `gorm:"column:prize_name"`
Level int32 `gorm:"column:level"`
WeightsTotal int64 `gorm:"column:weights_total"`
RandValue int64 `gorm:"column:rand_value"`
}
func main() {
// 从环境变量读取数据库连接信息
dsn := os.Getenv("DB_DSN")
if dsn == "" {
fmt.Println("请设置环境变量 DB_DSN例如")
fmt.Println("export DB_DSN='user:password@tcp(host:port)/dev_game?charset=utf8mb4&parseTime=True&loc=Local'")
fmt.Println("\n或者直接运行")
fmt.Println("DB_DSN='user:password@tcp(host:port)/dev_game?charset=utf8mb4&parseTime=True&loc=Local' go run main.go")
return
}
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
fmt.Printf("连接数据库失败: %v\n", err)
return
}
fmt.Println("========== 直播间抽奖概率分析工具 ==========\n")
// 1. 查询最近的活动
fmt.Println("【最近的直播间活动】")
var activities []struct {
ID int64 `gorm:"column:id"`
Name string `gorm:"column:name"`
}
if err := db.Table("livestream_activities").
Order("id DESC").
Limit(10).
Find(&activities).Error; err != nil {
fmt.Printf("查询活动失败: %v\n", err)
return
}
if len(activities) == 0 {
fmt.Println("没有找到直播间活动")
return
}
for i, act := range activities {
fmt.Printf("%d. ID: %d, 名称: %s\n", i+1, act.ID, act.Name)
}
// 选择第一个活动进行分析
activityID := activities[0].ID
fmt.Printf("\n分析活动ID: %d (%s)\n\n", activityID, activities[0].Name)
// 2. 查询奖品配置
fmt.Println("【奖品权重配置】")
var prizes []LivestreamPrize
if err := db.Table("livestream_prizes").
Where("activity_id = ?", activityID).
Order("weight ASC").
Find(&prizes).Error; err != nil {
fmt.Printf("查询奖品失败: %v\n", err)
return
}
if len(prizes) == 0 {
fmt.Println("该活动没有配置奖品")
return
}
var totalWeight int64
for _, p := range prizes {
totalWeight += int64(p.Weight)
}
fmt.Printf("总权重: %d\n\n", totalWeight)
fmt.Printf("%-5s %-30s %-10s %-10s %-10s\n", "ID", "名称", "权重", "概率", "期望")
fmt.Println("------------------------------------------------------------------------------------")
for _, p := range prizes {
prob := float64(p.Weight) / float64(totalWeight) * 100
expected := int(float64(totalWeight) / float64(p.Weight))
fmt.Printf("%-5d %-30s %-10d %-10.3f%% 1/%-10d\n",
p.ID, p.Name, p.Weight, prob, expected)
}
// 3. 查询中奖记录
fmt.Println("\n【中奖统计】")
var drawLogs []LivestreamDrawLog
if err := db.Table("livestream_draw_logs").
Where("activity_id = ?", activityID).
Find(&drawLogs).Error; err != nil {
fmt.Printf("查询中奖记录失败: %v\n", err)
return
}
if len(drawLogs) == 0 {
fmt.Println("该活动还没有中奖记录")
return
}
fmt.Printf("总抽奖次数: %d\n\n", len(drawLogs))
// 统计每个奖品的中奖次数
prizeStats := make(map[int64]int)
for _, log := range drawLogs {
prizeStats[log.PrizeID]++
}
// 创建奖品ID到奖品的映射
prizeMap := make(map[int64]LivestreamPrize)
for _, p := range prizes {
prizeMap[p.ID] = p
}
// 分析每个奖品的实际中奖率
fmt.Println("【实际中奖率分析】")
fmt.Printf("%-5s %-30s %-10s %-10s %-10s %-10s %-10s\n",
"ID", "名称", "权重", "理论概率", "实际次数", "实际概率", "偏差")
fmt.Println("------------------------------------------------------------------------------------")
type PrizeStat struct {
Prize LivestreamPrize
Count int
TheoryProb float64
ActualProb float64
Diff float64
}
var stats []PrizeStat
for _, p := range prizes {
count := prizeStats[p.ID]
theoryProb := float64(p.Weight) / float64(totalWeight) * 100
actualProb := float64(count) / float64(len(drawLogs)) * 100
diff := actualProb - theoryProb
stats = append(stats, PrizeStat{
Prize: p,
Count: count,
TheoryProb: theoryProb,
ActualProb: actualProb,
Diff: diff,
})
fmt.Printf("%-5d %-30s %-10d %-10.3f%% %-10d %-10.3f%% %+10.3f%%\n",
p.ID, p.Name, p.Weight, theoryProb, count, actualProb, diff)
}
// 4. 分析大奖出现频率
fmt.Println("\n【大奖分析】")
var bigPrizeCount int
var bigPrizeWeight int64
var bigPrizeNames []string
// 假设权重 <= 1000 的是大奖
for _, stat := range stats {
if stat.Prize.Weight <= 1000 {
bigPrizeCount += stat.Count
bigPrizeWeight += int64(stat.Prize.Weight)
if stat.Count > 0 {
bigPrizeNames = append(bigPrizeNames, fmt.Sprintf("%s(%d次)", stat.Prize.Name, stat.Count))
}
}
}
if bigPrizeWeight > 0 {
bigPrizeTheory := float64(bigPrizeWeight) / float64(totalWeight) * 100
bigPrizeActual := float64(bigPrizeCount) / float64(len(drawLogs)) * 100
fmt.Printf("大奖定义: 权重 <= 1000\n")
fmt.Printf("大奖总权重: %d\n", bigPrizeWeight)
fmt.Printf("大奖理论概率: %.3f%% (1/%d)\n", bigPrizeTheory, int(float64(totalWeight)/float64(bigPrizeWeight)))
fmt.Printf("大奖实际概率: %.3f%%\n", bigPrizeActual)
fmt.Printf("大奖出现次数: %d / %d\n", bigPrizeCount, len(drawLogs))
fmt.Printf("偏差: %+.3f%%\n", bigPrizeActual-bigPrizeTheory)
if len(bigPrizeNames) > 0 {
fmt.Printf("\n中奖明细: %v\n", bigPrizeNames)
}
// 判断是否异常
fmt.Println()
if bigPrizeActual > bigPrizeTheory*3 {
fmt.Println("🔴 严重警告:大奖实际概率是理论概率的 3 倍以上!")
fmt.Println(" 可能原因:")
fmt.Println(" 1. 权重配置错误")
fmt.Println(" 2. 随机数生成有问题")
fmt.Println(" 3. 缓存未更新(修改权重后未重新生成随机位置)")
} else if bigPrizeActual > bigPrizeTheory*2 {
fmt.Println("🟠 警告:大奖实际概率是理论概率的 2 倍以上!")
fmt.Println(" 建议检查权重配置和随机位置生成")
} else if bigPrizeActual > bigPrizeTheory*1.5 {
fmt.Println("🟡 注意:大奖实际概率偏高")
fmt.Println(" 可能是统计波动,建议继续观察")
} else {
fmt.Println("✅ 大奖概率在正常范围内")
}
}
// 5. 查询最近的中奖记录
fmt.Println("\n【最近 20 次中奖记录】")
var recentLogs []LivestreamDrawLog
if err := db.Table("livestream_draw_logs").
Where("activity_id = ?", activityID).
Order("id DESC").
Limit(20).
Find(&recentLogs).Error; err != nil {
fmt.Printf("查询中奖记录失败: %v\n", err)
return
}
for _, log := range recentLogs {
prize, ok := prizeMap[log.PrizeID]
isBigPrize := ""
if ok && prize.Weight <= 1000 {
isBigPrize = " [大奖]"
}
fmt.Printf("ID: %d, 奖品: %s, 随机值: %d/%d%s\n",
log.ID, log.PrizeName, log.RandValue, log.WeightsTotal, isBigPrize)
}
// 6. 随机值分布分析
fmt.Println("\n【随机值分布分析】")
if len(drawLogs) > 0 {
// 检查随机值是否均匀分布
bucketCount := 10
buckets := make([]int, bucketCount)
bucketSize := totalWeight / int64(bucketCount)
for _, log := range drawLogs {
if log.WeightsTotal > 0 {
bucket := int(log.RandValue / bucketSize)
if bucket >= bucketCount {
bucket = bucketCount - 1
}
buckets[bucket]++
}
}
fmt.Printf("将随机值范围 [0, %d) 分为 %d 个区间:\n", totalWeight, bucketCount)
expectedPerBucket := float64(len(drawLogs)) / float64(bucketCount)
for i, count := range buckets {
start := int64(i) * bucketSize
end := start + bucketSize
if i == bucketCount-1 {
end = totalWeight
}
deviation := (float64(count) - expectedPerBucket) / expectedPerBucket * 100
fmt.Printf("区间 [%6d, %6d): %4d 次 (期望: %.1f, 偏差: %+.1f%%)\n",
start, end, count, expectedPerBucket, deviation)
}
}
}