fix(web): v1.19.3 K线空body不再500 + SPA路由刷新404

两个独立问题(日志证据):

1. K线空body 500:SH600519 等正常股票偶发请求时,通达信返回
   ret_count>0 但 body 完全为空(偏移2 剩余0)。v1.18.3 的容错有
   'if bars:' 条件,bars 为空时 raise → 500。移除该条件,始终 return
   (空列表让前端分页重试比 500 友好)。GetIndexBarsCmd 同改。

2. SPA fallback 缺失:/optimize /portfolio 等前端路由刷新时 404。
   子类化 StaticFiles 为 SPAStaticFiles,404 时返回 index.html。
   API 路径不受影响。

测试:更新 test_security_bars_truncated_first_record(原断言 raise,
现断言返回空列表)+ 新增空 body 回归测试。902 全过。
This commit is contained in:
Justin Gu
2026-07-07 20:13:39 +08:00
parent 6b51487477
commit 032f5a40c0
6 changed files with 86 additions and 31 deletions
+30 -2
View File
@@ -248,8 +248,36 @@ def _create_app(
dist_dir = _resolve_web_dist_dir()
if dist_dir is not None:
app.mount("/", StaticFiles(directory=str(dist_dir), html=True), name="web-ui")
logger.info("Web UI mounted from %s", dist_dir)
# SPA fallback:前端用 createWebHistoryHTML5 history 模式),
# 用户直接访问 /optimize、/portfolio 等前端路由或刷新时,后端必须
# 返回 index.html 让 Vue Router 接管,而不是 404。
# 实现方式:用 StaticFiles 挂在 "/static" 提供真实文件(JS/CSS/图标),
# 再加一个 catch-all 路由把所有非 /api、非 /static 的 GET 请求导向
# index.html。但这样会改变 JS/CSS 的 URL 前缀(/assets → /static/assets),
# 需要改 vite base 配置,代价大。
# 更简单的方式:先尝试 StaticFiles 服务真实文件,找不到时 fallback。
# Starlette 的 StaticFiles(html=True) 不做 SPA fallback,故子类化它。
from pathlib import Path as _Path
from starlette.responses import FileResponse
class SPAStaticFiles(StaticFiles):
"""StaticFiles + SPA fallback404 时返回 index.html。"""
async def get_response(self, path: str, scope): # type: ignore[no-untyped-def]
try:
return await super().get_response(path, scope)
except Exception:
# 任何 404(路径非文件)都返回 index.html,让前端路由处理。
# 仅对 GET 请求生效;API 路径 (/api/v1/*) 已在前面注册,
# 不会走到这里。
index = _Path(str(self.directory)) / "index.html"
if index.is_file():
return FileResponse(str(index))
raise
app.mount("/", SPAStaticFiles(directory=str(dist_dir), html=True), name="web-ui")
logger.info("Web UI mounted from %s (SPA fallback enabled)", dist_dir)
else:
logger.info("Web UI dist not found — serving API only")