gamification_service.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. """
  2. 游戏化服务 - XP、等级、徽章、连击系统
  3. """
  4. from datetime import date, datetime
  5. from typing import List, Optional, Dict, Tuple
  6. from ..models.learning import GamificationProfile
  7. from .data_store import data_store
  8. # ========== 配置 ==========
  9. XP_PER_LEVEL = 100
  10. # 徽章定义
  11. BADGE_DEFINITIONS: Dict[str, dict] = {
  12. "first_lesson": {
  13. "id": "first_lesson",
  14. "name": "第一步",
  15. "description": "完成第一节课",
  16. "icon": "🌱",
  17. "condition": "完成1节课",
  18. },
  19. "ten_lessons": {
  20. "id": "ten_lessons",
  21. "name": "勤学苦练",
  22. "description": "累计完成10节课",
  23. "icon": "📚",
  24. "condition": "完成10节课",
  25. },
  26. "twenty_lessons": {
  27. "id": "twenty_lessons",
  28. "name": "学富五车",
  29. "description": "累计完成20节课",
  30. "icon": "🧠",
  31. "condition": "完成20节课",
  32. },
  33. "first_assessment": {
  34. "id": "first_assessment",
  35. "name": "自我认知",
  36. "description": "完成第一次水平检测",
  37. "icon": "📊",
  38. "condition": "完成1次测试",
  39. },
  40. "perfect_score": {
  41. "id": "perfect_score",
  42. "name": "完美主义者",
  43. "description": "水平检测获得满分",
  44. "icon": "💯",
  45. "condition": "测试得分100",
  46. },
  47. "speed_demon": {
  48. "id": "speed_demon",
  49. "name": "神速",
  50. "description": "同一天完成5节课",
  51. "icon": "⚡",
  52. "condition": "单日5节课",
  53. },
  54. "first_path": {
  55. "id": "first_path",
  56. "name": "选择方向",
  57. "description": "选择一条学习路径",
  58. "icon": "🛤️",
  59. "condition": "选择路径",
  60. },
  61. "all_modules": {
  62. "id": "all_modules",
  63. "name": "开拓者",
  64. "description": "完成一个路径的所有模块",
  65. "icon": "🏆",
  66. "condition": "完成全部模块",
  67. },
  68. "week_streak": {
  69. "id": "week_streak",
  70. "name": "坚持不懈",
  71. "description": "连续学习7天",
  72. "icon": "🔥",
  73. "condition": "连续7天",
  74. },
  75. "month_streak": {
  76. "id": "month_streak",
  77. "name": "铁杆学员",
  78. "description": "连续学习30天",
  79. "icon": "💎",
  80. "condition": "连续30天",
  81. },
  82. }
  83. class GamificationService:
  84. """游戏化服务"""
  85. def get_profile(self, user_id: str) -> GamificationProfile:
  86. """获取用户游戏化档案"""
  87. profile = data_store.get_gamification(user_id)
  88. if not profile:
  89. profile = GamificationProfile(user_id=user_id)
  90. data_store.save_gamification(profile)
  91. return profile
  92. def award_xp(self, user_id: str, amount: int, reason: str) -> GamificationProfile:
  93. """给用户增加XP"""
  94. profile = self.get_profile(user_id)
  95. profile.total_xp += amount
  96. profile.level = max(1, profile.total_xp // XP_PER_LEVEL + 1)
  97. if len(profile.xp_log) > 500:
  98. profile.xp_log = profile.xp_log[-500:]
  99. profile.xp_log.append({
  100. "amount": amount,
  101. "reason": reason,
  102. "timestamp": datetime.now().isoformat(),
  103. })
  104. # 更新连击
  105. today = date.today().isoformat()
  106. if profile.last_active_date == today:
  107. pass # 今天已经活跃过
  108. elif profile.last_active_date == _yesterday():
  109. profile.streak += 1
  110. else:
  111. profile.streak = 1
  112. profile.last_active_date = today
  113. # 检查新徽章
  114. new_badges = self._check_new_badges(profile, user_id)
  115. data_store.save_gamification(profile)
  116. return profile, new_badges
  117. def _check_new_badges(self, profile: GamificationProfile, user_id: str) -> List[dict]:
  118. """检查是否有新徽章获得"""
  119. progress = data_store.get_user_progress(user_id)
  120. if not progress:
  121. return []
  122. earned = set(profile.badges)
  123. new_badges = []
  124. # 按条件检查
  125. checks = [
  126. ("first_lesson", lambda: len(progress.completed_lessons) >= 1),
  127. ("ten_lessons", lambda: len(progress.completed_lessons) >= 10),
  128. ("twenty_lessons", lambda: len(progress.completed_lessons) >= 20),
  129. ("first_assessment", lambda: len(data_store.get_user_assessments(user_id)) >= 1),
  130. ("first_path", lambda: progress.current_path is not None),
  131. ("all_modules", lambda: progress.current_path is not None and
  132. _all_modules_completed(progress)),
  133. ("week_streak", lambda: profile.streak >= 7),
  134. ("month_streak", lambda: profile.streak >= 30),
  135. ("speed_demon", lambda: False), # 由外部触发
  136. ("perfect_score", lambda: False), # 由外部触发
  137. ]
  138. for badge_id, check_fn in checks:
  139. if badge_id not in earned and check_fn():
  140. badge = dict(BADGE_DEFINITIONS[badge_id])
  141. badge["awarded_at"] = datetime.now().isoformat()
  142. profile.badges.append(badge_id)
  143. new_badges.append(badge)
  144. return new_badges
  145. def check_perfect_score(self, user_id: str):
  146. """检查是否获得完美得分徽章"""
  147. profile = self.get_profile(user_id)
  148. if "perfect_score" in profile.badges:
  149. return
  150. assessments = data_store.get_user_assessments(user_id)
  151. if any(a.score >= 100 for a in assessments):
  152. profile.badges.append("perfect_score")
  153. data_store.save_gamification(profile)
  154. def check_speed_demon(self, user_id: str):
  155. """检查单日5课成就"""
  156. profile = self.get_profile(user_id)
  157. if "speed_demon" in profile.badges:
  158. return
  159. progress = data_store.get_user_progress(user_id)
  160. if not progress:
  161. return
  162. # 完成5节课即授予
  163. if len(progress.completed_lessons) >= 5:
  164. profile.badges.append("speed_demon")
  165. data_store.save_gamification(profile)
  166. def _yesterday() -> str:
  167. from datetime import timedelta
  168. return (date.today() - timedelta(days=1)).isoformat()
  169. def _all_modules_completed(progress) -> bool:
  170. """检查一个路径的所有模块是否完成"""
  171. if not progress.current_path:
  172. return False
  173. from .learning_content import get_learning_path
  174. path_data = get_learning_path(progress.current_path)
  175. if not path_data:
  176. return False
  177. return all(m.id in progress.completed_modules for m in path_data.modules)
  178. # 全局单例
  179. _gamification_service: Optional[GamificationService] = None
  180. def get_gamification_service() -> GamificationService:
  181. global _gamification_service
  182. if _gamification_service is None:
  183. _gamification_service = GamificationService()
  184. return _gamification_service