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

756 lines
28 KiB
Go

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