package wallet import ( "context" "testing" "hfb_sys/backend/internal/model" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) // setupTestDB 创建测试数据库连接(使用内存 SQLite) func setupTestDB(t *testing.T) *gorm.DB { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { t.Fatalf("无法创建测试数据库连接: %v", err) } // 迁移必要的表 if err := db.AutoMigrate( &model.WalletAccount{}, &model.WalletLedger{}, ); 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) } } // TestRepositoryRechargeIncreasesAvailableBalance 测试充值增加可用余额 func TestRepositoryRechargeIncreasesAvailableBalance(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewRepository(db) ctx := context.Background() userID := uint64(1002) // 第一次充值 account, err := repo.Recharge(ctx, userID, 10000) if err != nil { t.Fatalf("Recharge() error = %v", err) } if account.AvailableBalanceCent != 10000 { t.Fatalf("第一次充值后余额 = %d, want 10000", account.AvailableBalanceCent) } // 第二次充值 account, err = repo.Recharge(ctx, userID, 5000) if err != nil { t.Fatalf("Recharge() error = %v", err) } if account.AvailableBalanceCent != 15000 { t.Fatalf("第二次充值后余额 = %d, want 15000", account.AvailableBalanceCent) } // 验证账本记录 ledger, err := repo.Ledger(ctx, userID, 1, 10) if err != nil { t.Fatalf("Ledger() error = %v", err) } if ledger.Total != 2 { t.Fatalf("账本记录数 = %d, want 2", ledger.Total) } } // TestRepositoryConfirmRechargeFromChannelIsIdempotent 测试渠道充值幂等性 func TestRepositoryConfirmRechargeFromChannelIsIdempotent(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewRepository(db) ctx := context.Background() userID := uint64(1003) bizNo := "PAY123456" amount := int64(10000) // 第一次确认充值 err := repo.ConfirmRechargeFromChannel(ctx, userID, bizNo, amount) if err != nil { t.Fatalf("第一次 ConfirmRechargeFromChannel() error = %v", err) } account, _ := repo.Account(ctx, userID) if account.AvailableBalanceCent != amount { t.Fatalf("第一次充值后余额 = %d, want %d", account.AvailableBalanceCent, amount) } // 第二次确认充值(相同 bizNo)应该幂等,不重复入账 err = repo.ConfirmRechargeFromChannel(ctx, userID, bizNo, amount) if err != nil { t.Fatalf("第二次 ConfirmRechargeFromChannel() error = %v", err) } account, _ = repo.Account(ctx, userID) if account.AvailableBalanceCent != amount { t.Fatalf("第二次充值后余额 = %d, want %d(应保持不变)", account.AvailableBalanceCent, amount) } // 验证只有一条账本记录 ledger, _ := repo.Ledger(ctx, userID, 1, 10) if ledger.Total != 1 { t.Fatalf("账本记录数 = %d, want 1(幂等)", ledger.Total) } } // TestRepositoryConfirmRechargeFromChannelRejectsInvalidParams 测试参数验证 func TestRepositoryConfirmRechargeFromChannelRejectsInvalidParams(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewRepository(db) testCases := []struct { name string userID uint64 bizNo string amount int64 wantError error }{ {"userID 为 0", 0, "BIZ123", 1000, ErrInvalidAmount}, {"bizNo 为空", 1004, "", 1000, ErrInvalidAmount}, {"amount 为 0", 1004, "BIZ123", 0, ErrInvalidAmount}, {"amount 为负数", 1004, "BIZ123", -1000, ErrInvalidAmount}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { err := repo.ConfirmRechargeFromChannel(context.Background(), tc.userID, tc.bizNo, tc.amount) if err != tc.wantError { t.Fatalf("error = %v, want %v", err, tc.wantError) } }) } } // 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)) } }