新增完整的异步任务管理体系: 1. 新增Redis缓存层实现任务状态实时存储与降级机制 2. 实现并发拉取+批量写入的高性能入库流程 3. 添加任务管理HTTP接口与路由 4. 重构行情控制器使用共享SQLite存储 5. 配套添加Docker Compose Redis启动配置 6. 前端新增任务管理面板支持操作与进度查看
134 lines
4.0 KiB
Go
134 lines
4.0 KiB
Go
// 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()
|
||
}
|