113 lines
3.9 KiB
Go
113 lines
3.9 KiB
Go
package mysql
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"hyapp/pkg/appcode"
|
|
"hyapp/pkg/xerr"
|
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
|
)
|
|
|
|
// ApplyGameCoinChange 在同一事务内完成游戏 COIN 改账、分录和 outbox。
|
|
func (r *Repository) ApplyGameCoinChange(ctx context.Context, command ledger.GameCoinChangeCommand) (ledger.GameCoinChangeReceipt, error) {
|
|
if r == nil || r.db == nil {
|
|
return ledger.GameCoinChangeReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
|
}
|
|
ctx = contextWithCommandApp(ctx, command.AppCode)
|
|
command.AppCode = appcode.FromContext(ctx)
|
|
|
|
bizType, delta, err := gameBizTypeAndDelta(command.OpType, command.CoinAmount)
|
|
if err != nil {
|
|
return ledger.GameCoinChangeReceipt{}, err
|
|
}
|
|
|
|
tx, err := r.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return ledger.GameCoinChangeReceipt{}, err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
if txRow, exists, err := r.lookupTransactionWithConflictCode(ctx, tx, command.CommandID, command.RequestHash, bizType, xerr.IdempotencyConflict); err != nil || exists {
|
|
if err != nil || !exists {
|
|
return ledger.GameCoinChangeReceipt{}, err
|
|
}
|
|
receipt, receiptErr := r.receiptForGameTransaction(ctx, tx, txRow.TransactionID)
|
|
receipt.IdempotentReplay = true
|
|
return receipt, receiptErr
|
|
}
|
|
|
|
nowMs := time.Now().UnixMilli()
|
|
createIfMissing := delta > 0
|
|
account, err := r.lockAccount(ctx, tx, command.UserID, ledger.AssetCoin, createIfMissing, nowMs)
|
|
if err != nil {
|
|
return ledger.GameCoinChangeReceipt{}, err
|
|
}
|
|
if delta < 0 && account.AvailableAmount < -delta {
|
|
return ledger.GameCoinChangeReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
|
|
}
|
|
|
|
transactionID := transactionID(command.AppCode, command.CommandID)
|
|
balanceAfter := account.AvailableAmount + delta
|
|
metadata := gameCoinMetadata{
|
|
AppCode: command.AppCode,
|
|
UserID: command.UserID,
|
|
PlatformCode: command.PlatformCode,
|
|
GameID: command.GameID,
|
|
ProviderOrderID: command.ProviderOrderID,
|
|
ProviderRoundID: command.ProviderRoundID,
|
|
OpType: command.OpType,
|
|
AssetType: ledger.AssetCoin,
|
|
CoinAmount: command.CoinAmount,
|
|
AvailableDelta: delta,
|
|
RoomID: command.RoomID,
|
|
BalanceAfter: balanceAfter,
|
|
AppliedAtMS: nowMs,
|
|
}
|
|
if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizType, command.RequestHash, command.ProviderOrderID, metadata, nowMs); err != nil {
|
|
return ledger.GameCoinChangeReceipt{}, err
|
|
}
|
|
if err := r.applyAccountDelta(ctx, tx, account, delta, 0, nowMs); err != nil {
|
|
return ledger.GameCoinChangeReceipt{}, err
|
|
}
|
|
if err := r.insertEntry(ctx, tx, walletEntry{
|
|
TransactionID: transactionID,
|
|
UserID: command.UserID,
|
|
AssetType: ledger.AssetCoin,
|
|
AvailableDelta: delta,
|
|
FrozenDelta: 0,
|
|
AvailableAfter: balanceAfter,
|
|
FrozenAfter: account.FrozenAmount,
|
|
RoomID: command.RoomID,
|
|
CreatedAtMS: nowMs,
|
|
}); err != nil {
|
|
return ledger.GameCoinChangeReceipt{}, err
|
|
}
|
|
if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
|
|
balanceChangedEvent(transactionID, command.CommandID, command.UserID, ledger.AssetCoin, delta, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs, bizType),
|
|
}); err != nil {
|
|
return ledger.GameCoinChangeReceipt{}, err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return ledger.GameCoinChangeReceipt{}, err
|
|
}
|
|
|
|
return receiptFromGameMetadata(transactionID, metadata, false), nil
|
|
}
|
|
|
|
func gameBizTypeAndDelta(opType string, coinAmount int64) (string, int64, error) {
|
|
switch ledger.NormalizeGameOpType(opType) {
|
|
case ledger.GameOpDebit:
|
|
return bizTypeGameDebit, -coinAmount, nil
|
|
case ledger.GameOpCredit:
|
|
return bizTypeGameCredit, coinAmount, nil
|
|
case ledger.GameOpRefund:
|
|
return bizTypeGameRefund, coinAmount, nil
|
|
case ledger.GameOpReverse:
|
|
|
|
return bizTypeGameReverse, -coinAmount, nil
|
|
default:
|
|
return "", 0, xerr.New(xerr.InvalidArgument, "game op_type is invalid")
|
|
}
|
|
}
|