2026-06-28 15:11:56 +08:00

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")
}
}