add wallet repo

This commit is contained in:
daniel.w 2024-10-29 22:49:47 +08:00
parent 8687e467c9
commit fc748fb66f
13 changed files with 529 additions and 24 deletions

View File

@ -23,6 +23,12 @@ const (
TimeoutOrderErrorCode TimeoutOrderErrorCode
) )
// Error Code 統一這邊改
const (
_ ErrorCode = 20 + iota
CreateWalletErrorCode
)
const ( const (
_ ErrorCode = 10 + iota _ ErrorCode = 10 + iota
DataNotFoundErrorCode DataNotFoundErrorCode

View File

@ -0,0 +1,4 @@
package repository
// UserWalletOperator 針對使用者的錢包基本操作接口
type UserWalletOperator interface{}

View File

@ -0,0 +1,30 @@
package repository
import (
"app-cloudep-trade-service/internal/domain"
"app-cloudep-trade-service/internal/model"
"context"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
// WalletRepository 錢包基礎操作(可能有平台,使用者等多元的錢包)
type WalletRepository interface {
// Create 建立錢包組合(某人的可用,凍結,限制三種)
Create(ctx context.Context, uid, currency, brand string) ([]*model.Wallet, error)
// Balances 取得某個人的餘額
Balances(ctx context.Context, req BalanceReq) ([]model.Wallet, error)
// GetTxDatabaseConn 取得 sql 要做 tx 的連線
GetTxDatabaseConn() sqlx.SqlConn
// GetUserWalletOperator 取得使用者錢包操作看使否需要使用 transaction
GetUserWalletOperator(uid, currency string, opts ...Option) UserWalletOperator
}
// BalanceReq 取得全部的,因為一個人錢包種類的不會太多,故全撈
type BalanceReq struct {
UID []string
Currency []string
Kind []domain.WalletType
}
type Option func() sqlx.SqlConn

View File

@ -1,4 +1,81 @@
package usecase package usecase
type WalletUseCase interface { import (
"app-cloudep-trade-service/internal/domain"
"context"
"github.com/shopspring/decimal"
)
// WalletQueryUseCase 定義所有查詢行為(餘額查詢、檢查、歷史記錄)的行為
type WalletQueryUseCase interface {
// Balance 用戶餘額
Balance(ctx context.Context, req BalanceReq) ([]Balance, error)
// CheckBalance 根據tx檢查用戶餘額是否足夠
CheckBalance(ctx context.Context, tx Transaction) error
// HistoryBalance 歷史餘額變化
HistoryBalance(ctx context.Context, req BalanceReq) ([]Balance, error)
}
// WalletOperationUseCase 定義所有錢包操作(提款、充值、凍結等)的行為
type WalletOperationUseCase interface {
// Withdraw 提款
Withdraw(ctx context.Context, tx Transaction) error
// Deposit 充值
Deposit(ctx context.Context, tx Transaction) error
// DepositUnconfirmed 增加限制餘額
DepositUnconfirmed(ctx context.Context, tx Transaction) error
// Freeze 凍結
Freeze(ctx context.Context, tx Transaction) error
// AppendFreeze 追加凍結金額
AppendFreeze(ctx context.Context, tx Transaction) error
// UnFreeze 解凍
UnFreeze(ctx context.Context, tx Transaction) error
// RollbackFreeze Rollback 凍結,不可指定金額(剩餘order凍結金額)
RollbackFreeze(ctx context.Context, tx Transaction) error
// RollbackFreezeAddAvailable Rollback剩餘凍結可指定金額(剩餘order凍結金額)
RollbackFreezeAddAvailable(ctx context.Context, tx Transaction) error
// CancelFreeze 取消凍結,可指定金額
CancelFreeze(ctx context.Context, tx Transaction) error
// Unconfirmed 增加限制
Unconfirmed(ctx context.Context, tx Transaction) error
}
// WalletUseCase 基礎操作類別
type WalletUseCase interface {
WalletOperationUseCase
WalletQueryUseCase
}
// Transaction 交易
type Transaction struct {
OrderID string // 交易訂單
UID string // 交易發起人
ToUID string // 交易接收人
Currency string // 幣別
Amount decimal.Decimal // 交易金額
BeforeBalance decimal.Decimal // 交易前餘額
Type domain.TxType // 交易種類
BusinessType domain.BusinessName // 商業種類
Brand string // 轉帳平台
From domain.WalletType // 從哪種錢包類型
To domain.WalletType // 到哪種錢包類型
}
type BalanceReq struct {
UID string // 用戶 UID
Currency string // 幣值
BeforeHour int // 在某個時段之前
}
type Balance struct {
Currency string `json:"currency"`
Available decimal.Decimal `json:"available"`
Unavailable UnavailableBalance `json:"unavailable"`
UpdateTime int64 `json:"update_time"`
}
type UnavailableBalance struct {
Freeze decimal.Decimal `json:"freeze"`
Unconfirmed decimal.Decimal `json:"unconfirmed"`
} }

View File

@ -0,0 +1,52 @@
package domain
// ===================交易商業邏輯種類===================
type BusinessName string
const (
// NoBusinessName 非商業邏輯
NoBusinessName BusinessName = ""
// OrderBusinessTypeBusinessName order業務邏輯
OrderBusinessTypeBusinessName BusinessName = "order"
// SystemTransferBusinessTypeBusinessName 系統劃轉
SystemTransferBusinessTypeBusinessName BusinessName = "system_transfer"
)
const (
// NoBusinessType 非商業邏輯
NoBusinessType int8 = iota
// OrderBusinessTypeBusinessType order 業務邏輯
OrderBusinessTypeBusinessType
// SystemTransferBusinessTypeBusinessType 系統劃轉
SystemTransferBusinessTypeBusinessType
)
// 定義兩個map用於名稱和類型的相互映射
var nameToBusinessType = map[BusinessName]int8{
OrderBusinessTypeBusinessName: OrderBusinessTypeBusinessType,
SystemTransferBusinessTypeBusinessName: SystemTransferBusinessTypeBusinessType,
}
var businessTypeToName = map[int8]BusinessName{
OrderBusinessTypeBusinessType: OrderBusinessTypeBusinessName,
SystemTransferBusinessTypeBusinessType: SystemTransferBusinessTypeBusinessName,
}
// ToINT8 converts BusinessName to its corresponding int8 type
func (b BusinessName) ToINT8() int8 {
if val, ok := nameToBusinessType[b]; ok {
return val
}
return NoBusinessType
}
// BusinessTypeToString converts an int8 type to its corresponding BusinessName
func BusinessTypeToString(b int8) BusinessName {
if val, ok := businessTypeToName[b]; ok {
return val
}
return NoBusinessName
}

View File

@ -0,0 +1,48 @@
package domain
// ===================交易種類===================
type TxType int64
// 交易類型
const (
// TxDepositType 充值(增加可用餘額)
TxDepositType TxType = iota + 1
// TxWithdrawType 提現(減少可用餘額)
TxWithdrawType
// TxFreezeType 凍結(減少可用餘額,加在凍結餘額)
TxFreezeType
// TxUnFreezeType 解凍(減少凍結餘額)
TxUnFreezeType
// TxRollbackFreezeType rollback凍結(減少凍結餘額,加回可用餘額,不可指定金額)
TxRollbackFreezeType
// TxUnconfirmedType 限制(減少凍結餘額,加別人限制餘額)
TxUnconfirmedType
// TxCancelFreezeType 取消凍結(減少凍結餘額,加回可用餘額,,可指定金額)
TxCancelFreezeType
// TxDepositUnconfirmedType 充值(增加限制餘額)
TxDepositUnconfirmedType
// TxAppendFreezeType 追加凍結(減少可用餘額,加在凍結餘額)
TxAppendFreezeType
// TxRollbackFreezeAddAvailableType rollback凍結(rollback凍結餘額指定金額加回可用餘額)
TxRollbackFreezeAddAvailableType
// TxDistributionType 平台分發
TxDistributionType
// TxSystemTransfer 系統劃轉
TxSystemTransfer
)
func (t TxType) ToInt() int64 {
return int64(t)
}

View File

@ -0,0 +1,54 @@
package domain
// ===================錢包金額種類===================
type WalletType int64
// 錢包種類
const (
// WalletAvailableType 錢包可動用的金額
WalletAvailableType WalletType = iota + 1
// WalletFreezeType 交易過程,錢包被凍結的金額
WalletFreezeType
// WalletUnconfirmedType 已提交的交易但還未確認完成交易的金額
WalletUnconfirmedType
// WalletContractAvailableType 合約/交易可用餘額
WalletContractAvailableType
// WalletContractFreezeType 合約/交易凍結餘額
WalletContractFreezeType
)
var walletTypeToName = map[WalletType]string{
WalletAvailableType: "available",
WalletFreezeType: "freeze",
WalletUnconfirmedType: "unconfirmed",
WalletContractAvailableType: "contract_available",
WalletContractFreezeType: "contract_freeze",
}
var nameToWalletType = map[string]WalletType{
"available": WalletAvailableType,
"freeze": WalletFreezeType,
"unconfirmed": WalletUnconfirmedType,
"contract_available": WalletContractAvailableType,
"contract_freeze": WalletContractFreezeType,
}
// Name returns the name associated with the WalletType.
func (w WalletType) Name() string {
return walletTypeToName[w]
}
// GetWalletTypeByName returns the WalletType associated with the given name.
func GetWalletTypeByName(name string) WalletType {
return nameToWalletType[name]
}
var AllWalletType = []WalletType{
WalletAvailableType, WalletFreezeType, WalletUnconfirmedType,
WalletContractAvailableType, WalletContractFreezeType,
}

View File

@ -1,6 +1,18 @@
package model package model
import "github.com/zeromicro/go-zero/core/stores/sqlx" import (
"app-cloudep-trade-service/internal/domain"
"context"
"database/sql"
"errors"
"fmt"
"strings"
"github.com/zeromicro/go-zero/core/stores/sqlc"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
var _ WalletModel = (*customWalletModel)(nil) var _ WalletModel = (*customWalletModel)(nil)
@ -9,11 +21,19 @@ type (
// and implement the added methods in customWalletModel. // and implement the added methods in customWalletModel.
WalletModel interface { WalletModel interface {
walletModel walletModel
InsertMany(ctx context.Context, wallets []*Wallet) (sql.Result, error)
Balances(ctx context.Context, req BalanceReq) ([]Wallet, error)
} }
customWalletModel struct { customWalletModel struct {
*defaultWalletModel *defaultWalletModel
} }
BalanceReq struct {
UID []string
Currency []string
Kind []domain.WalletType
}
) )
// NewWalletModel returns a model for the database table. // NewWalletModel returns a model for the database table.
@ -22,3 +42,75 @@ func NewWalletModel(conn sqlx.SqlConn) WalletModel {
defaultWalletModel: newWalletModel(conn), defaultWalletModel: newWalletModel(conn),
} }
} }
func (m *customWalletModel) InsertMany(ctx context.Context, wallets []*Wallet) (sql.Result, error) {
if len(wallets) == 0 {
return nil, fmt.Errorf("no data to insert")
}
// 構建多條記錄的佔位符,例如: (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), ...
valueStrings := make([]string, 0, len(wallets))
valueArgs := make([]interface{}, 0, len(wallets)*7) // 每條記錄有7個值
for _, wallet := range wallets {
valueStrings = append(valueStrings, "(?, ?, ?, ?, ?, ?, ?)")
valueArgs = append(valueArgs, wallet.Uid, wallet.Brand, wallet.Currency, wallet.Balance, wallet.WalletType, wallet.CreatedAt, wallet.UpdatedAt)
}
// 構建批量插入的 SQL 語句
query := fmt.Sprintf("insert into %s (%s) values %s", m.table, walletRowsExpectAutoSet, strings.Join(valueStrings, ","))
// 使用單一連線執行批量插入
return m.conn.ExecCtx(ctx, query, valueArgs...)
}
func (m *customWalletModel) Balances(ctx context.Context, req BalanceReq) ([]Wallet, error) {
var data []Wallet
query := fmt.Sprintf("select 'id', 'uid', 'currency', 'balance', 'type', 'update_time' from %s", m.table)
var conditions []string
var args []any
// 根據條件動態拼接 WHERE 子句
if len(req.UID) > 0 {
placeholders := strings.Repeat("?,", len(req.UID))
placeholders = placeholders[:len(placeholders)-1] // 移除最後一個逗號
conditions = append(conditions, fmt.Sprintf("uid IN (%s)", placeholders))
args = append(args, convertSliceToInterface(req.UID)...)
}
if len(req.Currency) > 0 {
placeholders := strings.Repeat("?,", len(req.Currency))
placeholders = placeholders[:len(placeholders)-1]
conditions = append(conditions, fmt.Sprintf("currency IN (%s)", placeholders))
args = append(args, convertSliceToInterface(req.Currency)...)
}
if len(req.Kind) > 0 {
placeholders := strings.Repeat("?,", len(req.Kind))
placeholders = placeholders[:len(placeholders)-1]
conditions = append(conditions, fmt.Sprintf("type IN (%s)", placeholders))
args = append(args, convertSliceToInterface(req.Kind)...)
}
// 如果有條件,則拼接 WHERE 子句
if len(conditions) > 0 {
query += " WHERE " + strings.Join(conditions, " AND ")
}
// 執行查詢
err := m.conn.QueryRowCtx(ctx, &data, query, args...)
switch {
case err == nil:
return data, nil
case errors.Is(err, sqlc.ErrNotFound):
return nil, ErrNotFound
default:
return nil, err
}
}
func convertSliceToInterface[T any](slice []T) []any {
interfaces := make([]any, 0, len(slice))
for i, v := range slice {
interfaces[i] = v
}
return interfaces
}

