第一个大稳定版本 #2

Merged
hnu202326010318 merged 10 commits from develop into main 3 months ago

10
.gitignore vendored

@ -0,0 +1,10 @@
.idea/
out/
.vscode/
bin/
questions/
generated_papers/
target/
users.json
legal/
lib/

@ -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/ # 试卷组合策略子包
```

@ -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` 时,它向所有阅读代码的开发者传递了一个清晰的信号:**“我是一个简单、透明、不可变的数据载体”**。
这排除了该类包含复杂业务逻辑的可能性,使得代码的结构和意图更加清晰,降低了团队协作的沟通成本。

@ -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<String>` 集合用于查重。
### 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
```

@ -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

@ -0,0 +1,18 @@
# 程序运行说明
1. 环境配置java21及以上不推荐使用windows要设置更改字符否则中文字符会乱码建议使用ubuntumacos等系统
2. 在控制台输入
```bash
java -jar mathgenerator.jar
```
可以开始使用程序
3. 程序提供在windows运行的bat脚本需要可以双击运行
4. 第一次运行时在运行目录下会生成默认用户的json文件生成的试卷放在generated文件夹下
5. 注册功能的说明:注册功能是自己添加,由于项目要求登录时将用户名和密码同行隔空格输入,因此注册时的特殊情况,如密码含空格,会无法登录,但是登录不是课程考核之一,希望不要引起不必要的误会,特此说明

