add get symbol repository

This commit is contained in:
王性驊 2025-08-04 16:58:30 +08:00
parent fde3e3606f
commit d587e95442
10 changed files with 296 additions and 0 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"editor.fontFamily": "Fira Code, 'Fira Code', monospace"
}

View File

@ -4,3 +4,7 @@ Etcd:
Hosts:
- 127.0.0.1:2379
Key: blockchain.rpc
Binance:
Key: ""
Secret: ""
TestMode: true

10
go.mod
View File

@ -3,12 +3,22 @@ module blockchain
go 1.24.4
require (
github.com/alicebob/miniredis/v2 v2.35.0
github.com/zeromicro/go-zero v1.8.5
google.golang.org/grpc v1.74.2
google.golang.org/protobuf v1.36.6
)
require (
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
)
require (
github.com/adshao/go-binance/v2 v2.8.3
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect

12
go.sum
View File

@ -1,9 +1,15 @@
github.com/adshao/go-binance/v2 v2.8.3 h1:jwPRcX2u7FIO1pPoXgocyXpXhBI81A41kcmSDzS6uzo=
github.com/adshao/go-binance/v2 v2.8.3/go.mod h1:XkkuecSyJKPolaCGf/q4ovJYB3t0P+7RUYTbGr+LMGM=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@ -60,6 +66,8 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJY
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/pyroscope-go v1.2.2 h1:uvKCyZMD724RkaCEMrSTC38Yn7AnFe8S2wiAIYdDPCE=
github.com/grafana/pyroscope-go v1.2.2/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
@ -70,6 +78,8 @@ github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslC
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
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/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@ -125,6 +135,8 @@ github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUA
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
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=

View File

@ -4,4 +4,11 @@ import "github.com/zeromicro/go-zero/zrpc"
type Config struct {
zrpc.RpcServerConf
Binance
}
type Binance struct {
Key string
Secret string
TestMode bool
}

View File

@ -0,0 +1,5 @@
package blockchain
const RedisKeySymbolList = "symbol:all"
const SymbolExpire = 3600 // 秒(目前為一小時)

View File

@ -0,0 +1,38 @@
package entity
// Symbol 代表交易對資訊
type Symbol struct {
Symbol string `json:"symbol"` // 交易對名稱 BTCUSDT
Status string `json:"status"` // 狀態(如 "TRADING" 表示可交易)
BaseAsset string `json:"base_asset"` // 主幣種(如 BTCUSDT 的 BTC
BaseAssetPrecision int `json:"base_asset_precision"` // 主幣的小數點精度
QuoteAsset string `json:"quote_asset"` // 報價幣種(如 BTCUSDT 的 USDT
QuoteAssetPrecision int `json:"quote_asset_precision"` // 報價資產顯示的小數位數
}
func (s *Symbol) TableName() string {
return "symbol"
}
// Symbol 這個是幣安的
//type Symbol struct {
// Symbol string `json:"symbol"` // 交易對名稱(如 "BTCUSDT"
// Status string `json:"status"` // 狀態(如 "TRADING" 表示可交易)
// BaseAsset string `json:"baseAsset"` // 主幣種(如 BTCUSDT 的 BTC
// BaseAssetPrecision int `json:"baseAssetPrecision"` // 主幣的小數點精度
// QuoteAsset string `json:"quoteAsset"` // 報價幣種(如 BTCUSDT 的 USDT
// QuotePrecision int `json:"quotePrecision"` // 報價幣的小數點精度
// QuoteAssetPrecision int `json:"quoteAssetPrecision"` // 報價資產顯示的小數位數
// BaseCommissionPrecision int32 `json:"baseCommissionPrecision"` // 主幣手續費精度
// QuoteCommissionPrecision int32 `json:"quoteCommissionPrecision"` // 報價幣手續費精度
// OrderTypes []string `json:"orderTypes"` // 支援的下單類型(如 "LIMIT", "MARKET", "STOP_LOSS" 等)
// IcebergAllowed bool `json:"icebergAllowed"` // 是否允許冰山單
// OcoAllowed bool `json:"ocoAllowed"` // 是否允許 OCO 單(條件單組合)
// QuoteOrderQtyMarketAllowed bool `json:"quoteOrderQtyMarketAllowed"` // 市價單是否可直接輸入報價幣金額
// IsSpotTradingAllowed bool `json:"isSpotTradingAllowed"` // 是否允許現貨交易
// IsMarginTradingAllowed bool `json:"isMarginTradingAllowed"` // 是否允許槓桿/融資交易
// Filters []map[string]interface{} `json:"filters"` // 下單限制規則(如最小下單量、價格間隔等)
// Permissions []string `json:"permissions"` // 可用權限(如 "SPOT", "MARGIN"
// PermissionSets [][]string `json:"permissionSets"` // 權限組合(通常不常用)
//}

View File

@ -0,0 +1,10 @@
package repository
import (
"blockchain/internal/domain/entity"
"context"
)
type DataSourceRepository interface {
GetSymbols(ctx context.Context) ([]entity.Symbol, error)
}

View File

@ -0,0 +1,124 @@
package repository
import (
"blockchain/internal/config"
"blockchain/internal/domain/blockchain"
"blockchain/internal/domain/entity"
"blockchain/internal/domain/repository"
"context"
"encoding/json"
"fmt"
"github.com/adshao/go-binance/v2"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/redis"
"github.com/zeromicro/go-zero/core/syncx"
)
type BinanceRepositoryParam struct {
Conf *config.Binance
Redis *redis.Redis
}
type BinanceRepository struct {
Client *binance.Client
rds *redis.Redis
barrier syncx.SingleFlight
}
func MustBinanceRepository(param BinanceRepositoryParam) repository.DataSourceRepository {
apiKey := ""
secret := ""
if param.Conf.TestMode {
binance.UseTestnet = true
}
client := binance.NewClient(apiKey, secret)
return &BinanceRepository{
Client: client,
rds: param.Redis,
barrier: syncx.NewSingleFlight(),
}
}
func (repo *BinanceRepository) GetSymbols(ctx context.Context) ([]entity.Symbol, error) {
// 優先從 redis hash 拿
cached, err := repo.rds.Hgetall(blockchain.RedisKeySymbolList)
if err == nil && len(cached) > 0 {
symbols := make([]entity.Symbol, 0, len(cached))
canUseCache := true
for _, v := range cached {
var symbol entity.Symbol
if err := json.Unmarshal([]byte(v), &symbol); err == nil {
symbols = append(symbols, symbol)
} else {
// 如果任何一個反序列化失敗,代表快取可能已損壞,最好是回源重新拉取
canUseCache = false
break
}
}
if canUseCache {
return symbols, nil
}
}
// 用 SingleFlight 保證只有一個請求真的去 Binance
val, err := repo.barrier.Do(blockchain.RedisKeySymbolList, func() (any, error) {
// 拉 source
srcSymbols, err := repo.getSymbolsFromSource(ctx)
if err != nil {
return nil, err
}
result := make([]entity.Symbol, 0, len(srcSymbols))
hashData := make(map[string]string, len(srcSymbols))
for _, s := range srcSymbols {
// 只挑目前需要的欄位
symbolEntity := entity.Symbol{
Symbol: s.Symbol,
Status: s.Status,
BaseAsset: s.BaseAsset,
BaseAssetPrecision: s.BaseAssetPrecision,
QuoteAsset: s.QuoteAsset,
QuoteAssetPrecision: s.QuoteAssetPrecision,
}
result = append(result, symbolEntity)
// 將單一 symbol 序列化,準備寫入 hash
raw, err := json.Marshal(symbolEntity)
if err != nil {
logx.Error("failed to marshal symbol entity")
continue
}
hashData[symbolEntity.Symbol] = string(raw)
}
if len(hashData) > 0 {
// 使用 HMSET 一次寫入多個欄位到 hash
if err := repo.rds.Hmset(blockchain.RedisKeySymbolList, hashData); err == nil {
// 再對整個 key 設置過期時間
_ = repo.rds.Expire(blockchain.RedisKeySymbolList, blockchain.SymbolExpire)
}
}
return result, nil
})
if err != nil {
return nil, err
}
return val.([]entity.Symbol), nil
}
func (repo *BinanceRepository) getSymbolsFromSource(ctx context.Context) ([]binance.Symbol, error) {
if repo.Client == nil {
return nil, fmt.Errorf("binance client not initialized")
}
// 取得幣安交易所資訊
info, err := repo.Client.NewExchangeInfoService().Do(ctx)
if err != nil {
return nil, err
}
return info.Symbols, nil
}

View File

@ -0,0 +1,83 @@
package repository
import (
"blockchain/internal/config"
"context"
"fmt"
"sync"
"testing"
miniredis "github.com/alicebob/miniredis/v2"
"github.com/zeromicro/go-zero/core/stores/redis"
)
func setupMiniRedis() (*miniredis.Miniredis, *redis.Redis) {
// 啟動 setupMiniRedis 作為模擬的 Redis 服務
mr, err := miniredis.Run()
if err != nil {
panic("failed to start miniRedis: " + err.Error())
}
// 使用 setupMiniRedis 的地址配置 go-zero Redis 客戶端
redisConf := redis.RedisConf{
Host: mr.Addr(),
Type: "node",
}
r := redis.MustNewRedis(redisConf)
return mr, r
}
func TestGetSymbolsFromSource_TableDriven(t *testing.T) {
mr, rdb := setupMiniRedis()
defer mr.Close()
cases := []struct {
name string
wantSymbols []string
expectErr bool
}{
{
name: "ok",
//wantSymbols: []string{"BTCUSDT", "ETHUSDT"},
expectErr: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
repo := MustBinanceRepository(BinanceRepositoryParam{
Conf: &config.Binance{
Key: "",
Secret: "",
TestMode: true,
},
Redis: rdb,
})
var wg sync.WaitGroup
total := 10
for range total {
wg.Add(1)
go func() {
defer wg.Done()
symbols, err := repo.GetSymbols(context.Background())
fmt.Println(symbols)
if err != nil {
return
}
}()
}
wg.Wait()
//got, err := repo.GetSymbolsFromSource(context.Background())
//if tc.expectErr {
// assert.Error(t, err)
// assert.Nil(t, got)
//} else {
// assert.NoError(t, err)
// assert.Equal(t, tc.wantSymbols, got)
//}
})
}
}