diff --git a/README.md b/README.md index 6de3483..4558c89 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,98 @@ -# 中小学数学卷子自动生成程序 +# MathGenerator - 中小学数学学习软件 -本项目是一个基于 Java 开发的命令行应用(CLI),旨在为小学、初中和高中的数学老师提供一个便捷的工具,用于自动生成符合教学要求的数学练习卷。 +`MathGenerator` 是一个使用 JavaFX 构建的桌面应用程序,旨在为中小学生提供一个有趣且富有挑战性的数学练习平台。用户可以注册登录,选择不同难度(小学、初中、高中),生成定制数量的数学测验卷,并在完成后立即获得分数反馈。 -程序设计遵循了现代软件工程原则,通过模块化和面向对象的设计,实现了UI与逻辑分离、高内聚、低耦合以及良好的可扩展性。 +## 核心功能 -## 主要功能 - -* **多学段支持**:可为 **小学**、**初中**、**高中** 三个不同学段生成题目。 -* **分级与混合难度**: - * 题目难度逐级递增,高年级题目会自动包含低年级知识点(例如,高中题可能包含平方、开根号等)。 - * 生成的试卷会自动混合不同难度的题目(例如,一份高中试卷会包含一定比例的初中和小学基础题),更贴近真实考试。 -* **历史题目查重**:确保为同一位老师生成的题目不会与历史试卷中的题目重复,保证每次练习都是全新的。 -* **菜单式交互界面**:提供清晰、易用的数字菜单进行操作,无需记忆复杂命令。 -* **自动保存与归档**:生成的试卷会自动以“年-月-日-时-分-秒.txt”的格式命名,并保存在每位教师专属的文件夹下,方便管理和追溯。 +- **完整的用户系统**: 支持新用户注册、邮箱验证、登录以及密码修改功能。 +- **三级难度递进**: + - **小学**: “安全”的四则运算,确保运算过程和结果不为负数。 + - **初中**: 在小学基础上,增加了平方和开方运算。 + - **高中**: 在初中基础上,增加了基础的三角函数运算 (sin, cos, tan)。 +- **动态试卷生成**: 用户可自定义每套试卷的题目数量(10-30题),程序会根据所选难度动态生成不重复的题目。 +- **即时反馈与评分**: 完成答题后,系统会立刻计算分数,并根据分数高低给出不同的鼓励性评语。 +- **试卷存档**: 每次生成的试卷都会自动以 `.txt` 格式保存在本地,方便用户随时回顾和复习。 +- **精美的用户界面**: 使用 JavaFX 和 CSS精心设计了所有界面,包含自定义的背景图片和玻璃拟态效果,提升了用户体验。 ## 技术栈 -* **开发语言**:Java 21 -* **构建工具**:Apache Maven +- **核心框架**: Java 17, JavaFX 17 +- **JSON 处理**: Google Gson (用于持久化用户数据) +- **邮件服务**: Apache Commons Mail (用于发送注册验证码) +- **表达式计算**: Mozilla Rhino (用于动态计算生成的数学表达式字符串) +- **构建工具**: Maven -## 如何启动 +## 如何运行 -配置 **JDK 21 (或更高版本)** 和 **Apache Maven**。 +#### 1\. 先决条件 -### 1\. 环境要求 +- JDK 17 或更高版本 +- Apache Maven -* `JAVA_HOME` 环境变量已正确设置。 -* **全平台通用** +#### 2\. 配置邮件服务 -### 2\. 构建项目 +为了使“邮箱验证码”功能正常工作,您需要在项目根目录下创建一个 `config.properties` 文件,并填入您的 SMTP 服务信息。 -首先,需要使用 Maven 将项目打包成一个可执行的 `.jar` 文件。 +同时,在自己的邮箱设置中开通相应服务 -在项目的根目录(即 `pom.xml` 文件所在的目录)下,打开终端并执行以下命令: +**`config.properties` 文件模板:** -```bash -mvn clean package +```properties +# QQ邮箱 SMTP 服务器配置示例 +smtp.host=smtp.qq.com +smtp.port=465 +# 您的发件人邮箱地址 +smtp.username=your-email@qq.com +# 您的邮箱SMTP授权码 (注意:不是邮箱登录密码) +smtp.password=your_email_authorization_code ``` -该命令会编译所有代码,并 T 在 `target/` 目录下生成一个名为 `mathgenerator.jar` 的文件。 +#### 3\. 构建和运行 -### 3\. 运行程序 - -构建成功后,使用以下命令来启动程序: +在项目根目录下,执行以下 Maven 命令: ```bash -java -jar mathgenerator.jar +# 清理并构建项目,生成一个可执行的 fat-jar +mvn clean package + +# 运行生成的 JAR 文件 +java -jar MathGenerator-1.0.0.jar ``` -如果一切顺利,您将在终端看到欢迎界面和用户登录提示。 +*(注意: JAR 文件名可能因 `pom.xml` 配置而异)* ## 项目结构 -项目采用标准的 Maven 目录结构,核心逻辑清晰分离: - ``` -. -├── pom.xml # Maven 核心配置文件 -├── users.json # 用户数据文件 -└── src - └── main - └── java - └── com - └── mathgenerator - ├── Application.java # 程序主入口:负责组装和启动 - │ - ├── model/ # 数据模型包 - │ - ├── ui/ # 用户界面包 - | - ├── storage/ # 存储包 - │ - ├── generator/ # 题目生成器包 - │ - └── service/ # 核心业务服务包 - │ - └── strategy/ # 试卷组合策略子包 - -``` \ No newline at end of file +src +├── main +│ ├── java +│ │ └── com +│ │ └── mathgenerator +│ │ ├── controller # FXML视图的控制器类 +│ │ ├── generator # 不同难度题目的生成器 +│ │ ├── model # 数据模型 (User, Level, ChoiceQuestion) +│ │ ├── service # 核心业务逻辑 (用户服务, 试卷服务) +│ │ ├── storage # 文件读写管理 +│ │ ├── util # 工具类 (验证逻辑) +│ │ ├── Launcher.java # 解决 fat-jar 问题的启动器 +│ │ └── MainApplication.java # JavaFX主程序入口 +│ └── resources +│ └── com +│ └── mathgenerator +│ ├── images # UI图片资源 +│ ├── styles # CSS样式表 +│ │ +│ └── view # FXML界面布局文件 +│── users.json # (程序首次运行后生成) 用户数据文件 +└── config.properties # 邮箱配置文件 +``` + +## 界面概览 + +- **登录与注册**: 简洁的登录界面,提供注册新用户的入口。注册流程包含邮箱验证,确保用户真实性。 +- **主菜单**: 用户登录后,可以看到欢迎信息,并选择“小学”、“初中”、“高中”三种难度。同时可以自定义题目数量。 +- **答题界面**: 清晰地展示题目、选项和进度条。用户通过单选按钮作答,并提交答案进入下一题。 +- **分数界面**: 所有题目完成后,展示最终得分和鼓励评语,并提供“再做一组”或“退出登录”的选项。 + +----- \ No newline at end of file diff --git a/doc/record声明.md b/doc/record声明.md deleted file mode 100644 index 3a06636..0000000 --- a/doc/record声明.md +++ /dev/null @@ -1,84 +0,0 @@ -# 使用 `record` 定义 `User` 类的好处 -这是对 `User` 类的说明:[User.java](../src/main/java/com/mathgenerator/model/User.java) -在项目中,`User` 类被定义为一个 `record`: - -```java -public record User(String username, String password, Level level) {} -``` - -这个看似简单的单行代码,实际上是 Java 14 引入的一项强大特性。它不仅是“少写代码”的捷径,更代表了一种更现代化、更安全、更清晰的编程范式。下面,我们将详细阐述使用 `record` 的核心优势。 - -## 1\. 代码的极致简洁与可读性 - -在 `record` 出现之前,要创建一个简单的数据载体类,我们需要编写大量冗长的模板代码。 - -#### 传统写法(对比) - -```java -public final class OldUser { - private final String username; - private final String password; - private final Level level; - - public OldUser(String username, String password, Level level) { - this.username = username; - this.password = password; - this.level = level; - } - - public String getUsername() { return username; } - public String getPassword() { return password; } - public Level getLevel() { return level; } - - @Override - public boolean equals(Object o) { } - - @Override - public int hashCode() { } - - @Override - public String toString() { } -} -``` - -可以看到,一个简单的 `User` 类需要 **30-40 行** 代码。而使用 `record`,我们用 **1 行** 就完成了同样的功能。这极大地减少了视觉噪音,让代码库更整洁,开发者可以一眼看清 `User` 类的意图——它仅仅是一个数据容器。 - -## 2\. 默认实现的不可变性 (Immutability) - -`record` 声明的类具有一个至关重要的特性:**默认不可变**。 - -当编译器看到 `record User(...)` 时,它会自动执行以下操作: - -* 为 `username`, `password`, `level` 创建 **`private final`** 字段。 -* `final` 关键字意味着这些字段一旦在对象创建时被赋值,就**永远不能再被修改**。 - -**不可变性的好处:** - -* **线程安全**:不可变对象可以在多个线程之间自由共享,无需担心数据被意外篡改,从而简化了并发编程。 -* **可预测性**:当您将 `User` 对象传递给一个方法时,您完全不用担心这个方法会改变对象内部的状态。这使得程序的行为更容易推理和调试。 -* **减少 Bug**:许多难以追踪的 Bug 都源于对象状态的意外变化。不可变性从根本上杜绝了这类问题。 - -## 3\. 自动生成高质量的数据处理方法 - -`record` 不仅创建了字段和构造函数,还自动为我们生成了所有数据类都应该具备的核心方法,且这些方法的实现是标准和可靠的。 - -* **`equals()` 和 `hashCode()`** - - * 编译器会生成一个基于所有字段内容的 `equals()` 方法。如果两个 `User` 对象的用户名、密码和等级都相同,`equals()` 就会返回 `true`。 - * 同时生成的 `hashCode()` 方法与 `equals()` 保持一致,这对于将 `User` 对象用作 `HashMap` 的键或存入 `HashSet` 至关重要。手动编写这两个方法既繁琐又容易出错,而 `record` 则完美地解决了这个问题。 - -* **`toString()`** - - * 自动生成的 `toString()` 方法提供了清晰、可读的输出,非常便于日志记录和调试。 - * 例如,打印一个 `User` 对象会得到类似 `User[username=张三1, password=123, level=PRIMARY]` 的输出,而不是无意义的类名和哈希码。 - -* **简洁的访问器方法** - - * `record` 会为每个字段生成一个同名的、不带 `get` 前缀的公共访问器方法,如 `user.username()`, `user.password()`。这种命名方式更加简洁。 - -## 4\. 明确的语义和意图 - -当项目中的一个类被声明为 `record` 时,它向所有阅读代码的开发者传递了一个清晰的信号:**“我是一个简单、透明、不可变的数据载体”**。 - -这排除了该类包含复杂业务逻辑的可能性,使得代码的结构和意图更加清晰,降低了团队协作的沟通成本。 - diff --git a/doc/具体介绍.md b/doc/具体介绍.md index 5d85030..1c89e0d 100644 --- a/doc/具体介绍.md +++ b/doc/具体介绍.md @@ -1,120 +1,145 @@ -# 功能拆分文档 +# MathGenerator 项目详解 -本项目是一个基于Java开发的命令行应用程序,旨在为小学、初-中和高中三个学段的教师自动生成符合特定难度要求的数学练习卷。项目采用模块化和面向对象的思想进行设计,并引入了策略模式等设计模式,确保了代码的高内聚、低耦合以及良好的可扩展性。 +本文档提供了 `MathGenerator` 项目的深入介绍,旨在帮助开发者快速理解项目的设计、功能实现和部署方法。 +## 项目结构 -## 一、项目结构 (Maven) - -项目采用标准的 Maven 目录结构,并遵循了职责分离的设计原则,将不同功能的模块划分到独立的包中,最终结构如下: +项目采用标准的 Maven 目录结构,并通过分包将不同职责的类清晰地隔离开来,遵循了关注点分离 (SoC) 的设计原则。 ``` . -├── pom.xml # Maven 核心配置文件 -├── users.json # 用户数据文件 -└── src - └── main - └── java - └── com - └── mathgenerator - ├── Application.java # 程序主入口:负责组装和启动 - │ - ├── model/ # 数据模型包 - │ ├── Level.java # 学段枚举 - │ └── User.java # 用户数据模型 - │ - ├── ui/ # 用户界面包 - │ └── ConsoleUI.java # 负责所有命令行交互 - │ - ├── storage/ # 存储包 - │ └── FileManager.java # 负责所有文件的读写 - │ - ├── generator/ # 题目生成器包 - │ ├── QuestionGenerator.java - │ ├── PrimarySchoolGenerator.java - │ ├── JuniorHighSchoolGenerator.java - │ └── SeniorHighSchoolGenerator.java - │ - └── service/ # 核心业务服务包 - ├── UserService.java # 负责用户注册、登录和持久化 - ├── PaperService.java # 负责试卷的创建流程 - │ - └── strategy/ # 试卷组合策略子包 - ├── PaperStrategy.java - └── MixedDifficultyStrategy.java +├── pom.xml # Maven 项目配置文件 +├── config.properties # (需手动创建) 邮件服务配置文件 +├── generated_papers/ # (程序运行后生成) 保存试卷的目录 +├── users.json # (程序运行后生成) 用户数据库文件 +└── src/ + ├── main/ + │ ├── java/ + │ │ └── com/mathgenerator/ + │ │ ├── controller/ # MVC - 控制器层 + │ │ │ ├── LoginController.java + │ │ │ ├── RegisterController.java + │ │ │ ├── MainMenuController.java + │ │ │ └── ... (其他界面控制器) + │ │ ├── generator/ # 题目生成器 (工厂) + │ │ │ ├── QuestionGenerator.java (接口) + │ │ │ ├── PrimarySchoolGenerator.java + │ │ │ └── ... (其他难度生成器) + │ │ ├── model/ # MVC - 模型层 + │ │ │ ├── User.java + │ │ │ ├── Level.java + │ │ │ └── ChoiceQuestion.java + │ │ ├── service/ # 业务逻辑服务层 + │ │ │ ├── strategy/ # 策略模式包 + │ │ │ │ ├── PaperStrategy.java (接口) + │ │ │ │ └── MixedDifficultyStrategy.java + │ │ │ ├── UserService.java + │ │ │ ├── PaperService.java + │ │ │ └── EmailConfig.java + │ │ ├── storage/ # 文件持久化层 + │ │ │ └── FileManager.java + │ │ ├── util/ # 工具类 + │ │ │ └── ValidationUtils.java + │ │ ├── Launcher.java # 主启动器 (解决 fat-jar 问题) + │ │ └── MainApplication.java # JavaFX 应用主入口 + │ └── resources/ + │ └── com/mathgenerator/ + │ ├── images/ # UI 图片资源 + │ ├── styles/ # CSS 样式表 + │ │ └── styles.css + │ └── view/ # MVC - 视图层 (FXML) + │ ├── LoginView.fxml + │ ├── RegisterView.fxml + │ └── ... (其他 FXML 视图文件) + └── test/ + └── ... (测试代码目录) ``` +## 核心功能拆解 +项目的核心功能可以划分为以下几个主要模块: ---- - -## 二、核心功能拆分 +### 1\. 用户账户模块 -项目整体功能被划分为多个核心模块,每个模块由专门的Java包(Package)进行管理,职责清晰。 +- **职责**: 管理用户的整个生命周期,包括注册、登录、认证和信息修改。 +- **实现**: + - **数据持久化**: 用户信息(用户名、邮箱、加密密码)被序列化为 JSON 格式,并存储在根目录的 `users.json` 文件中。数据的读写由 `UserService` 通过 `Gson` 库完成。 + - **注册流程**: + 1. `RegisterController` 收集用户输入(用户名、邮箱)。 + 2. 调用 `ValidationUtils` 对输入格式进行校验。 + 3. 调用 `UserService` 的 `sendVerificationCode` 方法,该方法通过 `EmailConfig` 读取配置,并使用 `Apache Commons Mail` 发送验证码邮件。 + 4. 验证码校验通过后,`UserService` 会创建一个密码字段为 `null` 的新用户,并导航至 `SetPasswordController` 设置初始密码。 + - **登录与授权**: `LoginController` 调用 `UserService` 的 `login` 方法,验证用户名和密码的匹配性。成功后,将完整的 `User` 对象传递给 `MainMenuController`,完成授权。 -### 1\. 数据模型模块 (`model`包) +### 2\. 试卷生成与管理模块 -该模块定义了项目中使用的核心数据结构,确保数据的类型安全和一致性。 +- **职责**: 动态创建数学试卷,并将其持久化存储。 +- **实现**: + - **题目生成**: + - 采用**工厂方法**和**策略模式**。`QuestionGenerator` 作为工厂接口,定义了 `generateSingleQuestion` 方法。 + - `PrimarySchoolGenerator`、`JuniorHighSchoolGenerator` 等是具体工厂,分别负责生产不同难度的题目(产品)。 + - `PaperService` 作为上下文,利用 `MixedDifficultyStrategy` (具体策略) 来决定在特定难度下应调用哪个 `QuestionGenerator` 工厂。 + - **题目查重**: 在 `PaperService` 的 `createPaper` 方法中,首先会调用 `FileManager` 的 `loadExistingQuestions` 方法,加载用户以往做过的所有题目的题干。在生成新题目时,会确保新题目的题干既不与历史题目重复,也不与本轮已生成的题目重复。 + - **试卷存档**: `QuizController` 在生成试卷后,会立即调用 `PaperService` 的 `savePaper` 方法。该方法再委托 `FileManager` 将试卷内容格式化成一个用户友好的 `.txt` 文件,并存放在 `generated_papers//` 目录下。 -* **`Level` 枚举 (Enum)**: - * 定义了三个常量:`PRIMARY` (小学), `JUNIOR_HIGH` (初中), `SENIOR_HIGH` (高中)。 - * 使用枚举代替纯字符串,有效防止了因拼写错误导致的bug。 -* **`User` 类**: 一个数据类(采用 `Record` 实现),用于封装用户的核心属性:用户名 (`username`)、密码 (`password`) 和所属学段 (`level`)。 +### 3\. UI 与交互模块 -### 2\. 题目生成模块 (`generator`包) +- **职责**: 提供用户友好的图形界面,并处理用户的交互事件。 +- **实现**: + - **视图定义**: 所有界面布局均由 FXML 文件在 `resources/com/mathgenerator/view` 目录中定义。这种方式将界面设计与业务逻辑完全分离。 + - **样式**: 统一的视觉风格由 `resources/com/mathgenerator/styles/styles.css` 文件定义,实现了包括玻璃拟态、按钮悬停效果和动态状态标签在内的丰富样式。 + - **事件处理**: 每个 FXML 文件都绑定了一个 `Controller` 类。`@FXML` 注解将 FXML 中的控件(如 Button, TextField)注入到控制器中。控件的 `onAction` 属性(如 `onAction="#handleLoginButtonAction"`) 将用户操作(如点击)直接关联到控制器中的相应处理方法。 + - **场景导航**: 界面间的跳转是通过在控制器中加载新的 FXML 文件,获取其控制器实例,(如有需要)传递数据,然后将新场景设置到主舞台 (Stage) 上来实现的。 -这是实现项目核心业务逻辑的模块,采用接口驱动和继承链设计。 +## MAVEN 构建与运行 -* **`QuestionGenerator` 接口**: 定义了所有题目生成器必须遵守的统一规范,包含一个核心方法 `String generateSingleQuestion()`。 -* **三个实现类**: - * `PrimarySchoolGenerator`: 生成包含 `+`, `-`, `*`, `/` 和 `()` 的小学难度题目。 - * `JuniorHighSchoolGenerator`: **继承自 `PrimarySchoolGenerator`**,在小学题目基础上,确保至少包含一个平方或开根号运算。 - * `SeniorHighSchoolGenerator`: **继承自 `JuniorHighSchoolGenerator`**,在初中题目基础上,再确保至少包含一个 `sin`, `cos` 或 `tan` 三角函数运算。 - * 所有生成器都遵守“操作数在1-5个之间,取值范围1-100”的规则。 +要成功构建并运行此项目,请遵循以下步骤: -### 3\. 文件存储模块 (`storage`包) +#### 1\. 前置要求 -该模块封装了所有与**试卷文件**系统交互的操作。 +- **JDK 17** (或更高版本): 确保已安装并配置好环境变量。 +- **Apache Maven**: 确保已安装并配置好环境变量。 +- **SMTP 服务**: 您需要一个支持 SMTP 的邮箱账户,用于发送注册验证码。 -* **`FileManager` 类**: - * **保存试卷**: 提供 `savePaper` 方法,以“年-月-日-时-分-秒.txt”的格式保存试卷到用户专属的文件夹。 - * **读取历史题目 (查重)**: 提供 `loadExistingQuestions` 方法,扫描用户文件夹下的所有历史题目,返回一个 `Set` 集合用于查重。 +#### 2\. 配置邮件服务 -### 4\. 业务服务模块 (`service`包) +在项目的根目录下(与 `pom.xml` 同级),手动创建一个名为 `config.properties` 的文本文件。然后,根据您的邮箱服务商信息,填入以下内容。 -该模块是业务逻辑的协调者,包含了用户管理和试卷生成两大核心服务。 +**以 QQ 邮箱为例:** -* **`UserService` 类**: - * 是用户管理的**核心**。 - * 负责用户的**注册**、**登录**以及**持久化存储**。 - * 程序启动时从 `users.json` 文件加载用户数据,在用户注册时将新数据写回文件。 -* **`PaperService` 类**: - * 负责处理“生成并保存一份完整试卷”的**流程**。 - * 它持有一个 `PaperStrategy` 对象的引用,将具体的题目难度选择**委托**给策略对象来执行。 -* **`service.strategy` 子包**: - * **`PaperStrategy` 接口**: 定义了“试卷组合策略”的抽象规范。 - * **`MixedDifficultyStrategy` 类**: `PaperStrategy` 的一个具体实现,负责生成包含不同难度梯度的混合试卷。 +```properties +# SMTP 服务器地址 +smtp.host=smtp.qq.com +# SSL 加密端口 +smtp.port=465 +# 您的发件人邮箱地址 +smtp.username=your-email@qq.com +# 您的邮箱 SMTP 授权码 (注意:这不是邮箱的登录密码,需要在邮箱设置中生成) +smtp.password=your_email_authorization_code +``` -### 5\. 主程序与UI模块 (`Application.java` 和 `ui`包) +#### 3\. 使用 Maven 构建 -为遵循单一职责原则,程序的启动和UI交互被拆分到独立的单元中。 +打开终端或命令行,导航到项目的根目录,然后执行以下 Maven 命令: -* **`Application.java`**: 程序的唯一入口。其 `main` 方法非常简洁,仅负责创建和组装所有核心组件(如 `UserService`, `PaperService`, `ConsoleUI` 等),然后启动应用。 -* **`ui.ConsoleUI` 类**: 一个专门负责所有控制台用户界面显示和交互的类。它实现了**菜单驱动**的用户界面,处理用户的**登录**、**注册**、**难度切换**和**题目生成**等所有交互操作。 +```bash +# 1. 清理旧的构建产物 +mvn clean -## 三、如何使用 Maven 构建与运行 +# 2. 编译代码、运行测试并打包成一个可执行的 "fat-jar" +mvn package +``` -项目采用 Maven 进行自动化构建和打包。 +执行成功后,您会在 `target/` 目录下找到一个名为 `math-generator-1.0-SNAPSHOT.jar` (文件名可能略有不同) 的文件。这个 JAR 文件已经包含了所有依赖,可以直接运行。 -1. **构建 (打包)**: 在项目根目录(`pom.xml` 所在位置)打开终端,执行以下命令: +#### 4\. 运行应用程序 - ```bash - mvn clean package - ``` +继续在终端中,执行以下命令来启动应用程序: - 该命令会自动完成编译、测试和打包,并在 `target/` 目录下生成一个可执行的 `mathgenerator.jar` 文件。 +```bash +java -jar target/math-generator-1.0-SNAPSHOT.jar +``` -2. **运行**: 继续在终端中执行以下命令来启动程序。 +片刻之后,您将看到应用程序的登录窗口。 - ```bash - java -jar mathgenerator.jar - ``` \ No newline at end of file +----- \ No newline at end of file diff --git a/doc/双击启动.bat b/doc/双击启动.bat index 05146d9..08662ce 100644 --- a/doc/双击启动.bat +++ b/doc/双击启动.bat @@ -7,7 +7,7 @@ set JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8 :: 3. 运行你的 JAR 包 echo loading... -java -jar mathgenerator.jar +java -jar MathGenerator-1.0.0.jar :: 4. 清理环境变量并暂停,以便查看程序输出 set JAVA_TOOL_OPTIONS= diff --git a/doc/程序运行说明.md b/doc/程序运行说明.md index 20225b2..c1764c1 100644 --- a/doc/程序运行说明.md +++ b/doc/程序运行说明.md @@ -1,18 +1,98 @@ -# 程序运行说明 +# MathGenerator - 程序运行指南 -1. 环境配置:java21及以上,不推荐使用windows,要设置更改字符,否则中文字符会乱码,建议使用ubuntu,macos等系统 +本文档将详细引导您完成 `MathGenerator` 数学学习软件的下载、配置、构建和运行全过程。请遵循以下步骤以确保程序能够顺利启动。 -2. 在控制台输入 +## 1\. 系统环境要求 - ```bash - java -jar mathgenerator.jar - ``` +在开始之前,请确保您的计算机上已安装并正确配置了以下软件: - 可以开始使用程序 +- **Java Development Kit (JDK) 17**: 本项目基于 Java 17 开发。您需要安装 JDK 17 或更高版本。 + - *验证方法*: 打开终端或命令行,输入 `java -version`,确保显示的版本号是 17 或以上。 +- **Apache Maven**: 本项目使用 Maven 进行依赖管理和项目构建。 + - *验证方法*: 在终端或命令行中,输入 `mvn -version`,确保能够看到 Maven 的版本信息。 -3. 程序提供在windows运行的bat脚本,需要可以双击运行 +## 2\. 关键配置步骤:邮件服务 -4. 第一次运行时,在运行目录下会生成默认用户的json文件,生成的试卷放在generated文件夹下 +本程序的“注册”功能需要通过发送邮件验证码来验证用户邮箱的真实性。因此,您**必须**在运行程序前配置好 SMTP 邮件服务。 -5. 注册功能的说明:注册功能是自己添加,由于项目要求登录时将用户名和密码同行隔空格输入,因此注册时的特殊情况,如密码含空格,会无法登录,但是登录不是课程考核之一,希望不要引起不必要的误会,特此说明 +#### 第 1 步:创建配置文件 +在项目的根目录下 (与 `pom.xml` 文件位于同一级),手动创建一个名为 `config.properties` 的文本文件。 + +#### 第 2 步:填写配置信息 + +使用文本编辑器打开 `config.properties` 文件,并根据您自己的邮箱服务商信息,填入以下内容。 + +**我们强烈推荐使用 QQ 邮箱作为示例,其配置如下:** + +```properties +# 邮件服务器主机名 (QQ邮箱固定为此值) +smtp.host=smtp.qq.com +# SSL 加密端口 (QQ邮箱固定为此值) +smtp.port=465 +# 您的发件人邮箱地址 (例如: 12345678@qq.com) +smtp.username=your-email@qq.com +# 您的邮箱SMTP授权码 (!!! 注意:这不是邮箱的登录密码) +smtp.password=your_email_authorization_code +``` + +#### 第 3 步:获取 SMTP 授权码 + +“授权码”是用于登录第三方客户端的专用密码,以保证您主登录密码的安全。 + +**获取 QQ 邮箱授权码的步骤:** + +1. 登录您的 QQ 邮箱网页版。 +2. 点击顶部的 **“设置”** -\> **“账户”**。 +3. 向下滚动,找到 **“POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务”** 区域。 +4. 确保 **“POP3/SMTP服务”** 和 **“IMAP/SMTP服务”** 至少有一个是开启状态。 +5. 点击 **“生成授权码”**,根据弹出的窗口提示,使用您的密保手机发送一条短信。 +6. 发送成功后,窗口会显示一串16位的字符串,这就是您的**授权码**。 +7. 将这串授权码复制并粘贴到 `config.properties` 文件的 `smtp.password` 字段中。 + +> **重要提示**: 请妥善保管您的授权码,不要泄露给他人。 + +## 3\. 使用 Maven 构建项目 + +配置完成后,您需要使用 Maven 将项目源代码打包成一个可执行的 JAR 文件。 + +1. 打开您的终端或命令行工具 (例如 CMD, PowerShell, Terminal)。 + +2. 使用 `cd` 命令导航到 `MathGenerator` 项目的根目录。 + +3. 执行以下 Maven 命令来构建项目: + + ```bash + mvn clean package + ``` + +4. Maven 会自动下载所有必需的依赖库,并编译您的代码。如果一切顺利,您会在命令行的输出日志中看到 `[INFO] BUILD SUCCESS`。 + +5. 构建成功后,在项目目录下会生成一个新的 `target` 文件夹。其中,`math-generator-1.0-SNAPSHOT.jar` (文件名可能因 `pom.xml` 配置而略有不同) 就是我们需要的可执行文件。 + +## 4\. 运行应用程序 + +确保您仍然在项目的根目录的终端中,执行以下命令来启动应用程序: + +```bash +java -jar target/math-generator-1.0-SNAPSHOT.jar +``` + +执行命令后,程序的登录窗口将会启动。 + +## 5\. 首次使用说明 + +- **注册新用户**: 首次使用时,您需要点击 **“注册新用户”** 按钮。 +- **验证邮箱**: 在注册界面,请填写真实有效的邮箱地址,以便接收验证码。 +- **开始练习**: 注册并设置密码成功后,您将自动登录并进入主菜单,可以选择不同难度开始您的数学练习之旅。 + +## 6\. 程序生成的文件 + +当您运行程序并与之交互后,项目根目录下会自动生成以下文件和文件夹: + +- `users.json`: 这是一个 JSON 文件,用于存储所有已注册用户的信息。请不要手动修改或删除它。 +- `generated_papers/`: 这是一个文件夹,您生成的每一份试卷都会以 `.txt` 格式保存在这里,并以您的用户名进行归类。 + +----- + +如果您在运行过程中遇到任何问题,请首先检查 **JDK 和 Maven 的版本**是否正确,以及 `config.properties` 文件中的**邮箱配置和授权码**是否填写无误。 \ No newline at end of file diff --git a/doc/设计模式.md b/doc/设计模式.md index f0422b7..df14a8b 100644 --- a/doc/设计模式.md +++ b/doc/设计模式.md @@ -1,48 +1,62 @@ -# 设计模式 +# MathGenerator - 设计模式分析 -本项目在开发过程中,为了实现代码的高内聚、低耦合以及未来的高可扩展性,采纳了多种业界标准的设计模式和原则。本文档将详细解析其中最核心的几种设计模式。 +本文档旨在分析 `MathGenerator` 项目中所采用的核心软件设计模式。通过合理运用这些模式,项目在结构清晰度、可扩展性和可维护性方面都得到了显著提升。 -## 1. 策略模式 (Strategy Pattern) +## 1. 模型-视图-控制器 (Model-View-Controller, MVC) -策略模式是本项目架构中最为核心和体现拓展性的设计模式。 +MVC 是本项目最核心的架构模式,它将整个应用程序划分为三个紧密协作但相互独立的组件,有效地实现了业务逻辑与用户界面的解耦。 -* **意图**:定义一系列算法,将每一个算法封装起来,并使它们可以相互替换。此模式让算法的变化独立于使用算法的客户。 +- **模型 (Model)**: + - **描述**: 模型的职责是管理应用程序的数据和业务规则。它不关心数据如何被展示。 + - **实现**: 项目中的 `com.mathgenerator.model` 包完美地扮演了模型的角色。 + - `User.java`: 定义了用户的数据结构。 + - `Level.java`: 使用枚举定义了难度级别,是业务规则的一部分。 + - `ChoiceQuestion.java`: 封装了单道题目的数据结构。 -* **在项目中的体现**: - * **`PaperStrategy` 接口**:这是策略的抽象。它定义了一个 `selectGenerator(Level mainLevel)` 方法,所有具体的试卷组合算法都必须实现这个接口。 - * **`MixedDifficultyStrategy` 类**:这是一个具体的策略实现。它封装了我们设计的“混合难度”算法(例如,高中试卷包含60%高中题、30%初中题、10%小学题)。 - * **`PaperService` 类**:这是使用策略的上下文(Context)。`PaperService` 不再关心试卷的具体组合逻辑,它只持有一个 `PaperStrategy` 对象的引用,并在生成试卷时调用其 `selectGenerator` 方法。 +- **视图 (View)**: + - **描述**: 视图负责渲染用户界面,将模型中的数据显示给用户。在 JavaFX 中,这通常由 FXML 文件来定义。 + - **实现**: 项目 `resources/com/mathgenerator/view` 目录下的所有 `.fxml` 文件共同构成了视图层。每个 FXML 文件都精确地描述了一个界面的布局和组件,而具体的样式则由 CSS 文件 (`styles.css`) 提供。 -* **带来的好处**: - * **极高的可扩展性**:如果我们想新增一种“只出难题”的试卷模式,只需新增一个 `HardModeStrategy` 类实现 `PaperStrategy` 接口,然后在程序入口处注入这个新策略即可,**完全不需要修改 `PaperService` 的代码**。这完美遵循了“开闭原则”。 - * **职责分离**:`PaperService` 的职责被简化为控制试卷生成的整体流程,而具体的题目组合算法则被分离到各个策略类中,使得代码结构更清晰。 +- **控制器 (Controller)**: + - **描述**: 控制器是模型和视图之间的协调者。它接收来自视图的用户输入,处理这些输入(可能需要更新模型),并选择合适的视图来响应用户。 + - **实现**: `com.mathgenerator.controller` 包下的所有 Java 类都是控制器。例如,`LoginController.java` 负责处理登录按钮的点击事件,调用 `UserService` (业务逻辑) 来验证用户信息 (模型),并根据结果决定是停留在登录页还是跳转到主菜单 (更新视图)。 -## 2. 工厂方法模式 (Factory Method Pattern) 的简化应用 +## 2. 策略模式 (Strategy Pattern) -虽然没有严格地实现一个完整的工厂方法模式,但其核心思想在项目中得到了应用。 +策略模式在项目的试卷生成模块中得到了巧妙的应用,它允许在运行时动态地选择题目的生成算法。 -* **意图**:定义一个用于创建对象的接口,让子类决定实例化哪一个类。 +- **描述**: 该模式定义了一系列算法,并将每一个算法封装起来,使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户。 -* **在项目中的体现**: - * 我们的 `MixedDifficultyStrategy` 内部的 `selectGenerator` 方法实际上扮演了一个**简单工厂(Simple Factory)**的角色。它根据输入的 `Level` 和内部的随机逻辑,负责“生产”出具体的 `QuestionGenerator` 实例(`PrimarySchoolGenerator`, `JuniorHighSchoolGenerator` 等)。 +- **实现**: + - **策略接口 (`PaperStrategy.java`)**: 定义了一个所有具体策略都必须实现的公共接口,其中包含一个核心方法 `selectGenerator(Level mainLevel)`。 + - **具体策略 (`MixedDifficultyStrategy.java`)**: 这是策略接口的一个具体实现。它封装了“混合难度”这一算法:根据用户选择的主难度,按不同的概率混合使用小学、初中、高中的题目生成器。 + - **上下文 (`PaperService.java`)**: `PaperService` 类持有一个 `PaperStrategy` 对象的引用。当 `createPaper` 方法被调用时,它不关心具体的生成器是如何被选择的,而是将这个决策委托给当前的策略对象去完成。 -* **带来的好处**: - * **集中创建逻辑**:所有关于“如何根据难度创建对应题目生成器”的逻辑都被集中在一个地方,而不是散落在代码各处。当需要修改创建逻辑时,我们只需要改动这个“工厂”即可。 + 这种设计极大地增强了灵活性。未来如果需要增加新的试卷组合策略(例如“只考错题”策略),只需增加一个新的 `PaperStrategy` 实现类,而无需修改 `PaperService` 的代码。 -## 3. 模板方法模式 (Template Method Pattern) +## 3. 单例模式 (Singleton-like Service Layer) -此模式在我们的题目生成器继承链中得到了巧妙的运用。 +尽管项目中没有严格按照“私有构造函数 + 静态 `getInstance()` 方法”来实现单例,但其 `service` 层的设计思想与单例模式非常相似。 -* **意图**:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 +- **描述**: 确保一个类只有一个实例,并提供一个全局访问点。 -* **在项目中的体现**: - * **`PrimarySchoolGenerator`** 提供了最基础的题目生成逻辑。 - * **`JuniorHighSchoolGenerator`** 继承前者,它的 `generateSingleQuestion` 方法首先通过 `super.generateSingleQuestion()` 调用父类的“模板”来获得一个基础题目,然后在此基础上增加自己的特定步骤(添加平方或开根号)。 - * **`SeniorHighSchoolGenerator`** 同理,它调用父类(初中生成器)的方法,在这个“半成品”上增加自己的步骤(添加三角函数)。 +- **实现**: + - `UserService.java` 和 `PaperService.java` 这两个类封装了核心的业务逻辑。 + - 在各个 `Controller` 中,这些服务类都通过 `new` 关键字被实例化。然而,从整个应用程序的生命周期来看,每个服务在逻辑上都代表了一个单一、集中的功能单元(例如,管理所有用户或处理所有试卷生成请求)。 + - 这种“服务层”的架构方式,使得业务逻辑被集中管理,避免了代码的重复,并为将来引入依赖注入框架(如 Spring 或 Guice)打下了良好的基础。 -* **带来的好处**: - * **代码复用**:避免了在每个生成器中都重复编写基础表达式的生成逻辑。 - * **结构清晰**:清晰地定义了不同难度题目之间的层级递进关系。 +## 4. 工厂方法模式 (Factory Method Pattern) - 隐式应用 +在题目生成器的设计中,隐式地运用了工厂方法模式的思想。 -通过综合运用这些设计模式和原则,我们的项目不仅实现了所有功能需求,更拥有了一个专业、健壮且易于扩展的软件架构。 \ No newline at end of file +- **描述**: 定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。 + +- **实现**: + - **产品接口 (`QuestionGenerator.java`)**: 定义了所有生成器(工厂)必须创建的对象类型,即 `ChoiceQuestion`。 + - **具体产品**: `ChoiceQuestion` 对象本身。 + - **具体工厂**: `PrimarySchoolGenerator.java`, `JuniorHighSchoolGenerator.java`, `SeniorHighSchoolGenerator.java` 等类,每个类都是一个具体的“工厂”,负责生产特定类型的“产品”(即不同难度的题目)。 + - **决策者**: `MixedDifficultyStrategy` 在这里扮演了决策者的角色,它根据输入(难度级别)来决定调用哪一个具体的“工厂”来生产题目。 + + 虽然没有一个显式的 `QuestionFactory` 类,但将“创建题目”的职责委托给一系列可替换的生成器类,这正是工厂方法模式的核心思想。 + +--- \ No newline at end of file diff --git a/pom.xml b/pom.xml index 5b323ad..e087f19 100644 --- a/pom.xml +++ b/pom.xml @@ -21,25 +21,21 @@ gson 2.10.1 - org.openjfx javafx-controls ${javafx.version} - org.openjfx javafx-fxml ${javafx.version} - org.apache.commons commons-email 1.5 - org.mozilla rhino-engine @@ -54,15 +50,14 @@ maven-compiler-plugin 3.11.0 - 17 - 17 + 17 org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.5.2 package diff --git a/src/main/java/com/mathgenerator/Launcher.java b/src/main/java/com/mathgenerator/Launcher.java index 6773f2d..c20fbce 100644 --- a/src/main/java/com/mathgenerator/Launcher.java +++ b/src/main/java/com/mathgenerator/Launcher.java @@ -1,21 +1,12 @@ package com.mathgenerator; /** - * 应用程序启动器类。 - *

