AI 分析报告按用户隔离存储
个股/财务/复盘报告改为 data/user_data/reports/{username}/ 目录下,
API 端从 request.state.username 读取对应用户的报告。
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,24 +1,8 @@
|
||||
"""AI 个股分析报告持久化存储。
|
||||
"""AI 个股分析报告持久化存储 — 按用户名隔离。
|
||||
|
||||
与 ai_reports.py(财务分析报告)完全独立 —— 单独的文件、字段、上限,
|
||||
互不影响。刻意不复用,避免引入 kind 判别字段与分支(解耦 > 抽象)。
|
||||
|
||||
存储位置: data/user_data/ai_stock_reports.json (数组,按 created_at 降序)
|
||||
保留最近 MAX_REPORTS 条;超出自动裁剪最旧的。
|
||||
|
||||
每条报告结构:
|
||||
{
|
||||
"id": "sar_xxx", # 唯一 id(stock-analysis-report)
|
||||
"symbol": "600519.SH",
|
||||
"name": "贵州茅台",
|
||||
"focus": "", # 用户追加的关心点(可为空)
|
||||
"content": "# ...markdown", # 报告正文
|
||||
"summary": "当前价 12.3 · 压力位...", # 价位/数据摘要
|
||||
"levels": {...}, # 报告生成时的关键价位(供图表回放)
|
||||
"close": 12.3, # 报告生成时的收盘价
|
||||
"created_at": "2026-06-26T10:00:00"
|
||||
}
|
||||
存储位置: data/user_data/reports/{username}/ai_stock_reports.json
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
@@ -31,16 +15,19 @@ logger = logging.getLogger(__name__)
|
||||
MAX_REPORTS = 50
|
||||
|
||||
|
||||
def _path() -> Path:
|
||||
def _path(username: str | None = None) -> Path:
|
||||
from app.config import settings
|
||||
p = settings.data_dir / "user_data" / "ai_stock_reports.json"
|
||||
if username:
|
||||
p = settings.data_dir / "user_data" / "reports" / username / "ai_stock_reports.json"
|
||||
else:
|
||||
p = settings.data_dir / "user_data" / "ai_stock_reports.json"
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
return p
|
||||
|
||||
|
||||
def list_reports() -> list[dict]:
|
||||
"""返回全部报告(按 created_at 降序)。"""
|
||||
p = _path()
|
||||
def list_reports(username: str | None = None) -> list[dict]:
|
||||
"""返回指定用户的全部报告(按 created_at 降序)。"""
|
||||
p = _path(username)
|
||||
if not p.exists():
|
||||
return []
|
||||
try:
|
||||
@@ -52,36 +39,36 @@ def list_reports() -> list[dict]:
|
||||
return []
|
||||
|
||||
|
||||
def _save_all(reports: list[dict]) -> None:
|
||||
def _save_all(reports: list[dict], username: str | None = None) -> None:
|
||||
"""全量写入(裁剪到 MAX_REPORTS)。"""
|
||||
reports.sort(key=lambda r: r.get("created_at", ""), reverse=True)
|
||||
if len(reports) > MAX_REPORTS:
|
||||
reports = reports[:MAX_REPORTS]
|
||||
_path().write_text(
|
||||
_path(username).write_text(
|
||||
json.dumps(reports, indent=2, ensure_ascii=False), encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def save_report(report: dict) -> dict:
|
||||
def save_report(report: dict, username: str | None = None) -> dict:
|
||||
"""新增一条报告并持久化。返回保存后的报告(含 id / created_at)。"""
|
||||
reports = list_reports()
|
||||
reports = list_reports(username)
|
||||
if not report.get("id"):
|
||||
report["id"] = f"sar_{int(time.time() * 1000)}_{report.get('symbol', 'x')}"
|
||||
if not report.get("created_at"):
|
||||
report["created_at"] = _now_iso()
|
||||
reports.append(report)
|
||||
_save_all(reports)
|
||||
logger.info("Stock report saved: %s (%s), total %d", report.get("symbol"), report.get("id"), len(reports))
|
||||
_save_all(reports, username)
|
||||
logger.info("Stock report saved by %s: %s (%s), total %d", username or "?", report.get("symbol"), report.get("id"), len(reports))
|
||||
return report
|
||||
|
||||
|
||||
def delete_report(report_id: str) -> bool:
|
||||
"""删除指定报告。返回是否删除成功。"""
|
||||
reports = list_reports()
|
||||
def delete_report(report_id: str, username: str | None = None) -> bool:
|
||||
"""删除指定用户的报告。返回是否删除成功。"""
|
||||
reports = list_reports(username)
|
||||
before = len(reports)
|
||||
reports = [r for r in reports if r.get("id") != report_id]
|
||||
if len(reports) < before:
|
||||
_save_all(reports)
|
||||
_save_all(reports, username)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user