add wallet and user wallet
This commit is contained in:
parent
4cb6faefdc
commit
59064b0a69
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
package wallet
|
||||
|
||||
type BusinessName string
|
||||
|
||||
func (b BusinessName) ToINT8() int8 {
|
||||
return int8(0)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue