develop
hewendi23 7 months ago
parent d0499bab01
commit 0e10ec4b5a

196
app.py

@ -0,0 +1,196 @@
# src/app.py
import customtkinter as ctk
import random
import string
from user_manager import UserManager, send_verification_email
from quiz_generator import get_generator
class MathApp(ctk.CTk):
def __init__(self):
super().__init__()
self.title("小初高数学学习软件")
self.geometry("500x450")
ctk.set_appearance_mode("System")
self.user_manager = UserManager()
self.current_user = None
container = ctk.CTkFrame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
# 还原:只包含基础的五个页面
for F in (LoginPage, RegisterPage, MainPage, QuizPage, ScorePage):
page_name = F.__name__
frame = F(parent=container, controller=self)
self.frames[page_name] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame("LoginPage")
def show_frame(self, page_name):
frame = self.frames[page_name]
frame.tkraise()
class LoginPage(ctk.CTkFrame):
def __init__(self, parent, controller):
super().__init__(parent)
self.controller = controller
ctk.CTkLabel(self, text="登录", font=ctk.CTkFont(size=24, weight="bold")).pack(pady=20)
self.email_entry = ctk.CTkEntry(self, placeholder_text="邮箱", width=200)
self.email_entry.pack(pady=10)
self.password_entry = ctk.CTkEntry(self, placeholder_text="密码", show="*", width=200)
self.password_entry.pack(pady=10)
self.error_label = ctk.CTkLabel(self, text="", text_color="red")
self.error_label.pack(pady=5)
ctk.CTkButton(self, text="登录", command=self.login).pack(pady=10)
ctk.CTkButton(self, text="没有账户?去注册", fg_color="transparent", command=lambda: controller.show_frame("RegisterPage")).pack()
def login(self):
email, password = self.email_entry.get(), self.password_entry.get()
if self.controller.user_manager.validate_login(email, password):
self.controller.current_user = email
self.error_label.configure(text="")
self.controller.show_frame("MainPage")
else:
self.error_label.configure(text="邮箱或密码错误")
class RegisterPage(ctk.CTkFrame):
def __init__(self, parent, controller):
super().__init__(parent)
self.controller = controller
self.code = ""
self.cooldown_time = 0
ctk.CTkLabel(self, text="注册新账户", font=ctk.CTkFont(size=24, weight="bold")).pack(pady=10)
self.email_entry = ctk.CTkEntry(self, placeholder_text="邮箱", width=250)
self.email_entry.pack(pady=5)
self.send_code_button = ctk.CTkButton(self, text="发送验证码", command=self.send_code, width=250)
self.send_code_button.pack(pady=5)
self.code_entry = ctk.CTkEntry(self, placeholder_text="输入收到的邮件验证码", width=250)
self.code_entry.pack(pady=5)
self.password_entry = ctk.CTkEntry(self, placeholder_text="密码 (6-10位,含大小写和数字)", show="*", width=250)
self.password_entry.pack(pady=5)
self.confirm_password_entry = ctk.CTkEntry(self, placeholder_text="确认密码", show="*", width=250)
self.confirm_password_entry.pack(pady=5)
self.info_label = ctk.CTkLabel(self, text="", text_color="red")
self.info_label.pack(pady=5)
ctk.CTkButton(self, text="注册", command=self.register).pack(pady=10)
ctk.CTkButton(self, text="返回登录", fg_color="transparent", command=lambda: controller.show_frame("LoginPage")).pack()
def update_cooldown(self):
if self.cooldown_time > 0:
self.send_code_button.configure(text=f"{self.cooldown_time}秒后可重试")
self.cooldown_time -= 1
self.after(1000, self.update_cooldown)
else:
self.send_code_button.configure(text="发送验证码", state="normal")
def send_code(self):
if self.cooldown_time > 0: return
email = self.email_entry.get().strip()
if not email:
self.info_label.configure(text="请输入邮箱地址。", text_color="red")
return
if self.controller.user_manager.is_email_registered(email):
self.info_label.configure(text="该邮箱已被注册。", text_color="red")
return
self.code = ''.join(random.choices(string.digits, k=6))
self.info_label.configure(text="正在发送邮件,请稍候...", text_color="gray")
success = send_verification_email(email, self.code)
if success:
self.info_label.configure(text="邮件验证码已发送至您的邮箱,请查收。", text_color="green")
self.cooldown_time = 60
self.send_code_button.configure(state="disabled")
self.update_cooldown()
else:
self.info_label.configure(text="邮件发送失败,请检查邮箱或网络。", text_color="red")
self.code = ""
def register(self):
email, user_code, password, confirm_password = self.email_entry.get().strip(), self.code_entry.get().strip(), self.password_entry.get(), self.confirm_password_entry.get()
if not self.code or user_code != self.code:
self.info_label.configure(text="邮件验证码错误", text_color="red")
return
if password != confirm_password:
self.info_label.configure(text="两次输入的密码不一致", text_color="red")
return
if not self.controller.user_manager.validate_password_format(password):
self.info_label.configure(text="密码格式不符合要求", text_color="red")
return
success, msg = self.controller.user_manager.register_user(email, password)
self.info_label.configure(text=msg, text_color="green" if success else "red")
if success: self.code = ""
class MainPage(ctk.CTkFrame):
def __init__(self, parent, controller):
super().__init__(parent)
self.controller = controller
ctk.CTkLabel(self, text="选择学习阶段", font=ctk.CTkFont(size=24, weight="bold")).pack(pady=20)
ctk.CTkButton(self, text="小学", command=lambda: self.start_quiz("小学"), height=40, width=150).pack(pady=10)
ctk.CTkButton(self, text="初中", command=lambda: self.start_quiz("初中"), height=40, width=150).pack(pady=10)
ctk.CTkButton(self, text="高中", command=lambda: self.start_quiz("高中"), height=40, width=150).pack(pady=10)
ctk.CTkButton(self, text="退出登录", fg_color="transparent", command=lambda: controller.show_frame("LoginPage")).pack(side="bottom", pady=20)
def start_quiz(self, level):
dialog = ctk.CTkInputDialog(text="请输入题目数量 (1-20):", title="设置题目数量")
num_str = dialog.get_input()
if num_str:
try:
num = int(num_str)
if 1 <= num <= 20:
self.controller.frames["QuizPage"].setup_quiz(level, num)
self.controller.show_frame("QuizPage")
except ValueError: pass
class QuizPage(ctk.CTkFrame):
def __init__(self, parent, controller):
super().__init__(parent)
self.controller = controller
self.question_label = ctk.CTkLabel(self, text="问题", font=ctk.CTkFont(size=18))
self.question_label.pack(pady=20, padx=20)
self.radio_var = ctk.IntVar(value=-1)
self.radio_buttons = []
for i in range(4):
rb = ctk.CTkRadioButton(self, text="", variable=self.radio_var, value=i, font=ctk.CTkFont(size=14))
rb.pack(anchor="w", padx=100, pady=5)
self.radio_buttons.append(rb)
self.submit_button = ctk.CTkButton(self, text="下一题", command=self.next_question)
self.submit_button.pack(pady=20)
def setup_quiz(self, level, num_questions):
self.questions = get_generator(level).generate_bunch(num_questions)
self.current_question_index = 0
self.user_answers = []
self.display_question()
def display_question(self):
self.radio_var.set(-1)
q = self.questions[self.current_question_index]
self.question_label.configure(text=f"{self.current_question_index + 1}. {q.text} = ?")
for i, option in enumerate(q.options): self.radio_buttons[i].configure(text=str(option))
self.submit_button.configure(text="提交试卷" if self.current_question_index == len(self.questions) - 1 else "下一题")
def next_question(self):
if self.radio_var.get() == -1: return
self.user_answers.append(self.radio_var.get())
self.current_question_index += 1
if self.current_question_index < len(self.questions): self.display_question()
else: self.calculate_score()
def calculate_score(self):
correct_count = sum(1 for i, q in enumerate(self.questions) if self.user_answers[i] == q.correct_answer_index)
total = len(self.questions)
score_percent = (correct_count / total) * 100
self.controller.frames["ScorePage"].show_score(correct_count, total, score_percent)
self.controller.show_frame("ScorePage")
class ScorePage(ctk.CTkFrame):
def __init__(self, parent, controller):
super().__init__(parent)
self.controller = controller
self.score_label = ctk.CTkLabel(self, text="", font=ctk.CTkFont(size=24))
self.score_label.pack(pady=40, padx=20)
ctk.CTkButton(self, text="继续做题", command=lambda: controller.show_frame("MainPage")).pack(pady=10)
ctk.CTkButton(self, text="退出程序", command=controller.quit).pack(pady=10)
def show_score(self, correct, total, percent):
self.score_label.configure(text=f"答对了 {correct} / {total}\n\n得分: {percent:.2f}%")

