diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2fa42a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.idea/ +out/ +.vscode/ +bin/ +questions/ +generated_papers/ +target/ +users.json +legal/ +lib/ \ No newline at end of file diff --git a/README.md b/README.md index 38e4972..6de3483 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,80 @@ -# two_project +# 中小学数学卷子自动生成程序 +本项目是一个基于 Java 开发的命令行应用(CLI),旨在为小学、初中和高中的数学老师提供一个便捷的工具,用于自动生成符合教学要求的数学练习卷。 + +程序设计遵循了现代软件工程原则,通过模块化和面向对象的设计,实现了UI与逻辑分离、高内聚、低耦合以及良好的可扩展性。 + +## 主要功能 + +* **多学段支持**:可为 **小学**、**初中**、**高中** 三个不同学段生成题目。 +* **分级与混合难度**: + * 题目难度逐级递增,高年级题目会自动包含低年级知识点(例如,高中题可能包含平方、开根号等)。 + * 生成的试卷会自动混合不同难度的题目(例如,一份高中试卷会包含一定比例的初中和小学基础题),更贴近真实考试。 +* **历史题目查重**:确保为同一位老师生成的题目不会与历史试卷中的题目重复,保证每次练习都是全新的。 +* **菜单式交互界面**:提供清晰、易用的数字菜单进行操作,无需记忆复杂命令。 +* **自动保存与归档**:生成的试卷会自动以“年-月-日-时-分-秒.txt”的格式命名,并保存在每位教师专属的文件夹下,方便管理和追溯。 + +## 技术栈 + +* **开发语言**:Java 21 +* **构建工具**:Apache Maven + +## 如何启动 + +配置 **JDK 21 (或更高版本)** 和 **Apache Maven**。 + +### 1\. 环境要求 + +* `JAVA_HOME` 环境变量已正确设置。 +* **全平台通用** + +### 2\. 构建项目 + +首先,需要使用 Maven 将项目打包成一个可执行的 `.jar` 文件。 + +在项目的根目录(即 `pom.xml` 文件所在的目录)下,打开终端并执行以下命令: + +```bash +mvn clean package +``` + +该命令会编译所有代码,并 T 在 `target/` 目录下生成一个名为 `mathgenerator.jar` 的文件。 + +### 3\. 运行程序 + +构建成功后,使用以下命令来启动程序: + +```bash +java -jar mathgenerator.jar +``` + +如果一切顺利,您将在终端看到欢迎界面和用户登录提示。 + +## 项目结构 + +项目采用标准的 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 diff --git a/doc/record声明.md b/doc/record声明.md new file mode 100644 index 0000000..3a06636 --- /dev/null +++ b/doc/record声明.md @@ -0,0 +1,84 @@ +# 使用 `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 new file mode 100644 index 0000000..5d85030 --- /dev/null +++ b/doc/具体介绍.md @@ -0,0 +1,120 @@ +# 功能拆分文档 + +本项目是一个基于Java开发的命令行应用程序,旨在为小学、初-中和高中三个学段的教师自动生成符合特定难度要求的数学练习卷。项目采用模块化和面向对象的思想进行设计,并引入了策略模式等设计模式,确保了代码的高内聚、低耦合以及良好的可扩展性。 + + +## 一、项目结构 (Maven) + +项目采用标准的 Maven 目录结构,并遵循了职责分离的设计原则,将不同功能的模块划分到独立的包中,最终结构如下: + +``` +. +├── 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 +``` + + + +--- + +## 二、核心功能拆分 + +项目整体功能被划分为多个核心模块,每个模块由专门的Java包(Package)进行管理,职责清晰。 + +### 1\. 数据模型模块 (`model`包) + +该模块定义了项目中使用的核心数据结构,确保数据的类型安全和一致性。 + +* **`Level` 枚举 (Enum)**: + * 定义了三个常量:`PRIMARY` (小学), `JUNIOR_HIGH` (初中), `SENIOR_HIGH` (高中)。 + * 使用枚举代替纯字符串,有效防止了因拼写错误导致的bug。 +* **`User` 类**: 一个数据类(采用 `Record` 实现),用于封装用户的核心属性:用户名 (`username`)、密码 (`password`) 和所属学段 (`level`)。 + +### 2\. 题目生成模块 (`generator`包) + +这是实现项目核心业务逻辑的模块,采用接口驱动和继承链设计。 + +* **`QuestionGenerator` 接口**: 定义了所有题目生成器必须遵守的统一规范,包含一个核心方法 `String generateSingleQuestion()`。 +* **三个实现类**: + * `PrimarySchoolGenerator`: 生成包含 `+`, `-`, `*`, `/` 和 `()` 的小学难度题目。 + * `JuniorHighSchoolGenerator`: **继承自 `PrimarySchoolGenerator`**,在小学题目基础上,确保至少包含一个平方或开根号运算。 + * `SeniorHighSchoolGenerator`: **继承自 `JuniorHighSchoolGenerator`**,在初中题目基础上,再确保至少包含一个 `sin`, `cos` 或 `tan` 三角函数运算。 + * 所有生成器都遵守“操作数在1-5个之间,取值范围1-100”的规则。 + +### 3\. 文件存储模块 (`storage`包) + +该模块封装了所有与**试卷文件**系统交互的操作。 + +* **`FileManager` 类**: + * **保存试卷**: 提供 `savePaper` 方法,以“年-月-日-时-分-秒.txt”的格式保存试卷到用户专属的文件夹。 + * **读取历史题目 (查重)**: 提供 `loadExistingQuestions` 方法,扫描用户文件夹下的所有历史题目,返回一个 `Set` 集合用于查重。 + +### 4\. 业务服务模块 (`service`包) + +该模块是业务逻辑的协调者,包含了用户管理和试卷生成两大核心服务。 + +* **`UserService` 类**: + * 是用户管理的**核心**。 + * 负责用户的**注册**、**登录**以及**持久化存储**。 + * 程序启动时从 `users.json` 文件加载用户数据,在用户注册时将新数据写回文件。 +* **`PaperService` 类**: + * 负责处理“生成并保存一份完整试卷”的**流程**。 + * 它持有一个 `PaperStrategy` 对象的引用,将具体的题目难度选择**委托**给策略对象来执行。 +* **`service.strategy` 子包**: + * **`PaperStrategy` 接口**: 定义了“试卷组合策略”的抽象规范。 + * **`MixedDifficultyStrategy` 类**: `PaperStrategy` 的一个具体实现,负责生成包含不同难度梯度的混合试卷。 + +### 5\. 主程序与UI模块 (`Application.java` 和 `ui`包) + +为遵循单一职责原则,程序的启动和UI交互被拆分到独立的单元中。 + +* **`Application.java`**: 程序的唯一入口。其 `main` 方法非常简洁,仅负责创建和组装所有核心组件(如 `UserService`, `PaperService`, `ConsoleUI` 等),然后启动应用。 +* **`ui.ConsoleUI` 类**: 一个专门负责所有控制台用户界面显示和交互的类。它实现了**菜单驱动**的用户界面,处理用户的**登录**、**注册**、**难度切换**和**题目生成**等所有交互操作。 + +## 三、如何使用 Maven 构建与运行 + +项目采用 Maven 进行自动化构建和打包。 + +1. **构建 (打包)**: 在项目根目录(`pom.xml` 所在位置)打开终端,执行以下命令: + + ```bash + mvn clean package + ``` + + 该命令会自动完成编译、测试和打包,并在 `target/` 目录下生成一个可执行的 `mathgenerator.jar` 文件。 + +2. **运行**: 继续在终端中执行以下命令来启动程序。 + + ```bash + java -jar mathgenerator.jar + ``` \ No newline at end of file diff --git a/doc/双击启动.bat b/doc/双击启动.bat new file mode 100644 index 0000000..05146d9 --- /dev/null +++ b/doc/双击启动.bat @@ -0,0 +1,14 @@ +@echo off +:: 1. 将控制台环境设置为 UTF-8,确保中文输入输出正确 +chcp 65001 > nul + +:: 2. 告诉 Java 虚拟机(JVM) 使用 UTF-8 编码 +set JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8 + +:: 3. 运行你的 JAR 包 +echo loading... +java -jar mathgenerator.jar + +:: 4. 清理环境变量并暂停,以便查看程序输出 +set JAVA_TOOL_OPTIONS= +pause \ No newline at end of file diff --git a/doc/程序运行说明.md b/doc/程序运行说明.md new file mode 100644 index 0000000..20225b2 --- /dev/null +++ b/doc/程序运行说明.md @@ -0,0 +1,18 @@ +# 程序运行说明 + +1. 环境配置:java21及以上,不推荐使用windows,要设置更改字符,否则中文字符会乱码,建议使用ubuntu,macos等系统 + +2. 在控制台输入 + + ```bash + java -jar mathgenerator.jar + ``` + + 可以开始使用程序 + +3. 程序提供在windows运行的bat脚本,需要可以双击运行 + +4. 第一次运行时,在运行目录下会生成默认用户的json文件,生成的试卷放在generated文件夹下 + +5. 注册功能的说明:注册功能是自己添加,由于项目要求登录时将用户名和密码同行隔空格输入,因此注册时的特殊情况,如密码含空格,会无法登录,但是登录不是课程考核之一,希望不要引起不必要的误会,特此说明 + diff --git a/doc/设计模式.md b/doc/设计模式.md new file mode 100644 index 0000000..f0422b7 --- /dev/null +++ b/doc/设计模式.md @@ -0,0 +1,48 @@ +# 设计模式 + +本项目在开发过程中,为了实现代码的高内聚、低耦合以及未来的高可扩展性,采纳了多种业界标准的设计模式和原则。本文档将详细解析其中最核心的几种设计模式。 + +## 1. 策略模式 (Strategy Pattern) + +策略模式是本项目架构中最为核心和体现拓展性的设计模式。 + +* **意图**:定义一系列算法,将每一个算法封装起来,并使它们可以相互替换。此模式让算法的变化独立于使用算法的客户。 + +* **在项目中的体现**: + * **`PaperStrategy` 接口**:这是策略的抽象。它定义了一个 `selectGenerator(Level mainLevel)` 方法,所有具体的试卷组合算法都必须实现这个接口。 + * **`MixedDifficultyStrategy` 类**:这是一个具体的策略实现。它封装了我们设计的“混合难度”算法(例如,高中试卷包含60%高中题、30%初中题、10%小学题)。 + * **`PaperService` 类**:这是使用策略的上下文(Context)。`PaperService` 不再关心试卷的具体组合逻辑,它只持有一个 `PaperStrategy` 对象的引用,并在生成试卷时调用其 `selectGenerator` 方法。 + +* **带来的好处**: + * **极高的可扩展性**:如果我们想新增一种“只出难题”的试卷模式,只需新增一个 `HardModeStrategy` 类实现 `PaperStrategy` 接口,然后在程序入口处注入这个新策略即可,**完全不需要修改 `PaperService` 的代码**。这完美遵循了“开闭原则”。 + * **职责分离**:`PaperService` 的职责被简化为控制试卷生成的整体流程,而具体的题目组合算法则被分离到各个策略类中,使得代码结构更清晰。 + +## 2. 工厂方法模式 (Factory Method Pattern) 的简化应用 + +虽然没有严格地实现一个完整的工厂方法模式,但其核心思想在项目中得到了应用。 + +* **意图**:定义一个用于创建对象的接口,让子类决定实例化哪一个类。 + +* **在项目中的体现**: + * 我们的 `MixedDifficultyStrategy` 内部的 `selectGenerator` 方法实际上扮演了一个**简单工厂(Simple Factory)**的角色。它根据输入的 `Level` 和内部的随机逻辑,负责“生产”出具体的 `QuestionGenerator` 实例(`PrimarySchoolGenerator`, `JuniorHighSchoolGenerator` 等)。 + +* **带来的好处**: + * **集中创建逻辑**:所有关于“如何根据难度创建对应题目生成器”的逻辑都被集中在一个地方,而不是散落在代码各处。当需要修改创建逻辑时,我们只需要改动这个“工厂”即可。 + +## 3. 模板方法模式 (Template Method Pattern) + +此模式在我们的题目生成器继承链中得到了巧妙的运用。 + +* **意图**:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 + +* **在项目中的体现**: + * **`PrimarySchoolGenerator`** 提供了最基础的题目生成逻辑。 + * **`JuniorHighSchoolGenerator`** 继承前者,它的 `generateSingleQuestion` 方法首先通过 `super.generateSingleQuestion()` 调用父类的“模板”来获得一个基础题目,然后在此基础上增加自己的特定步骤(添加平方或开根号)。 + * **`SeniorHighSchoolGenerator`** 同理,它调用父类(初中生成器)的方法,在这个“半成品”上增加自己的步骤(添加三角函数)。 + +* **带来的好处**: + * **代码复用**:避免了在每个生成器中都重复编写基础表达式的生成逻辑。 + * **结构清晰**:清晰地定义了不同难度题目之间的层级递进关系。 + + +通过综合运用这些设计模式和原则,我们的项目不仅实现了所有功能需求,更拥有了一个专业、健壮且易于扩展的软件架构。 \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a0c33b8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + com.mathgenerator + MathGenerator + 1.0.0 + + + 17 + 17 + UTF-8 + + + + + com.google.code.gson + gson + 2.10.1 + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + default-jar + none + + + + + + + maven-assembly-plugin + 3.6.0 + + + + com.mathgenerator.Application + + + + jar-with-dependencies + + mathgenerator + false + + + + make-assembly + package + + single + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/MainApplication.java b/src/main/java/com/mathgenerator/MainApplication.java new file mode 100644 index 0000000..cb6f216 --- /dev/null +++ b/src/main/java/com/mathgenerator/MainApplication.java @@ -0,0 +1,25 @@ +package com.mathgenerator; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; +import java.io.IOException; + +public class MainApplication extends Application { + + @Override + public void start(Stage primaryStage) throws IOException { + // 启动时加载登录界面 + Parent root = FXMLLoader.load(getClass().getResource("/com/mathgenerator/view/LoginView.fxml")); + primaryStage.setTitle("中小学数学学习软件"); + primaryStage.setScene(new Scene(root)); + primaryStage.setResizable(false); + primaryStage.show(); + } + + public static void main(String[] args) { + launch(args); + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/controller/LoginController.java b/src/main/java/com/mathgenerator/controller/LoginController.java new file mode 100644 index 0000000..7c7150d --- /dev/null +++ b/src/main/java/com/mathgenerator/controller/LoginController.java @@ -0,0 +1,113 @@ +package com.mathgenerator.controller; + +import com.mathgenerator.model.User; +import com.mathgenerator.service.UserService; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.stage.Stage; + +import java.io.IOException; +import java.util.Optional; + +public class LoginController { + + // 依赖注入后端服务 + private final UserService userService = new UserService(); + + // @FXML注解将FXML文件中的控件与这里的变量关联起来 + @FXML + private TextField usernameField; + + @FXML + private PasswordField passwordField; + + @FXML + private Button loginButton; + + @FXML + private Button registerButton; + + @FXML + private Label statusLabel; + + /** + * 处理登录按钮点击事件。 + * @param event 事件对象 + */ + @FXML + private void handleLoginButtonAction(ActionEvent event) { + String username = usernameField.getText(); + String password = passwordField.getText(); + + if (username.isEmpty() || password.isEmpty()) { + statusLabel.setText("用户名和密码不能为空!"); + return; + } + + Optional userOptional = userService.login(username, password); + + if (userOptional.isPresent()) { + statusLabel.setText("登录成功!"); + // 登录成功,调用新方法跳转到主菜单 + loadMainMenu(userOptional.get()); + } else { + 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 文件 + FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/MainMenuView.fxml")); + Parent root = loader.load(); + + // 2. 获取新界面的控制器 + MainMenuController controller = loader.getController(); + + // 3. 调用控制器的方法,传递数据 + controller.initData(user); + + // 4. 显示新场景 + Stage stage = (Stage) loginButton.getScene().getWindow(); + stage.setScene(new Scene(root)); + stage.setTitle("主菜单"); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 切换到简单场景的辅助方法(如注册页)。 + * @param fxmlPath FXML文件的路径 + */ + private void loadScene(String fxmlPath) { + try { + Parent root = FXMLLoader.load(getClass().getResource(fxmlPath)); + Stage stage = (Stage) loginButton.getScene().getWindow(); + stage.setScene(new Scene(root)); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/controller/MainMenuController.java b/src/main/java/com/mathgenerator/controller/MainMenuController.java new file mode 100644 index 0000000..867deff --- /dev/null +++ b/src/main/java/com/mathgenerator/controller/MainMenuController.java @@ -0,0 +1,101 @@ +package com.mathgenerator.controller; + +import com.mathgenerator.model.Level; +import com.mathgenerator.model.User; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.stage.Stage; +import java.io.IOException; + +public class MainMenuController { + + private User currentUser; + + @FXML private Label welcomeLabel; + @FXML private TextField questionCountField; + @FXML private Label statusLabel; + @FXML private Button logoutButton; + + /** + * 初始化控制器,接收登录成功的用户信息。 + * @param user 当前登录的用户 + */ + public void initData(User user) { + this.currentUser = user; + welcomeLabel.setText("欢迎, " + currentUser.username() + "!"); + } + + @FXML + private void handlePrimaryAction(ActionEvent event) { + startQuiz(Level.PRIMARY); + } + + @FXML + private void handleJuniorHighAction(ActionEvent event) { + startQuiz(Level.JUNIOR_HIGH); + } + + @FXML + private void handleSeniorHighAction(ActionEvent event) { + startQuiz(Level.SENIOR_HIGH); + } + + @FXML + private void handleChangePasswordAction(ActionEvent event) { + // TODO: 跳转到修改密码界面 + statusLabel.setText("修改密码功能待实现。"); + } + + @FXML + private void handleLogoutAction(ActionEvent event) { + // 跳转回登录界面 + loadScene("/com/mathgenerator/view/LoginView.fxml"); + } + + /** + * 验证输入并准备开始答题。 + * @param level 选择的难度 + */ + private void startQuiz(Level level) { + try { + int count = Integer.parseInt(questionCountField.getText()); + if (count < 1 || count > 50) { + statusLabel.setText("题目数量必须在 1 到 50 之间!"); + return; + } + + // 加载答题界面,并传递数据 + FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/QuizView.fxml")); + Parent root = loader.load(); + + QuizController controller = loader.getController(); + controller.initData(currentUser, level, count); + + Stage stage = (Stage) logoutButton.getScene().getWindow(); + stage.setScene(new Scene(root)); + stage.setTitle(level.getChineseName() + " - 答题中"); + + } catch (NumberFormatException e) { + statusLabel.setText("请输入有效的题目数量!"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void loadScene(String fxmlPath) { + try { + Stage stage = (Stage) logoutButton.getScene().getWindow(); + Parent root = FXMLLoader.load(getClass().getResource(fxmlPath)); + stage.setScene(new Scene(root)); + stage.setTitle("用户登录"); // 返回登录时重置标题 + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/controller/QuizController.java b/src/main/java/com/mathgenerator/controller/QuizController.java new file mode 100644 index 0000000..abce998 --- /dev/null +++ b/src/main/java/com/mathgenerator/controller/QuizController.java @@ -0,0 +1,144 @@ +package com.mathgenerator.controller; + +import com.mathgenerator.model.ChoiceQuestion; +import com.mathgenerator.model.Level; +import com.mathgenerator.model.User; +import com.mathgenerator.service.PaperService; +import com.mathgenerator.service.strategy.MixedDifficultyStrategy; +import com.mathgenerator.storage.FileManager; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ToggleGroup; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +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; + @FXML private ToggleGroup optionsGroup; + @FXML private RadioButton option1, option2, option3, option4; + @FXML private Button submitButton; + @FXML private Label statusLabel; + + /** + * 构造函数,初始化后端服务 + */ + public QuizController() { + // 这是依赖注入的一种简化形式,在真实项目中会使用框架管理 + FileManager fileManager = new FileManager(); + MixedDifficultyStrategy strategy = new MixedDifficultyStrategy(); + this.paperService = new PaperService(fileManager, strategy); + } + + /** + * 接收从主菜单传递过来的数据,并开始答题 + */ + public void initData(User user, Level level, int questionCount) { + this.currentUser = user; + // 调用后端服务生成题目 + this.questions = paperService.createPaper(user, questionCount, level); + displayCurrentQuestion(); + } + + /** + * 显示当前的题目和选项 + */ + private void displayCurrentQuestion() { + ChoiceQuestion currentQuestion = questions.get(currentQuestionIndex); + + questionNumberLabel.setText(String.format("第 %d / %d 题", currentQuestionIndex + 1, questions.size())); + progressBar.setProgress((double) (currentQuestionIndex + 1) / questions.size()); + questionTextLabel.setText(currentQuestion.questionText()); + + List radioButtons = List.of(option1, option2, option3, option4); + for (int i = 0; i < radioButtons.size(); i++) { + radioButtons.get(i).setText(currentQuestion.options().get(i)); + } + + optionsGroup.selectToggle(null); // 清除上一次的选择 + statusLabel.setText(""); // 清除状态提示 + + if (currentQuestionIndex == questions.size() - 1) { + submitButton.setText("完成答题"); + } + } + + /** + * 处理提交按钮的点击事件 + */ + @FXML + private void handleSubmitButtonAction(ActionEvent event) { + RadioButton selectedRadioButton = (RadioButton) optionsGroup.getSelectedToggle(); + if (selectedRadioButton == null) { + statusLabel.setText("请选择一个答案!"); + return; + } + + // 记录用户答案的索引 + List radioButtons = List.of(option1, option2, option3, option4); + userAnswers.add(radioButtons.indexOf(selectedRadioButton)); + + // 移动到下一题或结束答题 + currentQuestionIndex++; + if (currentQuestionIndex < questions.size()) { + displayCurrentQuestion(); + } else { + // 答题结束,计算分数并跳转到分数界面 + calculateScoreAndShowResults(); + } + } + + /** + * 计算分数并准备跳转到结果页面 + */ + private void calculateScoreAndShowResults() { + int correctCount = 0; + for (int i = 0; i < questions.size(); i++) { + if (userAnswers.get(i) == questions.get(i).correctOptionIndex()) { + correctCount++; + } + } + double score = (double) correctCount / questions.size() * 100; + + // 禁用当前页面的按钮 + submitButton.setDisable(true); + statusLabel.setText("答题已完成,正在为您计算分数..."); + + // 加载分数界面并传递数据 + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/ScoreView.fxml")); + Parent root = loader.load(); + + ScoreController controller = loader.getController(); + controller.initData(currentUser, score); // 将用户和分数传递过去 + + Stage stage = (Stage) submitButton.getScene().getWindow(); + stage.setScene(new Scene(root)); + stage.setTitle("答题结果"); + + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/controller/RegisterController.java b/src/main/java/com/mathgenerator/controller/RegisterController.java new file mode 100644 index 0000000..c1a2835 --- /dev/null +++ b/src/main/java/com/mathgenerator/controller/RegisterController.java @@ -0,0 +1,95 @@ +package com.mathgenerator.controller; + +import com.mathgenerator.service.UserService; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.stage.Stage; +import java.io.IOException; + +public class RegisterController { + + private final UserService userService = new UserService(); + private String sentCode; // 用于存储已发送的验证码 + + @FXML private TextField usernameField; + @FXML private TextField emailField; + @FXML private TextField verificationCodeField; + @FXML private PasswordField passwordField; + @FXML private PasswordField confirmPasswordField; + @FXML private Button sendCodeButton; + @FXML private Button registerButton; + @FXML private Button backToLoginButton; + @FXML private Label statusLabel; + + @FXML + private void handleSendCodeAction(ActionEvent event) { + String email = emailField.getText(); + if (email.isEmpty() || !email.contains("@")) { + statusLabel.setText("请输入一个有效的邮箱地址!"); + return; + } + // 调用后端服务发送验证码(模拟) + this.sentCode = userService.sendVerificationCode(email); + statusLabel.setText("验证码已发送(请查看控制台输出)。"); + sendCodeButton.setDisable(true); // 防止重复点击 + } + + @FXML + private void handleRegisterAction(ActionEvent event) { + // 1. 字段校验 + if (usernameField.getText().isEmpty() || emailField.getText().isEmpty() || + verificationCodeField.getText().isEmpty() || passwordField.getText().isEmpty()) { + statusLabel.setText("所有字段都不能为空!"); + return; + } + if (!passwordField.getText().equals(confirmPasswordField.getText())) { + statusLabel.setText("两次输入的密码不匹配!"); + return; + } + if (this.sentCode == null || !this.sentCode.equals(verificationCodeField.getText())) { + statusLabel.setText("验证码错误!"); + return; + } + if (!UserService.isPasswordValid(passwordField.getText())) { + statusLabel.setText("密码格式错误!必须为6-10位,且包含大小写字母和数字。"); + return; + } + + // 2. 调用后端服务进行注册 + boolean success = userService.register( + usernameField.getText(), + emailField.getText(), + passwordField.getText() + ); + + // 3. 根据结果更新UI + if (success) { + statusLabel.setText("注册成功!请返回登录。"); + registerButton.setDisable(true); + } else { + statusLabel.setText("注册失败:用户名或邮箱已被占用。"); + } + } + + @FXML + private void handleBackToLoginAction(ActionEvent event) { + loadScene("/com/mathgenerator/view/LoginView.fxml"); + } + + private void loadScene(String fxmlPath) { + try { + Parent root = FXMLLoader.load(getClass().getResource(fxmlPath)); + Stage stage = (Stage) backToLoginButton.getScene().getWindow(); + stage.setScene(new Scene(root)); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ 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 new file mode 100644 index 0000000..dbcba69 --- /dev/null +++ b/src/main/java/com/mathgenerator/controller/ScoreController.java @@ -0,0 +1,77 @@ +package com.mathgenerator.controller; + +import com.mathgenerator.model.User; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.stage.Stage; +import java.io.IOException; + +public class ScoreController { + + private User currentUser; + + @FXML private Label scoreLabel; + @FXML private Label resultMessageLabel; + @FXML private Button tryAgainButton; + @FXML private Button logoutButton; + + /** + * 初始化控制器,接收答题结果数据 + * @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) { + resultMessageLabel.setText("非常不错!继续努力!"); + } else if (score >= 60) { + resultMessageLabel.setText("成绩合格,再接再厉!"); + } else { + resultMessageLabel.setText("别灰心,下次会更好的!"); + } + } + + /** + * 处理“再做一组”按钮事件,返回主菜单 + */ + @FXML + private void handleTryAgainAction(ActionEvent event) { + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/MainMenuView.fxml")); + Parent root = loader.load(); + MainMenuController controller = loader.getController(); + controller.initData(currentUser); // 将用户信息传回主菜单 + + Stage stage = (Stage) tryAgainButton.getScene().getWindow(); + stage.setScene(new Scene(root)); + stage.setTitle("主菜单"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 处理“退出登录”按钮事件,返回登录界面 + */ + @FXML + private void handleLogoutAction(ActionEvent event) { + try { + Parent root = FXMLLoader.load(getClass().getResource("/com/mathgenerator/view/LoginView.fxml")); + Stage stage = (Stage) logoutButton.getScene().getWindow(); + stage.setScene(new Scene(root)); + stage.setTitle("用户登录"); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java b/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java new file mode 100644 index 0000000..7922dc4 --- /dev/null +++ b/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java @@ -0,0 +1,78 @@ +package com.mathgenerator.generator; + +import com.mathgenerator.model.ChoiceQuestion; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 初中选择题生成器 (已修正)。 + * 通过构造而非随机的方式,确保题目包含平方或开根号,且最终解为整数。 + */ +public class JuniorHighSchoolGenerator extends PrimarySchoolGenerator { + + private static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+"); + private static final int[] PERFECT_SQUARES = {4, 9, 16, 25, 36, 49, 64, 81, 100}; + + @Override + public ChoiceQuestion generateSingleQuestion() { + String questionText; + int correctAnswer; + + // 循环直到生成一个有效的、有整数解的题目 + while (true) { + // 1. 获取一个基础的小学题目字符串 + String basicQuestionText = super.generateBasicQuestionText(); + + // 2. 随机选择在题目中加入平方还是开根号 + boolean useSquare = ThreadLocalRandom.current().nextBoolean(); + + if (useSquare) { + // --- 平方策略 --- + // 随机找到一个数字,给它加上平方 + Matcher matcher = NUMBER_PATTERN.matcher(basicQuestionText); + List numbers = new ArrayList<>(); + while (matcher.find()) { numbers.add(matcher.group()); } + if (numbers.isEmpty()) continue; + + String numToSquare = numbers.get(ThreadLocalRandom.current().nextInt(numbers.size())); + questionText = basicQuestionText.replaceFirst(Pattern.quote(numToSquare), numToSquare + "²"); + + } else { + // --- 开根号策略 (保证整数解) --- + // a. 随机找到一个数字 + Matcher matcher = NUMBER_PATTERN.matcher(basicQuestionText); + List numbers = new ArrayList<>(); + while (matcher.find()) { numbers.add(matcher.group()); } + if (numbers.isEmpty()) continue; + String numToReplace = numbers.get(ThreadLocalRandom.current().nextInt(numbers.size())); + + // b. 随机选一个完美的平方数 + int perfectSquare = PERFECT_SQUARES[ThreadLocalRandom.current().nextInt(PERFECT_SQUARES.length)]; + + // c. 用 "√(平方数)" 替换掉原数字 + String sqrtExpression = "√" + perfectSquare; + questionText = basicQuestionText.replaceFirst(Pattern.quote(numToReplace), sqrtExpression); + } + + // 3. 验证修改后的题目是否有整数解 + try { + Object result = evaluateExpression(questionText); + if (result instanceof Number && ((Number) result).doubleValue() == ((Number) result).intValue()) { + correctAnswer = ((Number) result).intValue(); + break; // 成功!得到整数解,跳出循环 + } + } catch (Exception e) { + // 如果表达式计算出错(例如语法错误),则忽略并重试 + } + } + + // 4. 为最终的题目生成选项 + List options = generateOptions(correctAnswer); + int correctIndex = options.indexOf(String.valueOf(correctAnswer)); + + return new ChoiceQuestion(questionText, options, correctIndex); + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java b/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java new file mode 100644 index 0000000..ff357b0 --- /dev/null +++ b/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java @@ -0,0 +1,144 @@ +package com.mathgenerator.generator; + +import com.mathgenerator.model.ChoiceQuestion; // 导入新模型 +import java.util.ArrayList; +import java.util.Collections; +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; + +/** + * 小学选择题生成器。 + * 生成包含 + - * / 和 () 的运算,并提供四个选项。 + */ +public class PrimarySchoolGenerator implements QuestionGenerator { + + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + + /** + * 生成一道小学难度的数学选择题。 + * @return 一个包含题干、四个选项和正确答案索引的 ChoiceQuestion 对象。 + */ + @Override + public ChoiceQuestion generateSingleQuestion() { + ThreadLocalRandom random = ThreadLocalRandom.current(); + int operandCount = random.nextInt(2, 5); // 2到4个操作数 + + String questionText; + int correctAnswer; + + // 循环直到生成一个可以计算出整数结果的表达式,避免类似 5/2 的情况 + while (true) { + List parts = new ArrayList<>(); + parts.add(String.valueOf(getOperand())); + for (int i = 1; i < operandCount; i++) { + parts.add(getRandomOperator()); + parts.add(String.valueOf(getOperand())); + } + if (operandCount > 2 && random.nextBoolean()) { + addParentheses(parts); + } + questionText = String.join(" ", parts); + + try { + // 使用脚本引擎计算表达式的精确值 + Object result = evaluateExpression(questionText); + // 确保结果是整数且没有余数 + if (result instanceof Number && ((Number) result).doubleValue() == ((Number) result).intValue()) { + correctAnswer = ((Number) result).intValue(); + break; // 成功计算,跳出循环 + } + } catch (Exception e) { + // 忽略异常 (如除以零),重新生成表达式 + } + } + + List options = generateOptions(correctAnswer); + int correctIndex = options.indexOf(String.valueOf(correctAnswer)); + + return new ChoiceQuestion(questionText, options, correctIndex); + } + + /** + * 生成四个选项 (1个正确,3个干扰项) + * @param correctAnswer 正确答案 + * @return 包含四个选项的随机排序列表 + */ + protected List generateOptions(int correctAnswer) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + Set options = new HashSet<>(); + options.add(String.valueOf(correctAnswer)); + + // 生成3个不重复的干扰项 + while (options.size() < 4) { + int delta = random.nextInt(1, 11); // 答案加减1-10 + // 随机决定是加还是减,增加干扰项的多样性 + int distractor = random.nextBoolean() ? correctAnswer + delta : correctAnswer - delta; + options.add(String.valueOf(distractor)); + } + + List sortedOptions = new ArrayList<>(options); + Collections.shuffle(sortedOptions); // 随机打乱选项顺序 + return sortedOptions; + } + + /** + * 使用JVM的脚本引擎计算字符串表达式的值。 + * @param expression 数学表达式字符串 + * @return 计算结果 + * @throws ScriptException 如果表达式有语法错误 + */ + protected Object evaluateExpression(String expression) throws ScriptException { + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName("JavaScript"); + // 为支持初高中计算,替换特殊符号 + // 注意√的正则表达式,确保只匹配数字 + expression = expression.replaceAll("²", "**2") + .replaceAll("√(\\d+)", "Math.sqrt($1)"); + return engine.eval(expression); + } + + private int getOperand() { + return ThreadLocalRandom.current().nextInt(1, 101); + } + + private String getRandomOperator() { + return OPERATORS[ThreadLocalRandom.current().nextInt(OPERATORS.length)]; + } + + private void addParentheses(List parts) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + int startOperandIndex = random.nextInt(parts.size() / 2); + int endOperandIndex = random.nextInt(startOperandIndex + 1, parts.size() / 2 + 1); + int startIndex = startOperandIndex * 2; + int endIndex = endOperandIndex * 2; + parts.add(endIndex + 1, ")"); + parts.add(startIndex, "("); + } + + // 在 PrimarySchoolGenerator.java 中添加这个方法 + /** + * 仅生成题目字符串,不包含答案和选项。 + * 这是为了方便子类(初中、高中)继承和修改题干。 + * @return 题目文本字符串 + */ + public String generateBasicQuestionText() { + ThreadLocalRandom random = ThreadLocalRandom.current(); + int operandCount = random.nextInt(2, 5); + List parts = new ArrayList<>(); + parts.add(String.valueOf(getOperand())); + for (int i = 1; i < operandCount; i++) { + parts.add(getRandomOperator()); + parts.add(String.valueOf(getOperand())); + } + if (operandCount > 2 && random.nextBoolean()) { + addParentheses(parts); + } + return String.join(" ", parts); + } + +} \ 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 new file mode 100644 index 0000000..e478038 --- /dev/null +++ b/src/main/java/com/mathgenerator/generator/QuestionGenerator.java @@ -0,0 +1,14 @@ +package com.mathgenerator.generator; + +import com.mathgenerator.model.ChoiceQuestion; // 导入新模型 + +/** + * 题目生成器接口,定义了所有具体生成器必须实现的方法。 + */ +public interface QuestionGenerator { + /** + * 生成一道符合特定难度的数学选择题。 + * @return 代表数学选择题的 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 new file mode 100644 index 0000000..d38cee6 --- /dev/null +++ b/src/main/java/com/mathgenerator/generator/SafePrimarySchoolGenerator.java @@ -0,0 +1,149 @@ +package com.mathgenerator.generator; + +import com.mathgenerator.model.ChoiceQuestion; +import java.util.ArrayList; +import java.util.Collections; +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; + +/** + * “安全”的小学题目生成器,确保运算过程中不产生负数,并生成选择题。 + */ +public class SafePrimarySchoolGenerator implements QuestionGenerator { + + /** + * 生成一道确保结果非负的小学难度的数学选择题。 + * @return 一个符合所有约束的 ChoiceQuestion 对象。 + */ + @Override + public ChoiceQuestion generateSingleQuestion() { + int operandsTotalBudget = ThreadLocalRandom.current().nextInt(2, 6); + Term finalExpression = generateSafeExpression(operandsTotalBudget); + String questionText = String.join(" ", finalExpression.parts()); + int correctAnswer = finalExpression.value(); + + List options = generateOptions(correctAnswer); + int correctIndex = options.indexOf(String.valueOf(correctAnswer)); + + return new ChoiceQuestion(questionText, options, correctIndex); + } + + /** + * 内部记录类,用于在递归生成表达式时传递部分结果。 + */ + private record Term(List parts, int value, int operandsUsed) {} + + /** + * 根据操作数预算,生成一个确保结果非负的(子)表达式。 + */ + private Term generateSafeExpression(int operandBudget) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + List fullExpressionParts = new ArrayList<>(); + int operandsRemaining = operandBudget; + + Term firstTerm = generateTerm(operandsRemaining); + fullExpressionParts.addAll(firstTerm.parts()); + int currentResult = firstTerm.value(); + operandsRemaining -= firstTerm.operandsUsed(); + + while (operandsRemaining > 0) { + Term nextTerm = generateTerm(operandsRemaining); + boolean useAddition = random.nextBoolean(); + if (!useAddition && currentResult < nextTerm.value()) { + useAddition = true; // 强制改为加法以避免负数 + } + + if (useAddition) { + fullExpressionParts.add("+"); + currentResult += nextTerm.value(); + } else { + fullExpressionParts.add("-"); + currentResult -= nextTerm.value(); + } + fullExpressionParts.addAll(nextTerm.parts()); + operandsRemaining -= nextTerm.operandsUsed(); + } + return new Term(fullExpressionParts, currentResult, operandBudget - operandsRemaining); + } + + /** + * 生成一个“项”(Term)。一个项可以是简单的乘除法序列,也可以是带括号的子表达式。 + */ + private Term generateTerm(int operandsRemaining) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + if (operandsRemaining >= 2 && random.nextInt(10) < 3) { // 30%概率生成括号 + int subExpressionBudget = random.nextInt(2, operandsRemaining + 1); + Term subExpression = generateSafeExpression(subExpressionBudget); + + List parts = new ArrayList<>(); + parts.add("("); + parts.addAll(subExpression.parts()); + parts.add(")"); + return new Term(parts, subExpression.value(), subExpression.operandsUsed()); + } else { + return generateSimpleTerm(operandsRemaining); + } + } + + /** + * 生成一个仅包含乘除法的简单项。 + */ + private Term generateSimpleTerm(int operandsRemaining) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + List parts = new ArrayList<>(); + int termValue = random.nextInt(1, 21); + parts.add(String.valueOf(termValue)); + int operandsUsed = 1; + + if (operandsRemaining > 1 && random.nextBoolean()) { + if (random.nextBoolean()) { + parts.add("*"); + int multiplier = random.nextInt(1, 10); + parts.add(String.valueOf(multiplier)); + termValue *= multiplier; + } else { + parts.add("/"); + List divisors = getDivisors(termValue); + int divisor = divisors.get(random.nextInt(divisors.size())); + parts.add(String.valueOf(divisor)); + termValue /= divisor; + } + operandsUsed++; + } + return new Term(parts, termValue, operandsUsed); + } + + private List getDivisors(int number) { + List divisors = new ArrayList<>(); + for (int i = 1; i <= number; i++) { + if (number % i == 0) divisors.add(i); + } + return divisors; + } + + /** + * 生成四个选项 (1个正确,3个干扰项) + */ + private List generateOptions(int correctAnswer) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + Set options = new HashSet<>(); + options.add(String.valueOf(correctAnswer)); + + while (options.size() < 4) { + int delta = random.nextInt(1, 11); + int distractor = random.nextBoolean() ? correctAnswer + delta : correctAnswer - delta; + // 确保干扰项非负 + if (distractor >= 0) { + options.add(String.valueOf(distractor)); + } + } + List sortedOptions = new ArrayList<>(options); + Collections.shuffle(sortedOptions); + return sortedOptions; + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java b/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java new file mode 100644 index 0000000..43ecb30 --- /dev/null +++ b/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java @@ -0,0 +1,53 @@ +package com.mathgenerator.generator; + +import com.mathgenerator.model.ChoiceQuestion; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 高中选择题生成器 (已修正)。 + * 通过在初中题目的基础上添加值为整数的三角函数项,确保最终解为整数。 + */ +public class SeniorHighSchoolGenerator extends JuniorHighSchoolGenerator { + + // 只使用值为整数的三角函数 + private static final Map TRIG_TERMS = Map.of( + "sin(90°)", 1, + "cos(0°)", 1, + "tan(45°)", 1 + ); + private static final String[] TRIG_KEYS = TRIG_TERMS.keySet().toArray(new String[0]); + + @Override + public ChoiceQuestion generateSingleQuestion() { + // 1. 先生成一个保证有整数解的初中选择题 + ChoiceQuestion juniorHighQuestion = super.generateSingleQuestion(); + String juniorQuestionText = juniorHighQuestion.questionText(); + int juniorCorrectAnswer = Integer.parseInt(juniorHighQuestion.options().get(juniorHighQuestion.correctOptionIndex())); + + // 2. 随机选择一个值为整数的三角函数项 + String trigKey = TRIG_KEYS[ThreadLocalRandom.current().nextInt(TRIG_KEYS.length)]; + int trigValue = TRIG_TERMS.get(trigKey); + + // 3. 随机决定是加还是减 + boolean useAddition = ThreadLocalRandom.current().nextBoolean(); + + String finalQuestionText; + int finalCorrectAnswer; + + if (useAddition) { + finalQuestionText = "(" + juniorQuestionText + ") + " + trigKey; + finalCorrectAnswer = juniorCorrectAnswer + trigValue; + } else { + finalQuestionText = "(" + juniorQuestionText + ") - " + trigKey; + finalCorrectAnswer = juniorCorrectAnswer - trigValue; + } + + // 4. 为最终的题目生成选项 + List options = generateOptions(finalCorrectAnswer); + int correctIndex = options.indexOf(String.valueOf(finalCorrectAnswer)); + + return new ChoiceQuestion(finalQuestionText, options, correctIndex); + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/model/ChoiceQuestion.java b/src/main/java/com/mathgenerator/model/ChoiceQuestion.java new file mode 100644 index 0000000..485b2ec --- /dev/null +++ b/src/main/java/com/mathgenerator/model/ChoiceQuestion.java @@ -0,0 +1,12 @@ +package com.mathgenerator.model; + +import java.util.List; + +/** + * 选择题数据模型。 + * @param questionText 题干 + * @param options 四个选项的列表 + * @param correctOptionIndex 正确答案在列表中的索引 (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 new file mode 100644 index 0000000..8b27203 --- /dev/null +++ b/src/main/java/com/mathgenerator/model/Level.java @@ -0,0 +1,28 @@ +package com.mathgenerator.model; + +/** + * 学段枚举,用于类型安全地表示小学、初中和高中。 + */ +public enum Level { + PRIMARY("小学"), + JUNIOR_HIGH("初中"), + SENIOR_HIGH("高中"); + + private final String chineseName; + /** + * 枚举的构造函数。 + * + * @param chineseName 学段对应的中文名称。 + */ + Level(String chineseName) { + this.chineseName = chineseName; + } + /** + * 获取学段的中文名称。 + * + * @return 表示学段的中文名称字符串。 + */ + public String getChineseName() { + return chineseName; + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/model/User.java b/src/main/java/com/mathgenerator/model/User.java new file mode 100644 index 0000000..66ecbd3 --- /dev/null +++ b/src/main/java/com/mathgenerator/model/User.java @@ -0,0 +1,10 @@ +package com.mathgenerator.model; + +/** + * 用户数据记录 (Record),用于封装不可变的用户信息。 + * @param username 用户名 + * @param email 邮箱地址 (新增) + * @param password 密码 + */ +public record User(String username, String email, String password) { +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/service/PaperService.java b/src/main/java/com/mathgenerator/service/PaperService.java new file mode 100644 index 0000000..3df7045 --- /dev/null +++ b/src/main/java/com/mathgenerator/service/PaperService.java @@ -0,0 +1,68 @@ +package com.mathgenerator.service; + +import com.mathgenerator.model.User; +import com.mathgenerator.model.Level; +import com.mathgenerator.model.ChoiceQuestion; // 导入新模型 +import com.mathgenerator.service.strategy.PaperStrategy; +import com.mathgenerator.storage.FileManager; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * 试卷服务,现在处理ChoiceQuestion对象。 + */ +public class PaperService { + private final FileManager fileManager; + private final PaperStrategy paperStrategy; + + public PaperService(FileManager fileManager, PaperStrategy paperStrategy) { + this.fileManager = fileManager; + this.paperStrategy = paperStrategy; + } + + /** + * 创建一份包含选择题的试卷。 + * @param user 当前用户 + * @param count 题目数量 + * @param currentLevel 当前难度 + * @return 生成的选择题列表 + */ + 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("正在根据策略生成选择题,请稍候..."); + while (newPaper.size() < count) { + // 1. 生成的是ChoiceQuestion对象 + ChoiceQuestion question = paperStrategy.selectGenerator(currentLevel).generateSingleQuestion(); + String questionText = question.questionText(); // 提取题干用于查重 + + // 2. 使用题干进行查重 + if (!existingQuestionTexts.contains(questionText) && !generatedInSession.contains(questionText)) { + newPaper.add(question); + generatedInSession.add(questionText); + } + } + return newPaper; + } + + /** + * 将生成的试卷保存到文件。 + * @param username 用户名 + * @param paper 试卷题目列表 + */ + public void savePaper(String username, List paper) { + try { + String filePath = fileManager.savePaper(username, paper); + System.out.println("成功!" + paper.size() + "道数学题目已生成。"); + System.out.println("文件已保存至: " + filePath); + } catch (IOException e) { + System.err.println("错误:保存文件失败 - " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/service/UserService.java b/src/main/java/com/mathgenerator/service/UserService.java new file mode 100644 index 0000000..05b2409 --- /dev/null +++ b/src/main/java/com/mathgenerator/service/UserService.java @@ -0,0 +1,117 @@ +package com.mathgenerator.service; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.mathgenerator.model.User; +import java.io.FileReader; +import java.io.FileWriter; +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.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.regex.Pattern; + +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; + private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + public UserService() { + this.userDatabase = loadUsersFromFile(); + } + + private Map loadUsersFromFile() { + try { + if (Files.exists(USER_FILE_PATH) && Files.size(USER_FILE_PATH) > 0) { + try (FileReader reader = new FileReader(USER_FILE_PATH.toFile())) { + Type type = new TypeToken>() {}.getType(); + Map loadedUsers = gson.fromJson(reader, type); + return loadedUsers != null ? new ConcurrentHashMap<>(loadedUsers) : new ConcurrentHashMap<>(); + } + } + } catch (IOException e) { + System.err.println("错误:加载用户文件失败 - " + e.getMessage()); + } + return new ConcurrentHashMap<>(); + } + + private void saveUsers() { + try (FileWriter writer = new FileWriter(USER_FILE_PATH.toFile())) { + gson.toJson(this.userDatabase, writer); + } catch (IOException e) { + System.err.println("错误:保存用户文件失败 - " + e.getMessage()); + } + } + + public Optional findUserByUsername(String username) { + return Optional.ofNullable(this.userDatabase.get(username)); + } + + public Optional login(String username, String password) { + return findUserByUsername(username) + .filter(user -> user.password().equals(password)); + } + + /** + * (模拟) 发送验证码。 + * @param email 用户的邮箱 + * @return 生成的6位验证码 + */ + public String sendVerificationCode(String email) { + String code = String.format("%06d", ThreadLocalRandom.current().nextInt(100000, 1000000)); + // 在真实项目中,这里会调用邮件API。我们在此处模拟。 + System.out.println("====== 验证码模拟发送 ======"); + System.out.println("发往邮箱: " + email); + System.out.println("验证码: " + code); + System.out.println("============================"); + return code; + } + + /** + * 注册新用户。 + * @return 成功返回true, 否则返回false + */ + public boolean register(String username, String email, String password) { + if (userDatabase.containsKey(username) || + userDatabase.values().stream().anyMatch(u -> u.email().equals(email))) { + return false; // 用户名或邮箱已存在 + } + User newUser = new User(username, email, password); + userDatabase.put(username, newUser); + saveUsers(); + return true; + } + + /** + * 验证密码是否符合复杂度要求。 + * @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)) + .map(user -> { + User updatedUser = new User(user.username(), user.email(), newPassword); + userDatabase.put(username, updatedUser); + saveUsers(); + return true; + }).orElse(false); + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/service/strategy/MixedDifficultyStrategy.java b/src/main/java/com/mathgenerator/service/strategy/MixedDifficultyStrategy.java new file mode 100644 index 0000000..cc7d78f --- /dev/null +++ b/src/main/java/com/mathgenerator/service/strategy/MixedDifficultyStrategy.java @@ -0,0 +1,38 @@ +package com.mathgenerator.service.strategy; + +import com.mathgenerator.generator.*; +import com.mathgenerator.model.Level; +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(); + + @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; + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/service/strategy/PaperStrategy.java b/src/main/java/com/mathgenerator/service/strategy/PaperStrategy.java new file mode 100644 index 0000000..80d2ac4 --- /dev/null +++ b/src/main/java/com/mathgenerator/service/strategy/PaperStrategy.java @@ -0,0 +1,18 @@ +package com.mathgenerator.service.strategy; + +import com.mathgenerator.generator.QuestionGenerator; +import com.mathgenerator.model.Level; + +/** + * 试卷组合策略接口。 + * 封装了如何根据主难度来选择具体题目生成器的算法。 + */ +public interface PaperStrategy { + /** + * 根据用户选择的主难度,选择一个具体的题目生成器。 + * + * @param mainLevel 用户选择的主难度级别。 + * @return 一个根据策略选择出的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 new file mode 100644 index 0000000..a1bb4a3 --- /dev/null +++ b/src/main/java/com/mathgenerator/storage/FileManager.java @@ -0,0 +1,89 @@ +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 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.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 负责文件读写,现已支持JSON格式的ChoiceQuestion对象。 + */ +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 保存成功后的文件路径 + */ + 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 + Path filePath = userDir.resolve(fileName); + + try (FileWriter writer = new FileWriter(filePath.toFile())) { + gson.toJson(paperContent, writer); + } + return filePath.toString(); + } + + /** + * 加载指定用户的所有历史题目的题干文本,用于查重。 + * @param username 用户名 + * @return 包含所有历史题干文本的Set集合 + */ + public Set loadExistingQuestions(String username) { + Path userDir = BASE_PATH.resolve(username); + if (!Files.exists(userDir)) { + return new HashSet<>(); + } + + try (Stream stream = Files.walk(userDir)) { + return stream + .filter(file -> !Files.isDirectory(file) && file.toString().endsWith(".json")) // 只读取 .json 文件 + .flatMap(this::readQuestionsFromFile) // 使用方法引用 + .map(ChoiceQuestion::questionText) // 提取每个对象的题干文本 + .collect(Collectors.toSet()); + } catch (IOException e) { + System.err.println("错误:读取历史文件失败 - " + e.getMessage()); + return new HashSet<>(); + } + } + + /** + * 从单个JSON文件中读取并解析ChoiceQuestion对象列表。 + * @param file 要读取的单个试卷文件的路径对象 (Path)。 + * @return 一个包含该文件中所有ChoiceQuestion对象的流 (Stream)。 + */ + 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(); + } catch (IOException e) { + System.err.println("错误:读取或解析文件 " + file + " 失败 - " + e.getMessage()); + return Stream.empty(); + } + } +} \ No newline at end of file diff --git a/src/main/resources/com/mathgenerator/view/LoginView.fxml b/src/main/resources/com/mathgenerator/view/LoginView.fxml new file mode 100644 index 0000000..caad929 --- /dev/null +++ b/src/main/resources/com/mathgenerator/view/LoginView.fxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + \ 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 new file mode 100644 index 0000000..082d3fe --- /dev/null +++ b/src/main/resources/com/mathgenerator/view/RegisterView.fxml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + +