1、暂存
This commit is contained in:
		| @ -3,9 +3,7 @@ package apis | |||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"go-admin/common/const/rediskey" |  | ||||||
| 	"go-admin/common/global" | 	"go-admin/common/global" | ||||||
| 	"go-admin/common/helper" |  | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| @ -297,13 +295,8 @@ func (e LinePreOrder) AddPreOrder(c *gin.Context) { | |||||||
| 	errs := make([]error, 0) | 	errs := make([]error, 0) | ||||||
| 	errStr := make([]string, 0) | 	errStr := make([]string, 0) | ||||||
| 	var msg string | 	var msg string | ||||||
| 	var tickerSymbol string |  | ||||||
| 	if req.SymbolType == global.SYMBOL_SPOT { | 	if err := s.AddPreOrderCheck(&req, p, &errs); err != nil { | ||||||
| 		tickerSymbol = helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() |  | ||||||
| 	} else { |  | ||||||
| 		tickerSymbol = helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val() |  | ||||||
| 	} |  | ||||||
| 	if err := s.AddPreOrderCheck(&req, p, &errs, tickerSymbol); err != nil { |  | ||||||
| 		e.Error(500, err, err.Error()) | 		e.Error(500, err, err.Error()) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -3,13 +3,13 @@ package apis | |||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"go-admin/common/const/rediskey" |  | ||||||
| 	"go-admin/common/global" | 	"go-admin/common/global" | ||||||
| 	"go-admin/common/helper" | 	"go-admin/common/helper" | ||||||
| 	"go-admin/config/serverinit" | 	"go-admin/config/serverinit" | ||||||
| 	models2 "go-admin/models" | 	models2 "go-admin/models" | ||||||
| 	"go-admin/pkg/utility" | 	"go-admin/pkg/utility" | ||||||
| 	"go-admin/services/binanceservice" | 	"go-admin/services/binanceservice" | ||||||
|  | 	"go-admin/services/cacheservice" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/bytedance/sonic" | 	"github.com/bytedance/sonic" | ||||||
| @ -289,9 +289,8 @@ func (e LineSymbol) SyncSpotSymbol(c *gin.Context) { | |||||||
| 		Symbol string `json:"symbol"` | 		Symbol string `json:"symbol"` | ||||||
| 		Price  string `json:"price"` | 		Price  string `json:"price"` | ||||||
| 	} | 	} | ||||||
| 	tickerSymbol := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() |  | ||||||
| 	tickerSymbolMaps := make([]Ticker, 0) | 	tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 1) | ||||||
| 	sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps) |  | ||||||
|  |  | ||||||
| 	for symbol, tradeSet := range tradeSets { | 	for symbol, tradeSet := range tradeSets { | ||||||
| 		var lineSymbol models.LineSymbol | 		var lineSymbol models.LineSymbol | ||||||
| @ -320,11 +319,10 @@ func (e LineSymbol) SyncSpotSymbol(c *gin.Context) { | |||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
|  |  | ||||||
| 			var tickerPrice decimal.Decimal | 			var tickerPrice decimal.Decimal | ||||||
| 			for _, symbolMap := range tickerSymbolMaps { | 			for _, symbolMap := range tickerSymbolMaps { | ||||||
| 				if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") { | 				if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") { | ||||||
| 					tickerPrice, _ = decimal.NewFromString(symbolMap.Price) | 					tickerPrice, _ = decimal.NewFromString(symbolMap.LastPrice) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			if tickerPrice.GreaterThan(decimal.Zero) { | 			if tickerPrice.GreaterThan(decimal.Zero) { | ||||||
| @ -412,10 +410,8 @@ func (e LineSymbol) SyncFutSymbol(c *gin.Context) { | |||||||
| 		Symbol string `json:"symbol"` | 		Symbol string `json:"symbol"` | ||||||
| 		Price  string `json:"price"` | 		Price  string `json:"price"` | ||||||
| 	} | 	} | ||||||
| 	tickerSymbol := helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val() |  | ||||||
| 	tickerSymbolMaps := make([]Ticker, 0) |  | ||||||
| 	sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps) |  | ||||||
|  |  | ||||||
|  | 	tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 2) | ||||||
| 	e.Orm.Model(&models.LineSymbolBlack{}).Where("type = 1").Find(&symbolBlack) | 	e.Orm.Model(&models.LineSymbolBlack{}).Where("type = 1").Find(&symbolBlack) | ||||||
| 	for symbol, tradeSet := range tradeSets { | 	for symbol, tradeSet := range tradeSets { | ||||||
| 		var lineSymbol models.LineSymbol | 		var lineSymbol models.LineSymbol | ||||||
| @ -447,7 +443,7 @@ func (e LineSymbol) SyncFutSymbol(c *gin.Context) { | |||||||
| 			var tickerPrice decimal.Decimal | 			var tickerPrice decimal.Decimal | ||||||
| 			for _, symbolMap := range tickerSymbolMaps { | 			for _, symbolMap := range tickerSymbolMaps { | ||||||
| 				if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") { | 				if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") { | ||||||
| 					tickerPrice, _ = decimal.NewFromString(symbolMap.Price) | 					tickerPrice, _ = decimal.NewFromString(symbolMap.LastPrice) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			if tickerPrice.GreaterThan(decimal.Zero) { | 			if tickerPrice.GreaterThan(decimal.Zero) { | ||||||
|  | |||||||
| @ -10,8 +10,7 @@ import ( | |||||||
| 	"go-admin/app/admin/service/appservice" | 	"go-admin/app/admin/service/appservice" | ||||||
| 	"go-admin/app/admin/service/common" | 	"go-admin/app/admin/service/common" | ||||||
| 	"go-admin/app/admin/service/dto" | 	"go-admin/app/admin/service/dto" | ||||||
| 	"go-admin/common/const/rediskey" | 	"go-admin/common/global" | ||||||
| 	"go-admin/common/helper" |  | ||||||
| 	"go-admin/common/service/sysservice/authservice" | 	"go-admin/common/service/sysservice/authservice" | ||||||
| 	"go-admin/common/service/sysservice/sysstatuscode" | 	"go-admin/common/service/sysservice/sysstatuscode" | ||||||
| 	statuscode "go-admin/common/status_code" | 	statuscode "go-admin/common/status_code" | ||||||
| @ -19,7 +18,7 @@ import ( | |||||||
| 	"go-admin/models/coingatedto" | 	"go-admin/models/coingatedto" | ||||||
| 	"go-admin/pkg/cryptohelper/inttostring" | 	"go-admin/pkg/cryptohelper/inttostring" | ||||||
| 	"go-admin/pkg/utility" | 	"go-admin/pkg/utility" | ||||||
| 	"go-admin/services/binanceservice" | 	"go-admin/services/cacheservice" | ||||||
| 	"go-admin/services/udunservice" | 	"go-admin/services/udunservice" | ||||||
|  |  | ||||||
| 	"github.com/bytedance/sonic" | 	"github.com/bytedance/sonic" | ||||||
| @ -373,20 +372,15 @@ func (e LineUserApi) Info(c *gin.Context) { | |||||||
| 		e.Logger.Error(500, err, err.Error()) | 		e.Logger.Error(500, err, err.Error()) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	//用户u的余额 |  | ||||||
| 	var tickerData []binanceservice.Ticker | 	tickerData, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 1) | ||||||
| 	val := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() |  | ||||||
| 	if val != "" { |  | ||||||
| 		sonic.Unmarshal([]byte(val), &tickerData) |  | ||||||
| 	} |  | ||||||
| 	// var usdtBalance decimal.Decimal |  | ||||||
| 	for i, asset := range resp { | 	for i, asset := range resp { | ||||||
| 		symbol := asset.Asset + "USDT" | 		symbol := asset.Asset + "USDT" | ||||||
| 		for _, datum := range tickerData { | 		for _, datum := range tickerData { | ||||||
| 			if datum.Symbol == symbol { | 			if datum.Symbol == symbol { | ||||||
| 				// mul := utility.StringToDecimal(datum.Price).Mul(utility.StringToDecimal(asset.Free)) | 				// mul := utility.StringToDecimal(datum.Price).Mul(utility.StringToDecimal(asset.Free)) | ||||||
| 				// usdtBalance = usdtBalance.Add(mul) | 				// usdtBalance = usdtBalance.Add(mul) | ||||||
| 				resp[i].UsdtValuation = datum.Price | 				resp[i].UsdtValuation = datum.LastPrice | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ type LineApiUser struct { | |||||||
| 	ApiName      string `json:"apiName" gorm:"type:varchar(255);comment:api用户名"` | 	ApiName      string `json:"apiName" gorm:"type:varchar(255);comment:api用户名"` | ||||||
| 	ApiKey       string `json:"apiKey" gorm:"type:varchar(255);comment:apiKey"` | 	ApiKey       string `json:"apiKey" gorm:"type:varchar(255);comment:apiKey"` | ||||||
| 	ApiSecret    string `json:"apiSecret" gorm:"type:varchar(255);comment:apiSecret"` | 	ApiSecret    string `json:"apiSecret" gorm:"type:varchar(255);comment:apiSecret"` | ||||||
|  | 	Passphrase   string `json:"passphrase" gorm:"type:varchar(255);comment:api密码"` | ||||||
| 	IpAddress    string `json:"ipAddress" gorm:"type:varchar(255);comment:代理地址"` | 	IpAddress    string `json:"ipAddress" gorm:"type:varchar(255);comment:代理地址"` | ||||||
| 	UserPass     string `json:"userPass" gorm:"type:varchar(255);comment:代码账号密码"` | 	UserPass     string `json:"userPass" gorm:"type:varchar(255);comment:代码账号密码"` | ||||||
| 	AdminId      int64  `json:"adminId" gorm:"type:int unsigned;comment:管理员id"` | 	AdminId      int64  `json:"adminId" gorm:"type:int unsigned;comment:管理员id"` | ||||||
|  | |||||||
| @ -264,8 +264,8 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	positions := map[string]positiondto.LinePreOrderPositioinDelReq{} | 	positions := map[string]positiondto.LinePreOrderPositioinDelReq{} | ||||||
| 	addPosition := binanceservice.AddPositionList{} | 	addPosition := positiondto.AddPositionList{} | ||||||
| 	reduceItem := binanceservice.ReduceListItem{} | 	reduceItem := positiondto.ReduceListItem{} | ||||||
| 	futAddPosition := map[int]string{} | 	futAddPosition := map[int]string{} | ||||||
| 	spotAddPosition := map[int]string{} | 	spotAddPosition := map[int]string{} | ||||||
| 	futReduces := map[int]string{} | 	futReduces := map[int]string{} | ||||||
| @ -395,7 +395,7 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi | |||||||
| } | } | ||||||
|  |  | ||||||
| // 单个新增订单 | // 单个新增订单 | ||||||
| func (e *LinePreOrder) AddPreOrderCheck(req *dto.LineAddPreOrderReq, p *actions.DataPermission, errs *[]error, tickerSymbol string) error { | func (e *LinePreOrder) AddPreOrderCheck(req *dto.LineAddPreOrderReq, p *actions.DataPermission, errs *[]error) error { | ||||||
| 	apiUserService := LineApiUser{Service: e.Service} | 	apiUserService := LineApiUser{Service: e.Service} | ||||||
| 	apiIds := []int{} | 	apiIds := []int{} | ||||||
| 	apiUserIds := strings.Split(req.ApiUserId, ",") | 	apiUserIds := strings.Split(req.ApiUserId, ",") | ||||||
| @ -423,14 +423,14 @@ func (e *LinePreOrder) AddPreOrderCheck(req *dto.LineAddPreOrderReq, p *actions. | |||||||
| 				*errs = append(*errs, fmt.Errorf("api用户未激活:%v", item)) | 				*errs = append(*errs, fmt.Errorf("api用户未激活:%v", item)) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		e.AddPreOrder(req, activeApiIds, p, errs, tickerSymbol) | 		e.AddPreOrder(req, activeApiIds, p, errs) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // AddPreOrder 单个添加 | // AddPreOrder 单个添加 | ||||||
| func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int, p *actions.DataPermission, errs *[]error, tickerSymbol string) error { | func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int, p *actions.DataPermission, errs *[]error) error { | ||||||
| 	if req.SaveTemplate == "2" || req.SaveTemplate == "1" { //2 = 只保存模板 1= 保存模板并下单 | 	if req.SaveTemplate == "2" || req.SaveTemplate == "1" { //2 = 只保存模板 1= 保存模板并下单 | ||||||
| 		var templateLog dto.LineAddPreOrderReq | 		var templateLog dto.LineAddPreOrderReq | ||||||
| 		copier.Copy(&templateLog, req) | 		copier.Copy(&templateLog, req) | ||||||
| @ -1250,15 +1250,7 @@ func (e *LinePreOrder) AddBatchPreOrder(batchReq *dto.LineBatchAddPreOrderReq, p | |||||||
| 		batchReq.Symbol = symbolGroupInfo.Symbol | 		batchReq.Symbol = symbolGroupInfo.Symbol | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	//脚本次数 |  | ||||||
| 	// if batchReq.OrderNum > 0 { |  | ||||||
| 	apiUserIds := strings.Split(batchReq.ApiUserId, ",") | 	apiUserIds := strings.Split(batchReq.ApiUserId, ",") | ||||||
| 	var tickerSymbol string |  | ||||||
| 	if batchReq.SymbolType == global.SYMBOL_SPOT { |  | ||||||
| 		tickerSymbol = helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() |  | ||||||
| 	} else { |  | ||||||
| 		tickerSymbol = helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val() |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if batchReq.Script == "1" { | 	if batchReq.Script == "1" { | ||||||
| 		//scriptLogs := make([]models.LinePreScript, 0) | 		//scriptLogs := make([]models.LinePreScript, 0) | ||||||
| @ -1317,15 +1309,11 @@ func (e *LinePreOrder) AddBatchPreOrder(batchReq *dto.LineBatchAddPreOrderReq, p | |||||||
| 				req.ReduceTakeProfitRatio = batchReq.ReduceTakeProfitRatio | 				req.ReduceTakeProfitRatio = batchReq.ReduceTakeProfitRatio | ||||||
| 				req.CreateBy = batchReq.CreateBy | 				req.CreateBy = batchReq.CreateBy | ||||||
|  |  | ||||||
| 				e.AddPreOrderCheck(&req, p, errs, tickerSymbol) | 				e.AddPreOrderCheck(&req, p, errs) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| 	// } else { |  | ||||||
| 	// 	*errs = append(*errs, errors.New("请选择运行次数")) |  | ||||||
| 	// 	return nil |  | ||||||
| 	// } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // QuickAddPreOrder 模板快速下单 | // QuickAddPreOrder 模板快速下单 | ||||||
| @ -1338,13 +1326,7 @@ func (e *LinePreOrder) QuickAddPreOrder(quickReq *dto.QuickAddPreOrderReq, p *ac | |||||||
| 			var addPreOrderParams dto.LineAddPreOrderReq | 			var addPreOrderParams dto.LineAddPreOrderReq | ||||||
| 			sonic.Unmarshal([]byte(log.Params), &addPreOrderParams) | 			sonic.Unmarshal([]byte(log.Params), &addPreOrderParams) | ||||||
|  |  | ||||||
| 			var tickerSymbol string | 			err := e.AddPreOrderCheck(&addPreOrderParams, p, errs) | ||||||
| 			if addPreOrderParams.OrderType == global.SYMBOL_SPOT { |  | ||||||
| 				tickerSymbol = helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() |  | ||||||
| 			} else { |  | ||||||
| 				tickerSymbol = helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val() |  | ||||||
| 			} |  | ||||||
| 			err := e.AddPreOrderCheck(&addPreOrderParams, p, errs, tickerSymbol) |  | ||||||
| 			// err := e.AddPreOrder(&addPreOrderParams, p, errs, tickerSymbol) | 			// err := e.AddPreOrder(&addPreOrderParams, p, errs, tickerSymbol) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				*errs = append(*errs, fmt.Errorf("api_id:%s 获取交易对:%s 生成订单失败", addPreOrderParams.ApiUserId, addPreOrderParams.Symbol)) | 				*errs = append(*errs, fmt.Errorf("api_id:%s 获取交易对:%s 生成订单失败", addPreOrderParams.ApiUserId, addPreOrderParams.Symbol)) | ||||||
| @ -1696,23 +1678,18 @@ func (e *LinePreOrder) CalculateAmount(req dto.ManuallyCover, totalNum, lastPric | |||||||
| 	} else { | 	} else { | ||||||
| 		decimalValue := utility.StringToDecimal(req.Value) | 		decimalValue := utility.StringToDecimal(req.Value) | ||||||
| 		if notUsdt { | 		if notUsdt { | ||||||
| 			tickerSymbolMaps := make([]dto.Ticker, 0) | 			tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 2) | ||||||
| 			tickerSymbol := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() |  | ||||||
| 			if err := sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps); err != nil { |  | ||||||
| 				return decimal.Zero, err |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			var tickerPrice decimal.Decimal | 			var tickerPrice decimal.Decimal | ||||||
| 			for _, symbolMap := range tickerSymbolMaps { | 			for _, symbolMap := range tickerSymbolMaps { | ||||||
| 				if symbolMap.Symbol == strings.ToUpper(symbolInfo.BaseAsset+"USDT") { | 				if symbolMap.Symbol == strings.ToUpper(symbolInfo.BaseAsset+"USDT") { | ||||||
| 					tickerPrice, _ = decimal.NewFromString(symbolMap.Price) | 					tickerPrice, _ = decimal.NewFromString(symbolMap.LastPrice) | ||||||
| 					break | 					break | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			for _, symbolMap := range tickerSymbolMaps { | 			for _, symbolMap := range tickerSymbolMaps { | ||||||
| 				if symbolMap.Symbol == strings.ToUpper(symbolInfo.QuoteAsset+"USDT") { | 				if symbolMap.Symbol == strings.ToUpper(symbolInfo.QuoteAsset+"USDT") { | ||||||
| 					uTickerPrice, _ := decimal.NewFromString(symbolMap.Price) | 					uTickerPrice, _ := decimal.NewFromString(symbolMap.LastPrice) | ||||||
| 					div := utility.SafeDiv(tickerPrice, decimal.NewFromInt(1).Div(uTickerPrice)) | 					div := utility.SafeDiv(tickerPrice, decimal.NewFromInt(1).Div(uTickerPrice)) | ||||||
| 					amt = utility.SafeDiv(decimalValue, div) | 					amt = utility.SafeDiv(decimalValue, div) | ||||||
| 					break | 					break | ||||||
| @ -2108,9 +2085,9 @@ func (e *LinePreOrder) GenerateOrder(req *dto.LineAddPreOrderReq) error { | |||||||
| 	var tickerPrice decimal.Decimal | 	var tickerPrice decimal.Decimal | ||||||
|  |  | ||||||
| 	if req.SymbolType == 1 { | 	if req.SymbolType == 1 { | ||||||
| 		tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, req.Symbol, 0) |  | ||||||
| 	} else { |  | ||||||
| 		tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, req.Symbol, 1) | 		tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, req.Symbol, 1) | ||||||
|  | 	} else { | ||||||
|  | 		tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, req.Symbol, 2) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if tradeSet.LastPrice == "" { | 	if tradeSet.LastPrice == "" { | ||||||
|  | |||||||
| @ -15,7 +15,6 @@ import ( | |||||||
| 	"go-admin/app/admin/models" | 	"go-admin/app/admin/models" | ||||||
| 	"go-admin/app/admin/service/dto" | 	"go-admin/app/admin/service/dto" | ||||||
| 	"go-admin/common/actions" | 	"go-admin/common/actions" | ||||||
| 	"go-admin/common/const/rediskey" |  | ||||||
| 	cDto "go-admin/common/dto" | 	cDto "go-admin/common/dto" | ||||||
| 	"go-admin/common/global" | 	"go-admin/common/global" | ||||||
| 	"go-admin/common/helper" | 	"go-admin/common/helper" | ||||||
| @ -23,6 +22,7 @@ import ( | |||||||
| 	models2 "go-admin/models" | 	models2 "go-admin/models" | ||||||
| 	"go-admin/pkg/utility" | 	"go-admin/pkg/utility" | ||||||
| 	"go-admin/services/binanceservice" | 	"go-admin/services/binanceservice" | ||||||
|  | 	"go-admin/services/cacheservice" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type LineSymbol struct { | type LineSymbol struct { | ||||||
| @ -354,9 +354,10 @@ func (e *LineSymbol) ResetSpotSymbol() error { | |||||||
| 		Symbol string `json:"symbol"` | 		Symbol string `json:"symbol"` | ||||||
| 		Price  string `json:"price"` | 		Price  string `json:"price"` | ||||||
| 	} | 	} | ||||||
| 	tickerSymbol := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() | 	// tickerSymbol := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() | ||||||
| 	tickerSymbolMaps := make([]Ticker, 0) | 	// tickerSymbolMaps := make([]Ticker, 0) | ||||||
| 	sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps) | 	// sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps) | ||||||
|  | 	tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 1) | ||||||
| 	oldMapSymbols, err := getMapSymbol(e, "1") | 	oldMapSymbols, err := getMapSymbol(e, "1") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| @ -389,7 +390,7 @@ func (e *LineSymbol) ResetSpotSymbol() error { | |||||||
| 			var tickerPrice decimal.Decimal | 			var tickerPrice decimal.Decimal | ||||||
| 			for _, symbolMap := range tickerSymbolMaps { | 			for _, symbolMap := range tickerSymbolMaps { | ||||||
| 				if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") { | 				if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") { | ||||||
| 					tickerPrice, _ = decimal.NewFromString(symbolMap.Price) | 					tickerPrice, _ = decimal.NewFromString(symbolMap.LastPrice) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			if tickerPrice.GreaterThan(decimal.Zero) { | 			if tickerPrice.GreaterThan(decimal.Zero) { | ||||||
| @ -527,9 +528,10 @@ func (e *LineSymbol) ResetFuturesSymbol() error { | |||||||
| 		Symbol string `json:"symbol"` | 		Symbol string `json:"symbol"` | ||||||
| 		Price  string `json:"price"` | 		Price  string `json:"price"` | ||||||
| 	} | 	} | ||||||
| 	tickerSymbol := helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val() | 	// tickerSymbol := helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val() | ||||||
| 	tickerSymbolMaps := make([]Ticker, 0) | 	// tickerSymbolMaps := make([]Ticker, 0) | ||||||
| 	sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps) | 	// sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps) | ||||||
|  | 	tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 2) | ||||||
| 	oldMapSymbols, err := getMapSymbol(e, "2") | 	oldMapSymbols, err := getMapSymbol(e, "2") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| @ -560,7 +562,7 @@ func (e *LineSymbol) ResetFuturesSymbol() error { | |||||||
| 			var tickerPrice decimal.Decimal | 			var tickerPrice decimal.Decimal | ||||||
| 			for _, symbolMap := range tickerSymbolMaps { | 			for _, symbolMap := range tickerSymbolMaps { | ||||||
| 				if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") { | 				if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") { | ||||||
| 					tickerPrice, _ = decimal.NewFromString(symbolMap.Price) | 					tickerPrice, _ = decimal.NewFromString(symbolMap.LastPrice) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			if tickerPrice.GreaterThan(decimal.Zero) { | 			if tickerPrice.GreaterThan(decimal.Zero) { | ||||||
|  | |||||||
| @ -18,6 +18,7 @@ import ( | |||||||
| 	"go-admin/pkg/utility" | 	"go-admin/pkg/utility" | ||||||
| 	"go-admin/pkg/utility/seqs" | 	"go-admin/pkg/utility/seqs" | ||||||
| 	"go-admin/services/binanceservice" | 	"go-admin/services/binanceservice" | ||||||
|  | 	"go-admin/services/cacheservice" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| @ -323,11 +324,13 @@ func (e *LineUser) RechargeNetworkList(req *dto.RechargeListReq) (data []models. | |||||||
| 	} | 	} | ||||||
| 	coin := req.Coin | 	coin := req.Coin | ||||||
| 	resp := make([]models.VtsCoinToNetWorkResp, 0) | 	resp := make([]models.VtsCoinToNetWorkResp, 0) | ||||||
| 	var tickerData []binanceservice.Ticker | 	// var tickerData []binanceservice.Ticker | ||||||
| 	val := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() | 	// val := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() | ||||||
| 	if val != "" { | 	// if val != "" { | ||||||
| 		sonic.Unmarshal([]byte(val), &tickerData) | 	// 	sonic.Unmarshal([]byte(val), &tickerData) | ||||||
| 	} | 	// } | ||||||
|  |  | ||||||
|  | 	tickerData, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 1) | ||||||
|  |  | ||||||
| 	for _, v1 := range list { | 	for _, v1 := range list { | ||||||
| 		if strings.EqualFold(coin, "USDT") { | 		if strings.EqualFold(coin, "USDT") { | ||||||
| @ -357,7 +360,7 @@ func (e *LineUser) RechargeNetworkList(req *dto.RechargeListReq) (data []models. | |||||||
| 					TransferFee: utility.FloatCutStr(v1.TransferFee, 8), | 					TransferFee: utility.FloatCutStr(v1.TransferFee, 8), | ||||||
| 					//TransferFeeUsdt string `json:"transfer_fee_usdt"` | 					//TransferFeeUsdt string `json:"transfer_fee_usdt"` | ||||||
| 				} | 				} | ||||||
| 				res.TransferFeeUsdt = utility.FloatCutStr(v1.TransferFee*utility.StringToFloat64(v2.Price), 8) | 				res.TransferFeeUsdt = utility.FloatCutStr(v1.TransferFee*utility.StringToFloat64(v2.LastPrice), 8) | ||||||
| 				resp = append(resp, res) | 				resp = append(resp, res) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -9,8 +9,8 @@ import ( | |||||||
| 	"go-admin/common/const/rediskey" | 	"go-admin/common/const/rediskey" | ||||||
| 	"go-admin/common/global" | 	"go-admin/common/global" | ||||||
| 	"go-admin/common/helper" | 	"go-admin/common/helper" | ||||||
|  | 	"go-admin/models/positiondto" | ||||||
| 	"go-admin/pkg/utility" | 	"go-admin/pkg/utility" | ||||||
| 	"go-admin/services/binanceservice" |  | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/bytedance/sonic" | 	"github.com/bytedance/sonic" | ||||||
| @ -128,10 +128,10 @@ func removeExpirateChildOrder(childOrders []models.LinePreOrder, db *gorm.DB) { | |||||||
| 	spotAdPositionStr, _ := helper.DefaultRedis.GetAllList(spotAddKey) | 	spotAdPositionStr, _ := helper.DefaultRedis.GetAllList(spotAddKey) | ||||||
| 	futReduceStr, _ := helper.DefaultRedis.GetAllList(futReduceKey) | 	futReduceStr, _ := helper.DefaultRedis.GetAllList(futReduceKey) | ||||||
| 	spotReduceStr, _ := helper.DefaultRedis.GetAllList(spotReduceKey) | 	spotReduceStr, _ := helper.DefaultRedis.GetAllList(spotReduceKey) | ||||||
| 	futAddPositionCache := binanceservice.AddPositionList{} | 	futAddPositionCache := positiondto.AddPositionList{} | ||||||
| 	spotAddPositionCache := binanceservice.AddPositionList{} | 	spotAddPositionCache := positiondto.AddPositionList{} | ||||||
| 	futReduceCache := binanceservice.ReduceListItem{} | 	futReduceCache := positiondto.ReduceListItem{} | ||||||
| 	spotReduceCache := binanceservice.ReduceListItem{} | 	spotReduceCache := positiondto.ReduceListItem{} | ||||||
| 	futAddPositionMap := map[int]string{} | 	futAddPositionMap := map[int]string{} | ||||||
| 	spotAddPositionMap := map[int]string{} | 	spotAddPositionMap := map[int]string{} | ||||||
| 	futReduceMap := map[int]string{} | 	futReduceMap := map[int]string{} | ||||||
|  | |||||||
| @ -242,7 +242,7 @@ func (t LimitOrderTimeoutDuration) ReSpotOrderPlace(db *gorm.DB, order models.Li | |||||||
| 	} else { | 	} else { | ||||||
| 		var remainingQuantity decimal.Decimal | 		var remainingQuantity decimal.Decimal | ||||||
| 		spotOrder, err := spotApi.GetOrderByOrderSnLoop(order.Symbol, order.OrderSn, apiUserinfo, 4) | 		spotOrder, err := spotApi.GetOrderByOrderSnLoop(order.Symbol, order.OrderSn, apiUserinfo, 4) | ||||||
| 		tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 0) | 		tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1) | ||||||
|  |  | ||||||
| 		if err == nil { | 		if err == nil { | ||||||
| 			origQty := utility.StrToDecimal(spotOrder.OrigQty) | 			origQty := utility.StrToDecimal(spotOrder.OrigQty) | ||||||
| @ -360,7 +360,7 @@ func (t LimitOrderTimeoutDuration) ReFutOrderPlace(db *gorm.DB, order models.Lin | |||||||
| 	} else { | 	} else { | ||||||
| 		var remainingQuantity decimal.Decimal | 		var remainingQuantity decimal.Decimal | ||||||
| 		spotOrder, err := futApi.GetOrderByOrderSnLoop(order.Symbol, order.OrderSn, apiUserinfo, 4) | 		spotOrder, err := futApi.GetOrderByOrderSnLoop(order.Symbol, order.OrderSn, apiUserinfo, 4) | ||||||
| 		tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1) | 		tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 2) | ||||||
|  |  | ||||||
| 		if err == nil { | 		if err == nil { | ||||||
| 			origQty := utility.StrToDecimal(spotOrder.OrigQty) | 			origQty := utility.StrToDecimal(spotOrder.OrigQty) | ||||||
|  | |||||||
							
								
								
									
										264
									
								
								cmd/binance/binance.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								cmd/binance/binance.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,264 @@ | |||||||
|  | package binance | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"go-admin/app/admin/models" | ||||||
|  | 	"go-admin/app/admin/service" | ||||||
|  | 	"go-admin/common/const/rediskey" | ||||||
|  | 	"go-admin/common/database" | ||||||
|  | 	"go-admin/common/global" | ||||||
|  | 	"go-admin/common/helper" | ||||||
|  | 	"go-admin/common/storage" | ||||||
|  | 	"go-admin/config/serverinit" | ||||||
|  | 	"go-admin/pkg/httputils" | ||||||
|  | 	"go-admin/pkg/utility" | ||||||
|  | 	"go-admin/services/excservice" | ||||||
|  | 	"go-admin/services/fileservice" | ||||||
|  | 	"os" | ||||||
|  | 	"os/signal" | ||||||
|  | 	"strconv" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	ext "go-admin/config" | ||||||
|  |  | ||||||
|  | 	"github.com/bytedance/sonic" | ||||||
|  | 	"github.com/go-admin-team/go-admin-core/config/source/file" | ||||||
|  | 	"github.com/go-admin-team/go-admin-core/logger" | ||||||
|  | 	"github.com/go-admin-team/go-admin-core/sdk" | ||||||
|  | 	"github.com/go-admin-team/go-admin-core/sdk/config" | ||||||
|  | 	"github.com/go-admin-team/go-admin-core/sdk/pkg" | ||||||
|  | 	"github.com/spf13/cobra" | ||||||
|  | 	"gorm.io/gorm" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	configYml string | ||||||
|  | 	StartCmd  = &cobra.Command{ | ||||||
|  | 		Use:          "binance", | ||||||
|  | 		Short:        "Start Binance service (market data + user private)", | ||||||
|  | 		Example:      "go-admin binance -c config/settings.yml", | ||||||
|  | 		SilenceUsage: true, | ||||||
|  | 		PreRun: func(cmd *cobra.Command, args []string) { | ||||||
|  | 			setup() | ||||||
|  | 		}, | ||||||
|  | 		RunE: func(cmd *cobra.Command, args []string) error { | ||||||
|  | 			return run() | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	StartCmd.PersistentFlags().StringVarP(&configYml, "config", "c", "config/settings.yml", "Start server with provided configuration file") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func setup() { | ||||||
|  | 	// 注入配置扩展项 | ||||||
|  | 	config.ExtendConfig = &ext.ExtConfig | ||||||
|  | 	//1. 读取配置 | ||||||
|  | 	config.Setup( | ||||||
|  | 		file.NewSource(file.WithPath(configYml)), | ||||||
|  | 		database.Setup, | ||||||
|  | 		storage.Setup, | ||||||
|  | 	) | ||||||
|  | 	//注册监听函数 | ||||||
|  | 	queue := sdk.Runtime.GetMemoryQueue("") | ||||||
|  | 	queue.Register(global.LoginLog, models.SaveLoginLog) | ||||||
|  | 	queue.Register(global.OperateLog, models.SaveOperaLog) | ||||||
|  | 	go queue.Run() | ||||||
|  |  | ||||||
|  | 	usageStr := `starting Binance service (market data + user private)...` | ||||||
|  | 	logger.Info(usageStr) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func run() error { | ||||||
|  | 	ctx, cancel := context.WithCancel(context.Background()) | ||||||
|  | 	defer cancel() | ||||||
|  |  | ||||||
|  | 	var db *gorm.DB | ||||||
|  | 	dbs := sdk.Runtime.GetDb() | ||||||
|  |  | ||||||
|  | 	for _, item := range dbs { | ||||||
|  | 		db = item | ||||||
|  | 		break | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	defaultInit(db, ctx) | ||||||
|  |  | ||||||
|  | 	utility.SafeGo(func() { | ||||||
|  | 		// 启动定时任务 | ||||||
|  | 		clearLogJob(db, ctx) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	//自动重启websocket | ||||||
|  | 	// utility.SafeGo(func() { | ||||||
|  | 	// 	reconnect(ctx) | ||||||
|  | 	// }) | ||||||
|  |  | ||||||
|  | 	//动态用户连接管理 | ||||||
|  | 	utility.SafeGo(func() { | ||||||
|  | 		manageBinanceUserConnections(ctx) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间) | ||||||
|  | 	quit := make(chan os.Signal, 1) | ||||||
|  | 	signal.Notify(quit, os.Interrupt) | ||||||
|  | 	<-quit | ||||||
|  |  | ||||||
|  | 	fmt.Printf("%s Shutdown Server ... \r\n", pkg.GetCurrentTimeStr()) | ||||||
|  | 	logger.Infof("Server exiting") | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func defaultInit(db *gorm.DB, ctx context.Context) { | ||||||
|  | 	httputils.InitProxy(ext.ExtConfig.ProxyUrl) | ||||||
|  | 	//初始化 默认redis | ||||||
|  | 	helper.InitDefaultRedis(ext.ExtConfig.Redis.Addr, ext.ExtConfig.Redis.Password, ext.ExtConfig.Redis.Db) | ||||||
|  | 	helper.InitLockRedisConn(ext.ExtConfig.Redis.Addr, ext.ExtConfig.Redis.Password, strconv.Itoa(ext.ExtConfig.Redis.Db)) | ||||||
|  |  | ||||||
|  | 	err := helper.DefaultRedis.Ping() | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Info("初始化redis失败!请检查配置") | ||||||
|  | 		_, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||||||
|  | 		cancel() | ||||||
|  | 	} else { | ||||||
|  | 		logger.Info("redis初始化成功") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	//初始化Binance服务(行情 + 用户私有) | ||||||
|  | 	logger.Info("=== 启动Binance完整服务 ===") | ||||||
|  |  | ||||||
|  | 	// 启动Binance行情服务 | ||||||
|  | 	logger.Info("启动Binance行情服务...") | ||||||
|  | 	if err := serverinit.BinanceMarketInit(); err != nil { | ||||||
|  | 		logger.Infof("Binance行情服务启动失败: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 初始化Binance用户连接 | ||||||
|  | 	logger.Info("启动Binance用户服务...") | ||||||
|  | 	initBinanceUserConnections(db, ctx) | ||||||
|  | 	logger.Info("=== Binance完整服务已启动 ===") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 初始化Binance用户连接 | ||||||
|  | func initBinanceUserConnections(db *gorm.DB, ctx context.Context) { | ||||||
|  | 	var list []models.LineApiUser | ||||||
|  | 	err := db.Model(&models.LineApiUser{}).Where("open_status=1 AND exchange_type =?", global.EXCHANGE_BINANCE).Find(&list).Error | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Info("获取Binance用户api失败: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	logger.Infof("找到 %d 个Binance用户,开始建立连接...", len(list)) | ||||||
|  | 	for _, item := range list { | ||||||
|  | 		service.OpenUserBinanceWebsocket(item) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 动态Binance用户连接管理 | ||||||
|  | func manageBinanceUserConnections(ctx context.Context) { | ||||||
|  | 	ticker := time.NewTicker(time.Second * 5) | ||||||
|  | 	defer ticker.Stop() | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case <-ctx.Done(): | ||||||
|  | 			logger.Info("Binanceç¨æ·åç»åç§°åé") | ||||||
|  | 			return | ||||||
|  | 		case <-ticker.C: | ||||||
|  | 			deleteKeys, _ := helper.DefaultRedis.GetAllList(rediskey.ApiUserDeleteList) | ||||||
|  | 			activeApis, _ := helper.DefaultRedis.GetAllList(rediskey.ApiUserActiveList) | ||||||
|  |  | ||||||
|  | 			//移除Binance连接 | ||||||
|  | 			for _, item := range deleteKeys { | ||||||
|  | 				logger.Infof("移除Binance用户连接: %s", item) | ||||||
|  |  | ||||||
|  | 				if wm, ok := excservice.SpotSockets[item]; ok { | ||||||
|  | 					wm.Stop() | ||||||
|  | 					delete(excservice.SpotSockets, item) | ||||||
|  | 				} | ||||||
|  | 				if wm, ok := excservice.FutureSockets[item]; ok { | ||||||
|  | 					wm.Stop() | ||||||
|  | 					delete(excservice.FutureSockets, item) | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				if _, err := helper.DefaultRedis.LRem(rediskey.ApiUserDeleteList, item); err != nil { | ||||||
|  | 					logger.Errorf("移除待关闭websocket失败: %v", err) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			//连接Binance用户websocket | ||||||
|  | 			var apiUser models.LineApiUser | ||||||
|  | 			for _, item := range activeApis { | ||||||
|  | 				if item == "" { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				if err := sonic.Unmarshal([]byte(item), &apiUser); err != nil { | ||||||
|  | 					logger.Errorf("解析用户信息失败: %v", err) | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				// 只处理Binance用户 | ||||||
|  | 				if apiUser.Id > 0 && apiUser.ExchangeType == global.EXCHANGE_BINANCE { | ||||||
|  | 					logger.Infof("为Binance用户 %d 建立连接", apiUser.Id) | ||||||
|  | 					service.OpenUserBinanceWebsocket(apiUser) | ||||||
|  |  | ||||||
|  | 					if _, err := helper.DefaultRedis.LRem(rediskey.ApiUserActiveList, item); err != nil { | ||||||
|  | 						logger.Errorf("删除待触发的apiUser失败: %v", err) | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 定时清理日志 | ||||||
|  | func clearLogJob(db *gorm.DB, ctx context.Context) { | ||||||
|  | 	ticker := time.NewTicker(time.Hour * 1) | ||||||
|  | 	defer ticker.Stop() | ||||||
|  |  | ||||||
|  | 	select { | ||||||
|  | 	case <-ctx.Done(): | ||||||
|  | 		return | ||||||
|  | 	case <-ticker.C: | ||||||
|  | 		fileservice.ClearLogs(db) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 定时重连websocket | ||||||
|  | func reconnect(ctx context.Context) error { | ||||||
|  | 	ticker := time.NewTicker(time.Hour * 1) | ||||||
|  | 	defer ticker.Stop() | ||||||
|  |  | ||||||
|  | 	select { | ||||||
|  | 	case <-ctx.Done(): | ||||||
|  | 		return nil | ||||||
|  | 	case <-ticker.C: | ||||||
|  | 		// 重启Binance连接 | ||||||
|  | 		spotSockets := excservice.SpotSockets | ||||||
|  | 		futuresSockets := excservice.FutureSockets | ||||||
|  | 		timeOut := 22 * time.Hour | ||||||
|  |  | ||||||
|  | 		for _, item := range spotSockets { | ||||||
|  | 			if time.Since(item.ConnectTime) > timeOut { | ||||||
|  | 				if err := item.ReplaceConnection(); err != nil { | ||||||
|  | 					logger.Errorf("Binance现货重启连接失败 key:%s,error:%s", item.GetKey(), err) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, item := range futuresSockets { | ||||||
|  | 			if time.Since(item.ConnectTime) > timeOut { | ||||||
|  | 				if err := item.ReplaceConnection(); err != nil { | ||||||
|  | 					logger.Errorf("Binance合约重启连接失败 key:%s,error:%s", item.GetKey(), err) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										271
									
								
								cmd/bitget/bitget.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										271
									
								
								cmd/bitget/bitget.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,271 @@ | |||||||
|  | package bitget | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"go-admin/app/admin/models" | ||||||
|  | 	"go-admin/common/const/rediskey" | ||||||
|  | 	"go-admin/common/database" | ||||||
|  | 	"go-admin/common/global" | ||||||
|  | 	"go-admin/common/helper" | ||||||
|  | 	"go-admin/common/storage" | ||||||
|  | 	"go-admin/config/serverinit" | ||||||
|  | 	"go-admin/pkg/utility" | ||||||
|  | 	"go-admin/services/bitgetservice" | ||||||
|  | 	"go-admin/services/fileservice" | ||||||
|  | 	"os" | ||||||
|  | 	"os/signal" | ||||||
|  | 	"strconv" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	ext "go-admin/config" | ||||||
|  |  | ||||||
|  | 	"github.com/bytedance/sonic" | ||||||
|  | 	"github.com/go-admin-team/go-admin-core/config/source/file" | ||||||
|  | 	"github.com/go-admin-team/go-admin-core/logger" | ||||||
|  | 	"github.com/go-admin-team/go-admin-core/sdk" | ||||||
|  | 	"github.com/go-admin-team/go-admin-core/sdk/config" | ||||||
|  | 	"github.com/go-admin-team/go-admin-core/sdk/pkg" | ||||||
|  | 	"github.com/spf13/cobra" | ||||||
|  | 	"gorm.io/gorm" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	configYml string | ||||||
|  | 	StartCmd  = &cobra.Command{ | ||||||
|  | 		Use:          "bitget", | ||||||
|  | 		Short:        "Start Bitget service (market data + user private)", | ||||||
|  | 		Example:      "go-admin bitget -c config/settings.yml", | ||||||
|  | 		SilenceUsage: true, | ||||||
|  | 		PreRun: func(cmd *cobra.Command, args []string) { | ||||||
|  | 			setup() | ||||||
|  | 		}, | ||||||
|  | 		RunE: func(cmd *cobra.Command, args []string) error { | ||||||
|  | 			return run() | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	StartCmd.PersistentFlags().StringVarP(&configYml, "config", "c", "config/settings.yml", "Start server with provided configuration file") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func setup() { | ||||||
|  | 	// 注入配置扩展项 | ||||||
|  | 	config.ExtendConfig = &ext.ExtConfig | ||||||
|  | 	//1. 读取配置 | ||||||
|  | 	config.Setup( | ||||||
|  | 		file.NewSource(file.WithPath(configYml)), | ||||||
|  | 		database.Setup, | ||||||
|  | 		storage.Setup, | ||||||
|  | 	) | ||||||
|  | 	//注册监听函数 | ||||||
|  | 	queue := sdk.Runtime.GetMemoryQueue("") | ||||||
|  | 	queue.Register(global.LoginLog, models.SaveLoginLog) | ||||||
|  | 	queue.Register(global.OperateLog, models.SaveOperaLog) | ||||||
|  | 	go queue.Run() | ||||||
|  |  | ||||||
|  | 	usageStr := `starting Bitget service (market data + user private)...` | ||||||
|  | 	logger.Info(usageStr) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func run() error { | ||||||
|  | 	ctx, cancel := context.WithCancel(context.Background()) | ||||||
|  | 	defer cancel() | ||||||
|  |  | ||||||
|  | 	var db *gorm.DB | ||||||
|  | 	dbs := sdk.Runtime.GetDb() | ||||||
|  |  | ||||||
|  | 	for _, item := range dbs { | ||||||
|  | 		db = item | ||||||
|  | 		break | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	defaultInit(db, ctx) | ||||||
|  |  | ||||||
|  | 	utility.SafeGo(func() { | ||||||
|  | 		// 启动定时任务 | ||||||
|  | 		clearLogJob(db, ctx) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	//自动重启websocket | ||||||
|  | 	utility.SafeGo(func() { | ||||||
|  | 		reconnect(ctx) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	//动态用户连接管理 | ||||||
|  | 	utility.SafeGo(func() { | ||||||
|  | 		manageBitgetUserConnections(ctx) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间) | ||||||
|  | 	quit := make(chan os.Signal, 1) | ||||||
|  | 	signal.Notify(quit, os.Interrupt) | ||||||
|  | 	<-quit | ||||||
|  |  | ||||||
|  | 	fmt.Printf("%s Shutdown Server ... \r\n", pkg.GetCurrentTimeStr()) | ||||||
|  | 	logger.Info("Server exiting") | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func defaultInit(db *gorm.DB, ctx context.Context) { | ||||||
|  | 	//初始化 默认redis | ||||||
|  | 	helper.InitDefaultRedis(ext.ExtConfig.Redis.Addr, ext.ExtConfig.Redis.Password, ext.ExtConfig.Redis.Db) | ||||||
|  | 	helper.InitLockRedisConn(ext.ExtConfig.Redis.Addr, ext.ExtConfig.Redis.Password, strconv.Itoa(ext.ExtConfig.Redis.Db)) | ||||||
|  |  | ||||||
|  | 	err := helper.DefaultRedis.Ping() | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Error("初始化redis失败!请检查配置") | ||||||
|  | 		_, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||||||
|  | 		cancel() | ||||||
|  | 	} else { | ||||||
|  | 		logger.Info("redis初始化成功") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	//初始化Bitget服务(行情 + 用户私有) | ||||||
|  | 	logger.Info("=== 启动Bitget完整服务 ===") | ||||||
|  |  | ||||||
|  | 	// 启动Bitget行情服务 | ||||||
|  | 	logger.Info("启动Bitget行情服务...") | ||||||
|  | 	if err := serverinit.BitgetMarketInit(); err != nil { | ||||||
|  | 		logger.Errorf("Bitget行情服务启动失败: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 初始化Bitget用户连接 | ||||||
|  | 	logger.Info("启动Bitget用户服务...") | ||||||
|  | 	initBitgetUserConnections(db, ctx) | ||||||
|  |  | ||||||
|  | 	logger.Info("=== Bitget完整服务已启动 ===") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 初始化Bitget用户连接 | ||||||
|  | func initBitgetUserConnections(db *gorm.DB, ctx context.Context) { | ||||||
|  | 	var list []models.LineApiUser | ||||||
|  | 	err := db.Model(&models.LineApiUser{}).Where("open_status=1 AND exchange_type =?", global.EXCHANGE_BITGET).Find(&list).Error | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Infof("获取Bitget用户api失败: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	logger.Infof("找到 %d 个Bitget用户,开始建立连接...", len(list)) | ||||||
|  | 	for _, item := range list { | ||||||
|  | 		// 遵循私有WebSocket密钥传递规范:外部传入API密钥参数 | ||||||
|  | 		createBitgetUserConnection(item) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 创建Bitget用户连接(遵循密钥传递规范) | ||||||
|  | func createBitgetUserConnection(apiUser models.LineApiUser) { | ||||||
|  | 	logger.Infof("为Bitget用户 %d 建立私有连接", apiUser.Id) | ||||||
|  |  | ||||||
|  | 	// 现货WebSocket (wsType=0) | ||||||
|  | 	spotManager := bitgetservice.GetOrCreateBitgetWebSocketManager( | ||||||
|  | 		0, // 现货 | ||||||
|  | 		apiUser.ApiKey, | ||||||
|  | 		apiUser.ApiSecret, | ||||||
|  | 		apiUser.Passphrase, | ||||||
|  | 		"", // proxyType - 从配置获取 | ||||||
|  | 		"", // proxyAddress - 从配置获取 | ||||||
|  | 	) | ||||||
|  | 	spotManager.Start() | ||||||
|  |  | ||||||
|  | 	// 合约WebSocket (wsType=1) | ||||||
|  | 	futuresManager := bitgetservice.GetOrCreateBitgetWebSocketManager( | ||||||
|  | 		1, // 合约 | ||||||
|  | 		apiUser.ApiKey, | ||||||
|  | 		apiUser.ApiSecret, | ||||||
|  | 		apiUser.Passphrase, | ||||||
|  | 		"", // proxyType | ||||||
|  | 		"", // proxyAddress | ||||||
|  | 	) | ||||||
|  | 	futuresManager.Start() | ||||||
|  |  | ||||||
|  | 	logger.Infof("Bitget用户 %d 私有连接已建立", apiUser.Id) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 动态Bitget用户连接管理 | ||||||
|  | func manageBitgetUserConnections(ctx context.Context) { | ||||||
|  | 	ticker := time.NewTicker(time.Second * 5) | ||||||
|  | 	defer ticker.Stop() | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case <-ctx.Done(): | ||||||
|  | 			logger.Info("Bitget用户连接管理器退出") | ||||||
|  | 			return | ||||||
|  | 		case <-ticker.C: | ||||||
|  | 			deleteKeys, _ := helper.DefaultRedis.GetAllList(rediskey.ApiUserDeleteList) | ||||||
|  | 			activeApis, _ := helper.DefaultRedis.GetAllList(rediskey.ApiUserActiveList) | ||||||
|  |  | ||||||
|  | 			//移除Bitget连接 | ||||||
|  | 			for _, item := range deleteKeys { | ||||||
|  | 				logger.Infof("移除Bitget用户连接: %s", item) | ||||||
|  |  | ||||||
|  | 				// 停止现货WebSocket | ||||||
|  | 				bitgetservice.StopBitgetWebSocketManager(0, item) | ||||||
|  | 				// 停止合约WebSocket | ||||||
|  | 				bitgetservice.StopBitgetWebSocketManager(1, item) | ||||||
|  |  | ||||||
|  | 				if _, err := helper.DefaultRedis.LRem(rediskey.ApiUserDeleteList, item); err != nil { | ||||||
|  | 					logger.Errorf("移除待关闭websocket失败: %v", err) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			//连接Bitget用户websocket | ||||||
|  | 			var apiUser models.LineApiUser | ||||||
|  | 			for _, item := range activeApis { | ||||||
|  | 				if item == "" { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				if err := sonic.Unmarshal([]byte(item), &apiUser); err != nil { | ||||||
|  | 					logger.Errorf("解析用户信息失败: %v", err) | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				// 只处理Bitget用户 | ||||||
|  | 				if apiUser.Id > 0 && apiUser.ExchangeType == global.EXCHANGE_BITGET { | ||||||
|  | 					logger.Info("为Bitget用户 %d 建立连接", apiUser.Id) | ||||||
|  | 					createBitgetUserConnection(apiUser) | ||||||
|  |  | ||||||
|  | 					if _, err := helper.DefaultRedis.LRem(rediskey.ApiUserActiveList, item); err != nil { | ||||||
|  | 						logger.Errorf("删除待触发的apiUser失败: %v", err) | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 定时清理日志 | ||||||
|  | func clearLogJob(db *gorm.DB, ctx context.Context) { | ||||||
|  | 	ticker := time.NewTicker(time.Hour * 1) | ||||||
|  | 	defer ticker.Stop() | ||||||
|  |  | ||||||
|  | 	select { | ||||||
|  | 	case <-ctx.Done(): | ||||||
|  | 		return | ||||||
|  | 	case <-ticker.C: | ||||||
|  | 		fileservice.ClearLogs(db) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 定时重连websocket | ||||||
|  | func reconnect(ctx context.Context) error { | ||||||
|  | 	ticker := time.NewTicker(time.Hour * 1) | ||||||
|  | 	defer ticker.Stop() | ||||||
|  |  | ||||||
|  | 	select { | ||||||
|  | 	case <-ctx.Done(): | ||||||
|  | 		return nil | ||||||
|  | 	case <-ticker.C: | ||||||
|  | 		// 这里可以添加Bitget连接的重连逻辑 | ||||||
|  | 		// 类似于Binance的重连机制 | ||||||
|  | 		logger.Info("检查Bitget连接状态...") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								cmd/cobra.go
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								cmd/cobra.go
									
									
									
									
									
								
							| @ -3,19 +3,19 @@ package cmd | |||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"go-admin/cmd/api" | ||||||
| 	"go-admin/cmd/app" | 	"go-admin/cmd/app" | ||||||
|  | 	"go-admin/cmd/binance" | ||||||
|  | 	"go-admin/cmd/bitget" | ||||||
|  | 	"go-admin/cmd/config" | ||||||
|  | 	"go-admin/cmd/migrate" | ||||||
| 	"go-admin/cmd/usersubscribe" | 	"go-admin/cmd/usersubscribe" | ||||||
|  | 	"go-admin/cmd/version" | ||||||
| 	"go-admin/common/global" | 	"go-admin/common/global" | ||||||
| 	"os" | 	"os" | ||||||
|  |  | ||||||
| 	"github.com/go-admin-team/go-admin-core/sdk/pkg" | 	"github.com/go-admin-team/go-admin-core/sdk/pkg" | ||||||
|  |  | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
|  |  | ||||||
| 	"go-admin/cmd/api" |  | ||||||
| 	"go-admin/cmd/config" |  | ||||||
| 	"go-admin/cmd/migrate" |  | ||||||
| 	"go-admin/cmd/version" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var rootCmd = &cobra.Command{ | var rootCmd = &cobra.Command{ | ||||||
| @ -46,6 +46,8 @@ func tip() { | |||||||
| func init() { | func init() { | ||||||
| 	rootCmd.AddCommand(api.StartCmd) | 	rootCmd.AddCommand(api.StartCmd) | ||||||
| 	rootCmd.AddCommand(usersubscribe.StartCmd) | 	rootCmd.AddCommand(usersubscribe.StartCmd) | ||||||
|  | 	rootCmd.AddCommand(binance.StartCmd) | ||||||
|  | 	rootCmd.AddCommand(bitget.StartCmd) | ||||||
| 	rootCmd.AddCommand(migrate.StartCmd) | 	rootCmd.AddCommand(migrate.StartCmd) | ||||||
| 	rootCmd.AddCommand(version.StartCmd) | 	rootCmd.AddCommand(version.StartCmd) | ||||||
| 	rootCmd.AddCommand(config.StartCmd) | 	rootCmd.AddCommand(config.StartCmd) | ||||||
|  | |||||||
| @ -19,11 +19,13 @@ const ( | |||||||
| 	PCRegisterMobile = "_PCRegisterMobile_%v" // 用户注册时手机key | 	PCRegisterMobile = "_PCRegisterMobile_%v" // 用户注册时手机key | ||||||
| 	PCResetPwdMobile = "_PCResetPwdMobile_%v" // 用户重置密码时手机key | 	PCResetPwdMobile = "_PCResetPwdMobile_%v" // 用户重置密码时手机key | ||||||
|  |  | ||||||
| 	SpotSymbolTicker   = "_SpotSymbolTicker_"    // 现货交易对行情 | 	// SpotSymbolTicker   = "_SpotSymbolTicker_"    // 现货交易对行情 | ||||||
| 	FutSymbolTicker    = "_FutSymbolTicker_"     // 合约交易对行情 | 	// FutSymbolTicker    = "_FutSymbolTicker_"     // 合约交易对行情 | ||||||
| 	PreOrderScriptList = "_ProOrderScriptList_"  // 脚本执行list | 	PreOrderScriptList = "_ProOrderScriptList_" // 脚本执行list | ||||||
| 	PreSpotOrderList   = "_PreSpotOrderList_:%s" // 待触发的现货订单集合{交易所类型 exchange_type} | 	// 待触发的现货订单集合{交易所类型 exchange_type} | ||||||
| 	PreFutOrderList    = "_PreFutOrderList_:%s"  // 待触发的订单集合 {交易所类型 exchange_type} | 	PreSpotOrderList = "_PreSpotOrderList_:%s" | ||||||
|  | 	// 待触发的订单集合 {交易所类型 exchange_type} | ||||||
|  | 	PreFutOrderList = "_PreFutOrderList_:%s" | ||||||
|  |  | ||||||
| 	//策略现货订单集合 {交易所类型 exchange_type} | 	//策略现货订单集合 {交易所类型 exchange_type} | ||||||
| 	StrategySpotOrderList = "strategy_spot_order_list:%s" | 	StrategySpotOrderList = "strategy_spot_order_list:%s" | ||||||
|  | |||||||
							
								
								
									
										107
									
								
								common/factory/exchange_factory.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								common/factory/exchange_factory.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,107 @@ | |||||||
|  | package factory | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"go-admin/common/global" | ||||||
|  | 	"go-admin/common/interfaces" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // ExchangeFactory 交易所工厂 | ||||||
|  | type ExchangeFactory struct { | ||||||
|  | 	clients map[string]interfaces.ExchangeInterface | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewExchangeFactory 创建交易所工厂实例 | ||||||
|  | func NewExchangeFactory() *ExchangeFactory { | ||||||
|  | 	return &ExchangeFactory{ | ||||||
|  | 		clients: make(map[string]interfaces.ExchangeInterface), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetExchange 获取指定交易所的客户端 | ||||||
|  | func (f *ExchangeFactory) GetExchange(exchangeType string) (interfaces.ExchangeInterface, error) { | ||||||
|  | 	exchangeType = strings.ToLower(strings.TrimSpace(exchangeType)) | ||||||
|  |  | ||||||
|  | 	// 检查是否已经创建了该交易所的客户端 | ||||||
|  | 	if client, exists := f.clients[exchangeType]; exists { | ||||||
|  | 		return client, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 根据交易所类型创建对应的客户端 | ||||||
|  | 	var client interfaces.ExchangeInterface | ||||||
|  | 	var err error | ||||||
|  |  | ||||||
|  | 	switch exchangeType { | ||||||
|  | 	case global.EXCHANGE_BINANCE: | ||||||
|  | 		client, err = f.createBinanceClient() | ||||||
|  | 	case global.EXCHANGE_BITGET: | ||||||
|  | 		client, err = f.createBitgetClient() | ||||||
|  | 	case global.EXCHANGE_OKEX: | ||||||
|  | 		return nil, fmt.Errorf("OKEx integration not implemented yet") | ||||||
|  | 	case global.EXCHANGE_GATE: | ||||||
|  | 		return nil, fmt.Errorf("Gate.io integration not implemented yet") | ||||||
|  | 	case global.EXCHANGE_COINBASE: | ||||||
|  | 		return nil, fmt.Errorf("Coinbase integration not implemented yet") | ||||||
|  | 	case global.EXCHANGE_BITFINEX: | ||||||
|  | 		return nil, fmt.Errorf("Bitfinex integration not implemented yet") | ||||||
|  | 	case global.EXCHANGE_BITMEX: | ||||||
|  | 		return nil, fmt.Errorf("BitMEX integration not implemented yet") | ||||||
|  | 	default: | ||||||
|  | 		return nil, fmt.Errorf("unsupported exchange type: %s", exchangeType) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to create %s client: %v", exchangeType, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 缓存客户端实例 | ||||||
|  | 	f.clients[exchangeType] = client | ||||||
|  |  | ||||||
|  | 	return client, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // createBinanceClient 创建Binance客户端包装器 | ||||||
|  | func (f *ExchangeFactory) createBinanceClient() (interfaces.ExchangeInterface, error) { | ||||||
|  | 	// Binance适配器尚未完全实现,暴时返回错误 | ||||||
|  | 	return nil, fmt.Errorf("Binance adapter not fully implemented yet") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // createBitgetClient 创建Bitget客户端 | ||||||
|  | func (f *ExchangeFactory) createBitgetClient() (interfaces.ExchangeInterface, error) { | ||||||
|  | 	// 使用默认空参数创建Bitget客户端(公开接口不需要认证) | ||||||
|  |  | ||||||
|  | 	return nil, fmt.Errorf("Bitget adapter not fully implemented yet") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetSupportedExchanges 获取支持的交易所列表 | ||||||
|  | func (f *ExchangeFactory) GetSupportedExchanges() []string { | ||||||
|  | 	return []string{ | ||||||
|  | 		// global.EXCHANGE_BINANCE, // Binance适配器尚未完全实现 | ||||||
|  | 		global.EXCHANGE_BITGET, | ||||||
|  | 		// 其他交易所可以在未来添加 | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RegisterExchange 注册自定义交易所客户端 | ||||||
|  | func (f *ExchangeFactory) RegisterExchange(exchangeType string, client interfaces.ExchangeInterface) { | ||||||
|  | 	f.clients[strings.ToLower(exchangeType)] = client | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ClearCache 清理缓存的客户端 | ||||||
|  | func (f *ExchangeFactory) ClearCache() { | ||||||
|  | 	for _, client := range f.clients { | ||||||
|  | 		if closer, ok := client.(interface{ Close() error }); ok { | ||||||
|  | 			closer.Close() | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	f.clients = make(map[string]interfaces.ExchangeInterface) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 全局工厂实例 | ||||||
|  | var DefaultExchangeFactory = NewExchangeFactory() | ||||||
|  |  | ||||||
|  | // GetExchangeClient 获取交易所客户端的便捷函数 | ||||||
|  | func GetExchangeClient(exchangeType string) (interfaces.ExchangeInterface, error) { | ||||||
|  | 	return DefaultExchangeFactory.GetExchange(exchangeType) | ||||||
|  | } | ||||||
| @ -3,6 +3,7 @@ package global | |||||||
| //交易所类型字典 | //交易所类型字典 | ||||||
| const ( | const ( | ||||||
| 	EXCHANGE_BINANCE  = "binance" | 	EXCHANGE_BINANCE  = "binance" | ||||||
|  | 	EXCHANGE_BITGET   = "bitget" | ||||||
| 	EXCHANGE_OKEX     = "okex" | 	EXCHANGE_OKEX     = "okex" | ||||||
| 	EXCHANGE_GATE     = "gate" | 	EXCHANGE_GATE     = "gate" | ||||||
| 	EXCHANGE_COINBASE = "coinbase" | 	EXCHANGE_COINBASE = "coinbase" | ||||||
|  | |||||||
| @ -17,8 +17,8 @@ const ( | |||||||
| 	K_SPOT = "k_spot" | 	K_SPOT = "k_spot" | ||||||
| 	//代币-配置 | 	//代币-配置 | ||||||
| 	COIN_DETAIL = "c_spot_detail" | 	COIN_DETAIL = "c_spot_detail" | ||||||
| 	//代币-现货24h涨跌幅 | 	//代币-现货24h涨跌幅 {exchange} | ||||||
| 	COIN_PRICE_CHANGE = "c_spot_priceChange" | 	COIN_PRICE_CHANGE = "c_spot_priceChange:%s" | ||||||
| 	//代币-现货热门排序 | 	//代币-现货热门排序 | ||||||
| 	COIN_HOT_SORT = "c_spot_hot_sort" | 	COIN_HOT_SORT = "c_spot_hot_sort" | ||||||
| 	//代币-现货新币排序 | 	//代币-现货新币排序 | ||||||
|  | |||||||
							
								
								
									
										255
									
								
								common/helper/bitgethttphelper.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								common/helper/bitgethttphelper.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,255 @@ | |||||||
|  | package helper | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"crypto" | ||||||
|  | 	"crypto/hmac" | ||||||
|  | 	"crypto/rand" | ||||||
|  | 	"crypto/rsa" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"crypto/tls" | ||||||
|  | 	"crypto/x509" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"encoding/pem" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"net/http" | ||||||
|  | 	"sort" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // BitgetClient Bitget HTTP客户端 | ||||||
|  | type BitgetClient struct { | ||||||
|  | 	APIKey     string | ||||||
|  | 	APISecret  string | ||||||
|  | 	Passphrase string | ||||||
|  | 	HTTPClient *http.Client | ||||||
|  | 	BaseURL    string | ||||||
|  | 	SignType   string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewBitgetClient 创建新的Bitget客户端 | ||||||
|  | func NewBitgetClient(apiKey, apiSecret, passphrase string, proxyUrl string) (*BitgetClient, error) { | ||||||
|  | 	var proxyType, proxyAddr string | ||||||
|  | 	if proxyUrl != "" { | ||||||
|  | 		if strings.HasPrefix(proxyUrl, "http://") { | ||||||
|  | 			proxyType = "http" | ||||||
|  | 			proxyAddr = proxyUrl | ||||||
|  | 		} else if strings.HasPrefix(proxyUrl, "https://") { | ||||||
|  | 			proxyType = "https" | ||||||
|  | 			proxyAddr = proxyUrl | ||||||
|  | 		} else if strings.HasPrefix(proxyUrl, "socks5://") { | ||||||
|  | 			proxyType = "socks5" | ||||||
|  | 			proxyAddr = proxyUrl | ||||||
|  | 		} else { | ||||||
|  | 			// 默认为http代理 | ||||||
|  | 			proxyType = "http" | ||||||
|  | 			proxyAddr = "http://" + proxyUrl | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	// 创建HTTP客户端 | ||||||
|  | 	client := &http.Client{ | ||||||
|  | 		Transport: &http.Transport{ | ||||||
|  | 			MaxIdleConns:    1000, | ||||||
|  | 			IdleConnTimeout: 10 * time.Second, | ||||||
|  | 			TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 设置代理 | ||||||
|  | 	if proxyAddr != "" { | ||||||
|  | 		err := CreateHtppProxy(proxyType, proxyAddr, client) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &BitgetClient{ | ||||||
|  | 		APIKey:     apiKey, | ||||||
|  | 		APISecret:  apiSecret, | ||||||
|  | 		Passphrase: passphrase, | ||||||
|  | 		HTTPClient: client, | ||||||
|  | 		BaseURL:    "https://api.bitget.com", | ||||||
|  | 		SignType:   "SHA256", | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DoGet 执行GET请求 | ||||||
|  | func (bc *BitgetClient) DoGet(uri string, params map[string]string) ([]byte, error) { | ||||||
|  | 	timestamp := bc.getTimestamp() | ||||||
|  | 	body := bc.buildGetParams(params) | ||||||
|  |  | ||||||
|  | 	sign := bc.sign("GET", uri, body, timestamp) | ||||||
|  | 	requestURL := bc.BaseURL + uri + body | ||||||
|  |  | ||||||
|  | 	request, err := http.NewRequest("GET", requestURL, nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	bc.setHeaders(request, timestamp, sign) | ||||||
|  |  | ||||||
|  | 	response, err := bc.HTTPClient.Do(request) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	defer response.Body.Close() | ||||||
|  |  | ||||||
|  | 	return ioutil.ReadAll(response.Body) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DoPost 执行POST请求 | ||||||
|  | func (bc *BitgetClient) DoPost(uri string, params interface{}) ([]byte, error) { | ||||||
|  | 	timestamp := bc.getTimestamp() | ||||||
|  |  | ||||||
|  | 	var body string | ||||||
|  | 	if params != nil { | ||||||
|  | 		bodyBytes, err := json.Marshal(params) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		body = string(bodyBytes) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sign := bc.sign("POST", uri, body, timestamp) | ||||||
|  | 	requestURL := bc.BaseURL + uri | ||||||
|  |  | ||||||
|  | 	request, err := http.NewRequest("POST", requestURL, strings.NewReader(body)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	bc.setHeaders(request, timestamp, sign) | ||||||
|  |  | ||||||
|  | 	response, err := bc.HTTPClient.Do(request) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	defer response.Body.Close() | ||||||
|  |  | ||||||
|  | 	return ioutil.ReadAll(response.Body) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getTimestamp 获取时间戳 | ||||||
|  | func (bc *BitgetClient) getTimestamp() string { | ||||||
|  | 	return strconv.FormatInt(time.Now().Unix()*1000, 10) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // setHeaders 设置请求头 | ||||||
|  | func (bc *BitgetClient) setHeaders(request *http.Request, timestamp, sign string) { | ||||||
|  | 	request.Header.Add("Content-Type", "application/json") | ||||||
|  | 	request.Header.Add("ACCESS-KEY", bc.APIKey) | ||||||
|  | 	request.Header.Add("ACCESS-SIGN", sign) | ||||||
|  | 	request.Header.Add("ACCESS-TIMESTAMP", timestamp) | ||||||
|  | 	request.Header.Add("ACCESS-PASSPHRASE", bc.Passphrase) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // sign 生成签名 | ||||||
|  | func (bc *BitgetClient) sign(method, requestPath, body, timestamp string) string { | ||||||
|  | 	var payload strings.Builder | ||||||
|  | 	payload.WriteString(timestamp) | ||||||
|  | 	payload.WriteString(method) | ||||||
|  | 	payload.WriteString(requestPath) | ||||||
|  | 	if body != "" && body != "?" { | ||||||
|  | 		payload.WriteString(body) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if bc.SignType == "RSA" { | ||||||
|  | 		return bc.signByRSA(payload.String()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return bc.signByHMAC(payload.String()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // signByHMAC HMAC签名 | ||||||
|  | func (bc *BitgetClient) signByHMAC(payload string) string { | ||||||
|  | 	hash := hmac.New(sha256.New, []byte(bc.APISecret)) | ||||||
|  | 	hash.Write([]byte(payload)) | ||||||
|  | 	return base64.StdEncoding.EncodeToString(hash.Sum(nil)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // signByRSA RSA签名 | ||||||
|  | func (bc *BitgetClient) signByRSA(payload string) string { | ||||||
|  | 	sign, _ := bc.rsaSign([]byte(payload), []byte(bc.APISecret), crypto.SHA256) | ||||||
|  | 	return base64.StdEncoding.EncodeToString(sign) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // rsaSign RSA签名实现 | ||||||
|  | func (bc *BitgetClient) rsaSign(src []byte, priKey []byte, hash crypto.Hash) ([]byte, error) { | ||||||
|  | 	block, _ := pem.Decode(priKey) | ||||||
|  | 	if block == nil { | ||||||
|  | 		return nil, errors.New("key is invalid format") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var pkixPrivateKey interface{} | ||||||
|  | 	var err error | ||||||
|  | 	if block.Type == "RSA PRIVATE KEY" { | ||||||
|  | 		pkixPrivateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) | ||||||
|  | 	} else if block.Type == "PRIVATE KEY" { | ||||||
|  | 		pkixPrivateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	h := hash.New() | ||||||
|  | 	_, err = h.Write(src) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	bytes := h.Sum(nil) | ||||||
|  | 	sign, err := rsa.SignPKCS1v15(rand.Reader, pkixPrivateKey.(*rsa.PrivateKey), hash, bytes) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return sign, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // buildGetParams 构建GET请求参数 | ||||||
|  | func (bc *BitgetClient) buildGetParams(params map[string]string) string { | ||||||
|  | 	if len(params) == 0 { | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  | 	return "?" + bc.sortParams(params) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // sortParams 参数排序 | ||||||
|  | func (bc *BitgetClient) sortParams(params map[string]string) string { | ||||||
|  | 	keys := make([]string, 0, len(params)) | ||||||
|  | 	for k := range params { | ||||||
|  | 		keys = append(keys, k) | ||||||
|  | 	} | ||||||
|  | 	sort.Strings(keys) | ||||||
|  |  | ||||||
|  | 	sorted := make([]string, 0, len(params)) | ||||||
|  | 	for _, k := range keys { | ||||||
|  | 		sorted = append(sorted, k+"="+params[k]) | ||||||
|  | 	} | ||||||
|  | 	return strings.Join(sorted, "&") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SendSpotRequestAuth 发送现货认证请求 | ||||||
|  | func (bc *BitgetClient) SendSpotRequestAuth(endpoint, method string, params map[string]interface{}) ([]byte, *http.Response, error) { | ||||||
|  | 	if method == "GET" { | ||||||
|  | 		paramStr := make(map[string]string) | ||||||
|  | 		for k, v := range params { | ||||||
|  | 			paramStr[k] = fmt.Sprintf("%v", v) | ||||||
|  | 		} | ||||||
|  | 		resp, err := bc.DoGet(endpoint, paramStr) | ||||||
|  | 		return resp, nil, err | ||||||
|  | 	} else { | ||||||
|  | 		resp, err := bc.DoPost(endpoint, params) | ||||||
|  | 		return resp, nil, err | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SendFuturesRequestAuth 发送合约认证请求 | ||||||
|  | func (bc *BitgetClient) SendFuturesRequestAuth(endpoint, method string, params map[string]interface{}) ([]byte, *http.Response, error) { | ||||||
|  | 	return bc.SendSpotRequestAuth(endpoint, method, params) | ||||||
|  | } | ||||||
							
								
								
									
										170
									
								
								common/interfaces/exchange.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								common/interfaces/exchange.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,170 @@ | |||||||
|  | package interfaces | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"go-admin/models" | ||||||
|  |  | ||||||
|  | 	"github.com/shopspring/decimal" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // ExchangeInterface 交易所通用接口抽象层 | ||||||
|  | type ExchangeInterface interface { | ||||||
|  | 	// 基础信息接口 | ||||||
|  | 	GetExchangeName() string | ||||||
|  | 	GetExchangeInfo() (ExchangeInfoResponse, error) | ||||||
|  |  | ||||||
|  | 	// 现货市场接口 | ||||||
|  | 	SpotInterface | ||||||
|  |  | ||||||
|  | 	// 合约接口(如果支持) | ||||||
|  | 	FuturesInterface | ||||||
|  |  | ||||||
|  | 	// WebSocket接口 | ||||||
|  | 	WebSocketInterface | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SpotInterface 现货交易接口 | ||||||
|  | type SpotInterface interface { | ||||||
|  | 	// 市场数据 | ||||||
|  | 	GetSpotTicker24h(symbol string) (models.Ticker24, error) | ||||||
|  | 	GetSpotTickers() ([]models.TradeSet, error) | ||||||
|  | 	// 订单管理 | ||||||
|  | 	PlaceSpotOrder(params SpotOrderParams) (SpotOrderResponse, error) | ||||||
|  | 	CancelSpotOrder(symbol, orderID string) error | ||||||
|  | 	GetSpotOrder(symbol, orderID string) (SpotOrderResponse, error) | ||||||
|  | 	GetSpotOrders(symbol string) ([]SpotOrderResponse, error) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // FuturesInterface 合约交易接口 | ||||||
|  | type FuturesInterface interface { | ||||||
|  | 	// 市场数据 | ||||||
|  | 	GetFuturesTicker24h(symbol string) (models.Ticker24, error) | ||||||
|  | 	GetFuturesTickers() ([]models.TradeSet, error) | ||||||
|  | 	// 订单管理 | ||||||
|  | 	PlaceFuturesOrder(params FuturesOrderParams) (FuturesOrderResponse, error) | ||||||
|  | 	CancelFuturesOrder(symbol, orderID string) error | ||||||
|  | 	GetFuturesOrder(symbol, orderID string) (FuturesOrderResponse, error) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WebSocketInterface WebSocket接口 | ||||||
|  | type WebSocketInterface interface { | ||||||
|  | 	SubscribeSpotTicker(symbols []string, callback func(models.Ticker24, string, string)) error | ||||||
|  | 	SubscribeFuturesTicker(symbols []string, callback func(models.Ticker24, string, string)) error | ||||||
|  | 	SubscribeSpotDepth(symbols []string, callback func(models.DepthBin, string, string)) error | ||||||
|  | 	SubscribeSpotTrades(symbols []string, callback func(models.NewDealPush, string, string)) error | ||||||
|  | 	SubscribeKline(symbols []string, interval string, callback func(models.Kline, int, string, string)) error | ||||||
|  |  | ||||||
|  | 	Close() error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 通用数据结构定义 | ||||||
|  | type ExchangeInfoResponse struct { | ||||||
|  | 	Symbols []SymbolInfo `json:"symbols"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type SymbolInfo struct { | ||||||
|  | 	Symbol      string `json:"symbol"` | ||||||
|  | 	BaseAsset   string `json:"baseAsset"` | ||||||
|  | 	QuoteAsset  string `json:"quoteAsset"` | ||||||
|  | 	Status      string `json:"status"` | ||||||
|  | 	MinQty      string `json:"minQty"` | ||||||
|  | 	MaxQty      string `json:"maxQty"` | ||||||
|  | 	StepSize    string `json:"stepSize"` | ||||||
|  | 	MinPrice    string `json:"minPrice"` | ||||||
|  | 	MaxPrice    string `json:"maxPrice"` | ||||||
|  | 	TickSize    string `json:"tickSize"` | ||||||
|  | 	MinNotional string `json:"minNotional"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 现货订单参数 | ||||||
|  | type SpotOrderParams struct { | ||||||
|  | 	Symbol           string          `json:"symbol"` | ||||||
|  | 	Side             string          `json:"side"` // BUY/SELL | ||||||
|  | 	Type             string          `json:"type"` // MARKET/LIMIT | ||||||
|  | 	Quantity         decimal.Decimal `json:"quantity"` | ||||||
|  | 	Price            decimal.Decimal `json:"price,omitempty"` | ||||||
|  | 	TimeInForce      string          `json:"timeInForce,omitempty"` // GTC/IOC/FOK | ||||||
|  | 	NewClientOrderId string          `json:"newClientOrderId,omitempty"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 合约订单参数 | ||||||
|  | type FuturesOrderParams struct { | ||||||
|  | 	Symbol           string          `json:"symbol"` | ||||||
|  | 	Side             string          `json:"side"` // BUY/SELL | ||||||
|  | 	Type             string          `json:"type"` // MARKET/LIMIT | ||||||
|  | 	Quantity         decimal.Decimal `json:"quantity"` | ||||||
|  | 	Price            decimal.Decimal `json:"price,omitempty"` | ||||||
|  | 	TimeInForce      string          `json:"timeInForce,omitempty"` | ||||||
|  | 	PositionSide     string          `json:"positionSide,omitempty"` // LONG/SHORT/BOTH | ||||||
|  | 	NewClientOrderId string          `json:"newClientOrderId,omitempty"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 响应数据结构 | ||||||
|  | type SpotAccountResponse struct { | ||||||
|  | 	Balances []SpotBalanceResponse `json:"balances"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type SpotBalanceResponse struct { | ||||||
|  | 	Asset  string `json:"asset"` | ||||||
|  | 	Free   string `json:"free"` | ||||||
|  | 	Locked string `json:"locked"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type SpotOrderResponse struct { | ||||||
|  | 	Symbol        string `json:"symbol"` | ||||||
|  | 	OrderId       int64  `json:"orderId"` | ||||||
|  | 	ClientOrderId string `json:"clientOrderId"` | ||||||
|  | 	Price         string `json:"price"` | ||||||
|  | 	OrigQty       string `json:"origQty"` | ||||||
|  | 	ExecutedQty   string `json:"executedQty"` | ||||||
|  | 	Status        string `json:"status"` | ||||||
|  | 	TimeInForce   string `json:"timeInForce"` | ||||||
|  | 	Type          string `json:"type"` | ||||||
|  | 	Side          string `json:"side"` | ||||||
|  | 	Time          int64  `json:"time"` | ||||||
|  | 	UpdateTime    int64  `json:"updateTime"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type FuturesAccountResponse struct { | ||||||
|  | 	TotalWalletBalance string                    `json:"totalWalletBalance"` | ||||||
|  | 	Assets             []FuturesBalanceResponse  `json:"assets"` | ||||||
|  | 	Positions          []FuturesPositionResponse `json:"positions"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type FuturesBalanceResponse struct { | ||||||
|  | 	Asset                  string `json:"asset"` | ||||||
|  | 	WalletBalance          string `json:"walletBalance"` | ||||||
|  | 	UnrealizedProfit       string `json:"unrealizedProfit"` | ||||||
|  | 	MarginBalance          string `json:"marginBalance"` | ||||||
|  | 	MaintMargin            string `json:"maintMargin"` | ||||||
|  | 	InitialMargin          string `json:"initialMargin"` | ||||||
|  | 	PositionInitialMargin  string `json:"positionInitialMargin"` | ||||||
|  | 	OpenOrderInitialMargin string `json:"openOrderInitialMargin"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type FuturesPositionResponse struct { | ||||||
|  | 	Symbol           string `json:"symbol"` | ||||||
|  | 	PositionAmt      string `json:"positionAmt"` | ||||||
|  | 	EntryPrice       string `json:"entryPrice"` | ||||||
|  | 	MarkPrice        string `json:"markPrice"` | ||||||
|  | 	UnrealizedProfit string `json:"unrealizedProfit"` | ||||||
|  | 	LiquidationPrice string `json:"liquidationPrice"` | ||||||
|  | 	Leverage         string `json:"leverage"` | ||||||
|  | 	PositionSide     string `json:"positionSide"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type FuturesOrderResponse struct { | ||||||
|  | 	Symbol        string `json:"symbol"` | ||||||
|  | 	OrderId       int64  `json:"orderId"` | ||||||
|  | 	ClientOrderId string `json:"clientOrderId"` | ||||||
|  | 	Price         string `json:"price"` | ||||||
|  | 	OrigQty       string `json:"origQty"` | ||||||
|  | 	ExecutedQty   string `json:"executedQty"` | ||||||
|  | 	Status        string `json:"status"` | ||||||
|  | 	TimeInForce   string `json:"timeInForce"` | ||||||
|  | 	Type          string `json:"type"` | ||||||
|  | 	Side          string `json:"side"` | ||||||
|  | 	PositionSide  string `json:"positionSide"` | ||||||
|  | 	ReduceOnly    bool   `json:"reduceOnly"` | ||||||
|  | 	Time          int64  `json:"time"` | ||||||
|  | 	UpdateTime    int64  `json:"updateTime"` | ||||||
|  | } | ||||||
| @ -14,7 +14,6 @@ type Extend struct { | |||||||
| 	Redis           RedisConfig     `mapstructure:"redis"` | 	Redis           RedisConfig     `mapstructure:"redis"` | ||||||
| 	ServiceId       int64           `mapstructure:"serviceId"` //雪花算法id(服务id,比如1,2,3等,当多机部署需定义唯一id) | 	ServiceId       int64           `mapstructure:"serviceId"` //雪花算法id(服务id,比如1,2,3等,当多机部署需定义唯一id) | ||||||
| 	EmailConfig     EmailConfig     `mapstructure:"emailConfig"` | 	EmailConfig     EmailConfig     `mapstructure:"emailConfig"` | ||||||
| 	BinanceSet      BinanceConfig   `mapstructure:"binanceSet"` //binance配置 |  | ||||||
| 	Domain          string          //网站域名 | 	Domain          string          //网站域名 | ||||||
| 	UDunConfig      UDunConfig      `mapstructure:"UDunConfig"` | 	UDunConfig      UDunConfig      `mapstructure:"UDunConfig"` | ||||||
| 	ProxyUrl        string          //代理地址 | 	ProxyUrl        string          //代理地址 | ||||||
| @ -58,12 +57,6 @@ type EmailSend struct { | |||||||
| 	To      string `json:"to"`      // 收件地址 | 	To      string `json:"to"`      // 收件地址 | ||||||
| } | } | ||||||
|  |  | ||||||
| // BinanceConfig 币安路由配置 |  | ||||||
| type BinanceConfig struct { |  | ||||||
| 	SpotRestURL string |  | ||||||
| 	FutRestURL  string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type GoToneSmsConfig struct { | type GoToneSmsConfig struct { | ||||||
| 	SenderId      string `json:"sender_id"` | 	SenderId      string `json:"sender_id"` | ||||||
| 	APIEndpoint   string `json:"api_endpoint"` | 	APIEndpoint   string `json:"api_endpoint"` | ||||||
| @ -82,3 +75,14 @@ type UDunConfig struct { | |||||||
| 	UDunKey        string `json:"UDunKey"` | 	UDunKey        string `json:"UDunKey"` | ||||||
| 	CurrServerIp   string `json:"CurrServerIp"` | 	CurrServerIp   string `json:"CurrServerIp"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Binance配置结构体 | ||||||
|  |  */ | ||||||
|  | type BinanceConfig struct { | ||||||
|  | 	ApiKey       string `json:"apiKey" mapstructure:"apiKey"`             // API密钥 | ||||||
|  | 	ApiSecret    string `json:"apiSecret" mapstructure:"apiSecret"`       // API密钥 | ||||||
|  | 	ProxyType    string `json:"proxyType" mapstructure:"proxyType"`       // 代理类型 | ||||||
|  | 	ProxyAddress string `json:"proxyAddress" mapstructure:"proxyAddress"` // 代理地址 | ||||||
|  | 	TestNet      bool   `json:"testNet" mapstructure:"testNet"`           // 是否使用测试网 | ||||||
|  | } | ||||||
|  | |||||||
| @ -5,7 +5,6 @@ import ( | |||||||
| 	"go-admin/models" | 	"go-admin/models" | ||||||
| 	"go-admin/pkg/httputils" | 	"go-admin/pkg/httputils" | ||||||
| 	"go-admin/services/binanceservice" | 	"go-admin/services/binanceservice" | ||||||
| 	"go-admin/services/futureservice" |  | ||||||
|  |  | ||||||
| 	log "github.com/go-admin-team/go-admin-core/logger" | 	log "github.com/go-admin-team/go-admin-core/logger" | ||||||
| 	"github.com/robfig/cron/v3" | 	"github.com/robfig/cron/v3" | ||||||
| @ -33,9 +32,6 @@ func FuturesInit() error { | |||||||
| 	} | 	} | ||||||
| 	log.Info("初始化24h行情-结束") | 	log.Info("初始化24h行情-结束") | ||||||
|  |  | ||||||
| 	log.Info("订阅合约-开始") |  | ||||||
| 	go futureservice.StartBinanceProWs("normal") |  | ||||||
|  |  | ||||||
| 	//定时任务 | 	//定时任务 | ||||||
| 	RunJobs() | 	RunJobs() | ||||||
| 	return nil | 	return nil | ||||||
|  | |||||||
							
								
								
									
										144
									
								
								config/serverinit/bitget_init.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								config/serverinit/bitget_init.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,144 @@ | |||||||
|  | package serverinit | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"go-admin/config" | ||||||
|  | 	"go-admin/models" | ||||||
|  | 	"go-admin/pkg/httputils" | ||||||
|  | 	"go-admin/pkg/utility" | ||||||
|  | 	"go-admin/services/bitgetservice" | ||||||
|  |  | ||||||
|  | 	log "github.com/go-admin-team/go-admin-core/logger" | ||||||
|  | 	"github.com/robfig/cron/v3" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // BitgetInit 初始化Bitget交易所 | ||||||
|  | func BitgetMarketInit() error { | ||||||
|  | 	// 设置代理(如果配置了的话) | ||||||
|  | 	httputils.InitProxy(config.ExtConfig.ProxyUrl) | ||||||
|  |  | ||||||
|  | 	log.Info("初始化Bitget交易对-开始") | ||||||
|  | 	spotData := make(map[string]models.TradeSet, 0) | ||||||
|  | 	futData := make(map[string]models.TradeSet, 0) | ||||||
|  | 	err := bitgetservice.GetAndReloadSymbols(&spotData, 1) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("初始化Bitget交易对失败,可能是网络问题,将跳过初始化:", err) | ||||||
|  | 		// 不返回错误,允许程序继续运行 | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = bitgetservice.GetAndReloadSymbols(&futData, 2) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("初始化Bitget合约交易对失败,可能是网络问题,将跳过初始化:", err) | ||||||
|  | 		// 不返回错误,允许程序继续运行 | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Info("初始化Bitget交易对-结束") | ||||||
|  |  | ||||||
|  | 	log.Info("初始化Bitget 24h行情-开始") | ||||||
|  | 	_, err = bitgetservice.InitSymbolsTicker24h(&spotData, 1) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("初始化Bitget 24h行情失败,可能是网络问题,将跳过初始化:", err) | ||||||
|  | 		// 不返回错误,允许程序继续运行 | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, err = bitgetservice.InitSymbolsTicker24h(&futData, 2) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("初始化Bitget合约24h行情失败,可能是网络问题,将跳过初始化:", err) | ||||||
|  | 		// 不返回错误,允许程序继续运行 | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	log.Info("初始化Bitget 24h行情-结束") | ||||||
|  |  | ||||||
|  | 	//现货行情订阅 | ||||||
|  | 	spotSymbols := []string{} | ||||||
|  | 	for k, _ := range spotData { | ||||||
|  | 		spotSymbols = append(spotSymbols, k) | ||||||
|  | 	} | ||||||
|  | 	spotMaps := utility.SplitSlice(spotSymbols, 50) | ||||||
|  | 	bitgetSpot := bitgetservice.BitgetMarketSpotClient{} | ||||||
|  |  | ||||||
|  | 	for _, v := range spotMaps { | ||||||
|  | 		if err := bitgetSpot.Subscribe(v); err != nil { | ||||||
|  | 			log.Error("订阅Bitget现货行情失败", err) | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	//合约行情订阅 | ||||||
|  | 	futSymbols := []string{} | ||||||
|  | 	for k, _ := range futData { | ||||||
|  | 		futSymbols = append(futSymbols, k) | ||||||
|  | 	} | ||||||
|  | 	futMaps := utility.SplitSlice(futSymbols, 50) | ||||||
|  | 	bitgetFut := bitgetservice.BitgetMarketFutureClient{} | ||||||
|  |  | ||||||
|  | 	for _, v := range futMaps { | ||||||
|  | 		if err := bitgetFut.Subscribe(v); err != nil { | ||||||
|  | 			log.Error("订阅Bitget合约行情失败", err) | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 启动定时任务 | ||||||
|  | 	runBitgetJobs() | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // runBitgetJobs 启动Bitget相关的定时任务 | ||||||
|  | func runBitgetJobs() { | ||||||
|  | 	cronJob := cron.New() | ||||||
|  |  | ||||||
|  | 	// 每小时更新一次交易对信息 | ||||||
|  | 	_, err := cronJob.AddFunc("@every 1h", func() { | ||||||
|  | 		log.Info("执行Bitget交易对更新任务") | ||||||
|  | 		data := make(map[string]models.TradeSet, 0) | ||||||
|  | 		if err := bitgetservice.GetAndReloadSymbols(&data, 1); err != nil { | ||||||
|  | 			log.Error("更新Bitget交易对失败", err) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("添加Bitget交易对更新任务失败", err) | ||||||
|  | 	} else { | ||||||
|  | 		log.Info("添加Bitget交易对更新任务成功") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 启动定时任务 | ||||||
|  | 	cronJob.Start() | ||||||
|  | 	log.Info("Bitget定时任务启动完成") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // CreateBitgetPrivateWebSocket 创建Bitget私有WebSocket连接的示例函数 | ||||||
|  | func CreateBitgetPrivateWebSocket(apiKey, apiSecret, passphrase, proxyType, proxyAddress string) { | ||||||
|  | 	log.Info("创建Bitget私有WebSocket连接 key:", apiKey) | ||||||
|  |  | ||||||
|  | 	// 现货WebSocket (wsType=0) | ||||||
|  | 	spotManager := bitgetservice.GetOrCreateBitgetWebSocketManager( | ||||||
|  | 		0, apiKey, apiSecret, passphrase, proxyType, proxyAddress) | ||||||
|  | 	spotManager.Start() | ||||||
|  |  | ||||||
|  | 	// 合约WebSocket (wsType=1) | ||||||
|  | 	futuresManager := bitgetservice.GetOrCreateBitgetWebSocketManager( | ||||||
|  | 		1, apiKey, apiSecret, passphrase, proxyType, proxyAddress) | ||||||
|  | 	futuresManager.Start() | ||||||
|  |  | ||||||
|  | 	log.Info("Bitget私有WebSocket连接已创建") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // StopBitgetPrivateWebSocket 停止Bitget私有WebSocket连接的示例函数 | ||||||
|  | func StopBitgetPrivateWebSocket(apiKey string) { | ||||||
|  | 	log.Info("停止Bitget私有WebSocket连接 key:", apiKey) | ||||||
|  |  | ||||||
|  | 	// 停止现货WebSocket | ||||||
|  | 	bitgetservice.StopBitgetWebSocketManager(0, apiKey) | ||||||
|  |  | ||||||
|  | 	// 停止合约WebSocket | ||||||
|  | 	bitgetservice.StopBitgetWebSocketManager(1, apiKey) | ||||||
|  |  | ||||||
|  | 	log.Info("Bitget私有WebSocket连接已停止") | ||||||
|  | } | ||||||
| @ -8,7 +8,6 @@ import ( | |||||||
| 	"go-admin/common/helper" | 	"go-admin/common/helper" | ||||||
| 	"go-admin/config" | 	"go-admin/config" | ||||||
| 	"go-admin/pkg/httputils" | 	"go-admin/pkg/httputils" | ||||||
| 	"go-admin/services/binanceservice" |  | ||||||
| 	"go-admin/services/cacheservice" | 	"go-admin/services/cacheservice" | ||||||
| 	"go-admin/services/futureservice" | 	"go-admin/services/futureservice" | ||||||
| 	"go-admin/services/scriptservice" | 	"go-admin/services/scriptservice" | ||||||
| @ -46,54 +45,34 @@ func BusinessInit(db *gorm.DB) { | |||||||
| 		os.Exit(-1) | 		os.Exit(-1) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	//币安 现货交易对 |  | ||||||
| 	SpotCurrencyInit() |  | ||||||
| 	//币安 合约交易对 |  | ||||||
| 	FuturesInit() |  | ||||||
|  |  | ||||||
| 	//第一次初始化交易对价格 |  | ||||||
| 	spotApi := binanceservice.SpotRestApi{} |  | ||||||
| 	futApi := binanceservice.FutRestApi{} |  | ||||||
| 	spotApi.Ticker() |  | ||||||
| 	futApi.Ticker() |  | ||||||
|  |  | ||||||
| 	//现货 订阅 |  | ||||||
| 	SpotSubscribeInit("normal") |  | ||||||
| 	//合约订阅 |  | ||||||
| 	futureservice.StartBinanceProWs("normal") |  | ||||||
| 	//定时任务 | 	//定时任务 | ||||||
| 	RunSpotJobs(db) | 	RunSpotJobs(db) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // 行情监听初始化 | ||||||
|  | func BinanceMarketInit() error { | ||||||
|  | 	//币安 现货交易对(仅数据初始化,不启动WebSocket) | ||||||
|  | 	SpotCurrencyInit() | ||||||
|  | 	//币安 合约交易对(仅数据初始化,不启动WebSocket) | ||||||
|  | 	FuturesInit() | ||||||
|  |  | ||||||
|  | 	//现货 订阅 | ||||||
|  | 	// SpotSubscribeInit("normal") | ||||||
|  | 	//合约订阅 | ||||||
|  | 	futureservice.StartBinanceProWs("normal") | ||||||
|  | 	// log.Info("订阅合约-开始") | ||||||
|  | 	// go futureservice.StartBinanceProWs("normal") | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
| // 开启现货ws监听 | // 开启现货ws监听 | ||||||
| func SpotWsInit() { | func SpotWsInit() { | ||||||
| 	//启动websocket服务 |  | ||||||
| 	//go func() { |  | ||||||
| 	//	err := wsservice.StartServer(":3333") |  | ||||||
| 	//	if err != nil { |  | ||||||
| 	//		log.Info("wsservice.StartServer", zap.Error(err)) |  | ||||||
| 	// |  | ||||||
| 	//		_, cancel := context.WithTimeout(context.Background(), 5*time.Second) |  | ||||||
| 	//		cancel() |  | ||||||
| 	//	} |  | ||||||
| 	//}() |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // 定义任务 | // 定义任务 | ||||||
| func RunSpotJobs(db *gorm.DB) { | func RunSpotJobs(db *gorm.DB) { | ||||||
| 	cronJob := cron.New() | 	cronJob := cron.New() | ||||||
| 	//添加任务 同步交易对行情 |  | ||||||
| 	// spotApi := spotservice.SpotRestApi{} |  | ||||||
| 	// futApi := futureservice.FutRestApi{} |  | ||||||
| 	// _, err := cronJob.AddFunc("@every 10s", func() { |  | ||||||
| 	// 	//BuySellOrderNew(db) |  | ||||||
| 	// spotApi.Ticker() |  | ||||||
| 	// futApi.Ticker() |  | ||||||
| 	// }) |  | ||||||
| 	// if err != nil { |  | ||||||
| 	// 	return |  | ||||||
| 	// } |  | ||||||
|  |  | ||||||
| 	//定时执行脚本任务 | 	//定时执行脚本任务 | ||||||
| 	_, err := cronJob.AddFunc("@every 3s", func() { | 	_, err := cronJob.AddFunc("@every 3s", func() { | ||||||
| 		order := scriptservice.PreOrder{} | 		order := scriptservice.PreOrder{} | ||||||
|  | |||||||
							
								
								
									
										20
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								go.mod
									
									
									
									
									
								
							| @ -1,8 +1,8 @@ | |||||||
| module go-admin | module go-admin | ||||||
|  |  | ||||||
| go 1.21.0 | go 1.24.0 | ||||||
|  |  | ||||||
| toolchain go1.22.5 | toolchain go1.24.2 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/alibaba/sentinel-golang v1.0.4 | 	github.com/alibaba/sentinel-golang v1.0.4 | ||||||
| @ -47,8 +47,8 @@ require ( | |||||||
| 	github.com/vmihailenco/msgpack/v5 v5.4.1 | 	github.com/vmihailenco/msgpack/v5 v5.4.1 | ||||||
| 	github.com/xuri/excelize/v2 v2.9.0 | 	github.com/xuri/excelize/v2 v2.9.0 | ||||||
| 	go.uber.org/zap v1.26.0 | 	go.uber.org/zap v1.26.0 | ||||||
| 	golang.org/x/crypto v0.31.0 | 	golang.org/x/crypto v0.43.0 | ||||||
| 	golang.org/x/net v0.33.0 | 	golang.org/x/net v0.45.0 | ||||||
| 	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df | 	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df | ||||||
| 	gorm.io/driver/mysql v1.5.2 | 	gorm.io/driver/mysql v1.5.2 | ||||||
| 	gorm.io/driver/postgres v1.5.4 | 	gorm.io/driver/postgres v1.5.4 | ||||||
| @ -174,13 +174,13 @@ require ( | |||||||
| 	go.uber.org/multierr v1.10.0 // indirect | 	go.uber.org/multierr v1.10.0 // indirect | ||||||
| 	golang.org/x/arch v0.12.0 // indirect | 	golang.org/x/arch v0.12.0 // indirect | ||||||
| 	golang.org/x/image v0.18.0 // indirect | 	golang.org/x/image v0.18.0 // indirect | ||||||
| 	golang.org/x/mod v0.17.0 // indirect | 	golang.org/x/mod v0.28.0 // indirect | ||||||
| 	golang.org/x/sync v0.10.0 // indirect | 	golang.org/x/sync v0.17.0 // indirect | ||||||
| 	golang.org/x/sys v0.28.0 // indirect | 	golang.org/x/sys v0.37.0 // indirect | ||||||
| 	golang.org/x/term v0.27.0 // indirect | 	golang.org/x/term v0.36.0 // indirect | ||||||
| 	golang.org/x/text v0.21.0 // indirect | 	golang.org/x/text v0.30.0 // indirect | ||||||
| 	golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect | 	golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect | ||||||
| 	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect | 	golang.org/x/tools v0.37.0 // indirect | ||||||
| 	google.golang.org/protobuf v1.36.1 // indirect | 	google.golang.org/protobuf v1.36.1 // indirect | ||||||
| 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect | 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect | ||||||
| 	gopkg.in/yaml.v2 v2.4.0 // indirect | 	gopkg.in/yaml.v2 v2.4.0 // indirect | ||||||
|  | |||||||
| @ -20,6 +20,7 @@ type TradeSet struct { | |||||||
| 	PriceDigit   int     `db:"pricecdigit" json:"pricecdigit"` //价格小数点位数 | 	PriceDigit   int     `db:"pricecdigit" json:"pricecdigit"` //价格小数点位数 | ||||||
| 	Currency     string  `db:"currency" json:"currency"`       //法币 | 	Currency     string  `db:"currency" json:"currency"`       //法币 | ||||||
| 	Coin         string  `db:"coin" json:"coin"`               //币种 | 	Coin         string  `db:"coin" json:"coin"`               //币种 | ||||||
|  | 	Symbol       string  `db:"symbol" json:"symbol"`           //交易对 | ||||||
| 	PriceChange  float64 `db:"pricechange" json:"pricechange"` //价格波动价位 | 	PriceChange  float64 `db:"pricechange" json:"pricechange"` //价格波动价位 | ||||||
| 	MinBuyVal    float64 `db:"minbuyval"`                      //最小下单金额 | 	MinBuyVal    float64 `db:"minbuyval"`                      //最小下单金额 | ||||||
| 	OpenPrice    float64 `db:"openprice"`                      //开盘价格 | 	OpenPrice    float64 `db:"openprice"`                      //开盘价格 | ||||||
|  | |||||||
| @ -31,3 +31,28 @@ type LinePreOrderPositioinDelReq struct { | |||||||
| 	ApiId        int    `json:"apiId"`        //apiid | 	ApiId        int    `json:"apiId"`        //apiid | ||||||
| 	ExchangeType string `json:"exchangeType"` //交易所类型 | 	ExchangeType string `json:"exchangeType"` //交易所类型 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // 待触发加仓单 | ||||||
|  | type AddPositionList struct { | ||||||
|  | 	Id         int             `json:"id"`     //订单id | ||||||
|  | 	Pid        int             `json:"pid"`    //父级id | ||||||
|  | 	MainId     int             `json:"mainId"` //主单Id | ||||||
|  | 	ApiId      int             `json:"apiId"`  //触发账户id | ||||||
|  | 	Symbol     string          `json:"symbol"` //交易对 | ||||||
|  | 	Price      decimal.Decimal `json:"price"`  //触发价 | ||||||
|  | 	Side       string          `json:"side"`   //买卖方向 | ||||||
|  | 	SymbolType int             `json:"type" comment:"交易对类别 1-现货 2-合约"` | ||||||
|  | 	OrderSn    string          `json:"prderSn"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ReduceListItem struct { | ||||||
|  | 	Id      int             `json:"id"` | ||||||
|  | 	ApiId   int             `json:"apiId"` | ||||||
|  | 	MainId  int             `json:"mainId"` | ||||||
|  | 	Pid     int             `json:"pid"` | ||||||
|  | 	Symbol  string          `json:"symbol"` | ||||||
|  | 	Price   decimal.Decimal `json:"price"` | ||||||
|  | 	Side    string          `json:"side"` | ||||||
|  | 	Num     decimal.Decimal `json:"num"` | ||||||
|  | 	OrderSn string          `json:"orderSn"` | ||||||
|  | } | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ import ( | |||||||
| 	"go-admin/models/spot" | 	"go-admin/models/spot" | ||||||
| 	"go-admin/pkg/httputils" | 	"go-admin/pkg/httputils" | ||||||
| 	"go-admin/pkg/utility" | 	"go-admin/pkg/utility" | ||||||
|  | 	"go-admin/services/commonservice" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| @ -126,7 +127,7 @@ func (e SpotRestApi) GetSpotTicker24h(tradeSet *map[string]models.TradeSet) (del | |||||||
| 			log.Error("缓存交易对失败|", item.Symbol, err) | 			log.Error("缓存交易对失败|", item.Symbol, err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		helper.DefaultRedis.AddSortSet(global.COIN_PRICE_CHANGE, symbol.PriceChange, symbol.Coin) | 		helper.DefaultRedis.AddSortSet(fmt.Sprintf(global.COIN_PRICE_CHANGE, global.EXCHANGE_BINANCE), symbol.PriceChange, symbol.Coin) | ||||||
| 	} | 	} | ||||||
| 	return deleteSymbols, nil | 	return deleteSymbols, nil | ||||||
| } | } | ||||||
| @ -169,12 +170,12 @@ type Ticker struct { | |||||||
| 	Price  string `json:"price"` | 	Price  string `json:"price"` | ||||||
| } | } | ||||||
|  |  | ||||||
| func (e SpotRestApi) Ticker() { | // func (e SpotRestApi) Ticker() { | ||||||
| 	tickerApi := fmt.Sprintf("%s%s", binanceRestApi, "/api/v3/ticker/price") | // 	tickerApi := fmt.Sprintf("%s%s", binanceRestApi, "/api/v3/ticker/price") | ||||||
| 	mapData, _ := httputils.NewHttpRequestWithFasthttp("GET", tickerApi, "", map[string]string{}) | // 	mapData, _ := httputils.NewHttpRequestWithFasthttp("GET", tickerApi, "", map[string]string{}) | ||||||
| 	//sonic.Unmarshal(mapData, &tickerData) | // 	//sonic.Unmarshal(mapData, &tickerData) | ||||||
| 	helper.DefaultRedis.SetString(rediskey.SpotSymbolTicker, string(mapData)) | // 	helper.DefaultRedis.SetString(rediskey.SpotSymbolTicker, string(mapData)) | ||||||
| } | // } | ||||||
|  |  | ||||||
| // 循环下单 | // 循环下单 | ||||||
| func (e SpotRestApi) OrderPlaceLoop(db *gorm.DB, params OrderPlacementService, retryCount int) error { | func (e SpotRestApi) OrderPlaceLoop(db *gorm.DB, params OrderPlacementService, retryCount int) error { | ||||||
| @ -452,7 +453,7 @@ func GetApiInfo(apiId int) (DbModels.LineApiUser, error) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	db := GetDBConnection() | 	db := commonservice.GetDBConnection() | ||||||
|  |  | ||||||
| 	if err := db.Model(&api).Where("id =?", apiId).First(&api).Error; err != nil { | 	if err := db.Model(&api).Where("id =?", apiId).First(&api).Error; err != nil { | ||||||
| 		return api, err | 		return api, err | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ import ( | |||||||
| 	"go-admin/common/helper" | 	"go-admin/common/helper" | ||||||
| 	"go-admin/models/positiondto" | 	"go-admin/models/positiondto" | ||||||
| 	"go-admin/pkg/utility" | 	"go-admin/pkg/utility" | ||||||
|  | 	"go-admin/services/commonservice" | ||||||
| 	"go-admin/services/positionservice" | 	"go-admin/services/positionservice" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| @ -16,7 +17,6 @@ import ( | |||||||
| 	"github.com/bytedance/sonic" | 	"github.com/bytedance/sonic" | ||||||
| 	"github.com/go-admin-team/go-admin-core/logger" | 	"github.com/go-admin-team/go-admin-core/logger" | ||||||
| 	log "github.com/go-admin-team/go-admin-core/logger" | 	log "github.com/go-admin-team/go-admin-core/logger" | ||||||
| 	"github.com/go-admin-team/go-admin-core/sdk" |  | ||||||
| 	"github.com/shopspring/decimal" | 	"github.com/shopspring/decimal" | ||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
| @ -105,7 +105,7 @@ func CancelSpotOrder(symbol string, apiUserInfo *DbModels.LineApiUser, side stri | |||||||
| 	} | 	} | ||||||
| 	client := GetClient(apiUserInfo) | 	client := GetClient(apiUserInfo) | ||||||
| 	resp, _, err := client.SendSpotAuth(searchEndpoint, "GET", searchParams) | 	resp, _, err := client.SendSpotAuth(searchEndpoint, "GET", searchParams) | ||||||
| 	db := GetDBConnection() | 	db := commonservice.GetDBConnection() | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if len(resp) > 0 { | 		if len(resp) > 0 { | ||||||
| @ -209,44 +209,6 @@ func (e *AddPosition) GetFutSpotOrderInfo(req dto.ManuallyCover, symbol, orderTy | |||||||
| 	return orderInfo, nil | 	return orderInfo, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // CalculateAmount 计算加仓数量 |  | ||||||
| func (e *AddPosition) CalculateAmount(req dto.ManuallyCover, totalNum, lastPrice decimal.Decimal, amountDigit int, notUsdt bool, symbolInfo DbModels.LineSymbol) (decimal.Decimal, error) { |  | ||||||
| 	var amt decimal.Decimal |  | ||||||
| 	if req.CoverType == 1 { |  | ||||||
| 		decimalValue := utility.StringToDecimal(req.Value).Div(decimal.NewFromInt(100)) |  | ||||||
| 		amt = totalNum.Mul(decimalValue) |  | ||||||
| 	} else { |  | ||||||
| 		decimalValue := utility.StringToDecimal(req.Value) |  | ||||||
| 		if notUsdt { |  | ||||||
| 			tickerSymbolMaps := make([]dto.Ticker, 0) |  | ||||||
| 			tickerSymbol := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() |  | ||||||
| 			if err := sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps); err != nil { |  | ||||||
| 				return decimal.Zero, err |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			var tickerPrice decimal.Decimal |  | ||||||
| 			for _, symbolMap := range tickerSymbolMaps { |  | ||||||
| 				if symbolMap.Symbol == strings.ToUpper(symbolInfo.BaseAsset+"USDT") { |  | ||||||
| 					tickerPrice, _ = decimal.NewFromString(symbolMap.Price) |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			for _, symbolMap := range tickerSymbolMaps { |  | ||||||
| 				if symbolMap.Symbol == strings.ToUpper(symbolInfo.QuoteAsset+"USDT") { |  | ||||||
| 					uTickerPrice, _ := decimal.NewFromString(symbolMap.Price) |  | ||||||
| 					div := tickerPrice.Div(decimal.NewFromInt(1).Div(uTickerPrice)) |  | ||||||
| 					amt = decimalValue.Div(div) |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} else { |  | ||||||
| 			amt = decimalValue.Div(lastPrice) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return amt.Truncate(int32(amountDigit)), nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // 主单平仓删除缓存 | // 主单平仓删除缓存 | ||||||
| // mainOrderId 主单id | // mainOrderId 主单id | ||||||
| // symbolType 1现货 2合约 | // symbolType 1现货 2合约 | ||||||
| @ -256,7 +218,7 @@ func MainClosePositionClearCache(mainId int, symbolType int) { | |||||||
| 		keySpotAddposition := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE) | 		keySpotAddposition := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE) | ||||||
| 		spotStopArray, _ := helper.DefaultRedis.GetAllList(keySpotStop) | 		spotStopArray, _ := helper.DefaultRedis.GetAllList(keySpotStop) | ||||||
| 		spotAddpositionArray, _ := helper.DefaultRedis.GetAllList(keySpotAddposition) | 		spotAddpositionArray, _ := helper.DefaultRedis.GetAllList(keySpotAddposition) | ||||||
| 		var position AddPositionList | 		var position positiondto.AddPositionList | ||||||
| 		var stop dto.StopLossRedisList | 		var stop dto.StopLossRedisList | ||||||
|  |  | ||||||
| 		for _, item := range spotAddpositionArray { | 		for _, item := range spotAddpositionArray { | ||||||
| @ -284,7 +246,7 @@ func MainClosePositionClearCache(mainId int, symbolType int) { | |||||||
| 		keyFutAddposition := fmt.Sprintf(rediskey.FuturesStopLossList, global.EXCHANGE_BINANCE) | 		keyFutAddposition := fmt.Sprintf(rediskey.FuturesStopLossList, global.EXCHANGE_BINANCE) | ||||||
| 		futAddpositionArray, _ := helper.DefaultRedis.GetAllList(keyFutStop) | 		futAddpositionArray, _ := helper.DefaultRedis.GetAllList(keyFutStop) | ||||||
| 		futStopArray, _ := helper.DefaultRedis.GetAllList(keyFutAddposition) | 		futStopArray, _ := helper.DefaultRedis.GetAllList(keyFutAddposition) | ||||||
| 		var position AddPositionList | 		var position positiondto.AddPositionList | ||||||
| 		var stop dto.StopLossRedisList | 		var stop dto.StopLossRedisList | ||||||
|  |  | ||||||
| 		for _, item := range futAddpositionArray { | 		for _, item := range futAddpositionArray { | ||||||
| @ -309,15 +271,6 @@ func MainClosePositionClearCache(mainId int, symbolType int) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // 获取数据库连接 |  | ||||||
| func GetDBConnection() *gorm.DB { |  | ||||||
| 	dbs := sdk.Runtime.GetDb() |  | ||||||
| 	for _, db := range dbs { |  | ||||||
| 		return db |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // 查询订单 | // 查询订单 | ||||||
| func getPreOrder(db *gorm.DB, orderSn interface{}) (*DbModels.LinePreOrder, error) { | func getPreOrder(db *gorm.DB, orderSn interface{}) (*DbModels.LinePreOrder, error) { | ||||||
| 	preOrder := &DbModels.LinePreOrder{} | 	preOrder := &DbModels.LinePreOrder{} | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ import ( | |||||||
| 	"go-admin/common/global" | 	"go-admin/common/global" | ||||||
| 	"go-admin/common/helper" | 	"go-admin/common/helper" | ||||||
| 	"go-admin/models" | 	"go-admin/models" | ||||||
|  | 	"go-admin/models/positiondto" | ||||||
| 	"go-admin/services/cacheservice" | 	"go-admin/services/cacheservice" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| @ -29,7 +30,7 @@ func TestFutureJudge(t *testing.T) { | |||||||
|  |  | ||||||
| 	key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) | 	key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) | ||||||
| 	item := `{"id":50,"apiId":49,"mainId":47,"pid":47,"symbol":"ADAUSDT","price":"0.5936","side":"SELL","num":"12","orderSn":"397913127842217984"}` | 	item := `{"id":50,"apiId":49,"mainId":47,"pid":47,"symbol":"ADAUSDT","price":"0.5936","side":"SELL","num":"12","orderSn":"397913127842217984"}` | ||||||
| 	reduceOrder := ReduceListItem{} | 	reduceOrder := positiondto.ReduceListItem{} | ||||||
| 	futApi := FutRestApi{} | 	futApi := FutRestApi{} | ||||||
| 	setting, err := cacheservice.GetSystemSetting(db) | 	setting, err := cacheservice.GetSystemSetting(db) | ||||||
|  |  | ||||||
|  | |||||||
| @ -5,7 +5,6 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	DbModels "go-admin/app/admin/models" | 	DbModels "go-admin/app/admin/models" | ||||||
| 	"go-admin/app/admin/service/dto" | 	"go-admin/app/admin/service/dto" | ||||||
| 	"go-admin/common/const/rediskey" |  | ||||||
| 	"go-admin/common/global" | 	"go-admin/common/global" | ||||||
| 	"go-admin/common/helper" | 	"go-admin/common/helper" | ||||||
| 	"go-admin/models" | 	"go-admin/models" | ||||||
| @ -13,6 +12,7 @@ import ( | |||||||
| 	"go-admin/models/futuresdto" | 	"go-admin/models/futuresdto" | ||||||
| 	"go-admin/pkg/httputils" | 	"go-admin/pkg/httputils" | ||||||
| 	"go-admin/pkg/utility" | 	"go-admin/pkg/utility" | ||||||
|  | 	"go-admin/services/cacheservice" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| @ -261,18 +261,18 @@ func GetAndReloadSymbols(data *map[string]models.TradeSet) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (e FutRestApi) Ticker() { | // func (e FutRestApi) Ticker() { | ||||||
| 	url := fmt.Sprintf("%s%s", futureApi, "/fapi/v1/ticker/price") | // 	url := fmt.Sprintf("%s%s", futureApi, "/fapi/v1/ticker/price") | ||||||
| 	mapData, err := httputils.NewHttpRequestWithFasthttp("GET", url, "", map[string]string{}) | // 	mapData, err := httputils.NewHttpRequestWithFasthttp("GET", url, "", map[string]string{}) | ||||||
| 	if err != nil { | // 	if err != nil { | ||||||
| 		log.Error("获取合约交易对行情失败:err:", err) | // 		log.Error("获取合约交易对行情失败:err:", err) | ||||||
| 	} | // 	} | ||||||
|  |  | ||||||
| 	if len(mapData) == 0 { | // 	if len(mapData) == 0 { | ||||||
| 		log.Error("获取合约交易对行情失败,或数量为空") | // 		log.Error("获取合约交易对行情失败,或数量为空") | ||||||
| 	} | // 	} | ||||||
| 	helper.DefaultRedis.SetString(rediskey.FutSymbolTicker, string(mapData)) | // 	helper.DefaultRedis.SetString(rediskey.FutSymbolTicker, string(mapData)) | ||||||
| } | // } | ||||||
|  |  | ||||||
| // 获取合约行情 | // 获取合约行情 | ||||||
| func GetTicker24h() ([]futuresdto.FutureTicker24h, error) { | func GetTicker24h() ([]futuresdto.FutureTicker24h, error) { | ||||||
| @ -862,9 +862,10 @@ func (e FutRestApi) CancelBatchFutOrder(apiUserInfo DbModels.LineApiUser, symbol | |||||||
| // quoteSymbol 计价货币  ETHBTC -> 计价货币就是BTC | // quoteSymbol 计价货币  ETHBTC -> 计价货币就是BTC | ||||||
| // totalMoney 兑换的总金额 | // totalMoney 兑换的总金额 | ||||||
| func (e FutRestApi) CalcSymbolExchangeAmt(symbol string, quoteSymbol string, totalMoney decimal.Decimal) decimal.Decimal { | func (e FutRestApi) CalcSymbolExchangeAmt(symbol string, quoteSymbol string, totalMoney decimal.Decimal) decimal.Decimal { | ||||||
| 	tickerSymbol := helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val() | 	// tickerSymbol := helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val() | ||||||
| 	tickerSymbolMaps := make([]dto.Ticker, 0) | 	// tickerSymbolMaps := make([]dto.Ticker, 0) | ||||||
| 	sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps) | 	// sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps) | ||||||
|  | 	tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 1) | ||||||
|  |  | ||||||
| 	targetSymbol := strings.Replace(symbol, quoteSymbol, "USDT", 1) //ETHBTC -》 ETHUSDT | 	targetSymbol := strings.Replace(symbol, quoteSymbol, "USDT", 1) //ETHBTC -》 ETHUSDT | ||||||
| 	key := fmt.Sprintf("%s:%s", global.TICKER_FUTURES, targetSymbol) | 	key := fmt.Sprintf("%s:%s", global.TICKER_FUTURES, targetSymbol) | ||||||
| @ -873,7 +874,7 @@ func (e FutRestApi) CalcSymbolExchangeAmt(symbol string, quoteSymbol string, tot | |||||||
| 	quantity := decimal.Zero | 	quantity := decimal.Zero | ||||||
| 	for _, symbolMap := range tickerSymbolMaps { | 	for _, symbolMap := range tickerSymbolMaps { | ||||||
| 		if symbolMap.Symbol == strings.ToUpper(targetSymbol) { | 		if symbolMap.Symbol == strings.ToUpper(targetSymbol) { | ||||||
| 			targetPrice, _ = decimal.NewFromString(symbolMap.Price) | 			targetPrice, _ = decimal.NewFromString(symbolMap.LastPrice) | ||||||
| 			quantity = totalMoney.Div(targetPrice).Truncate(int32(tradeSet.AmountDigit)) | 			quantity = totalMoney.Div(targetPrice).Truncate(int32(tradeSet.AmountDigit)) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -10,8 +10,11 @@ import ( | |||||||
| 	"go-admin/common/global" | 	"go-admin/common/global" | ||||||
| 	"go-admin/common/helper" | 	"go-admin/common/helper" | ||||||
| 	"go-admin/models" | 	"go-admin/models" | ||||||
|  | 	"go-admin/models/positiondto" | ||||||
| 	"go-admin/pkg/utility" | 	"go-admin/pkg/utility" | ||||||
| 	"go-admin/services/cacheservice" | 	"go-admin/services/cacheservice" | ||||||
|  | 	"go-admin/services/commonservice" | ||||||
|  | 	"go-admin/services/orderservice" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| @ -26,7 +29,7 @@ import ( | |||||||
| */ | */ | ||||||
| func JudgeFuturesPrice(tradeSet models.TradeSet) { | func JudgeFuturesPrice(tradeSet models.TradeSet) { | ||||||
| 	preOrderVal, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.PreFutOrderList, global.EXCHANGE_BINANCE)) | 	preOrderVal, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.PreFutOrderList, global.EXCHANGE_BINANCE)) | ||||||
| 	db := GetDBConnection() | 	db := commonservice.GetDBConnection() | ||||||
|  |  | ||||||
| 	if len(preOrderVal) == 0 { | 	if len(preOrderVal) == 0 { | ||||||
| 		return | 		return | ||||||
| @ -153,7 +156,7 @@ func JudgeFuturesReduce(trade models.TradeSet) { | |||||||
| 	key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) | 	key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) | ||||||
| 	reduceVal, _ := helper.DefaultRedis.GetAllList(key) | 	reduceVal, _ := helper.DefaultRedis.GetAllList(key) | ||||||
|  |  | ||||||
| 	db := GetDBConnection() | 	db := commonservice.GetDBConnection() | ||||||
| 	futApi := FutRestApi{} | 	futApi := FutRestApi{} | ||||||
| 	setting, err := cacheservice.GetSystemSetting(db) | 	setting, err := cacheservice.GetSystemSetting(db) | ||||||
|  |  | ||||||
| @ -162,12 +165,14 @@ func JudgeFuturesReduce(trade models.TradeSet) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	reduceOrder := ReduceListItem{} | 	reduceOrder := positiondto.ReduceListItem{} | ||||||
| 	tradePrice, _ := decimal.NewFromString(trade.LastPrice) | 	tradePrice, _ := decimal.NewFromString(trade.LastPrice) | ||||||
| 	//减仓单减仓策略 | 	//减仓单减仓策略 | ||||||
| 	reduceReduceListKey := fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BINANCE) | 	reduceReduceListKey := fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BINANCE) | ||||||
| 	orderReduceVal, _ := helper.DefaultRedis.HGetAllFields(reduceReduceListKey) | 	orderReduceVal, _ := helper.DefaultRedis.HGetAllFields(reduceReduceListKey) | ||||||
| 	reduceOrderStrategy := dto.LineOrderReduceStrategyResp{} | 	reduceOrderStrategy := dto.LineOrderReduceStrategyResp{} | ||||||
|  | 	orderService := orderservice.OrderService{} | ||||||
|  | 	orderService.Orm = db | ||||||
|  |  | ||||||
| 	for _, item := range orderReduceVal { | 	for _, item := range orderReduceVal { | ||||||
| 		sonic.Unmarshal([]byte(item), &reduceOrderStrategy) | 		sonic.Unmarshal([]byte(item), &reduceOrderStrategy) | ||||||
| @ -193,7 +198,7 @@ func JudgeFuturesReduce(trade models.TradeSet) { | |||||||
| 							return | 							return | ||||||
| 						} | 						} | ||||||
|  |  | ||||||
| 						order, err := CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit) | 						order, err := orderService.CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit) | ||||||
|  |  | ||||||
| 						if err != nil { | 						if err != nil { | ||||||
| 							log.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId) | 							log.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId) | ||||||
| @ -261,8 +266,8 @@ func JudgeFuturesReduce(trade models.TradeSet) { | |||||||
| // 触发合约减仓 | // 触发合约减仓 | ||||||
| // isStrategy 是否是策略减仓 | // isStrategy 是否是策略减仓 | ||||||
| // reduceId 父减仓单id | // reduceId 父减仓单id | ||||||
| func FuturesReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, futApi FutRestApi, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool { | func FuturesReduceTrigger(db *gorm.DB, reduceOrder positiondto.ReduceListItem, futApi FutRestApi, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool { | ||||||
| 	tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 1) | 	tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 2) | ||||||
| 	result := true | 	result := true | ||||||
|  |  | ||||||
| 	if tradeSet.LastPrice == "" { | 	if tradeSet.LastPrice == "" { | ||||||
| @ -362,7 +367,7 @@ func FuturesReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, futApi FutRes | |||||||
| func JudgeFutAddPosition(trade models.TradeSet) { | func JudgeFutAddPosition(trade models.TradeSet) { | ||||||
| 	key := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE) | 	key := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE) | ||||||
| 	preOrderVal, _ := helper.DefaultRedis.GetAllList(key) | 	preOrderVal, _ := helper.DefaultRedis.GetAllList(key) | ||||||
| 	db := GetDBConnection() | 	db := commonservice.GetDBConnection() | ||||||
|  |  | ||||||
| 	if len(preOrderVal) == 0 { | 	if len(preOrderVal) == 0 { | ||||||
| 		return | 		return | ||||||
| @ -370,7 +375,7 @@ func JudgeFutAddPosition(trade models.TradeSet) { | |||||||
|  |  | ||||||
| 	futApi := FutRestApi{} | 	futApi := FutRestApi{} | ||||||
| 	for _, item := range preOrderVal { | 	for _, item := range preOrderVal { | ||||||
| 		preOrder := AddPositionList{} | 		preOrder := positiondto.AddPositionList{} | ||||||
| 		if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil { | 		if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil { | ||||||
| 			log.Error("反序列化失败") | 			log.Error("反序列化失败") | ||||||
| 			continue | 			continue | ||||||
| @ -394,7 +399,7 @@ func JudgeFutAddPosition(trade models.TradeSet) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // 合约加仓触发 | // 合约加仓触发 | ||||||
| func FutAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, futApi FutRestApi) { | func FutAddPositionTrigger(db *gorm.DB, v *positiondto.AddPositionList, item string, futApi FutRestApi) { | ||||||
| 	lock := helper.NewRedisLock(fmt.Sprintf(rediskey.FutTrigger, v.ApiId, v.Symbol), 20, 5, 100*time.Millisecond) | 	lock := helper.NewRedisLock(fmt.Sprintf(rediskey.FutTrigger, v.ApiId, v.Symbol), 20, 5, 100*time.Millisecond) | ||||||
|  |  | ||||||
| 	if ok, err := lock.AcquireWait(context.Background()); err != nil { | 	if ok, err := lock.AcquireWait(context.Background()); err != nil { | ||||||
| @ -404,7 +409,7 @@ func FutAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, futApi | |||||||
| 		defer lock.Release() | 		defer lock.Release() | ||||||
|  |  | ||||||
| 		setting, _ := cacheservice.GetSystemSetting(db) | 		setting, _ := cacheservice.GetSystemSetting(db) | ||||||
| 		tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, v.Symbol, 1) | 		tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, v.Symbol, 2) | ||||||
|  |  | ||||||
| 		if tradeSet.LastPrice == "" { | 		if tradeSet.LastPrice == "" { | ||||||
| 			log.Errorf("合约加仓触发 查询交易对失败 交易对:%s ordersn:%s", v.Symbol, v.OrderSn) | 			log.Errorf("合约加仓触发 查询交易对失败 交易对:%s ordersn:%s", v.Symbol, v.OrderSn) | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ import ( | |||||||
| 	"go-admin/models/positiondto" | 	"go-admin/models/positiondto" | ||||||
| 	"go-admin/pkg/utility" | 	"go-admin/pkg/utility" | ||||||
| 	"go-admin/services/cacheservice" | 	"go-admin/services/cacheservice" | ||||||
|  | 	"go-admin/services/commonservice" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| @ -42,7 +43,7 @@ func ChangeFutureOrder(mapData map[string]interface{}) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// 获取数据库连接 | 	// 获取数据库连接 | ||||||
| 	db := GetDBConnection() | 	db := commonservice.GetDBConnection() | ||||||
| 	if db == nil { | 	if db == nil { | ||||||
| 		logger.Error("合约订单回调失败,无法获取数据库连接") | 		logger.Error("合约订单回调失败,无法获取数据库连接") | ||||||
| 		return | 		return | ||||||
| @ -127,7 +128,7 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 1) | 	tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 2) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logger.Errorf("handleReduceFilled 获取交易对设置失败,订单号:%s", preOrder.OrderSn) | 		logger.Errorf("handleReduceFilled 获取交易对设置失败,订单号:%s", preOrder.OrderSn) | ||||||
| @ -266,7 +267,7 @@ func nextFuturesReduceTrigger(db *gorm.DB, mainId int, totalNum decimal.Decimal, | |||||||
| 	//移除缓存 | 	//移除缓存 | ||||||
| 	key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) | 	key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) | ||||||
| 	vals, _ := helper.DefaultRedis.GetAllList(key) | 	vals, _ := helper.DefaultRedis.GetAllList(key) | ||||||
| 	item := ReduceListItem{} | 	item := positiondto.ReduceListItem{} | ||||||
|  |  | ||||||
| 	for _, val := range vals { | 	for _, val := range vals { | ||||||
| 		sonic.Unmarshal([]byte(val), &item) | 		sonic.Unmarshal([]byte(val), &item) | ||||||
| @ -438,8 +439,8 @@ func removeFutLossAndAddPosition(mainId int, orderSn string) { | |||||||
| 	reduceKey := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) | 	reduceKey := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) | ||||||
| 	reduceVal, _ := helper.DefaultRedis.GetAllList(reduceKey) | 	reduceVal, _ := helper.DefaultRedis.GetAllList(reduceKey) | ||||||
| 	stoploss := dto.StopLossRedisList{} | 	stoploss := dto.StopLossRedisList{} | ||||||
| 	addPosition := AddPositionList{} | 	addPosition := positiondto.AddPositionList{} | ||||||
| 	reduce := ReduceListItem{} | 	reduce := positiondto.ReduceListItem{} | ||||||
| 	//移除减仓后减仓策略 | 	//移除减仓后减仓策略 | ||||||
| 	RemoveReduceReduceCacheByMainId(mainId, 2) | 	RemoveReduceReduceCacheByMainId(mainId, 2) | ||||||
|  |  | ||||||
| @ -483,7 +484,7 @@ func removeFutLossAndAddPosition(mainId int, orderSn string) { | |||||||
| // 处理主单成交,处理止盈、止损、减仓订单 | // 处理主单成交,处理止盈、止损、减仓订单 | ||||||
| func handleFutMainOrderFilled(db *gorm.DB, preOrder *models.LinePreOrder, extOrderId int, first bool) { | func handleFutMainOrderFilled(db *gorm.DB, preOrder *models.LinePreOrder, extOrderId int, first bool) { | ||||||
| 	// 获取交易对配置和API信息 | 	// 获取交易对配置和API信息 | ||||||
| 	tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 1) | 	tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 2) | ||||||
| 	mainId := preOrder.Id | 	mainId := preOrder.Id | ||||||
| 	if err != nil || tradeSet.Coin == "" { | 	if err != nil || tradeSet.Coin == "" { | ||||||
| 		logger.Errorf("获取交易对配置失败, 回调订单号:%s, 错误信息: %v", preOrder.OrderSn, err) | 		logger.Errorf("获取交易对配置失败, 回调订单号:%s, 错误信息: %v", preOrder.OrderSn, err) | ||||||
| @ -522,7 +523,7 @@ func handleFutMainOrderFilled(db *gorm.DB, preOrder *models.LinePreOrder, extOrd | |||||||
| 		keyFutAddpositionKey := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE) | 		keyFutAddpositionKey := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE) | ||||||
|  |  | ||||||
| 		for _, addPositionOrder := range addPositionOrders { | 		for _, addPositionOrder := range addPositionOrders { | ||||||
| 			addPositionData := AddPositionList{ | 			addPositionData := positiondto.AddPositionList{ | ||||||
| 				Id:         addPositionOrder.Id, | 				Id:         addPositionOrder.Id, | ||||||
| 				OrderSn:    addPositionOrder.OrderSn, | 				OrderSn:    addPositionOrder.OrderSn, | ||||||
| 				MainId:     addPositionOrder.MainId, | 				MainId:     addPositionOrder.MainId, | ||||||
| @ -728,7 +729,7 @@ func updateOrderQuantity(db *gorm.DB, order models.LinePreOrder, preOrder *model | |||||||
| // 减仓单 | // 减仓单 | ||||||
| func processFutReduceOrder(order DbModels.LinePreOrder, price, num decimal.Decimal) { | func processFutReduceOrder(order DbModels.LinePreOrder, price, num decimal.Decimal) { | ||||||
| 	key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) | 	key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) | ||||||
| 	item := ReduceListItem{ | 	item := positiondto.ReduceListItem{ | ||||||
| 		Id:      order.Id, | 		Id:      order.Id, | ||||||
| 		ApiId:   order.ApiId, | 		ApiId:   order.ApiId, | ||||||
| 		Pid:     order.Pid, | 		Pid:     order.Pid, | ||||||
| @ -756,7 +757,7 @@ func processFutReduceOrder(order DbModels.LinePreOrder, price, num decimal.Decim | |||||||
| // 处理止盈订单 | // 处理止盈订单 | ||||||
| func processFutTakeProfitOrder(db *gorm.DB, futApi FutRestApi, order models.LinePreOrder, num decimal.Decimal) { | func processFutTakeProfitOrder(db *gorm.DB, futApi FutRestApi, order models.LinePreOrder, num decimal.Decimal) { | ||||||
| 	price, _ := decimal.NewFromString(order.Price) | 	price, _ := decimal.NewFromString(order.Price) | ||||||
| 	tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1) | 	tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 2) | ||||||
|  |  | ||||||
| 	params := FutOrderPlace{ | 	params := FutOrderPlace{ | ||||||
| 		ApiId:            order.ApiId, | 		ApiId:            order.ApiId, | ||||||
| @ -793,7 +794,7 @@ func processFutTakeProfitOrder(db *gorm.DB, futApi FutRestApi, order models.Line | |||||||
| // 处理止损订单 | // 处理止损订单 | ||||||
| // order 止损单 | // order 止损单 | ||||||
| func processFutStopLossOrder(db *gorm.DB, order models.LinePreOrder, price, num decimal.Decimal) error { | func processFutStopLossOrder(db *gorm.DB, order models.LinePreOrder, price, num decimal.Decimal) error { | ||||||
| 	tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1) | 	tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 2) | ||||||
|  |  | ||||||
| 	params := FutOrderPlace{ | 	params := FutOrderPlace{ | ||||||
| 		ApiId:            order.ApiId, | 		ApiId:            order.ApiId, | ||||||
|  | |||||||
| @ -186,19 +186,6 @@ type OpenOrders struct { | |||||||
| 	GoodTillDate            int    `json:"goodTillDate"`            //order pre-set auot cancel time for TIF GTD order | 	GoodTillDate            int    `json:"goodTillDate"`            //order pre-set auot cancel time for TIF GTD order | ||||||
| } | } | ||||||
|  |  | ||||||
| // 待触发加仓单 |  | ||||||
| type AddPositionList struct { |  | ||||||
| 	Id         int             `json:"id"`     //订单id |  | ||||||
| 	Pid        int             `json:"pid"`    //父级id |  | ||||||
| 	MainId     int             `json:"mainId"` //主单Id |  | ||||||
| 	ApiId      int             `json:"apiId"`  //触发账户id |  | ||||||
| 	Symbol     string          `json:"symbol"` //交易对 |  | ||||||
| 	Price      decimal.Decimal `json:"price"`  //触发价 |  | ||||||
| 	Side       string          `json:"side"`   //买卖方向 |  | ||||||
| 	SymbolType int             `json:"type" comment:"交易对类别 1-现货 2-合约"` |  | ||||||
| 	OrderSn    string          `json:"prderSn"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // SpotAccountInfo 现货账户信息 | // SpotAccountInfo 现货账户信息 | ||||||
| type SpotAccountInfo struct { | type SpotAccountInfo struct { | ||||||
| 	MakerCommission  int `json:"makerCommission"` | 	MakerCommission  int `json:"makerCommission"` | ||||||
| @ -227,15 +214,3 @@ type SpotAccountInfo struct { | |||||||
| 	Permissions []string `json:"permissions"` | 	Permissions []string `json:"permissions"` | ||||||
| 	Uid         int      `json:"uid"` | 	Uid         int      `json:"uid"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type ReduceListItem struct { |  | ||||||
| 	Id      int             `json:"id"` |  | ||||||
| 	ApiId   int             `json:"apiId"` |  | ||||||
| 	MainId  int             `json:"mainId"` |  | ||||||
| 	Pid     int             `json:"pid"` |  | ||||||
| 	Symbol  string          `json:"symbol"` |  | ||||||
| 	Price   decimal.Decimal `json:"price"` |  | ||||||
| 	Side    string          `json:"side"` |  | ||||||
| 	Num     decimal.Decimal `json:"num"` |  | ||||||
| 	OrderSn string          `json:"orderSn"` |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -1,15 +1,10 @@ | |||||||
| package binanceservice | package binanceservice | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" |  | ||||||
| 	"go-admin/app/admin/models" | 	"go-admin/app/admin/models" | ||||||
| 	DbModels "go-admin/app/admin/models" | 	DbModels "go-admin/app/admin/models" | ||||||
| 	"go-admin/pkg/utility" |  | ||||||
| 	"go-admin/pkg/utility/snowflakehelper" |  | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"github.com/go-admin-team/go-admin-core/logger" | 	"github.com/go-admin-team/go-admin-core/logger" | ||||||
| 	"github.com/jinzhu/copier" |  | ||||||
| 	"github.com/shopspring/decimal" | 	"github.com/shopspring/decimal" | ||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
| @ -175,69 +170,3 @@ func GetChildTpOrder(db *gorm.DB, pid int) (int, error) { | |||||||
|  |  | ||||||
| 	return int(count), nil | 	return int(count), nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // 创建减仓后减仓单 |  | ||||||
| func CreateReduceReduceOrder(db *gorm.DB, pid int, price, num decimal.Decimal, priceDigit int) (models.LinePreOrder, error) { |  | ||||||
| 	var preOrder models.LinePreOrder |  | ||||||
| 	var result models.LinePreOrder |  | ||||||
| 	var ext models.LinePreOrderExt |  | ||||||
|  |  | ||||||
| 	if err := db.Model(&models.LinePreOrder{}).Preload("Childs").Where("id =? ", pid).First(&preOrder).Error; err != nil { |  | ||||||
| 		return preOrder, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := db.Model(&models.LinePreOrderExt{}).Where("order_id =? ", pid).Find(&ext).Error; err != nil { |  | ||||||
| 		return preOrder, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	copier.Copy(&result, &preOrder) |  | ||||||
|  |  | ||||||
| 	result.Id = 0 |  | ||||||
| 	result.OrderSn = utility.Int64ToString(snowflakehelper.GetOrderId()) |  | ||||||
| 	result.Status = 0 |  | ||||||
| 	result.CreatedAt = time.Now() |  | ||||||
| 	result.TriggerTime = nil |  | ||||||
| 	result.UpdatedAt = time.Now() |  | ||||||
| 	result.BuyPrice = decimal.Zero.String() |  | ||||||
| 	result.Price = price.String() |  | ||||||
| 	result.Num = num.String() |  | ||||||
| 	result.ReduceOrderId = preOrder.Id |  | ||||||
|  |  | ||||||
| 	for index := range result.Childs { |  | ||||||
| 		result.Childs[index].Id = 0 |  | ||||||
| 		result.Childs[index].OrderSn = utility.Int64ToString(snowflakehelper.GetOrderId()) |  | ||||||
| 		result.Childs[index].Status = 0 |  | ||||||
| 		result.Childs[index].CreatedAt = time.Now() |  | ||||||
| 		result.Childs[index].TriggerTime = nil |  | ||||||
| 		result.Childs[index].UpdatedAt = time.Now() |  | ||||||
| 		result.Childs[index].BuyPrice = decimal.Zero.String() |  | ||||||
| 		var pricePercent decimal.Decimal |  | ||||||
|  |  | ||||||
| 		if result.Childs[index].OrderType == 1 && ext.TakeProfitRatio.Cmp(decimal.Zero) > 0 { |  | ||||||
| 			// 减仓单卖出 |  | ||||||
| 			if preOrder.Site == "SELL" { |  | ||||||
| 				pricePercent = decimal.NewFromInt(100).Add(ext.TakeProfitRatio).Div(decimal.NewFromInt(100)) |  | ||||||
| 			} else { |  | ||||||
| 				pricePercent = decimal.NewFromInt(100).Sub(ext.TakeProfitRatio).Div(decimal.NewFromInt(100)) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 		} else if result.Childs[index].OrderType == 2 && ext.StopLossRatio.Cmp(decimal.Zero) > 0 { |  | ||||||
| 			if preOrder.Site == "SELL" { |  | ||||||
| 				pricePercent = decimal.NewFromInt(100).Sub(ext.StopLossRatio).Div(decimal.NewFromInt(100)) |  | ||||||
| 			} else { |  | ||||||
| 				pricePercent = decimal.NewFromInt(100).Add(ext.StopLossRatio).Div(decimal.NewFromInt(100)) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		//重新计算止盈止损价 |  | ||||||
| 		if pricePercent.Cmp(decimal.Zero) > 0 { |  | ||||||
| 			result.Childs[index].Price = price.Mul(pricePercent).Truncate(int32(priceDigit)).String() |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := db.Create(&result).Error; err != nil { |  | ||||||
| 		return result, fmt.Errorf("复制减仓单失败:pid:%d err:%v", pid, err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return result, nil |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ import ( | |||||||
| 	"go-admin/common/global" | 	"go-admin/common/global" | ||||||
| 	"go-admin/common/helper" | 	"go-admin/common/helper" | ||||||
| 	models2 "go-admin/models" | 	models2 "go-admin/models" | ||||||
|  | 	"go-admin/models/positiondto" | ||||||
| 	"go-admin/pkg/utility" | 	"go-admin/pkg/utility" | ||||||
|  |  | ||||||
| 	"github.com/bytedance/sonic" | 	"github.com/bytedance/sonic" | ||||||
| @ -19,7 +20,7 @@ import ( | |||||||
| // 缓存减仓节点和重新生成 | // 缓存减仓节点和重新生成 | ||||||
| // reduceOrder:原始减仓单 | // reduceOrder:原始减仓单 | ||||||
| // reduceOrderStrategy:减仓策略 | // reduceOrderStrategy:减仓策略 | ||||||
| func CacheOrderStrategyAndReCreate(db *gorm.DB, reduceOrder ReduceListItem, symbolType int, tradeSet models2.TradeSet, setting models.LineSystemSetting) error { | func CacheOrderStrategyAndReCreate(db *gorm.DB, reduceOrder positiondto.ReduceListItem, symbolType int, tradeSet models2.TradeSet, setting models.LineSystemSetting) error { | ||||||
| 	reduceOrderStrategy := models.LineOrderReduceStrategy{} | 	reduceOrderStrategy := models.LineOrderReduceStrategy{} | ||||||
| 	var key string | 	var key string | ||||||
|  |  | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ import ( | |||||||
| 	"go-admin/common/global" | 	"go-admin/common/global" | ||||||
| 	"go-admin/common/helper" | 	"go-admin/common/helper" | ||||||
| 	"go-admin/models" | 	"go-admin/models" | ||||||
|  | 	"go-admin/models/positiondto" | ||||||
| 	"go-admin/services/cacheservice" | 	"go-admin/services/cacheservice" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| @ -29,7 +30,7 @@ func TestSpotJudge(t *testing.T) { | |||||||
|  |  | ||||||
| 	key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) | 	key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) | ||||||
| 	item := `{"id":66,"apiId":49,"mainId":63,"pid":63,"symbol":"ADAUSDT","price":"0.5912","side":"SELL","num":"12.6","orderSn":"397919643961917440"}` | 	item := `{"id":66,"apiId":49,"mainId":63,"pid":63,"symbol":"ADAUSDT","price":"0.5912","side":"SELL","num":"12.6","orderSn":"397919643961917440"}` | ||||||
| 	reduceOrder := ReduceListItem{} | 	reduceOrder := positiondto.ReduceListItem{} | ||||||
| 	spotApi := SpotRestApi{} | 	spotApi := SpotRestApi{} | ||||||
| 	setting, err := cacheservice.GetSystemSetting(db) | 	setting, err := cacheservice.GetSystemSetting(db) | ||||||
|  |  | ||||||
|  | |||||||
| @ -10,8 +10,11 @@ import ( | |||||||
| 	"go-admin/common/global" | 	"go-admin/common/global" | ||||||
| 	"go-admin/common/helper" | 	"go-admin/common/helper" | ||||||
| 	"go-admin/models" | 	"go-admin/models" | ||||||
|  | 	"go-admin/models/positiondto" | ||||||
| 	"go-admin/pkg/utility" | 	"go-admin/pkg/utility" | ||||||
| 	"go-admin/services/cacheservice" | 	"go-admin/services/cacheservice" | ||||||
|  | 	"go-admin/services/commonservice" | ||||||
|  | 	"go-admin/services/orderservice" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| @ -27,13 +30,16 @@ import ( | |||||||
| func JudgeSpotPrice(trade models.TradeSet) { | func JudgeSpotPrice(trade models.TradeSet) { | ||||||
| 	key := fmt.Sprintf(rediskey.PreSpotOrderList, global.EXCHANGE_BINANCE) | 	key := fmt.Sprintf(rediskey.PreSpotOrderList, global.EXCHANGE_BINANCE) | ||||||
| 	preOrderVal, _ := helper.DefaultRedis.GetAllList(key) | 	preOrderVal, _ := helper.DefaultRedis.GetAllList(key) | ||||||
| 	db := GetDBConnection() | 	db := commonservice.GetDBConnection() | ||||||
|  |  | ||||||
| 	if len(preOrderVal) == 0 { | 	if len(preOrderVal) == 0 { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	spotApi := SpotRestApi{} | 	spotApi := SpotRestApi{} | ||||||
|  | 	orderService := orderservice.OrderService{} | ||||||
|  | 	orderService.Orm = db | ||||||
|  |  | ||||||
| 	for _, item := range preOrderVal { | 	for _, item := range preOrderVal { | ||||||
| 		preOrder := dto.PreOrderRedisList{} | 		preOrder := dto.PreOrderRedisList{} | ||||||
| 		if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil { | 		if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil { | ||||||
| @ -46,6 +52,18 @@ func JudgeSpotPrice(trade models.TradeSet) { | |||||||
| 			tradePrice, _ := decimal.NewFromString(trade.LastPrice) | 			tradePrice, _ := decimal.NewFromString(trade.LastPrice) | ||||||
| 			//买入 | 			//买入 | ||||||
| 			if strings.ToUpper(preOrder.Site) == "BUY" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 { | 			if strings.ToUpper(preOrder.Site) == "BUY" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 { | ||||||
|  | 				//判断个人websocket是否超时 | ||||||
|  |  | ||||||
|  | 				apiInfo, _ := GetApiInfo(preOrder.ApiId) | ||||||
|  | 				if err := commonservice.JudgeWebsocketTimeout(apiInfo.ApiKey, 1); err != nil { | ||||||
|  | 					log.Errorf("现货行情订阅超时,apiKey:%s err:%v", err) | ||||||
|  |  | ||||||
|  | 					if err1 := orderService.ErrorTrigger(&preOrder, fmt.Sprintf("行情触发失败,err:%v", err)); err1 != nil { | ||||||
|  | 						log.Error("触发失败", err1) | ||||||
|  | 					} | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  |  | ||||||
| 				SpotOrderLock(db, &preOrder, item, spotApi) | 				SpotOrderLock(db, &preOrder, item, spotApi) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @ -140,7 +158,7 @@ func JudgeSpotStopLoss(trade models.TradeSet) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	db := GetDBConnection() | 	db := commonservice.GetDBConnection() | ||||||
| 	spotApi := SpotRestApi{} | 	spotApi := SpotRestApi{} | ||||||
| 	setting, err := cacheservice.GetSystemSetting(db) | 	setting, err := cacheservice.GetSystemSetting(db) | ||||||
|  |  | ||||||
| @ -149,7 +167,7 @@ func JudgeSpotStopLoss(trade models.TradeSet) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, trade.Coin+trade.Currency, 0) | 	tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, trade.Coin+trade.Currency, 1) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("获取交易设置失败") | 		log.Error("获取交易设置失败") | ||||||
| @ -257,7 +275,7 @@ func SpotStopLossTrigger(db *gorm.DB, stopOrder dto.StopLossRedisList, spotApi S | |||||||
| // 判断是否触发现货减仓 | // 判断是否触发现货减仓 | ||||||
| func JudgeSpotReduce(trade models.TradeSet) { | func JudgeSpotReduce(trade models.TradeSet) { | ||||||
| 	key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) | 	key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) | ||||||
| 	db := GetDBConnection() | 	db := commonservice.GetDBConnection() | ||||||
| 	spotApi := SpotRestApi{} | 	spotApi := SpotRestApi{} | ||||||
| 	setting, err := cacheservice.GetSystemSetting(db) | 	setting, err := cacheservice.GetSystemSetting(db) | ||||||
|  |  | ||||||
| @ -266,12 +284,14 @@ func JudgeSpotReduce(trade models.TradeSet) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	reduceOrder := ReduceListItem{} | 	reduceOrder := positiondto.ReduceListItem{} | ||||||
| 	tradePrice, _ := decimal.NewFromString(trade.LastPrice) | 	tradePrice, _ := decimal.NewFromString(trade.LastPrice) | ||||||
| 	//减仓单减仓策略 | 	//减仓单减仓策略 | ||||||
| 	reduceReduceListKey := fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BINANCE) | 	reduceReduceListKey := fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BINANCE) | ||||||
| 	orderReduceVal, _ := helper.DefaultRedis.HGetAllFields(reduceReduceListKey) | 	orderReduceVal, _ := helper.DefaultRedis.HGetAllFields(reduceReduceListKey) | ||||||
| 	reduceOrderStrategy := dto.LineOrderReduceStrategyResp{} | 	reduceOrderStrategy := dto.LineOrderReduceStrategyResp{} | ||||||
|  | 	orderService := orderservice.OrderService{} | ||||||
|  | 	orderService.Orm = db | ||||||
|  |  | ||||||
| 	for _, item := range orderReduceVal { | 	for _, item := range orderReduceVal { | ||||||
| 		sonic.Unmarshal([]byte(item), &reduceOrderStrategy) | 		sonic.Unmarshal([]byte(item), &reduceOrderStrategy) | ||||||
| @ -297,7 +317,7 @@ func JudgeSpotReduce(trade models.TradeSet) { | |||||||
| 							return | 							return | ||||||
| 						} | 						} | ||||||
|  |  | ||||||
| 						order, err := CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit) | 						order, err := orderService.CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit) | ||||||
|  |  | ||||||
| 						if err != nil { | 						if err != nil { | ||||||
| 							log.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId) | 							log.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId) | ||||||
| @ -367,8 +387,8 @@ func JudgeSpotReduce(trade models.TradeSet) { | |||||||
| // 触发现货减仓 | // 触发现货减仓 | ||||||
| // isStrategy 是否是策略减仓单 | // isStrategy 是否是策略减仓单 | ||||||
| // reduceId 策略主减仓单id | // reduceId 策略主减仓单id | ||||||
| func SpotReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, spotApi SpotRestApi, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool { | func SpotReduceTrigger(db *gorm.DB, reduceOrder positiondto.ReduceListItem, spotApi SpotRestApi, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool { | ||||||
| 	tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 0) | 	tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 1) | ||||||
| 	result := true | 	result := true | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -470,7 +490,7 @@ func SpotReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, spotApi SpotRest | |||||||
| func JudgeSpotAddPosition(trade models.TradeSet) { | func JudgeSpotAddPosition(trade models.TradeSet) { | ||||||
| 	key := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE) | 	key := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE) | ||||||
| 	preOrderVal, _ := helper.DefaultRedis.GetAllList(key) | 	preOrderVal, _ := helper.DefaultRedis.GetAllList(key) | ||||||
| 	db := GetDBConnection() | 	db := commonservice.GetDBConnection() | ||||||
|  |  | ||||||
| 	if len(preOrderVal) == 0 { | 	if len(preOrderVal) == 0 { | ||||||
| 		return | 		return | ||||||
| @ -478,7 +498,7 @@ func JudgeSpotAddPosition(trade models.TradeSet) { | |||||||
|  |  | ||||||
| 	spotApi := SpotRestApi{} | 	spotApi := SpotRestApi{} | ||||||
| 	for _, item := range preOrderVal { | 	for _, item := range preOrderVal { | ||||||
| 		preOrder := AddPositionList{} | 		preOrder := positiondto.AddPositionList{} | ||||||
| 		if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil { | 		if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil { | ||||||
| 			log.Error("反序列化失败") | 			log.Error("反序列化失败") | ||||||
| 			continue | 			continue | ||||||
| @ -495,7 +515,7 @@ func JudgeSpotAddPosition(trade models.TradeSet) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func SpotAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, spotApi SpotRestApi) { | func SpotAddPositionTrigger(db *gorm.DB, v *positiondto.AddPositionList, item string, spotApi SpotRestApi) { | ||||||
| 	lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, v.ApiId, v.Symbol), 20, 5, 100*time.Millisecond) | 	lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, v.ApiId, v.Symbol), 20, 5, 100*time.Millisecond) | ||||||
|  |  | ||||||
| 	if ok, err := lock.AcquireWait(context.Background()); err != nil { | 	if ok, err := lock.AcquireWait(context.Background()); err != nil { | ||||||
| @ -505,7 +525,7 @@ func SpotAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, spotAp | |||||||
| 		defer lock.Release() | 		defer lock.Release() | ||||||
|  |  | ||||||
| 		setting, _ := cacheservice.GetSystemSetting(db) | 		setting, _ := cacheservice.GetSystemSetting(db) | ||||||
| 		tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, v.Symbol, 0) | 		tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, v.Symbol, 1) | ||||||
|  |  | ||||||
| 		if tradeSet.LastPrice == "" { | 		if tradeSet.LastPrice == "" { | ||||||
| 			log.Errorf("现货加仓触发 查询交易对失败 交易对:%s ordersn:%s", v.Symbol, v.OrderSn) | 			log.Errorf("现货加仓触发 查询交易对失败 交易对:%s ordersn:%s", v.Symbol, v.OrderSn) | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ import ( | |||||||
| 	"go-admin/models/positiondto" | 	"go-admin/models/positiondto" | ||||||
| 	"go-admin/pkg/utility" | 	"go-admin/pkg/utility" | ||||||
| 	"go-admin/services/cacheservice" | 	"go-admin/services/cacheservice" | ||||||
|  | 	"go-admin/services/commonservice" | ||||||
| 	"go-admin/services/positionservice" | 	"go-admin/services/positionservice" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| @ -44,7 +45,7 @@ func ChangeSpotOrder(mapData map[string]interface{}) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// 获取数据库连接 | 	// 获取数据库连接 | ||||||
| 	db := GetDBConnection() | 	db := commonservice.GetDBConnection() | ||||||
| 	if db == nil { | 	if db == nil { | ||||||
| 		logger.Error("订单回调失败, 无法获取数据库连接") | 		logger.Error("订单回调失败, 无法获取数据库连接") | ||||||
| 		return | 		return | ||||||
| @ -166,7 +167,7 @@ func handleMainReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 0) | 	tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 1) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logger.Errorf("handleMainReduceFilled 获取交易对设置失败,订单号:%s", preOrder.OrderSn) | 		logger.Errorf("handleMainReduceFilled 获取交易对设置失败,订单号:%s", preOrder.OrderSn) | ||||||
| @ -300,7 +301,7 @@ func nextSpotReduceTrigger(db *gorm.DB, mainId int, totalNum decimal.Decimal, tr | |||||||
| 	//移除缓存 | 	//移除缓存 | ||||||
| 	key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) | 	key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) | ||||||
| 	vals, _ := helper.DefaultRedis.GetAllList(key) | 	vals, _ := helper.DefaultRedis.GetAllList(key) | ||||||
| 	item := ReduceListItem{} | 	item := positiondto.ReduceListItem{} | ||||||
|  |  | ||||||
| 	for _, val := range vals { | 	for _, val := range vals { | ||||||
| 		sonic.Unmarshal([]byte(val), &item) | 		sonic.Unmarshal([]byte(val), &item) | ||||||
| @ -553,8 +554,8 @@ func removeSpotLossAndAddPosition(mainId int, orderSn string) { | |||||||
| 	reduceKey := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) | 	reduceKey := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) | ||||||
| 	reduceVal, _ := helper.DefaultRedis.GetAllList(reduceKey) | 	reduceVal, _ := helper.DefaultRedis.GetAllList(reduceKey) | ||||||
| 	stoploss := dto.StopLossRedisList{} | 	stoploss := dto.StopLossRedisList{} | ||||||
| 	addPosition := AddPositionList{} | 	addPosition := positiondto.AddPositionList{} | ||||||
| 	reduce := ReduceListItem{} | 	reduce := positiondto.ReduceListItem{} | ||||||
|  |  | ||||||
| 	//移除减仓后减仓策略 | 	//移除减仓后减仓策略 | ||||||
| 	RemoveReduceReduceCacheByMainId(mainId, 1) | 	RemoveReduceReduceCacheByMainId(mainId, 1) | ||||||
| @ -646,7 +647,7 @@ func handleMainOrderFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) { | |||||||
| 		keySpotAddPosition := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE) | 		keySpotAddPosition := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE) | ||||||
|  |  | ||||||
| 		for _, addPositionOrder := range addPositionOrders { | 		for _, addPositionOrder := range addPositionOrders { | ||||||
| 			addPositionData := AddPositionList{ | 			addPositionData := positiondto.AddPositionList{ | ||||||
| 				Id:         addPositionOrder.Id, | 				Id:         addPositionOrder.Id, | ||||||
| 				OrderSn:    addPositionOrder.OrderSn, | 				OrderSn:    addPositionOrder.OrderSn, | ||||||
| 				MainId:     addPositionOrder.MainId, | 				MainId:     addPositionOrder.MainId, | ||||||
| @ -744,7 +745,7 @@ func updateOrderStatus(db *gorm.DB, preOrder *models.LinePreOrder, status int, r | |||||||
| // fist 首次止盈止损 | // fist 首次止盈止损 | ||||||
| func processTakeProfitAndStopLossOrders(db *gorm.DB, preOrder *models.LinePreOrder, positionData *positiondto.PositionDto, extOrderId int, fist bool) { | func processTakeProfitAndStopLossOrders(db *gorm.DB, preOrder *models.LinePreOrder, positionData *positiondto.PositionDto, extOrderId int, fist bool) { | ||||||
| 	orders := []models.LinePreOrder{} | 	orders := []models.LinePreOrder{} | ||||||
| 	tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 0) | 	tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 1) | ||||||
| 	mainId := preOrder.Id | 	mainId := preOrder.Id | ||||||
|  |  | ||||||
| 	if preOrder.MainId > 0 { | 	if preOrder.MainId > 0 { | ||||||
| @ -855,7 +856,7 @@ func SetPrice(order *models.LinePreOrder, preOrder *models.LinePreOrder, tradeSe | |||||||
| // 现货减仓 | // 现货减仓 | ||||||
| func processSpotReduceOrder(preOrder DbModels.LinePreOrder) { | func processSpotReduceOrder(preOrder DbModels.LinePreOrder) { | ||||||
| 	key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) | 	key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) | ||||||
| 	item := ReduceListItem{ | 	item := positiondto.ReduceListItem{ | ||||||
| 		Id:      preOrder.Id, | 		Id:      preOrder.Id, | ||||||
| 		ApiId:   preOrder.ApiId, | 		ApiId:   preOrder.ApiId, | ||||||
| 		Pid:     preOrder.Pid, | 		Pid:     preOrder.Pid, | ||||||
| @ -882,7 +883,7 @@ func processSpotReduceOrder(preOrder DbModels.LinePreOrder) { | |||||||
|  |  | ||||||
| // 处理止盈订单 | // 处理止盈订单 | ||||||
| func processTakeProfitOrder(db *gorm.DB, spotApi SpotRestApi, order models.LinePreOrder) { | func processTakeProfitOrder(db *gorm.DB, spotApi SpotRestApi, order models.LinePreOrder) { | ||||||
| 	tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 0) | 	tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1) | ||||||
|  |  | ||||||
| 	if tradeSet.Coin == "" { | 	if tradeSet.Coin == "" { | ||||||
| 		logger.Error("获取交易对失败") | 		logger.Error("获取交易对失败") | ||||||
|  | |||||||
							
								
								
									
										447
									
								
								services/bitgetservice/bitget_client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										447
									
								
								services/bitgetservice/bitget_client.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,447 @@ | |||||||
|  | package bitgetservice | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"go-admin/common/global" | ||||||
|  | 	"go-admin/common/helper" | ||||||
|  | 	"go-admin/common/interfaces" | ||||||
|  | 	"go-admin/models" | ||||||
|  | 	"go-admin/pkg/utility" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/bytedance/sonic" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // BitgetClient Bitget交易所客户端实现 | ||||||
|  | type BitgetClient struct { | ||||||
|  | 	httpClient   *helper.BitgetClient | ||||||
|  | 	marketClient *BitgetMarketClient // 公开行情客户端 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewBitgetClient 创建新的Bitget客户端实例 | ||||||
|  | func NewBitgetClient(apiKey, apiSecret, passphrase, proxyUrl string) (*BitgetClient, error) { | ||||||
|  | 	httpClient, err := helper.NewBitgetClient(apiKey, apiSecret, passphrase, proxyUrl) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	client := &BitgetClient{ | ||||||
|  | 		httpClient: httpClient, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 初始化公开行情客户端 | ||||||
|  | 	marketConfig := DefaultMarketConfig() | ||||||
|  | 	client.marketClient = NewBitgetMarketClient(marketConfig) | ||||||
|  |  | ||||||
|  | 	return client, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetExchangeName 获取交易所名称 | ||||||
|  | func (c *BitgetClient) GetExchangeName() string { | ||||||
|  | 	return global.EXCHANGE_BITGET | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetExchangeInfo 获取交易所信息 | ||||||
|  | func (c *BitgetClient) GetExchangeInfo() (interfaces.ExchangeInfoResponse, error) { | ||||||
|  | 	restApi := NewBitgetRestApi() | ||||||
|  |  | ||||||
|  | 	datas, err := restApi.GetSpotSymbols() | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return interfaces.ExchangeInfoResponse{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	symbols := make([]interfaces.SymbolInfo, 0, len(datas)) | ||||||
|  | 	for _, item := range datas { | ||||||
|  | 		symbols = append(symbols, interfaces.SymbolInfo{ | ||||||
|  | 			Symbol:     item.Symbol, | ||||||
|  | 			BaseAsset:  item.BaseCoin, | ||||||
|  | 			QuoteAsset: item.QuoteCoin, | ||||||
|  | 			Status:     item.Status, | ||||||
|  | 			MinQty:     item.MinQty, | ||||||
|  | 			MaxQty:     item.MaxQty, | ||||||
|  | 			StepSize:   strconv.Itoa(item.QuantityDigit), | ||||||
|  | 			TickSize:   strconv.Itoa(item.PriceDigit), | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return interfaces.ExchangeInfoResponse{Symbols: symbols}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetSpotTickers 获取所有现货行情 | ||||||
|  | func (c *BitgetClient) GetSpotTickers() ([]models.TradeSet, error) { | ||||||
|  | 	params := map[string]interface{}{} | ||||||
|  | 	resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/market/tickers", "GET", params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("获取Bitget现货行情列表失败: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var response struct { | ||||||
|  | 		Code string `json:"code"` | ||||||
|  | 		Msg  string `json:"msg"` | ||||||
|  | 		Data []struct { | ||||||
|  | 			Symbol    string `json:"symbol"` | ||||||
|  | 			High24h   string `json:"high24h"` | ||||||
|  | 			Low24h    string `json:"low24h"` | ||||||
|  | 			Close     string `json:"close"` | ||||||
|  | 			Open      string `json:"open"` | ||||||
|  | 			Volume24h string `json:"volume24h"` | ||||||
|  | 			QuoteVol  string `json:"quoteVol"` | ||||||
|  | 			Change24h string `json:"change24h"` | ||||||
|  | 			Change    string `json:"change"` | ||||||
|  | 		} `json:"data"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := sonic.Unmarshal(resp, &response); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("解析Bitget行情列表失败: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if response.Code != "00000" { | ||||||
|  | 		return nil, fmt.Errorf("bitget API错误: %s", response.Msg) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tradeSets := make([]models.TradeSet, 0, len(response.Data)) | ||||||
|  | 	for _, item := range response.Data { | ||||||
|  | 		// 解析交易对,获取基础币种和计价币种 | ||||||
|  | 		var coin, currency string | ||||||
|  | 		if strings.HasSuffix(item.Symbol, "USDT") { | ||||||
|  | 			coin = strings.TrimSuffix(item.Symbol, "USDT") | ||||||
|  | 			currency = "USDT" | ||||||
|  | 		} else if strings.HasSuffix(item.Symbol, "BTC") { | ||||||
|  | 			coin = strings.TrimSuffix(item.Symbol, "BTC") | ||||||
|  | 			currency = "BTC" | ||||||
|  | 		} else if strings.HasSuffix(item.Symbol, "ETH") { | ||||||
|  | 			coin = strings.TrimSuffix(item.Symbol, "ETH") | ||||||
|  | 			currency = "ETH" | ||||||
|  | 		} else { | ||||||
|  | 			continue // 跳过不支持的交易对 | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		tradeSet := models.TradeSet{ | ||||||
|  | 			Coin:        coin, | ||||||
|  | 			Currency:    currency, | ||||||
|  | 			LastPrice:   item.Close, | ||||||
|  | 			HighPrice:   item.High24h, | ||||||
|  | 			LowPrice:    item.Low24h, | ||||||
|  | 			Volume:      item.Volume24h, | ||||||
|  | 			QuoteVolume: item.QuoteVol, | ||||||
|  | 			OpenPrice:   utility.StringAsFloat(item.Open), | ||||||
|  | 			PriceChange: utility.StringAsFloat(item.Change), | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		tradeSets = append(tradeSets, tradeSet) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return tradeSets, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetSpotTicker24h 获取现货24小时行情 | ||||||
|  | func (c *BitgetClient) GetSpotTicker24h(symbol string) (models.Ticker24, error) { | ||||||
|  | 	params := map[string]interface{}{ | ||||||
|  | 		"symbol": symbol, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/market/ticker", "GET", params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return models.Ticker24{}, fmt.Errorf("获取Bitget现货24h行情失败: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var response struct { | ||||||
|  | 		Code string `json:"code"` | ||||||
|  | 		Msg  string `json:"msg"` | ||||||
|  | 		Data struct { | ||||||
|  | 			Symbol    string `json:"symbol"` | ||||||
|  | 			High24h   string `json:"high24h"` | ||||||
|  | 			Low24h    string `json:"low24h"` | ||||||
|  | 			Close     string `json:"close"` | ||||||
|  | 			Open      string `json:"open"` | ||||||
|  | 			Volume24h string `json:"volume24h"` | ||||||
|  | 			QuoteVol  string `json:"quoteVol"` | ||||||
|  | 			Change24h string `json:"change24h"` | ||||||
|  | 			Change    string `json:"change"` | ||||||
|  | 		} `json:"data"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := sonic.Unmarshal(resp, &response); err != nil { | ||||||
|  | 		return models.Ticker24{}, fmt.Errorf("解析Bitget 24h行情失败: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if response.Code != "00000" { | ||||||
|  | 		return models.Ticker24{}, fmt.Errorf("Bitget API错误: %s", response.Msg) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ticker := models.Ticker24{ | ||||||
|  | 		HighPrice:     response.Data.High24h, | ||||||
|  | 		LowPrice:      response.Data.Low24h, | ||||||
|  | 		LastPrice:     response.Data.Close, | ||||||
|  | 		OpenPrice:     response.Data.Open, | ||||||
|  | 		Volume:        response.Data.Volume24h, | ||||||
|  | 		QuoteVolume:   response.Data.QuoteVol, | ||||||
|  | 		ChangePercent: response.Data.Change, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return ticker, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // PlaceSpotOrder 下现货订单 | ||||||
|  | func (c *BitgetClient) PlaceSpotOrder(params interfaces.SpotOrderParams) (interfaces.SpotOrderResponse, error) { | ||||||
|  | 	orderParams := map[string]interface{}{ | ||||||
|  | 		"symbol":    params.Symbol, | ||||||
|  | 		"side":      strings.ToLower(params.Side), | ||||||
|  | 		"orderType": strings.ToLower(params.Type), | ||||||
|  | 		"quantity":  params.Quantity.String(), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if params.Type == "LIMIT" { | ||||||
|  | 		orderParams["price"] = params.Price.String() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if params.TimeInForce != "" { | ||||||
|  | 		orderParams["timeInForce"] = params.TimeInForce | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if params.NewClientOrderId != "" { | ||||||
|  | 		orderParams["clientOrderId"] = params.NewClientOrderId | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/trade/orders", "POST", orderParams) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return interfaces.SpotOrderResponse{}, fmt.Errorf("Bitget现货下单失败: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var response struct { | ||||||
|  | 		Code string `json:"code"` | ||||||
|  | 		Msg  string `json:"msg"` | ||||||
|  | 		Data struct { | ||||||
|  | 			OrderId       string `json:"orderId"` | ||||||
|  | 			ClientOrderId string `json:"clientOrderId"` | ||||||
|  | 		} `json:"data"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := sonic.Unmarshal(resp, &response); err != nil { | ||||||
|  | 		return interfaces.SpotOrderResponse{}, fmt.Errorf("解析Bitget下单响应失败: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if response.Code != "00000" { | ||||||
|  | 		return interfaces.SpotOrderResponse{}, fmt.Errorf("Bitget下单API错误: %s", response.Msg) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	orderId, _ := strconv.ParseInt(response.Data.OrderId, 10, 64) | ||||||
|  |  | ||||||
|  | 	return interfaces.SpotOrderResponse{ | ||||||
|  | 		Symbol:        params.Symbol, | ||||||
|  | 		OrderId:       orderId, | ||||||
|  | 		ClientOrderId: response.Data.ClientOrderId, | ||||||
|  | 		Price:         params.Price.String(), | ||||||
|  | 		OrigQty:       params.Quantity.String(), | ||||||
|  | 		Side:          params.Side, | ||||||
|  | 		Type:          params.Type, | ||||||
|  | 		Status:        "NEW", | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // CancelSpotOrder 取消现货订单 | ||||||
|  | func (c *BitgetClient) CancelSpotOrder(symbol, orderID string) error { | ||||||
|  | 	params := map[string]interface{}{ | ||||||
|  | 		"symbol":  symbol, | ||||||
|  | 		"orderId": orderID, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/trade/cancel-order", "POST", params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("Bitget取消现货订单失败: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var response struct { | ||||||
|  | 		Code string `json:"code"` | ||||||
|  | 		Msg  string `json:"msg"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := sonic.Unmarshal(resp, &response); err != nil { | ||||||
|  | 		return fmt.Errorf("解析Bitget取消订单响应失败: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if response.Code != "00000" { | ||||||
|  | 		return fmt.Errorf("Bitget取消订单API错误: %s", response.Msg) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetSpotOrder 获取现货订单详情 | ||||||
|  | func (c *BitgetClient) GetSpotOrder(symbol, orderID string) (interfaces.SpotOrderResponse, error) { | ||||||
|  | 	// 实现订单查询逻辑 | ||||||
|  | 	return interfaces.SpotOrderResponse{}, fmt.Errorf("GetSpotOrder not implemented yet") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetSpotOrders 获取现货订单列表 | ||||||
|  | func (c *BitgetClient) GetSpotOrders(symbol string) ([]interfaces.SpotOrderResponse, error) { | ||||||
|  | 	params := map[string]interface{}{ | ||||||
|  | 		"symbol": symbol, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/trade/open-orders", "GET", params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("获取Bitget现货订单列表失败: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 这里需要根据实际的Bitget API响应格式来解析 | ||||||
|  | 	return []interfaces.SpotOrderResponse{}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 合约相关接口实现(暂时返回错误) | ||||||
|  | func (c *BitgetClient) GetFuturesTicker24h(symbol string) (models.Ticker24, error) { | ||||||
|  | 	return models.Ticker24{}, fmt.Errorf("futures not implemented yet") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *BitgetClient) GetFuturesTickers() ([]models.TradeSet, error) { | ||||||
|  | 	return nil, fmt.Errorf("futures not implemented yet") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *BitgetClient) PlaceFuturesOrder(params interfaces.FuturesOrderParams) (interfaces.FuturesOrderResponse, error) { | ||||||
|  | 	return interfaces.FuturesOrderResponse{}, fmt.Errorf("futures not implemented yet") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *BitgetClient) CancelFuturesOrder(symbol, orderID string) error { | ||||||
|  | 	return fmt.Errorf("futures not implemented yet") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *BitgetClient) GetFuturesOrder(symbol, orderID string) (interfaces.FuturesOrderResponse, error) { | ||||||
|  | 	return interfaces.FuturesOrderResponse{}, fmt.Errorf("futures not implemented yet") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WebSocket相关接口实现(已重构为使用公开行情客户端) | ||||||
|  | // 注意:这些方法已被重构,现在使用新的公开行情客户端 | ||||||
|  | // 如需私有数据订阅(如订单、持仓),请使用 BitgetWebSocketManager | ||||||
|  |  | ||||||
|  | // 旧的订阅方法(保留兼容性,但建议使用新方法) | ||||||
|  | func (c *BitgetClient) SubscribeSpotTickerLegacy(symbols []string, callback func(models.Ticker24, string, string)) error { | ||||||
|  | 	return fmt.Errorf("已废弃,请使用 SubscribeSpotTicker(symbol, listener) 方法") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *BitgetClient) SubscribeFuturesTickerLegacy(symbols []string, callback func(models.Ticker24, string, string)) error { | ||||||
|  | 	return fmt.Errorf("已废弃,请使用 SubscribeFuturesTicker(symbol, listener) 方法") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *BitgetClient) SubscribeSpotDepthLegacy(symbols []string, callback func(models.DepthBin, string, string)) error { | ||||||
|  | 	return fmt.Errorf("已废弃,请使用 SubscribeSpotDepth(symbol, listener) 方法") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *BitgetClient) SubscribeSpotTradesLegacy(symbols []string, callback func(models.NewDealPush, string, string)) error { | ||||||
|  | 	return fmt.Errorf("已废弃,请使用 SubscribeSpotTrades(symbol, listener) 方法") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *BitgetClient) SubscribeKlineLegacy(symbols []string, interval string, callback func(models.Kline, int, string, string)) error { | ||||||
|  | 	return fmt.Errorf("已废弃,请使用 SubscribeKline(symbol, interval, listener) 方法") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *BitgetClient) Close() error { | ||||||
|  | 	// 关闭HTTP客户端等清理工作 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetMarketProxy 设置市场数据代理 | ||||||
|  | func (c *BitgetClient) SetMarketProxy(proxyType, proxyAddress string) { | ||||||
|  | 	if c.marketClient != nil { | ||||||
|  | 		c.marketClient.SetProxy(proxyType, proxyAddress) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetMarketListeners 设置市场数据监听器 | ||||||
|  | func (c *BitgetClient) SetMarketListeners(msgListener, errorListener OnReceive) { | ||||||
|  | 	if c.marketClient != nil { | ||||||
|  | 		c.marketClient.SetListeners(msgListener, errorListener) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ConnectMarketData 连接市场数据WebSocket | ||||||
|  | func (c *BitgetClient) ConnectMarketData() error { | ||||||
|  | 	if c.marketClient == nil { | ||||||
|  | 		return errors.New("市场数据客户端未初始化") | ||||||
|  | 	} | ||||||
|  | 	return c.marketClient.Connect() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SubscribeMarketData 订阅市场数据(公开行情) | ||||||
|  | func (c *BitgetClient) SubscribeMarketData(subscribeReqs []SubscribeReq, listener OnReceive) error { | ||||||
|  | 	if c.marketClient == nil { | ||||||
|  | 		return errors.New("市场数据客户端未初始化") | ||||||
|  | 	} | ||||||
|  | 	return c.marketClient.Subscribe(subscribeReqs, listener) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // UnsubscribeMarketData 取消订阅市场数据 | ||||||
|  | func (c *BitgetClient) UnsubscribeMarketData(subscribeReqs []SubscribeReq) error { | ||||||
|  | 	if c.marketClient == nil { | ||||||
|  | 		return errors.New("市场数据客户端未初始化") | ||||||
|  | 	} | ||||||
|  | 	return c.marketClient.Unsubscribe(subscribeReqs) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsMarketDataConnected 检查市场数据连接状态 | ||||||
|  | func (c *BitgetClient) IsMarketDataConnected() bool { | ||||||
|  | 	if c.marketClient == nil { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	return c.marketClient.IsConnected() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // CloseMarketData 关闭市场数据连接 | ||||||
|  | func (c *BitgetClient) CloseMarketData() error { | ||||||
|  | 	if c.marketClient == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	return c.marketClient.Close() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SubscribeSpotTicker 订阅现货行情(使用公开行情客户端) | ||||||
|  | func (c *BitgetClient) SubscribeSpotTicker(symbol string, listener OnReceive) error { | ||||||
|  | 	subscribeReq := SubscribeReq{ | ||||||
|  | 		InstType: "SPOT", | ||||||
|  | 		Channel:  "ticker", | ||||||
|  | 		InstId:   symbol, | ||||||
|  | 	} | ||||||
|  | 	return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SubscribeFuturesTicker 订阅合约行情(使用公开行情客户端) | ||||||
|  | func (c *BitgetClient) SubscribeFuturesTicker(symbol string, listener OnReceive) error { | ||||||
|  | 	subscribeReq := SubscribeReq{ | ||||||
|  | 		InstType: "UMCBL", | ||||||
|  | 		Channel:  "ticker", | ||||||
|  | 		InstId:   symbol, | ||||||
|  | 	} | ||||||
|  | 	return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SubscribeSpotDepth 订阅现货深度(使用公开行情客户端) | ||||||
|  | func (c *BitgetClient) SubscribeSpotDepth(symbol string, listener OnReceive) error { | ||||||
|  | 	subscribeReq := SubscribeReq{ | ||||||
|  | 		InstType: "SPOT", | ||||||
|  | 		Channel:  "books", | ||||||
|  | 		InstId:   symbol, | ||||||
|  | 	} | ||||||
|  | 	return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SubscribeSpotTrades 订阅现货成交(使用公开行情客户端) | ||||||
|  | func (c *BitgetClient) SubscribeSpotTrades(symbol string, listener OnReceive) error { | ||||||
|  | 	subscribeReq := SubscribeReq{ | ||||||
|  | 		InstType: "SPOT", | ||||||
|  | 		Channel:  "trade", | ||||||
|  | 		InstId:   symbol, | ||||||
|  | 	} | ||||||
|  | 	return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SubscribeKline 订阅K线数据(使用公开行情客户端) | ||||||
|  | func (c *BitgetClient) SubscribeKline(symbol, interval string, listener OnReceive) error { | ||||||
|  | 	subscribeReq := SubscribeReq{ | ||||||
|  | 		InstType: "SPOT", | ||||||
|  | 		Channel:  "candle" + interval, | ||||||
|  | 		InstId:   symbol, | ||||||
|  | 	} | ||||||
|  | 	return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener) | ||||||
|  | } | ||||||
							
								
								
									
										229
									
								
								services/bitgetservice/bitget_example.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								services/bitgetservice/bitget_example.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,229 @@ | |||||||
|  | package bitgetservice | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	log "github.com/go-admin-team/go-admin-core/logger" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // ExampleBitgetMarketClient 展示如何使用Bitget公开行情客户端 | ||||||
|  | func ExampleBitgetMarketClient() { | ||||||
|  | 	// 1. 创建Bitget客户端(不需要API密钥用于公开数据) | ||||||
|  | 	client := &BitgetClient{} | ||||||
|  |  | ||||||
|  | 	// 初始化公开行情客户端 | ||||||
|  | 	marketConfig := DefaultMarketConfig() | ||||||
|  | 	client.marketClient = NewBitgetMarketClient(marketConfig) | ||||||
|  |  | ||||||
|  | 	// 2. 设置代理(可选) | ||||||
|  | 	// client.SetMarketProxy("socks5", "127.0.0.1:1080") | ||||||
|  |  | ||||||
|  | 	// 3. 设置消息监听器 | ||||||
|  | 	msgListener := func(message string) { | ||||||
|  | 		log.Info("收到市场数据: %s", message) | ||||||
|  | 		// 在这里处理市场数据 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	errorListener := func(message string) { | ||||||
|  | 		log.Error("收到错误消息: %s", message) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	client.SetMarketListeners(msgListener, errorListener) | ||||||
|  |  | ||||||
|  | 	// 4. 连接WebSocket | ||||||
|  | 	if err := client.ConnectMarketData(); err != nil { | ||||||
|  | 		log.Error("连接市场数据失败: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 5. 订阅现货行情 | ||||||
|  | 	tickerListener := func(message string) { | ||||||
|  | 		log.Info("收到BTC-USDT行情: %s", message) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := client.SubscribeSpotTicker("BTCUSDT", tickerListener); err != nil { | ||||||
|  | 		log.Error("订阅现货行情失败: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 6. 订阅深度数据 | ||||||
|  | 	depthListener := func(message string) { | ||||||
|  | 		log.Info("收到BTC-USDT深度: %s", message) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := client.SubscribeSpotDepth("BTCUSDT", depthListener); err != nil { | ||||||
|  | 		log.Error("订阅深度数据失败: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 7. 订阅成交数据 | ||||||
|  | 	tradeListener := func(message string) { | ||||||
|  | 		log.Info("收到BTC-USDT成交: %s", message) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := client.SubscribeSpotTrades("BTCUSDT", tradeListener); err != nil { | ||||||
|  | 		log.Error("订阅成交数据失败: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 8. 订阅K线数据 | ||||||
|  | 	klineListener := func(message string) { | ||||||
|  | 		log.Info("收到BTC-USDT K线: %s", message) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := client.SubscribeKline("BTCUSDT", "1m", klineListener); err != nil { | ||||||
|  | 		log.Error("订阅K线数据失败: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Info("所有订阅完成,开始接收数据...") | ||||||
|  |  | ||||||
|  | 	// 保持连接(在实际应用中,你可能需要其他方式来保持程序运行) | ||||||
|  | 	// select {} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ExampleBitgetPrivateWebSocket 展示如何使用私有WebSocket(订单、持仓等) | ||||||
|  | func ExampleBitgetPrivateWebSocket() { | ||||||
|  | 	// 私有数据订阅仍然使用 BitgetWebSocketManager | ||||||
|  | 	// apiKey := "your_api_key" | ||||||
|  | 	// secretKey := "your_secret_key" | ||||||
|  | 	// passphrase := "your_passphrase" | ||||||
|  |  | ||||||
|  | 	// // 创建私有WebSocket管理器 | ||||||
|  | 	// manager := GetOrCreateBitgetWebSocketManager(apiKey, secretKey, passphrase, "spot") | ||||||
|  |  | ||||||
|  | 	// // 启动连接 | ||||||
|  | 	// if err := manager.Start(); err != nil { | ||||||
|  | 	// 	log.Error("启动私有WebSocket连接失败: %v", err) | ||||||
|  | 	// 	return | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | 	// log.Info("私有WebSocket连接成功,可以接收订单、持仓等私有数据") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ExampleMixedUsage 展示混合使用公开和私有订阅 | ||||||
|  | func ExampleMixedUsage() { | ||||||
|  | 	// apiKey := "your_api_key" | ||||||
|  | 	// secretKey := "your_secret_key" | ||||||
|  | 	// passphrase := "your_passphrase" | ||||||
|  |  | ||||||
|  | 	// // 1. 创建完整的Bitget客户端 | ||||||
|  | 	// client, err := NewBitgetClient(apiKey, secretKey, passphrase) | ||||||
|  | 	// if err != nil { | ||||||
|  | 	// 	log.Error("创建Bitget客户端失败: %v", err) | ||||||
|  | 	// 	return | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | 	// // 2. 设置公开行情监听器 | ||||||
|  | 	// client.SetMarketListeners( | ||||||
|  | 	// 	func(message string) { | ||||||
|  | 	// 		log.Info("公开行情数据: %s", message) | ||||||
|  | 	// 	}, | ||||||
|  | 	// 	func(message string) { | ||||||
|  | 	// 		log.Error("公开行情错误: %s", message) | ||||||
|  | 	// 	}, | ||||||
|  | 	// ) | ||||||
|  |  | ||||||
|  | 	// // 3. 连接公开行情 | ||||||
|  | 	// if err := client.ConnectMarketData(); err != nil { | ||||||
|  | 	// 	log.Error("连接公开行情失败: %v", err) | ||||||
|  | 	// 	return | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | 	// // 4. 订阅公开行情 | ||||||
|  | 	// if err := client.SubscribeSpotTicker("BTCUSDT", func(message string) { | ||||||
|  | 	// 	log.Info("BTC行情: %s", message) | ||||||
|  | 	// }); err != nil { | ||||||
|  | 	// 	log.Error("订阅行情失败: %v", err) | ||||||
|  | 	// 	return | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | 	// // 5. 同时使用私有WebSocket(如果需要) | ||||||
|  | 	// privateManager, err := GetOrCreateBitgetWebSocketManager(apiKey, secretKey, passphrase, "spot") | ||||||
|  | 	// if err != nil { | ||||||
|  | 	// 	log.Error("创建私有WebSocket失败: %v", err) | ||||||
|  | 	// 	return | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | 	// if err := privateManager.Start(); err != nil { | ||||||
|  | 	// 	log.Error("启动私有WebSocket失败: %v", err) | ||||||
|  | 	// 	return | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | 	// log.Info("混合使用设置完成:公开行情 + 私有数据") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ExampleSubscribeMultipleSymbols 展示如何订阅多个交易对 | ||||||
|  | func ExampleSubscribeMultipleSymbols() { | ||||||
|  | 	client := &BitgetClient{} | ||||||
|  | 	marketConfig := DefaultMarketConfig() | ||||||
|  | 	client.marketClient = NewBitgetMarketClient(marketConfig) | ||||||
|  |  | ||||||
|  | 	// 连接 | ||||||
|  | 	if err := client.ConnectMarketData(); err != nil { | ||||||
|  | 		log.Error("连接失败: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 批量订阅多个交易对的行情 | ||||||
|  | 	symbols := []string{"BTCUSDT", "ETHUSDT", "ADAUSDT", "DOTUSDT"} | ||||||
|  |  | ||||||
|  | 	for _, symbol := range symbols { | ||||||
|  | 		listener := func(sym string) OnReceive { | ||||||
|  | 			return func(message string) { | ||||||
|  | 				log.Info("%s 行情更新: %s", sym, message) | ||||||
|  | 			} | ||||||
|  | 		}(symbol) | ||||||
|  |  | ||||||
|  | 		if err := client.SubscribeSpotTicker(symbol, listener); err != nil { | ||||||
|  | 			log.Error("订阅 %s 行情失败: %v", symbol, err) | ||||||
|  | 		} else { | ||||||
|  | 			log.Info("成功订阅 %s 行情", symbol) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Info("批量订阅完成") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ExampleCustomSubscription 展示如何使用自定义订阅 | ||||||
|  | func ExampleCustomSubscription() { | ||||||
|  | 	client := &BitgetClient{} | ||||||
|  | 	marketConfig := DefaultMarketConfig() | ||||||
|  | 	client.marketClient = NewBitgetMarketClient(marketConfig) | ||||||
|  |  | ||||||
|  | 	if err := client.ConnectMarketData(); err != nil { | ||||||
|  | 		log.Error("连接失败: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 自定义订阅请求 | ||||||
|  | 	customSubscriptions := []SubscribeReq{ | ||||||
|  | 		{ | ||||||
|  | 			InstType: "SPOT", | ||||||
|  | 			Channel:  "ticker", | ||||||
|  | 			InstId:   "BTCUSDT", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			InstType: "SPOT", | ||||||
|  | 			Channel:  "books", | ||||||
|  | 			InstId:   "ETHUSDT", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			InstType: "UMCBL", // 合约 | ||||||
|  | 			Channel:  "ticker", | ||||||
|  | 			InstId:   "BTCUSDT", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 统一的消息处理器 | ||||||
|  | 	messageHandler := func(message string) { | ||||||
|  | 		log.Info("自定义订阅数据: %s", message) | ||||||
|  | 		// 在这里根据消息内容进行不同的处理 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 批量订阅 | ||||||
|  | 	if err := client.SubscribeMarketData(customSubscriptions, messageHandler); err != nil { | ||||||
|  | 		log.Error("自定义订阅失败: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Info("自定义订阅完成") | ||||||
|  | } | ||||||
							
								
								
									
										326
									
								
								services/bitgetservice/bitget_judge.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										326
									
								
								services/bitgetservice/bitget_judge.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,326 @@ | |||||||
|  | package bitgetservice | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	DbModels "go-admin/app/admin/models" | ||||||
|  | 	"go-admin/models" | ||||||
|  | 	"go-admin/models/positiondto" | ||||||
|  |  | ||||||
|  | 	"gorm.io/gorm" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // JudgeCreateOrder 判断是否需要创建订单 | ||||||
|  | // symbolType 1:现货 2:合约 | ||||||
|  | func JudgeCreateOrder(trade models.TradeSet, symbolType int) error { | ||||||
|  | 	// var key string | ||||||
|  |  | ||||||
|  | 	// if symbolType == 1 { | ||||||
|  | 	// 	key = fmt.Sprintf(rediskey.PreSpotOrderList, global.EXCHANGE_BITGET) | ||||||
|  | 	// } else if symbolType == 2 { | ||||||
|  | 	// 	key = fmt.Sprintf(rediskey.PreFutOrderList, global.EXCHANGE_BITGET) | ||||||
|  | 	// } else { | ||||||
|  | 	// 	return fmt.Errorf("不支持的交易类型") | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | 	// preOrderVal, _ := helper.DefaultRedis.GetAllList(key) | ||||||
|  | 	// db := commonservice.GetDBConnection() | ||||||
|  |  | ||||||
|  | 	// if len(preOrderVal) == 0 { | ||||||
|  | 	// 	return fmt.Errorf("预订单列表为空") | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | 	// // spotApi := SpotRestApi{} | ||||||
|  | 	// for _, item := range preOrderVal { | ||||||
|  | 	// 	preOrder := dto.PreOrderRedisList{} | ||||||
|  | 	// 	if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil { | ||||||
|  | 	// 		logger.Error("反序列化失败") | ||||||
|  | 	// 		continue | ||||||
|  | 	// 	} | ||||||
|  |  | ||||||
|  | 	// 	if preOrder.Symbol == trade.Coin+trade.Currency { | ||||||
|  | 	// 		orderPrice, _ := decimal.NewFromString(preOrder.Price) | ||||||
|  | 	// 		tradePrice, _ := decimal.NewFromString(trade.LastPrice) | ||||||
|  | 	// 		//买入 | ||||||
|  | 	// 		if strings.ToUpper(preOrder.Site) == "BUY" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 { | ||||||
|  | 	// 			// SpotOrderLock(db, &preOrder, item, spotApi) | ||||||
|  | 	// 		} | ||||||
|  | 	// 	} | ||||||
|  | 	// } | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 判断是否触发加仓 | ||||||
|  | func JudgeAddPositionOrder(trade models.TradeSet, symbolType int) error { | ||||||
|  | 	// var key string | ||||||
|  |  | ||||||
|  | 	// if symbolType == 1 { | ||||||
|  | 	// 	key = fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BITGET) | ||||||
|  | 	// } else if symbolType == 2 { | ||||||
|  | 	// 	key = fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BITGET) | ||||||
|  | 	// } else { | ||||||
|  | 	// 	return fmt.Errorf("不支持的交易类型") | ||||||
|  | 	// } | ||||||
|  | 	// preOrderVal, _ := helper.DefaultRedis.GetAllList(key) | ||||||
|  | 	// db := commonservice.GetDBConnection() | ||||||
|  |  | ||||||
|  | 	// if len(preOrderVal) == 0 { | ||||||
|  | 	// 	return fmt.Errorf("预订单列表为空") | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | 	// // spotApi := SpotRestApi{} | ||||||
|  | 	// for _, item := range preOrderVal { | ||||||
|  | 	// 	preOrder := positiondto.AddPositionList{} | ||||||
|  | 	// 	if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil { | ||||||
|  | 	// 		logger.Error("反序列化失败") | ||||||
|  | 	// 		continue | ||||||
|  | 	// 	} | ||||||
|  |  | ||||||
|  | 	// 	if preOrder.Symbol == trade.Coin+trade.Currency { | ||||||
|  | 	// 		orderPrice := preOrder.Price | ||||||
|  | 	// 		tradePrice, _ := decimal.NewFromString(trade.LastPrice) | ||||||
|  | 	// 		//买入 | ||||||
|  | 	// 		if strings.ToUpper(preOrder.Side) == "BUY" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 { | ||||||
|  | 	// 			// SpotAddPositionTrigger(db, &preOrder, item, spotApi) | ||||||
|  | 	// 		} | ||||||
|  | 	// 	} | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // JudgeReduceOrder 判断是否需要减仓 | ||||||
|  | func JudgeReduceOrder(trade models.TradeSet, symbolType int) error { | ||||||
|  | 	// var key string | ||||||
|  | 	// var reduceReduceListKey string | ||||||
|  |  | ||||||
|  | 	// if symbolType == 1 { | ||||||
|  | 	// 	key = fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BITGET) | ||||||
|  | 	// 	reduceReduceListKey = fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BITGET) | ||||||
|  | 	// } else if symbolType == 2 { | ||||||
|  | 	// 	key = fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BITGET) | ||||||
|  | 	// 	reduceReduceListKey = fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BITGET) | ||||||
|  | 	// } else { | ||||||
|  | 	// 	return fmt.Errorf("不支持的交易类型") | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | 	// db := commonservice.GetDBConnection() | ||||||
|  | 	// // spotApi := SpotRestApi{} | ||||||
|  | 	// setting, err := cacheservice.GetSystemSetting(db) | ||||||
|  |  | ||||||
|  | 	// if err != nil { | ||||||
|  | 	// 	logger.Error("获取系统设置失败") | ||||||
|  | 	// 	return errors.New("获取系统设置失败") | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | 	// reduceOrder := positiondto.ReduceListItem{} | ||||||
|  | 	// tradePrice, _ := decimal.NewFromString(trade.LastPrice) | ||||||
|  | 	// //减仓单减仓策略 | ||||||
|  | 	// orderReduceVal, _ := helper.DefaultRedis.HGetAllFields(reduceReduceListKey) | ||||||
|  | 	// reduceOrderStrategy := dto.LineOrderReduceStrategyResp{} | ||||||
|  | 	// orderService := orderservice.OrderService{} | ||||||
|  | 	// orderService.Orm = db | ||||||
|  |  | ||||||
|  | 	// for _, item := range orderReduceVal { | ||||||
|  | 	// 	sonic.Unmarshal([]byte(item), &reduceOrderStrategy) | ||||||
|  |  | ||||||
|  | 	// 	for index, item2 := range reduceOrderStrategy.Items { | ||||||
|  | 	// 		if reduceOrderStrategy.Symbol == trade.Coin+trade.Currency { | ||||||
|  | 	// 			//买入 | ||||||
|  | 	// 			if strings.ToUpper(reduceOrderStrategy.Side) == "SELL" && | ||||||
|  | 	// 				item2.TriggerPrice.Cmp(tradePrice) >= 0 && | ||||||
|  | 	// 				item2.TriggerPrice.Cmp(decimal.Zero) > 0 && | ||||||
|  | 	// 				tradePrice.Cmp(decimal.Zero) > 0 { | ||||||
|  | 	// 				lock := helper.NewRedisLock(fmt.Sprintf(rediskey.ReduceStrategySpotTriggerLock, reduceOrder.ApiId, reduceOrder.Symbol), 50, 15, 100*time.Millisecond) | ||||||
|  |  | ||||||
|  | 	// 				if ok, err := lock.AcquireWait(context.Background()); err != nil { | ||||||
|  | 	// 					logger.Error("获取锁失败", err) | ||||||
|  | 	// 					return errors.New("获取锁失败") | ||||||
|  | 	// 				} else if ok { | ||||||
|  | 	// 					defer lock.Release() | ||||||
|  | 	// 					hasrecord, _ := helper.DefaultRedis.HExists(reduceReduceListKey, utility.IntTostring(reduceOrderStrategy.OrderId), item) | ||||||
|  |  | ||||||
|  | 	// 					if !hasrecord { | ||||||
|  | 	// 						logger.Debug("减仓缓存中不存在", item) | ||||||
|  | 	// 						return fmt.Errorf("减仓缓存中不存在") | ||||||
|  | 	// 					} | ||||||
|  |  | ||||||
|  | 	// 					order, err := orderService.CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit) | ||||||
|  |  | ||||||
|  | 	// 					if err != nil { | ||||||
|  | 	// 						logger.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId) | ||||||
|  | 	// 					} | ||||||
|  |  | ||||||
|  | 	// 					reduceOrder.ApiId = order.ApiId | ||||||
|  | 	// 					reduceOrder.Id = order.Id | ||||||
|  | 	// 					reduceOrder.Pid = order.Pid | ||||||
|  | 	// 					reduceOrder.MainId = order.MainId | ||||||
|  | 	// 					reduceOrder.Symbol = order.Symbol | ||||||
|  | 	// 					reduceOrder.Side = reduceOrderStrategy.Side | ||||||
|  | 	// 					reduceOrder.OrderSn = order.OrderSn | ||||||
|  | 	// 					reduceOrder.Price = item2.Price | ||||||
|  | 	// 					reduceOrder.Num = item2.Num | ||||||
|  | 	// 					if SpotReduceTrigger(db, reduceOrder, spotApi, setting, reduceReduceListKey, item, true, reduceOrderStrategy.OrderId) { | ||||||
|  | 	// 						reduceOrderStrategy.Items[index].Actived = true | ||||||
|  | 	// 						allActive := true | ||||||
|  | 	// 						orderId := utility.IntToString(reduceOrderStrategy.OrderId) | ||||||
|  |  | ||||||
|  | 	// 						for _, item3 := range reduceOrderStrategy.Items { | ||||||
|  | 	// 							if !item3.Actived { | ||||||
|  | 	// 								allActive = false | ||||||
|  | 	// 								break | ||||||
|  | 	// 							} | ||||||
|  | 	// 						} | ||||||
|  |  | ||||||
|  | 	// 						if allActive { | ||||||
|  | 	// 							if err := helper.DefaultRedis.HDelField(reduceReduceListKey, orderId); err != nil { | ||||||
|  | 	// 								logger.Errorf("删除redis reduceReduceListKey失败 %s", err.Error()) | ||||||
|  | 	// 							} | ||||||
|  | 	// 						} else { | ||||||
|  | 	// 							str, _ := sonic.MarshalString(reduceOrderStrategy) | ||||||
|  | 	// 							if err := helper.DefaultRedis.HSetField(reduceReduceListKey, orderId, str); err != nil { | ||||||
|  | 	// 								logger.Errorf("更新redis reduceReduceListKey失败 %s", err.Error()) | ||||||
|  | 	// 							} | ||||||
|  | 	// 						} | ||||||
|  | 	// 					} | ||||||
|  | 	// 				} | ||||||
|  | 	// 			} | ||||||
|  | 	// 		} | ||||||
|  | 	// 	} | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | 	// //减仓单 | ||||||
|  | 	// reduceVal, _ := helper.DefaultRedis.GetAllList(key) | ||||||
|  |  | ||||||
|  | 	// for _, item := range reduceVal { | ||||||
|  | 	// 	if err := sonic.Unmarshal([]byte(item), &reduceOrder); err != nil { | ||||||
|  | 	// 		logger.Error("反序列化失败") | ||||||
|  | 	// 		continue | ||||||
|  | 	// 	} | ||||||
|  |  | ||||||
|  | 	// 	if reduceOrder.Symbol == trade.Coin+trade.Currency { | ||||||
|  | 	// 		orderPrice := reduceOrder.Price | ||||||
|  | 	// 		//买入 | ||||||
|  | 	// 		if strings.ToUpper(reduceOrder.Side) == "SELL" && | ||||||
|  | 	// 			orderPrice.Cmp(tradePrice) >= 0 && | ||||||
|  | 	// 			orderPrice.Cmp(decimal.Zero) > 0 && | ||||||
|  | 	// 			tradePrice.Cmp(decimal.Zero) > 0 { | ||||||
|  |  | ||||||
|  | 	// 			// SpotReduceTrigger(db, reduceOrder, spotApi, setting, key, item, false, 0) | ||||||
|  | 	// 		} | ||||||
|  | 	// 	} | ||||||
|  | 	// } | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 触发现货减仓 | ||||||
|  | // isStrategy 是否是策略减仓单 | ||||||
|  | // reduceId 策略主减仓单id | ||||||
|  | func SpotReduceTrigger(db *gorm.DB, reduceOrder positiondto.ReduceListItem, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool { | ||||||
|  | 	// tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 1) | ||||||
|  | 	result := true | ||||||
|  |  | ||||||
|  | 	// if err != nil { | ||||||
|  | 	// 	logger.Error("获取交易设置失败") | ||||||
|  | 	// 	return false | ||||||
|  | 	// } | ||||||
|  | 	// lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, reduceOrder.ApiId, reduceOrder.Symbol), 20, 5, 100*time.Millisecond) | ||||||
|  |  | ||||||
|  | 	// if ok, err := lock.AcquireWait(context.Background()); err != nil { | ||||||
|  | 	// 	logger.Error("获取锁失败", err) | ||||||
|  | 	// 	return false | ||||||
|  | 	// } else if ok { | ||||||
|  | 	// 	defer lock.Release() | ||||||
|  | 	// 	takeOrders := make([]DbModels.LinePreOrder, 0) | ||||||
|  | 	// 	if err := db.Model(&DbModels.LinePreOrder{}).Where("main_id =? AND order_type IN (1,2,4) AND status IN (1,5)", reduceOrder.MainId).Find(&takeOrders).Error; err != nil { | ||||||
|  | 	// 		logger.Error("查询止盈单失败") | ||||||
|  | 	// 		return false | ||||||
|  | 	// 	} | ||||||
|  | 	// 	var hasrecord bool | ||||||
|  |  | ||||||
|  | 	// 	if isStrategy { | ||||||
|  | 	// 		hasrecord, _ = helper.DefaultRedis.HExists(key, utility.IntToString(reduceId), item) | ||||||
|  | 	// 	} else { | ||||||
|  | 	// 		hasrecord, _ = helper.DefaultRedis.IsElementInList(key, item) | ||||||
|  | 	// 	} | ||||||
|  |  | ||||||
|  | 	// 	if !hasrecord { | ||||||
|  | 	// 		logger.Debug("减仓缓存中不存在", item) | ||||||
|  | 	// 		return false | ||||||
|  | 	// 	} | ||||||
|  |  | ||||||
|  | 	// 	apiInfo, _ := GetApiInfo(reduceOrder.ApiId) | ||||||
|  |  | ||||||
|  | 	// 	if apiInfo.Id == 0 { | ||||||
|  | 	// 		logger.Error("现货减仓 查询api用户不存在") | ||||||
|  | 	// 		return false | ||||||
|  | 	// 	} | ||||||
|  |  | ||||||
|  | 	// 	for _, takeOrder := range takeOrders { | ||||||
|  | 	// 		err := CancelOpenOrderByOrderSnLoop(apiInfo, takeOrder.Symbol, takeOrder.OrderSn) | ||||||
|  |  | ||||||
|  | 	// 		if err != nil { | ||||||
|  | 	// 			logger.Error("现货撤单失败", err) | ||||||
|  | 	// 			return false | ||||||
|  | 	// 		} | ||||||
|  | 	// 	} | ||||||
|  | 	// 	price := reduceOrder.Price.Mul(decimal.NewFromInt(1).Sub(setting.ReducePremium.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)) | ||||||
|  |  | ||||||
|  | 	// 	params := OrderPlacementService{ | ||||||
|  | 	// 		ApiId:            reduceOrder.ApiId, | ||||||
|  | 	// 		Side:             reduceOrder.Side, | ||||||
|  | 	// 		Type:             "LIMIT", | ||||||
|  | 	// 		TimeInForce:      "GTC", | ||||||
|  | 	// 		Symbol:           reduceOrder.Symbol, | ||||||
|  | 	// 		Price:            price, | ||||||
|  | 	// 		Quantity:         reduceOrder.Num, | ||||||
|  | 	// 		NewClientOrderId: reduceOrder.OrderSn, | ||||||
|  | 	// 	} | ||||||
|  |  | ||||||
|  | 	// 	if err := spotApi.OrderPlaceLoop(db, params, 3); err != nil { | ||||||
|  | 	// 		result = false | ||||||
|  | 	// 		logger.Errorf("现货减仓挂单失败 id:%s err:%v", reduceOrder.Id, err) | ||||||
|  |  | ||||||
|  | 	// 		if err2 := db.Model(&DbModels.LinePreOrder{}). | ||||||
|  | 	// 			Where("id =? AND status =0", reduceOrder.Id). | ||||||
|  | 	// 			Updates(map[string]interface{}{"status": 2, "desc": err.Error()}).Error; err2 != nil { | ||||||
|  | 	// 			logger.Errorf("修改现货减仓状态失败 id:%s err:%v", reduceOrder.Id, err2) | ||||||
|  | 	// 		} | ||||||
|  | 	// 	} else { | ||||||
|  | 	// 		if err := db.Model(&DbModels.LinePreOrder{}).Where("id =?", reduceOrder.Id).Updates(map[string]interface{}{"trigger_time": time.Now()}).Error; err != nil { | ||||||
|  | 	// 			logger.Error("更新预下单触发事件失败 ordersn:", reduceOrder.OrderSn) | ||||||
|  | 	// 		} | ||||||
|  |  | ||||||
|  | 	// 		if err := db.Model(&DbModels.LinePreOrder{}). | ||||||
|  | 	// 			Where("id =? AND status =0", reduceOrder.Id). | ||||||
|  | 	// 			Updates(map[string]interface{}{"status": 1}).Error; err != nil { | ||||||
|  | 	// 			logger.Errorf("修改现货减仓状态失败 id:%s err:%v", reduceOrder.Id, err) | ||||||
|  | 	// 		} | ||||||
|  |  | ||||||
|  | 	// 		//处理减仓单减仓策略 | ||||||
|  | 	// 		if err := CacheOrderStrategyAndReCreate(db, reduceOrder, 1, tradeSet, setting); err != nil { | ||||||
|  | 	// 			logger.Errorf("现货减仓 处理减仓策略失败 id:%v err:%v", reduceOrder.Id, err) | ||||||
|  | 	// 		} | ||||||
|  | 	// 	} | ||||||
|  |  | ||||||
|  | 	// 	if _, err := helper.DefaultRedis.LRem(key, item); err != nil { | ||||||
|  | 	// 		result = false | ||||||
|  | 	// 		logger.Errorf("现货减仓 删除缓存失败 id:%v err:%v", reduceOrder.Id, err) | ||||||
|  | 	// 	} | ||||||
|  | 	// } else { | ||||||
|  | 	// 	logger.Error("获取锁失败") | ||||||
|  | 	// 	result = false | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | 	return result | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // JudgeTakeOrder 判断是否止盈 | ||||||
|  | func JudgeTakeOrder(trades []models.TradeSet, symbolType int) error { | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // JudgeStopOrder 判断是否止损 | ||||||
|  | func JudgeStopOrder(trades []models.TradeSet, symbolType int) error { | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										430
									
								
								services/bitgetservice/bitget_market_client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										430
									
								
								services/bitgetservice/bitget_market_client.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,430 @@ | |||||||
|  | package bitgetservice | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"go-admin/services/proxy" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/bytedance/sonic" | ||||||
|  | 	log "github.com/go-admin-team/go-admin-core/logger" | ||||||
|  | 	"github.com/gorilla/websocket" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // BitgetMarketClient Bitget公开行情订阅客户端 | ||||||
|  | // 基于官方SDK设计,专门用于公开市场数据订阅 | ||||||
|  | type BitgetMarketClient struct { | ||||||
|  | 	config         *BitgetMarketConfig | ||||||
|  | 	conn           *websocket.Conn | ||||||
|  | 	subscriptions  map[SubscribeReq]OnReceive | ||||||
|  | 	allSubscribes  *Set | ||||||
|  | 	listener       OnReceive | ||||||
|  | 	errorListener  OnReceive | ||||||
|  | 	sendMutex      sync.Mutex | ||||||
|  | 	reconnectMutex sync.Mutex | ||||||
|  | 	ctx            context.Context | ||||||
|  | 	cancel         context.CancelFunc | ||||||
|  | 	connected      bool | ||||||
|  | 	lastPongTime   time.Time | ||||||
|  | 	proxyType      string | ||||||
|  | 	proxyAddress   string | ||||||
|  | 	reconnectCount int | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewBitgetMarketClient 创建新的Bitget市场数据客户端 | ||||||
|  | func NewBitgetMarketClient(config *BitgetMarketConfig) *BitgetMarketClient { | ||||||
|  | 	if config == nil { | ||||||
|  | 		config = DefaultMarketConfig() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx, cancel := context.WithCancel(context.Background()) | ||||||
|  |  | ||||||
|  | 	return &BitgetMarketClient{ | ||||||
|  | 		config:        config, | ||||||
|  | 		subscriptions: make(map[SubscribeReq]OnReceive), | ||||||
|  | 		allSubscribes: NewSet(), | ||||||
|  | 		ctx:           ctx, | ||||||
|  | 		cancel:        cancel, | ||||||
|  | 		connected:     false, | ||||||
|  | 		lastPongTime:  time.Now(), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetProxy 设置代理 | ||||||
|  | func (c *BitgetMarketClient) SetProxy(proxyType, proxyAddress string) { | ||||||
|  | 	c.proxyType = proxyType | ||||||
|  | 	c.proxyAddress = proxyAddress | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetListeners 设置消息监听器 | ||||||
|  | func (c *BitgetMarketClient) SetListeners(msgListener, errorListener OnReceive) { | ||||||
|  | 	c.listener = msgListener | ||||||
|  | 	c.errorListener = errorListener | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Connect 连接WebSocket | ||||||
|  | func (c *BitgetMarketClient) Connect() error { | ||||||
|  | 	c.reconnectMutex.Lock() | ||||||
|  | 	defer c.reconnectMutex.Unlock() | ||||||
|  |  | ||||||
|  | 	if c.connected { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 创建WebSocket连接 | ||||||
|  | 	dialer := websocket.DefaultDialer | ||||||
|  |  | ||||||
|  | 	// 设置代理 | ||||||
|  | 	if c.proxyAddress != "" { | ||||||
|  | 		var err error | ||||||
|  | 		dialer, err = proxy.GetDialer(c.proxyType, c.proxyAddress) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("设置代理失败: %v", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	dialer.HandshakeTimeout = 10 * time.Second | ||||||
|  | 	log.Infof("正在连接Bitget市场数据WebSocket: %s", c.config.WsUrl) | ||||||
|  | 	conn, _, err := dialer.Dial(c.config.WsUrl, nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("webSocket连接失败: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	c.conn = conn | ||||||
|  | 	c.connected = true | ||||||
|  | 	c.lastPongTime = time.Now() | ||||||
|  | 	c.reconnectCount = 0 | ||||||
|  |  | ||||||
|  | 	log.Info("Bitget市场数据WebSocket连接成功") | ||||||
|  |  | ||||||
|  | 	// 启动消息处理协程 | ||||||
|  | 	go c.readLoop() | ||||||
|  | 	go c.pingLoop() | ||||||
|  | 	go c.monitorConnection() | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Subscribe 订阅市场数据 | ||||||
|  | func (c *BitgetMarketClient) Subscribe(subscribeReqs []SubscribeReq, listener OnReceive) error { | ||||||
|  | 	if !c.connected { | ||||||
|  | 		if err := c.Connect(); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var args []interface{} | ||||||
|  | 	for _, req := range subscribeReqs { | ||||||
|  | 		// 标准化订阅请求 | ||||||
|  | 		normalizedReq := c.normalizeSubscribeReq(req) | ||||||
|  | 		args = append(args, normalizedReq) | ||||||
|  |  | ||||||
|  | 		// 保存订阅信息 | ||||||
|  | 		c.subscriptions[normalizedReq] = listener | ||||||
|  | 		c.allSubscribes.Add(normalizedReq) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 发送订阅消息 | ||||||
|  | 	wsReq := WsBaseReq{ | ||||||
|  | 		Op:   "subscribe", | ||||||
|  | 		Args: args, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return c.sendMessage(wsReq) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Unsubscribe 取消订阅 | ||||||
|  | func (c *BitgetMarketClient) Unsubscribe(subscribeReqs []SubscribeReq) error { | ||||||
|  | 	if !c.connected { | ||||||
|  | 		return errors.New("WebSocket未连接") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var args []interface{} | ||||||
|  | 	for _, req := range subscribeReqs { | ||||||
|  | 		normalizedReq := c.normalizeSubscribeReq(req) | ||||||
|  | 		args = append(args, normalizedReq) | ||||||
|  |  | ||||||
|  | 		// 移除订阅信息 | ||||||
|  | 		delete(c.subscriptions, normalizedReq) | ||||||
|  | 		c.allSubscribes.Remove(normalizedReq) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 发送取消订阅消息 | ||||||
|  | 	wsReq := WsBaseReq{ | ||||||
|  | 		Op:   "unsubscribe", | ||||||
|  | 		Args: args, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return c.sendMessage(wsReq) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // normalizeSubscribeReq 标准化订阅请求 | ||||||
|  | func (c *BitgetMarketClient) normalizeSubscribeReq(req SubscribeReq) SubscribeReq { | ||||||
|  | 	return SubscribeReq{ | ||||||
|  | 		InstType: strings.ToUpper(req.InstType), | ||||||
|  | 		Channel:  strings.ToLower(req.Channel), | ||||||
|  | 		InstId:   strings.ToUpper(req.InstId), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // sendMessage 发送消息 | ||||||
|  | func (c *BitgetMarketClient) sendMessage(msg interface{}) error { | ||||||
|  | 	if c.conn == nil { | ||||||
|  | 		return errors.New("WebSocket连接不可用") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	c.sendMutex.Lock() | ||||||
|  | 	defer c.sendMutex.Unlock() | ||||||
|  |  | ||||||
|  | 	data, err := sonic.Marshal(msg) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("序列化消息失败: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Info("发送消息: %s", string(data)) | ||||||
|  |  | ||||||
|  | 	c.conn.SetWriteDeadline(time.Now().Add(c.config.WriteTimeout)) | ||||||
|  | 	return c.conn.WriteMessage(websocket.TextMessage, data) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // readLoop 消息读取循环 | ||||||
|  | func (c *BitgetMarketClient) readLoop() { | ||||||
|  | 	defer func() { | ||||||
|  | 		if r := recover(); r != nil { | ||||||
|  | 			log.Error("readLoop panic: %v", r) | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case <-c.ctx.Done(): | ||||||
|  | 			return | ||||||
|  | 		default: | ||||||
|  | 			if c.conn == nil { | ||||||
|  | 				time.Sleep(time.Second) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			c.conn.SetReadDeadline(time.Now().Add(c.config.ReadTimeout)) | ||||||
|  | 			_, message, err := c.conn.ReadMessage() | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error("读取消息失败: %v", err) | ||||||
|  | 				c.handleDisconnect() | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			c.handleMessage(string(message)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleMessage 处理接收到的消息 | ||||||
|  | func (c *BitgetMarketClient) handleMessage(message string) { | ||||||
|  | 	log.Info("收到消息: %s", message) | ||||||
|  |  | ||||||
|  | 	// 处理pong消息 | ||||||
|  | 	if message == "pong" { | ||||||
|  | 		c.lastPongTime = time.Now() | ||||||
|  | 		log.Debug("收到pong消息") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 解析JSON消息 | ||||||
|  | 	var jsonMap map[string]interface{} | ||||||
|  | 	if err := json.Unmarshal([]byte(message), &jsonMap); err != nil { | ||||||
|  | 		log.Error("解析JSON消息失败: %v", err) | ||||||
|  | 		if c.errorListener != nil { | ||||||
|  | 			c.errorListener(message) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 检查错误码 | ||||||
|  | 	if code, exists := jsonMap["code"]; exists { | ||||||
|  | 		if codeNum, ok := code.(float64); ok && int(codeNum) != 0 { | ||||||
|  | 			log.Error("收到错误消息: %s", message) | ||||||
|  | 			if c.errorListener != nil { | ||||||
|  | 				c.errorListener(message) | ||||||
|  | 			} | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 处理订阅确认消息 | ||||||
|  | 	if event, exists := jsonMap["event"]; exists && event == "subscribe" { | ||||||
|  | 		log.Info("订阅确认: %s", message) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 处理数据推送 | ||||||
|  | 	if data, exists := jsonMap["data"]; exists && data != nil { | ||||||
|  | 		if arg, argExists := jsonMap["arg"]; argExists { | ||||||
|  | 			listener := c.getListener(arg) | ||||||
|  | 			if listener != nil { | ||||||
|  | 				listener(message) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 默认处理 | ||||||
|  | 	if c.listener != nil { | ||||||
|  | 		c.listener(message) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getListener 获取对应的监听器 | ||||||
|  | func (c *BitgetMarketClient) getListener(argJson interface{}) OnReceive { | ||||||
|  | 	argMap, ok := argJson.(map[string]interface{}) | ||||||
|  | 	if !ok { | ||||||
|  | 		return c.listener | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	subscribeReq := SubscribeReq{ | ||||||
|  | 		InstType: fmt.Sprintf("%v", argMap["instType"]), | ||||||
|  | 		Channel:  fmt.Sprintf("%v", argMap["channel"]), | ||||||
|  | 		InstId:   fmt.Sprintf("%v", argMap["instId"]), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if listener, exists := c.subscriptions[subscribeReq]; exists { | ||||||
|  | 		return listener | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return c.listener | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // pingLoop Ping循环 | ||||||
|  | func (c *BitgetMarketClient) pingLoop() { | ||||||
|  | 	ticker := time.NewTicker(c.config.PingInterval) | ||||||
|  | 	defer ticker.Stop() | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case <-c.ctx.Done(): | ||||||
|  | 			return | ||||||
|  | 		case <-ticker.C: | ||||||
|  | 			if c.connected && c.conn != nil { | ||||||
|  | 				c.sendMutex.Lock() | ||||||
|  | 				c.conn.SetWriteDeadline(time.Now().Add(c.config.WriteTimeout)) | ||||||
|  | 				err := c.conn.WriteMessage(websocket.TextMessage, []byte("ping")) | ||||||
|  | 				c.sendMutex.Unlock() | ||||||
|  |  | ||||||
|  | 				if err != nil { | ||||||
|  | 					log.Error("发送ping失败: %v", err) | ||||||
|  | 					c.handleDisconnect() | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // monitorConnection 监控连接状态 | ||||||
|  | func (c *BitgetMarketClient) monitorConnection() { | ||||||
|  | 	ticker := time.NewTicker(30 * time.Second) | ||||||
|  | 	defer ticker.Stop() | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case <-c.ctx.Done(): | ||||||
|  | 			return | ||||||
|  | 		case <-ticker.C: | ||||||
|  | 			if c.connected && time.Since(c.lastPongTime) > 60*time.Second { | ||||||
|  | 				log.Warn("长时间未收到pong消息,触发重连") | ||||||
|  | 				c.handleDisconnect() | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleDisconnect 处理断开连接 | ||||||
|  | func (c *BitgetMarketClient) handleDisconnect() { | ||||||
|  | 	c.reconnectMutex.Lock() | ||||||
|  | 	defer c.reconnectMutex.Unlock() | ||||||
|  |  | ||||||
|  | 	if !c.connected { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Warn("WebSocket连接断开,准备重连") | ||||||
|  | 	c.connected = false | ||||||
|  |  | ||||||
|  | 	if c.conn != nil { | ||||||
|  | 		c.conn.Close() | ||||||
|  | 		c.conn = nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 重连逻辑 | ||||||
|  | 	go c.reconnect() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // reconnect 重连 | ||||||
|  | func (c *BitgetMarketClient) reconnect() { | ||||||
|  | 	for c.reconnectCount < c.config.MaxReconnectTimes { | ||||||
|  | 		select { | ||||||
|  | 		case <-c.ctx.Done(): | ||||||
|  | 			return | ||||||
|  | 		default: | ||||||
|  | 			c.reconnectCount++ | ||||||
|  | 			log.Info("尝试重连 (%d/%d)", c.reconnectCount, c.config.MaxReconnectTimes) | ||||||
|  |  | ||||||
|  | 			time.Sleep(c.config.ReconnectInterval) | ||||||
|  |  | ||||||
|  | 			if err := c.Connect(); err != nil { | ||||||
|  | 				log.Error("重连失败: %v", err) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// 重新订阅 | ||||||
|  | 			if err := c.resubscribe(); err != nil { | ||||||
|  | 				log.Error("重新订阅失败: %v", err) | ||||||
|  | 				c.handleDisconnect() | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			log.Info("重连成功") | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Error("达到最大重连次数,停止重连") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // resubscribe 重新订阅 | ||||||
|  | func (c *BitgetMarketClient) resubscribe() error { | ||||||
|  | 	if len(c.subscriptions) == 0 { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var args []interface{} | ||||||
|  | 	for req := range c.subscriptions { | ||||||
|  | 		args = append(args, req) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	wsReq := WsBaseReq{ | ||||||
|  | 		Op:   "subscribe", | ||||||
|  | 		Args: args, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return c.sendMessage(wsReq) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Close 关闭客户端 | ||||||
|  | func (c *BitgetMarketClient) Close() error { | ||||||
|  | 	c.cancel() | ||||||
|  | 	c.connected = false | ||||||
|  |  | ||||||
|  | 	if c.conn != nil { | ||||||
|  | 		return c.conn.Close() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsConnected 检查连接状态 | ||||||
|  | func (c *BitgetMarketClient) IsConnected() bool { | ||||||
|  | 	return c.connected | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								services/bitgetservice/bitget_market_future.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								services/bitgetservice/bitget_market_future.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,61 @@ | |||||||
|  | package bitgetservice | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"go-admin/config" | ||||||
|  |  | ||||||
|  | 	"github.com/go-admin-team/go-admin-core/logger" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type BitgetMarketFutureClient struct { | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *BitgetMarketFutureClient) Subscribe(symbols []string) error { | ||||||
|  | 	// 1. 创建Bitget客户端(不需要API密钥用于公开数据) | ||||||
|  | 	client := &BitgetClient{} | ||||||
|  |  | ||||||
|  | 	// 初始化公开行情客户端 | ||||||
|  | 	marketConfig := DefaultMarketConfig() | ||||||
|  | 	client.marketClient = NewBitgetMarketClient(marketConfig) | ||||||
|  | 	if config.ExtConfig.ProxyUrl != "" { | ||||||
|  | 		client.SetMarketProxy("", config.ExtConfig.ProxyUrl) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 3. 设置消息监听器 | ||||||
|  | 	msgListener := func(message string) { | ||||||
|  | 		logger.Info("收到市场数据: %s", message) | ||||||
|  | 		// 在这里处理市场数据 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	errorListener := func(message string) { | ||||||
|  | 		logger.Error("收到错误消息: %s", message) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	client.SetMarketListeners(msgListener, errorListener) | ||||||
|  |  | ||||||
|  | 	// 4. 连接WebSocket | ||||||
|  | 	if err := client.ConnectMarketData(); err != nil { | ||||||
|  | 		logger.Error("连接市场数据失败: %v", err) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 5. 订阅合约行情 | ||||||
|  | 	tickerListener := func(message string) { | ||||||
|  | 		logger.Info("收到BTC-USDT合约行情: %s", message) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	params := []SubscribeReq{} | ||||||
|  |  | ||||||
|  | 	for _, v := range symbols { | ||||||
|  | 		params = append(params, SubscribeReq{ | ||||||
|  | 			InstType: "USDT-FUTURES", | ||||||
|  | 			Channel:  "ticker", | ||||||
|  | 			InstId:   v, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := client.SubscribeMarketData(params, tickerListener); err != nil { | ||||||
|  | 		logger.Error("订阅合约行情失败: %v", err) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										126
									
								
								services/bitgetservice/bitget_market_models.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								services/bitgetservice/bitget_market_models.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,126 @@ | |||||||
|  | package bitgetservice | ||||||
|  |  | ||||||
|  | import "time" | ||||||
|  |  | ||||||
|  | // SubscribeReq 订阅请求结构(基于官方SDK) | ||||||
|  | type SubscribeReq struct { | ||||||
|  | 	InstType string `json:"instType"` // 产品类型:SPOT-现货,UMCBL-U本位合约,DMCBL-币本位合约 | ||||||
|  | 	Channel  string `json:"channel"`  // 频道名称 | ||||||
|  | 	InstId   string `json:"instId"`   // 产品ID | ||||||
|  | 	// Coin     string `json:"coin"`     // 币种(可选) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WsBaseReq WebSocket基础请求结构 | ||||||
|  | type WsBaseReq struct { | ||||||
|  | 	Op   string        `json:"op"`   // 操作类型:subscribe, unsubscribe, login | ||||||
|  | 	Args []interface{} `json:"args"` // 参数列表 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WsLoginReq WebSocket登录请求 | ||||||
|  | type WsLoginReq struct { | ||||||
|  | 	ApiKey     string `json:"apiKey"` | ||||||
|  | 	Passphrase string `json:"passphrase"` | ||||||
|  | 	Timestamp  string `json:"timestamp"` | ||||||
|  | 	Sign       string `json:"sign"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // OnReceive 消息接收回调函数类型 | ||||||
|  | type OnReceive func(message string) | ||||||
|  |  | ||||||
|  | // Set 集合数据结构(简化实现) | ||||||
|  | type Set struct { | ||||||
|  | 	items map[SubscribeReq]bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewSet 创建新的集合 | ||||||
|  | func NewSet() *Set { | ||||||
|  | 	return &Set{ | ||||||
|  | 		items: make(map[SubscribeReq]bool), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Add 添加元素到集合 | ||||||
|  | func (s *Set) Add(item SubscribeReq) { | ||||||
|  | 	s.items[item] = true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Remove 从集合中移除元素 | ||||||
|  | func (s *Set) Remove(item SubscribeReq) { | ||||||
|  | 	delete(s.items, item) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Contains 检查集合是否包含元素 | ||||||
|  | func (s *Set) Contains(item SubscribeReq) bool { | ||||||
|  | 	_, exists := s.items[item] | ||||||
|  | 	return exists | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // BitgetMarketData 市场数据基础结构 | ||||||
|  | type BitgetMarketData struct { | ||||||
|  | 	Action string       `json:"action"` // 推送类型 | ||||||
|  | 	Arg    SubscribeReq `json:"arg"`    // 订阅参数 | ||||||
|  | 	Data   interface{}  `json:"data"`   // 数据内容 | ||||||
|  | 	Ts     int64        `json:"ts"`     // 时间戳 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TickerData 行情数据 | ||||||
|  | type TickerData struct { | ||||||
|  | 	InstId    string `json:"instId"`    // 产品ID | ||||||
|  | 	Last      string `json:"last"`      // 最新价 | ||||||
|  | 	Open24h   string `json:"open24h"`   // 24小时开盘价 | ||||||
|  | 	High24h   string `json:"high24h"`   // 24小时最高价 | ||||||
|  | 	Low24h    string `json:"low24h"`    // 24小时最低价 | ||||||
|  | 	Change24h string `json:"change24h"` // 24小时涨跌幅 | ||||||
|  | 	Vol24h    string `json:"vol24h"`    // 24小时成交量 | ||||||
|  | 	VolCcy24h string `json:"volCcy24h"` // 24小时成交额 | ||||||
|  | 	Ts        string `json:"ts"`        // 时间戳 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DepthData 深度数据 | ||||||
|  | type DepthData struct { | ||||||
|  | 	InstId string     `json:"instId"` // 产品ID | ||||||
|  | 	Asks   [][]string `json:"asks"`   // 卖盘 [价格, 数量] | ||||||
|  | 	Bids   [][]string `json:"bids"`   // 买盘 [价格, 数量] | ||||||
|  | 	Ts     string     `json:"ts"`     // 时间戳 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TradeData 成交数据 | ||||||
|  | type TradeData struct { | ||||||
|  | 	InstId  string `json:"instId"`  // 产品ID | ||||||
|  | 	TradeId string `json:"tradeId"` // 成交ID | ||||||
|  | 	Px      string `json:"px"`      // 成交价格 | ||||||
|  | 	Sz      string `json:"sz"`      // 成交数量 | ||||||
|  | 	Side    string `json:"side"`    // 成交方向 buy/sell | ||||||
|  | 	Ts      string `json:"ts"`      // 成交时间 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // KlineData K线数据 | ||||||
|  | type KlineData struct { | ||||||
|  | 	InstId string   `json:"instId"` // 产品ID | ||||||
|  | 	Candle []string `json:"candle"` // K线数据 [时间戳, 开盘价, 最高价, 最低价, 收盘价, 成交量, 成交额] | ||||||
|  | 	Ts     string   `json:"ts"`     // 时间戳 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // BitgetMarketConfig 市场数据配置 | ||||||
|  | type BitgetMarketConfig struct { | ||||||
|  | 	WsUrl             string        // WebSocket地址 | ||||||
|  | 	ReconnectInterval time.Duration // 重连间隔 | ||||||
|  | 	PingInterval      time.Duration // Ping间隔 | ||||||
|  | 	ReadTimeout       time.Duration // 读取超时 | ||||||
|  | 	WriteTimeout      time.Duration // 写入超时 | ||||||
|  | 	MaxReconnectTimes int           // 最大重连次数 | ||||||
|  | 	EnableCompression bool          // 是否启用压缩 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DefaultMarketConfig 默认市场数据配置 | ||||||
|  | func DefaultMarketConfig() *BitgetMarketConfig { | ||||||
|  | 	return &BitgetMarketConfig{ | ||||||
|  | 		WsUrl:             "wss://ws.bitget.com/v2/ws/public", | ||||||
|  | 		ReconnectInterval: 5 * time.Second, | ||||||
|  | 		PingInterval:      15 * time.Second, | ||||||
|  | 		ReadTimeout:       60 * time.Second, | ||||||
|  | 		WriteTimeout:      10 * time.Second, | ||||||
|  | 		MaxReconnectTimes: 10, | ||||||
|  | 		EnableCompression: false, | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								services/bitgetservice/bitget_market_spot.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								services/bitgetservice/bitget_market_spot.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,61 @@ | |||||||
|  | package bitgetservice | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"go-admin/config" | ||||||
|  |  | ||||||
|  | 	"github.com/go-admin-team/go-admin-core/logger" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type BitgetMarketSpotClient struct { | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *BitgetMarketSpotClient) Subscribe(symbols []string) error { | ||||||
|  | 	// 1. 创建Bitget客户端(不需要API密钥用于公开数据) | ||||||
|  | 	client := &BitgetClient{} | ||||||
|  |  | ||||||
|  | 	// 初始化公开行情客户端 | ||||||
|  | 	marketConfig := DefaultMarketConfig() | ||||||
|  | 	client.marketClient = NewBitgetMarketClient(marketConfig) | ||||||
|  | 	if config.ExtConfig.ProxyUrl != "" { | ||||||
|  | 		client.SetMarketProxy("", config.ExtConfig.ProxyUrl) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 3. 设置消息监听器 | ||||||
|  | 	msgListener := func(message string) { | ||||||
|  | 		logger.Infof("收到市场数据: %s", message) | ||||||
|  | 		// 在这里处理市场数据 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	errorListener := func(message string) { | ||||||
|  | 		logger.Errorf("收到错误消息: %s", message) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	client.SetMarketListeners(msgListener, errorListener) | ||||||
|  |  | ||||||
|  | 	// 4. 连接WebSocket | ||||||
|  | 	if err := client.ConnectMarketData(); err != nil { | ||||||
|  | 		logger.Errorf("连接市场数据失败: %v", err) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 5. 订阅现货行情 | ||||||
|  | 	tickerListener := func(message string) { | ||||||
|  | 		logger.Infof("收到行情: %s", message) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	params := []SubscribeReq{} | ||||||
|  |  | ||||||
|  | 	for _, v := range symbols { | ||||||
|  | 		params = append(params, SubscribeReq{ | ||||||
|  | 			InstType: "SPOT", | ||||||
|  | 			Channel:  "ticker", | ||||||
|  | 			InstId:   v, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := client.SubscribeMarketData(params, tickerListener); err != nil { | ||||||
|  | 		logger.Errorf("订阅现货行情失败: %v", err) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										54
									
								
								services/bitgetservice/bitget_market_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								services/bitgetservice/bitget_market_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | |||||||
|  | package bitgetservice | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"go-admin/common/helper" | ||||||
|  | 	"go-admin/config" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/go-admin-team/go-admin-core/sdk" | ||||||
|  | 	"gorm.io/driver/mysql" | ||||||
|  | 	"gorm.io/gorm" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func initSetting() { | ||||||
|  | 	dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms" | ||||||
|  | 	db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{}) | ||||||
|  | 	sdk.Runtime.SetDb("default", db) | ||||||
|  | 	helper.InitDefaultRedis("127.0.0.1:6379", "", 2) | ||||||
|  | 	helper.InitLockRedisConn("127.0.0.1:6379", "", "2") | ||||||
|  | 	config.ExtConfig.ProxyUrl = "http://127.0.0.1:7890" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestBitgetMarketFutureClient_Subscribe(t *testing.T) { | ||||||
|  | 	initSetting() | ||||||
|  | 	// 初始化Bitget合约行情客户端 | ||||||
|  | 	bitgetFut := BitgetMarketFutureClient{} | ||||||
|  |  | ||||||
|  | 	// 测试用的合约符号列表 | ||||||
|  | 	testSymbols := []string{"ETHUSDT"} | ||||||
|  |  | ||||||
|  | 	// 调用订阅方法 | ||||||
|  | 	err := bitgetFut.Subscribe(testSymbols) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("订阅Bitget合约行情失败: %v", err) | ||||||
|  | 	} | ||||||
|  | 	time.Sleep(60 * time.Second) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestBitgetMarketSpotClient_Subscribe(t *testing.T) { | ||||||
|  | 	initSetting() | ||||||
|  | 	// 初始化Bitget现货行情客户端 | ||||||
|  | 	bitgetSpot := BitgetMarketSpotClient{} | ||||||
|  |  | ||||||
|  | 	// 测试用的现货符号列表 | ||||||
|  | 	testSymbols := []string{"ETHUSDT"} | ||||||
|  |  | ||||||
|  | 	// 调用订阅方法 | ||||||
|  | 	err := bitgetSpot.Subscribe(testSymbols) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("订阅Bitget现货行情失败: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	time.Sleep(60 * time.Second) | ||||||
|  | } | ||||||
							
								
								
									
										417
									
								
								services/bitgetservice/bitget_rest.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										417
									
								
								services/bitgetservice/bitget_rest.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,417 @@ | |||||||
|  | package bitgetservice | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"go-admin/common/global" | ||||||
|  | 	"go-admin/common/helper" | ||||||
|  | 	"go-admin/config" | ||||||
|  | 	"go-admin/models" | ||||||
|  | 	"go-admin/pkg/utility" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/bytedance/sonic" | ||||||
|  | 	log "github.com/go-admin-team/go-admin-core/logger" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // BitgetRestApi Bitget REST API服务 | ||||||
|  | type BitgetRestApi struct { | ||||||
|  | 	httpClient *helper.BitgetClient | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewBitgetRestApi 创建新的Bitget REST API实例 | ||||||
|  | func NewBitgetRestApi() *BitgetRestApi { | ||||||
|  | 	// 从配置中获取代理设置 | ||||||
|  | 	proxyUrl := config.ExtConfig.ProxyUrl | ||||||
|  |  | ||||||
|  | 	// 使用代理配置创建HTTP客户端 | ||||||
|  | 	httpClient, err := helper.NewBitgetClient("", "", "", proxyUrl) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("创建Bitget HTTP客户端失败", err) | ||||||
|  | 		// 如果代理创建失败,使用无代理的客户端 | ||||||
|  | 		httpClient, _ = helper.NewBitgetClient("", "", "", "") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &BitgetRestApi{ | ||||||
|  | 		httpClient: httpClient, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewBitgetSimpleClient 创建简单的Bitget客户端(兼容性函数) | ||||||
|  | func NewBitgetSimpleClient() *BitgetClient { | ||||||
|  | 	// 从配置中获取代理设置 | ||||||
|  | 	proxyUrl := config.ExtConfig.ProxyUrl | ||||||
|  |  | ||||||
|  | 	client, err := NewBitgetClient("", "", "", proxyUrl) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("创建Bitget简单客户端失败", err) | ||||||
|  | 		// 如果代理创建失败,使用无代理的客户端 | ||||||
|  | 		client, _ = NewBitgetClient("", "", "", "") | ||||||
|  | 	} | ||||||
|  | 	return client | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // bitgetSymbolResponse 通用的符号响应结构体 | ||||||
|  | type bitgetSymbolResponse struct { | ||||||
|  | 	Code string `json:"code"` | ||||||
|  | 	Msg  string `json:"msg"` | ||||||
|  | 	Data []struct { | ||||||
|  | 		Symbol            string `json:"symbol"` | ||||||
|  | 		BaseCoin          string `json:"baseCoin"` | ||||||
|  | 		QuoteCoin         string `json:"quoteCoin"` | ||||||
|  | 		Status            string `json:"status"` | ||||||
|  | 		MinTradeAmount    string `json:"minTradeAmount"` | ||||||
|  | 		MaxTradeAmount    string `json:"maxTradeAmount"` | ||||||
|  | 		TakerFeeRate      string `json:"takerFeeRate"` | ||||||
|  | 		MakerFeeRate      string `json:"makerFeeRate"` | ||||||
|  | 		PricePrecision    string `json:"pricePrecision"` | ||||||
|  | 		QuantityPrecision string `json:"quantityPrecision"` | ||||||
|  | 	} `json:"data"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getSymbolsFromAPI 通用的符号获取方法 | ||||||
|  | // symbolType: "spot" 现货, "futures" 合约 | ||||||
|  | func (api *BitgetRestApi) getSymbolsFromAPI(symbolType string) ([]BitgetSymbol, error) { | ||||||
|  | 	var url string | ||||||
|  | 	var errorPrefix string | ||||||
|  |  | ||||||
|  | 	switch symbolType { | ||||||
|  | 	case "spot": | ||||||
|  | 		url = "/api/v2/spot/public/symbols" | ||||||
|  | 		errorPrefix = "现货" | ||||||
|  | 	case "futures": | ||||||
|  | 		url = "/api/v2/mix/market/contracts?productType=USDT-FUTURES" | ||||||
|  | 		errorPrefix = "合约" | ||||||
|  | 	default: | ||||||
|  | 		return nil, fmt.Errorf("不支持的符号类型: %s", symbolType) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 发送API请求 | ||||||
|  | 	resp, _, err := api.httpClient.SendSpotRequestAuth(url, "GET", map[string]interface{}{}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("获取bitget %s交易所信息失败: %v", errorPrefix, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 解析响应数据 | ||||||
|  | 	var response bitgetSymbolResponse | ||||||
|  | 	if err := sonic.Unmarshal(resp, &response); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("解析bitget %s交易所信息失败: %v", errorPrefix, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 检查API响应状态 | ||||||
|  | 	if response.Code != "00000" { | ||||||
|  | 		return nil, fmt.Errorf("bitget API错误: %s", response.Msg) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 转换数据格式 | ||||||
|  | 	return api.convertToSymbols(response.Data), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // convertToSymbols 将API响应数据转换为BitgetSymbol切片 | ||||||
|  | func (api *BitgetRestApi) convertToSymbols(data []struct { | ||||||
|  | 	Symbol            string `json:"symbol"` | ||||||
|  | 	BaseCoin          string `json:"baseCoin"` | ||||||
|  | 	QuoteCoin         string `json:"quoteCoin"` | ||||||
|  | 	Status            string `json:"status"` | ||||||
|  | 	MinTradeAmount    string `json:"minTradeAmount"` | ||||||
|  | 	MaxTradeAmount    string `json:"maxTradeAmount"` | ||||||
|  | 	TakerFeeRate      string `json:"takerFeeRate"` | ||||||
|  | 	MakerFeeRate      string `json:"makerFeeRate"` | ||||||
|  | 	PricePrecision    string `json:"pricePrecision"` | ||||||
|  | 	QuantityPrecision string `json:"quantityPrecision"` | ||||||
|  | }) []BitgetSymbol { | ||||||
|  | 	symbols := make([]BitgetSymbol, 0, len(data)) | ||||||
|  | 	 | ||||||
|  | 	for _, item := range data { | ||||||
|  | 		if item.Status == "online" { | ||||||
|  | 			priceDigit, _ := strconv.Atoi(item.PricePrecision) | ||||||
|  | 			quantileDigit, _ := strconv.Atoi(item.QuantityPrecision) | ||||||
|  |  | ||||||
|  | 			symbol := BitgetSymbol{ | ||||||
|  | 				Symbol:        item.Symbol, | ||||||
|  | 				BaseAsset:     item.BaseCoin, | ||||||
|  | 				QuoteAsset:    item.QuoteCoin, | ||||||
|  | 				Status:        item.Status, | ||||||
|  | 				MinQty:        item.MinTradeAmount, | ||||||
|  | 				MaxQty:        item.MaxTradeAmount, | ||||||
|  | 				PriceDigit:    priceDigit, | ||||||
|  | 				QuantityDigit: quantileDigit, | ||||||
|  | 			} | ||||||
|  | 			symbols = append(symbols, symbol) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	return symbols | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetSpotSymbols 获取现货交易对信息 | ||||||
|  | func (api *BitgetRestApi) GetSpotSymbols() ([]BitgetSymbol, error) { | ||||||
|  | 	return api.getSymbolsFromAPI("spot") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetFutSymbols 获取合约交易对信息 | ||||||
|  | func (api *BitgetRestApi) GetFutSymbols() ([]BitgetSymbol, error) { | ||||||
|  | 	return api.getSymbolsFromAPI("futures") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // symbolConfig 符号配置结构体 | ||||||
|  | type symbolConfig struct { | ||||||
|  | 	symbolType        string | ||||||
|  | 	quoteAssetSymbols []string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // buildTradeSetFromSymbols 通用的TradeSet构建方法 | ||||||
|  | func buildTradeSetFromSymbols(symbols []BitgetSymbol, config symbolConfig) (map[string]models.TradeSet, []string) { | ||||||
|  | 	tradeSets := make(map[string]models.TradeSet, len(symbols)) | ||||||
|  | 	deleteSymbols := make([]string, 0) | ||||||
|  |  | ||||||
|  | 	for _, item := range symbols { | ||||||
|  | 		if utility.ContainsStr(config.quoteAssetSymbols, item.QuoteAsset) && item.Status == "online" { | ||||||
|  | 			tradeSet := models.TradeSet{ | ||||||
|  | 				Coin:        item.BaseAsset, | ||||||
|  | 				Currency:    item.QuoteAsset, | ||||||
|  | 				PriceDigit:  item.PriceDigit, | ||||||
|  | 				AmountDigit: item.QuantityDigit, | ||||||
|  | 				MinQty:      utility.StringAsFloat(item.MinQty), | ||||||
|  | 			} | ||||||
|  | 			tradeSets[item.Symbol] = tradeSet | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return tradeSets, deleteSymbols | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetSpotSymbols 获取现货交易对并构建TradeSet映射 | ||||||
|  | func GetSpotSymbols() (map[string]models.TradeSet, []string, error) { | ||||||
|  | 	api := NewBitgetRestApi() | ||||||
|  | 	symbols, err := api.GetSpotSymbols() | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("获取bitget规范信息失败", err) | ||||||
|  | 		return make(map[string]models.TradeSet, 0), []string{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	config := symbolConfig{ | ||||||
|  | 		symbolType:        "spot", | ||||||
|  | 		quoteAssetSymbols: []string{"USDT", "ETH", "BTC", "SOL", "BNB", "DOGE"}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tradeSets, deleteSymbols := buildTradeSetFromSymbols(symbols, config) | ||||||
|  | 	return tradeSets, deleteSymbols, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetFutSymbols 获取合约交易对并构建TradeSet映射 | ||||||
|  | func GetFutSymbols() (map[string]models.TradeSet, []string, error) { | ||||||
|  | 	api := NewBitgetRestApi() | ||||||
|  | 	symbols, err := api.GetFutSymbols() | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("获取bitget合约规范信息失败", err) | ||||||
|  | 		return make(map[string]models.TradeSet, 0), []string{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	config := symbolConfig{ | ||||||
|  | 		symbolType:        "futures", | ||||||
|  | 		quoteAssetSymbols: []string{"USDT"}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tradeSets, deleteSymbols := buildTradeSetFromSymbols(symbols, config) | ||||||
|  | 	return tradeSets, deleteSymbols, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getTicker24hData 获取24小时行情数据的通用方法 | ||||||
|  | func (api *BitgetRestApi) getTicker24hData(url, tickerType, description string, tradeSet *map[string]models.TradeSet) ([]string, error) { | ||||||
|  | 	// 定义响应数据结构 | ||||||
|  | 	type tickerResponse struct { | ||||||
|  | 		Code string `json:"code"` | ||||||
|  | 		Msg  string `json:"msg"` | ||||||
|  | 		Data []struct { | ||||||
|  | 			Symbol    string `json:"symbol"` | ||||||
|  | 			High24h   string `json:"high24h"` | ||||||
|  | 			Low24h    string `json:"low24h"` | ||||||
|  | 			Close     string `json:"lastPr"` | ||||||
|  | 			Open      string `json:"open"` | ||||||
|  | 			Volume24h string `json:"baseVolume"`   // 基础币成交量 | ||||||
|  | 			QuoteVol  string `json:"usdtVolume"`   // USDT成交量 | ||||||
|  | 			Change24h string `json:"changeUtc24h"` // UTC 24h涨跌幅 | ||||||
|  | 			Change    string `json:"change24h"`    // 24h涨跌幅 | ||||||
|  | 		} `json:"data"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 发送API请求 | ||||||
|  | 	resp, _, err := api.httpClient.SendSpotRequestAuth(url, "GET", map[string]interface{}{}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return []string{}, fmt.Errorf("获取bitget %s 24h行情失败: %v", description, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 解析响应数据 | ||||||
|  | 	var response tickerResponse | ||||||
|  | 	if err := sonic.Unmarshal(resp, &response); err != nil { | ||||||
|  | 		return []string{}, fmt.Errorf("解析bitget %s 24h行情失败: %v", description, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 检查API响应状态 | ||||||
|  | 	if response.Code != "00000" { | ||||||
|  | 		return []string{}, fmt.Errorf("bitget %s API错误: %s", description, response.Msg) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 处理行情数据 | ||||||
|  | 	deleteSymbols := make([]string, 0) | ||||||
|  | 	processedCount := 0 | ||||||
|  | 	errorCount := 0 | ||||||
|  |  | ||||||
|  | 	for _, item := range response.Data { | ||||||
|  | 		// 构建缓存键 | ||||||
|  | 		key := fmt.Sprintf(tickerType, global.EXCHANGE_BITGET, item.Symbol) | ||||||
|  |  | ||||||
|  | 		// 检查交易对是否存在 | ||||||
|  | 		symbol, exists := (*tradeSet)[item.Symbol] | ||||||
|  | 		if !exists { | ||||||
|  | 			// 清理不存在的交易对缓存 | ||||||
|  | 			if err := helper.DefaultRedis.DeleteString(key); err != nil { | ||||||
|  | 				log.Warn("删除无效缓存失败: %s, error: %v", key, err) | ||||||
|  | 			} | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// 解析交易对获取coin和currency | ||||||
|  | 		var coin, currency string | ||||||
|  | 		if strings.HasSuffix(item.Symbol, symbol.Currency) { | ||||||
|  | 			coin = strings.TrimSuffix(item.Symbol, symbol.Currency) | ||||||
|  | 			currency = symbol.Currency | ||||||
|  | 		} else { | ||||||
|  | 			deleteSymbols = append(deleteSymbols, item.Symbol) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// 构建交易对数据 | ||||||
|  | 		symbol.Coin = coin | ||||||
|  | 		symbol.Currency = currency | ||||||
|  | 		symbol.OpenPrice = utility.StringAsFloat(item.Open) | ||||||
|  | 		symbol.PriceChange = utility.StringAsFloat(item.Change) | ||||||
|  | 		symbol.LowPrice = item.Low24h | ||||||
|  | 		symbol.HighPrice = item.High24h | ||||||
|  | 		symbol.Volume = item.Volume24h | ||||||
|  | 		symbol.QuoteVolume = item.QuoteVol | ||||||
|  | 		symbol.LastPrice = item.Close | ||||||
|  |  | ||||||
|  | 		// 验证数据有效性 | ||||||
|  | 		if symbol.Coin == "" || symbol.Currency == "" || | ||||||
|  | 			utility.StringToFloat64(symbol.QuoteVolume) <= 0 || | ||||||
|  | 			symbol.LastPrice == "" || utility.StringToFloat64(symbol.LastPrice) <= 0 { | ||||||
|  | 			if err := helper.DefaultRedis.DeleteString(key); err != nil { | ||||||
|  | 				log.Warn("删除无效数据缓存失败: %s, error: %v", key, err) | ||||||
|  | 			} | ||||||
|  | 			deleteSymbols = append(deleteSymbols, item.Symbol) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// 缓存数据 | ||||||
|  | 		val, err := sonic.Marshal(symbol) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error("序列化交易对数据失败: %s, error: %v", item.Symbol, err) | ||||||
|  | 			errorCount++ | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err := helper.DefaultRedis.SetString(key, string(val)); err != nil { | ||||||
|  | 			log.Error("缓存交易对失败: %s, error: %v", item.Symbol, err) | ||||||
|  | 			errorCount++ | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		processedCount++ | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Info("处理%s行情数据完成: 成功=%d, 错误=%d, 删除=%d", | ||||||
|  | 		description, processedCount, errorCount, len(deleteSymbols)) | ||||||
|  |  | ||||||
|  | 	return deleteSymbols, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetSpotTicker24h 获取现货24小时行情变更并缓存 | ||||||
|  | func (api *BitgetRestApi) GetSpotTicker24h(tradeSet *map[string]models.TradeSet) ([]string, error) { | ||||||
|  | 	return api.getTicker24hData("/api/v2/spot/market/tickers", global.TICKER_SPOT, "现货", tradeSet) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetFutTicker24h 获取U本位合约24小时行情变更并缓存 | ||||||
|  | func (api *BitgetRestApi) GetFutTicker24h(tradeSet *map[string]models.TradeSet) ([]string, error) { | ||||||
|  | 	return api.getTicker24hData("/api/v2/mix/market/tickers?productType=USDT-FUTURES", global.TICKER_FUTURES, "U本位合约", tradeSet) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // BitgetSymbol Bitget交易对信息 | ||||||
|  | type BitgetSymbol struct { | ||||||
|  | 	Symbol        string `json:"symbol"` | ||||||
|  | 	BaseCoin      string `json:"baseCoin"` | ||||||
|  | 	QuoteCoin     string `json:"quoteCoin"` | ||||||
|  | 	BaseAsset     string `json:"baseAsset"` | ||||||
|  | 	QuoteAsset    string `json:"quoteAsset"` | ||||||
|  | 	Status        string `json:"status"` | ||||||
|  | 	MinQty        string `json:"minQty"` | ||||||
|  | 	MaxQty        string `json:"maxQty"` | ||||||
|  | 	PriceDigit    int    `json:"priceDigit"` | ||||||
|  | 	QuantityDigit int    `json:"quantityDigit"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetAndReloadSymbols 获取并重新加载交易对信息 | ||||||
|  | // symbolType 1-现货 2-合约 | ||||||
|  | func GetAndReloadSymbols(data *map[string]models.TradeSet, symbolType int) error { | ||||||
|  | 	var tradeSets map[string]models.TradeSet | ||||||
|  | 	var err error | ||||||
|  |  | ||||||
|  | 	switch symbolType { | ||||||
|  | 	case 1: | ||||||
|  | 		tradeSets, _, err = GetSpotSymbols() | ||||||
|  | 	case 2: | ||||||
|  | 		tradeSets, _, err = GetFutSymbols() | ||||||
|  | 	default: | ||||||
|  | 		return fmt.Errorf("无效的symbolType: %d", symbolType) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	*data = tradeSets | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // InitSymbolsTicker24h 初始化24小时行情数据 | ||||||
|  | // symbolType 1-现货 2-合约 | ||||||
|  | func InitSymbolsTicker24h(data *map[string]models.TradeSet, symbolType int) ([]string, error) { | ||||||
|  | 	api := NewBitgetRestApi() | ||||||
|  |  | ||||||
|  | 	switch symbolType { | ||||||
|  | 	case 1: | ||||||
|  | 		return api.GetSpotTicker24h(data) | ||||||
|  | 	case 2: | ||||||
|  | 		return api.GetFutTicker24h(data) | ||||||
|  | 	default: | ||||||
|  | 		return []string{}, fmt.Errorf("无效的symbolType: %d", symbolType) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetSpotTicker24 获取单个交易对24h行情 | ||||||
|  | func (api *BitgetRestApi) GetSpotTicker24(symbol string, data *models.Ticker24, tradeSet *models.TradeSet) error { | ||||||
|  | 	key := fmt.Sprintf(global.TICKER_SPOT, global.EXCHANGE_BITGET, symbol) | ||||||
|  | 	val, err := helper.DefaultRedis.GetString(key) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = sonic.Unmarshal([]byte(val), tradeSet) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if tradeSet.Coin != "" { | ||||||
|  | 		data.HighPrice = tradeSet.HighPrice | ||||||
|  | 		data.ChangePercent = fmt.Sprintf("%g", tradeSet.PriceChange) | ||||||
|  | 		data.LastPrice = tradeSet.LastPrice | ||||||
|  | 		data.LowPrice = tradeSet.LowPrice | ||||||
|  | 		data.OpenPrice = fmt.Sprintf("%g", tradeSet.OpenPrice) | ||||||
|  | 		data.QuoteVolume = tradeSet.QuoteVolume | ||||||
|  | 		data.Volume = tradeSet.Volume | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										41
									
								
								services/bitgetservice/bitget_rest_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								services/bitgetservice/bitget_rest_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | |||||||
|  | package bitgetservice | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"go-admin/config" | ||||||
|  | 	"go-admin/test" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestGetSpotSymbols(t *testing.T) { | ||||||
|  | 	test.InitSettings() | ||||||
|  | 	api := NewBitgetRestApi() | ||||||
|  |  | ||||||
|  | 	symbols, err := api.GetSpotSymbols() | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Error(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	t.Log(symbols) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 获取交易所信息 | ||||||
|  | func TestGetExchangeInfo(t *testing.T) { | ||||||
|  | 	test.InitSettings() | ||||||
|  |  | ||||||
|  | 	client, err := NewBitgetClient("", "", "", config.ExtConfig.ProxyUrl) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Error(err) | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	data, err := client.GetExchangeInfo() | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Error(err) | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	t.Log(data) | ||||||
|  | } | ||||||
							
								
								
									
										658
									
								
								services/bitgetservice/bitget_socketmanager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										658
									
								
								services/bitgetservice/bitget_socketmanager.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,658 @@ | |||||||
|  | package bitgetservice | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"crypto/hmac" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"go-admin/pkg/utility" | ||||||
|  | 	"net" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  | 	"os" | ||||||
|  | 	"os/signal" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  | 	"sync/atomic" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/bytedance/sonic" | ||||||
|  | 	log "github.com/go-admin-team/go-admin-core/logger" | ||||||
|  | 	"github.com/gorilla/websocket" | ||||||
|  | 	"golang.org/x/net/proxy" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // BitgetWebSocketManager Bitget WebSocket管理器 | ||||||
|  | // 类似于BinanceWebSocketManager,支持外部传入API密钥 | ||||||
|  | type BitgetWebSocketManager struct { | ||||||
|  | 	ws          *websocket.Conn | ||||||
|  | 	stopChannel chan struct{} | ||||||
|  | 	url         string | ||||||
|  | 	/* 0-现货 1-合约 */ | ||||||
|  | 	wsType       int | ||||||
|  | 	apiKey       string | ||||||
|  | 	apiSecret    string | ||||||
|  | 	passphrase   string | ||||||
|  | 	proxyType    string | ||||||
|  | 	proxyAddress string | ||||||
|  | 	reconnect    chan struct{} | ||||||
|  | 	isStopped    bool       // 标记 WebSocket 是否已主动停止 | ||||||
|  | 	mu           sync.Mutex // 用于控制并发访问 isStopped | ||||||
|  | 	cancelFunc   context.CancelFunc | ||||||
|  | 	reconnecting atomic.Bool // 防止重复重连 | ||||||
|  | 	ConnectTime  time.Time   // 当前连接建立时间 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 已有连接存储 | ||||||
|  | var BitgetSpotSockets = map[string]*BitgetWebSocketManager{} | ||||||
|  | var BitgetFutureSockets = map[string]*BitgetWebSocketManager{} | ||||||
|  |  | ||||||
|  | // NewBitgetWebSocketManager 创建新的Bitget WebSocket管理器 | ||||||
|  | func NewBitgetWebSocketManager(wsType int, apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager { | ||||||
|  | 	var wsUrl string | ||||||
|  |  | ||||||
|  | 	switch wsType { | ||||||
|  | 	case 0: // 现货 | ||||||
|  | 		wsUrl = "wss://ws.bitget.com/spot/v1/stream" | ||||||
|  | 	case 1: // 合约 | ||||||
|  | 		wsUrl = "wss://ws.bitget.com/mix/v1/stream" | ||||||
|  | 	default: | ||||||
|  | 		wsUrl = "wss://ws.bitget.com/spot/v1/stream" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &BitgetWebSocketManager{ | ||||||
|  | 		stopChannel:  make(chan struct{}, 10), | ||||||
|  | 		reconnect:    make(chan struct{}, 10), | ||||||
|  | 		isStopped:    false, | ||||||
|  | 		url:          wsUrl, | ||||||
|  | 		wsType:       wsType, | ||||||
|  | 		apiKey:       apiKey, | ||||||
|  | 		apiSecret:    apiSecret, | ||||||
|  | 		passphrase:   passphrase, | ||||||
|  | 		proxyType:    proxyType, | ||||||
|  | 		proxyAddress: proxyAddress, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetKey 获取API Key | ||||||
|  | func (wm *BitgetWebSocketManager) GetKey() string { | ||||||
|  | 	return wm.apiKey | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Start 启动WebSocket连接 | ||||||
|  | func (wm *BitgetWebSocketManager) Start() { | ||||||
|  | 	utility.SafeGo(wm.run) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Restart 重启连接 | ||||||
|  | func (wm *BitgetWebSocketManager) Restart(apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager { | ||||||
|  | 	wm.mu.Lock() | ||||||
|  | 	defer wm.mu.Unlock() | ||||||
|  |  | ||||||
|  | 	wm.apiKey = apiKey | ||||||
|  | 	wm.apiSecret = apiSecret | ||||||
|  | 	wm.passphrase = passphrase | ||||||
|  | 	wm.proxyType = proxyType | ||||||
|  | 	wm.proxyAddress = proxyAddress | ||||||
|  |  | ||||||
|  | 	if wm.isStopped { | ||||||
|  | 		wm.isStopped = false | ||||||
|  | 		utility.SafeGo(wm.run) | ||||||
|  | 	} else { | ||||||
|  | 		log.Warnf("调用Bitget WebSocket restart") | ||||||
|  | 		wm.triggerReconnect(true) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return wm | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // triggerReconnect 触发重连 | ||||||
|  | func (wm *BitgetWebSocketManager) triggerReconnect(force bool) { | ||||||
|  | 	if force { | ||||||
|  | 		wm.reconnecting.Store(false) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if wm.reconnecting.CompareAndSwap(false, true) { | ||||||
|  | 		log.Warnf("准备重连Bitget WebSocket key: %s wsType: %v", wm.apiKey, wm.wsType) | ||||||
|  | 		select { | ||||||
|  | 		case wm.reconnect <- struct{}{}: | ||||||
|  | 		default: | ||||||
|  | 			log.Debugf("Bitget reconnect 信号已存在,跳过 key:%s", wm.apiKey) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // run 主运行循环 | ||||||
|  | func (wm *BitgetWebSocketManager) run() { | ||||||
|  | 	ctx, cancel := context.WithCancel(context.Background()) | ||||||
|  | 	wm.cancelFunc = cancel | ||||||
|  |  | ||||||
|  | 	utility.SafeGo(wm.handleSignal) | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case <-ctx.Done(): | ||||||
|  | 			return | ||||||
|  | 		default: | ||||||
|  | 			if err := wm.connect(ctx); err != nil { | ||||||
|  | 				wm.handleConnectionError(err) | ||||||
|  |  | ||||||
|  | 				if wm.isErrorCountExceeded() { | ||||||
|  | 					log.Error("连接Bitget WebSocket时出错次数过多,停止WebSocket管理器: wsType=%v, key=%s", wm.wsType, wm.apiKey) | ||||||
|  | 					wm.Stop() | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				time.Sleep(5 * time.Second) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			<-wm.stopChannel | ||||||
|  | 			log.Info("停止Bitget WebSocket管理器... wsType=%s", getBitgetWsTypeName(wm.wsType)) | ||||||
|  | 			wm.Stop() | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleConnectionError 处理连接错误 | ||||||
|  | func (wm *BitgetWebSocketManager) handleConnectionError(err error) { | ||||||
|  | 	// 这里可以添加错误记录到Redis的逻辑,类似binance的实现 | ||||||
|  | 	log.Error("连接Bitget WebSocket时出错: wsType=%v, key=%v, 错误=%v", wm.wsType, wm.apiKey, err) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // isErrorCountExceeded 检查错误次数是否超过阈值 | ||||||
|  | func (wm *BitgetWebSocketManager) isErrorCountExceeded() bool { | ||||||
|  | 	// 简化版本,可以根据需要实现更复杂的错误计数逻辑 | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleSignal 处理终止信号 | ||||||
|  | func (wm *BitgetWebSocketManager) handleSignal() { | ||||||
|  | 	ch := make(chan os.Signal, 1) | ||||||
|  | 	signal.Notify(ch, os.Interrupt) | ||||||
|  | 	<-ch | ||||||
|  | 	wm.Stop() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // connect 建立WebSocket连接 | ||||||
|  | func (wm *BitgetWebSocketManager) connect(ctx context.Context) error { | ||||||
|  | 	dialer, err := wm.getDialer() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 对于私有频道,需要进行身份验证 | ||||||
|  | 	headers := wm.getAuthHeaders() | ||||||
|  |  | ||||||
|  | 	wm.ws, _, err = dialer.Dial(wm.url, headers) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 连接成功,更新连接时间 | ||||||
|  | 	wm.ConnectTime = time.Now() | ||||||
|  | 	log.Info("已连接到Bitget %s WebSocket key:%s", getBitgetWsTypeName(wm.wsType), wm.apiKey) | ||||||
|  |  | ||||||
|  | 	// 设置Ping处理器 | ||||||
|  | 	wm.ws.SetPingHandler(func(appData string) error { | ||||||
|  | 		log.Info("收到Bitget Ping消息 wstype:%v key:%s data:%s", wm.wsType, wm.apiKey, appData) | ||||||
|  |  | ||||||
|  | 		for x := 0; x < 5; x++ { | ||||||
|  | 			if err := wm.ws.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(time.Second*10)); err != nil { | ||||||
|  | 				log.Error("Bitget回应Pong失败 次数:%d err:%v", x, err) | ||||||
|  | 				time.Sleep(time.Second * 1) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	// 启动各种处理协程 | ||||||
|  | 	utility.SafeGo(func() { wm.readMessages(ctx) }) | ||||||
|  | 	utility.SafeGo(func() { wm.handleReconnect(ctx) }) | ||||||
|  | 	utility.SafeGo(func() { wm.startPingLoop(ctx) }) | ||||||
|  |  | ||||||
|  | 	// 发送登录认证消息 | ||||||
|  | 	if err := wm.authenticate(); err != nil { | ||||||
|  | 		return fmt.Errorf("bitget WebSocket认证失败: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getDialer 获取WebSocket拨号器 | ||||||
|  | func (wm *BitgetWebSocketManager) getDialer() (*websocket.Dialer, error) { | ||||||
|  | 	if wm.proxyAddress == "" { | ||||||
|  | 		return &websocket.Dialer{}, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !strings.HasPrefix(wm.proxyAddress, "http://") && | ||||||
|  | 		!strings.HasPrefix(wm.proxyAddress, "https://") && | ||||||
|  | 		!strings.HasPrefix(wm.proxyAddress, "socks5://") { | ||||||
|  | 		wm.proxyAddress = wm.proxyType + "://" + wm.proxyAddress | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	proxyURL, err := url.Parse(wm.proxyAddress) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to parse proxy URL: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	switch proxyURL.Scheme { | ||||||
|  | 	case "socks5": | ||||||
|  | 		return wm.createSocks5Dialer(proxyURL) | ||||||
|  | 	case "http", "https": | ||||||
|  | 		return &websocket.Dialer{ | ||||||
|  | 			Proxy: func(req *http.Request) (*url.URL, error) { | ||||||
|  | 				return proxyURL, nil | ||||||
|  | 			}, | ||||||
|  | 		}, nil | ||||||
|  | 	default: | ||||||
|  | 		return nil, fmt.Errorf("unsupported proxy scheme: %s", proxyURL.Scheme) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // createSocks5Dialer 创建SOCKS5代理拨号器 | ||||||
|  | func (wm *BitgetWebSocketManager) createSocks5Dialer(proxyURL *url.URL) (*websocket.Dialer, error) { | ||||||
|  | 	auth := &proxy.Auth{} | ||||||
|  | 	if proxyURL.User != nil { | ||||||
|  | 		auth.User = proxyURL.User.Username() | ||||||
|  | 		auth.Password, _ = proxyURL.User.Password() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	socksDialer, err := proxy.SOCKS5("tcp", proxyURL.Host, auth, proxy.Direct) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to create SOCKS5 proxy dialer: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &websocket.Dialer{ | ||||||
|  | 		NetDialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { | ||||||
|  | 			return socksDialer.Dial(network, addr) | ||||||
|  | 		}, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getAuthHeaders 获取认证头信息 | ||||||
|  | func (wm *BitgetWebSocketManager) getAuthHeaders() http.Header { | ||||||
|  | 	headers := http.Header{} | ||||||
|  |  | ||||||
|  | 	if wm.apiKey != "" { | ||||||
|  | 		timestamp := strconv.FormatInt(time.Now().Unix(), 10) | ||||||
|  | 		sign := wm.generateSignature(timestamp) | ||||||
|  |  | ||||||
|  | 		headers.Set("ACCESS-KEY", wm.apiKey) | ||||||
|  | 		headers.Set("ACCESS-SIGN", sign) | ||||||
|  | 		headers.Set("ACCESS-TIMESTAMP", timestamp) | ||||||
|  | 		headers.Set("ACCESS-PASSPHRASE", wm.passphrase) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return headers | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // generateSignature 生成签名 | ||||||
|  | func (wm *BitgetWebSocketManager) generateSignature(timestamp string) string { | ||||||
|  | 	message := timestamp + "GET" + "/user/verify" | ||||||
|  |  | ||||||
|  | 	h := hmac.New(sha256.New, []byte(wm.apiSecret)) | ||||||
|  | 	h.Write([]byte(message)) | ||||||
|  |  | ||||||
|  | 	return base64.StdEncoding.EncodeToString(h.Sum(nil)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // authenticate 发送认证消息 | ||||||
|  | func (wm *BitgetWebSocketManager) authenticate() error { | ||||||
|  | 	if wm.apiKey == "" { | ||||||
|  | 		return nil // 公共频道不需要认证 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	timestamp := strconv.FormatInt(time.Now().Unix(), 10) | ||||||
|  | 	sign := wm.generateSignature(timestamp) | ||||||
|  |  | ||||||
|  | 	authMsg := map[string]interface{}{ | ||||||
|  | 		"op": "login", | ||||||
|  | 		"args": []map[string]string{ | ||||||
|  | 			{ | ||||||
|  | 				"apiKey":     wm.apiKey, | ||||||
|  | 				"passphrase": wm.passphrase, | ||||||
|  | 				"timestamp":  timestamp, | ||||||
|  | 				"sign":       sign, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	msgBytes, err := sonic.Marshal(authMsg) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return wm.ws.WriteMessage(websocket.TextMessage, msgBytes) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // readMessages 读取消息 | ||||||
|  | func (wm *BitgetWebSocketManager) readMessages(ctx context.Context) { | ||||||
|  | 	defer func() { | ||||||
|  | 		if r := recover(); r != nil { | ||||||
|  | 			log.Error("Bitget WebSocket消息处理器panic: %v", r) | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case <-ctx.Done(): | ||||||
|  | 			return | ||||||
|  | 		default: | ||||||
|  | 			wm.mu.Lock() | ||||||
|  | 			if wm.isStopped || wm.ws == nil { | ||||||
|  | 				wm.mu.Unlock() | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			_, message, err := wm.ws.ReadMessage() | ||||||
|  | 			wm.mu.Unlock() | ||||||
|  |  | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error("Bitget WebSocket读取消息错误: %v", err) | ||||||
|  | 				wm.triggerReconnect(false) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// 处理消息 | ||||||
|  | 			wm.handleMessage(message) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleMessage 处理收到的消息 | ||||||
|  | func (wm *BitgetWebSocketManager) handleMessage(message []byte) { | ||||||
|  | 	// 解析消息并处理 | ||||||
|  | 	var baseMsg map[string]interface{} | ||||||
|  | 	if err := sonic.Unmarshal(message, &baseMsg); err != nil { | ||||||
|  | 		log.Error("解析Bitget WebSocket消息失败: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 检查是否是认证响应 | ||||||
|  | 	if op, exists := baseMsg["op"]; exists && op == "login" { | ||||||
|  | 		wm.handleAuthResponse(baseMsg) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 检查是否是订阅数据 | ||||||
|  | 	if arg, exists := baseMsg["arg"]; exists { | ||||||
|  | 		wm.handleSubscriptionData(baseMsg, arg) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Info("收到Bitget WebSocket消息: %s", string(message)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleAuthResponse 处理认证响应 | ||||||
|  | func (wm *BitgetWebSocketManager) handleAuthResponse(msg map[string]interface{}) { | ||||||
|  | 	if code, exists := msg["code"]; exists { | ||||||
|  | 		if code == "0" || code == 0 { | ||||||
|  | 			log.Info("Bitget WebSocket认证成功 key:%s", wm.apiKey) | ||||||
|  | 			// 认证成功后可以订阅私有频道 | ||||||
|  | 			wm.subscribePrivateChannels() | ||||||
|  | 		} else { | ||||||
|  | 			log.Error("Bitget WebSocket认证失败 key:%s code:%v", wm.apiKey, code) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleSubscriptionData 处理订阅数据 | ||||||
|  | func (wm *BitgetWebSocketManager) handleSubscriptionData(msg map[string]interface{}, arg interface{}) { | ||||||
|  | 	// 根据频道类型处理不同的数据 | ||||||
|  | 	// 这里可以根据具体需求实现不同频道的数据处理逻辑 | ||||||
|  | 	log.Info("收到Bitget订阅数据: %v", arg) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // subscribePrivateChannels 订阅私有频道 | ||||||
|  | func (wm *BitgetWebSocketManager) subscribePrivateChannels() { | ||||||
|  | 	// 订阅账户更新 | ||||||
|  | 	wm.subscribeAccount() | ||||||
|  | 	// 订阅订单更新 | ||||||
|  | 	wm.subscribeOrders() | ||||||
|  | 	// 订阅持仓更新 | ||||||
|  | 	wm.subscribePositions() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // subscribeAccount 订阅账户更新 | ||||||
|  | func (wm *BitgetWebSocketManager) subscribeAccount() { | ||||||
|  | 	var channel string | ||||||
|  | 	if wm.wsType == 0 { | ||||||
|  | 		channel = "account" | ||||||
|  | 	} else { | ||||||
|  | 		channel = "account" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	subMsg := map[string]interface{}{ | ||||||
|  | 		"op": "subscribe", | ||||||
|  | 		"args": []map[string]string{ | ||||||
|  | 			{ | ||||||
|  | 				"instType": wm.getInstType(), | ||||||
|  | 				"channel":  channel, | ||||||
|  | 				"instId":   "default", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	wm.sendMessage(subMsg) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // subscribeOrders 订阅订单更新 | ||||||
|  | func (wm *BitgetWebSocketManager) subscribeOrders() { | ||||||
|  | 	var channel string | ||||||
|  | 	if wm.wsType == 0 { | ||||||
|  | 		channel = "orders" | ||||||
|  | 	} else { | ||||||
|  | 		channel = "orders" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	subMsg := map[string]interface{}{ | ||||||
|  | 		"op": "subscribe", | ||||||
|  | 		"args": []map[string]string{ | ||||||
|  | 			{ | ||||||
|  | 				"instType": wm.getInstType(), | ||||||
|  | 				"channel":  channel, | ||||||
|  | 				"instId":   "default", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	wm.sendMessage(subMsg) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // subscribePositions 订阅持仓更新(仅合约) | ||||||
|  | func (wm *BitgetWebSocketManager) subscribePositions() { | ||||||
|  | 	if wm.wsType != 1 { | ||||||
|  | 		return // 只有合约才有持仓 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	subMsg := map[string]interface{}{ | ||||||
|  | 		"op": "subscribe", | ||||||
|  | 		"args": []map[string]string{ | ||||||
|  | 			{ | ||||||
|  | 				"instType": "umcbl", | ||||||
|  | 				"channel":  "positions", | ||||||
|  | 				"instId":   "default", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	wm.sendMessage(subMsg) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getInstType 获取产品类型 | ||||||
|  | func (wm *BitgetWebSocketManager) getInstType() string { | ||||||
|  | 	if wm.wsType == 0 { | ||||||
|  | 		return "sp" // 现货 | ||||||
|  | 	} | ||||||
|  | 	return "umcbl" // U本位合约 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SendMessage 发送消息(公开方法) | ||||||
|  | func (wm *BitgetWebSocketManager) SendMessage(msg interface{}) error { | ||||||
|  | 	msgBytes, err := sonic.Marshal(msg) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	wm.mu.Lock() | ||||||
|  | 	defer wm.mu.Unlock() | ||||||
|  |  | ||||||
|  | 	if wm.ws == nil { | ||||||
|  | 		return errors.New("WebSocket连接不可用") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return wm.ws.WriteMessage(websocket.TextMessage, msgBytes) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // sendMessage 发送消息(私有方法,保持向后兼容) | ||||||
|  | func (wm *BitgetWebSocketManager) sendMessage(msg interface{}) error { | ||||||
|  | 	return wm.SendMessage(msg) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleReconnect 处理重连 | ||||||
|  | func (wm *BitgetWebSocketManager) handleReconnect(ctx context.Context) { | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case <-ctx.Done(): | ||||||
|  | 			return | ||||||
|  | 		case <-wm.reconnect: | ||||||
|  | 			wm.reconnecting.Store(false) | ||||||
|  | 			log.Info("开始重连Bitget WebSocket key:%s", wm.apiKey) | ||||||
|  |  | ||||||
|  | 			wm.mu.Lock() | ||||||
|  | 			if wm.ws != nil { | ||||||
|  | 				wm.ws.Close() | ||||||
|  | 				wm.ws = nil | ||||||
|  | 			} | ||||||
|  | 			wm.mu.Unlock() | ||||||
|  |  | ||||||
|  | 			// 触发主循环重新连接 | ||||||
|  | 			select { | ||||||
|  | 			case wm.stopChannel <- struct{}{}: | ||||||
|  | 			default: | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // startPingLoop 启动心跳循环 | ||||||
|  | func (wm *BitgetWebSocketManager) startPingLoop(ctx context.Context) { | ||||||
|  | 	ticker := time.NewTicker(30 * time.Second) | ||||||
|  | 	defer ticker.Stop() | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case <-ctx.Done(): | ||||||
|  | 			return | ||||||
|  | 		case <-ticker.C: | ||||||
|  | 			wm.sendPing() | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // sendPing 发送心跳 | ||||||
|  | func (wm *BitgetWebSocketManager) sendPing() { | ||||||
|  | 	pingMsg := "ping" | ||||||
|  |  | ||||||
|  | 	wm.mu.Lock() | ||||||
|  | 	defer wm.mu.Unlock() | ||||||
|  |  | ||||||
|  | 	if wm.ws == nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := wm.ws.WriteMessage(websocket.TextMessage, []byte(pingMsg)); err != nil { | ||||||
|  | 		log.Error("Bitget WebSocket发送ping失败: %v", err) | ||||||
|  | 		wm.triggerReconnect(false) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Stop 停止WebSocket连接 | ||||||
|  | func (wm *BitgetWebSocketManager) Stop() { | ||||||
|  | 	wm.mu.Lock() | ||||||
|  | 	defer wm.mu.Unlock() | ||||||
|  |  | ||||||
|  | 	if wm.isStopped { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	wm.isStopped = true | ||||||
|  |  | ||||||
|  | 	if wm.cancelFunc != nil { | ||||||
|  | 		wm.cancelFunc() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if wm.ws != nil { | ||||||
|  | 		wm.ws.Close() | ||||||
|  | 		wm.ws = nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 从全局map中移除 | ||||||
|  | 	if wm.wsType == 0 { | ||||||
|  | 		delete(BitgetSpotSockets, wm.apiKey) | ||||||
|  | 	} else { | ||||||
|  | 		delete(BitgetFutureSockets, wm.apiKey) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Info("Bitget WebSocket已停止 key:%s", wm.apiKey) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getBitgetWsTypeName 获取WebSocket类型名称 | ||||||
|  | func getBitgetWsTypeName(wsType int) string { | ||||||
|  | 	switch wsType { | ||||||
|  | 	case 0: | ||||||
|  | 		return "现货" | ||||||
|  | 	case 1: | ||||||
|  | 		return "合约" | ||||||
|  | 	default: | ||||||
|  | 		return "未知" | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 工具函数:获取或创建Bitget WebSocket管理器 | ||||||
|  | func GetOrCreateBitgetWebSocketManager(wsType int, apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager { | ||||||
|  | 	var socketsMap map[string]*BitgetWebSocketManager | ||||||
|  |  | ||||||
|  | 	if wsType == 0 { | ||||||
|  | 		socketsMap = BitgetSpotSockets | ||||||
|  | 	} else { | ||||||
|  | 		socketsMap = BitgetFutureSockets | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if manager, exists := socketsMap[apiKey]; exists { | ||||||
|  | 		// 如果参数有变化,重启连接 | ||||||
|  | 		if manager.apiSecret != apiSecret || manager.passphrase != passphrase || | ||||||
|  | 			manager.proxyType != proxyType || manager.proxyAddress != proxyAddress { | ||||||
|  | 			manager.Restart(apiKey, apiSecret, passphrase, proxyType, proxyAddress) | ||||||
|  | 		} | ||||||
|  | 		return manager | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 创建新的管理器 | ||||||
|  | 	manager := NewBitgetWebSocketManager(wsType, apiKey, apiSecret, passphrase, proxyType, proxyAddress) | ||||||
|  | 	socketsMap[apiKey] = manager | ||||||
|  |  | ||||||
|  | 	return manager | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // StopBitgetWebSocketManager 停止指定的WebSocket管理器 | ||||||
|  | func StopBitgetWebSocketManager(wsType int, apiKey string) { | ||||||
|  | 	var socketsMap map[string]*BitgetWebSocketManager | ||||||
|  |  | ||||||
|  | 	if wsType == 0 { | ||||||
|  | 		socketsMap = BitgetSpotSockets | ||||||
|  | 	} else { | ||||||
|  | 		socketsMap = BitgetFutureSockets | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if manager, exists := socketsMap[apiKey]; exists { | ||||||
|  | 		manager.Stop() | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -64,16 +64,16 @@ func GetConfigCacheByKey(db *gorm.DB, key string) models.SysConfig { | |||||||
| } | } | ||||||
|  |  | ||||||
| // 获取缓存交易对 | // 获取缓存交易对 | ||||||
| // symbolType 0-现货 1-合约 | // symbolType 1-现货 2-合约 | ||||||
| func GetTradeSet(exchangeType string, symbol string, symbolType int) (models2.TradeSet, error) { | func GetTradeSet(exchangeType string, symbol string, symbolType int) (models2.TradeSet, error) { | ||||||
| 	result := models2.TradeSet{} | 	result := models2.TradeSet{} | ||||||
| 	val := "" | 	val := "" | ||||||
|  |  | ||||||
| 	switch symbolType { | 	switch symbolType { | ||||||
| 	case 0: | 	case 1: | ||||||
| 		key := fmt.Sprintf(global.TICKER_SPOT, exchangeType, symbol) | 		key := fmt.Sprintf(global.TICKER_SPOT, exchangeType, symbol) | ||||||
| 		val, _ = helper.DefaultRedis.GetString(key) | 		val, _ = helper.DefaultRedis.GetString(key) | ||||||
| 	case 1: | 	case 2: | ||||||
| 		key := fmt.Sprintf(global.TICKER_FUTURES, exchangeType, symbol) | 		key := fmt.Sprintf(global.TICKER_FUTURES, exchangeType, symbol) | ||||||
| 		val, _ = helper.DefaultRedis.GetString(key) | 		val, _ = helper.DefaultRedis.GetString(key) | ||||||
| 	} | 	} | ||||||
| @ -89,6 +89,36 @@ func GetTradeSet(exchangeType string, symbol string, symbolType int) (models2.Tr | |||||||
| 	return result, nil | 	return result, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // 获取交易所所有交易对 | ||||||
|  | // symbolType 1-现货 2-合约 | ||||||
|  | func GetExchangeTradeSets(exchange string, symbolType int) ([]models2.TradeSet, error) { | ||||||
|  | 	result := []models2.TradeSet{} | ||||||
|  | 	val := []string{} | ||||||
|  |  | ||||||
|  | 	switch symbolType { | ||||||
|  | 	case 1: | ||||||
|  | 		key := fmt.Sprintf(global.TICKER_SPOT, exchange, "*") | ||||||
|  | 		val, _ = helper.DefaultRedis.GetAllKeysAndValues(key) | ||||||
|  | 	case 2: | ||||||
|  | 		key := fmt.Sprintf(global.TICKER_FUTURES, exchange, "*") | ||||||
|  | 		val, _ = helper.DefaultRedis.GetAllKeysAndValues(key) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, v := range val { | ||||||
|  | 		item := models2.TradeSet{} | ||||||
|  | 		if err := sonic.Unmarshal([]byte(v), &item); err != nil { | ||||||
|  | 			return result, err | ||||||
|  | 		} | ||||||
|  | 		result = append(result, item) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(result) == 0 { | ||||||
|  | 		return result, errors.New("未找到交易对信息") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return result, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func GetSystemSetting(db *gorm.DB) (models.LineSystemSetting, error) { | func GetSystemSetting(db *gorm.DB) (models.LineSystemSetting, error) { | ||||||
| 	key := fmt.Sprintf(rediskey.SystemSetting) | 	key := fmt.Sprintf(rediskey.SystemSetting) | ||||||
| 	val, _ := helper.DefaultRedis.GetString(key) | 	val, _ := helper.DefaultRedis.GetString(key) | ||||||
|  | |||||||
							
								
								
									
										15
									
								
								services/commonservice/commonservice.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								services/commonservice/commonservice.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | package commonservice | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/go-admin-team/go-admin-core/sdk" | ||||||
|  | 	"gorm.io/gorm" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // 获取数据库连接 | ||||||
|  | func GetDBConnection() *gorm.DB { | ||||||
|  | 	dbs := sdk.Runtime.GetDb() | ||||||
|  | 	for _, db := range dbs { | ||||||
|  | 		return db | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										63
									
								
								services/commonservice/judge.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								services/commonservice/judge.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | |||||||
|  | package commonservice | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"go-admin/common/global" | ||||||
|  | 	"go-admin/common/helper" | ||||||
|  | 	"go-admin/models/binancedto" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/bytedance/sonic" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // 判断api订阅是否超时 | ||||||
|  | // wsType 1 现货 2 合约 | ||||||
|  | func JudgeWebsocketTimeout(apiKey string, wsType int) error { | ||||||
|  | 	key := fmt.Sprintf(global.USER_SUBSCRIBE, apiKey) | ||||||
|  | 	cache := binancedto.UserSubscribeState{} | ||||||
|  | 	// 从redis中获取订阅信息 | ||||||
|  | 	subscribeInfo, err := helper.DefaultRedis.GetString(key) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errors.New("获取订阅信息失败") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sonic.Unmarshal([]byte(subscribeInfo), &cache) | ||||||
|  |  | ||||||
|  | 	if cache.FuturesLastTime == nil && cache.SpotLastTime == nil { | ||||||
|  | 		return errors.New("未订阅任何行情") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var timeOutInterval time.Duration | ||||||
|  |  | ||||||
|  | 	if wsType == 1 { | ||||||
|  | 		timeOutInterval = time.Minute * 2 | ||||||
|  | 	} else { | ||||||
|  | 		timeOutInterval = time.Minute * 4 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	switch wsType { | ||||||
|  | 	case 1: | ||||||
|  | 		// 现货 | ||||||
|  | 		if cache.SpotLastTime == nil { | ||||||
|  | 			return errors.New("未订阅现货行情") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if time.Since(*cache.SpotLastTime) > timeOutInterval { | ||||||
|  | 			return errors.New("现货行情订阅超时") | ||||||
|  | 		} | ||||||
|  | 	case 2: | ||||||
|  | 		// 合约 | ||||||
|  | 		if cache.FuturesLastTime == nil { | ||||||
|  | 			return errors.New("未订阅合约行情") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if time.Since(*cache.FuturesLastTime) > timeOutInterval { | ||||||
|  | 			return errors.New("合约行情订阅超时") | ||||||
|  | 		} | ||||||
|  | 	default: | ||||||
|  | 		return errors.New("无效的wsType") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
| @ -1,6 +1,7 @@ | |||||||
| package excservice | package excservice | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"go-admin/models/futuresdto" | 	"go-admin/models/futuresdto" | ||||||
| 	"go-admin/pkg/utility" | 	"go-admin/pkg/utility" | ||||||
| 	"go-admin/services/binanceservice" | 	"go-admin/services/binanceservice" | ||||||
| @ -15,27 +16,43 @@ import ( | |||||||
|   - @msg 消息内容 |   - @msg 消息内容 | ||||||
|   - @listenType 订阅类型 0-现货 1-合约 |   - @listenType 订阅类型 0-现货 1-合约 | ||||||
| */ | */ | ||||||
| func ReceiveListen(msg []byte, listenType int, apiKey string) (reconnect bool, err error) { | // ReceiveListen 处理用户订单订阅 | ||||||
|  | // 返回: | ||||||
|  | //   - reconnect: bool, 是否需要立即重连 (通常用于 listenKey 过期) | ||||||
|  | //   - fatal: bool, 是否是致命错误 (例如认证失败),需要停止整个服务 | ||||||
|  | //   - err: error, 解析或处理过程中的错误 | ||||||
|  | func ReceiveListen(msg []byte, listenType int, apiKey string) (reconnect bool, fatal bool, err error) { | ||||||
| 	var dataMap map[string]interface{} | 	var dataMap map[string]interface{} | ||||||
| 	err = sonic.Unmarshal(msg, &dataMap) | 	err = sonic.Unmarshal(msg, &dataMap) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("接收ws 反序列化失败:", err) | 		log.Error("接收ws 反序列化失败:", err) | ||||||
| 		return | 		return false, false, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 检查是否是币安的错误消息, e.g. {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} | ||||||
|  | 	if code, ok := dataMap["code"].(float64); ok && code != 0 { | ||||||
|  | 		errMsg, _ := dataMap["msg"].(string) | ||||||
|  | 		log.Errorf("收到币安错误码: %d, 消息: %s, API Key: %s", int(code), errMsg, apiKey) | ||||||
|  | 		// -2015 是无效API Key的错误码,这是一个致命错误,不需要重连 | ||||||
|  | 		if int(code) == -2015 { | ||||||
|  | 			return false, true, fmt.Errorf("币安认证失败 (code: %d): %s", int(code), errMsg) | ||||||
|  | 		} | ||||||
|  | 		// 其他错误码可能也需要特殊处理,但暂时我们只将-2015视为致命错误 | ||||||
|  | 		return false, false, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	event, exits := dataMap["e"] | 	event, exits := dataMap["e"] | ||||||
|  |  | ||||||
| 	if !exits { | 	if !exits { | ||||||
| 		log.Error("不存在event") | 		// 如果没有 event 字段,但也不是一个错误消息,则可能是一个未知的消息格式 | ||||||
| 		return | 		log.Warn("收到的消息中不存在event字段: %s", string(msg)) | ||||||
|  | 		return false, false, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	switch event { | 	switch event { | ||||||
| 	//listenKey过期 | 	//listenKey过期 | ||||||
| 	case "listenKeyExpired": | 	case "listenKeyExpired": | ||||||
| 		log.Info("listenKey过期", string(msg)) | 		log.Info("listenKey过期", string(msg)) | ||||||
| 		return true, nil | 		return true, false, nil | ||||||
|  |  | ||||||
| 	//订单变更 | 	//订单变更 | ||||||
| 	case "ORDER_TRADE_UPDATE": | 	case "ORDER_TRADE_UPDATE": | ||||||
| @ -101,11 +118,12 @@ func ReceiveListen(msg []byte, listenType int, apiKey string) (reconnect bool, e | |||||||
|  |  | ||||||
| 	case "eventStreamTerminated": | 	case "eventStreamTerminated": | ||||||
| 		log.Info("账户数据流被终止 type:", getWsTypeName(listenType)) | 		log.Info("账户数据流被终止 type:", getWsTypeName(listenType)) | ||||||
|  | 		return true, false, nil | ||||||
| 	default: | 	default: | ||||||
| 		log.Info("未知事件 内容:", string(msg)) | 		log.Info("未知事件 内容:", string(msg)) | ||||||
| 		log.Info("未知事件", event) | 		log.Info("未知事件", event) | ||||||
| 		return false, nil | 		return false, false, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return false, nil | 	return false, false, nil | ||||||
| } | } | ||||||
|  | |||||||
| @ -2,7 +2,6 @@ package excservice | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"crypto/tls" |  | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"go-admin/common/global" | 	"go-admin/common/global" | ||||||
| @ -11,12 +10,8 @@ import ( | |||||||
| 	"go-admin/models/commondto" | 	"go-admin/models/commondto" | ||||||
| 	"go-admin/pkg/jsonhelper" | 	"go-admin/pkg/jsonhelper" | ||||||
| 	"go-admin/pkg/utility" | 	"go-admin/pkg/utility" | ||||||
|  | 	"go-admin/services/proxy" | ||||||
| 	"math/rand" | 	"math/rand" | ||||||
| 	"net" |  | ||||||
| 	"net/http" |  | ||||||
| 	"net/url" |  | ||||||
| 	"os" |  | ||||||
| 	"os/signal" |  | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| @ -26,32 +21,135 @@ import ( | |||||||
| 	"github.com/bytedance/sonic" | 	"github.com/bytedance/sonic" | ||||||
| 	log "github.com/go-admin-team/go-admin-core/logger" | 	log "github.com/go-admin-team/go-admin-core/logger" | ||||||
| 	"github.com/gorilla/websocket" | 	"github.com/gorilla/websocket" | ||||||
| 	"golang.org/x/net/proxy" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type BinanceWebSocketManager struct { | type BinanceWebSocketManager struct { | ||||||
| 	ws          *websocket.Conn | 	mu                  sync.Mutex | ||||||
| 	stopChannel chan struct{} | 	stopOnce            sync.Once // 新增,确保Stop只执行一次 | ||||||
| 	url         string | 	ws                  *websocket.Conn | ||||||
| 	/* 0-现货 1-合约 */ | 	stopChannel         chan struct{} | ||||||
| 	wsType       int | 	url                 string | ||||||
| 	apiKey       string | 	wsType              int | ||||||
| 	apiSecret    string | 	apiKey              string | ||||||
| 	proxyType    string | 	apiSecret           string | ||||||
| 	proxyAddress string | 	proxyType           string | ||||||
| 	reconnect    chan struct{} | 	proxyAddress        string | ||||||
| 	isStopped    bool       // 标记 WebSocket 是否已主动停止 | 	reconnect           chan struct{} | ||||||
| 	mu           sync.Mutex // 用于控制并发访问 isStopped | 	isStopped           bool | ||||||
| 	cancelFunc   context.CancelFunc | 	cancelFunc          context.CancelFunc | ||||||
| 	listenKey    string      // 新增字段 | 	listenKey           string | ||||||
| 	reconnecting atomic.Bool // 防止重复重连 | 	reconnecting        atomic.Bool | ||||||
| 	ConnectTime  time.Time   // 当前连接建立时间 | 	ConnectTime         time.Time | ||||||
|  | 	lastPingTime        atomic.Value | ||||||
|  | 	forceReconnectTimer *time.Timer | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ... (省略中间代码) | ||||||
|  |  | ||||||
|  | // sendPing 使用锁来确保发送ping时的线程安全 | ||||||
|  | func (wm *BinanceWebSocketManager) sendPing() error { | ||||||
|  | 	wm.mu.Lock() | ||||||
|  | 	defer wm.mu.Unlock() | ||||||
|  | 	if wm.ws == nil { | ||||||
|  | 		return errors.New("websocket connection is nil") | ||||||
|  | 	} | ||||||
|  | 	wm.lastPingTime.Store(time.Now()) | ||||||
|  | 	return wm.ws.WriteMessage(websocket.PingMessage, []byte("ping")) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Stop 优雅地停止WebSocket管理器 | ||||||
|  | func (wm *BinanceWebSocketManager) Stop() { | ||||||
|  | 	wm.stopOnce.Do(func() { | ||||||
|  | 		wm.mu.Lock() | ||||||
|  | 		if wm.isStopped { | ||||||
|  | 			wm.mu.Unlock() | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		wm.isStopped = true | ||||||
|  | 		wm.mu.Unlock() | ||||||
|  |  | ||||||
|  | 		if wm.cancelFunc != nil { | ||||||
|  | 			wm.cancelFunc() | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		wm.closeConn() // 统一调用带锁的关闭方法 | ||||||
|  |  | ||||||
|  | 		// 尝试关闭stopChannel,如果已经关闭则忽略 | ||||||
|  | 		select { | ||||||
|  | 		case <-wm.stopChannel: | ||||||
|  | 		default: | ||||||
|  | 			close(wm.stopChannel) | ||||||
|  | 		} | ||||||
|  | 		log.Infof("WebSocket 已完全停止 key:%s", wm.apiKey) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleReconnect 处理重连逻辑 | ||||||
|  | func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) { | ||||||
|  | 	const maxRetries = 10 | ||||||
|  | 	backoff := time.Second | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case <-wm.reconnect: | ||||||
|  | 			if !wm.reconnecting.CompareAndSwap(false, true) { | ||||||
|  | 				continue // 如果已经在重连,则忽略 | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// 在开始重连循环之前,先安全地关闭旧的连接 | ||||||
|  | 			wm.closeConn() | ||||||
|  |  | ||||||
|  | 			retryCount := 0 | ||||||
|  | 			for retryCount < maxRetries { | ||||||
|  | 				select { | ||||||
|  | 				case <-wm.stopChannel: | ||||||
|  | 					wm.reconnecting.Store(false) | ||||||
|  | 					return | ||||||
|  | 				default: | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				log.Infof("WebSocket (%s) 正在尝试重连,第 %d 次...", wm.wsType, retryCount+1) | ||||||
|  | 				if err := wm.connect(ctx); err == nil { | ||||||
|  | 					log.Infof("WebSocket (%s) 重连成功", wm.wsType) | ||||||
|  | 					wm.reconnecting.Store(false) | ||||||
|  | 					go wm.startDeadCheck(ctx) // 重连成功后,重新启动假死检测 | ||||||
|  | 					break                     // 跳出重连循环 | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				retryCount++ | ||||||
|  | 				time.Sleep(backoff) | ||||||
|  | 				backoff *= 2 | ||||||
|  | 				if backoff > time.Minute { | ||||||
|  | 					backoff = time.Minute | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if retryCount >= maxRetries { | ||||||
|  | 				log.Errorf("WebSocket (%s) 重连失败次数过多,停止重连", wm.wsType) | ||||||
|  | 				wm.Stop() | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 		case <-ctx.Done(): | ||||||
|  | 			wm.reconnecting.Store(false) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // 已有连接 | // 已有连接 | ||||||
| var SpotSockets = map[string]*BinanceWebSocketManager{} | var SpotSockets = map[string]*BinanceWebSocketManager{} | ||||||
| var FutureSockets = map[string]*BinanceWebSocketManager{} | var FutureSockets = map[string]*BinanceWebSocketManager{} | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 创建新的Binance WebSocket管理器 | ||||||
|  |  * @param wsType WebSocket类型:0-现货,1-合约 | ||||||
|  |  * @param apiKey API密钥 | ||||||
|  |  * @param apiSecret API密钥 | ||||||
|  |  * @param proxyType 代理类型 | ||||||
|  |  * @param proxyAddress 代理地址 | ||||||
|  |  * @return WebSocket管理器实例 | ||||||
|  |  */ | ||||||
| func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyAddress string) *BinanceWebSocketManager { | func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyAddress string) *BinanceWebSocketManager { | ||||||
| 	url := "" | 	url := "" | ||||||
|  |  | ||||||
| @ -62,7 +160,7 @@ func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyA | |||||||
| 		url = "wss://fstream.binance.com/ws" | 		url = "wss://fstream.binance.com/ws" | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return &BinanceWebSocketManager{ | 	wm := &BinanceWebSocketManager{ | ||||||
| 		stopChannel:  make(chan struct{}, 10), | 		stopChannel:  make(chan struct{}, 10), | ||||||
| 		reconnect:    make(chan struct{}, 10), | 		reconnect:    make(chan struct{}, 10), | ||||||
| 		isStopped:    false, | 		isStopped:    false, | ||||||
| @ -73,18 +171,36 @@ func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyA | |||||||
| 		proxyType:    proxyType, | 		proxyType:    proxyType, | ||||||
| 		proxyAddress: proxyAddress, | 		proxyAddress: proxyAddress, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// 初始化最后ping时间 | ||||||
|  | 	wm.lastPingTime.Store(time.Now()) | ||||||
|  |  | ||||||
|  | 	return wm | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取API密钥 | ||||||
|  |  * @return API密钥 | ||||||
|  |  */ | ||||||
| func (wm *BinanceWebSocketManager) GetKey() string { | func (wm *BinanceWebSocketManager) GetKey() string { | ||||||
| 	return wm.apiKey | 	return wm.apiKey | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 启动WebSocket连接 | ||||||
|  |  */ | ||||||
| func (wm *BinanceWebSocketManager) Start() { | func (wm *BinanceWebSocketManager) Start() { | ||||||
| 	utility.SafeGo(wm.run) | 	utility.SafeGo(wm.run) | ||||||
| 	// wm.run() |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // 重启连接 | /** | ||||||
|  |  * 重启连接,更新配置参数 | ||||||
|  |  * @param apiKey 新的API密钥 | ||||||
|  |  * @param apiSecret 新的API密钥 | ||||||
|  |  * @param proxyType 新的代理类型 | ||||||
|  |  * @param proxyAddress 新的代理地址 | ||||||
|  |  * @return WebSocket管理器实例 | ||||||
|  |  */ | ||||||
| func (wm *BinanceWebSocketManager) Restart(apiKey, apiSecret, proxyType, proxyAddress string) *BinanceWebSocketManager { | func (wm *BinanceWebSocketManager) Restart(apiKey, apiSecret, proxyType, proxyAddress string) *BinanceWebSocketManager { | ||||||
| 	wm.mu.Lock() | 	wm.mu.Lock() | ||||||
| 	defer wm.mu.Unlock() | 	defer wm.mu.Unlock() | ||||||
| @ -105,7 +221,10 @@ func (wm *BinanceWebSocketManager) Restart(apiKey, apiSecret, proxyType, proxyAd | |||||||
| 	return wm | 	return wm | ||||||
| } | } | ||||||
|  |  | ||||||
| // 触发重连 | /** | ||||||
|  |  * 触发重连机制 | ||||||
|  |  * @param force 是否强制重连 | ||||||
|  |  */ | ||||||
| func (wm *BinanceWebSocketManager) triggerReconnect(force bool) { | func (wm *BinanceWebSocketManager) triggerReconnect(force bool) { | ||||||
| 	if force { | 	if force { | ||||||
| 		wm.reconnecting.Store(false) // 强制重置标志位 | 		wm.reconnecting.Store(false) // 强制重置标志位 | ||||||
| @ -122,15 +241,24 @@ func (wm *BinanceWebSocketManager) triggerReconnect(force bool) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 外部重启函数 | ||||||
|  |  * @param wm WebSocket管理器实例 | ||||||
|  |  */ | ||||||
| func Restart(wm *BinanceWebSocketManager) { | func Restart(wm *BinanceWebSocketManager) { | ||||||
| 	log.Warnf("调用restart") | 	log.Warnf("调用restart") | ||||||
| 	wm.triggerReconnect(true) | 	wm.triggerReconnect(true) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 主运行循环 | ||||||
|  |  */ | ||||||
| func (wm *BinanceWebSocketManager) run() { | func (wm *BinanceWebSocketManager) run() { | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) | 	ctx, cancel := context.WithCancel(context.Background()) | ||||||
| 	wm.cancelFunc = cancel | 	wm.cancelFunc = cancel | ||||||
|  |  | ||||||
| 	utility.SafeGo(wm.handleSignal) | 	// utility.SafeGo(wm.handleSignal) | ||||||
|  |  | ||||||
| 	// 计算错误记录键 | 	// 计算错误记录键 | ||||||
| 	errKey := fmt.Sprintf(global.API_WEBSOCKET_ERR, wm.apiKey) | 	errKey := fmt.Sprintf(global.API_WEBSOCKET_ERR, wm.apiKey) | ||||||
| @ -163,7 +291,11 @@ func (wm *BinanceWebSocketManager) run() { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // handleConnectionError 处理 WebSocket 连接错误 | /** | ||||||
|  |  * 处理WebSocket连接错误 | ||||||
|  |  * @param errKey Redis错误记录键 | ||||||
|  |  * @param err 错误信息 | ||||||
|  |  */ | ||||||
| func (wm *BinanceWebSocketManager) handleConnectionError(errKey string, err error) { | func (wm *BinanceWebSocketManager) handleConnectionError(errKey string, err error) { | ||||||
| 	// 从 Redis 获取错误记录 | 	// 从 Redis 获取错误记录 | ||||||
| 	var errMessage commondto.WebSocketErr | 	var errMessage commondto.WebSocketErr | ||||||
| @ -186,7 +318,11 @@ func (wm *BinanceWebSocketManager) handleConnectionError(errKey string, err erro | |||||||
| 	log.Error("连接 %s WebSocket 时出错: %v, 错误: %v", wm.wsType, wm.apiKey, err) | 	log.Error("连接 %s WebSocket 时出错: %v, 错误: %v", wm.wsType, wm.apiKey, err) | ||||||
| } | } | ||||||
|  |  | ||||||
| // isErrorCountExceeded 检查错误次数是否超过阈值 | /** | ||||||
|  |  * 检查错误次数是否超过阈值 | ||||||
|  |  * @param errKey Redis错误记录键 | ||||||
|  |  * @return 是否超过阈值 | ||||||
|  |  */ | ||||||
| func (wm *BinanceWebSocketManager) isErrorCountExceeded(errKey string) bool { | func (wm *BinanceWebSocketManager) isErrorCountExceeded(errKey string) bool { | ||||||
| 	val, _ := helper.DefaultRedis.GetString(errKey) | 	val, _ := helper.DefaultRedis.GetString(errKey) | ||||||
| 	if val == "" { | 	if val == "" { | ||||||
| @ -201,16 +337,13 @@ func (wm *BinanceWebSocketManager) isErrorCountExceeded(errKey string) bool { | |||||||
| 	return errMessage.Count >= 5 | 	return errMessage.Count >= 5 | ||||||
| } | } | ||||||
|  |  | ||||||
| // 处理终止信号 | /** | ||||||
| func (wm *BinanceWebSocketManager) handleSignal() { |  * 建立WebSocket连接 | ||||||
| 	ch := make(chan os.Signal) |  * @param ctx 上下文 | ||||||
| 	signal.Notify(ch, os.Interrupt) |  * @return 错误信息 | ||||||
| 	<-ch |  */ | ||||||
| 	wm.Stop() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (wm *BinanceWebSocketManager) connect(ctx context.Context) error { | func (wm *BinanceWebSocketManager) connect(ctx context.Context) error { | ||||||
| 	dialer, err := wm.getDialer() | 	dialer, err := proxy.GetDialer(wm.proxyType, wm.proxyAddress) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @ -227,8 +360,9 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// 连接成功,更新连接时间 | 	// 连接成功,更新连接时间和ping时间 | ||||||
| 	wm.ConnectTime = time.Now() | 	wm.ConnectTime = time.Now() | ||||||
|  | 	wm.lastPingTime.Store(time.Now()) | ||||||
| 	log.Info(fmt.Sprintf("已连接到 Binance %s WebSocket【%s】 key:%s", getWsTypeName(wm.wsType), wm.apiKey, listenKey)) | 	log.Info(fmt.Sprintf("已连接到 Binance %s WebSocket【%s】 key:%s", getWsTypeName(wm.wsType), wm.apiKey, listenKey)) | ||||||
|  |  | ||||||
| 	// Ping处理 | 	// Ping处理 | ||||||
| @ -238,23 +372,25 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error { | |||||||
| 		for x := 0; x < 5; x++ { | 		for x := 0; x < 5; x++ { | ||||||
| 			if err := wm.ws.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(time.Second*10)); err != nil { | 			if err := wm.ws.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(time.Second*10)); err != nil { | ||||||
| 				log.Error("binance 回应pong失败 次数:", strconv.Itoa(x), " err:", err) | 				log.Error("binance 回应pong失败 次数:", strconv.Itoa(x), " err:", err) | ||||||
|  |  | ||||||
| 				time.Sleep(time.Second * 1) | 				time.Sleep(time.Second * 1) | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			break | 			break | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// 更新ping时间和Redis记录 | ||||||
|  | 		wm.lastPingTime.Store(time.Now()) | ||||||
| 		setLastTime(wm) | 		setLastTime(wm) | ||||||
| 		return nil | 		return nil | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|  | 	// 启动各种协程 | ||||||
| 	utility.SafeGo(func() { wm.startListenKeyRenewal2(ctx) }) | 	utility.SafeGo(func() { wm.startListenKeyRenewal2(ctx) }) | ||||||
| 	utility.SafeGo(func() { wm.readMessages(ctx) }) | 	utility.SafeGo(func() { wm.readMessages(ctx) }) | ||||||
| 	utility.SafeGo(func() { wm.handleReconnect(ctx) }) | 	utility.SafeGo(func() { wm.handleReconnect(ctx) }) | ||||||
| 	utility.SafeGo(func() { wm.startPingLoop(ctx) }) | 	utility.SafeGo(func() { wm.startPingLoop(ctx) }) | ||||||
| 	// utility.SafeGo(func() { wm.startDeadCheck(ctx) }) | 	utility.SafeGo(func() { wm.startDeadCheck(ctx) })            // 连接成功后立即启动假死检测 | ||||||
|  | 	utility.SafeGo(func() { wm.start24HourReconnectTimer(ctx) }) // 启动24小时强制重连 | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| @ -278,7 +414,7 @@ func (wm *BinanceWebSocketManager) ReplaceConnection() error { | |||||||
| 		return fmt.Errorf("获取新 listenKey 失败: %w", err) | 		return fmt.Errorf("获取新 listenKey 失败: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	dialer, err := wm.getDialer() | 	dialer, err := proxy.GetDialer(wm.proxyType, wm.proxyAddress) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @ -360,54 +496,6 @@ func setLastTime(wm *BinanceWebSocketManager) { | |||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (wm *BinanceWebSocketManager) getDialer() (*websocket.Dialer, error) { |  | ||||||
| 	if wm.proxyAddress == "" { |  | ||||||
| 		return &websocket.Dialer{}, nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if !strings.HasPrefix(wm.proxyAddress, "http://") && !strings.HasPrefix(wm.proxyAddress, "https://") && !strings.HasPrefix(wm.proxyAddress, "socks5://") { |  | ||||||
| 		wm.proxyAddress = wm.proxyType + "://" + wm.proxyAddress |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	proxyURL, err := url.Parse(wm.proxyAddress) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("failed to parse proxy URL: %v", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	transport := &http.Transport{ |  | ||||||
| 		TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	switch proxyURL.Scheme { |  | ||||||
| 	case "socks5": |  | ||||||
| 		return wm.createSocks5Dialer(proxyURL) |  | ||||||
| 	case "http", "https": |  | ||||||
| 		transport.Proxy = http.ProxyURL(proxyURL) |  | ||||||
| 		return &websocket.Dialer{Proxy: transport.Proxy, TLSClientConfig: transport.TLSClientConfig}, nil |  | ||||||
| 	default: |  | ||||||
| 		return nil, fmt.Errorf("unsupported proxy scheme: %s", proxyURL.Scheme) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (wm *BinanceWebSocketManager) createSocks5Dialer(proxyURL *url.URL) (*websocket.Dialer, error) { |  | ||||||
| 	auth := &proxy.Auth{} |  | ||||||
| 	if proxyURL.User != nil { |  | ||||||
| 		auth.User = proxyURL.User.Username() |  | ||||||
| 		auth.Password, _ = proxyURL.User.Password() |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	socksDialer, err := proxy.SOCKS5("tcp", proxyURL.Host, auth, proxy.Direct) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("failed to create SOCKS5 proxy dialer: %v", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return &websocket.Dialer{ |  | ||||||
| 		NetDialContext: func(ctx context.Context, network, address string) (net.Conn, error) { |  | ||||||
| 			return socksDialer.Dial(network, address) |  | ||||||
| 		}, |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // 复用创建HTTP客户端的逻辑 | // 复用创建HTTP客户端的逻辑 | ||||||
| func (wm *BinanceWebSocketManager) createBinanceClient() (*helper.BinanceClient, error) { | func (wm *BinanceWebSocketManager) createBinanceClient() (*helper.BinanceClient, error) { | ||||||
| 	return helper.NewBinanceClient(wm.apiKey, wm.apiSecret, wm.proxyType, wm.proxyAddress) | 	return helper.NewBinanceClient(wm.apiKey, wm.apiSecret, wm.proxyType, wm.proxyAddress) | ||||||
| @ -450,7 +538,6 @@ func (wm *BinanceWebSocketManager) getListenKey() (string, error) { | |||||||
|  |  | ||||||
| // 接收消息 | // 接收消息 | ||||||
| func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) { | func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) { | ||||||
| 	defer wm.ws.Close() |  | ||||||
|  |  | ||||||
| 	for { | 	for { | ||||||
| 		select { | 		select { | ||||||
| @ -462,20 +549,23 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) { | |||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			_, msg, err := wm.ws.ReadMessage() | 			_, msg, err := wm.ws.ReadMessage() | ||||||
| 			if err != nil && strings.Contains(err.Error(), "websocket: close") { | 			if err != nil { | ||||||
| 				if !wm.isStopped { | 				// 检查是否是由于我们主动关闭连接导致的错误 | ||||||
| 					log.Error("收到关闭消息", err.Error()) | 				if wm.isStopped { | ||||||
| 					wm.triggerReconnect(false) | 					log.Infof("WebSocket read loop gracefully stopped for key: %s", wm.apiKey) | ||||||
|  | 					return | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				log.Error("websocket 关闭") | 				// 如果不是主动关闭,再判断是否是连接关闭错误,并触发重连 | ||||||
| 				return | 				if strings.Contains(err.Error(), "websocket: close") { | ||||||
| 			} else if err != nil { | 					log.Error("WebSocket connection closed unexpectedly, triggering reconnect for key: %s. Error: %v", wm.apiKey, err) | ||||||
| 				log.Error("读取消息时出错: %v", err) | 					wm.triggerReconnect(false) | ||||||
| 				return | 				} else { | ||||||
|  | 					log.Error("Error reading message for key: %s. Error: %v", wm.apiKey, err) | ||||||
|  | 				} | ||||||
|  | 				return // 任何错误发生后都应退出读取循环 | ||||||
| 			} | 			} | ||||||
| 			wm.handleOrderUpdate(msg) | 			wm.handleOrderUpdate(msg) | ||||||
|  |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @ -483,165 +573,32 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) { | |||||||
| func (wm *BinanceWebSocketManager) handleOrderUpdate(msg []byte) { | func (wm *BinanceWebSocketManager) handleOrderUpdate(msg []byte) { | ||||||
| 	setLastTime(wm) | 	setLastTime(wm) | ||||||
|  |  | ||||||
| 	if reconnect, _ := ReceiveListen(msg, wm.wsType, wm.apiKey); reconnect { | 	if reconnect, fatal, err := ReceiveListen(msg, wm.wsType, wm.apiKey); err != nil { | ||||||
| 		log.Errorf("收到重连请求") | 		log.Errorf("处理消息时出错: %v", err) | ||||||
|  | 		// 根据错误类型决定是否停止 | ||||||
|  | 		if fatal { | ||||||
|  | 			log.Errorf("收到致命错误,将停止 WebSocket 管理器 for key: %s", wm.apiKey) | ||||||
|  | 			wm.Stop() | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} else if fatal { | ||||||
|  | 		log.Errorf("收到致命错误,将停止 WebSocket 管理器 for key: %s", wm.apiKey) | ||||||
|  | 		wm.Stop() | ||||||
|  | 		return | ||||||
|  | 	} else if reconnect { | ||||||
|  | 		log.Warnf("收到重连请求,将触发重连 for key: %s", wm.apiKey) | ||||||
| 		wm.triggerReconnect(false) | 		wm.triggerReconnect(false) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Stop 安全停止 WebSocket | // closeConn 安全地关闭WebSocket连接 | ||||||
| func (wm *BinanceWebSocketManager) Stop() { | func (wm *BinanceWebSocketManager) closeConn() { | ||||||
| 	wm.mu.Lock() | 	wm.mu.Lock() | ||||||
| 	defer wm.mu.Unlock() | 	defer wm.mu.Unlock() | ||||||
|  |  | ||||||
| 	if wm.isStopped { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	wm.isStopped = true |  | ||||||
|  |  | ||||||
| 	select { |  | ||||||
| 	case <-wm.stopChannel: |  | ||||||
| 	default: |  | ||||||
| 		close(wm.stopChannel) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if wm.cancelFunc != nil { |  | ||||||
| 		wm.cancelFunc() |  | ||||||
| 		wm.cancelFunc = nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if wm.ws != nil { | 	if wm.ws != nil { | ||||||
| 		if err := wm.ws.Close(); err != nil { | 		_ = wm.ws.Close() | ||||||
| 			log.Errorf("WebSocket Close 错误 key:%s err:%v", wm.apiKey, err) |  | ||||||
| 		} |  | ||||||
| 		wm.ws = nil | 		wm.ws = nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Infof("WebSocket 已完全停止 key:%s", wm.apiKey) |  | ||||||
| 	wm.stopChannel = make(chan struct{}, 10) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // handleReconnect 使用指数退避并保持永不退出 |  | ||||||
| func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) { |  | ||||||
| 	const maxRetries = 100 |  | ||||||
| 	baseDelay := time.Second * 2 |  | ||||||
| 	retryCount := 0 |  | ||||||
|  |  | ||||||
| 	for { |  | ||||||
| 		select { |  | ||||||
| 		case <-ctx.Done(): |  | ||||||
| 			log.Infof("handleReconnect context done: %s", wm.apiKey) |  | ||||||
| 			return |  | ||||||
|  |  | ||||||
| 		case <-wm.reconnect: |  | ||||||
| 			wm.mu.Lock() |  | ||||||
| 			if wm.isStopped { |  | ||||||
| 				wm.mu.Unlock() |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 			wm.mu.Unlock() |  | ||||||
|  |  | ||||||
| 			log.Warnf("WebSocket 连接断开,准备重连 key:%s", wm.apiKey) |  | ||||||
|  |  | ||||||
| 			for { |  | ||||||
| 				wm.mu.Lock() |  | ||||||
| 				if wm.ws != nil { |  | ||||||
| 					_ = wm.ws.Close() |  | ||||||
| 					wm.ws = nil |  | ||||||
| 				} |  | ||||||
| 				if wm.cancelFunc != nil { |  | ||||||
| 					wm.cancelFunc() |  | ||||||
| 					wm.cancelFunc = nil |  | ||||||
| 				} |  | ||||||
| 				wm.mu.Unlock() |  | ||||||
|  |  | ||||||
| 				newCtx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 				wm.mu.Lock() |  | ||||||
| 				wm.cancelFunc = cancel |  | ||||||
| 				wm.mu.Unlock() |  | ||||||
|  |  | ||||||
| 				if err := wm.connect(newCtx); err != nil { |  | ||||||
| 					log.Errorf("🔌 重连失败(%d/%d)key:%s,err: %v", retryCount+1, maxRetries, wm.apiKey, err) |  | ||||||
| 					cancel() |  | ||||||
| 					retryCount++ |  | ||||||
|  |  | ||||||
| 					if retryCount >= maxRetries { |  | ||||||
| 						log.Errorf("❌ 重连失败次数过多,停止重连逻辑 key:%s", wm.apiKey) |  | ||||||
| 						wm.reconnecting.Store(false) |  | ||||||
| 						return |  | ||||||
| 					} |  | ||||||
|  |  | ||||||
| 					delay := baseDelay * time.Duration(1<<retryCount) |  | ||||||
| 					if delay > time.Minute*5 { |  | ||||||
| 						delay = time.Minute * 5 |  | ||||||
| 					} |  | ||||||
| 					log.Warnf("等待 %v 后重试...", delay) |  | ||||||
| 					time.Sleep(delay) |  | ||||||
| 					continue |  | ||||||
| 				} |  | ||||||
|  |  | ||||||
| 				log.Infof("✅ 重连成功 key:%s", wm.apiKey) |  | ||||||
| 				retryCount = 0 |  | ||||||
| 				wm.reconnecting.Store(false) |  | ||||||
|  |  | ||||||
| 				// ✅ 重连成功后开启假死检测 |  | ||||||
| 				utility.SafeGo(func() { wm.startDeadCheck(newCtx) }) |  | ||||||
|  |  | ||||||
| 				break |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // startDeadCheck 替代 Start 中的定时器,绑定连接生命周期 |  | ||||||
| func (wm *BinanceWebSocketManager) startDeadCheck(ctx context.Context) { |  | ||||||
| 	ticker := time.NewTicker(1 * time.Minute) |  | ||||||
| 	defer ticker.Stop() |  | ||||||
|  |  | ||||||
| 	for { |  | ||||||
| 		select { |  | ||||||
| 		case <-ctx.Done(): |  | ||||||
| 			return |  | ||||||
| 		case <-ticker.C: |  | ||||||
| 			if wm.isStopped { |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 			wm.DeadCheck() |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // 假死检测 |  | ||||||
| func (wm *BinanceWebSocketManager) DeadCheck() { |  | ||||||
| 	subKey := fmt.Sprintf(global.USER_SUBSCRIBE, wm.apiKey) |  | ||||||
| 	val, _ := helper.DefaultRedis.GetString(subKey) |  | ||||||
| 	if val == "" { |  | ||||||
| 		log.Warnf("没有订阅信息,无法进行假死检测") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var data binancedto.UserSubscribeState |  | ||||||
| 	_ = sonic.Unmarshal([]byte(val), &data) |  | ||||||
|  |  | ||||||
| 	var lastTime *time.Time |  | ||||||
| 	if wm.wsType == 0 { |  | ||||||
| 		lastTime = data.SpotLastTime |  | ||||||
| 	} else { |  | ||||||
| 		lastTime = data.FuturesLastTime |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// 定义最大静默时间(超出视为假死) |  | ||||||
| 	var timeout time.Duration |  | ||||||
| 	if wm.wsType == 0 { |  | ||||||
| 		timeout = 40 * time.Second // Spot 每 20s ping,40s 足够 |  | ||||||
| 	} else { |  | ||||||
| 		timeout = 6 * time.Minute // Futures 每 3 分钟 ping |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if lastTime != nil && time.Since(*lastTime) > timeout { |  | ||||||
| 		log.Warnf("检测到假死连接 key:%s type:%v, 距离上次通信: %v, 触发重连", wm.apiKey, wm.wsType, time.Since(*lastTime)) |  | ||||||
| 		wm.triggerReconnect(true) |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // 主动心跳发送机制 | // 主动心跳发送机制 | ||||||
| @ -657,10 +614,10 @@ func (wm *BinanceWebSocketManager) startPingLoop(ctx context.Context) { | |||||||
| 			if wm.isStopped { | 			if wm.isStopped { | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| 			err := wm.ws.WriteMessage(websocket.PingMessage, []byte("ping")) | 			if err := wm.sendPing(); err != nil { | ||||||
| 			if err != nil { |  | ||||||
| 				log.Error("主动 Ping Binance 失败:", err) | 				log.Error("主动 Ping Binance 失败:", err) | ||||||
| 				wm.triggerReconnect(false) | 				wm.triggerReconnect(false) | ||||||
|  | 				return // 退出循环,等待重连 | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @ -784,3 +741,112 @@ func randomHex(n int) string { | |||||||
| 	} | 	} | ||||||
| 	return string(bytes) | 	return string(bytes) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 启动24小时强制重连定时器 | ||||||
|  |  * @param ctx 上下文 | ||||||
|  |  */ | ||||||
|  | func (wm *BinanceWebSocketManager) start24HourReconnectTimer(ctx context.Context) { | ||||||
|  | 	// Binance要求不到24小时就需要主动断开重连 | ||||||
|  | 	// 设置为23小时50分钟,留出一些缓冲时间 | ||||||
|  | 	duration := 23*time.Hour + 50*time.Minute | ||||||
|  |  | ||||||
|  | 	wm.forceReconnectTimer = time.NewTimer(duration) | ||||||
|  | 	defer func() { | ||||||
|  | 		if wm.forceReconnectTimer != nil { | ||||||
|  | 			wm.forceReconnectTimer.Stop() | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	select { | ||||||
|  | 	case <-ctx.Done(): | ||||||
|  | 		return | ||||||
|  | 	case <-wm.forceReconnectTimer.C: | ||||||
|  | 		if !wm.isStopped { | ||||||
|  | 			log.Warnf("24小时强制重连触发 key:%s wsType:%v", wm.apiKey, wm.wsType) | ||||||
|  | 			wm.triggerReconnect(true) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 改进的假死检测机制 | ||||||
|  |  * @param ctx 上下文 | ||||||
|  |  */ | ||||||
|  | func (wm *BinanceWebSocketManager) startDeadCheck(ctx context.Context) { | ||||||
|  | 	// 缩短检测间隔,提高响应速度 | ||||||
|  | 	ticker := time.NewTicker(30 * time.Second) | ||||||
|  | 	defer ticker.Stop() | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case <-ctx.Done(): | ||||||
|  | 			return | ||||||
|  | 		case <-ticker.C: | ||||||
|  | 			if wm.isStopped { | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			wm.DeadCheck() | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 改进的假死检测逻辑 | ||||||
|  |  */ | ||||||
|  | func (wm *BinanceWebSocketManager) DeadCheck() { | ||||||
|  | 	// 检查最后ping时间 | ||||||
|  | 	lastPing := wm.lastPingTime.Load().(time.Time) | ||||||
|  |  | ||||||
|  | 	// 定义最大静默时间(根据WebSocket类型调整) | ||||||
|  | 	var timeout time.Duration | ||||||
|  | 	if wm.wsType == 0 { | ||||||
|  | 		timeout = 3 * time.Minute // 现货:3分钟无ping视为假死 | ||||||
|  | 	} else { | ||||||
|  | 		timeout = 6 * time.Minute // 合约:6分钟无ping视为假死 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if time.Since(lastPing) > timeout { | ||||||
|  | 		log.Warnf("检测到假死连接(基于ping时间) key:%s type:%v, 距离上次ping: %v, 触发重连", | ||||||
|  | 			wm.apiKey, wm.wsType, time.Since(lastPing)) | ||||||
|  | 		wm.triggerReconnect(true) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 同时检查Redis中的记录作为备用检测 | ||||||
|  | 	subKey := fmt.Sprintf(global.USER_SUBSCRIBE, wm.apiKey) | ||||||
|  | 	val, _ := helper.DefaultRedis.GetString(subKey) | ||||||
|  | 	if val == "" { | ||||||
|  | 		log.Debugf("没有订阅信息,跳过Redis假死检测") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var data binancedto.UserSubscribeState | ||||||
|  | 	_ = sonic.Unmarshal([]byte(val), &data) | ||||||
|  |  | ||||||
|  | 	var lastTime *time.Time | ||||||
|  | 	if wm.wsType == 0 { | ||||||
|  | 		lastTime = data.SpotLastTime | ||||||
|  | 	} else { | ||||||
|  | 		lastTime = data.FuturesLastTime | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Redis记录的超时时间设置得更长一些 | ||||||
|  | 	var redisTimeout time.Duration | ||||||
|  | 	if wm.wsType == 0 { | ||||||
|  | 		redisTimeout = 3 * time.Minute | ||||||
|  | 	} else { | ||||||
|  | 		redisTimeout = 6 * time.Minute | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if lastTime != nil && time.Since(*lastTime) > redisTimeout { | ||||||
|  | 		log.Warnf("检测到假死连接(基于Redis记录) key:%s type:%v, 距离上次通信: %v, 触发重连", | ||||||
|  | 			wm.apiKey, wm.wsType, time.Since(*lastTime)) | ||||||
|  | 		wm.triggerReconnect(true) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 优化的重连处理逻辑 | ||||||
|  |  * @param ctx 上下文 | ||||||
|  |  */ | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"go-admin/models" | 	"go-admin/models" | ||||||
| @ -19,6 +20,7 @@ type BinanceWs struct { | |||||||
| 	proxyUrl        string | 	proxyUrl        string | ||||||
| 	WorkType        string | 	WorkType        string | ||||||
| 	wsConns         []*WsConn | 	wsConns         []*WsConn | ||||||
|  | 	mu              sync.Mutex // 新增互斥锁 | ||||||
| 	tickerCallback  func(models.Ticker24, string, string) | 	tickerCallback  func(models.Ticker24, string, string) | ||||||
| 	forceCallback   func(models.ForceOrder, string, string) | 	forceCallback   func(models.ForceOrder, string, string) | ||||||
| 	depthCallback   func(models.DepthBin, string, string) | 	depthCallback   func(models.DepthBin, string, string) | ||||||
| @ -70,11 +72,15 @@ func (bnWs *BinanceWs) subscribe(endpoint string, handle func(msg []byte) error) | |||||||
| 	if wsConn == nil { | 	if wsConn == nil { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 	bnWs.mu.Lock() | ||||||
| 	bnWs.wsConns = append(bnWs.wsConns, wsConn) | 	bnWs.wsConns = append(bnWs.wsConns, wsConn) | ||||||
|  | 	bnWs.mu.Unlock() | ||||||
| 	go bnWs.exitHandler(wsConn) | 	go bnWs.exitHandler(wsConn) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (bnWs *BinanceWs) Close() { | func (bnWs *BinanceWs) Close() { | ||||||
|  | 	bnWs.mu.Lock() | ||||||
|  | 	defer bnWs.mu.Unlock() | ||||||
| 	for _, con := range bnWs.wsConns { | 	for _, con := range bnWs.wsConns { | ||||||
| 		con.CloseWs() | 		con.CloseWs() | ||||||
| 	} | 	} | ||||||
|  | |||||||
							
								
								
									
										218
									
								
								services/excservice/bitget_receive.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								services/excservice/bitget_receive.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,218 @@ | |||||||
|  | package excservice | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"go-admin/pkg/utility" | ||||||
|  |  | ||||||
|  | 	"github.com/bytedance/sonic" | ||||||
|  | 	log "github.com/go-admin-team/go-admin-core/logger" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | /* | ||||||
|  | Bitget用户订单订阅处理 | ||||||
|  |   - @msg 消息内容 | ||||||
|  |   - @listenType 订阅类型 0-现货 1-合约 | ||||||
|  | */ | ||||||
|  | func ReceiveBitgetListen(msg []byte, listenType int, apiKey string) (reconnect bool, err error) { | ||||||
|  | 	var dataMap map[string]interface{} | ||||||
|  | 	err = sonic.Unmarshal(msg, &dataMap) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("接收Bitget ws 反序列化失败:", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 检查是否是操作响应 | ||||||
|  | 	if op, exists := dataMap["op"]; exists { | ||||||
|  | 		switch op { | ||||||
|  | 		case "login": | ||||||
|  | 			return handleBitgetLogin(dataMap, apiKey) | ||||||
|  | 		case "subscribe": | ||||||
|  | 			return handleBitgetSubscribe(dataMap, apiKey) | ||||||
|  | 		case "unsubscribe": | ||||||
|  | 			return handleBitgetUnsubscribe(dataMap, apiKey) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 检查是否是数据推送 | ||||||
|  | 	if arg, exists := dataMap["arg"]; exists { | ||||||
|  | 		return handleBitgetDataPush(dataMap, arg, listenType, apiKey) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Info("未知Bitget事件 内容:", string(msg)) | ||||||
|  | 	return false, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleBitgetLogin 处理登录响应 | ||||||
|  | func handleBitgetLogin(dataMap map[string]interface{}, apiKey string) (bool, error) { | ||||||
|  | 	if code, exists := dataMap["code"]; exists { | ||||||
|  | 		if code == "0" || code == 0 { | ||||||
|  | 			log.Info("Bitget WebSocket登录成功 key:%s", apiKey) | ||||||
|  | 		} else { | ||||||
|  | 			log.Error("Bitget WebSocket登录失败 key:%s code:%v", apiKey, code) | ||||||
|  | 			return true, nil // 需要重连 | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleBitgetSubscribe 处理订阅响应 | ||||||
|  | func handleBitgetSubscribe(dataMap map[string]interface{}, apiKey string) (bool, error) { | ||||||
|  | 	if code, exists := dataMap["code"]; exists { | ||||||
|  | 		if code == "0" || code == 0 { | ||||||
|  | 			log.Info("Bitget WebSocket订阅成功 key:%s", apiKey) | ||||||
|  | 		} else { | ||||||
|  | 			log.Error("Bitget WebSocket订阅失败 key:%s code:%v", apiKey, code) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleBitgetUnsubscribe 处理取消订阅响应 | ||||||
|  | func handleBitgetUnsubscribe(dataMap map[string]interface{}, apiKey string) (bool, error) { | ||||||
|  | 	if code, exists := dataMap["code"]; exists { | ||||||
|  | 		if code == "0" || code == 0 { | ||||||
|  | 			log.Info("Bitget WebSocket取消订阅成功 key:%s", apiKey) | ||||||
|  | 		} else { | ||||||
|  | 			log.Error("Bitget WebSocket取消订阅失败 key:%s code:%v", apiKey, code) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleBitgetDataPush 处理数据推送 | ||||||
|  | func handleBitgetDataPush(dataMap map[string]interface{}, arg interface{}, listenType int, apiKey string) (bool, error) { | ||||||
|  | 	argMap, ok := arg.(map[string]interface{}) | ||||||
|  | 	if !ok { | ||||||
|  | 		log.Error("Bitget数据推送参数格式错误 key:%s", apiKey) | ||||||
|  | 		return false, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	channel, exists := argMap["channel"] | ||||||
|  | 	if !exists { | ||||||
|  | 		log.Error("Bitget数据推送缺少channel参数 key:%s", apiKey) | ||||||
|  | 		return false, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	switch channel { | ||||||
|  | 	case "account": | ||||||
|  | 		return handleBitgetAccount(dataMap, listenType, apiKey) | ||||||
|  | 	case "orders": | ||||||
|  | 		return handleBitgetOrders(dataMap, listenType, apiKey) | ||||||
|  | 	case "positions": | ||||||
|  | 		return handleBitgetPositions(dataMap, listenType, apiKey) | ||||||
|  | 	case "balance": | ||||||
|  | 		return handleBitgetBalance(dataMap, listenType, apiKey) | ||||||
|  | 	default: | ||||||
|  | 		log.Info("收到Bitget未处理的频道数据 channel:%v key:%s", channel, apiKey) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return false, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleBitgetAccount 处理账户更新 | ||||||
|  | func handleBitgetAccount(dataMap map[string]interface{}, listenType int, apiKey string) (bool, error) { | ||||||
|  | 	log.Info("收到Bitget账户更新 listenType:%d key:%s", listenType, apiKey) | ||||||
|  |  | ||||||
|  | 	data, exists := dataMap["data"] | ||||||
|  | 	if !exists { | ||||||
|  | 		return false, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 这里可以根据需要实现具体的账户更新处理逻辑 | ||||||
|  | 	// 类似于binanceservice.ChangeSpotOrder或binanceservice.ChangeFutureOrder | ||||||
|  |  | ||||||
|  | 	utility.SafeGo(func() { | ||||||
|  | 		handleBitgetAccountUpdate(data, listenType, apiKey) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	return false, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleBitgetOrders 处理订单更新 | ||||||
|  | func handleBitgetOrders(dataMap map[string]interface{}, listenType int, apiKey string) (bool, error) { | ||||||
|  | 	log.Info("收到Bitget订单更新 listenType:%d key:%s", listenType, apiKey) | ||||||
|  |  | ||||||
|  | 	data, exists := dataMap["data"] | ||||||
|  | 	if !exists { | ||||||
|  | 		return false, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	utility.SafeGo(func() { | ||||||
|  | 		handleBitgetOrderUpdate(data, listenType, apiKey) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	return false, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleBitgetPositions 处理持仓更新 | ||||||
|  | func handleBitgetPositions(dataMap map[string]interface{}, listenType int, apiKey string) (bool, error) { | ||||||
|  | 	log.Info("收到Bitget持仓更新 listenType:%d key:%s", listenType, apiKey) | ||||||
|  |  | ||||||
|  | 	data, exists := dataMap["data"] | ||||||
|  | 	if !exists { | ||||||
|  | 		return false, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	utility.SafeGo(func() { | ||||||
|  | 		handleBitgetPositionUpdate(data, listenType, apiKey) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	return false, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleBitgetBalance 处理余额更新 | ||||||
|  | func handleBitgetBalance(dataMap map[string]interface{}, listenType int, apiKey string) (bool, error) { | ||||||
|  | 	log.Info("收到Bitget余额更新 listenType:%d key:%s", listenType, apiKey) | ||||||
|  |  | ||||||
|  | 	data, exists := dataMap["data"] | ||||||
|  | 	if !exists { | ||||||
|  | 		return false, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	utility.SafeGo(func() { | ||||||
|  | 		handleBitgetBalanceUpdate(data, listenType, apiKey) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	return false, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 具体的处理函数,可以根据业务需求实现 | ||||||
|  |  | ||||||
|  | // handleBitgetAccountUpdate 处理账户更新的具体逻辑 | ||||||
|  | func handleBitgetAccountUpdate(data interface{}, listenType int, apiKey string) { | ||||||
|  | 	// TODO: 实现具体的账户更新处理逻辑 | ||||||
|  | 	log.Info("处理Bitget账户更新 listenType:%d key:%s data:%v", listenType, apiKey, data) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleBitgetOrderUpdate 处理订单更新的具体逻辑 | ||||||
|  | func handleBitgetOrderUpdate(data interface{}, listenType int, apiKey string) { | ||||||
|  | 	// TODO: 实现具体的订单更新处理逻辑 | ||||||
|  | 	log.Info("处理Bitget订单更新 listenType:%d key:%s data:%v", listenType, apiKey, data) | ||||||
|  |  | ||||||
|  | 	// 这里可以参考binanceservice中的订单处理逻辑 | ||||||
|  | 	// 例如:更新数据库中的订单状态、触发回调等 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleBitgetPositionUpdate 处理持仓更新的具体逻辑 | ||||||
|  | func handleBitgetPositionUpdate(data interface{}, listenType int, apiKey string) { | ||||||
|  | 	// TODO: 实现具体的持仓更新处理逻辑 | ||||||
|  | 	log.Info("处理Bitget持仓更新 listenType:%d key:%s data:%v", listenType, apiKey, data) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleBitgetBalanceUpdate 处理余额更新的具体逻辑 | ||||||
|  | func handleBitgetBalanceUpdate(data interface{}, listenType int, apiKey string) { | ||||||
|  | 	// TODO: 实现具体的余额更新处理逻辑 | ||||||
|  | 	log.Info("处理Bitget余额更新 listenType:%d key:%s data:%v", listenType, apiKey, data) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getBitgetWsTypeName 获取WebSocket类型名称 | ||||||
|  | func getBitgetWsTypeName(listenType int) string { | ||||||
|  | 	switch listenType { | ||||||
|  | 	case 0: | ||||||
|  | 		return "现货" | ||||||
|  | 	case 1: | ||||||
|  | 		return "合约" | ||||||
|  | 	default: | ||||||
|  | 		return "未知" | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										108
									
								
								services/orderservice/order_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								services/orderservice/order_service.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,108 @@ | |||||||
|  | package orderservice | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"go-admin/app/admin/models" | ||||||
|  | 	"go-admin/app/admin/service/dto" | ||||||
|  | 	"go-admin/pkg/utility" | ||||||
|  | 	"go-admin/pkg/utility/snowflakehelper" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/go-admin-team/go-admin-core/sdk/service" | ||||||
|  | 	"github.com/jinzhu/copier" | ||||||
|  | 	"github.com/shopspring/decimal" | ||||||
|  | 	"gorm.io/gorm" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type OrderService struct { | ||||||
|  | 	service.Service | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 创建减仓后减仓单 | ||||||
|  | func (e *OrderService) CreateReduceReduceOrder(db *gorm.DB, pid int, price, num decimal.Decimal, priceDigit int) (models.LinePreOrder, error) { | ||||||
|  | 	var preOrder models.LinePreOrder | ||||||
|  | 	var result models.LinePreOrder | ||||||
|  | 	var ext models.LinePreOrderExt | ||||||
|  |  | ||||||
|  | 	if err := db.Model(&models.LinePreOrder{}).Preload("Childs").Where("id =? ", pid).First(&preOrder).Error; err != nil { | ||||||
|  | 		return preOrder, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := db.Model(&models.LinePreOrderExt{}).Where("order_id =? ", pid).Find(&ext).Error; err != nil { | ||||||
|  | 		return preOrder, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	copier.Copy(&result, &preOrder) | ||||||
|  |  | ||||||
|  | 	result.Id = 0 | ||||||
|  | 	result.OrderSn = utility.Int64ToString(snowflakehelper.GetOrderId()) | ||||||
|  | 	result.Status = 0 | ||||||
|  | 	result.CreatedAt = time.Now() | ||||||
|  | 	result.TriggerTime = nil | ||||||
|  | 	result.UpdatedAt = time.Now() | ||||||
|  | 	result.BuyPrice = decimal.Zero.String() | ||||||
|  | 	result.Price = price.String() | ||||||
|  | 	result.Num = num.String() | ||||||
|  | 	result.ReduceOrderId = preOrder.Id | ||||||
|  |  | ||||||
|  | 	for index := range result.Childs { | ||||||
|  | 		result.Childs[index].Id = 0 | ||||||
|  | 		result.Childs[index].OrderSn = utility.Int64ToString(snowflakehelper.GetOrderId()) | ||||||
|  | 		result.Childs[index].Status = 0 | ||||||
|  | 		result.Childs[index].CreatedAt = time.Now() | ||||||
|  | 		result.Childs[index].TriggerTime = nil | ||||||
|  | 		result.Childs[index].UpdatedAt = time.Now() | ||||||
|  | 		result.Childs[index].BuyPrice = decimal.Zero.String() | ||||||
|  | 		var pricePercent decimal.Decimal | ||||||
|  |  | ||||||
|  | 		if result.Childs[index].OrderType == 1 && ext.TakeProfitRatio.Cmp(decimal.Zero) > 0 { | ||||||
|  | 			// 减仓单卖出 | ||||||
|  | 			if preOrder.Site == "SELL" { | ||||||
|  | 				pricePercent = decimal.NewFromInt(100).Add(ext.TakeProfitRatio).Div(decimal.NewFromInt(100)) | ||||||
|  | 			} else { | ||||||
|  | 				pricePercent = decimal.NewFromInt(100).Sub(ext.TakeProfitRatio).Div(decimal.NewFromInt(100)) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 		} else if result.Childs[index].OrderType == 2 && ext.StopLossRatio.Cmp(decimal.Zero) > 0 { | ||||||
|  | 			if preOrder.Site == "SELL" { | ||||||
|  | 				pricePercent = decimal.NewFromInt(100).Sub(ext.StopLossRatio).Div(decimal.NewFromInt(100)) | ||||||
|  | 			} else { | ||||||
|  | 				pricePercent = decimal.NewFromInt(100).Add(ext.StopLossRatio).Div(decimal.NewFromInt(100)) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		//重新计算止盈止损价 | ||||||
|  | 		if pricePercent.Cmp(decimal.Zero) > 0 { | ||||||
|  | 			result.Childs[index].Price = price.Mul(pricePercent).Truncate(int32(priceDigit)).String() | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := db.Create(&result).Error; err != nil { | ||||||
|  | 		return result, fmt.Errorf("复制减仓单失败:pid:%d err:%v", pid, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return result, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 触发主单失败 | ||||||
|  | func (e *OrderService) ErrorTrigger(preOrder *dto.PreOrderRedisList, errData string) error { | ||||||
|  | 	// var cacheOrderKey string | ||||||
|  |  | ||||||
|  | 	// if preOrder.OrderType == 1 { | ||||||
|  | 	// 	cacheOrderKey = fmt.Sprintf(rediskey.PreSpotOrderList, preOrder.ExchangeType) | ||||||
|  | 	// } else { | ||||||
|  | 	// 	cacheOrderKey = fmt.Sprintf(rediskey.PreFutOrderList, preOrder.ExchangeType) | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | 	if err := e.Orm.Model(&models.LinePreOrder{}). | ||||||
|  | 		Where("id = ? and status =1", preOrder.Id). | ||||||
|  | 		Updates(map[string]interface{}{ | ||||||
|  | 			"status":       2, | ||||||
|  | 			"desc":         errData, | ||||||
|  | 			"trigger_time": time.Now(), | ||||||
|  | 		}).Error; err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										63
									
								
								services/proxy/proxy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								services/proxy/proxy.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | |||||||
|  | package proxy | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"crypto/tls" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/gorilla/websocket" | ||||||
|  | 	"golang.org/x/net/proxy" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // GetDialer 获取代理拨号器 | ||||||
|  | func GetDialer(proxyType, proxyAddress string) (*websocket.Dialer, error) { | ||||||
|  | 	if proxyAddress == "" { | ||||||
|  | 		return &websocket.Dialer{}, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !strings.HasPrefix(proxyAddress, "http://") && !strings.HasPrefix(proxyAddress, "https://") && !strings.HasPrefix(proxyAddress, "socks5://") { | ||||||
|  | 		proxyAddress = proxyType + "://" + proxyAddress | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	proxyURL, err := url.Parse(proxyAddress) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to parse proxy URL: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	transport := &http.Transport{ | ||||||
|  | 		TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	switch proxyURL.Scheme { | ||||||
|  | 	case "socks5": | ||||||
|  | 		return createSocks5Dialer(proxyURL) | ||||||
|  | 	case "http", "https": | ||||||
|  | 		transport.Proxy = http.ProxyURL(proxyURL) | ||||||
|  | 		return &websocket.Dialer{Proxy: transport.Proxy, TLSClientConfig: transport.TLSClientConfig}, nil | ||||||
|  | 	default: | ||||||
|  | 		return nil, fmt.Errorf("unsupported proxy scheme: %s", proxyURL.Scheme) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func createSocks5Dialer(proxyURL *url.URL) (*websocket.Dialer, error) { | ||||||
|  | 	auth := &proxy.Auth{} | ||||||
|  | 	if proxyURL.User != nil { | ||||||
|  | 		auth.User = proxyURL.User.Username() | ||||||
|  | 		auth.Password, _ = proxyURL.User.Password() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	socksDialer, err := proxy.SOCKS5("tcp", proxyURL.Host, auth, proxy.Direct) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to create SOCKS5 proxy dialer: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &websocket.Dialer{ | ||||||
|  | 		NetDialContext: func(ctx context.Context, network, address string) (net.Conn, error) { | ||||||
|  | 			return socksDialer.Dial(network, address) | ||||||
|  | 		}, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
| @ -182,14 +182,7 @@ func handleTickerMessage(msg []byte) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	pairs := make([]map[string]interface{}, 0) |  | ||||||
| 	trades := make([]models.TradeSet, 0) | 	trades := make([]models.TradeSet, 0) | ||||||
| 	pairsVal, _ := helper.DefaultRedis.GetString(rediskey.SpotSymbolTicker) |  | ||||||
|  |  | ||||||
| 	if err := sonic.UnmarshalString(pairsVal, &pairs); err != nil { |  | ||||||
| 		log.Error("解码ticker消息失败", zap.Error(err)) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for _, dataMap := range streamResp.Data { | 	for _, dataMap := range streamResp.Data { | ||||||
| 		symbolName, ok := dataMap["s"].(string) | 		symbolName, ok := dataMap["s"].(string) | ||||||
| @ -232,15 +225,6 @@ func handleTickerMessage(msg []byte) { | |||||||
| 		tradeSet.Volume = utility.StringFloat64Cut(dataMap["v"].(string), int32(tradeSet.AmountDigit)) | 		tradeSet.Volume = utility.StringFloat64Cut(dataMap["v"].(string), int32(tradeSet.AmountDigit)) | ||||||
| 		tradeSet.QuoteVolume = utility.StringFloat64Cut(dataMap["q"].(string), 5) | 		tradeSet.QuoteVolume = utility.StringFloat64Cut(dataMap["q"].(string), 5) | ||||||
|  |  | ||||||
| 		for index := range pairs { |  | ||||||
| 			if symbol, ok := pairs[index]["symbol"].(string); ok { |  | ||||||
| 				if symbol == symbolName { |  | ||||||
| 					pairs[index]["price"] = tradeSet.LastPrice |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		trades = append(trades, tradeSet) | 		trades = append(trades, tradeSet) | ||||||
| 		tcVal, _ = sonic.MarshalString(&tradeSet) | 		tcVal, _ = sonic.MarshalString(&tradeSet) | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										68
									
								
								services/tradsetservice/trade_set.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								services/tradsetservice/trade_set.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | |||||||
|  | package tradsetservice | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"go-admin/common/global" | ||||||
|  | 	"go-admin/common/helper" | ||||||
|  | 	"go-admin/models" | ||||||
|  |  | ||||||
|  | 	"github.com/bytedance/sonic" | ||||||
|  | 	"github.com/go-admin-team/go-admin-core/logger" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // 获取交易所交易对 | ||||||
|  | // symbolType 1:spot 2:future | ||||||
|  | func GetExchangeTradeSets(exchange string, symbolType int) (*[]models.TradeSet, error) { | ||||||
|  | 	var tradeSets []models.TradeSet | ||||||
|  | 	var scanKey string | ||||||
|  |  | ||||||
|  | 	if symbolType == 1 { | ||||||
|  | 		scanKey = fmt.Sprintf(global.TICKER_SPOT, exchange, "*") | ||||||
|  | 	} else if symbolType == 2 { | ||||||
|  | 		scanKey = fmt.Sprintf(global.TICKER_FUTURES, exchange, "*") | ||||||
|  | 	} else { | ||||||
|  | 		return nil, fmt.Errorf("symbolType error") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	vals, err := helper.DefaultRedis.GetAllKeysAndValues(scanKey) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, val := range vals { | ||||||
|  | 		var tradeSet models.TradeSet | ||||||
|  | 		if err := sonic.Unmarshal([]byte(val), &tradeSet); err != nil { | ||||||
|  | 			logger.Errorf("unmarshal tradeSet err:%v", err) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		tradeSets = append(tradeSets, tradeSet) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &tradeSets, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 获取单个交易对 | ||||||
|  | func GetExchangeTradeSet(exchange string, symbolType int, symbol string) (*models.TradeSet, error) { | ||||||
|  | 	var tradeSet models.TradeSet | ||||||
|  | 	var scanKey string | ||||||
|  |  | ||||||
|  | 	if symbolType == 1 { | ||||||
|  | 		scanKey = fmt.Sprintf(global.TICKER_SPOT, exchange, symbol) | ||||||
|  | 	} else if symbolType == 2 { | ||||||
|  | 		scanKey = fmt.Sprintf(global.TICKER_FUTURES, exchange, symbol) | ||||||
|  | 	} else { | ||||||
|  | 		return nil, fmt.Errorf("symbolType error") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	val, err := helper.DefaultRedis.GetString(scanKey) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := sonic.Unmarshal([]byte(val), &tradeSet); err != nil { | ||||||
|  | 		logger.Errorf("unmarshal tradeSet err:%v", err) | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &tradeSet, nil | ||||||
|  | } | ||||||
							
								
								
									
										22
									
								
								test/init_settings.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								test/init_settings.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | package test | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"go-admin/common/helper" | ||||||
|  | 	"go-admin/config" | ||||||
|  |  | ||||||
|  | 	"github.com/go-admin-team/go-admin-core/sdk" | ||||||
|  | 	"gorm.io/driver/mysql" | ||||||
|  | 	"gorm.io/gorm" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func InitSettings() *gorm.DB { | ||||||
|  | 	dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms" | ||||||
|  | 	db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{}) | ||||||
|  | 	sdk.Runtime.SetDb("default", db) | ||||||
|  | 	helper.InitDefaultRedis("127.0.0.1:6379", "", 2) | ||||||
|  | 	helper.InitLockRedisConn("127.0.0.1:6379", "", "2") | ||||||
|  |  | ||||||
|  | 	config.ExtConfig.ProxyUrl = "http://127.0.0.1:7890" | ||||||
|  |  | ||||||
|  | 	return db | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user