Files
xiaobaifupan/app/backend/features/alerts/service.py
T

96 lines
4.0 KiB
Python

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")