632 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

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 taskcenter
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
tcmodel "bindbox-game/internal/repository/mysql/task_center"
"gorm.io/datatypes"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// ================================
// 配置组合生成器
// ================================
// TaskCombination 表示一种任务配置组合
type TaskCombination struct {
Name string // 任务名称
Metric string // 指标类型
Operator string // 操作符
Threshold int64 // 阈值
Window string // 时间窗口
RewardType 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}, // 消费金额(单位分 = 100元
{MetricInviteCount, []string{OperatorGTE, OperatorEQ}, 2}, // 邀请人数
}
windows := []string{WindowDaily, WindowWeekly, 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("测试任务%d_%s_%s_%s", idx, m.name, w, r),
Metric: m.name,
Operator: op,
Threshold: m.threshold,
Window: w,
RewardType: r,
})
}
}
}
}
return combinations
}
// CreateTestDB 创建内存数据库并初始化表结构
func CreateTestDB(t *testing.T) *gorm.DB {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("创建数据库失败: %v", err)
}
// 创建任务中心相关表
if err := db.Exec(`CREATE TABLE task_center_tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
status INTEGER NOT NULL DEFAULT 1,
start_time DATETIME,
end_time DATETIME,
visibility INTEGER NOT NULL DEFAULT 1,
conditions_schema TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);`).Error; err != nil {
t.Fatalf("创建 task_center_tasks 表失败: %v", err)
}
if err := db.Exec(`CREATE TABLE task_center_task_tiers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL,
metric TEXT NOT NULL,
operator TEXT NOT NULL,
threshold INTEGER NOT NULL,
window TEXT NOT NULL,
repeatable INTEGER NOT NULL DEFAULT 1,
priority INTEGER NOT NULL DEFAULT 0,
activity_id INTEGER NOT NULL DEFAULT 0,
extra_params TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);`).Error; err != nil {
t.Fatalf("创建 task_center_task_tiers 表失败: %v", err)
}
if err := db.Exec(`CREATE TABLE task_center_task_rewards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL,
tier_id INTEGER NOT NULL,
reward_type TEXT NOT NULL,
reward_payload TEXT NOT NULL,
quantity INTEGER NOT NULL DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);`).Error; err != nil {
t.Fatalf("创建 task_center_task_rewards 表失败: %v", err)
}
if err := db.Exec(`CREATE TABLE task_center_user_progress (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
task_id INTEGER NOT NULL,
activity_id INTEGER NOT NULL DEFAULT 0,
order_count INTEGER NOT NULL DEFAULT 0,
order_amount INTEGER NOT NULL DEFAULT 0,
invite_count INTEGER NOT NULL DEFAULT 0,
first_order INTEGER NOT NULL DEFAULT 0,
claimed_tiers TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, task_id, activity_id)
);`).Error; err != nil {
t.Fatalf("创建 task_center_user_progress 表失败: %v", err)
}
if err := db.Exec(`CREATE TABLE task_center_event_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT,
source_type TEXT,
source_id INTEGER,
user_id INTEGER,
task_id INTEGER,
tier_id INTEGER,
idempotency_key TEXT UNIQUE,
status TEXT,
result TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);`).Error; err != nil {
t.Fatalf("创建 task_center_event_logs 表失败: %v", err)
}
return db
}
// InsertTaskWithTierAndReward 插入一个完整的任务配置(任务+档位+奖励)
func InsertTaskWithTierAndReward(t *testing.T, db *gorm.DB, combo TaskCombination) (taskID, tierID int64) {
// 插入任务
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 := db.Create(task).Error; err != nil {
t.Fatalf("插入任务失败: %v", err)
}
// 插入档位
tier := &tcmodel.TaskTier{
TaskID: task.ID,
Metric: combo.Metric,
Operator: combo.Operator,
Threshold: combo.Threshold,
Window: combo.Window,
Priority: 0,
}
if err := db.Create(tier).Error; err != nil {
t.Fatalf("插入档位失败: %v", err)
}
// 生成奖励 payload
payload := generateRewardPayload(combo.RewardType)
reward := &tcmodel.TaskReward{
TaskID: task.ID,
TierID: tier.ID,
RewardType: combo.RewardType,
RewardPayload: datatypes.JSON(payload),
Quantity: 10, // 默认数量
}
if err := db.Create(reward).Error; err != nil {
t.Fatalf("插入奖励失败: %v", err)
}
return task.ID, tier.ID
}
// 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 `{}`
}
}
// ================================
// 单元测试
// ================================
// TestGenerateAllCombinations 测试配置组合生成器
func TestGenerateAllCombinations(t *testing.T) {
combos := GenerateAllCombinations()
// 预期组合数: (1*1 + 3*2) * 3 * 5 = 7 * 3 * 5 = 105
expectedCount := 105
if len(combos) != expectedCount {
t.Errorf("组合数量不正确: 期望 %d, 实际 %d", expectedCount, len(combos))
}
// 验证每种指标都有覆盖
metricCounts := make(map[string]int)
for _, c := range combos {
metricCounts[c.Metric]++
}
t.Logf("配置组合统计:")
t.Logf(" 总数: %d", len(combos))
for metric, count := range metricCounts {
t.Logf(" %s: %d 种组合", metric, count)
}
}
// TestInsertAllCombinations 测试将所有配置组合插入数据库
func TestInsertAllCombinations(t *testing.T) {
db := CreateTestDB(t)
combos := GenerateAllCombinations()
for _, combo := range combos {
taskID, tierID := InsertTaskWithTierAndReward(t, db, combo)
if taskID == 0 || tierID == 0 {
t.Errorf("插入失败: %s", combo.Name)
}
}
// 验证数据库记录数
var taskCount, tierCount, rewardCount int64
db.Model(&tcmodel.Task{}).Count(&taskCount)
db.Model(&tcmodel.TaskTier{}).Count(&tierCount)
db.Model(&tcmodel.TaskReward{}).Count(&rewardCount)
if taskCount != int64(len(combos)) {
t.Errorf("任务数量不正确: 期望 %d, 实际 %d", len(combos), taskCount)
}
if tierCount != int64(len(combos)) {
t.Errorf("档位数量不正确: 期望 %d, 实际 %d", len(combos), tierCount)
}
if rewardCount != int64(len(combos)) {
t.Errorf("奖励数量不正确: 期望 %d, 实际 %d", len(combos), rewardCount)
}
t.Logf("成功插入 %d 个任务配置组合", taskCount)
}
// TestFirstOrderMetric 测试首单指标
func TestFirstOrderMetric(t *testing.T) {
db := CreateTestDB(t)
combo := TaskCombination{
Name: "首单测试任务",
Metric: MetricFirstOrder,
Operator: OperatorEQ,
Threshold: 1,
Window: WindowLifetime,
RewardType: RewardTypePoints,
}
taskID, tierID := InsertTaskWithTierAndReward(t, db, combo)
// 模拟用户首单进度
userID := int64(1001)
progress := &tcmodel.UserTaskProgress{
UserID: userID,
TaskID: taskID,
OrderCount: 1,
OrderAmount: 10000,
FirstOrder: 1, // 首单标记
ClaimedTiers: datatypes.JSON("[]"),
}
if err := db.Create(progress).Error; err != nil {
t.Fatalf("创建进度失败: %v", err)
}
// 验证进度状态
var p tcmodel.UserTaskProgress
db.Where("user_id = ? AND task_id = ?", userID, taskID).First(&p)
if p.FirstOrder != 1 {
t.Error("首单标记未正确设置")
}
t.Logf("首单指标测试通过: taskID=%d, tierID=%d", taskID, tierID)
}
// TestOrderCountMetric 测试订单数量指标
func TestOrderCountMetric(t *testing.T) {
db := CreateTestDB(t)
combo := TaskCombination{
Name: "订单数量测试任务",
Metric: MetricOrderCount,
Operator: OperatorGTE,
Threshold: 3,
Window: WindowDaily,
RewardType: RewardTypeCoupon,
}
taskID, _ := InsertTaskWithTierAndReward(t, db, combo)
// 模拟用户下单进度
userID := int64(1002)
for i := 1; i <= 5; i++ {
progress := &tcmodel.UserTaskProgress{
UserID: userID,
TaskID: taskID,
OrderCount: int64(i),
ClaimedTiers: datatypes.JSON("[]"),
}
// Upsert 模拟
db.Where("user_id = ? AND task_id = ?", userID, taskID).Assign(progress).FirstOrCreate(progress)
}
var p tcmodel.UserTaskProgress
db.Where("user_id = ? AND task_id = ?", userID, taskID).First(&p)
if p.OrderCount < 3 {
t.Error("订单数量未达到阈值")
}
t.Logf("订单数量指标测试通过: 当前订单数=%d", p.OrderCount)
}
// TestOrderAmountMetric 测试消费金额指标
func TestOrderAmountMetric(t *testing.T) {
db := CreateTestDB(t)
combo := TaskCombination{
Name: "消费金额测试任务",
Metric: MetricOrderAmount,
Operator: OperatorGTE,
Threshold: 10000, // 100元
Window: WindowWeekly,
RewardType: RewardTypeItemCard,
}
taskID, _ := InsertTaskWithTierAndReward(t, db, combo)
// 模拟用户累计消费
userID := int64(1003)
amounts := []int64{3000, 4000, 5000} // 累计 120 元
totalAmount := int64(0)
for _, amt := range amounts {
totalAmount += amt
}
progress := &tcmodel.UserTaskProgress{
UserID: userID,
TaskID: taskID,
OrderAmount: totalAmount,
ClaimedTiers: datatypes.JSON("[]"),
}
db.Create(progress)
var p tcmodel.UserTaskProgress
db.Where("user_id = ? AND task_id = ?", userID, taskID).First(&p)
if p.OrderAmount < 10000 {
t.Error("消费金额未达到阈值")
}
t.Logf("消费金额指标测试通过: 当前消费=%d分", p.OrderAmount)
}
// TestInviteCountMetric 测试邀请人数指标
func TestInviteCountMetric(t *testing.T) {
db := CreateTestDB(t)
combo := TaskCombination{
Name: "邀请人数测试任务",
Metric: MetricInviteCount,
Operator: OperatorGTE,
Threshold: 2,
Window: WindowLifetime,
RewardType: RewardTypeTitle,
}
taskID, _ := InsertTaskWithTierAndReward(t, db, combo)
// 模拟邀请进度
userID := int64(1004)
progress := &tcmodel.UserTaskProgress{
UserID: userID,
TaskID: taskID,
InviteCount: 3,
ClaimedTiers: datatypes.JSON("[]"),
}
db.Create(progress)
var p tcmodel.UserTaskProgress
db.Where("user_id = ? AND task_id = ?", userID, taskID).First(&p)
if p.InviteCount < 2 {
t.Error("邀请人数未达到阈值")
}
t.Logf("邀请人数指标测试通过: 当前邀请数=%d", p.InviteCount)
}
// TestAllRewardTypes 测试所有奖励类型的 payload 解析
func TestAllRewardTypes(t *testing.T) {
rewardTypes := []string{
RewardTypePoints,
RewardTypeCoupon,
RewardTypeItemCard,
RewardTypeTitle,
RewardTypeGameTicket,
}
for _, rt := range rewardTypes {
payload := generateRewardPayload(rt)
var data map[string]interface{}
if err := json.Unmarshal([]byte(payload), &data); err != nil {
t.Errorf("奖励类型 %s 的 payload 解析失败: %v", rt, err)
continue
}
switch rt {
case RewardTypePoints:
if _, ok := data["points"]; !ok {
t.Errorf("points 类型缺少 points 字段")
}
case RewardTypeCoupon:
if _, ok := data["coupon_id"]; !ok {
t.Errorf("coupon 类型缺少 coupon_id 字段")
}
case RewardTypeItemCard:
if _, ok := data["card_id"]; !ok {
t.Errorf("item_card 类型缺少 card_id 字段")
}
case RewardTypeTitle:
if _, ok := data["title_id"]; !ok {
t.Errorf("title 类型缺少 title_id 字段")
}
case RewardTypeGameTicket:
if _, ok := data["game_code"]; !ok {
t.Errorf("game_ticket 类型缺少 game_code 字段")
}
if _, ok := data["amount"]; !ok {
t.Errorf("game_ticket 类型缺少 amount 字段")
}
}
t.Logf("奖励类型 %s 验证通过: %s", rt, payload)
}
}
// TestTimeWindowDaily 测试每日时间窗口重置逻辑
func TestTimeWindowDaily(t *testing.T) {
db := CreateTestDB(t)
combo := TaskCombination{
Name: "每日重置测试任务",
Metric: MetricOrderCount,
Operator: OperatorGTE,
Threshold: 3,
Window: WindowDaily,
RewardType: RewardTypePoints,
}
taskID, _ := InsertTaskWithTierAndReward(t, db, combo)
userID := int64(1005)
// 模拟昨天的进度
yesterday := time.Now().Add(-25 * time.Hour)
progress := &tcmodel.UserTaskProgress{
UserID: userID,
TaskID: taskID,
OrderCount: 5,
ClaimedTiers: datatypes.JSON("[]"),
UpdatedAt: yesterday,
}
db.Create(progress)
// 检查日期判断逻辑
now := time.Now()
y1, m1, d1 := yesterday.Date()
y2, m2, d2 := now.Date()
shouldReset := !(y1 == y2 && m1 == m2 && d1 == d2)
if !shouldReset {
t.Error("每日重置逻辑有误:昨天的进度应该需要重置")
}
t.Logf("时间窗口测试通过: 昨天=%v, 今天=%v, 需要重置=%v", yesterday.Format("2006-01-02"), now.Format("2006-01-02"), shouldReset)
}
// TestOperatorComparison 测试操作符比较逻辑
func TestOperatorComparison(t *testing.T) {
testCases := []struct {
name string
operator string
threshold int64
value int64
expected bool
}{
{"GTE-达到阈值", OperatorGTE, 3, 3, true},
{"GTE-超过阈值", OperatorGTE, 3, 5, true},
{"GTE-未达阈值", OperatorGTE, 3, 2, false},
{"EQ-精确匹配", OperatorEQ, 3, 3, true},
{"EQ-超过不匹配", OperatorEQ, 3, 5, false},
{"EQ-未达不匹配", OperatorEQ, 3, 2, false},
}
for _, tc := range testCases {
var result bool
switch tc.operator {
case OperatorGTE:
result = tc.value >= tc.threshold
case OperatorEQ:
result = tc.value == tc.threshold
}
if result != tc.expected {
t.Errorf("%s: 期望 %v, 实际 %v", tc.name, tc.expected, result)
} else {
t.Logf("%s: 通过", tc.name)
}
}
}
// TestIdempotency 测试幂等性(同一事件不重复处理)
func TestIdempotency(t *testing.T) {
db := CreateTestDB(t)
ctx := context.Background()
_ = ctx // 用于后续扩展
// 插入第一条事件日志
idk := "1001:1:1:order:100"
log1 := &tcmodel.TaskEventLog{
EventID: "evt_001",
SourceType: "order",
SourceID: 100,
UserID: 1001,
TaskID: 1,
TierID: 1,
IdempotencyKey: idk,
Status: "granted",
}
if err := db.Create(log1).Error; err != nil {
t.Fatalf("创建事件日志失败: %v", err)
}
// 尝试插入重复的幂等键(应该失败)
log2 := &tcmodel.TaskEventLog{
EventID: "evt_002",
SourceType: "order",
SourceID: 100,
UserID: 1001,
TaskID: 1,
TierID: 1,
IdempotencyKey: idk, // 相同的幂等键
Status: "granted",
}
err := db.Create(log2).Error
if err == nil {
t.Error("幂等性检查失败:重复的幂等键应该被拒绝")
} else {
t.Logf("幂等性测试通过: 重复记录被正确拒绝")
}
}
// TestClaimedTiersTracking 测试已领取档位追踪
func TestClaimedTiersTracking(t *testing.T) {
db := CreateTestDB(t)
userID := int64(1006)
taskID := int64(1)
// 初始化进度
progress := &tcmodel.UserTaskProgress{
UserID: userID,
TaskID: taskID,
ClaimedTiers: datatypes.JSON("[]"),
}
db.Create(progress)
// 模拟领取档位
claimedTiers := []int64{1, 2, 3}
b, _ := json.Marshal(claimedTiers)
db.Model(&tcmodel.UserTaskProgress{}).
Where("user_id = ? AND task_id = ?", userID, taskID).
Update("claimed_tiers", datatypes.JSON(b))
// 验证领取状态
var p tcmodel.UserTaskProgress
db.Where("user_id = ? AND task_id = ?", userID, taskID).First(&p)
var claimed []int64
json.Unmarshal([]byte(p.ClaimedTiers), &claimed)
if len(claimed) != 3 {
t.Errorf("已领取档位数量不正确: 期望 3, 实际 %d", len(claimed))
}
// 检查是否包含特定档位
tierToCheck := int64(2)
found := false
for _, id := range claimed {
if id == tierToCheck {
found = true
break
}
}
if !found {
t.Errorf("档位 %d 应该已被领取", tierToCheck)
}
t.Logf("已领取档位追踪测试通过: %v", claimed)
}