feat: add account-scoped in-app reminder center

This commit is contained in:
leefer
2026-07-23 00:06:33 +08:00
parent 5ae2791dd3
commit a2a5db8f6a
8 changed files with 608 additions and 0 deletions
+66
View File
@@ -15,6 +15,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Any
from urllib.parse import parse_qs, unquote, urlparse
from alert_service import AlertService
from api_access import required_role
from app_config import (
DATA_DIR,
@@ -129,6 +130,7 @@ class DashboardService:
self._system_credentials = self._load_system_credentials(environment_credentials)
self.screener = ScreenerEngine(self.database)
self.strategy_tracking = StrategyTrackingService(self.database)
self.alert_service = AlertService(self.database)
self.mentor_skills = MentorSkillRegistry(MENTOR_SKILLS_DIR)
self.realtime_aggregator = WebRealtimeAggregator()
self.screener.ensure_builtin_strategies()
@@ -1104,6 +1106,30 @@ class DashboardService:
"notice": notice,
}
def alert_center(self, status: str = "all", as_of: str = "") -> dict[str, Any]:
tracking = self.strategy_tracking.list_tracking(self.current_user_id, 12)
self.alert_service.sync_strategy_tracking(self.current_user_id, tracking)
return self.alert_service.list_alerts(
self.current_user_id, status, as_of
)
def create_alert(self, payload: dict[str, Any]) -> dict[str, Any]:
alert_id = self.alert_service.create_manual(self.current_user_id, payload)
return {"id": alert_id, **self.alert_center()}
def mark_alert_read(self, alert_id: int) -> dict[str, Any]:
self.database.mark_alert_read(self.current_user_id, alert_id)
return self.alert_center()
def mark_all_alerts_read(self, as_of: str = "") -> dict[str, Any]:
compact_date = self.alert_service.calendar_date(as_of or date.today().isoformat())
self.database.mark_all_alerts_read(self.current_user_id, compact_date)
return self.alert_center(as_of=compact_date)
def delete_alert(self, alert_id: int) -> dict[str, Any]:
deleted = self.database.delete_alert(self.current_user_id, alert_id)
return {"deleted": deleted, **self.alert_center()}
def sync_screener_data(self, trade_date: str, lookback: int = 45) -> dict[str, Any]:
if not self.configured:
raise ValueError("请先配置 Tushare Token。")
@@ -3368,6 +3394,18 @@ class RequestHandler(BaseHTTPRequestHandler):
if parsed.path == "/api/account/status":
self.send_json({"ok": True, **SERVICE.status()})
return
if parsed.path == "/api/alerts":
query = parse_qs(parsed.query)
try:
self.send_json(
SERVICE.alert_center(
query.get("status", ["all"])[0],
query.get("as_of", [date.today().isoformat()])[0],
)
)
except ValueError as exc:
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
return
if parsed.path == "/api/dashboard":
query = parse_qs(parsed.query)
trade_date = query.get("trade_date", [date.today().isoformat()])[0]
@@ -3574,6 +3612,21 @@ class RequestHandler(BaseHTTPRequestHandler):
if parsed.path == "/api/account/password":
self.change_password()
return
alert_read_match = re.fullmatch(r"/api/alerts/(\d+)/read", parsed.path)
if alert_read_match:
self.send_json(
{"ok": True, **SERVICE.mark_alert_read(int(alert_read_match.group(1)))}
)
return
if parsed.path == "/api/alerts/read-all":
body = self.read_json_body(True)
self.send_json(
{"ok": True, **SERVICE.mark_all_alerts_read(str(body.get("as_of") or ""))}
)
return
if parsed.path == "/api/alerts":
self.save_alert()
return
if parsed.path == "/api/admin/settings":
self.save_system_settings()
return
@@ -3676,6 +3729,12 @@ class RequestHandler(BaseHTTPRequestHandler):
)
self.send_json({"ok": True, "deleted": deleted})
return
alert_match = re.fullmatch(r"/api/alerts/(\d+)", parsed.path)
if alert_match:
self.send_json(
{"ok": True, **SERVICE.delete_alert(int(alert_match.group(1)))}
)
return
sector_phase_match = re.fullmatch(r"/api/heaven/sector-phases/(.+)", parsed.path)
if sector_phase_match:
name = unquote(sector_phase_match.group(1)).strip()
@@ -3777,6 +3836,13 @@ class RequestHandler(BaseHTTPRequestHandler):
except (ValueError, json.JSONDecodeError) as exc:
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
def save_alert(self) -> None:
try:
body = self.read_json_body()
self.send_json({"ok": True, **SERVICE.create_alert(body)}, HTTPStatus.CREATED)
except (ValueError, json.JSONDecodeError) as exc:
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
def session_token(self) -> str:
cookie = SimpleCookie()
try: