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

@@ -0,0 +1,379 @@
"""
智能学习推荐服务
基于用户能力评估、错题记录和学习历史推荐学习内容
"""
import logging
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, func, desc
from sqlalchemy.orm import selectinload
from app.models.user import User
from app.models.course import Course, CourseStatus, CourseMaterial, KnowledgePoint
from app.models.exam import ExamResult
from app.models.exam_mistake import ExamMistake
from app.models.user_course_progress import UserCourseProgress, ProgressStatus
from app.models.ability import AbilityAssessment
logger = logging.getLogger(__name__)
class RecommendationService:
"""
智能学习推荐服务
推荐策略:
1. 基于错题分析:推荐与错题相关的知识点和课程
2. 基于能力评估:推荐弱项能力相关的课程
3. 基于学习进度:推荐未完成的课程继续学习
4. 基于热门课程:推荐学习人数多的课程
5. 基于岗位要求:推荐岗位必修课程
"""
def __init__(self, db: AsyncSession):
self.db = db
async def get_recommendations(
self,
user_id: int,
limit: int = 10,
include_reasons: bool = True,
) -> List[Dict[str, Any]]:
"""
获取个性化学习推荐
Args:
user_id: 用户ID
limit: 推荐数量上限
include_reasons: 是否包含推荐理由
Returns:
推荐课程列表,包含课程信息和推荐理由
"""
recommendations = []
# 1. 基于错题推荐
mistake_recs = await self._get_mistake_based_recommendations(user_id)
recommendations.extend(mistake_recs)
# 2. 基于能力评估推荐
ability_recs = await self._get_ability_based_recommendations(user_id)
recommendations.extend(ability_recs)
# 3. 基于未完成课程推荐
progress_recs = await self._get_progress_based_recommendations(user_id)
recommendations.extend(progress_recs)
# 4. 基于热门课程推荐
popular_recs = await self._get_popular_recommendations(user_id)
recommendations.extend(popular_recs)
# 去重并排序
seen_course_ids = set()
unique_recs = []
for rec in recommendations:
if rec["course_id"] not in seen_course_ids:
seen_course_ids.add(rec["course_id"])
unique_recs.append(rec)
# 按优先级排序
priority_map = {
"mistake": 1,
"ability": 2,
"progress": 3,
"popular": 4,
}
unique_recs.sort(key=lambda x: priority_map.get(x.get("source", ""), 5))
# 限制数量
result = unique_recs[:limit]
# 移除 source 字段如果不需要理由
if not include_reasons:
for rec in result:
rec.pop("source", None)
rec.pop("reason", None)
return result
async def _get_mistake_based_recommendations(
self,
user_id: int,
limit: int = 3,
) -> List[Dict[str, Any]]:
"""基于错题推荐"""
recommendations = []
try:
# 获取用户最近的错题
result = await self.db.execute(
select(ExamMistake).where(
ExamMistake.user_id == user_id
).order_by(
desc(ExamMistake.created_at)
).limit(50)
)
mistakes = result.scalars().all()
if not mistakes:
return recommendations
# 统计错题涉及的知识点
knowledge_point_counts = {}
for mistake in mistakes:
if hasattr(mistake, 'knowledge_point_id') and mistake.knowledge_point_id:
kp_id = mistake.knowledge_point_id
knowledge_point_counts[kp_id] = knowledge_point_counts.get(kp_id, 0) + 1
if not knowledge_point_counts:
return recommendations
# 找出错误最多的知识点对应的课程
top_kp_ids = sorted(
knowledge_point_counts.keys(),
key=lambda x: knowledge_point_counts[x],
reverse=True
)[:5]
course_result = await self.db.execute(
select(Course, KnowledgePoint).join(
KnowledgePoint, Course.id == KnowledgePoint.course_id
).where(
and_(
KnowledgePoint.id.in_(top_kp_ids),
Course.status == CourseStatus.PUBLISHED,
Course.is_deleted == False,
)
).distinct()
)
for course, kp in course_result.all()[:limit]:
recommendations.append({
"course_id": course.id,
"course_name": course.name,
"category": course.category.value if course.category else None,
"cover_image": course.cover_image,
"description": course.description,
"source": "mistake",
"reason": f"您在「{kp.name}」知识点上有错题,建议复习相关内容",
})
except Exception as e:
logger.error(f"基于错题推荐失败: {str(e)}")
return recommendations
async def _get_ability_based_recommendations(
self,
user_id: int,
limit: int = 3,
) -> List[Dict[str, Any]]:
"""基于能力评估推荐"""
recommendations = []
try:
# 获取用户最近的能力评估
result = await self.db.execute(
select(AbilityAssessment).where(
AbilityAssessment.user_id == user_id
).order_by(
desc(AbilityAssessment.created_at)
).limit(1)
)
assessment = result.scalar_one_or_none()
if not assessment:
return recommendations
# 解析能力评估结果,找出弱项
scores = {}
if hasattr(assessment, 'dimension_scores') and assessment.dimension_scores:
scores = assessment.dimension_scores
elif hasattr(assessment, 'scores') and assessment.scores:
scores = assessment.scores
if not scores:
return recommendations
# 找出分数最低的维度
weak_dimensions = sorted(
scores.items(),
key=lambda x: x[1] if isinstance(x[1], (int, float)) else 0
)[:3]
# 根据弱项维度推荐课程(按分类匹配)
category_map = {
"专业知识": "technology",
"沟通能力": "business",
"管理能力": "management",
}
for dim_name, score in weak_dimensions:
if isinstance(score, (int, float)) and score < 70:
category = category_map.get(dim_name)
if category:
course_result = await self.db.execute(
select(Course).where(
and_(
Course.category == category,
Course.status == CourseStatus.PUBLISHED,
Course.is_deleted == False,
)
).order_by(
desc(Course.student_count)
).limit(1)
)
course = course_result.scalar_one_or_none()
if course:
recommendations.append({
"course_id": course.id,
"course_name": course.name,
"category": course.category.value if course.category else None,
"cover_image": course.cover_image,
"description": course.description,
"source": "ability",
"reason": f"您的「{dim_name}」能力评分较低({score}分),推荐学习此课程提升",
})
except Exception as e:
logger.error(f"基于能力评估推荐失败: {str(e)}")
return recommendations[:limit]
async def _get_progress_based_recommendations(
self,
user_id: int,
limit: int = 3,
) -> List[Dict[str, Any]]:
"""基于学习进度推荐"""
recommendations = []
try:
# 获取未完成的课程
result = await self.db.execute(
select(UserCourseProgress, Course).join(
Course, UserCourseProgress.course_id == Course.id
).where(
and_(
UserCourseProgress.user_id == user_id,
UserCourseProgress.status == ProgressStatus.IN_PROGRESS.value,
Course.is_deleted == False,
)
).order_by(
desc(UserCourseProgress.last_accessed_at)
).limit(limit)
)
for progress, course in result.all():
recommendations.append({
"course_id": course.id,
"course_name": course.name,
"category": course.category.value if course.category else None,
"cover_image": course.cover_image,
"description": course.description,
"progress_percent": progress.progress_percent,
"source": "progress",
"reason": f"继续学习,已完成 {progress.progress_percent:.0f}%",
})
except Exception as e:
logger.error(f"基于进度推荐失败: {str(e)}")
return recommendations
async def _get_popular_recommendations(
self,
user_id: int,
limit: int = 3,
) -> List[Dict[str, Any]]:
"""基于热门课程推荐"""
recommendations = []
try:
# 获取用户已学习的课程ID
learned_result = await self.db.execute(
select(UserCourseProgress.course_id).where(
UserCourseProgress.user_id == user_id
)
)
learned_course_ids = [row[0] for row in learned_result.all()]
# 获取热门课程(排除已学习的)
query = select(Course).where(
and_(
Course.status == CourseStatus.PUBLISHED,
Course.is_deleted == False,
)
).order_by(
desc(Course.student_count)
).limit(limit + len(learned_course_ids))
result = await self.db.execute(query)
courses = result.scalars().all()
for course in courses:
if course.id not in learned_course_ids:
recommendations.append({
"course_id": course.id,
"course_name": course.name,
"category": course.category.value if course.category else None,
"cover_image": course.cover_image,
"description": course.description,
"student_count": course.student_count,
"source": "popular",
"reason": f"热门课程,已有 {course.student_count} 人学习",
})
if len(recommendations) >= limit:
break
except Exception as e:
logger.error(f"基于热门推荐失败: {str(e)}")
return recommendations
async def get_knowledge_point_recommendations(
self,
user_id: int,
limit: int = 5,
) -> List[Dict[str, Any]]:
"""
获取知识点级别的推荐
基于错题和能力评估推荐具体的知识点
"""
recommendations = []
try:
# 获取错题涉及的知识点
mistake_result = await self.db.execute(
select(
KnowledgePoint,
func.count(ExamMistake.id).label('mistake_count')
).join(
ExamMistake,
ExamMistake.knowledge_point_id == KnowledgePoint.id
).where(
ExamMistake.user_id == user_id
).group_by(
KnowledgePoint.id
).order_by(
desc('mistake_count')
).limit(limit)
)
for kp, count in mistake_result.all():
recommendations.append({
"knowledge_point_id": kp.id,
"name": kp.name,
"description": kp.description,
"type": kp.type,
"course_id": kp.course_id,
"mistake_count": count,
"reason": f"您在此知识点有 {count} 道错题,建议重点复习",
})
except Exception as e:
logger.error(f"知识点推荐失败: {str(e)}")
return recommendations
# 便捷函数
def get_recommendation_service(db: AsyncSession) -> RecommendationService:
"""获取推荐服务实例"""
return RecommendationService(db)