feat: add wallet repo

This commit is contained in:
王性驊 2025-04-17 17:00:42 +08:00
parent 45fab245d9
commit 4f6262d489
12 changed files with 473 additions and 35 deletions

5
go.mod
View File

@ -3,7 +3,10 @@ module code.30cm.net/digimon/app-cloudep-wallet-service
go 1.24.2 go 1.24.2
require ( 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/shopspring/decimal v1.4.0
github.com/stretchr/testify v1.10.0
github.com/testcontainers/testcontainers-go v0.36.0 github.com/testcontainers/testcontainers-go v0.36.0
github.com/zeromicro/go-zero v1.8.2 github.com/zeromicro/go-zero v1.8.2
google.golang.org/grpc v1.71.1 google.golang.org/grpc v1.71.1
@ -48,7 +51,6 @@ require (
github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.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/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // 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/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spaolacci/murmur3 v1.1.0 // 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/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect

2
go.sum
View File

@ -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 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=

View File

@ -14,11 +14,11 @@ type Transaction struct {
Brand string `gorm:"column:brand"` // 所屬品牌(支援多品牌場景) Brand string `gorm:"column:brand"` // 所屬品牌(支援多品牌場景)
UID string `gorm:"column:uid"` // 交易發起者的 UID UID string `gorm:"column:uid"` // 交易發起者的 UID
ToUID string `gorm:"column:to_uid"` // 交易對象的 UID如為轉帳場景 ToUID string `gorm:"column:to_uid"` // 交易對象的 UID如為轉帳場景
Type wallet.TxType `gorm:"column:type"` // 交易類型(如轉帳、入金、出金等,自定義列舉) TxType wallet.TxType `gorm:"column:type"` // 交易類型(如轉帳、入金、出金等,自定義列舉)
BusinessType int8 `gorm:"column:business_type"` // 業務類型(如合約、模擬、一般用途等,數字代碼) BusinessType int8 `gorm:"column:business_type"` // 業務類型(如合約、模擬、一般用途等,數字代碼)
Crypto string `gorm:"column:crypto"` // 幣種(如 BTC、ETH、USD、TWD 等) Asset string `gorm:"column:asset"` // 幣種(如 BTC、ETH、USD、TWD 等)
Amount decimal.Decimal `gorm:"column:amount"` // 本次變動金額(正數為增加,負數為扣減) Amount decimal.Decimal `gorm:"column:amount"` // 本次變動金額(正數為增加,負數為扣減)
Balance decimal.Decimal `gorm:"column:balance"` // 交易完成後的錢包餘額 PostTransferBalance decimal.Decimal `gorm:"column:post_transfer_balance"` // 交易完成後的錢包餘額
BeforeBalance decimal.Decimal `gorm:"column:before_balance"` // 交易前的錢包餘額(方便審計與對帳) BeforeBalance decimal.Decimal `gorm:"column:before_balance"` // 交易前的錢包餘額(方便審計與對帳)
Status wallet.Enable `gorm:"column:status"` // 狀態1: 有效、0: 無效/已取消) Status wallet.Enable `gorm:"column:status"` // 狀態1: 有效、0: 無效/已取消)
CreateAt int64 `gorm:"column:create_time;autoCreateTime"` // 建立時間Unix 秒數) CreateAt int64 `gorm:"column:create_time;autoCreateTime"` // 建立時間Unix 秒數)

View File

@ -3,4 +3,4 @@ package repository
import "errors" import "errors"
var ErrRecordNotFound = errors.New("query record not found") var ErrRecordNotFound = errors.New("query record not found")
var ErrBalanceInsufficient = errors.New("balance insufficient") var ErrBalanceInsufficient = errors.New("balance insufficient") // 餘額不足

View File

@ -72,16 +72,22 @@ type UserWalletService interface {
LocalBalance(kind wallet.Types) decimal.Decimal LocalBalance(kind wallet.Types) decimal.Decimal
// LockByIDs 根據錢包 ID 鎖定(資料一致性用) // LockByIDs 根據錢包 ID 鎖定(資料一致性用)
LockByIDs(ctx context.Context, ids []int64) ([]entity.Wallet, error) LockByIDs(ctx context.Context, ids []int64) ([]entity.Wallet, error)
// CheckReady 檢查錢包是否已經存在並準備好 // CheckReady 檢查錢包是否已經存在並準備好(可用餘額的錢包)
CheckReady(ctx context.Context) (bool, error) CheckReady(ctx context.Context) (bool, error)
// Add 加值與扣款邏輯(含業務類別) // Add 加值與扣款邏輯(含業務類別)
Add(kind wallet.Types, orderID string, amount decimal.Decimal) error Add(kind wallet.Types, orderID string, amount decimal.Decimal) error
Sub(kind wallet.Types, orderID string, amount decimal.Decimal) error Sub(kind wallet.Types, orderID string, amount decimal.Decimal) error
// AddTransaction 新增一筆交易紀錄(建立資料) // AddTransaction 新增一筆交易紀錄(建立資料)
AddTransaction(txID int64, orderID string, brand string, business wallet.BusinessName, kind wallet.Types, amount decimal.Decimal) AddTransaction(txID int64, orderID string, brand string, business wallet.BusinessName, kind wallet.Types, amount decimal.Decimal)
//// PendingTransactions 查詢尚未執行的交易清單(會在 Execute 中一次提交) Transactions(
//PendingTransactions() []entity.WalletTransaction txID int64,
orderID string,
brand string,
businessType wallet.BusinessName,
) []entity.WalletTransaction
// Commit 提交所有操作(更新錢包與新增交易紀錄) // Commit 提交所有操作(更新錢包與新增交易紀錄)
Commit(ctx context.Context) error Commit(ctx context.Context) error
// CommitOrder 提交所有訂單
CommitOrder(ctx context.Context) error
} }

View File

@ -0,0 +1 @@
package usecase

View File

@ -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 // 資產代號
}

View File

@ -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)) 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) { // Transactions 為本次整筆交易 (txID) 給所有暫存的 WalletTransaction 設置共用欄位,
balance := repo.LocalBalance(kind).Add(amount) // 並回傳整批交易紀錄以便後續寫入資料庫。
repo.transactions = append(repo.transactions, entity.WalletTransaction{ func (repo *userWallet) Transactions(
TransactionID: txID, txID int64,
OrderID: orderID, orderID string,
Brand: brand, brand string,
UID: repo.uid, businessType wallet.BusinessName,
WalletType: kind, ) []entity.WalletTransaction {
BusinessType: business.ToInt8(), for i := range repo.transactions {
Asset: repo.asset, repo.transactions[i].TransactionID = txID
Amount: amount, repo.transactions[i].OrderID = orderID
Balance: balance, repo.transactions[i].Brand = brand
CreateAt: time.Now().UTC().UnixNano(), repo.transactions[i].BusinessType = businessType.ToInt8()
}) }
return repo.transactions
} }
func (repo *userWallet) Commit(ctx context.Context) error { func (repo *userWallet) Commit(ctx context.Context) error {
@ -282,6 +283,11 @@ func (repo *userWallet) CommitOrder(ctx context.Context) error {
return nil 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 { func (repo *userWallet) buildCommonWhereSQL(uid, asset string) *gorm.DB {

View File

@ -26,6 +26,7 @@ func (repo *WalletRepository) NewDB() *gorm.DB {
return repo.DB.Begin() return repo.DB.Begin()
} }
// Transaction 指 DB 事務,非記錄一筆交易
func (repo *WalletRepository) Transaction(fn func(db *gorm.DB) error) error { func (repo *WalletRepository) Transaction(fn func(db *gorm.DB) error) error {
db := repo.DB.Begin(&sql.TxOptions{ db := repo.DB.Begin(&sql.TxOptions{
Isolation: sql.LevelReadCommitted, Isolation: sql.LevelReadCommitted,

124
pkg/usecase/wallet.go Normal file
View File

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

View File

@ -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
}
}

View File

@ -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{}{}
}