View File

@ -3,11 +3,14 @@
package model package model
import ( import (
"app-cloudep-trade-service/internal/domain"
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
"strings" "strings"
"github.com/shopspring/decimal"
"github.com/zeromicro/go-zero/core/stores/builder" "github.com/zeromicro/go-zero/core/stores/builder"
"github.com/zeromicro/go-zero/core/stores/sqlc" "github.com/zeromicro/go-zero/core/stores/sqlc"
"github.com/zeromicro/go-zero/core/stores/sqlx" "github.com/zeromicro/go-zero/core/stores/sqlx"
@ -36,14 +39,14 @@ type (
} }
Wallet struct { Wallet struct {
Id int64 `db:"id"` // 錢包流水號 Id int64 `db:"id"` // 錢包流水號
Uid string `db:"uid"` // 用戶ID Uid string `db:"uid"` // 用戶ID
Brand string `db:"brand"` // 品牌名稱 Brand string `db:"brand"` // 品牌名稱
Currency string `db:"currency"` // 幣別(或平台點數) Currency string `db:"currency"` // 幣別(或平台點數)
Balance float64 `db:"balance"` // 錢包餘額 Balance decimal.Decimal `db:"balance"` // 錢包餘額
WalletType int64 `db:"wallet_type"` // 錢包種類: 1=可用, 2=凍結, 3=限制(僅出金) WalletType domain.WalletType `db:"wallet_type"` // 錢包種類: 1=可用, 2=凍結, 3=限制(僅出金)
CreatedAt int64 `db:"created_at"` // 創建時間 CreatedAt int64 `db:"created_at"` // 創建時間
UpdatedAt int64 `db:"updated_at"` // 更新時間 UpdatedAt int64 `db:"updated_at"` // 更新時間
} }
) )

