更新BK缓存策略
This commit is contained in:
@@ -318,14 +318,14 @@ def _get_sector_session() -> AsyncSession:
|
|||||||
global _sector_session
|
global _sector_session
|
||||||
if _sector_session is None:
|
if _sector_session is None:
|
||||||
_sector_session = AsyncSession(
|
_sector_session = AsyncSession(
|
||||||
impersonate="chrome120",
|
impersonate="chrome131",
|
||||||
headers={
|
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",
|
"Referer": "https://data.eastmoney.com/bkzj/hy.html",
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||||
},
|
},
|
||||||
timeout=10,
|
timeout=5,
|
||||||
)
|
)
|
||||||
return _sector_session
|
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)
|
set_cache(cache_key, json.dumps(push2_data, ensure_ascii=False), ttl_hours=ttl)
|
||||||
return push2_data
|
return push2_data
|
||||||
|
|
||||||
# push2 失败,用 akshare
|
# push2 失败,用 akshare(不写磁盘缓存)
|
||||||
akshare_data = await akshare_task
|
akshare_data = await akshare_task
|
||||||
if akshare_data:
|
if akshare_data:
|
||||||
_sector_cache[sector_type] = (akshare_data, now)
|
_sector_cache[sector_type] = (akshare_data, now)
|
||||||
@@ -385,14 +385,14 @@ async def _fetch_push2(sector_type: str) -> list[dict]:
|
|||||||
|
|
||||||
session = _get_sector_session()
|
session = _get_sector_session()
|
||||||
ut = await get_em_ut()
|
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):
|
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:
|
try:
|
||||||
resp = await session.get(url)
|
resp = await session.get(url)
|
||||||
if resp.status_code != 200:
|
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:
|
if "disconnect" in err.lower() or "refused" in err.lower() or attempt == 1:
|
||||||
await get_em_ut(force_refresh=True)
|
await get_em_ut(force_refresh=True)
|
||||||
ut = _em_ut
|
ut = _em_ut
|
||||||
|
# 先尝试更新现有会话的 headers
|
||||||
|
try:
|
||||||
|
session.headers.update({"Referer": "https://data.eastmoney.com/bkzj/hy.html"})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
# 重建会话(TLS 指纹可能会被缓存)
|
# 重建会话(TLS 指纹可能会被缓存)
|
||||||
global _sector_session
|
global _sector_session
|
||||||
_sector_session = None
|
_sector_session = None
|
||||||
@@ -445,10 +450,12 @@ import akshare as ak
|
|||||||
|
|
||||||
|
|
||||||
async def _fetch_akshare(sector_type: str) -> list[dict]:
|
async def _fetch_akshare(sector_type: str) -> list[dict]:
|
||||||
"""akshare 降级方案(同花顺数据源)"""
|
"""akshare 降级方案(东方财富数据源)"""
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
code_map_key = f"board_codes:{sector_type}"
|
||||||
|
|
||||||
def _get():
|
def _build_code_map():
|
||||||
|
"""获取板块代码映射(HTTP 较慢,结果单独缓存 24h)"""
|
||||||
code_map = {}
|
code_map = {}
|
||||||
try:
|
try:
|
||||||
if sector_type == "industry":
|
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", ""))
|
code_map[str(r.get("name", ""))] = str(r.get("code", ""))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
return code_map
|
||||||
|
|
||||||
|
def _get_fund_flow():
|
||||||
if sector_type == "industry":
|
if sector_type == "industry":
|
||||||
df = ak.stock_fund_flow_industry()
|
return ak.stock_fund_flow_industry()
|
||||||
else:
|
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:
|
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:
|
if df is None or df.empty:
|
||||||
return []
|
return []
|
||||||
df = df.sort_values("净额", ascending=False)
|
df = df.sort_values("净额", ascending=False)
|
||||||
|
|||||||
@@ -458,12 +458,12 @@ export interface SectorResponse {
|
|||||||
* 获取东方财富板块列表(按主力净流入排序)
|
* 获取东方财富板块列表(按主力净流入排序)
|
||||||
* @param type industry=行业板块, concept=概念板块
|
* @param type industry=行业板块, concept=概念板块
|
||||||
*/
|
*/
|
||||||
export async function fetchSectors(type: SectorType): Promise<SectorItem[]> {
|
export async function fetchSectors(type: SectorType, signal?: AbortSignal): Promise<SectorItem[]> {
|
||||||
const baseUrl = getApiBaseUrl();
|
const baseUrl = getApiBaseUrl();
|
||||||
const url = `${baseUrl}/api/sectors?type=${type}`;
|
const url = `${baseUrl}/api/sectors?type=${type}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(url, { method: "GET" });
|
const resp = await fetch(url, { method: "GET", signal });
|
||||||
if (!resp.ok) return [];
|
if (!resp.ok) return [];
|
||||||
const result: SectorResponse = await resp.json();
|
const result: SectorResponse = await resp.json();
|
||||||
return result.data || [];
|
return result.data || [];
|
||||||
|
|||||||
+12
-4
@@ -1,5 +1,5 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
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 { fetchSectors, type SectorItem, type SectorType } from "@/lib/stock-api";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { ArrowLeft, TrendingUp, TrendingDown, RefreshCw } from "lucide-react";
|
import { ArrowLeft, TrendingUp, TrendingDown, RefreshCw } from "lucide-react";
|
||||||
@@ -21,17 +21,25 @@ function SectorsPage() {
|
|||||||
const [data, setData] = useState<SectorItem[]>([]);
|
const [data, setData] = useState<SectorItem[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [sort, setSort] = useState<SortMode>("mainNetInflow");
|
const [sort, setSort] = useState<SortMode>("mainNetInflow");
|
||||||
|
const abortRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
const loadData = (t: SectorType) => {
|
const loadData = (t: SectorType) => {
|
||||||
|
// 取消上次未完成的请求
|
||||||
|
abortRef.current?.abort();
|
||||||
|
const controller = new AbortController();
|
||||||
|
abortRef.current = controller;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
fetchSectors(t).then((items) => {
|
fetchSectors(t, controller.signal).then((items) => {
|
||||||
setData(items);
|
if (!controller.signal.aborted) {
|
||||||
setLoading(false);
|
setData(items);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData(tab);
|
loadData(tab);
|
||||||
|
return () => abortRef.current?.abort();
|
||||||
}, [tab]);
|
}, [tab]);
|
||||||
|
|
||||||
const sorted = useMemo(() => {
|
const sorted = useMemo(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user