data_store.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. """
  2. JSON持久化存储服务
  3. 将用户进度和测试结果保存到本地JSON文件
  4. """
  5. import json
  6. import os
  7. from pathlib import Path
  8. from typing import Optional, List, Dict
  9. from datetime import datetime
  10. from ..models.learning import (
  11. UserProgress, AssessmentResult, LearningPath, UserLevel, GamificationProfile,
  12. CodeSubmission
  13. )
  14. # 数据存储目录
  15. DATA_DIR = Path(__file__).parent.parent.parent / "data"
  16. USER_PROGRESS_FILE = DATA_DIR / "user_progress.json"
  17. ASSESSMENTS_FILE = DATA_DIR / "assessments.json"
  18. GAMIFICATION_FILE = DATA_DIR / "gamification.json"
  19. SUBMISSIONS_FILE = DATA_DIR / "submissions.json"
  20. class DataStore:
  21. """JSON文件存储服务"""
  22. def __init__(self):
  23. # 确保数据目录存在
  24. DATA_DIR.mkdir(parents=True, exist_ok=True)
  25. # 初始化进度存储
  26. self._progress: Dict[str, UserProgress] = {}
  27. self._load_progress()
  28. # 初始化测试结果存储
  29. self._assessments: Dict[str, List[AssessmentResult]] = {}
  30. self._load_assessments()
  31. # 初始化游戏化存储
  32. self._gamifications: Dict[str, GamificationProfile] = {}
  33. self._load_gamifications()
  34. # 初始化代码提交存储
  35. self._submissions: Dict[str, List[CodeSubmission]] = {}
  36. self._load_submissions()
  37. def _load_progress(self):
  38. """从文件加载用户进度"""
  39. if USER_PROGRESS_FILE.exists():
  40. try:
  41. with open(USER_PROGRESS_FILE, "r", encoding="utf-8") as f:
  42. data = json.load(f)
  43. for user_id, progress_data in data.items():
  44. # 处理 datetime 字段
  45. for field in ["started_at", "last_activity_at"]:
  46. if progress_data.get(field):
  47. progress_data[field] = datetime.fromisoformat(
  48. progress_data[field]
  49. )
  50. # 处理 assessments 中的 datetime
  51. for assessment in progress_data.get("assessments", []):
  52. if assessment.get("completed_at"):
  53. assessment["completed_at"] = datetime.fromisoformat(
  54. assessment["completed_at"]
  55. )
  56. self._progress[user_id] = UserProgress(**progress_data)
  57. print(f"[OK] 已加载 {len(self._progress)} 个用户进度")
  58. except Exception as e:
  59. print(f"[WARN] 加载用户进度失败: {e}")
  60. self._progress = {}
  61. def _save_progress(self):
  62. """保存用户进度到文件"""
  63. try:
  64. data = {}
  65. for user_id, progress in self._progress.items():
  66. progress_dict = progress.model_dump()
  67. # 处理 datetime 序列化
  68. for field in ["started_at", "last_activity_at"]:
  69. if progress_dict.get(field):
  70. progress_dict[field] = progress_dict[field].isoformat()
  71. for assessment in progress_dict.get("assessments", []):
  72. if assessment.get("completed_at"):
  73. assessment["completed_at"] = assessment["completed_at"].isoformat()
  74. data[user_id] = progress_dict
  75. with open(USER_PROGRESS_FILE, "w", encoding="utf-8") as f:
  76. json.dump(data, f, ensure_ascii=False, indent=2)
  77. except Exception as e:
  78. print(f"[ERROR] 保存用户进度失败: {e}")
  79. def _load_assessments(self):
  80. """从文件加载测试结果"""
  81. if ASSESSMENTS_FILE.exists():
  82. try:
  83. with open(ASSESSMENTS_FILE, "r", encoding="utf-8") as f:
  84. data = json.load(f)
  85. for user_id, assessments_data in data.items():
  86. self._assessments[user_id] = []
  87. for assessment in assessments_data:
  88. if assessment.get("completed_at"):
  89. assessment["completed_at"] = datetime.fromisoformat(
  90. assessment["completed_at"]
  91. )
  92. self._assessments[user_id].append(
  93. AssessmentResult(**assessment)
  94. )
  95. print(f"[OK] 已加载 {len(self._assessments)} 个用户测试记录")
  96. except Exception as e:
  97. print(f"[WARN] 加载测试记录失败: {e}")
  98. self._assessments = {}
  99. def _save_assessments(self):
  100. """保存测试结果到文件"""
  101. try:
  102. data = {}
  103. for user_id, assessments in self._assessments.items():
  104. data[user_id] = []
  105. for assessment in assessments:
  106. assessment_dict = assessment.model_dump()
  107. if assessment_dict.get("completed_at"):
  108. assessment_dict["completed_at"] = (
  109. assessment_dict["completed_at"].isoformat()
  110. )
  111. data[user_id].append(assessment_dict)
  112. with open(ASSESSMENTS_FILE, "w", encoding="utf-8") as f:
  113. json.dump(data, f, ensure_ascii=False, indent=2)
  114. except Exception as e:
  115. print(f"[ERROR] 保存测试记录失败: {e}")
  116. # ==================== 游戏化存储 ====================
  117. def _load_gamifications(self):
  118. """从文件加载游戏化数据"""
  119. if GAMIFICATION_FILE.exists():
  120. try:
  121. with open(GAMIFICATION_FILE, "r", encoding="utf-8") as f:
  122. data = json.load(f)
  123. for user_id, profile_data in data.items():
  124. self._gamifications[user_id] = GamificationProfile(**profile_data)
  125. print(f"[OK] 已加载 {len(self._gamifications)} 个游戏化档案")
  126. except Exception as e:
  127. print(f"[WARN] 加载游戏化数据失败: {e}")
  128. self._gamifications = {}
  129. def _save_gamifications(self):
  130. """保存游戏化数据到文件"""
  131. try:
  132. data = {}
  133. for user_id, profile in self._gamifications.items():
  134. data[user_id] = profile.model_dump()
  135. with open(GAMIFICATION_FILE, "w", encoding="utf-8") as f:
  136. json.dump(data, f, ensure_ascii=False, indent=2)
  137. except Exception as e:
  138. print(f"[ERROR] 保存游戏化数据失败: {e}")
  139. def get_gamification(self, user_id: str = "default") -> Optional[GamificationProfile]:
  140. """获取用户游戏化档案"""
  141. return self._gamifications.get(user_id)
  142. def save_gamification(self, profile: GamificationProfile):
  143. """保存用户游戏化档案"""
  144. self._gamifications[profile.user_id] = profile
  145. self._save_gamifications()
  146. # ==================== 用户进度 API ====================
  147. def get_user_progress(
  148. self, user_id: str = "default"
  149. ) -> Optional[UserProgress]:
  150. """获取用户进度"""
  151. return self._progress.get(user_id)
  152. def save_user_progress(self, progress: UserProgress):
  153. """保存用户进度"""
  154. self._progress[progress.user_id] = progress
  155. self._save_progress()
  156. def update_user_progress(
  157. self,
  158. user_id: str = "default",
  159. **kwargs
  160. ) -> UserProgress:
  161. """更新用户进度"""
  162. progress = self._progress.get(user_id)
  163. if not progress:
  164. progress = UserProgress(user_id=user_id)
  165. for key, value in kwargs.items():
  166. if hasattr(progress, key):
  167. setattr(progress, key, value)
  168. progress.last_activity_at = datetime.now()
  169. self._progress[user_id] = progress
  170. self._save_progress()
  171. return progress
  172. # ==================== 测试结果 API ====================
  173. def get_user_assessments(
  174. self, user_id: str = "default", path_type: str = None
  175. ) -> List[AssessmentResult]:
  176. """获取用户测试结果"""
  177. assessments = self._assessments.get(user_id, [])
  178. if path_type:
  179. assessments = [a for a in assessments if a.path_type == path_type]
  180. return assessments
  181. def get_current_assessment(
  182. self, user_id: str = "default", path_type: str = None
  183. ) -> Optional[AssessmentResult]:
  184. """获取用户当前有效的测试结果"""
  185. assessments = self.get_user_assessments(user_id, path_type)
  186. # 返回最新的有效结果
  187. for assessment in reversed(assessments):
  188. if assessment.is_current:
  189. return assessment
  190. return assessments[-1] if assessments else None
  191. def save_assessment(self, assessment: AssessmentResult):
  192. """保存测试结果"""
  193. user_id = assessment.user_id
  194. if user_id not in self._assessments:
  195. self._assessments[user_id] = []
  196. # 如果是"学习前重测",将旧结果标记为非当前
  197. if assessment.is_current:
  198. for existing in self._assessments[user_id]:
  199. if existing.path_type == assessment.path_type:
  200. existing.is_current = False
  201. self._assessments[user_id].append(assessment)
  202. self._save_assessments()
  203. self._save_user_assessment_to_progress(assessment)
  204. def _save_user_assessment_to_progress(self, assessment: AssessmentResult):
  205. """同步测试结果到用户进度"""
  206. progress = self._progress.get(assessment.user_id)
  207. if progress:
  208. progress.assessments = self.get_user_assessments(
  209. assessment.user_id, assessment.path_type
  210. )
  211. progress.skill_levels = assessment.category_scores
  212. self._save_progress()
  213. def has_assessment(
  214. self, user_id: str = "default", path_type: str = None
  215. ) -> bool:
  216. """检查用户是否已测试"""
  217. if path_type:
  218. return len(self.get_user_assessments(user_id, path_type)) > 0
  219. return len(self._assessments.get(user_id, [])) > 0
  220. # ==================== 代码提交流持久化 ====================
  221. def _load_submissions(self):
  222. """从文件加载代码提交记录"""
  223. if SUBMISSIONS_FILE.exists():
  224. try:
  225. with open(SUBMISSIONS_FILE, "r", encoding="utf-8") as f:
  226. data = json.load(f)
  227. for user_id, submissions_data in data.items():
  228. self._submissions[user_id] = []
  229. for sub in submissions_data:
  230. if sub.get("created_at"):
  231. sub["created_at"] = datetime.fromisoformat(sub["created_at"])
  232. self._submissions[user_id].append(CodeSubmission(**sub))
  233. print(f"[OK] 已加载 {sum(len(v) for v in self._submissions.values())} 条代码提交记录")
  234. except Exception as e:
  235. print(f"[WARN] 加载代码提交记录失败: {e}")
  236. self._submissions = {}
  237. def _save_submissions(self):
  238. """保存代码提交记录到文件"""
  239. try:
  240. data = {}
  241. for user_id, submissions in self._submissions.items():
  242. data[user_id] = []
  243. for sub in submissions:
  244. sub_dict = sub.model_dump()
  245. if sub_dict.get("created_at"):
  246. sub_dict["created_at"] = sub_dict["created_at"].isoformat()
  247. data[user_id].append(sub_dict)
  248. with open(SUBMISSIONS_FILE, "w", encoding="utf-8") as f:
  249. json.dump(data, f, ensure_ascii=False, indent=2)
  250. except Exception as e:
  251. print(f"[ERROR] 保存代码提交记录失败: {e}")
  252. def save_submission(self, submission: CodeSubmission):
  253. """保存代码提交记录"""
  254. user_id = submission.user_id
  255. if user_id not in self._submissions:
  256. self._submissions[user_id] = []
  257. self._submissions[user_id].append(submission)
  258. self._save_submissions()
  259. def get_user_submissions(
  260. self, user_id: str = "default", limit: int = 20
  261. ) -> List[CodeSubmission]:
  262. """获取用户最近提交记录"""
  263. submissions = self._submissions.get(user_id, [])
  264. return sorted(submissions, key=lambda s: s.created_at, reverse=True)[:limit]
  265. # 全局单例
  266. data_store = DataStore()