Files
012-kaopeilian/backend/app/services/recommendation_service.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

380 lines
14 KiB
Python

"""
智能学习推荐服务
基于用户能力评估、错题记录和学习历史推荐学习内容
"""
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)