From 59064b0a6927a5d3eec76b82c4e63ce965607d2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Fri, 11 Apr 2025 17:10:34 +0800 Subject: [PATCH] add wallet and user wallet --- pkg/domain/entity/transaction.go | 37 ++ pkg/domain/entity/wallet_transaction.go | 26 + pkg/domain/repository/error.go | 19 + pkg/domain/repository/user_wallet_action.go | 98 ++++ pkg/domain/repository/wallet.go | 34 +- pkg/domain/wallet/business_name.go | 4 + pkg/domain/wallet/enable_type.go | 16 + pkg/domain/wallet/tx_type.go | 40 ++ pkg/domain/wallet/wallet_type.go | 19 +- pkg/repository/user_wallet.go | 502 +++++++++++++++++++- pkg/repository/wallet.go | 40 +- 11 files changed, 789 insertions(+), 46 deletions(-) create mode 100644 pkg/domain/entity/transaction.go create mode 100644 pkg/domain/entity/wallet_transaction.go create mode 100644 pkg/domain/repository/error.go create mode 100644 pkg/domain/repository/user_wallet_action.go create mode 100644 pkg/domain/wallet/enable_type.go create mode 100644 pkg/domain/wallet/tx_type.go diff --git a/pkg/domain/entity/transaction.go b/pkg/domain/entity/transaction.go new file mode 100644 index 0000000..1442372 --- /dev/null +++ b/pkg/domain/entity/transaction.go @@ -0,0 +1,37 @@ +package entity + +import ( + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet" + "github.com/shopspring/decimal" +) + +//graph TD +// A[使用者下單/轉帳/出金等行為] --> B[建立 Transaction 主交易紀錄] +// B --> C1[WalletTransaction:可用餘額 -100] +// B --> C2[WalletTransaction:凍結餘額 +100] +// C1 --> D1[錢包資料庫:更新可用餘額] +// C2 --> D2[錢包資料庫:更新凍結餘額] + +// Transaction 表示一次錢包資金交易的紀錄 +type Transaction struct { + ID int64 `gorm:"column:id"` // 資料表主鍵 ID + OrderID string `gorm:"column:order_id"` // 外部訂單編號(例如交易訂單編號) + TransactionID string `gorm:"column:transaction_id"` // 此筆資金交易的唯一識別碼 + Brand string `gorm:"column:brand"` // 品牌或平台識別(用於多租戶區分) + UID string `gorm:"column:uid"` // 發起交易的用戶 UID + ToUID string `gorm:"column:to_uid"` // 接收方用戶 UID(如為轉帳類型) + Type wallet.TransactionType `gorm:"column:type"` // 資金異動類型(如充值、提領、轉帳等) + BusinessType int8 `gorm:"column:business_type"` // 業務類型(對應平台不同業務,例如系統轉帳、合約轉帳等) + Asset string `gorm:"column:asset"` // 幣種(或平台定義的資產名稱) + Amount decimal.Decimal `gorm:"column:amount"` // 異動金額(正負表示收入或支出) + Balance decimal.Decimal `gorm:"column:balance"` // 異動後錢包餘額 + BeforeBalance decimal.Decimal `gorm:"column:before_balance"` // 異動前錢包餘額 + Status bool `gorm:"column:status"` // 狀態(true 表示成功,false 表示失敗或未確認) + CreateAt int64 `gorm:"column:create_at;autoCreateTime"` // 建立時間(Unix timestamp) + DueTime int64 `gorm:"column:due_time"` // 到期時間(可用於未完成交易的失效邏輯) +} + +// TableName 回傳對應的資料表名稱 +func (t *Transaction) TableName() string { + return "transaction" +} diff --git a/pkg/domain/entity/wallet_transaction.go b/pkg/domain/entity/wallet_transaction.go new file mode 100644 index 0000000..cb2e313 --- /dev/null +++ b/pkg/domain/entity/wallet_transaction.go @@ -0,0 +1,26 @@ +package entity + +import ( + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet" + "github.com/shopspring/decimal" +) + +// WalletTransaction 表示錢包中每一個餘額類型的交易細節紀錄(例如:可用金額、凍結金額分別異動) +type WalletTransaction struct { + ID int64 `gorm:"column:id"` // 資料表主鍵 ID + TransactionID int64 `gorm:"column:transaction_id"` // 對應的主交易 ID(Transaction.ID) + OrderID string `gorm:"column:order_id"` // 對應的訂單編號(可能與主交易相同) + Brand string `gorm:"column:brand"` // 品牌識別(用於多租戶區分) + UID string `gorm:"column:uid"` // 用戶 UID(這筆交易所屬的使用者) + WalletType wallet.Types `gorm:"column:wallet_type"` // 錢包類型(例如:可用、凍結、未確認等) + BusinessType int8 `gorm:"column:business_type"` // 業務類型(例如:合約、系統轉帳、分潤等) + Asset string `gorm:"column:asset"` // 幣別或資產識別碼 + Amount decimal.Decimal `gorm:"column:amount"` // 異動金額(正數入帳,負數出帳) + Balance decimal.Decimal `gorm:"column:balance"` // 異動後的該錢包類型餘額 + CreateTime int64 `gorm:"column:create_time;autoCreateTime"` // 創建時間(Unix timestamp) +} + +// TableName 指定對應的資料表名稱 +func (t *WalletTransaction) TableName() string { + return "wallet_transaction" +} diff --git a/pkg/domain/repository/error.go b/pkg/domain/repository/error.go new file mode 100644 index 0000000..9da943a --- /dev/null +++ b/pkg/domain/repository/error.go @@ -0,0 +1,19 @@ +package repository + +import ( + "errors" + "gorm.io/gorm" +) + +var ( + ErrRecordNotFound = errors.New("query record not found") + ErrBalanceInsufficient = errors.New("balance insufficient") +) + +func WrapNotFoundError(err error) error { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrRecordNotFound + } + + return err +} diff --git a/pkg/domain/repository/user_wallet_action.go b/pkg/domain/repository/user_wallet_action.go new file mode 100644 index 0000000..bb05bc5 --- /dev/null +++ b/pkg/domain/repository/user_wallet_action.go @@ -0,0 +1,98 @@ +package repository + +import ( + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet" + "context" + "github.com/shopspring/decimal" +) + +// WalletReader 定義查詢錢包餘額的操作,純讀取,不涉及鎖或修改。 +type WalletReader interface { + // AllBalances 查詢此用戶單一幣別所有錢包(可用/凍結/...) + AllBalances(ctx context.Context) ([]entity.Wallet, error) + // Balance 查詢單一餘額種類 + Balance(ctx context.Context, kind wallet.Types) (entity.Wallet, error) + // Balances 查詢多個餘額種類 + Balances(ctx context.Context, kind []wallet.Types) ([]entity.Wallet, error) + // LocalBalance 取得本地記憶體暫存餘額 + LocalBalance(kind wallet.Types) decimal.Decimal +} + +// WalletLocker 提供所有需要「加鎖(XLock)」的錢包操作,用於避免併發交易衝突。 +// 通常搭配交易流程使用,保證同時只有一個執行緒可以修改該錢包類型或訂單相關餘額。 +type WalletLocker interface { + // BalanceXLock 查詢指定錢包類型的餘額,並加上排他鎖(FOR UPDATE) + BalanceXLock(ctx context.Context, kind wallet.Types) (entity.Wallet, error) + // BalancesXLock 查詢多個錢包類型的餘額,並加上排他鎖(FOR UPDATE) + BalancesXLock(ctx context.Context, kind []wallet.Types) ([]entity.Wallet, error) + // XLock 針對指定錢包紀錄 ID 加鎖(FOR UPDATE) + XLock(ctx context.Context, id int64) (entity.Wallet, error) + // XLocks 針對多筆錢包紀錄 ID 加鎖(FOR UPDATE) + XLocks(ctx context.Context, ids []int64) ([]entity.Wallet, error) + // GetAvailableBalanceXLock 查詢指定業務邏輯下的可用餘額並加鎖 + GetAvailableBalanceXLock(ctx context.Context, b wallet.BusinessName) (entity.Wallet, error) + // GetFreezeBalanceXLock 查詢指定業務邏輯下的凍結餘額並加鎖 + GetFreezeBalanceXLock(ctx context.Context, b wallet.BusinessName) (entity.Wallet, error) + // GetUnconfirmedBalanceXLock 查詢尚未確認(unconfirmed)的餘額並加鎖 + GetUnconfirmedBalanceXLock(ctx context.Context) (entity.Wallet, error) + // GetOrderBalanceXLock 查詢某筆訂單的餘額資料並加鎖 + GetOrderBalanceXLock(ctx context.Context, id int64) (entity.Transaction, error) +} + +// WalletOperator 負責錢包的餘額加減操作與訂單金額變動邏輯。 +// 此介面不包含「資料鎖定(Lock)」與「資料持久化(Execute)」等責任,單純聚焦於記錄錢包與訂單餘額的變動邏輯。 +type WalletOperator interface { + // AddBalance 增加指定錢包類型的餘額(使用前必須已查詢該餘額並存在本地記憶體) + AddBalance(kind wallet.Types, amount decimal.Decimal) error + // AddBalanceSetOrderID 增加指定錢包類型的餘額並記錄訂單 ID(用於與訂單綁定的交易) + AddBalanceSetOrderID(orderID string, kind wallet.Types, amount decimal.Decimal) error + // AddAvailable 增加可用餘額(根據業務類型自動判斷使用哪種錢包) + AddAvailable(b wallet.BusinessName, amount decimal.Decimal) error + // AddFreeze 增加凍結餘額(根據業務類型自動判斷使用哪種錢包) + AddFreeze(b wallet.BusinessName, amount decimal.Decimal) error + // AddUnconfirmed 增加尚未確認(unconfirmed)的餘額 + AddUnconfirmed(amount decimal.Decimal) error + // AddOrder 增加與訂單相關的餘額(僅適用於交易表中訂單類型紀錄) + AddOrder(id int64, amount decimal.Decimal) error + // SubAvailable 減少可用餘額(根據業務類型自動判斷使用哪種錢包) + SubAvailable(b wallet.BusinessName, amount decimal.Decimal) error + // SubFreeze 減少凍結餘額(根據業務類型自動判斷使用哪種錢包) + SubFreeze(b wallet.BusinessName, amount decimal.Decimal) error + // SubUnconfirmed 減少尚未確認(unconfirmed)的餘額 + SubUnconfirmed(amount decimal.Decimal) error + // GetAvailableBalance 查詢某業務類型對應的可用餘額 + GetAvailableBalance(ctx context.Context, b wallet.BusinessName) (entity.Wallet, error) + // GetFreezeBalance 查詢某業務類型對應的凍結餘額 + GetFreezeBalance(ctx context.Context, b wallet.BusinessName) (entity.Wallet, error) + // GetUnconfirmedBalance 查詢尚未確認的餘額 + GetUnconfirmedBalance(ctx context.Context) (entity.Wallet, error) + // GetOrderBalance 查詢與指定訂單 ID 相關的餘額紀錄(訂單錢包交易) + GetOrderBalance(ctx context.Context, orderID string) (entity.Transaction, error) + // CheckWallet 確認錢包是否存在(通常用於初始化前檢查) + CheckWallet(ctx context.Context) (bool, error) +} + +// WalletTransactional 負責執行錢包與訂單餘額的最終更新(實際寫入資料庫), +// 並提供錢包交易紀錄的取得方法,主要用途為提交交易與後續紀錄保存。 +type WalletTransactional interface { + // Execute 實際寫入錢包餘額的變動(例如:可用、凍結、未確認餘額等) + // 此方法應在交易邏輯處理完畢後被呼叫,用於同步 local 記憶體中的餘額至資料庫。 + Execute(ctx context.Context) error + // ExecuteOrder 實際寫入訂單餘額的變動(通常指向 Transaction 表中的餘額欄位) + // 適用於與特定訂單相關的錢包操作(如訂單鎖定、釋放等)。 + ExecuteOrder(ctx context.Context) error + // Transactions 建立錢包交易紀錄列表,用於最終寫入錢包交易表 wallet_transaction。 + // 會補齊必要的交易相關欄位(如 txID、orderID、brand、businessType)。 + Transactions(txID int64, orderID, brand string, businessType wallet.BusinessName) []entity.WalletTransaction + // GetTransactions 回傳目前記憶體中所累積的錢包交易紀錄,用於外部查詢或後續處理。 + GetTransactions() []entity.WalletTransaction +} + +// UserWalletService 統整所有錢包相關功能,對外仍可以使用這個 interface。 +type UserWalletService interface { + WalletReader + WalletLocker + WalletOperator + WalletTransactional +} diff --git a/pkg/domain/repository/wallet.go b/pkg/domain/repository/wallet.go index 977e646..bb4078f 100644 --- a/pkg/domain/repository/wallet.go +++ b/pkg/domain/repository/wallet.go @@ -42,13 +42,13 @@ type WalletRepository interface { // Transaction 資料庫交易包裝器(確保交易一致性) Transaction(fn func(db *gorm.DB) error) error // InitWallets 初始化使用者的所有錢包類型(如可用、凍結等) - InitWallets(ctx context.Context, param []Wallet) error + InitWallets(ctx context.Context, param Wallet) ([]entity.Wallet, error) // QueryBalances 查詢特定資產的錢包餘額 QueryBalances(ctx context.Context, req BalanceQuery) ([]entity.Wallet, error) // QueryBalancesByUIDs 查詢多個使用者在特定資產下的錢包餘額 QueryBalancesByUIDs(ctx context.Context, uids []string, req BalanceQuery) ([]entity.Wallet, error) - //// GetDailyTxAmount 查詢使用者今日交易總金額(指定類型與業務) - //GetDailyTxAmount(ctx context.Context, uid string, txTypes []domain.TxType, business wallet.BusinessName) ([]entity.Wallet, error) + // GetDailyTxAmount 查詢使用者今日交易總金額(指定類型與業務) + GetDailyTxAmount(ctx context.Context, uid string, txTypes []wallet.TransactionType, business wallet.BusinessName) ([]entity.Wallet, error) } // BalanceQuery 是查詢餘額時的篩選條件 @@ -57,31 +57,3 @@ type BalanceQuery struct { Asset string // 資產類型(Crypto、寶石等) Kinds []wallet.Types // 錢包類型(如可用、凍結等) } - -// UserWalletService 專注於某位使用者在單一資產下的錢包操作邏輯 -type UserWalletService interface { - // Init 初始化錢包(如建立可用、凍結、未確認等錢包) - Init(ctx context.Context, uid, asset, brand string) ([]entity.Wallet, error) - // All 查詢所有錢包餘額 - All(ctx context.Context) ([]entity.Wallet, error) - // Get 查詢單一或多種類型的餘額 - Get(ctx context.Context, kinds []wallet.Types) ([]entity.Wallet, error) - // GetWithLock 查詢鎖定後的錢包(交易使用) - GetWithLock(ctx context.Context, kinds []wallet.Types) ([]entity.Wallet, error) - // LocalBalance 查詢記憶中的快取值(非查資料庫) - LocalBalance(kind wallet.Types) decimal.Decimal - // LockByIDs 根據錢包 ID 鎖定(資料一致性用) - LockByIDs(ctx context.Context, ids []int64) ([]entity.Wallet, error) - // CheckReady 檢查錢包是否已經存在並準備好 - CheckReady(ctx context.Context) (bool, error) - // Add 加值與扣款邏輯(含業務類別) - Add(kind wallet.Types, business wallet.BusinessName, amount decimal.Decimal) error - Sub(kind wallet.Types, business wallet.BusinessName, 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 - - // Commit 提交所有操作(更新錢包與新增交易紀錄) - Commit(ctx context.Context) error -} diff --git a/pkg/domain/wallet/business_name.go b/pkg/domain/wallet/business_name.go index a9aaac0..2ee0aae 100644 --- a/pkg/domain/wallet/business_name.go +++ b/pkg/domain/wallet/business_name.go @@ -1,3 +1,7 @@ package wallet type BusinessName string + +func (b BusinessName) ToINT8() int8 { + return int8(0) +} diff --git a/pkg/domain/wallet/enable_type.go b/pkg/domain/wallet/enable_type.go new file mode 100644 index 0000000..16d506d --- /dev/null +++ b/pkg/domain/wallet/enable_type.go @@ -0,0 +1,16 @@ +package wallet + +type Enable int8 + +const ( + EnableTrue Enable = 1 + EnableFalse Enable = 2 +) + +func (e Enable) ToINT8() int8 { + return int8(e) +} + +func (e Enable) ToBool() bool { + return e == EnableTrue +} diff --git a/pkg/domain/wallet/tx_type.go b/pkg/domain/wallet/tx_type.go new file mode 100644 index 0000000..1e79c15 --- /dev/null +++ b/pkg/domain/wallet/tx_type.go @@ -0,0 +1,40 @@ +package wallet + +// TransactionType 交易類型 +type TransactionType int8 + +// 交易類型 +const ( + // Deposit 充值(增加可用餘額) + Deposit TransactionType = iota + 1 + // Withdraw 提現(減少可用餘額) + Withdraw + // Freeze 凍結(減少可用餘額,加在凍結餘額) + Freeze + // UnFreeze 解凍(減少凍結餘額) + UnFreeze + // RollbackFreeze 凍結 (減少凍結餘額,加回可用餘額,不可指定金額) + RollbackFreeze + // Unconfirmed 限制 (減少凍結餘額,加別人限制餘額) + Unconfirmed + // CancelFreeze 取消凍結 (減少凍結餘額,加回可用餘額,,可指定金額) + CancelFreeze + // DepositUnconfirmed 充值 (增加限制餘額) + DepositUnconfirmed + // AppendFreeze 追加凍結(減少可用餘額,加在凍結餘額) + AppendFreeze + // RollbackFreezeAddAvailable 凍結(rollback凍結餘額,指定金額加回可用餘額) + RollbackFreezeAddAvailable + // ContractTransfer 合約劃轉 + ContractTransfer + // FundingFee 合約倉位資金費 + FundingFee + // Distribution 平台分發 + Distribution + // SystemTransfer 系統劃轉 + SystemTransfer +) + +func (t TransactionType) ToInt64() int64 { + return int64(t) +} diff --git a/pkg/domain/wallet/wallet_type.go b/pkg/domain/wallet/wallet_type.go index e455c25..50b7599 100644 --- a/pkg/domain/wallet/wallet_type.go +++ b/pkg/domain/wallet/wallet_type.go @@ -1,10 +1,12 @@ package wallet +import "github.com/shopspring/decimal" + type Types int8 const ( TypeAvailable Types = iota + 1 // 可動用金額(使用者可以自由花用的餘額) - TypeFreezeType // 被凍結金額(交易進行中或風控鎖住的金額) + TypeFreeze // 被凍結金額(交易進行中或風控鎖住的金額) TypeUnconfirmed // 未確認金額(交易已送出但區塊鏈尚未確認) // 以下為進階用途:合約或模擬交易錢包 @@ -13,3 +15,18 @@ const ( TypeSimulationAvailable // 模擬交易可用金額(例如沙盒環境) TypeSimulationFreeze // 模擬交易凍結金額 ) + +const ( + ContractSimulationAvailable int64 = 100000 +) + +var AllTypes = []Types{ + TypeAvailable, TypeFreeze, TypeUnconfirmed, + TypeContractAvailable, TypeContractFreeze, TypeSimulationAvailable, TypeSimulationFreeze, +} + +var AllAvailableTypes = []Types{ + TypeAvailable, TypeContractAvailable, TypeSimulationAvailable, +} + +var InitContractSimulationAvailable = decimal.NewFromInt(ContractSimulationAvailable) diff --git a/pkg/repository/user_wallet.go b/pkg/repository/user_wallet.go index eefbd5e..cf43dc5 100644 --- a/pkg/repository/user_wallet.go +++ b/pkg/repository/user_wallet.go @@ -2,17 +2,505 @@ package repository 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/wallet" + "context" + "database/sql" + "fmt" "github.com/shopspring/decimal" "gorm.io/gorm" + "gorm.io/gorm/clause" + "time" ) -func NewUserWallet(db *gorm.DB, uid, crypto string) repository.UserWallet { - return &userWallet{ - db: db, - uid: uid, - crypto: crypto, +// 用戶某個幣種餘額 +type userWallet struct { + db *gorm.DB + uid string + asset string + // local wallet相關計算的餘額存在這裡 + localWalletBalance map[wallet.Types]entity.Wallet + // local order wallet相關計算的餘額存在這裡 + localOrderBalance map[int64]decimal.Decimal + // local wallet內所有餘額變化紀錄 + transactions []entity.WalletTransaction +} - localWalletBalance: make(map[domain.WalletType]entity.Wallet, len(domain.WalletAllType)), - localOrderBalance: make(map[int64]decimal.Decimal, len(domain.WalletAllType)), +func NewUserWallet(db *gorm.DB, uid, asset string) repository.UserWalletService { + return &userWallet{ + db: db, + uid: uid, + asset: asset, + localWalletBalance: make(map[wallet.Types]entity.Wallet, len(wallet.AllTypes)), + localOrderBalance: make(map[int64]decimal.Decimal, len(wallet.AllTypes)), } } + +func (repo *userWallet) Create(ctx context.Context, uid, asset, brand string) ([]entity.Wallet, error) { + wallets := make([]entity.Wallet, 0, len(wallet.AllTypes)) + for _, t := range wallet.AllTypes { + var balance decimal.Decimal + + // 合約模擬初始資金 + if t == wallet.TypeSimulationAvailable { + balance = wallet.InitContractSimulationAvailable + } + + wallets = append(wallets, entity.Wallet{ + Brand: brand, + UID: uid, + Asset: asset, + Balance: balance, + Type: t, + }) + } + + if err := repo.db.WithContext(ctx).Create(&wallets).Error; err != nil { + return nil, err + } + + for _, v := range wallets { + repo.localWalletBalance[v.Type] = v + } + + return wallets, nil +} + +func (repo *userWallet) AllBalances(ctx context.Context) ([]entity.Wallet, error) { + var result []entity.Wallet + + err := repo.walletUserWhere(repo.uid, repo.asset). + WithContext(ctx). + Select("id, crypto, balance, type"). + Find(&result).Error + + if err != nil { + return []entity.Wallet{}, err + } + + for _, v := range result { + repo.localWalletBalance[v.Type] = v + } + + return result, nil +} + +func (repo *userWallet) Balance(ctx context.Context, kind wallet.Types) (entity.Wallet, error) { + var result entity.Wallet + + err := repo.walletUserWhere(repo.uid, repo.asset). + WithContext(ctx). + Model(&result). + Select("id, crypto, balance, type"). + Where("type = ?", kind). + Take(&result).Error + + if err != nil { + return entity.Wallet{}, repository.WrapNotFoundError(err) + } + + repo.localWalletBalance[result.Type] = result + + return result, nil +} + +func (repo *userWallet) Balances(ctx context.Context, kind []wallet.Types) ([]entity.Wallet, error) { + var wallets []entity.Wallet + + err := repo.walletUserWhere(repo.uid, repo.asset). + WithContext(ctx). + Model(&entity.Wallet{}). + Select("id, crypto, balance, type"). + Where("type IN ?", kind). + Find(&wallets).Error + + if err != nil { + return []entity.Wallet{}, repository.WrapNotFoundError(err) + } + + for _, w := range wallets { + repo.localWalletBalance[w.Type] = w + } + + return wallets, nil +} + +func (repo *userWallet) LocalBalance(kind wallet.Types) decimal.Decimal { + w, ok := repo.localWalletBalance[kind] + if !ok { + return decimal.Zero + } + + return w.Balance +} + +func (repo *userWallet) BalanceXLock(ctx context.Context, kind wallet.Types) (entity.Wallet, error) { + var result entity.Wallet + + err := repo.walletUserWhere(repo.uid, repo.asset). + WithContext(ctx). + Model(&result). + Select("id, crypto, balance, type"). + Where("type = ?", kind). + Clauses(clause.Locking{Strength: "UPDATE"}). + Take(&result).Error + + if err != nil { + return entity.Wallet{}, repository.WrapNotFoundError(err) + } + + repo.localWalletBalance[result.Type] = result + + return result, nil +} + +func (repo *userWallet) BalancesXLock(ctx context.Context, kind []wallet.Types) ([]entity.Wallet, error) { + var wallets []entity.Wallet + + err := repo.walletUserWhere(repo.uid, repo.asset). + WithContext(ctx). + Model(&entity.Wallet{}). + Select("id, crypto, balance, type"). + Where("type IN ?", kind). + Clauses(clause.Locking{Strength: "UPDATE"}). + Find(&wallets).Error + + if err != nil { + return []entity.Wallet{}, repository.WrapNotFoundError(err) + } + + for _, w := range wallets { + repo.localWalletBalance[w.Type] = w + } + + return wallets, nil +} + +func (repo *userWallet) XLock(ctx context.Context, id int64) (entity.Wallet, error) { + var result entity.Wallet + + err := repo.db.WithContext(ctx). + Model(&result). + Select("id"). + Where("id = ?", id). + Clauses(clause.Locking{Strength: "UPDATE"}). + Take(&result).Error + + if err != nil { + return entity.Wallet{}, repository.WrapNotFoundError(err) + } + + repo.localWalletBalance[result.Type] = result + + return result, nil +} + +func (repo *userWallet) XLocks(ctx context.Context, ids []int64) ([]entity.Wallet, error) { + var wallets []entity.Wallet + + err := repo.db.WithContext(ctx). + Model(&entity.Wallet{}). + Select("id, crypto, balance, type"). + Where("id IN ?", ids). + Clauses(clause.Locking{Strength: "UPDATE"}). + Find(&wallets).Error + + if err != nil { + return []entity.Wallet{}, repository.WrapNotFoundError(err) + } + + for _, w := range wallets { + repo.localWalletBalance[w.Type] = w + } + + return wallets, nil +} + +func (repo *userWallet) GetAvailableBalanceXLock(ctx context.Context, _ wallet.BusinessName) (entity.Wallet, error) { + //switch b { + //case domain.ContractBusinessTypeBusinessName, domain.SystemTransferBusinessTypeBusinessName: + // return repo.BalanceXLock(ctx, domain.WalletContractAvailableType) + //case domain.ContractSimulationBusinessTypeBusinessName: + // return repo.BalanceXLock(ctx, domain.WalletSimulationAvailableType) + //} + + return repo.BalanceXLock(ctx, wallet.TypeAvailable) +} + +func (repo *userWallet) GetFreezeBalanceXLock(ctx context.Context, _ wallet.BusinessName) (entity.Wallet, error) { + //switch b { + //case domain.ContractBusinessTypeBusinessName, domain.SystemTransferBusinessTypeBusinessName, domain.SystemTransferCommissionBusinessTypeBusinessName: + // return w.Balance(ctx, domain.WalletContractFreezeType) + //case domain.ContractSimulationBusinessTypeBusinessName: + // return w.Balance(ctx, domain.WalletSimulationFreezeType) + //} + + return repo.Balance(ctx, wallet.TypeFreeze) +} + +func (repo *userWallet) GetUnconfirmedBalanceXLock(ctx context.Context) (entity.Wallet, error) { + return repo.BalanceXLock(ctx, wallet.TypeUnconfirmed) +} + +func (repo *userWallet) GetOrderBalanceXLock(ctx context.Context, id int64) (entity.Transaction, error) { + var result entity.Transaction + + err := repo.db.WithContext(ctx). + Model(&result). + Select("id, transaction_id, order_id, uid, crypto, balance, type, business_type"). + Where("id = ?", id). + Clauses(clause.Locking{Strength: "UPDATE"}). + Take(&result).Error + + if err != nil { + return entity.Transaction{}, repository.WrapNotFoundError(err) + } + + repo.localOrderBalance[result.ID] = result.Balance + + return result, nil +} + +func (repo *userWallet) AddBalance(kind wallet.Types, amount decimal.Decimal) error { + return repo.AddBalanceSetOrderID("", kind, amount) +} + +func (repo *userWallet) AddBalanceSetOrderID(orderID string, kind wallet.Types, amount decimal.Decimal) error { + w, ok := repo.localWalletBalance[kind] + if !ok { + return repository.ErrRecordNotFound + } + + w.Balance = w.Balance.Add(amount) + if w.Balance.LessThan(decimal.Zero) { + return repository.ErrBalanceInsufficient + } + + repo.transactions = append(repo.transactions, entity.WalletTransaction{ + OrderID: orderID, + UID: repo.uid, + WalletType: kind, + Asset: repo.asset, + Amount: amount, + Balance: w.Balance, + }) + + repo.localWalletBalance[kind] = w + + return nil +} + +func (repo *userWallet) AddAvailable(_ wallet.BusinessName, amount decimal.Decimal) error { + //switch b { + //case domain.ContractBusinessTypeBusinessName, domain.SystemTransferBusinessTypeBusinessName: + // return w.AddBalance(domain.WalletContractAvailableType, amount) + //case domain.ContractSimulationBusinessTypeBusinessName: + // return w.AddBalance(domain.WalletSimulationAvailableType, amount) + //case domain.DistributionBusinessTypeBusinessName: + // return w.AddBalance(domain.WalletAvailableType, amount) + //} + + return repo.AddBalance(wallet.TypeAvailable, amount) +} + +func (repo *userWallet) AddFreeze(_ wallet.BusinessName, amount decimal.Decimal) error { + //switch b { + //case domain.ContractBusinessTypeBusinessName, domain.SystemTransferBusinessTypeBusinessName: + // return w.AddBalance(domain.WalletContractFreezeType, amount) + //case domain.ContractSimulationBusinessTypeBusinessName: + // return w.AddBalance(domain.WalletSimulationFreezeType, amount) + //} + + return repo.AddBalance(wallet.TypeFreeze, amount) +} + +func (repo *userWallet) AddUnconfirmed(amount decimal.Decimal) error { + return repo.AddBalance(wallet.TypeUnconfirmed, amount) +} + +func (repo *userWallet) AddOrder(id int64, amount decimal.Decimal) error { + return repo.addOrderBalance(id, amount) +} + +func (repo *userWallet) SubAvailable(_ wallet.BusinessName, amount decimal.Decimal) error { + //switch b { + //case domain.ContractBusinessTypeBusinessName, domain.SystemTransferBusinessTypeBusinessName: + // return w.AddBalance(domain.WalletContractAvailableType, decimal.Zero.Sub(amount)) + //case domain.ContractSimulationBusinessTypeBusinessName: + // return w.AddBalance(domain.WalletSimulationAvailableType, decimal.Zero.Sub(amount)) + //} + + return repo.AddBalance(wallet.TypeAvailable, decimal.Zero.Sub(amount)) +} + +func (repo *userWallet) SubFreeze(_ wallet.BusinessName, amount decimal.Decimal) error { + //switch b { + //case domain.ContractBusinessTypeBusinessName, domain.SystemTransferBusinessTypeBusinessName, domain.SystemTransferCommissionBusinessTypeBusinessName: + // return w.AddBalance(domain.WalletContractFreezeType, decimal.Zero.Sub(amount)) + //case domain.ContractSimulationBusinessTypeBusinessName: + // return w.AddBalance(domain.WalletSimulationFreezeType, decimal.Zero.Sub(amount)) + //} + + return repo.AddBalance(wallet.TypeFreeze, decimal.Zero.Sub(amount)) +} + +func (repo *userWallet) SubUnconfirmed(amount decimal.Decimal) error { + return repo.AddBalance(wallet.TypeUnconfirmed, decimal.Zero.Sub(amount)) +} + +func (repo *userWallet) GetAvailableBalance(ctx context.Context, _ wallet.BusinessName) (entity.Wallet, error) { + //switch b { + //case domain.ContractBusinessTypeBusinessName, domain.SystemTransferBusinessTypeBusinessName: + // return w.Balance(ctx, domain.WalletContractAvailableType) + //case domain.ContractSimulationBusinessTypeBusinessName: + // return w.Balance(ctx, domain.WalletSimulationAvailableType) + //case domain.DistributionBusinessTypeBusinessName: + // return w.Balance(ctx, domain.WalletAvailableType) + //} + + return repo.Balance(ctx, wallet.TypeAvailable) +} + +func (repo *userWallet) GetFreezeBalance(ctx context.Context, _ wallet.BusinessName) (entity.Wallet, error) { + //switch b { + //case domain.ContractBusinessTypeBusinessName, domain.SystemTransferBusinessTypeBusinessName, domain.SystemTransferCommissionBusinessTypeBusinessName: + // return w.Balance(ctx, domain.WalletContractFreezeType) + //case domain.ContractSimulationBusinessTypeBusinessName: + // return w.Balance(ctx, domain.WalletSimulationFreezeType) + //} + + return repo.Balance(ctx, wallet.TypeFreeze) +} + +func (repo *userWallet) GetUnconfirmedBalance(ctx context.Context) (entity.Wallet, error) { + return repo.Balance(ctx, wallet.TypeUnconfirmed) +} + +func (repo *userWallet) GetOrderBalance(ctx context.Context, orderID string) (entity.Transaction, error) { + var result entity.Transaction + + err := repo.db.WithContext(ctx). + Model(&result). + Select("id, transaction_id, order_id, uid, crypto, balance, type, business_type"). + Where("order_id = ?", orderID). + Take(&result).Error + + if err != nil { + return entity.Transaction{}, repository.WrapNotFoundError(err) + } + + // 要確實set這兩個Balance,不然在做計算會有問題 + repo.localOrderBalance[result.ID] = result.Balance + + return result, nil +} + +func (repo *userWallet) CheckWallet(ctx context.Context) (bool, error) { + var exists bool + err := repo.walletUserWhere(repo.uid, repo.asset).WithContext(ctx). + Model(&entity.Wallet{}). + Select("1"). + Where("type = ?", wallet.TypeAvailable). + Limit(1). + Scan(&exists).Error + + if err != nil { + return false, err + } + + return exists, nil +} + +func (repo *userWallet) Execute(ctx context.Context) error { + // 處理wallet table + for _, walletType := range wallet.AllTypes { + w, ok := repo.localWalletBalance[walletType] + if !ok { + continue + } + + rc := &sql.TxOptions{ + Isolation: sql.LevelReadCommitted, + ReadOnly: false, + } + err := repo.db.Transaction(func(tx *gorm.DB) error { + return tx.WithContext(ctx). + Model(&entity.Wallet{}). + Where("id = ?", w.ID). + UpdateColumns(map[string]interface{}{ + "balance": w.Balance, + "update_time": time.Now().UTC().Unix(), + }).Error + }, rc) + + if err != nil { + err = fmt.Errorf("update uid: %s crypto: %s type: %d wallet error: %w", repo.uid, repo.asset, walletType, err) + + return err + } + } + + return nil +} + +func (repo *userWallet) ExecuteOrder(ctx context.Context) error { + for id, localBalance := range repo.localOrderBalance { + rc := &sql.TxOptions{ + Isolation: sql.LevelReadCommitted, + ReadOnly: false, + } + + err := repo.db.Transaction(func(tx *gorm.DB) error { + return tx.WithContext(ctx). + Where("id = ?", id). + Model(&entity.Transaction{}). + Update("balance", localBalance).Error + }, rc) + + if err != nil { + return err + } + } + + return nil +} + +func (repo *userWallet) Transactions(txID int64, orderID, 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) GetTransactions() []entity.WalletTransaction { + return repo.transactions +} + +// ============================================================================ + +func (repo *userWallet) walletUserWhere(uid, asset string) *gorm.DB { + return repo.db.Where("uid = ?", uid). + Where("asset = ?", asset) +} + +// addOrderBalance 新增訂單餘額 +// 使用前local Balance必須有資料,所以必須執行過GetOrderBalanceXLock才會有資料 +func (repo *userWallet) addOrderBalance(id int64, amount decimal.Decimal) error { + balance, ok := repo.localOrderBalance[id] + if !ok { + return repository.ErrRecordNotFound + } + + balance = balance.Add(amount) + if balance.LessThan(decimal.Zero) { + return repository.ErrBalanceInsufficient + } + + repo.localOrderBalance[id] = balance + + return nil +} diff --git a/pkg/repository/wallet.go b/pkg/repository/wallet.go index 500f656..e2efa84 100644 --- a/pkg/repository/wallet.go +++ b/pkg/repository/wallet.go @@ -3,8 +3,10 @@ package repository 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/wallet" "context" "database/sql" + "github.com/shopspring/decimal" "gorm.io/gorm" ) @@ -45,18 +47,37 @@ func (repo *WalletRepository) Transaction(fn func(db *gorm.DB) error) error { } func (repo *WalletRepository) Session(uid, asset string) repository.UserWalletService { - //TODO implement me - panic("implement me") + return NewUserWallet(repo.DB, uid, asset) } func (repo *WalletRepository) SessionWithTx(db *gorm.DB, uid, asset string) repository.UserWalletService { - //TODO implement me - panic("implement me") + return NewUserWallet(db, uid, asset) } -func (repo *WalletRepository) InitWallets(ctx context.Context, param []repository.Wallet) error { - //TODO implement me - panic("implement me") +func (repo *WalletRepository) InitWallets(ctx context.Context, param repository.Wallet) ([]entity.Wallet, error) { + w := make([]entity.Wallet, 0, len(wallet.AllTypes)) + for _, t := range wallet.AllTypes { + var balance decimal.Decimal + + // 合約模擬初始資金 + if t == wallet.TypeSimulationAvailable { + balance = wallet.InitContractSimulationAvailable + } + + w = append(w, entity.Wallet{ + Brand: param.Brand, + UID: param.UID, + Asset: param.Asset, + Balance: balance, + Type: t, + }) + } + + if err := repo.DB.WithContext(ctx).Create(&w).Error; err != nil { + return nil, err + } + + return w, nil } func (repo *WalletRepository) QueryBalances(ctx context.Context, req repository.BalanceQuery) ([]entity.Wallet, error) { @@ -68,3 +89,8 @@ func (repo *WalletRepository) QueryBalancesByUIDs(ctx context.Context, uids []st //TODO implement me panic("implement me") } + +func (repo *WalletRepository) GetDailyTxAmount(ctx context.Context, uid string, txTypes []wallet.TransactionType, business wallet.BusinessName) ([]entity.Wallet, error) { + //TODO implement me + panic("implement me") +}