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

225 lines
6.0 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (
"database/sql"
"fmt"
"log"
"os"
_ "github.com/go-sql-driver/mysql"
)
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'")
return
}
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal("连接数据库失败:", err)
}
defer db.Close()
if err := db.Ping(); err != nil {
log.Fatal("数据库连接测试失败:", err)
}
fmt.Println("========== 抽奖数据分析工具 ==========\n")
// 1. 查询最近的活动和期次
fmt.Println("【最近的活动期次】")
rows, err := db.Query(`
SELECT ai.id, ai.activity_id, a.name, ai.issue_number, ai.status
FROM activity_issues ai
LEFT JOIN activities a ON ai.activity_id = a.id
WHERE a.play_type = 'default'
ORDER BY ai.created_at DESC
LIMIT 5
`)
if err != nil {
log.Fatal("查询期次失败:", err)
}
defer rows.Close()
var issueIDs []int64
for rows.Next() {
var issueID, activityID int64
var activityName string
var issueNumber, status int32
if err := rows.Scan(&issueID, &activityID, &activityName, &issueNumber, &status); err != nil {
log.Fatal(err)
}
fmt.Printf("期次ID: %d, 活动: %s, 期号: %d, 状态: %d\n", issueID, activityName, issueNumber, status)
issueIDs = append(issueIDs, issueID)
}
if len(issueIDs) == 0 {
fmt.Println("没有找到活动期次")
return
}
// 选择第一个期次进行分析
issueID := issueIDs[0]
fmt.Printf("\n分析期次ID: %d\n\n", issueID)
// 2. 查询该期次的奖品权重配置
fmt.Println("【奖品权重配置】")
rows, err = db.Query(`
SELECT id, product_id, weight, quantity, level, is_boss
FROM activity_reward_settings
WHERE issue_id = ?
ORDER BY weight ASC
`, issueID)
if err != nil {
log.Fatal("查询奖品配置失败:", err)
}
defer rows.Close()
type Reward struct {
ID int64
ProductID int64
Weight int32
Quantity int32
Level int32
IsBoss int32
}
var rewards []Reward
var totalWeight int64
for rows.Next() {
var r Reward
if err := rows.Scan(&r.ID, &r.ProductID, &r.Weight, &r.Quantity, &r.Level, &r.IsBoss); err != nil {
log.Fatal(err)
}
rewards = append(rewards, r)
totalWeight += int64(r.Weight)
}
fmt.Printf("总权重: %d\n\n", totalWeight)
for _, r := range rewards {
prob := float64(r.Weight) / float64(totalWeight) * 100
fmt.Printf("奖品ID: %d, 权重: %6d, 概率: %6.3f%%, 数量: %d, 等级: %d, 是否大奖: %d\n",
r.ID, r.Weight, prob, r.Quantity, r.Level, r.IsBoss)
}
// 3. 查询该期次的中奖记录
fmt.Println("\n【中奖统计】")
rows, err = db.Query(`
SELECT reward_id, COUNT(*) as count
FROM activity_draw_logs
WHERE issue_id = ?
GROUP BY reward_id
ORDER BY count DESC
`, issueID)
if err != nil {
log.Fatal("查询中奖记录失败:", err)
}
defer rows.Close()
type DrawStat struct {
RewardID int64
Count int64
}
var drawStats []DrawStat
var totalDraws int64
for rows.Next() {
var ds DrawStat
if err := rows.Scan(&ds.RewardID, &ds.Count); err != nil {
log.Fatal(err)
}
drawStats = append(drawStats, ds)
totalDraws += ds.Count
}
fmt.Printf("总抽奖次数: %d\n\n", totalDraws)
// 创建奖品ID到权重的映射
rewardMap := make(map[int64]Reward)
for _, r := range rewards {
rewardMap[r.ID] = r
}
// 分析每个奖品的实际中奖率
fmt.Println("【实际中奖率分析】")
fmt.Printf("%-10s %-10s %-10s %-10s %-10s %-10s\n", "奖品ID", "权重", "理论概率", "实际次数", "实际概率", "偏差")
for _, ds := range drawStats {
reward, ok := rewardMap[ds.RewardID]
if !ok {
continue
}
theoryProb := float64(reward.Weight) / float64(totalWeight) * 100
actualProb := float64(ds.Count) / float64(totalDraws) * 100
diff := actualProb - theoryProb
fmt.Printf("%-10d %-10d %-10.3f%% %-10d %-10.3f%% %+10.3f%%\n",
ds.RewardID, reward.Weight, theoryProb, ds.Count, actualProb, diff)
}
// 4. 分析大奖出现频率
fmt.Println("\n【大奖分析】")
var bigPrizeCount int64
var bigPrizeWeight int64
for _, ds := range drawStats {
reward, ok := rewardMap[ds.RewardID]
if !ok {
continue
}
// 假设权重 <= 1000 的是大奖
if reward.Weight <= 1000 {
bigPrizeCount += ds.Count
bigPrizeWeight += int64(reward.Weight)
}
}
if bigPrizeWeight > 0 {
bigPrizeTheory := float64(bigPrizeWeight) / float64(totalWeight) * 100
bigPrizeActual := float64(bigPrizeCount) / float64(totalDraws) * 100
fmt.Printf("大奖总权重: %d\n", bigPrizeWeight)
fmt.Printf("大奖理论概率: %.3f%%\n", bigPrizeTheory)
fmt.Printf("大奖实际概率: %.3f%%\n", bigPrizeActual)
fmt.Printf("大奖出现次数: %d / %d\n", bigPrizeCount, totalDraws)
fmt.Printf("偏差: %+.3f%%\n", bigPrizeActual-bigPrizeTheory)
// 判断是否异常
if bigPrizeActual > bigPrizeTheory*2 {
fmt.Println("\n⚠ 警告:大奖实际概率是理论概率的 2 倍以上,可能存在问题!")
} else if bigPrizeActual > bigPrizeTheory*1.5 {
fmt.Println("\n⚠ 注意:大奖实际概率偏高,建议进一步调查")
} else {
fmt.Println("\n✅ 大奖概率在正常范围内")
}
}
// 5. 查询最近的中奖记录
fmt.Println("\n【最近 20 次中奖记录】")
rows, err = db.Query(`
SELECT id, user_id, reward_id, created_at
FROM activity_draw_logs
WHERE issue_id = ?
ORDER BY created_at DESC
LIMIT 20
`, issueID)
if err != nil {
log.Fatal("查询中奖记录失败:", err)
}
defer rows.Close()
for rows.Next() {
var logID, userID, rewardID int64
var createdAt string
if err := rows.Scan(&logID, &userID, &rewardID, &createdAt); err != nil {
log.Fatal(err)
}
reward, ok := rewardMap[rewardID]
isBigPrize := ""
if ok && reward.Weight <= 1000 {
isBigPrize = " [大奖]"
}
fmt.Printf("用户: %d, 奖品ID: %d, 时间: %s%s\n", userID, rewardID, createdAt, isBigPrize)
}
}