diff --git a/serve/backend/app/api/financials.py b/serve/backend/app/api/financials.py index ba884ec..781b718 100644 --- a/serve/backend/app/api/financials.py +++ b/serve/backend/app/api/financials.py @@ -151,11 +151,12 @@ class SaveReportRequest(BaseModel): @router.get("/reports") def list_reports(request: Request): - """获取全部历史报告(按时间降序,后端已裁剪到上限)。无需 FINANCIAL 能力读取列表元信息。""" + """获取当前用户的历史报告(按时间降序,后端已裁剪到上限)。""" capset = request.app.state.capabilities if not capset.has(Cap.FINANCIAL): return {"reports": []} - return {"reports": ai_reports.list_reports()} + username = getattr(request.state, "username", None) + return {"reports": ai_reports.list_reports(username)} @router.post("/reports") @@ -163,6 +164,7 @@ def save_report(request: Request, req: SaveReportRequest): """保存一条报告。""" capset = request.app.state.capabilities capset.require(Cap.FINANCIAL) + username = getattr(request.state, "username", None) report = ai_reports.save_report({ "symbol": req.symbol, "name": req.name, @@ -170,7 +172,7 @@ def save_report(request: Request, req: SaveReportRequest): "content": req.content, "periods": req.periods, "summary": req.summary, - }) + }, username) return {"ok": True, "report": report} @@ -179,5 +181,6 @@ def delete_report(request: Request, report_id: str): """删除一条报告。""" capset = request.app.state.capabilities capset.require(Cap.FINANCIAL) - ok = ai_reports.delete_report(report_id) + username = getattr(request.state, "username", None) + ok = ai_reports.delete_report(report_id, username) return {"ok": ok} diff --git a/serve/backend/app/api/market_recap.py b/serve/backend/app/api/market_recap.py index 8ec5c1d..3edf8b2 100644 --- a/serve/backend/app/api/market_recap.py +++ b/serve/backend/app/api/market_recap.py @@ -81,13 +81,15 @@ class SaveReportRequest(BaseModel): @router.get("/reports") def list_reports(request: Request): - """获取全部历史复盘(按时间降序,后端已裁剪到上限)。""" - return {"reports": market_recap_reports.list_reports()} + """获取当前用户的历史复盘(按时间降序,后端已裁剪到上限)。""" + username = getattr(request.state, "username", None) + return {"reports": market_recap_reports.list_reports(username)} @router.post("/reports") def save_report(request: Request, req: SaveReportRequest): """保存一条复盘报告。""" + username = getattr(request.state, "username", None) report = market_recap_reports.save_report({ "as_of": req.as_of, "focus": req.focus, @@ -95,9 +97,7 @@ def save_report(request: Request, req: SaveReportRequest): "summary": req.summary, "emotion_score": req.emotion_score, "emotion_label": req.emotion_label, - }) - # 推送到飞书(可选): 与定时复盘共用同一开关 review_push_enabled 与 _maybe_push_review。 - # 内部 try/except 静默降级, 不影响归档返回值。 + }, username) from app.jobs.daily_pipeline import _maybe_push_review _maybe_push_review(req.content, { "as_of": req.as_of, @@ -109,5 +109,6 @@ def save_report(request: Request, req: SaveReportRequest): @router.delete("/reports/{report_id}") def delete_report(request: Request, report_id: str): """删除一条复盘报告。""" - ok = market_recap_reports.delete_report(report_id) + username = getattr(request.state, "username", None) + ok = market_recap_reports.delete_report(report_id, username) return {"ok": ok} diff --git a/serve/backend/app/api/stock_analysis.py b/serve/backend/app/api/stock_analysis.py index 8926fc1..0b776bb 100644 --- a/serve/backend/app/api/stock_analysis.py +++ b/serve/backend/app/api/stock_analysis.py @@ -191,13 +191,15 @@ class SaveReportRequest(BaseModel): @router.get("/reports") def list_reports(request: Request): - """获取全部历史报告(按时间降序,后端已裁剪到上限)。""" - return {"reports": stock_reports.list_reports()} + """获取当前用户的历史报告(按时间降序,后端已裁剪到上限)。""" + username = getattr(request.state, "username", None) + return {"reports": stock_reports.list_reports(username)} @router.post("/reports") def save_report(request: Request, req: SaveReportRequest): """保存一条报告。""" + username = getattr(request.state, "username", None) report = stock_reports.save_report({ "symbol": req.symbol, "name": req.name, @@ -206,12 +208,13 @@ def save_report(request: Request, req: SaveReportRequest): "summary": req.summary, "close": req.close, "levels": req.levels, - }) + }, username) return {"ok": True, "report": report} @router.delete("/reports/{report_id}") def delete_report(request: Request, report_id: str): """删除一条报告。""" - ok = stock_reports.delete_report(report_id) + username = getattr(request.state, "username", None) + ok = stock_reports.delete_report(report_id, username) return {"ok": ok} diff --git a/serve/backend/app/services/ai_reports.py b/serve/backend/app/services/ai_reports.py index caf4115..cbe55f0 100644 --- a/serve/backend/app/services/ai_reports.py +++ b/serve/backend/app/services/ai_reports.py @@ -1,20 +1,8 @@ -"""AI 财务分析报告持久化存储。 +"""AI 财务分析报告持久化存储 — 按用户名隔离。 -存储位置: data/user_data/ai_reports.json (数组,按 created_at 降序) -保留最近 MAX_REPORTS 条;超出自动裁剪最旧的。 - -每条报告结构: -{ - "id": "rpt_xxx", # 唯一 id - "symbol": "600519.SH", - "name": "贵州茅台", - "focus": "", # 用户追加的关心点(可为空) - "content": "# ...markdown", # 报告正文 - "periods": 4, # 基于几期数据生成 - "summary": "metrics: 1期...", # 数据摘要 - "created_at": "2026-06-25T10:00:00" -} +存储位置: data/user_data/reports/{username}/ai_reports.json """ + from __future__ import annotations import json @@ -27,16 +15,19 @@ logger = logging.getLogger(__name__) MAX_REPORTS = 20 -def _path() -> Path: +def _path(username: str | None = None) -> Path: from app.config import settings - p = settings.data_dir / "user_data" / "ai_reports.json" + if username: + p = settings.data_dir / "user_data" / "reports" / username / "ai_reports.json" + else: + p = settings.data_dir / "user_data" / "ai_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: @@ -48,54 +39,49 @@ 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: - """新增一条报告并持久化。返回保存后的报告(含 id / created_at)。 - - 自动补全 id 与 created_at(若缺),并裁剪到上限。 - """ - reports = list_reports() +def save_report(report: dict, username: str | None = None) -> dict: + """新增一条报告并持久化。返回保存后的报告(含 id / created_at)。""" + reports = list_reports(username) if not report.get("id"): report["id"] = f"rpt_{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("AI report saved: %s (%s), total %d", report.get("symbol"), report.get("id"), len(reports)) + _save_all(reports, username) + logger.info("AI 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 -def clear_reports() -> int: - """清空全部报告。返回删除数量。""" - reports = list_reports() +def clear_reports(username: str | None = None) -> int: + """清空指定用户的全部报告。返回删除数量。""" + reports = list_reports(username) n = len(reports) if n > 0: - _save_all([]) + _save_all([], username) return n def _now_iso() -> str: - """当前本地时间 ISO 字符串(带秒精度,前端 toLocaleString 友好)。""" from datetime import datetime return datetime.now().isoformat(timespec="seconds") diff --git a/serve/backend/app/services/market_recap_reports.py b/serve/backend/app/services/market_recap_reports.py index 06be304..323aaf7 100644 --- a/serve/backend/app/services/market_recap_reports.py +++ b/serve/backend/app/services/market_recap_reports.py @@ -1,24 +1,8 @@ -"""AI 大盘复盘报告持久化存储。 +"""AI 大盘复盘报告持久化存储 — 按用户名隔离。 -与 stock_reports.py(个股分析报告)/ ai_reports.py(财务分析报告)完全独立 —— -单独的文件、字段、上限,互不影响。刻意不复用,避免引入 kind 判别字段与分支 -(解耦 > 抽象)。 - -存储位置: data/user_data/ai_market_recaps.json (数组,按 created_at 降序) -保留最近 MAX_REPORTS 条;超出自动裁剪最旧的。 - -每条报告结构: -{ - "id": "mkr_xxx", # 唯一 id(market-recap-report) - "as_of": "2026-06-27", # 复盘日期 - "focus": "", # 用户追加的关心点(可为空) - "content": "# ...markdown", # 报告正文 - "summary": "三大指数齐涨...", # 一句话摘要 - "emotion_score": 68, # 情绪分(0-100, 复盘生成时的市场情绪雷达均分) - "emotion_label": "偏暖", # 情绪标签(强势/偏暖/震荡/偏冷/冰点) - "created_at": "2026-06-27T15:35:00" -} +存储位置: data/user_data/reports/{username}/ai_market_recaps.json """ + from __future__ import annotations import json @@ -31,16 +15,19 @@ logger = logging.getLogger(__name__) MAX_REPORTS = 20 -def _path() -> Path: +def _path(username: str | None = None) -> Path: from app.config import settings - p = settings.data_dir / "user_data" / "ai_market_recaps.json" + if username: + p = settings.data_dir / "user_data" / "reports" / username / "ai_market_recaps.json" + else: + p = settings.data_dir / "user_data" / "ai_market_recaps.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,37 +39,37 @@ 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"mkr_{int(time.time() * 1000)}" if not report.get("created_at"): report["created_at"] = _now_iso() reports.append(report) - _save_all(reports) - logger.info("Market recap saved: %s (as_of=%s), total %d", - report.get("id"), report.get("as_of"), len(reports)) + _save_all(reports, username) + logger.info("Market recap saved by %s: %s (as_of=%s), total %d", + username or "?", report.get("id"), report.get("as_of"), 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 diff --git a/serve/backend/app/services/stock_reports.py b/serve/backend/app/services/stock_reports.py index d522d23..5a5cafb 100644 --- a/serve/backend/app/services/stock_reports.py +++ b/serve/backend/app/services/stock_reports.py @@ -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