""" 定时任务服务 使用 APScheduler 管理定时任务 """ import logging from datetime import datetime, timedelta from typing import Optional from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker from sqlalchemy import select, and_, func from app.core.config import settings from app.models.user import User from app.models.user_course_progress import UserCourseProgress, ProgressStatus from app.models.task import Task, TaskAssignment logger = logging.getLogger(__name__) # 全局调度器实例 scheduler: Optional[AsyncIOScheduler] = None async def get_db_session() -> AsyncSession: """获取数据库会话""" engine = create_async_engine(settings.DATABASE_URL, echo=False) async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) return async_session() async def send_learning_reminders(): """ 发送学习提醒 检查所有用户的学习进度,对长时间未学习的用户发送提醒 """ logger.info("开始执行学习提醒任务") try: db = await get_db_session() from app.services.notification_service import NotificationService notification_service = NotificationService(db) # 查找超过3天未学习的用户 three_days_ago = datetime.now() - timedelta(days=3) result = await db.execute( select(UserCourseProgress, User).join( User, UserCourseProgress.user_id == User.id ).where( and_( UserCourseProgress.status == ProgressStatus.IN_PROGRESS.value, UserCourseProgress.last_accessed_at < three_days_ago, ) ) ) inactive_progress = result.all() for progress, user in inactive_progress: # 获取课程名称 from app.models.course import Course course_result = await db.execute( select(Course.name).where(Course.id == progress.course_id) ) course_name = course_result.scalar() or "未知课程" days_inactive = (datetime.now() - progress.last_accessed_at).days # 发送提醒 await notification_service.send_learning_reminder( user_id=user.id, course_name=course_name, days_inactive=days_inactive, ) logger.info(f"已发送学习提醒: user_id={user.id}, course={course_name}") await db.close() logger.info(f"学习提醒任务完成,发送了 {len(inactive_progress)} 条提醒") except Exception as e: logger.error(f"学习提醒任务失败: {str(e)}") async def send_task_deadline_reminders(): """ 发送任务截止提醒 检查即将到期的任务,发送提醒给相关用户 """ logger.info("开始执行任务截止提醒") try: db = await get_db_session() from app.services.notification_service import NotificationService notification_service = NotificationService(db) # 查找3天内到期的未完成任务 now = datetime.now() three_days_later = now + timedelta(days=3) result = await db.execute( select(Task, TaskAssignment, User).join( TaskAssignment, Task.id == TaskAssignment.task_id ).join( User, TaskAssignment.user_id == User.id ).where( and_( Task.end_time.between(now, three_days_later), TaskAssignment.status.in_(["not_started", "in_progress"]), ) ) ) upcoming_tasks = result.all() for task, assignment, user in upcoming_tasks: await notification_service.send_task_deadline_reminder( user_id=user.id, task_name=task.name, deadline=task.end_time, ) logger.info(f"已发送任务截止提醒: user_id={user.id}, task={task.name}") await db.close() logger.info(f"任务截止提醒完成,发送了 {len(upcoming_tasks)} 条提醒") except Exception as e: logger.error(f"任务截止提醒失败: {str(e)}") async def send_weekly_reports(): """ 发送周学习报告 每周一发送上周的学习统计报告 """ logger.info("开始生成周学习报告") try: db = await get_db_session() from app.services.notification_service import NotificationService notification_service = NotificationService(db) # 获取所有活跃用户 result = await db.execute( select(User).where(User.is_active == True) ) users = result.scalars().all() # 计算上周时间范围 today = datetime.now().date() last_week_start = today - timedelta(days=today.weekday() + 7) last_week_end = last_week_start + timedelta(days=6) for user in users: # 统计学习时长 study_time_result = await db.execute( select(func.coalesce(func.sum(UserCourseProgress.total_study_time), 0)).where( and_( UserCourseProgress.user_id == user.id, UserCourseProgress.last_accessed_at.between( datetime.combine(last_week_start, datetime.min.time()), datetime.combine(last_week_end, datetime.max.time()), ) ) ) ) study_time = study_time_result.scalar() or 0 # 统计完成课程数 completed_result = await db.execute( select(func.count(UserCourseProgress.id)).where( and_( UserCourseProgress.user_id == user.id, UserCourseProgress.status == ProgressStatus.COMPLETED.value, UserCourseProgress.completed_at.between( datetime.combine(last_week_start, datetime.min.time()), datetime.combine(last_week_end, datetime.max.time()), ) ) ) ) courses_completed = completed_result.scalar() or 0 # 如果有学习活动,发送报告 if study_time > 0 or courses_completed > 0: await notification_service.send_weekly_report( user_id=user.id, study_time=study_time, courses_completed=courses_completed, exams_passed=0, # TODO: 统计考试通过数 ) logger.info(f"已发送周报: user_id={user.id}") await db.close() logger.info("周学习报告发送完成") except Exception as e: logger.error(f"周学习报告发送失败: {str(e)}") def init_scheduler(): """初始化定时任务调度器""" global scheduler if scheduler is not None: return scheduler scheduler = AsyncIOScheduler() # 学习提醒:每天上午9点执行 scheduler.add_job( send_learning_reminders, CronTrigger(hour=9, minute=0), id="learning_reminders", name="学习提醒", replace_existing=True, ) # 任务截止提醒:每天上午10点执行 scheduler.add_job( send_task_deadline_reminders, CronTrigger(hour=10, minute=0), id="task_deadline_reminders", name="任务截止提醒", replace_existing=True, ) # 周学习报告:每周一上午8点发送 scheduler.add_job( send_weekly_reports, CronTrigger(day_of_week="mon", hour=8, minute=0), id="weekly_reports", name="周学习报告", replace_existing=True, ) logger.info("定时任务调度器初始化完成") return scheduler def start_scheduler(): """启动调度器""" global scheduler if scheduler is None: scheduler = init_scheduler() if not scheduler.running: scheduler.start() logger.info("定时任务调度器已启动") def stop_scheduler(): """停止调度器""" global scheduler if scheduler and scheduler.running: scheduler.shutdown() logger.info("定时任务调度器已停止") def get_scheduler() -> Optional[AsyncIOScheduler]: """获取调度器实例""" return scheduler