diff --git a/doc/README.md b/doc/README.md index 75fb071..3a931dd 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,177 +1,102 @@ 软1_[彭云昊]_[王祖旺]_结对项目 中小学数学学习软件 - 结对编程项目 - -## 项目简介 +项目简介 本项目是一个面向中小学学生的数学学习桌面应用程序,提供个性化的数学题目练习和评估功能。系统根据学生所在学段(小学、初中、高中)生成相应难度的数学题目,通过图形化界面提供友好的学习体验。 - -## 功能特性 - -### 用户管理 -- ✅ 用户注册:通过邮箱验证码完成注册 -- ✅ 密码设置:6-10位,必须包含大小写字母和数字 -- ✅ 用户登录:安全的身份验证机制 -- ✅ 密码修改:支持原密码验证 -- ✅ 账户注销:永久删除账户及所有数据 - -### 题目生成 -- ✅ 小学题目:加减乘除四则运算,支持括号 -- ✅ 初中题目:平方、开根运算 -- ✅ 高中题目:三角函数计算(sin, cos, tan) -- ✅ 智能防重复:确保同一试卷无重复题目 - -### 学习流程 -- ✅ 难度选择:小学/初中/高中三级难度 -- ✅ 题目数量:用户自定义题目数量(10-30题) -- ✅ 选择题形式:每题4个选项,单选作答 -- ✅ 实时评分:提交后立即显示得分情况 -- ✅ 学习延续:支持连续练习或退出选择 -- ✅ 答题进度:支持上一题/下一题导航 -- ✅ 成绩反馈:根据得分提供个性化评语 - -## 技术栈 -- 编程语言:Python 3.x -- GUI框架:Tkinter(内置Python GUI库) -- 数据存储:JSON文件(无需数据库) -- 邮件服务:SMTP协议(QQ邮箱) -- 架构模式:前后端分离架构 - -## 项目结构 - -``` +功能特性 +用户管理 +• ✅ 用户注册:通过邮箱验证码完成注册 +• ✅ 密码设置:6-10位,必须包含大小写字母和数字 +• ✅ 用户登录:安全的身份验证机制 +• ✅ 密码修改:支持原密码验证 +• ✅ 账户注销:永久删除账户及所有数据 +题目生成 +• ✅ 小学题目:加减乘除四则运算,支持括号 +• ✅ 初中题目:平方、开根运算 +• ✅ 高中题目:三角函数计算 +• ✅ 智能防重复:确保同一试卷无重复题目 +学习流程 +• ✅ 难度选择:小学/初中/高中三级难度 +• ✅ 题目数量:用户自定义题目数量(10-30题) +• ✅ 选择题形式:每题4个选项,单选作答 +• ✅ 实时评分:提交后立即显示得分情况 +• ✅ 学习延续:支持连续练习或退出选择 +• ✅ 答题进度:支持上一题/下一题导航 +技术栈 +• 编程语言:Python 3.x +• GUI框架:Tkinter(内置Python GUI库) +• 数据存储:JSON文件(无需数据库) +• 邮件服务:SMTP协议(QQ邮箱) +• 架构模式:MVC(模型-视图-控制器) +项目结构 +text +复制 +下载 软1_[彭云昊]_[王祖旺]_结对项目/ -├── src/ -│ ├── main.py # 程序入口 -│ ├── main_app.py # 主应用模块,界面控制器 -│ ├── backend_service.py # 后端服务模块,前后端通信中介 -│ ├── user_manager.py # 用户管理模块 -│ ├── question_bank.py # 题库管理模块 -│ ├── question_generator.py # 题目生成器模块 -│ ├── quiz.py # 测验管理模块 -│ └── users.json # 用户数据文件(运行时生成) -└── doc/ - └── README.md # 项目说明文档 -``` - -## 架构设计 - -本项目采用前后端分离的架构设计,确保代码的可维护性和可扩展性: - -- **前端层** (`main_app.py`):负责UI展示和用户交互,不包含业务逻辑 -- **服务层** (`backend_service.py`):作为前后端通信中介,转发请求到业务模块 -- **业务逻辑层** (`user_manager.py`, `question_bank.py`, `quiz.py`):处理具体业务逻辑 -- **数据层** (`users.json`):持久化存储用户数据 - -### 设计原则 -1. 严格的前后端分离,前端仅负责界面展示和用户交互 -2. 业务逻辑完全封装在后端模块中 -3. 模块间通过定义良好的接口进行通信 -4. 高内聚低耦合,便于独立测试和维护 - -## 安装与运行 - -### 环境要求 -- Python 3.11.9或更高版本 -- 网络连接(用于邮箱验证码发送) -- Windows 操作系统 - -### 运行步骤 -1. 下载项目文件到本地 -2. 确保所有Python文件在同一目录下 -3. 运行主程序: -```bash +├── main.py # 程序入口 +├── main_app.py # 主应用模块,界面控制器 +├── user_manager.py # 用户管理模块 +├── question_bank.py # 题库管理模块 +├── question_generator.py # 题目生成器模块 +├── quiz.py # 测验管理模块 +├── users.json # 用户数据文件(运行时生成) +└── README.md # 项目说明文档 +安装与运行 +环境要求 +• Python 3.7 或更高版本 +• 网络连接(用于邮箱验证码发送) +运行步骤 +1. 下载项目文件到本地 +2. 确保所有Python文件在同一目录下 +3. 运行主程序: +bash +复制 +下载 python main.py -``` -4. 按照界面提示进行注册和登录 - -## 使用说明 - -### 用户注册流程 -1. 点击"注册新用户" -2. 输入用户名和邮箱 -3. 点击"获取注册码",系统将发送验证码到邮箱 -4. 输入收到的注册码 -5. 设置符合要求的密码(6-10位,包含大小写字母和数字) -6. 完成注册并登录 - -### 学习流程 -1. 登录系统后进入主菜单 -2. 选择题目难度(小学/初中/高中) -3. 输入题目数量(10-30题) -4. 开始答题,选择正确答案 -5. 提交答案或切换题目 -6. 完成所有题目后查看成绩和评语 - -## 预设测试账号 - -系统支持新用户注册,也可使用以下方式测试: -- 邮箱:任意有效邮箱(接收验证码) -- 密码:符合规范的密码(如:Abc123) - -## 分支管理 - +4. 按照界面提示进行注册和登录 +预设测试账号 +系统支持新用户注册,也可使用以下测试账号: +• 邮箱:任意有效邮箱(接收验证码) +• 密码:符合规范的密码(如:Abc123) +分支管理 本项目遵循Git分支管理规范: -- main分支:稳定版本,存放经过测试的代码 -- develop分支:开发主线,集成最新功能 -- 个人分支:每位开发者的功能分支(如:zhangsan_branch) - -## 代码提交规则 - -- 源代码:必须通过个人分支 + Pull Request -- 文档:直接推送到develop分支 - -## 开发规范 - -### 代码规范 -- 遵循PEP 8 Python编码规范 -- 使用类型注解提高代码可读性 -- 模块化设计,高内聚低耦合 -- 严格遵循前后端分离原则 - -### 提交信息规范 -- feat: 新功能 -- fix: 修复bug -- docs: 文档更新 -- style: 代码格式调整 -- refactor: 代码重构 - -## 功能模块详解 - -### 1. 用户认证模块 (user_manager.py) -- 邮箱格式验证 -- 密码强度校验(6-10位,包含大小写字母和数字) -- 验证码发送与验证(通过QQ邮箱SMTP服务) -- 用户数据持久化(JSON文件) -- 用户会话管理 - -### 2. 题目生成模块 (question_generator.py) -- 小学题目:2-5个数字的加减乘除运算,支持括号,确保结果非负 -- 初中题目:平方运算、开根运算(完全平方数) -- 高中题目:三角函数(sin/cos/tan)特殊角度(0°, 30°, 45°, 60°, 90°)计算 -- 智能选项生成:为每道题生成4个合理选项,包含正确答案和干扰项 - -### 3. 测验模块 (quiz.py) -- 管理一次答题会话的所有题目 -- 跟踪用户答题进度和答案 -- 计算最终得分和正确率 -- 提供题目导航功能 - -### 4. 界面模块 (main_app.py) -- 响应式图形界面设计 -- 实时输入验证 -- 友好的用户交互反馈 -- 多框架界面切换 -- 美观的UI主题和颜色方案 - -### 5. 后端服务模块 (backend_service.py) -- 作为前端与业务逻辑模块之间的通信中介 -- 封装所有业务规则和逻辑判断 -- 提供统一的API接口给前端调用 - -## 数据存储 - +• 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 +json +复制 +下载 { "user@example.com": { "username": "张三", @@ -180,37 +105,28 @@ python main.py "is_registered": true } } -``` - -## 配置说明 - -在 `user_manager.py` 中配置以下参数: -- 邮箱服务配置(SMTP服务器、端口、授权码) -- 用户数据文件路径 - -## 测试用例 - -### 功能测试 -- 用户注册流程测试 -- 登录验证测试 -- 密码修改测试 -- 题目生成测试(各学段) -- 答题评分测试 - -### 边界测试 -- 密码格式边界测试 -- 题目数量边界测试(10-30题) -- 邮箱格式验证测试 - -## 已知限制 - -- 邮箱服务:依赖QQ邮箱SMTP服务,需配置正确的授权码 -- 题目数量:建议10-30题,过多可能影响性能 -- 网络要求:发送验证码需要网络连接 -- 平台兼容:主要支持Windows,其他平台可能需调整 - -## 开发团队 +配置说明 +在 user_manager.py 中配置以下参数: +• 邮箱服务配置(SMTP服务器、端口、授权码) +• 用户数据文件路径 +测试用例 +功能测试 +• 用户注册流程测试 +• 登录验证测试 +• 密码修改测试 +• 题目生成测试(各学段) +• 答题评分测试 +边界测试 +• 密码格式边界测试 +• 题目数量边界测试 +• 邮箱格式验证测试 +已知限制 +• 邮箱服务:依赖QQ邮箱SMTP服务,需配置正确的授权码 +• 题目数量:建议10-30题,过多可能影响性能 +• 网络要求:发送验证码需要网络连接 +• 平台兼容:主要支持Windows,其他平台可能需调整 +开发团队 +• 班级:软1 +• 组长:[彭云昊](202326010111) +• 组员:[王祖旺](202326010117) -- 班级:软1 -- 组长:[彭云昊](202326010111) -- 组员:[王祖旺](202326010117) \ No newline at end of file diff --git a/src/__pycache__/backend_service.cpython-311.pyc b/src/__pycache__/backend_service.cpython-311.pyc deleted file mode 100644 index fa4694d..0000000 Binary files a/src/__pycache__/backend_service.cpython-311.pyc and /dev/null differ diff --git a/src/__pycache__/main_app.cpython-311.pyc b/src/__pycache__/main_app.cpython-311.pyc index 7db10b0..e3f5ecf 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__/question_bank.cpython-311.pyc b/src/__pycache__/question_bank.cpython-311.pyc index 68d5b96..bd940b3 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_generator.cpython-311.pyc b/src/__pycache__/question_generator.cpython-311.pyc index 1af138a..d0555e0 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__/quiz.cpython-311.pyc b/src/__pycache__/quiz.cpython-311.pyc index 7e32e5b..7fb1b35 100644 Binary files a/src/__pycache__/quiz.cpython-311.pyc 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 index e5767b9..04ceb36 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/backend_service.py b/src/backend_service.py deleted file mode 100644 index bce17ed..0000000 --- a/src/backend_service.py +++ /dev/null @@ -1,270 +0,0 @@ -#!/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_score_grade(self, score: float) -> Dict[str, str]: - """ - 根据分数获取评分等级和评语 - - @param score: 分数 - @return: 包含等级、颜色和评语的字典 - """ - if score >= 90: - return { - "grade": "优秀", - "comment": "优秀! 继续保持!", - "color": "success" - } - elif score >= 80: - return { - "grade": "良好", - "comment": "良好! 还可以做得更好!", - "color": "success" - } - elif score >= 60: - return { - "grade": "及格", - "comment": "及格了,需要继续努力!", - "color": "warning" - } - else: - return { - "grade": "不及格", - "comment": "需要加强练习哦!", - "color": "danger" - } - - 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 ae3dd42..238d6a9 100644 --- a/src/main_app.py +++ b/src/main_app.py @@ -1,3 +1,4 @@ + # !/usr/bin/env python3 # -*- coding: utf-8 -*- @@ -7,11 +8,14 @@ import tkinter as tk from tkinter import messagebox, ttk +import random from typing import List, Optional -from backend_service import BackendService -from question_generator import Expression +from user_manager import UserManager +from question_bank import QuestionBank from quiz import Quiz +from question_generator import Expression + class MathQuizApp: """ @@ -31,7 +35,8 @@ class MathQuizApp: self.root.configure(bg="#f0f0f0") # 初始化系统组件 - self.backend_service = BackendService() + self.user_manager = UserManager() + self.question_bank = QuestionBank() # 设置样式 self.setup_styles() @@ -45,7 +50,7 @@ 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 @@ -88,7 +93,7 @@ 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): """ @@ -97,12 +102,9 @@ class MathQuizApp: @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() # 显示指定框架 @@ -117,53 +119,37 @@ class MathQuizApp: 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, + 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, + 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, + 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, + 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) + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) self.show_frame(self.login_frame) @@ -178,10 +164,10 @@ class MathQuizApp: messagebox.showerror("错误", "请输入用户名和密码") return - if self.backend_service.login(username, password): + if self.user_manager.login(username, password): messagebox.showinfo("成功", "登录成功") self.show_main_menu_frame() - # 错误信息在backend_service.login中已经显示 + # 错误信息在user_manager.login中已经显示 def show_register_frame(self): """ @@ -192,62 +178,44 @@ class MathQuizApp: 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, + 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, + 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, + 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, + 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, + 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", + 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) + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) self.show_frame(self.register_frame) @@ -263,7 +231,7 @@ class MathQuizApp: return # 注意:register_user 方法内部已经包含了消息提示,不需要额外的消息框 - self.backend_service.register_user(email, username) + self.user_manager.register_user(email, username) def verify_registration_code(self): """ @@ -276,7 +244,7 @@ class MathQuizApp: messagebox.showerror("错误", "请输入邮箱和注册码") return - if self.backend_service.verify_registration_code(email, code): + if self.user_manager.verify_registration_code(email, code): messagebox.showinfo("成功", "注册码验证成功,请设置密码") self.show_set_password_frame(email) @@ -291,54 +259,40 @@ class MathQuizApp: 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="邮箱: " + 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( + 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", + 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, + 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, + 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, + 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", + 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) + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) self.show_frame(self.set_password_frame) @@ -359,7 +313,8 @@ class MathQuizApp: messagebox.showerror("错误", "两次输入的密码不一致") return - if self.backend_service.set_password(email, password): + if self.user_manager.set_password(email, password): + messagebox.showinfo("成功", "密码设置成功,请登录") self.show_login_frame() def show_main_menu_frame(self): @@ -371,47 +326,34 @@ class MathQuizApp: 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) + 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) + 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( + 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( + 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( + 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, + 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=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) @@ -428,45 +370,34 @@ class MathQuizApp: 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], + 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="请输入题目数量 (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") + 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", + 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) + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) self.show_frame(self.quiz_setup_frame) @@ -486,17 +417,18 @@ class MathQuizApp: self.question_count = count # 生成题目 - if self.backend_service.start_quiz(self.current_level, self.question_count): + try: + questions = self.question_bank.generate_questions(self.current_level, self.question_count) + self.current_quiz = Quiz(questions) self.show_quiz_frame() - else: - messagebox.showerror("错误", "生成题目时出错") + except Exception as e: + messagebox.showerror("错误", f"生成题目时出错: {str(e)}") def show_quiz_frame(self): """ 显示答题界面 """ - current_question = self.backend_service.get_current_question() - if not current_question: + if not self.current_quiz: return # 清除之前的内容 @@ -504,40 +436,40 @@ class MathQuizApp: widget.destroy() # 创建答题界面 - main_frame = tk.Frame(self.quiz_frame, bg="#ffffff", - relief=tk.RAISED, bd=2) + 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_info = self.backend_service.get_quiz_progress() - progress = f"题目 {progress_info['current_index'] + 1}/{progress_info['total_questions']}" + 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) + 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( + 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() + options = self.current_quiz.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() # 设置默认选项为用户之前的选择(如果有) - current_answer = progress_info['current_answer'] + current_answer = self.current_quiz.answers[self.current_quiz.current_question_index] + # print(current_answer) + # print(self.current_quiz.current_question_index) + # print(self.current_quiz.answers[:5]) 调试功能 if current_answer is not None: self.answer_var.set(current_answer) else: @@ -547,50 +479,84 @@ class MathQuizApp: 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=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 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) + 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 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, + 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) + 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: + # 生成不同长度的干扰项,避免正确答案总是最长的 + 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 = 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("错误", "请选择一个答案") @@ -599,12 +565,10 @@ class MathQuizApp: try: # 验证答案是否为有效数字 float(answer_str) - if not self.backend_service.answer_question(answer_str): - messagebox.showerror("错误", "提交答案失败") - return + self.current_quiz.answer_question(answer_str) # 如果是最后一题,显示结果 - if self.backend_service.is_quiz_finished(): + if self.current_quiz.is_finished(): self.show_result_frame() else: messagebox.showinfo("提示", "答案已提交") @@ -615,22 +579,24 @@ class MathQuizApp: """ 下一题 """ + if not self.current_quiz: + return + # 保存当前答案(如果有选择) answer_str = self.answer_var.get() if answer_str: # 检查字符串是否非空 try: # 验证答案是否为有效数字 float(answer_str) - self.backend_service.answer_question(answer_str) + self.current_quiz.answer_question(answer_str) except ValueError: pass # 如果答案无效,保持为None - if self.backend_service.next_question(): + if self.current_quiz.next_question(): self.show_quiz_frame() else: # 已经是最后一题 - progress_info = self.backend_service.get_quiz_progress() - if progress_info['current_answer'] is not None: + if self.current_quiz.answers[self.current_quiz.current_question_index] is not None: # 如果最后一题已答题,显示结果 self.show_result_frame() else: @@ -640,66 +606,83 @@ class MathQuizApp: """ 上一题 """ + if not self.current_quiz: + return + # 保存当前答案(如果有选择) answer_str = self.answer_var.get() if answer_str: # 检查字符串是否非空 try: # 验证答案是否为有效数字 float(answer_str) - self.backend_service.answer_question(answer_str) + self.current_quiz.answer_question(answer_str) except ValueError: pass # 如果答案无效,保持为None - if self.backend_service.previous_question(): + 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.backend_service.calculate_score() - grade_info = self.backend_service.get_score_grade(score) + score = self.current_quiz.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) - # 根据后端返回的颜色标识获取对应颜色 - color_map = { - "success": self.success_color, - "warning": self.warning_color, - "danger": self.danger_color - } - score_color = color_map.get(grade_info["color"], self.dark_text) + # 根据得分显示不同颜色的分数 + 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, + tk.Label(result_frame, text=f"您的得分: {score:.1f}%", font=("Arial", 20, "bold"), fg=score_color, bg="#ffffff").pack(pady=10) - tk.Label(result_frame, text=grade_info["comment"], font=self.header_font, - 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) # 显示答题详情 - 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) + 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 - tk.Label(result_frame, text=f"答对: {correct_count}/{total_count}", - font=self.normal_font, bg="#ffffff").pack( + 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) # 按钮 @@ -707,19 +690,14 @@ class MathQuizApp: 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, + 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) + 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", + 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) + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) self.show_frame(self.result_frame) @@ -732,60 +710,43 @@ class MathQuizApp: 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, + 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( + 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", + 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, + 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, + 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", + 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) + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) self.show_frame(self.change_password_frame) @@ -805,16 +766,16 @@ class MathQuizApp: messagebox.showerror("错误", "新密码和确认密码不一致") return - if self.backend_service.change_password(old_password, new_password): + if self.user_manager.change_password(old_password, new_password): messagebox.showinfo("成功", "密码修改成功") self.show_main_menu_frame() - # 错误信息在backend_service.change_password中已经显示 + # 错误信息在user_manager.change_password中已经显示 def logout(self): """ 退出登录 """ - self.backend_service.logout() + self.user_manager.logout() self.show_login_frame() def show_delete_account_frame(self): @@ -826,42 +787,31 @@ class MathQuizApp: 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, + 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, + 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, + 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, + 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) @@ -869,16 +819,11 @@ class MathQuizApp: 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, + 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, + 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) @@ -896,14 +841,15 @@ class MathQuizApp: # 确认操作 if messagebox.askyesno("确认注销", "确定要注销账户吗?此操作无法撤销!"): - if self.backend_service.delete_account(email, password): - messagebox.showinfo("成功", - "账户已注销,您可以使用该邮箱重新注册") + if self.user_manager.delete_account(email, password): + messagebox.showinfo("成功", "账户已注销,您可以使用该邮箱重新注册") self.show_login_frame() - # 错误信息在backend_service.delete_account中已经显示 + # 错误信息在user_manager.delete_account中已经显示 def delete_account(self): """ 注销账户(从主菜单调用的旧方法,保持兼容性) """ - self.show_delete_account_frame() \ No newline at end of file + self.show_delete_account_frame() + + diff --git a/src/question_generator.py b/src/question_generator.py index 54bdca4..8d33c26 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,16 +97,42 @@ class ElementaryQuestionGenerator(QuestionGenerator): """ max_attempts = 100 for _ in range(max_attempts): - expression_str = self._generate_elementary_expression() + # 随机生成操作数数量(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, (int, float)) and result >= 0 and abs(result) < 1000: + # 格式化结果,保留合适的小数位数 if isinstance(result, float) and result.is_integer(): result = int(result) @@ -115,8 +141,7 @@ class ElementaryQuestionGenerator(QuestionGenerator): if str(expr) not in self.generated_questions: self.generated_questions.add(str(expr)) # 将表达式中的乘号和除号替换为更易读的形式 - readable_expr_str = expression_str.replace('*', '×') - readable_expr_str = readable_expr_str.replace('/', '÷') + readable_expr_str = expression_str.replace('*', '×').replace('/', '÷') readable_expr = Expression(readable_expr_str, result) return readable_expr except: @@ -128,65 +153,6 @@ 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): """ @@ -201,161 +167,93 @@ class MiddleQuestionGenerator(QuestionGenerator): """ max_attempts = 100 for _ in range(max_attempts): - expression_str = self._generate_middle_expression() + 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]) # 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.7: # 提高特殊操作概率从0.6到0.7 + # 添加特殊操作(平方或开根号) + 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: + # 普通操作数 + 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 = self._prepare_middle_expression_for_eval(expression_str) + 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, (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('*', '×') - readable_expr_str = readable_expr_str.replace('/', '÷') + # 将表达式中的乘号和除号替换为更易读的形式 + 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 _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: """ 修复表达式语法问题 @@ -379,34 +277,120 @@ class HighQuestionGenerator(QuestionGenerator): def generate_question(self) -> Expression: """ 生成高中题目 + @return: 数学表达式 """ max_attempts = 100 for _ in range(max_attempts): - expression_str = self._generate_high_expression() + expression_parts = [] + + # 确保至少有一个三角函数 + has_trig_function = True # 确保至少有一个三角函数 + + # 生成操作数数量(2-4个) + num_operands = random.randint(2, 4) + + # 标记是否已添加三角函数 + trig_added = False + + for i in range(num_operands): + # 确保至少添加一个三角函数 + if not trig_added and i == num_operands - 1: + # 如果还没添加三角函数,强制在最后一个操作数添加 + 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}°)") + trig_added = True + else: + # 其他操作数 + rand_val = random.random() + if not trig_added and rand_val < 0.4: + # 添加三角函数 + 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}°)") + trig_added = True + elif rand_val < 0.65: + # 普通数字 + expression_parts.append(str(random.randint(1, 20))) + elif rand_val < 0.8: + # 平方 + 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 = self._prepare_high_expression_for_eval(expression_str) + 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, (int, float)) and abs(result) < 1000 and not math.isnan(result) and not math.isinf(result): # 格式化结果,保留两位小数 - result = self._format_high_result(result) + 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) + # 整数保持不变 + 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('*', '×') - readable_expr_str = readable_expr_str.replace('/', '÷') + # 将表达式中的乘号和除号替换为更易读的形式 + 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) @@ -415,189 +399,6 @@ 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: """ 修复表达式语法问题 @@ -606,22 +407,10 @@ 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) - return expression \ No newline at end of file + 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 diff --git a/src/quiz.py b/src/quiz.py index 6500f32..cf09164 100644 --- a/src/quiz.py +++ b/src/quiz.py @@ -22,7 +22,7 @@ class Quiz: """ self.questions = questions self.answers: List[Optional[str]] = [None] * len(questions) # 改为字符串类型存储 - self.options: List[List[str]] = self._generate_options_for_questions() + self.options: List[List[str]] = self._generate_options_for_questions() # 为每道题生成固定选项 self.current_question_index = 0 self.score = 0 @@ -79,8 +79,7 @@ class Quiz: 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.add(round(random.uniform(0, 100), random.choice([0, 1, 2]))) # 将所有选项转换为字符串 options_list = [str(opt) for opt in options] @@ -155,10 +154,7 @@ class Quiz: # 如果转换失败,说明答案无效,不计入正确答案 pass - if self.questions: - self.score = (correct_count / len(self.questions)) * 100 - else: - self.score = 0 + self.score = (correct_count / len(self.questions)) * 100 if self.questions else 0 return self.score def is_finished(self) -> bool: @@ -167,5 +163,4 @@ 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 e1ce150..198fb7d 100644 --- a/src/user_manager.py +++ b/src/user_manager.py @@ -11,7 +11,6 @@ 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 @@ -23,8 +22,7 @@ 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 = ""): """ 初始化用户对象 @@ -40,48 +38,7 @@ class User: self.is_registered = bool(password_hash) -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): +class UserManager: """ 用户管理类,负责处理用户注册、登录和密码管理 """ @@ -115,11 +72,9 @@ class UserManager(AbstractUserManager): 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 = {} @@ -199,69 +154,13 @@ class UserManager(AbstractUserManager): return has_lower and has_upper and has_digit - 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: + def register_user(self, email: str, username: str) -> bool: """ - 检查用户是否符合注册条件 + 注册新用户(发送注册码) @param email: 用户邮箱 @param username: 用户名 - @return: 是否符合注册条件 + @return: 是否成功发送注册码 """ if not self.is_valid_email(email): messagebox.showerror("错误", "邮箱格式不正确") @@ -281,23 +180,9 @@ class UserManager(AbstractUserManager): 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 # 尝试发送邮件 @@ -306,23 +191,65 @@ class UserManager(AbstractUserManager): 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] @@ -341,9 +268,7 @@ class UserManager(AbstractUserManager): @return: 是否设置成功 """ if not self.is_valid_password(password): - messagebox.showerror( - "错误", - "密码必须为6-10位,且包含大小写字母和数字") + messagebox.showerror("错误", "密码必须为6-10位,且包含大小写字母和数字") return False if email not in self.users: @@ -354,21 +279,8 @@ class UserManager(AbstractUserManager): 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: """ 用户登录 @@ -377,7 +289,12 @@ class UserManager(AbstractUserManager): @param password: 用户密码 @return: 是否登录成功 """ - user = self._find_user_by_username(username) + # 根据用户名查找用户 + user = None + for u in self.users.values(): + if u.username == username: + user = u + break if user is None: messagebox.showerror("错误", "用户不存在") @@ -411,9 +328,7 @@ class UserManager(AbstractUserManager): 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) @@ -462,4 +377,4 @@ class UserManager(AbstractUserManager): self.current_user = None self.save_users() - return True \ No newline at end of file + return True diff --git a/src/users.json b/src/users.json index 91d824c..672adb6 100644 --- a/src/users.json +++ b/src/users.json @@ -1,14 +1,14 @@ { + "1426688201@qq.com": { + "username": "111", + "password_hash": "c4318372f98f4c46ed3a32c16ee4d7a76c832886d887631c0294b3314f34edf1", + "registration_code": "939598", + "is_registered": true + }, "3154420541@qq.com": { "username": "123", "password_hash": "b17e1e0450dac425ea318253f6f852972d69731d6c7499c001468b695b6da219", "registration_code": "926322", "is_registered": true - }, - "1426688201@qq.com": { - "username": "111", - "password_hash": "c4318372f98f4c46ed3a32c16ee4d7a76c832886d887631c0294b3314f34edf1", - "registration_code": "769790", - "is_registered": true } } \ No newline at end of file