Files
stock-analysis/internal/datasource/eastmoney/client.go
WarrenZhou 273fc90a95 feat: 添加异步数据入库任务系统,支持全量/增量更新与实时进度展示
新增完整的异步任务管理体系:
1. 新增Redis缓存层实现任务状态实时存储与降级机制
2. 实现并发拉取+批量写入的高性能入库流程
3. 添加任务管理HTTP接口与路由
4. 重构行情控制器使用共享SQLite存储
5. 配套添加Docker Compose Redis启动配置
6. 前端新增任务管理面板支持操作与进度查看
2026-08-18 10:09:52 +08:00

134 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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()
}