package mysql import ( "context" "fmt" "math" "time" "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" ) // AdminCreditAsset 在一个事务内写入后台调账交易、分录和 outbox。 func (r *Repository) AdminCreditAsset(ctx context.Context, command ledger.AdminCreditAssetCommand) (ledger.AssetBalance, string, error) { if r == nil || r.db == nil { return ledger.AssetBalance{}, "", 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.AssetBalance{}, "", err } defer func() { _ = tx.Rollback() }() requestHash := adminCreditRequestHash(command) if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeManualCredit); err != nil || exists { if err != nil || !exists { return ledger.AssetBalance{}, "", err } balance, balanceErr := r.balanceAfterTransaction(ctx, tx, txRow.TransactionID, command.TargetUserID, command.AssetType) return balance, txRow.TransactionID, balanceErr } nowMs := time.Now().UnixMilli() account, err := r.lockAccount(ctx, tx, command.TargetUserID, command.AssetType, command.Amount > 0, nowMs) if err != nil { return ledger.AssetBalance{}, "", err } if command.Amount < 0 && (command.Amount == math.MinInt64 || account.AvailableAmount < -command.Amount) { return ledger.AssetBalance{}, "", xerr.New(xerr.InsufficientBalance, "insufficient balance") } transactionID := transactionID(command.AppCode, command.CommandID) metadata := adminCreditMetadata{ AppCode: command.AppCode, TargetUserID: command.TargetUserID, AssetType: command.AssetType, Amount: command.Amount, OperatorUserID: command.OperatorUserID, Reason: command.Reason, EvidenceRef: command.EvidenceRef, } if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeManualCredit, requestHash, command.EvidenceRef, metadata, nowMs); err != nil { return ledger.AssetBalance{}, "", err } if err := r.applyAccountDelta(ctx, tx, account, command.Amount, 0, nowMs); err != nil { return ledger.AssetBalance{}, "", err } balance := ledger.AssetBalance{ AppCode: command.AppCode, UserID: command.TargetUserID, AssetType: command.AssetType, AvailableAmount: account.AvailableAmount + command.Amount, FrozenAmount: account.FrozenAmount, Version: account.Version + 1, UpdatedAtMs: nowMs, } if err := r.insertEntry(ctx, tx, walletEntry{ TransactionID: transactionID, UserID: command.TargetUserID, AssetType: command.AssetType, AvailableDelta: command.Amount, FrozenDelta: 0, AvailableAfter: balance.AvailableAmount, FrozenAfter: balance.FrozenAmount, CreatedAtMS: nowMs, }); err != nil { return ledger.AssetBalance{}, "", err } if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, command.AssetType, command.Amount, 0, balance.AvailableAmount, balance.FrozenAmount, balance.Version, metadata, nowMs, bizTypeManualCredit), }); err != nil { return ledger.AssetBalance{}, "", err } if err := tx.Commit(); err != nil { return ledger.AssetBalance{}, "", err } return balance, transactionID, nil } func adminCreditRequestHash(command ledger.AdminCreditAssetCommand) string { return stableHash(fmt.Sprintf("admin_credit|%s|%d|%s|%d|%d|%s|%s", appcode.Normalize(command.AppCode), command.TargetUserID, command.AssetType, command.Amount, command.OperatorUserID, command.Reason, command.EvidenceRef, )) }