// 任务中心配置组合测试工具 // 功能: // 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=") 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) } }