diff --git a/backend/services/eastmoney.py b/backend/services/eastmoney.py index 0c1a8a9..edafd56 100644 --- a/backend/services/eastmoney.py +++ b/backend/services/eastmoney.py @@ -318,14 +318,14 @@ def _get_sector_session() -> AsyncSession: global _sector_session if _sector_session is None: _sector_session = AsyncSession( - impersonate="chrome120", + impersonate="chrome131", headers={ - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", "Referer": "https://data.eastmoney.com/bkzj/hy.html", "Accept": "*/*", "Accept-Language": "zh-CN,zh;q=0.9", }, - timeout=10, + timeout=5, ) return _sector_session @@ -370,7 +370,7 @@ async def fetch_sector_list(sector_type: str) -> list[dict]: set_cache(cache_key, json.dumps(push2_data, ensure_ascii=False), ttl_hours=ttl) return push2_data - # push2 失败,用 akshare + # push2 失败,用 akshare(不写磁盘缓存) akshare_data = await akshare_task if akshare_data: _sector_cache[sector_type] = (akshare_data, now) @@ -385,14 +385,14 @@ async def _fetch_push2(sector_type: str) -> list[dict]: session = _get_sector_session() ut = await get_em_ut() - url = ( - f"https://push2.eastmoney.com/api/qt/clist/get" - f"?fs={fs}&fields={SECTOR_FIELDS}" - f"&fid=f62&po=1&pz=500&pn=1&np=1&fltt=2" - f"&invt=2&ut={ut}" - ) for attempt in range(2): + url = ( + f"https://push2.eastmoney.com/api/qt/clist/get" + f"?fs={fs}&fields={SECTOR_FIELDS}" + f"&fid=f62&po=1&pz=500&pn=1&np=1&fltt=2" + f"&invt=2&ut={ut}" + ) try: resp = await session.get(url) if resp.status_code != 200: @@ -432,6 +432,11 @@ async def _fetch_push2(sector_type: str) -> list[dict]: if "disconnect" in err.lower() or "refused" in err.lower() or attempt == 1: await get_em_ut(force_refresh=True) ut = _em_ut + # 先尝试更新现有会话的 headers + try: + session.headers.update({"Referer": "https://data.eastmoney.com/bkzj/hy.html"}) + except Exception: + pass # 重建会话(TLS 指纹可能会被缓存) global _sector_session _sector_session = None @@ -445,10 +450,12 @@ import akshare as ak async def _fetch_akshare(sector_type: str) -> list[dict]: - """akshare 降级方案(同花顺数据源)""" + """akshare 降级方案(东方财富数据源)""" loop = asyncio.get_event_loop() + code_map_key = f"board_codes:{sector_type}" - def _get(): + def _build_code_map(): + """获取板块代码映射(HTTP 较慢,结果单独缓存 24h)""" code_map = {} try: if sector_type == "industry": @@ -469,16 +476,34 @@ async def _fetch_akshare(sector_type: str) -> list[dict]: code_map[str(r.get("name", ""))] = str(r.get("code", "")) except Exception: pass + return code_map + def _get_fund_flow(): if sector_type == "industry": - df = ak.stock_fund_flow_industry() + return ak.stock_fund_flow_industry() else: - df = ak.stock_fund_flow_concept() + return ak.stock_fund_flow_concept() - return code_map, df + # 1. 尝试从缓存读取 code_map + code_map = {} + cached_map = get_cache(code_map_key) + if cached_map is not None: + code_map = json.loads(cached_map) try: - code_map, df = await loop.run_in_executor(None, _get) + if code_map: + # 已有缓存,只需获取资金流 + df = await loop.run_in_executor(None, _get_fund_flow) + else: + # 首次:code_map + 资金流并发获取 + map_data, df = await asyncio.gather( + loop.run_in_executor(None, _build_code_map), + loop.run_in_executor(None, _get_fund_flow), + ) + if map_data: + code_map = map_data + set_cache(code_map_key, json.dumps(code_map, ensure_ascii=False), ttl_hours=24) + if df is None or df.empty: return [] df = df.sort_values("净额", ascending=False) diff --git a/src/lib/stock-api.ts b/src/lib/stock-api.ts index 849b970..710f364 100755 --- a/src/lib/stock-api.ts +++ b/src/lib/stock-api.ts @@ -458,12 +458,12 @@ export interface SectorResponse { * 获取东方财富板块列表(按主力净流入排序) * @param type industry=行业板块, concept=概念板块 */ -export async function fetchSectors(type: SectorType): Promise { +export async function fetchSectors(type: SectorType, signal?: AbortSignal): Promise { const baseUrl = getApiBaseUrl(); const url = `${baseUrl}/api/sectors?type=${type}`; try { - const resp = await fetch(url, { method: "GET" }); + const resp = await fetch(url, { method: "GET", signal }); if (!resp.ok) return []; const result: SectorResponse = await resp.json(); return result.data || []; diff --git a/src/routes/sectors.tsx b/src/routes/sectors.tsx index 00f3eab..6fb6eac 100644 --- a/src/routes/sectors.tsx +++ b/src/routes/sectors.tsx @@ -1,5 +1,5 @@ import { createFileRoute } from "@tanstack/react-router"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect, useMemo, useRef } from "react"; import { fetchSectors, type SectorItem, type SectorType } from "@/lib/stock-api"; import { Card, CardContent } from "@/components/ui/card"; import { ArrowLeft, TrendingUp, TrendingDown, RefreshCw } from "lucide-react"; @@ -21,17 +21,25 @@ function SectorsPage() { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [sort, setSort] = useState("mainNetInflow"); + const abortRef = useRef(null); const loadData = (t: SectorType) => { + // 取消上次未完成的请求 + abortRef.current?.abort(); + const controller = new AbortController(); + abortRef.current = controller; setLoading(true); - fetchSectors(t).then((items) => { - setData(items); - setLoading(false); + fetchSectors(t, controller.signal).then((items) => { + if (!controller.signal.aborted) { + setData(items); + setLoading(false); + } }); }; useEffect(() => { loadData(tab); + return () => abortRef.current?.abort(); }, [tab]); const sorted = useMemo(() => {