@ -0,0 +1,5 @@
from app import MathApp
if __name__ == "__main__":
app = MathApp()
app.mainloop()

@ -0,0 +1,96 @@
import random
from abc import ABC, abstractmethod
# 安全的表达式求值函数
def safe_eval(expression):
try:
return eval(expression, {"__builtins__": None}, {})
except Exception:
return None
class Question:
def __init__(self, text, options, correct_answer_index):
self.text = text
self.options = options
self.correct_answer_index = correct_answer_index
class QuestionGenerator(ABC):
def generate_bunch(self, count):
questions = []
generated_texts = set()
while len(questions) < count:
text, answer = self._create_question_and_answer()
if text not in generated_texts:
options = self._generate_options(answer)
correct_index = options.index(answer)
questions.append(Question(text, options, correct_index))
generated_texts.add(text)
return questions
def _generate_options(self, correct_answer):
options = {correct_answer}
while len(options) < 4:
offset = random.randint(-10, 10)
if offset == 0:
offset = random.choice([-1, 1]) * 10
distractor = correct_answer + offset
options.add(distractor)
option_list = list(options)
random.shuffle(option_list)
return option_list
@abstractmethod
def _create_question_and_answer(self):
pass
class PrimarySchoolGenerator(QuestionGenerator):
def _create_question_and_answer(self):
ops = ['+', '-', '*', '/']
op = random.choice(ops)
if op == '+':
num1, num2 = random.randint(1, 100), random.randint(1, 100)
expr = f"{num1} + {num2}"
elif op == '-':
num1, num2 = random.randint(1, 100), random.randint(1, 100)
if num1 < num2: num1, num2 = num2, num1
expr = f"{num1} - {num2}"
elif op == '*':
num1, num2 = random.randint(1, 20), random.randint(1, 20)
expr = f"{num1} * {num2}"
else: # op == '/'
divisor = random.randint(2, 10)
answer = random.randint(2, 20)
dividend = divisor * answer
expr = f"{dividend} / {divisor}"
return expr, int(safe_eval(expr))
class MiddleSchoolGenerator(PrimarySchoolGenerator):
def _create_question_and_answer(self):
if random.choice([True, False]):
return super()._create_question_and_answer()
else:
base = random.randint(2, 10)
expr = f"{base}**2"
return expr, int(safe_eval(expr))
class HighSchoolGenerator(MiddleSchoolGenerator):
def _create_question_and_answer(self):
# 简化版高中题,可扩展为三角函数等
return super()._create_question_and_answer()
def get_generator(level):
generators = {
"小学": PrimarySchoolGenerator,
"初中": MiddleSchoolGenerator,
"高中": HighSchoolGenerator,
}
generator_class = generators.get(level)
if generator_class:
return generator_class()
raise ValueError("无效的等级")

