From 7294dbe2000214d6e08eb7e9e2a1537c558b1932 Mon Sep 17 00:00:00 2001 From: echo Date: Sun, 12 Oct 2025 16:20:29 +0800 Subject: [PATCH] first --- src/app.py | 2 +- .../question_service.cpython-311.pyc | Bin 6001 -> 10953 bytes .../storage_service.cpython-311.pyc | Bin 7232 -> 8088 bytes .../__pycache__/user_service.cpython-311.pyc | Bin 7677 -> 10970 bytes src/services/question_service.py | 191 +++++- src/services/storage_service.py | 12 + src/services/user_service.py | 83 ++- src/storage/users.json | 14 +- .../__pycache__/main_window.cpython-311.pyc | Bin 10854 -> 37542 bytes src/ui/main_window.py | 574 ++++++++++++++++-- .../security_utils.cpython-311.pyc | Bin 4720 -> 4720 bytes 11 files changed, 765 insertions(+), 111 deletions(-) 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 8921db7a3d6a0361ac7d939e97cef627198e727f..10281f92b9db898217f401ea8e91ec3577ccdfe5 100644 GIT binary patch literal 10953 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 delta 1956 zcmZ`)ZA?>F7`~tVxZJkdBGjq#0@_-z2#6UtK}F*ampP3eb8M4UZkZ^w^R!Kx<`Rh0 z?4l;toH=j&$m%{2;}?b>bBi-UK zocDb6KF@RdVDGbP`&FAwpx~)%`14SQ?X2A`ZGhH!N~T&VnU2{E&NfmGaQO!`Oc6WjAz+M6#2|J zEG`yM6)YMt1K_X4HNBOj^*ulzl@pz(cWAZR!BMenOe|CBM{_!CuG8;pTT(7<{iw|| zX7i{_rpT6boorMaGfr1BaCW7-CF5GH9ZkDdLrLv2YCv_Xx^;YRieq+81}}1K7kD@h z3-4dQ<7f1eSrlWprMO&SiYSFn6ahi(9QrsQh#_=%DInZ(!#V(zU)O1eC8-`BdWDNz zk4fQxJbs$Y2G7D?xk%?uj5R{uEbi3HkMG``;SLDRo%k3qf8$F2vu`H`Pt78cY*ir4 zArFa)0b(M+ldE4E*@R0j4rJ=xz-P_Vd#B^l0U&mdkIo7mUEx5qtvf6Sr`YPc4n!#O zaY$d%4>4LpfC$WIM(`0RL?naO0%YkpT0|OzW>;I13;VaJbjDGy1=5asD8Q*+1FBrcS@N56#jnm4sa-?$Y3GVjXTzAY zK{aOZl@Pd>4wjC1fA;_APw#viKd30i&e9Uoo|hSz1vig4rriwQ#J?=?z5$*8 zBas8Y(h1Os!s%#unU~GFmkqaC6&4&VC>&tXWh)RqEeu@hUMTdK!MJ9{Z1B%hQTV)g zf!mEAuCxoXs4Kx1+m8pAz1o|*eJ_7*$S~pz)xcuMs<^Ky5I;~?bueDlF6Ax_O7R*g z_tkM?aD5J#+E4+Jbm8CU&z~3mAwaG{^cujl#qUe{S|AHpEWyO9Vk84(k-u|H+SlP- zmEe8E_=Xnv`Luv)VTR8#M-c5a?S??}N1| zW>l;g6DvS6a337dT2l0=t$fT@t}^3xN3yIxm^|2jSgY+1scgp4q}8S!O;A#0)AdoK z9%@ND1Tp{xlmyOiVz;67!{Ze{F|WK1M=EdtXcK{r1U3_B!6Tju6Zx&kj|-mi>h%z2 zIk@j4sELHU9PEk)Ufgn$@gQPBv<5eN%jvg<-}AmhTgf6>7A{YGf7MEfnCcpZ>?BRk zHIaw_`B;%WW_|lU5czj$O`fDkJr6Fm@~yPCXObfIJXj)-`Sp_&spmmA{=wI5F`=Dg zYbzEjmsIQ~;mo0?EF0inh;;><(OwuJ{}+)E-d?#Jqe}P2{WM)VNzISX^!~qJGOF+J EAALbassI20 diff --git a/src/services/__pycache__/storage_service.cpython-311.pyc b/src/services/__pycache__/storage_service.cpython-311.pyc index a9bbf847ad18181118838e2be26af9cd0050bee3..99a793ee465a04a8ce3f1ec2c2faafe8dcfdc78d 100644 GIT binary patch delta 820 zcmZ`%OK1~87@pZ|n)pa3&2C82NJ&kFMC*fsAU+xpwV`UIL{S$_nmCghXd9SK#OBf> z9(pMhM`(`0gGK~x6S47DJP6*BQc!ZSP!bDXB6#xT%!(C4{1?9e_5JgI!|eL0e5m_{ z&9)bC%WL27PoUNAes~}X0aykEXb=NU!ca3|1CE#H+Q4)~I-s_)D+ z$k(uvvZkEjL9_6_x%m3q(_(XBt+BMoX4RoNK^UVKIR<6w{Vm2!0X%d zkH&(Kj`HJ!(!Xoy5-H%1<&^o*YCYg66UzbXWK{|H9PXQT!;P@w3pp`lGQ zR2itE;i`4GoN#`C@De-Z^7o1XdRl;wL*1NAXJ>F7rsxPuxX!`xa=~>T!Z6!*Uw|~tDur^qmkvXb{& U=q7|GTA(xjFzP@5(=`tL4cdn3mjD0& delta 281 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 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 3d0fd81f5efa21676bb7f9ce206a53a96d0275e0..68aae03bbed1b6ac84262b649e9c0b533e3d069d 100644 GIT binary patch literal 37542 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 U81(4jWYMo#%)iEnXHUZa2mZ9PumAu6 delta 4579 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 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 b6e3f390ad52263f64b61eea1ba0f10a3350a28c..f02b4980c68d914afeb8ac3da11ec96eabcddb08 100644 GIT binary patch delta 19 ZcmeyM@