@ -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`** 同理,它调用父类(初中生成器)的方法,在这个“半成品”上增加自己的步骤(添加三角函数)。
* **带来的好处**
* **代码复用**:避免了在每个生成器中都重复编写基础表达式的生成逻辑。
* **结构清晰**:清晰地定义了不同难度题目之间的层级递进关系。
通过综合运用这些设计模式和原则,我们的项目不仅实现了所有功能需求,更拥有了一个专业、健壮且易于扩展的软件架构。

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mathgenerator</groupId>
<artifactId>MathGenerator</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 禁用默认的 maven-jar-plugin 生成基础JAR -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>default-jar</id>
<phase>none</phase> <!-- 移除默认的打包阶段不生成基础JAR -->
</execution>
</executions>
</plugin>
<!-- 仅保留 assembly 插件生成自定义JAR -->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<archive>
<manifest>
<mainClass>com.mathgenerator.Application</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<finalName>mathgenerator</finalName> <!-- 自定义文件名 -->
<appendAssemblyId>false</appendAssemblyId> <!-- 去掉后缀 -->
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

@ -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);
}
}

@ -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<User> 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();
}
}
}

@ -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();
}
}
}

@ -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<ChoiceQuestion> questions;
private List<Integer> 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<RadioButton> 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<RadioButton> 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();
}
}
}

@ -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();
}
}
}

@ -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();
}
}
}

@ -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<String> 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<String> 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<String> options = generateOptions(correctAnswer);
int correctIndex = options.indexOf(String.valueOf(correctAnswer));
return new ChoiceQuestion(questionText, options, correctIndex);
}
}

@ -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<String> 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<String> options = generateOptions(correctAnswer);
int correctIndex = options.indexOf(String.valueOf(correctAnswer));
return new ChoiceQuestion(questionText, options, correctIndex);
}
/**
* (13)
* @param correctAnswer
* @return
*/
protected List<String> generateOptions(int correctAnswer) {
ThreadLocalRandom random = ThreadLocalRandom.current();
Set<String> 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<String> 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<String> 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<String> 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);
}
}

@ -0,0 +1,14 @@
package com.mathgenerator.generator;
import com.mathgenerator.model.ChoiceQuestion; // 导入新模型
/**
*
*/
public interface QuestionGenerator {
/**
*
* @return ChoiceQuestion
*/
ChoiceQuestion generateSingleQuestion();
}

@ -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<String> options = generateOptions(correctAnswer);
int correctIndex = options.indexOf(String.valueOf(correctAnswer));
return new ChoiceQuestion(questionText, options, correctIndex);
}
/**
*
*/
private record Term(List<String> parts, int value, int operandsUsed) {}
/**
*
*/
private Term generateSafeExpression(int operandBudget) {
ThreadLocalRandom random = ThreadLocalRandom.current();
List<String> 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<String> 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<String> 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<Integer> 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<Integer> getDivisors(int number) {
List<Integer> divisors = new ArrayList<>();
for (int i = 1; i <= number; i++) {
if (number % i == 0) divisors.add(i);
}
return divisors;
}
/**
* (13)
*/
private List<String> generateOptions(int correctAnswer) {
ThreadLocalRandom random = ThreadLocalRandom.current();
Set<String> 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<String> sortedOptions = new ArrayList<>(options);
Collections.shuffle(sortedOptions);
return sortedOptions;
}
}

@ -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<String, Integer> 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<String> options = generateOptions(finalCorrectAnswer);
int correctIndex = options.indexOf(String.valueOf(finalCorrectAnswer));
return new ChoiceQuestion(finalQuestionText, options, correctIndex);
}
}

@ -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<String> options, int correctOptionIndex) {
}

@ -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;
}
}

@ -0,0 +1,10 @@
package com.mathgenerator.model;
/**
* (Record)
* @param username
* @param email ()
* @param password
*/
public record User(String username, String email, String password) {
}

@ -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<ChoiceQuestion> createPaper(User user, int count, Level currentLevel) {
// 查重集合现在存储题干字符串
Set<String> existingQuestionTexts = fileManager.loadExistingQuestions(user.username());
List<ChoiceQuestion> newPaper = new ArrayList<>();
Set<String> 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<ChoiceQuestion> 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());
}
}
}

@ -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<String, User> userDatabase;
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
public UserService() {
this.userDatabase = loadUsersFromFile();
}
private Map<String, User> 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<Map<String, User>>() {}.getType();
Map<String, User> 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<User> findUserByUsername(String username) {
return Optional.ofNullable(this.userDatabase.get(username));
}
public Optional<User> 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);
}
}

@ -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;
}
};
}
}

@ -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);
}

@ -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;
/**
* JSONChoiceQuestion
*/
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<ChoiceQuestion> 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<String> loadExistingQuestions(String username) {
Path userDir = BASE_PATH.resolve(username);
if (!Files.exists(userDir)) {
return new HashSet<>();
}
try (Stream<Path> 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<>();
}
}
/**
* JSONChoiceQuestion
* @param file (Path)
* @return ChoiceQuestion (Stream)
*/
private Stream<ChoiceQuestion> readQuestionsFromFile(Path file) {
try (FileReader reader = new FileReader(file.toFile())) {
Type listType = new TypeToken<List<ChoiceQuestion>>() {}.getType();
List<ChoiceQuestion> questions = gson.fromJson(reader, listType);
return questions != null ? questions.stream() : Stream.empty();
} catch (IOException e) {
System.err.println("错误:读取或解析文件 " + file + " 失败 - " + e.getMessage());
return Stream.empty();
}
}
}

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="350.0" prefWidth="400.0" spacing="15.0" style="-fx-background-color: #f4f4f4;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.mathgenerator.controller.LoginController">
<children>
<Label text="用户登录">
<font>
<Font name="System Bold" size="24.0" />
</font>
</Label>
<TextField fx:id="usernameField" maxWidth="250.0" promptText="用户名" />
<PasswordField fx:id="passwordField" maxWidth="250.0" promptText="密码" />
<Button fx:id="loginButton" mnemonicParsing="false" onAction="#handleLoginButtonAction" prefWidth="100.0" text="登录" />
<Button fx:id="registerButton" mnemonicParsing="false" onAction="#handleRegisterButtonAction" style="-fx-background-color: #6c757d;" text="注册新用户" textFill="WHITE" />
<Label fx:id="statusLabel" textFill="RED">
<VBox.margin>
<Insets top="10.0" />
</VBox.margin>
</Label>
</children>
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
</VBox>

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="500.0" spacing="20.0" style="-fx-background-color: #f4f4f4;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.mathgenerator.controller.MainMenuController">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
<children>
<Label fx:id="welcomeLabel" text="欢迎, [用户名]!">
<font>
<Font name="System Bold" size="28.0" />
</font>
</Label>
<VBox alignment="CENTER" spacing="15.0" VBox.vgrow="ALWAYS">
<children>
<Label text="请选择题目难度">
<font>
<Font size="18.0" />
</font>
</Label>
<Button fx:id="primaryButton" mnemonicParsing="false" onAction="#handlePrimaryAction" prefHeight="50.0" prefWidth="200.0" text="小学" />
<Button fx:id="juniorHighButton" mnemonicParsing="false" onAction="#handleJuniorHighAction" prefHeight="50.0" prefWidth="200.0" text="初中" />
<Button fx:id="seniorHighButton" mnemonicParsing="false" onAction="#handleSeniorHighAction" prefHeight="50.0" prefWidth="200.0" text="高中" />
<HBox alignment="CENTER" spacing="10.0">
<children>
<Label text="题目数量:" />
<TextField fx:id="questionCountField" prefWidth="80.0" text="10" />
</children>
<VBox.margin>
<Insets top="20.0" />
</VBox.margin>
</HBox>
<Label fx:id="statusLabel" textFill="RED" />
</children>
</VBox>
<HBox alignment="CENTER_RIGHT" spacing="10.0">
<children>
<Button fx:id="changePasswordButton" mnemonicParsing="false" onAction="#handleChangePasswordAction" text="修改密码" />
<Button fx:id="logoutButton" mnemonicParsing="false" onAction="#handleLogoutAction" style="-fx-background-color: #dc3545;" text="退出登录" textFill="WHITE" />
</children>
</HBox>
</children>
</VBox>

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?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.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="500.0" prefWidth="600.0" spacing="20.0" style="-fx-background-color: #f4f4f4;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.mathgenerator.controller.QuizController">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
<children>
<Label fx:id="questionNumberLabel" text="第 1 / 10 题">
<font>
<Font size="18.0" />
</font>
</Label>
<ProgressBar fx:id="progressBar" prefWidth="560.0" progress="0.1" />
<Label fx:id="questionTextLabel" style="-fx-font-weight: bold;" text="题目内容: 1 + 1 = ?" wrapText="true">
<font>
<Font size="24.0" />
</font>
<VBox.margin>
<Insets top="20.0" />
</VBox.margin>
</Label>
<VBox fx:id="optionsVBox" alignment="CENTER_LEFT" spacing="15.0">
<children>
<RadioButton fx:id="option1" mnemonicParsing="false" text="选项1">
<toggleGroup>
<ToggleGroup fx:id="optionsGroup" />
</toggleGroup>
<font>
<Font size="16.0" />
</font>
</RadioButton>
<RadioButton fx:id="option2" mnemonicParsing="false" text="选项2" toggleGroup="$optionsGroup">
<font>
<Font size="16.0" />
</font>
</RadioButton>
<RadioButton fx:id="option3" mnemonicParsing="false" text="选项3" toggleGroup="$optionsGroup">
<font>
<Font size="16.0" />
</font>
</RadioButton>
<RadioButton fx:id="option4" mnemonicParsing="false" text="选项4" toggleGroup="$optionsGroup">
<font>
<Font size="16.0" />
</font>
</RadioButton>
</children>
<VBox.margin>
<Insets left="50.0" top="20.0" />
</VBox.margin>
</VBox>
<Button fx:id="submitButton" mnemonicParsing="false" onAction="#handleSubmitButtonAction" prefHeight="40.0" prefWidth="150.0" text="提交答案">
<VBox.margin>
<Insets top="30.0" />
</VBox.margin>
</Button>
<Label fx:id="statusLabel" textFill="RED" />
</children>
</VBox>

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="500.0" prefWidth="450.0" spacing="15.0" style="-fx-background-color: #f4f4f4;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.mathgenerator.controller.RegisterController">
<children>
<Label text="新用户注册">
<font>
<Font name="System Bold" size="24.0" />
</font>
</Label>
<TextField fx:id="usernameField" maxWidth="300.0" promptText="用户名" />
<TextField fx:id="emailField" maxWidth="300.0" promptText="邮箱地址" />
<HBox alignment="CENTER" maxWidth="300.0" spacing="10.0">
<children>
<TextField fx:id="verificationCodeField" promptText="邮箱验证码" HBox.hgrow="ALWAYS" />
<Button fx:id="sendCodeButton" mnemonicParsing="false" onAction="#handleSendCodeAction" text="发送验证码" />
</children>
</HBox>
<PasswordField fx:id="passwordField" maxWidth="300.0" promptText="设置密码 (6-10位, 含大小写字母和数字)" />
<PasswordField fx:id="confirmPasswordField" maxWidth="300.0" promptText="确认密码" />
<Button fx:id="registerButton" mnemonicParsing="false" onAction="#handleRegisterAction" prefWidth="120.0" text="确认注册" />
<Button fx:id="backToLoginButton" mnemonicParsing="false" onAction="#handleBackToLoginAction" style="-fx-background-color: #6c757d;" text="返回登录" textFill="WHITE" />
<Label fx:id="statusLabel" textFill="RED" wrapText="true">
<VBox.margin>
<Insets top="10.0" />
</VBox.margin>
</Label>
</children>
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
</VBox>

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="500.0" spacing="25.0" style="-fx-background-color: #f4f4f4;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.mathgenerator.controller.ScoreController">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
<children>
<Label text="答题完成!">
<font>
<Font name="System Bold" size="36.0" />
</font>
</Label>
<VBox alignment="CENTER" spacing="10.0">
<children>
<Label text="您的最终得分是:">
<font>
<Font size="18.0" />
</font>
</Label>
<Label fx:id="scoreLabel" text="100.00" textFill="#007bff">
<font>
<Font name="System Bold" size="64.0" />
</font>
</Label>
</children>
</VBox>
<Label fx:id="resultMessageLabel" text="太棒了!你答对了所有题目!" />
<VBox alignment="CENTER" spacing="15.0">
<VBox.margin>
<Insets top="20.0" />
</VBox.margin>
<children>
<Button fx:id="tryAgainButton" mnemonicParsing="false" onAction="#handleTryAgainAction" prefWidth="150.0" text="再做一组" />
<Button fx:id="logoutButton" mnemonicParsing="false" onAction="#handleLogoutAction" style="-fx-background-color: #6c757d;" text="退出登录" textFill="WHITE" />
</children>
</VBox>
</children>
</VBox>
Loading…
Cancel
Save