273 lines
8.0 KiB
Go
Executable File
273 lines
8.0 KiB
Go
Executable File
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)
|
||
}
|
||
}
|
||
}
|