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 4d69a1a..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,32 +50,27 @@ maven-compiler-plugin 3.11.0 - 17 - 17 + 17 org.apache.maven.plugins - maven-assembly-plugin - 3.6.0 - - - - com.mathgenerator.MainApplication - - - - jar-with-dependencies - - + maven-shade-plugin + 3.5.2 - make-assembly package - single + shade + + + + com.mathgenerator.Launcher + + + diff --git a/src/main/java/com/mathgenerator/Launcher.java b/src/main/java/com/mathgenerator/Launcher.java new file mode 100644 index 0000000..c20fbce --- /dev/null +++ b/src/main/java/com/mathgenerator/Launcher.java @@ -0,0 +1,13 @@ +package com.mathgenerator; + +/** + * 应用程序的非 JavaFX 启动器。 + * 这是解决 JavaFX fat JAR 打包时 "unnamed module" 警告的 + * 标准解决方案。它通过一个非 JavaFX 的 main 方法来启动真正的 + * JavaFX Application 类。 + */ +public class Launcher { + public static void main(String[] args) { + MainApplication.main(args); + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/MainApplication.java b/src/main/java/com/mathgenerator/MainApplication.java index cb6f216..a93e940 100644 --- a/src/main/java/com/mathgenerator/MainApplication.java +++ b/src/main/java/com/mathgenerator/MainApplication.java @@ -4,21 +4,56 @@ import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; +import javafx.scene.image.Image; import javafx.stage.Stage; import java.io.IOException; +/** + * JavaFX 应用程序的主类。 + *

+ * 该类继承自 {@link Application},是整个 JavaFX 应用的生命周期入口。 + * 它负责初始化主舞台 (Stage),加载初始视图 (LoginView.fxml),设置窗口标题、 + * 图标和样式表,并最终显示应用程序窗口。 + */ public class MainApplication extends Application { + /** + * JavaFX 应用程序的启动方法。 + *

+ * 当应用启动时,JavaFX 平台会调用此方法。它负责配置和显示主窗口。 + * + * @param primaryStage 由 JavaFX 平台自动创建和传入的主舞台对象。 + * @throws IOException 如果在加载 FXML 文件时发生 I/O 错误。 + */ @Override public void start(Stage primaryStage) throws IOException { - // 启动时加载登录界面 Parent root = FXMLLoader.load(getClass().getResource("/com/mathgenerator/view/LoginView.fxml")); + + Scene scene = new Scene(root); + scene.getStylesheets().add(getClass().getResource("/com/mathgenerator/styles/styles.css").toExternalForm()); + primaryStage.setTitle("中小学数学学习软件"); - primaryStage.setScene(new Scene(root)); + + try { + Image appIcon = new Image(getClass().getResourceAsStream("/com/mathgenerator/images/icon.png")); + primaryStage.getIcons().add(appIcon); + } catch (Exception e) { + System.err.println("错误:无法加载应用程序图标!请检查 'icon.png' 是否在 images 文件夹中。"); + e.printStackTrace(); + } + + primaryStage.setScene(scene); primaryStage.setResizable(false); primaryStage.show(); } + /** + * 应用程序的静态 main 方法。 + *

+ * 这是 Java 程序的标准入口点,它通过调用 {@link #launch(String...)} 来启动 JavaFX 应用程序。 + * + * @param args 传递给应用程序的命令行参数。 + */ public static void main(String[] args) { launch(args); } diff --git a/src/main/java/com/mathgenerator/controller/ChangePasswordController.java b/src/main/java/com/mathgenerator/controller/ChangePasswordController.java index ceab172..6b959a6 100644 --- a/src/main/java/com/mathgenerator/controller/ChangePasswordController.java +++ b/src/main/java/com/mathgenerator/controller/ChangePasswordController.java @@ -13,6 +13,10 @@ import javafx.scene.control.PasswordField; import javafx.stage.Stage; import java.io.IOException; +/** + * “修改密码” 视图 (ChangePasswordView.fxml) 的 FXML 控制器类。 + * 该类负责处理已登录用户修改自己密码的逻辑。 + */ public class ChangePasswordController { private final UserService userService = new UserService(); @@ -26,51 +30,96 @@ public class ChangePasswordController { @FXML private Label statusLabel; /** - * 初始化控制器,接收当前用户信息 + * 初始化控制器,并从主菜单接收当前登录的用户信息。 + * 这个方法在 FXML 文件加载完成后被调用。 + * + * @param user 当前登录的 User 对象。 */ public void initData(User user) { this.currentUser = user; } + /** + * 处理“确认修改”按钮的点击事件。 + * 该方法会验证用户输入的密码,并调用用户服务来执行密码修改操作。 + * + * @param event 由按钮点击触发的 ActionEvent 事件。 + */ @FXML private void handleConfirmAction(ActionEvent event) { - // 1. 获取输入 String oldPassword = oldPasswordField.getText(); String newPassword = newPasswordField.getText(); String confirmNewPassword = confirmNewPasswordField.getText(); - // 2. 输入校验 if (oldPassword.isEmpty() || newPassword.isEmpty() || confirmNewPassword.isEmpty()) { - statusLabel.setText("所有密码字段都不能为空!"); + showStatusMessage("所有密码字段都不能为空!", true); return; } if (!newPassword.equals(confirmNewPassword)) { - statusLabel.setText("两次输入的新密码不匹配!"); + showStatusMessage("两次输入的新密码不匹配!", true); + return; + } + // 新增校验:新密码不能与当前密码相同 + if (oldPassword.equals(newPassword)) { + showStatusMessage("新密码不能与当前密码相同!", true); return; } if (!UserService.isPasswordValid(newPassword)) { - statusLabel.setText("新密码格式错误!必须为6-10位,且包含大小写字母和数字。"); + showStatusMessage("新密码格式错误!必须为6-10位,且包含大小写字母和数字。", true); return; } - // 3. 调用后端服务修改密码 boolean success = userService.changePassword( currentUser.username(), oldPassword, newPassword ); - // 4. 更新UI反馈 if (success) { - statusLabel.setText("密码修改成功!请返回主菜单。"); + showStatusMessage("密码修改成功!请返回主菜单。", true); confirmButton.setDisable(true); // 防止重复点击 } else { - statusLabel.setText("修改失败:当前密码错误。"); + showStatusMessage("修改失败:当前密码错误。", true); + } + } + + /** + * 在状态标签 (statusLabel) 中显示一条消息,并更新其样式。 + * + * @param message 要显示的消息文本。 + * @param hasContent 一个布尔值,指示消息是否为空,用于确定应用何种样式。 + */ + 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("password-status-label"); + if (!statusLabel.getStyleClass().contains("password-status-label-with-text")) { + statusLabel.getStyleClass().add("password-status-label-with-text"); + } + } else { + // 移除有内容样式,添加空样式 + statusLabel.getStyleClass().removeAll("password-status-label-with-text"); + if (!statusLabel.getStyleClass().contains("password-status-label")) { + statusLabel.getStyleClass().add("password-status-label"); + } } } /** - * 处理返回按钮事件,返回主菜单 + * 处理“返回主菜单”按钮的点击事件。 + * 导航用户返回到主菜单界面。 + * + * @param event 由按钮点击触发的 ActionEvent 事件。 */ @FXML private void handleBackAction(ActionEvent event) { diff --git a/src/main/java/com/mathgenerator/controller/LoginController.java b/src/main/java/com/mathgenerator/controller/LoginController.java index 264c0c0..fae2b97 100644 --- a/src/main/java/com/mathgenerator/controller/LoginController.java +++ b/src/main/java/com/mathgenerator/controller/LoginController.java @@ -43,44 +43,39 @@ public class LoginController { */ @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 事件对象 - */ + // ... (该文件中的其他方法无需修改) @FXML private void handleRegisterButtonAction(ActionEvent event) { loadScene("/com/mathgenerator/view/RegisterView.fxml"); } - /** - * 加载主菜单界面,并传递用户信息。 - * @param user 登录成功的用户对象 - */ private void loadMainMenu(User user) { try { // 1. 加载 FXML 文件 @@ -103,10 +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/MainMenuController.java b/src/main/java/com/mathgenerator/controller/MainMenuController.java index 60f7930..8af25e8 100644 --- a/src/main/java/com/mathgenerator/controller/MainMenuController.java +++ b/src/main/java/com/mathgenerator/controller/MainMenuController.java @@ -13,6 +13,11 @@ import javafx.scene.control.TextField; import javafx.stage.Stage; import java.io.IOException; + +/** + * “主菜单”视图 (MainMenuView.fxml) 的 FXML 控制器类。 + * 用户登录后进入此界面,可以选择题目难度、数量,并开始答题或进行其他操作。 + */ public class MainMenuController { private User currentUser; @@ -23,68 +28,109 @@ public class MainMenuController { @FXML private Button logoutButton; /** - * 初始化控制器,接收登录成功的用户信息。 - * @param user 当前登录的用户 + * 初始化控制器,并接收登录成功的用户信息。 + * + * @param user 当前登录的用户对象。 */ public void initData(User user) { this.currentUser = user; - welcomeLabel.setText("欢迎, " + currentUser.username() + "!"); + welcomeLabel.setText("🎉 欢迎, " + currentUser.username() + "!"); // 添加图标 + // 初始化状态标签 + clearStatus(); } + /** + * 处理选择“小学”难度按钮的事件。 + * + * @param event 事件对象。 + */ @FXML private void handlePrimaryAction(ActionEvent event) { startQuiz(Level.PRIMARY); } + /** + * 处理选择“初中”难度按钮的事件。 + * + * @param event 事件对象。 + */ @FXML private void handleJuniorHighAction(ActionEvent event) { startQuiz(Level.JUNIOR_HIGH); } + /** + * 处理选择“高中”难度按钮的事件。 + * + * @param event 事件对象。 + */ @FXML private void handleSeniorHighAction(ActionEvent event) { startQuiz(Level.SENIOR_HIGH); } + /** + * 处理“修改密码”按钮的点击事件,导航到修改密码界面。 + * + * @param event 事件对象。 + */ @FXML private void handleChangePasswordAction(ActionEvent event) { try { - // 1\. 加载 FXML 文件 + // 1. 加载 FXML 文件 FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/ChangePasswordView.fxml")); Parent root = loader.load(); - // 2\. 获取新界面的控制器 + // 2. 获取新界面的控制器 ChangePasswordController controller = loader.getController(); - // 3\. 调用控制器的方法,传递当前用户信息 + // 3. 调用控制器的方法,传递当前用户信息 controller.initData(currentUser); - // 4\. 显示新场景 + // 4. 显示新场景 Stage stage = (Stage) logoutButton.getScene().getWindow(); stage.setScene(new Scene(root)); stage.setTitle("修改密码"); } catch (IOException e) { e.printStackTrace(); + setErrorStatus("加载修改密码界面失败!"); } } - @FXML + + /** + * 处理“退出登录”按钮的点击事件,返回到登录界面。 + * + * @param event 事件对象。 + */ + @FXML private void handleLogoutAction(ActionEvent event) { // 跳转回登录界面 loadScene("/com/mathgenerator/view/LoginView.fxml"); } /** - * 验证输入并准备开始答题。 - * @param level 选择的难度 + * 验证用户输入的题目数量,并准备开始答题。 + * 如果输入有效,则加载答题界面并传递相关数据。 + * + * @param level 用户选择的题目难度级别。 */ private void startQuiz(Level level) { try { - int count = Integer.parseInt(questionCountField.getText()); - if (count < 1 || count > 50) { - statusLabel.setText("题目数量必须在 1 到 50 之间!"); + String countText = questionCountField.getText().trim(); + + // 检查是否为空 + if (countText.isEmpty()) { + setErrorStatus("请输入题目数量!"); return; } - + int count = Integer.parseInt(countText); + // 修改范围检查:从1-50改为10-30 + if (count < 10 || count > 30) { + setErrorStatus("题目数量必须在 10 到 30 之间!"); + return; + } + // 清除状态信息 + clearStatus(); // 加载答题界面,并传递数据 FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/QuizView.fxml")); Parent root = loader.load(); @@ -97,12 +143,36 @@ public class MainMenuController { stage.setTitle(level.getChineseName() + " - 答题中"); } catch (NumberFormatException e) { - statusLabel.setText("请输入有效的题目数量!"); + setErrorStatus("请输入有效的数字!"); } catch (IOException e) { e.printStackTrace(); + setErrorStatus("加载答题界面失败,请重试!"); } } + /** + * 在状态标签中设置一条错误消息。 + * + * @param message 要显示的错误消息文本。 + */ + private void setErrorStatus(String message) { + statusLabel.setText(message); + statusLabel.getStyleClass().setAll("status-label"); + } + + /** + * 清除状态标签中的任何消息。 + */ + private void clearStatus() { + statusLabel.setText(""); + statusLabel.getStyleClass().clear(); + } + + /** + * 一个辅助工具方法,用于加载并切换到新的场景。 + * + * @param fxmlPath 要加载的 FXML 文件的路径。 + */ private void loadScene(String fxmlPath) { try { Stage stage = (Stage) logoutButton.getScene().getWindow(); diff --git a/src/main/java/com/mathgenerator/controller/QuizController.java b/src/main/java/com/mathgenerator/controller/QuizController.java index 830e06d..fa13f33 100644 --- a/src/main/java/com/mathgenerator/controller/QuizController.java +++ b/src/main/java/com/mathgenerator/controller/QuizController.java @@ -21,18 +21,18 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +/** + * “答题”视图 (QuizView.fxml) 的 FXML 控制器类。 + * 负责展示题目、接收用户答案、处理答题流程,并在结束后跳转到分数界面。 + */ public class QuizController { - // --- 后端服务 --- private final PaperService paperService; - - // --- 答题状态 --- private User currentUser; private List questions; private List userAnswers = new ArrayList<>(); private int currentQuestionIndex = 0; - // --- FXML 控件 --- @FXML private Label questionNumberLabel; @FXML private ProgressBar progressBar; @FXML private Label questionTextLabel; @@ -42,27 +42,32 @@ public class QuizController { @FXML private Label statusLabel; /** - * 构造函数,初始化后端服务 + * 构造函数。 + * 在这里初始化后端服务,如 PaperService。 */ public QuizController() { - // 这是依赖注入的一种简化形式,在真实项目中会使用框架管理 FileManager fileManager = new FileManager(); MixedDifficultyStrategy strategy = new MixedDifficultyStrategy(); this.paperService = new PaperService(fileManager, strategy); } /** - * 接收从主菜单传递过来的数据,并开始答题 + * 初始化控制器,从主菜单接收用户、难度和题目数量,并开始答题。 + * + * @param user 当前登录的用户。 + * @param level 选择的题目难度。 + * @param questionCount 生成的题目数量。 */ public void initData(User user, Level level, int questionCount) { this.currentUser = user; - // 调用后端服务生成题目 this.questions = paperService.createPaper(user, questionCount, level); + paperService.savePaper(user.username(), this.questions); displayCurrentQuestion(); } /** - * 显示当前的题目和选项 (已更新,增加ABCD前缀) + * 在界面上显示当前的题目和选项。 + * 同时更新进度条和题目编号。 */ private void displayCurrentQuestion() { ChoiceQuestion currentQuestion = questions.get(currentQuestionIndex); @@ -72,47 +77,71 @@ public class QuizController { questionTextLabel.setText(currentQuestion.questionText()); List radioButtons = List.of(option1, option2, option3, option4); - String[] prefixes = {"A. ", "B. ", "C. ", "D. "}; // 定义选项前缀 + String[] prefixes = {"A. ", "B. ", "C. ", "D. "}; for (int i = 0; i < radioButtons.size(); i++) { - // 将前缀和选项文本结合起来 radioButtons.get(i).setText(prefixes[i] + currentQuestion.options().get(i)); } - optionsGroup.selectToggle(null); // 清除上一次的选择 - statusLabel.setText(""); // 清除状态提示 + optionsGroup.selectToggle(null); + updateStatusLabelStyle(false); + // 更新按钮文字:如果不是最后一题显示"下一题",最后一题显示"完成答题" if (currentQuestionIndex == questions.size() - 1) { submitButton.setText("完成答题"); + } else { + submitButton.setText("下一题"); } } /** - * 处理提交按钮的点击事件 + * 处理“下一题”按钮的点击事件。 + * 记录用户的选择,然后切换到下一题或结束答题。 + * + * @param event 事件对象。 */ @FXML private void handleSubmitButtonAction(ActionEvent event) { RadioButton selectedRadioButton = (RadioButton) optionsGroup.getSelectedToggle(); if (selectedRadioButton == null) { statusLabel.setText("请选择一个答案!"); + updateStatusLabelStyle(true); return; } - // 记录用户答案的索引 + updateStatusLabelStyle(false); + List radioButtons = List.of(option1, option2, option3, option4); userAnswers.add(radioButtons.indexOf(selectedRadioButton)); - // 移动到下一题或结束答题 currentQuestionIndex++; if (currentQuestionIndex < questions.size()) { displayCurrentQuestion(); } else { - // 答题结束,计算分数并跳转到分数界面 calculateScoreAndShowResults(); } } /** - * 计算分数并准备跳转到结果页面 + * 根据状态标签是否有内容来更新其 CSS 样式。 + * + * @param hasContent 如果为 true,应用带文本的样式;否则,应用空标签的样式。 + */ + private void updateStatusLabelStyle(boolean hasContent) { + if (hasContent) { + statusLabel.getStyleClass().removeAll("quiz-status-label-empty"); + if (!statusLabel.getStyleClass().contains("quiz-status-label-with-text")) { + statusLabel.getStyleClass().add("quiz-status-label-with-text"); + } + } else { + statusLabel.getStyleClass().removeAll("quiz-status-label-with-text"); + if (!statusLabel.getStyleClass().contains("quiz-status-label-empty")) { + statusLabel.getStyleClass().add("quiz-status-label-empty"); + } + } + } + + /** + * 在所有题目回答完毕后,计算最终得分并导航到分数显示界面。 */ private void calculateScoreAndShowResults() { int correctCount = 0; @@ -123,22 +152,19 @@ public class QuizController { } double score = (double) correctCount / questions.size() * 100; - // 禁用当前页面的按钮 submitButton.setDisable(true); statusLabel.setText("答题已完成,正在为您计算分数..."); + updateStatusLabelStyle(true); - // 加载分数界面并传递数据 try { FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/ScoreView.fxml")); Parent root = loader.load(); - ScoreController controller = loader.getController(); - controller.initData(currentUser, score); // 将用户和分数传递过去 + controller.initData(currentUser, score); Stage stage = (Stage) submitButton.getScene().getWindow(); stage.setScene(new Scene(root)); stage.setTitle("答题结果"); - } catch (IOException e) { e.printStackTrace(); } diff --git a/src/main/java/com/mathgenerator/controller/RegisterController.java b/src/main/java/com/mathgenerator/controller/RegisterController.java index d7fd31a..efa4ca5 100644 --- a/src/main/java/com/mathgenerator/controller/RegisterController.java +++ b/src/main/java/com/mathgenerator/controller/RegisterController.java @@ -1,6 +1,9 @@ package com.mathgenerator.controller; import com.mathgenerator.service.UserService; +import com.mathgenerator.util.ValidationUtils; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; @@ -11,12 +14,20 @@ import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.control.TextField; import javafx.stage.Stage; +import javafx.util.Duration; + import java.io.IOException; -import com.mathgenerator.util.ValidationUtils; + +/** + * “注册”视图 (RegisterView.fxml) 的 FXML 控制器类。 + * 负责处理新用户的注册流程,包括发送验证码和验证用户输入。 + */ public class RegisterController { private final UserService userService = new UserService(); - private String sentCode; // 用于存储已发送的验证码 + private String sentCode; + private Timeline countdownTimeline; + private int countdownSeconds = 60; @FXML private TextField usernameField; @FXML private TextField emailField; @@ -28,65 +39,129 @@ public class RegisterController { @FXML private Button backToLoginButton; @FXML private Label statusLabel; + /** + * 处理“发送验证码”按钮的点击事件。 + * 验证邮箱格式,并调用服务发送验证码邮件,然后启动一个60秒的冷却倒计时。 + * + * @param event 事件对象。 + */ @FXML private void handleSendCodeAction(ActionEvent event) { String email = emailField.getText(); - if (email.isEmpty() || !email.contains("@")) { - statusLabel.setText("请输入一个有效的邮箱地址!"); + if (!ValidationUtils.isEmailValid(email)) { + showStatusMessage("请输入一个有效的邮箱地址!", true); return; } - // 调用后端服务发送验证码 this.sentCode = userService.sendVerificationCode(email); - // 处理发送结果 if (this.sentCode != null) { - statusLabel.setText("验证码已成功发送,请查收您的邮箱。"); - sendCodeButton.setDisable(true); // 防止重复点击 + showStatusMessage("验证码已成功发送,请查收您的邮箱。", true); + startCountdown(); } else { - statusLabel.setText("验证码发送失败!请检查配置或联系管理员。"); + showStatusMessage("验证码发送失败!请检查配置或联系管理员。", true); } } + /** + * 启动发送验证码按钮的60秒冷却倒计时。 + * 在倒计时期间,按钮将被禁用。 + */ + private void startCountdown() { + sendCodeButton.setDisable(true); + countdownTimeline = new Timeline(new KeyFrame(Duration.seconds(1), e -> { + countdownSeconds--; + sendCodeButton.setText(countdownSeconds + "s后重试"); + if (countdownSeconds <= 0) { + countdownTimeline.stop(); + sendCodeButton.setDisable(false); + sendCodeButton.setText("发送验证码"); + countdownSeconds = 60; + } + })); + countdownTimeline.setCycleCount(60); + countdownTimeline.play(); + } + + + /** + * 处理“前往设置密码”按钮的点击事件。 + * (已修改) 现在只验证信息,不写入文件,然后导航到设置密码界面。 + * + * @param event 事件对象。 + */ @FXML private void handleRegisterAction(ActionEvent event) { - // 1. 字段校验 (已简化,不再校验密码) String username = usernameField.getText(); String email = emailField.getText(); if (!ValidationUtils.isUsernameValid(username) || !ValidationUtils.isEmailValid(email) || verificationCodeField.getText().isEmpty()) { - statusLabel.setText("所有字段都不能为空且格式正确!"); + showStatusMessage("所有字段都不能为空且格式正确!", true); return; } + if (this.sentCode == null || !this.sentCode.equals(verificationCodeField.getText())) { - statusLabel.setText("验证码错误!"); + showStatusMessage("验证码错误!", true); return; } - // 2. 调用后端服务进行无密码注册 - boolean success = userService.register(username, email); + // --- 核心修改:不再调用 register,而是检查用户名和邮箱是否已存在 --- + if (userService.isUsernameOrEmailTaken(username, email)) { + showStatusMessage("注册失败:用户名或邮箱已被占用。", true); + return; + } + + // 验证通过,导航到设置密码界面,并传递用户名和邮箱 + showStatusMessage("验证成功!请设置您的密码。", true); + loadSetPasswordScene(username, email); + } + + - // 3. 根据结果更新UI或跳转 - if (success) { - statusLabel.setText("注册成功!请设置您的密码。"); - // 成功后,加载设置密码界面,并传递用户名 - loadSetPasswordScene(username); + /** + * 在状态标签中显示一条消息,并根据内容更新其样式。 + * + * @param message 要显示的消息。 + * @param hasContent 如果消息非空则为true。 + */ + 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("register-status-label"); + if (!statusLabel.getStyleClass().contains("register-status-label-with-text")) { + statusLabel.getStyleClass().add("register-status-label-with-text"); + } } else { - statusLabel.setText("注册失败:用户名或邮箱已被占用。"); + statusLabel.getStyleClass().removeAll("register-status-label-with-text"); + if (!statusLabel.getStyleClass().contains("register-status-label")) { + statusLabel.getStyleClass().add("register-status-label"); + } } } /** - * (新增) 加载设置密码界面,并传递用户名。 + * (已修改) 加载“设置密码”场景,并将新注册的用户名和邮箱传递过去。 + * + * @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)); @@ -96,11 +171,21 @@ public class RegisterController { } } + /** + * 处理“返回登录”按钮的点击事件,导航回登录界面。 + * + * @param event 事件对象。 + */ @FXML private void handleBackToLoginAction(ActionEvent event) { loadScene("/com/mathgenerator/view/LoginView.fxml"); } + /** + * 辅助方法,用于加载并切换到指定的FXML场景。 + * + * @param fxmlPath FXML文件的路径。 + */ private void loadScene(String fxmlPath) { try { Parent root = FXMLLoader.load(getClass().getResource(fxmlPath)); @@ -110,4 +195,13 @@ public class RegisterController { e.printStackTrace(); } } + + /** + * 在控制器销毁前调用的清理方法,用于停止任何正在运行的后台任务,如倒计时。 + */ + public void cleanup() { + if (countdownTimeline != null) { + countdownTimeline.stop(); + } + } } \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/controller/ScoreController.java b/src/main/java/com/mathgenerator/controller/ScoreController.java index dbcba69..584d1e2 100644 --- a/src/main/java/com/mathgenerator/controller/ScoreController.java +++ b/src/main/java/com/mathgenerator/controller/ScoreController.java @@ -11,6 +11,10 @@ import javafx.scene.control.Label; import javafx.stage.Stage; import java.io.IOException; +/** + * “分数”视图 (ScoreView.fxml) 的 FXML 控制器类。 + * 负责在用户完成答题后,显示最终得分和相应的鼓励消息。 + */ public class ScoreController { private User currentUser; @@ -21,15 +25,15 @@ public class ScoreController { @FXML private Button logoutButton; /** - * 初始化控制器,接收答题结果数据 - * @param user 当前用户 - * @param score 最终得分 + * 初始化控制器,并接收从答题界面传递过来的用户和分数数据。 + * + * @param user 当前登录的用户对象。 + * @param score 用户在上一轮答题中获得的最终分数。 */ public void initData(User user, double score) { this.currentUser = user; scoreLabel.setText(String.format("%.2f", score)); - // 根据分数显示不同的鼓励语 if (score == 100.0) { resultMessageLabel.setText("太棒了!你答对了所有题目!"); } else if (score >= 80) { @@ -42,7 +46,10 @@ public class ScoreController { } /** - * 处理“再做一组”按钮事件,返回主菜单 + * 处理“再做一组”按钮的点击事件。 + * 导航用户返回到主菜单,以便开始新一轮的练习。 + * + * @param event 由按钮点击触发的 ActionEvent 事件。 */ @FXML private void handleTryAgainAction(ActionEvent event) { @@ -50,7 +57,7 @@ public class ScoreController { FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/MainMenuView.fxml")); Parent root = loader.load(); MainMenuController controller = loader.getController(); - controller.initData(currentUser); // 将用户信息传回主菜单 + controller.initData(currentUser); Stage stage = (Stage) tryAgainButton.getScene().getWindow(); stage.setScene(new Scene(root)); @@ -61,7 +68,10 @@ public class ScoreController { } /** - * 处理“退出登录”按钮事件,返回登录界面 + * 处理“退出登录”按钮的点击事件。 + * 导航用户返回到登录界面。 + * + * @param event 由按钮点击触发的 ActionEvent 事件。 */ @FXML private void handleLogoutAction(ActionEvent event) { diff --git a/src/main/java/com/mathgenerator/controller/SetPasswordController.java b/src/main/java/com/mathgenerator/controller/SetPasswordController.java index 278c80c..3630f46 100644 --- a/src/main/java/com/mathgenerator/controller/SetPasswordController.java +++ b/src/main/java/com/mathgenerator/controller/SetPasswordController.java @@ -19,6 +19,7 @@ public class SetPasswordController { private final UserService userService = new UserService(); private String username; + private String email; // 新增一个字段来存储邮箱 @FXML private Label promptLabel; @FXML private PasswordField newPasswordField; @@ -27,48 +28,78 @@ 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 事件对象。 + */ @FXML private void handleConfirmAction(ActionEvent event) { String newPassword = newPasswordField.getText(); String confirmPassword = confirmPasswordField.getText(); if (!newPassword.equals(confirmPassword)) { - statusLabel.setText("两次输入的密码不匹配!"); + showStatusMessage("两次输入的密码不匹配!", true); return; } + if (!ValidationUtils.isPasswordValid(newPassword)) { - statusLabel.setText("新密码格式错误!必须为6-10位,且包含大小写字母和数字。"); + showStatusMessage("新密码格式错误!必须为6-10位,且包含大小写字母和数字。", true); return; } - // 调用后端服务设置密码 - boolean success = userService.setPassword(this.username, newPassword); + // --- 核心修改:调用新的服务方法,完成最终的注册和密码设置 --- + boolean success = userService.createUserWithPassword(this.username, this.email, newPassword); if (success) { - statusLabel.setText("密码设置成功!正在进入主菜单..."); - // 密码设置成功后,获取完整的用户信息并直接跳转到主菜单 + showStatusMessage("注册成功!正在进入主菜单...", true); + // 注册成功后,直接查找这个新创建的用户并登录 userService.findUserByUsername(this.username).ifPresent(this::loadMainMenu); } else { - statusLabel.setText("密码设置失败,请稍后重试或重新注册。"); + // 这个分支理论上很难进入,除非在极短时间内有其他人注册了相同的用户名/邮箱 + showStatusMessage("注册失败,用户名或邮箱可能刚刚被占用,请重新注册。", true); + } + } + + // ... (文件中的 showStatusMessage, updateStatusLabelStyle, loadMainMenu 方法保持不变) ... + + private void showStatusMessage(String message, boolean hasContent) { + statusLabel.setText(message); + updateStatusLabelStyle(hasContent); + } + + private void updateStatusLabelStyle(boolean hasContent) { + if (hasContent) { + statusLabel.getStyleClass().removeAll("setpassword-status-label"); + if (!statusLabel.getStyleClass().contains("setpassword-status-label-with-text")) { + statusLabel.getStyleClass().add("setpassword-status-label-with-text"); + } + } else { + statusLabel.getStyleClass().removeAll("setpassword-status-label-with-text"); + if (!statusLabel.getStyleClass().contains("setpassword-status-label")) { + statusLabel.getStyleClass().add("setpassword-status-label"); + } } } - /** - * 加载主菜单界面,并传递用户信息 (实现自动登录) - */ private void loadMainMenu(User user) { try { FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/MainMenuView.fxml")); Parent root = loader.load(); MainMenuController controller = loader.getController(); - controller.initData(user); // 将完整的User对象传递给主菜单 + controller.initData(user); Stage stage = (Stage) confirmButton.getScene().getWindow(); stage.setScene(new Scene(root)); diff --git a/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java b/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java index add01a1..6d51dc5 100644 --- a/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java +++ b/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java @@ -6,58 +6,56 @@ import java.util.List; import java.util.concurrent.ThreadLocalRandom; /** - * 初中选择题生成器 (最终版 - 采用结构化插入)。 - * 通过直接在表达式结构中插入运算项,确保语法正确性和高性能。 + * 初中难度选择题的生成器。 + *

+ * 该类继承自 {@link PrimarySchoolGenerator},并在其基础上增加了平方和开平方根运算。 + * 它通过在基础表达式中结构化地插入这些新运算项,来确保生成的题目语法正确且性能高。 */ public class JuniorHighSchoolGenerator extends PrimarySchoolGenerator { private static final int[] PERFECT_SQUARES = {1, 4, 9, 16, 25, 36, 49, 64, 81, 100}; + /** + * 生成一道初中难度的数学选择题。 + *

+ * 此方法首先生成一个基础的四则运算表达式,然后随机地将其中一个操作数替换为 + * 平方或开方运算,最后计算结果并生成选项。如果过程中发生任何计算错误, + * 它会安全地回退到生成一道小学难度的题目。 + * + * @return 一个封装了初中难度题目的 {@link ChoiceQuestion} 对象。 + */ @Override public ChoiceQuestion generateSingleQuestion() { ThreadLocalRandom random = ThreadLocalRandom.current(); - int operandCount = random.nextInt(2, 5); // 2到4个操作数 + int operandCount = random.nextInt(2, 5); - // 1. 生成基础的表达式组件列表 List parts = new ArrayList<>(); - // 使用 getOperand() 和 getRandomOperator() 这些继承自父类的方法 parts.add(String.valueOf(getOperand())); for (int i = 1; i < operandCount; i++) { parts.add(getRandomOperator()); parts.add(String.valueOf(getOperand())); } - - // 2. 结构化地插入初中特色运算 - int modificationIndex = random.nextInt(operandCount) * 2; // 随机选择一个操作数的位置 + int modificationIndex = random.nextInt(operandCount) * 2; boolean useSquare = random.nextBoolean(); - if (useSquare) { - // 平方策略:直接在数字后附加平方符号 parts.set(modificationIndex, parts.get(modificationIndex) + "²"); } else { - // 开根号策略:用一个完美的开根号表达式替换整个数字 int perfectSquare = PERFECT_SQUARES[random.nextInt(PERFECT_SQUARES.length)]; parts.set(modificationIndex, "√" + perfectSquare); } - // 3. (可选)为增强后的表达式添加括号 if (operandCount > 2 && random.nextBoolean()) { - super.addParentheses(parts); // 调用父类的protected方法 + super.addParentheses(parts); } String finalQuestionText = String.join(" ", parts); - - // 4. 计算答案 double finalCorrectAnswer; try { Object result = evaluateExpression(finalQuestionText); finalCorrectAnswer = ((Number) result).doubleValue(); } catch (Exception e) { - // 发生意外,安全返回一个小学题 return super.generateSingleQuestion(); } - - // 5. 生成选项 List options = generateDecimalOptions(finalCorrectAnswer); int correctIndex = options.indexOf(formatNumber(finalCorrectAnswer)); diff --git a/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java b/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java index 530e6dc..e896739 100644 --- a/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java +++ b/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java @@ -1,6 +1,7 @@ package com.mathgenerator.generator; -import com.mathgenerator.model.ChoiceQuestion; // 导入新模型 +import com.mathgenerator.model.ChoiceQuestion; +import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -10,11 +11,14 @@ import java.util.concurrent.ThreadLocalRandom; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; -import java.text.DecimalFormat; /** - * 小学选择题生成器。 - * 生成包含 + - * / 和 () 的运算,并提供四个选项。 + * 小学难度选择题的生成器。 + *

+ * 该类负责生成包含加、减、乘、除和括号的四则运算题目。 + * 生成的题目会确保其计算结果为整数,并提供四个选项。 + * + * @see QuestionGenerator */ public class PrimarySchoolGenerator implements QuestionGenerator { @@ -22,7 +26,11 @@ public class PrimarySchoolGenerator implements QuestionGenerator { /** * 生成一道小学难度的数学选择题。 - * @return 一个包含题干、四个选项和正确答案索引的 ChoiceQuestion 对象。 + *

+ * 该方法会循环生成一个包含2到4个操作数的随机表达式,直到表达式的计算结果为整数。 + * 然后,它会围绕正确答案生成三个干扰项,并将它们随机排序后,封装成一个 {@link ChoiceQuestion} 对象。 + * + * @return 一个包含题干、四个选项和正确答案索引的 {@code ChoiceQuestion} 对象。 */ @Override public ChoiceQuestion generateSingleQuestion() { @@ -65,9 +73,13 @@ public class PrimarySchoolGenerator implements QuestionGenerator { } /** - * 生成四个选项 (1个正确,3个干扰项) - * @param correctAnswer 正确答案 - * @return 包含四个选项的随机排序列表 + * 为给定的正确答案生成四个选项(一个正确,三个干扰项)。 + *

+ * 干扰项是通过在正确答案上加或减一个1到10之间的随机数生成的。 + * 所有选项(包括正确答案)会被放入一个列表中并随机打乱顺序。 + * + * @param correctAnswer 正确的整数答案。 + * @return 一个包含四个选项字符串的随机排序列表。 */ protected List generateOptions(int correctAnswer) { ThreadLocalRandom random = ThreadLocalRandom.current(); @@ -88,28 +100,28 @@ public class PrimarySchoolGenerator implements QuestionGenerator { } /** - * 使用JVM的脚本引擎计算字符串表达式的值 (已优化兼容性)。 - * @param expression 数学表达式字符串 - * @return 计算结果 (可能是Integer或Double) - * @throws ScriptException 如果表达式有语法错误 + * 使用JVM的脚本引擎计算字符串表达式的值。 + *

+ * 此方法已优化以兼容 Rhino 引擎,并能处理平方、开方和三角函数等运算。 + * + * @param expression 数学表达式字符串,例如 " (3 + 5) * 2 "。 + * @return 计算结果,通常是 {@code Integer} 或 {@code Double} 类型。 + * @throws ScriptException 如果表达式包含语法错误。 + * @throws IllegalStateException 如果找不到 Rhino JavaScript 引擎。 */ protected Object evaluateExpression(String expression) throws ScriptException { ScriptEngineManager manager = new ScriptEngineManager(); - // --- 核心修改在这里 --- - // 使用 "rhino" 作为引擎名称,这是Rhino引擎的官方名称 ScriptEngine engine = manager.getEngineByName("rhino"); if (engine == null) { - // 增加一个健壮性检查,如果引擎还是没找到,就给出清晰的错误提示 throw new IllegalStateException("错误:找不到Rhino JavaScript引擎。请检查pom.xml中是否已添加rhino-engine的依赖。"); } - // Rhino不需要预定义函数,可以直接计算 + // 预处理表达式以兼容Rhino引擎的数学函数 String script = expression.replaceAll("(\\d+(\\.\\d+)?)²", "Math.pow($1, 2)") .replaceAll("√(\\d+(\\.\\d+)?)", "Math.sqrt($1)") - .replaceAll("(\\d+)°", " * (Math.PI / 180)"); // Rhino对角度计算的语法要求更严格 + .replaceAll("(\\d+)°", " * (Math.PI / 180)"); - // 为了让sin/cos/tan能正确计算,需要特殊处理 script = script.replaceAll("sin\\(", "Math.sin(") .replaceAll("cos\\(", "Math.cos(") .replaceAll("tan\\(", "Math.tan("); @@ -118,24 +130,27 @@ public class PrimarySchoolGenerator implements QuestionGenerator { } /** - * 格式化数字,最多保留两位小数。 - * @param number 待格式化的数字 - * @return 格式化后的字符串 + * 将一个 double 类型的数字格式化为字符串,最多保留两位小数。 + *

+ * 如果数字是整数,则不显示小数位。 + * + * @param number 待格式化的数字。 + * @return 格式化后的字符串。 */ protected String formatNumber(double number) { if (number == (long) number) { - return String.format("%d", (long) number); // 如果是整数,不显示小数位 + return String.format("%d", (long) number); } else { - // 使用DecimalFormat来去除末尾多余的0 DecimalFormat df = new DecimalFormat("#.##"); return df.format(number); } } /** - * 为小数答案生成四个选项。 - * @param correctAnswer 正确答案 - * @return 包含四个选项的随机排序列表 + * 为给定的小数正确答案生成四个选项。 + * + * @param correctAnswer 正确的小数答案。 + * @return 一个包含四个格式化后选项字符串的随机排序列表。 */ protected List generateDecimalOptions(double correctAnswer) { ThreadLocalRandom random = ThreadLocalRandom.current(); @@ -143,8 +158,7 @@ public class PrimarySchoolGenerator implements QuestionGenerator { options.add(formatNumber(correctAnswer)); while (options.size() < 4) { - double delta = random.nextDouble(1, 11); // 答案加减1-10之间的随机小数 - // 随机决定是加还是减 + double delta = random.nextDouble(1, 11); double distractor = random.nextBoolean() ? correctAnswer + delta : correctAnswer - delta; options.add(formatNumber(distractor)); } @@ -154,14 +168,29 @@ public class PrimarySchoolGenerator implements QuestionGenerator { return sortedOptions; } + /** + * 获取一个1到100之间的随机整数作为操作数。 + * + * @return 一个随机整数。 + */ protected int getOperand() { return ThreadLocalRandom.current().nextInt(1, 101); } + /** + * 从 {@code OPERATORS} 数组中随机选择一个运算符。 + * + * @return 一个随机的运算符字符串("+"、"-"、"*" 或 "/")。 + */ protected String getRandomOperator() { return OPERATORS[ThreadLocalRandom.current().nextInt(OPERATORS.length)]; } + /** + * 为给定的表达式组件列表随机添加一对括号。 + * + * @param parts 包含数字和运算符的字符串列表,将被原地修改。 + */ protected void addParentheses(List parts) { ThreadLocalRandom random = ThreadLocalRandom.current(); int startOperandIndex = random.nextInt(parts.size() / 2); @@ -171,7 +200,4 @@ public class PrimarySchoolGenerator implements QuestionGenerator { parts.add(endIndex + 1, ")"); parts.add(startIndex, "("); } - - - } \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/generator/QuestionGenerator.java b/src/main/java/com/mathgenerator/generator/QuestionGenerator.java index e478038..c6e930b 100644 --- a/src/main/java/com/mathgenerator/generator/QuestionGenerator.java +++ b/src/main/java/com/mathgenerator/generator/QuestionGenerator.java @@ -1,14 +1,22 @@ package com.mathgenerator.generator; -import com.mathgenerator.model.ChoiceQuestion; // 导入新模型 +import com.mathgenerator.model.ChoiceQuestion; /** - * 题目生成器接口,定义了所有具体生成器必须实现的方法。 + * 题目生成器接口。 + *

+ * 该接口定义了所有具体题目生成器(如小学、初中、高中)必须实现的核心方法。 + * 任何实现此接口的类都应具备生成单个数学选择题的能力。 */ public interface QuestionGenerator { + /** * 生成一道符合特定难度的数学选择题。 - * @return 代表数学选择题的 ChoiceQuestion 对象 + *

+ * 此方法的实现应确保生成的题目包含题干、四个选项以及正确答案的索引, + * 并将这些信息封装在一个 {@link ChoiceQuestion} 对象中返回。 + * + * @return 一个代表新生成的数学选择题的 {@code ChoiceQuestion} 对象。 */ ChoiceQuestion generateSingleQuestion(); } \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/generator/SafePrimarySchoolGenerator.java b/src/main/java/com/mathgenerator/generator/SafePrimarySchoolGenerator.java index d38cee6..80f4b4d 100644 --- a/src/main/java/com/mathgenerator/generator/SafePrimarySchoolGenerator.java +++ b/src/main/java/com/mathgenerator/generator/SafePrimarySchoolGenerator.java @@ -7,18 +7,24 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; -import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; -import javax.script.ScriptException; /** - * “安全”的小学题目生成器,确保运算过程中不产生负数,并生成选择题。 + * “安全”的小学题目生成器。 + *

+ * 这个生成器专门用于创建小学难度的数学题,并确保在整个运算过程中不会产生任何负数结果。 + * 它通过一种递归下降的算法来构造表达式,从而在生成阶段就避免了负数的出现。 + * + * @see QuestionGenerator */ public class SafePrimarySchoolGenerator implements QuestionGenerator { /** - * 生成一道确保结果非负的小学难度的数学选择题。 - * @return 一个符合所有约束的 ChoiceQuestion 对象。 + * 生成一道确保结果非负的小学难度数学选择题。 + *

+ * 该方法首先确定一个操作数的总预算,然后调用递归方法 {@code generateSafeExpression} + * 来构造一个保证结果为正数的表达式,并最终封装成 {@link ChoiceQuestion} 对象。 + * + * @return 一个符合所有非负约束的 {@code ChoiceQuestion} 对象。 */ @Override public ChoiceQuestion generateSingleQuestion() { @@ -34,12 +40,21 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator { } /** - * 内部记录类,用于在递归生成表达式时传递部分结果。 + * 内部记录 (Record),用于在递归生成表达式时传递部分结果。 + * + * @param parts 表达式的字符串组件列表。 + * @param value 该表达式的计算结果。 + * @param operandsUsed 生成此表达式已使用的操作数数量。 */ private record Term(List parts, int value, int operandsUsed) {} /** - * 根据操作数预算,生成一个确保结果非负的(子)表达式。 + * 根据给定的操作数预算,递归地生成一个确保结果非负的(子)表达式。 + *

+ * 算法的核心在于,当执行减法时,会检查确保被减数大于减数。 + * + * @param operandBudget 当前可用于生成表达式的操作数数量。 + * @return 一个包含表达式、其值和所用操作数数量的 {@code Term} 对象。 */ private Term generateSafeExpression(int operandBudget) { ThreadLocalRandom random = ThreadLocalRandom.current(); @@ -72,7 +87,12 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator { } /** - * 生成一个“项”(Term)。一个项可以是简单的乘除法序列,也可以是带括号的子表达式。 + * 生成一个“项”(Term)。 + *

+ * 一个项可以是一个简单的乘除法序列,或者是一个带括号的、更复杂的子表达式。 + * + * @param operandsRemaining 剩余可用的操作数数量。 + * @return 代表所生成项的 {@code Term} 对象。 */ private Term generateTerm(int operandsRemaining) { ThreadLocalRandom random = ThreadLocalRandom.current(); @@ -91,7 +111,12 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator { } /** - * 生成一个仅包含乘除法的简单项。 + * 生成一个仅包含乘法或除法的简单项。 + *

+ * 为了确保除法的结果是整数,除数会从被除数的所有因数中选取。 + * + * @param operandsRemaining 剩余可用的操作数数量。 + * @return 代表简单项的 {@code Term} 对象。 */ private Term generateSimpleTerm(int operandsRemaining) { ThreadLocalRandom random = ThreadLocalRandom.current(); @@ -118,6 +143,12 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator { return new Term(parts, termValue, operandsUsed); } + /** + * 获取一个正整数的所有因数。 + * + * @param number 要查找因数的数字。 + * @return 包含该数字所有因数的列表。 + */ private List getDivisors(int number) { List divisors = new ArrayList<>(); for (int i = 1; i <= number; i++) { @@ -127,7 +158,12 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator { } /** - * 生成四个选项 (1个正确,3个干扰项) + * 为给定的正确答案生成四个选项(一个正确,三个干扰项)。 + *

+ * 此方法确保所有生成的干扰项也都是非负数。 + * + * @param correctAnswer 正确的整数答案。 + * @return 一个包含四个非负选项的随机排序列表。 */ private List generateOptions(int correctAnswer) { ThreadLocalRandom random = ThreadLocalRandom.current(); @@ -137,8 +173,7 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator { while (options.size() < 4) { int delta = random.nextInt(1, 11); int distractor = random.nextBoolean() ? correctAnswer + delta : correctAnswer - delta; - // 确保干扰项非负 - if (distractor >= 0) { + if (distractor >= 0) { // 确保干扰项非负 options.add(String.valueOf(distractor)); } } diff --git a/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java b/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java index 4dc6be4..c038127 100644 --- a/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java +++ b/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java @@ -6,8 +6,10 @@ import java.util.Map; import java.util.concurrent.ThreadLocalRandom; /** - * 高中选择题生成器 (最终版 - 采用构造式添加)。 - * 通过在已生成的初中题基础上添加预设的三角函数项来构造题目。 + * 高中难度选择题的生成器。 + *

+ * 该类继承自 {@link JuniorHighSchoolGenerator},通过在已生成的初中难度题目基础上, + * 构造性地添加一个预设的、计算结果简单的三角函数项来生成题目。 */ public class SeniorHighSchoolGenerator extends JuniorHighSchoolGenerator { @@ -24,6 +26,14 @@ public class SeniorHighSchoolGenerator extends JuniorHighSchoolGenerator { // 将Map的键转换为数组,方便随机选取 private static final String[] TRIG_KEYS = TRIG_TERMS.keySet().toArray(new String[0]); + /** + * 生成一道高中难度的数学选择题。 + *

+ * 此方法首先调用父类方法生成一个初中难度的题目作为基础。然后,它随机选择一个 + * 预设的三角函数项,并将其与初中题目通过加法或减法结合,形成最终的高中题目。 + * + * @return 一个封装了高中难度题目的 {@link ChoiceQuestion} 对象。 + */ @Override public ChoiceQuestion generateSingleQuestion() { // 1. 先生成一个保证可计算的、高性能的初中选择题,作为基础 diff --git a/src/main/java/com/mathgenerator/model/ChoiceQuestion.java b/src/main/java/com/mathgenerator/model/ChoiceQuestion.java index 485b2ec..7d18220 100644 --- a/src/main/java/com/mathgenerator/model/ChoiceQuestion.java +++ b/src/main/java/com/mathgenerator/model/ChoiceQuestion.java @@ -3,10 +3,14 @@ package com.mathgenerator.model; import java.util.List; /** - * 选择题数据模型。 - * @param questionText 题干 - * @param options 四个选项的列表 - * @param correctOptionIndex 正确答案在列表中的索引 (0-3) + * 选择题数据模型 (Record)。 + *

+ * 该记录 (Record) 用于以不可变的方式封装一道选择题的所有核心信息, + * 包括题干、选项列表以及正确答案的索引。 + * + * @param questionText 题目的文本内容,即题干。 + * @param options 一个包含四个选项字符串的列表 (List)。 + * @param correctOptionIndex 正确答案在 {@code options} 列表中的索引,范围从 0 到 3。 */ public record ChoiceQuestion(String questionText, List options, int correctOptionIndex) { } \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/model/Level.java b/src/main/java/com/mathgenerator/model/Level.java index 8b27203..7f47178 100644 --- a/src/main/java/com/mathgenerator/model/Level.java +++ b/src/main/java/com/mathgenerator/model/Level.java @@ -1,14 +1,29 @@ package com.mathgenerator.model; /** - * 学段枚举,用于类型安全地表示小学、初中和高中。 + * 学段枚举 (Enum)。 + *

+ * 该枚举用于以类型安全的方式表示程序支持的不同题目难度级别, + * 包括小学、初中和高中。每个枚举常量都关联一个中文名称。 */ public enum Level { + /** + * 代表小学难度。 + */ PRIMARY("小学"), + + /** + * 代表初中难度。 + */ JUNIOR_HIGH("初中"), + + /** + * 代表高中难度。 + */ SENIOR_HIGH("高中"); private final String chineseName; + /** * 枚举的构造函数。 * @@ -17,10 +32,11 @@ public enum Level { Level(String chineseName) { this.chineseName = chineseName; } + /** - * 获取学段的中文名称。 + * 获取该学段对应的中文名称。 * - * @return 表示学段的中文名称字符串。 + * @return 表示学段的中文名称字符串 (例如, "小学")。 */ public String getChineseName() { return chineseName; diff --git a/src/main/java/com/mathgenerator/model/User.java b/src/main/java/com/mathgenerator/model/User.java index 66ecbd3..eccf84e 100644 --- a/src/main/java/com/mathgenerator/model/User.java +++ b/src/main/java/com/mathgenerator/model/User.java @@ -1,10 +1,14 @@ package com.mathgenerator.model; /** - * 用户数据记录 (Record),用于封装不可变的用户信息。 - * @param username 用户名 - * @param email 邮箱地址 (新增) - * @param password 密码 + * 用户数据记录 (Record)。 + *

+ * 该记录 (Record) 用于以不可变的方式封装一个用户的基本信息, + * 包括用户名、邮箱地址和密码。 + * + * @param username 用户的唯一标识符,即用户名。 + * @param email 用户的电子邮箱地址,用于接收验证码等。 + * @param password 用户账户的密码。在用户首次注册但还未设置密码时,此字段可能为 {@code null}。 */ public record User(String username, String email, String password) { } \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/service/EmailConfig.java b/src/main/java/com/mathgenerator/service/EmailConfig.java index 44618f8..a28c5ce 100644 --- a/src/main/java/com/mathgenerator/service/EmailConfig.java +++ b/src/main/java/com/mathgenerator/service/EmailConfig.java @@ -5,6 +5,13 @@ import java.io.IOException; import java.io.InputStream; import java.util.Properties; +/** + * 邮件配置加载类。 + *

+ * 该类负责从项目根目录下的 {@code config.properties} 文件中读取和提供 + * 发送邮件所需的 SMTP 服务器信息,如主机、端口、用户名和密码。 + * 所有配置项通过静态方法提供。 + */ public class EmailConfig { private static final Properties properties = new Properties(); @@ -17,18 +24,38 @@ public class EmailConfig { } } + /** + * 获取 SMTP 服务器的主机名。 + * + * @return SMTP 主机名字符串。 + */ public static String getHost() { return properties.getProperty("smtp.host"); } + /** + * 获取 SMTP 服务器的端口号。 + * + * @return SMTP 端口号。 + */ public static int getPort() { return Integer.parseInt(properties.getProperty("smtp.port")); } + /** + * 获取用于 SMTP 认证的用户名(通常是发件人邮箱地址)。 + * + * @return SMTP 用户名。 + */ public static String getUsername() { return properties.getProperty("smtp.username"); } + /** + * 获取用于 SMTP 认证的密码或授权码。 + * + * @return SMTP 密码或授权码。 + */ public static String getPassword() { return properties.getProperty("smtp.password"); } diff --git a/src/main/java/com/mathgenerator/service/PaperService.java b/src/main/java/com/mathgenerator/service/PaperService.java index 3df7045..d3ed77c 100644 --- a/src/main/java/com/mathgenerator/service/PaperService.java +++ b/src/main/java/com/mathgenerator/service/PaperService.java @@ -2,7 +2,7 @@ package com.mathgenerator.service; import com.mathgenerator.model.User; import com.mathgenerator.model.Level; -import com.mathgenerator.model.ChoiceQuestion; // 导入新模型 +import com.mathgenerator.model.ChoiceQuestion; import com.mathgenerator.service.strategy.PaperStrategy; import com.mathgenerator.storage.FileManager; import java.io.IOException; @@ -12,37 +12,48 @@ import java.util.List; import java.util.Set; /** - * 试卷服务,现在处理ChoiceQuestion对象。 + * 试卷服务类。 + *

+ * 该服务负责处理与试卷相关的核心业务逻辑,包括根据指定的策略生成试卷 + * (并处理题目查重),以及将生成的试卷保存到文件系统中。 */ public class PaperService { private final FileManager fileManager; private final PaperStrategy paperStrategy; + /** + * 构造一个新的 PaperService 实例。 + * + * @param fileManager 用于处理文件读写操作的 {@link FileManager} 实例。 + * @param paperStrategy 用于选择具体题目生成器策略的 {@link PaperStrategy} 实例。 + */ public PaperService(FileManager fileManager, PaperStrategy paperStrategy) { this.fileManager = fileManager; this.paperStrategy = paperStrategy; } /** - * 创建一份包含选择题的试卷。 - * @param user 当前用户 - * @param count 题目数量 - * @param currentLevel 当前难度 - * @return 生成的选择题列表 + * 创建一份包含指定数量选择题的试卷。 + *

+ * 该方法会首先加载用户历史题目以进行查重,然后根据传入的难度级别和策略 + * 循环生成新题目,直到满足指定的数量要求。所有在本轮生成过程中的题目也会 + * 进行内部查重。 + * + * @param user 当前请求生成试卷的用户对象。 + * @param count 需要生成的题目数量。 + * @param currentLevel 用户选择的主要难度级别。 + * @return 一个包含新生成的 {@link ChoiceQuestion} 对象的列表。 */ public List createPaper(User user, int count, Level currentLevel) { - // 查重集合现在存储题干字符串 Set existingQuestionTexts = fileManager.loadExistingQuestions(user.username()); List newPaper = new ArrayList<>(); Set generatedInSession = new HashSet<>(); - System.out.println("正在根据策略生成选择题,请稍候..."); + //System.out.println("正在根据策略生成选择题,请稍候..."); while (newPaper.size() < count) { - // 1. 生成的是ChoiceQuestion对象 ChoiceQuestion question = paperStrategy.selectGenerator(currentLevel).generateSingleQuestion(); - String questionText = question.questionText(); // 提取题干用于查重 + String questionText = question.questionText(); - // 2. 使用题干进行查重 if (!existingQuestionTexts.contains(questionText) && !generatedInSession.contains(questionText)) { newPaper.add(question); generatedInSession.add(questionText); @@ -52,15 +63,16 @@ public class PaperService { } /** - * 将生成的试卷保存到文件。 - * @param username 用户名 - * @param paper 试卷题目列表 + * 将一份生成的试卷保存到用户专属的文件夹中。 + * + * @param username 用户的用户名,用于确定文件夹路径。 + * @param paper 包含试卷所有 {@link ChoiceQuestion} 的列表。 */ 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 2c816a7..0cb9453 100644 --- a/src/main/java/com/mathgenerator/service/UserService.java +++ b/src/main/java/com/mathgenerator/service/UserService.java @@ -1,10 +1,14 @@ package com.mathgenerator.service; import com.google.gson.Gson; -import java.util.Objects; 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; + import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; @@ -17,14 +21,9 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadLocalRandom; import java.util.regex.Pattern; -import org.apache.commons.mail.Email; -import org.apache.commons.mail.EmailException; -import org.apache.commons.mail.SimpleEmail; - public class UserService { private static final Path USER_FILE_PATH = Paths.get("users.json"); - // 密码策略: 6-10位, 必须包含大小写字母和数字 private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{6,10}$"); private Map userDatabase; @@ -34,16 +33,51 @@ public class UserService { this.userDatabase = loadUsersFromFile(); } + // --- 新增方法:检查用户名或邮箱是否已被占用 --- + /** + * 检查指定的用户名或邮箱是否已经被注册。 + * @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() { - // 如果文件不存在,直接返回一个空的Map,不再创建默认用户 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); - // 如果文件为空或格式错误,也返回一个空的Map return loadedUsers != null ? new ConcurrentHashMap<>(loadedUsers) : new ConcurrentHashMap<>(); } catch (IOException e) { System.err.println("错误:加载用户文件失败 - " + e.getMessage()); @@ -63,104 +97,44 @@ public class UserService { return Optional.ofNullable(this.userDatabase.get(username)); } - 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)); } - /** - * (已更新) 发送真实的邮件验证码。 - * @param email 用户的邮箱 - * @return 成功发送则返回生成的6位验证码, 失败则返回null - */ public String sendVerificationCode(String email) { String code = String.format("%06d", ThreadLocalRandom.current().nextInt(100000, 1000000)); - try { Email mail = new SimpleEmail(); - - // 1. 设置SMTP服务器信息 mail.setHostName(EmailConfig.getHost()); mail.setSmtpPort(EmailConfig.getPort()); mail.setAuthentication(EmailConfig.getUsername(), EmailConfig.getPassword()); - mail.setSSLOnConnect(true); // 开启SSL加密 - - // 2. 设置邮件内容 - mail.setFrom(EmailConfig.getUsername()); // 发件人 - mail.setSubject("【数学学习软件】您的注册验证码"); // 邮件主题 - mail.setMsg("您好!\n\n感谢您注册数学学习软件。您的验证码是:" + code + "\n\n请在5分钟内使用。"); // 邮件正文 - mail.addTo(email); // 收件人 - - // 3. 发送邮件 + mail.setSSLOnConnect(true); + mail.setFrom(EmailConfig.getUsername()); + mail.setSubject("【数学学习软件】您的注册验证码"); + mail.setMsg("您好!\n\n感谢您注册数学学习软件。您的验证码是:" + code + "\n\n请在5分钟内使用。"); + mail.addTo(email); mail.send(); - System.out.println("验证码邮件已成功发送至: " + email); return code; - } catch (EmailException e) { System.err.println("错误:发送验证码邮件失败!请检查您的 config.properties 配置或网络连接。"); e.printStackTrace(); - return null; // 发送失败 - } - } - - /** - * (已修正) 注册一个没有初始密码的新用户。 - * @param username 新用户的用户名 - * @param email 新用户的邮箱 - * @return 注册成功返回true, 如果用户名或邮箱已存在则返回false。 - */ - public boolean register(String username, String email) { - if (userDatabase.containsKey(username)) { - return false; // 用户名已存在 - } - // 检查数据库中已存在的用户的email是否与新email相同 - // 使用 email.equals(u.email()) 可以安全地处理 u.email() 为 null 的情况 - if (userDatabase.values().stream() - .anyMatch(u -> email.equals(u.email()))) { - return false; // 邮箱已存在 + return null; } - - // --- 核心修正在这里 --- - // 创建用户时,密码字段设为 null,表示该用户处于“待设置密码”状态 - User newUser = new User(username, email, null); - userDatabase.put(username, newUser); - saveUsers(); - return true; } - /** - * (新增) 为指定用户设置初始密码。 - * @param username 要设置密码的用户名 - * @param password 要设置的新密码 - * @return 成功设置返回 true, 如果用户不存在则返回 false - */ - public boolean setPassword(String username, String password) { - return findUserByUsername(username) - .map(user -> { - // 只有当用户当前密码为 null 时才允许设置 - 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 true如果符合要求 - */ public static boolean isPasswordValid(String password) { return password != null && PASSWORD_PATTERN.matcher(password).matches(); } - /** - * 修改密码。 - * @return 成功返回true - */ public boolean changePassword(String username, String oldPassword, String newPassword) { return findUserByUsername(username) .filter(user -> user.password().equals(oldPassword)) diff --git a/src/main/java/com/mathgenerator/service/strategy/MixedDifficultyStrategy.java b/src/main/java/com/mathgenerator/service/strategy/MixedDifficultyStrategy.java index cc7d78f..8954b05 100644 --- a/src/main/java/com/mathgenerator/service/strategy/MixedDifficultyStrategy.java +++ b/src/main/java/com/mathgenerator/service/strategy/MixedDifficultyStrategy.java @@ -6,29 +6,39 @@ import java.util.concurrent.ThreadLocalRandom; /** * 混合难度策略的具体实现。 - * (已更新,会根据主难度选择不同的小学基础生成器) + *

+ * 该策略根据用户选择的主难度级别,按一定的概率混合使用不同难度的题目生成器, + * 以生成一张难度更多样化的试卷。 + * 例如,高中难度的试卷也可能包含一部分初中或小学的题目。 */ public class MixedDifficultyStrategy implements PaperStrategy { - // 持有所有可能的生成器 + // 持有所有可能的生成器实例 private final QuestionGenerator primaryGenerator = new PrimarySchoolGenerator(); private final QuestionGenerator safePrimaryGenerator = new SafePrimarySchoolGenerator(); // 新增 private final QuestionGenerator juniorHighGenerator = new JuniorHighSchoolGenerator(); private final QuestionGenerator seniorHighGenerator = new SeniorHighSchoolGenerator(); + /** + * 根据主难度级别选择一个题目生成器。 + *

+ * - **小学**: 100% 使用 {@link SafePrimarySchoolGenerator},确保题目不含负数。 + * - **初中**: 70% 概率使用 {@link JuniorHighSchoolGenerator},30% 使用 {@link PrimarySchoolGenerator}。 + * - **高中**: 60% 概率使用 {@link SeniorHighSchoolGenerator},30% 使用 {@link JuniorHighSchoolGenerator},10% 使用 {@link PrimarySchoolGenerator}。 + * + * @param mainLevel 用户选择的主难度级别。 + * @return 根据预设概率随机选择出的一个 {@link QuestionGenerator} 实例。 + */ @Override public QuestionGenerator selectGenerator(Level mainLevel) { double randomValue = ThreadLocalRandom.current().nextDouble(); return switch (mainLevel) { - // 当主难度是小学时,100%使用“安全”的生成器 case PRIMARY -> safePrimaryGenerator; case JUNIOR_HIGH -> { - // 初中试卷:70%初中难度,30%使用“不安全”的小学难度(允许负数) if (randomValue < 0.7) yield juniorHighGenerator; else yield primaryGenerator; } case SENIOR_HIGH -> { - // 高中试卷:60%高中,30%初中,10%使用“不安全”的小学难度 if (randomValue < 0.6) yield seniorHighGenerator; else if (randomValue < 0.9) yield juniorHighGenerator; else yield primaryGenerator; diff --git a/src/main/java/com/mathgenerator/service/strategy/PaperStrategy.java b/src/main/java/com/mathgenerator/service/strategy/PaperStrategy.java index 80d2ac4..5cc6b13 100644 --- a/src/main/java/com/mathgenerator/service/strategy/PaperStrategy.java +++ b/src/main/java/com/mathgenerator/service/strategy/PaperStrategy.java @@ -5,14 +5,17 @@ import com.mathgenerator.model.Level; /** * 试卷组合策略接口。 - * 封装了如何根据主难度来选择具体题目生成器的算法。 + *

+ * 该接口遵循策略模式,用于封装根据主难度级别选择不同题目生成器的算法。 + * 任何实现此接口的类都需要提供一个具体的生成器选择逻辑。 */ public interface PaperStrategy { + /** - * 根据用户选择的主难度,选择一个具体的题目生成器。 + * 根据用户选择的主要难度级别,选择一个具体的题目生成器。 * - * @param mainLevel 用户选择的主难度级别。 - * @return 一个根据策略选择出的QuestionGenerator实例。 + * @param mainLevel 用户选择的主难度级别 ({@link Level})。 + * @return 一个根据策略选择出的 {@link QuestionGenerator} 实例。 */ QuestionGenerator selectGenerator(Level mainLevel); } \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/storage/FileManager.java b/src/main/java/com/mathgenerator/storage/FileManager.java index a1bb4a3..93f62ce 100644 --- a/src/main/java/com/mathgenerator/storage/FileManager.java +++ b/src/main/java/com/mathgenerator/storage/FileManager.java @@ -1,57 +1,86 @@ package com.mathgenerator.storage; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; -import com.mathgenerator.model.ChoiceQuestion; // 导入新模型 - -import java.io.FileReader; -import java.io.FileWriter; +import com.mathgenerator.model.ChoiceQuestion; import java.io.IOException; -import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; /** - * 负责文件读写,现已支持JSON格式的ChoiceQuestion对象。 + * 文件管理器类。 + *

+ * 该类负责处理所有与文件系统相关的读写操作。主要功能包括: + *

    + *
  • 将生成的试卷内容格式化并保存为文本文件。
  • + *
  • 从用户历史试卷文件中读取并解析出所有题目的题干,用于题目查重。
  • + *
+ * 此版本已修复了先前版本中因未正确关闭文件流(Stream)而可能导致的资源泄漏问题。 */ public class FileManager { private static final Path BASE_PATH = Paths.get("generated_papers"); private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"); - private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); /** - * 将生成的选择题试卷以JSON格式保存到文件。 - * @param username 用户名 - * @param paperContent 包含ChoiceQuestion对象的试卷列表 - * @return 保存成功后的文件路径 + * 将一份试卷的内容保存到指定用户的文件夹下。 + *

+ * 文件会以当前的时间戳命名,并保存为 {@code .txt} 格式。文件内容会进行格式化, + * 包含用户信息、生成时间、题目、选项和正确答案。 + * + * @param username 用户的用户名,用于确定保存文件的子目录。 + * @param paperContent 包含所有 {@link ChoiceQuestion} 对象的试卷列表。 + * @return 保存成功后返回文件的绝对路径字符串。 + * @throws IOException 如果在文件创建或写入过程中发生 I/O 错误。 */ public String savePaper(String username, List paperContent) throws IOException { Path userDir = BASE_PATH.resolve(username); Files.createDirectories(userDir); String timestamp = LocalDateTime.now().format(FORMATTER); - String fileName = timestamp + ".json"; // 文件后缀改为 .json + String fileName = timestamp + ".txt"; Path filePath = userDir.resolve(fileName); - try (FileWriter writer = new FileWriter(filePath.toFile())) { - gson.toJson(paperContent, writer); + StringBuilder formattedContent = new StringBuilder(); + formattedContent.append("数学试卷\n"); + formattedContent.append("用户: ").append(username).append("\n"); + formattedContent.append("生成时间: ").append(timestamp).append("\n"); + formattedContent.append("========================================\n\n"); + + for (int i = 0; i < paperContent.size(); i++) { + ChoiceQuestion question = paperContent.get(i); + formattedContent.append(i + 1).append(". ").append(question.questionText()).append("\n"); + + char optionChar = 'A'; + for (String option : question.options()) { + formattedContent.append(" ").append(optionChar++).append(". ").append(option).append("\n"); + } + + String correctAnswer = question.options().get(question.correctOptionIndex()); + formattedContent.append(" [正确答案: ").append(correctAnswer).append("]\n\n"); } + + Files.writeString(filePath, formattedContent.toString(), StandardOpenOption.CREATE); return filePath.toString(); } /** - * 加载指定用户的所有历史题目的题干文本,用于查重。 - * @param username 用户名 - * @return 包含所有历史题干文本的Set集合 + * 加载指定用户所有历史试卷中的题目题干。 + *

+ * 该方法会遍历用户文件夹下的所有 {@code .txt} 文件,解析出每道题的题干文本, + * 并将它们聚合到一个 Set 集合中返回,用于后续的题目查重。 + * + * @param username 要加载历史题目的用户名。 + * @return 一个包含该用户所有历史题干的 {@code Set} 集合。如果用户没有历史文件或读取失败,则返回空集合。 */ public Set loadExistingQuestions(String username) { Path userDir = BASE_PATH.resolve(username); @@ -61,9 +90,8 @@ public class FileManager { try (Stream stream = Files.walk(userDir)) { return stream - .filter(file -> !Files.isDirectory(file) && file.toString().endsWith(".json")) // 只读取 .json 文件 - .flatMap(this::readQuestionsFromFile) // 使用方法引用 - .map(ChoiceQuestion::questionText) // 提取每个对象的题干文本 + .filter(file -> !Files.isDirectory(file) && file.toString().endsWith(".txt")) + .flatMap(file -> readQuestionTextsFromTxtFile(file).stream()) .collect(Collectors.toSet()); } catch (IOException e) { System.err.println("错误:读取历史文件失败 - " + e.getMessage()); @@ -72,18 +100,25 @@ public class FileManager { } /** - * 从单个JSON文件中读取并解析ChoiceQuestion对象列表。 - * @param file 要读取的单个试卷文件的路径对象 (Path)。 - * @return 一个包含该文件中所有ChoiceQuestion对象的流 (Stream)。 + * 从单个试卷文本文件中解析出所有题目的题干。 + *

+ * 使用正则表达式匹配以 "数字." 开头的行,并提取其后的文本作为题干。 + * + * @param file 要读取的单个试卷文件的 {@link Path} 对象。 + * @return 一个包含该文件中所有题干字符串的列表 (List)。如果读取失败,则返回空列表。 */ - private Stream readQuestionsFromFile(Path file) { - try (FileReader reader = new FileReader(file.toFile())) { - Type listType = new TypeToken>() {}.getType(); - List questions = gson.fromJson(reader, listType); - return questions != null ? questions.stream() : Stream.empty(); + private List readQuestionTextsFromTxtFile(Path file) { + try (Stream lines = Files.lines(file)) { + Pattern questionPattern = Pattern.compile("^\\d+\\.\\s+(.*)"); + return lines + .map(String::trim) + .map(questionPattern::matcher) + .filter(Matcher::matches) + .map(matcher -> matcher.group(1).trim()) + .collect(Collectors.toList()); } catch (IOException e) { - System.err.println("错误:读取或解析文件 " + file + " 失败 - " + e.getMessage()); - return Stream.empty(); + System.err.println("错误:读取文件 " + file + " 失败 - " + e.getMessage()); + return Collections.emptyList(); } } } \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/util/ValidationUtils.java b/src/main/java/com/mathgenerator/util/ValidationUtils.java index 845a2f7..940168f 100644 --- a/src/main/java/com/mathgenerator/util/ValidationUtils.java +++ b/src/main/java/com/mathgenerator/util/ValidationUtils.java @@ -4,7 +4,10 @@ import java.util.regex.Pattern; /** * 一个包含静态校验方法的工具类。 - * 用于集中管理项目中所有的数据格式验证逻辑。 + *

+ * 该类用于集中管理项目中所有的数据格式验证逻辑,例如用户名、密码和邮箱的格式校验。 + * 通过提供统一的静态方法,确保了验证规则的一致性和代码的复用性。 + * 此类不可被实例化。 */ public final class ValidationUtils { @@ -16,14 +19,27 @@ public final class ValidationUtils { private static final Pattern USERNAME_NO_WHITESPACE_PATTERN = Pattern.compile("^\\S+$"); - // 私有构造函数,防止这个工具类被实例化 + // 邮箱策略:使用标准的正则表达式进行格式校验 + private static final Pattern EMAIL_PATTERN = + Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$"); + + + /** + * 私有构造函数,防止这个工具类被实例化。 + */ private ValidationUtils() {} /** * 验证用户名格式是否有效。 - * 当前规则:不允许包含任何空格或空白字符。 - * @param username 待验证的用户名 - * @return 如果有效返回true, 否则返回false + *

+ * 当前规则为: + *

    + *
  • 用户名不能为空。
  • + *
  • 用户名不允许包含任何空格或空白字符。
  • + *
+ * + * @param username 待验证的用户名字符串。 + * @return 如果用户名格式有效,返回 {@code true};否则返回 {@code false}。 */ public static boolean isUsernameValid(String username) { if (username == null || username.isEmpty()) { @@ -33,20 +49,33 @@ public final class ValidationUtils { } /** - * 验证密码是否符合复杂度要求。 - * @param password 待验证的密码 - * @return 如果符合要求返回true + * 验证密码是否符合预设的复杂度要求。 + *

+ * 当前规则为: + *

    + *
  • 密码长度必须在 6 到 10 位之间。
  • + *
  • 密码必须同时包含大写字母、小写字母和数字。
  • + *
+ * + * @param password 待验证的密码字符串。 + * @return 如果密码符合复杂度要求,返回 {@code true};否则返回 {@code false}。 */ public static boolean isPasswordValid(String password) { return password != null && PASSWORD_PATTERN.matcher(password).matches(); } /** - * 验证邮箱格式是否有效 (简单校验)。 - * @param email 待验证的邮箱 - * @return 如果格式基本正确返回true + * 验证邮箱地址格式是否有效。 + *

+ * 使用一个标准的正则表达式来检查邮箱地址是否符合常规格式 (例如, "user@example.com")。 + * + * @param email 待验证的邮箱地址字符串。 + * @return 如果邮箱格式基本正确,返回 {@code true};否则返回 {@code false}。 */ public static boolean isEmailValid(String email) { - return email != null && !email.isEmpty() && email.contains("@"); + if (email == null || email.isEmpty()) { + return false; + } + return EMAIL_PATTERN.matcher(email).matches(); } } \ No newline at end of file diff --git a/src/main/resources/com/mathgenerator/images/1.png b/src/main/resources/com/mathgenerator/images/1.png new file mode 100644 index 0000000..ac38781 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/1.png differ diff --git a/src/main/resources/com/mathgenerator/images/2.png b/src/main/resources/com/mathgenerator/images/2.png new file mode 100644 index 0000000..495a0dc Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/2.png differ diff --git a/src/main/resources/com/mathgenerator/images/3.png b/src/main/resources/com/mathgenerator/images/3.png new file mode 100644 index 0000000..7948648 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/3.png differ diff --git a/src/main/resources/com/mathgenerator/images/5.png b/src/main/resources/com/mathgenerator/images/5.png new file mode 100644 index 0000000..84f20bb Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/5.png differ diff --git a/src/main/resources/com/mathgenerator/images/6.png b/src/main/resources/com/mathgenerator/images/6.png new file mode 100644 index 0000000..8377cda Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/6.png differ diff --git a/src/main/resources/com/mathgenerator/images/7.png b/src/main/resources/com/mathgenerator/images/7.png new file mode 100644 index 0000000..3db09d5 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/7.png differ diff --git a/src/main/resources/com/mathgenerator/images/background.png b/src/main/resources/com/mathgenerator/images/background.png new file mode 100644 index 0000000..1c99681 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/background.png differ diff --git a/src/main/resources/com/mathgenerator/images/cute-dog-train.png b/src/main/resources/com/mathgenerator/images/cute-dog-train.png new file mode 100644 index 0000000..b6b6c4e Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/cute-dog-train.png differ diff --git a/src/main/resources/com/mathgenerator/images/icon.png b/src/main/resources/com/mathgenerator/images/icon.png new file mode 100644 index 0000000..b081c90 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/icon.png differ diff --git a/src/main/resources/com/mathgenerator/images/image.png b/src/main/resources/com/mathgenerator/images/image.png new file mode 100644 index 0000000..695347a Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/image.png differ diff --git a/src/main/resources/com/mathgenerator/images/mainmenu-bg.png b/src/main/resources/com/mathgenerator/images/mainmenu-bg.png new file mode 100644 index 0000000..4479bc0 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/mainmenu-bg.png differ diff --git a/src/main/resources/com/mathgenerator/images/password-bg.png b/src/main/resources/com/mathgenerator/images/password-bg.png new file mode 100644 index 0000000..4479bc0 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/password-bg.png differ diff --git a/src/main/resources/com/mathgenerator/images/quiz-bg.png b/src/main/resources/com/mathgenerator/images/quiz-bg.png new file mode 100644 index 0000000..d1fba93 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/quiz-bg.png differ diff --git a/src/main/resources/com/mathgenerator/images/register-bg.png b/src/main/resources/com/mathgenerator/images/register-bg.png new file mode 100644 index 0000000..31f6e39 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/register-bg.png differ diff --git a/src/main/resources/com/mathgenerator/images/score-bg.png b/src/main/resources/com/mathgenerator/images/score-bg.png new file mode 100644 index 0000000..3136b1e Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/score-bg.png differ diff --git a/src/main/resources/com/mathgenerator/styles/styles.css b/src/main/resources/com/mathgenerator/styles/styles.css new file mode 100644 index 0000000..d86f8bf --- /dev/null +++ b/src/main/resources/com/mathgenerator/styles/styles.css @@ -0,0 +1,955 @@ +/* ===== 统一界面尺寸 ===== */ +.root-container { + -fx-pref-width: 400px; + -fx-pref-height: 700px; + -fx-min-width: 400px; + -fx-min-height: 700px; + -fx-max-width: 400px; + -fx-max-height: 700px; +} + +/* ===== 登录界面专用样式 - 淡黄色和淡紫色主题 ===== */ +.login-background { + -fx-background-image: url('../images/background.png'); /* 修改 */ + -fx-background-size: cover; + -fx-background-position: center; + -fx-background-repeat: no-repeat; + -fx-background-color: linear-gradient(to bottom right, #fff9c4, #f3e5f5); +} + +.login-glass-panel { + -fx-background-color: rgba(255, 255, 255, 0.85); + -fx-background-radius: 25px; + -fx-border-radius: 25px; + -fx-border-color: rgba(186, 104, 200, 0.3); + -fx-border-width: 1px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.15), 20, 0.3, 0, 6); +} + +.login-title { + -fx-text-fill: #7b1fa2; + -fx-font-weight: bold; + -fx-font-size: 26px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.2), 4, 0.5, 2, 2); +} + +.login-textfield { + -fx-background-color: rgba(255, 255, 255, 0.95); + -fx-border-color: rgba(186, 104, 200, 0.5); + -fx-border-radius: 15px; + -fx-background-radius: 15px; + -fx-padding: 12px 16px; + -fx-font-size: 14px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 6, 0.2, 2, 2); +} + +.login-textfield:focused { + -fx-border-color: rgba(156, 39, 176, 0.8); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.3), 8, 0.3, 2, 2); +} + +.login-primary-button { + -fx-background-color: linear-gradient(to bottom, #ba68c8, #ab47bc); + -fx-text-fill: white; + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-background-radius: 25px; + -fx-border-radius: 25px; + -fx-border-color: #ab47bc; + -fx-border-width: 1px; + -fx-padding: 12px 24px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.4), 8, 0.3, 2, 2); +} + +.login-primary-button:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ba68c8); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.6), 10, 0.4, 3, 3); + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; +} + +.login-secondary-button { + -fx-background-color: linear-gradient(to bottom, #fff59d, #fff176); + -fx-text-fill: #7b1fa2; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-background-radius: 20px; + -fx-border-radius: 20px; + -fx-border-color: #fff176; + -fx-border-width: 1px; + -fx-padding: 10px 20px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(255, 245, 157, 0.4), 6, 0.3, 2, 2); +} + +.login-secondary-button:hover { + -fx-background-color: linear-gradient(to bottom, #fff9c4, #fff59d); + -fx-effect: dropshadow(gaussian, rgba(255, 245, 157, 0.6), 8, 0.4, 2, 2); +} + +.login-status-label { + -fx-text-fill: #7b1fa2; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.1); + -fx-background-radius: 8px; + -fx-padding: 8px 12px; + -fx-border-color: rgba(186, 104, 200, 0.2); + -fx-border-radius: 8px; + -fx-border-width: 1px; +} + +/* ===== 注册界面专用样式 - 修复版本 ===== */ +.register-background-new { + -fx-background-image: url('../images/register-bg.png'); /* 修改 */ + -fx-background-size: cover; + -fx-background-position: center; + -fx-background-repeat: no-repeat; + -fx-background-color: linear-gradient(to bottom right, #fff9c4, #f3e5f5); +} + +.register-glass-panel { + -fx-background-color: rgba(255, 255, 255, 0.85); + -fx-background-radius: 25px; + -fx-border-radius: 25px; + -fx-border-color: rgba(186, 104, 200, 0.3); + -fx-border-width: 1px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.15), 20, 0.3, 0, 6); +} + +.register-title { + -fx-text-fill: #7b1fa2; + -fx-font-weight: bold; + -fx-font-size: 26px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.2), 4, 0.5, 2, 2); +} + +.register-textfield { + -fx-background-color: rgba(255, 255, 255, 0.95); + -fx-border-color: rgba(186, 104, 200, 0.5); + -fx-border-radius: 15px; + -fx-background-radius: 15px; + -fx-padding: 12px 16px; + -fx-font-size: 14px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 6, 0.2, 2, 2); +} + +.register-textfield:focused { + -fx-border-color: rgba(156, 39, 176, 0.8); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.3), 8, 0.3, 2, 2); +} + +.register-primary-button { + -fx-background-color: linear-gradient(to bottom, #ba68c8, #ab47bc); + -fx-text-fill: white; + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-background-radius: 25px; + -fx-border-radius: 25px; + -fx-border-color: #ab47bc; + -fx-border-width: 1px; + -fx-padding: 12px 24px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.4), 8, 0.3, 2, 2); +} + +.register-primary-button:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ba68c8); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.6), 10, 0.4, 3, 3); + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; +} + +.register-secondary-button { + -fx-background-color: linear-gradient(to bottom, #fff59d, #fff176); + -fx-text-fill: #7b1fa2; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-background-radius: 20px; + -fx-border-radius: 20px; + -fx-border-color: #fff176; + -fx-border-width: 1px; + -fx-padding: 10px 20px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(255, 245, 157, 0.4), 6, 0.3, 2, 2); +} + +.register-secondary-button:hover { + -fx-background-color: linear-gradient(to bottom, #fff9c4, #fff59d); + -fx-effect: dropshadow(gaussian, rgba(255, 245, 157, 0.6), 8, 0.4, 2, 2); +} + +.register-security-tip { + -fx-text-fill: #7b1fa2; + -fx-font-size: 12px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.1); + -fx-background-radius: 8px; + -fx-padding: 8px 12px; + -fx-border-color: rgba(186, 104, 200, 0.2); + -fx-border-radius: 8px; + -fx-border-width: 1px; +} + +.register-code-container { + -fx-background-color: rgba(255, 255, 255, 0.9); + -fx-background-radius: 15px; + -fx-border-radius: 15px; + -fx-border-color: rgba(186, 104, 200, 0.4); + -fx-border-width: 1px; + -fx-padding: 8px 12px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 6, 0.2, 2, 2); +} + +/* 修复验证码按钮大小 */ +.register-code-button { + -fx-background-color: linear-gradient(to bottom, #ba68c8, #ab47bc); + -fx-text-fill: white; + -fx-font-size: 12px; + -fx-font-weight: bold; + -fx-background-radius: 10px; + -fx-border-radius: 10px; + -fx-border-color: #ab47bc; + -fx-border-width: 1px; + -fx-padding: 10px 16px; + -fx-min-width: 100px; + -fx-min-height: 40px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.3), 6, 0.3, 2, 2); +} + +.register-code-button:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ba68c8); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.5), 8, 0.4, 2, 2); +} + +/* ===== 注册界面状态标签样式 - 修复长句子版本 ===== */ +.register-status-label { + -fx-text-fill: transparent; + -fx-font-size: 12px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: transparent; + -fx-background-radius: 8px; + -fx-padding: 0px; + -fx-border-color: transparent; + -fx-border-radius: 8px; + -fx-border-width: 0px; + -fx-alignment: center; + -fx-text-alignment: center; + -fx-min-height: 0px; + -fx-pref-height: 0px; + -fx-max-width: 280px; + -fx-line-spacing: 2px; +} + +.register-status-label-with-text { + -fx-text-fill: #7b1fa2; + -fx-font-size: 12px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.15); + -fx-background-radius: 10px; + -fx-padding: 12px 15px; + -fx-border-color: rgba(186, 104, 200, 0.3); + -fx-border-radius: 10px; + -fx-border-width: 1px; + -fx-alignment: center; + -fx-text-alignment: center; + -fx-min-height: 70px; + -fx-pref-height: 70px; + -fx-max-width: 280px; + -fx-line-spacing: 2px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 6, 0.3, 2, 2); +} + +/* ===== 主菜单界面专用样式 - 浅紫色为主,浅黄色为辅 ===== */ +.mainmenu-background-new { + -fx-background-image: url('../images/mainmenu-bg.png'); /* 修改 */ + -fx-background-size: cover; + -fx-background-position: center center; + -fx-background-repeat: no-repeat; + -fx-background-color: linear-gradient(to bottom right, #f3e5f5, #e1bee7); +} + +.mainmenu-glass-panel { + -fx-background-color: rgba(255, 255, 255, 0.92); + -fx-background-radius: 30px; + -fx-border-radius: 30px; + -fx-border-color: rgba(186, 104, 200, 0.5); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.25), 25, 0.5, 0, 8); +} + +/* ===== 主菜单欢迎标题样式 - 修复长用户名显示 ===== */ +.mainmenu-welcome-title { + -fx-text-fill: #7b1fa2; + -fx-font-weight: bold; + -fx-font-size: 24px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.25), 4, 0.6, 2, 2); + -fx-wrap-text: true; + -fx-text-alignment: center; + -fx-alignment: center; + -fx-max-width: 360px; +} + +.mainmenu-difficulty-label { + -fx-text-fill: #7b1fa2; + -fx-font-size: 20px; + -fx-font-weight: bold; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.2), 2, 0.4, 1, 1); +} + +/* ===== 难度按钮样式 - 浅紫色质感 ===== */ +.mainmenu-difficulty-button { + -fx-background-color: linear-gradient(to bottom, #e1bee7, #ba68c8); + -fx-text-fill: white; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-background-radius: 25px; + -fx-border-radius: 25px; + -fx-border-color: #ba68c8; + -fx-border-width: 2px; + -fx-padding: 15px 30px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.5), 12, 0.5, 3, 3); +} + +.mainmenu-difficulty-button:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ab47bc); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.7), 15, 0.6, 4, 4); + -fx-scale-x: 1.08; + -fx-scale-y: 1.08; +} + +.mainmenu-difficulty-button:pressed { + -fx-background-color: linear-gradient(to bottom, #ab47bc, #8e24aa); +} + +/* ===== 数量标签样式 ===== */ +.mainmenu-count-label { + -fx-text-fill: #7b1fa2; + -fx-font-size: 16px; + -fx-font-weight: bold; +} + +/* ===== 数量输入框样式 ===== */ +.mainmenu-count-textfield { + -fx-background-color: rgba(255, 255, 255, 0.95); + -fx-border-color: rgba(186, 104, 200, 0.6); + -fx-border-radius: 10px; + -fx-background-radius: 10px; + -fx-padding: 8px 12px; + -fx-font-size: 14px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.3), 6, 0.2, 2, 2); + -fx-alignment: center; +} + +.mainmenu-count-textfield:focused { + -fx-border-color: rgba(156, 39, 176, 0.8); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.4), 8, 0.3, 2, 2); +} + +/* ===== 功能按钮样式 - 浅黄色辅助 ===== */ +.mainmenu-function-button { + -fx-background-color: linear-gradient(to bottom, #fff59d, #ffeb3b); + -fx-text-fill: #7b1fa2; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-background-radius: 20px; + -fx-border-radius: 20px; + -fx-border-color: #ffeb3b; + -fx-border-width: 1px; + -fx-padding: 10px 25px; + -fx-min-width: 140px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(255, 235, 59, 0.4), 8, 0.3, 2, 2); +} + +.mainmenu-function-button:hover { + -fx-background-color: linear-gradient(to bottom, #fff9c4, #fff59d); + -fx-effect: dropshadow(gaussian, rgba(255, 235, 59, 0.6), 10, 0.4, 3, 3); + -fx-text-fill: #4a148c; +} + +/* ===== 退出按钮样式 - 浅紫色 ===== */ +.mainmenu-logout-button { + -fx-background-color: linear-gradient(to bottom, #ba68c8, #ab47bc); + -fx-text-fill: white; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-background-radius: 20px; + -fx-border-radius: 20px; + -fx-border-color: #ab47bc; + -fx-border-width: 1px; + -fx-padding: 10px 25px; + -fx-min-width: 140px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.4), 8, 0.3, 2, 2); +} + +.mainmenu-logout-button:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ba68c8); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.6), 10, 0.4, 3, 3); +} + +/* ===== 状态标签样式 ===== */ +.mainmenu-status-label { + -fx-text-fill: #7b1fa2; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.15); + -fx-background-radius: 8px; + -fx-padding: 8px 12px; + -fx-border-color: rgba(186, 104, 200, 0.3); + -fx-border-radius: 8px; + -fx-border-width: 1px; + -fx-alignment: center; +} + +/* ===== 答题界面专用样式 - 优化布局版本 ===== */ +.quiz-background-new { + -fx-background-image: url('../images/quiz-bg.png'); /* 修改 */ + -fx-background-size: cover; + -fx-background-position: center center; + -fx-background-repeat: no-repeat; + -fx-background-color: linear-gradient(to bottom right, #fff9c4, #f3e5f5); +} + +.quiz-title-new { + -fx-text-fill: #7b1fa2; + -fx-font-weight: bold; + -fx-font-size: 18px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.25), 4, 0.5, 2, 2); + -fx-alignment: center; +} + +/* ===== 进度条样式 ===== */ +.quiz-progress-bar-new { + -fx-accent: #ba68c8; + -fx-background-color: #f3e5f5; + -fx-background-radius: 8px; + -fx-border-radius: 8px; + -fx-padding: 2px; +} + +.quiz-progress-bar-new .track { + -fx-background-color: #f3e5f5; + -fx-background-radius: 8px; +} + +.quiz-progress-bar-new .bar { + -fx-background-color: linear-gradient(to right, #ba68c8, #ab47bc); + -fx-background-radius: 8px; +} + +/* ===== 题目内容样式 - 减小高度 ===== */ +.quiz-question-text { + -fx-text-fill: #4a148c; + -fx-font-weight: bold; + -fx-font-size: 18px; + -fx-wrap-text: true; + -fx-background-color: rgba(255, 255, 255, 0.92); + -fx-background-radius: 15px; + -fx-padding: 15px 20px; + -fx-border-color: rgba(186, 104, 200, 0.6); + -fx-border-radius: 15px; + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.3), 10, 0.4, 3, 3); + -fx-alignment: center; + -fx-text-alignment: center; + -fx-min-height: 80px; + -fx-pref-height: 80px; +} + +/* ===== 选项容器样式 - 减小高度 ===== */ +.quiz-options-container { + -fx-background-color: rgba(255, 255, 255, 0.88); + -fx-background-radius: 20px; + -fx-border-radius: 20px; + -fx-border-color: rgba(186, 104, 200, 0.4); + -fx-border-width: 2px; + -fx-padding: 15px 20px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.25), 12, 0.4, 3, 3); + -fx-min-height: 180px; + -fx-pref-height: 180px; +} + +/* ===== 单选按钮样式 - 减小间距 ===== */ +.quiz-radio-button-new { + -fx-text-fill: #4a148c; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-cursor: hand; + -fx-padding: 8px 0px; +} + +.quiz-radio-button-new .radio { + -fx-background-color: white; + -fx-border-color: #ba68c8; + -fx-border-radius: 12px; + -fx-background-radius: 12px; + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 4, 0.3, 1, 1); +} + +.quiz-radio-button-new:selected .radio { + -fx-background-color: #ba68c8; + -fx-border-color: #7b1fa2; + -fx-border-width: 3px; +} + +.quiz-radio-button-new:selected .dot { + -fx-background-color: white; +} + +.quiz-radio-button-new:hover .radio { + -fx-border-color: #7b1fa2; + -fx-border-width: 3px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.3), 6, 0.4, 2, 2); +} + +/* ===== 提交按钮样式 ===== */ +.quiz-submit-button { + -fx-background-color: linear-gradient(to bottom, #ba68c8, #ab47bc); + -fx-text-fill: white; + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-background-radius: 20px; + -fx-border-radius: 20px; + -fx-border-color: #ab47bc; + -fx-border-width: 2px; + -fx-padding: 10px 25px; + -fx-min-width: 150px; + -fx-min-height: 45px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.4), 8, 0.4, 2, 2); +} + +.quiz-submit-button:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ba68c8); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.6), 10, 0.5, 3, 3); + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; +} + +.quiz-submit-button:pressed { + -fx-background-color: linear-gradient(to bottom, #ab47bc, #8e24aa); +} + +/* ===== 答题状态标签样式 - 固定位置 ===== */ +.quiz-status-label-empty { + -fx-text-fill: transparent; + -fx-font-size: 13px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: transparent; + -fx-background-radius: 8px; + -fx-padding: 0px; + -fx-border-color: transparent; + -fx-border-radius: 8px; + -fx-border-width: 0px; + -fx-alignment: center; + -fx-min-height: 0px; + -fx-pref-height: 0px; + -fx-max-height: 0px; +} + +.quiz-status-label-with-text { + -fx-text-fill: #7b1fa2; + -fx-font-size: 13px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.15); + -fx-background-radius: 8px; + -fx-padding: 8px 12px; + -fx-border-color: rgba(186, 104, 200, 0.3); + -fx-border-radius: 8px; + -fx-border-width: 1px; + -fx-alignment: center; + -fx-min-height: 35px; + -fx-pref-height: 35px; + -fx-max-height: 35px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 4, 0.3, 1, 1); +} + +/* ===== 分数界面专用样式 - 修复文字换行 ===== */ +.score-background-new { + -fx-background-image: url('../images/score-bg.png'); /* 修改 */ + -fx-background-size: cover; + -fx-background-position: center center; + -fx-background-repeat: no-repeat; + -fx-background-color: linear-gradient(to bottom right, #fff9c4, #f3e5f5); +} + +.score-glass-panel-new { + -fx-background-color: rgba(255, 255, 255, 0.9); + -fx-background-radius: 35px; + -fx-border-radius: 35px; + -fx-border-color: rgba(186, 104, 200, 0.4); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.25), 30, 0.6, 0, 10); + -fx-padding: 30px 25px; +} + +.score-complete-title-new { + -fx-text-fill: #7b1fa2; + -fx-font-weight: bold; + -fx-font-size: 32px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.3), 6, 0.6, 3, 3); + -fx-wrap-text: true; + -fx-text-alignment: center; + -fx-alignment: center; +} + +.score-label-new { + -fx-text-fill: linear-gradient(to bottom, #ba68c8, #7b1fa2); + -fx-font-weight: bold; + -fx-font-size: 60px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.4), 8, 0.7, 4, 4); + -fx-alignment: center; +} + +.score-description-new { + -fx-text-fill: #7b1fa2; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-alignment: center; +} + +/* ===== 结果消息样式 - 完全修复文字换行 ===== */ +.result-message-new { + -fx-text-fill: #7b1fa2; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(255, 245, 157, 0.3); + -fx-background-radius: 15px; + -fx-padding: 12px 20px; + -fx-border-color: rgba(255, 235, 59, 0.4); + -fx-border-radius: 15px; + -fx-border-width: 1px; + -fx-effect: dropshadow(gaussian, rgba(255, 235, 59, 0.2), 8, 0.3, 2, 2); + -fx-alignment: center; + -fx-text-alignment: center; + -fx-max-width: 300px; + -fx-min-height: 60px; + -fx-pref-height: 60px; +} + +/* ===== 庆祝按钮样式 ===== */ +.celebrate-button-new { + -fx-background-color: linear-gradient(to bottom, #ba68c8, #ab47bc); + -fx-text-fill: white; + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-background-radius: 25px; + -fx-border-radius: 25px; + -fx-border-color: #ab47bc; + -fx-border-width: 2px; + -fx-padding: 12px 30px; + -fx-min-width: 160px; + -fx-min-height: 50px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.4), 10, 0.4, 3, 3); +} + +.celebrate-button-new:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ba68c8); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.6), 12, 0.5, 4, 4); + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; +} + +.celebrate-button-new:pressed { + -fx-background-color: linear-gradient(to bottom, #ab47bc, #8e24aa); +} + +/* ===== 退出按钮样式 ===== */ +.score-logout-button-new { + -fx-background-color: linear-gradient(to bottom, #fff59d, #ffeb3b); + -fx-text-fill: #7b1fa2; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-background-radius: 20px; + -fx-border-radius: 20px; + -fx-border-color: #ffeb3b; + -fx-border-width: 1px; + -fx-padding: 10px 25px; + -fx-min-width: 140px; + -fx-min-height: 45px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(255, 235, 59, 0.4), 8, 0.3, 2, 2); +} + +.score-logout-button-new:hover { + -fx-background-color: linear-gradient(to bottom, #fff9c4, #fff59d); + -fx-effect: dropshadow(gaussian, rgba(255, 235, 59, 0.6), 10, 0.4, 3, 3); + -fx-text-fill: #4a148c; +} + +/* ===== 修改密码界面专用样式 - 淡黄色和淡紫色主题 ===== */ +.password-background-new { + -fx-background-image: url('../images/password-bg.png'); /* 修改 */ + -fx-background-size: cover; + -fx-background-position: center center; + -fx-background-repeat: no-repeat; + -fx-background-color: linear-gradient(to bottom right, #fff9c4, #f3e5f5); +} + +.password-glass-panel { + -fx-background-color: rgba(255, 255, 255, 0.88); + -fx-background-radius: 30px; + -fx-border-radius: 30px; + -fx-border-color: rgba(186, 104, 200, 0.4); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.2), 25, 0.5, 0, 8); +} + +.password-title { + -fx-text-fill: #7b1fa2; + -fx-font-weight: bold; + -fx-font-size: 26px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.25), 4, 0.6, 2, 2); +} + +.password-security-tip { + -fx-text-fill: #7b1fa2; + -fx-font-size: 13px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.1); + -fx-background-radius: 10px; + -fx-padding: 10px 15px; + -fx-border-color: rgba(186, 104, 200, 0.2); + -fx-border-radius: 10px; + -fx-border-width: 1px; + -fx-alignment: center; +} + +.password-textfield { + -fx-background-color: rgba(255, 255, 255, 0.95); + -fx-border-color: rgba(186, 104, 200, 0.5); + -fx-border-radius: 15px; + -fx-background-radius: 15px; + -fx-padding: 12px 16px; + -fx-font-size: 14px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 6, 0.2, 2, 2); +} + +.password-textfield:focused { + -fx-border-color: rgba(156, 39, 176, 0.8); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.3), 8, 0.3, 2, 2); +} + +.password-primary-button { + -fx-background-color: linear-gradient(to bottom, #ba68c8, #ab47bc); + -fx-text-fill: white; + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-background-radius: 25px; + -fx-border-radius: 25px; + -fx-border-color: #ab47bc; + -fx-border-width: 2px; + -fx-padding: 12px 24px; + -fx-min-width: 150px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.4), 8, 0.3, 2, 2); +} + +.password-primary-button:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ba68c8); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.6), 10, 0.4, 3, 3); + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; +} + +.password-secondary-button { + -fx-background-color: linear-gradient(to bottom, #fff59d, #ffeb3b); + -fx-text-fill: #7b1fa2; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-background-radius: 20px; + -fx-border-radius: 20px; + -fx-border-color: #ffeb3b; + -fx-border-width: 1px; + -fx-padding: 10px 20px; + -fx-min-width: 150px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(255, 235, 59, 0.4), 6, 0.3, 2, 2); +} + +.password-secondary-button:hover { + -fx-background-color: linear-gradient(to bottom, #fff9c4, #fff59d); + -fx-effect: dropshadow(gaussian, rgba(255, 235, 59, 0.6), 8, 0.4, 2, 2); +} + +/* ===== 修改密码界面状态标签样式 - 修复换行版本 ===== */ +.password-status-label { + -fx-text-fill: transparent; + -fx-font-size: 13px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: transparent; + -fx-background-radius: 10px; + -fx-padding: 0px; + -fx-border-color: transparent; + -fx-border-radius: 10px; + -fx-border-width: 0px; + -fx-alignment: center; + -fx-text-alignment: center; + -fx-min-height: 0px; + -fx-pref-height: 0px; + -fx-max-width: 300px; +} + +.password-status-label-with-text { + -fx-text-fill: #7b1fa2; + -fx-font-size: 13px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.15); + -fx-background-radius: 10px; + -fx-padding: 10px 15px; + -fx-border-color: rgba(186, 104, 200, 0.3); + -fx-border-radius: 10px; + -fx-border-width: 1px; + -fx-alignment: center; + -fx-text-alignment: center; + -fx-min-height: 60px; + -fx-pref-height: 60px; + -fx-max-width: 300px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 6, 0.3, 2, 2); +} + +/* ===== 设置密码界面专用样式 - 淡黄色和淡紫色主题 ===== */ +.setpassword-background { + -fx-background-image: url('../images/background.png'); + -fx-background-size: cover; + -fx-background-position: center center; + -fx-background-repeat: no-repeat; + -fx-background-color: linear-gradient(to bottom right, #fff9c4, #f3e5f5); +} + +.setpassword-glass-panel { + -fx-background-color: rgba(255, 255, 255, 0.88); + -fx-background-radius: 30px; + -fx-border-radius: 30px; + -fx-border-color: rgba(186, 104, 200, 0.4); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.2), 25, 0.5, 0, 8); +} + +.setpassword-title { + -fx-text-fill: #7b1fa2; + -fx-font-weight: bold; + -fx-font-size: 26px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.25), 4, 0.6, 2, 2); +} + +.setpassword-prompt { + -fx-text-fill: #7b1fa2; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.1); + -fx-background-radius: 10px; + -fx-padding: 10px 15px; + -fx-border-color: rgba(186, 104, 200, 0.2); + -fx-border-radius: 10px; + -fx-border-width: 1px; + -fx-alignment: center; +} + +.setpassword-textfield { + -fx-background-color: rgba(255, 255, 255, 0.95); + -fx-border-color: rgba(186, 104, 200, 0.5); + -fx-border-radius: 15px; + -fx-background-radius: 15px; + -fx-padding: 12px 16px; + -fx-font-size: 14px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 6, 0.2, 2, 2); +} + +.setpassword-textfield:focused { + -fx-border-color: rgba(156, 39, 176, 0.8); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.3), 8, 0.3, 2, 2); +} + +.setpassword-primary-button { + -fx-background-color: linear-gradient(to bottom, #ba68c8, #ab47bc); + -fx-text-fill: white; + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-background-radius: 25px; + -fx-border-radius: 25px; + -fx-border-color: #ab47bc; + -fx-border-width: 2px; + -fx-padding: 12px 24px; + -fx-min-width: 150px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.4), 8, 0.3, 2, 2); +} + +.setpassword-primary-button:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ba68c8); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.6), 10, 0.4, 3, 3); + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; +} + +/* ===== 设置密码界面状态标签样式 - 修复页面滑动 ===== */ +.setpassword-status-label { + -fx-text-fill: transparent; + -fx-font-size: 12px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: transparent; + -fx-background-radius: 10px; + -fx-padding: 0px; + -fx-border-color: transparent; + -fx-border-radius: 10px; + -fx-border-width: 0px; + -fx-alignment: center; + -fx-text-alignment: center; + -fx-min-height: 70px; + -fx-pref-height: 70px; + -fx-max-width: 280px; + -fx-line-spacing: 2px; +} + +.setpassword-status-label-with-text { + -fx-text-fill: #7b1fa2; + -fx-font-size: 12px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.15); + -fx-background-radius: 10px; + -fx-padding: 12px 15px; + -fx-border-color: rgba(186, 104, 200, 0.3); + -fx-border-radius: 10px; + -fx-border-width: 1px; + -fx-alignment: center; + -fx-text-alignment: center; + -fx-min-height: 70px; + -fx-pref-height: 70px; + -fx-max-width: 280px; + -fx-line-spacing: 2px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 6, 0.3, 2, 2); +} +/* ===== 表单字段标签 (新增) ===== */ +.form-label { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: #7b1fa2; /* 使用与标题一致的紫色 */ + -fx-padding: 0 0 4px 8px; /* 在标签下方和左侧留出一点空间 */ +} \ No newline at end of file diff --git a/src/main/resources/com/mathgenerator/view/ChangePasswordView.fxml b/src/main/resources/com/mathgenerator/view/ChangePasswordView.fxml index 480d327..7e386ca 100644 --- a/src/main/resources/com/mathgenerator/view/ChangePasswordView.fxml +++ b/src/main/resources/com/mathgenerator/view/ChangePasswordView.fxml @@ -7,25 +7,85 @@ - + + - - - - - - \ No newline at end of file diff --git a/src/main/resources/com/mathgenerator/view/RegisterView.fxml b/src/main/resources/com/mathgenerator/view/RegisterView.fxml index 5012e91..88bcd37 100644 --- a/src/main/resources/com/mathgenerator/view/RegisterView.fxml +++ b/src/main/resources/com/mathgenerator/view/RegisterView.fxml @@ -3,36 +3,98 @@ - - + + - - - - + + - -