Files
platforms/backend/api/internal/logic/common/wallet_balance.go

254 lines
7.9 KiB
Go

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,
}
}