migration: establish exact preserved app baseline
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from .service import AlertService
|
||||
|
||||
__all__ = ["AlertService"]
|
||||
@@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
from datetime import date, datetime
|
||||
from typing import Any
|
||||
|
||||
from app_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")
|
||||
Reference in New Issue
Block a user