"""报表定时调度服务 - Schedule report tasks via APScheduler and send results by email.""" import logging from datetime import date, timedelta, datetime, timezone from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger from sqlalchemy import select from app.core.database import async_session from app.models.report import ReportTask, ReportTemplate from app.services.report_generator import ReportGenerator from app.services.email_service import send_email logger = logging.getLogger("report_scheduler") _scheduler: AsyncIOScheduler | None = None def _parse_cron(cron_expr: str) -> dict: """Parse a 5-field cron expression into APScheduler CronTrigger kwargs.""" parts = cron_expr.strip().split() if len(parts) != 5: raise ValueError(f"Invalid cron expression (need 5 fields): {cron_expr}") return { "minute": parts[0], "hour": parts[1], "day": parts[2], "month": parts[3], "day_of_week": parts[4], } async def _run_report_task(task_id: int): """Execute a single report task: generate the report and email it to recipients.""" logger.info(f"Running scheduled report task id={task_id}") async with async_session() as session: # Load task task_result = await session.execute( select(ReportTask).where(ReportTask.id == task_id) ) task = task_result.scalar_one_or_none() if not task: logger.warning(f"Report task id={task_id} not found, skipping.") return if not task.is_active: logger.info(f"Report task id={task_id} is inactive, skipping.") return # Update status task.status = "running" task.last_run = datetime.now(timezone.utc) await session.flush() # Load template to determine report type tmpl_result = await session.execute( select(ReportTemplate).where(ReportTemplate.id == task.template_id) ) template = tmpl_result.scalar_one_or_none() if not template: logger.error(f"Template id={task.template_id} not found for task id={task_id}") task.status = "failed" await session.commit() return try: generator = ReportGenerator(session) today = date.today() export_format = task.export_format or "xlsx" # Choose generation method based on template report_type if template.report_type == "daily": yesterday = today - timedelta(days=1) filepath = await generator.generate_energy_daily_report( start_date=yesterday, end_date=yesterday, export_format=export_format ) elif template.report_type == "monthly": # Generate for previous month first_of_month = today.replace(day=1) last_month_end = first_of_month - timedelta(days=1) last_month_start = last_month_end.replace(day=1) filepath = await generator.generate_monthly_summary( month=last_month_start.month, year=last_month_start.year, export_format=export_format, ) elif template.report_type == "custom" and "device" in template.name.lower(): filepath = await generator.generate_device_status_report( export_format=export_format ) else: # Default: daily report for yesterday yesterday = today - timedelta(days=1) filepath = await generator.generate_energy_daily_report( start_date=yesterday, end_date=yesterday, export_format=export_format ) task.file_path = filepath task.status = "completed" logger.info(f"Report task id={task_id} completed: {filepath}") # Send email with attachment if recipients configured recipients = task.recipients or [] if isinstance(recipients, list) and recipients: report_name = task.name or template.name subject = f"{report_name} - 天普EMS自动报表" body_html = f"""
您好,
系统已自动生成 {report_name},请查收附件。
生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
报表类型: {template.report_type}
格式: {export_format.upper()}
此为系统自动发送,请勿回复。