feat: 实现 KPL 系统功能改进计划
Some checks failed
continuous-integration/drone/push Build is failing

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 格式
This commit is contained in:
yuliang_guo
2026-01-30 14:22:35 +08:00
parent 9793013a56
commit 64f5d567fa
66 changed files with 18067 additions and 14330 deletions

View File

@@ -23,7 +23,9 @@ class Settings(BaseSettings):
# 应用基础配置
APP_NAME: str = "KaoPeiLian"
APP_VERSION: str = "1.0.0"
DEBUG: bool = Field(default=True)
# DEBUG 模式:生产环境必须设置为 False
# 通过环境变量 DEBUG=false 或在 .env 文件中设置
DEBUG: bool = Field(default=False, description="调试模式,生产环境必须设置为 False")
# 租户配置(用于多租户部署)
TENANT_CODE: str = Field(default="demo", description="租户编码,如 hua, yy, hl")
@@ -56,7 +58,12 @@ class Settings(BaseSettings):
REDIS_URL: str = Field(default="redis://localhost:6379/0")
# JWT配置
SECRET_KEY: str = Field(default="your-secret-key-here")
# 安全警告:必须在生产环境设置 SECRET_KEY 环境变量
# 可以使用命令生成python -c "import secrets; print(secrets.token_urlsafe(32))"
SECRET_KEY: str = Field(
default="INSECURE-DEFAULT-KEY-CHANGE-IN-PRODUCTION",
description="JWT 密钥,生产环境必须通过环境变量设置安全的随机密钥"
)
ALGORITHM: str = Field(default="HS256")
ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=30)
REFRESH_TOKEN_EXPIRE_DAYS: int = Field(default=7)
@@ -165,6 +172,57 @@ def get_settings() -> Settings:
settings = get_settings()
def check_security_settings() -> list[str]:
"""
检查安全配置
返回安全警告列表,生产环境应确保列表为空
"""
warnings = []
# 检查 DEBUG 模式
if settings.DEBUG:
warnings.append(
"⚠️ DEBUG 模式已开启。生产环境请设置 DEBUG=false"
)
# 检查 SECRET_KEY
if settings.SECRET_KEY == "INSECURE-DEFAULT-KEY-CHANGE-IN-PRODUCTION":
warnings.append(
"⚠️ 使用默认 SECRET_KEY 不安全。生产环境请设置安全的 SECRET_KEY 环境变量。"
"生成命令python -c \"import secrets; print(secrets.token_urlsafe(32))\""
)
elif len(settings.SECRET_KEY) < 32:
warnings.append(
"⚠️ SECRET_KEY 长度不足 32 字符,安全性较弱"
)
# 检查数据库密码
if settings.MYSQL_PASSWORD in ["password", "123456", "root", ""]:
warnings.append(
"⚠️ 数据库密码不安全,请使用强密码"
)
return warnings
def print_security_warnings():
"""打印安全警告(应用启动时调用)"""
import logging
logger = logging.getLogger(__name__)
warnings = check_security_settings()
if warnings:
logger.warning("=" * 60)
logger.warning("安全配置警告:")
for warning in warnings:
logger.warning(warning)
logger.warning("=" * 60)
else:
logger.info("✅ 安全配置检查通过")
# ============================================
# 动态配置获取(支持从数据库读取)
# ============================================

View File

@@ -1,242 +1,242 @@
"""
定时任务调度模块
使用 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()
"""
定时任务调度模块
使用 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()