feat: add account-scoped in-app reminder center
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user