learning.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. from fastapi import APIRouter, HTTPException, Query
  2. from pydantic import BaseModel
  3. from typing import Optional
  4. from datetime import datetime
  5. from ...models.learning import (
  6. LearningPath, UserProgress, LearningPathData, CoachResponse,
  7. CoachRecommendation, ModuleStatus
  8. )
  9. from ...services.learning_content import get_learning_path, get_all_paths, find_next_lesson
  10. from ...services.data_store import data_store
  11. from ...services.gamification_service import get_gamification_service
  12. router = APIRouter(prefix="/api/learning", tags=["learning"])
  13. def get_user_progress(user_id: str = "default") -> UserProgress:
  14. """获取用户进度,使用data_store持久化"""
  15. progress = data_store.get_user_progress(user_id)
  16. if not progress:
  17. progress = UserProgress(
  18. user_id=user_id,
  19. started_at=datetime.now(),
  20. last_activity_at=datetime.now()
  21. )
  22. data_store.save_user_progress(progress)
  23. return progress
  24. def save_user_progress(progress: UserProgress):
  25. """保存用户进度"""
  26. progress.last_activity_at = datetime.now()
  27. data_store.save_user_progress(progress)
  28. @router.get("/paths")
  29. async def list_learning_paths(user_id: str = Query("default")):
  30. """获取所有学习路径概览"""
  31. paths = get_all_paths()
  32. return {
  33. "paths": [
  34. {
  35. "path": p.path.value,
  36. "title": p.title,
  37. "description": p.description,
  38. "icon": p.icon,
  39. "total_modules": len(p.modules),
  40. "total_lessons": sum(len(m.lessons) for m in p.modules)
  41. }
  42. for p in paths
  43. ]
  44. }
  45. @router.get("/paths/{path_type}")
  46. async def get_learning_path_detail(path_type: LearningPath, user_id: str = Query("default")):
  47. """获取指定学习路径详情"""
  48. path_data = get_learning_path(path_type)
  49. progress = get_user_progress(user_id)
  50. # 根据用户进度更新模块状态
  51. for module in path_data.modules:
  52. module_progress = calculate_module_progress(module.id, progress)
  53. module.progress = module_progress
  54. if module_progress >= 100:
  55. module.status = ModuleStatus.COMPLETED
  56. elif module_progress > 0:
  57. module.status = ModuleStatus.IN_PROGRESS
  58. elif is_module_unlocked(module.order, progress):
  59. module.status = ModuleStatus.NOT_STARTED
  60. else:
  61. module.status = ModuleStatus.LOCKED
  62. # 计算总进度
  63. total_lessons = sum(len(m.lessons) for m in path_data.modules)
  64. completed_lessons = sum(
  65. len([l for l in m.lessons if l.id in progress.completed_lessons])
  66. for m in path_data.modules
  67. )
  68. path_data.total_lessons = total_lessons
  69. path_data.completed_lessons = completed_lessons
  70. path_data.progress = (completed_lessons / total_lessons * 100) if total_lessons > 0 else 0
  71. return path_data
  72. @router.get("/progress")
  73. async def get_progress(user_id: str = Query("default")):
  74. """获取用户学习进度"""
  75. progress = get_user_progress(user_id)
  76. return progress
  77. @router.post("/select-path/{path_type}")
  78. async def select_learning_path(path_type: LearningPath, user_id: str = Query("default")):
  79. """选择学习路径"""
  80. progress = get_user_progress(user_id)
  81. progress.current_path = path_type
  82. # 获取路径数据,设置第一个模块为当前
  83. path_data = get_learning_path(path_type)
  84. if path_data.modules:
  85. progress.current_module = path_data.modules[0].id
  86. save_user_progress(progress)
  87. return {"message": f"已选择{path_type.value}路径", "progress": progress}
  88. @router.post("/complete-lesson/{lesson_id}")
  89. async def complete_lesson(lesson_id: str, user_id: str = Query("default")):
  90. """标记课程完成"""
  91. progress = get_user_progress(user_id)
  92. is_new = False
  93. if lesson_id not in progress.completed_lessons:
  94. progress.completed_lessons.append(lesson_id)
  95. is_new = True
  96. # 检查是否完成整个模块
  97. path_data = get_learning_path(progress.current_path) if progress.current_path else None
  98. if path_data:
  99. for module in path_data.modules:
  100. lesson_ids = [l.id for l in module.lessons]
  101. if lesson_id in lesson_ids:
  102. if all(lid in progress.completed_lessons for lid in lesson_ids):
  103. if module.id not in progress.completed_modules:
  104. progress.completed_modules.append(module.id)
  105. break
  106. save_user_progress(progress)
  107. # 发放XP奖励
  108. xp_awarded = 0
  109. new_badges = []
  110. if is_new:
  111. svc = get_gamification_service()
  112. # 基础XP:每节课10XP
  113. xp_awarded = 10
  114. profile, new_badges = svc.award_xp(user_id, xp_awarded, f"完成课程: {lesson_id}")
  115. # 查找下一课程(用于前端自动跳转)
  116. next_lesson = None
  117. if progress.current_path and is_new:
  118. next_lesson = find_next_lesson(
  119. path_type=progress.current_path.value,
  120. current_lesson_id=lesson_id,
  121. completed_lessons=progress.completed_lessons,
  122. )
  123. return {
  124. "message": "课程已标记完成",
  125. "progress": progress,
  126. "xp_awarded": xp_awarded,
  127. "new_badges": new_badges,
  128. "next_lesson": next_lesson,
  129. }
  130. def calculate_module_progress(module_id: str, progress: UserProgress) -> float:
  131. """计算模块进度"""
  132. path_data = get_learning_path(progress.current_path) if progress.current_path else None
  133. if not path_data:
  134. return 0.0
  135. for module in path_data.modules:
  136. if module.id == module_id:
  137. total = len(module.lessons)
  138. if total == 0:
  139. return 0.0
  140. completed = sum(1 for l in module.lessons if l.id in progress.completed_lessons)
  141. return (completed / total) * 100
  142. return 0.0
  143. def is_module_unlocked(module_order: int, progress: UserProgress) -> bool:
  144. """检查模块是否解锁"""
  145. if module_order <= 1:
  146. return True
  147. path_data = get_learning_path(progress.current_path) if progress.current_path else None
  148. if not path_data:
  149. return False
  150. # 找到前一个模块
  151. prev_module = None
  152. for m in path_data.modules:
  153. if m.order == module_order - 1:
  154. prev_module = m
  155. break
  156. if prev_module:
  157. return prev_module.id in progress.completed_modules
  158. return False
  159. @router.get("/coach")
  160. async def get_coach_recommendations(user_id: str = Query("default")):
  161. """获取学习教练推荐"""
  162. progress = get_user_progress(user_id)
  163. if not progress.current_path:
  164. return CoachResponse(
  165. greeting="👋 你好!我是你的学习教练。",
  166. recommendations=[
  167. CoachRecommendation(
  168. type="select_path",
  169. title="选择学习路径",
  170. description="首先选择一个学习路径开始你的学习之旅",
  171. priority=5
  172. )
  173. ],
  174. encouragement="每个人都有自己的学习节奏,加油!",
  175. stats={"total_study_minutes": 0, "completed_lessons": 0},
  176. learning_plan=progress.learning_plan # 如果有AI生成的计划则带上
  177. )
  178. path_data = get_learning_path(progress.current_path)
  179. recommendations = []
  180. # 找到下一个未完成的课程
  181. next_lesson = None
  182. next_module = None
  183. for module in path_data.modules:
  184. if module.id in progress.completed_modules:
  185. continue
  186. for lesson in module.lessons:
  187. if lesson.id not in progress.completed_lessons:
  188. next_lesson = lesson
  189. next_module = module
  190. break
  191. if next_lesson:
  192. break
  193. if next_lesson and next_module:
  194. recommendations.append(
  195. CoachRecommendation(
  196. type="next_lesson",
  197. title=f"继续学习: {next_lesson.title}",
  198. description=f"来自 {next_module.title} 模块",
  199. module_id=next_module.id,
  200. lesson_id=next_lesson.id,
  201. priority=5
  202. )
  203. )
  204. # 如果有完成的模块,建议复习
  205. if progress.completed_modules:
  206. recommendations.append(
  207. CoachRecommendation(
  208. type="review",
  209. title="复习已完成内容",
  210. description="巩固已学知识,加深理解",
  211. priority=3
  212. )
  213. )
  214. # 生成鼓励语
  215. completed_count = len(progress.completed_lessons)
  216. if completed_count == 0:
  217. encouragement = "🌱 刚开始学习,每一步都是进步!"
  218. elif completed_count < 5:
  219. encouragement = "💪 开了个好头,继续努力!"
  220. elif completed_count < 15:
  221. encouragement = "🚀 学习势头很好,保持下去!"
  222. else:
  223. encouragement = "🌟 你已经学了很多,快要成为专家了!"
  224. return CoachResponse(
  225. greeting=f"👋 你好!你正在学习 {path_data.title}。",
  226. recommendations=recommendations,
  227. encouragement=encouragement,
  228. stats={
  229. "total_study_minutes": progress.total_study_minutes,
  230. "completed_lessons": completed_count,
  231. "completed_modules": len(progress.completed_modules),
  232. "current_path": progress.current_path.value if progress.current_path else None
  233. },
  234. learning_plan=progress.learning_plan
  235. )
  236. @router.post("/ai-plan")
  237. async def save_ai_plan(data: dict, user_id: str = Query("default")):
  238. """保存AI生成的个性化学习计划"""
  239. plan_text = data.get("plan_text", "")
  240. progress = get_user_progress(user_id)
  241. progress.learning_plan = plan_text
  242. save_user_progress(progress)
  243. return {"message": "学习计划已保存"}
  244. class SessionUpdate(BaseModel):
  245. lesson_id: Optional[str] = None
  246. module_id: Optional[str] = None
  247. lesson_title: Optional[str] = None
  248. module_title: Optional[str] = None
  249. path_type: Optional[str] = None
  250. last_reply_summary: Optional[str] = None
  251. @router.get("/session")
  252. async def get_session(user_id: str = Query("default")):
  253. """获取用户最近的会话数据"""
  254. progress = get_user_progress(user_id)
  255. return {"session": progress.last_session or None}
  256. @router.post("/session")
  257. async def save_session(update: SessionUpdate, user_id: str = Query("default"), clear: bool = Query(False)):
  258. """保存/更新用户会话数据。clear=true 则清除会话"""
  259. progress = get_user_progress(user_id)
  260. if clear:
  261. progress.last_session = None
  262. save_user_progress(progress)
  263. return {"message": "ok", "session": None}
  264. if progress.last_session is None:
  265. progress.last_session = {}
  266. update_data = update.model_dump(exclude_unset=True)
  267. progress.last_session.update(update_data)
  268. save_user_progress(progress)
  269. return {"message": "ok", "session": progress.last_session}