- 新增数字员工财务报告生成、邮件投递与渲染调度器 - 引入员工画像扫描调度与定时提醒任务 - 完善财务看板快照、排行口径与部门人员占比计算 - 优化数字员工工作看板仪表盘与技能目录 - 增强前端总览页图表、工作台摘要与顶部导航栏交互 - 新增差旅申请规划推动提醒与报销创建会话状态管理 - 补充财务报告、看板调度、数字员工工作记录测试覆盖
103 lines
3.9 KiB
Python
103 lines
3.9 KiB
Python
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()
|