mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 15:34:16 +08:00
fix(backend): 清除数据彻底清空三层内存缓存 + 监控触发记录 + 修复版本号
修复清除数据后看板/监控仍显示旧数据的根因: 数据缓存清理 (repository/screener/overview): - repository.py: 新增 clear_cache() 无条件清空全部 Polars 内存缓存; 修复 _refresh_enriched() 磁盘无数据时不清内存导致旧数据残留的 bug - screener.py: 新增 clear_history_cache() 清进程级 _history_cache (TTL 缓存) - overview.py: 新增 invalidate_overview_cache() 清看板聚合结果 5s TTL 缓存 清除数据完整性 (data.py): - clear_data 删除 parquet 后统一调用三层缓存清理 - 新增清除监控运行数据: alert_store.clear() 清 alerts.jsonl + _pending_alerts 内存队列 (原 clear_data 漏删 user_data/alerts.jsonl, 导致触发记录清不掉) 版本号统一: - /api/data/version 改为优先读 app.__version__ (与 /health 同源), 原 pyproject.toml 路径解析有误读不到, 回退到陈旧的 VERSION 文件 - Windows GBK 编码修复: __init__.py 强制 stdout/stderr UTF-8, 避免 TickFlow SDK 输出含 emoji 的指数名称时崩溃 (gbk codec can't encode)
This commit is contained in:
@@ -1,3 +1,15 @@
|
||||
"""TickFlow Stock Panel backend."""
|
||||
|
||||
import sys
|
||||
|
||||
__version__ = "0.1.38"
|
||||
|
||||
# Windows 默认 stdout/stderr 编码为 GBK(cp936),TickFlow SDK 内部输出含 emoji 的
|
||||
# 指数/标的名称(如 \U0001f193)时会抛 UnicodeEncodeError,导致请求失败。
|
||||
# 进程加载最早阶段强制 UTF-8,根治此类编码崩溃。
|
||||
for _stream in (sys.stdout, sys.stderr):
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
+32
-14
@@ -564,9 +564,31 @@ def clear_data(request: Request):
|
||||
fp.unlink()
|
||||
deleted += 1
|
||||
|
||||
# 清除监控运行数据 (user_data 下仅清运行产物, 不动 monitor_rules/preferences/secrets 等用户配置)
|
||||
# - 触发记录 alerts.jsonl
|
||||
from app.services import alert_store
|
||||
alert_store.clear(data_dir)
|
||||
# - 待推送的实时通知队列 (进程内存)
|
||||
qs = getattr(request.app.state, "quote_service", None)
|
||||
if qs is not None:
|
||||
with qs._lock:
|
||||
qs._pending_alerts.clear()
|
||||
|
||||
# 清除 Polars 缓存
|
||||
# 先 clear_cache 无条件清空内存 (refresh_cache 在磁盘无数据时会提前 return,
|
||||
# 导致 _enriched_cache 等旧数据残留 —— 清数据后看板仍显示旧数据的根因),
|
||||
# 再 refresh_cache 尝试重载 (磁盘有数据则重建缓存)。
|
||||
repo.clear_cache()
|
||||
repo.refresh_cache()
|
||||
|
||||
# 清除 Screener 进程级 _history_cache (TTL 缓存)
|
||||
from app.services.screener import ScreenerService
|
||||
ScreenerService.clear_history_cache()
|
||||
|
||||
# 清除 Overview 总览聚合结果缓存 (5s TTL)
|
||||
from app.api.overview import invalidate_overview_cache
|
||||
invalidate_overview_cache()
|
||||
|
||||
# 刷新 DuckDB 视图(空 parquet 目录也需要重新挂载)
|
||||
d = data_dir.as_posix()
|
||||
for name, path in {
|
||||
@@ -703,23 +725,19 @@ def table_schema(request: Request, table: str) -> list[dict]:
|
||||
def get_version(request: Request) -> dict:
|
||||
"""返回当前项目版本号。
|
||||
|
||||
优先从 pyproject.toml 读取 (项目权威版本源),
|
||||
回退到 VERSION 文件, 最后兜底 v0.0.0。
|
||||
优先读 app.__version__ (与 /health 接口同源, 唯一权威版本),
|
||||
回退到项目根 VERSION 文件, 最后兜底 v0.0.0。
|
||||
"""
|
||||
from app import __version__
|
||||
|
||||
# 1. 优先用 app.__version__ (开发期 bump_version.py 写入, 打包期由 PyInstaller 注入)
|
||||
if __version__:
|
||||
v = __version__.strip()
|
||||
return {"version": v if v.startswith("v") else f"v{v}"}
|
||||
|
||||
# 2. 回退到项目根 VERSION 文件
|
||||
from app.config import settings
|
||||
project_root = Path(settings.data_dir).parent
|
||||
|
||||
# 1. 优先读 pyproject.toml
|
||||
pyproject = project_root / "pyproject.toml"
|
||||
if pyproject.exists():
|
||||
for line in pyproject.read_text(encoding="utf-8").splitlines():
|
||||
if line.strip().startswith("version"):
|
||||
# version = "0.1.28"
|
||||
v = line.split("=", 1)[1].strip().strip('"').strip("'")
|
||||
if v:
|
||||
return {"version": f"v{v}" if not v.startswith("v") else v}
|
||||
|
||||
# 2. 回退到 VERSION 文件
|
||||
version_file = project_root / "VERSION"
|
||||
if version_file.exists():
|
||||
v = version_file.read_text(encoding="utf-8").strip()
|
||||
|
||||
@@ -20,6 +20,18 @@ _cache: dict[str, Any] | None = None
|
||||
_cache_key: str | None = None
|
||||
_cache_ts: float = 0.0
|
||||
|
||||
|
||||
def invalidate_overview_cache() -> None:
|
||||
"""清空总览聚合结果缓存。
|
||||
|
||||
清除数据后调用, 避免看板在 TTL 窗口内继续返回旧的聚合结果。
|
||||
"""
|
||||
global _cache, _cache_key, _cache_ts
|
||||
_cache = None
|
||||
_cache_key = None
|
||||
_cache_ts = 0.0
|
||||
|
||||
|
||||
CORE_INDEX_NAMES = {
|
||||
"000001.SH": "上证指数",
|
||||
"399001.SZ": "深证成指",
|
||||
|
||||
@@ -185,6 +185,14 @@ class ScreenerService:
|
||||
def __init__(self, repo: KlineRepository) -> None:
|
||||
self.repo = repo
|
||||
|
||||
@staticmethod
|
||||
def clear_history_cache() -> None:
|
||||
"""清空进程级 _history_cache (TTL 缓存)。
|
||||
|
||||
清除数据后调用, 避免内存里的旧历史窗口残留导致策略/看板仍命中旧数据。
|
||||
"""
|
||||
_history_cache.clear()
|
||||
|
||||
def _load_enriched_for_date(self, target_date: date) -> pl.DataFrame:
|
||||
"""从 enriched parquet 读取指定日期的基础数据并即时计算完整指标+信号。
|
||||
|
||||
|
||||
@@ -152,6 +152,22 @@ class KlineRepository:
|
||||
self._refresh_index_instruments()
|
||||
self._refresh_enriched()
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""清空所有 Polars 内存缓存。
|
||||
|
||||
与 refresh_cache 的区别: refresh_cache 在磁盘无数据时会提前 return,
|
||||
导致内存里的旧缓存残留 (clear 数据后看板仍显示旧数据的根因)。
|
||||
本方法无条件清空, 供清除数据/重置场景调用。
|
||||
"""
|
||||
self._enriched_cache = None
|
||||
self._enriched_cache_date = None
|
||||
self._enriched_history_cache = None
|
||||
self._enriched_history_start = None
|
||||
self._live_agg_cache = None
|
||||
self._live_agg_cache_date = None
|
||||
self._instruments_cache = None
|
||||
self._index_instruments_cache = None
|
||||
|
||||
def _refresh_enriched(self) -> None:
|
||||
"""从 parquet 加载 enriched 最新日到内存 + 构建聚合表。
|
||||
|
||||
@@ -163,6 +179,9 @@ class KlineRepository:
|
||||
try:
|
||||
latest = self._latest_enriched_date_duckdb()
|
||||
if not latest:
|
||||
# 磁盘已无数据: 必须清空内存缓存, 否则旧数据会残留
|
||||
# (清数据后看板仍显示旧数据的根因)
|
||||
self.clear_cache()
|
||||
return
|
||||
|
||||
# Step 1: 直接读最新日期的分区文件 (仅 14 列)
|
||||
|
||||
Reference in New Issue
Block a user