|
1 | 1 | import logging |
2 | | -from datetime import datetime |
| 2 | +from datetime import datetime, timezone |
3 | 3 |
|
4 | 4 | from fastapi import APIRouter, Depends, HTTPException |
5 | 5 | from pydantic import BaseModel, Field |
6 | 6 | from sqlalchemy.orm import Session |
| 7 | +from zoneinfo import ZoneInfo |
7 | 8 |
|
| 9 | +from src.config import Settings |
8 | 10 | from src.core.price_alert_engine import ENGINE |
9 | 11 | from src.web.database import get_db |
10 | 12 | from src.web.models import PriceAlertHit, PriceAlertRule, Stock |
|
13 | 15 | router = APIRouter() |
14 | 16 |
|
15 | 17 |
|
| 18 | +def _format_datetime(dt) -> str: |
| 19 | + """格式化时间为当前时区的 ISO 格式(naive datetime 视为 UTC)。""" |
| 20 | + if not dt: |
| 21 | + return "" |
| 22 | + tz_name = Settings().app_timezone or "UTC" |
| 23 | + try: |
| 24 | + tzinfo = ZoneInfo(tz_name) |
| 25 | + except Exception: |
| 26 | + tzinfo = timezone.utc |
| 27 | + if dt.tzinfo is None: |
| 28 | + dt = dt.replace(tzinfo=timezone.utc) |
| 29 | + return dt.astimezone(tzinfo).isoformat(timespec="seconds") |
| 30 | + |
| 31 | + |
16 | 32 | class AlertConditionItem(BaseModel): |
17 | 33 | type: str = Field(..., description="price/change_pct/turnover/volume/volume_ratio") |
18 | 34 | op: str = Field(..., description=">=/<=/>/</==/between") |
@@ -85,14 +101,14 @@ def _to_response(rule: PriceAlertRule) -> dict: |
85 | 101 | "cooldown_minutes": rule.cooldown_minutes, |
86 | 102 | "max_triggers_per_day": rule.max_triggers_per_day, |
87 | 103 | "repeat_mode": rule.repeat_mode, |
88 | | - "expire_at": rule.expire_at.isoformat() if rule.expire_at else None, |
| 104 | + "expire_at": _format_datetime(rule.expire_at) or None, |
89 | 105 | "notify_channel_ids": rule.notify_channel_ids or [], |
90 | | - "last_trigger_at": rule.last_trigger_at.isoformat() if rule.last_trigger_at else None, |
| 106 | + "last_trigger_at": _format_datetime(rule.last_trigger_at) or None, |
91 | 107 | "last_trigger_price": rule.last_trigger_price, |
92 | 108 | "trigger_count_today": rule.trigger_count_today or 0, |
93 | 109 | "trigger_date": rule.trigger_date or "", |
94 | | - "created_at": rule.created_at.isoformat() if rule.created_at else "", |
95 | | - "updated_at": rule.updated_at.isoformat() if rule.updated_at else "", |
| 110 | + "created_at": _format_datetime(rule.created_at), |
| 111 | + "updated_at": _format_datetime(rule.updated_at), |
96 | 112 | } |
97 | 113 |
|
98 | 114 |
|
@@ -165,6 +181,11 @@ def update_alert_rule(rule_id: int, body: PriceAlertUpdate, db: Session = Depend |
165 | 181 |
|
166 | 182 | for k, v in updates.items(): |
167 | 183 | setattr(row, k, v) |
| 184 | + |
| 185 | + # 修改提醒规则后重置当日触发计数 |
| 186 | + row.trigger_count_today = 0 |
| 187 | + row.trigger_date = "" |
| 188 | + |
168 | 189 | db.commit() |
169 | 190 | db.refresh(row) |
170 | 191 | return _to_response(row) |
@@ -206,7 +227,7 @@ def list_alert_hits(rule_id: int, limit: int = 50, db: Session = Depends(get_db) |
206 | 227 | "id": r.id, |
207 | 228 | "rule_id": r.rule_id, |
208 | 229 | "stock_id": r.stock_id, |
209 | | - "trigger_time": r.trigger_time.isoformat() if r.trigger_time else "", |
| 230 | + "trigger_time": _format_datetime(r.trigger_time), |
210 | 231 | "trigger_snapshot": r.trigger_snapshot or {}, |
211 | 232 | "notify_success": bool(r.notify_success), |
212 | 233 | "notify_error": r.notify_error or "", |
|
0 commit comments