diff --git a/go.mod b/go.mod index 66a4a43..c7e8a9f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,10 @@ module code.30cm.net/digimon/app-cloudep-wallet-service go 1.24.2 require ( + code.30cm.net/digimon/library-go/errs v1.2.14 + github.com/google/uuid v1.6.0 github.com/shopspring/decimal v1.4.0 + github.com/stretchr/testify v1.10.0 github.com/testcontainers/testcontainers-go v0.36.0 github.com/zeromicro/go-zero v1.8.2 google.golang.org/grpc v1.71.1 @@ -48,7 +51,6 @@ require ( github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -85,7 +87,6 @@ require ( github.com/shirou/gopsutil/v4 v4.25.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/stretchr/testify v1.10.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect diff --git a/go.sum b/go.sum index a1b89a4..3542b91 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +code.30cm.net/digimon/library-go/errs v1.2.14 h1:Un9wcIIjjJW8D2i0ISf8ibzp9oNT4OqLsaSKW0T4RJU= +code.30cm.net/digimon/library-go/errs v1.2.14/go.mod h1:Hs4v7SbXNggDVBGXSYsFMjkii1qLF+rugrIpWePN4/o= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= diff --git a/pkg/domain/entity/transaction.go b/pkg/domain/entity/transaction.go index 930fa3c..0fbee9a 100644 --- a/pkg/domain/entity/transaction.go +++ b/pkg/domain/entity/transaction.go @@ -8,21 +8,21 @@ import ( // Transaction 代表一筆錢包交易紀錄(例如充值、扣款、轉帳等) // 此表記錄所有交易的詳細資訊,包括金額、對象、餘額狀態與交易型態等 type Transaction struct { - ID int64 `gorm:"column:id"` // 交易主鍵 ID,自動遞增 - OrderID string `gorm:"column:order_id"` // 關聯的訂單 ID,可為空(若不是由訂單觸發) - TransactionID string `gorm:"column:transaction_id"` // 此筆交易的唯一識別碼(系統內部使用,可為 UUID) - Brand string `gorm:"column:brand"` // 所屬品牌(支援多品牌場景) - UID string `gorm:"column:uid"` // 交易發起者的 UID - ToUID string `gorm:"column:to_uid"` // 交易對象的 UID(如為轉帳場景) - Type wallet.TxType `gorm:"column:type"` // 交易類型(如轉帳、入金、出金等,自定義列舉) - BusinessType int8 `gorm:"column:business_type"` // 業務類型(如合約、模擬、一般用途等,數字代碼) - Crypto string `gorm:"column:crypto"` // 幣種(如 BTC、ETH、USD、TWD 等) - Amount decimal.Decimal `gorm:"column:amount"` // 本次變動金額(正數為增加,負數為扣減) - Balance decimal.Decimal `gorm:"column:balance"` // 交易完成後的錢包餘額 - BeforeBalance decimal.Decimal `gorm:"column:before_balance"` // 交易前的錢包餘額(方便審計與對帳) - Status wallet.Enable `gorm:"column:status"` // 狀態(1: 有效、0: 無效/已取消) - CreateAt int64 `gorm:"column:create_time;autoCreateTime"` // 建立時間(Unix 秒數) - DueTime int64 `gorm:"column:due_time"` // 到期時間(適用於凍結或延後入帳等場景) + ID int64 `gorm:"column:id"` // 交易主鍵 ID,自動遞增 + OrderID string `gorm:"column:order_id"` // 關聯的訂單 ID,可為空(若不是由訂單觸發) + TransactionID string `gorm:"column:transaction_id"` // 此筆交易的唯一識別碼(系統內部使用,可為 UUID) + Brand string `gorm:"column:brand"` // 所屬品牌(支援多品牌場景) + UID string `gorm:"column:uid"` // 交易發起者的 UID + ToUID string `gorm:"column:to_uid"` // 交易對象的 UID(如為轉帳場景) + TxType wallet.TxType `gorm:"column:type"` // 交易類型(如轉帳、入金、出金等,自定義列舉) + BusinessType int8 `gorm:"column:business_type"` // 業務類型(如合約、模擬、一般用途等,數字代碼) + Asset string `gorm:"column:asset"` // 幣種(如 BTC、ETH、USD、TWD 等) + Amount decimal.Decimal `gorm:"column:amount"` // 本次變動金額(正數為增加,負數為扣減) + PostTransferBalance decimal.Decimal `gorm:"column:post_transfer_balance"` // 交易完成後的錢包餘額 + BeforeBalance decimal.Decimal `gorm:"column:before_balance"` // 交易前的錢包餘額(方便審計與對帳) + Status wallet.Enable `gorm:"column:status"` // 狀態(1: 有效、0: 無效/已取消) + CreateAt int64 `gorm:"column:create_time;autoCreateTime"` // 建立時間(Unix 秒數) + DueTime int64 `gorm:"column:due_time"` // 到期時間(適用於凍結或延後入帳等場景) } // TableName 指定 GORM 對應的資料表名稱 diff --git a/pkg/domain/repository/error.go b/pkg/domain/repository/error.go index 735b876..b4ec74b 100644 --- a/pkg/domain/repository/error.go +++ b/pkg/domain/repository/error.go @@ -3,4 +3,4 @@ package repository import "errors" var ErrRecordNotFound = errors.New("query record not found") -var ErrBalanceInsufficient = errors.New("balance insufficient") +var ErrBalanceInsufficient = errors.New("balance insufficient") // 餘額不足 diff --git a/pkg/domain/repository/wallet.go b/pkg/domain/repository/wallet.go index 860f2b7..e2136c6 100644 --- a/pkg/domain/repository/wallet.go +++ b/pkg/domain/repository/wallet.go @@ -72,16 +72,22 @@ type UserWalletService interface { LocalBalance(kind wallet.Types) decimal.Decimal // LockByIDs 根據錢包 ID 鎖定(資料一致性用) LockByIDs(ctx context.Context, ids []int64) ([]entity.Wallet, error) - // CheckReady 檢查錢包是否已經存在並準備好 + // CheckReady 檢查錢包是否已經存在並準備好(可用餘額的錢包) CheckReady(ctx context.Context) (bool, error) // Add 加值與扣款邏輯(含業務類別) Add(kind wallet.Types, orderID string, amount decimal.Decimal) error Sub(kind wallet.Types, orderID string, amount decimal.Decimal) error // AddTransaction 新增一筆交易紀錄(建立資料) AddTransaction(txID int64, orderID string, brand string, business wallet.BusinessName, kind wallet.Types, amount decimal.Decimal) - //// PendingTransactions 查詢尚未執行的交易清單(會在 Execute 中一次提交) - //PendingTransactions() []entity.WalletTransaction + Transactions( + txID int64, + orderID string, + brand string, + businessType wallet.BusinessName, + ) []entity.WalletTransaction // Commit 提交所有操作(更新錢包與新增交易紀錄) Commit(ctx context.Context) error + // CommitOrder 提交所有訂單 + CommitOrder(ctx context.Context) error } diff --git a/pkg/domain/usecase/errors.go b/pkg/domain/usecase/errors.go new file mode 100644 index 0000000..aed2454 --- /dev/null +++ b/pkg/domain/usecase/errors.go @@ -0,0 +1 @@ +package usecase diff --git a/pkg/domain/usecase/wallet.go b/pkg/domain/usecase/wallet.go new file mode 100644 index 0000000..7f77d10 --- /dev/null +++ b/pkg/domain/usecase/wallet.go @@ -0,0 +1,91 @@ +package usecase + +import ( + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet" + "context" + "github.com/shopspring/decimal" +) + +// WalletTransferUseCase 處理錢包資產轉移的核心業務邏輯(支援多種錢包操作) +type WalletTransferUseCase interface { + // Process 處理一次完整的錢包轉帳(含內部轉帳、跨使用者轉帳) + Process(ctx context.Context, req WalletTransferRequest) error + // Withdraw 出金操作(從錢包扣款) + Withdraw(ctx context.Context, tx WalletTransferRequest) error + // Deposit 入金操作(錢包加值) + Deposit(ctx context.Context, tx WalletTransferRequest) error + // DepositUnconfirmed 入金至未確認餘額(如轉帳待確認、預約加值) + DepositUnconfirmed(ctx context.Context, tx WalletTransferRequest) error + // Freeze 將資產從可用餘額移動至凍結餘額(常用於下單等鎖倉需求) + Freeze(ctx context.Context, tx WalletTransferRequest) error + // AppendFreeze 追加凍結金額(已有凍結金額的情況下再次追加) + AppendFreeze(ctx context.Context, tx WalletTransferRequest) error + // UnFreeze 將凍結金額移回可用餘額(如訂單取消) + UnFreeze(ctx context.Context, tx WalletTransferRequest) error + // RollbackFreeze 將凍結金額退回,並作為取消交易的一部分 + RollbackFreeze(ctx context.Context, tx WalletTransferRequest) error + // RollbackFreezeAddAvailable 退回凍結金額並補回可用金額(全額退款流程) + RollbackFreezeAddAvailable(ctx context.Context, tx WalletTransferRequest) error + // CancelFreeze 取消凍結操作(不轉移回可用餘額,只做作廢處理) + CancelFreeze(ctx context.Context, tx WalletTransferRequest) error + // Unconfirmed 將未確認金額移回可用餘額(例如:加值完成確認) + Unconfirmed(ctx context.Context, tx WalletTransferRequest) error + // Balance 查詢目前錢包餘額(含可用與不可用) + Balance(ctx context.Context, req BalanceReq) ([]Balance, error) + // HistoryBalance 查詢歷史錢包快照(指定時間前的餘額狀態) + HistoryBalance(ctx context.Context, req BalanceReq) ([]Balance, error) + // BalanceByAssets 查詢使用者特定資產的各錢包類型餘額 + BalanceByAssets(ctx context.Context, uid, cryptoCode string, walletTypes []wallet.Types) (BalanceAssetsResp, error) + // CheckBalance 驗證可用餘額是否足夠(通常用於風控前置驗證) + CheckBalance(ctx context.Context, tx WalletTransferRequest) error + // GetTodayWithdraw 查詢使用者今日累積出金額 + GetTodayWithdraw(ctx context.Context, uid, toCrypto string) (TodayWithdrawResp, error) +} + +// WalletTransferRequest 表示一次錢包轉帳的請求資料 +type WalletTransferRequest struct { + ReferenceOrderID string // 對應的訂單編號,可為空(非訂單觸發) + FromUID string // 付款方 UID + ToUID string // 收款方 UID,若為錢包內部轉帳可與 FromUID 相同 + Asset string // 資產代號(例如 BTC、ETH、TWD 等) + Amount decimal.Decimal // 轉移金額(正數,系統控制正負方向) + PostTransferBalance decimal.Decimal // 轉帳後餘額(可選填,便於日誌追蹤與審計) + TxType wallet.TxType // 交易類型(如入金、出金、轉帳等) + Business wallet.BusinessName // 業務場景類型(如合約、模擬、一般用途等) + Brand string // 所屬品牌(支援多品牌架構) + FromWalletType wallet.Types // 扣款來源錢包類型 + ToWalletType wallet.Types // 收款目標錢包類型 +} + +// BalanceReq 表示查詢錢包餘額的請求參數 +type BalanceReq struct { + UID string // 使用者 UID + Asset string // 指定資產(如 BTC) + BeforeHour int // 幾小時前的快照(若查歷史快照) +} + +// Balance 表示錢包的當前或歷史餘額狀態 +type Balance struct { + Asset string // 資產代號 + Available decimal.Decimal // 可用餘額 + Unavailable UnavailableBalance // 不可用餘額(凍結、未確認) + UpdateTime int64 // 更新時間(Unix 時間戳) +} + +// UnavailableBalance 表示錢包中不可用的部分 +type UnavailableBalance struct { + Freeze decimal.Decimal // 凍結金額(如掛單中) + Unconfirmed decimal.Decimal // 未確認金額(如等待轉帳) +} + +// BalanceAssetsResp 表示使用者所有錢包類型在某資產的總和 +type BalanceAssetsResp struct { + Balances map[string]decimal.Decimal // 各錢包類型對應的餘額 + Asset string // 查詢的資產代號 +} + +// TodayWithdrawResp 表示使用者今天的累積出金結果 +type TodayWithdrawResp struct { + Withdraw decimal.Decimal // 今日總出金金額 + Asset string // 資產代號 +} diff --git a/pkg/repository/user_wallet.go b/pkg/repository/user_wallet.go index 67fce12..e4180a0 100644 --- a/pkg/repository/user_wallet.go +++ b/pkg/repository/user_wallet.go @@ -202,20 +202,21 @@ func (repo *userWallet) Sub(kind wallet.Types, orderID string, amount decimal.De return repo.Add(kind, orderID, decimal.Zero.Sub(amount)) } -func (repo *userWallet) AddTransaction(txID int64, orderID string, brand string, business wallet.BusinessName, kind wallet.Types, amount decimal.Decimal) { - balance := repo.LocalBalance(kind).Add(amount) - repo.transactions = append(repo.transactions, entity.WalletTransaction{ - TransactionID: txID, - OrderID: orderID, - Brand: brand, - UID: repo.uid, - WalletType: kind, - BusinessType: business.ToInt8(), - Asset: repo.asset, - Amount: amount, - Balance: balance, - CreateAt: time.Now().UTC().UnixNano(), - }) +// Transactions 為本次整筆交易 (txID) 給所有暫存的 WalletTransaction 設置共用欄位, +// 並回傳整批交易紀錄以便後續寫入資料庫。 +func (repo *userWallet) Transactions( + txID int64, + orderID string, + brand string, + businessType wallet.BusinessName, +) []entity.WalletTransaction { + for i := range repo.transactions { + repo.transactions[i].TransactionID = txID + repo.transactions[i].OrderID = orderID + repo.transactions[i].Brand = brand + repo.transactions[i].BusinessType = businessType.ToInt8() + } + return repo.transactions } func (repo *userWallet) Commit(ctx context.Context) error { @@ -282,6 +283,11 @@ func (repo *userWallet) CommitOrder(ctx context.Context) error { return nil } +func (repo *userWallet) AddTransaction(txID int64, orderID string, brand string, business wallet.BusinessName, kind wallet.Types, amount decimal.Decimal) { + //TODO implement me + panic("implement me") +} + // ============================================================================= func (repo *userWallet) buildCommonWhereSQL(uid, asset string) *gorm.DB { diff --git a/pkg/repository/wallet.go b/pkg/repository/wallet.go index a2db79b..f8b3991 100644 --- a/pkg/repository/wallet.go +++ b/pkg/repository/wallet.go @@ -26,6 +26,7 @@ func (repo *WalletRepository) NewDB() *gorm.DB { return repo.DB.Begin() } +// Transaction 指 DB 事務,非記錄一筆交易 func (repo *WalletRepository) Transaction(fn func(db *gorm.DB) error) error { db := repo.DB.Begin(&sql.TxOptions{ Isolation: sql.LevelReadCommitted, diff --git a/pkg/usecase/wallet.go b/pkg/usecase/wallet.go new file mode 100644 index 0000000..848d8ca --- /dev/null +++ b/pkg/usecase/wallet.go @@ -0,0 +1,124 @@ +package usecase + +import ( + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/repository" + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/usecase" + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet" + "code.30cm.net/digimon/library-go/errs" + "context" + "sync" +) + +type WalletUseCaseParam struct { + WalletRepo repository.WalletRepository + TransactionRepo repository.TransactionRepository + WalletTransactionRepo repository.WalletTransactionRepo +} + +type WalletUseCase struct { + WalletUseCaseParam + + // 內存讀寫所記錄有哪些玩家已經確認有存在錢包過了,減少確認錢包是否存在頻率 + sync.RWMutex + existUIDAsset map[string]struct{} +} + +func (use *WalletUseCase) Process(ctx context.Context, req usecase.WalletTransferRequest) error { + //TODO implement me + panic("implement me") +} + +// Withdraw 提幣 +// 1. 新增一筆提幣交易 +// 2. 錢包減少可用餘額 +// 3. 錢包變化新增一筆減少可用餘額資料 +func (use *WalletUseCase) Withdraw(ctx context.Context, tx usecase.WalletTransferRequest) error { + if !tx.Amount.IsPositive() { + return errs.InvalidRange("failed to get correct amount") + } + tx.TxType = wallet.Withdraw + + return use.ProcessTransaction(ctx, tx, userWalletFlow{ + UID: tx.FromUID, + Asset: tx.Asset, + Actions: []walletActionOption{use.withLockAvailable(), use.withSubAvailable()}, //use.lockAvailable(), use.subAvailable() + }) + +} + +func (use *WalletUseCase) Deposit(ctx context.Context, tx usecase.WalletTransferRequest) error { + //TODO implement me + panic("implement me") +} + +func (use *WalletUseCase) DepositUnconfirmed(ctx context.Context, tx usecase.WalletTransferRequest) error { + //TODO implement me + panic("implement me") +} + +func (use *WalletUseCase) Freeze(ctx context.Context, tx usecase.WalletTransferRequest) error { + //TODO implement me + panic("implement me") +} + +func (use *WalletUseCase) AppendFreeze(ctx context.Context, tx usecase.WalletTransferRequest) error { + //TODO implement me + panic("implement me") +} + +func (use *WalletUseCase) UnFreeze(ctx context.Context, tx usecase.WalletTransferRequest) error { + //TODO implement me + panic("implement me") +} + +func (use *WalletUseCase) RollbackFreeze(ctx context.Context, tx usecase.WalletTransferRequest) error { + //TODO implement me + panic("implement me") +} + +func (use *WalletUseCase) RollbackFreezeAddAvailable(ctx context.Context, tx usecase.WalletTransferRequest) error { + //TODO implement me + panic("implement me") +} + +func (use *WalletUseCase) CancelFreeze(ctx context.Context, tx usecase.WalletTransferRequest) error { + //TODO implement me + panic("implement me") +} + +func (use *WalletUseCase) Unconfirmed(ctx context.Context, tx usecase.WalletTransferRequest) error { + //TODO implement me + panic("implement me") +} + +func (use *WalletUseCase) Balance(ctx context.Context, req usecase.BalanceReq) ([]usecase.Balance, error) { + //TODO implement me + panic("implement me") +} + +func (use *WalletUseCase) HistoryBalance(ctx context.Context, req usecase.BalanceReq) ([]usecase.Balance, error) { + //TODO implement me + panic("implement me") +} + +func (use *WalletUseCase) BalanceByAssets(ctx context.Context, uid, cryptoCode string, walletTypes []wallet.Types) (usecase.BalanceAssetsResp, error) { + //TODO implement me + panic("implement me") +} + +func (use *WalletUseCase) CheckBalance(ctx context.Context, tx usecase.WalletTransferRequest) error { + //TODO implement me + panic("implement me") +} + +func (use *WalletUseCase) GetTodayWithdraw(ctx context.Context, uid, toCrypto string) (usecase.TodayWithdrawResp, error) { + //TODO implement me + panic("implement me") +} + +func MustWalletUseCase(param WalletUseCaseParam) usecase.WalletTransferUseCase { + return &WalletUseCase{ + WalletUseCaseParam: param, + existUIDAsset: make(map[string]struct{}), + } +} diff --git a/pkg/usecase/wallet_tx_option.go b/pkg/usecase/wallet_tx_option.go new file mode 100644 index 0000000..3441c32 --- /dev/null +++ b/pkg/usecase/wallet_tx_option.go @@ -0,0 +1,79 @@ +package usecase + +import ( + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/repository" + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/usecase" + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet" + "context" + "errors" + "fmt" +) + +type uidAssetKey struct { + uid string + asset string +} + +// walletActionOption 表示一個「錢包操作函式」,可在錢包流程中插入自定義動作(例如:扣款、加值、凍結) +type walletActionOption func( + ctx context.Context, + tx *usecase.WalletTransferRequest, + wallet repository.UserWalletService, +) error + +// withLockAvailable 鎖定用戶可用餘額 +func (use *WalletUseCase) withLockAvailable() walletActionOption { + return func(ctx context.Context, tx *usecase.WalletTransferRequest, w repository.UserWalletService) error { + uidAsset := uidAssetKey{ + uid: tx.FromUID, + asset: tx.Asset, + } + + if !use.checkWalletExistence(uidAsset) { + // 找不到錢包存不存在 + wStatus, err := w.CheckReady(ctx) + if err != nil { + return fmt.Errorf("failed to check wallet: %w", err) + } + // 錢包不存在要做新增 + if !wStatus { + //// 是合約模擬交易或帳變且錢包不存在才建立錢包 + //if !(tx.Business == wa.ContractSimulationBusinessTypeBusinessName || tx.BusinessType == domain.DistributionBusinessTypeBusinessName) { + // // 新增錢包有命中 UK 不需要額外上鎖 + // return use.translateError(err) + //} + + if _, err := w.Init(ctx, tx.FromUID, tx.Asset, tx.Brand); err != nil { + return err + } + + return nil + } + + use.markWalletAsExisting(uidAsset) + } + + _, err := w.GetWithLock(ctx, []wallet.Types{wallet.TypeAvailable}) + if err != nil { + return err + } + + return nil + } +} + +// subAvailable 減少用戶可用餘額 +func (use *WalletUseCase) withSubAvailable() walletActionOption { + return func(_ context.Context, tx *usecase.WalletTransferRequest, w repository.UserWalletService) error { + if err := w.Sub(wallet.TypeAvailable, tx.ReferenceOrderID, tx.Amount); err != nil { + if errors.Is(err, repository.ErrBalanceInsufficient) { + // todo 錯誤要看怎麼給(餘額不足) + return fmt.Errorf("balance insufficient") + } + + return err + } + + return nil + } +} diff --git a/pkg/usecase/wallet_tx_processer.go b/pkg/usecase/wallet_tx_processer.go new file mode 100644 index 0000000..c34fcfa --- /dev/null +++ b/pkg/usecase/wallet_tx_processer.go @@ -0,0 +1,127 @@ +package usecase + +import ( + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/repository" + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/usecase" + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet" + repo "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/repository" + "context" + "fmt" + "github.com/google/uuid" + "gorm.io/gorm" +) + +// userWalletFlow 表示一組要對某使用者、某資產執行的錢包操作(動作串列) +// 這些操作通常會在轉帳流程中依序套用,例如:從主錢包扣款 → 加到對方凍結錢包 +type userWalletFlow struct { + UID string // 目標使用者 UID + Asset string // 目標資產代號(如 BTC、ETH、TWD) + Actions []walletActionOption // 要依序執行的錢包操作 +} + +// ProcessTransaction 處理一次完整的「錢包 + 訂單」交易流程: +// 1. 透過 Repository 開啟 DB 事務 +// 2. 依序對每個 userWalletFlow 建立對應的 UserWalletService 實例 +// 3. 依序執行每個 flow.Actions(扣款、加值、凍結、解凍…) +// 4. 建立一條 Transaction 記錄並寫進 transactionRepository +// 5. 將所有 walletTransactions 寫進 walletTransactionRepository +// 6. 最後在同一事務中執行每個 wallet 的 Execute / ExecuteOrder +// 7. 特別注意 flow 會按照順序做,所以順序是重要的 +func (use *WalletUseCase) ProcessTransaction( + ctx context.Context, + req usecase.WalletTransferRequest, + flows ...userWalletFlow, +) error { + return use.WalletRepo.Transaction(func(db *gorm.DB) error { + // 暫存所有建立好的 UserWalletService + wallets := make([]repository.UserWalletService, 0, len(flows)) + + // flows 會按照順序做.順序是重要的 + for _, flow := range flows { + // 1️⃣ 建立針對該使用者+資產的 UserWalletService + wSvc := repo.NewUserWallet(db, flow.UID, flow.Asset) + + // 2️⃣ 依序執行所有定義好的錢包操作 + for _, action := range flow.Actions { + if err := action(ctx, &req, wSvc); err != nil { + return err + } + } + + wallets = append(wallets, wSvc) + } + + // 3️⃣ 準備寫入 Transaction 主檔 + txRecord := &entity.Transaction{ + OrderID: req.ReferenceOrderID, + TransactionID: uuid.New().String(), + UID: req.FromUID, + ToUID: req.ToUID, + Asset: req.Asset, + TxType: req.TxType, + Amount: req.Amount, + Brand: req.Brand, + PostTransferBalance: req.PostTransferBalance, + BusinessType: req.Business.ToInt8(), + Status: wallet.EnableTrue, + DueTime: 0, + } + + // 4️⃣ TODO 計算 DueTime (T+N 結算時間) + + // 5️⃣ 寫入 Transaction 主檔 + if err := use.TransactionRepo.Insert(ctx, txRecord); err != nil { + return fmt.Errorf("TransactionRepo.Insert 失敗: %w", err) + } + + // 6️⃣ 聚合所有 wallet 內的交易歷程 + var walletTxs []entity.WalletTransaction + for _, w := range wallets { + walletTxs = append( + walletTxs, + w.Transactions( + txRecord.ID, + txRecord.OrderID, + req.Brand, + req.Business, + )..., + ) + } + + // 7️⃣ 批次寫入所有 WalletTransaction + if err := use.WalletTransactionRepo.Create(ctx, db, walletTxs); err != nil { + return fmt.Errorf("WalletTransactionRepository.Create 失敗: %w", err) + } + + // 8️⃣ 最後才真正把錢包的餘額更新到資料庫(同一事務) + for _, wSvc := range wallets { + if err := wSvc.Commit(ctx); err != nil { + return err + } + if err := wSvc.CommitOrder(ctx); err != nil { + return err + } + } + + return nil + }) +} + +// checkWalletExistence 檢查錢包是否存在於內存中 +func (use *WalletUseCase) checkWalletExistence(uidAsset uidAssetKey) bool { + use.RLock() + defer use.RUnlock() + rk := fmt.Sprintf("%s-%s", uidAsset.uid, uidAsset.asset) + _, exists := use.existUIDAsset[rk] + + return exists +} + +// markWalletAsExisting 標記錢包為存在 +func (use *WalletUseCase) markWalletAsExisting(uidAsset uidAssetKey) { + use.Lock() + defer use.Unlock() + rk := fmt.Sprintf("%s-%s", uidAsset.uid, uidAsset.asset) + use.existUIDAsset[rk] = struct{}{} +}