diff --git a/pom.xml b/pom.xml
index 4d69a1a..5b323ad 100644
--- a/pom.xml
+++ b/pom.xml
@@ -61,25 +61,21 @@
org.apache.maven.plugins
- maven-assembly-plugin
- 3.6.0
-
-
-
- com.mathgenerator.MainApplication
-
-
-
- jar-with-dependencies
-
-
+ maven-shade-plugin
+ 3.2.4
- make-assembly
package
- single
+ shade
+
+
+
+ com.mathgenerator.Launcher
+
+
+
diff --git a/src/main/java/com/mathgenerator/Launcher.java b/src/main/java/com/mathgenerator/Launcher.java
new file mode 100644
index 0000000..6773f2d
--- /dev/null
+++ b/src/main/java/com/mathgenerator/Launcher.java
@@ -0,0 +1,22 @@
+package com.mathgenerator;
+
+/**
+ * 应用程序启动器类。
+ *
+ * 这个类的唯一目的是为了解决在使用某些构建工具(如 Maven Shade Plugin)将 JavaFX 应用程序
+ * 打包成一个可执行的 "fat JAR" 文件时可能出现的运行时问题。
+ *
+ * 它通过一个标准的 {@code main} 方法来调用 {@link MainApplication#main(String[])} 方法,
+ * 充当了 JavaFX 应用程序的实际入口点。
+ */
+public class Launcher {
+
+ /**
+ * 程序的主入口点。
+ *
+ * @param args 传递给应用程序的命令行参数。
+ */
+ public static void main(String[] args) {
+ MainApplication.main(args);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/mathgenerator/MainApplication.java b/src/main/java/com/mathgenerator/MainApplication.java
index cb6f216..a93e940 100644
--- a/src/main/java/com/mathgenerator/MainApplication.java
+++ b/src/main/java/com/mathgenerator/MainApplication.java
@@ -4,21 +4,56 @@ import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
+import javafx.scene.image.Image;
import javafx.stage.Stage;
import java.io.IOException;
+/**
+ * JavaFX 应用程序的主类。
+ *
+ * 该类继承自 {@link Application},是整个 JavaFX 应用的生命周期入口。
+ * 它负责初始化主舞台 (Stage),加载初始视图 (LoginView.fxml),设置窗口标题、
+ * 图标和样式表,并最终显示应用程序窗口。
+ */
public class MainApplication extends Application {
+ /**
+ * JavaFX 应用程序的启动方法。
+ *
+ * 当应用启动时,JavaFX 平台会调用此方法。它负责配置和显示主窗口。
+ *
+ * @param primaryStage 由 JavaFX 平台自动创建和传入的主舞台对象。
+ * @throws IOException 如果在加载 FXML 文件时发生 I/O 错误。
+ */
@Override
public void start(Stage primaryStage) throws IOException {
- // 启动时加载登录界面
Parent root = FXMLLoader.load(getClass().getResource("/com/mathgenerator/view/LoginView.fxml"));
+
+ Scene scene = new Scene(root);
+ scene.getStylesheets().add(getClass().getResource("/com/mathgenerator/styles/styles.css").toExternalForm());
+
primaryStage.setTitle("中小学数学学习软件");
- primaryStage.setScene(new Scene(root));
+
+ try {
+ Image appIcon = new Image(getClass().getResourceAsStream("/com/mathgenerator/images/icon.png"));
+ primaryStage.getIcons().add(appIcon);
+ } catch (Exception e) {
+ System.err.println("错误:无法加载应用程序图标!请检查 'icon.png' 是否在 images 文件夹中。");
+ e.printStackTrace();
+ }
+
+ primaryStage.setScene(scene);
primaryStage.setResizable(false);
primaryStage.show();
}
+ /**
+ * 应用程序的静态 main 方法。
+ *
+ * 这是 Java 程序的标准入口点,它通过调用 {@link #launch(String...)} 来启动 JavaFX 应用程序。
+ *
+ * @param args 传递给应用程序的命令行参数。
+ */
public static void main(String[] args) {
launch(args);
}
diff --git a/src/main/java/com/mathgenerator/controller/ChangePasswordController.java b/src/main/java/com/mathgenerator/controller/ChangePasswordController.java
index ceab172..6b959a6 100644
--- a/src/main/java/com/mathgenerator/controller/ChangePasswordController.java
+++ b/src/main/java/com/mathgenerator/controller/ChangePasswordController.java
@@ -13,6 +13,10 @@ import javafx.scene.control.PasswordField;
import javafx.stage.Stage;
import java.io.IOException;
+/**
+ * “修改密码” 视图 (ChangePasswordView.fxml) 的 FXML 控制器类。
+ * 该类负责处理已登录用户修改自己密码的逻辑。
+ */
public class ChangePasswordController {
private final UserService userService = new UserService();
@@ -26,51 +30,96 @@ public class ChangePasswordController {
@FXML private Label statusLabel;
/**
- * 初始化控制器,接收当前用户信息
+ * 初始化控制器,并从主菜单接收当前登录的用户信息。
+ * 这个方法在 FXML 文件加载完成后被调用。
+ *
+ * @param user 当前登录的 User 对象。
*/
public void initData(User user) {
this.currentUser = user;
}
+ /**
+ * 处理“确认修改”按钮的点击事件。
+ * 该方法会验证用户输入的密码,并调用用户服务来执行密码修改操作。
+ *
+ * @param event 由按钮点击触发的 ActionEvent 事件。
+ */
@FXML
private void handleConfirmAction(ActionEvent event) {
- // 1. 获取输入
String oldPassword = oldPasswordField.getText();
String newPassword = newPasswordField.getText();
String confirmNewPassword = confirmNewPasswordField.getText();
- // 2. 输入校验
if (oldPassword.isEmpty() || newPassword.isEmpty() || confirmNewPassword.isEmpty()) {
- statusLabel.setText("所有密码字段都不能为空!");
+ showStatusMessage("所有密码字段都不能为空!", true);
return;
}
if (!newPassword.equals(confirmNewPassword)) {
- statusLabel.setText("两次输入的新密码不匹配!");
+ showStatusMessage("两次输入的新密码不匹配!", true);
+ return;
+ }
+ // 新增校验:新密码不能与当前密码相同
+ if (oldPassword.equals(newPassword)) {
+ showStatusMessage("新密码不能与当前密码相同!", true);
return;
}
if (!UserService.isPasswordValid(newPassword)) {
- statusLabel.setText("新密码格式错误!必须为6-10位,且包含大小写字母和数字。");
+ showStatusMessage("新密码格式错误!必须为6-10位,且包含大小写字母和数字。", true);
return;
}
-
// 3. 调用后端服务修改密码
boolean success = userService.changePassword(
currentUser.username(),
oldPassword,
newPassword
);
-
// 4. 更新UI反馈
if (success) {
- statusLabel.setText("密码修改成功!请返回主菜单。");
+ showStatusMessage("密码修改成功!请返回主菜单。", true);
confirmButton.setDisable(true); // 防止重复点击
} else {
- statusLabel.setText("修改失败:当前密码错误。");
+ showStatusMessage("修改失败:当前密码错误。", true);
+ }
+ }
+
+ /**
+ * 在状态标签 (statusLabel) 中显示一条消息,并更新其样式。
+ *
+ * @param message 要显示的消息文本。
+ * @param hasContent 一个布尔值,指示消息是否为空,用于确定应用何种样式。
+ */
+ private void showStatusMessage(String message, boolean hasContent) {
+ statusLabel.setText(message);
+ updateStatusLabelStyle(hasContent);
+ }
+
+ /**
+ * 根据状态标签是否有内容来更新其 CSS 样式。
+ *
+ * @param hasContent 如果为 true,则应用包含文本内容的样式;否则,应用空标签的样式。
+ */
+ private void updateStatusLabelStyle(boolean hasContent) {
+ if (hasContent) {
+ // 移除空样式,添加有内容样式
+ statusLabel.getStyleClass().removeAll("password-status-label");
+ if (!statusLabel.getStyleClass().contains("password-status-label-with-text")) {
+ statusLabel.getStyleClass().add("password-status-label-with-text");
+ }
+ } else {
+ // 移除有内容样式,添加空样式
+ statusLabel.getStyleClass().removeAll("password-status-label-with-text");
+ if (!statusLabel.getStyleClass().contains("password-status-label")) {
+ statusLabel.getStyleClass().add("password-status-label");
+ }
}
}
/**
- * 处理返回按钮事件,返回主菜单
+ * 处理“返回主菜单”按钮的点击事件。
+ * 导航用户返回到主菜单界面。
+ *
+ * @param event 由按钮点击触发的 ActionEvent 事件。
*/
@FXML
private void handleBackAction(ActionEvent event) {
diff --git a/src/main/java/com/mathgenerator/controller/LoginController.java b/src/main/java/com/mathgenerator/controller/LoginController.java
index 264c0c0..0e65ce7 100644
--- a/src/main/java/com/mathgenerator/controller/LoginController.java
+++ b/src/main/java/com/mathgenerator/controller/LoginController.java
@@ -16,6 +16,11 @@ import javafx.stage.Stage;
import java.io.IOException;
import java.util.Optional;
import com.mathgenerator.util.ValidationUtils;
+
+/**
+ * “登录”视图 (LoginView.fxml) 的 FXML 控制器类。
+ * 负责处理用户身份验证,并导航到注册界面或主菜单。
+ */
public class LoginController {
// 依赖注入后端服务
@@ -38,8 +43,10 @@ public class LoginController {
private Label statusLabel;
/**
- * 处理登录按钮点击事件。
- * @param event 事件对象
+ * 处理“登录”按钮的点击事件。
+ * 该方法会验证用户的输入,并尝试登录。如果成功,则导航到主菜单界面。
+ *
+ * @param event 由按钮点击触发的 ActionEvent 事件。
*/
@FXML
private void handleLoginButtonAction(ActionEvent event) {
@@ -69,8 +76,10 @@ public class LoginController {
}
/**
- * 处理注册按钮点击事件,跳转到注册界面。
- * @param event 事件对象
+ * 处理“注册”按钮的点击事件。
+ * 导航用户到注册界面。
+ *
+ * @param event 由按钮点击触发的 ActionEvent 事件。
*/
@FXML
private void handleRegisterButtonAction(ActionEvent event) {
@@ -78,8 +87,10 @@ public class LoginController {
}
/**
- * 加载主菜单界面,并传递用户信息。
- * @param user 登录成功的用户对象
+ * 在用户成功登录后加载主菜单界面。
+ * 此方法会将登录用户的完整信息传递给 MainMenuController。
+ *
+ * @param user 成功登录的用户的 User 对象。
*/
private void loadMainMenu(User user) {
try {
@@ -104,8 +115,9 @@ public class LoginController {
}
/**
- * 切换到简单场景的辅助方法(如注册页)。
- * @param fxmlPath FXML文件的路径
+ * 一个辅助工具方法,用于在当前窗口加载一个新的场景。
+ *
+ * @param fxmlPath 要加载的场景的 FXML 文件路径。
*/
private void loadScene(String fxmlPath) {
try {
diff --git a/src/main/java/com/mathgenerator/controller/MainMenuController.java b/src/main/java/com/mathgenerator/controller/MainMenuController.java
index 60f7930..8af25e8 100644
--- a/src/main/java/com/mathgenerator/controller/MainMenuController.java
+++ b/src/main/java/com/mathgenerator/controller/MainMenuController.java
@@ -13,6 +13,11 @@ import javafx.scene.control.TextField;
import javafx.stage.Stage;
import java.io.IOException;
+
+/**
+ * “主菜单”视图 (MainMenuView.fxml) 的 FXML 控制器类。
+ * 用户登录后进入此界面,可以选择题目难度、数量,并开始答题或进行其他操作。
+ */
public class MainMenuController {
private User currentUser;
@@ -23,68 +28,109 @@ public class MainMenuController {
@FXML private Button logoutButton;
/**
- * 初始化控制器,接收登录成功的用户信息。
- * @param user 当前登录的用户
+ * 初始化控制器,并接收登录成功的用户信息。
+ *
+ * @param user 当前登录的用户对象。
*/
public void initData(User user) {
this.currentUser = user;
- welcomeLabel.setText("欢迎, " + currentUser.username() + "!");
+ welcomeLabel.setText("🎉 欢迎, " + currentUser.username() + "!"); // 添加图标
+ // 初始化状态标签
+ clearStatus();
}
+ /**
+ * 处理选择“小学”难度按钮的事件。
+ *
+ * @param event 事件对象。
+ */
@FXML
private void handlePrimaryAction(ActionEvent event) {
startQuiz(Level.PRIMARY);
}
+ /**
+ * 处理选择“初中”难度按钮的事件。
+ *
+ * @param event 事件对象。
+ */
@FXML
private void handleJuniorHighAction(ActionEvent event) {
startQuiz(Level.JUNIOR_HIGH);
}
+ /**
+ * 处理选择“高中”难度按钮的事件。
+ *
+ * @param event 事件对象。
+ */
@FXML
private void handleSeniorHighAction(ActionEvent event) {
startQuiz(Level.SENIOR_HIGH);
}
+ /**
+ * 处理“修改密码”按钮的点击事件,导航到修改密码界面。
+ *
+ * @param event 事件对象。
+ */
@FXML
private void handleChangePasswordAction(ActionEvent event) {
try {
- // 1\. 加载 FXML 文件
+ // 1. 加载 FXML 文件
FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/ChangePasswordView.fxml"));
Parent root = loader.load();
- // 2\. 获取新界面的控制器
+ // 2. 获取新界面的控制器
ChangePasswordController controller = loader.getController();
- // 3\. 调用控制器的方法,传递当前用户信息
+ // 3. 调用控制器的方法,传递当前用户信息
controller.initData(currentUser);
- // 4\. 显示新场景
+ // 4. 显示新场景
Stage stage = (Stage) logoutButton.getScene().getWindow();
stage.setScene(new Scene(root));
stage.setTitle("修改密码");
} catch (IOException e) {
e.printStackTrace();
+ setErrorStatus("加载修改密码界面失败!");
}
}
- @FXML
+
+ /**
+ * 处理“退出登录”按钮的点击事件,返回到登录界面。
+ *
+ * @param event 事件对象。
+ */
+ @FXML
private void handleLogoutAction(ActionEvent event) {
// 跳转回登录界面
loadScene("/com/mathgenerator/view/LoginView.fxml");
}
/**
- * 验证输入并准备开始答题。
- * @param level 选择的难度
+ * 验证用户输入的题目数量,并准备开始答题。
+ * 如果输入有效,则加载答题界面并传递相关数据。
+ *
+ * @param level 用户选择的题目难度级别。
*/
private void startQuiz(Level level) {
try {
- int count = Integer.parseInt(questionCountField.getText());
- if (count < 1 || count > 50) {
- statusLabel.setText("题目数量必须在 1 到 50 之间!");
+ String countText = questionCountField.getText().trim();
+
+ // 检查是否为空
+ if (countText.isEmpty()) {
+ setErrorStatus("请输入题目数量!");
return;
}
-
+ int count = Integer.parseInt(countText);
+ // 修改范围检查:从1-50改为10-30
+ if (count < 10 || count > 30) {
+ setErrorStatus("题目数量必须在 10 到 30 之间!");
+ return;
+ }
+ // 清除状态信息
+ clearStatus();
// 加载答题界面,并传递数据
FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/QuizView.fxml"));
Parent root = loader.load();
@@ -97,12 +143,36 @@ public class MainMenuController {
stage.setTitle(level.getChineseName() + " - 答题中");
} catch (NumberFormatException e) {
- statusLabel.setText("请输入有效的题目数量!");
+ setErrorStatus("请输入有效的数字!");
} catch (IOException e) {
e.printStackTrace();
+ setErrorStatus("加载答题界面失败,请重试!");
}
}
+ /**
+ * 在状态标签中设置一条错误消息。
+ *
+ * @param message 要显示的错误消息文本。
+ */
+ private void setErrorStatus(String message) {
+ statusLabel.setText(message);
+ statusLabel.getStyleClass().setAll("status-label");
+ }
+
+ /**
+ * 清除状态标签中的任何消息。
+ */
+ private void clearStatus() {
+ statusLabel.setText("");
+ statusLabel.getStyleClass().clear();
+ }
+
+ /**
+ * 一个辅助工具方法,用于加载并切换到新的场景。
+ *
+ * @param fxmlPath 要加载的 FXML 文件的路径。
+ */
private void loadScene(String fxmlPath) {
try {
Stage stage = (Stage) logoutButton.getScene().getWindow();
diff --git a/src/main/java/com/mathgenerator/controller/QuizController.java b/src/main/java/com/mathgenerator/controller/QuizController.java
index d2c7e0d..2b948bd 100644
--- a/src/main/java/com/mathgenerator/controller/QuizController.java
+++ b/src/main/java/com/mathgenerator/controller/QuizController.java
@@ -21,18 +21,18 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
+/**
+ * “答题”视图 (QuizView.fxml) 的 FXML 控制器类。
+ * 负责展示题目、接收用户答案、处理答题流程,并在结束后跳转到分数界面。
+ */
public class QuizController {
- // --- 后端服务 ---
private final PaperService paperService;
-
- // --- 答题状态 ---
private User currentUser;
private List questions;
private List userAnswers = new ArrayList<>();
private int currentQuestionIndex = 0;
- // --- FXML 控件 ---
@FXML private Label questionNumberLabel;
@FXML private ProgressBar progressBar;
@FXML private Label questionTextLabel;
@@ -42,34 +42,32 @@ public class QuizController {
@FXML private Label statusLabel;
/**
- * 构造函数,初始化后端服务
+ * 构造函数。
+ * 在这里初始化后端服务,如 PaperService。
*/
public QuizController() {
- // 这是依赖注入的一种简化形式,在真实项目中会使用框架管理
FileManager fileManager = new FileManager();
MixedDifficultyStrategy strategy = new MixedDifficultyStrategy();
this.paperService = new PaperService(fileManager, strategy);
}
/**
- * 接收从主菜单传递过来的数据,并开始答题
+ * 初始化控制器,从主菜单接收用户、难度和题目数量,并开始答题。
+ *
+ * @param user 当前登录的用户。
+ * @param level 选择的题目难度。
+ * @param questionCount 生成的题目数量。
*/
public void initData(User user, Level level, int questionCount) {
this.currentUser = user;
-
- // 1. 调用后端服务生成题目
this.questions = paperService.createPaper(user, questionCount, level);
-
- // --- 核心修改在这里 ---
- // 2. 立即调用后端服务,在后台自动保存生成的试卷
paperService.savePaper(user.username(), this.questions);
-
- // 3. 正常显示第一道题
displayCurrentQuestion();
}
/**
- * 显示当前的题目和选项 (已更新,增加ABCD前缀)
+ * 在界面上显示当前的题目和选项。
+ * 同时更新进度条和题目编号。
*/
private void displayCurrentQuestion() {
ChoiceQuestion currentQuestion = questions.get(currentQuestionIndex);
@@ -79,14 +77,13 @@ public class QuizController {
questionTextLabel.setText(currentQuestion.questionText());
List radioButtons = List.of(option1, option2, option3, option4);
- String[] prefixes = {"A. ", "B. ", "C. ", "D. "}; // 定义选项前缀
+ String[] prefixes = {"A. ", "B. ", "C. ", "D. "};
for (int i = 0; i < radioButtons.size(); i++) {
- // 将前缀和选项文本结合起来
radioButtons.get(i).setText(prefixes[i] + currentQuestion.options().get(i));
}
- optionsGroup.selectToggle(null); // 清除上一次的选择
- statusLabel.setText(""); // 清除状态提示
+ optionsGroup.selectToggle(null);
+ updateStatusLabelStyle(false);
if (currentQuestionIndex == questions.size() - 1) {
submitButton.setText("完成答题");
@@ -94,32 +91,54 @@ public class QuizController {
}
/**
- * 处理提交按钮的点击事件
+ * 处理“提交答案”按钮的点击事件。
+ * 记录用户的选择,然后切换到下一题或结束答题。
+ *
+ * @param event 事件对象。
*/
@FXML
private void handleSubmitButtonAction(ActionEvent event) {
RadioButton selectedRadioButton = (RadioButton) optionsGroup.getSelectedToggle();
if (selectedRadioButton == null) {
statusLabel.setText("请选择一个答案!");
+ updateStatusLabelStyle(true);
return;
}
- // 记录用户答案的索引
+ updateStatusLabelStyle(false);
+
List radioButtons = List.of(option1, option2, option3, option4);
userAnswers.add(radioButtons.indexOf(selectedRadioButton));
- // 移动到下一题或结束答题
currentQuestionIndex++;
if (currentQuestionIndex < questions.size()) {
displayCurrentQuestion();
} else {
- // 答题结束,计算分数并跳转到分数界面
calculateScoreAndShowResults();
}
}
/**
- * 计算分数并准备跳转到结果页面
+ * 根据状态标签是否有内容来更新其 CSS 样式。
+ *
+ * @param hasContent 如果为 true,应用带文本的样式;否则,应用空标签的样式。
+ */
+ private void updateStatusLabelStyle(boolean hasContent) {
+ if (hasContent) {
+ statusLabel.getStyleClass().removeAll("quiz-status-label-empty");
+ if (!statusLabel.getStyleClass().contains("quiz-status-label-with-text")) {
+ statusLabel.getStyleClass().add("quiz-status-label-with-text");
+ }
+ } else {
+ statusLabel.getStyleClass().removeAll("quiz-status-label-with-text");
+ if (!statusLabel.getStyleClass().contains("quiz-status-label-empty")) {
+ statusLabel.getStyleClass().add("quiz-status-label-empty");
+ }
+ }
+ }
+
+ /**
+ * 在所有题目回答完毕后,计算最终得分并导航到分数显示界面。
*/
private void calculateScoreAndShowResults() {
int correctCount = 0;
@@ -130,22 +149,19 @@ public class QuizController {
}
double score = (double) correctCount / questions.size() * 100;
- // 禁用当前页面的按钮
submitButton.setDisable(true);
statusLabel.setText("答题已完成,正在为您计算分数...");
+ updateStatusLabelStyle(true);
- // 加载分数界面并传递数据
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/ScoreView.fxml"));
Parent root = loader.load();
-
ScoreController controller = loader.getController();
- controller.initData(currentUser, score); // 将用户和分数传递过去
+ controller.initData(currentUser, score);
Stage stage = (Stage) submitButton.getScene().getWindow();
stage.setScene(new Scene(root));
stage.setTitle("答题结果");
-
} catch (IOException e) {
e.printStackTrace();
}
diff --git a/src/main/java/com/mathgenerator/controller/RegisterController.java b/src/main/java/com/mathgenerator/controller/RegisterController.java
index d7fd31a..8f98a43 100644
--- a/src/main/java/com/mathgenerator/controller/RegisterController.java
+++ b/src/main/java/com/mathgenerator/controller/RegisterController.java
@@ -1,6 +1,9 @@
package com.mathgenerator.controller;
import com.mathgenerator.service.UserService;
+import com.mathgenerator.util.ValidationUtils;
+import javafx.animation.KeyFrame;
+import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
@@ -11,12 +14,20 @@ import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
+import javafx.util.Duration;
+
import java.io.IOException;
-import com.mathgenerator.util.ValidationUtils;
+
+/**
+ * “注册”视图 (RegisterView.fxml) 的 FXML 控制器类。
+ * 负责处理新用户的注册流程,包括发送验证码和验证用户输入。
+ */
public class RegisterController {
private final UserService userService = new UserService();
- private String sentCode; // 用于存储已发送的验证码
+ private String sentCode;
+ private Timeline countdownTimeline;
+ private int countdownSeconds = 60;
@FXML private TextField usernameField;
@FXML private TextField emailField;
@@ -28,65 +39,123 @@ public class RegisterController {
@FXML private Button backToLoginButton;
@FXML private Label statusLabel;
+ /**
+ * 处理“发送验证码”按钮的点击事件。
+ * 验证邮箱格式,并调用服务发送验证码邮件,然后启动一个60秒的冷却倒计时。
+ *
+ * @param event 事件对象。
+ */
@FXML
private void handleSendCodeAction(ActionEvent event) {
String email = emailField.getText();
- if (email.isEmpty() || !email.contains("@")) {
- statusLabel.setText("请输入一个有效的邮箱地址!");
+ if (!ValidationUtils.isEmailValid(email)) {
+ showStatusMessage("请输入一个有效的邮箱地址!", true);
return;
}
- // 调用后端服务发送验证码
this.sentCode = userService.sendVerificationCode(email);
- // 处理发送结果
if (this.sentCode != null) {
- statusLabel.setText("验证码已成功发送,请查收您的邮箱。");
- sendCodeButton.setDisable(true); // 防止重复点击
+ showStatusMessage("验证码已成功发送,请查收您的邮箱。", true);
+ startCountdown();
} else {
- statusLabel.setText("验证码发送失败!请检查配置或联系管理员。");
+ showStatusMessage("验证码发送失败!请检查配置或联系管理员。", true);
}
}
+ /**
+ * 启动发送验证码按钮的60秒冷却倒计时。
+ * 在倒计时期间,按钮将被禁用。
+ */
+ private void startCountdown() {
+ sendCodeButton.setDisable(true);
+ countdownTimeline = new Timeline(new KeyFrame(Duration.seconds(1), e -> {
+ countdownSeconds--;
+ sendCodeButton.setText(countdownSeconds + "s后重试");
+ if (countdownSeconds <= 0) {
+ countdownTimeline.stop();
+ sendCodeButton.setDisable(false);
+ sendCodeButton.setText("发送验证码");
+ countdownSeconds = 60;
+ }
+ }));
+ countdownTimeline.setCycleCount(60);
+ countdownTimeline.play();
+ }
+
+ /**
+ * 处理“确认注册”按钮的点击事件。
+ * 验证所有输入字段,如果有效,则调用服务注册新用户,并导航到设置密码界面。
+ *
+ * @param event 事件对象。
+ */
@FXML
private void handleRegisterAction(ActionEvent event) {
- // 1. 字段校验 (已简化,不再校验密码)
String username = usernameField.getText();
String email = emailField.getText();
if (!ValidationUtils.isUsernameValid(username) || !ValidationUtils.isEmailValid(email) ||
verificationCodeField.getText().isEmpty()) {
- statusLabel.setText("所有字段都不能为空且格式正确!");
+ showStatusMessage("所有字段都不能为空且格式正确!", true);
return;
}
+
if (this.sentCode == null || !this.sentCode.equals(verificationCodeField.getText())) {
- statusLabel.setText("验证码错误!");
+ showStatusMessage("验证码错误!", true);
return;
}
- // 2. 调用后端服务进行无密码注册
boolean success = userService.register(username, email);
- // 3. 根据结果更新UI或跳转
if (success) {
- statusLabel.setText("注册成功!请设置您的密码。");
- // 成功后,加载设置密码界面,并传递用户名
+ showStatusMessage("注册成功!请设置您的密码。", true);
loadSetPasswordScene(username);
} else {
- statusLabel.setText("注册失败:用户名或邮箱已被占用。");
+ showStatusMessage("注册失败:用户名或邮箱已被占用。", true);
}
}
/**
- * (新增) 加载设置密码界面,并传递用户名。
+ * 在状态标签中显示一条消息,并根据内容更新其样式。
+ *
+ * @param message 要显示的消息。
+ * @param hasContent 如果消息非空则为true。
+ */
+ private void showStatusMessage(String message, boolean hasContent) {
+ statusLabel.setText(message);
+ updateStatusLabelStyle(hasContent);
+ }
+
+ /**
+ * 根据标签是否有内容来更新其CSS样式。
+ *
+ * @param hasContent 如果有内容则为true。
+ */
+ private void updateStatusLabelStyle(boolean hasContent) {
+ if (hasContent) {
+ statusLabel.getStyleClass().removeAll("register-status-label");
+ if (!statusLabel.getStyleClass().contains("register-status-label-with-text")) {
+ statusLabel.getStyleClass().add("register-status-label-with-text");
+ }
+ } else {
+ statusLabel.getStyleClass().removeAll("register-status-label-with-text");
+ if (!statusLabel.getStyleClass().contains("register-status-label")) {
+ statusLabel.getStyleClass().add("register-status-label");
+ }
+ }
+ }
+
+ /**
+ * 加载“设置密码”场景,并将新注册的用户名传递过去。
+ *
+ * @param username 新注册的用户名。
*/
private void loadSetPasswordScene(String username) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/SetPasswordView.fxml"));
Parent root = loader.load();
-
SetPasswordController controller = loader.getController();
- controller.initData(username); // 将用户名传递给新界面的控制器
+ controller.initData(username);
Stage stage = (Stage) registerButton.getScene().getWindow();
stage.setScene(new Scene(root));
@@ -96,11 +165,21 @@ public class RegisterController {
}
}
+ /**
+ * 处理“返回登录”按钮的点击事件,导航回登录界面。
+ *
+ * @param event 事件对象。
+ */
@FXML
private void handleBackToLoginAction(ActionEvent event) {
loadScene("/com/mathgenerator/view/LoginView.fxml");
}
+ /**
+ * 辅助方法,用于加载并切换到指定的FXML场景。
+ *
+ * @param fxmlPath FXML文件的路径。
+ */
private void loadScene(String fxmlPath) {
try {
Parent root = FXMLLoader.load(getClass().getResource(fxmlPath));
@@ -110,4 +189,13 @@ public class RegisterController {
e.printStackTrace();
}
}
+
+ /**
+ * 在控制器销毁前调用的清理方法,用于停止任何正在运行的后台任务,如倒计时。
+ */
+ public void cleanup() {
+ if (countdownTimeline != null) {
+ countdownTimeline.stop();
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/mathgenerator/controller/ScoreController.java b/src/main/java/com/mathgenerator/controller/ScoreController.java
index dbcba69..584d1e2 100644
--- a/src/main/java/com/mathgenerator/controller/ScoreController.java
+++ b/src/main/java/com/mathgenerator/controller/ScoreController.java
@@ -11,6 +11,10 @@ import javafx.scene.control.Label;
import javafx.stage.Stage;
import java.io.IOException;
+/**
+ * “分数”视图 (ScoreView.fxml) 的 FXML 控制器类。
+ * 负责在用户完成答题后,显示最终得分和相应的鼓励消息。
+ */
public class ScoreController {
private User currentUser;
@@ -21,15 +25,15 @@ public class ScoreController {
@FXML private Button logoutButton;
/**
- * 初始化控制器,接收答题结果数据
- * @param user 当前用户
- * @param score 最终得分
+ * 初始化控制器,并接收从答题界面传递过来的用户和分数数据。
+ *
+ * @param user 当前登录的用户对象。
+ * @param score 用户在上一轮答题中获得的最终分数。
*/
public void initData(User user, double score) {
this.currentUser = user;
scoreLabel.setText(String.format("%.2f", score));
- // 根据分数显示不同的鼓励语
if (score == 100.0) {
resultMessageLabel.setText("太棒了!你答对了所有题目!");
} else if (score >= 80) {
@@ -42,7 +46,10 @@ public class ScoreController {
}
/**
- * 处理“再做一组”按钮事件,返回主菜单
+ * 处理“再做一组”按钮的点击事件。
+ * 导航用户返回到主菜单,以便开始新一轮的练习。
+ *
+ * @param event 由按钮点击触发的 ActionEvent 事件。
*/
@FXML
private void handleTryAgainAction(ActionEvent event) {
@@ -50,7 +57,7 @@ public class ScoreController {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/MainMenuView.fxml"));
Parent root = loader.load();
MainMenuController controller = loader.getController();
- controller.initData(currentUser); // 将用户信息传回主菜单
+ controller.initData(currentUser);
Stage stage = (Stage) tryAgainButton.getScene().getWindow();
stage.setScene(new Scene(root));
@@ -61,7 +68,10 @@ public class ScoreController {
}
/**
- * 处理“退出登录”按钮事件,返回登录界面
+ * 处理“退出登录”按钮的点击事件。
+ * 导航用户返回到登录界面。
+ *
+ * @param event 由按钮点击触发的 ActionEvent 事件。
*/
@FXML
private void handleLogoutAction(ActionEvent event) {
diff --git a/src/main/java/com/mathgenerator/controller/SetPasswordController.java b/src/main/java/com/mathgenerator/controller/SetPasswordController.java
index 278c80c..9d6a583 100644
--- a/src/main/java/com/mathgenerator/controller/SetPasswordController.java
+++ b/src/main/java/com/mathgenerator/controller/SetPasswordController.java
@@ -15,6 +15,10 @@ import javafx.stage.Stage;
import java.io.IOException;
+/**
+ * “设置密码”视图 (SetPasswordView.fxml) 的 FXML 控制器类。
+ * 用户在注册成功后会进入此界面,以设置他们的初始密码。
+ */
public class SetPasswordController {
private final UserService userService = new UserService();
@@ -27,48 +31,88 @@ public class SetPasswordController {
@FXML private Label statusLabel;
/**
- * 接收从注册界面传递过来的用户名
+ * 初始化控制器,并接收从注册界面传递过来的用户名。
+ *
+ * @param username 新注册的用户名。
*/
public void initData(String username) {
this.username = username;
promptLabel.setText("为您的账户 " + username + " 设置密码");
}
+ /**
+ * 处理“确认”按钮的点击事件。
+ * 该方法会验证两次输入的密码是否一致且符合格式要求,然后调用服务为用户设置密码,
+ * 成功后直接登录并跳转到主菜单。
+ *
+ * @param event 事件对象。
+ */
@FXML
private void handleConfirmAction(ActionEvent event) {
String newPassword = newPasswordField.getText();
String confirmPassword = confirmPasswordField.getText();
if (!newPassword.equals(confirmPassword)) {
- statusLabel.setText("两次输入的密码不匹配!");
+ showStatusMessage("两次输入的密码不匹配!", true);
return;
}
+
if (!ValidationUtils.isPasswordValid(newPassword)) {
- statusLabel.setText("新密码格式错误!必须为6-10位,且包含大小写字母和数字。");
+ showStatusMessage("新密码格式错误!必须为6-10位,且包含大小写字母和数字。", true);
return;
}
- // 调用后端服务设置密码
boolean success = userService.setPassword(this.username, newPassword);
if (success) {
- statusLabel.setText("密码设置成功!正在进入主菜单...");
- // 密码设置成功后,获取完整的用户信息并直接跳转到主菜单
+ showStatusMessage("密码设置成功!正在进入主菜单...", true);
userService.findUserByUsername(this.username).ifPresent(this::loadMainMenu);
} else {
- statusLabel.setText("密码设置失败,请稍后重试或重新注册。");
+ showStatusMessage("密码设置失败,请稍后重试或重新注册。", true);
+ }
+ }
+
+ /**
+ * 在状态标签中显示一条消息,并根据内容更新其样式。
+ *
+ * @param message 要显示的消息。
+ * @param hasContent 如果消息非空则为true。
+ */
+ private void showStatusMessage(String message, boolean hasContent) {
+ statusLabel.setText(message);
+ updateStatusLabelStyle(hasContent);
+ }
+
+ /**
+ * 根据标签是否有内容来更新其CSS样式。
+ *
+ * @param hasContent 如果有内容则为true。
+ */
+ private void updateStatusLabelStyle(boolean hasContent) {
+ if (hasContent) {
+ statusLabel.getStyleClass().removeAll("setpassword-status-label");
+ if (!statusLabel.getStyleClass().contains("setpassword-status-label-with-text")) {
+ statusLabel.getStyleClass().add("setpassword-status-label-with-text");
+ }
+ } else {
+ statusLabel.getStyleClass().removeAll("setpassword-status-label-with-text");
+ if (!statusLabel.getStyleClass().contains("setpassword-status-label")) {
+ statusLabel.getStyleClass().add("setpassword-status-label");
+ }
}
}
/**
- * 加载主菜单界面,并传递用户信息 (实现自动登录)
+ * 加载主菜单界面,并传递完整的用户信息,以实现自动登录。
+ *
+ * @param user 包含新设置密码的完整 User 对象。
*/
private void loadMainMenu(User user) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/MainMenuView.fxml"));
Parent root = loader.load();
MainMenuController controller = loader.getController();
- controller.initData(user); // 将完整的User对象传递给主菜单
+ controller.initData(user);
Stage stage = (Stage) confirmButton.getScene().getWindow();
stage.setScene(new Scene(root));
diff --git a/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java b/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java
index add01a1..6d51dc5 100644
--- a/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java
+++ b/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java
@@ -6,58 +6,56 @@ import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
/**
- * 初中选择题生成器 (最终版 - 采用结构化插入)。
- * 通过直接在表达式结构中插入运算项,确保语法正确性和高性能。
+ * 初中难度选择题的生成器。
+ *
+ * 该类继承自 {@link PrimarySchoolGenerator},并在其基础上增加了平方和开平方根运算。
+ * 它通过在基础表达式中结构化地插入这些新运算项,来确保生成的题目语法正确且性能高。
*/
public class JuniorHighSchoolGenerator extends PrimarySchoolGenerator {
private static final int[] PERFECT_SQUARES = {1, 4, 9, 16, 25, 36, 49, 64, 81, 100};
+ /**
+ * 生成一道初中难度的数学选择题。
+ *
+ * 此方法首先生成一个基础的四则运算表达式,然后随机地将其中一个操作数替换为
+ * 平方或开方运算,最后计算结果并生成选项。如果过程中发生任何计算错误,
+ * 它会安全地回退到生成一道小学难度的题目。
+ *
+ * @return 一个封装了初中难度题目的 {@link ChoiceQuestion} 对象。
+ */
@Override
public ChoiceQuestion generateSingleQuestion() {
ThreadLocalRandom random = ThreadLocalRandom.current();
- int operandCount = random.nextInt(2, 5); // 2到4个操作数
+ int operandCount = random.nextInt(2, 5);
- // 1. 生成基础的表达式组件列表
List parts = new ArrayList<>();
- // 使用 getOperand() 和 getRandomOperator() 这些继承自父类的方法
parts.add(String.valueOf(getOperand()));
for (int i = 1; i < operandCount; i++) {
parts.add(getRandomOperator());
parts.add(String.valueOf(getOperand()));
}
-
- // 2. 结构化地插入初中特色运算
- int modificationIndex = random.nextInt(operandCount) * 2; // 随机选择一个操作数的位置
+ int modificationIndex = random.nextInt(operandCount) * 2;
boolean useSquare = random.nextBoolean();
-
if (useSquare) {
- // 平方策略:直接在数字后附加平方符号
parts.set(modificationIndex, parts.get(modificationIndex) + "²");
} else {
- // 开根号策略:用一个完美的开根号表达式替换整个数字
int perfectSquare = PERFECT_SQUARES[random.nextInt(PERFECT_SQUARES.length)];
parts.set(modificationIndex, "√" + perfectSquare);
}
- // 3. (可选)为增强后的表达式添加括号
if (operandCount > 2 && random.nextBoolean()) {
- super.addParentheses(parts); // 调用父类的protected方法
+ super.addParentheses(parts);
}
String finalQuestionText = String.join(" ", parts);
-
- // 4. 计算答案
double finalCorrectAnswer;
try {
Object result = evaluateExpression(finalQuestionText);
finalCorrectAnswer = ((Number) result).doubleValue();
} catch (Exception e) {
- // 发生意外,安全返回一个小学题
return super.generateSingleQuestion();
}
-
- // 5. 生成选项
List options = generateDecimalOptions(finalCorrectAnswer);
int correctIndex = options.indexOf(formatNumber(finalCorrectAnswer));
diff --git a/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java b/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java
index 530e6dc..e896739 100644
--- a/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java
+++ b/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java
@@ -1,6 +1,7 @@
package com.mathgenerator.generator;
-import com.mathgenerator.model.ChoiceQuestion; // 导入新模型
+import com.mathgenerator.model.ChoiceQuestion;
+import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@@ -10,11 +11,14 @@ import java.util.concurrent.ThreadLocalRandom;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
-import java.text.DecimalFormat;
/**
- * 小学选择题生成器。
- * 生成包含 + - * / 和 () 的运算,并提供四个选项。
+ * 小学难度选择题的生成器。
+ *
+ * 该类负责生成包含加、减、乘、除和括号的四则运算题目。
+ * 生成的题目会确保其计算结果为整数,并提供四个选项。
+ *
+ * @see QuestionGenerator
*/
public class PrimarySchoolGenerator implements QuestionGenerator {
@@ -22,7 +26,11 @@ public class PrimarySchoolGenerator implements QuestionGenerator {
/**
* 生成一道小学难度的数学选择题。
- * @return 一个包含题干、四个选项和正确答案索引的 ChoiceQuestion 对象。
+ *
+ * 该方法会循环生成一个包含2到4个操作数的随机表达式,直到表达式的计算结果为整数。
+ * 然后,它会围绕正确答案生成三个干扰项,并将它们随机排序后,封装成一个 {@link ChoiceQuestion} 对象。
+ *
+ * @return 一个包含题干、四个选项和正确答案索引的 {@code ChoiceQuestion} 对象。
*/
@Override
public ChoiceQuestion generateSingleQuestion() {
@@ -65,9 +73,13 @@ public class PrimarySchoolGenerator implements QuestionGenerator {
}
/**
- * 生成四个选项 (1个正确,3个干扰项)
- * @param correctAnswer 正确答案
- * @return 包含四个选项的随机排序列表
+ * 为给定的正确答案生成四个选项(一个正确,三个干扰项)。
+ *
+ * 干扰项是通过在正确答案上加或减一个1到10之间的随机数生成的。
+ * 所有选项(包括正确答案)会被放入一个列表中并随机打乱顺序。
+ *
+ * @param correctAnswer 正确的整数答案。
+ * @return 一个包含四个选项字符串的随机排序列表。
*/
protected List generateOptions(int correctAnswer) {
ThreadLocalRandom random = ThreadLocalRandom.current();
@@ -88,28 +100,28 @@ public class PrimarySchoolGenerator implements QuestionGenerator {
}
/**
- * 使用JVM的脚本引擎计算字符串表达式的值 (已优化兼容性)。
- * @param expression 数学表达式字符串
- * @return 计算结果 (可能是Integer或Double)
- * @throws ScriptException 如果表达式有语法错误
+ * 使用JVM的脚本引擎计算字符串表达式的值。
+ *
+ * 此方法已优化以兼容 Rhino 引擎,并能处理平方、开方和三角函数等运算。
+ *
+ * @param expression 数学表达式字符串,例如 " (3 + 5) * 2 "。
+ * @return 计算结果,通常是 {@code Integer} 或 {@code Double} 类型。
+ * @throws ScriptException 如果表达式包含语法错误。
+ * @throws IllegalStateException 如果找不到 Rhino JavaScript 引擎。
*/
protected Object evaluateExpression(String expression) throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
- // --- 核心修改在这里 ---
- // 使用 "rhino" 作为引擎名称,这是Rhino引擎的官方名称
ScriptEngine engine = manager.getEngineByName("rhino");
if (engine == null) {
- // 增加一个健壮性检查,如果引擎还是没找到,就给出清晰的错误提示
throw new IllegalStateException("错误:找不到Rhino JavaScript引擎。请检查pom.xml中是否已添加rhino-engine的依赖。");
}
- // Rhino不需要预定义函数,可以直接计算
+ // 预处理表达式以兼容Rhino引擎的数学函数
String script = expression.replaceAll("(\\d+(\\.\\d+)?)²", "Math.pow($1, 2)")
.replaceAll("√(\\d+(\\.\\d+)?)", "Math.sqrt($1)")
- .replaceAll("(\\d+)°", " * (Math.PI / 180)"); // Rhino对角度计算的语法要求更严格
+ .replaceAll("(\\d+)°", " * (Math.PI / 180)");
- // 为了让sin/cos/tan能正确计算,需要特殊处理
script = script.replaceAll("sin\\(", "Math.sin(")
.replaceAll("cos\\(", "Math.cos(")
.replaceAll("tan\\(", "Math.tan(");
@@ -118,24 +130,27 @@ public class PrimarySchoolGenerator implements QuestionGenerator {
}
/**
- * 格式化数字,最多保留两位小数。
- * @param number 待格式化的数字
- * @return 格式化后的字符串
+ * 将一个 double 类型的数字格式化为字符串,最多保留两位小数。
+ *
+ * 如果数字是整数,则不显示小数位。
+ *
+ * @param number 待格式化的数字。
+ * @return 格式化后的字符串。
*/
protected String formatNumber(double number) {
if (number == (long) number) {
- return String.format("%d", (long) number); // 如果是整数,不显示小数位
+ return String.format("%d", (long) number);
} else {
- // 使用DecimalFormat来去除末尾多余的0
DecimalFormat df = new DecimalFormat("#.##");
return df.format(number);
}
}
/**
- * 为小数答案生成四个选项。
- * @param correctAnswer 正确答案
- * @return 包含四个选项的随机排序列表
+ * 为给定的小数正确答案生成四个选项。
+ *
+ * @param correctAnswer 正确的小数答案。
+ * @return 一个包含四个格式化后选项字符串的随机排序列表。
*/
protected List generateDecimalOptions(double correctAnswer) {
ThreadLocalRandom random = ThreadLocalRandom.current();
@@ -143,8 +158,7 @@ public class PrimarySchoolGenerator implements QuestionGenerator {
options.add(formatNumber(correctAnswer));
while (options.size() < 4) {
- double delta = random.nextDouble(1, 11); // 答案加减1-10之间的随机小数
- // 随机决定是加还是减
+ double delta = random.nextDouble(1, 11);
double distractor = random.nextBoolean() ? correctAnswer + delta : correctAnswer - delta;
options.add(formatNumber(distractor));
}
@@ -154,14 +168,29 @@ public class PrimarySchoolGenerator implements QuestionGenerator {
return sortedOptions;
}
+ /**
+ * 获取一个1到100之间的随机整数作为操作数。
+ *
+ * @return 一个随机整数。
+ */
protected int getOperand() {
return ThreadLocalRandom.current().nextInt(1, 101);
}
+ /**
+ * 从 {@code OPERATORS} 数组中随机选择一个运算符。
+ *
+ * @return 一个随机的运算符字符串("+"、"-"、"*" 或 "/")。
+ */
protected String getRandomOperator() {
return OPERATORS[ThreadLocalRandom.current().nextInt(OPERATORS.length)];
}
+ /**
+ * 为给定的表达式组件列表随机添加一对括号。
+ *
+ * @param parts 包含数字和运算符的字符串列表,将被原地修改。
+ */
protected void addParentheses(List parts) {
ThreadLocalRandom random = ThreadLocalRandom.current();
int startOperandIndex = random.nextInt(parts.size() / 2);
@@ -171,7 +200,4 @@ public class PrimarySchoolGenerator implements QuestionGenerator {
parts.add(endIndex + 1, ")");
parts.add(startIndex, "(");
}
-
-
-
}
\ No newline at end of file
diff --git a/src/main/java/com/mathgenerator/generator/QuestionGenerator.java b/src/main/java/com/mathgenerator/generator/QuestionGenerator.java
index e478038..c6e930b 100644
--- a/src/main/java/com/mathgenerator/generator/QuestionGenerator.java
+++ b/src/main/java/com/mathgenerator/generator/QuestionGenerator.java
@@ -1,14 +1,22 @@
package com.mathgenerator.generator;
-import com.mathgenerator.model.ChoiceQuestion; // 导入新模型
+import com.mathgenerator.model.ChoiceQuestion;
/**
- * 题目生成器接口,定义了所有具体生成器必须实现的方法。
+ * 题目生成器接口。
+ *
+ * 该接口定义了所有具体题目生成器(如小学、初中、高中)必须实现的核心方法。
+ * 任何实现此接口的类都应具备生成单个数学选择题的能力。
*/
public interface QuestionGenerator {
+
/**
* 生成一道符合特定难度的数学选择题。
- * @return 代表数学选择题的 ChoiceQuestion 对象
+ *
+ * 此方法的实现应确保生成的题目包含题干、四个选项以及正确答案的索引,
+ * 并将这些信息封装在一个 {@link ChoiceQuestion} 对象中返回。
+ *
+ * @return 一个代表新生成的数学选择题的 {@code ChoiceQuestion} 对象。
*/
ChoiceQuestion generateSingleQuestion();
}
\ No newline at end of file
diff --git a/src/main/java/com/mathgenerator/generator/SafePrimarySchoolGenerator.java b/src/main/java/com/mathgenerator/generator/SafePrimarySchoolGenerator.java
index d38cee6..80f4b4d 100644
--- a/src/main/java/com/mathgenerator/generator/SafePrimarySchoolGenerator.java
+++ b/src/main/java/com/mathgenerator/generator/SafePrimarySchoolGenerator.java
@@ -7,18 +7,24 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
-import javax.script.ScriptEngine;
-import javax.script.ScriptEngineManager;
-import javax.script.ScriptException;
/**
- * “安全”的小学题目生成器,确保运算过程中不产生负数,并生成选择题。
+ * “安全”的小学题目生成器。
+ *
+ * 这个生成器专门用于创建小学难度的数学题,并确保在整个运算过程中不会产生任何负数结果。
+ * 它通过一种递归下降的算法来构造表达式,从而在生成阶段就避免了负数的出现。
+ *
+ * @see QuestionGenerator
*/
public class SafePrimarySchoolGenerator implements QuestionGenerator {
/**
- * 生成一道确保结果非负的小学难度的数学选择题。
- * @return 一个符合所有约束的 ChoiceQuestion 对象。
+ * 生成一道确保结果非负的小学难度数学选择题。
+ *
+ * 该方法首先确定一个操作数的总预算,然后调用递归方法 {@code generateSafeExpression}
+ * 来构造一个保证结果为正数的表达式,并最终封装成 {@link ChoiceQuestion} 对象。
+ *
+ * @return 一个符合所有非负约束的 {@code ChoiceQuestion} 对象。
*/
@Override
public ChoiceQuestion generateSingleQuestion() {
@@ -34,12 +40,21 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator {
}
/**
- * 内部记录类,用于在递归生成表达式时传递部分结果。
+ * 内部记录 (Record),用于在递归生成表达式时传递部分结果。
+ *
+ * @param parts 表达式的字符串组件列表。
+ * @param value 该表达式的计算结果。
+ * @param operandsUsed 生成此表达式已使用的操作数数量。
*/
private record Term(List parts, int value, int operandsUsed) {}
/**
- * 根据操作数预算,生成一个确保结果非负的(子)表达式。
+ * 根据给定的操作数预算,递归地生成一个确保结果非负的(子)表达式。
+ *
+ * 算法的核心在于,当执行减法时,会检查确保被减数大于减数。
+ *
+ * @param operandBudget 当前可用于生成表达式的操作数数量。
+ * @return 一个包含表达式、其值和所用操作数数量的 {@code Term} 对象。
*/
private Term generateSafeExpression(int operandBudget) {
ThreadLocalRandom random = ThreadLocalRandom.current();
@@ -72,7 +87,12 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator {
}
/**
- * 生成一个“项”(Term)。一个项可以是简单的乘除法序列,也可以是带括号的子表达式。
+ * 生成一个“项”(Term)。
+ *
+ * 一个项可以是一个简单的乘除法序列,或者是一个带括号的、更复杂的子表达式。
+ *
+ * @param operandsRemaining 剩余可用的操作数数量。
+ * @return 代表所生成项的 {@code Term} 对象。
*/
private Term generateTerm(int operandsRemaining) {
ThreadLocalRandom random = ThreadLocalRandom.current();
@@ -91,7 +111,12 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator {
}
/**
- * 生成一个仅包含乘除法的简单项。
+ * 生成一个仅包含乘法或除法的简单项。
+ *
+ * 为了确保除法的结果是整数,除数会从被除数的所有因数中选取。
+ *
+ * @param operandsRemaining 剩余可用的操作数数量。
+ * @return 代表简单项的 {@code Term} 对象。
*/
private Term generateSimpleTerm(int operandsRemaining) {
ThreadLocalRandom random = ThreadLocalRandom.current();
@@ -118,6 +143,12 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator {
return new Term(parts, termValue, operandsUsed);
}
+ /**
+ * 获取一个正整数的所有因数。
+ *
+ * @param number 要查找因数的数字。
+ * @return 包含该数字所有因数的列表。
+ */
private List getDivisors(int number) {
List divisors = new ArrayList<>();
for (int i = 1; i <= number; i++) {
@@ -127,7 +158,12 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator {
}
/**
- * 生成四个选项 (1个正确,3个干扰项)
+ * 为给定的正确答案生成四个选项(一个正确,三个干扰项)。
+ *
+ * 此方法确保所有生成的干扰项也都是非负数。
+ *
+ * @param correctAnswer 正确的整数答案。
+ * @return 一个包含四个非负选项的随机排序列表。
*/
private List generateOptions(int correctAnswer) {
ThreadLocalRandom random = ThreadLocalRandom.current();
@@ -137,8 +173,7 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator {
while (options.size() < 4) {
int delta = random.nextInt(1, 11);
int distractor = random.nextBoolean() ? correctAnswer + delta : correctAnswer - delta;
- // 确保干扰项非负
- if (distractor >= 0) {
+ if (distractor >= 0) { // 确保干扰项非负
options.add(String.valueOf(distractor));
}
}
diff --git a/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java b/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java
index 4dc6be4..c038127 100644
--- a/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java
+++ b/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java
@@ -6,8 +6,10 @@ import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
/**
- * 高中选择题生成器 (最终版 - 采用构造式添加)。
- * 通过在已生成的初中题基础上添加预设的三角函数项来构造题目。
+ * 高中难度选择题的生成器。
+ *
+ * 该类继承自 {@link JuniorHighSchoolGenerator},通过在已生成的初中难度题目基础上,
+ * 构造性地添加一个预设的、计算结果简单的三角函数项来生成题目。
*/
public class SeniorHighSchoolGenerator extends JuniorHighSchoolGenerator {
@@ -24,6 +26,14 @@ public class SeniorHighSchoolGenerator extends JuniorHighSchoolGenerator {
// 将Map的键转换为数组,方便随机选取
private static final String[] TRIG_KEYS = TRIG_TERMS.keySet().toArray(new String[0]);
+ /**
+ * 生成一道高中难度的数学选择题。
+ *
+ * 此方法首先调用父类方法生成一个初中难度的题目作为基础。然后,它随机选择一个
+ * 预设的三角函数项,并将其与初中题目通过加法或减法结合,形成最终的高中题目。
+ *
+ * @return 一个封装了高中难度题目的 {@link ChoiceQuestion} 对象。
+ */
@Override
public ChoiceQuestion generateSingleQuestion() {
// 1. 先生成一个保证可计算的、高性能的初中选择题,作为基础
diff --git a/src/main/java/com/mathgenerator/model/ChoiceQuestion.java b/src/main/java/com/mathgenerator/model/ChoiceQuestion.java
index 485b2ec..7d18220 100644
--- a/src/main/java/com/mathgenerator/model/ChoiceQuestion.java
+++ b/src/main/java/com/mathgenerator/model/ChoiceQuestion.java
@@ -3,10 +3,14 @@ package com.mathgenerator.model;
import java.util.List;
/**
- * 选择题数据模型。
- * @param questionText 题干
- * @param options 四个选项的列表
- * @param correctOptionIndex 正确答案在列表中的索引 (0-3)
+ * 选择题数据模型 (Record)。
+ *
+ * 该记录 (Record) 用于以不可变的方式封装一道选择题的所有核心信息,
+ * 包括题干、选项列表以及正确答案的索引。
+ *
+ * @param questionText 题目的文本内容,即题干。
+ * @param options 一个包含四个选项字符串的列表 (List)。
+ * @param correctOptionIndex 正确答案在 {@code options} 列表中的索引,范围从 0 到 3。
*/
public record ChoiceQuestion(String questionText, List options, int correctOptionIndex) {
}
\ No newline at end of file
diff --git a/src/main/java/com/mathgenerator/model/Level.java b/src/main/java/com/mathgenerator/model/Level.java
index 8b27203..7f47178 100644
--- a/src/main/java/com/mathgenerator/model/Level.java
+++ b/src/main/java/com/mathgenerator/model/Level.java
@@ -1,14 +1,29 @@
package com.mathgenerator.model;
/**
- * 学段枚举,用于类型安全地表示小学、初中和高中。
+ * 学段枚举 (Enum)。
+ *
+ * 该枚举用于以类型安全的方式表示程序支持的不同题目难度级别,
+ * 包括小学、初中和高中。每个枚举常量都关联一个中文名称。
*/
public enum Level {
+ /**
+ * 代表小学难度。
+ */
PRIMARY("小学"),
+
+ /**
+ * 代表初中难度。
+ */
JUNIOR_HIGH("初中"),
+
+ /**
+ * 代表高中难度。
+ */
SENIOR_HIGH("高中");
private final String chineseName;
+
/**
* 枚举的构造函数。
*
@@ -17,10 +32,11 @@ public enum Level {
Level(String chineseName) {
this.chineseName = chineseName;
}
+
/**
- * 获取学段的中文名称。
+ * 获取该学段对应的中文名称。
*
- * @return 表示学段的中文名称字符串。
+ * @return 表示学段的中文名称字符串 (例如, "小学")。
*/
public String getChineseName() {
return chineseName;
diff --git a/src/main/java/com/mathgenerator/model/User.java b/src/main/java/com/mathgenerator/model/User.java
index 66ecbd3..eccf84e 100644
--- a/src/main/java/com/mathgenerator/model/User.java
+++ b/src/main/java/com/mathgenerator/model/User.java
@@ -1,10 +1,14 @@
package com.mathgenerator.model;
/**
- * 用户数据记录 (Record),用于封装不可变的用户信息。
- * @param username 用户名
- * @param email 邮箱地址 (新增)
- * @param password 密码
+ * 用户数据记录 (Record)。
+ *
+ * 该记录 (Record) 用于以不可变的方式封装一个用户的基本信息,
+ * 包括用户名、邮箱地址和密码。
+ *
+ * @param username 用户的唯一标识符,即用户名。
+ * @param email 用户的电子邮箱地址,用于接收验证码等。
+ * @param password 用户账户的密码。在用户首次注册但还未设置密码时,此字段可能为 {@code null}。
*/
public record User(String username, String email, String password) {
}
\ No newline at end of file
diff --git a/src/main/java/com/mathgenerator/service/EmailConfig.java b/src/main/java/com/mathgenerator/service/EmailConfig.java
index 44618f8..a28c5ce 100644
--- a/src/main/java/com/mathgenerator/service/EmailConfig.java
+++ b/src/main/java/com/mathgenerator/service/EmailConfig.java
@@ -5,6 +5,13 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
+/**
+ * 邮件配置加载类。
+ *
+ * 该类负责从项目根目录下的 {@code config.properties} 文件中读取和提供
+ * 发送邮件所需的 SMTP 服务器信息,如主机、端口、用户名和密码。
+ * 所有配置项通过静态方法提供。
+ */
public class EmailConfig {
private static final Properties properties = new Properties();
@@ -17,18 +24,38 @@ public class EmailConfig {
}
}
+ /**
+ * 获取 SMTP 服务器的主机名。
+ *
+ * @return SMTP 主机名字符串。
+ */
public static String getHost() {
return properties.getProperty("smtp.host");
}
+ /**
+ * 获取 SMTP 服务器的端口号。
+ *
+ * @return SMTP 端口号。
+ */
public static int getPort() {
return Integer.parseInt(properties.getProperty("smtp.port"));
}
+ /**
+ * 获取用于 SMTP 认证的用户名(通常是发件人邮箱地址)。
+ *
+ * @return SMTP 用户名。
+ */
public static String getUsername() {
return properties.getProperty("smtp.username");
}
+ /**
+ * 获取用于 SMTP 认证的密码或授权码。
+ *
+ * @return SMTP 密码或授权码。
+ */
public static String getPassword() {
return properties.getProperty("smtp.password");
}
diff --git a/src/main/java/com/mathgenerator/service/PaperService.java b/src/main/java/com/mathgenerator/service/PaperService.java
index 3df7045..a44437f 100644
--- a/src/main/java/com/mathgenerator/service/PaperService.java
+++ b/src/main/java/com/mathgenerator/service/PaperService.java
@@ -2,7 +2,7 @@ package com.mathgenerator.service;
import com.mathgenerator.model.User;
import com.mathgenerator.model.Level;
-import com.mathgenerator.model.ChoiceQuestion; // 导入新模型
+import com.mathgenerator.model.ChoiceQuestion;
import com.mathgenerator.service.strategy.PaperStrategy;
import com.mathgenerator.storage.FileManager;
import java.io.IOException;
@@ -12,37 +12,48 @@ import java.util.List;
import java.util.Set;
/**
- * 试卷服务,现在处理ChoiceQuestion对象。
+ * 试卷服务类。
+ *
+ * 该服务负责处理与试卷相关的核心业务逻辑,包括根据指定的策略生成试卷
+ * (并处理题目查重),以及将生成的试卷保存到文件系统中。
*/
public class PaperService {
private final FileManager fileManager;
private final PaperStrategy paperStrategy;
+ /**
+ * 构造一个新的 PaperService 实例。
+ *
+ * @param fileManager 用于处理文件读写操作的 {@link FileManager} 实例。
+ * @param paperStrategy 用于选择具体题目生成器策略的 {@link PaperStrategy} 实例。
+ */
public PaperService(FileManager fileManager, PaperStrategy paperStrategy) {
this.fileManager = fileManager;
this.paperStrategy = paperStrategy;
}
/**
- * 创建一份包含选择题的试卷。
- * @param user 当前用户
- * @param count 题目数量
- * @param currentLevel 当前难度
- * @return 生成的选择题列表
+ * 创建一份包含指定数量选择题的试卷。
+ *
+ * 该方法会首先加载用户历史题目以进行查重,然后根据传入的难度级别和策略
+ * 循环生成新题目,直到满足指定的数量要求。所有在本轮生成过程中的题目也会
+ * 进行内部查重。
+ *
+ * @param user 当前请求生成试卷的用户对象。
+ * @param count 需要生成的题目数量。
+ * @param currentLevel 用户选择的主要难度级别。
+ * @return 一个包含新生成的 {@link ChoiceQuestion} 对象的列表。
*/
public List createPaper(User user, int count, Level currentLevel) {
- // 查重集合现在存储题干字符串
Set existingQuestionTexts = fileManager.loadExistingQuestions(user.username());
List newPaper = new ArrayList<>();
Set generatedInSession = new HashSet<>();
System.out.println("正在根据策略生成选择题,请稍候...");
while (newPaper.size() < count) {
- // 1. 生成的是ChoiceQuestion对象
ChoiceQuestion question = paperStrategy.selectGenerator(currentLevel).generateSingleQuestion();
- String questionText = question.questionText(); // 提取题干用于查重
+ String questionText = question.questionText();
- // 2. 使用题干进行查重
if (!existingQuestionTexts.contains(questionText) && !generatedInSession.contains(questionText)) {
newPaper.add(question);
generatedInSession.add(questionText);
@@ -52,9 +63,10 @@ public class PaperService {
}
/**
- * 将生成的试卷保存到文件。
- * @param username 用户名
- * @param paper 试卷题目列表
+ * 将一份生成的试卷保存到用户专属的文件夹中。
+ *
+ * @param username 用户的用户名,用于确定文件夹路径。
+ * @param paper 包含试卷所有 {@link ChoiceQuestion} 的列表。
*/
public void savePaper(String username, List paper) {
try {
diff --git a/src/main/java/com/mathgenerator/service/UserService.java b/src/main/java/com/mathgenerator/service/UserService.java
index 2c816a7..1a2eb64 100644
--- a/src/main/java/com/mathgenerator/service/UserService.java
+++ b/src/main/java/com/mathgenerator/service/UserService.java
@@ -1,10 +1,13 @@
package com.mathgenerator.service;
import com.google.gson.Gson;
-import java.util.Objects;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.mathgenerator.model.User;
+import org.apache.commons.mail.Email;
+import org.apache.commons.mail.EmailException;
+import org.apache.commons.mail.SimpleEmail;
+
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
@@ -17,25 +20,35 @@ import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Pattern;
-import org.apache.commons.mail.Email;
-import org.apache.commons.mail.EmailException;
-import org.apache.commons.mail.SimpleEmail;
-
+/**
+ * 用户服务类。
+ *
+ * 负责处理所有与用户账户相关的业务逻辑,包括用户的注册、登录、密码修改、
+ * 信息查询以及发送验证码等功能。该类通过读写 JSON 文件来持久化用户数据。
+ */
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();
+ /**
+ * 构造一个新的 UserService 实例。
+ * 在构造时会自动从 {@code users.json} 文件加载用户数据。
+ */
public UserService() {
this.userDatabase = loadUsersFromFile();
}
+ /**
+ * 从 {@code users.json} 文件加载用户数据到内存中的 Map。
+ * 如果文件不存在或为空,则返回一个空的 Map。
+ *
+ * @return 一个包含所有用户数据的 {@code ConcurrentHashMap}。
+ */
private Map loadUsersFromFile() {
- // 如果文件不存在,直接返回一个空的Map,不再创建默认用户
if (!Files.exists(USER_FILE_PATH)) {
return new ConcurrentHashMap<>();
}
@@ -43,7 +56,6 @@ public class UserService {
try (FileReader reader = new FileReader(USER_FILE_PATH.toFile())) {
Type type = new TypeToken