478 lines
12 KiB
Go

// 任务中心配置组合测试工具
// 功能:
// 1. 生成所有有效的任务配置组合到 MySQL 数据库
// 2. 模拟用户任务进度
// 3. 验证任务功能是否正常
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"time"
"bindbox-game/configs"
"bindbox-game/internal/repository/mysql"
tcmodel "bindbox-game/internal/repository/mysql/task_center"
"gorm.io/datatypes"
)
// ================================
// 常量定义
// ================================
const (
// 任务指标
MetricFirstOrder = "first_order"
MetricOrderCount = "order_count"
MetricOrderAmount = "order_amount"
MetricInviteCount = "invite_count"
// 操作符
OperatorGTE = ">="
OperatorEQ = "="
// 时间窗口
WindowDaily = "daily"
WindowWeekly = "weekly"
WindowMonthly = "monthly"
WindowLifetime = "lifetime"
// 奖励类型
RewardTypePoints = "points"
RewardTypeCoupon = "coupon"
RewardTypeItemCard = "item_card"
RewardTypeTitle = "title"
RewardTypeGameTicket = "game_ticket"
)
// TaskCombination 表示一种任务配置组合
type TaskCombination struct {
Name string
Metric string
Operator string
Threshold int64
Window string
RewardType string
}
// TestResult 测试结果
type TestResult struct {
Name string
Passed bool
Message string
}
// ================================
// 配置组合生成器
// ================================
// GenerateAllCombinations 生成所有有效的任务配置组合
func GenerateAllCombinations() []TaskCombination {
metrics := []struct {
name string
operators []string
threshold int64
}{
{MetricFirstOrder, []string{OperatorEQ}, 1},
{MetricOrderCount, []string{OperatorGTE, OperatorEQ}, 3},
{MetricOrderAmount, []string{OperatorGTE, OperatorEQ}, 10000},
{MetricInviteCount, []string{OperatorGTE, OperatorEQ}, 2},
}
windows := []string{WindowDaily, WindowWeekly, WindowMonthly, WindowLifetime}
rewards := []string{RewardTypePoints, RewardTypeCoupon, RewardTypeItemCard, RewardTypeTitle, RewardTypeGameTicket}
var combinations []TaskCombination
idx := 0
for _, m := range metrics {
for _, op := range m.operators {
for _, w := range windows {
for _, r := range rewards {
idx++
combinations = append(combinations, TaskCombination{
Name: fmt.Sprintf("测试任务%03d_%s_%s_%s", idx, m.name, w, r),
Metric: m.name,
Operator: op,
Threshold: m.threshold,
Window: w,
RewardType: r,
})
}
}
}
}
return combinations
}
// generateRewardPayload 根据奖励类型生成对应的 JSON payload
func generateRewardPayload(rewardType string) string {
switch rewardType {
case RewardTypePoints:
return `{"points": 100}`
case RewardTypeCoupon:
return `{"coupon_id": 1, "quantity": 1}`
case RewardTypeItemCard:
return `{"card_id": 1, "quantity": 1}`
case RewardTypeTitle:
return `{"title_id": 1}`
case RewardTypeGameTicket:
return `{"game_code": "minesweeper", "amount": 5}`
default:
return `{}`
}
}
// ================================
// 数据库操作
// ================================
// SeedAllCombinations 将所有配置组合写入数据库
func SeedAllCombinations(repo mysql.Repo, dryRun bool) error {
db := repo.GetDbW()
combos := GenerateAllCombinations()
fmt.Printf("准备生成 %d 个任务配置组合\n", len(combos))
if dryRun {
fmt.Println("【试运行模式】不会实际写入数据库")
for i, c := range combos {
fmt.Printf(" %3d. %s (指标=%s, 操作符=%s, 窗口=%s, 奖励=%s)\n",
i+1, c.Name, c.Metric, c.Operator, c.Window, c.RewardType)
}
return nil
}
// 开始事务
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 清理旧的测试数据
if err := tx.Where("name LIKE ?", "测试任务%").Delete(&tcmodel.Task{}).Error; err != nil {
tx.Rollback()
return fmt.Errorf("清理旧任务失败: %v", err)
}
fmt.Println("已清理旧的测试任务数据")
created := 0
for _, combo := range combos {
// 检查是否已存在
var exists tcmodel.Task
if err := tx.Where("name = ?", combo.Name).First(&exists).Error; err == nil {
fmt.Printf(" 跳过: %s (已存在)\n", combo.Name)
continue
}
// 插入任务
task := &tcmodel.Task{
Name: combo.Name,
Description: fmt.Sprintf("测试 %s + %s + %s + %s", combo.Metric, combo.Operator, combo.Window, combo.RewardType),
Status: 1,
Visibility: 1,
}
if err := tx.Create(task).Error; err != nil {
tx.Rollback()
return fmt.Errorf("插入任务失败: %v", err)
}
// 插入档位
tier := &tcmodel.TaskTier{
TaskID: task.ID,
Metric: combo.Metric,
Operator: combo.Operator,
Threshold: combo.Threshold,
Window: combo.Window,
Priority: 0,
}
if err := tx.Create(tier).Error; err != nil {
tx.Rollback()
return fmt.Errorf("插入档位失败: %v", err)
}
// 插入奖励
payload := generateRewardPayload(combo.RewardType)
reward := &tcmodel.TaskReward{
TaskID: task.ID,
TierID: tier.ID,
RewardType: combo.RewardType,
RewardPayload: datatypes.JSON(payload),
Quantity: 10,
}
if err := tx.Create(reward).Error; err != nil {
tx.Rollback()
return fmt.Errorf("插入奖励失败: %v", err)
}
created++
if created%10 == 0 {
fmt.Printf(" 已创建 %d 个任务...\n", created)
}
}
if err := tx.Commit().Error; err != nil {
return fmt.Errorf("提交事务失败: %v", err)
}
fmt.Printf("✅ 成功创建 %d 个任务配置组合\n", created)
return nil
}
// ================================
// 模拟用户任务
// ================================
// SimulateUserTask 模拟用户完成任务
func SimulateUserTask(repo mysql.Repo, userID int64, taskID int64) error {
db := repo.GetDbW()
// 查询任务和档位
var task tcmodel.Task
if err := db.Where("id = ?", taskID).First(&task).Error; err != nil {
return fmt.Errorf("任务不存在: %v", err)
}
var tier tcmodel.TaskTier
if err := db.Where("task_id = ?", taskID).First(&tier).Error; err != nil {
return fmt.Errorf("档位不存在: %v", err)
}
fmt.Printf("模拟任务: %s (指标=%s, 阈值=%d)\n", task.Name, tier.Metric, tier.Threshold)
// 创建或更新用户进度
progress := &tcmodel.UserTaskProgress{
UserID: userID,
TaskID: taskID,
ClaimedTiers: datatypes.JSON("[]"),
}
// 根据指标类型设置进度
switch tier.Metric {
case MetricFirstOrder:
progress.FirstOrder = 1
progress.OrderCount = 1
progress.OrderAmount = 10000
case MetricOrderCount:
progress.OrderCount = tier.Threshold
case MetricOrderAmount:
progress.OrderAmount = tier.Threshold
progress.OrderCount = 1
case MetricInviteCount:
progress.InviteCount = tier.Threshold
}
// Upsert
if err := db.Where("user_id = ? AND task_id = ?", userID, taskID).
Assign(progress).
FirstOrCreate(progress).Error; err != nil {
return fmt.Errorf("创建进度失败: %v", err)
}
fmt.Printf("✅ 用户 %d 的任务进度已更新: order_count=%d, order_amount=%d, invite_count=%d, first_order=%d\n",
userID, progress.OrderCount, progress.OrderAmount, progress.InviteCount, progress.FirstOrder)
return nil
}
// ================================
// 验证功能
// ================================
// VerifyAllConfigs 验证所有配置是否正确
func VerifyAllConfigs(repo mysql.Repo) []TestResult {
db := repo.GetDbR()
var results []TestResult
// 1. 检查任务数量
var taskCount int64
var sampleTasks []tcmodel.Task
db.Model(&tcmodel.Task{}).Where("name LIKE ?", "测试任务%").Count(&taskCount)
db.Model(&tcmodel.Task{}).Where("name LIKE ?", "测试任务%").Limit(5).Find(&sampleTasks)
var sampleMsg string
for _, t := range sampleTasks {
sampleMsg += fmt.Sprintf("[%d:%s] ", t.ID, t.Name)
}
results = append(results, TestResult{
Name: "任务数量检查",
Passed: taskCount > 0,
Message: fmt.Sprintf("找到 %d 个测试任务. 样本: %s", taskCount, sampleMsg),
})
// 2. 检查每种指标的覆盖
metrics := []string{MetricFirstOrder, MetricOrderCount, MetricOrderAmount, MetricInviteCount}
for _, m := range metrics {
var count int64
db.Model(&tcmodel.TaskTier{}).Where("metric = ?", m).Count(&count)
results = append(results, TestResult{
Name: fmt.Sprintf("指标覆盖: %s", m),
Passed: count > 0,
Message: fmt.Sprintf("找到 %d 个档位使用此指标", count),
})
}
// 3. 检查每种时间窗口的覆盖
windows := []string{WindowDaily, WindowWeekly, WindowMonthly, WindowLifetime}
for _, w := range windows {
var count int64
db.Model(&tcmodel.TaskTier{}).Where("window = ?", w).Count(&count)
results = append(results, TestResult{
Name: fmt.Sprintf("时间窗口覆盖: %s", w),
Passed: count > 0,
Message: fmt.Sprintf("找到 %d 个档位使用此时间窗口", count),
})
}
// 4. 检查每种奖励类型的覆盖
rewards := []string{RewardTypePoints, RewardTypeCoupon, RewardTypeItemCard, RewardTypeTitle, RewardTypeGameTicket}
for _, r := range rewards {
var count int64
db.Model(&tcmodel.TaskReward{}).Where("reward_type = ?", r).Count(&count)
results = append(results, TestResult{
Name: fmt.Sprintf("奖励类型覆盖: %s", r),
Passed: count > 0,
Message: fmt.Sprintf("找到 %d 个奖励使用此类型", count),
})
}
// 5. 检查奖励 payload 格式
var rewardList []tcmodel.TaskReward
db.Limit(20).Find(&rewardList)
for _, r := range rewardList {
var data map[string]interface{}
err := json.Unmarshal([]byte(r.RewardPayload), &data)
passed := err == nil
msg := "JSON 格式正确"
if err != nil {
msg = fmt.Sprintf("JSON 解析失败: %v", err)
}
results = append(results, TestResult{
Name: fmt.Sprintf("奖励Payload格式: ID=%d, Type=%s", r.ID, r.RewardType),
Passed: passed,
Message: msg,
})
}
return results
}
// PrintResults 打印测试结果
func PrintResults(results []TestResult) {
passed := 0
failed := 0
fmt.Println("\n========== 测试结果 ==========")
for _, r := range results {
status := "✅ PASS"
if !r.Passed {
status = "❌ FAIL"
failed++
} else {
passed++
}
fmt.Printf("%s | %s | %s\n", status, r.Name, r.Message)
}
fmt.Println("==============================")
fmt.Printf("总计: %d 通过, %d 失败\n", passed, failed)
}
// ================================
// 主程序
// ================================
func main() {
// 命令行参数
action := flag.String("action", "help", "操作类型: seed/simulate/verify/integration/invite-test/help")
dryRun := flag.Bool("dry-run", false, "试运行模式,不实际写入数据库")
userID := flag.Int64("user", 8888, "用户ID (用于 simulate 或 integration)")
taskID := flag.Int64("task", 0, "任务ID")
flag.Parse()
// 显示帮助
if *action == "help" {
fmt.Println(`
任务中心配置组合测试工具
用法:
go run main.go -action=<操作>
操作类型:
seed - 生成所有配置组合到数据库
simulate - 简单模拟用户进度 (仅修改进度表)
integration - 真实集成测试 (触发 OnOrderPaid, 验证全流程)
invite-test - 邀请全链路测试 (模拟邀请、下单、双端奖励发放)
verify - 验证配置是否正确
参数:
-dry-run - 试运行模式,不实际写入数据库
-user - 用户ID (默认: 8888)
-task - 任务ID
示例:
# 邀请全链路测试
go run main.go -action=invite-test
`)
return
}
// 初始化数据库连接
repo, err := mysql.New()
if err != nil {
log.Fatalf("连接数据库失败: %v", err)
}
cfg := configs.Get()
fmt.Printf("已连接到数据库: %s\n", cfg.MySQL.Write.Name)
fmt.Printf("时间: %s\n", time.Now().Format("2006-01-02 15:04:05"))
// 执行操作
switch *action {
case "seed":
if err := SeedAllCombinations(repo, *dryRun); err != nil {
log.Printf("生成配置失败: %v", err)
os.Exit(1)
}
case "simulate":
if *taskID == 0 {
fmt.Println("请指定任务ID: -task=<ID>")
os.Exit(1)
}
if err := SimulateUserTask(repo, *userID, *taskID); err != nil {
log.Printf("模拟失败: %v", err)
os.Exit(1)
}
case "integration":
// 确保用户存在
if err := ensureUserExists(repo, *userID, "测试用户"); err != nil {
log.Printf("预检用户失败: %v", err)
os.Exit(1)
}
if err := IntegrationTest(repo); err != nil {
log.Printf("集成测试失败: %v", err)
os.Exit(1)
}
case "invite-test":
if err := InviteAndTaskIntegrationTest(repo); err != nil {
log.Printf("邀请测试失败: %v", err)
os.Exit(1)
}
case "verify":
results := VerifyAllConfigs(repo)
PrintResults(results)
default:
fmt.Printf("未知操作: %s\n", *action)
os.Exit(1)
}
}