from __future__ import annotations import secrets from datetime import date, datetime from typing import Any from backend.bootstrap.config import validate_text from backend.database.repositories import AlertRepository class AlertService: def __init__(self, repository: AlertRepository) -> None: self.repository = repository def create_manual(self, user_id: int, payload: dict[str, Any]) -> int: title = validate_text(payload.get("title"), "提醒标题", 80, required=True) content = validate_text(payload.get("content"), "提醒内容", 500) code = validate_text(payload.get("code"), "股票代码", 12) available_date = self.calendar_date( str(payload.get("remind_date") or date.today().isoformat()) ) return self.repository.save_alert( user_id=user_id, kind="manual", title=title, content=content, available_date=available_date, code=code, dedupe_key=f"manual:{secrets.token_hex(12)}", ) def sync_strategy_tracking(self, user_id: int, tracking: dict[str, Any]) -> int: synced = 0 today = date.today().strftime("%Y%m%d") for batch in tracking.get("batches") or []: items = batch.get("items") or [] summary = batch.get("summary") or {} if not items: continue run_id = int(batch.get("run_id") or 0) strategy_name = str(batch.get("strategy_name") or "选股策略") observed = int(summary.get("observed") or 0) completed = int(summary.get("completed") or 0) if observed: win_rate = summary.get("t1_win_rate") suffix = f",当前红盘率 {win_rate:.1f}%" if win_rate is not None else "" self.repository.save_alert( user_id, "strategy_t1", f"{strategy_name} 已有 T+1 反馈", f"{observed}/{len(items)} 只标的已有首日表现{suffix}。", today, "", f"strategy:{run_id}:t1", ) synced += 1 if completed == len(items): average = summary.get("average_t5") suffix = f",平均收益 {average:+.2f}%" if average is not None else "" self.repository.save_alert( user_id, "strategy_t5", f"{strategy_name} 五日跟踪完成", f"本批 {len(items)} 只标的已完成 T+5 跟踪{suffix}。", today, "", f"strategy:{run_id}:t5", ) synced += 1 return synced def list_alerts( self, user_id: int, status: str = "all", as_of: str = "" ) -> dict[str, Any]: if status not in {"all", "unread"}: raise ValueError("提醒筛选不支持。") compact_date = self.calendar_date(as_of or date.today().isoformat()) items = self.repository.list_alerts(user_id, compact_date, status == "unread") for item in items: item["due"] = str(item.get("available_date") or "") <= compact_date return { "items": items, "unread_count": self.repository.count_unread_alerts(user_id, compact_date), "as_of": compact_date, } def mark_read(self, user_id: int, alert_id: int) -> bool: return self.repository.mark_alert_read(user_id, alert_id) def mark_all_read(self, user_id: int, as_of: str) -> int: return self.repository.mark_all_alerts_read(user_id, as_of) def delete(self, user_id: int, alert_id: int) -> bool: return self.repository.delete_alert(user_id, alert_id) @staticmethod def calendar_date(value: str) -> str: compact = value.replace("-", "").strip() try: parsed = datetime.strptime(compact, "%Y%m%d") except ValueError as exc: raise ValueError("提醒日期格式应为 YYYY-MM-DD。") from exc return parsed.strftime("%Y%m%d")