diff --git a/src/__pycache__/main_app.cpython-311.pyc b/src/__pycache__/main_app.cpython-311.pyc new file mode 100644 index 0000000..e3f5ecf Binary files /dev/null and b/src/__pycache__/main_app.cpython-311.pyc differ diff --git a/src/__pycache__/question_bank.cpython-311.pyc b/src/__pycache__/question_bank.cpython-311.pyc new file mode 100644 index 0000000..bd940b3 Binary files /dev/null and b/src/__pycache__/question_bank.cpython-311.pyc differ diff --git a/src/__pycache__/question_generator.cpython-311.pyc b/src/__pycache__/question_generator.cpython-311.pyc new file mode 100644 index 0000000..d0555e0 Binary files /dev/null and b/src/__pycache__/question_generator.cpython-311.pyc differ diff --git a/src/__pycache__/quiz.cpython-311.pyc b/src/__pycache__/quiz.cpython-311.pyc new file mode 100644 index 0000000..7fb1b35 Binary files /dev/null and b/src/__pycache__/quiz.cpython-311.pyc differ diff --git a/src/__pycache__/user_manager.cpython-311.pyc b/src/__pycache__/user_manager.cpython-311.pyc new file mode 100644 index 0000000..04ceb36 Binary files /dev/null and b/src/__pycache__/user_manager.cpython-311.pyc differ diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..1524dbd --- /dev/null +++ b/src/main.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +数学练习系统 - 面向小学、初中和高中学生的数学题目练习应用 +支持用户注册、登录、密码管理、题目生成和答题功能 +""" + +import tkinter as tk +from main_app import MathQuizApp + + +def main(): + """ + 主函数 + """ + root = tk.Tk() + app = MathQuizApp(root) + root.mainloop() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/main_app.py b/src/main_app.py new file mode 100644 index 0000000..99591b2 --- /dev/null +++ b/src/main_app.py @@ -0,0 +1,775 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +主应用模块 +""" + +import tkinter as tk +from tkinter import messagebox, ttk +import random +from typing import List, Optional + +from user_manager import UserManager +from question_bank import QuestionBank +from quiz import Quiz +from question_generator import Expression + + +class MathQuizApp: + """ + 数学测验应用程序主类 + """ + + def __init__(self, root: tk.Tk): + """ + 初始化应用程序 + + @param root: Tk根窗口 + """ + self.root = root + self.root.title("数学练习系统") + self.root.geometry("700x600") + self.root.resizable(True, True) + self.root.configure(bg="#f0f0f0") + + # 初始化系统组件 + self.user_manager = UserManager() + self.question_bank = QuestionBank() + + # 设置样式 + self.setup_styles() + + # 创建不同的界面框架 + self.login_frame = tk.Frame(self.root, bg="#f0f0f0") + self.register_frame = tk.Frame(self.root, bg="#f0f0f0") + self.set_password_frame = tk.Frame(self.root, bg="#f0f0f0") + self.main_menu_frame = tk.Frame(self.root, bg="#f0f0f0") + self.quiz_setup_frame = tk.Frame(self.root, bg="#f0f0f0") + self.quiz_frame = tk.Frame(self.root, bg="#f0f0f0") + self.result_frame = tk.Frame(self.root, bg="#f0f0f0") + self.change_password_frame = tk.Frame(self.root, bg="#f0f0f0") + self.delete_account_frame = tk.Frame(self.root, bg="#f0f0f0") # 添加删除账户框架 + + # 初始化应用状态 + self.current_quiz: Optional[Quiz] = None + self.current_level = "" + self.question_count = 0 + + # 创建界面 + self.create_widgets() + self.show_login_frame() + + def setup_styles(self): + """ + 设置界面样式 + """ + self.title_font = ("Arial", 20, "bold") + self.header_font = ("Arial", 16, "bold") + self.normal_font = ("Arial", 12) + self.button_font = ("Arial", 11, "bold") + + # 定义颜色方案 + self.primary_color = "#4a6fa5" + self.secondary_color = "#6b8cbc" + self.accent_color = "#ff6b6b" + self.success_color = "#4caf50" + self.warning_color = "#ffc107" + self.danger_color = "#f44336" + self.light_bg = "#f8f9fa" + self.dark_text = "#333333" + + def create_widgets(self): + """ + 创建界面组件 + """ + # 创建不同的界面框架 + self.login_frame = tk.Frame(self.root, bg="#f0f0f0") + self.register_frame = tk.Frame(self.root, bg="#f0f0f0") + self.set_password_frame = tk.Frame(self.root, bg="#f0f0f0") + self.main_menu_frame = tk.Frame(self.root, bg="#f0f0f0") + self.quiz_setup_frame = tk.Frame(self.root, bg="#f0f0f0") + self.quiz_frame = tk.Frame(self.root, bg="#f0f0f0") + self.result_frame = tk.Frame(self.root, bg="#f0f0f0") + self.change_password_frame = tk.Frame(self.root, bg="#f0f0f0") + self.delete_account_frame = tk.Frame(self.root, bg="#f0f0f0") # 添加删除账户框架 + + def show_frame(self, frame: tk.Frame): + """ + 显示指定的界面框架 + + @param frame: 要显示的框架 + """ + # 隐藏所有框架 + for f in [self.login_frame, self.register_frame, self.set_password_frame, + self.main_menu_frame, self.quiz_setup_frame, self.quiz_frame, + self.result_frame, self.change_password_frame, self.delete_account_frame]: + f.pack_forget() + + # 显示指定框架 + frame.pack(fill="both", expand=True) + + def show_login_frame(self): + """ + 显示登录界面 + """ + # 清除之前的内容 + for widget in self.login_frame.winfo_children(): + widget.destroy() + + # 创建登录界面 + main_frame = tk.Frame(self.login_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=50, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 30)) + tk.Label(title_frame, text="用户登录", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) + + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=10) + + tk.Label(form_frame, text="用户名:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.login_email_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, bg="#f0f0f0") + self.login_email_entry.pack(pady=5) + + tk.Label(form_frame, text="密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.login_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") + self.login_password_entry.pack(pady=5) + + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="登录", command=self.login, width=20, bg=self.primary_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="注册新用户", command=self.show_register_frame, width=20, bg=self.secondary_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) + tk.Button(button_frame, text="注销账户", command=self.show_delete_account_frame, width=20, bg=self.danger_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) + + self.show_frame(self.login_frame) + + def login(self): + """ + 处理登录逻辑 + """ + username = self.login_email_entry.get().strip() + password = self.login_password_entry.get() + + if not username or not password: + messagebox.showerror("错误", "请输入用户名和密码") + return + + if self.user_manager.login(username, password): + messagebox.showinfo("成功", "登录成功") + self.show_main_menu_frame() + # 错误信息在user_manager.login中已经显示 + + def show_register_frame(self): + """ + 显示注册界面 + """ + # 清除之前的内容 + for widget in self.register_frame.winfo_children(): + widget.destroy() + + # 创建注册界面 + main_frame = tk.Frame(self.register_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="用户注册", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) + + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=10) + + tk.Label(form_frame, text="用户名:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.register_username_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, bg="#f0f0f0") + self.register_username_entry.pack(pady=5) + + tk.Label(form_frame, text="邮箱:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.register_email_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, bg="#f0f0f0") + self.register_email_entry.pack(pady=5) + + tk.Button(form_frame, text="获取注册码", command=self.send_registration_code, width=20, bg=self.primary_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + + tk.Label(form_frame, text="注册码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.registration_code_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, bg="#f0f0f0") + self.registration_code_entry.pack(pady=5) + + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="验证注册码", command=self.verify_registration_code, width=20, bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="返回登录", command=self.show_login_frame, width=20, bg="#cccccc", fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) + + self.show_frame(self.register_frame) + + def send_registration_code(self): + """ + 发送注册码 + """ + username = self.register_username_entry.get().strip() + email = self.register_email_entry.get().strip() + + if not username or not email: + messagebox.showerror("错误", "请输入用户名和邮箱") + return + + if self.user_manager.register_user(email, username): + messagebox.showinfo("成功", "注册码已发送,请查收") + + def verify_registration_code(self): + """ + 验证注册码 + """ + email = self.register_email_entry.get().strip() + code = self.registration_code_entry.get().strip() + + if not email or not code: + messagebox.showerror("错误", "请输入邮箱和注册码") + return + + if self.user_manager.verify_registration_code(email, code): + messagebox.showinfo("成功", "注册码验证成功,请设置密码") + self.show_set_password_frame(email) + + def show_set_password_frame(self, email: str): + """ + 显示设置密码界面 + + @param email: 用户邮箱 + """ + # 清除之前的内容 + for widget in self.set_password_frame.winfo_children(): + widget.destroy() + + # 创建设置密码界面 + main_frame = tk.Frame(self.set_password_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="设置密码", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) + + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=10) + + tk.Label(form_frame, text="邮箱: " + email, font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + + tk.Label(form_frame, text="密码:", font=self.normal_font, bg="#ffffff", fg=self.primary_color).pack(pady=(10, 5), anchor="w") + tk.Label(form_frame, text="(6-10位,必须包含大小写字母和数字)", font=("Arial", 10), bg="#ffffff", fg="gray").pack(pady=5, anchor="w") + self.set_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") + self.set_password_entry.pack(pady=5) + + tk.Label(form_frame, text="确认密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.confirm_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") + self.confirm_password_entry.pack(pady=5) + + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="设置密码", command=lambda: self.set_password(email), width=20, bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="返回登录", command=self.show_login_frame, width=20, bg="#cccccc", fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) + + self.show_frame(self.set_password_frame) + + def set_password(self, email: str): + """ + 设置密码 + + @param email: 用户邮箱 + """ + password = self.set_password_entry.get() + confirm_password = self.confirm_password_entry.get() + + if not password or not confirm_password: + messagebox.showerror("错误", "请输入密码和确认密码") + return + + if password != confirm_password: + messagebox.showerror("错误", "两次输入的密码不一致") + return + + if self.user_manager.set_password(email, password): + messagebox.showinfo("成功", "密码设置成功,请登录") + self.show_login_frame() + + def show_main_menu_frame(self): + """ + 显示主菜单界面 + """ + # 清除之前的内容 + for widget in self.main_menu_frame.winfo_children(): + widget.destroy() + + # 创建主菜单界面 + main_frame = tk.Frame(self.main_menu_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="主菜单", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) + + user_email = self.user_manager.current_user.email if self.user_manager.current_user else "未知用户" + tk.Label(main_frame, text=f"欢迎, {user_email}!", font=self.header_font, bg="#ffffff").pack(pady=10) + + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="小学题目", command=lambda: self.show_quiz_setup_frame("elementary"), + width=20, height=2, bg="#4CAF50", fg="white", font=self.button_font, relief=tk.FLAT, bd=0).pack(pady=10) + tk.Button(button_frame, text="初中题目", command=lambda: self.show_quiz_setup_frame("middle"), + width=20, height=2, bg="#2196F3", fg="white", font=self.button_font, relief=tk.FLAT, bd=0).pack(pady=10) + tk.Button(button_frame, text="高中题目", command=lambda: self.show_quiz_setup_frame("high"), + width=20, height=2, bg="#FF9800", fg="white", font=self.button_font, relief=tk.FLAT, bd=0).pack(pady=10) + + tk.Button(button_frame, text="修改密码", command=self.show_change_password_frame, + width=20, bg=self.secondary_color, fg="white", font=self.button_font, relief=tk.FLAT, bd=0, pady=5).pack(pady=10) + tk.Button(button_frame, text="退出登录", command=self.logout, width=20, bg=self.danger_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, pady=5).pack(pady=5) + + self.show_frame(self.main_menu_frame) + + def show_quiz_setup_frame(self, level: str): + """ + 显示测验设置界面 + + @param level: 题目难度 + """ + self.current_level = level + + # 清除之前的内容 + for widget in self.quiz_setup_frame.winfo_children(): + widget.destroy() + + # 创建测验设置界面 + main_frame = tk.Frame(self.quiz_setup_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + level_names = {"elementary": "小学", "middle": "初中", "high": "高中"} + level_colors = {"elementary": "#4CAF50", "middle": "#2196F3", "high": "#FF9800"} + + title_frame = tk.Frame(main_frame, bg=level_colors[level]) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text=f"{level_names[level]}数学题目", font=self.title_font, bg=level_colors[level], fg="white").pack(pady=20) + + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=20) + + tk.Label(form_frame, text="请输入题目数量 (1-20):", font=self.normal_font, bg="#ffffff").pack(pady=10) + self.question_count_entry = tk.Entry(form_frame, width=20, font=self.normal_font, justify="center", relief=tk.FLAT, bd=5, bg="#f0f0f0") + self.question_count_entry.pack(pady=5) + self.question_count_entry.insert(0, "10") # 默认10题 + + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="开始答题", command=self.start_quiz, width=20, bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="返回主菜单", command=self.show_main_menu_frame, width=20, bg="#cccccc", fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) + + self.show_frame(self.quiz_setup_frame) + + def start_quiz(self): + """ + 开始测验 + """ + try: + count = int(self.question_count_entry.get()) + if not (1 <= count <= 20): + messagebox.showerror("错误", "题目数量必须在1-20之间") + return + except ValueError: + messagebox.showerror("错误", "请输入有效的数字") + return + + self.question_count = count + + # 生成题目 + try: + questions = self.question_bank.generate_questions(self.current_level, self.question_count) + self.current_quiz = Quiz(questions) + self.show_quiz_frame() + except Exception as e: + messagebox.showerror("错误", f"生成题目时出错: {str(e)}") + + def show_quiz_frame(self): + """ + 显示答题界面 + """ + if not self.current_quiz: + return + + # 清除之前的内容 + for widget in self.quiz_frame.winfo_children(): + widget.destroy() + + # 创建答题界面 + current_question = self.current_quiz.get_current_question() + if not current_question: + return + + main_frame = tk.Frame(self.quiz_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=20, padx=30, fill="both", expand=True) + + # 显示题目进度 + progress = f"题目 {self.current_quiz.current_question_index + 1}/{len(self.current_quiz.questions)}" + progress_frame = tk.Frame(main_frame, bg=self.primary_color) + progress_frame.pack(fill="x", pady=(0, 20)) + tk.Label(progress_frame, text=progress, font=self.header_font, bg=self.primary_color, fg="white").pack(pady=10) + + # 显示题目 + question_frame = tk.Frame(main_frame, bg="#ffffff") + question_frame.pack(pady=10) + + tk.Label(question_frame, text="题目:", font=self.header_font, bg="#ffffff").pack(pady=(10, 5)) + question_text = str(current_question) + tk.Label(question_frame, text=question_text, font=("Arial", 16, "bold"), bg="#ffffff", wraplength=500).pack(pady=10) + + # 计算选项 + options = self.generate_options(current_question.answer) + + # 显示选项 + tk.Label(main_frame, text="请选择答案:", font=self.normal_font, bg="#ffffff").pack(pady=(20, 10)) + + self.answer_var = tk.StringVar() + self.answer_var.set(" ") # 明确设置初始值为空字符串,确保不选中任何选项 + + options_frame = tk.Frame(main_frame, bg="#ffffff") + options_frame.pack(pady=10) + + for i, option in enumerate(options): + tk.Radiobutton(options_frame, text=f"{['A', 'B', 'C', 'D'][i]}. {option}", + variable=self.answer_var, value=str(option), font=self.normal_font, + bg="#ffffff", selectcolor="#e0e0e0", activebackground="#f0f0f0").pack(anchor="w", padx=50, pady=5) + + # 按钮框架 + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + if self.current_quiz.current_question_index > 0: + tk.Button(button_frame, text="上一题", command=self.previous_question, bg="#cccccc", fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) + + tk.Button(button_frame, text="提交答案", command=self.submit_answer, bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) + + if self.current_quiz.current_question_index < len(self.current_quiz.questions) - 1: + tk.Button(button_frame, text="下一题", command=self.next_question, bg=self.primary_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) + + tk.Button(main_frame, text="返回主菜单", command=self.show_main_menu_frame, width=15, bg=self.danger_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + + self.show_frame(self.quiz_frame) + + def generate_options(self, correct_answer) -> List[float]: + """ + 为题目生成选项 + + @param correct_answer: 正确答案 + @return: 选项列表 + """ + # 生成4个选项,其中一个是正确答案 + options = {correct_answer} + + # 添加一些干扰项 + if isinstance(correct_answer, int): + while len(options) < 4: + offset = random.randint(-10, 10) + if offset != 0: + options.add(correct_answer + offset) + else: + # 浮点数情况 + while len(options) < 4: + offset = random.uniform(-10, 10) + if abs(offset) > 0.1: # 避免太接近正确答案 + options.add(round(correct_answer + offset, 2)) + + # 如果选项不足4个,补充一些随机数 + while len(options) < 4: + options.add(round(random.uniform(correct_answer - 20, correct_answer + 20), 2)) + + options_list = list(options) + random.shuffle(options_list) + return options_list[:4] + + def submit_answer(self): + """ + 提交答案 + """ + if not self.current_quiz: + return + + answer_str = self.answer_var.get() + if not answer_str: + messagebox.showerror("错误", "请选择一个答案") + return + + try: + answer = float(answer_str) + self.current_quiz.answer_question(answer) + + # 如果是最后一题,显示结果 + if self.current_quiz.is_finished(): + self.show_result_frame() + else: + messagebox.showinfo("提示", "答案已提交") + except ValueError: + messagebox.showerror("错误", "请选择有效答案") + + def next_question(self): + """ + 下一题 + """ + if not self.current_quiz: + return + + # 保存当前答案(如果有选择) + answer_str = self.answer_var.get() + if answer_str: + try: + answer = float(answer_str) + self.current_quiz.answer_question(answer) + except ValueError: + pass # 如果答案无效,保持为None + + if self.current_quiz.next_question(): + self.show_quiz_frame() + else: + # 已经是最后一题 + if self.current_quiz.answers[self.current_quiz.current_question_index] is not None: + # 如果最后一题已答题,显示结果 + self.show_result_frame() + else: + messagebox.showinfo("提示", "已经是最后一题") + + def previous_question(self): + """ + 上一题 + """ + if not self.current_quiz: + return + + # 保存当前答案(如果有选择) + answer_str = self.answer_var.get() + if answer_str: + try: + answer = float(answer_str) + self.current_quiz.answer_question(answer) + except ValueError: + pass # 如果答案无效,保持为None + + if self.current_quiz.previous_question(): + self.show_quiz_frame() + + def show_result_frame(self): + """ + 显示结果界面 + """ + if not self.current_quiz: + return + + # 清除之前的内容 + for widget in self.result_frame.winfo_children(): + widget.destroy() + + # 计算得分 + score = self.current_quiz.calculate_score() + + # 创建结果界面 + main_frame = tk.Frame(self.result_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="测验结果", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) + + result_frame = tk.Frame(main_frame, bg="#ffffff") + result_frame.pack(pady=20) + + # 根据得分显示不同颜色的分数 + score_color = self.danger_color if score < 60 else self.warning_color if score < 80 else self.success_color + + tk.Label(result_frame, text=f"您的得分: {score:.1f}%", font=("Arial", 20, "bold"), fg=score_color, bg="#ffffff").pack(pady=10) + + # 根据得分显示评语 + if score >= 90: + comment = "优秀! 继续保持!" + comment_color = self.success_color + elif score >= 80: + comment = "良好! 还可以做得更好!" + comment_color = self.success_color + elif score >= 60: + comment = "及格了,需要继续努力!" + comment_color = self.warning_color + else: + comment = "需要加强练习哦!" + comment_color = self.danger_color + + tk.Label(result_frame, text=comment, font=self.header_font, fg=comment_color, bg="#ffffff").pack(pady=10) + + # 显示答题详情 + correct_count = sum(1 for q, a in zip(self.current_quiz.questions, self.current_quiz.answers) + if a is not None and abs(q.answer - a) < 1e-6) + total_count = len(self.current_quiz.questions) + tk.Label(result_frame, text=f"答对: {correct_count}/{total_count}", font=self.normal_font, bg="#ffffff").pack(pady=5) + + # 按钮 + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=30) + + level_names = {"elementary": "小学", "middle": "初中", "high": "高中"} + tk.Button(button_frame, text=f"继续{level_names[self.current_level]}题目", + command=lambda: self.show_quiz_setup_frame(self.current_level), width=20, bg=self.primary_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) + + tk.Button(button_frame, text="返回主菜单", command=self.show_main_menu_frame, width=15, bg="#cccccc", fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) + + self.show_frame(self.result_frame) + + def show_change_password_frame(self): + """ + 显示修改密码界面 + """ + # 清除之前的内容 + for widget in self.change_password_frame.winfo_children(): + widget.destroy() + + # 创建修改密码界面 + main_frame = tk.Frame(self.change_password_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="修改密码", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) + + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=10) + + tk.Label(form_frame, text="原密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.old_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") + self.old_password_entry.pack(pady=5) + + tk.Label(form_frame, text="新密码:", font=self.normal_font, bg="#ffffff", fg=self.primary_color).pack(pady=(10, 5), anchor="w") + tk.Label(form_frame, text="(6-10位,必须包含大小写字母和数字)", font=("Arial", 10), bg="#ffffff", fg="gray").pack(pady=5, anchor="w") + self.new_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") + self.new_password_entry.pack(pady=5) + + tk.Label(form_frame, text="确认新密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.confirm_new_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") + self.confirm_new_password_entry.pack(pady=5) + + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="修改密码", command=self.change_password, width=20, bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="返回主菜单", command=self.show_main_menu_frame, width=20, bg="#cccccc", fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) + + self.show_frame(self.change_password_frame) + + def change_password(self): + """ + 修改密码 + """ + old_password = self.old_password_entry.get() + new_password = self.new_password_entry.get() + confirm_new_password = self.confirm_new_password_entry.get() + + if not old_password or not new_password or not confirm_new_password: + messagebox.showerror("错误", "请填写所有字段") + return + + if new_password != confirm_new_password: + messagebox.showerror("错误", "新密码和确认密码不一致") + return + + if self.user_manager.change_password(old_password, new_password): + messagebox.showinfo("成功", "密码修改成功") + self.show_main_menu_frame() + # 错误信息在user_manager.change_password中已经显示 + + def logout(self): + """ + 退出登录 + """ + self.user_manager.logout() + self.show_login_frame() + + def show_delete_account_frame(self): + """ + 显示注销账户界面 + """ + # 清除之前的内容 + for widget in self.delete_account_frame.winfo_children(): + widget.destroy() + + # 创建注销账户界面 + main_frame = tk.Frame(self.delete_account_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.danger_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="注销账户", font=self.title_font, bg=self.danger_color, fg="white").pack(pady=20) + + warning_frame = tk.Frame(main_frame, bg="#ffffff") + warning_frame.pack(pady=10) + + tk.Label(warning_frame, text="警告:此操作将永久删除您的账户和所有数据!", font=self.normal_font, fg=self.danger_color, bg="#ffffff").pack(pady=5) + tk.Label(warning_frame, text="请确认您的邮箱和密码:", font=self.normal_font, fg=self.danger_color, bg="#ffffff").pack(pady=5) + + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=10) + + tk.Label(form_frame, text="邮箱:", font=self.normal_font, bg="#ffffff").pack(pady=10, anchor="w") + self.delete_email_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, bg="#f0f0f0") + self.delete_email_entry.pack(pady=5) + + tk.Label(form_frame, text="密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.delete_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") + self.delete_password_entry.pack(pady=5) + + # 按钮框架 + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="确认注销", command=self.confirm_delete_account, + width=15, bg=self.danger_color, fg="white", font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) + tk.Button(button_frame, text="取消", command=self.show_login_frame, + width=15, bg="#cccccc", fg=self.dark_text, font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) + + self.show_frame(self.delete_account_frame) + + def confirm_delete_account(self): + """ + 确认并执行账户删除操作 + """ + email = self.delete_email_entry.get().strip() + password = self.delete_password_entry.get() + + if not email or not password: + messagebox.showerror("错误", "请输入邮箱和密码") + return + + # 确认操作 + if messagebox.askyesno("确认注销", "确定要注销账户吗?此操作无法撤销!"): + if self.user_manager.delete_account(email, password): + messagebox.showinfo("成功", "账户已注销,您可以使用该邮箱重新注册") + self.show_login_frame() + # 错误信息在user_manager.delete_account中已经显示 + + def delete_account(self): + """ + 注销账户(从主菜单调用的旧方法,保持兼容性) + """ + self.show_delete_account_frame() \ No newline at end of file diff --git a/src/question_bank.py b/src/question_bank.py new file mode 100644 index 0000000..20925a9 --- /dev/null +++ b/src/question_bank.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +题库模块 +""" + +from typing import List, Dict +from question_generator import Expression, ElementaryQuestionGenerator, MiddleQuestionGenerator, HighQuestionGenerator + + +class QuestionBank: + """ + 题库类,管理不同难度的题目生成器 + """ + + def __init__(self): + """ + 初始化题库 + """ + self.generators = { + "elementary": ElementaryQuestionGenerator(), + "middle": MiddleQuestionGenerator(), + "high": HighQuestionGenerator() + } + + def generate_questions(self, level: str, count: int) -> List[Expression]: + """ + 生成指定数量和难度的题目 + + @param level: 题目难度(elementary, middle, high) + @param count: 题目数量 + @return: 题目列表 + """ + if level not in self.generators: + raise ValueError("无效的题目难度") + + generator = self.generators[level] + questions = [] + + for _ in range(count): + question = generator.generate_question() + questions.append(question) + + return questions \ No newline at end of file diff --git a/src/question_generator.py b/src/question_generator.py new file mode 100644 index 0000000..ed473d3 --- /dev/null +++ b/src/question_generator.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +题目生成器模块 +""" + +import random +import math +import re +from abc import ABC, abstractmethod +from typing import List, Set, Optional + + +class Expression: + """ + 数学表达式类 + """ + + def __init__(self, expression_str: str, answer: float): + """ + 初始化表达式 + + @param expression_str: 表达式字符串 + @param answer: 表达式答案 + """ + self.expression_str = expression_str + self.answer = answer + + def __str__(self) -> str: + """ + 返回表达式的字符串表示 + + @return: 表达式字符串 + """ + return self.expression_str + + def __eq__(self, other) -> bool: + """ + 比较两个表达式是否相等 + + @param other: 另一个表达式 + @return: 是否相等 + """ + if isinstance(other, Expression): + return self.expression_str == other.expression_str + return False + + def __hash__(self) -> int: + """ + 计算表达式的哈希值 + + @return: 哈希值 + """ + return hash(self.expression_str) + + +class QuestionGenerator(ABC): + """ + 题目生成器抽象基类 + """ + + def __init__(self): + """ + 初始化题目生成器 + """ + self.generated_questions: Set[str] = set() + self.load_existing_questions() + + def load_existing_questions(self) -> None: + """ + 加载已存在的题目用于查重 + """ + # 在实际应用中,可以从文件或其他存储中加载已存在的题目 + pass + + @abstractmethod + def generate_question(self) -> Expression: + """ + 生成题目(抽象方法) + + @return: 数学表达式 + """ + pass + + +class ElementaryQuestionGenerator(QuestionGenerator): + """ + 小学题目生成器(+, -, *, /, 括号) + """ + + def generate_question(self) -> Expression: + """ + 生成小学题目 + + @return: 数学表达式 + """ + max_attempts = 100 + for _ in range(max_attempts): + # 随机生成操作数数量(2-5个) + num_operands = random.randint(2, 5) + operands = [random.randint(1, 50) for _ in range(num_operands)] + operators = [random.choice(['+', '-', '*']) if random.random() < 0.8 else '/' + for _ in range(num_operands - 1)] # 减少减法和除法概率以避免负数 + + # 随机添加括号 + expression_parts = [] + for i in range(num_operands): + expression_parts.append(str(operands[i])) + if i < len(operators): + expression_parts.append(operators[i]) + + # 随机添加括号 + if num_operands >= 3 and random.random() < 0.3: + # 在随机位置添加括号 + open_pos = random.randint(0, len(expression_parts) - 3) + # 确保括号内至少有两个操作数 + close_pos = min(open_pos + 2 + random.randint(1, 2) * 2, len(expression_parts)) + + # 确保括号位置是操作数位置 + if open_pos % 2 == 0 and close_pos % 2 == 0: + expression_parts.insert(open_pos, '(') + expression_parts.insert(close_pos + 1, ')') + + expression_str = ''.join(expression_parts) + + # 验证表达式是否有效 + try: + # 替换除法符号以便计算 + eval_expr = expression_str.replace('/', '/').replace('*', '*') + result = eval(eval_expr) + + # 确保结果是非负数且是合理的(整数或有限小数) + if isinstance(result, (int, float)) and result >= 0 and abs(result) < 1000: + # 格式化结果,保留合适的小数位数 + if isinstance(result, float) and result.is_integer(): + result = int(result) + + expr = Expression(expression_str, result) + # 检查是否已生成过相同题目 + if str(expr) not in self.generated_questions: + self.generated_questions.add(str(expr)) + # 将表达式中的乘号和除号替换为更易读的形式 + readable_expr_str = expression_str.replace('*', '×').replace('/', '÷') + readable_expr = Expression(readable_expr_str, result) + return readable_expr + except: + continue + + # 如果无法生成有效题目,返回默认题目 + expr = Expression("1+1", 2) + self.generated_questions.add(str(expr)) + readable_expr = Expression("1+1", 2) + return readable_expr + + +class MiddleQuestionGenerator(QuestionGenerator): + """ + 初中题目生成器(包含平方或开根号) + """ + + def generate_question(self) -> Expression: + """ + 生成初中题目 + + @return: 数学表达式 + """ + max_attempts = 100 + for _ in range(max_attempts): + expression_parts = [] + + # 确保至少有一个平方或开根号 + has_square_or_sqrt = True # 确保至少有一个平方或开根号 + + # 生成操作数数量(2-5个) + num_operands = random.randint(2, 5) + + # 标记是否已添加特殊操作 + special_added = False + + for i in range(num_operands): + # 确保至少添加一个平方或开根号 + if not special_added and i == num_operands - 1: + # 如果还没添加特殊操作,强制在最后一个操作数添加 + choice = random.choice([0, 1]) # 提高开根号概率 + if choice == 0: + # 添加平方 + base = random.randint(1, 12) + expression_parts.append(f"{base}²") + else: + # 添加开根号 + square = random.randint(1, 12) + value = square * square + expression_parts.append(f"√{value}") + special_added = True + else: + rand_val = random.random() + # 修改这里的概率,增加开根号和平方的出现频率 + if not special_added and rand_val < 0.6: # 提高特殊操作概率从0.4到0.6 + # 添加特殊操作(平方或开根号) + choice = random.choice([0, 1]) if random.random() < 0.6 else 1 # 提高开根号概率 + if choice == 0: + # 添加平方 + base = random.randint(1, 12) + expression_parts.append(f"{base}²") + else: + # 添加开根号 + square = random.randint(1, 12) + value = square * square + expression_parts.append(f"√{value}") + special_added = True + else: + # 普通操作数 + expression_parts.append(str(random.randint(1, 50))) + + # 添加运算符(除了最后一个操作数) + if i < num_operands - 1: + expression_parts.append(random.choice(['+', '-', '*', '/'])) + + expression_str = ''.join(expression_parts) + + # 处理可能的语法问题 + expression_str = self.fix_expression_syntax(expression_str) + + # 计算结果 + try: + # 替换表达式中的函数以便计算 + eval_expr = expression_str.replace('²', '**2').replace('√', 'math.sqrt') + result = eval(eval_expr) + + # 确保结果是合理的 + if isinstance(result, (int, float)) and abs(result) < 1000 and not math.isnan(result): + # 格式化结果 + if isinstance(result, float) and abs(result - round(result)) < 1e-10: + result = int(round(result)) + + expr = Expression(expression_str, result) + # 检查是否已生成过相同题目 + if str(expr) not in self.generated_questions: + self.generated_questions.add(str(expr)) + # 将表达式中的乘号和除号替换为更易读的形式 + readable_expr_str = expression_str.replace('*', '×').replace('/', '÷') + readable_expr = Expression(readable_expr_str, result) + return readable_expr + except: + continue + + # 如果无法生成有效题目,返回默认题目 + expr = Expression("√4+2²", 6) + self.generated_questions.add(str(expr)) + readable_expr = Expression("√4+2²", 6) + # 将表达式中的乘号和除号替换为更易读的形式 + readable_expr_str = "√4+2²".replace('*', '×').replace('/', '÷') + readable_expr = Expression(readable_expr_str, 6) + return readable_expr + + def fix_expression_syntax(self, expression: str) -> str: + """ + 修复表达式语法问题 + + @param expression: 原始表达式 + @return: 修复后的表达式 + """ + # 确保函数调用之间有运算符 + expression = re.sub(r'(\d)([√])', r'\1*\2', expression) + expression = re.sub(r'(²)([√])', r'\1*\2', expression) + expression = re.sub(r'(\))(\d)', r'\1*\2', expression) + expression = re.sub(r'(\d)(\()', r'\1*\2', expression) + # 修复根号表达式显示,确保根号符号正确显示 + expression = re.sub(r'√(\d+)', r'√\1', expression) + return expression + + +class HighQuestionGenerator(QuestionGenerator): + """ + 高中题目生成器(包含三角函数) + """ + + def generate_question(self) -> Expression: + """ + 生成高中题目 + + @return: 数学表达式 + """ + max_attempts = 100 + for _ in range(max_attempts): + expression_parts = [] + + # 确保至少有一个三角函数 + has_trig_function = random.random() < 0.8 + + # 生成操作数数量(2-4个) + num_operands = random.randint(2, 4) + + for i in range(num_operands): + if has_trig_function and i == 0: + # 第一个操作数有较高概率是三角函数 + if random.random() < 0.33: + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"sin({angle}°)") + elif random.random() < 0.5: + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"cos({angle}°)") + else: + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"tan({angle}°)") + else: + # 其他操作数 + rand_val = random.random() + if rand_val < 0.1 and has_trig_function: + # 添加三角函数 + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"sin({angle}°)") + elif rand_val < 0.2 and has_trig_function: + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"cos({angle}°)") + elif rand_val < 0.3 and has_trig_function: + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"tan({angle}°)") + elif rand_val < 0.55: + # 普通数字 + expression_parts.append(str(random.randint(1, 20))) + elif rand_val < 0.7: + # 平方 + base = random.randint(1, 10) + expression_parts.append(f"{base}²") + else: + # 开根号 + square = random.randint(1, 10) + value = square * square + expression_parts.append(f"√{value}") + + # 添加运算符(除了最后一个操作数) + if i < num_operands - 1: + expression_parts.append(random.choice(['+', '-', '*', '/'])) + + expression_str = ''.join(expression_parts) + + # 处理可能的语法问题 + expression_str = self.fix_expression_syntax(expression_str) + + # 计算结果 + try: + # 替换表达式中的函数以便计算 + eval_expr = expression_str.replace('²', '**2').replace('√', 'math.sqrt') + # 修复:确保正确的替换顺序,先替换角度制三角函数 + eval_expr = re.sub(r'sin\((\d+)°\)', r'math.sin(math.radians(\1))', eval_expr) + eval_expr = re.sub(r'cos\((\d+)°\)', r'math.cos(math.radians(\1))', eval_expr) + eval_expr = re.sub(r'tan\((\d+)°\)', r'math.tan(math.radians(\1))', eval_expr) + + result = eval(eval_expr) + + # 确保结果是合理的 + if isinstance(result, (int, float)) and abs(result) < 1000 and not math.isnan(result) and not math.isinf(result): + # 格式化结果 + if isinstance(result, float) and abs(result - round(result, 10)) < 1e-10: + result = int(round(result)) + # 特殊处理常见的三角函数值,使其更加准确 + elif isinstance(result, float): + # 对于常见的三角函数值进行舍入处理 + if abs(result - 0.5) < 1e-10: # sin(30°) = 0.5 + result = 0.5 + elif abs(result - 0.7071067811865476) < 1e-10: # sin(45°) = cos(45°) ≈ 0.707 + result = round(result, 10) + elif abs(result - 0.8660254037844386) < 1e-10: # sin(60°) ≈ 0.866 + result = round(result, 10) + elif abs(result - 0.5773502691896257) < 1e-10: # tan(30°) ≈ 0.577 + result = round(result, 10) + elif abs(result - 1.7320508075688772) < 1e-10: # tan(60°) ≈ 1.732 + result = round(result, 10) + elif abs(result - 1.0) < 1e-10: # tan(45°) = 1, sin(90°) = 1 + result = 1.0 + + expr = Expression(expression_str, result) + # 检查是否已生成过相同题目 + if str(expr) not in self.generated_questions: + self.generated_questions.add(str(expr)) + # 将表达式中的乘号和除号替换为更易读的形式 + readable_expr_str = expression_str.replace('*', '×').replace('/', '÷') + readable_expr = Expression(readable_expr_str, result) + return readable_expr + except: + continue + + # 如果无法生成有效题目,返回默认题目 + expr = Expression("sin(30°)", 0.5) + self.generated_questions.add(str(expr)) + readable_expr = Expression("sin(30°)", 0.5) + # 将表达式中的乘号和除号替换为更易读的形式 + readable_expr_str = "sin(30°)".replace('*', '×').replace('/', '÷') + readable_expr = Expression(readable_expr_str, 0.5) + return readable_expr + + def fix_expression_syntax(self, expression: str) -> str: + """ + 修复表达式语法问题 + + @param expression: 原始表达式 + @return: 修复后的表达式 + """ + # 确保函数调用之间有运算符 + expression = re.sub(r'(\d)([sincostan√])', r'\1*\2', expression) + expression = re.sub(r'(²)([sincostan√])', r'\1*\2', expression) + expression = re.sub(r'(sqrt\(\d+\))(\d)', r'\1*\2', expression) + expression = re.sub(r'(\))(\d)', r'\1*\2', expression) + expression = re.sub(r'(\d)(\()', r'\1*\2', expression) + expression = re.sub(r'°\)(\d)', r'°)*\1', expression) + return expression \ No newline at end of file diff --git a/src/quiz.py b/src/quiz.py new file mode 100644 index 0000000..80ae9a1 --- /dev/null +++ b/src/quiz.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +测验模块 +""" + +from typing import List, Optional +from question_generator import Expression + + +class Quiz: + """ + 测验类,管理一次答题过程 + """ + + def __init__(self, questions: List[Expression]): + """ + 初始化测验 + + @param questions: 题目列表 + """ + self.questions = questions + self.answers: List[Optional[float]] = [None] * len(questions) + self.current_question_index = 0 + self.score = 0 + + def answer_question(self, answer: float) -> None: + """ + 回答当前题目 + + @param answer: 用户答案 + """ + if 0 <= self.current_question_index < len(self.questions): + self.answers[self.current_question_index] = answer + + def next_question(self) -> bool: + """ + 跳转到下一题 + + @return: 是否有下一题 + """ + if self.current_question_index < len(self.questions) - 1: + self.current_question_index += 1 + return True + return False + + def previous_question(self) -> bool: + """ + 返回上一题 + + @return: 是否有上一题 + """ + if self.current_question_index > 0: + self.current_question_index -= 1 + return True + return False + + def get_current_question(self) -> Optional[Expression]: + """ + 获取当前题目 + + @return: 当前题目 + """ + if 0 <= self.current_question_index < len(self.questions): + return self.questions[self.current_question_index] + return None + + def calculate_score(self) -> float: + """ + 计算得分 + + @return: 得分(百分比) + """ + correct_count = 0 + for i, (question, answer) in enumerate(zip(self.questions, self.answers)): + if answer is not None: + # 允许一定的浮点数误差 + if abs(question.answer - answer) < 1e-6: + correct_count += 1 + + self.score = (correct_count / len(self.questions)) * 100 if self.questions else 0 + return self.score + + def is_finished(self) -> bool: + """ + 检查测验是否已完成 + + @return: 是否已完成 + """ + return self.current_question_index == len(self.questions) - 1 and self.answers[self.current_question_index] is not None \ No newline at end of file diff --git a/src/user_manager.py b/src/user_manager.py new file mode 100644 index 0000000..a214d7e --- /dev/null +++ b/src/user_manager.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +用户管理模块 +""" + +import json +import os +import re +import random +import hashlib +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from typing import Dict, Optional +from tkinter import messagebox + + +class User: + """ + 用户类,存储用户信息 + """ + + def __init__(self, email: str, username: str = "", password_hash: str = "", registration_code: str = ""): + """ + 初始化用户对象 + + @param email: 用户邮箱 + @param username: 用户名 + @param password_hash: 密码哈希值 + @param registration_code: 注册码 + """ + self.email = email + self.username = username + self.password_hash = password_hash + self.registration_code = registration_code + self.is_registered = bool(password_hash) + + +class UserManager: + """ + 用户管理类,负责处理用户注册、登录和密码管理 + """ + + def __init__(self, data_file: str = "users.json"): + """ + 初始化用户管理器 + + @param data_file: 存储用户数据的文件路径 + """ + self.data_file = data_file + self.users: Dict[str, User] = {} + self.current_user: Optional[User] = None + # 邮件配置 + self.smtp_server = "smtp.qq.com" # 可根据需要修改SMTP服务器 + self.smtp_port = 465 + self.sender_email = "3257534544@qq.com" # 需要替换为实际邮箱 + self.sender_password = "pmfyurbkwfpkdbed" # 需要替换为实际应用密码 + self.load_users() + + def load_users(self) -> None: + """ + 从文件加载用户数据 + """ + if os.path.exists(self.data_file): + try: + with open(self.data_file, 'r', encoding='utf-8') as f: + data = json.load(f) + for email, user_data in data.items(): + user = User( + email=email, + username=user_data.get('username', ''), + password_hash=user_data.get('password_hash', ''), + registration_code=user_data.get('registration_code', '') + ) + user.is_registered = user_data.get('is_registered', False) + self.users[email] = user + except (json.JSONDecodeError, FileNotFoundError): + self.users = {} + + def save_users(self) -> None: + """ + 保存用户数据到文件 + """ + data = {} + for email, user in self.users.items(): + data[email] = { + 'username': user.username, + 'password_hash': user.password_hash, + 'registration_code': user.registration_code, + 'is_registered': user.is_registered + } + + try: + with open(self.data_file, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + except IOError as e: + messagebox.showerror("错误", f"保存用户数据失败: {str(e)}") + + def is_valid_email(self, email: str) -> bool: + """ + 验证邮箱格式 + + @param email: 邮箱地址 + @return: 邮箱格式是否有效 + """ + pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + return re.match(pattern, email) is not None + + def is_valid_username(self, username: str) -> bool: + """ + 验证用户名是否符合要求 + + @param username: 用户名 + @return: 用户名是否有效 + """ + # 用户名长度应在3-20个字符之间 + if not (3 <= len(username) <= 20): + return False + # 用户名只能包含字母、数字和下划线 + pattern = r'^[a-zA-Z0-9_]+$' + return re.match(pattern, username) is not None + + def generate_registration_code(self) -> str: + """ + 生成注册码 + + @return: 6位数字注册码 + """ + return str(random.randint(100000, 999999)) + + def hash_password(self, password: str) -> str: + """ + 对密码进行哈希处理 + + @param password: 原始密码 + @return: 哈希后的密码 + """ + return hashlib.sha256(password.encode('utf-8')).hexdigest() + + def is_valid_password(self, password: str) -> bool: + """ + 验证密码是否符合要求 + + @param password: 密码 + @return: 密码是否有效 + """ + if not (6 <= len(password) <= 10): + return False + + has_lower = any(c.islower() for c in password) + has_upper = any(c.isupper() for c in password) + has_digit = any(c.isdigit() for c in password) + + return has_lower and has_upper and has_digit + + def register_user(self, email: str, username: str) -> bool: + """ + 注册新用户(发送注册码) + + @param email: 用户邮箱 + @param username: 用户名 + @return: 是否成功发送注册码 + """ + if not self.is_valid_email(email): + messagebox.showerror("错误", "邮箱格式不正确") + return False + + if not self.is_valid_username(username): + messagebox.showerror("错误", "用户名应为3-20位,只能包含字母、数字和下划线") + return False + + # 检查邮箱是否已被注册 + if email in self.users and self.users[email].is_registered: + messagebox.showerror("错误", "该邮箱已注册") + return False + + # 检查用户名是否已被使用 + for user in self.users.values(): + if user.username == username and user.is_registered: + messagebox.showerror("错误", "该用户名已存在") + return False + + registration_code = self.generate_registration_code() + user = User(email=email, username=username, registration_code=registration_code) + self.users[email] = user + + # 尝试发送邮件 + if self.send_registration_code_via_email(email, registration_code): + self.save_users() + messagebox.showinfo("成功", "注册码已发送到您的邮箱,请查收") + return True + else: + messagebox.showerror("错误", "无法发送注册码到邮箱,请检查网络连接或邮箱地址") + # 即使邮件发送失败,也保存用户信息以便重试 + self.save_users() + return False + + def send_registration_code_via_email(self, email: str, code: str) -> bool: + """ + 通过电子邮件发送注册码 + + @param email: 接收邮箱 + @param code: 注册码 + @return: 是否发送成功 + """ + try: + # 创建邮件内容 + message = MIMEMultipart() + message["From"] = self.sender_email + message["To"] = email + message["Subject"] = "数学练习系统注册码" + + body = f""" + 您好! + + 欢迎使用数学练习系统! + + 您的注册码是: {code} + + 请在注册界面输入此注册码完成注册。 + + 如果您没有请求此注册码,请忽略此邮件。 + + 祝学习愉快! + 数学练习系统团队 + """ + + message.attach(MIMEText(body, "plain", "utf-8")) + + # 使用SMTP_SSL连接QQ邮箱服务器 + server = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) + server.login(self.sender_email, self.sender_password) + text = message.as_string() + server.sendmail(self.sender_email, email, text) + server.quit() + + return True + except Exception as e: + print(f"邮件发送失败: {str(e)}") + return False + + def verify_registration_code(self, email: str, code: str) -> bool: + """ + 验证注册码 + + @param email: 用户邮箱 + @param code: 用户输入的注册码 + @return: 注册码是否正确 + """ + if email not in self.users: + messagebox.showerror("错误", "请先获取注册码") + return False + + user = self.users[email] + if user.registration_code != code: + messagebox.showerror("错误", "注册码不正确") + return False + + return True + + def set_password(self, email: str, password: str) -> bool: + """ + 设置用户密码 + + @param email: 用户邮箱 + @param password: 用户密码 + @return: 是否设置成功 + """ + if not self.is_valid_password(password): + messagebox.showerror("错误", "密码必须为6-10位,且包含大小写字母和数字") + return False + + if email not in self.users: + messagebox.showerror("错误", "用户不存在") + return False + + user = self.users[email] + user.password_hash = self.hash_password(password) + user.is_registered = True + self.save_users() + return True + + def login(self, username: str, password: str) -> bool: + """ + 用户登录 + + @param username: 用户名 + @param password: 用户密码 + @return: 是否登录成功 + """ + # 根据用户名查找用户 + user = None + for u in self.users.values(): + if u.username == username: + user = u + break + + if user is None: + messagebox.showerror("错误", "用户不存在") + return False + + if not user.is_registered: + messagebox.showerror("错误", "请先完成注册") + return False + + if user.password_hash != self.hash_password(password): + messagebox.showerror("错误", "密码不正确") + return False + + self.current_user = user + return True + + def change_password(self, old_password: str, new_password: str) -> bool: + """ + 修改用户密码 + + @param old_password: 原密码 + @param new_password: 新密码 + @return: 是否修改成功 + """ + if not self.current_user: + messagebox.showerror("错误", "用户未登录") + return False + + if self.current_user.password_hash != self.hash_password(old_password): + messagebox.showerror("错误", "原密码不正确") + return False + + if not self.is_valid_password(new_password): + messagebox.showerror("错误", "新密码必须为6-10位,且包含大小写字母和数字") + return False + + self.current_user.password_hash = self.hash_password(new_password) + self.save_users() + return True + + def logout(self) -> None: + """ + 用户登出 + """ + self.current_user = None + + def delete_account(self, email: str, password: str) -> bool: + """ + 注销账户 + + @param email: 用户邮箱 + @param password: 用户密码 + @return: 是否注销成功 + """ + if not self.is_valid_email(email): + messagebox.showerror("错误", "邮箱格式不正确") + return False + + # 检查用户是否存在 + if email not in self.users: + messagebox.showerror("错误", "该邮箱未注册") + return False + + user = self.users[email] + + # 检查用户是否已完成注册 + if not user.is_registered: + messagebox.showerror("错误", "该账户未完成注册") + return False + + # 验证密码 + if user.password_hash != self.hash_password(password): + messagebox.showerror("错误", "密码不正确") + return False + + # 删除用户 + del self.users[email] + # 如果当前用户是被删除的用户,则登出 + if self.current_user and self.current_user.email == email: + self.current_user = None + + self.save_users() + return True diff --git a/src/users.json b/src/users.json new file mode 100644 index 0000000..0e0979a --- /dev/null +++ b/src/users.json @@ -0,0 +1,8 @@ +{ + "1426688201@qq.com": { + "username": "111", + "password_hash": "c4318372f98f4c46ed3a32c16ee4d7a76c832886d887631c0294b3314f34edf1", + "registration_code": "939598", + "is_registered": true + } +} \ No newline at end of file