package wallet import ( "context" "testing" "hfb_sys/backend/internal/database" "hfb_sys/backend/internal/model" "gorm.io/gorm" ) // setupTestDB 创建测试数据库连接(MySQL 测试库) func setupTestDB(t *testing.T) *gorm.DB { db := database.NewTestDB() // 迁移必要的表 if err := database.AutoMigrateIfMissing(db, &model.WalletAccount{}, &model.WalletLedger{}, &model.DistributionSite{}, ); err != nil { t.Fatalf("数据库迁移失败: %v", err) } return db } // cleanupTestDB 清理测试数据(内存数据库无需清理) func cleanupTestDB(t *testing.T, db *gorm.DB) { // 内存数据库,测试结束后自动清理 } // TestRepositoryAccountCreatesAccountIfNotExists 测试账户自动创建 func TestRepositoryAccountCreatesAccountIfNotExists(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewRepository(db) ctx := context.Background() userID := uint64(1001) account, err := repo.Account(ctx, userID) if err != nil { t.Fatalf("Account() error = %v", err) } if account.UserID != userID { t.Fatalf("UserID = %d, want %d", account.UserID, userID) } if account.AvailableBalanceCent != 0 { t.Fatalf("AvailableBalanceCent = %d, want 0", account.AvailableBalanceCent) } if account.FrozenBalanceCent != 0 { t.Fatalf("FrozenBalanceCent = %d, want 0", account.FrozenBalanceCent) } if account.Status != "active" { t.Fatalf("Status = %s, want active", account.Status) } } // TestAppendEntriesUpdatesBalanceCorrectly 测试 AppendEntries 余额计算 func TestAppendEntriesUpdatesBalanceCorrectly(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) userID := uint64(1005) testCases := []struct { name string entries []Entry wantAvailable int64 wantFrozen int64 }{ { name: "可用余额入账", entries: []Entry{ {UserID: userID, Direction: "in", AmountCent: 10000, BalanceType: "available", BizType: "test", BizNo: "T1"}, }, wantAvailable: 10000, wantFrozen: 0, }, { name: "冻结余额入账", entries: []Entry{ {UserID: userID, Direction: "in", AmountCent: 5000, BalanceType: "frozen", BizType: "test", BizNo: "T2"}, }, wantAvailable: 10000, wantFrozen: 5000, }, { name: "可用余额出账", entries: []Entry{ {UserID: userID, Direction: "out", AmountCent: 3000, BalanceType: "available", BizType: "test", BizNo: "T3"}, }, wantAvailable: 7000, wantFrozen: 5000, }, { name: "冻结余额出账", entries: []Entry{ {UserID: userID, Direction: "out", AmountCent: 2000, BalanceType: "frozen", BizType: "test", BizNo: "T4"}, }, wantAvailable: 7000, wantFrozen: 3000, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { err := db.Transaction(func(tx *gorm.DB) error { return AppendEntries(tx, tc.entries...) }) if err != nil { t.Fatalf("AppendEntries() error = %v", err) } var account model.WalletAccount db.Where("user_id = ?", userID).First(&account) if account.AvailableBalanceCent != tc.wantAvailable { t.Fatalf("AvailableBalanceCent = %d, want %d", account.AvailableBalanceCent, tc.wantAvailable) } if account.FrozenBalanceCent != tc.wantFrozen { t.Fatalf("FrozenBalanceCent = %d, want %d", account.FrozenBalanceCent, tc.wantFrozen) } }) } } // TestAppendEntriesRejectsInsufficientBalance 测试余额不足时拒绝扣款 func TestAppendEntriesRejectsInsufficientBalance(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) userID := uint64(1006) // 先入账 1000 db.Transaction(func(tx *gorm.DB) error { return AppendEntries(tx, Entry{ UserID: userID, Direction: "in", AmountCent: 1000, BalanceType: "available", BizType: "test", BizNo: "INIT", }) }) // 尝试扣款 2000(余额不足) err := db.Transaction(func(tx *gorm.DB) error { return AppendEntries(tx, Entry{ UserID: userID, Direction: "out", AmountCent: 2000, BalanceType: "available", BizType: "test", BizNo: "FAIL", }) }) if err != ErrInsufficientBalance { t.Fatalf("error = %v, want ErrInsufficientBalance", err) } // 验证余额未变化 var account model.WalletAccount db.Where("user_id = ?", userID).First(&account) if account.AvailableBalanceCent != 1000 { t.Fatalf("余额 = %d, want 1000(回滚后应保持不变)", account.AvailableBalanceCent) } } // TestAppendEntriesSkipsZeroAmount 测试跳过零金额条目 func TestAppendEntriesSkipsZeroAmount(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) userID := uint64(1007) err := db.Transaction(func(tx *gorm.DB) error { return AppendEntries(tx, Entry{UserID: userID, Direction: "in", AmountCent: 0, BalanceType: "available", BizType: "test", BizNo: "ZERO"}, Entry{UserID: userID, Direction: "in", AmountCent: 1000, BalanceType: "available", BizType: "test", BizNo: "VALID"}, ) }) if err != nil { t.Fatalf("AppendEntries() error = %v", err) } // 验证只有 1 条账本记录(零金额被跳过) var count int64 db.Model(&model.WalletLedger{}).Where("user_id = ?", userID).Count(&count) if count != 1 { t.Fatalf("账本记录数 = %d, want 1(零金额应被跳过)", count) } } // TestRepositoryLedgerPagination 测试账本分页 func TestRepositoryLedgerPagination(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewRepository(db) ctx := context.Background() userID := uint64(1008) // 创建 25 条记录 entries := make([]Entry, 25) for i := 0; i < 25; i++ { entries[i] = Entry{ UserID: userID, Direction: "in", AmountCent: 100, BalanceType: "available", BizType: "test", BizNo: "PAGE" + string(rune(i)), } } db.Transaction(func(tx *gorm.DB) error { return AppendEntries(tx, entries...) }) // 测试第一页 page1, err := repo.Ledger(ctx, userID, 1, 10) if err != nil { t.Fatalf("Ledger() page 1 error = %v", err) } if page1.Total != 25 { t.Fatalf("Total = %d, want 25", page1.Total) } // 断言 Items 类型并检查长度 items1, ok := page1.Items.([]LedgerDTO) if !ok { t.Fatalf("Items type = %T, want []LedgerDTO", page1.Items) } if len(items1) != 10 { t.Fatalf("Page 1 items = %d, want 10", len(items1)) } // 测试第三页 page3, err := repo.Ledger(ctx, userID, 3, 10) if err != nil { t.Fatalf("Ledger() page 3 error = %v", err) } items3, ok := page3.Items.([]LedgerDTO) if !ok { t.Fatalf("Items type = %T, want []LedgerDTO", page3.Items) } if len(items3) != 5 { t.Fatalf("Page 3 items = %d, want 5", len(items3)) } } func TestWalletTenantIsolation(t *testing.T) { db := setupTestDB(t) repo := NewRepository(db) ctx := context.Background() userID := uint64(2001) // 创建测试分销站点 site := model.DistributionSite{ ID: 10, SiteCode: "branch_alpha", SiteName: "阿尔法分站", Domain: "alpha.hfb.com", Status: "active", } if err := db.Create(&site).Error; err != nil { t.Fatalf("创建分销站点失败: %v", err) } // 1. 在主站入账 5000 分 err := db.Transaction(func(tx *gorm.DB) error { return AppendEntries(tx, Entry{ UserID: userID, DistributionSiteID: 0, Direction: "in", AmountCent: 5000, BalanceType: "available", BizType: "main_recharge", BizNo: "MAIN_001", Remark: "主站充值", }) }) if err != nil { t.Fatalf("主站充值失败: %v", err) } // 2. 在分销站点入账 3000 分 err = db.Transaction(func(tx *gorm.DB) error { return AppendEntries(tx, Entry{ UserID: userID, DistributionSiteID: site.ID, Direction: "in", AmountCent: 3000, BalanceType: "available", BizType: "site_income", BizNo: "SITE_001", Remark: "分销站收益", }) }) if err != nil { t.Fatalf("分站入账失败: %v", err) } // 3. 校验主站钱包余额 mainAccount, err := repo.Account(ctx, userID) if err != nil { t.Fatalf("查询主站钱包失败: %v", err) } if mainAccount.AvailableBalanceCent != 5000 { t.Fatalf("主站余额 = %d, want 5000", mainAccount.AvailableBalanceCent) } if mainAccount.DistributionSiteID != 0 { t.Fatalf("主站账号 DistributionSiteID = %d, want 0", mainAccount.DistributionSiteID) } // 4. 校验分销站点钱包余额 siteAccount, err := repo.Account(ctx, userID, site.SiteCode) if err != nil { t.Fatalf("查询分站钱包失败: %v", err) } if siteAccount.AvailableBalanceCent != 3000 { t.Fatalf("分站余额 = %d, want 3000", siteAccount.AvailableBalanceCent) } if siteAccount.DistributionSiteID != site.ID { t.Fatalf("分站账号 DistributionSiteID = %d, want %d", siteAccount.DistributionSiteID, site.ID) } // 5. 校验流水隔离 mainLedger, err := repo.Ledger(ctx, userID, 1, 10) if err != nil { t.Fatalf("查询主站流水失败: %v", err) } if mainLedger.Total != 1 { t.Fatalf("主站流水条数 = %d, want 1", mainLedger.Total) } mainItems := mainLedger.Items.([]LedgerDTO) if mainItems[0].BizNo != "MAIN_001" { t.Fatalf("主站流水第一条 BizNo = %s, want MAIN_001", mainItems[0].BizNo) } siteLedger, err := repo.Ledger(ctx, userID, 1, 10, site.SiteCode) if err != nil { t.Fatalf("查询分站流水失败: %v", err) } if siteLedger.Total != 1 { t.Fatalf("分站流水条数 = %d, want 1", siteLedger.Total) } siteItems := siteLedger.Items.([]LedgerDTO) if siteItems[0].BizNo != "SITE_001" { t.Fatalf("分站流水第一条 BizNo = %s, want SITE_001", siteItems[0].BizNo) } // 6. 分站扣款 2000 分,主站余额不受任何影响 err = db.Transaction(func(tx *gorm.DB) error { return AppendEntries(tx, Entry{ UserID: userID, DistributionSiteID: site.ID, Direction: "out", AmountCent: 2000, BalanceType: "available", BizType: "site_withdraw", BizNo: "SITE_WD_001", Remark: "分站提现支出", }) }) if err != nil { t.Fatalf("分站扣款失败: %v", err) } siteAccountAfter, err := repo.Account(ctx, userID, site.SiteCode) if err != nil { t.Fatalf("查询扣款后分站钱包失败: %v", err) } if siteAccountAfter.AvailableBalanceCent != 1000 { t.Fatalf("分站扣款后余额 = %d, want 1000", siteAccountAfter.AvailableBalanceCent) } mainAccountAfter, err := repo.Account(ctx, userID) if err != nil { t.Fatalf("再次查询主站钱包失败: %v", err) } if mainAccountAfter.AvailableBalanceCent != 5000 { t.Fatalf("主站余额受影响: got %d, want 5000", mainAccountAfter.AvailableBalanceCent) } }