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

106 lines
3.7 KiB
Go

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