测试1版 #5

Merged
hnu202326010101 merged 1 commits from shenyongjia_branch into develop 3 months ago

@ -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:

@ -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} = 0x = ?"
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()

@ -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()

@ -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)

@ -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
}
]
}

@ -1,13 +1,12 @@
from PyQt6.QtWidgets import (
QMainWindow, QWidget, QStackedWidget, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QMessageBox, QCheckBox
QLabel, QLineEdit, QPushButton, QMessageBox, QRadioButton, QButtonGroup, QSpinBox
)
from PyQt6.QtCore import Qt
from src.services.storage_service import StorageService
from src.services.email_service import EmailService
from src.services.user_service import UserService
from src.services.question_service import QuestionService
from services.storage_service import StorageService
from services.user_service import UserService
from services.question_service import QuestionService
class MainWindow(QMainWindow):
@ -21,23 +20,29 @@ class MainWindow(QMainWindow):
# 业务服务初始化
self.storage_service = StorageService()
self.email_service = EmailService(self.storage_service)
self.user_service = UserService(self.storage_service, self.email_service)
self.session_email = None
self.user_service = UserService(self.storage_service)
self.question_service = QuestionService()
# 页面容器
self.stack = QStackedWidget()
self.setCentralWidget(self.stack)
# 先构建最基础的两个页面登录与SMTP设置
self._build_login_page()
self._build_smtp_page()
# 构建页面:登录、注册(邮箱/验证码/用户名密码)、选择、答题、结果、修改密码
self._build_login_page() # index 0
self._build_register_email_page() # index 1
self._build_verify_page() # index 2
self._build_set_credentials_page() # index 3
self._build_choice_page() # index 4
self._build_quiz_page() # index 5
self._build_result_page() # index 6
self._build_change_password_page() # index 7
# 默认显示登录页
self.stack.setCurrentIndex(0)
def _build_login_page(self) -> None:
"""构建登录页面:邮箱+密码+登录按钮。"""
"""构建登录页面:支持用户名或邮箱登录。"""
page = QWidget()
layout = QVBoxLayout(page)
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
@ -47,9 +52,9 @@ class MainWindow(QMainWindow):
title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;")
layout.addWidget(title)
self.login_email = QLineEdit()
self.login_email.setPlaceholderText("请输入邮箱")
layout.addWidget(self.login_email)
self.login_identifier = QLineEdit()
self.login_identifier.setPlaceholderText("请输入邮箱或用户名")
layout.addWidget(self.login_identifier)
self.login_password = QLineEdit()
self.login_password.setPlaceholderText("请输入密码")
@ -58,84 +63,523 @@ class MainWindow(QMainWindow):
btn_row = QHBoxLayout()
self.btn_login = QPushButton("登录")
self.btn_to_smtp = QPushButton("SMTP设置")
self.btn_to_register = QPushButton("去注册")
btn_row.addWidget(self.btn_login)
btn_row.addWidget(self.btn_to_smtp)
btn_row.addWidget(self.btn_to_register)
layout.addLayout(btn_row)
self.btn_login.clicked.connect(self._on_login)
self.btn_to_smtp.clicked.connect(lambda: self.stack.setCurrentIndex(1))
self.btn_to_register.clicked.connect(lambda: self.stack.setCurrentIndex(1))
self.stack.addWidget(page)
def _on_login(self) -> None:
"""处理登录逻辑:校验输入并调用用户服务"""
email = (self.login_email.text() or "").strip()
"""处理登录逻辑:根据输入自动识别邮箱或用户名进行登录,并在成功后进入选择页"""
identifier = (self.login_identifier.text() or "").strip()
password = self.login_password.text() or ""
if not email or not password:
QMessageBox.warning(self, "提示", "邮箱与密码均不能为空")
if not identifier or not password:
QMessageBox.warning(self, "提示", "账号与密码均不能为空")
return
ok, msg = self.user_service.login(email, password)
# 判断是否为邮箱
if "@" in identifier:
ok, msg = self.user_service.login(identifier, password)
if ok:
self.session_email = identifier
else:
QMessageBox.warning(self, "提示", msg or "登录失败")
return
else:
ok, msg = self.user_service.login_by_username(identifier, password)
if ok:
# 通过用户名获取邮箱,记录会话
user = self.storage_service.get_user_by_username(identifier)
self.session_email = user.get('email') if user else None
else:
QMessageBox.warning(self, "提示", msg or "登录失败")
return
QMessageBox.information(self, "提示", "登录成功")
# 跳转到选择页
self.stack.setCurrentIndex(4)
def _build_register_email_page(self) -> None:
"""构建注册第一步:邮箱输入页面。"""
page = QWidget()
layout = QVBoxLayout(page)
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
title = QLabel("注册 - 邮箱验证")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;")
layout.addWidget(title)
self.register_email = QLineEdit()
self.register_email.setPlaceholderText("请输入邮箱地址")
layout.addWidget(self.register_email)
btn_row = QHBoxLayout()
self.btn_send_code = QPushButton("获取验证码")
self.btn_back_to_login = QPushButton("返回登录")
btn_row.addWidget(self.btn_send_code)
btn_row.addWidget(self.btn_back_to_login)
layout.addLayout(btn_row)
self.btn_send_code.clicked.connect(self._on_send_code)
self.btn_back_to_login.clicked.connect(lambda: self.stack.setCurrentIndex(0))
self.stack.addWidget(page)
def _on_send_code(self) -> None:
"""处理发送验证码逻辑:验证邮箱格式并生成验证码。"""
email = (self.register_email.text() or "").strip()
if not email:
QMessageBox.warning(self, "提示", "请输入邮箱地址")
return
ok, msg = self.user_service.request_registration(email)
if ok:
QMessageBox.information(self, "提示", "登录成功")
self.session_email = email
QMessageBox.information(self, "提示", msg)
# 跳转到验证码页面
self.stack.setCurrentIndex(2)
else:
QMessageBox.warning(self, "提示", msg or "登录失败")
QMessageBox.warning(self, "提示", msg)
def _build_smtp_page(self) -> None:
"""构建 SMTP 设置页面服务器、端口、账号、授权码、TLS/SSL、发件人名。"""
def _build_verify_page(self) -> None:
"""构建注册第二步:验证码验证页面"""
page = QWidget()
layout = QVBoxLayout(page)
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
title = QLabel("SMTP 设置")
title = QLabel("注册 - 验证码验证")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;")
layout.addWidget(title)
self.smtp_server = QLineEdit(); self.smtp_server.setPlaceholderText("服务器,例如 smtp.qq.com")
self.smtp_port = QLineEdit(); self.smtp_port.setPlaceholderText("端口,通常 465 或 587")
self.smtp_user = QLineEdit(); self.smtp_user.setPlaceholderText("邮箱账号,例如 123456@qq.com")
self.smtp_pass = QLineEdit(); self.smtp_pass.setPlaceholderText("邮箱授权码/应用密码")
self.smtp_pass.setEchoMode(QLineEdit.EchoMode.Password)
self.smtp_tls = QCheckBox("启用 TLS")
self.smtp_ssl = QCheckBox("启用 SSL")
self.smtp_sender = QLineEdit(); self.smtp_sender.setPlaceholderText("发件人显示名称,可选")
layout.addWidget(self.smtp_server)
layout.addWidget(self.smtp_port)
layout.addWidget(self.smtp_user)
layout.addWidget(self.smtp_pass)
layout.addWidget(self.smtp_tls)
layout.addWidget(self.smtp_ssl)
layout.addWidget(self.smtp_sender)
self.verify_code = QLineEdit()
self.verify_code.setPlaceholderText("请输入6位验证码")
layout.addWidget(self.verify_code)
btn_row = QHBoxLayout()
self.btn_save_smtp = QPushButton("保存设置")
self.btn_back_login = QPushButton("返回登录")
btn_row.addWidget(self.btn_save_smtp)
btn_row.addWidget(self.btn_back_login)
self.btn_verify = QPushButton("验证")
self.btn_back_to_email = QPushButton("返回上一步")
btn_row.addWidget(self.btn_verify)
btn_row.addWidget(self.btn_back_to_email)
layout.addLayout(btn_row)
self.btn_save_smtp.clicked.connect(self._on_save_smtp)
self.btn_back_login.clicked.connect(lambda: self.stack.setCurrentIndex(0))
self.btn_verify.clicked.connect(self._on_verify_code)
self.btn_back_to_email.clicked.connect(lambda: self.stack.setCurrentIndex(1))
self.stack.addWidget(page)
def _on_save_smtp(self) -> None:
"""保存 SMTP 配置到本地 JSON以支持验证码发送。"""
def _on_verify_code(self) -> None:
"""处理验证码验证逻辑。"""
code = (self.verify_code.text() or "").strip()
if not code:
QMessageBox.warning(self, "提示", "请输入验证码")
return
if not self.session_email:
QMessageBox.warning(self, "提示", "会话已过期,请重新注册")
self.stack.setCurrentIndex(1)
return
ok, msg = self.user_service.verify_code(self.session_email, code)
if ok:
QMessageBox.information(self, "提示", msg)
# 跳转到设置用户名密码页面
self.stack.setCurrentIndex(3)
else:
QMessageBox.warning(self, "提示", msg)
def _build_set_credentials_page(self) -> None:
"""构建注册第三步:设置用户名和密码页面。"""
page = QWidget()
layout = QVBoxLayout(page)
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
title = QLabel("注册 - 设置用户名和密码")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;")
layout.addWidget(title)
self.register_username = QLineEdit()
self.register_username.setPlaceholderText("请输入用户名3-16位字母数字下划线")
layout.addWidget(self.register_username)
self.register_password = QLineEdit()
self.register_password.setPlaceholderText("请输入密码6-10位包含大小写字母和数字")
self.register_password.setEchoMode(QLineEdit.EchoMode.Password)
layout.addWidget(self.register_password)
self.register_confirm = QLineEdit()
self.register_confirm.setPlaceholderText("请确认密码")
self.register_confirm.setEchoMode(QLineEdit.EchoMode.Password)
layout.addWidget(self.register_confirm)
btn_row = QHBoxLayout()
self.btn_complete_register = QPushButton("完成注册")
self.btn_back_to_verify = QPushButton("返回上一步")
btn_row.addWidget(self.btn_complete_register)
btn_row.addWidget(self.btn_back_to_verify)
layout.addLayout(btn_row)
self.btn_complete_register.clicked.connect(self._on_complete_register)
self.btn_back_to_verify.clicked.connect(lambda: self.stack.setCurrentIndex(2))
self.stack.addWidget(page)
def _on_complete_register(self) -> None:
"""处理完成注册逻辑:设置用户名和密码。"""
username = (self.register_username.text() or "").strip()
password = self.register_password.text() or ""
confirm = self.register_confirm.text() or ""
if not username or not password or not confirm:
QMessageBox.warning(self, "提示", "请填写完整信息")
return
if not self.session_email:
QMessageBox.warning(self, "提示", "会话已过期,请重新注册")
self.stack.setCurrentIndex(1)
return
# 设置用户名
ok, msg = self.user_service.set_username(self.session_email, username)
if not ok:
QMessageBox.warning(self, "提示", msg)
return
# 设置密码
ok, msg = self.user_service.set_password(self.session_email, password, confirm)
if not ok:
QMessageBox.warning(self, "提示", msg)
return
# 完成注册
ok, msg = self.user_service.complete_registration(self.session_email)
if ok:
QMessageBox.information(self, "提示", "注册成功!请登录")
# 清空表单并跳转到登录页
self.register_email.clear()
self.verify_code.clear()
self.register_username.clear()
self.register_password.clear()
self.register_confirm.clear()
self.session_email = None
self.stack.setCurrentIndex(0)
else:
QMessageBox.warning(self, "提示", msg)
def _build_choice_page(self) -> None:
"""构建选择页面:选择年级和题目数量。"""
page = QWidget()
layout = QVBoxLayout(page)
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
title = QLabel("选择题目难度和数量")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;")
layout.addWidget(title)
# 年级选择
grade_label = QLabel("选择年级:")
layout.addWidget(grade_label)
self.grade_group = QButtonGroup()
grade_layout = QHBoxLayout()
self.primary_radio = QRadioButton("小学")
self.middle_radio = QRadioButton("初中")
self.high_radio = QRadioButton("高中")
self.grade_group.addButton(self.primary_radio, 0)
self.grade_group.addButton(self.middle_radio, 1)
self.grade_group.addButton(self.high_radio, 2)
grade_layout.addWidget(self.primary_radio)
grade_layout.addWidget(self.middle_radio)
grade_layout.addWidget(self.high_radio)
layout.addLayout(grade_layout)
# 默认选择小学
self.primary_radio.setChecked(True)
# 题目数量选择
count_label = QLabel("选择题目数量:")
layout.addWidget(count_label)
self.question_count = QSpinBox()
self.question_count.setMinimum(10)
self.question_count.setMaximum(30)
self.question_count.setValue(15)
layout.addWidget(self.question_count)
btn_row = QHBoxLayout()
self.btn_start_quiz = QPushButton("开始答题")
self.btn_change_password = QPushButton("修改密码")
self.btn_logout = QPushButton("退出登录")
btn_row.addWidget(self.btn_start_quiz)
btn_row.addWidget(self.btn_change_password)
btn_row.addWidget(self.btn_logout)
layout.addLayout(btn_row)
self.btn_start_quiz.clicked.connect(self._on_start_quiz)
self.btn_change_password.clicked.connect(lambda: self.stack.setCurrentIndex(7))
self.btn_logout.clicked.connect(self._on_logout)
self.stack.addWidget(page)
def _on_start_quiz(self) -> None:
"""开始答题:生成题目并跳转到答题页面。"""
# 获取选择的年级
grade_map = {0: 'primary', 1: 'middle', 2: 'high'}
grade = grade_map[self.grade_group.checkedId()]
count = self.question_count.value()
try:
port_val = int(self.smtp_port.text().strip() or '587')
except ValueError:
QMessageBox.warning(self, '提示', '端口必须为数字')
self.questions = self.question_service.generate_questions(grade, count)
self.current_question = 0
self.user_answers = []
self._show_current_question()
self.stack.setCurrentIndex(5)
except Exception as e:
QMessageBox.warning(self, "提示", f"生成题目失败:{str(e)}")
def _on_logout(self) -> None:
"""退出登录:清空会话并返回登录页。"""
self.session_email = None
self.login_identifier.clear()
self.login_password.clear()
QMessageBox.information(self, "提示", "已退出登录")
self.stack.setCurrentIndex(0)
def _build_quiz_page(self) -> None:
"""构建答题页面:显示题目和选项。"""
page = QWidget()
layout = QVBoxLayout(page)
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# 进度显示
self.progress_label = QLabel()
self.progress_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.progress_label.setStyleSheet("font-size: 16px; margin: 10px 0;")
layout.addWidget(self.progress_label)
# 题目显示
self.question_label = QLabel()
self.question_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.question_label.setStyleSheet("font-size: 18px; margin: 20px 0; padding: 10px; border: 1px solid #ccc;")
self.question_label.setWordWrap(True)
layout.addWidget(self.question_label)
# 选项
self.option_group = QButtonGroup()
self.option_radios = []
for i in range(4):
radio = QRadioButton()
radio.setStyleSheet("font-size: 16px; margin: 5px 0;")
self.option_group.addButton(radio, i)
self.option_radios.append(radio)
layout.addWidget(radio)
# 按钮
btn_row = QHBoxLayout()
self.btn_prev = QPushButton("上一题")
self.btn_next = QPushButton("下一题")
self.btn_submit = QPushButton("提交答案")
btn_row.addWidget(self.btn_prev)
btn_row.addWidget(self.btn_next)
btn_row.addWidget(self.btn_submit)
layout.addLayout(btn_row)
self.btn_prev.clicked.connect(self._on_prev_question)
self.btn_next.clicked.connect(self._on_next_question)
self.btn_submit.clicked.connect(self._on_submit_quiz)
self.stack.addWidget(page)
def _show_current_question(self) -> None:
"""显示当前题目。"""
if not hasattr(self, 'questions') or not self.questions:
return
question = self.questions[self.current_question]
total = len(self.questions)
# 更新进度
self.progress_label.setText(f"{self.current_question + 1} 题 / 共 {total}")
# 更新题目
self.question_label.setText(question['stem'])
# 更新选项
for i, option in enumerate(question['options']):
self.option_radios[i].setText(f"{chr(65+i)}. {option}")
# 恢复之前的选择
if self.current_question < len(self.user_answers):
selected = self.user_answers[self.current_question]
if selected is not None:
self.option_radios[selected].setChecked(True)
else:
# 清空选择
for radio in self.option_radios:
radio.setChecked(False)
# 更新按钮状态
self.btn_prev.setEnabled(self.current_question > 0)
self.btn_next.setEnabled(self.current_question < total - 1)
def _on_prev_question(self) -> None:
"""上一题。"""
self._save_current_answer()
if self.current_question > 0:
self.current_question -= 1
self._show_current_question()
def _on_next_question(self) -> None:
"""下一题。"""
self._save_current_answer()
if self.current_question < len(self.questions) - 1:
self.current_question += 1
self._show_current_question()
def _save_current_answer(self) -> None:
"""保存当前题目的答案。"""
selected = self.option_group.checkedId()
# 确保user_answers列表足够长
while len(self.user_answers) <= self.current_question:
self.user_answers.append(None)
self.user_answers[self.current_question] = selected if selected >= 0 else None
def _on_submit_quiz(self) -> None:
"""提交答案并计算分数。"""
self._save_current_answer()
# 检查是否有未答题目
unanswered = []
for i, answer in enumerate(self.user_answers):
if answer is None:
unanswered.append(i + 1)
if unanswered:
reply = QMessageBox.question(
self, "提示",
f"还有第 {', '.join(map(str, unanswered))} 题未作答,确定要提交吗?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
return
# 计算分数
correct = 0
total = len(self.questions)
for i, question in enumerate(self.questions):
if i < len(self.user_answers) and self.user_answers[i] == question['answer_index']:
correct += 1
self.score = correct
self.total_questions = total
self._show_result()
self.stack.setCurrentIndex(6)
def _build_result_page(self) -> None:
"""构建结果页面:显示分数和操作选项。"""
page = QWidget()
layout = QVBoxLayout(page)
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
title = QLabel("答题结果")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;")
layout.addWidget(title)
self.score_label = QLabel()
self.score_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.score_label.setStyleSheet("font-size: 24px; margin: 20px 0; color: #2196F3;")
layout.addWidget(self.score_label)
btn_row = QHBoxLayout()
self.btn_continue = QPushButton("继续做题")
self.btn_exit = QPushButton("退出")
btn_row.addWidget(self.btn_continue)
btn_row.addWidget(self.btn_exit)
layout.addLayout(btn_row)
self.btn_continue.clicked.connect(lambda: self.stack.setCurrentIndex(4))
self.btn_exit.clicked.connect(self._on_logout)
self.stack.addWidget(page)
def _show_result(self) -> None:
"""显示答题结果。"""
if hasattr(self, 'score') and hasattr(self, 'total_questions'):
percentage = (self.score / self.total_questions) * 100
self.score_label.setText(
f"您答对了 {self.score} 题,共 {self.total_questions}\n"
f"正确率:{percentage:.1f}%"
)
def _build_change_password_page(self) -> None:
"""构建修改密码页面。"""
page = QWidget()
layout = QVBoxLayout(page)
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
title = QLabel("修改密码")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;")
layout.addWidget(title)
self.old_password = QLineEdit()
self.old_password.setPlaceholderText("请输入原密码")
self.old_password.setEchoMode(QLineEdit.EchoMode.Password)
layout.addWidget(self.old_password)
self.new_password = QLineEdit()
self.new_password.setPlaceholderText("请输入新密码6-10位包含大小写字母和数字")
self.new_password.setEchoMode(QLineEdit.EchoMode.Password)
layout.addWidget(self.new_password)
self.confirm_new_password = QLineEdit()
self.confirm_new_password.setPlaceholderText("请确认新密码")
self.confirm_new_password.setEchoMode(QLineEdit.EchoMode.Password)
layout.addWidget(self.confirm_new_password)
btn_row = QHBoxLayout()
self.btn_change_pwd = QPushButton("修改密码")
self.btn_back_to_choice = QPushButton("返回")
btn_row.addWidget(self.btn_change_pwd)
btn_row.addWidget(self.btn_back_to_choice)
layout.addLayout(btn_row)
self.btn_change_pwd.clicked.connect(self._on_change_password)
self.btn_back_to_choice.clicked.connect(lambda: self.stack.setCurrentIndex(4))
self.stack.addWidget(page)
def _on_change_password(self) -> None:
"""处理修改密码逻辑。"""
old_pwd = self.old_password.text() or ""
new_pwd = self.new_password.text() or ""
confirm_pwd = self.confirm_new_password.text() or ""
if not old_pwd or not new_pwd or not confirm_pwd:
QMessageBox.warning(self, "提示", "请填写完整信息")
return
config = {
'server': self.smtp_server.text().strip(),
'port': port_val,
'username': self.smtp_user.text().strip(),
'password': self.smtp_pass.text(),
'use_tls': self.smtp_tls.isChecked(),
'use_ssl': self.smtp_ssl.isChecked(),
'sender_name': self.smtp_sender.text().strip() or 'Math Study App',
}
self.email_service.update_smtp_config(config)
QMessageBox.information(self, '提示', 'SMTP设置已保存')
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)
Loading…
Cancel
Save