from __future__ import annotations import os import threading from datetime import datetime, time, timedelta from zoneinfo import ZoneInfo from app.core.agent_enums import AgentRunSource from app.core.logging import get_logger from app.db.session import get_session_factory from app.services.digital_employee_reminder_task import DigitalEmployeeReminderTaskService logger = get_logger("app.services.digital_employee_reminder_scheduler") class DigitalEmployeeReminderScheduler: def __init__(self) -> None: timezone_name = str(os.environ.get("X_FINANCIAL_SCHEDULER_TZ") or "Asia/Shanghai").strip() reminder_time = str(os.environ.get("X_FINANCIAL_REMINDER_SCAN_TIME") or "02:00").strip() initial_delay = int(os.environ.get("X_FINANCIAL_REMINDER_INITIAL_DELAY_SECONDS") or "24") self._timezone = ZoneInfo(timezone_name or "Asia/Shanghai") self._scan_time = self._parse_scan_time(reminder_time) self._initial_delay_seconds = max(1, initial_delay) self._stop_event = threading.Event() self._thread: threading.Thread | None = None self._lock = threading.Lock() def start(self) -> None: with self._lock: if self._thread is not None and self._thread.is_alive(): return self._stop_event.clear() self._thread = threading.Thread( target=self._run_loop, name="digital-employee-reminder-scheduler", daemon=True, ) self._thread.start() logger.info( "Digital employee reminder scheduler started timezone=%s scan_time=%s", self._timezone.key, self._scan_time.strftime("%H:%M"), ) def shutdown(self) -> None: with self._lock: thread = self._thread self._thread = None self._stop_event.set() if thread is not None and thread.is_alive(): thread.join(timeout=3) logger.info("Digital employee reminder scheduler stopped") def _run_loop(self) -> None: if self._stop_event.wait(self._initial_delay_seconds): return self._refresh_reminders(reason="startup_warmup") while not self._stop_event.is_set(): wait_seconds = self._seconds_until_next_scan() if self._stop_event.wait(wait_seconds): break self._refresh_reminders(reason="scheduled_0200") def _refresh_reminders(self, *, reason: str) -> None: db = get_session_factory()() try: result = DigitalEmployeeReminderTaskService(db).refresh_reminders( source=AgentRunSource.SCHEDULE.value ) summary = result.get("summary") or {} logger.info( "Digital employee reminder scan generated reason=%s recipients=%s reminders=%s", reason, summary.get("recipient_count"), summary.get("reminder_count"), ) except Exception: db.rollback() logger.exception("Scheduled digital employee reminder scan failed") finally: db.close() def _seconds_until_next_scan(self) -> float: now = datetime.now(self._timezone) target = datetime.combine(now.date(), self._scan_time, tzinfo=self._timezone) if target <= now: target = target + timedelta(days=1) return max(1.0, (target - now).total_seconds()) @staticmethod def _parse_scan_time(raw_value: str) -> time: value = str(raw_value or "").strip() try: hour_text, minute_text = value.split(":", 1) hour = min(max(int(hour_text), 0), 23) minute = min(max(int(minute_text), 0), 59) return time(hour=hour, minute=minute) except Exception: return time(hour=2, minute=0) digital_employee_reminder_scheduler = DigitalEmployeeReminderScheduler()