package invitation import ( "context" "fmt" "testing" "time" "hfb_sys/backend/internal/database" "hfb_sys/backend/internal/model" "gorm.io/gorm" ) func setupTestDB(t *testing.T) *gorm.DB { db := database.NewTestDB() if err := database.AutoMigrateIfMissing(db, &model.User{}, &model.UserInvitation{}, &model.InvitationRebateConfig{}, &model.InvitationRebateLedger{}, &model.RentalOrder{}, &model.DistributionSite{}, &model.DistributionOrderParty{}, &model.WalletAccount{}, &model.WalletLedger{}, ); err != nil { t.Fatalf("数据库迁移失败: %v", err) } return db } func TestCalculateRebateRate(t *testing.T) { now := time.Now() cfg := &model.InvitationRebateConfig{ FirstMonthRate: 0.60, SecondMonthRate: 0.30, SubsequentRate: 0.15, Enabled: true, } // 10 天(首月) rate, cycle, days := CalculateRebateRate(now.AddDate(0, 0, -10), now, cfg) if cycle != CycleFirstMonth || rate != 0.60 || days != 10 { t.Fatalf("首月比例计算错误: cycle=%s, rate=%v, days=%d", cycle, rate, days) } // 30 天(首月边界) rate, cycle, days = CalculateRebateRate(now.AddDate(0, 0, -30), now, cfg) if cycle != CycleFirstMonth || rate != 0.60 || days != 30 { t.Fatalf("首月30天边界计算错误: cycle=%s, rate=%v, days=%d", cycle, rate, days) } // 45 天(次月) rate, cycle, days = CalculateRebateRate(now.AddDate(0, 0, -45), now, cfg) if cycle != CycleSecondMonth || rate != 0.30 || days != 45 { t.Fatalf("次月比例计算错误: cycle=%s, rate=%v, days=%d", cycle, rate, days) } // 60 天(次月边界) rate, cycle, days = CalculateRebateRate(now.AddDate(0, 0, -60), now, cfg) if cycle != CycleSecondMonth || rate != 0.30 || days != 60 { t.Fatalf("次月60天边界计算错误: cycle=%s, rate=%v, days=%d", cycle, rate, days) } // 90 天(往后长期) rate, cycle, days = CalculateRebateRate(now.AddDate(0, 0, -90), now, cfg) if cycle != CycleSubsequent || rate != 0.15 || days != 90 { t.Fatalf("长期比例计算错误: cycle=%s, rate=%v, days=%d", cycle, rate, days) } } func TestInvitationFlowAndOrderRebate(t *testing.T) { db := setupTestDB(t) repo := NewRepository(db) ctx := context.Background() // 1. 创建邀请人 Inviter (已实名) inviterCode := "inv0001" inviter := model.User{ Phone: "13800000001", Nickname: "邀请人老张", RealnameStatus: "verified", Status: "active", InviteCode: &inviterCode, } if err := db.Create(&inviter).Error; err != nil { t.Fatalf("创建邀请人失败: %v", err) } // 2. 创建被邀请人 Invitee (未实名) invitee := model.User{ Phone: "13800000002", Nickname: "新人小李", RealnameStatus: "unverified", Status: "active", } if err := db.Create(&invitee).Error; err != nil { t.Fatalf("创建被邀请人失败: %v", err) } // 3. 绑定邀请码 if err := repo.BindInvitation(ctx, invitee.ID, inviterCode, 0); err != nil { t.Fatalf("绑定邀请码失败: %v", err) } // 校验处于 pending 状态(未实名尚未正式生效) var invRecord model.UserInvitation if err := db.Where("invitee_id = ?", invitee.ID).First(&invRecord).Error; err != nil { t.Fatalf("查询邀请记录失败: %v", err) } if invRecord.Status != StatusPending || invRecord.VerifiedAt != nil { t.Fatalf("邀请状态应为 pending,实际: %s", invRecord.Status) } // 4. 被邀请人完成实名认证 if err := db.Model(&model.User{}).Where("id = ?", invitee.ID).Update("realname_status", "verified").Error; err != nil { t.Fatalf("更新实名失败: %v", err) } if err := repo.OnUserRealnameVerified(ctx, invitee.ID); err != nil { t.Fatalf("触发实名生效钩子失败: %v", err) } if err := db.Where("invitee_id = ?", invitee.ID).First(&invRecord).Error; err != nil { t.Fatalf("查询生效后邀请记录失败: %v", err) } if invRecord.Status != StatusActive || invRecord.VerifiedAt == nil { t.Fatalf("邀请状态应为 active 且包含 verified_at,实际: %s", invRecord.Status) } // 检查被邀请人自身是否自动生成了邀请码 var updatedInvitee model.User if err := db.First(&updatedInvitee, invitee.ID).Error; err != nil { t.Fatalf("查询被邀请人失败: %v", err) } if updatedInvitee.InviteCode == nil || *updatedInvitee.InviteCode == "" { t.Fatalf("实名通过后自身应自动生成邀请码") } // 5. 模拟订单完成终态结算 orderNo := fmt.Sprintf("ORD-%d", time.Now().UnixNano()) order := model.RentalOrder{ OrderNo: orderNo, RenterID: invitee.ID, // 被邀请人作为买家 OwnerID: 99999, // 独立卖家(无邀请人) Status: "completed", SettlementStatus: "settled", RentAmountCent: 20000, // 200元 OwnerRentAmountCent: 10000, // 100元号主成本 // 平台毛利: 10000 (100元) // 单边基准: 5000 (50元) // 首月 60% 返利: 5000 * 0.6 = 3000 (30元) } if err := db.Create(&order).Error; err != nil { t.Fatalf("创建测试订单失败: %v", err) } if err := repo.ProcessOrderInvitationRebate(ctx, db, order.ID); err != nil { t.Fatalf("订单返利处理失败: %v", err) } // 验证流水表 var ledgers []model.InvitationRebateLedger if err := db.Where("order_id = ?", order.ID).Find(&ledgers).Error; err != nil { t.Fatalf("查询返利记录失败: %v", err) } if len(ledgers) != 1 { t.Fatalf("应生成 1 条买家端返利流水,实际: %d", len(ledgers)) } l := ledgers[0] if l.InviterID != inviter.ID || l.TradeRole != "buyer" || l.RebateAmountCent != 3000 { t.Fatalf("返利金额错误: expected=3000, got=%d, role=%s", l.RebateAmountCent, l.TradeRole) } // 验证邀请人钱包余额 var walletAcct model.WalletAccount if err := db.Where("user_id = ? AND distribution_site_id = 0", inviter.ID).First(&walletAcct).Error; err != nil { t.Fatalf("查询邀请人钱包失败: %v", err) } if walletAcct.AvailableBalanceCent != 3000 { t.Fatalf("邀请人可用余额错误: expected=3000, got=%d", walletAcct.AvailableBalanceCent) } // 幂等性测试:再次触发不应重复生成 if err := repo.ProcessOrderInvitationRebate(ctx, db, order.ID); err != nil { t.Fatalf("再次调用不应报错: %v", err) } var count int64 db.Model(&model.InvitationRebateLedger{}).Where("order_id = ?", order.ID).Count(&count) if count != 1 { t.Fatalf("流水重复写入! count=%d", count) } } func TestCrossSiteDistributionRebate(t *testing.T) { db := setupTestDB(t) repo := NewRepository(db) ctx := context.Background() // 邀请人 1 (邀请了买家) c1 := "code001" buyerInviter := model.User{Phone: "13900000001", RealnameStatus: "verified", Status: "active", InviteCode: &c1} db.Create(&buyerInviter) // 邀请人 2 (邀请了卖家) c2 := "code002" sellerInviter := model.User{Phone: "13900000002", RealnameStatus: "verified", Status: "active", InviteCode: &c2} db.Create(&sellerInviter) // 买家 now := time.Now() buyer := model.User{Phone: "13900000003", RealnameStatus: "verified", Status: "active"} db.Create(&buyer) db.Create(&model.UserInvitation{InviterID: buyerInviter.ID, InviteeID: buyer.ID, Status: StatusActive, VerifiedAt: &now}) // 卖家 seller := model.User{Phone: "13900000004", RealnameStatus: "verified", Status: "active"} db.Create(&seller) db.Create(&model.UserInvitation{InviterID: sellerInviter.ID, InviteeID: seller.ID, Status: StatusActive, VerifiedAt: &now}) // 分销站点收号拆分: // 主站卖出分站收的账号 (site) // platformSellCent = 4000 (主站利润 40 元) // distributorReceiveCent = 6000 (分站利润 60 元) distSite := model.DistributionSite{ SiteName: "加盟分站88", SiteCode: "site88", Domain: "sub88.example.com", Status: "active", } if err := db.Create(&distSite).Error; err != nil { t.Fatalf("创建测试分销站点失败: %v", err) } distributorSiteID := distSite.ID order := model.RentalOrder{ OrderNo: fmt.Sprintf("ORD-CROSS-%d", time.Now().UnixNano()), RenterID: buyer.ID, OwnerID: seller.ID, Status: "completed", SettlementStatus: "settled", RentAmountCent: 30000, OwnerRentAmountCent: 20000, // 总毛利 10000 } db.Create(&order) if err := db.Create(&model.DistributionOrderParty{ OrderID: order.ID, SiteID: distributorSiteID, PartyRole: "platform_sell", PartyType: "platform", PartyID: 0, ProfitAmountCent: 4000, IncomeAmountCent: 4000, }).Error; err != nil { t.Fatalf("创建平台分成方失败: %v", err) } if err := db.Create(&model.DistributionOrderParty{ OrderID: order.ID, SiteID: distributorSiteID, PartyRole: "distributor_receive", PartyType: "site", PartyID: distributorSiteID, ProfitAmountCent: 6000, IncomeAmountCent: 6000, }).Error; err != nil { t.Fatalf("创建分销分成方失败: %v", err) } if err := repo.ProcessOrderInvitationRebate(ctx, db, order.ID); err != nil { t.Fatalf("跨站分销返利结算失败: %v", err) } // 验证买家端流水:基数 4000 / 2 = 2000,首月 60% = 1200,站点为 0 (主站) var buyerLedger model.InvitationRebateLedger if err := db.Where("order_id = ? AND trade_role = 'buyer'", order.ID).First(&buyerLedger).Error; err != nil { t.Fatalf("未找到买家端流水: %v", err) } if buyerLedger.DistributionSiteID != 0 || buyerLedger.RebateAmountCent != 1200 { t.Fatalf("买家端跨站返利错误: siteID=%d, amount=%d", buyerLedger.DistributionSiteID, buyerLedger.RebateAmountCent) } // 验证卖家端流水:基数 6000 / 2 = 3000,首月 60% = 1800,站点为 88 (分站) var sellerLedger model.InvitationRebateLedger if err := db.Where("order_id = ? AND trade_role = 'seller'", order.ID).First(&sellerLedger).Error; err != nil { t.Fatalf("未找到卖家端流水: %v", err) } if sellerLedger.DistributionSiteID != distributorSiteID || sellerLedger.RebateAmountCent != 1800 { t.Fatalf("卖家端跨站返利错误: siteID=%d, amount=%d", sellerLedger.DistributionSiteID, sellerLedger.RebateAmountCent) } }