// Package eastmoney 实现 datasource.MarketDataSource 接口。 // 数据来源:push2.eastmoney.com(实时行情)、push2his.eastmoney.com(历史K线)。 // 注意:该接口为非官方免费接口,无 SLA 保障,使用需限速并做好容错。 package eastmoney import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "sync" "time" ) const ( // requestInterval 请求间隔,避免高频调用被封 IP(东财对短时高频请求限流较严格)。 requestInterval = 2 * time.Second // maxRetry 每个域名上的失败重试次数。 maxRetry = 2 ) // realTimeHosts 实时行情域名列表,主域名被限流时轮询备用(东财数字前缀节点)。 var realTimeHosts = []string{ "https://push2.eastmoney.com", "https://16.push2.eastmoney.com", "https://20.push2.eastmoney.com", "https://30.push2.eastmoney.com", "https://41.push2.eastmoney.com", "https://54.push2.eastmoney.com", "https://69.push2.eastmoney.com", "https://82.push2.eastmoney.com", } // historyHosts 历史K线域名列表(主域名被限流时轮询备用节点)。 var historyHosts = []string{ "https://push2his.eastmoney.com", "https://16.push2his.eastmoney.com", "https://20.push2his.eastmoney.com", "https://30.push2his.eastmoney.com", "https://41.push2his.eastmoney.com", "https://54.push2his.eastmoney.com", "https://69.push2his.eastmoney.com", "https://82.push2his.eastmoney.com", } // Client 是东方财富行情接口客户端,实现 datasource.MarketDataSource。 type Client struct { http *http.Client lastReq time.Time mu sync.Mutex // 保护 lastReq,支持多 goroutine 并发调用 interval time.Duration } // NewClient 创建东财行情客户端(默认 2s 节流)。 func NewClient() *Client { return &Client{ http: &http.Client{Timeout: 15 * time.Second}, interval: requestInterval, } } // NewClientWithThrottle 创建可配置节流间隔的客户端。 // interval=0 关闭内部节流,由外部全局限速器统一控制(job runner 场景)。 func NewClientWithThrottle(interval time.Duration) *Client { return &Client{ http: &http.Client{Timeout: 15 * time.Second}, interval: interval, } } // getJSON 带限速与多域名轮询重试的 GET 请求,解析 JSON 到 out。 // hostIdx 标识域名列表(0 实时 / 1 历史),path 为接口路径(如 /api/qt/clist/get)。 func (c *Client) getJSON(ctx context.Context, hosts []string, path string, params url.Values, out any) error { var lastErr error for _, base := range hosts { reqURL := base + path + "?" + params.Encode() for attempt := 0; attempt < maxRetry; attempt++ { if attempt > 0 { // 退避:3s、6s time.Sleep(time.Duration(3*(attempt+1)) * time.Second) } if err := c.fetch(ctx, reqURL, out); err != nil { lastErr = err continue } return nil } } return fmt.Errorf("eastmoney request failed across all hosts: %w", lastErr) } // fetch 执行 HTTP GET 并解析 JSON。 func (c *Client) fetch(ctx context.Context, reqURL string, out any) error { c.throttle() req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) if err != nil { return err } // 模拟浏览器 UA,降低被风控概率。 req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36") req.Header.Set("Referer", "https://quote.eastmoney.com/") resp, err := c.http.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("http status %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return err } return json.Unmarshal(body, out) } // throttle 保证请求间隔不小于 interval,线程安全。 // interval=0 时为空操作,由外部全局限速器统一控制速率。 func (c *Client) throttle() { if c.interval == 0 { return } c.mu.Lock() defer c.mu.Unlock() elapsed := time.Since(c.lastReq) if wait := c.interval - elapsed; wait > 0 { time.Sleep(wait) } c.lastReq = time.Now() }