diff --git a/src/core/data_handler.py b/src/core/data_handler.py
new file mode 100644
index 0000000..59d8d28
--- /dev/null
+++ b/src/core/data_handler.py
@@ -0,0 +1,53 @@
+import json
+import os
+from pathlib import Path
+
+
+class DataHandler:
+ def __init__(self):
+ self.base_dir = Path("math_learning_data")
+ self.users_file = self.base_dir / "users.json"
+ self.ensure_data_dir()
+
+ def ensure_data_dir(self):
+ """确保数据目录和文件存在,并且文件内容是有效的JSON"""
+ if not self.base_dir.exists():
+ self.base_dir.mkdir(parents=True)
+
+ if not self.users_file.exists():
+ with open(self.users_file, "w", encoding="utf-8") as f:
+ json.dump({}, f, ensure_ascii=False) # 写入空对象
+ else:
+ # 如果文件存在但为空,写入空对象
+ if os.path.getsize(self.users_file) == 0:
+ with open(self.users_file, "w", encoding="utf-8") as f:
+ json.dump({}, f, ensure_ascii=False)
+
+ def load_users(self):
+ """加载所有用户数据"""
+ with open(self.users_file, "r", encoding="utf-8") as f:
+ try:
+ return json.load(f)
+ except json.JSONDecodeError:
+ # 如果文件内容损坏,返回空字典
+ return {}
+
+ def save_users(self, users_data):
+ """保存用户数据"""
+ with open(self.users_file, "w", encoding="utf-8") as f:
+ json.dump(users_data, f, ensure_ascii=False, indent=2)
+
+ def get_user(self, email):
+ """获取用户信息"""
+ users = self.load_users()
+ return users.get(email)
+
+ def add_user(self, email, user_data):
+ """添加新用户"""
+ users = self.load_users()
+ users[email] = user_data
+ self.save_users(users)
+
+ def update_user(self, email, user_data):
+ """更新用户信息"""
+ self.add_user(email, user_data) # 直接覆盖更新
\ No newline at end of file
diff --git a/src/core/email_service.py b/src/core/email_service.py
new file mode 100644
index 0000000..ecca850
--- /dev/null
+++ b/src/core/email_service.py
@@ -0,0 +1,167 @@
+import random
+import time
+import smtplib
+import logging
+from email.mime.text import MIMEText
+from email.header import Header
+from email.utils import formataddr
+from tkinter import messagebox
+from typing import Dict
+from smtplib import SMTPResponseException
+
+# 配置日志
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+
+class EmailService:
+ def __init__(self, smtp_server: str, smtp_port: int, sender_email: str, sender_password: str):
+ self.smtp_server = smtp_server
+ self.smtp_port = smtp_port
+ self.sender_email = sender_email
+ self.sender_password = sender_password
+ self.verification_codes: Dict[str, tuple[str, float]] = {} # 邮箱: (验证码, 过期时间)
+ self.code_validity = 60 # 验证码有效期1分钟
+ self.last_send_time: Dict[str, float] = {} # 记录上次发送时间
+ self.send_interval = 60 # 发送间隔60秒
+
+ def generate_code(self) -> str:
+ """生成6位数字注册码"""
+ return str(random.randint(100000, 999999))
+
+ def can_send_code(self, email: str) -> bool:
+ """检查是否可以发送注册码(防止频繁发送)"""
+ if email in self.last_send_time:
+ elapsed = time.time() - self.last_send_time[email]
+ return elapsed >= self.send_interval
+ return True
+
+ def send_verification_code(self, email: str, username: str = "用户") -> bool:
+ """发送验证码到用户邮箱。
+
+ Args:
+ email: 目标邮箱地址。
+ username: 用户昵称,用于邮件正文。
+
+ Returns:
+ 成功返回 True,失败返回 False。
+ """
+ if not self.can_send_code(email):
+ logger.warning("发送过于频繁,邮箱: %s", email)
+ return False
+
+ code = self.generate_code()
+ now = time.time()
+ self.verification_codes[email] = (code, now + self.code_validity)
+ self.last_send_time[email] = now
+
+ subject = "数学学习软件 - 注册验证码"
+ formatted_email = self._format_email(email)
+ body = self._build_email_body(username, code)
+
+ try:
+ self._send_email(email, subject, body)
+ logger.info("注册码已发送到 %s", email)
+ return True
+ except smtplib.SMTPAuthenticationError as e:
+ logger.error("邮箱认证失败: %s", e)
+ self._show_warning("邮箱认证失败", "请检查邮箱账号和授权码是否正确")
+ return False
+ except smtplib.SMTPConnectError as e:
+ logger.error("无法连接到SMTP服务器: %s", e)
+ self._show_warning("连接失败", "无法连接到邮件服务器,请检查网络连接")
+ return False
+ except Exception as e:
+ logger.error("邮件发送失败: %s", e, exc_info=True)
+ self._show_warning("发送失败", f"邮件发送失败: {e}")
+ return False
+
+
+ def _format_email(self, email: str) -> str:
+ """隐藏部分邮箱字符,保护隐私。"""
+ try:
+ at_index = email.index('@')
+ if at_index >= 4:
+ return email[:3] + '...' + email[at_index - 4:at_index]
+ return email
+ except ValueError:
+ return email
+
+
+ def _build_email_body(self, username: str, code: str) -> str:
+ """构建HTML邮件正文。"""
+ return f"""
+
+
+
+
+
+
亲爱的 {username},您好!
+
您正在注册数学学习软件,请使用以下注册码完成注册:
+
+
+ {code}
+
+
+
+ ⚠️ 注册码有效期1分钟,请尽快完成注册。
+
+
+
+
如果这不是您的操作,请忽略此邮件。
+
此为系统邮件,请勿回复
+
数学学习软件团队
+
+
+
+
+ """
+
+
+ def _send_email(self, to_email: str, subject: str, body: str) -> None:
+ """发送邮件。"""
+ msg = MIMEText(body, "html", "utf-8")
+ msg["Subject"] = Header(subject, "utf-8")
+ msg["From"] = formataddr(("数学学习软件", self.sender_email))
+ msg["To"] = to_email
+
+ if self.smtp_port == 587:
+ server = smtplib.SMTP(self.smtp_server, self.smtp_port, timeout=10)
+ server.starttls()
+ else:
+ server = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port, timeout=10)
+
+ server.set_debuglevel(1)
+ server.login(self.sender_email, self.sender_password)
+ server.sendmail(self.sender_email, to_email, msg.as_string())
+ server.quit()
+
+
+ def _show_warning(self, title: str, message: str) -> None:
+ """弹出警告框。"""
+ messagebox.warning(None, title, message)
+
+ def verify_code(self, email: str, code: str) -> bool:
+ """验证注册码是否有效"""
+ if email not in self.verification_codes:
+ return False
+
+ stored_code, expire_time = self.verification_codes[email]
+ if time.time() > expire_time:
+ del self.verification_codes[email]
+ return False
+
+ return stored_code == code
\ No newline at end of file
diff --git a/src/core/question_bank.py b/src/core/question_bank.py
new file mode 100644
index 0000000..2d75ede
--- /dev/null
+++ b/src/core/question_bank.py
@@ -0,0 +1,276 @@
+# -*- coding: utf-8 -*-
+"""数学学习软件 - 题目生成与试卷管理模块
+
+提供小学、初中、高中三个学段的数学选择题自动生成、查重、保存功能。
+"""
+
+from __future__ import annotations
+
+import json
+import math
+import os
+import random
+from datetime import datetime
+from typing import List, Tuple
+
+# 模块级常量
+PRIMARY_OPS = ("+", "-", "*", "/")
+MIDDLE_OPS = ("+", "-", "*", "/", "^2", "sqrt")
+HIGH_OPS = ("+", "-", "*", "/", "sin", "cos", "tan")
+PRIMARY_RANGE = (1, 50)
+MIDDLE_RANGE = (1, 100)
+HIGH_ANGLE_RANGE = (0, 90) # 角度制
+MAX_OPTS = 4
+MAX_TRIES = 100
+PAPERS_DIR = "generated_papers"
+
+
+class Question:
+ """单道选择题."""
+
+ __slots__ = ("content", "options", "answer")
+
+ def __init__(self, content: str, options: List[str], answer: str):
+ """初始化.
+
+ Args:
+ content: 题干(含表达式)。
+ options: 四个选项文本。
+ answer: 正确答案文本。
+ """
+ self.content = content
+ self.options = options
+ self.answer = answer
+
+
+class QuestionBank:
+ """题库:按学段生成不重复的选择题,并保存为文本试卷."""
+
+ def __init__(self) -> None:
+ """初始化."""
+ self.operators = {
+ "primary": PRIMARY_OPS,
+ "middle": MIDDLE_OPS,
+ "high": HIGH_OPS,
+ }
+ self.generated_questions: List[Question] = []
+ self.papers_dir = PAPERS_DIR
+ os.makedirs(self.papers_dir, exist_ok=True)
+
+ # ------------------------------------------------------------------
+ # 公共接口
+ # ------------------------------------------------------------------
+
+ def generate_question(self, level: str) -> Question:
+ for attempt in range(MAX_TRIES):
+ try:
+ expr, value = self._make_expression(level)
+ if not self._is_valid_value(value) or expr in [q.content for q in self.generated_questions]:
+ continue
+ opts, ans = self._make_options(value, level)
+ return Question(f"计算:{expr}", opts, ans)
+ except Exception as e:
+ print(f"[WARNING] 题目生成失败,尝试 {attempt + 1}/{MAX_TRIES},错误:{e}")
+ continue
+
+ # 兜底题目
+ print("[WARNING] 使用兜底题目")
+ expr, value = self._fallback_expression(level)
+ opts, ans = self._make_options(value, level)
+ return Question(f"计算:{expr}", opts, ans)
+
+ def generate_paper(self, level: str, count: int, username: str = "unknown") -> List[Question]:
+ """生成整张试卷并落盘.
+
+ Args:
+ level: 学段
+ count: 题目数量
+ username: 用户昵称
+
+ Returns:
+ 题目列表
+ """
+ self.generated_questions.clear()
+ questions = [self.generate_question(level) for _ in range(count)]
+ self._save_paper(questions, level, username)
+ return questions
+
+ def calculate_score(self, answers: List[bool]) -> int:
+ """计算百分制得分.
+
+ Args:
+ answers: 每题是否正确
+
+ Returns:
+ 0~100
+ """
+ if not answers:
+ return 0
+ return round(sum(answers) / len(answers) * 100)
+
+ # ------------------------------------------------------------------
+ # 私有辅助
+ # ------------------------------------------------------------------
+
+ def _make_expression(self, level: str) -> Tuple[str, float]:
+ """返回表达式字符串与数值结果."""
+ if level == "primary":
+ return self._primary_expr()
+ if level == "middle":
+ return self._middle_expr()
+ return self._high_expr()
+
+ def _primary_expr(self) -> Tuple[str, float]:
+ """小学表达式:整数结果."""
+ nums = [random.randint(*PRIMARY_RANGE) for _ in range(random.randint(2, 3))]
+ ops = list(random.choices(PRIMARY_OPS, k=len(nums) - 1))
+ self._ensure_int_div_sub(nums, ops)
+
+ parts = [str(nums[0])]
+ for o, n in zip(ops, nums[1:]):
+ parts.extend([o, str(n)])
+ expr = self._add_parentheses(parts)
+ val = self._safe_int_eval(expr)
+ if val is not None:
+ return expr, val
+
+ a, b = sorted(random.randint(*PRIMARY_RANGE) for _ in range(2))
+ op = random.choice(["+", "-", "*"])
+ expr = f"{a} {op} {b}"
+ return expr, eval(expr)
+
+ def _middle_expr(self) -> Tuple[str, float]:
+ """初中表达式:含平方或开方."""
+ base = random.randint(*MIDDLE_RANGE)
+ if random.choice([True, False]):
+ inner = f"{base}^2"
+ val = base ** 2
+ else:
+ inner = f"sqrt({base})"
+ val = math.sqrt(base)
+
+ if random.choice([True, False]):
+ n2 = random.randint(*MIDDLE_RANGE)
+ op = random.choice(PRIMARY_OPS)
+ inner = f"({inner} {op} {n2})"
+ val = eval(f"{val} {op} {n2}")
+
+ return inner, val
+
+ def _high_expr(self) -> Tuple[str, float]:
+ """高中表达式:含三角函数."""
+ angle = random.randint(*HIGH_ANGLE_RANGE)
+ func = random.choice(["sin", "cos", "tan"])
+ expr = f"{func}({angle})"
+ val = getattr(math, func)(math.radians(angle))
+
+ if random.choice([True, False]):
+ n2 = random.randint(1, 90)
+ op = random.choice(PRIMARY_OPS)
+ expr = f"({expr} {op} {n2})"
+ val = eval(f"{val} {op} {n2}")
+
+ return expr, val
+
+ def _fallback_expression(self, level: str) -> Tuple[str, float]:
+ """兜底简单表达式."""
+ if level == "primary":
+ a, b = sorted(random.randint(*PRIMARY_RANGE) for _ in range(2))
+ expr = f"{a} + {b}"
+ return expr, eval(expr)
+ if level == "middle":
+ n = random.randint(1, 10)
+ expr = f"{n}^2"
+ return expr, n ** 2
+ angle = random.randint(1, 89)
+ expr = f"sin({angle})"
+ return expr, math.sin(math.radians(angle))
+
+ # -- 工具 -----------------------------------------------------------
+
+ @staticmethod
+ def _ensure_int_div_sub(nums: List[int], ops: List[str]) -> None:
+ """调整 nums/ops 保证整数结果."""
+ for i, op in enumerate(ops):
+ if op == "/":
+ nums[i] *= nums[i + 1]
+ elif op == "-" and nums[i] < nums[i + 1]:
+ nums[i], nums[i + 1] = nums[i + 1], nums[i]
+
+ @staticmethod
+ def _add_parentheses(parts: List[str]) -> str:
+ """随机给表达式加括号."""
+ if len(parts) >= 5 and random.choice([True, False]):
+ start = random.randint(0, len(parts) - 4)
+ if start % 2 == 0:
+ parts.insert(start, "(")
+ parts.insert(start + 4, ")")
+ return " ".join(parts)
+
+ @staticmethod
+ def _safe_int_eval(expr: str) -> float | None:
+ """安全计算并返回整数结果."""
+ try:
+ val = eval(expr)
+ if abs(val - round(val)) < 1e-4:
+ return round(val)
+ except Exception:
+ pass
+ return None
+
+ @staticmethod
+ def _is_valid_value(val: float) -> bool:
+ """检查数值是否合法."""
+ return not (math.isnan(val) or math.isinf(val) or abs(val) > 1e10)
+
+ # -- 选项 & 保存 ------------------------------------------------------
+
+ def _make_options(self, correct: float, level: str) -> Tuple[List[str], str]:
+ """生成四个选项(1正确+3干扰)。"""
+ if level == "primary":
+ correct_val = int(round(correct))
+ else:
+ correct_val = round(correct, 2)
+
+ opts = [correct_val]
+ while len(opts) < 4:
+ distractor = self._make_distractor(opts, correct_val, level)
+ if distractor not in opts:
+ opts.append(distractor)
+
+ random.shuffle(opts)
+
+ # ✅ 确保答案是从 opts 中获取的,而不是原始浮点数
+ ans = str(opts[opts.index(correct_val)])
+ return [str(o) for o in opts], ans
+
+ def _make_distractor(self, existing: List[float], correct: float, level: str) -> float:
+ """生成一个不重复的干扰项."""
+ while True:
+ if level == "primary":
+ d = int(correct) + random.randint(-5, 5)
+ if d != correct and d > 0:
+ return d
+ else:
+ delta = abs(correct) * 0.3 if correct else 1
+ d = round(correct + random.uniform(-delta, delta), 2)
+ if d not in existing:
+ return d
+
+ def _save_paper(self, questions: List[Question], level: str, username: str) -> None:
+ """试卷落盘."""
+ level_name = {"primary": "小学", "middle": "初中", "high": "高中"}.get(level, level)
+ timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
+ path = os.path.join(self.papers_dir, f"{timestamp}.txt")
+ with open(path, "w", encoding="utf-8") as f:
+ f.write(f"数学学习软件 - {level_name}数学试卷\n")
+ f.write(f"生成时间:{datetime.now()}\n")
+ f.write("=" * 50 + "\n\n")
+ for idx, q in enumerate(questions, 1):
+ f.write(f"第{idx}题:{q.content}\n选项:\n")
+ for i, opt in enumerate(q.options):
+ f.write(f" {chr(65+i)}. {opt}\n")
+ f.write(f"正确答案:{q.answer}\n\n")
+ f.write("=" * 50 + "\n")
+ f.write(f"共{len(questions)}题\n")
+ print(f"试卷已保存到:{path}")
\ No newline at end of file
diff --git a/src/core/user_system.py b/src/core/user_system.py
new file mode 100644
index 0000000..d7074a3
--- /dev/null
+++ b/src/core/user_system.py
@@ -0,0 +1,163 @@
+import re
+import json
+import os
+import sys
+from typing import Dict, Optional
+from .email_service import EmailService
+
+# 获取exe所在目录的绝对路径
+if getattr(sys, 'frozen', False):
+ # 如果是打包后的exe
+ BASE_DIR = os.path.dirname(sys.executable)
+else:
+ # 如果是开发环境
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+
+USER_DATA_FILE = os.path.join(BASE_DIR, "data", "users.json")
+
+class UserSystem:
+ def __init__(self, email_service: EmailService):
+ self.email_service = email_service
+ # 确保data目录存在
+ os.makedirs(os.path.dirname(USER_DATA_FILE), exist_ok=True)
+ self.users: Dict[str, dict] = self.load_users()
+ self.current_user: Optional[str] = None
+ self.current_level: Optional[str] = None
+
+ def load_users(self) -> Dict[str, dict]:
+ """从文件加载用户数据"""
+ print(f"尝试加载用户文件: {USER_DATA_FILE}") # 调试信息
+ print(f"文件是否存在: {os.path.exists(USER_DATA_FILE)}") # 调试信息
+
+ if os.path.exists(USER_DATA_FILE):
+ try:
+ with open(USER_DATA_FILE, "r", encoding="utf-8") as f:
+ return json.load(f)
+ except (json.JSONDecodeError, FileNotFoundError) as e:
+ print(f"加载用户文件失败: {e}") # 调试信息
+ return {}
+ else:
+ print("用户文件不存在,创建空用户字典") # 调试信息
+ return {}
+
+ def save_users(self):
+ """保存用户数据到文件"""
+ print(f"保存用户数据到: {USER_DATA_FILE}") # 调试信息
+ # 确保目录存在
+ os.makedirs(os.path.dirname(USER_DATA_FILE), exist_ok=True)
+ try:
+ with open(USER_DATA_FILE, "w", encoding="utf-8") as f:
+ json.dump(self.users, f, ensure_ascii=False, indent=4)
+ print("用户数据保存成功") # 调试信息
+ except Exception as e:
+ print(f"保存用户数据失败: {e}") # 调试信息
+
+ # 其他方法保持不变...
+
+ def is_valid_email(self, email: str) -> bool:
+ """验证邮箱格式"""
+ pattern = r"^[\w\.-]+@[\w\.-]+\.\w+$"
+ return re.match(pattern, email) is not None
+
+ def is_valid_password(self, password: str) -> bool:
+ """密码要求:6-10位,包含大小写字母和数字"""
+ if len(password) < 6 or len(password) > 10:
+ return False
+ has_upper = any(c.isupper() for c in password)
+ has_lower = any(c.islower() for c in password)
+ has_digit = any(c.isdigit() for c in password)
+ return has_upper and has_lower and has_digit
+
+ def is_valid_username(self, username: str) -> bool:
+ """用户名要求:2-10位中文、字母或数字"""
+ if len(username) < 2 or len(username) > 10:
+ return False
+ pattern = r"^[\u4e00-\u9fa5a-zA-Z0-9]+$"
+ return re.match(pattern, username) is not None
+
+ def is_username_exists(self, username: str) -> bool:
+ """检查用户名是否已存在"""
+ return username in self.users
+
+ def send_verification(self, email: str, username: str = "用户") -> bool:
+ """发送验证码"""
+ # 检查邮箱是否已被其他用户使用
+ for user_data in self.users.values():
+ if user_data.get("email") == email:
+ return False
+ return self.email_service.send_verification_code(email, username)
+
+ def register(self, username: str, email: str, code: str, password: str) -> bool:
+ """注册新用户"""
+ if self.is_username_exists(username):
+ return False
+ if not self.is_valid_password(password):
+ return False
+ if not self.is_valid_username(username):
+ return False
+ if not self.is_valid_email(email):
+ return False
+ # 检查邮箱是否已被使用
+ for user_data in self.users.values():
+ if user_data.get("email") == email:
+ return False
+ if not self.email_service.verify_code(email, code):
+ return False
+
+ self.users[username] = {
+ "password": password,
+ "email": email,
+ "level": None
+ }
+ self.save_users()
+ return True
+
+ def login(self, username: str, password: str) -> bool:
+ """登录验证 - 使用用户名登录"""
+ user = self.users.get(username)
+ if user and user["password"] == password:
+ self.current_user = username
+ self.current_level = user.get("level")
+ return True
+ return False
+
+ def change_password(self, old_password: str, new_password: str) -> bool:
+ """修改当前登录用户密码"""
+ if not self.current_user:
+ return False
+ user = self.users.get(self.current_user)
+ if not user or user["password"] != old_password:
+ return False
+ if not self.is_valid_password(new_password):
+ return False
+ user["password"] = new_password
+ self.save_users()
+ return True
+
+ def set_username(self, new_username: str) -> bool:
+ """修改当前用户名"""
+ if not self.current_user:
+ return False
+ if not self.is_valid_username(new_username):
+ return False
+ if self.is_username_exists(new_username):
+ return False
+
+ # 更新用户名
+ user_data = self.users.pop(self.current_user)
+ self.users[new_username] = user_data
+ self.current_user = new_username
+ self.save_users()
+ return True
+
+ def set_level(self, level: str):
+ """设置当前用户学段"""
+ if self.current_user:
+ self.users[self.current_user]["level"] = level
+ self.current_level = level
+ self.save_users()
+
+ def get_user_email(self, username: str) -> Optional[str]:
+ """获取用户的邮箱"""
+ user = self.users.get(username)
+ return user.get("email") if user else None
\ No newline at end of file