fix: add optimistic lock retry for wallet topup and withdraw

This commit is contained in:
zetaloop
2026-04-22 20:29:35 +08:00
parent 17daff03bd
commit 59256897e9
2 changed files with 93 additions and 85 deletions
@@ -7,6 +7,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strings"
"time" "time"
"juwan-backend/app/wallet/rpc/pb" "juwan-backend/app/wallet/rpc/pb"
@@ -53,9 +54,6 @@ func (l *TopupLogic) Topup(req *types.TopupReq) (resp *types.EmptyResp, err erro
return nil, err return nil, err
} }
currentBalance := decimal.Zero
frozenBalance := decimal.Zero
expectedVersion := int64(0)
if walletResp.GetWallets() == nil { if walletResp.GetWallets() == nil {
zeroAmount := "0" zeroAmount := "0"
_, err = l.svcCtx.WalletRpc.AddWallets(l.ctx, &pb.AddWalletsReq{ _, err = l.svcCtx.WalletRpc.AddWallets(l.ctx, &pb.AddWalletsReq{
@@ -74,42 +72,32 @@ func (l *TopupLogic) Topup(req *types.TopupReq) (resp *types.EmptyResp, err erro
if walletResp.GetWallets() == nil { if walletResp.GetWallets() == nil {
return nil, errors.New("wallet not found") return nil, errors.New("wallet not found")
} }
currentBalance, err = decimal.NewFromString(walletResp.Wallets.Balance) }
if err != nil {
const maxRetries = 3
for i := 0; i < maxRetries; i++ {
currentBalance, perr := decimal.NewFromString(walletResp.Wallets.Balance)
if perr != nil {
return nil, errors.New("invalid wallet balance") return nil, errors.New("invalid wallet balance")
} }
frozenBalance, err = decimal.NewFromString(walletResp.Wallets.FrozenBalance) frozenBalance, perr := decimal.NewFromString(walletResp.Wallets.FrozenBalance)
if err != nil { if perr != nil {
return nil, errors.New("invalid wallet frozen balance") return nil, errors.New("invalid wallet frozen balance")
} }
expectedVersion = walletResp.Wallets.Version expectedVersion := walletResp.Wallets.Version
} else {
currentBalance, err = decimal.NewFromString(walletResp.Wallets.Balance)
if err != nil {
return nil, errors.New("invalid wallet balance")
}
frozenBalance, err = decimal.NewFromString(walletResp.Wallets.FrozenBalance)
if err != nil {
return nil, errors.New("invalid wallet frozen balance")
}
expectedVersion = walletResp.Wallets.Version
}
newBalance := currentBalance.Add(amount) newBalance := currentBalance.Add(amount)
newBalanceStr := newBalance.String() newBalanceStr := newBalance.String()
frozenBalanceStr := frozenBalance.String() frozenBalanceStr := frozenBalance.String()
updatedAt := time.Now().Unix() updatedAt := time.Now().Unix()
_, err = l.svcCtx.WalletRpc.UpdateWallets(l.ctx, &pb.UpdateWalletsReq{ _, uerr := l.svcCtx.WalletRpc.UpdateWallets(l.ctx, &pb.UpdateWalletsReq{
UserId: userID, UserId: userID,
Balance: &newBalanceStr, Balance: &newBalanceStr,
FrozenBalance: &frozenBalanceStr, FrozenBalance: &frozenBalanceStr,
UpdatedAt: &updatedAt, UpdatedAt: &updatedAt,
Version: &expectedVersion, Version: &expectedVersion,
}) })
if err != nil { if uerr == nil {
return nil, err
}
desc := fmt.Sprintf("topup via %s", req.Method) desc := fmt.Sprintf("topup via %s", req.Method)
_, err = l.svcCtx.WalletRpc.AddWalletTransactions(l.ctx, &pb.AddWalletTransactionsReq{ _, err = l.svcCtx.WalletRpc.AddWalletTransactions(l.ctx, &pb.AddWalletTransactionsReq{
UserId: userID, UserId: userID,
@@ -122,6 +110,16 @@ func (l *TopupLogic) Topup(req *types.TopupReq) (resp *types.EmptyResp, err erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &types.EmptyResp{}, nil return &types.EmptyResp{}, nil
} }
if !strings.Contains(uerr.Error(), "conflict") {
return nil, uerr
}
walletResp, err = l.svcCtx.WalletRpc.GetWalletsById(l.ctx, &pb.GetWalletsByIdReq{Id: userID})
if err != nil {
return nil, err
}
}
return nil, errors.New("wallet update conflict, please retry")
}
@@ -7,6 +7,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strings"
"time" "time"
"juwan-backend/app/wallet/rpc/pb" "juwan-backend/app/wallet/rpc/pb"
@@ -56,12 +57,14 @@ func (l *WithdrawLogic) Withdraw(req *types.TopupReq) (resp *types.EmptyResp, er
return nil, errors.New("wallet not found") return nil, errors.New("wallet not found")
} }
currentBalance, err := decimal.NewFromString(walletResp.Wallets.Balance) const maxRetries = 3
if err != nil { for i := 0; i < maxRetries; i++ {
currentBalance, perr := decimal.NewFromString(walletResp.Wallets.Balance)
if perr != nil {
return nil, errors.New("invalid wallet balance") return nil, errors.New("invalid wallet balance")
} }
frozenBalance, err := decimal.NewFromString(walletResp.Wallets.FrozenBalance) frozenBalance, perr := decimal.NewFromString(walletResp.Wallets.FrozenBalance)
if err != nil { if perr != nil {
return nil, errors.New("invalid wallet frozen balance") return nil, errors.New("invalid wallet frozen balance")
} }
expectedVersion := walletResp.Wallets.Version expectedVersion := walletResp.Wallets.Version
@@ -73,17 +76,14 @@ func (l *WithdrawLogic) Withdraw(req *types.TopupReq) (resp *types.EmptyResp, er
newBalanceStr := newBalance.String() newBalanceStr := newBalance.String()
frozenBalanceStr := frozenBalance.String() frozenBalanceStr := frozenBalance.String()
updatedAt := time.Now().Unix() updatedAt := time.Now().Unix()
_, err = l.svcCtx.WalletRpc.UpdateWallets(l.ctx, &pb.UpdateWalletsReq{ _, uerr := l.svcCtx.WalletRpc.UpdateWallets(l.ctx, &pb.UpdateWalletsReq{
UserId: userID, UserId: userID,
Balance: &newBalanceStr, Balance: &newBalanceStr,
FrozenBalance: &frozenBalanceStr, FrozenBalance: &frozenBalanceStr,
UpdatedAt: &updatedAt, UpdatedAt: &updatedAt,
Version: &expectedVersion, Version: &expectedVersion,
}) })
if err != nil { if uerr == nil {
return nil, err
}
desc := fmt.Sprintf("withdraw via %s", req.Method) desc := fmt.Sprintf("withdraw via %s", req.Method)
_, err = l.svcCtx.WalletRpc.AddWalletTransactions(l.ctx, &pb.AddWalletTransactionsReq{ _, err = l.svcCtx.WalletRpc.AddWalletTransactions(l.ctx, &pb.AddWalletTransactionsReq{
UserId: userID, UserId: userID,
@@ -96,6 +96,16 @@ func (l *WithdrawLogic) Withdraw(req *types.TopupReq) (resp *types.EmptyResp, er
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &types.EmptyResp{}, nil return &types.EmptyResp{}, nil
} }
if !strings.Contains(uerr.Error(), "conflict") {
return nil, uerr
}
walletResp, err = l.svcCtx.WalletRpc.GetWalletsById(l.ctx, &pb.GetWalletsByIdReq{Id: userID})
if err != nil {
return nil, err
}
}
return nil, errors.New("wallet update conflict, please retry")
}