package mysql import ( "context" "fmt" "strings" "time" "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" ) // DebitCPBreakupFee 在钱包账本中扣除解除关系费用;关系状态不在 wallet 中修改,避免 wallet 反向拥有用户关系事实。 func (r *Repository) DebitCPBreakupFee(ctx context.Context, command ledger.CPBreakupFeeCommand) (ledger.CPBreakupFeeReceipt, error) { if r == nil || r.db == nil { return ledger.CPBreakupFeeReceipt{}, 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.CPBreakupFeeReceipt{}, err } defer func() { _ = tx.Rollback() }() requestHash := cpBreakupFeeRequestHash(command) if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeCPBreakupFee); err != nil || exists { if err != nil || !exists { return ledger.CPBreakupFeeReceipt{}, err } balance, balanceErr := r.balanceAfterTransaction(ctx, tx, txRow.TransactionID, command.UserID, ledger.AssetCoin) if balanceErr != nil { return ledger.CPBreakupFeeReceipt{}, balanceErr } return ledger.CPBreakupFeeReceipt{ TransactionID: txRow.TransactionID, CoinSpent: command.Amount, CoinBalanceAfter: balance.AvailableAmount, Balance: balance, }, nil } nowMs := time.Now().UnixMilli() account, err := r.lockAccount(ctx, tx, command.UserID, ledger.AssetCoin, false, nowMs) if err != nil { return ledger.CPBreakupFeeReceipt{}, err } if account.AvailableAmount < command.Amount { return ledger.CPBreakupFeeReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance") } transactionID := transactionID(command.AppCode, command.CommandID) balanceAfter := account.AvailableAmount - command.Amount metadata := cpBreakupFeeMetadata{ AppCode: command.AppCode, UserID: command.UserID, RelationshipID: command.RelationshipID, RelationType: command.RelationType, Amount: command.Amount, CoinBalanceAfter: balanceAfter, } if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeCPBreakupFee, requestHash, command.RelationshipID, metadata, nowMs); err != nil { return ledger.CPBreakupFeeReceipt{}, err } if err := r.applyAccountDelta(ctx, tx, account, -command.Amount, 0, nowMs); err != nil { return ledger.CPBreakupFeeReceipt{}, err } balance := ledger.AssetBalance{ AppCode: command.AppCode, UserID: command.UserID, AssetType: ledger.AssetCoin, AvailableAmount: balanceAfter, FrozenAmount: account.FrozenAmount, Version: account.Version + 1, UpdatedAtMs: nowMs, } if err := r.insertEntry(ctx, tx, walletEntry{ TransactionID: transactionID, UserID: command.UserID, AssetType: ledger.AssetCoin, AvailableDelta: -command.Amount, FrozenDelta: 0, AvailableAfter: balance.AvailableAmount, FrozenAfter: balance.FrozenAmount, CreatedAtMS: nowMs, }); err != nil { return ledger.CPBreakupFeeReceipt{}, err } if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ balanceChangedEvent(transactionID, command.CommandID, command.UserID, ledger.AssetCoin, -command.Amount, 0, balance.AvailableAmount, balance.FrozenAmount, balance.Version, metadata, nowMs, bizTypeCPBreakupFee), }); err != nil { return ledger.CPBreakupFeeReceipt{}, err } if err := tx.Commit(); err != nil { return ledger.CPBreakupFeeReceipt{}, err } return ledger.CPBreakupFeeReceipt{ TransactionID: transactionID, CoinSpent: command.Amount, CoinBalanceAfter: balance.AvailableAmount, Balance: balance, }, nil } // DebitWheelDraw 在钱包账本中扣除转盘抽奖金币;中奖和返奖由 activity-service 后续处理,钱包只保证付费事实可审计、可幂等。 func (r *Repository) DebitWheelDraw(ctx context.Context, command ledger.WheelDrawDebitCommand) (ledger.WheelDrawDebitReceipt, error) { if r == nil || r.db == nil { return ledger.WheelDrawDebitReceipt{}, 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.WheelDrawDebitReceipt{}, err } defer func() { _ = tx.Rollback() }() requestHash := wheelDrawDebitRequestHash(command) if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeWheelDrawDebit); err != nil || exists { if err != nil || !exists { return ledger.WheelDrawDebitReceipt{}, err } balance, balanceErr := r.balanceAfterTransaction(ctx, tx, txRow.TransactionID, command.UserID, ledger.AssetCoin) if balanceErr != nil { return ledger.WheelDrawDebitReceipt{}, balanceErr } return ledger.WheelDrawDebitReceipt{ TransactionID: txRow.TransactionID, CoinSpent: command.Amount, CoinBalanceAfter: balance.AvailableAmount, Balance: balance, }, nil } nowMs := time.Now().UnixMilli() account, err := r.lockAccount(ctx, tx, command.UserID, ledger.AssetCoin, false, nowMs) if err != nil { return ledger.WheelDrawDebitReceipt{}, err } if account.AvailableAmount < command.Amount { return ledger.WheelDrawDebitReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance") } transactionID := transactionID(command.AppCode, command.CommandID) balanceAfter := account.AvailableAmount - command.Amount metadata := wheelDrawDebitMetadata{ AppCode: command.AppCode, UserID: command.UserID, WheelID: command.WheelID, DrawCount: command.DrawCount, Amount: command.Amount, CoinBalanceAfter: balanceAfter, } if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeWheelDrawDebit, requestHash, command.WheelID, metadata, nowMs); err != nil { return ledger.WheelDrawDebitReceipt{}, err } if err := r.applyAccountDelta(ctx, tx, account, -command.Amount, 0, nowMs); err != nil { return ledger.WheelDrawDebitReceipt{}, err } balance := ledger.AssetBalance{ AppCode: command.AppCode, UserID: command.UserID, AssetType: ledger.AssetCoin, AvailableAmount: balanceAfter, FrozenAmount: account.FrozenAmount, Version: account.Version + 1, UpdatedAtMs: nowMs, } if err := r.insertEntry(ctx, tx, walletEntry{ TransactionID: transactionID, UserID: command.UserID, AssetType: ledger.AssetCoin, AvailableDelta: -command.Amount, FrozenDelta: 0, AvailableAfter: balance.AvailableAmount, FrozenAfter: balance.FrozenAmount, CreatedAtMS: nowMs, }); err != nil { return ledger.WheelDrawDebitReceipt{}, err } if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ balanceChangedEvent(transactionID, command.CommandID, command.UserID, ledger.AssetCoin, -command.Amount, 0, balance.AvailableAmount, balance.FrozenAmount, balance.Version, metadata, nowMs, bizTypeWheelDrawDebit), }); err != nil { return ledger.WheelDrawDebitReceipt{}, err } if err := tx.Commit(); err != nil { return ledger.WheelDrawDebitReceipt{}, err } return ledger.WheelDrawDebitReceipt{ TransactionID: transactionID, CoinSpent: command.Amount, CoinBalanceAfter: balance.AvailableAmount, Balance: balance, }, nil } // CreditTaskReward 在一个事务内完成任务奖励 COIN 入账、交易分录和 outbox 事件。 func (r *Repository) CreditTaskReward(ctx context.Context, command ledger.TaskRewardCommand) (ledger.TaskRewardReceipt, error) { if r == nil || r.db == nil { return ledger.TaskRewardReceipt{}, 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.TaskRewardReceipt{}, err } defer func() { _ = tx.Rollback() }() requestHash := taskRewardRequestHash(command) if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeTaskReward); err != nil || exists { if err != nil || !exists { return ledger.TaskRewardReceipt{}, err } return r.receiptForTaskRewardTransaction(ctx, tx, txRow.TransactionID) } nowMs := time.Now().UnixMilli() account, err := r.lockAccount(ctx, tx, command.TargetUserID, ledger.AssetCoin, true, nowMs) if err != nil { return ledger.TaskRewardReceipt{}, err } transactionID := transactionID(command.AppCode, command.CommandID) balanceAfter := account.AvailableAmount + command.Amount metadata := taskRewardMetadata{ AppCode: command.AppCode, TargetUserID: command.TargetUserID, AssetType: ledger.AssetCoin, Amount: command.Amount, TaskType: command.TaskType, TaskID: command.TaskID, CycleKey: command.CycleKey, Reason: command.Reason, BalanceAfter: balanceAfter, GrantedAtMS: nowMs, } if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeTaskReward, requestHash, fmt.Sprintf("%s:%s", command.TaskID, command.CycleKey), metadata, nowMs); err != nil { return ledger.TaskRewardReceipt{}, err } if err := r.applyAccountDelta(ctx, tx, account, command.Amount, 0, nowMs); err != nil { return ledger.TaskRewardReceipt{}, err } if err := r.insertEntry(ctx, tx, walletEntry{ TransactionID: transactionID, UserID: command.TargetUserID, AssetType: ledger.AssetCoin, AvailableDelta: command.Amount, FrozenDelta: 0, AvailableAfter: balanceAfter, FrozenAfter: account.FrozenAmount, CreatedAtMS: nowMs, }); err != nil { return ledger.TaskRewardReceipt{}, err } if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, ledger.AssetCoin, command.Amount, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs), taskRewardCreditedEvent(transactionID, command.CommandID, metadata, nowMs), }); err != nil { return ledger.TaskRewardReceipt{}, err } if err := tx.Commit(); err != nil { return ledger.TaskRewardReceipt{}, err } return receiptFromTaskRewardMetadata(transactionID, metadata), nil } // CreditLuckyGiftReward 在同一事务里完成幸运礼物中奖 COIN 入账、交易分录和钱包 outbox。 func (r *Repository) CreditLuckyGiftReward(ctx context.Context, command ledger.LuckyGiftRewardCommand) (ledger.LuckyGiftRewardReceipt, error) { if r == nil || r.db == nil { return ledger.LuckyGiftRewardReceipt{}, 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.LuckyGiftRewardReceipt{}, err } defer func() { _ = tx.Rollback() }() requestHash := luckyGiftRewardRequestHash(command) if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeLuckyGiftReward); err != nil || exists { if err != nil || !exists { return ledger.LuckyGiftRewardReceipt{}, err } return r.receiptForLuckyGiftRewardTransaction(ctx, tx, txRow.TransactionID) } nowMs := time.Now().UnixMilli() account, err := r.lockAccount(ctx, tx, command.TargetUserID, ledger.AssetCoin, true, nowMs) if err != nil { return ledger.LuckyGiftRewardReceipt{}, err } transactionID := transactionID(command.AppCode, command.CommandID) balanceAfter := account.AvailableAmount + command.Amount metadata := luckyGiftRewardMetadata{ AppCode: command.AppCode, TargetUserID: command.TargetUserID, AssetType: ledger.AssetCoin, Amount: command.Amount, DrawID: command.DrawID, RoomID: command.RoomID, VisibleRegionID: command.VisibleRegionID, CountryID: command.CountryID, GiftID: command.GiftID, PoolID: command.PoolID, Reason: command.Reason, BalanceAfter: balanceAfter, GrantedAtMS: nowMs, } if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeLuckyGiftReward, requestHash, command.DrawID, metadata, nowMs); err != nil { return ledger.LuckyGiftRewardReceipt{}, err } if err := r.applyAccountDelta(ctx, tx, account, command.Amount, 0, nowMs); err != nil { return ledger.LuckyGiftRewardReceipt{}, err } if err := r.insertEntry(ctx, tx, walletEntry{ TransactionID: transactionID, UserID: command.TargetUserID, AssetType: ledger.AssetCoin, AvailableDelta: command.Amount, FrozenDelta: 0, AvailableAfter: balanceAfter, FrozenAfter: account.FrozenAmount, RoomID: command.RoomID, CreatedAtMS: nowMs, }); err != nil { return ledger.LuckyGiftRewardReceipt{}, err } if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, ledger.AssetCoin, command.Amount, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs), luckyGiftRewardCreditedEvent(transactionID, command.CommandID, metadata, nowMs), }); err != nil { return ledger.LuckyGiftRewardReceipt{}, err } if err := tx.Commit(); err != nil { return ledger.LuckyGiftRewardReceipt{}, err } return receiptFromLuckyGiftRewardMetadata(transactionID, metadata), nil } // CreditWheelReward 在同一事务里完成转盘金币奖品入账、交易分录和钱包 outbox。 func (r *Repository) CreditWheelReward(ctx context.Context, command ledger.WheelRewardCommand) (ledger.WheelRewardReceipt, error) { if r == nil || r.db == nil { return ledger.WheelRewardReceipt{}, 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.WheelRewardReceipt{}, err } defer func() { _ = tx.Rollback() }() requestHash := wheelRewardRequestHash(command) if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeWheelReward); err != nil || exists { if err != nil || !exists { return ledger.WheelRewardReceipt{}, err } return r.receiptForWheelRewardTransaction(ctx, tx, txRow.TransactionID) } nowMs := time.Now().UnixMilli() account, err := r.lockAccount(ctx, tx, command.TargetUserID, ledger.AssetCoin, true, nowMs) if err != nil { return ledger.WheelRewardReceipt{}, err } transactionID := transactionID(command.AppCode, command.CommandID) balanceAfter := account.AvailableAmount + command.Amount metadata := wheelRewardMetadata{ AppCode: command.AppCode, TargetUserID: command.TargetUserID, AssetType: ledger.AssetCoin, Amount: command.Amount, DrawID: command.DrawID, WheelID: command.WheelID, SelectedTierID: command.SelectedTierID, VisibleRegionID: command.VisibleRegionID, Reason: command.Reason, BalanceAfter: balanceAfter, GrantedAtMS: nowMs, } if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeWheelReward, requestHash, command.DrawID, metadata, nowMs); err != nil { return ledger.WheelRewardReceipt{}, err } if err := r.applyAccountDelta(ctx, tx, account, command.Amount, 0, nowMs); err != nil { return ledger.WheelRewardReceipt{}, err } if err := r.insertEntry(ctx, tx, walletEntry{ TransactionID: transactionID, UserID: command.TargetUserID, AssetType: ledger.AssetCoin, AvailableDelta: command.Amount, FrozenDelta: 0, AvailableAfter: balanceAfter, FrozenAfter: account.FrozenAmount, CreatedAtMS: nowMs, }); err != nil { return ledger.WheelRewardReceipt{}, err } if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, ledger.AssetCoin, command.Amount, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs), wheelRewardCreditedEvent(transactionID, command.CommandID, metadata, nowMs), }); err != nil { return ledger.WheelRewardReceipt{}, err } if err := tx.Commit(); err != nil { return ledger.WheelRewardReceipt{}, err } return receiptFromWheelRewardMetadata(transactionID, metadata), nil } // CreditRoomTurnoverReward 在同一事务里完成每周房间流水奖励 COIN 入账、交易分录和钱包 outbox。 func (r *Repository) CreditRoomTurnoverReward(ctx context.Context, command ledger.RoomTurnoverRewardCommand) (ledger.RoomTurnoverRewardReceipt, error) { if r == nil || r.db == nil { return ledger.RoomTurnoverRewardReceipt{}, 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.RoomTurnoverRewardReceipt{}, err } defer func() { _ = tx.Rollback() }() requestHash := roomTurnoverRewardRequestHash(command) if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeRoomTurnoverReward); err != nil || exists { if err != nil || !exists { return ledger.RoomTurnoverRewardReceipt{}, err } return r.receiptForRoomTurnoverRewardTransaction(ctx, tx, txRow.TransactionID) } nowMs := time.Now().UnixMilli() account, err := r.lockAccount(ctx, tx, command.TargetUserID, ledger.AssetCoin, true, nowMs) if err != nil { return ledger.RoomTurnoverRewardReceipt{}, err } transactionID := transactionID(command.AppCode, command.CommandID) balanceAfter := account.AvailableAmount + command.Amount metadata := roomTurnoverRewardMetadata{ AppCode: command.AppCode, TargetUserID: command.TargetUserID, AssetType: ledger.AssetCoin, Amount: command.Amount, SettlementID: command.SettlementID, RoomID: command.RoomID, PeriodStartMS: command.PeriodStartMS, PeriodEndMS: command.PeriodEndMS, CoinSpent: command.CoinSpent, TierID: command.TierID, TierCode: command.TierCode, Reason: command.Reason, BalanceAfter: balanceAfter, GrantedAtMS: nowMs, } if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeRoomTurnoverReward, requestHash, command.SettlementID, metadata, nowMs); err != nil { return ledger.RoomTurnoverRewardReceipt{}, err } if err := r.applyAccountDelta(ctx, tx, account, command.Amount, 0, nowMs); err != nil { return ledger.RoomTurnoverRewardReceipt{}, err } if err := r.insertEntry(ctx, tx, walletEntry{ TransactionID: transactionID, UserID: command.TargetUserID, AssetType: ledger.AssetCoin, AvailableDelta: command.Amount, FrozenDelta: 0, AvailableAfter: balanceAfter, FrozenAfter: account.FrozenAmount, RoomID: command.RoomID, CreatedAtMS: nowMs, }); err != nil { return ledger.RoomTurnoverRewardReceipt{}, err } if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, ledger.AssetCoin, command.Amount, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs), roomTurnoverRewardCreditedEvent(transactionID, command.CommandID, metadata, nowMs), }); err != nil { return ledger.RoomTurnoverRewardReceipt{}, err } if err := tx.Commit(); err != nil { return ledger.RoomTurnoverRewardReceipt{}, err } return receiptFromRoomTurnoverRewardMetadata(transactionID, metadata), nil } // CreditInviteActivityReward 在同一事务内完成邀请活动 COIN 奖励入账、交易分录和钱包 outbox。 func (r *Repository) CreditInviteActivityReward(ctx context.Context, command ledger.InviteActivityRewardCommand) (ledger.InviteActivityRewardReceipt, error) { if r == nil || r.db == nil { return ledger.InviteActivityRewardReceipt{}, 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.InviteActivityRewardReceipt{}, err } defer func() { _ = tx.Rollback() }() requestHash := inviteActivityRewardRequestHash(command) if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeInviteActivityReward); err != nil || exists { if err != nil || !exists { return ledger.InviteActivityRewardReceipt{}, err } return r.receiptForInviteActivityRewardTransaction(ctx, tx, txRow.TransactionID) } nowMs := time.Now().UnixMilli() account, err := r.lockAccount(ctx, tx, command.TargetUserID, ledger.AssetCoin, true, nowMs) if err != nil { return ledger.InviteActivityRewardReceipt{}, err } transactionID := transactionID(command.AppCode, command.CommandID) balanceAfter := account.AvailableAmount + command.Amount metadata := inviteActivityRewardMetadata{ AppCode: command.AppCode, TargetUserID: command.TargetUserID, AssetType: ledger.AssetCoin, Amount: command.Amount, ClaimID: command.ClaimID, RewardType: command.RewardType, TierID: command.TierID, TierCode: command.TierCode, CycleKey: command.CycleKey, ReachedValue: command.ReachedValue, Reason: command.Reason, BalanceAfter: balanceAfter, GrantedAtMS: nowMs, } if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeInviteActivityReward, requestHash, command.ClaimID, metadata, nowMs); err != nil { return ledger.InviteActivityRewardReceipt{}, err } if err := r.applyAccountDelta(ctx, tx, account, command.Amount, 0, nowMs); err != nil { return ledger.InviteActivityRewardReceipt{}, err } if err := r.insertEntry(ctx, tx, walletEntry{ TransactionID: transactionID, UserID: command.TargetUserID, AssetType: ledger.AssetCoin, AvailableDelta: command.Amount, FrozenDelta: 0, AvailableAfter: balanceAfter, FrozenAfter: account.FrozenAmount, CreatedAtMS: nowMs, }); err != nil { return ledger.InviteActivityRewardReceipt{}, err } if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, ledger.AssetCoin, command.Amount, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs), inviteActivityRewardCreditedEvent(transactionID, command.CommandID, metadata, nowMs), }); err != nil { return ledger.InviteActivityRewardReceipt{}, err } if err := tx.Commit(); err != nil { return ledger.InviteActivityRewardReceipt{}, err } return receiptFromInviteActivityRewardMetadata(transactionID, metadata), nil } // CreditAgencyOpeningReward 在同一事务内完成代理开业流水档位 COIN 奖励入账、交易分录和钱包 outbox。 func (r *Repository) CreditAgencyOpeningReward(ctx context.Context, command ledger.AgencyOpeningRewardCommand) (ledger.AgencyOpeningRewardReceipt, error) { if r == nil || r.db == nil { return ledger.AgencyOpeningRewardReceipt{}, 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.AgencyOpeningRewardReceipt{}, err } defer func() { _ = tx.Rollback() }() requestHash := agencyOpeningRewardRequestHash(command) if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeAgencyOpeningReward); err != nil || exists { if err != nil || !exists { return ledger.AgencyOpeningRewardReceipt{}, err } return r.receiptForAgencyOpeningRewardTransaction(ctx, tx, txRow.TransactionID) } nowMs := time.Now().UnixMilli() account, err := r.lockAccount(ctx, tx, command.TargetUserID, ledger.AssetCoin, true, nowMs) if err != nil { return ledger.AgencyOpeningRewardReceipt{}, err } transactionID := transactionID(command.AppCode, command.CommandID) balanceAfter := account.AvailableAmount + command.Amount metadata := agencyOpeningRewardMetadata{ AppCode: command.AppCode, TargetUserID: command.TargetUserID, AssetType: ledger.AssetCoin, Amount: command.Amount, ApplicationID: command.ApplicationID, CycleID: command.CycleID, AgencyID: command.AgencyID, RankNo: command.RankNo, ScoreCoins: command.ScoreCoins, Reason: command.Reason, BalanceAfter: balanceAfter, GrantedAtMS: nowMs, } if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeAgencyOpeningReward, requestHash, command.ApplicationID, metadata, nowMs); err != nil { return ledger.AgencyOpeningRewardReceipt{}, err } if err := r.applyAccountDelta(ctx, tx, account, command.Amount, 0, nowMs); err != nil { return ledger.AgencyOpeningRewardReceipt{}, err } if err := r.insertEntry(ctx, tx, walletEntry{ TransactionID: transactionID, UserID: command.TargetUserID, AssetType: ledger.AssetCoin, AvailableDelta: command.Amount, FrozenDelta: 0, AvailableAfter: balanceAfter, FrozenAfter: account.FrozenAmount, CreatedAtMS: nowMs, }); err != nil { return ledger.AgencyOpeningRewardReceipt{}, err } if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, ledger.AssetCoin, command.Amount, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs), agencyOpeningRewardCreditedEvent(transactionID, command.CommandID, metadata, nowMs), }); err != nil { return ledger.AgencyOpeningRewardReceipt{}, err } if err := tx.Commit(); err != nil { return ledger.AgencyOpeningRewardReceipt{}, err } return receiptFromAgencyOpeningRewardMetadata(transactionID, metadata), nil } func cpBreakupFeeRequestHash(command ledger.CPBreakupFeeCommand) string { return stableHash(fmt.Sprintf("cp_breakup_fee|%s|%d|%s|%s|%d", appcode.Normalize(command.AppCode), command.UserID, strings.TrimSpace(command.RelationshipID), strings.ToLower(strings.TrimSpace(command.RelationType)), command.Amount, )) } func wheelDrawDebitRequestHash(command ledger.WheelDrawDebitCommand) string { return stableHash(fmt.Sprintf("wheel_draw_debit|%s|%d|%s|%d|%d", appcode.Normalize(command.AppCode), command.UserID, strings.TrimSpace(command.WheelID), command.DrawCount, command.Amount, )) } func taskRewardRequestHash(command ledger.TaskRewardCommand) string { return stableHash(fmt.Sprintf("task_reward|%s|%d|%d|%s|%s|%s|%s", appcode.Normalize(command.AppCode), command.TargetUserID, command.Amount, command.TaskType, command.TaskID, command.CycleKey, strings.TrimSpace(command.Reason), )) } func luckyGiftRewardRequestHash(command ledger.LuckyGiftRewardCommand) string { return stableHash(fmt.Sprintf("lucky_gift_reward|%s|%d|%d|%s|%s|%d|%d|%s|%s|%s", appcode.Normalize(command.AppCode), command.TargetUserID, command.Amount, strings.TrimSpace(command.DrawID), strings.TrimSpace(command.RoomID), command.VisibleRegionID, command.CountryID, strings.TrimSpace(command.GiftID), strings.TrimSpace(command.PoolID), strings.TrimSpace(command.Reason), )) } func wheelRewardRequestHash(command ledger.WheelRewardCommand) string { return stableHash(fmt.Sprintf("wheel_reward|%s|%d|%d|%s|%s|%s|%d|%s", appcode.Normalize(command.AppCode), command.TargetUserID, command.Amount, strings.TrimSpace(command.DrawID), strings.TrimSpace(command.WheelID), strings.TrimSpace(command.SelectedTierID), command.VisibleRegionID, strings.TrimSpace(command.Reason), )) } func roomTurnoverRewardRequestHash(command ledger.RoomTurnoverRewardCommand) string { return stableHash(fmt.Sprintf("room_turnover_reward|%s|%d|%d|%s|%s|%d|%d|%d|%d|%s|%s", appcode.Normalize(command.AppCode), command.TargetUserID, command.Amount, strings.TrimSpace(command.SettlementID), strings.TrimSpace(command.RoomID), command.PeriodStartMS, command.PeriodEndMS, command.CoinSpent, command.TierID, strings.TrimSpace(command.TierCode), strings.TrimSpace(command.Reason), )) } func inviteActivityRewardRequestHash(command ledger.InviteActivityRewardCommand) string { return stableHash(fmt.Sprintf("invite_activity_reward|%s|%d|%d|%s|%s|%d|%s|%s|%d|%s", appcode.Normalize(command.AppCode), command.TargetUserID, command.Amount, strings.TrimSpace(command.ClaimID), strings.TrimSpace(command.RewardType), command.TierID, strings.TrimSpace(command.TierCode), strings.TrimSpace(command.CycleKey), command.ReachedValue, strings.TrimSpace(command.Reason), )) } func agencyOpeningRewardRequestHash(command ledger.AgencyOpeningRewardCommand) string { return stableHash(fmt.Sprintf("agency_opening_reward|%s|%d|%d|%s|%s|%d|%d|%d|%s", appcode.Normalize(command.AppCode), command.TargetUserID, command.Amount, strings.TrimSpace(command.ApplicationID), strings.TrimSpace(command.CycleID), command.AgencyID, command.RankNo, command.ScoreCoins, strings.TrimSpace(command.Reason), )) }