148 lines
6.0 KiB
Go
148 lines
6.0 KiB
Go
package mysql
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/pkg/xerr"
|
||
"hyapp/services/wallet-service/internal/domain/ledger"
|
||
)
|
||
|
||
// FreezeSalaryWithdrawal 在同一事务里把工资 available 转入 frozen,并落交易、分录和余额 outbox。
|
||
func (r *Repository) FreezeSalaryWithdrawal(ctx context.Context, command ledger.SalaryWithdrawalCommand) (ledger.SalaryWithdrawalReceipt, error) {
|
||
return r.applySalaryWithdrawal(ctx, command, salaryWithdrawalMutation{
|
||
BizType: bizTypeSalaryWithdrawalFreeze,
|
||
AvailableDelta: -command.SalaryUSDMinor,
|
||
FrozenDelta: command.SalaryUSDMinor,
|
||
ExternalRef: command.WithdrawalRef,
|
||
})
|
||
}
|
||
|
||
// SettleSalaryWithdrawal 在审核通过时只扣 frozen;这保证提交申请时已经冻结的金额不会被重复扣 available。
|
||
func (r *Repository) SettleSalaryWithdrawal(ctx context.Context, command ledger.SalaryWithdrawalCommand) (ledger.SalaryWithdrawalReceipt, error) {
|
||
return r.applySalaryWithdrawal(ctx, command, salaryWithdrawalMutation{
|
||
BizType: bizTypeSalaryWithdrawalSettle,
|
||
AvailableDelta: 0,
|
||
FrozenDelta: -command.SalaryUSDMinor,
|
||
ExternalRef: command.WithdrawalApplicationID,
|
||
})
|
||
}
|
||
|
||
// ReleaseSalaryWithdrawal 在拒绝或创建申请失败回滚时把 frozen 返还 available。
|
||
func (r *Repository) ReleaseSalaryWithdrawal(ctx context.Context, command ledger.SalaryWithdrawalCommand) (ledger.SalaryWithdrawalReceipt, error) {
|
||
return r.applySalaryWithdrawal(ctx, command, salaryWithdrawalMutation{
|
||
BizType: bizTypeSalaryWithdrawalRelease,
|
||
AvailableDelta: command.SalaryUSDMinor,
|
||
FrozenDelta: -command.SalaryUSDMinor,
|
||
ExternalRef: command.WithdrawalApplicationID,
|
||
})
|
||
}
|
||
|
||
type salaryWithdrawalMutation struct {
|
||
BizType string
|
||
AvailableDelta int64
|
||
FrozenDelta int64
|
||
ExternalRef string
|
||
}
|
||
|
||
func (r *Repository) applySalaryWithdrawal(ctx context.Context, command ledger.SalaryWithdrawalCommand, mutation salaryWithdrawalMutation) (ledger.SalaryWithdrawalReceipt, error) {
|
||
if r == nil || r.db == nil {
|
||
return ledger.SalaryWithdrawalReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||
}
|
||
ctx = contextWithCommandApp(ctx, command.AppCode)
|
||
command.AppCode = appcode.FromContext(ctx)
|
||
|
||
tx, err := r.db.BeginTx(ctx, nil)
|
||
if err != nil {
|
||
return ledger.SalaryWithdrawalReceipt{}, err
|
||
}
|
||
defer func() { _ = tx.Rollback() }()
|
||
|
||
requestHash := salaryWithdrawalRequestHash(command, mutation.BizType)
|
||
if txRow, exists, err := r.lookupTransactionWithConflictCode(ctx, tx, command.CommandID, requestHash, mutation.BizType, xerr.IdempotencyConflict); err != nil || exists {
|
||
if err != nil || !exists {
|
||
return ledger.SalaryWithdrawalReceipt{}, err
|
||
}
|
||
return r.receiptForSalaryWithdrawalTransaction(ctx, tx, txRow.TransactionID)
|
||
}
|
||
|
||
nowMs := time.Now().UTC().UnixMilli()
|
||
account, err := r.lockAccount(ctx, tx, command.UserID, command.SalaryAssetType, false, nowMs)
|
||
if err != nil {
|
||
return ledger.SalaryWithdrawalReceipt{}, err
|
||
}
|
||
if account.AvailableAmount+mutation.AvailableDelta < 0 || account.FrozenAmount+mutation.FrozenDelta < 0 {
|
||
return ledger.SalaryWithdrawalReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
|
||
}
|
||
|
||
transactionID := transactionID(command.AppCode, command.CommandID)
|
||
availableAfter := account.AvailableAmount + mutation.AvailableDelta
|
||
frozenAfter := account.FrozenAmount + mutation.FrozenDelta
|
||
metadata := salaryWithdrawalMetadata{
|
||
AppCode: command.AppCode,
|
||
UserID: command.UserID,
|
||
SalaryAssetType: command.SalaryAssetType,
|
||
SalaryUSDMinor: command.SalaryUSDMinor,
|
||
PointFeeAmount: command.PointFeeAmount,
|
||
PointNetAmount: command.PointNetAmount,
|
||
PointsPerUSD: command.PointsPerUSD,
|
||
PointWithdrawFeeBPS: command.PointWithdrawFeeBPS,
|
||
OperatorUserID: command.OperatorUserID,
|
||
Reason: command.Reason,
|
||
WithdrawalRef: command.WithdrawalRef,
|
||
WithdrawalApplicationID: command.WithdrawalApplicationID,
|
||
AvailableAfter: availableAfter,
|
||
FrozenAfter: frozenAfter,
|
||
Version: account.Version + 1,
|
||
CreatedAtMS: nowMs,
|
||
}
|
||
if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, mutation.BizType, requestHash, strings.TrimSpace(mutation.ExternalRef), metadata, nowMs); err != nil {
|
||
return ledger.SalaryWithdrawalReceipt{}, err
|
||
}
|
||
if err := r.applyAccountDelta(ctx, tx, account, mutation.AvailableDelta, mutation.FrozenDelta, nowMs); err != nil {
|
||
return ledger.SalaryWithdrawalReceipt{}, err
|
||
}
|
||
if err := r.insertEntry(ctx, tx, walletEntry{
|
||
TransactionID: transactionID,
|
||
UserID: command.UserID,
|
||
AssetType: command.SalaryAssetType,
|
||
AvailableDelta: mutation.AvailableDelta,
|
||
FrozenDelta: mutation.FrozenDelta,
|
||
AvailableAfter: availableAfter,
|
||
FrozenAfter: frozenAfter,
|
||
CreatedAtMS: nowMs,
|
||
}); err != nil {
|
||
return ledger.SalaryWithdrawalReceipt{}, err
|
||
}
|
||
if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
|
||
balanceChangedEvent(transactionID, command.CommandID, command.UserID, command.SalaryAssetType, mutation.AvailableDelta, mutation.FrozenDelta, availableAfter, frozenAfter, account.Version+1, metadata, nowMs, mutation.BizType),
|
||
}); err != nil {
|
||
return ledger.SalaryWithdrawalReceipt{}, err
|
||
}
|
||
if err := tx.Commit(); err != nil {
|
||
return ledger.SalaryWithdrawalReceipt{}, err
|
||
}
|
||
return receiptFromSalaryWithdrawalMetadata(transactionID, metadata), nil
|
||
}
|
||
|
||
func salaryWithdrawalRequestHash(command ledger.SalaryWithdrawalCommand, bizType string) string {
|
||
return stableHash(fmt.Sprintf("salary_withdrawal|%s|%s|%d|%s|%d|%d|%d|%d|%d|%d|%s|%s|%s",
|
||
bizType,
|
||
appcode.Normalize(command.AppCode),
|
||
command.UserID,
|
||
strings.ToUpper(strings.TrimSpace(command.SalaryAssetType)),
|
||
command.SalaryUSDMinor,
|
||
command.PointFeeAmount,
|
||
command.PointNetAmount,
|
||
command.PointsPerUSD,
|
||
command.PointWithdrawFeeBPS,
|
||
command.OperatorUserID,
|
||
strings.TrimSpace(command.Reason),
|
||
strings.TrimSpace(command.WithdrawalRef),
|
||
strings.TrimSpace(command.WithdrawalApplicationID),
|
||
))
|
||
}
|