From d6f8ac0226a16e9e8f96bb0383a5626d88f9f081 Mon Sep 17 00:00:00 2001 From: longgexx Date: Mon, 5 Jan 2026 18:48:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(billing):=20=E4=BF=AE=E5=A4=8D=E8=AE=A1?= =?UTF-8?q?=E8=B4=B9=E6=BC=8F=E6=B4=9E=20=20=20=20=20-=20=E5=85=81?= =?UTF-8?q?=E8=AE=B8=E4=BD=99=E9=A2=9D=E9=80=8F=E6=94=AF=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 问题 - 扣费失败时只记录日志,不阻止请求完成 - 用户可以用极少余额无限次免费使用服务 - 数据库层使用 BalanceGTE 条件防止余额变负,导致余额不足时扣费失败 ## 修复 - 移除 DeductBalance 方法中的 BalanceGTE 条件,允许余额变为负数 - 修改错误返回:用户不存在时返回 ErrUserNotFound - 实现透支策略:余额不足时允许本次请求完成,余额变负后阻止后续请求 ## 测试 - 更新 TestDeductBalance_InsufficientFunds 测试,验证透支功能 - 更新 TestDeductBalance_NotFound 测试,验证正确的错误类型 - 新增 TestDeductBalance_AllowsOverdraft 测试,专门测试透支场景 - 所有测试通过 ✅ --- backend/internal/repository/user_repo.go | 7 +++-- .../repository/user_repo_integration_test.go | 26 ++++++++++++++++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/backend/internal/repository/user_repo.go b/backend/internal/repository/user_repo.go index 0d8c25c6..006a5464 100644 --- a/backend/internal/repository/user_repo.go +++ b/backend/internal/repository/user_repo.go @@ -329,17 +329,20 @@ func (r *userRepository) UpdateBalance(ctx context.Context, id int64, amount flo return nil } +// DeductBalance 扣除用户余额 +// 透支策略:允许余额变为负数,确保当前请求能够完成 +// 中间件会阻止余额 <= 0 的用户发起后续请求 func (r *userRepository) DeductBalance(ctx context.Context, id int64, amount float64) error { client := clientFromContext(ctx, r.client) n, err := client.User.Update(). - Where(dbuser.IDEQ(id), dbuser.BalanceGTE(amount)). + Where(dbuser.IDEQ(id)). AddBalance(-amount). Save(ctx) if err != nil { return err } if n == 0 { - return service.ErrInsufficientBalance + return service.ErrUserNotFound } return nil } diff --git a/backend/internal/repository/user_repo_integration_test.go b/backend/internal/repository/user_repo_integration_test.go index ab2195e3..19e6d6a3 100644 --- a/backend/internal/repository/user_repo_integration_test.go +++ b/backend/internal/repository/user_repo_integration_test.go @@ -290,9 +290,14 @@ func (s *UserRepoSuite) TestDeductBalance() { func (s *UserRepoSuite) TestDeductBalance_InsufficientFunds() { user := s.mustCreateUser(&service.User{Email: "insuf@test.com", Balance: 5}) + // 透支策略:允许扣除超过余额的金额 err := s.repo.DeductBalance(s.ctx, user.ID, 999) - s.Require().Error(err, "expected error for insufficient balance") - s.Require().ErrorIs(err, service.ErrInsufficientBalance) + s.Require().NoError(err, "DeductBalance should allow overdraft") + + // 验证余额变为负数 + got, err := s.repo.GetByID(s.ctx, user.ID) + s.Require().NoError(err) + s.Require().InDelta(-994.0, got.Balance, 1e-6, "Balance should be negative after overdraft") } func (s *UserRepoSuite) TestDeductBalance_ExactAmount() { @@ -306,6 +311,19 @@ func (s *UserRepoSuite) TestDeductBalance_ExactAmount() { s.Require().InDelta(0.0, got.Balance, 1e-6) } +func (s *UserRepoSuite) TestDeductBalance_AllowsOverdraft() { + user := s.mustCreateUser(&service.User{Email: "overdraft@test.com", Balance: 5.0}) + + // 扣除超过余额的金额 - 应该成功 + err := s.repo.DeductBalance(s.ctx, user.ID, 10.0) + s.Require().NoError(err, "DeductBalance should allow overdraft") + + // 验证余额为负 + got, err := s.repo.GetByID(s.ctx, user.ID) + s.Require().NoError(err) + s.Require().InDelta(-5.0, got.Balance, 1e-6, "Balance should be -5.0 after overdraft") +} + // --- Concurrency --- func (s *UserRepoSuite) TestUpdateConcurrency() { @@ -511,6 +529,6 @@ func (s *UserRepoSuite) TestUpdateConcurrency_NotFound() { func (s *UserRepoSuite) TestDeductBalance_NotFound() { err := s.repo.DeductBalance(s.ctx, 999999, 5) s.Require().Error(err, "expected error for non-existent user") - // DeductBalance 在用户不存在时返回 ErrInsufficientBalance 因为 WHERE 条件不匹配 - s.Require().ErrorIs(err, service.ErrInsufficientBalance) + // DeductBalance 在用户不存在时返回 ErrUserNotFound + s.Require().ErrorIs(err, service.ErrUserNotFound) }