254 lines
7.9 KiB
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,
|
|
}
|
|
}
|