diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..e6108d0 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,132 @@ +软1_[彭云昊]_[王祖旺]_结对项目 +中小学数学学习软件 - 结对编程项目 +项目简介 +本项目是一个面向中小学学生的数学学习桌面应用程序,提供个性化的数学题目练习和评估功能。系统根据学生所在学段(小学、初中、高中)生成相应难度的数学题目,通过图形化界面提供友好的学习体验。 +功能特性 +用户管理 +• ✅ 用户注册:通过邮箱验证码完成注册 +• ✅ 密码设置:6-10位,必须包含大小写字母和数字 +• ✅ 用户登录:安全的身份验证机制 +• ✅ 密码修改:支持原密码验证 +• ✅ 账户注销:永久删除账户及所有数据 +题目生成 +• ✅ 小学题目:加减乘除四则运算,支持括号 +• ✅ 初中题目:平方、开根运算 +• ✅ 高中题目:三角函数计算 +• ✅ 智能防重复:确保同一试卷无重复题目 +学习流程 +• ✅ 难度选择:小学/初中/高中三级难度 +• ✅ 题目数量:用户自定义题目数量(10-30题) +• ✅ 选择题形式:每题4个选项,单选作答 +• ✅ 实时评分:提交后立即显示得分情况 +• ✅ 学习延续:支持连续练习或退出选择 +• ✅ 答题进度:支持上一题/下一题导航 +技术栈 +• 编程语言:Python 3.x +• GUI框架:Tkinter(内置Python GUI库) +• 数据存储:JSON文件(无需数据库) +• 邮件服务:SMTP协议(QQ邮箱) +• 架构模式:MVC(模型-视图-控制器) +项目结构 +text +复制 +下载 +软1_[彭云昊]_[王祖旺]_结对项目/ +├── main.py # 程序入口 +├── main_app.py # 主应用模块,界面控制器 +├── user_manager.py # 用户管理模块 +├── question_bank.py # 题库管理模块 +├── question_generator.py # 题目生成器模块 +├── quiz.py # 测验管理模块 +├── users.json # 用户数据文件(运行时生成) +└── README.md # 项目说明文档 +安装与运行 +环境要求 +• Python 3.11.9 或更高版本 +• 网络连接(用于邮箱验证码发送) +运行步骤 +1. 下载项目文件到本地 +2. 确保所有Python文件在同一目录下 +3. 运行主程序: +bash +复制 +下载 +python main.py +4. 按照界面提示进行注册和登录 +预设测试账号 +系统支持新用户注册,也可使用以下测试账号: +• 邮箱:任意有效邮箱(接收验证码) +• 密码:符合规范的密码(如:Abc123) +分支管理 +本项目遵循Git分支管理规范: +• main分支:稳定版本,存放经过测试的代码 +• develop分支:开发主线,集成最新功能 +• 个人分支:每位开发者的功能分支(如:zhangsan_branch) +代码提交规则 +• 源代码:必须通过个人分支 + Pull Request +• 文档:直接推送到develop分支 +开发规范 +代码规范 +• 遵循PEP 8 Python编码规范 +• 使用类型注解提高代码可读性 +• 模块化设计,高内聚低耦合 +提交信息规范 +• feat: 新功能 +• fix: 修复bug +• docs: 文档更新 +• style: 代码格式调整 +• refactor: 代码重构 +功能模块详解 +1. 用户认证模块 (user_manager.py) +• 邮箱格式验证 +• 密码强度校验 +• 验证码发送与验证 +• 用户数据持久化(JSON文件) +2. 题目生成模块 (question_generator.py) +• 小学题目:2-5个数字的加减乘除运算,确保结果非负 +• 初中题目:平方运算(1-12)、开根运算(完全平方数) +• 高中题目:三角函数(sin/cos/tan)特殊角度计算 +• 选项生成:智能生成4个合理选项,包含正确答案 +3. 界面模块 (main_app.py) +• 响应式图形界面设计 +• 实时输入验证 +• 友好的用户交互反馈 +• 多框架界面切换 +数据存储 +项目使用JSON文件存储用户数据,文件结构如下: +json +复制 +下载 +{ + "user@example.com": { + "username": "张三", + "password_hash": "加密密码", + "registration_code": "注册码", + "is_registered": true + } +} +配置说明 +在 user_manager.py 中配置以下参数: +• 邮箱服务配置(SMTP服务器、端口、授权码) +• 用户数据文件路径 +测试用例 +功能测试 +• 用户注册流程测试 +• 登录验证测试 +• 密码修改测试 +• 题目生成测试(各学段) +• 答题评分测试 +边界测试 +• 密码格式边界测试 +• 题目数量边界测试 +• 邮箱格式验证测试 +已知限制 +• 邮箱服务:依赖QQ邮箱SMTP服务,需配置正确的授权码 +• 题目数量:建议10-30题,过多可能影响性能 +• 网络要求:发送验证码需要网络连接 +• 平台兼容:主要支持Windows,其他平台可能需调整 +开发团队 +• 班级:软1 +• 组长:[彭云昊](202326010111) +• 组员:[王祖旺](202326010117) + diff --git a/src/__pycache__/backend_service.cpython-311.pyc b/src/__pycache__/backend_service.cpython-311.pyc new file mode 100644 index 0000000..fa4694d Binary files /dev/null and b/src/__pycache__/backend_service.cpython-311.pyc differ diff --git a/src/__pycache__/main_app.cpython-311.pyc b/src/__pycache__/main_app.cpython-311.pyc index e3f5ecf..7db10b0 100644 Binary files a/src/__pycache__/main_app.cpython-311.pyc and b/src/__pycache__/main_app.cpython-311.pyc differ diff --git a/src/__pycache__/main_app.cpython-313.pyc b/src/__pycache__/main_app.cpython-313.pyc new file mode 100644 index 0000000..bd55c36 Binary files /dev/null and b/src/__pycache__/main_app.cpython-313.pyc differ diff --git a/src/__pycache__/question_bank.cpython-311.pyc b/src/__pycache__/question_bank.cpython-311.pyc index bd940b3..68d5b96 100644 Binary files a/src/__pycache__/question_bank.cpython-311.pyc and b/src/__pycache__/question_bank.cpython-311.pyc differ diff --git a/src/__pycache__/question_bank.cpython-313.pyc b/src/__pycache__/question_bank.cpython-313.pyc new file mode 100644 index 0000000..430b106 Binary files /dev/null and b/src/__pycache__/question_bank.cpython-313.pyc differ diff --git a/src/__pycache__/question_generator.cpython-311.pyc b/src/__pycache__/question_generator.cpython-311.pyc index d0555e0..1af138a 100644 Binary files a/src/__pycache__/question_generator.cpython-311.pyc and b/src/__pycache__/question_generator.cpython-311.pyc differ diff --git a/src/__pycache__/question_generator.cpython-313.pyc b/src/__pycache__/question_generator.cpython-313.pyc new file mode 100644 index 0000000..4e94173 Binary files /dev/null and b/src/__pycache__/question_generator.cpython-313.pyc differ diff --git a/src/__pycache__/quiz.cpython-311.pyc b/src/__pycache__/quiz.cpython-311.pyc index 7fb1b35..7e32e5b 100644 Binary files a/src/__pycache__/quiz.cpython-311.pyc and b/src/__pycache__/quiz.cpython-311.pyc differ diff --git a/src/__pycache__/quiz.cpython-313.pyc b/src/__pycache__/quiz.cpython-313.pyc new file mode 100644 index 0000000..068c528 Binary files /dev/null and b/src/__pycache__/quiz.cpython-313.pyc differ diff --git a/src/__pycache__/user_manager.cpython-311.pyc b/src/__pycache__/user_manager.cpython-311.pyc index 04ceb36..e5767b9 100644 Binary files a/src/__pycache__/user_manager.cpython-311.pyc and b/src/__pycache__/user_manager.cpython-311.pyc differ diff --git a/src/__pycache__/user_manager.cpython-313.pyc b/src/__pycache__/user_manager.cpython-313.pyc new file mode 100644 index 0000000..273925c Binary files /dev/null and b/src/__pycache__/user_manager.cpython-313.pyc differ diff --git a/src/backend_service.py b/src/backend_service.py new file mode 100644 index 0000000..f8e2f43 --- /dev/null +++ b/src/backend_service.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +后端服务模块,作为前端与后端之间的通信中介 +""" + +from typing import List, Optional, Dict, Any + +from user_manager import UserManager +from question_bank import QuestionBank +from quiz import Quiz + + +class BackendService: + """ + 后端服务类,作为前端与实际业务逻辑模块之间的通信中介 + """ + + def __init__(self): + """ + 初始化后端服务 + """ + self.user_manager = UserManager() + self.question_bank = QuestionBank() + self.current_quiz: Optional[Quiz] = None + + def login(self, username: str, password: str) -> bool: + """ + 用户登录 + + @param username: 用户名 + @param password: 密码 + @return: 登录是否成功 + """ + return self.user_manager.login(username, password) + + def register_user(self, email: str, username: str) -> bool: + """ + 注册新用户 + + @param email: 邮箱 + @param username: 用户名 + @return: 是否成功发送注册码 + """ + return self.user_manager.register_user(email, username) + + def verify_registration_code(self, email: str, code: str) -> bool: + """ + 验证注册码 + + @param email: 邮箱 + @param code: 注册码 + @return: 验证是否成功 + """ + return self.user_manager.verify_registration_code(email, code) + + def set_password(self, email: str, password: str) -> bool: + """ + 设置密码 + + @param email: 邮箱 + @param password: 密码 + @return: 是否设置成功 + """ + return self.user_manager.set_password(email, password) + + def change_password(self, old_password: str, new_password: str) -> bool: + """ + 修改密码 + + @param old_password: 原密码 + @param new_password: 新密码 + @return: 是否修改成功 + """ + return self.user_manager.change_password(old_password, new_password) + + def logout(self): + """ + 退出登录 + """ + self.user_manager.logout() + + def delete_account(self, email: str, password: str) -> bool: + """ + 删除账户 + + @param email: 邮箱 + @param password: 密码 + @return: 是否删除成功 + """ + return self.user_manager.delete_account(email, password) + + def start_quiz(self, level: str, question_count: int) -> bool: + """ + 开始测验 + + @param level: 题目难度等级 + @param question_count: 题目数量 + @return: 是否成功开始测验 + """ + try: + questions = self.question_bank.generate_questions(level, question_count) + self.current_quiz = Quiz(questions) + return True + except Exception: + return False + + def get_current_question(self): + """ + 获取当前题目 + + @return: 当前题目 + """ + if self.current_quiz: + return self.current_quiz.get_current_question() + return None + + def get_current_options(self) -> List[str]: + """ + 获取当前题目的选项 + + @return: 选项列表 + """ + if self.current_quiz: + return self.current_quiz.get_current_options() + return [] + + def answer_question(self, answer: str) -> bool: + """ + 回答当前题目 + + @param answer: 答案 + @return: 是否回答成功 + """ + if self.current_quiz: + try: + self.current_quiz.answer_question(answer) + return True + except Exception: + return False + return False + + def next_question(self) -> bool: + """ + 跳转到下一题 + + @return: 是否成功跳转 + """ + if self.current_quiz: + return self.current_quiz.next_question() + return False + + def previous_question(self) -> bool: + """ + 跳转到上一题 + + @return: 是否成功跳转 + """ + if self.current_quiz: + return self.current_quiz.previous_question() + return False + + def is_quiz_finished(self) -> bool: + """ + 检查测验是否已完成 + + @return: 测验是否已完成 + """ + if self.current_quiz: + return self.current_quiz.is_finished() + return True + + def calculate_score(self) -> float: + """ + 计算测验得分 + + @return: 得分百分比 + """ + if self.current_quiz: + return self.current_quiz.calculate_score() + return 0.0 + + def get_quiz_progress(self) -> Dict[str, Any]: + """ + 获取测验进度信息 + + @return: 进度信息字典 + """ + if self.current_quiz: + return { + "current_index": self.current_quiz.current_question_index, + "total_questions": len(self.current_quiz.questions), + "current_answer": self.current_quiz.answers[ + self.current_quiz.current_question_index] if self.current_quiz.answers else None + } + return { + "current_index": 0, + "total_questions": 0, + "current_answer": None + } + + def get_quiz_result_details(self) -> Dict[str, Any]: + """ + 获取测验结果详情 + + @return: 结果详情字典 + """ + if not self.current_quiz: + return {} + + correct_count = 0 + for q, a in zip(self.current_quiz.questions, self.current_quiz.answers): + if a is not None: + try: + # 将字符串答案转换为浮点数进行比较 + if abs(q.answer - float(a)) < 1e-6: + correct_count += 1 + except ValueError: + # 如果转换失败,说明答案无效,不计入正确答案 + pass + + total_count = len(self.current_quiz.questions) + + return { + "correct_count": correct_count, + "total_count": total_count + } + + def get_current_username(self) -> str: + """ + 获取当前登录用户的用户名 + + @return: 用户名 + """ + if self.user_manager.current_user: + return self.user_manager.current_user.username + return "未知用户" diff --git a/src/main_app.py b/src/main_app.py index 99591b2..644a768 100644 --- a/src/main_app.py +++ b/src/main_app.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +# !/usr/bin/env python3 # -*- coding: utf-8 -*- """ @@ -7,14 +7,11 @@ 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 backend_service import BackendService from question_generator import Expression - +from quiz import Quiz class MathQuizApp: """ @@ -24,7 +21,7 @@ class MathQuizApp: def __init__(self, root: tk.Tk): """ 初始化应用程序 - + @param root: Tk根窗口 """ self.root = root @@ -32,14 +29,13 @@ class MathQuizApp: self.root.geometry("700x600") self.root.resizable(True, True) self.root.configure(bg="#f0f0f0") - + # 初始化系统组件 - self.user_manager = UserManager() - self.question_bank = QuestionBank() - + self.backend_service = BackendService() + # 设置样式 self.setup_styles() - + # 创建不同的界面框架 self.login_frame = tk.Frame(self.root, bg="#f0f0f0") self.register_frame = tk.Frame(self.root, bg="#f0f0f0") @@ -49,13 +45,13 @@ class MathQuizApp: 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.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() @@ -68,7 +64,7 @@ class MathQuizApp: 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" @@ -92,20 +88,23 @@ class MathQuizApp: 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.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]: + 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) @@ -116,36 +115,56 @@ class MathQuizApp: # 清除之前的内容 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 = 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) - + 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") + + 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") + + 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) - + + 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): @@ -154,15 +173,15 @@ class MathQuizApp: """ 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): + + if self.backend_service.login(username, password): messagebox.showinfo("成功", "登录成功") self.show_main_menu_frame() - # 错误信息在user_manager.login中已经显示 + # 错误信息在backend_service.login中已经显示 def show_register_frame(self): """ @@ -171,41 +190,65 @@ class MathQuizApp: # 清除之前的内容 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 = 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) - + 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") + + 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") + + 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") + + 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) - + + 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): @@ -214,13 +257,13 @@ class MathQuizApp: """ 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("成功", "注册码已发送,请查收") + + # 注意:register_user 方法内部已经包含了消息提示,不需要额外的消息框 + self.backend_service.register_user(email, username) def verify_registration_code(self): """ @@ -228,76 +271,95 @@ class MathQuizApp: """ 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): + + if self.backend_service.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 = 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) - + 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") + + 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") + + 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) - + + 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("成功", "密码设置成功,请登录") + + if self.backend_service.set_password(email, password): self.show_login_frame() def show_main_menu_frame(self): @@ -307,74 +369,105 @@ class MathQuizApp: # 清除之前的内容 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 = 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) - + tk.Label(title_frame, text="主菜单", font=self.title_font, + bg=self.primary_color, fg="white").pack(pady=20) + + username = self.backend_service.get_current_username() + tk.Label(main_frame, text=f"欢迎, {username}!", + 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) - + + 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 = 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"} - + 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) - + 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") + + tk.Label(form_frame, text="请输入题目数量 (10-30):", + 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) - + + 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): @@ -383,142 +476,135 @@ class MathQuizApp: """ try: count = int(self.question_count_entry.get()) - if not (1 <= count <= 20): - messagebox.showerror("错误", "题目数量必须在1-20之间") + if not (10 <= count <= 30): + messagebox.showerror("错误", "题目数量必须在10-30之间") 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) + if self.backend_service.start_quiz(self.current_level, self.question_count): self.show_quiz_frame() - except Exception as e: - messagebox.showerror("错误", f"生成题目时出错: {str(e)}") + else: + messagebox.showerror("错误", "生成题目时出错") def show_quiz_frame(self): """ 显示答题界面 """ - if not self.current_quiz: + current_question = self.backend_service.get_current_question() + if not current_question: 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 = 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_info = self.backend_service.get_quiz_progress() + progress = f"题目 {progress_info['current_index'] + 1}/{progress_info['total_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) - + 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)) + + 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(question_frame, text=question_text, + font=("Arial", 16, "bold"), bg="#ffffff", + wraplength=500).pack( + pady=10) + + # 获取固定选项 + options = self.backend_service.get_current_options() + # 显示选项 - tk.Label(main_frame, text="请选择答案:", font=self.normal_font, bg="#ffffff").pack(pady=(20, 10)) - + tk.Label(main_frame, text="请选择答案:", font=self.normal_font, + bg="#ffffff").pack(pady=(20, 10)) + self.answer_var = tk.StringVar() - self.answer_var.set(" ") # 明确设置初始值为空字符串,确保不选中任何选项 - + # 设置默认选项为用户之前的选择(如果有) + current_answer = progress_info['current_answer'] + if current_answer is not None: + self.answer_var.set(current_answer) + else: + 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) - + tk.Radiobutton(options_frame, + text=f"{['A', 'B', 'C', 'D'][i]}. {option}", + variable=self.answer_var, value=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] + if progress_info['current_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 progress_info['current_index'] < progress_info['total_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 submit_answer(self): """ 提交答案 """ - if not self.current_quiz: - return - answer_str = self.answer_var.get() - if not answer_str: + if not answer_str: # 检查字符串是否为空 messagebox.showerror("错误", "请选择一个答案") return - + try: - answer = float(answer_str) - self.current_quiz.answer_question(answer) - + # 验证答案是否为有效数字 + float(answer_str) + if not self.backend_service.answer_question(answer_str): + messagebox.showerror("错误", "提交答案失败") + return + # 如果是最后一题,显示结果 - if self.current_quiz.is_finished(): + if self.backend_service.is_quiz_finished(): self.show_result_frame() else: messagebox.showinfo("提示", "答案已提交") @@ -529,23 +615,22 @@ class MathQuizApp: """ 下一题 """ - if not self.current_quiz: - return - # 保存当前答案(如果有选择) answer_str = self.answer_var.get() - if answer_str: + if answer_str: # 检查字符串是否非空 try: - answer = float(answer_str) - self.current_quiz.answer_question(answer) + # 验证答案是否为有效数字 + float(answer_str) + self.backend_service.answer_question(answer_str) except ValueError: pass # 如果答案无效,保持为None - - if self.current_quiz.next_question(): + + if self.backend_service.next_question(): self.show_quiz_frame() else: # 已经是最后一题 - if self.current_quiz.answers[self.current_quiz.current_question_index] is not None: + progress_info = self.backend_service.get_quiz_progress() + if progress_info['current_answer'] is not None: # 如果最后一题已答题,显示结果 self.show_result_frame() else: @@ -555,51 +640,52 @@ class MathQuizApp: """ 上一题 """ - if not self.current_quiz: - return - # 保存当前答案(如果有选择) answer_str = self.answer_var.get() - if answer_str: + if answer_str: # 检查字符串是否非空 try: - answer = float(answer_str) - self.current_quiz.answer_question(answer) + # 验证答案是否为有效数字 + float(answer_str) + self.backend_service.answer_question(answer_str) except ValueError: pass # 如果答案无效,保持为None - - if self.current_quiz.previous_question(): + + if self.backend_service.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() - + score = self.backend_service.calculate_score() + # 创建结果界面 - main_frame = tk.Frame(self.result_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + 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) - + 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) - + 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 = "优秀! 继续保持!" @@ -613,27 +699,38 @@ class MathQuizApp: 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) - + + 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) + result_details = self.backend_service.get_quiz_result_details() + correct_count = result_details.get("correct_count", 0) + total_count = result_details.get("total_count", 0) + 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) - + 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): @@ -643,39 +740,63 @@ class MathQuizApp: # 清除之前的内容 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 = 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) - + 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") + + 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") + + 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") + + 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) - + + 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): @@ -685,25 +806,25 @@ class MathQuizApp: 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): + + if self.backend_service.change_password(old_password, new_password): messagebox.showinfo("成功", "密码修改成功") self.show_main_menu_frame() - # 错误信息在user_manager.change_password中已经显示 + # 错误信息在backend_service.change_password中已经显示 def logout(self): """ 退出登录 """ - self.user_manager.logout() + self.backend_service.logout() self.show_login_frame() def show_delete_account_frame(self): @@ -713,41 +834,63 @@ class MathQuizApp: # 清除之前的内容 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 = 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) - + 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) - + + 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") + + 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") + + 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.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) - + 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): @@ -756,17 +899,18 @@ class MathQuizApp: """ 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("成功", "账户已注销,您可以使用该邮箱重新注册") + if self.backend_service.delete_account(email, password): + messagebox.showinfo("成功", + "账户已注销,您可以使用该邮箱重新注册") self.show_login_frame() - # 错误信息在user_manager.delete_account中已经显示 + # 错误信息在backend_service.delete_account中已经显示 def delete_account(self): """ diff --git a/src/question_generator.py b/src/question_generator.py index ed473d3..54bdca4 100644 --- a/src/question_generator.py +++ b/src/question_generator.py @@ -14,7 +14,7 @@ from typing import List, Set, Optional class Expression: """ - 数学表达式类 + 数学表达式类(辅助类) """ def __init__(self, expression_str: str, answer: float): @@ -97,42 +97,16 @@ class ElementaryQuestionGenerator(QuestionGenerator): """ 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) - + expression_str = self._generate_elementary_expression() # 验证表达式是否有效 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, (int, float)) and + result >= 0 and + abs(result) < 1000): if isinstance(result, float) and result.is_integer(): result = int(result) @@ -141,7 +115,8 @@ class ElementaryQuestionGenerator(QuestionGenerator): if str(expr) not in self.generated_questions: self.generated_questions.add(str(expr)) # 将表达式中的乘号和除号替换为更易读的形式 - readable_expr_str = expression_str.replace('*', '×').replace('/', '÷') + readable_expr_str = expression_str.replace('*', '×') + readable_expr_str = readable_expr_str.replace('/', '÷') readable_expr = Expression(readable_expr_str, result) return readable_expr except: @@ -153,6 +128,65 @@ class ElementaryQuestionGenerator(QuestionGenerator): readable_expr = Expression("1+1", 2) return readable_expr + def _generate_elementary_expression(self) -> str: + """ + 生成小学题目表达式字符串 + + @return: 表达式字符串 + """ + # 随机生成操作数数量(2-5个) + num_operands = random.randint(2, 5) + # 修改操作数范围从1-50到1-100 + operands = [random.randint(1, 100) for _ in range(num_operands)] + operators = [random.choice(['+', '-', '*']) if random.random() < 0.8 else '/' + for _ in range(num_operands - 1)] # 减少减法和除法概率以避免负数 + + # 构建表达式部分 + expression_parts = self._build_expression_parts(operands, operators) + + # 随机添加括号 + expression_parts = self._add_parentheses(expression_parts, num_operands) + + return ''.join(expression_parts) + + def _build_expression_parts(self, operands: List[int], + operators: List[str]) -> List[str]: + """ + 构建表达式部分 + + @param operands: 操作数列表 + @param operators: 操作符列表 + @return: 表达式部分列表 + """ + expression_parts = [] + for i in range(len(operands)): + expression_parts.append(str(operands[i])) + if i < len(operators): + expression_parts.append(operators[i]) + return expression_parts + + def _add_parentheses(self, expression_parts: List[str], + num_operands: int) -> List[str]: + """ + 随机添加括号到表达式 + + @param expression_parts: 表达式部分列表 + @param num_operands: 操作数数量 + @return: 添加括号后的表达式部分列表 + """ + 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, ')') + return expression_parts + class MiddleQuestionGenerator(QuestionGenerator): """ @@ -167,93 +201,161 @@ class MiddleQuestionGenerator(QuestionGenerator): """ 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._generate_middle_expression() expression_str = self.fix_expression_syntax(expression_str) - # 计算结果 try: # 替换表达式中的函数以便计算 - eval_expr = expression_str.replace('²', '**2').replace('√', 'math.sqrt') + eval_expr = self._prepare_middle_expression_for_eval(expression_str) result = eval(eval_expr) - # 确保结果是合理的 - if isinstance(result, (int, float)) and abs(result) < 1000 and not math.isnan(result): + 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_str = expression_str.replace('*', '×') + readable_expr_str = readable_expr_str.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 _generate_middle_expression(self) -> str: + """ + 生成初中题目表达式字符串 + + @return: 表达式字符串 + """ + expression_parts = [] + + # 生成操作数数量(2-5个) + num_operands = random.randint(2, 5) + + # 标记是否已添加特殊操作 + special_added = False + + # 生成表达式部分 + expression_parts, special_added = self._build_middle_expression_parts( + expression_parts, num_operands, special_added) + + return ''.join(expression_parts) + + def _build_middle_expression_parts(self, expression_parts: List[str], + num_operands: int, + special_added: bool) -> tuple: + """ + 构建初中表达式部分 + + @param expression_parts: 表达式部分列表 + @param num_operands: 操作数数量 + @param special_added: 是否已添加特殊操作标记 + @return: (表达式部分列表, 是否已添加特殊操作标记) + """ + for i in range(num_operands): + # 确保至少添加一个平方或开根号 + if not special_added and i == num_operands - 1: + # 如果还没添加特殊操作,强制在最后一个操作数添加 + expression_parts, special_added = self._add_forced_special_operation( + expression_parts) + else: + expression_parts, special_added = self._add_middle_operand_or_operation( + expression_parts, special_added, i, num_operands) + + # 添加运算符(除了最后一个操作数) + if i < num_operands - 1: + expression_parts.append(random.choice(['+', '-', '*', '/'])) + + return expression_parts, special_added + + def _add_forced_special_operation(self, expression_parts: List[str]) -> tuple: + """ + 强制添加特殊操作(平方或开根号) + + @param expression_parts: 表达式部分列表 + @return: (表达式部分列表, 是否已添加特殊操作标记) + """ + choice = random.choice([0, 1]) # 0为平方,1为开根号 + if choice == 0: + # 添加平方 + base = random.randint(1, 100) + expression_parts.append(f"{base}²") + else: + # 添加开根号(确保至少有一个开根号) + square = random.randint(1, 10) + value = square * square + expression_parts.append(f"√{value}") + return expression_parts, True + + def _add_middle_operand_or_operation(self, expression_parts: List[str], + special_added: bool, i: int, + num_operands: int) -> tuple: + """ + 添加初中操作数或操作 + + @param expression_parts: 表达式部分列表 + @param special_added: 是否已添加特殊操作标记 + @param i: 当前索引 + @param num_operands: 操作数总数 + @return: (表达式部分列表, 是否已添加特殊操作标记) + """ + rand_val = random.random() + # 修改这里的概率,增加开根号和平方的出现频率 + if not special_added and rand_val < 0.6: + # 添加特殊操作(平方或开根号) + expression_parts, special_added = self._add_middle_special_operation() + else: + # 普通操作数 + expression_parts.append(str(random.randint(1, 100))) + return expression_parts, special_added + + return expression_parts, special_added + + def _add_middle_special_operation(self) -> tuple: + """ + 添加初中特殊操作(平方或开根号) + + @return: (表达式部分列表, 是否已添加特殊操作标记) + """ + choice = random.choice([0, 1]) # 平方和开根号的概率相等 + expression_parts = [] + if choice == 0: + # 添加平方 + base = random.randint(1, 100) + expression_parts.append(f"{base}²") + else: + # 添加开根号 + square = random.randint(1, 10) + value = square * square + expression_parts.append(f"√{value}") + return expression_parts, True + + def _prepare_middle_expression_for_eval(self, expression_str: str) -> str: + """ + 准备初中题目表达式用于eval计算 + + @param expression_str: 原始表达式字符串 + @return: 可用于eval计算的表达式字符串 + """ + # 替换表达式中的函数以便计算 + eval_expr = expression_str.replace('²', '**2') + # 使用正则表达式正确处理平方根 + eval_expr = re.sub(r'√(\d+)', + r'math.sqrt(\1)', + eval_expr) + return eval_expr + def fix_expression_syntax(self, expression: str) -> str: """ 修复表达式语法问题 @@ -266,8 +368,6 @@ class MiddleQuestionGenerator(QuestionGenerator): 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 @@ -279,110 +379,34 @@ 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._generate_high_expression() 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) - + eval_expr = self._prepare_high_expression_for_eval(expression_str) 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 - + if (isinstance(result, (int, float)) and + abs(result) < 1000 and + not math.isnan(result) and + not math.isinf(result)): + # 格式化结果,保留两位小数 + result = self._format_high_result(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_str = expression_str.replace('*', '×') + readable_expr_str = readable_expr_str.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) @@ -391,6 +415,189 @@ class HighQuestionGenerator(QuestionGenerator): readable_expr = Expression(readable_expr_str, 0.5) return readable_expr + def _generate_high_expression(self) -> str: + """ + 生成高中题目表达式字符串 + + @return: 表达式字符串 + """ + expression_parts = [] + + # 生成操作数数量(2-4个) + num_operands = random.randint(2, 5) + + # 标记是否已添加三角函数 + trig_added = False + + # 生成表达式部分 + expression_parts, trig_added = self._build_high_expression_parts( + expression_parts, num_operands, trig_added) + + return ''.join(expression_parts) + + def _build_high_expression_parts(self, expression_parts: List[str], + num_operands: int, + trig_added: bool) -> tuple: + """ + 构建高中表达式部分 + + @param expression_parts: 表达式部分列表 + @param num_operands: 操作数数量 + @param trig_added: 是否已添加三角函数标记 + @return: (表达式部分列表, 是否已添加三角函数标记) + """ + for i in range(num_operands): + # 确保至少添加一个三角函数 + if not trig_added and i == num_operands - 1: + # 如果还没添加三角函数,强制在最后一个操作数添加 + expression_parts, trig_added = self._add_forced_trig_function( + expression_parts) + else: + # 其他操作数 + expression_parts, trig_added = self._add_high_operand_or_operation( + expression_parts, trig_added) + + # 添加运算符(除了最后一个操作数) + if i < num_operands - 1: + expression_parts.append(random.choice(['+', '-', '*', '/'])) + + return expression_parts, trig_added + + def _add_forced_trig_function(self, expression_parts: List[str]) -> tuple: + """ + 强制添加三角函数 + + @param expression_parts: 表达式部分列表 + @return: (表达式部分列表, 是否已添加三角函数标记) + """ + choice = random.randint(0, 2) + if choice == 0: + angle = random.choice([0, 30, 45, 60]) # 不包含90度 + expression_parts.append(f"sin({angle}°)") + elif choice == 1: + 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}°)") + return expression_parts, True + + def _add_high_operand_or_operation(self, expression_parts: List[str], + trig_added: bool) -> tuple: + """ + 添加高中操作数或操作 + + @param expression_parts: 表达式部分列表 + @param trig_added: 是否已添加三角函数标记 + @return: (表达式部分列表, 是否已添加三角函数标记) + """ + rand_val = random.random() + if not trig_added and rand_val < 0.4: + # 添加三角函数 + return self._add_trig_function() + elif rand_val < 0.65: + # 普通数字 + expression_parts.append(str(random.randint(1, 20))) + return expression_parts, trig_added + elif rand_val < 0.8: + # 平方 + base = random.randint(1, 10) + expression_parts.append(f"{base}²") + return expression_parts, trig_added + else: + # 开根号 + square = random.randint(1, 10) + value = square * square + expression_parts.append(f"√{value}") + return expression_parts, trig_added + + def _add_trig_function(self) -> tuple: + """ + 添加三角函数 + + @return: (表达式部分列表, 是否已添加三角函数标记) + """ + expression_parts = [] + func_choice = random.randint(0, 2) + if func_choice == 0: + angle = random.choice([0, 30, 45, 60]) # 不包含90度 + expression_parts.append(f"sin({angle}°)") + elif func_choice == 1: + 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}°)") + return expression_parts, True + + def _prepare_high_expression_for_eval(self, expression_str: str) -> str: + """ + 准备高中题目表达式用于eval计算 + + @param expression_str: 原始表达式字符串 + @return: 可用于eval计算的表达式字符串 + """ + eval_expr = self._replace_powers_and_roots(expression_str) + # 修复:确保正确的替换顺序,先替换角度制三角函数 + eval_expr = self._replace_trig_functions(eval_expr) + return eval_expr + + def _replace_powers_and_roots(self, expression_str: str) -> str: + """ + 替换幂运算和开根号符号 + + @param expression_str: 原始表达式字符串 + @return: 替换后的表达式字符串 + """ + eval_expr = expression_str.replace('²', '**2') + eval_expr = eval_expr.replace('√', 'math.sqrt') + return eval_expr + + def _replace_trig_functions(self, eval_expr: str) -> str: + """ + 替换三角函数 + + @param eval_expr: 表达式字符串 + @return: 替换三角函数后的表达式字符串 + """ + 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) + return eval_expr + + def _format_high_result(self, result: float) -> float: + """ + 格式化高中题目计算结果 + + @param result: 计算结果 + @return: 格式化后的结果 + """ + if 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, 2) + elif abs(result - 0.8660254037844386) < 1e-10: # sin(60°) ≈ 0.866 + result = round(result, 2) + elif abs(result - 0.5773502691896257) < 1e-10: # tan(30°) ≈ 0.577 + result = round(result, 2) + elif abs(result - 1.7320508075688772) < 1e-10: # tan(60°) ≈ 1.732 + result = round(result, 2) + elif abs(result - 1.0) < 1e-10: # tan(45°) = 1, sin(90°) = 1 + result = 1.0 + else: + # 保留两位小数 + result = round(result, 2) + # 整数保持不变 + return result + def fix_expression_syntax(self, expression: str) -> str: """ 修复表达式语法问题 @@ -399,10 +606,22 @@ class HighQuestionGenerator(QuestionGenerator): @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) + 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 index 80ae9a1..6500f32 100644 --- a/src/quiz.py +++ b/src/quiz.py @@ -5,7 +5,7 @@ 测验模块 """ -from typing import List, Optional +from typing import List, Optional, Dict, Tuple from question_generator import Expression @@ -21,15 +21,77 @@ class Quiz: @param questions: 题目列表 """ self.questions = questions - self.answers: List[Optional[float]] = [None] * len(questions) + self.answers: List[Optional[str]] = [None] * len(questions) # 改为字符串类型存储 + self.options: List[List[str]] = self._generate_options_for_questions() self.current_question_index = 0 self.score = 0 - def answer_question(self, answer: float) -> None: + def _generate_options_for_questions(self) -> List[List[str]]: + """ + 为所有题目生成固定选项 + + @return: 每道题的选项列表 + """ + options_list = [] + for question in self.questions: + options = self._generate_options(question.answer) + options_list.append(options) + return options_list + + def _generate_options(self, correct_answer) -> List[str]: + """ + 为题目生成选项 + + @param correct_answer: 正确答案 + @return: 选项列表 + """ + import random + + # 生成4个选项,其中一个是正确答案 + options = {correct_answer} + + # 添加一些干扰项 + if isinstance(correct_answer, int): + while len(options) < 4: + # 生成不同长度的干扰项,避免正确答案总是最长的 + if random.random() < 0.5: + # 生成1-2位数的干扰项 + options.add(random.randint(0, 99)) + else: + # 生成2-3位数的干扰项 + options.add(random.randint(10, 999)) + else: + # 浮点数情况 + while len(options) < 4: + # 随机生成不同长度的浮点数选项 + if random.random() < 0.33: + # 生成1位小数的数 + options.add(round(random.uniform(0, 100), 1)) + elif random.random() < 0.66: + # 生成2位小数的数 + options.add(round(random.uniform(0, 100), 2)) + else: + # 生成整数 + options.add(random.randint(0, 100)) + + # 如果选项不足4个,补充一些随机数 + while len(options) < 4: + if random.random() < 0.5: + options.add(random.randint(0, 100)) + else: + options.add(round(random.uniform(0, 100), + random.choice([0, 1, 2]))) + + # 将所有选项转换为字符串 + options_list = [str(opt) for opt in options] + random.shuffle(options_list) + return options_list[:4] + + def answer_question(self, answer: str) -> None: """ 回答当前题目 - @param answer: 用户答案 + @param answer: 用户答案(字符串形式) """ if 0 <= self.current_question_index < len(self.questions): self.answers[self.current_question_index] = answer @@ -66,6 +128,16 @@ class Quiz: return self.questions[self.current_question_index] return None + def get_current_options(self) -> List[str]: + """ + 获取当前题目的选项 + + @return: 当前题目的选项列表 + """ + if 0 <= self.current_question_index < len(self.questions): + return self.options[self.current_question_index] + return [] + def calculate_score(self) -> float: """ 计算得分 @@ -75,11 +147,18 @@ class Quiz: 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 + try: + # 将字符串答案转换为浮点数进行比较 + if abs(question.answer - float(answer)) < 1e-6: + correct_count += 1 + except ValueError: + # 如果转换失败,说明答案无效,不计入正确答案 + pass - self.score = (correct_count / len(self.questions)) * 100 if self.questions else 0 + if self.questions: + self.score = (correct_count / len(self.questions)) * 100 + else: + self.score = 0 return self.score def is_finished(self) -> bool: @@ -88,4 +167,5 @@ class Quiz: @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 + 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 index a214d7e..e1ce150 100644 --- a/src/user_manager.py +++ b/src/user_manager.py @@ -11,6 +11,7 @@ import re import random import hashlib import smtplib +from abc import ABC, abstractmethod from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from typing import Dict, Optional @@ -22,7 +23,8 @@ class User: 用户类,存储用户信息 """ - def __init__(self, email: str, username: str = "", password_hash: str = "", registration_code: str = ""): + def __init__(self, email: str, username: str = "", + password_hash: str = "", registration_code: str = ""): """ 初始化用户对象 @@ -38,7 +40,48 @@ class User: self.is_registered = bool(password_hash) -class UserManager: +class AbstractUserManager(ABC): + """ + 抽象用户管理类,定义用户管理的接口 + """ + + @abstractmethod + def register_user(self, email: str, username: str) -> bool: + """注册新用户""" + pass + + @abstractmethod + def verify_registration_code(self, email: str, code: str) -> bool: + """验证注册码""" + pass + + @abstractmethod + def set_password(self, email: str, password: str) -> bool: + """设置用户密码""" + pass + + @abstractmethod + def login(self, username: str, password: str) -> bool: + """用户登录""" + pass + + @abstractmethod + def change_password(self, old_password: str, new_password: str) -> bool: + """修改用户密码""" + pass + + @abstractmethod + def logout(self) -> None: + """用户登出""" + pass + + @abstractmethod + def delete_account(self, email: str, password: str) -> bool: + """注销账户""" + pass + + +class UserManager(AbstractUserManager): """ 用户管理类,负责处理用户注册、登录和密码管理 """ @@ -72,9 +115,11 @@ class UserManager: email=email, username=user_data.get('username', ''), password_hash=user_data.get('password_hash', ''), - registration_code=user_data.get('registration_code', '') + registration_code=user_data.get( + 'registration_code', '') ) - user.is_registered = user_data.get('is_registered', False) + user.is_registered = user_data.get('is_registered', + False) self.users[email] = user except (json.JSONDecodeError, FileNotFoundError): self.users = {} @@ -118,9 +163,8 @@ class UserManager: # 用户名长度应在3-20个字符之间 if not (3 <= len(username) <= 20): return False - # 用户名只能包含字母、数字和下划线 - pattern = r'^[a-zA-Z0-9_]+$' - return re.match(pattern, username) is not None + # 不再限制用户名只能包含字母、数字和下划线,允许使用各种字符包括中文 + return True def generate_registration_code(self) -> str: """ @@ -155,20 +199,76 @@ class UserManager: return has_lower and has_upper and has_digit - def register_user(self, email: str, username: str) -> bool: + def _create_registration_email(self, email: str, code: str + ) -> MIMEMultipart: """ - 注册新用户(发送注册码) + 创建注册邮件内容 + + @param email: 接收邮箱 + @param code: 注册码 + @return: 邮件对象 + """ + message = MIMEMultipart() + message["From"] = self.sender_email + message["To"] = email + message["Subject"] = "数学练习系统注册码" + + body = f""" + 您好! + + 欢迎使用数学练习系统! + + 您的注册码是: {code} + + 请在注册界面输入此注册码完成注册。 + + 如果您没有请求此注册码,请忽略此邮件。 + + 祝学习愉快! + 数学练习系统团队 + """ + + message.attach(MIMEText(body, "plain", "utf-8")) + return message + + def send_registration_code_via_email(self, email: str, code: str) -> bool: + """ + 通过电子邮件发送注册码 + + @param email: 接收邮箱 + @param code: 注册码 + @return: 是否发送成功 + """ + try: + message = self._create_registration_email(email, code) + + # 使用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 _check_user_registration_eligibility(self, email: str, + username: str) -> bool: + """ + 检查用户是否符合注册条件 @param email: 用户邮箱 @param username: 用户名 - @return: 是否成功发送注册码 + @return: 是否符合注册条件 """ if not self.is_valid_email(email): messagebox.showerror("错误", "邮箱格式不正确") return False if not self.is_valid_username(username): - messagebox.showerror("错误", "用户名应为3-20位,只能包含字母、数字和下划线") + messagebox.showerror("错误", "用户名长度应为3-20个字符") return False # 检查邮箱是否已被注册 @@ -181,9 +281,23 @@ class UserManager: if user.username == username and user.is_registered: messagebox.showerror("错误", "该用户名已存在") return False + + return True + + def register_user(self, email: str, username: str) -> bool: + """ + 注册新用户(发送注册码) + + @param email: 用户邮箱 + @param username: 用户名 + @return: 是否成功发送注册码 + """ + if not self._check_user_registration_eligibility(email, username): + return False registration_code = self.generate_registration_code() - user = User(email=email, username=username, registration_code=registration_code) + user = User(email=email, username=username, + registration_code=registration_code) self.users[email] = user # 尝试发送邮件 @@ -192,65 +306,23 @@ class UserManager: messagebox.showinfo("成功", "注册码已发送到您的邮箱,请查收") return True else: - messagebox.showerror("错误", "无法发送注册码到邮箱,请检查网络连接或邮箱地址") + 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: 用户输入的注册码 + @param code: 注册码 @return: 注册码是否正确 """ if email not in self.users: - messagebox.showerror("错误", "请先获取注册码") + messagebox.showerror("错误", "用户不存在") return False user = self.users[email] @@ -269,7 +341,9 @@ class UserManager: @return: 是否设置成功 """ if not self.is_valid_password(password): - messagebox.showerror("错误", "密码必须为6-10位,且包含大小写字母和数字") + messagebox.showerror( + "错误", + "密码必须为6-10位,且包含大小写字母和数字") return False if email not in self.users: @@ -280,8 +354,21 @@ class UserManager: user.password_hash = self.hash_password(password) user.is_registered = True self.save_users() + messagebox.showinfo("成功", "密码设置成功,请登录") return True + def _find_user_by_username(self, username: str) -> Optional[User]: + """ + 根据用户名查找用户 + + @param username: 用户名 + @return: 用户对象或None + """ + for u in self.users.values(): + if u.username == username: + return u + return None + def login(self, username: str, password: str) -> bool: """ 用户登录 @@ -290,12 +377,7 @@ class UserManager: @param password: 用户密码 @return: 是否登录成功 """ - # 根据用户名查找用户 - user = None - for u in self.users.values(): - if u.username == username: - user = u - break + user = self._find_user_by_username(username) if user is None: messagebox.showerror("错误", "用户不存在") @@ -329,7 +411,9 @@ class UserManager: return False if not self.is_valid_password(new_password): - messagebox.showerror("错误", "新密码必须为6-10位,且包含大小写字母和数字") + messagebox.showerror( + "错误", + "新密码必须为6-10位,且包含大小写字母和数字") return False self.current_user.password_hash = self.hash_password(new_password) @@ -378,4 +462,4 @@ class UserManager: self.current_user = None self.save_users() - return True + return True \ No newline at end of file diff --git a/src/users.json b/src/users.json index 0e0979a..91d824c 100644 --- a/src/users.json +++ b/src/users.json @@ -1,8 +1,14 @@ { + "3154420541@qq.com": { + "username": "123", + "password_hash": "b17e1e0450dac425ea318253f6f852972d69731d6c7499c001468b695b6da219", + "registration_code": "926322", + "is_registered": true + }, "1426688201@qq.com": { "username": "111", "password_hash": "c4318372f98f4c46ed3a32c16ee4d7a76c832886d887631c0294b3314f34edf1", - "registration_code": "939598", + "registration_code": "769790", "is_registered": true } } \ No newline at end of file