mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 22:44:22 +08:00
revert: 移除拼音声母搜索,回到 6 位代码输入
拼音搜索的底层依赖(首次需爬沪深 A 股 5000 条全名单,几十次 TDX 协议 往返,慢机器几十秒到超时)太重,反复优化(按需加载/遮罩/单飞/预热) 都无法兼顾'不阻塞核心行情请求'与'首次可用'。用户决定放弃此功能, 回到简单稳定的 6 位代码输入。 回退 e2bf29e..2523605 共 6 个 commit 的全部改动: - 删除 StockSearchInput / AppInitOverlay / useStockSearch - 移除 pypinyin 依赖、/security/search-index 端点、lifespan 预热 - SymbolPicker / StocksPicker 恢复为纯 6 位代码输入 - README / CHANGELOG 同步回退 代码状态等同 v1.18.1(一键寻优多进程并发)发布后的干净基线
This commit is contained in:
@@ -64,24 +64,6 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
ex_client = None
|
||||
app.state.ex_client = ex_client
|
||||
|
||||
# --- 股票搜索索引预热(后台,不阻塞服务启动) ---
|
||||
# 首次 get_security_list_all("all") 要几十次 TDX 协议往返(几十秒),
|
||||
# 后台提前跑,让本地缓存尽早建立。用户打开页面时大概率已就绪。
|
||||
# 仅触发构建(fire-and-forget),不 await 不超时——预热是 best effort,
|
||||
# 没有权力取消构建 task(否则会波及同时到达的 /security/search-index 请求)。
|
||||
import asyncio
|
||||
|
||||
async def _warmup_security_list() -> None:
|
||||
try:
|
||||
from easy_tdx.web.routers.market import _build_search_index
|
||||
|
||||
await _build_search_index(client)
|
||||
logger.info("Security list warmup done")
|
||||
except Exception:
|
||||
logger.warning("Security list warmup failed (non-fatal)", exc_info=True)
|
||||
|
||||
asyncio.create_task(_warmup_security_list())
|
||||
|
||||
yield
|
||||
|
||||
# --- 依次关闭 ---
|
||||
|
||||
@@ -53,92 +53,6 @@ async def security_list_all(
|
||||
return _df_response(df)
|
||||
|
||||
|
||||
# ── 股票搜索索引(声母检索) ───────────────────────────────────────────────────
|
||||
# 进程级缓存:5206 条记录的 {code, name, initials} 只算一次,热重启即丢。
|
||||
# 前端拉一次后模块级缓存,按 code/name.includes/initials.includes 三路过滤。
|
||||
_SEARCH_INDEX: list[dict[str, str]] | None = None
|
||||
# 单飞标记:True 表示后台构建 task 正在跑。调用方据此判断是否需要启动新 task。
|
||||
# 注意:不持有 task/future 引用,避免调用方被 cancel 时波及后台构建。
|
||||
_SEARCH_BUILDING: bool = False
|
||||
# 后台构建失败的最近一次异常(供等待中的调用方读取;None 表示无错或未发生)
|
||||
_SEARCH_BUILD_ERROR: BaseException | None = None
|
||||
|
||||
|
||||
async def _build_search_index(client: Any) -> list[dict[str, str]]:
|
||||
"""构建搜索索引:拉全名单 + pypinyin 预计算声母。耗时几十秒(首次)。
|
||||
|
||||
单飞 + 轮询设计:后台构建 task 与调用方解耦,调用方被 cancel(如预热超时)
|
||||
不会波及正在跑的构建 task,也不会让其他等待的请求收到 CancelledError。
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
global _SEARCH_INDEX, _SEARCH_BUILDING, _SEARCH_BUILD_ERROR
|
||||
|
||||
# 已就绪:直接返回
|
||||
if _SEARCH_INDEX is not None:
|
||||
return _SEARCH_INDEX
|
||||
|
||||
# 未启动构建:启动后台 task(fire and forget,调用方不持有它的引用)
|
||||
if not _SEARCH_BUILDING:
|
||||
_SEARCH_BUILDING = True
|
||||
_SEARCH_BUILD_ERROR = None
|
||||
|
||||
async def _do_build() -> None:
|
||||
global _SEARCH_INDEX, _SEARCH_BUILDING, _SEARCH_BUILD_ERROR
|
||||
from pypinyin import Style, lazy_pinyin
|
||||
|
||||
try:
|
||||
df = await client.get_security_list_all(pages="all")
|
||||
index: list[dict[str, str]] = []
|
||||
for row in df.itertuples(index=False):
|
||||
name = str(getattr(row, "name", "") or "")
|
||||
if not name:
|
||||
continue
|
||||
code = str(getattr(row, "code", ""))
|
||||
initials = "".join(lazy_pinyin(name, style=Style.FIRST_LETTER))
|
||||
index.append({"code": code, "name": name, "initials": initials})
|
||||
_SEARCH_INDEX = index
|
||||
except BaseException as e:
|
||||
_SEARCH_BUILD_ERROR = e
|
||||
finally:
|
||||
_SEARCH_BUILDING = False
|
||||
|
||||
asyncio.create_task(_do_build())
|
||||
|
||||
# 轮询等待结果(每 0.5s 检查一次)。
|
||||
# 这样调用方被 cancel 时,只是退出轮询,不影响后台 _do_build task。
|
||||
# 用 asyncio.shield 保护轮询本身不被取消传播,并在每次循环检查错误。
|
||||
for _ in range(600): # 上限 300 秒(600 × 0.5s)
|
||||
if _SEARCH_INDEX is not None:
|
||||
return _SEARCH_INDEX
|
||||
if not _SEARCH_BUILDING and _SEARCH_BUILD_ERROR is not None:
|
||||
# 构建已结束但失败:抛错给调用方(下次调用会重新触发构建)
|
||||
err = _SEARCH_BUILD_ERROR
|
||||
_SEARCH_BUILD_ERROR = None
|
||||
raise err
|
||||
await asyncio.sleep(0.5)
|
||||
raise TimeoutError("搜索索引构建超时(300s)")
|
||||
|
||||
|
||||
@router.get("/security/search-index")
|
||||
async def security_search_index(
|
||||
client: Any = Depends(get_client),
|
||||
) -> dict[str, Any]:
|
||||
"""返回股票搜索索引 ``[{code, name, initials}]``(供前端声母/代码/名字搜索)。
|
||||
|
||||
数据源复用 :meth:`get_security_list_all`(沪深 A 股,已有本地日级缓存)。
|
||||
声母用 pypinyin ``FIRST_LETTER`` 预计算(如 中际旭创→zjxc)。
|
||||
进程内缓存,首次请求算一次后常驻;强制刷新重启进程即可。
|
||||
|
||||
与 lifespan 预热共享同一单飞任务(``_build_search_index``),
|
||||
避免预热和首次端点请求并发各爬一次全名单。
|
||||
"""
|
||||
if _SEARCH_INDEX is not None:
|
||||
return {"count": len(_SEARCH_INDEX), "data": _SEARCH_INDEX}
|
||||
index = await _build_search_index(client)
|
||||
return {"count": len(index), "data": index}
|
||||
|
||||
|
||||
@router.post("/quotes", response_model=DataFrameResponse)
|
||||
async def security_quotes(
|
||||
req: QuoteRequest,
|
||||
|
||||
Reference in New Issue
Block a user