659 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			659 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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()
 | ||
| 	}
 | ||
| }
 |