package common import ( "errors" "math" "time" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "gorm.io/gorm" "gorm.io/gorm/clause" ) var ( ErrInvalidWalletAmount = errors.New("invalid wallet amount") ErrWalletUnavailable = errors.New("wallet is unavailable") ErrWalletBalance = errors.New("insufficient wallet balance") ErrWalletOverflow = errors.New("wallet balance overflow") ErrIdempotencyConflict = errors.New("idempotency request conflicts with existing withdrawal") ) // WalletWithdrawalInput 是统一提现预扣所需的最小业务输入。 type WalletWithdrawalInput struct { WalletBasicID uint64 WalletBankID uint64 RequestNo string CashNo string Amount int64 Channel string Remark string OperatorIdentity string OperatorName string } // LockWalletForUpdate 锁定钱包事实行,所有资金扣减必须在同一事务内调用。 func LockWalletForUpdate(tx *gorm.DB, walletID uint64, requireEnabled bool) (models.WalletBasic, error) { var wallet models.WalletBasic query := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", walletID) if requireEnabled { query = query.Where("status = ?", StatusEnable) } if err := query.First(&wallet).Error; err != nil { return wallet, err } return wallet, nil } // SpendWalletBalance 扣减普通消费,并保证可提现余额始终不超过总余额。 func SpendWalletBalance(wallet *models.WalletBasic, amount int64) error { if amount <= 0 { return ErrInvalidWalletAmount } if wallet.Status != StatusEnable { return ErrWalletUnavailable } if wallet.Balance < 0 || wallet.WithdrawalBalance < 0 { return ErrWalletBalance } if wallet.Balance < amount { return ErrWalletBalance } wallet.Balance -= amount if wallet.WithdrawalBalance > wallet.Balance { wallet.WithdrawalBalance = wallet.Balance } return nil } // ReserveWalletWithdrawal 在申请时同时预扣总余额和可提现余额。 func ReserveWalletWithdrawal(wallet *models.WalletBasic, amount int64) error { if amount <= 0 { return ErrInvalidWalletAmount } if wallet.Status != StatusEnable { return ErrWalletUnavailable } if wallet.Balance < 0 || wallet.WithdrawalBalance < 0 || wallet.WithdrawalBalance > wallet.Balance { return ErrWalletBalance } if wallet.Balance < amount || wallet.WithdrawalBalance < amount { return ErrWalletBalance } wallet.Balance -= amount wallet.WithdrawalBalance -= amount return nil } // ReleaseWalletWithdrawal 在提现驳回时原样返还此前预扣的两类余额。 func ReleaseWalletWithdrawal(wallet *models.WalletBasic, amount int64) error { if amount <= 0 { return ErrInvalidWalletAmount } if wallet.Balance < 0 || wallet.WithdrawalBalance < 0 || wallet.WithdrawalBalance > wallet.Balance { return ErrWalletBalance } if wallet.Balance > math.MaxInt64-amount || wallet.WithdrawalBalance > math.MaxInt64-amount { return ErrWalletOverflow } wallet.Balance += amount wallet.WithdrawalBalance += amount return nil } // ReleaseLegacyWithdrawalBalance 兼容旧客户端申请只预扣可提现余额的历史记录。 func ReleaseLegacyWithdrawalBalance(wallet *models.WalletBasic, amount int64) error { if amount <= 0 { return ErrInvalidWalletAmount } if wallet.WithdrawalBalance > math.MaxInt64-amount { return ErrWalletOverflow } wallet.WithdrawalBalance += amount if wallet.WithdrawalBalance > wallet.Balance { wallet.WithdrawalBalance = wallet.Balance } return nil } // ReleaseWithdrawalApplication 按新旧申请的实际预扣方式返还余额。 func ReleaseWithdrawalApplication(wallet *models.WalletBasic, application models.WalletApplyCash) (bool, error) { if application.BalanceReserved { return true, ReleaseWalletWithdrawal(wallet, application.Amount) } if wallet.OwnerType == "user" || wallet.OwnerType == "staff" { return true, ReleaseLegacyWithdrawalBalance(wallet, application.Amount) } return false, nil } // SettleLegacyWithdrawal 为升级前未完整预扣的申请补扣余额。 func SettleLegacyWithdrawal(wallet *models.WalletBasic, application models.WalletApplyCash) (bool, error) { if application.BalanceReserved { return false, nil } if wallet.OwnerType == "user" || wallet.OwnerType == "staff" { return true, SpendWalletBalance(wallet, application.Amount) } return true, ReserveWalletWithdrawal(wallet, application.Amount) } // SaveWalletBalances 将内存中已校验的余额快照写回当前事务。 func SaveWalletBalances(tx *gorm.DB, wallet models.WalletBasic) error { result := tx.Model(&models.WalletBasic{}).Where("id = ?", wallet.ID).Updates(map[string]any{ "balance": wallet.Balance, "withdrawal_balance": wallet.WithdrawalBalance, }) if result.Error != nil { return result.Error } if result.RowsAffected != 1 { return ErrWalletUnavailable } return nil } // CreateReservedWithdrawal 幂等创建提现申请,并在同一事务中预扣两类余额和写入流水。 func CreateReservedWithdrawal(tx *gorm.DB, input WalletWithdrawalInput) (models.WalletApplyCash, bool, error) { wallet, err := LockWalletForUpdate(tx, input.WalletBasicID, false) if err != nil { return models.WalletApplyCash{}, false, err } var existing models.WalletApplyCash err = tx.Where("request_no = ?", input.RequestNo).First(&existing).Error if err == nil { if existing.WalletBasicID != input.WalletBasicID || existing.WalletBankID != input.WalletBankID || existing.Amount != input.Amount || existing.Channel != input.Channel { return existing, false, ErrIdempotencyConflict } return existing, false, nil } if !errors.Is(err, gorm.ErrRecordNotFound) { return models.WalletApplyCash{}, false, err } if err := ReserveWalletWithdrawal(&wallet, input.Amount); err != nil { return models.WalletApplyCash{}, false, err } if input.CashNo == "" { input.CashNo = models.NewIdentity() } application := models.WalletApplyCash{ Entity: NewEntity(StatusEnable), ApplyStatus: StatusPending, WalletBasicID: input.WalletBasicID, WalletBankID: input.WalletBankID, CashNo: input.CashNo, RequestNo: input.RequestNo, Amount: input.Amount, Channel: input.Channel, Remark: input.Remark, BalanceReserved: true, } if err := tx.Create(&application).Error; err != nil { return models.WalletApplyCash{}, false, err } if err := SaveWalletBalances(tx, wallet); err != nil { return models.WalletApplyCash{}, false, err } record := NewWalletBalanceRecord( wallet, "withdrawal-reserve:"+application.Identity, "expense", "withdrawal_reserve", application.Amount, "", application.CashNo, application.Channel, input.OperatorIdentity, input.OperatorName, input.Remark, ) if err := tx.Create(&record).Error; err != nil { return models.WalletApplyCash{}, false, err } return application, true, nil } // NewWalletBalanceRecord 创建带完整余额快照的不可变资金流水。 func NewWalletBalanceRecord( wallet models.WalletBasic, requestNo string, direction string, tradeType string, amount int64, inTradeNo string, outTradeNo string, channel string, operatorIdentity string, operatorName string, remark string, ) models.WalletRecord { now := time.Now() return models.WalletRecord{ Entity: NewEntity(StatusEnable), WalletBasicID: wallet.ID, RecordNo: models.NewIdentity(), RequestNo: requestNo, Direction: direction, TradeType: tradeType, Amount: amount, BalanceAfter: wallet.Balance, WithdrawalBalanceAfter: wallet.WithdrawalBalance, InTradeNo: inTradeNo, OutTradeNo: outTradeNo, PayChannel: channel, OperatorIdentity: operatorIdentity, OperatorName: operatorName, Ymd: int32(now.Year()*10000 + int(now.Month())*100 + now.Day()), Ym: int32(now.Year()*100 + int(now.Month())), Remark: remark, } }