package taskcenter import ( "context" "encoding/json" "fmt" "testing" "time" "bindbox-game/internal/pkg/logger" "bindbox-game/internal/repository/mysql" tcmodel "bindbox-game/internal/repository/mysql/task_center" "gorm.io/datatypes" "gorm.io/gorm" ) func ensureExtraTablesForServiceTest(t *testing.T, db *gorm.DB) { if !db.Migrator().HasTable("orders") { if err := db.Exec(`CREATE TABLE orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, status INTEGER NOT NULL DEFAULT 1, source_type INTEGER NOT NULL DEFAULT 0, total_amount INTEGER NOT NULL DEFAULT 0, actual_amount INTEGER NOT NULL DEFAULT 0, remark TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, deleted_at DATETIME );`).Error; err != nil { t.Fatalf("创建 orders 表失败: %v", err) } } if !db.Migrator().HasTable("activity_draw_logs") { if err := db.Exec(`CREATE TABLE activity_draw_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL, issue_id INTEGER NOT NULL );`).Error; err != nil { t.Fatalf("创建 activity_draw_logs 表失败: %v", err) } } if !db.Migrator().HasTable("activity_issues") { if err := db.Exec(`CREATE TABLE activity_issues ( id INTEGER PRIMARY KEY AUTOINCREMENT, activity_id INTEGER NOT NULL );`).Error; err != nil { t.Fatalf("创建 activity_issues 表失败: %v", err) } } if !db.Migrator().HasTable("activities") { if err := db.Exec(`CREATE TABLE activities ( id INTEGER PRIMARY KEY AUTOINCREMENT, price_draw INTEGER NOT NULL DEFAULT 0 );`).Error; err != nil { t.Fatalf("创建 activities 表失败: %v", err) } } if !db.Migrator().HasTable("user_invites") { if err := db.Exec(`CREATE TABLE user_invites ( id INTEGER PRIMARY KEY AUTOINCREMENT, inviter_id INTEGER NOT NULL, invitee_id INTEGER NOT NULL, accumulated_amount INTEGER NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, deleted_at DATETIME );`).Error; err != nil { t.Fatalf("创建 user_invites 表失败: %v", err) } } } func TestGetUserProgress_TimeWindow_Integration(t *testing.T) { repo, err := mysql.NewSQLiteRepoForTest() if err != nil { t.Fatalf("创建 repo 失败: %v", err) } db := repo.GetDbW() initTestTables(t, db) ensureExtraTablesForServiceTest(t, db) testLogger, err := logger.NewCustomLogger() if err != nil { t.Fatalf("创建 logger 失败: %v", err) } svc := New(testLogger, repo, nil, nil, nil) now := time.Now() taskStart := now.Add(-200 * 24 * time.Hour) taskEnd := now.Add(200 * 24 * time.Hour) // 创建一个具有任务有效期的任务 task := &tcmodel.Task{ Name: "时效性测试任务", Description: "测试各档位时效隔离", Status: 1, Visibility: 1, StartTime: &taskStart, EndTime: &taskEnd, } if err := db.Create(task).Error; err != nil { t.Fatalf("创建任务失败: %v", err) } db.Exec("INSERT INTO activities (id, price_draw) VALUES (1, 100)") db.Exec("INSERT INTO activity_issues (id, activity_id) VALUES (1, 1)") windows := []string{WindowDaily, WindowWeekly, WindowMonthly, WindowActivityPeriod, WindowLifetime} tierIDMap := make(map[string]int64) for _, w := range windows { tier := &tcmodel.TaskTier{ TaskID: task.ID, Metric: MetricOrderCount, Operator: OperatorGTE, Threshold: 1, Window: w, ActivityID: 0, } if err := db.Create(tier).Error; err != nil { t.Fatalf("创建档位失败: %v", err) } tierIDMap[w] = tier.ID } userID := int64(888) // 插入三笔订单与邀请,处于不同时间段 o1Time := now.Format(time.DateTime) db.Exec("INSERT INTO orders (id, user_id, status, source_type, total_amount, created_at) VALUES (101, ?, 2, 0, 100, ?)", userID, o1Time) db.Exec("INSERT INTO activity_draw_logs (order_id, issue_id) VALUES (101, 1)") db.Exec("INSERT INTO user_invites (inviter_id, invitee_id, created_at) VALUES (?, 901, ?)", userID, o1Time) o2Time := now.AddDate(0, -2, 0).Format(time.DateTime) db.Exec("INSERT INTO orders (id, user_id, status, source_type, total_amount, created_at) VALUES (102, ?, 2, 0, 100, ?)", userID, o2Time) db.Exec("INSERT INTO activity_draw_logs (order_id, issue_id) VALUES (102, 1)") db.Exec("INSERT INTO user_invites (inviter_id, invitee_id, created_at) VALUES (?, 902, ?)", userID, o2Time) o3Time := now.AddDate(-1, 0, 0).Format(time.DateTime) db.Exec("INSERT INTO orders (id, user_id, status, source_type, total_amount, created_at) VALUES (103, ?, 2, 0, 100, ?)", userID, o3Time) db.Exec("INSERT INTO activity_draw_logs (order_id, issue_id) VALUES (103, 1)") db.Exec("INSERT INTO user_invites (inviter_id, invitee_id, created_at) VALUES (?, 903, ?)", userID, o3Time) // 调用统计 progress, err := svc.GetUserProgress(context.Background(), userID, task.ID) if err != nil { t.Fatalf("获取进度失败: %v", err) } // 验证各 Tier 的统计数据符合预期 for w, tid := range tierIDMap { tp, ok := progress.TierProgressMap[tid] if !ok { t.Errorf("缺少 %s 的进度", w) continue } var expectedCount int64 switch w { case WindowDaily, WindowWeekly, WindowMonthly: expectedCount = 1 case WindowActivityPeriod, WindowLifetime: // CRITICAL FIX: lifetime 现在受任务时间约束 // taskStart = now - 200天,o2Time = now - 60天 (在范围内),o3Time = now - 365天 (超出范围) expectedCount = 2 // O1, O2 } if tp.OrderCount != expectedCount { t.Errorf("[%s] OrderCount 不符: Expected %d, Got %d", w, expectedCount, tp.OrderCount) } else { t.Logf("[%s] OrderCount 验证成功: %d", w, tp.OrderCount) } if tp.InviteCount != expectedCount { t.Errorf("[%s] InviteCount 不符: Expected %d, Got %d", w, expectedCount, tp.InviteCount) } else { t.Logf("[%s] InviteCount 验证成功: %d", w, tp.InviteCount) } } } func TestUpsertTaskRewards_AllowsMultipleRewardsSameType(t *testing.T) { repo, err := mysql.NewSQLiteRepoForTest() if err != nil { t.Fatalf("创建 repo 失败: %v", err) } db := repo.GetDbW() initTestTables(t, db) svc := New(nil, repo, nil, nil, nil) task := &tcmodel.Task{Name: "奖励重入", Description: "测试奖励更新", Status: 1, Visibility: 1} if err := db.Create(task).Error; err != nil { t.Fatalf("创建任务失败: %v", err) } tier := &tcmodel.TaskTier{ TaskID: task.ID, Metric: MetricOrderCount, Operator: OperatorGTE, Threshold: 1, Window: WindowLifetime, } if err := db.Create(tier).Error; err != nil { t.Fatalf("创建档位失败: %v", err) } initialRewards := []TaskRewardInput{ {TierID: tier.ID, RewardType: RewardTypeCoupon, RewardPayload: datatypes.JSON([]byte(`{"coupon_id":1,"quantity":1}`)), Quantity: 1}, {TierID: tier.ID, RewardType: RewardTypeCoupon, RewardPayload: datatypes.JSON([]byte(`{"coupon_id":2,"quantity":1}`)), Quantity: 2}, } if err := svc.UpsertTaskRewards(context.Background(), task.ID, initialRewards, nil); err != nil { t.Fatalf("首次保存奖励失败: %v", err) } var stored []tcmodel.TaskReward if err := db.Where("task_id = ?", task.ID).Order("id asc").Find(&stored).Error; err != nil { t.Fatalf("查询奖励失败: %v", err) } if len(stored) != 2 { t.Fatalf("奖励数量不正确, 期望 2 实际 %d", len(stored)) } updatePayload := datatypes.JSON([]byte(`{"coupon_id":99,"quantity":3}`)) secondPayload := datatypes.JSON([]byte(`{"coupon_id":200,"quantity":1}`)) updateInput := []TaskRewardInput{ {ID: stored[0].ID, TierID: tier.ID, RewardType: RewardTypeCoupon, RewardPayload: updatePayload, Quantity: 5}, {TierID: tier.ID, RewardType: RewardTypeCoupon, RewardPayload: secondPayload, Quantity: 1}, } if err := svc.UpsertTaskRewards(context.Background(), task.ID, updateInput, []int64{stored[1].ID}); err != nil { t.Fatalf("更新奖励失败: %v", err) } var refreshed []tcmodel.TaskReward if err := db.Where("task_id = ?", task.ID).Order("id asc").Find(&refreshed).Error; err != nil { t.Fatalf("查询更新后奖励失败: %v", err) } if len(refreshed) != 2 { t.Fatalf("更新后奖励数量不正确, 期望 2 实际 %d", len(refreshed)) } if refreshed[0].ID != stored[0].ID { t.Fatalf("原有奖励记录未被更新") } var pl map[string]int64 if err := json.Unmarshal(refreshed[0].RewardPayload, &pl); err != nil { t.Fatalf("解析奖励 payload 失败: %v", err) } if pl["coupon_id"] != 99 { t.Errorf("奖励 payload 未更新, 期望 99 实际 %d", pl["coupon_id"]) } if refreshed[0].Quantity != 5 { t.Errorf("奖励数量未更新, 期望 5 实际 %d", refreshed[0].Quantity) } for _, r := range refreshed { if r.ID == stored[1].ID { t.Fatalf("待删除的奖励仍存在, id=%d", r.ID) } } } func TestGetUserProgress_UsesEffectiveAmount(t *testing.T) { repo, err := mysql.NewSQLiteRepoForTest() if err != nil { t.Fatalf("创建 repo 失败: %v", err) } db := repo.GetDbW() initTestTables(t, db) ensureExtraTablesForServiceTest(t, db) testLogger, err := logger.NewCustomLogger() if err != nil { t.Fatalf("创建 logger 失败: %v", err) } svc := New(testLogger, repo, nil, nil, nil) task := &tcmodel.Task{Name: "真实消费口径", Status: 1, Visibility: 1} if err := db.Create(task).Error; err != nil { t.Fatalf("创建任务失败: %v", err) } tier := &tcmodel.TaskTier{ TaskID: task.ID, Metric: MetricOrderAmount, Operator: OperatorGTE, Threshold: 1, Window: WindowLifetime, ActivityID: 201, } if err := db.Create(tier).Error; err != nil { t.Fatalf("创建档位失败: %v", err) } secondaryTier := &tcmodel.TaskTier{ TaskID: task.ID, Metric: MetricOrderAmount, Operator: OperatorGTE, Threshold: 1, Window: WindowLifetime, ActivityID: 202, } if err := db.Create(secondaryTier).Error; err != nil { t.Fatalf("创建第二个档位失败: %v", err) } db.Exec("INSERT INTO activities (id, price_draw) VALUES (201, 1000)") db.Exec("INSERT INTO activities (id, price_draw) VALUES (202, 0)") db.Exec("INSERT INTO activity_issues (id, activity_id) VALUES (301, 201)") db.Exec("INSERT INTO activity_issues (id, activity_id) VALUES (302, 202)") userID := int64(6001) now := time.Now() inside := now.Format(time.DateTime) // 次卡订单:total_amount=0,但 price_draw>0, draw_count=2 db.Exec("INSERT INTO orders (id, user_id, status, source_type, total_amount, created_at) VALUES (401, ?, 2, 0, 0, ?)", userID, inside) db.Exec("INSERT INTO activity_draw_logs (order_id, issue_id) VALUES (401, 301)") db.Exec("INSERT INTO activity_draw_logs (order_id, issue_id) VALUES (401, 301)") // 现金订单:price_draw=0,需回退 total_amount db.Exec("INSERT INTO orders (id, user_id, status, source_type, total_amount, created_at) VALUES (402, ?, 2, 0, 1500, ?)", userID, inside) db.Exec("INSERT INTO activity_draw_logs (order_id, issue_id) VALUES (402, 302)") progress, err := svc.GetUserProgress(context.Background(), userID, task.ID) if err != nil { t.Fatalf("获取进度失败: %v", err) } if progress.OrderAmount != 3500 { t.Fatalf("订单金额统计错误,期望 3500 实际 %d", progress.OrderAmount) } if progress.OrderCount != 2 { t.Fatalf("订单数量统计错误,期望 2 实际 %d", progress.OrderCount) } tierProgress, ok := progress.TierProgressMap[tier.ID] if !ok { t.Fatalf("未找到档位进度") } if tierProgress.OrderAmount != 2000 { t.Fatalf("档位金额错误,期望 2000 实际 %d", tierProgress.OrderAmount) } if tierProgress.OrderCount != 1 { t.Fatalf("档位订单数错误,期望 1 实际 %d", tierProgress.OrderCount) } } func TestTimeWindow_ActivityPeriod(t *testing.T) { repo, err := mysql.NewSQLiteRepoForTest() if err != nil { t.Fatalf("创建 repo 失败: %v", err) } db := repo.GetDbW() initTestTables(t, db) ensureExtraTablesForServiceTest(t, db) svc := New(nil, repo, nil, nil, nil) start := time.Now().AddDate(0, -1, 0) end := start.AddDate(0, 0, 10) task := &tcmodel.Task{ Name: "任务窗口期", Status: 1, Visibility: 1, StartTime: &start, EndTime: &end, } if err := db.Create(task).Error; err != nil { t.Fatalf("创建任务失败: %v", err) } tier := &tcmodel.TaskTier{ TaskID: task.ID, Metric: MetricOrderCount, Operator: OperatorGTE, Threshold: 1, Window: WindowActivityPeriod, ActivityID: 501, } if err := db.Create(tier).Error; err != nil { t.Fatalf("创建档位失败: %v", err) } db.Exec("INSERT INTO activities (id, price_draw) VALUES (501, 500)") db.Exec("INSERT INTO activity_issues (id, activity_id) VALUES (601, 501)") userID := int64(7007) inside := start.Add(24 * time.Hour).Format(time.DateTime) outside := end.Add(24 * time.Hour).Format(time.DateTime) db.Exec("INSERT INTO orders (id, user_id, status, source_type, total_amount, created_at) VALUES (701, ?, 2, 0, 0, ?)", userID, inside) db.Exec("INSERT INTO activity_draw_logs (order_id, issue_id) VALUES (701, 601)") db.Exec("INSERT INTO orders (id, user_id, status, source_type, total_amount, created_at) VALUES (702, ?, 2, 0, 0, ?)", userID, outside) db.Exec("INSERT INTO activity_draw_logs (order_id, issue_id) VALUES (702, 601)") progress, err := svc.GetUserProgress(context.Background(), userID, task.ID) if err != nil { t.Fatalf("获取进度失败: %v", err) } tierProgress, ok := progress.TierProgressMap[tier.ID] if !ok { t.Fatalf("未找到活动有效期档位进度") } if tierProgress.OrderCount != 1 { t.Fatalf("活动有效期窗口统计错误,期望 1 实际 %d", tierProgress.OrderCount) } if progress.OrderCount != 2 { t.Fatalf("总体订单统计错误,期望 2 实际 %d", progress.OrderCount) } } func TestCalculateCrossTaskConsumedThreshold_RespectsTaskWindow(t *testing.T) { repo, err := mysql.NewSQLiteRepoForTest() if err != nil { t.Fatalf("创建 repo 失败: %v", err) } db := repo.GetDbW() initTestTables(t, db) svc := New(nil, repo, nil, nil, nil).(*service) now := time.Now() startA := now.AddDate(0, -2, 0) endA := now.AddDate(0, -1, 0) startB := now.AddDate(0, -1, 0) endB := now.AddDate(0, 0, 15) startC := now.AddDate(0, -1, 15) endC := now.AddDate(0, 1, 0) taskA := &tcmodel.Task{Name: "历史任务", Status: 1, Visibility: 1, StartTime: &startA, EndTime: &endA} taskB := &tcmodel.Task{Name: "当前任务", Status: 1, Visibility: 1, StartTime: &startB, EndTime: &endB} taskC := &tcmodel.Task{Name: "重叠任务", Status: 1, Visibility: 1, StartTime: &startC, EndTime: &endC} if err := db.Create(taskA).Error; err != nil { t.Fatalf("创建任务 A 失败: %v", err) } if err := db.Create(taskB).Error; err != nil { t.Fatalf("创建任务 B 失败: %v", err) } if err := db.Create(taskC).Error; err != nil { t.Fatalf("创建任务 C 失败: %v", err) } activityID := int64(9001) tierA := &tcmodel.TaskTier{ TaskID: taskA.ID, Metric: MetricOrderAmount, Operator: OperatorGTE, Threshold: 20000, Window: WindowLifetime, ActivityID: activityID, } tierB := &tcmodel.TaskTier{ TaskID: taskB.ID, Metric: MetricOrderAmount, Operator: OperatorGTE, Threshold: 30000, Window: WindowLifetime, ActivityID: activityID, } tierC := &tcmodel.TaskTier{ TaskID: taskC.ID, Metric: MetricOrderAmount, Operator: OperatorGTE, Threshold: 40000, Window: WindowLifetime, ActivityID: activityID, } for _, tier := range []*tcmodel.TaskTier{tierA, tierB, tierC} { if err := db.Create(tier).Error; err != nil { t.Fatalf("创建档位失败: %v", err) } } userID := int64(9527) payloadA := datatypes.JSON([]byte(fmt.Sprintf("[%d]", tierA.ID))) payloadC := datatypes.JSON([]byte(fmt.Sprintf("[%d]", tierC.ID))) if err := db.Create(&tcmodel.UserTaskProgress{UserID: userID, TaskID: taskA.ID, ActivityID: 0, ClaimedTiers: payloadA}).Error; err != nil { t.Fatalf("写入任务 A 进度失败: %v", err) } if err := db.Create(&tcmodel.UserTaskProgress{UserID: userID, TaskID: taskC.ID, ActivityID: 0, ClaimedTiers: payloadC}).Error; err != nil { t.Fatalf("写入任务 C 进度失败: %v", err) } consumed, err := svc.calculateCrossTaskConsumedThreshold(userID, taskB, tierB) if err != nil { t.Fatalf("计算交叉占用失败: %v", err) } if consumed != tierC.Threshold { t.Fatalf("交叉占用计算错误,期望 %d 实际 %d", tierC.Threshold, consumed) } // 新增一个创建时间晚于 B 的任务 D,并标记为已领取 taskD := &tcmodel.Task{Name: "后续任务", Status: 1, Visibility: 1} if err := db.Create(taskD).Error; err != nil { t.Fatalf("创建任务 D 失败: %v", err) } tierD := &tcmodel.TaskTier{ TaskID: taskD.ID, Metric: MetricOrderAmount, Operator: OperatorGTE, Threshold: 20000, Window: WindowLifetime, ActivityID: activityID, } if err := db.Create(tierD).Error; err != nil { t.Fatalf("创建任务 D 档位失败: %v", err) } payloadD := datatypes.JSON([]byte(fmt.Sprintf("[%d]", tierD.ID))) if err := db.Create(&tcmodel.UserTaskProgress{UserID: userID, TaskID: taskD.ID, ActivityID: 0, ClaimedTiers: payloadD}).Error; err != nil { t.Fatalf("写入任务 D 进度失败: %v", err) } consumed, err = svc.calculateCrossTaskConsumedThreshold(userID, taskB, tierB) if err != nil { t.Fatalf("再次计算交叉占用失败: %v", err) } expected := tierC.Threshold + tierD.Threshold if consumed != expected { t.Fatalf("交叉占用应包含任务 C+D,期望 %d 实际 %d", expected, consumed) } } // TestLifetimeWindow_RespectsTaskStartTime 验证 CRITICAL-1 修复: // lifetime 窗口现在受任务 StartTime 约束,防止历史数据被用于领取新任务 func TestLifetimeWindow_RespectsTaskStartTime(t *testing.T) { repo, err := mysql.NewSQLiteRepoForTest() if err != nil { t.Fatalf("创建 repo 失败: %v", err) } db := repo.GetDbW() initTestTables(t, db) ensureExtraTablesForServiceTest(t, db) svc := New(nil, repo, nil, nil, nil) now := time.Now() taskStart := now.Add(-30 * 24 * time.Hour) // 任务30天前开始 task := &tcmodel.Task{ Name: "历史数据阻断测试", Status: 1, Visibility: 1, StartTime: &taskStart, } if err := db.Create(task).Error; err != nil { t.Fatalf("创建任务失败: %v", err) } // 创建 lifetime 窗口档位 tier := &tcmodel.TaskTier{ TaskID: task.ID, Metric: MetricOrderCount, Operator: OperatorGTE, Threshold: 3, // 需要3单才能领取 Window: WindowLifetime, } if err := db.Create(tier).Error; err != nil { t.Fatalf("创建档位失败: %v", err) } db.Exec("INSERT INTO activities (id, price_draw) VALUES (1, 100)") db.Exec("INSERT INTO activity_issues (id, activity_id) VALUES (1, 1)") userID := int64(10001) // 插入历史订单(任务开始之前) historicalOrder := taskStart.Add(-10 * 24 * time.Hour).Format(time.DateTime) for i := int64(101); i <= 105; i++ { db.Exec("INSERT INTO orders (id, user_id, status, source_type, total_amount, created_at) VALUES (?, ?, 2, 0, 100, ?)", i, userID, historicalOrder) db.Exec("INSERT INTO activity_draw_logs (order_id, issue_id) VALUES (?, 1)", i) } // 插入新订单(任务开始之后) recentOrder := now.Add(-1 * 24 * time.Hour).Format(time.DateTime) for i := int64(201); i <= 202; i++ { db.Exec("INSERT INTO orders (id, user_id, status, source_type, total_amount, created_at) VALUES (?, ?, 2, 0, 100, ?)", i, userID, recentOrder) db.Exec("INSERT INTO activity_draw_logs (order_id, issue_id) VALUES (?, 1)", i) } // 获取进度 progress, err := svc.GetUserProgress(context.Background(), userID, task.ID) if err != nil { t.Fatalf("获取进度失败: %v", err) } tierProgress, ok := progress.TierProgressMap[tier.ID] if !ok { t.Fatalf("未找到档位进度") } // 验证:只统计任务开始后的订单(2单),不包含历史订单(5单) if tierProgress.OrderCount != 2 { t.Errorf("lifetime 窗口应受任务时间约束: 期望 2 单, 实际 %d 单", tierProgress.OrderCount) } // 验证:阈值未达到(需要3单,实际只有2单) if tierProgress.OrderCount >= tier.Threshold { t.Errorf("历史数据不应计入进度,用户不应能够领取奖励") } t.Logf("✓ CRITICAL-1 修复验证通过: lifetime 窗口正确受任务 StartTime 约束") t.Logf(" - 历史订单: 5 单 (任务开始前)") t.Logf(" - 有效订单: %d 单 (任务开始后)", tierProgress.OrderCount) } // TestEmptyWindow_RespectsTaskStartTime 验证空窗口也受任务时间约束 func TestEmptyWindow_RespectsTaskStartTime(t *testing.T) { repo, err := mysql.NewSQLiteRepoForTest() if err != nil { t.Fatalf("创建 repo 失败: %v", err) } db := repo.GetDbW() initTestTables(t, db) ensureExtraTablesForServiceTest(t, db) svc := New(nil, repo, nil, nil, nil) now := time.Now() taskStart := now.Add(-7 * 24 * time.Hour) task := &tcmodel.Task{ Name: "空窗口测试", Status: 1, Visibility: 1, StartTime: &taskStart, } if err := db.Create(task).Error; err != nil { t.Fatalf("创建任务失败: %v", err) } // 创建空窗口档位 tier := &tcmodel.TaskTier{ TaskID: task.ID, Metric: MetricOrderCount, Operator: OperatorGTE, Threshold: 1, Window: "", // 空窗口 } if err := db.Create(tier).Error; err != nil { t.Fatalf("创建档位失败: %v", err) } db.Exec("INSERT INTO activities (id, price_draw) VALUES (1, 100)") db.Exec("INSERT INTO activity_issues (id, activity_id) VALUES (1, 1)") userID := int64(10002) // 历史订单(任务开始前) oldTime := taskStart.Add(-24 * time.Hour).Format(time.DateTime) db.Exec("INSERT INTO orders (id, user_id, status, source_type, total_amount, created_at) VALUES (301, ?, 2, 0, 100, ?)", userID, oldTime) db.Exec("INSERT INTO activity_draw_logs (order_id, issue_id) VALUES (301, 1)") // 新订单(任务开始后) newTime := now.Add(-1 * time.Hour).Format(time.DateTime) db.Exec("INSERT INTO orders (id, user_id, status, source_type, total_amount, created_at) VALUES (302, ?, 2, 0, 100, ?)", userID, newTime) db.Exec("INSERT INTO activity_draw_logs (order_id, issue_id) VALUES (302, 1)") progress, err := svc.GetUserProgress(context.Background(), userID, task.ID) if err != nil { t.Fatalf("获取进度失败: %v", err) } tierProgress, ok := progress.TierProgressMap[tier.ID] if !ok { t.Fatalf("未找到档位进度") } // 空窗口也应受任务时间约束,只统计1单 if tierProgress.OrderCount != 1 { t.Errorf("空窗口应受任务时间约束: 期望 1 单, 实际 %d 单", tierProgress.OrderCount) } t.Logf("✓ 空窗口测试通过: OrderCount=%d", tierProgress.OrderCount) } func TestClaimTier_AfterEndControlledByTaskConfig(t *testing.T) { repo, err := mysql.NewSQLiteRepoForTest() if err != nil { t.Fatalf("创建 repo 失败: %v", err) } db := repo.GetDbW() initTestTables(t, db) ensureExtraTablesForServiceTest(t, db) testLogger, err := logger.NewCustomLogger() if err != nil { t.Fatalf("创建 logger 失败: %v", err) } svc := New(testLogger, repo, nil, nil, nil) now := time.Now() start := now.Add(-2 * time.Hour) end := now.Add(-1 * time.Hour) task := &tcmodel.Task{ Name: "截止领取开关测试", Status: 1, Visibility: 1, StartTime: &start, EndTime: &end, } if err := db.Create(task).Error; err != nil { t.Fatalf("创建任务失败: %v", err) } tier := &tcmodel.TaskTier{ TaskID: task.ID, Metric: MetricOrderCount, Operator: OperatorGTE, Threshold: 0, Window: WindowLifetime, ActivityID: 0, } if err := db.Create(tier).Error; err != nil { t.Fatalf("创建档位失败: %v", err) } userID := int64(20001) err = svc.ClaimTier(context.Background(), userID, task.ID, tier.ID) if err == nil || err.Error() != "任务已经结束" { t.Fatalf("未开启 allow_claim_after_end 时应拦截,实际 err=%v", err) } if err := db.Model(&tcmodel.Task{}).Where("id = ?", task.ID).Update("allow_claim_after_end", 1).Error; err != nil { t.Fatalf("更新 allow_claim_after_end 失败: %v", err) } err = svc.ClaimTier(context.Background(), userID, task.ID, tier.ID) if err == nil { t.Fatalf("无奖励配置时应返回错误,避免误判测试") } if err.Error() == "任务已经结束" { t.Fatalf("开启 allow_claim_after_end 后不应再返回任务已结束错误") } }