From ba30c061da737c7d842a02a58691b341698e323d Mon Sep 17 00:00:00 2001 From: echo Date: Sun, 12 Oct 2025 14:01:49 +0800 Subject: [PATCH 1/4] init --- doc/README.md | 0 requirements.txt | 1 + src/__pycache__/app.cpython-311.pyc | Bin 0 -> 848 bytes src/app.py | 14 ++ src/services/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 199 bytes .../__pycache__/email_service.cpython-311.pyc | Bin 0 -> 4396 bytes .../question_service.cpython-311.pyc | Bin 0 -> 6001 bytes .../storage_service.cpython-311.pyc | Bin 0 -> 7232 bytes .../__pycache__/user_service.cpython-311.pyc | Bin 0 -> 7677 bytes src/services/email_service.py | 66 ++++++++ src/services/question_service.py | 96 ++++++++++++ src/services/storage_service.py | 85 +++++++++++ src/services/user_service.py | 123 +++++++++++++++ src/storage/config.json | 11 ++ src/storage/users.json | 3 + src/ui/__init__.py | 0 src/ui/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 193 bytes .../__pycache__/main_window.cpython-311.pyc | Bin 0 -> 10854 bytes src/ui/main_window.py | 141 ++++++++++++++++++ src/utils/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 196 bytes .../security_utils.cpython-311.pyc | Bin 0 -> 4720 bytes src/utils/security_utils.py | 79 ++++++++++ ...导论-结对编程项目需求-2025.docx | Bin 0 -> 162 bytes ...导论-结对编程项目需求-2025.docx | Bin 0 -> 12250 bytes 26 files changed, 619 insertions(+) create mode 100644 doc/README.md create mode 100644 requirements.txt create mode 100644 src/__pycache__/app.cpython-311.pyc create mode 100644 src/app.py create mode 100644 src/services/__init__.py create mode 100644 src/services/__pycache__/__init__.cpython-311.pyc create mode 100644 src/services/__pycache__/email_service.cpython-311.pyc create mode 100644 src/services/__pycache__/question_service.cpython-311.pyc create mode 100644 src/services/__pycache__/storage_service.cpython-311.pyc create mode 100644 src/services/__pycache__/user_service.cpython-311.pyc create mode 100644 src/services/email_service.py create mode 100644 src/services/question_service.py create mode 100644 src/services/storage_service.py create mode 100644 src/services/user_service.py create mode 100644 src/storage/config.json create mode 100644 src/storage/users.json create mode 100644 src/ui/__init__.py create mode 100644 src/ui/__pycache__/__init__.cpython-311.pyc create mode 100644 src/ui/__pycache__/main_window.cpython-311.pyc create mode 100644 src/ui/main_window.py create mode 100644 src/utils/__init__.py create mode 100644 src/utils/__pycache__/__init__.cpython-311.pyc create mode 100644 src/utils/__pycache__/security_utils.cpython-311.pyc create mode 100644 src/utils/security_utils.py create mode 100644 ~$工程导论-结对编程项目需求-2025.docx create mode 100644 软件工程导论-结对编程项目需求-2025.docx diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0f26f21 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +PyQt6==6.7.0 \ No newline at end of file diff --git a/src/__pycache__/app.cpython-311.pyc b/src/__pycache__/app.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..563b10c0860098d13a746ec40f4dc12eb675956f GIT binary patch literal 848 zcmZ8f&1)J#6rb50jaVh6g_d~o*u8knp_D?uBsD0NPy(_b;x38QFV>YbC4p6JV~I3E zLoM-QXlv7i#?nHFA4&fOTY8X%oZ`BX9wJCDJ@xHY)7UpNZ{8bz@5k@G-S<|j8G#+$ z{yFc&2pwz5(5OMscR^4<9O7^oMX(QJEvCX$g!a)8k8y?!@e$s@86W92f`vjnDMQuH zkP88h%;<+Pb+uOp%NFmgcsv{m2EuR$o+(rL>13f+j>D;AE1RhY1n>F&|_onGVF`vT;ElTSDu~ZvS;3u6x&6){?q*dj?y9?+YW^A9`JC;1+HW6xQR^?2N>y#V0`+V{2(rNJ zh%d?{0^dcj0H*;?ViKH$6!Vq5@Xc)dVYW$kj;vRovpH6BwupIUIz63oXWWvzMaEX9 z(o-o{#wF>i50ywZ`TZK9-_KBjM(AOT^7w;AuQ=)OinAeZju#VpxAG+r;YfxYI{a^k z`UG?>8c#G3;r;#(NU#ErZ~@$J(~U@sONRNI!evOc{~-|o*P}7UEp)9px?6~8j_#h( mfX91i)Llfxl6~@>o@mk&En<@Hrkok4q^};0hz*`~34Z`~Hu%^8 literal 0 HcmV?d00001 diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..38bee2f --- /dev/null +++ b/src/app.py @@ -0,0 +1,14 @@ +from PyQt6.QtWidgets import QApplication +from src.ui.main_window import MainWindow + + +def main() -> None: + """应用程序入口:创建 QApplication,显示主窗口,并进入事件循环。""" + app = QApplication([]) + window = MainWindow() + window.show() + app.exec() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/services/__init__.py b/src/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/services/__pycache__/__init__.cpython-311.pyc b/src/services/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2d809dd93e760132d8a9d513ac6fe0960562637 GIT binary patch literal 199 zcmZ3^%ge<81naF{XMpI(AOZ#$p^VRLK*n^26oz01O-8?!3`I;p{%4Tn*Ki==Y8CTz z?Xo937rbnk6_b)*o|luKm=g11@A@aZx4m4r^ZD#`&(|${KA|n<*`|(X8+JU|yZQO< z$xqkse7PQnaQceG4b)4d6^~g@p=W7zc_4i^HWN5QtgUZfwq7g VQ_K$}J}@&fGJary5k<^EF#tXfMF0Q* literal 0 HcmV?d00001 diff --git a/src/services/__pycache__/email_service.cpython-311.pyc b/src/services/__pycache__/email_service.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da4c53f1183da56b643eb9a17cb70320622e111e GIT binary patch literal 4396 zcmc&1ZEO=qcGkPL*B^;-VmnSQfLlItPTHhE;M9P)LxMENmrhEqD!J%Z#=A)!Id*P$ z%}3-&0a4CCTGAXP5j2OpgJ z<3uKULgoK{;AHy3jr8CNSKI#9hSt{oKrqf@`3K#EY~Iu3@%ayMg5c@mpiS-edBu4G zdZAI;Dh7F=w{rXupO^FELHVM~ArsOuqgaCq^UT(iFSxJ)yQFW?gyrq8Y{Y|fMQd9no*5J~_H zpeGe;BGjNc0?#ubAek0}j3Q8`yg`=pVrS(GUTwvBy|ClKd8j+zgQ9q|yi~@4a;-p( zE?$TNl_h+rc)$0PVR8jt4NETJr9^!}6k2f{?7`x3}8rIZ`L{r`h zgo;z808`g#O z#r_}=@R}Dm7O=2ssm5#tewL08nAV|9NT7~tj**AZQG!5+5CKo%M3L6`r&J2ox>$yy zG0oS2CZ&0<#r3QB2)t0V?-jfL?>=A+t!Nz**DP4G9`TST4f&A@;ayS8s3VHEJycbtSFms>SKV~>FbioE>4t5UqF%_r zBU89rrUlW%i{M1ru;-YU>lJ;$fV-5(KRUk(11p|gr>e%|69ezSpaKIY29+38VSwEu zlRZKg@4`(SiUjsYRDT~W4Po=Fk>E$oT`lr2ML zjtkX~tnPp&+rHZLlvfMeeHC0-5CNLO?kjcs&5i;`YGo{JO^>N1*cw@9$1B9ODGp3}i z8j#&*3`yE_-g4Fwcm95o1)#nOnvrNGPBU4m)KHnUII_rCQunxOwN%xZsM;n~Z5z@* zwpUB`4bjdRJM~W7zB^&xE!lTxk;Ym#LnUp_k-fuvM~}sv3EO7LwmELw475kK4G(P_ zz=O70$yOV;)n>K8u8dK@Q#|@P1`D+1uoo|0EKB~8Mvm1NI-(n6>l5|{$=;Afl(lZM zFm%e-}eQ`2%=l1M( zV{nsLa&=Hb$3Zu#1V$ z&>JOsV?6)z_=E;mH@JG>?87j!kzsm*Y{-vsGsFBLy+vr>y>ILc$c8GH8C4(er?Sput2pUP?f~`xsCkWSk0efun~Y~<^#dscD4PxKDeqdR$gMa8!l;T!CeMZEZZg6iVMZ&tUB0Ha?9PciNJG98Mt{{aR$ Bq1yle literal 0 HcmV?d00001 diff --git a/src/services/__pycache__/question_service.cpython-311.pyc b/src/services/__pycache__/question_service.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8921db7a3d6a0361ac7d939e97cef627198e727f GIT binary patch literal 6001 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 literal 0 HcmV?d00001 diff --git a/src/services/__pycache__/storage_service.cpython-311.pyc b/src/services/__pycache__/storage_service.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a9bbf847ad18181118838e2be26af9cd0050bee3 GIT binary patch literal 7232 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 literal 0 HcmV?d00001 diff --git a/src/services/__pycache__/user_service.cpython-311.pyc b/src/services/__pycache__/user_service.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af9ce8721e1f0b451915d98e59208dc202909f0d GIT binary patch literal 7677 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 None: + """初始化邮件服务,依赖存储服务获取 SMTP 配置。""" + self.storage = storage + + def send_verification_code(self, to_email: str, code: str) -> bool: + """发送验证码到指定邮箱。 + + 参数: + to_email: 收件人邮箱。 + code: 验证码字符串。 + 返回: + True 表示发送成功,False 表示失败。 + """ + config = self.storage.load_config().get('smtp', {}) + server = config.get('server', '') + port = int(config.get('port', 587)) + username = config.get('username', '') + password = config.get('password', '') + use_tls = bool(config.get('use_tls', True)) + use_ssl = bool(config.get('use_ssl', False)) + sender_name = config.get('sender_name', 'Math Study App') + + if not server or not username or not password: + return False + + msg = EmailMessage() + msg['Subject'] = '数学学习软件注册验证码' + msg['From'] = f"{sender_name} <{username}>" + msg['To'] = to_email + msg.set_content(f"您的注册验证码为:{code}\n该验证码10分钟内有效。") + + try: + if use_ssl: + context = ssl.create_default_context() + with smtplib.SMTP_SSL(server, port, context=context) as smtp: + smtp.login(username, password) + smtp.send_message(msg) + else: + with smtplib.SMTP(server, port) as smtp: + if use_tls: + smtp.starttls(context=ssl.create_default_context()) + smtp.login(username, password) + smtp.send_message(msg) + return True + except Exception: + return False + + def update_smtp_config(self, config: Dict) -> None: + """更新并保存 SMTP 配置。""" + data = self.storage.load_config() + data['smtp'] = config + self.storage.save_config(data) \ No newline at end of file diff --git a/src/services/question_service.py b/src/services/question_service.py new file mode 100644 index 0000000..7265765 --- /dev/null +++ b/src/services/question_service.py @@ -0,0 +1,96 @@ +""" +试题服务模块:按小学/初中/高中生成选择题试卷,并保证同一试卷题目不重复。 +""" +import random +from typing import List, Dict + + +class QuestionService: + """负责生成不同年级难度的选择题。""" + + def __init__(self) -> None: + """初始化题目服务。""" + random.seed() + + def generate_questions(self, level: str, count: int) -> List[Dict]: + """生成指定年级与数量的题目列表。 + + 参数: + level: 'primary'|'middle'|'high' 三选一。 + count: 题目数量。 + 返回: + 题目字典列表,每个字典包含 stem, options, answer_index。 + """ + generators = { + 'primary': self._gen_primary, + 'middle': self._gen_middle, + 'high': self._gen_high, + } + gen = generators.get(level) + if not gen: + raise ValueError('无效的年级选项') + seen = set() + questions: List[Dict] = [] + while len(questions) < count: + q = gen() + if q['stem'] in seen: + continue + seen.add(q['stem']) + questions.append(q) + 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} = ?" + else: + ans = a - b + stem = f"计算:{a} - {b} = ?" + 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}) = ?" + options = self._make_options(ans) + return {'stem': stem, 'options': options, 'answer_index': options.index(str(ans))} + + def _make_options(self, answer, float_mode: bool = False) -> List[str]: + """根据正确答案生成 4 个选项(包含正确答案与3个干扰项)。""" + opts = set() + if float_mode: + correct = f"{answer:.2f}" + opts.add(correct) + while len(opts) < 4: + delta = random.uniform(-5, 5) + opts.add(f"{answer + delta:.2f}") + else: + correct = str(answer) + opts.add(correct) + while len(opts) < 4: + delta = random.randint(-10, 10) + opts.add(str(answer + delta)) + options = list(opts) + random.shuffle(options) + return options \ No newline at end of file diff --git a/src/services/storage_service.py b/src/services/storage_service.py new file mode 100644 index 0000000..7c41bd4 --- /dev/null +++ b/src/services/storage_service.py @@ -0,0 +1,85 @@ +""" +存储服务模块:使用 JSON 文件持久化数据(不使用数据库)。 +提供用户数据与配置数据的读写接口。 +""" +import json +import os +from typing import Dict, Any, List + + +class StorageService: + """使用 JSON 文件进行数据持久化的服务类。""" + + def __init__(self) -> None: + """初始化存储路径并确保必要文件存在。""" + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.storage_dir = os.path.join(base_dir, 'storage') + self.users_file = os.path.join(self.storage_dir, 'users.json') + self.config_file = os.path.join(self.storage_dir, 'config.json') + self._ensure_files() + + def _ensure_files(self) -> None: + """确保存储目录与文件存在,如不存在则创建。""" + os.makedirs(self.storage_dir, exist_ok=True) + if not os.path.exists(self.users_file): + with open(self.users_file, 'w', encoding='utf-8') as f: + json.dump({'users': []}, f, ensure_ascii=False, indent=2) + if not os.path.exists(self.config_file): + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump({ + 'smtp': { + 'server': '', + 'port': 587, + 'username': '', + 'password': '', + 'use_tls': True, + 'use_ssl': False, + 'sender_name': 'Math Study App' + } + }, f, ensure_ascii=False, indent=2) + + def load_users(self) -> Dict[str, List[Dict[str, Any]]]: + """读取用户列表数据。返回字典 {'users': [...]}。""" + with open(self.users_file, 'r', encoding='utf-8') as f: + return json.load(f) + + def save_users(self, data: Dict[str, List[Dict[str, Any]]]) -> None: + """写入用户列表数据到文件。""" + with open(self.users_file, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + def load_config(self) -> Dict[str, Any]: + """读取配置数据(包含 SMTP 配置)。""" + with open(self.config_file, 'r', encoding='utf-8') as f: + return json.load(f) + + def save_config(self, data: Dict[str, Any]) -> None: + """写入配置数据到文件。""" + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + def get_user(self, email: str) -> Dict[str, Any] | None: + """根据邮箱获取用户字典,不存在返回 None。""" + users = self.load_users().get('users', []) + for u in users: + if u.get('email') == email: + return u + return None + + def upsert_user(self, user: Dict[str, Any]) -> None: + """插入或更新用户字典,并立即持久化。""" + data = self.load_users() + users = data.get('users', []) + found = False + for i, u in enumerate(users): + if u.get('email') == user.get('email'): + users[i] = user + found = True + break + if not found: + users.append(user) + self.save_users({'users': users}) + + def user_exists(self, email: str) -> bool: + """判断指定邮箱的用户是否存在。""" + return self.get_user(email) is not None \ No newline at end of file diff --git a/src/services/user_service.py b/src/services/user_service.py new file mode 100644 index 0000000..2006ff6 --- /dev/null +++ b/src/services/user_service.py @@ -0,0 +1,123 @@ +""" +用户服务模块:处理注册、验证码、设置密码、登录与修改密码逻辑。 +""" +import time +from typing import Any, Dict + +from ..utils.security_utils import ( + validate_email, + validate_password_strength, + generate_verification_code, + hash_password, + verify_password, +) +from .storage_service import StorageService +from .email_service import EmailService + + +class UserService: + """封装用户相关业务逻辑的服务类。""" + + def __init__(self, storage: StorageService, email_service: EmailService) -> None: + """初始化用户服务,注入存储与邮件服务。""" + self.storage = storage + self.email_service = email_service + + def request_registration(self, email: str) -> tuple[bool, str]: + """发起注册请求:校验邮箱,生成验证码并发送邮件。 + + 参数: + email: 用户邮箱。 + 返回: + (success, message) 二元组,表示结果与提示信息。 + """ + if not validate_email(email): + return False, '邮箱格式不正确' + existing = self.storage.get_user(email) + if existing and existing.get('verified'): + return False, '该邮箱已注册' + code = generate_verification_code() + salt, code_hash = hash_password(code) + user = existing or {'email': email} + user.update({ + 'verified': False, + 'code_salt': salt.hex(), + 'code_hash': code_hash.hex(), + 'code_time': int(time.time()), + 'password_salt': '', + 'password_hash': '', + '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, '验证码已发送,请查收邮箱' + + def verify_code(self, email: str, code: str) -> tuple[bool, str]: + """校验验证码并标记邮箱已验证。 + + 参数: + email: 用户邮箱。 + code: 用户输入的验证码。 + 返回: + (success, message)。 + """ + 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', '') + hash_hex = user.get('code_hash', '') + if not salt_hex or not hash_hex: + return False, '未找到验证码信息' + salt = bytes.fromhex(salt_hex) + hash_bytes = bytes.fromhex(hash_hex) + if not verify_password(code, salt, hash_bytes): + return False, '验证码不正确' + user['verified'] = True + # 清除验证码信息 + user['code_salt'] = '' + user['code_hash'] = '' + self.storage.upsert_user(user) + return True, '邮箱验证成功,请设置密码' + + def set_password(self, email: str, password: str, confirm: str) -> tuple[bool, str]: + """设置或重置密码:校验强度、确认一致并持久化哈希。""" + if password != confirm: + return False, '两次输入的密码不一致' + if not validate_password_strength(password): + return False, '密码需为6-10位且包含大小写字母与数字' + user = self.storage.get_user(email) + if not user or not user.get('verified'): + return False, '邮箱未验证或用户不存在' + salt, pwd_hash = hash_password(password) + user['password_salt'] = salt.hex() + user['password_hash'] = pwd_hash.hex() + self.storage.upsert_user(user) + 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, '用户不存在或未设置密码' + 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) + if not user: + return False, '用户不存在' + salt_hex = user.get('password_salt', '') + hash_hex = user.get('password_hash', '') + if not verify_password(old_password, bytes.fromhex(salt_hex), bytes.fromhex(hash_hex)): + return False, '原密码不正确' + return self.set_password(email, new_password, confirm) \ No newline at end of file diff --git a/src/storage/config.json b/src/storage/config.json new file mode 100644 index 0000000..e358e09 --- /dev/null +++ b/src/storage/config.json @@ -0,0 +1,11 @@ +{ + "smtp": { + "server": "", + "port": 587, + "username": "", + "password": "", + "use_tls": true, + "use_ssl": false, + "sender_name": "Math Study App" + } +} \ No newline at end of file diff --git a/src/storage/users.json b/src/storage/users.json new file mode 100644 index 0000000..4ef6c8c --- /dev/null +++ b/src/storage/users.json @@ -0,0 +1,3 @@ +{ + "users": [] +} \ No newline at end of file diff --git a/src/ui/__init__.py b/src/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/__pycache__/__init__.cpython-311.pyc b/src/ui/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8689912d783718686ab4af9208922d2018f4c2a GIT binary patch literal 193 zcmZ3^%ge<81Z%8bXMpI(AOZ#$p^VRLK*n^26oz01O-8?!3`I;p{%4Tn*FYfQY8CTz z?Xo937rbnk6_b)*o|luKm=g11@A@aZx4m4r^ZD#`&(|${KA|n<*`|(X8+JU|yZQO< z$xqkse7gPc*!4!VO&CS literal 0 HcmV?d00001 diff --git a/src/ui/__pycache__/main_window.cpython-311.pyc b/src/ui/__pycache__/main_window.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d0fd81f5efa21676bb7f9ce206a53a96d0275e0 GIT binary patch literal 10854 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 None: + """初始化主窗口并构建基础页面。""" + super().__init__() + self.setWindowTitle("数学学习软件") + self.setFixedSize(900, 600) + + # 业务服务初始化 + self.storage_service = StorageService() + self.email_service = EmailService(self.storage_service) + self.user_service = UserService(self.storage_service, self.email_service) + self.question_service = QuestionService() + + # 页面容器 + self.stack = QStackedWidget() + self.setCentralWidget(self.stack) + + # 先构建最基础的两个页面:登录与SMTP设置 + self._build_login_page() + self._build_smtp_page() + + # 默认显示登录页 + self.stack.setCurrentIndex(0) + + def _build_login_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.login_email = QLineEdit() + self.login_email.setPlaceholderText("请输入邮箱") + layout.addWidget(self.login_email) + + self.login_password = QLineEdit() + self.login_password.setPlaceholderText("请输入密码") + self.login_password.setEchoMode(QLineEdit.EchoMode.Password) + layout.addWidget(self.login_password) + + btn_row = QHBoxLayout() + self.btn_login = QPushButton("登录") + self.btn_to_smtp = QPushButton("SMTP设置") + btn_row.addWidget(self.btn_login) + btn_row.addWidget(self.btn_to_smtp) + layout.addLayout(btn_row) + + self.btn_login.clicked.connect(self._on_login) + self.btn_to_smtp.clicked.connect(lambda: self.stack.setCurrentIndex(1)) + + self.stack.addWidget(page) + + def _on_login(self) -> None: + """处理登录逻辑:校验输入并调用用户服务。""" + email = (self.login_email.text() or "").strip() + password = self.login_password.text() or "" + if not email or not password: + QMessageBox.warning(self, "提示", "邮箱与密码均不能为空") + return + ok, msg = self.user_service.login(email, password) + if ok: + QMessageBox.information(self, "提示", "登录成功") + else: + QMessageBox.warning(self, "提示", msg or "登录失败") + + def _build_smtp_page(self) -> None: + """构建 SMTP 设置页面:服务器、端口、账号、授权码、TLS/SSL、发件人名。""" + page = QWidget() + layout = QVBoxLayout(page) + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + title = QLabel("SMTP 设置") + 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) + + 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) + 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.stack.addWidget(page) + + def _on_save_smtp(self) -> None: + """保存 SMTP 配置到本地 JSON,以支持验证码发送。""" + try: + port_val = int(self.smtp_port.text().strip() or '587') + except ValueError: + 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 diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/__pycache__/__init__.cpython-311.pyc b/src/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98fa03055daae7c8aa7b32ab7ac46094bf1597b7 GIT binary patch literal 196 zcmZ3^%ge<81e>g0XMpI(AOZ#$p^VRLK*n^26oz01O-8?!3`I;p{%4Tn*AO7#Y8CTz z?Xo937rbnk6_b)*o|luKm=g11@A@aZx4m4r^ZD#`&(|${KA|n<*`|(X8+JU|yZQO< z$xqkse7usH%Yrtsgg^08Jdu$T|yDcMn^*s73xZhsw~VhzBh@%@wM)~ zE)6r1VF?Lf8W%#761qAn1Ov^3qCv<5_{c|R@&|M!q-pqG+uDbE%BoHMwEsER*RfqP z(P`VU@Be=u|MUL+ALp9cT#6w1oB!H9Q-aV_T+kAfNm~8}*a%`!J7OqC=cd{z!gcLB z!u9QX!s&JzxZZ8(F}52i#L#RJV_=IrX~x(`x0f(Q*>W+IOBoAO%9=o;2qa8>hW0Y% z1tL?%ni<`(B7~sL$eLMmr*2JVDF}5M88gV1XC=!)8qZS9RKTbejBY=&Rr(g8L#aG< zTJDd?cdyF*ca_Mv{N>1Z3zw9kQ2hQ_>f}^n>Xs57Pu&PU4V;jtPbI=9v+>2-iTMli^u5Hw=ZVPK4uN-e_(Ydm=nz<^k9Uc^c2cbO_Dba+JKQeD zA+mOIZoCPmD}zsX6-WT3EvVvcDzN*B$sF7k+CBQ=lBqUks*SuEHEoU3TbDryNyT1= zAROg*=Gid#Gss{VbBHp#n(db<;_5n@C%@5RU`bvJ3~81_MG4I+1}y3YE3TX zu|E8-Hp^GS;OchW+)^dU~`96qv z65z?T7$|RMeA`mKieeBDd-OhJvi4~0YN3dT zAQ_$AoXg2dMzB3DPnTp=u?SxMMHp71?9b3?Y{Gc#dOv1)SEtMA5M7+d?&KKu8HTF? zh7sz4WDHkjTePxm=GPaShg+_0xKg)N)ex&{fU1H&!4RkseVB83Bm?j8bg?#y#}-1s z{#MhH)z`w~NjG8nEkKBtl$8%PpJ`TST3r9oc&~1u4oXXvZ84yxwwS3cO1F`zC0cN} zMJEnsw3Nl|8K?m_K?SOFLxYw0kkRE|09>9y8w8=1Yhv_%6wbN86&Z%kAHh#Ch6#|o znUy}SgD6zjiKBF*@1M{U!Y0*&CLfF?$Ai}Q-rl>bwb3e*Ap3b-@uy$O*UqX#Xt>7s z#K!=RdT2ToI1NBfjD_UD!gmW}$+K6L%Qw{MTORx(IdDTh6VW34XUD0V@K~=|y+;{4 z*urj`H9i-V`+pBgf+LFlI}pT^>8zk!sY^r32q$>cqBgz0RwclS7)l9Bab z_}cWuRP$SLw_H^(YBM|RC@{F`29G5#F^3ACk+sLT?o8ElEtYg8lp z7L4l5>NOOyAPhiZ1g!!@N{;Rk*c~NSYodZa1comSj z0@}R+YBeEsloegT;}jb0oL7`g77NJBeLvxsQ+5-00KO6S&g0AH@rx1Q)`auTm~6v@ zps(D?^>`gTYX`&13SuT2AQ0p6?MfvO={bo)6hr_O#Ju*dU=4?;yWkUWzX1Ba8of{x zv($#$BHh#64Q{q;slGK<-};Dt)U{+e5VITrmu{+znO+UsA|EZ8nqsD=DBYx5R*Lv% z)g4%*BD0KU?m|Mv} zw2Wv4kC%I04dv9}w|GYn$N1cA3*QP&*oq7FK+}4PqS87&MW+!a#fT~ml;GcER30F| z$H*KYznq$yD9Zb%jU`mWXl)t+3$G)rkkpu|*CSPF1Z<|7utH)jruI;2glSk%`OF(C ze+GT2oO(HpfQ bool: + """验证邮箱格式是否正确。 + + 参数: + email: 待验证的邮箱字符串。 + 返回: + True 表示格式合法,False 表示格式不合法。 + """ + pattern = r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$" + return re.match(pattern, email) is not None + + +def validate_password_strength(password: str) -> bool: + """验证密码是否满足强度要求:6-10位,必须包含大小写字母和数字。 + + 参数: + password: 待验证的密码字符串。 + 返回: + True 表示满足要求,False 表示不满足。 + """ + if not (6 <= 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 generate_verification_code(length: int = 6) -> str: + """生成数字验证码字符串。 + + 参数: + length: 验证码长度,默认为 6。 + 返回: + 由数字组成的验证码字符串。 + """ + return ''.join(secrets.choice(string.digits) for _ in range(length)) + + +def hash_password(password: str, salt: bytes | None = None) -> Tuple[bytes, bytes]: + """对密码进行 PBKDF2 哈希。 + + 参数: + password: 原始密码。 + salt: 可选的盐值;若未提供则自动生成。 + 返回: + (salt, pwd_hash) 二元组,其中 salt 为随机盐,pwd_hash 为哈希值。 + """ + if salt is None: + salt = secrets.token_bytes(16) + pwd_hash = hashlib.pbkdf2_hmac( + 'sha256', password.encode('utf-8'), salt, 100_000 + ) + return salt, pwd_hash + + +def verify_password(password: str, salt: bytes, pwd_hash: bytes) -> bool: + """校验密码是否与存储的哈希匹配。 + + 参数: + password: 用户输入的密码。 + salt: 存储的盐值。 + pwd_hash: 存储的密码哈希。 + 返回: + True 表示匹配,False 表示不匹配。 + """ + calc_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100_000) + return secrets.compare_digest(calc_hash, pwd_hash) \ No newline at end of file diff --git a/~$工程导论-结对编程项目需求-2025.docx b/~$工程导论-结对编程项目需求-2025.docx new file mode 100644 index 0000000000000000000000000000000000000000..4718cf0dc124f980400c7436a4f12b3aba180f65 GIT binary patch literal 162 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 h^A|jW;NS57 Date: Sun, 12 Oct 2025 14:23:04 +0800 Subject: [PATCH 2/4] init --- ...程导论-结对编程项目需求-2025.docx | Bin ~$工程导论-结对编程项目需求-2025.docx | Bin 162 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) rename 软件工程导论-结对编程项目需求-2025.docx => doc/软件工程导论-结对编程项目需求-2025.docx (100%) delete mode 100644 ~$工程导论-结对编程项目需求-2025.docx diff --git a/软件工程导论-结对编程项目需求-2025.docx b/doc/软件工程导论-结对编程项目需求-2025.docx similarity index 100% rename from 软件工程导论-结对编程项目需求-2025.docx rename to doc/软件工程导论-结对编程项目需求-2025.docx diff --git a/~$工程导论-结对编程项目需求-2025.docx b/~$工程导论-结对编程项目需求-2025.docx deleted file mode 100644 index 4718cf0dc124f980400c7436a4f12b3aba180f65..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 162 ZcmZQcNzKV&AQiAMq%fp1 Date: Sun, 12 Oct 2025 16:20:29 +0800 Subject: [PATCH 3/4] 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@ Date: Sun, 12 Oct 2025 19:45:17 +0800 Subject: [PATCH 4/4] 1 --- doc/README.md | 0 ...导论-结对编程项目需求-2025.docx | Bin 0 -> 12250 bytes requirements.txt | 1 + src/__pycache__/app.cpython-311.pyc | Bin 0 -> 848 bytes src/app.py | 14 + src/services/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 199 bytes .../__pycache__/email_service.cpython-311.pyc | Bin 0 -> 4396 bytes .../question_service.cpython-311.pyc | Bin 0 -> 10953 bytes .../storage_service.cpython-311.pyc | Bin 0 -> 8088 bytes .../__pycache__/user_service.cpython-311.pyc | Bin 0 -> 10970 bytes src/services/email_service.py | 66 ++ src/services/question_service.py | 329 ++++++++++ src/services/storage_service.py | 97 +++ src/services/user_service.py | 174 ++++++ src/storage/config.json | 11 + src/storage/users.json | 15 + src/ui/__init__.py | 0 src/ui/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 193 bytes .../__pycache__/main_window.cpython-311.pyc | Bin 0 -> 37542 bytes src/ui/main_window.py | 585 ++++++++++++++++++ src/utils/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 196 bytes .../security_utils.cpython-311.pyc | Bin 0 -> 4720 bytes src/utils/security_utils.py | 79 +++ 25 files changed, 1371 insertions(+) create mode 100644 doc/README.md create mode 100644 doc/软件工程导论-结对编程项目需求-2025.docx create mode 100644 requirements.txt create mode 100644 src/__pycache__/app.cpython-311.pyc create mode 100644 src/app.py create mode 100644 src/services/__init__.py create mode 100644 src/services/__pycache__/__init__.cpython-311.pyc create mode 100644 src/services/__pycache__/email_service.cpython-311.pyc create mode 100644 src/services/__pycache__/question_service.cpython-311.pyc create mode 100644 src/services/__pycache__/storage_service.cpython-311.pyc create mode 100644 src/services/__pycache__/user_service.cpython-311.pyc create mode 100644 src/services/email_service.py create mode 100644 src/services/question_service.py create mode 100644 src/services/storage_service.py create mode 100644 src/services/user_service.py create mode 100644 src/storage/config.json create mode 100644 src/storage/users.json create mode 100644 src/ui/__init__.py create mode 100644 src/ui/__pycache__/__init__.cpython-311.pyc create mode 100644 src/ui/__pycache__/main_window.cpython-311.pyc create mode 100644 src/ui/main_window.py create mode 100644 src/utils/__init__.py create mode 100644 src/utils/__pycache__/__init__.cpython-311.pyc create mode 100644 src/utils/__pycache__/security_utils.cpython-311.pyc create mode 100644 src/utils/security_utils.py diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/软件工程导论-结对编程项目需求-2025.docx b/doc/软件工程导论-结对编程项目需求-2025.docx new file mode 100644 index 0000000000000000000000000000000000000000..8cf90012a8d38180444b8c945b92a3dee51f25f9 GIT binary patch literal 12250 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 h^A|jW;NS57YbC4p6JV~I3E zLoM-QXlv7i#?nHFA4&fOTY8X%oZ`BX9wJCDJ@xHY)7UpNZ{8bz@5k@G-S<|j8G#+$ z{yFc&2pwz5(5OMscR^4<9O7^oMX(QJEvCX$g!a)8k8y?!@e$s@86W92f`vjnDMQuH zkP88h%;<+Pb+uOp%NFmgcsv{m2EuR$o+(rL>13f+j>D;AE1RhY1n>F&|_onGVF`vT;ElTSDu~ZvS;3u6x&6){?q*dj?y9?+YW^A9`JC;1+HW6xQR^?2N>y#V0`+V{2(rNJ zh%d?{0^dcj0H*;?ViKH$6!Vq5@Xc)dVYW$kj;vRovpH6BwupIUIz63oXWWvzMaEX9 z(o-o{#wF>i50ywZ`TZK9-_KBjM(AOT^7w;AuQ=)OinAeZju#VpxAG+r;YfxYI{a^k z`UG?>8c#G3;r;#(NU#ErZ~@$J(~U@sONRNI!evOc{~-|o*P}7UEp)9px?6~8j_#h( mfX91i)Llfxl6~@>o@mk&En<@Hrkok4q^};0hz*`~34Z`~Hu%^8 literal 0 HcmV?d00001 diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..1aca5dc --- /dev/null +++ b/src/app.py @@ -0,0 +1,14 @@ +from PyQt6.QtWidgets import QApplication +from ui.main_window import MainWindow + + +def main() -> None: + """应用程序入口:创建 QApplication,显示主窗口,并进入事件循环。""" + app = QApplication([]) + window = MainWindow() + window.show() + app.exec() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/services/__init__.py b/src/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/services/__pycache__/__init__.cpython-311.pyc b/src/services/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2d809dd93e760132d8a9d513ac6fe0960562637 GIT binary patch literal 199 zcmZ3^%ge<81naF{XMpI(AOZ#$p^VRLK*n^26oz01O-8?!3`I;p{%4Tn*Ki==Y8CTz z?Xo937rbnk6_b)*o|luKm=g11@A@aZx4m4r^ZD#`&(|${KA|n<*`|(X8+JU|yZQO< z$xqkse7PQnaQceG4b)4d6^~g@p=W7zc_4i^HWN5QtgUZfwq7g VQ_K$}J}@&fGJary5k<^EF#tXfMF0Q* literal 0 HcmV?d00001 diff --git a/src/services/__pycache__/email_service.cpython-311.pyc b/src/services/__pycache__/email_service.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da4c53f1183da56b643eb9a17cb70320622e111e GIT binary patch literal 4396 zcmc&1ZEO=qcGkPL*B^;-VmnSQfLlItPTHhE;M9P)LxMENmrhEqD!J%Z#=A)!Id*P$ z%}3-&0a4CCTGAXP5j2OpgJ z<3uKULgoK{;AHy3jr8CNSKI#9hSt{oKrqf@`3K#EY~Iu3@%ayMg5c@mpiS-edBu4G zdZAI;Dh7F=w{rXupO^FELHVM~ArsOuqgaCq^UT(iFSxJ)yQFW?gyrq8Y{Y|fMQd9no*5J~_H zpeGe;BGjNc0?#ubAek0}j3Q8`yg`=pVrS(GUTwvBy|ClKd8j+zgQ9q|yi~@4a;-p( zE?$TNl_h+rc)$0PVR8jt4NETJr9^!}6k2f{?7`x3}8rIZ`L{r`h zgo;z808`g#O z#r_}=@R}Dm7O=2ssm5#tewL08nAV|9NT7~tj**AZQG!5+5CKo%M3L6`r&J2ox>$yy zG0oS2CZ&0<#r3QB2)t0V?-jfL?>=A+t!Nz**DP4G9`TST4f&A@;ayS8s3VHEJycbtSFms>SKV~>FbioE>4t5UqF%_r zBU89rrUlW%i{M1ru;-YU>lJ;$fV-5(KRUk(11p|gr>e%|69ezSpaKIY29+38VSwEu zlRZKg@4`(SiUjsYRDT~W4Po=Fk>E$oT`lr2ML zjtkX~tnPp&+rHZLlvfMeeHC0-5CNLO?kjcs&5i;`YGo{JO^>N1*cw@9$1B9ODGp3}i z8j#&*3`yE_-g4Fwcm95o1)#nOnvrNGPBU4m)KHnUII_rCQunxOwN%xZsM;n~Z5z@* zwpUB`4bjdRJM~W7zB^&xE!lTxk;Ym#LnUp_k-fuvM~}sv3EO7LwmELw475kK4G(P_ zz=O70$yOV;)n>K8u8dK@Q#|@P1`D+1uoo|0EKB~8Mvm1NI-(n6>l5|{$=;Afl(lZM zFm%e-}eQ`2%=l1M( zV{nsLa&=Hb$3Zu#1V$ z&>JOsV?6)z_=E;mH@JG>?87j!kzsm*Y{-vsGsFBLy+vr>y>ILc$c8GH8C4(er?Sput2pUP?f~`xsCkWSk0efun~Y~<^#dscD4PxKDeqdR$gMa8!l;T!CeMZEZZg6iVMZ&tUB0Ha?9PciNJG98Mt{{aR$ Bq1yle literal 0 HcmV?d00001 diff --git a/src/services/__pycache__/question_service.cpython-311.pyc b/src/services/__pycache__/question_service.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..10281f92b9db898217f401ea8e91ec3577ccdfe5 GIT binary patch literal 10953 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 literal 0 HcmV?d00001 diff --git a/src/services/__pycache__/storage_service.cpython-311.pyc b/src/services/__pycache__/storage_service.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99a793ee465a04a8ce3f1ec2c2faafe8dcfdc78d GIT binary patch literal 8088 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 X&F;`@UYKTPr++8y|H#lw?)1L_=@dh5 literal 0 HcmV?d00001 diff --git a/src/services/email_service.py b/src/services/email_service.py new file mode 100644 index 0000000..2d05d0c --- /dev/null +++ b/src/services/email_service.py @@ -0,0 +1,66 @@ +""" +邮件服务模块:基于 SMTP 发送验证码邮件。支持 TLS/SSL。 +""" +import smtplib +import ssl +from email.message import EmailMessage +from typing import Dict + +from .storage_service import StorageService + + +class EmailService: + """负责发送邮件验证码的服务类。""" + + def __init__(self, storage: StorageService) -> None: + """初始化邮件服务,依赖存储服务获取 SMTP 配置。""" + self.storage = storage + + def send_verification_code(self, to_email: str, code: str) -> bool: + """发送验证码到指定邮箱。 + + 参数: + to_email: 收件人邮箱。 + code: 验证码字符串。 + 返回: + True 表示发送成功,False 表示失败。 + """ + config = self.storage.load_config().get('smtp', {}) + server = config.get('server', '') + port = int(config.get('port', 587)) + username = config.get('username', '') + password = config.get('password', '') + use_tls = bool(config.get('use_tls', True)) + use_ssl = bool(config.get('use_ssl', False)) + sender_name = config.get('sender_name', 'Math Study App') + + if not server or not username or not password: + return False + + msg = EmailMessage() + msg['Subject'] = '数学学习软件注册验证码' + msg['From'] = f"{sender_name} <{username}>" + msg['To'] = to_email + msg.set_content(f"您的注册验证码为:{code}\n该验证码10分钟内有效。") + + try: + if use_ssl: + context = ssl.create_default_context() + with smtplib.SMTP_SSL(server, port, context=context) as smtp: + smtp.login(username, password) + smtp.send_message(msg) + else: + with smtplib.SMTP(server, port) as smtp: + if use_tls: + smtp.starttls(context=ssl.create_default_context()) + smtp.login(username, password) + smtp.send_message(msg) + return True + except Exception: + return False + + def update_smtp_config(self, config: Dict) -> None: + """更新并保存 SMTP 配置。""" + data = self.storage.load_config() + data['smtp'] = config + self.storage.save_config(data) \ No newline at end of file diff --git a/src/services/question_service.py b/src/services/question_service.py new file mode 100644 index 0000000..a3272cf --- /dev/null +++ b/src/services/question_service.py @@ -0,0 +1,329 @@ +""" +试题服务模块:按小学/初中/高中生成选择题试卷,并保证同一试卷题目不重复。 +""" +import random +import math +from typing import List, Dict, Callable, Tuple, Set, Optional, Union +from enum import Enum + + +class Level(Enum): + """题目难度级别枚举""" + PRIMARY = 'primary' + MIDDLE = 'middle' + HIGH = 'high' + + +class QuestionService: + """负责生成不同年级难度的选择题。""" + + # 特殊角度和完全平方数常量 + SPECIAL_ANGLES = [0, 30, 45, 60, 90, 120, 135, 150, 180] + PERFECT_SQUARES = [4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225] + + def __init__(self) -> None: + """初始化题目服务。""" + random.seed() + # 映射难度级别到对应的生成函数 + self._generators = { + Level.PRIMARY.value: self._gen_primary, + Level.MIDDLE.value: self._gen_middle, + Level.HIGH.value: self._gen_high, + } + + def generate_questions(self, level: str, count: int) -> List[Dict]: + """生成指定年级与数量的题目列表。 + + 参数: + level: 'primary'|'middle'|'high' 三选一。 + count: 题目数量。 + 返回: + 题目字典列表,每个字典包含 stem, options, answer_index。 + """ + if count <= 0: + raise ValueError('题目数量必须为正整数') + + gen = self._generators.get(level) + if not gen: + raise ValueError(f'无效的年级选项: {level},可选值为 primary, middle, high') + + seen: Set[str] = set() + questions: List[Dict] = [] + + # 使用集合去重,避免重复题目 + while len(questions) < count: + q = gen() + if q['stem'] in seen: + continue + seen.add(q['stem']) + questions.append(q) + + return questions + + def _gen_primary(self) -> Dict: + """生成一题小学难度的四则运算选择题(可带括号)。""" + # 随机选择运算类型:简单四则运算或带括号的复合运算 + if random.choice([True, False]): + stem, ans = self._gen_simple_arithmetic() + else: + stem, ans = self._gen_bracket_arithmetic() + + options = self._make_options(ans) + return {'stem': stem, 'options': options, 'answer_index': options.index(str(ans))} + + def _gen_simple_arithmetic(self) -> Tuple[str, int]: + """生成简单四则运算题目""" + 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} = ?" + + return stem, ans + + def _gen_bracket_arithmetic(self) -> Tuple[str, int]: + """生成带括号的复合运算题目""" + 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}) = ?" + + return stem, ans + + def _gen_middle(self) -> Dict: + """生成一题初中难度的题目,至少包含一个平方或开根号运算。""" + # 随机选择题目类型:平方运算、开根号运算或混合运算 + question_type = random.choice(['square', 'sqrt', 'mixed']) + + if question_type == 'square': + stem, ans = self._gen_square_question() + elif question_type == 'sqrt': + stem, ans = self._gen_sqrt_question() + else: # mixed + stem, ans = self._gen_mixed_middle_question() + + options = self._make_options(ans) + return {'stem': stem, 'options': options, 'answer_index': options.index(str(ans))} + + def _gen_square_question(self) -> Tuple[str, int]: + """生成平方运算题目""" + 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}² = ?" + + return stem, ans + + def _gen_sqrt_question(self) -> Tuple[str, int]: + """生成开根号运算题目""" + # 选择完全平方数确保结果为整数 + a = random.choice(self.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} = ?" + + return stem, ans + + def _gen_mixed_middle_question(self) -> Tuple[str, int]: + """生成混合运算:既有平方又有开根号""" + perfect_square = random.choice(self.PERFECT_SQUARES[:9]) # 使用较小的完全平方数 + a = random.randint(2, 8) + + # √perfect_square + a² + ans = int(math.sqrt(perfect_square)) + a ** 2 + stem = f"计算:√{perfect_square} + {a}² = ?" + + return stem, ans + + def _gen_high(self) -> Dict: + """生成一题高中难度的题目,至少包含一个sin、cos或tan运算符。""" + # 随机选择题目类型:基础三角函数、三角函数运算或复合运算 + question_type = random.choice(['basic_trig', 'trig_calc', 'mixed_trig']) + + if question_type == 'basic_trig': + stem, ans = self._gen_basic_trig_question() + elif question_type == 'trig_calc': + stem, ans = self._gen_trig_calc_question() + else: # mixed_trig + stem, ans = self._gen_mixed_trig_question() + + options = self._make_options(ans, float_mode=True) + correct = f"{ans:.2f}" + return {'stem': stem, 'options': options, 'answer_index': options.index(correct)} + + def _gen_basic_trig_question(self) -> Tuple[str, float]: + """生成基础三角函数值计算题目""" + # 使用特殊角度确保结果为常见值 + angle = random.choice(self.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}°) = ?(保留两位小数)" + + return stem, ans + + def _gen_trig_calc_question(self) -> Tuple[str, float]: + """生成三角函数运算题目""" + 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) + + val1 = math.sin(rad1) if func1 == 'sin' else math.cos(rad1) + val2 = math.sin(rad2) if func2 == 'sin' else 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}°) = ?(保留两位小数)" + + return stem, ans + + def _gen_mixed_trig_question(self) -> Tuple[str, float]: + """生成混合运算:三角函数与代数运算""" + 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} = ?(保留两位小数)" + + return stem, ans + + def _make_options(self, answer: Union[int, float], float_mode: bool = False) -> List[str]: + """根据正确答案生成 4 个选项(包含正确答案与3个干扰项)。 + + 参数: + answer: 正确答案 + float_mode: 是否为浮点数模式 + 返回: + 包含4个选项的列表 + """ + if float_mode: + return self._make_float_options(answer) + else: + return self._make_int_options(answer) + + def _make_float_options(self, answer: float) -> List[str]: + """生成浮点数选项""" + opts: Set[str] = set() + correct = f"{answer:.2f}" + opts.add(correct) + + # 生成干扰项,确保不重复 + attempts = 0 + while len(opts) < 4 and attempts < 20: + delta = random.uniform(-5, 5) + # 确保干扰项与正确答案有一定差距 + if abs(delta) < 0.1: + continue + opts.add(f"{answer + delta:.2f}") + attempts += 1 + + # 如果生成的选项不足4个,添加固定偏移的选项 + if len(opts) < 4: + for delta in [0.5, -0.5, 1.0, -1.0]: + if len(opts) >= 4: + break + opts.add(f"{answer + delta:.2f}") + + options = list(opts) + random.shuffle(options) + return options + + def _make_int_options(self, answer: int) -> List[str]: + """生成整数选项""" + opts: Set[str] = set() + correct = str(answer) + opts.add(correct) + + # 生成干扰项,确保不重复且有一定差距 + attempts = 0 + while len(opts) < 4 and attempts < 20: + delta = random.randint(-10, 10) + # 确保干扰项与正确答案有一定差距 + if delta == 0 or abs(delta) < 2: + continue + opts.add(str(answer + delta)) + attempts += 1 + + # 如果生成的选项不足4个,添加固定偏移的选项 + if len(opts) < 4: + for delta in [2, -2, 5, -5]: + if len(opts) >= 4: + break + opts.add(str(answer + delta)) + + options = list(opts) + random.shuffle(options) + return options \ No newline at end of file diff --git a/src/services/storage_service.py b/src/services/storage_service.py new file mode 100644 index 0000000..5755c20 --- /dev/null +++ b/src/services/storage_service.py @@ -0,0 +1,97 @@ +""" +存储服务模块:使用 JSON 文件持久化数据(不使用数据库)。 +提供用户数据与配置数据的读写接口。 +""" +import json +import os +from typing import Dict, Any, List + + +class StorageService: + """使用 JSON 文件进行数据持久化的服务类。""" + + def __init__(self) -> None: + """初始化存储路径并确保必要文件存在。""" + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.storage_dir = os.path.join(base_dir, 'storage') + self.users_file = os.path.join(self.storage_dir, 'users.json') + self.config_file = os.path.join(self.storage_dir, 'config.json') + self._ensure_files() + + def _ensure_files(self) -> None: + """确保存储目录与文件存在,如不存在则创建。""" + os.makedirs(self.storage_dir, exist_ok=True) + if not os.path.exists(self.users_file): + with open(self.users_file, 'w', encoding='utf-8') as f: + json.dump({'users': []}, f, ensure_ascii=False, indent=2) + if not os.path.exists(self.config_file): + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump({ + 'smtp': { + 'server': '', + 'port': 587, + 'username': '', + 'password': '', + 'use_tls': True, + 'use_ssl': False, + 'sender_name': 'Math Study App' + } + }, f, ensure_ascii=False, indent=2) + + def load_users(self) -> Dict[str, List[Dict[str, Any]]]: + """读取用户列表数据。返回字典 {'users': [...]}。""" + with open(self.users_file, 'r', encoding='utf-8') as f: + return json.load(f) + + def save_users(self, data: Dict[str, List[Dict[str, Any]]]) -> None: + """写入用户列表数据到文件。""" + with open(self.users_file, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + def load_config(self) -> Dict[str, Any]: + """读取配置数据(包含 SMTP 配置)。""" + with open(self.config_file, 'r', encoding='utf-8') as f: + return json.load(f) + + def save_config(self, data: Dict[str, Any]) -> None: + """写入配置数据到文件。""" + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + def get_user(self, email: str) -> Dict[str, Any] | None: + """根据邮箱获取用户字典,不存在返回 None。""" + users = self.load_users().get('users', []) + for u in users: + if u.get('email') == email: + 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() + users = data.get('users', []) + found = False + for i, u in enumerate(users): + if u.get('email') == user.get('email'): + users[i] = user + found = True + break + if not found: + users.append(user) + self.save_users({'users': users}) + + def user_exists(self, email: str) -> bool: + """判断指定邮箱的用户是否存在。""" + return self.get_user(email) is not None \ No newline at end of file diff --git a/src/services/user_service.py b/src/services/user_service.py new file mode 100644 index 0000000..2070105 --- /dev/null +++ b/src/services/user_service.py @@ -0,0 +1,174 @@ +""" +用户服务模块:处理注册、验证码、用户名设置、登录与修改密码逻辑。 +不依赖真实邮件与SMTP,仅进行邮箱格式校验与本地验证码生成/校验。 +""" +import time +from typing import Any, Dict + +from utils.security_utils import ( + validate_email, + validate_password_strength, + generate_verification_code, + hash_password, + verify_password, +) +from .storage_service import StorageService +# 删除 EmailService 依赖 + + +class UserService: + """封装用户相关业务逻辑的服务类。""" + + def __init__(self, storage: StorageService) -> None: + """初始化用户服务,仅注入存储服务。""" + self.storage = storage + + def request_registration(self, email: str) -> tuple[bool, str]: + """发起注册请求:校验邮箱,生成验证码并本地保存(不发送邮件)。 + + 参数: + email: 用户邮箱。 + 返回: + (success, message) 二元组,message包含提示与模拟验证码信息。 + """ + if not validate_email(email): + return False, '邮箱格式不正确' + existing = self.storage.get_user(email) + if existing and existing.get('verified'): + return False, '该邮箱已注册' + code = generate_verification_code() + salt, code_hash = hash_password(code) + user = existing or {'email': email} + user.update({ + 'verified': False, + 'code_salt': salt.hex(), + 'code_hash': code_hash.hex(), + 'code_time': int(time.time()), + 'password_salt': '', + 'password_hash': '', + 'username': user.get('username', ''), + 'created_at': user.get('created_at') or int(time.time()), + }) + self.storage.upsert_user(user) + # 不发送邮件,直接提示验证码供用户在下一步手动输入 + return True, f'验证码已生成(模拟):{code},请前往验证码页面手动输入' + + def verify_code(self, email: str, code: str) -> tuple[bool, str]: + """校验验证码并标记邮箱已验证。 + + 参数: + email: 用户邮箱。 + code: 用户输入的验证码。 + 返回: + (success, message)。 + """ + user = self.storage.get_user(email) + if not user: + return False, '用户不存在,请先注册' + if int(time.time()) - int(user.get('code_time', 0)) > 600: + return False, '验证码已过期,请重新获取' + salt_hex = user.get('code_salt', '') + hash_hex = user.get('code_hash', '') + if not salt_hex or not hash_hex: + return False, '未找到验证码信息' + salt = bytes.fromhex(salt_hex) + hash_bytes = bytes.fromhex(hash_hex) + if not verify_password(code, salt, hash_bytes): + return False, '验证码不正确' + user['verified'] = True + # 清除验证码信息 + user['code_salt'] = '' + user['code_hash'] = '' + self.storage.upsert_user(user) + 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]: + """设置或重置密码:校验强度、确认一致并持久化哈希。""" + if password != confirm: + return False, '两次输入的密码不一致' + if not validate_password_strength(password): + return False, '密码需为6-10位且包含大小写字母与数字' + user = self.storage.get_user(email) + if not user or not user.get('verified'): + return False, '邮箱未验证或用户不存在' + salt, pwd_hash = hash_password(password) + user['password_salt'] = salt.hex() + user['password_hash'] = pwd_hash.hex() + 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, '用户不存在或未设置密码' + 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 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) + if not user: + return False, '用户不存在' + salt_hex = user.get('password_salt', '') + hash_hex = user.get('password_hash', '') + if not verify_password(old_password, bytes.fromhex(salt_hex), bytes.fromhex(hash_hex)): + return False, '原密码不正确' + return self.set_password(email, new_password, confirm) \ No newline at end of file diff --git a/src/storage/config.json b/src/storage/config.json new file mode 100644 index 0000000..7f5d8ac --- /dev/null +++ b/src/storage/config.json @@ -0,0 +1,11 @@ +{ + "smtp": { + "server": "", + "port": 587, + "username": "", + "password": "", + "use_tls": true, + "use_ssl": false, + "sender_name": "Math Study App" + } +} \ No newline at end of file diff --git a/src/storage/users.json b/src/storage/users.json new file mode 100644 index 0000000..318369b --- /dev/null +++ b/src/storage/users.json @@ -0,0 +1,15 @@ +{ + "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/__init__.py b/src/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/__pycache__/__init__.cpython-311.pyc b/src/ui/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8689912d783718686ab4af9208922d2018f4c2a GIT binary patch literal 193 zcmZ3^%ge<81Z%8bXMpI(AOZ#$p^VRLK*n^26oz01O-8?!3`I;p{%4Tn*FYfQY8CTz z?Xo937rbnk6_b)*o|luKm=g11@A@aZx4m4r^ZD#`&(|${KA|n<*`|(X8+JU|yZQO< z$xqkse7gPc*!4!VO&CS literal 0 HcmV?d00001 diff --git a/src/ui/__pycache__/main_window.cpython-311.pyc b/src/ui/__pycache__/main_window.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68aae03bbed1b6ac84262b649e9c0b533e3d069d GIT binary patch literal 37542 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 U81(4jWYMo#%)iEnXHUZa2mZ9PumAu6 literal 0 HcmV?d00001 diff --git a/src/ui/main_window.py b/src/ui/main_window.py new file mode 100644 index 0000000..22196b7 --- /dev/null +++ b/src/ui/main_window.py @@ -0,0 +1,585 @@ +from PyQt6.QtWidgets import ( + QMainWindow, QWidget, QStackedWidget, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QPushButton, QMessageBox, QRadioButton, QButtonGroup, QSpinBox +) +from PyQt6.QtCore import Qt + +from services.storage_service import StorageService +from services.user_service import UserService +from services.question_service import QuestionService + + +class MainWindow(QMainWindow): + """应用主窗口:组织各个页面并承载业务服务。""" + + def __init__(self) -> None: + """初始化主窗口并构建基础页面。""" + super().__init__() + self.setWindowTitle("数学学习软件") + self.setFixedSize(900, 600) + + # 业务服务初始化 + self.storage_service = StorageService() + self.session_email = None + self.user_service = UserService(self.storage_service) + self.question_service = QuestionService() + + # 页面容器 + self.stack = QStackedWidget() + self.setCentralWidget(self.stack) + + # 构建页面:登录、注册(邮箱/验证码/用户名密码)、选择、答题、结果、修改密码 + 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) + + title = QLabel("登录") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") + layout.addWidget(title) + + self.login_identifier = QLineEdit() + self.login_identifier.setPlaceholderText("请输入邮箱或用户名") + layout.addWidget(self.login_identifier) + + self.login_password = QLineEdit() + self.login_password.setPlaceholderText("请输入密码") + self.login_password.setEchoMode(QLineEdit.EchoMode.Password) + layout.addWidget(self.login_password) + + btn_row = QHBoxLayout() + self.btn_login = QPushButton("登录") + self.btn_to_register = QPushButton("去注册") + btn_row.addWidget(self.btn_login) + btn_row.addWidget(self.btn_to_register) + layout.addLayout(btn_row) + + self.btn_login.clicked.connect(self._on_login) + self.btn_to_register.clicked.connect(lambda: self.stack.setCurrentIndex(1)) + + self.stack.addWidget(page) + + def _on_login(self) -> None: + """处理登录逻辑:根据输入自动识别邮箱或用户名进行登录,并在成功后进入选择页。""" + identifier = (self.login_identifier.text() or "").strip() + password = self.login_password.text() or "" + if not identifier or not password: + QMessageBox.warning(self, "提示", "账号与密码均不能为空") + return + # 判断是否为邮箱 + 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: + self.session_email = email + QMessageBox.information(self, "提示", msg) + # 跳转到验证码页面 + self.stack.setCurrentIndex(2) + else: + QMessageBox.warning(self, "提示", msg) + + def _build_verify_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.verify_code = QLineEdit() + self.verify_code.setPlaceholderText("请输入6位验证码") + layout.addWidget(self.verify_code) + + btn_row = QHBoxLayout() + 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_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_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: + 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 + + 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/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/__pycache__/__init__.cpython-311.pyc b/src/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98fa03055daae7c8aa7b32ab7ac46094bf1597b7 GIT binary patch literal 196 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 bool: + """验证邮箱格式是否正确。 + + 参数: + email: 待验证的邮箱字符串。 + 返回: + True 表示格式合法,False 表示格式不合法。 + """ + pattern = r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$" + return re.match(pattern, email) is not None + + +def validate_password_strength(password: str) -> bool: + """验证密码是否满足强度要求:6-10位,必须包含大小写字母和数字。 + + 参数: + password: 待验证的密码字符串。 + 返回: + True 表示满足要求,False 表示不满足。 + """ + if not (6 <= 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 generate_verification_code(length: int = 6) -> str: + """生成数字验证码字符串。 + + 参数: + length: 验证码长度,默认为 6。 + 返回: + 由数字组成的验证码字符串。 + """ + return ''.join(secrets.choice(string.digits) for _ in range(length)) + + +def hash_password(password: str, salt: bytes | None = None) -> Tuple[bytes, bytes]: + """对密码进行 PBKDF2 哈希。 + + 参数: + password: 原始密码。 + salt: 可选的盐值;若未提供则自动生成。 + 返回: + (salt, pwd_hash) 二元组,其中 salt 为随机盐,pwd_hash 为哈希值。 + """ + if salt is None: + salt = secrets.token_bytes(16) + pwd_hash = hashlib.pbkdf2_hmac( + 'sha256', password.encode('utf-8'), salt, 100_000 + ) + return salt, pwd_hash + + +def verify_password(password: str, salt: bytes, pwd_hash: bytes) -> bool: + """校验密码是否与存储的哈希匹配。 + + 参数: + password: 用户输入的密码。 + salt: 存储的盐值。 + pwd_hash: 存储的密码哈希。 + 返回: + True 表示匹配,False 表示不匹配。 + """ + calc_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100_000) + return secrets.compare_digest(calc_hash, pwd_hash) \ No newline at end of file -- 2.34.1