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