add wallet and user wallet

This commit is contained in:
王性驊 2025-04-11 17:10:34 +08:00
parent 4cb6faefdc
commit 59064b0a69
11 changed files with 789 additions and 46 deletions

View File

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

View File

@ -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"` // 對應的主交易 IDTransaction.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"
}

View File

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

View File

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

View File

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

View File

@ -1,3 +1,7 @@
package wallet
type BusinessName string
func (b BusinessName) ToINT8() int8 {
return int8(0)
}

View File

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

View File

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

View File

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

View File

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

View File

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