diff --git a/src/app.py b/src/app.py index 38bee2f..1aca5dc 100644 --- a/src/app.py +++ b/src/app.py @@ -1,5 +1,5 @@ from PyQt6.QtWidgets import QApplication -from src.ui.main_window import MainWindow +from ui.main_window import MainWindow def main() -> None: diff --git a/src/services/__pycache__/question_service.cpython-311.pyc b/src/services/__pycache__/question_service.cpython-311.pyc index 8921db7..10281f9 100644 Binary files a/src/services/__pycache__/question_service.cpython-311.pyc and b/src/services/__pycache__/question_service.cpython-311.pyc differ diff --git a/src/services/__pycache__/storage_service.cpython-311.pyc b/src/services/__pycache__/storage_service.cpython-311.pyc index a9bbf84..99a793e 100644 Binary files a/src/services/__pycache__/storage_service.cpython-311.pyc and b/src/services/__pycache__/storage_service.cpython-311.pyc differ diff --git a/src/services/__pycache__/user_service.cpython-311.pyc b/src/services/__pycache__/user_service.cpython-311.pyc index af9ce87..fb4bf37 100644 Binary files a/src/services/__pycache__/user_service.cpython-311.pyc and b/src/services/__pycache__/user_service.cpython-311.pyc differ diff --git a/src/services/question_service.py b/src/services/question_service.py index 7265765..003636e 100644 --- a/src/services/question_service.py +++ b/src/services/question_service.py @@ -2,6 +2,7 @@ 试题服务模块:按小学/初中/高中生成选择题试卷,并保证同一试卷题目不重复。 """ import random +import math from typing import List, Dict @@ -40,42 +41,176 @@ class QuestionService: return questions def _gen_primary(self) -> Dict: - """生成一题小学难度的加减法选择题。""" - a = random.randint(1, 50) - b = random.randint(1, 50) - op = random.choice(['+', '-']) - if op == '+': - ans = a + b - stem = f"计算:{a} + {b} = ?" + """生成一题小学难度的四则运算选择题(可带括号)。""" + # 随机选择运算类型:简单四则运算或带括号的复合运算 + if random.choice([True, False]): + # 简单四则运算 + a = random.randint(1, 50) + b = random.randint(1, 50) + op = random.choice(['+', '-', '*', '/']) + + if op == '+': + ans = a + b + stem = f"计算:{a} + {b} = ?" + elif op == '-': + ans = a - b + stem = f"计算:{a} - {b} = ?" + elif op == '*': + ans = a * b + stem = f"计算:{a} × {b} = ?" + else: # 除法,确保整除 + ans = a + b = random.randint(1, 10) + a = ans * b # 确保整除 + stem = f"计算:{a} ÷ {b} = ?" else: - ans = a - b - stem = f"计算:{a} - {b} = ?" + # 带括号的复合运算 + a = random.randint(1, 20) + b = random.randint(1, 20) + c = random.randint(1, 20) + + # 随机选择括号运算类型 + bracket_type = random.choice(['add_mul', 'sub_mul', 'mul_add', 'mul_sub']) + + if bracket_type == 'add_mul': + ans = (a + b) * c + stem = f"计算:({a} + {b}) × {c} = ?" + elif bracket_type == 'sub_mul': + ans = (a - b) * c + stem = f"计算:({a} - {b}) × {c} = ?" + elif bracket_type == 'mul_add': + ans = a * (b + c) + stem = f"计算:{a} × ({b} + {c}) = ?" + else: # mul_sub + ans = a * (b - c) + stem = f"计算:{a} × ({b} - {c}) = ?" + options = self._make_options(ans) return {'stem': stem, 'options': options, 'answer_index': options.index(str(ans))} def _gen_middle(self) -> Dict: - """生成一题初中难度的一元一次方程选择题。""" - a = random.randint(2, 9) - b = random.randint(1, 20) - # ax + b = 0 => x = -b / a - x = -b / a - stem = f"解方程:{a}x + {b} = 0,x = ?" - options = self._make_options(x, float_mode=True) - correct = f"{x:.2f}" - return {'stem': stem, 'options': options, 'answer_index': options.index(correct)} - - def _gen_high(self) -> Dict: - """生成一题高中难度的导数计算选择题:f(x)=ax^2+bx+c 在 x0 处的导数。""" - a = random.randint(1, 5) - b = random.randint(-5, 5) - c = random.randint(-10, 10) - x0 = random.randint(-5, 5) - # f'(x) = 2ax + b => f'(x0) = 2a*x0 + b - ans = 2 * a * x0 + b - stem = f"设 f(x)={a}x^2+{b}x+{c},求 f'({x0}) = ?" + """生成一题初中难度的题目,至少包含一个平方或开根号运算。""" + # 随机选择题目类型:平方运算、开根号运算或混合运算 + question_type = random.choice(['square', 'sqrt', 'mixed']) + + if question_type == 'square': + # 平方运算题目 + a = random.randint(2, 15) + b = random.randint(1, 10) + + if random.choice([True, False]): + # (a + b)² + ans = (a + b) ** 2 + stem = f"计算:({a} + {b})² = ?" + else: + # a² + b² + ans = a ** 2 + b ** 2 + stem = f"计算:{a}² + {b}² = ?" + + elif question_type == 'sqrt': + # 开根号运算题目 + # 选择完全平方数确保结果为整数 + perfect_squares = [4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225] + a = random.choice(perfect_squares) + b = random.randint(1, 10) + + if random.choice([True, False]): + # √a + b + ans = int(math.sqrt(a)) + b + stem = f"计算:√{a} + {b} = ?" + else: + # √a × b + ans = int(math.sqrt(a)) * b + stem = f"计算:√{a} × {b} = ?" + + else: # mixed + # 混合运算:既有平方又有开根号 + perfect_square = random.choice([4, 9, 16, 25, 36, 49, 64, 81, 100]) + a = random.randint(2, 8) + + # √perfect_square + a² + ans = int(math.sqrt(perfect_square)) + a ** 2 + stem = f"计算:√{perfect_square} + {a}² = ?" + options = self._make_options(ans) return {'stem': stem, 'options': options, 'answer_index': options.index(str(ans))} + def _gen_high(self) -> Dict: + """生成一题高中难度的题目,至少包含一个sin、cos或tan运算符。""" + # 随机选择题目类型:基础三角函数、三角函数运算或复合运算 + question_type = random.choice(['basic_trig', 'trig_calc', 'mixed_trig']) + + if question_type == 'basic_trig': + # 基础三角函数值计算 + # 使用特殊角度确保结果为常见值 + special_angles = [0, 30, 45, 60, 90, 120, 135, 150, 180] + angle = random.choice(special_angles) + func = random.choice(['sin', 'cos', 'tan']) + + # 计算三角函数值(转换为弧度) + rad = math.radians(angle) + if func == 'sin': + ans = round(math.sin(rad), 2) + stem = f"计算:sin({angle}°) = ?(保留两位小数)" + elif func == 'cos': + ans = round(math.cos(rad), 2) + stem = f"计算:cos({angle}°) = ?(保留两位小数)" + else: # tan + if angle in [90, 270]: # tan在这些角度未定义 + angle = 45 + rad = math.radians(angle) + ans = round(math.tan(rad), 2) + stem = f"计算:tan({angle}°) = ?(保留两位小数)" + + elif question_type == 'trig_calc': + # 三角函数运算 + angle1 = random.choice([30, 45, 60]) + angle2 = random.choice([30, 45, 60]) + func1 = random.choice(['sin', 'cos']) + func2 = random.choice(['sin', 'cos']) + + rad1 = math.radians(angle1) + rad2 = math.radians(angle2) + + if func1 == 'sin': + val1 = math.sin(rad1) + else: + val1 = math.cos(rad1) + + if func2 == 'sin': + val2 = math.sin(rad2) + else: + val2 = math.cos(rad2) + + # 随机选择运算符 + if random.choice([True, False]): + ans = round(val1 + val2, 2) + stem = f"计算:{func1}({angle1}°) + {func2}({angle2}°) = ?(保留两位小数)" + else: + ans = round(val1 * val2, 2) + stem = f"计算:{func1}({angle1}°) × {func2}({angle2}°) = ?(保留两位小数)" + + else: # mixed_trig + # 混合运算:三角函数与代数运算 + angle = random.choice([30, 45, 60]) + a = random.randint(2, 5) + func = random.choice(['sin', 'cos', 'tan']) + + rad = math.radians(angle) + if func == 'sin': + trig_val = math.sin(rad) + elif func == 'cos': + trig_val = math.cos(rad) + else: + trig_val = math.tan(rad) + + ans = round(a * trig_val + a, 2) + stem = f"计算:{a} × {func}({angle}°) + {a} = ?(保留两位小数)" + + options = self._make_options(ans, float_mode=True) + correct = f"{ans:.2f}" + return {'stem': stem, 'options': options, 'answer_index': options.index(correct)} + def _make_options(self, answer, float_mode: bool = False) -> List[str]: """根据正确答案生成 4 个选项(包含正确答案与3个干扰项)。""" opts = set() diff --git a/src/services/storage_service.py b/src/services/storage_service.py index 7c41bd4..5755c20 100644 --- a/src/services/storage_service.py +++ b/src/services/storage_service.py @@ -66,6 +66,18 @@ class StorageService: return u return None + def get_user_by_username(self, username: str) -> Dict[str, Any] | None: + """根据用户名获取用户字典,不存在返回 None。""" + users = self.load_users().get('users', []) + for u in users: + if u.get('username') == username: + return u + return None + + def username_exists(self, username: str) -> bool: + """判断指定用户名是否已被占用。""" + return self.get_user_by_username(username) is not None + def upsert_user(self, user: Dict[str, Any]) -> None: """插入或更新用户字典,并立即持久化。""" data = self.load_users() diff --git a/src/services/user_service.py b/src/services/user_service.py index 2006ff6..2070105 100644 --- a/src/services/user_service.py +++ b/src/services/user_service.py @@ -1,10 +1,11 @@ """ -用户服务模块:处理注册、验证码、设置密码、登录与修改密码逻辑。 +用户服务模块:处理注册、验证码、用户名设置、登录与修改密码逻辑。 +不依赖真实邮件与SMTP,仅进行邮箱格式校验与本地验证码生成/校验。 """ import time from typing import Any, Dict -from ..utils.security_utils import ( +from utils.security_utils import ( validate_email, validate_password_strength, generate_verification_code, @@ -12,24 +13,23 @@ from ..utils.security_utils import ( verify_password, ) from .storage_service import StorageService -from .email_service import EmailService +# 删除 EmailService 依赖 class UserService: """封装用户相关业务逻辑的服务类。""" - def __init__(self, storage: StorageService, email_service: EmailService) -> None: - """初始化用户服务,注入存储与邮件服务。""" + def __init__(self, storage: StorageService) -> None: + """初始化用户服务,仅注入存储服务。""" self.storage = storage - self.email_service = email_service def request_registration(self, email: str) -> tuple[bool, str]: - """发起注册请求:校验邮箱,生成验证码并发送邮件。 + """发起注册请求:校验邮箱,生成验证码并本地保存(不发送邮件)。 参数: email: 用户邮箱。 返回: - (success, message) 二元组,表示结果与提示信息。 + (success, message) 二元组,message包含提示与模拟验证码信息。 """ if not validate_email(email): return False, '邮箱格式不正确' @@ -46,13 +46,12 @@ class UserService: 'code_time': int(time.time()), 'password_salt': '', 'password_hash': '', - 'created_at': user.get('created_at') or int(time.time()) + 'username': user.get('username', ''), + 'created_at': user.get('created_at') or int(time.time()), }) self.storage.upsert_user(user) - sent = self.email_service.send_verification_code(email, code) - if not sent: - return False, '验证码发送失败,请检查SMTP设置' - return True, '验证码已发送,请查收邮箱' + # 不发送邮件,直接提示验证码供用户在下一步手动输入 + return True, f'验证码已生成(模拟):{code},请前往验证码页面手动输入' def verify_code(self, email: str, code: str) -> tuple[bool, str]: """校验验证码并标记邮箱已验证。 @@ -66,7 +65,6 @@ class UserService: user = self.storage.get_user(email) if not user: return False, '用户不存在,请先注册' - # 10 分钟有效期 if int(time.time()) - int(user.get('code_time', 0)) > 600: return False, '验证码已过期,请重新获取' salt_hex = user.get('code_salt', '') @@ -82,7 +80,28 @@ class UserService: user['code_salt'] = '' user['code_hash'] = '' self.storage.upsert_user(user) - return True, '邮箱验证成功,请设置密码' + return True, '邮箱验证成功,请设置用户名与密码' + + def set_username(self, email: str, username: str) -> tuple[bool, str]: + """设置用户名:校验格式与唯一性,并写入用户信息。 + + 参数: + email: 目标用户邮箱。 + username: 待设置的用户名(3-16位,字母数字与下划线)。 + 返回: + (success, message)。 + """ + user = self.storage.get_user(email) + if not user or not user.get('verified'): + return False, '邮箱未验证或用户不存在' + uname = (username or '').strip() + if not (3 <= len(uname) <= 16) or not all(c.isalnum() or c == '_' for c in uname): + return False, '用户名需为3-16位且仅包含字母、数字或下划线' + if self.storage.username_exists(uname): + return False, '该用户名已被占用' + user['username'] = uname + self.storage.upsert_user(user) + return True, '用户名设置成功' def set_password(self, email: str, password: str, confirm: str) -> tuple[bool, str]: """设置或重置密码:校验强度、确认一致并持久化哈希。""" @@ -99,8 +118,28 @@ class UserService: self.storage.upsert_user(user) return True, '密码设置成功' + def complete_registration(self, email: str) -> tuple[bool, str]: + """完成注册:要求用户已设置用户名与密码,方视为完成。 + + 参数: + email: 用户邮箱。 + 返回: + (success, message)。 + """ + user = self.storage.get_user(email) + if not user: + return False, '用户不存在' + if not user.get('verified'): + return False, '邮箱未验证' + if not user.get('username'): + return False, '请先设置用户名' + if not user.get('password_hash'): + return False, '请先设置密码' + # 已经持久化,无需额外操作,这里仅作为流程校验提示 + return True, '注册完成' + def login(self, email: str, password: str) -> tuple[bool, str]: - """登录:校验邮箱存在与密码匹配。""" + """邮箱登录:校验邮箱存在与密码匹配。""" user = self.storage.get_user(email) if not user or not user.get('password_hash'): return False, '用户不存在或未设置密码' @@ -111,6 +150,18 @@ class UserService: return False, '密码不正确' return True, '登录成功' + def login_by_username(self, username: str, password: str) -> tuple[bool, str]: + """用户名登录:根据用户名查询并校验密码。""" + user = self.storage.get_user_by_username(username) + if not user or not user.get('password_hash'): + return False, '用户不存在或未设置密码' + salt_hex = user.get('password_salt', '') + hash_hex = user.get('password_hash', '') + ok = verify_password(password, bytes.fromhex(salt_hex), bytes.fromhex(hash_hex)) + if not ok: + return False, '密码不正确' + return True, '登录成功' + def change_password(self, email: str, old_password: str, new_password: str, confirm: str) -> tuple[bool, str]: """修改密码:校验原密码正确并设置新密码。""" user = self.storage.get_user(email) diff --git a/src/storage/users.json b/src/storage/users.json index 4ef6c8c..3ffd32d 100644 --- a/src/storage/users.json +++ b/src/storage/users.json @@ -1,3 +1,15 @@ { - "users": [] + "users": [ + { + "email": "shenyongye@163.com", + "verified": true, + "code_salt": "", + "code_hash": "", + "code_time": 1760256425, + "password_salt": "e09f40c04b33d5f482ff682b3d43192b", + "password_hash": "dffeab45544b194e5f7efeb632cbe75eef3b46fbff23531692c7ef0e9a83f24b", + "username": "echo", + "created_at": 1760256425 + } + ] } \ No newline at end of file diff --git a/src/ui/__pycache__/main_window.cpython-311.pyc b/src/ui/__pycache__/main_window.cpython-311.pyc index 3d0fd81..68aae03 100644 Binary files a/src/ui/__pycache__/main_window.cpython-311.pyc and b/src/ui/__pycache__/main_window.cpython-311.pyc differ diff --git a/src/ui/main_window.py b/src/ui/main_window.py index 1888ab6..4d5550d 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -1,13 +1,12 @@ from PyQt6.QtWidgets import ( QMainWindow, QWidget, QStackedWidget, QVBoxLayout, QHBoxLayout, - QLabel, QLineEdit, QPushButton, QMessageBox, QCheckBox + QLabel, QLineEdit, QPushButton, QMessageBox, QRadioButton, QButtonGroup, QSpinBox ) from PyQt6.QtCore import Qt -from src.services.storage_service import StorageService -from src.services.email_service import EmailService -from src.services.user_service import UserService -from src.services.question_service import QuestionService +from services.storage_service import StorageService +from services.user_service import UserService +from services.question_service import QuestionService class MainWindow(QMainWindow): @@ -21,23 +20,29 @@ class MainWindow(QMainWindow): # 业务服务初始化 self.storage_service = StorageService() - self.email_service = EmailService(self.storage_service) - self.user_service = UserService(self.storage_service, self.email_service) + self.session_email = None + self.user_service = UserService(self.storage_service) self.question_service = QuestionService() # 页面容器 self.stack = QStackedWidget() self.setCentralWidget(self.stack) - # 先构建最基础的两个页面:登录与SMTP设置 - self._build_login_page() - self._build_smtp_page() + # 构建页面:登录、注册(邮箱/验证码/用户名密码)、选择、答题、结果、修改密码 + self._build_login_page() # index 0 + self._build_register_email_page() # index 1 + self._build_verify_page() # index 2 + self._build_set_credentials_page() # index 3 + self._build_choice_page() # index 4 + self._build_quiz_page() # index 5 + self._build_result_page() # index 6 + self._build_change_password_page() # index 7 # 默认显示登录页 self.stack.setCurrentIndex(0) def _build_login_page(self) -> None: - """构建登录页面:邮箱+密码+登录按钮。""" + """构建登录页面:支持用户名或邮箱登录。""" page = QWidget() layout = QVBoxLayout(page) layout.setAlignment(Qt.AlignmentFlag.AlignTop) @@ -47,9 +52,9 @@ class MainWindow(QMainWindow): title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") layout.addWidget(title) - self.login_email = QLineEdit() - self.login_email.setPlaceholderText("请输入邮箱") - layout.addWidget(self.login_email) + self.login_identifier = QLineEdit() + self.login_identifier.setPlaceholderText("请输入邮箱或用户名") + layout.addWidget(self.login_identifier) self.login_password = QLineEdit() self.login_password.setPlaceholderText("请输入密码") @@ -58,84 +63,523 @@ class MainWindow(QMainWindow): btn_row = QHBoxLayout() self.btn_login = QPushButton("登录") - self.btn_to_smtp = QPushButton("SMTP设置") + self.btn_to_register = QPushButton("去注册") btn_row.addWidget(self.btn_login) - btn_row.addWidget(self.btn_to_smtp) + btn_row.addWidget(self.btn_to_register) layout.addLayout(btn_row) self.btn_login.clicked.connect(self._on_login) - self.btn_to_smtp.clicked.connect(lambda: self.stack.setCurrentIndex(1)) + self.btn_to_register.clicked.connect(lambda: self.stack.setCurrentIndex(1)) self.stack.addWidget(page) def _on_login(self) -> None: - """处理登录逻辑:校验输入并调用用户服务。""" - email = (self.login_email.text() or "").strip() + """处理登录逻辑:根据输入自动识别邮箱或用户名进行登录,并在成功后进入选择页。""" + identifier = (self.login_identifier.text() or "").strip() password = self.login_password.text() or "" - if not email or not password: - QMessageBox.warning(self, "提示", "邮箱与密码均不能为空") + if not identifier or not password: + QMessageBox.warning(self, "提示", "账号与密码均不能为空") return - ok, msg = self.user_service.login(email, password) + # 判断是否为邮箱 + if "@" in identifier: + ok, msg = self.user_service.login(identifier, password) + if ok: + self.session_email = identifier + else: + QMessageBox.warning(self, "提示", msg or "登录失败") + return + else: + ok, msg = self.user_service.login_by_username(identifier, password) + if ok: + # 通过用户名获取邮箱,记录会话 + user = self.storage_service.get_user_by_username(identifier) + self.session_email = user.get('email') if user else None + else: + QMessageBox.warning(self, "提示", msg or "登录失败") + return + QMessageBox.information(self, "提示", "登录成功") + # 跳转到选择页 + self.stack.setCurrentIndex(4) + + def _build_register_email_page(self) -> None: + """构建注册第一步:邮箱输入页面。""" + page = QWidget() + layout = QVBoxLayout(page) + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + title = QLabel("注册 - 邮箱验证") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") + layout.addWidget(title) + + self.register_email = QLineEdit() + self.register_email.setPlaceholderText("请输入邮箱地址") + layout.addWidget(self.register_email) + + btn_row = QHBoxLayout() + self.btn_send_code = QPushButton("获取验证码") + self.btn_back_to_login = QPushButton("返回登录") + btn_row.addWidget(self.btn_send_code) + btn_row.addWidget(self.btn_back_to_login) + layout.addLayout(btn_row) + + self.btn_send_code.clicked.connect(self._on_send_code) + self.btn_back_to_login.clicked.connect(lambda: self.stack.setCurrentIndex(0)) + + self.stack.addWidget(page) + + def _on_send_code(self) -> None: + """处理发送验证码逻辑:验证邮箱格式并生成验证码。""" + email = (self.register_email.text() or "").strip() + if not email: + QMessageBox.warning(self, "提示", "请输入邮箱地址") + return + + ok, msg = self.user_service.request_registration(email) if ok: - QMessageBox.information(self, "提示", "登录成功") + self.session_email = email + QMessageBox.information(self, "提示", msg) + # 跳转到验证码页面 + self.stack.setCurrentIndex(2) else: - QMessageBox.warning(self, "提示", msg or "登录失败") + QMessageBox.warning(self, "提示", msg) - def _build_smtp_page(self) -> None: - """构建 SMTP 设置页面:服务器、端口、账号、授权码、TLS/SSL、发件人名。""" + def _build_verify_page(self) -> None: + """构建注册第二步:验证码验证页面。""" page = QWidget() layout = QVBoxLayout(page) layout.setAlignment(Qt.AlignmentFlag.AlignTop) - title = QLabel("SMTP 设置") + title = QLabel("注册 - 验证码验证") title.setAlignment(Qt.AlignmentFlag.AlignCenter) title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") layout.addWidget(title) - self.smtp_server = QLineEdit(); self.smtp_server.setPlaceholderText("服务器,例如 smtp.qq.com") - self.smtp_port = QLineEdit(); self.smtp_port.setPlaceholderText("端口,通常 465 或 587") - self.smtp_user = QLineEdit(); self.smtp_user.setPlaceholderText("邮箱账号,例如 123456@qq.com") - self.smtp_pass = QLineEdit(); self.smtp_pass.setPlaceholderText("邮箱授权码/应用密码") - self.smtp_pass.setEchoMode(QLineEdit.EchoMode.Password) - self.smtp_tls = QCheckBox("启用 TLS") - self.smtp_ssl = QCheckBox("启用 SSL") - self.smtp_sender = QLineEdit(); self.smtp_sender.setPlaceholderText("发件人显示名称,可选") - - layout.addWidget(self.smtp_server) - layout.addWidget(self.smtp_port) - layout.addWidget(self.smtp_user) - layout.addWidget(self.smtp_pass) - layout.addWidget(self.smtp_tls) - layout.addWidget(self.smtp_ssl) - layout.addWidget(self.smtp_sender) + self.verify_code = QLineEdit() + self.verify_code.setPlaceholderText("请输入6位验证码") + layout.addWidget(self.verify_code) btn_row = QHBoxLayout() - self.btn_save_smtp = QPushButton("保存设置") - self.btn_back_login = QPushButton("返回登录") - btn_row.addWidget(self.btn_save_smtp) - btn_row.addWidget(self.btn_back_login) + self.btn_verify = QPushButton("验证") + self.btn_back_to_email = QPushButton("返回上一步") + btn_row.addWidget(self.btn_verify) + btn_row.addWidget(self.btn_back_to_email) layout.addLayout(btn_row) - self.btn_save_smtp.clicked.connect(self._on_save_smtp) - self.btn_back_login.clicked.connect(lambda: self.stack.setCurrentIndex(0)) + self.btn_verify.clicked.connect(self._on_verify_code) + self.btn_back_to_email.clicked.connect(lambda: self.stack.setCurrentIndex(1)) self.stack.addWidget(page) - def _on_save_smtp(self) -> None: - """保存 SMTP 配置到本地 JSON,以支持验证码发送。""" + def _on_verify_code(self) -> None: + """处理验证码验证逻辑。""" + code = (self.verify_code.text() or "").strip() + if not code: + QMessageBox.warning(self, "提示", "请输入验证码") + return + + if not self.session_email: + QMessageBox.warning(self, "提示", "会话已过期,请重新注册") + self.stack.setCurrentIndex(1) + return + + ok, msg = self.user_service.verify_code(self.session_email, code) + if ok: + QMessageBox.information(self, "提示", msg) + # 跳转到设置用户名密码页面 + self.stack.setCurrentIndex(3) + else: + QMessageBox.warning(self, "提示", msg) + + def _build_set_credentials_page(self) -> None: + """构建注册第三步:设置用户名和密码页面。""" + page = QWidget() + layout = QVBoxLayout(page) + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + title = QLabel("注册 - 设置用户名和密码") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") + layout.addWidget(title) + + self.register_username = QLineEdit() + self.register_username.setPlaceholderText("请输入用户名(3-16位,字母数字下划线)") + layout.addWidget(self.register_username) + + self.register_password = QLineEdit() + self.register_password.setPlaceholderText("请输入密码(6-10位,包含大小写字母和数字)") + self.register_password.setEchoMode(QLineEdit.EchoMode.Password) + layout.addWidget(self.register_password) + + self.register_confirm = QLineEdit() + self.register_confirm.setPlaceholderText("请确认密码") + self.register_confirm.setEchoMode(QLineEdit.EchoMode.Password) + layout.addWidget(self.register_confirm) + + btn_row = QHBoxLayout() + self.btn_complete_register = QPushButton("完成注册") + self.btn_back_to_verify = QPushButton("返回上一步") + btn_row.addWidget(self.btn_complete_register) + btn_row.addWidget(self.btn_back_to_verify) + layout.addLayout(btn_row) + + self.btn_complete_register.clicked.connect(self._on_complete_register) + self.btn_back_to_verify.clicked.connect(lambda: self.stack.setCurrentIndex(2)) + + self.stack.addWidget(page) + + def _on_complete_register(self) -> None: + """处理完成注册逻辑:设置用户名和密码。""" + username = (self.register_username.text() or "").strip() + password = self.register_password.text() or "" + confirm = self.register_confirm.text() or "" + + if not username or not password or not confirm: + QMessageBox.warning(self, "提示", "请填写完整信息") + return + + if not self.session_email: + QMessageBox.warning(self, "提示", "会话已过期,请重新注册") + self.stack.setCurrentIndex(1) + return + + # 设置用户名 + ok, msg = self.user_service.set_username(self.session_email, username) + if not ok: + QMessageBox.warning(self, "提示", msg) + return + + # 设置密码 + ok, msg = self.user_service.set_password(self.session_email, password, confirm) + if not ok: + QMessageBox.warning(self, "提示", msg) + return + + # 完成注册 + ok, msg = self.user_service.complete_registration(self.session_email) + if ok: + QMessageBox.information(self, "提示", "注册成功!请登录") + # 清空表单并跳转到登录页 + self.register_email.clear() + self.verify_code.clear() + self.register_username.clear() + self.register_password.clear() + self.register_confirm.clear() + self.session_email = None + self.stack.setCurrentIndex(0) + else: + QMessageBox.warning(self, "提示", msg) + + def _build_choice_page(self) -> None: + """构建选择页面:选择年级和题目数量。""" + page = QWidget() + layout = QVBoxLayout(page) + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + title = QLabel("选择题目难度和数量") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") + layout.addWidget(title) + + # 年级选择 + grade_label = QLabel("选择年级:") + layout.addWidget(grade_label) + + self.grade_group = QButtonGroup() + grade_layout = QHBoxLayout() + + self.primary_radio = QRadioButton("小学") + self.middle_radio = QRadioButton("初中") + self.high_radio = QRadioButton("高中") + + self.grade_group.addButton(self.primary_radio, 0) + self.grade_group.addButton(self.middle_radio, 1) + self.grade_group.addButton(self.high_radio, 2) + + grade_layout.addWidget(self.primary_radio) + grade_layout.addWidget(self.middle_radio) + grade_layout.addWidget(self.high_radio) + layout.addLayout(grade_layout) + + # 默认选择小学 + self.primary_radio.setChecked(True) + + # 题目数量选择 + count_label = QLabel("选择题目数量:") + layout.addWidget(count_label) + + self.question_count = QSpinBox() + self.question_count.setMinimum(10) + self.question_count.setMaximum(30) + self.question_count.setValue(15) + layout.addWidget(self.question_count) + + btn_row = QHBoxLayout() + self.btn_start_quiz = QPushButton("开始答题") + self.btn_change_password = QPushButton("修改密码") + self.btn_logout = QPushButton("退出登录") + btn_row.addWidget(self.btn_start_quiz) + btn_row.addWidget(self.btn_change_password) + btn_row.addWidget(self.btn_logout) + layout.addLayout(btn_row) + + self.btn_start_quiz.clicked.connect(self._on_start_quiz) + self.btn_change_password.clicked.connect(lambda: self.stack.setCurrentIndex(7)) + self.btn_logout.clicked.connect(self._on_logout) + + self.stack.addWidget(page) + + def _on_start_quiz(self) -> None: + """开始答题:生成题目并跳转到答题页面。""" + # 获取选择的年级 + grade_map = {0: 'primary', 1: 'middle', 2: 'high'} + grade = grade_map[self.grade_group.checkedId()] + count = self.question_count.value() + try: - port_val = int(self.smtp_port.text().strip() or '587') - except ValueError: - QMessageBox.warning(self, '提示', '端口必须为数字') + self.questions = self.question_service.generate_questions(grade, count) + self.current_question = 0 + self.user_answers = [] + self._show_current_question() + self.stack.setCurrentIndex(5) + except Exception as e: + QMessageBox.warning(self, "提示", f"生成题目失败:{str(e)}") + + def _on_logout(self) -> None: + """退出登录:清空会话并返回登录页。""" + self.session_email = None + self.login_identifier.clear() + self.login_password.clear() + QMessageBox.information(self, "提示", "已退出登录") + self.stack.setCurrentIndex(0) + + def _build_quiz_page(self) -> None: + """构建答题页面:显示题目和选项。""" + page = QWidget() + layout = QVBoxLayout(page) + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + # 进度显示 + self.progress_label = QLabel() + self.progress_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.progress_label.setStyleSheet("font-size: 16px; margin: 10px 0;") + layout.addWidget(self.progress_label) + + # 题目显示 + self.question_label = QLabel() + self.question_label.setAlignment(Qt.AlignmentFlag.AlignLeft) + self.question_label.setStyleSheet("font-size: 18px; margin: 20px 0; padding: 10px; border: 1px solid #ccc;") + self.question_label.setWordWrap(True) + layout.addWidget(self.question_label) + + # 选项 + self.option_group = QButtonGroup() + self.option_radios = [] + for i in range(4): + radio = QRadioButton() + radio.setStyleSheet("font-size: 16px; margin: 5px 0;") + self.option_group.addButton(radio, i) + self.option_radios.append(radio) + layout.addWidget(radio) + + # 按钮 + btn_row = QHBoxLayout() + self.btn_prev = QPushButton("上一题") + self.btn_next = QPushButton("下一题") + self.btn_submit = QPushButton("提交答案") + btn_row.addWidget(self.btn_prev) + btn_row.addWidget(self.btn_next) + btn_row.addWidget(self.btn_submit) + layout.addLayout(btn_row) + + self.btn_prev.clicked.connect(self._on_prev_question) + self.btn_next.clicked.connect(self._on_next_question) + self.btn_submit.clicked.connect(self._on_submit_quiz) + + self.stack.addWidget(page) + + def _show_current_question(self) -> None: + """显示当前题目。""" + if not hasattr(self, 'questions') or not self.questions: + return + + question = self.questions[self.current_question] + total = len(self.questions) + + # 更新进度 + self.progress_label.setText(f"第 {self.current_question + 1} 题 / 共 {total} 题") + + # 更新题目 + self.question_label.setText(question['stem']) + + # 更新选项 + for i, option in enumerate(question['options']): + self.option_radios[i].setText(f"{chr(65+i)}. {option}") + + # 恢复之前的选择 + if self.current_question < len(self.user_answers): + selected = self.user_answers[self.current_question] + if selected is not None: + self.option_radios[selected].setChecked(True) + else: + # 清空选择 + for radio in self.option_radios: + radio.setChecked(False) + + # 更新按钮状态 + self.btn_prev.setEnabled(self.current_question > 0) + self.btn_next.setEnabled(self.current_question < total - 1) + + def _on_prev_question(self) -> None: + """上一题。""" + self._save_current_answer() + if self.current_question > 0: + self.current_question -= 1 + self._show_current_question() + + def _on_next_question(self) -> None: + """下一题。""" + self._save_current_answer() + if self.current_question < len(self.questions) - 1: + self.current_question += 1 + self._show_current_question() + + def _save_current_answer(self) -> None: + """保存当前题目的答案。""" + selected = self.option_group.checkedId() + # 确保user_answers列表足够长 + while len(self.user_answers) <= self.current_question: + self.user_answers.append(None) + self.user_answers[self.current_question] = selected if selected >= 0 else None + + def _on_submit_quiz(self) -> None: + """提交答案并计算分数。""" + self._save_current_answer() + + # 检查是否有未答题目 + unanswered = [] + for i, answer in enumerate(self.user_answers): + if answer is None: + unanswered.append(i + 1) + + if unanswered: + reply = QMessageBox.question( + self, "提示", + f"还有第 {', '.join(map(str, unanswered))} 题未作答,确定要提交吗?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + if reply == QMessageBox.StandardButton.No: + return + + # 计算分数 + correct = 0 + total = len(self.questions) + for i, question in enumerate(self.questions): + if i < len(self.user_answers) and self.user_answers[i] == question['answer_index']: + correct += 1 + + self.score = correct + self.total_questions = total + self._show_result() + self.stack.setCurrentIndex(6) + + def _build_result_page(self) -> None: + """构建结果页面:显示分数和操作选项。""" + page = QWidget() + layout = QVBoxLayout(page) + layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + + title = QLabel("答题结果") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") + layout.addWidget(title) + + self.score_label = QLabel() + self.score_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.score_label.setStyleSheet("font-size: 24px; margin: 20px 0; color: #2196F3;") + layout.addWidget(self.score_label) + + btn_row = QHBoxLayout() + self.btn_continue = QPushButton("继续做题") + self.btn_exit = QPushButton("退出") + btn_row.addWidget(self.btn_continue) + btn_row.addWidget(self.btn_exit) + layout.addLayout(btn_row) + + self.btn_continue.clicked.connect(lambda: self.stack.setCurrentIndex(4)) + self.btn_exit.clicked.connect(self._on_logout) + + self.stack.addWidget(page) + + def _show_result(self) -> None: + """显示答题结果。""" + if hasattr(self, 'score') and hasattr(self, 'total_questions'): + percentage = (self.score / self.total_questions) * 100 + self.score_label.setText( + f"您答对了 {self.score} 题,共 {self.total_questions} 题\n" + f"正确率:{percentage:.1f}%" + ) + + def _build_change_password_page(self) -> None: + """构建修改密码页面。""" + page = QWidget() + layout = QVBoxLayout(page) + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + title = QLabel("修改密码") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") + layout.addWidget(title) + + self.old_password = QLineEdit() + self.old_password.setPlaceholderText("请输入原密码") + self.old_password.setEchoMode(QLineEdit.EchoMode.Password) + layout.addWidget(self.old_password) + + self.new_password = QLineEdit() + self.new_password.setPlaceholderText("请输入新密码(6-10位,包含大小写字母和数字)") + self.new_password.setEchoMode(QLineEdit.EchoMode.Password) + layout.addWidget(self.new_password) + + self.confirm_new_password = QLineEdit() + self.confirm_new_password.setPlaceholderText("请确认新密码") + self.confirm_new_password.setEchoMode(QLineEdit.EchoMode.Password) + layout.addWidget(self.confirm_new_password) + + btn_row = QHBoxLayout() + self.btn_change_pwd = QPushButton("修改密码") + self.btn_back_to_choice = QPushButton("返回") + btn_row.addWidget(self.btn_change_pwd) + btn_row.addWidget(self.btn_back_to_choice) + layout.addLayout(btn_row) + + self.btn_change_pwd.clicked.connect(self._on_change_password) + self.btn_back_to_choice.clicked.connect(lambda: self.stack.setCurrentIndex(4)) + + self.stack.addWidget(page) + + def _on_change_password(self) -> None: + """处理修改密码逻辑。""" + old_pwd = self.old_password.text() or "" + new_pwd = self.new_password.text() or "" + confirm_pwd = self.confirm_new_password.text() or "" + + if not old_pwd or not new_pwd or not confirm_pwd: + QMessageBox.warning(self, "提示", "请填写完整信息") return - config = { - 'server': self.smtp_server.text().strip(), - 'port': port_val, - 'username': self.smtp_user.text().strip(), - 'password': self.smtp_pass.text(), - 'use_tls': self.smtp_tls.isChecked(), - 'use_ssl': self.smtp_ssl.isChecked(), - 'sender_name': self.smtp_sender.text().strip() or 'Math Study App', - } - self.email_service.update_smtp_config(config) - QMessageBox.information(self, '提示', 'SMTP设置已保存') \ No newline at end of file + + if not self.session_email: + QMessageBox.warning(self, "提示", "会话已过期,请重新登录") + self.stack.setCurrentIndex(0) + return + + ok, msg = self.user_service.change_password(self.session_email, old_pwd, new_pwd, confirm_pwd) + if ok: + QMessageBox.information(self, "提示", "密码修改成功") + # 清空表单并返回选择页 + self.old_password.clear() + self.new_password.clear() + self.confirm_new_password.clear() + self.stack.setCurrentIndex(4) + else: + QMessageBox.warning(self, "提示", msg) \ No newline at end of file diff --git a/src/utils/__pycache__/security_utils.cpython-311.pyc b/src/utils/__pycache__/security_utils.cpython-311.pyc index b6e3f39..f02b498 100644 Binary files a/src/utils/__pycache__/security_utils.cpython-311.pyc and b/src/utils/__pycache__/security_utils.cpython-311.pyc differ