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:
379
backend/app/services/recommendation_service.py
Normal file
379
backend/app/services/recommendation_service.py
Normal 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)
|
||||
Reference in New Issue
Block a user