View File

@ -0,0 +1,41 @@
package repository
import (
"app-cloudep-trade-service/internal/domain"
"app-cloudep-trade-service/internal/domain/repository"
"app-cloudep-trade-service/internal/model"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"github.com/shopspring/decimal"
)
// 用戶某個幣種餘額
type userLocalWallet struct {
wm model.WalletModel
txConn sqlx.SqlConn
uid string
crypto string
// local wallet 相關計算的餘額存在這裡
walletBalance map[domain.WalletType]model.Wallet
// local order wallet 相關計算的餘額存在這裡
localOrderBalance map[int64]decimal.Decimal
// local wallet 內所有餘額變化紀錄
transactions []model.WalletJournal
}
func NewUserWalletOperator(uid, crypto string, wm model.WalletModel, txConn sqlx.SqlConn) repository.UserWalletOperator {
return &userLocalWallet{
wm: wm,
txConn: txConn,
uid: uid,
crypto: crypto,
walletBalance: make(map[domain.WalletType]model.Wallet, len(domain.AllWalletType)),
localOrderBalance: make(map[int64]decimal.Decimal, len(domain.AllWalletType)),
}
}

View File

@ -0,0 +1,106 @@
package repository
import (
"app-cloudep-trade-service/internal/domain"
"app-cloudep-trade-service/internal/domain/repository"
"app-cloudep-trade-service/internal/model"
"context"
"fmt"
"time"
"github.com/shopspring/decimal"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type WalletRepositoryParam struct {
WalletModel model.WalletModel
TxConn sqlx.SqlConn
}
type WalletRepository struct {
WalletRepositoryParam
}
// GetUserWalletOperator 取得本地操作使用者錢包的操作運算元
func (repo *WalletRepository) GetUserWalletOperator(uid, currency string, opts ...repository.Option) repository.UserWalletOperator {
db := repo.TxConn
// 看是否有最新的DB 連線要傳入,做 tx
for _, fn := range opts {
db = fn()
}
return NewUserWalletOperator(uid, currency, repo.WalletModel, db)
}
// Create 創建錢包,如果有相同的就跳過不建立
func (repo *WalletRepository) Create(ctx context.Context, uid, currency, brand string) ([]*model.Wallet, error) {
wallets := make([]*model.Wallet, 0, len(domain.AllWalletType))
// 建立個人所有種類的錢包
now := time.Now().UTC().UnixNano()
for _, item := range domain.AllWalletType {
balance := decimal.Zero
wallets = append(wallets, &model.Wallet{
Brand: brand,
Currency: currency,
Uid: uid,
Balance: balance,
WalletType: item,
CreatedAt: now,
UpdatedAt: now,
})
}
_, err := repo.WalletModel.InsertMany(ctx, wallets)
if err != nil {
// 錯誤代碼 06-021-20
e := domain.CommentErrorL(
domain.CreateWalletErrorCode,
logx.WithContext(ctx),
[]logx.LogField{
{Key: "param", Value: fmt.Sprintf("uid: %s, currency:%s, brand:%s", uid, currency, brand)},
{Key: "func", Value: "WalletModel.InsertMany"},
{Key: "err", Value: err},
},
"failed to insert wallet into mysql:").Wrap(err)
return nil, e
}
return wallets, nil
}
func (repo *WalletRepository) Balances(ctx context.Context, req repository.BalanceReq) ([]model.Wallet, error) {
data, err := repo.WalletModel.Balances(ctx, model.BalanceReq{
UID: req.UID,
Currency: req.Currency,
})
if err != nil {
// 錯誤代碼 06-021-20
e := domain.CommentErrorL(
domain.CreateWalletErrorCode,
logx.WithContext(ctx),
[]logx.LogField{
{Key: "param", Value: req},
{Key: "func", Value: "WalletModel.Balances"},
{Key: "err", Value: err},
},
"failed to find balance into mongo:").Wrap(err)
return []model.Wallet{}, e
}
return data, nil
}
func (repo *WalletRepository) GetTxDatabaseConn() sqlx.SqlConn {
return repo.TxConn
}
func NewWalletRepository(param WalletRepositoryParam) repository.WalletRepository {
return &WalletRepository{
WalletRepositoryParam: param,
}
}

View File

@ -2,12 +2,10 @@ package svc
import ( import (
"app-cloudep-trade-service/internal/config" "app-cloudep-trade-service/internal/config"
model "app-cloudep-trade-service/internal/model"
"database/sql" "database/sql"
"fmt" "fmt"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
) )
func mustDSN(c config.Config) string { func mustDSN(c config.Config) string {
@ -38,11 +36,3 @@ func newDatabase(c config.Config) (*sql.DB, error) {
return db, nil return db, nil
} }
// MustWalletModel 連線 wallet 時
func MustWalletModel(db *sql.DB) model.WalletModel {
// 創建並返回 *sqlx.SqlConn
sqlConn := sqlx.NewSqlConnFromDB(db)
return model.NewWalletModel(sqlConn)
}

View File

@ -3,9 +3,10 @@ package svc
import ( import (
"app-cloudep-trade-service/internal/config" "app-cloudep-trade-service/internal/config"
duc "app-cloudep-trade-service/internal/domain/usecase" duc "app-cloudep-trade-service/internal/domain/usecase"
"app-cloudep-trade-service/internal/model"
"app-cloudep-trade-service/internal/usecase" "app-cloudep-trade-service/internal/usecase"
"github.com/zeromicro/go-zero/core/stores/sqlx"
ers "code.30cm.net/digimon/library-go/errs" ers "code.30cm.net/digimon/library-go/errs"
"code.30cm.net/digimon/library-go/errs/code" "code.30cm.net/digimon/library-go/errs/code"
vi "code.30cm.net/digimon/library-go/validator" vi "code.30cm.net/digimon/library-go/validator"
@ -16,7 +17,7 @@ type ServiceContext struct {
Validate vi.Validate Validate vi.Validate
OrderUseCase duc.OrderUseCase OrderUseCase duc.OrderUseCase
WalletModel model.WalletModel SQLConn sqlx.SqlConn
} }
func NewServiceContext(c config.Config) *ServiceContext { func NewServiceContext(c config.Config) *ServiceContext {
@ -32,6 +33,8 @@ func NewServiceContext(c config.Config) *ServiceContext {
if err != nil { if err != nil {
panic("failed to connect to wallet") panic("failed to connect to wallet")
} }
// 創建 SQL 連線並返回
sqlConn := sqlx.NewSqlConnFromDB(mysql)
return &ServiceContext{ return &ServiceContext{
Config: c, Config: c,
@ -39,8 +42,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
WithDecimalGt(), WithDecimalGt(),
WithDecimalGte(), WithDecimalGte(),
), ),
SQLConn: sqlConn,
OrderUseCase: orderUseCase, OrderUseCase: orderUseCase,
WalletModel: MustWalletModel(mysql),
} }
} }