669 lines
22 KiB
Go
669 lines
22 KiB
Go
package taskcenter
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"testing"
|
||
"time"
|
||
|
||
"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)
|
||
|
||
svc := New(nil, 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)
|
||
|
||
svc := New(nil, 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)
|
||
}
|