package orderLogic import ( "context" "fmt" "os" "strings" "testing" "time" "github.com/perfect-panel/server/internal/config" userLogic "github.com/perfect-panel/server/internal/logic/public/user" modelLog "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/model/order" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/constant" "github.com/redis/go-redis/v9" "gorm.io/driver/mysql" "gorm.io/gorm" ) // 普通用户 + 首单 → 双方赠N天 func TestHandleCommission_GrantGiftDaysWhenCommissionDisabled_FirstOrder(t *testing.T) { logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{ ReferralPercentage: 0, OnlyFirstPurchase: false, GiftDays: 2, }) defer cleanup() referee := seedUser(t, db, 0, false) referer := seedUser(t, db, 0, false) referee.RefererId = referer.Id baseExpire := time.Now().Add(72 * time.Hour).Truncate(time.Second) refereeSub := seedActiveSubscribe(t, db, referee.Id, baseExpire) refererSub := seedActiveSubscribe(t, db, referer.Id, baseExpire) logic.handleCommission(context.Background(), referee, &order.Order{ OrderNo: "ORD-GIFT-001", Type: OrderTypeSubscribe, IsNew: true, // 首单 Amount: 100, FeeAmount: 0, CreatedAt: time.Now(), }) assertExpireIncreasedByDays(t, db, refereeSub.Id, baseExpire, 2) assertExpireIncreasedByDays(t, db, refererSub.Id, baseExpire, 2) var giftCount int64 if err := db.Model(&modelLog.SystemLog{}).Where("type = ?", modelLog.TypeGift.Uint8()).Count(&giftCount).Error; err != nil { t.Fatalf("count gift logs failed: %v", err) } if giftCount != 2 { t.Fatalf("expected 2 gift logs, got %d", giftCount) } } // 普通用户 + 非首单 → 不赠送 func TestHandleCommission_NoGiftDaysWhenCommissionDisabled_NotFirstOrder(t *testing.T) { logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{ ReferralPercentage: 0, OnlyFirstPurchase: false, GiftDays: 2, }) defer cleanup() referee := seedUser(t, db, 0, false) referer := seedUser(t, db, 0, false) referee.RefererId = referer.Id baseExpire := time.Now().Add(72 * time.Hour).Truncate(time.Second) refereeSub := seedActiveSubscribe(t, db, referee.Id, baseExpire) refererSub := seedActiveSubscribe(t, db, referer.Id, baseExpire) logic.handleCommission(context.Background(), referee, &order.Order{ OrderNo: "ORD-GIFT-002", Type: OrderTypeSubscribe, IsNew: false, // 非首单 Amount: 100, FeeAmount: 0, CreatedAt: time.Now(), }) // 到期时间不应延长 assertExpireIncreasedByDays(t, db, refereeSub.Id, baseExpire, 0) assertExpireIncreasedByDays(t, db, refererSub.Id, baseExpire, 0) var giftCount int64 if err := db.Model(&modelLog.SystemLog{}).Where("type = ?", modelLog.TypeGift.Uint8()).Count(&giftCount).Error; err != nil { t.Fatalf("count gift logs failed: %v", err) } if giftCount != 0 { t.Fatalf("expected 0 gift logs for non-first order, got %d", giftCount) } } // 渠道 + 首单 → 被邀请人赠N天 + 邀请人获佣金 func TestHandleCommission_GiftDaysAndCommissionWhenChannelFirstOrder(t *testing.T) { logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{ ReferralPercentage: 10, OnlyFirstPurchase: false, GiftDays: 2, }) defer cleanup() referee := seedUser(t, db, 0, false) referer := seedUser(t, db, 0, false) referee.RefererId = referer.Id baseExpire := time.Now().Add(96 * time.Hour).Truncate(time.Second) refereeSub := seedActiveSubscribe(t, db, referee.Id, baseExpire) logic.handleCommission(context.Background(), referee, &order.Order{ OrderNo: "ORD-COMM-001", Type: OrderTypeSubscribe, IsNew: true, // 首单 Amount: 100, FeeAmount: 0, CreatedAt: time.Now(), }) // 被邀请人(首单)应获得赠送天数 assertExpireIncreasedByDays(t, db, refereeSub.Id, baseExpire, 2) // 邀请人应获得佣金 var refererAfter user.User if err := db.First(&refererAfter, referer.Id).Error; err != nil { t.Fatalf("query referer failed: %v", err) } if refererAfter.Commission != 10 { t.Fatalf("expected referer commission=10, got %d", refererAfter.Commission) } var giftCount int64 if err := db.Model(&modelLog.SystemLog{}).Where("type = ?", modelLog.TypeGift.Uint8()).Count(&giftCount).Error; err != nil { t.Fatalf("count gift logs failed: %v", err) } if giftCount != 1 { t.Fatalf("expected 1 gift log for referee on first order with commission, got %d", giftCount) } } // 渠道 + 非首单 → 只给邀请人佣金,不赠天 func TestHandleCommission_OnlyCommissionWhenChannelNotFirstOrder(t *testing.T) { logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{ ReferralPercentage: 10, OnlyFirstPurchase: false, GiftDays: 2, }) defer cleanup() referee := seedUser(t, db, 0, false) referer := seedUser(t, db, 0, false) referee.RefererId = referer.Id baseExpire := time.Now().Add(96 * time.Hour).Truncate(time.Second) refereeSub := seedActiveSubscribe(t, db, referee.Id, baseExpire) logic.handleCommission(context.Background(), referee, &order.Order{ OrderNo: "ORD-COMM-002", Type: OrderTypeSubscribe, IsNew: false, // 非首单 Amount: 100, FeeAmount: 0, CreatedAt: time.Now(), }) // 被邀请人不应获得赠送天数 assertExpireIncreasedByDays(t, db, refereeSub.Id, baseExpire, 0) // 邀请人应获得佣金 var refererAfter user.User if err := db.First(&refererAfter, referer.Id).Error; err != nil { t.Fatalf("query referer failed: %v", err) } if refererAfter.Commission != 10 { t.Fatalf("expected referer commission=10, got %d", refererAfter.Commission) } var giftCount int64 if err := db.Model(&modelLog.SystemLog{}).Where("type = ?", modelLog.TypeGift.Uint8()).Count(&giftCount).Error; err != nil { t.Fatalf("count gift logs failed: %v", err) } if giftCount != 0 { t.Fatalf("expected 0 gift logs when channel non-first order, got %d", giftCount) } } func TestHandleCommission_NoGiftDaysWhenNoInviteRelation(t *testing.T) { logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{ ReferralPercentage: 0, OnlyFirstPurchase: false, GiftDays: 2, }) defer cleanup() // 没有邀请人的独立用户 loneUser := seedUser(t, db, 0, false) // RefererId == 0,无邀请关系 baseExpire := time.Now().Add(72 * time.Hour).Truncate(time.Second) loneSub := seedActiveSubscribe(t, db, loneUser.Id, baseExpire) logic.handleCommission(context.Background(), loneUser, &order.Order{ OrderNo: "ORD-LONE-001", Type: OrderTypeSubscribe, IsNew: true, Amount: 100, FeeAmount: 0, CreatedAt: time.Now(), }) // 订阅到期时间不应该被延长 var subAfter user.Subscribe if err := db.First(&subAfter, loneSub.Id).Error; err != nil { t.Fatalf("query subscribe failed: %v", err) } if !subAfter.ExpireTime.Equal(baseExpire) { t.Fatalf("expected no gift days for user without inviter, before=%v after=%v", baseExpire, subAfter.ExpireTime) } // 不应产生赠天日志 var giftCount int64 if err := db.Model(&modelLog.SystemLog{}).Where("type = ?", modelLog.TypeGift.Uint8()).Count(&giftCount).Error; err != nil { t.Fatalf("count gift logs failed: %v", err) } if giftCount != 0 { t.Fatalf("expected 0 gift logs for user without inviter, got %d", giftCount) } } // 先绑码后首单 → 双方赠N天 func TestInviteFlow_BindThenFirstOrder_GrantGiftDays(t *testing.T) { logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{ ReferralPercentage: 0, OnlyFirstPurchase: false, GiftDays: 2, }) defer cleanup() referee := seedUser(t, db, 0, false) referer := seedUser(t, db, 0, false) referer.ReferCode = fmt.Sprintf("REF-%d", referer.Id) if err := db.Model(&user.User{}).Where("id = ?", referer.Id).Update("refer_code", referer.ReferCode).Error; err != nil { t.Fatalf("update referer code failed: %v", err) } refereeBaseExpire := time.Now().Add(48 * time.Hour).Truncate(time.Second) refererBaseExpire := time.Now().Add(72 * time.Hour).Truncate(time.Second) refereeSub := seedActiveSubscribe(t, db, referee.Id, refereeBaseExpire) refererSub := seedActiveSubscribe(t, db, referer.Id, refererBaseExpire) ctx := context.WithValue(context.Background(), constant.CtxKeyUser, referee) bindLogic := userLogic.NewBindInviteCodeLogic(ctx, logic.svc) if err := bindLogic.BindInviteCode(&types.BindInviteCodeRequest{InviteCode: referer.ReferCode}); err != nil { t.Fatalf("bind invite code failed: %v", err) } var refereeAfterBind user.User if err := db.First(&refereeAfterBind, referee.Id).Error; err != nil { t.Fatalf("query referee after bind failed: %v", err) } if refereeAfterBind.RefererId != referer.Id { t.Fatalf("bind invite failed, expected referer_id=%d got=%d", referer.Id, refereeAfterBind.RefererId) } // 首单 IsNew=true → 双方赠N天 logic.handleCommission(context.Background(), &refereeAfterBind, &order.Order{ OrderNo: "ORD-FLOW-001", Type: OrderTypeSubscribe, IsNew: true, Amount: 100, FeeAmount: 0, CreatedAt: time.Now(), }) assertExpireIncreasedByDays(t, db, refereeSub.Id, refereeBaseExpire, 2) assertExpireIncreasedByDays(t, db, refererSub.Id, refererBaseExpire, 2) } // 先买订单后绑码再续费 → 不赠送(IsNew=false) func TestInviteFlow_OrderThenBind_NoGiftDays(t *testing.T) { logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{ ReferralPercentage: 0, OnlyFirstPurchase: false, GiftDays: 2, }) defer cleanup() referee := seedUser(t, db, 0, false) referer := seedUser(t, db, 0, false) referee.RefererId = referer.Id baseExpire := time.Now().Add(72 * time.Hour).Truncate(time.Second) refereeSub := seedActiveSubscribe(t, db, referee.Id, baseExpire) refererSub := seedActiveSubscribe(t, db, referer.Id, baseExpire) // 先前已有订单,IsNew=false(模拟先买订单后绑码的场景) logic.handleCommission(context.Background(), referee, &order.Order{ OrderNo: "ORD-FLOW-002", Type: OrderTypeSubscribe, IsNew: false, // 已有历史订单 Amount: 100, FeeAmount: 0, CreatedAt: time.Now(), }) assertExpireIncreasedByDays(t, db, refereeSub.Id, baseExpire, 0) assertExpireIncreasedByDays(t, db, refererSub.Id, baseExpire, 0) } func TestHandleCommission_GiftDaysToRefereeFamilyOwnerWhenChannelFirstOrder(t *testing.T) { logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{ ReferralPercentage: 10, OnlyFirstPurchase: false, GiftDays: 2, }) defer cleanup() refereeOwner := seedUser(t, db, 0, false) referee := seedUser(t, db, 0, false) referer := seedUser(t, db, 0, false) seedFamily(t, db, refereeOwner.Id, referee.Id) referee.RefererId = referer.Id ownerBaseExpire := time.Now().Add(96 * time.Hour).Truncate(time.Second) ownerSub := seedActiveSubscribe(t, db, refereeOwner.Id, ownerBaseExpire) logic.handleCommission(context.Background(), referee, &order.Order{ OrderNo: "ORD-FAMILY-REFEREE-001", Type: OrderTypeSubscribe, IsNew: true, Amount: 100, FeeAmount: 0, SubscriptionUserId: refereeOwner.Id, CreatedAt: time.Now(), }) assertExpireIncreasedByDays(t, db, ownerSub.Id, ownerBaseExpire, 2) assertUserCommission(t, db, referer.Id, 10) } func TestHandleCommission_GiftDaysToRefererFamilyOwnerWhenCommissionDisabled(t *testing.T) { logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{ ReferralPercentage: 0, OnlyFirstPurchase: false, GiftDays: 2, }) defer cleanup() referee := seedUser(t, db, 0, false) refererOwner := seedUser(t, db, 0, false) referer := seedUser(t, db, 0, false) seedFamily(t, db, refererOwner.Id, referer.Id) referee.RefererId = referer.Id refereeBaseExpire := time.Now().Add(72 * time.Hour).Truncate(time.Second) refererOwnerBaseExpire := time.Now().Add(96 * time.Hour).Truncate(time.Second) refereeSub := seedActiveSubscribe(t, db, referee.Id, refereeBaseExpire) refererOwnerSub := seedActiveSubscribe(t, db, refererOwner.Id, refererOwnerBaseExpire) logic.handleCommission(context.Background(), referee, &order.Order{ OrderNo: "ORD-FAMILY-REFERER-001", Type: OrderTypeSubscribe, IsNew: true, Amount: 100, FeeAmount: 0, CreatedAt: time.Now(), }) assertExpireIncreasedByDays(t, db, refereeSub.Id, refereeBaseExpire, 2) assertExpireIncreasedByDays(t, db, refererOwnerSub.Id, refererOwnerBaseExpire, 2) } func TestHandleCommission_RefererFamilyMemberCommissionBehaviorUnchanged(t *testing.T) { logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{ ReferralPercentage: 10, OnlyFirstPurchase: false, GiftDays: 2, }) defer cleanup() referee := seedUser(t, db, 0, false) refererOwner := seedUser(t, db, 0, false) referer := seedUser(t, db, 0, false) seedFamily(t, db, refererOwner.Id, referer.Id) referee.RefererId = referer.Id refereeBaseExpire := time.Now().Add(96 * time.Hour).Truncate(time.Second) refereeSub := seedActiveSubscribe(t, db, referee.Id, refereeBaseExpire) logic.handleCommission(context.Background(), referee, &order.Order{ OrderNo: "ORD-FAMILY-COMMISSION-001", Type: OrderTypeSubscribe, IsNew: true, Amount: 100, FeeAmount: 0, CreatedAt: time.Now(), }) assertExpireIncreasedByDays(t, db, refereeSub.Id, refereeBaseExpire, 2) assertUserCommission(t, db, referer.Id, 10) assertUserCommission(t, db, refererOwner.Id, 0) } func TestHandleCommission_GiftDaysRecognizesUnlimitedSubscription(t *testing.T) { logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{ ReferralPercentage: 10, OnlyFirstPurchase: false, GiftDays: 2, }) defer cleanup() referee := seedUser(t, db, 0, false) referer := seedUser(t, db, 0, false) referee.RefererId = referer.Id unlimitedExpire := time.UnixMilli(0) refereeSub := seedActiveSubscribe(t, db, referee.Id, unlimitedExpire) logic.handleCommission(context.Background(), referee, &order.Order{ OrderNo: "ORD-UNLIMITED-001", Type: OrderTypeSubscribe, IsNew: true, Amount: 100, FeeAmount: 0, CreatedAt: time.Now(), }) assertExpireIncreasedByDays(t, db, refereeSub.Id, unlimitedExpire, 0) var giftCount int64 if err := db.Model(&modelLog.SystemLog{}).Where("type = ? AND object_id = ?", modelLog.TypeGift.Uint8(), referee.Id).Count(&giftCount).Error; err != nil { t.Fatalf("count gift logs failed: %v", err) } if giftCount != 1 { t.Fatalf("expected 1 gift log for unlimited subscription, got %d", giftCount) } } func TestHandleCommission_IdempotentForRepeatedActivation(t *testing.T) { logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{ ReferralPercentage: 10, OnlyFirstPurchase: false, GiftDays: 2, }) defer cleanup() referee := seedUser(t, db, 0, false) referer := seedUser(t, db, 0, false) referee.RefererId = referer.Id baseExpire := time.Now().Add(96 * time.Hour).Truncate(time.Second) refereeSub := seedActiveSubscribe(t, db, referee.Id, baseExpire) orderInfo := &order.Order{ OrderNo: "ORD-IDEMPOTENT-001", Type: OrderTypeSubscribe, IsNew: true, Amount: 100, FeeAmount: 0, CreatedAt: time.Now(), } logic.handleCommission(context.Background(), referee, orderInfo) logic.handleCommission(context.Background(), referee, orderInfo) assertExpireIncreasedByDays(t, db, refereeSub.Id, baseExpire, 2) assertUserCommission(t, db, referer.Id, 10) assertLogCountForOrder(t, db, modelLog.TypeCommission.Uint8(), orderInfo.OrderNo, 1) assertLogCountForOrder(t, db, modelLog.TypeGift.Uint8(), orderInfo.OrderNo, 1) } func TestHandleCommission_NoActiveSubscriptionWritesSkippedGiftLog(t *testing.T) { logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{ ReferralPercentage: 10, OnlyFirstPurchase: false, GiftDays: 2, }) defer cleanup() referee := seedUser(t, db, 0, false) referer := seedUser(t, db, 0, false) referee.RefererId = referer.Id orderInfo := &order.Order{ OrderNo: "ORD-SKIPPED-001", Type: OrderTypeSubscribe, IsNew: true, Amount: 100, FeeAmount: 0, CreatedAt: time.Now(), } logic.handleCommission(context.Background(), referee, orderInfo) assertUserCommission(t, db, referer.Id, 10) assertLogCountForOrder(t, db, modelLog.TypeCommission.Uint8(), orderInfo.OrderNo, 1) assertLogCountForOrder(t, db, modelLog.TypeGift.Uint8(), orderInfo.OrderNo, 1) assertGiftLogRemarkContains(t, db, orderInfo.OrderNo, "skipped: no active subscription") } func TestHandleCommission_GiftDaysZeroDoesNotWriteGiftLog(t *testing.T) { logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{ ReferralPercentage: 10, OnlyFirstPurchase: false, GiftDays: 0, }) defer cleanup() referee := seedUser(t, db, 0, false) referer := seedUser(t, db, 0, false) referee.RefererId = referer.Id baseExpire := time.Now().Add(96 * time.Hour).Truncate(time.Second) refereeSub := seedActiveSubscribe(t, db, referee.Id, baseExpire) orderInfo := &order.Order{ OrderNo: "ORD-GIFT-ZERO-001", Type: OrderTypeSubscribe, IsNew: true, Amount: 100, FeeAmount: 0, CreatedAt: time.Now(), } logic.handleCommission(context.Background(), referee, orderInfo) assertExpireIncreasedByDays(t, db, refereeSub.Id, baseExpire, 0) assertUserCommission(t, db, referer.Id, 10) assertLogCountForOrder(t, db, modelLog.TypeCommission.Uint8(), orderInfo.OrderNo, 1) assertLogCountForOrder(t, db, modelLog.TypeGift.Uint8(), orderInfo.OrderNo, 0) } func TestHandleCommission_GlobalOnlyFirstPurchaseBlocksCommissionAndGiftForNonFirstOrder(t *testing.T) { logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{ ReferralPercentage: 10, OnlyFirstPurchase: true, GiftDays: 2, }) defer cleanup() referee := seedUser(t, db, 0, false) referer := seedUser(t, db, 0, false) referee.RefererId = referer.Id baseExpire := time.Now().Add(96 * time.Hour).Truncate(time.Second) refereeSub := seedActiveSubscribe(t, db, referee.Id, baseExpire) orderInfo := &order.Order{ OrderNo: "ORD-NONFIRST-GLOBAL-001", Type: OrderTypeRenewal, IsNew: false, Amount: 100, FeeAmount: 0, CreatedAt: time.Now(), } logic.handleCommission(context.Background(), referee, orderInfo) assertExpireIncreasedByDays(t, db, refereeSub.Id, baseExpire, 0) assertUserCommission(t, db, referer.Id, 0) assertLogCountForOrder(t, db, modelLog.TypeCommission.Uint8(), orderInfo.OrderNo, 0) assertLogCountForOrder(t, db, modelLog.TypeGift.Uint8(), orderInfo.OrderNo, 0) } func TestHandleCommission_BothFamilySidesUseCorrectGiftOwners(t *testing.T) { logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{ ReferralPercentage: 0, OnlyFirstPurchase: false, GiftDays: 2, }) defer cleanup() refereeOwner := seedUser(t, db, 0, false) referee := seedUser(t, db, 0, false) refererOwner := seedUser(t, db, 0, false) referer := seedUser(t, db, 0, false) seedFamily(t, db, refereeOwner.Id, referee.Id) seedFamily(t, db, refererOwner.Id, referer.Id) referee.RefererId = referer.Id refereeOwnerBaseExpire := time.Now().Add(72 * time.Hour).Truncate(time.Second) refererOwnerBaseExpire := time.Now().Add(96 * time.Hour).Truncate(time.Second) refereeOwnerSub := seedActiveSubscribe(t, db, refereeOwner.Id, refereeOwnerBaseExpire) refererOwnerSub := seedActiveSubscribe(t, db, refererOwner.Id, refererOwnerBaseExpire) orderInfo := &order.Order{ OrderNo: "ORD-BOTH-FAMILIES-001", Type: OrderTypeSubscribe, IsNew: true, Amount: 100, FeeAmount: 0, SubscriptionUserId: refereeOwner.Id, CreatedAt: time.Now(), } logic.handleCommission(context.Background(), referee, orderInfo) assertExpireIncreasedByDays(t, db, refereeOwnerSub.Id, refereeOwnerBaseExpire, 2) assertExpireIncreasedByDays(t, db, refererOwnerSub.Id, refererOwnerBaseExpire, 2) assertNoSubscribeForUser(t, db, referee.Id) assertNoSubscribeForUser(t, db, referer.Id) assertLogCountForOrder(t, db, modelLog.TypeGift.Uint8(), orderInfo.OrderNo, 2) } func setupInviteTestLogic(t *testing.T, inviteCfg config.InviteConfig) (*ActivateOrderLogic, *gorm.DB, func()) { t.Helper() mysqlAddr := getenvDefault("TEST_MYSQL_ADDR", "127.0.0.1:3306") mysqlUser := getenvDefault("TEST_MYSQL_USER", "root") mysqlPassword := getenvDefault("TEST_MYSQL_PASSWORD", "rootpassword") adminDSN := fmt.Sprintf("%s:%s@tcp(%s)/?charset=utf8mb4&parseTime=true&loc=Local&multiStatements=true", mysqlUser, mysqlPassword, mysqlAddr) adminDB, err := gorm.Open(mysql.Open(adminDSN), &gorm.Config{}) if err != nil { t.Fatalf("open mysql admin connection failed: %v", err) } dbName := fmt.Sprintf("ppanel_test_invite_%d", time.Now().UnixNano()) if err := adminDB.Exec(fmt.Sprintf("CREATE DATABASE `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci", dbName)).Error; err != nil { t.Fatalf("create test database failed: %v", err) } testDSN := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=true&loc=Local", mysqlUser, mysqlPassword, mysqlAddr, dbName) db, err := gorm.Open(mysql.Open(testDSN), &gorm.Config{}) if err != nil { t.Fatalf("open test database failed: %v", err) } if err := db.AutoMigrate(&user.User{}, &user.Device{}, &user.AuthMethods{}, &user.Subscribe{}, &user.UserFamily{}, &user.UserFamilyMember{}, &modelLog.SystemLog{}); err != nil { t.Fatalf("auto migrate failed: %v", err) } redisAddr := getenvDefault("TEST_REDIS_ADDR", "127.0.0.1:6379") redisPassword := getenvDefault("TEST_REDIS_PASSWORD", "") rdb := redis.NewClient(&redis.Options{ Addr: redisAddr, Password: redisPassword, DB: 0, }) if err := rdb.Ping(context.Background()).Err(); err != nil { t.Fatalf("connect redis failed: %v", err) } _ = rdb.FlushDB(context.Background()).Err() svcCtx := &svc.ServiceContext{ DB: db, Redis: rdb, UserModel: user.NewModel(db, rdb), LogModel: modelLog.NewModel(db), Config: config.Config{ Invite: inviteCfg, }, } return NewActivateOrderLogic(svcCtx), db, func() { _ = rdb.Close() sqlDB, _ := db.DB() if sqlDB != nil { _ = sqlDB.Close() } _ = adminDB.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS `%s`", dbName)).Error } } func seedUser(t *testing.T, db *gorm.DB, referralPercentage uint8, onlyFirstPurchase bool) *user.User { t.Helper() u := &user.User{ Password: "pwd", Algo: "default", ReferralPercentage: referralPercentage, OnlyFirstPurchase: boolPtr(onlyFirstPurchase), Enable: boolPtr(true), IsAdmin: boolPtr(false), EnableBalanceNotify: boolPtr(false), EnableLoginNotify: boolPtr(false), EnableSubscribeNotify: boolPtr(false), EnableTradeNotify: boolPtr(false), } if err := db.Create(u).Error; err != nil { t.Fatalf("seed user failed: %v", err) } return u } func seedFamily(t *testing.T, db *gorm.DB, ownerID int64, memberID int64) { t.Helper() family := &user.UserFamily{ OwnerUserId: ownerID, MaxMembers: 3, Status: user.FamilyStatusActive, } if err := db.Create(family).Error; err != nil { t.Fatalf("seed family failed: %v", err) } now := time.Now() members := []user.UserFamilyMember{ { FamilyId: family.Id, UserId: ownerID, Role: user.FamilyRoleOwner, Status: user.FamilyMemberActive, JoinSource: "test", JoinedAt: now, }, { FamilyId: family.Id, UserId: memberID, Role: user.FamilyRoleMember, Status: user.FamilyMemberActive, JoinSource: "test", JoinedAt: now, }, } if err := db.Create(&members).Error; err != nil { t.Fatalf("seed family members failed: %v", err) } } func seedActiveSubscribe(t *testing.T, db *gorm.DB, userID int64, expireAt time.Time) *user.Subscribe { t.Helper() sub := &user.Subscribe{ UserId: userID, OrderId: 1, SubscribeId: 1, StartTime: time.Now().Add(-24 * time.Hour), ExpireTime: expireAt, Traffic: 1024, Token: fmt.Sprintf("token-%d-%d", userID, time.Now().UnixNano()), UUID: fmt.Sprintf("uuid-%d-%d", userID, time.Now().UnixNano()), Status: 1, } if err := db.Create(sub).Error; err != nil { t.Fatalf("seed subscribe failed: %v", err) } return sub } func assertExpireIncreasedByDays(t *testing.T, db *gorm.DB, subscribeID int64, before time.Time, days int) { t.Helper() var after user.Subscribe if err := db.First(&after, subscribeID).Error; err != nil { t.Fatalf("query subscribe failed: %v", err) } expected := before.Add(time.Duration(days) * 24 * time.Hour) if !after.ExpireTime.Equal(expected) { t.Fatalf("expire time mismatch, expected=%v got=%v", expected, after.ExpireTime) } } func assertUserCommission(t *testing.T, db *gorm.DB, userID int64, expected int64) { t.Helper() var u user.User if err := db.First(&u, userID).Error; err != nil { t.Fatalf("query user failed: %v", err) } if u.Commission != expected { t.Fatalf("expected user %d commission=%d, got %d", userID, expected, u.Commission) } } func assertLogCountForOrder(t *testing.T, db *gorm.DB, logType uint8, orderNo string, expected int64) { t.Helper() var count int64 if err := db.Model(&modelLog.SystemLog{}). Where("type = ? AND content LIKE ?", logType, "%"+orderNo+"%"). Count(&count).Error; err != nil { t.Fatalf("count logs failed: %v", err) } if count != expected { t.Fatalf("expected log type %d count=%d for order %s, got %d", logType, expected, orderNo, count) } } func assertGiftLogRemarkContains(t *testing.T, db *gorm.DB, orderNo string, want string) { t.Helper() var row modelLog.SystemLog if err := db.Model(&modelLog.SystemLog{}). Where("type = ? AND content LIKE ?", modelLog.TypeGift.Uint8(), "%"+orderNo+"%"). First(&row).Error; err != nil { t.Fatalf("query gift log failed: %v", err) } if !strings.Contains(row.Content, want) { t.Fatalf("expected gift log content to contain %q, got %s", want, row.Content) } } func assertNoSubscribeForUser(t *testing.T, db *gorm.DB, userID int64) { t.Helper() var count int64 if err := db.Model(&user.Subscribe{}).Where("user_id = ?", userID).Count(&count).Error; err != nil { t.Fatalf("count subscribes failed: %v", err) } if count != 0 { t.Fatalf("expected user %d to have no direct subscriptions, got %d", userID, count) } } func boolPtr(v bool) *bool { return &v } func getenvDefault(key, fallback string) string { v := os.Getenv(key) if v == "" { return fallback } return v }