- * 这个类的唯一目的是为了解决在使用某些构建工具(如 Maven Shade Plugin)将 JavaFX 应用程序 - * 打包成一个可执行的 "fat JAR" 文件时可能出现的运行时问题。 - *

- * 它通过一个标准的 {@code main} 方法来调用 {@link MainApplication#main(String[])} 方法, - * 充当了 JavaFX 应用程序的实际入口点。 + * 应用程序的非 JavaFX 启动器。 + * 这是解决 JavaFX fat JAR 打包时 "unnamed module" 警告的 + * 标准解决方案。它通过一个非 JavaFX 的 main 方法来启动真正的 + * JavaFX Application 类。 */ public class Launcher { - - /** - * 程序的主入口点。 - * - * @param args 传递给应用程序的命令行参数。 - */ public static void main(String[] args) { MainApplication.main(args); } diff --git a/src/main/java/com/mathgenerator/controller/LoginController.java b/src/main/java/com/mathgenerator/controller/LoginController.java index 0e65ce7..fae2b97 100644 --- a/src/main/java/com/mathgenerator/controller/LoginController.java +++ b/src/main/java/com/mathgenerator/controller/LoginController.java @@ -16,11 +16,6 @@ import javafx.stage.Stage; import java.io.IOException; import java.util.Optional; import com.mathgenerator.util.ValidationUtils; - -/** - * “登录”视图 (LoginView.fxml) 的 FXML 控制器类。 - * 负责处理用户身份验证,并导航到注册界面或主菜单。 - */ public class LoginController { // 依赖注入后端服务 @@ -43,55 +38,44 @@ public class LoginController { private Label statusLabel; /** - * 处理“登录”按钮的点击事件。 - * 该方法会验证用户的输入,并尝试登录。如果成功,则导航到主菜单界面。 - * - * @param event 由按钮点击触发的 ActionEvent 事件。 + * 处理登录按钮点击事件。 + * @param event 事件对象 */ @FXML private void handleLoginButtonAction(ActionEvent event) { - String username = usernameField.getText(); + // 1. 获取输入,现在它可能是用户名或邮箱 + String identifier = usernameField.getText(); String password = passwordField.getText(); - // --- 2. 使用工具类进行校验 --- - if (!ValidationUtils.isUsernameValid(username)) { - statusLabel.setText("登录失败:用户名不能为空且不能包含空格。"); + // 2. 基本的非空校验 + if (identifier.isEmpty()) { + statusLabel.setText("用户名或邮箱不能为空!"); return; } if (password.isEmpty()) { - statusLabel.setText("用户名和密码不能为空!"); + statusLabel.setText("密码不能为空!"); return; } - Optional userOptional = userService.login(username, password); + // 3. 调用更新后的 login 方法 + Optional userOptional = userService.login(identifier, password); if (userOptional.isPresent()) { statusLabel.setText("登录成功!"); - // 登录成功,调用新方法跳转到主菜单 loadMainMenu(userOptional.get()); } else { - statusLabel.setText("登录失败:用户名或密码错误。"); + // 4. (已修改) 更新错误提示 + statusLabel.setText("登录失败:用户名/邮箱或密码错误。"); } } - /** - * 处理“注册”按钮的点击事件。 - * 导航用户到注册界面。 - * - * @param event 由按钮点击触发的 ActionEvent 事件。 - */ + // ... (该文件中的其他方法无需修改) @FXML private void handleRegisterButtonAction(ActionEvent event) { loadScene("/com/mathgenerator/view/RegisterView.fxml"); } - /** - * 在用户成功登录后加载主菜单界面。 - * 此方法会将登录用户的完整信息传递给 MainMenuController。 - * - * @param user 成功登录的用户的 User 对象。 - */ private void loadMainMenu(User user) { try { // 1. 加载 FXML 文件 @@ -114,11 +98,6 @@ public class LoginController { } } - /** - * 一个辅助工具方法,用于在当前窗口加载一个新的场景。 - * - * @param fxmlPath 要加载的场景的 FXML 文件路径。 - */ private void loadScene(String fxmlPath) { try { Parent root = FXMLLoader.load(getClass().getResource(fxmlPath)); diff --git a/src/main/java/com/mathgenerator/controller/QuizController.java b/src/main/java/com/mathgenerator/controller/QuizController.java index 2b948bd..fa13f33 100644 --- a/src/main/java/com/mathgenerator/controller/QuizController.java +++ b/src/main/java/com/mathgenerator/controller/QuizController.java @@ -85,13 +85,16 @@ public class QuizController { optionsGroup.selectToggle(null); updateStatusLabelStyle(false); + // 更新按钮文字:如果不是最后一题显示"下一题",最后一题显示"完成答题" if (currentQuestionIndex == questions.size() - 1) { submitButton.setText("完成答题"); + } else { + submitButton.setText("下一题"); } } /** - * 处理“提交答案”按钮的点击事件。 + * 处理“下一题”按钮的点击事件。 * 记录用户的选择,然后切换到下一题或结束答题。 * * @param event 事件对象。 diff --git a/src/main/java/com/mathgenerator/controller/RegisterController.java b/src/main/java/com/mathgenerator/controller/RegisterController.java index 8f98a43..efa4ca5 100644 --- a/src/main/java/com/mathgenerator/controller/RegisterController.java +++ b/src/main/java/com/mathgenerator/controller/RegisterController.java @@ -83,9 +83,10 @@ public class RegisterController { countdownTimeline.play(); } + /** - * 处理“确认注册”按钮的点击事件。 - * 验证所有输入字段,如果有效,则调用服务注册新用户,并导航到设置密码界面。 + * 处理“前往设置密码”按钮的点击事件。 + * (已修改) 现在只验证信息,不写入文件,然后导航到设置密码界面。 * * @param event 事件对象。 */ @@ -105,16 +106,19 @@ public class RegisterController { return; } - boolean success = userService.register(username, email); - - if (success) { - showStatusMessage("注册成功!请设置您的密码。", true); - loadSetPasswordScene(username); - } else { + // --- 核心修改:不再调用 register,而是检查用户名和邮箱是否已存在 --- + if (userService.isUsernameOrEmailTaken(username, email)) { showStatusMessage("注册失败:用户名或邮箱已被占用。", true); + return; } + + // 验证通过,导航到设置密码界面,并传递用户名和邮箱 + showStatusMessage("验证成功!请设置您的密码。", true); + loadSetPasswordScene(username, email); } + + /** * 在状态标签中显示一条消息,并根据内容更新其样式。 * @@ -146,16 +150,18 @@ public class RegisterController { } /** - * 加载“设置密码”场景,并将新注册的用户名传递过去。 + * (已修改) 加载“设置密码”场景,并将新注册的用户名和邮箱传递过去。 * * @param username 新注册的用户名。 + * @param email 新注册的邮箱。 */ - private void loadSetPasswordScene(String username) { + private void loadSetPasswordScene(String username, String email) { try { FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/SetPasswordView.fxml")); Parent root = loader.load(); SetPasswordController controller = loader.getController(); - controller.initData(username); + // --- 核心修改:同时传递用户名和邮箱 --- + controller.initData(username, email); Stage stage = (Stage) registerButton.getScene().getWindow(); stage.setScene(new Scene(root)); diff --git a/src/main/java/com/mathgenerator/controller/SetPasswordController.java b/src/main/java/com/mathgenerator/controller/SetPasswordController.java index 9d6a583..3630f46 100644 --- a/src/main/java/com/mathgenerator/controller/SetPasswordController.java +++ b/src/main/java/com/mathgenerator/controller/SetPasswordController.java @@ -15,14 +15,11 @@ import javafx.stage.Stage; import java.io.IOException; -/** - * “设置密码”视图 (SetPasswordView.fxml) 的 FXML 控制器类。 - * 用户在注册成功后会进入此界面,以设置他们的初始密码。 - */ public class SetPasswordController { private final UserService userService = new UserService(); private String username; + private String email; // 新增一个字段来存储邮箱 @FXML private Label promptLabel; @FXML private PasswordField newPasswordField; @@ -31,19 +28,20 @@ public class SetPasswordController { @FXML private Label statusLabel; /** - * 初始化控制器,并接收从注册界面传递过来的用户名。 + * (已修改) 初始化控制器,并接收用户名和邮箱。 * * @param username 新注册的用户名。 + * @param email 新注册的邮箱。 */ - public void initData(String username) { + public void initData(String username, String email) { this.username = username; + this.email = email; // 保存邮箱 promptLabel.setText("为您的账户 " + username + " 设置密码"); } /** - * 处理“确认”按钮的点击事件。 - * 该方法会验证两次输入的密码是否一致且符合格式要求,然后调用服务为用户设置密码, - * 成功后直接登录并跳转到主菜单。 + * (已修改) 处理“确认”按钮的点击事件。 + * 现在这个方法会完成用户的最终注册。 * * @param event 事件对象。 */ @@ -62,32 +60,26 @@ public class SetPasswordController { return; } - boolean success = userService.setPassword(this.username, newPassword); + // --- 核心修改:调用新的服务方法,完成最终的注册和密码设置 --- + boolean success = userService.createUserWithPassword(this.username, this.email, newPassword); if (success) { - showStatusMessage("密码设置成功!正在进入主菜单...", true); + showStatusMessage("注册成功!正在进入主菜单...", true); + // 注册成功后,直接查找这个新创建的用户并登录 userService.findUserByUsername(this.username).ifPresent(this::loadMainMenu); } else { - showStatusMessage("密码设置失败,请稍后重试或重新注册。", true); + // 这个分支理论上很难进入,除非在极短时间内有其他人注册了相同的用户名/邮箱 + showStatusMessage("注册失败,用户名或邮箱可能刚刚被占用,请重新注册。", true); } } - /** - * 在状态标签中显示一条消息,并根据内容更新其样式。 - * - * @param message 要显示的消息。 - * @param hasContent 如果消息非空则为true。 - */ + // ... (文件中的 showStatusMessage, updateStatusLabelStyle, loadMainMenu 方法保持不变) ... + private void showStatusMessage(String message, boolean hasContent) { statusLabel.setText(message); updateStatusLabelStyle(hasContent); } - /** - * 根据标签是否有内容来更新其CSS样式。 - * - * @param hasContent 如果有内容则为true。 - */ private void updateStatusLabelStyle(boolean hasContent) { if (hasContent) { statusLabel.getStyleClass().removeAll("setpassword-status-label"); @@ -102,11 +94,6 @@ public class SetPasswordController { } } - /** - * 加载主菜单界面,并传递完整的用户信息,以实现自动登录。 - * - * @param user 包含新设置密码的完整 User 对象。 - */ private void loadMainMenu(User user) { try { FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/MainMenuView.fxml")); diff --git a/src/main/java/com/mathgenerator/service/PaperService.java b/src/main/java/com/mathgenerator/service/PaperService.java index a44437f..d3ed77c 100644 --- a/src/main/java/com/mathgenerator/service/PaperService.java +++ b/src/main/java/com/mathgenerator/service/PaperService.java @@ -49,7 +49,7 @@ public class PaperService { List newPaper = new ArrayList<>(); Set generatedInSession = new HashSet<>(); - System.out.println("正在根据策略生成选择题,请稍候..."); + //System.out.println("正在根据策略生成选择题,请稍候..."); while (newPaper.size() < count) { ChoiceQuestion question = paperStrategy.selectGenerator(currentLevel).generateSingleQuestion(); String questionText = question.questionText(); @@ -71,8 +71,8 @@ public class PaperService { public void savePaper(String username, List paper) { try { String filePath = fileManager.savePaper(username, paper); - System.out.println("成功!" + paper.size() + "道数学题目已生成。"); - System.out.println("文件已保存至: " + filePath); + //System.out.println("成功!" + paper.size() + "道数学题目已生成。"); + //System.out.println("文件已保存至: " + filePath); } catch (IOException e) { System.err.println("错误:保存文件失败 - " + e.getMessage()); } diff --git a/src/main/java/com/mathgenerator/service/UserService.java b/src/main/java/com/mathgenerator/service/UserService.java index 1a2eb64..0cb9453 100644 --- a/src/main/java/com/mathgenerator/service/UserService.java +++ b/src/main/java/com/mathgenerator/service/UserService.java @@ -4,6 +4,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.mathgenerator.model.User; +import com.mathgenerator.util.ValidationUtils; import org.apache.commons.mail.Email; import org.apache.commons.mail.EmailException; import org.apache.commons.mail.SimpleEmail; @@ -21,12 +22,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadLocalRandom; import java.util.regex.Pattern; -/** - * 用户服务类。 - *

- * 负责处理所有与用户账户相关的业务逻辑,包括用户的注册、登录、密码修改、 - * 信息查询以及发送验证码等功能。该类通过读写 JSON 文件来持久化用户数据。 - */ public class UserService { private static final Path USER_FILE_PATH = Paths.get("users.json"); private static final Pattern PASSWORD_PATTERN = @@ -34,25 +29,52 @@ public class UserService { private Map userDatabase; private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); - /** - * 构造一个新的 UserService 实例。 - * 在构造时会自动从 {@code users.json} 文件加载用户数据。 - */ public UserService() { this.userDatabase = loadUsersFromFile(); } + // --- 新增方法:检查用户名或邮箱是否已被占用 --- /** - * 从 {@code users.json} 文件加载用户数据到内存中的 Map。 - * 如果文件不存在或为空,则返回一个空的 Map。 - * - * @return 一个包含所有用户数据的 {@code ConcurrentHashMap}。 + * 检查指定的用户名或邮箱是否已经被注册。 + * @param username 要检查的用户名。 + * @param email 要检查的邮箱地址。 + * @return 如果用户名或邮箱中任意一个已被占用,则返回 true。 */ + public boolean isUsernameOrEmailTaken(String username, String email) { + if (userDatabase.containsKey(username)) { + return true; // 用户名已存在 + } + return userDatabase.values().stream() + .anyMatch(u -> email.equals(u.email())); // 邮箱已存在 + } + + // --- 核心修改:创建一个全新的、原子化的注册方法 --- + /** + * 在用户设置密码的最后一步,创建并保存一个完整的新用户。 + * @param username 用户名。 + * @param email 邮箱地址。 + * @param password 经过验证的密码。 + * @return 如果成功创建用户,返回 true。 + */ + public boolean createUserWithPassword(String username, String email, String password) { + // 双重检查,确保在并发场景下数据的一致性 + if (isUsernameOrEmailTaken(username, email)) { + return false; + } + User newUser = new User(username, email, password); + userDatabase.put(username, newUser); + saveUsers(); + return true; + } + + // --- 旧的 register 和 setPassword 方法已被上面的新方法取代,可以删除 --- + + // ... (文件中的其他方法,如 login, sendVerificationCode, changePassword 等保持不变) ... + private Map loadUsersFromFile() { if (!Files.exists(USER_FILE_PATH)) { return new ConcurrentHashMap<>(); } - try (FileReader reader = new FileReader(USER_FILE_PATH.toFile())) { Type type = new TypeToken>() {}.getType(); Map loadedUsers = gson.fromJson(reader, type); @@ -63,9 +85,6 @@ public class UserService { } } - /** - * 将内存中的用户数据保存到 {@code users.json} 文件中。 - */ private void saveUsers() { try (FileWriter writer = new FileWriter(USER_FILE_PATH.toFile())) { gson.toJson(this.userDatabase, writer); @@ -74,37 +93,24 @@ public class UserService { } } - /** - * 根据用户名查找用户。 - * - * @param username 要查找的用户名。 - * @return 一个包含 {@link User} 对象的 {@code Optional},如果找不到则为空。 - */ public Optional findUserByUsername(String username) { return Optional.ofNullable(this.userDatabase.get(username)); } - /** - * 验证用户名和密码,执行登录操作。 - * - * @param username 用户的用户名。 - * @param password 用户的密码。 - * @return 如果登录成功,返回一个包含 {@link User} 对象的 {@code Optional};否则返回空的 {@code Optional}。 - */ - public Optional login(String username, String password) { - return findUserByUsername(username) - .filter(user -> user.password().equals(password)); + public Optional login(String identifier, String password) { + Optional userOptional; + if (identifier.contains("@") && ValidationUtils.isEmailValid(identifier)) { + userOptional = this.userDatabase.values().stream() + .filter(user -> identifier.equals(user.email())) + .findFirst(); + } else { + userOptional = findUserByUsername(identifier); + } + return userOptional.filter(user -> user.password() != null && user.password().equals(password)); } - /** - * 向指定的邮箱地址发送一个6位数的随机验证码。 - * - * @param email 接收验证码的目标邮箱地址。 - * @return 如果邮件发送成功,则返回生成的6位验证码字符串;如果失败,则返回 {@code null}。 - */ public String sendVerificationCode(String email) { String code = String.format("%06d", ThreadLocalRandom.current().nextInt(100000, 1000000)); - try { Email mail = new SimpleEmail(); mail.setHostName(EmailConfig.getHost()); @@ -125,66 +131,10 @@ public class UserService { } } - /** - * 注册一个新用户,该用户初始时没有密码。 - * - * @param username 新用户的用户名。 - * @param email 新用户的邮箱地址。 - * @return 如果注册成功,返回 {@code true};如果用户名或邮箱已存在,则返回 {@code false}。 - */ - public boolean register(String username, String email) { - if (userDatabase.containsKey(username)) { - return false; // 用户名已存在 - } - if (userDatabase.values().stream().anyMatch(u -> email.equals(u.email()))) { - return false; // 邮箱已存在 - } - - User newUser = new User(username, email, null); - userDatabase.put(username, newUser); - saveUsers(); - return true; - } - - /** - * 为指定用户设置其初始密码。 - * 此方法只在用户当前密码为 {@code null} 时才允许操作。 - * - * @param username 要设置密码的用户名。 - * @param password 要设置的新密码。 - * @return 如果密码设置成功,返回 {@code true};如果用户不存在或已有密码,则返回 {@code false}。 - */ - public boolean setPassword(String username, String password) { - return findUserByUsername(username) - .map(user -> { - if (user.password() == null) { - User updatedUser = new User(user.username(), user.email(), password); - userDatabase.put(username, updatedUser); - saveUsers(); - return true; - } - return false; - }).orElse(false); - } - - /** - * 验证给定的密码字符串是否符合预设的复杂度要求。 - * - * @param password 待验证的密码。 - * @return 如果密码有效,返回 {@code true};否则返回 {@code false}。 - */ public static boolean isPasswordValid(String password) { return password != null && PASSWORD_PATTERN.matcher(password).matches(); } - /** - * 修改指定用户的密码。 - * - * @param username 要修改密码的用户名。 - * @param oldPassword 用户的当前密码,用于验证。 - * @param newPassword 用户的新密码。 - * @return 如果旧密码正确且新密码设置成功,返回 {@code true};否则返回 {@code false}。 - */ public boolean changePassword(String username, String oldPassword, String newPassword) { return findUserByUsername(username) .filter(user -> user.password().equals(oldPassword)) diff --git a/src/main/resources/com/mathgenerator/view/LoginView.fxml b/src/main/resources/com/mathgenerator/view/LoginView.fxml index 9698a26..8f4ea08 100644 --- a/src/main/resources/com/mathgenerator/view/LoginView.fxml +++ b/src/main/resources/com/mathgenerator/view/LoginView.fxml @@ -30,7 +30,7 @@ - diff --git a/src/main/resources/com/mathgenerator/view/QuizView.fxml b/src/main/resources/com/mathgenerator/view/QuizView.fxml index 52295b0..13942dc 100644 --- a/src/main/resources/com/mathgenerator/view/QuizView.fxml +++ b/src/main/resources/com/mathgenerator/view/QuizView.fxml @@ -83,10 +83,10 @@ - +