@ -0,0 +1,105 @@
# src/user_manager.py
import json
import hashlib
import os
import re
import smtplib
import ssl
from email.message import EmailMessage
def send_verification_email(recipient_email, code):
"""
发送验证码邮件的函数
"""
# --- 配置区域 ---
SENDER_EMAIL = "876413020@qq.com"
APP_PASSWORD = "wbeyuimdbvkxbdgf"
SMTP_SERVER = "smtp.qq.com"
SMTP_PORT = 465
# --- 构建邮件内容 ---
msg = EmailMessage()
msg['Subject'] = "【数学学习软件】您的注册验证码"
msg['From'] = SENDER_EMAIL
msg['To'] = recipient_email
msg.set_content(
f"您好!\n\n"
f"感谢您注册数学学习软件。您的验证码是:\n\n"
f"{code}\n\n"
f"请在5分钟内使用。如果不是您本人操作请忽略此邮件。"
)
# --- 发送邮件(健壮的结构)---
smtp_server = None
try:
context = ssl.create_default_context()
smtp_server = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, context=context)
smtp_server.login(SENDER_EMAIL, APP_PASSWORD)
smtp_server.send_message(msg)
print(f"邮件已成功发送至 {recipient_email}")
return True
except smtplib.SMTPAuthenticationError:
print("邮件发送失败: 认证错误。请检查邮箱或授权码是否正确。")
return False
except Exception as e:
print(f"邮件发送时发生错误: {e}")
return False
finally:
if smtp_server:
try:
smtp_server.quit()
except Exception:
pass
class UserManager:
def __init__(self):
self.users = self._load_users()
def _load_users(self):
if os.path.exists("users.json"):
try:
with open("users.json", 'r', encoding='utf-8') as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
return {}
return {}
def _save_users(self):
with open("users.json", 'w', encoding='utf-8') as f:
json.dump(self.users, f, indent=4)
def _hash_password(self, password, salt):
return hashlib.sha256((password + salt).encode()).hexdigest()
def is_email_registered(self, email):
return email in self.users
def validate_password_format(self, password):
"""密码必须 6-10 位,包含大小写字母和数字"""
if 6 <= len(password) <= 10 and \
re.search(r"[a-z]", password) and \
re.search(r"[A-Z]", password) and \
re.search(r"\d", password):
return True
return False
def register_user(self, email, password):
if self.is_email_registered(email):
return False, "该邮箱已被注册。"
salt = os.urandom(16).hex()
password_hash = self._hash_password(password, salt)
self.users[email] = {"password_hash": password_hash, "salt": salt}
self._save_users()
return True, "注册成功!"
def validate_login(self, email, password):
if not self.is_email_registered(email):
return False
user_data = self.users[email]
salt = user_data["salt"]
password_hash = self._hash_password(password, salt)
return password_hash == user_data["password_hash"]
Loading…
Cancel
Save