Files
012-kaopeilian/backend/app/core/scheduler.py
yuliang_guo 64f5d567fa
Some checks failed
continuous-integration/drone/push Build is failing
feat: 实现 KPL 系统功能改进计划
1. 课程学习进度追踪
   - 新增 UserCourseProgress 和 UserMaterialProgress 模型
   - 新增 /api/v1/progress/* 进度追踪 API
   - 更新 admin.py 使用真实课程完成率数据

2. 路由权限检查完善
   - 新增前端 permissionChecker.ts 权限检查工具
   - 更新 router/guard.ts 实现团队和课程权限验证
   - 新增后端 permission_service.py

3. AI 陪练音频转文本
   - 新增 speech_recognition.py 语音识别服务
   - 新增 /api/v1/speech/* API
   - 更新 ai-practice-coze.vue 支持语音输入

4. 双人对练报告生成
   - 更新 practice_room_service.py 添加报告生成功能
   - 新增 /rooms/{room_code}/report API
   - 更新 duo-practice-report.vue 调用真实 API

5. 学习提醒推送
   - 新增 notification_service.py 通知服务
   - 新增 scheduler_service.py 定时任务服务
   - 支持钉钉、企微、站内消息推送

6. 智能学习推荐
   - 新增 recommendation_service.py 推荐服务
   - 新增 /api/v1/recommendations/* API
   - 支持错题、能力、进度、热门多维度推荐

7. 安全问题修复
   - DEBUG 默认值改为 False
   - 添加 SECRET_KEY 安全警告
   - 新增 check_security_settings() 检查函数

8. 证书 PDF 生成
   - 更新 certificate_service.py 添加 PDF 生成
   - 添加 weasyprint、Pillow、qrcode 依赖
   - 更新下载 API 支持 PDF 和 PNG 格式
2026-01-30 14:22:35 +08:00

243 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
定时任务调度模块
使用 APScheduler 实现定时任务:
- 通讯录增量同步每30分钟
- 通讯录完整同步每天凌晨2点
"""
import os
import asyncio
from datetime import datetime
from typing import Optional
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.interval import IntervalTrigger
from apscheduler.triggers.cron import CronTrigger
from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_EXECUTED
from app.core.logger import logger
class SchedulerManager:
"""
定时任务调度管理器
单例模式,统一管理所有定时任务
"""
_instance: Optional['SchedulerManager'] = None
_scheduler: Optional[AsyncIOScheduler] = None
_initialized: bool = False
# 配置(可通过环境变量覆盖)
AUTO_SYNC_ENABLED: bool = True
INCREMENTAL_SYNC_INTERVAL_MINUTES: int = 30 # 增量同步间隔(分钟)
FULL_SYNC_HOUR: int = 2 # 完整同步执行时间小时24小时制
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
@classmethod
def get_instance(cls) -> 'SchedulerManager':
"""获取调度管理器实例"""
if cls._instance is None:
cls._instance = cls()
return cls._instance
@classmethod
def _load_config(cls):
"""从环境变量加载配置"""
cls.AUTO_SYNC_ENABLED = os.getenv('AUTO_SYNC_ENABLED', 'true').lower() == 'true'
cls.INCREMENTAL_SYNC_INTERVAL_MINUTES = int(os.getenv('AUTO_SYNC_INTERVAL_MINUTES', '30'))
cls.FULL_SYNC_HOUR = int(os.getenv('FULL_SYNC_HOUR', '2'))
async def init(self, db_session_factory):
"""
初始化调度器
Args:
db_session_factory: 数据库会话工厂async_sessionmaker
"""
if self._initialized:
logger.info("调度器已初始化,跳过")
return
self._load_config()
if not self.AUTO_SYNC_ENABLED:
logger.info("自动同步已禁用,调度器不启动")
return
self._db_session_factory = db_session_factory
self._scheduler = AsyncIOScheduler(timezone='Asia/Shanghai')
# 添加任务执行监听器
self._scheduler.add_listener(self._job_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
# 注册定时任务
self._register_jobs()
self._initialized = True
logger.info("调度器初始化完成")
def _register_jobs(self):
"""注册所有定时任务"""
if not self._scheduler:
return
# 1. 增量同步任务每30分钟
self._scheduler.add_job(
self._run_incremental_sync,
IntervalTrigger(minutes=self.INCREMENTAL_SYNC_INTERVAL_MINUTES),
id='employee_incremental_sync',
name='员工增量同步',
replace_existing=True,
max_instances=1, # 防止任务堆积
)
logger.info(f"已注册任务: 员工增量同步(每{self.INCREMENTAL_SYNC_INTERVAL_MINUTES}分钟)")
# 2. 完整同步任务每天凌晨2点
self._scheduler.add_job(
self._run_full_sync,
CronTrigger(hour=self.FULL_SYNC_HOUR, minute=0),
id='employee_full_sync',
name='员工完整同步',
replace_existing=True,
max_instances=1,
)
logger.info(f"已注册任务: 员工完整同步(每天{self.FULL_SYNC_HOUR}:00")
def _job_listener(self, event):
"""任务执行监听器"""
job_id = event.job_id
if event.exception:
logger.error(
f"定时任务执行失败",
job_id=job_id,
error=str(event.exception),
traceback=event.traceback
)
else:
logger.info(
f"定时任务执行完成",
job_id=job_id,
return_value=str(event.retval) if event.retval else None
)
async def _run_incremental_sync(self):
"""执行增量同步"""
from app.services.employee_sync_service import EmployeeSyncService
logger.info("开始执行定时增量同步任务")
start_time = datetime.now()
try:
async with self._db_session_factory() as db:
async with EmployeeSyncService(db) as sync_service:
stats = await sync_service.incremental_sync_employees()
duration = (datetime.now() - start_time).total_seconds()
logger.info(
"定时增量同步完成",
duration_seconds=duration,
stats=stats
)
return stats
except Exception as e:
logger.error(f"定时增量同步失败: {str(e)}")
raise
async def _run_full_sync(self):
"""执行完整同步"""
from app.services.employee_sync_service import EmployeeSyncService
logger.info("开始执行定时完整同步任务")
start_time = datetime.now()
try:
async with self._db_session_factory() as db:
async with EmployeeSyncService(db) as sync_service:
stats = await sync_service.sync_employees()
duration = (datetime.now() - start_time).total_seconds()
logger.info(
"定时完整同步完成",
duration_seconds=duration,
stats=stats
)
return stats
except Exception as e:
logger.error(f"定时完整同步失败: {str(e)}")
raise
def start(self):
"""启动调度器"""
if not self._scheduler:
logger.warning("调度器未初始化,无法启动")
return
if self._scheduler.running:
logger.info("调度器已在运行")
return
self._scheduler.start()
logger.info("调度器已启动")
# 打印已注册的任务
jobs = self._scheduler.get_jobs()
for job in jobs:
logger.info(f" - {job.name} (ID: {job.id}, 下次执行: {job.next_run_time})")
def stop(self):
"""停止调度器"""
if self._scheduler and self._scheduler.running:
self._scheduler.shutdown(wait=True)
logger.info("调度器已停止")
def get_jobs(self):
"""获取所有任务列表"""
if not self._scheduler:
return []
return [
{
'id': job.id,
'name': job.name,
'next_run_time': job.next_run_time.isoformat() if job.next_run_time else None,
'pending': job.pending,
}
for job in self._scheduler.get_jobs()
]
async def trigger_job(self, job_id: str):
"""
手动触发任务
Args:
job_id: 任务ID
"""
if not self._scheduler:
raise RuntimeError("调度器未初始化")
job = self._scheduler.get_job(job_id)
if not job:
raise ValueError(f"任务不存在: {job_id}")
# 立即执行
if job_id == 'employee_incremental_sync':
return await self._run_incremental_sync()
elif job_id == 'employee_full_sync':
return await self._run_full_sync()
else:
raise ValueError(f"未知任务: {job_id}")
# 全局调度管理器实例
scheduler_manager = SchedulerManager.get_instance()