diff --git a/go.mod b/go.mod index 0b6a876..fdc58d5 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,11 @@ module code.30cm.net/digimon/app-cloudep-wallet-service go 1.24.2 require ( + github.com/shopspring/decimal v1.4.0 github.com/zeromicro/go-zero v1.8.2 google.golang.org/grpc v1.71.1 google.golang.org/protobuf v1.36.6 + gorm.io/gorm v1.25.12 ) require ( @@ -31,6 +33,8 @@ require ( 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 github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect diff --git a/go.sum b/go.sum index 26c6c42..71c3838 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,10 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -123,6 +127,8 @@ github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0 github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -259,6 +265,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q= diff --git a/pkg/domain/const.go b/pkg/domain/const.go new file mode 100644 index 0000000..4188b5a --- /dev/null +++ b/pkg/domain/const.go @@ -0,0 +1 @@ +package domain diff --git a/pkg/domain/entity/wallet.go b/pkg/domain/entity/wallet.go new file mode 100644 index 0000000..500d690 --- /dev/null +++ b/pkg/domain/entity/wallet.go @@ -0,0 +1,34 @@ +package entity + +import ( + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet" + "github.com/shopspring/decimal" +) + +// 🔐 重點設計理念: +// 避免競爭與資金衝突 +// 將錢包用途拆分成不同 type,避免一個錢包同時處理可用資金與凍結資金,降低邏輯錯誤風險。 +// +// 資金安全性與追蹤性強 +// 分別記錄「凍結」「未確認」「可用」等不同狀態,方便核對資產總額與可提資金。 +// +// 合約與模擬隔離 +// 保留合約交易與模擬交易專屬錢包,實現清楚的邊界控制與風險隔離。 +// +// 多品牌支援 +// Brand 欄位讓平台可以支援多租戶架構(不同品牌有不同的錢包群組)。 + +type Wallet struct { + ID int64 `gorm:"column:id"` // 主鍵 ID(錢包的唯一識別) + Brand string `gorm:"column:brand"` // 品牌/平台(區分多租戶、多品牌情境) + UID string `gorm:"column:uid"` // 使用者 UID + Asset string `gorm:"column:asset"` // 資產代號,可為 BTC、ETH、GEM_RED、GEM_BLUE、POINTS ,USD, TWD 等.... + Balance decimal.Decimal `gorm:"column:balance"` // 餘額(使用高精度 decimal 避免浮點誤差) + Type wallet.Types `gorm:"column:type"` // 錢包類型 + CreateTime int64 `gorm:"column:create_time;autoCreateTime"` // 建立時間(UnixNano timestamp) + UpdateTime int64 `gorm:"column:update_time;autoUpdateTime"` // 更新時間(UnixNano timestamp) +} + +func (c *Wallet) TableName() string { + return "wallet" // 對應的資料表名稱 +} diff --git a/pkg/domain/error.go b/pkg/domain/error.go new file mode 100644 index 0000000..4188b5a --- /dev/null +++ b/pkg/domain/error.go @@ -0,0 +1 @@ +package domain diff --git a/pkg/domain/repository/wallet.go b/pkg/domain/repository/wallet.go new file mode 100644 index 0000000..977e646 --- /dev/null +++ b/pkg/domain/repository/wallet.go @@ -0,0 +1,87 @@ +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" + + "gorm.io/gorm" +) + +type Wallet struct { + Brand string `gorm:"column:brand"` // 品牌/平台(區分多租戶、多品牌情境) + UID string `gorm:"column:uid"` // 使用者 UID + Asset string `gorm:"column:asset"` // 資產代號,可為 BTC、ETH、GEM_RED、GEM_BLUE、POINTS ,USD, TWD 等.... + Balance decimal.Decimal `gorm:"column:balance"` // 餘額(使用高精度 decimal 避免浮點誤差) + Type wallet.Types `gorm:"column:type"` // 錢包類型 +} + +// WalletRepository 是錢包的總入口,負責查詢、初始化與跨帳戶查詢邏輯 +type WalletRepository interface { + // NewDB 建立新的 DB 實例(提供給需要操作 tx 的場景) + NewDB() *gorm.DB + // Session 取得單一使用者資產的錢包服務(非交易模式) + //📌 使用場景: + // 用在 不需要交易機制的場景,例如: + // 純查詢錢包餘額,查詢快取、Log、統計報表,非敏感資料更新,失敗可以重試的情境 + // ✅ 優點: + // 簡單快速、使用預設的資料庫連線 + // 不用包 Transaction ,沒有 Rollback 負擔 + Session(uid, asset string) UserWalletService + // SessionWithTx 在資料庫交易內取得錢包服務 + // 📌 使用場景: + // 用在 資料需要一致性與原子性保證的邏輯中,例如: + // 加值與扣款(同時操作多個錢包)檢查餘額後立刻寫入交易記錄,綁定訂單與錢包扣款的行為 + // 所有與 Add/Commit 有關的處理,與其他模組(訂單、KYC)共用一個 transaction + // ✅ 優點: + // 保障操作過程中不被其他並發操作影響 + // 可控制 rollback 行為避免中間失敗導致不一致 + // 可組合複雜操作(如:更新錢包同時寫入交易紀錄) + SessionWithTx(db *gorm.DB, uid, asset string) UserWalletService + // Transaction 資料庫交易包裝器(確保交易一致性) + Transaction(fn func(db *gorm.DB) error) error + // InitWallets 初始化使用者的所有錢包類型(如可用、凍結等) + InitWallets(ctx context.Context, param []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) +} + +// BalanceQuery 是查詢餘額時的篩選條件 +type BalanceQuery struct { + UID string // 使用者 ID + Asset string // 資產類型(Crypto、寶石等) + Kinds []wallet.Types // 錢包類型(如可用、凍結等) +} + +// UserWalletService 專注於某位使用者在單一資產下的錢包操作邏輯 +type UserWalletService interface { + // Init 初始化錢包(如建立可用、凍結、未確認等錢包) + Init(ctx context.Context, uid, asset, brand string) ([]entity.Wallet, error) + // All 查詢所有錢包餘額 + All(ctx context.Context) ([]entity.Wallet, error) + // Get 查詢單一或多種類型的餘額 + Get(ctx context.Context, kinds []wallet.Types) ([]entity.Wallet, error) + // GetWithLock 查詢鎖定後的錢包(交易使用) + GetWithLock(ctx context.Context, kinds []wallet.Types) ([]entity.Wallet, error) + // LocalBalance 查詢記憶中的快取值(非查資料庫) + LocalBalance(kind wallet.Types) decimal.Decimal + // LockByIDs 根據錢包 ID 鎖定(資料一致性用) + LockByIDs(ctx context.Context, ids []int64) ([]entity.Wallet, error) + // CheckReady 檢查錢包是否已經存在並準備好 + CheckReady(ctx context.Context) (bool, error) + // Add 加值與扣款邏輯(含業務類別) + Add(kind wallet.Types, business wallet.BusinessName, amount decimal.Decimal) error + Sub(kind wallet.Types, business wallet.BusinessName, amount decimal.Decimal) error + // AddTransaction 新增一筆交易紀錄(建立資料) + AddTransaction(txID int64, orderID string, brand string, business wallet.BusinessName, kind wallet.Types, amount decimal.Decimal) + //// PendingTransactions 查詢尚未執行的交易清單(會在 Execute 中一次提交) + //PendingTransactions() []entity.WalletTransaction + + // Commit 提交所有操作(更新錢包與新增交易紀錄) + Commit(ctx context.Context) error +} diff --git a/pkg/domain/wallet/business_name.go b/pkg/domain/wallet/business_name.go new file mode 100644 index 0000000..a9aaac0 --- /dev/null +++ b/pkg/domain/wallet/business_name.go @@ -0,0 +1,3 @@ +package wallet + +type BusinessName string diff --git a/pkg/domain/wallet/wallet_type.go b/pkg/domain/wallet/wallet_type.go new file mode 100644 index 0000000..e455c25 --- /dev/null +++ b/pkg/domain/wallet/wallet_type.go @@ -0,0 +1,15 @@ +package wallet + +type Types int8 + +const ( + TypeAvailable Types = iota + 1 // 可動用金額(使用者可以自由花用的餘額) + TypeFreezeType // 被凍結金額(交易進行中或風控鎖住的金額) + TypeUnconfirmed // 未確認金額(交易已送出但區塊鏈尚未確認) + // 以下為進階用途:合約或模擬交易錢包 + + TypeContractAvailable // 合約系統的可用金額 + TypeContractFreeze // 合約中被凍結的金額 + TypeSimulationAvailable // 模擬交易可用金額(例如沙盒環境) + TypeSimulationFreeze // 模擬交易凍結金額 +) diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..c5d62f7 --- /dev/null +++ b/readme.md @@ -0,0 +1,54 @@ + + // 充值-增加可用餘額 + v1.POST("deposit", wallet.Deposit) + + // 充值-增加限制餘額 + v1.POST("deposit/unconfirmed", wallet.DepositUnconfirmed) + + // 提現-減少可用餘額 + v1.POST("withdraw", wallet.Withdraw) + + // 凍結-減少可用餘額,加在凍結餘額 + v1.POST("freeze", wallet.Freeze) + + // 追加凍結(原凍結金額上)-減少可用餘額,加在凍結餘額 + v1.POST("freeze/append", wallet.AppendFreeze) + + // 解凍-減少凍結餘額 + v1.POST("unfreeze", wallet.UnFreeze) + + // rollback凍結-減少凍結餘額,加回可用餘額,不可指定金額 + v1.POST("freeze/rollback", wallet.RollbackFreeze) + + // rollback凍結-rollback凍結餘額,指定金額加回可用餘額 + v1.POST("freeze/rollback/add", wallet.RollbackFreezeAddAvailable) + + // 取消凍結-減少凍結餘額,加回可用餘額,可指定金額 + v1.POST("freeze/cancel", wallet.CancelFreeze) + + // 限制-減少凍結餘額,加別人限制餘額 + v1.POST("unconfirmed", wallet.Unconfirmed) + + // 合約劃轉 + v1.POST("contract/transfer", wallet.ContractTransfer) + + // 系統劃轉 + v1.POST("system-transfer", wallet.systemTransfer) + + // 餘額 + v1.GET("balance/:uid/user", wallet.Balance) + + // 歷史餘額 + v1.GET("balance/history/:uid/user", wallet.HistoryBalance) + + // 資產 + v1.GET("assets/balance/:uid/user", wallet.BalanceByAssets) + + // 檢查餘額 + v1.POST("balance", wallet.CheckBalance) + + // 取得今日已提現金額 + v1.GET("withdraw/today/user/:uid", wallet.GetTodayWithdraw) + + // 合約平台餘額 + v1.GET("balance/contract/system", wallet.ContractSystemBalance) \ No newline at end of file