From 6faac5ba2f0bc8fabb97e4600d632574ca55a8c4 Mon Sep 17 00:00:00 2001 From: hnu202326010328 <2655155213@qq.com> Date: Wed, 8 Oct 2025 17:01:28 +0800 Subject: [PATCH 01/13] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=87=B3=20'src/main/java/com/mathgenerator/controller'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/LoginController.java | 113 ++++++++++++++ .../controller/MainMenuController.java | 113 ++++++++++++++ .../controller/QuizController.java | 144 ++++++++++++++++++ .../controller/RegisterController.java | 102 +++++++++++++ .../controller/ScoreController.java | 77 ++++++++++ 5 files changed, 549 insertions(+) create mode 100644 src/main/java/com/mathgenerator/controller/LoginController.java create mode 100644 src/main/java/com/mathgenerator/controller/MainMenuController.java create mode 100644 src/main/java/com/mathgenerator/controller/QuizController.java create mode 100644 src/main/java/com/mathgenerator/controller/RegisterController.java create mode 100644 src/main/java/com/mathgenerator/controller/ScoreController.java diff --git a/src/main/java/com/mathgenerator/controller/LoginController.java b/src/main/java/com/mathgenerator/controller/LoginController.java new file mode 100644 index 0000000..7c7150d --- /dev/null +++ b/src/main/java/com/mathgenerator/controller/LoginController.java @@ -0,0 +1,113 @@ +package com.mathgenerator.controller; + +import com.mathgenerator.model.User; +import com.mathgenerator.service.UserService; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.stage.Stage; + +import java.io.IOException; +import java.util.Optional; + +public class LoginController { + + // 依赖注入后端服务 + private final UserService userService = new UserService(); + + // @FXML注解将FXML文件中的控件与这里的变量关联起来 + @FXML + private TextField usernameField; + + @FXML + private PasswordField passwordField; + + @FXML + private Button loginButton; + + @FXML + private Button registerButton; + + @FXML + private Label statusLabel; + + /** + * 处理登录按钮点击事件。 + * @param event 事件对象 + */ + @FXML + private void handleLoginButtonAction(ActionEvent event) { + String username = usernameField.getText(); + String password = passwordField.getText(); + + if (username.isEmpty() || password.isEmpty()) { + statusLabel.setText("用户名和密码不能为空!"); + return; + } + + Optional userOptional = userService.login(username, password); + + if (userOptional.isPresent()) { + statusLabel.setText("登录成功!"); + // 登录成功,调用新方法跳转到主菜单 + loadMainMenu(userOptional.get()); + } else { + statusLabel.setText("登录失败:用户名或密码错误。"); + } + } + + /** + * 处理注册按钮点击事件,跳转到注册界面。 + * @param event 事件对象 + */ + @FXML + private void handleRegisterButtonAction(ActionEvent event) { + loadScene("/com/mathgenerator/view/RegisterView.fxml"); + } + + /** + * 加载主菜单界面,并传递用户信息。 + * @param user 登录成功的用户对象 + */ + private void loadMainMenu(User user) { + try { + // 1. 加载 FXML 文件 + FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/MainMenuView.fxml")); + Parent root = loader.load(); + + // 2. 获取新界面的控制器 + MainMenuController controller = loader.getController(); + + // 3. 调用控制器的方法,传递数据 + controller.initData(user); + + // 4. 显示新场景 + Stage stage = (Stage) loginButton.getScene().getWindow(); + stage.setScene(new Scene(root)); + stage.setTitle("主菜单"); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 切换到简单场景的辅助方法(如注册页)。 + * @param fxmlPath FXML文件的路径 + */ + private void loadScene(String fxmlPath) { + try { + Parent root = FXMLLoader.load(getClass().getResource(fxmlPath)); + Stage stage = (Stage) loginButton.getScene().getWindow(); + stage.setScene(new Scene(root)); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/controller/MainMenuController.java b/src/main/java/com/mathgenerator/controller/MainMenuController.java new file mode 100644 index 0000000..cc0d567 --- /dev/null +++ b/src/main/java/com/mathgenerator/controller/MainMenuController.java @@ -0,0 +1,113 @@ +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 handleLogoutAction(ActionEvent event) { + // 跳转回登录界面 + loadScene("/com/mathgenerator/view/LoginView.fxml"); + } + + @FXML + private void handleChangePasswordAction(ActionEvent event) { + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/ChangePasswordView.fxml")); + Parent root = loader.load(); + + ChangePasswordController controller = loader.getController(); + controller.initData(currentUser); + + Stage stage = (Stage) logoutButton.getScene().getWindow(); + stage.setScene(new Scene(root)); + stage.setTitle("修改密码"); + + } catch (IOException e) { + e.printStackTrace(); + statusLabel.setText("加载修改密码界面失败!"); + } +} + /** + * 验证输入并准备开始答题。 + * @param level 选择的难度 + */ + private void startQuiz(Level level) { + try { + int count = Integer.parseInt(questionCountField.getText()); + if (count < 1 || count > 50) { + statusLabel.setText("题目数量必须在 1 到 50 之间!"); + return; + } + + // 加载答题界面,并传递数据 + FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/QuizView.fxml")); + Parent root = loader.load(); + + QuizController controller = loader.getController(); + controller.initData(currentUser, level, count); + + Stage stage = (Stage) logoutButton.getScene().getWindow(); + stage.setScene(new Scene(root)); + stage.setTitle(level.getChineseName() + " - 答题中"); + + } catch (NumberFormatException e) { + statusLabel.setText("请输入有效的题目数量!"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void loadScene(String fxmlPath) { + try { + Stage stage = (Stage) logoutButton.getScene().getWindow(); + Parent root = FXMLLoader.load(getClass().getResource(fxmlPath)); + stage.setScene(new Scene(root)); + stage.setTitle("用户登录"); // 返回登录时重置标题 + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/controller/QuizController.java b/src/main/java/com/mathgenerator/controller/QuizController.java new file mode 100644 index 0000000..abce998 --- /dev/null +++ b/src/main/java/com/mathgenerator/controller/QuizController.java @@ -0,0 +1,144 @@ +package com.mathgenerator.controller; + +import com.mathgenerator.model.ChoiceQuestion; +import com.mathgenerator.model.Level; +import com.mathgenerator.model.User; +import com.mathgenerator.service.PaperService; +import com.mathgenerator.service.strategy.MixedDifficultyStrategy; +import com.mathgenerator.storage.FileManager; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ToggleGroup; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class QuizController { + + // --- 后端服务 --- + private final PaperService paperService; + + // --- 答题状态 --- + private User currentUser; + private List questions; + private List userAnswers = new ArrayList<>(); + private int currentQuestionIndex = 0; + + // --- FXML 控件 --- + @FXML private Label questionNumberLabel; + @FXML private ProgressBar progressBar; + @FXML private Label questionTextLabel; + @FXML private ToggleGroup optionsGroup; + @FXML private RadioButton option1, option2, option3, option4; + @FXML private Button submitButton; + @FXML private Label statusLabel; + + /** + * 构造函数,初始化后端服务 + */ + public QuizController() { + // 这是依赖注入的一种简化形式,在真实项目中会使用框架管理 + FileManager fileManager = new FileManager(); + MixedDifficultyStrategy strategy = new MixedDifficultyStrategy(); + this.paperService = new PaperService(fileManager, strategy); + } + + /** + * 接收从主菜单传递过来的数据,并开始答题 + */ + public void initData(User user, Level level, int questionCount) { + this.currentUser = user; + // 调用后端服务生成题目 + this.questions = paperService.createPaper(user, questionCount, level); + displayCurrentQuestion(); + } + + /** + * 显示当前的题目和选项 + */ + private void displayCurrentQuestion() { + ChoiceQuestion currentQuestion = questions.get(currentQuestionIndex); + + questionNumberLabel.setText(String.format("第 %d / %d 题", currentQuestionIndex + 1, questions.size())); + progressBar.setProgress((double) (currentQuestionIndex + 1) / questions.size()); + questionTextLabel.setText(currentQuestion.questionText()); + + List radioButtons = List.of(option1, option2, option3, option4); + for (int i = 0; i < radioButtons.size(); i++) { + radioButtons.get(i).setText(currentQuestion.options().get(i)); + } + + optionsGroup.selectToggle(null); // 清除上一次的选择 + statusLabel.setText(""); // 清除状态提示 + + if (currentQuestionIndex == questions.size() - 1) { + submitButton.setText("完成答题"); + } + } + + /** + * 处理提交按钮的点击事件 + */ + @FXML + private void handleSubmitButtonAction(ActionEvent event) { + RadioButton selectedRadioButton = (RadioButton) optionsGroup.getSelectedToggle(); + if (selectedRadioButton == null) { + statusLabel.setText("请选择一个答案!"); + return; + } + + // 记录用户答案的索引 + List radioButtons = List.of(option1, option2, option3, option4); + userAnswers.add(radioButtons.indexOf(selectedRadioButton)); + + // 移动到下一题或结束答题 + currentQuestionIndex++; + if (currentQuestionIndex < questions.size()) { + displayCurrentQuestion(); + } else { + // 答题结束,计算分数并跳转到分数界面 + calculateScoreAndShowResults(); + } + } + + /** + * 计算分数并准备跳转到结果页面 + */ + private void calculateScoreAndShowResults() { + int correctCount = 0; + for (int i = 0; i < questions.size(); i++) { + if (userAnswers.get(i) == questions.get(i).correctOptionIndex()) { + correctCount++; + } + } + double score = (double) correctCount / questions.size() * 100; + + // 禁用当前页面的按钮 + submitButton.setDisable(true); + statusLabel.setText("答题已完成,正在为您计算分数..."); + + // 加载分数界面并传递数据 + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/ScoreView.fxml")); + Parent root = loader.load(); + + ScoreController controller = loader.getController(); + controller.initData(currentUser, score); // 将用户和分数传递过去 + + Stage stage = (Stage) submitButton.getScene().getWindow(); + stage.setScene(new Scene(root)); + stage.setTitle("答题结果"); + + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/controller/RegisterController.java b/src/main/java/com/mathgenerator/controller/RegisterController.java new file mode 100644 index 0000000..0227866 --- /dev/null +++ b/src/main/java/com/mathgenerator/controller/RegisterController.java @@ -0,0 +1,102 @@ +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); + + // 处理发送结果 + if (this.sentCode != null) { + statusLabel.setText("验证码已成功发送,请查收您的邮箱。"); + sendCodeButton.setDisable(true); // 防止重复点击 + } else { + statusLabel.setText("验证码发送失败!请检查配置或联系管理员。"); + } + } + + @FXML + private void handleRegisterAction(ActionEvent event) { + // 1. 字段校验 + if (usernameField.getText().isEmpty() || emailField.getText().isEmpty() || + verificationCodeField.getText().isEmpty() || passwordField.getText().isEmpty()) { + statusLabel.setText("所有字段都不能为空!"); + return; + } + if (!passwordField.getText().equals(confirmPasswordField.getText())) { + statusLabel.setText("两次输入的密码不匹配!"); + return; + } + if (this.sentCode == null || !this.sentCode.equals(verificationCodeField.getText())) { + statusLabel.setText("验证码错误!"); + return; + } + if (!UserService.isPasswordValid(passwordField.getText())) { + statusLabel.setText("密码格式错误!必须为6-10位,且包含大小写字母和数字。"); + return; + } + + // 2. 调用后端服务进行注册 + boolean success = userService.register( + usernameField.getText(), + emailField.getText(), + passwordField.getText() + ); + + // 3. 根据结果更新UI + if (success) { + statusLabel.setText("注册成功!请返回登录。"); + registerButton.setDisable(true); + } else { + statusLabel.setText("注册失败:用户名或邮箱已被占用。"); + } + } + + @FXML + private void handleBackToLoginAction(ActionEvent event) { + loadScene("/com/mathgenerator/view/LoginView.fxml"); + } + + private void loadScene(String fxmlPath) { + try { + Parent root = FXMLLoader.load(getClass().getResource(fxmlPath)); + Stage stage = (Stage) backToLoginButton.getScene().getWindow(); + stage.setScene(new Scene(root)); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/controller/ScoreController.java b/src/main/java/com/mathgenerator/controller/ScoreController.java new file mode 100644 index 0000000..dbcba69 --- /dev/null +++ b/src/main/java/com/mathgenerator/controller/ScoreController.java @@ -0,0 +1,77 @@ +package com.mathgenerator.controller; + +import com.mathgenerator.model.User; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.stage.Stage; +import java.io.IOException; + +public class ScoreController { + + private User currentUser; + + @FXML private Label scoreLabel; + @FXML private Label resultMessageLabel; + @FXML private Button tryAgainButton; + @FXML private Button logoutButton; + + /** + * 初始化控制器,接收答题结果数据 + * @param user 当前用户 + * @param score 最终得分 + */ + public void initData(User user, double score) { + this.currentUser = user; + scoreLabel.setText(String.format("%.2f", score)); + + // 根据分数显示不同的鼓励语 + if (score == 100.0) { + resultMessageLabel.setText("太棒了!你答对了所有题目!"); + } else if (score >= 80) { + resultMessageLabel.setText("非常不错!继续努力!"); + } else if (score >= 60) { + resultMessageLabel.setText("成绩合格,再接再厉!"); + } else { + resultMessageLabel.setText("别灰心,下次会更好的!"); + } + } + + /** + * 处理“再做一组”按钮事件,返回主菜单 + */ + @FXML + private void handleTryAgainAction(ActionEvent event) { + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/MainMenuView.fxml")); + Parent root = loader.load(); + MainMenuController controller = loader.getController(); + controller.initData(currentUser); // 将用户信息传回主菜单 + + Stage stage = (Stage) tryAgainButton.getScene().getWindow(); + stage.setScene(new Scene(root)); + stage.setTitle("主菜单"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 处理“退出登录”按钮事件,返回登录界面 + */ + @FXML + private void handleLogoutAction(ActionEvent event) { + try { + Parent root = FXMLLoader.load(getClass().getResource("/com/mathgenerator/view/LoginView.fxml")); + Stage stage = (Stage) logoutButton.getScene().getWindow(); + stage.setScene(new Scene(root)); + stage.setTitle("用户登录"); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file -- 2.34.1 From 8e1a61566f019c53f1e260d1cba08c12cbd8a4bd Mon Sep 17 00:00:00 2001 From: hnu202326010328 <2655155213@qq.com> Date: Wed, 8 Oct 2025 17:11:28 +0800 Subject: [PATCH 02/13] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=87=B3=20'src/main/java/com/mathgenerator/generator'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../generator/JuniorHighSchoolGenerator.java | 66 ++++++ .../generator/PrimarySchoolGenerator.java | 196 ++++++++++++++++++ .../generator/QuestionGenerator.java | 14 ++ .../generator/SafePrimarySchoolGenerator.java | 149 +++++++++++++ .../generator/SeniorHighSchoolGenerator.java | 58 ++++++ 5 files changed, 483 insertions(+) create mode 100644 src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java create mode 100644 src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java create mode 100644 src/main/java/com/mathgenerator/generator/QuestionGenerator.java create mode 100644 src/main/java/com/mathgenerator/generator/SafePrimarySchoolGenerator.java create mode 100644 src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java diff --git a/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java b/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java new file mode 100644 index 0000000..add01a1 --- /dev/null +++ b/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java @@ -0,0 +1,66 @@ +package com.mathgenerator.generator; + +import com.mathgenerator.model.ChoiceQuestion; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 初中选择题生成器 (最终版 - 采用结构化插入)。 + * 通过直接在表达式结构中插入运算项,确保语法正确性和高性能。 + */ +public class JuniorHighSchoolGenerator extends PrimarySchoolGenerator { + + private static final int[] PERFECT_SQUARES = {1, 4, 9, 16, 25, 36, 49, 64, 81, 100}; + + @Override + public ChoiceQuestion generateSingleQuestion() { + ThreadLocalRandom random = ThreadLocalRandom.current(); + int operandCount = random.nextInt(2, 5); // 2到4个操作数 + + // 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; // 随机选择一个操作数的位置 + 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方法 + } + + 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)); + + return new ChoiceQuestion(finalQuestionText, options, correctIndex); + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java b/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java new file mode 100644 index 0000000..9ed1061 --- /dev/null +++ b/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java @@ -0,0 +1,196 @@ +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; +import java.text.DecimalFormat; + +/** + * 小学选择题生成器。 + * 生成包含 + - * / 和 () 的运算,并提供四个选项。 + */ +public class PrimarySchoolGenerator implements QuestionGenerator { + + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + + /** + * 生成一道小学难度的数学选择题。 + * @return 一个包含题干、四个选项和正确答案索引的 ChoiceQuestion 对象。 + */ + @Override + public ChoiceQuestion generateSingleQuestion() { + ThreadLocalRandom random = ThreadLocalRandom.current(); + int operandCount = random.nextInt(2, 5); // 2到4个操作数 + + String questionText; + int correctAnswer; + + // 循环直到生成一个可以计算出整数结果的表达式,避免类似 5/2 的情况 + while (true) { + List parts = new ArrayList<>(); + parts.add(String.valueOf(getOperand())); + for (int i = 1; i < operandCount; i++) { + parts.add(getRandomOperator()); + parts.add(String.valueOf(getOperand())); + } + if (operandCount > 2 && random.nextBoolean()) { + addParentheses(parts); + } + questionText = String.join(" ", parts); + + try { + // 使用脚本引擎计算表达式的精确值 + Object result = evaluateExpression(questionText); + // 确保结果是整数且没有余数 + if (result instanceof Number && ((Number) result).doubleValue() == ((Number) result).intValue()) { + correctAnswer = ((Number) result).intValue(); + break; // 成功计算,跳出循环 + } + } catch (Exception e) { + // 忽略异常 (如除以零),重新生成表达式 + } + } + + List options = generateOptions(correctAnswer); + int correctIndex = options.indexOf(String.valueOf(correctAnswer)); + + return new ChoiceQuestion(questionText, options, correctIndex); + } + + /** + * 生成四个选项 (1个正确,3个干扰项) + * @param correctAnswer 正确答案 + * @return 包含四个选项的随机排序列表 + */ + protected List generateOptions(int correctAnswer) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + Set options = new HashSet<>(); + options.add(String.valueOf(correctAnswer)); + + // 生成3个不重复的干扰项 + while (options.size() < 4) { + int delta = random.nextInt(1, 11); // 答案加减1-10 + // 随机决定是加还是减,增加干扰项的多样性 + int distractor = random.nextBoolean() ? correctAnswer + delta : correctAnswer - delta; + options.add(String.valueOf(distractor)); + } + + List sortedOptions = new ArrayList<>(options); + Collections.shuffle(sortedOptions); // 随机打乱选项顺序 + return sortedOptions; + } + + /** + * 使用JVM的脚本引擎计算字符串表达式的值 (已优化兼容性)。 + * @param expression 数学表达式字符串 + * @return 计算结果 (可能是Integer或Double) + * @throws ScriptException 如果表达式有语法错误 + */ + 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不需要预定义函数,可以直接计算 + String script = expression.replaceAll("(\\d+(\\.\\d+)?)²", "Math.pow($1, 2)") + .replaceAll("√(\\d+(\\.\\d+)?)", "Math.sqrt($1)") + .replaceAll("(\\d+)°", " * (Math.PI / 180)"); // Rhino对角度计算的语法要求更严格 + + // 为了让sin/cos/tan能正确计算,需要特殊处理 + script = script.replaceAll("sin\\(", "Math.sin(") + .replaceAll("cos\\(", "Math.cos(") + .replaceAll("tan\\(", "Math.tan("); + + return engine.eval(script); + } + + /** + * 格式化数字,最多保留两位小数。 + * @param number 待格式化的数字 + * @return 格式化后的字符串 + */ + protected String formatNumber(double number) { + if (number == (long) number) { + return String.format("%d", (long) number); // 如果是整数,不显示小数位 + } else { + // 使用DecimalFormat来去除末尾多余的0 + DecimalFormat df = new DecimalFormat("#.##"); + return df.format(number); + } + } + + /** + * 为小数答案生成四个选项。 + * @param correctAnswer 正确答案 + * @return 包含四个选项的随机排序列表 + */ + protected List generateDecimalOptions(double correctAnswer) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + Set options = new HashSet<>(); + options.add(formatNumber(correctAnswer)); + + while (options.size() < 4) { + double delta = random.nextDouble(1, 11); // 答案加减1-10之间的随机小数 + // 随机决定是加还是减 + double distractor = random.nextBoolean() ? correctAnswer + delta : correctAnswer - delta; + options.add(formatNumber(distractor)); + } + + List sortedOptions = new ArrayList<>(options); + Collections.shuffle(sortedOptions); + return sortedOptions; + } + + protected int getOperand() { + return ThreadLocalRandom.current().nextInt(1, 101); + } + + protected String getRandomOperator() { + return OPERATORS[ThreadLocalRandom.current().nextInt(OPERATORS.length)]; + } + + protected void addParentheses(List parts) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + int startOperandIndex = random.nextInt(parts.size() / 2); + int endOperandIndex = random.nextInt(startOperandIndex + 1, parts.size() / 2 + 1); + int startIndex = startOperandIndex * 2; + int endIndex = endOperandIndex * 2; + parts.add(endIndex + 1, ")"); + parts.add(startIndex, "("); + } + + // 在 PrimarySchoolGenerator.java 中添加这个方法 + /** + * 仅生成题目字符串,不包含答案和选项。 + * 这是为了方便子类(初中、高中)继承和修改题干。 + * @return 题目文本字符串 + */ + public String generateBasicQuestionText() { + ThreadLocalRandom random = ThreadLocalRandom.current(); + int operandCount = random.nextInt(2, 5); + List parts = new ArrayList<>(); + parts.add(String.valueOf(getOperand())); + for (int i = 1; i < operandCount; i++) { + parts.add(getRandomOperator()); + parts.add(String.valueOf(getOperand())); + } + if (operandCount > 2 && random.nextBoolean()) { + addParentheses(parts); + } + return String.join(" ", parts); + } + +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/generator/QuestionGenerator.java b/src/main/java/com/mathgenerator/generator/QuestionGenerator.java new file mode 100644 index 0000000..e478038 --- /dev/null +++ b/src/main/java/com/mathgenerator/generator/QuestionGenerator.java @@ -0,0 +1,14 @@ +package com.mathgenerator.generator; + +import com.mathgenerator.model.ChoiceQuestion; // 导入新模型 + +/** + * 题目生成器接口,定义了所有具体生成器必须实现的方法。 + */ +public interface QuestionGenerator { + /** + * 生成一道符合特定难度的数学选择题。 + * @return 代表数学选择题的 ChoiceQuestion 对象 + */ + ChoiceQuestion generateSingleQuestion(); +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/generator/SafePrimarySchoolGenerator.java b/src/main/java/com/mathgenerator/generator/SafePrimarySchoolGenerator.java new file mode 100644 index 0000000..d38cee6 --- /dev/null +++ b/src/main/java/com/mathgenerator/generator/SafePrimarySchoolGenerator.java @@ -0,0 +1,149 @@ +package com.mathgenerator.generator; + +import com.mathgenerator.model.ChoiceQuestion; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +/** + * “安全”的小学题目生成器,确保运算过程中不产生负数,并生成选择题。 + */ +public class SafePrimarySchoolGenerator implements QuestionGenerator { + + /** + * 生成一道确保结果非负的小学难度的数学选择题。 + * @return 一个符合所有约束的 ChoiceQuestion 对象。 + */ + @Override + public ChoiceQuestion generateSingleQuestion() { + int operandsTotalBudget = ThreadLocalRandom.current().nextInt(2, 6); + Term finalExpression = generateSafeExpression(operandsTotalBudget); + String questionText = String.join(" ", finalExpression.parts()); + int correctAnswer = finalExpression.value(); + + List options = generateOptions(correctAnswer); + int correctIndex = options.indexOf(String.valueOf(correctAnswer)); + + return new ChoiceQuestion(questionText, options, correctIndex); + } + + /** + * 内部记录类,用于在递归生成表达式时传递部分结果。 + */ + private record Term(List parts, int value, int operandsUsed) {} + + /** + * 根据操作数预算,生成一个确保结果非负的(子)表达式。 + */ + private Term generateSafeExpression(int operandBudget) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + List fullExpressionParts = new ArrayList<>(); + int operandsRemaining = operandBudget; + + Term firstTerm = generateTerm(operandsRemaining); + fullExpressionParts.addAll(firstTerm.parts()); + int currentResult = firstTerm.value(); + operandsRemaining -= firstTerm.operandsUsed(); + + while (operandsRemaining > 0) { + Term nextTerm = generateTerm(operandsRemaining); + boolean useAddition = random.nextBoolean(); + if (!useAddition && currentResult < nextTerm.value()) { + useAddition = true; // 强制改为加法以避免负数 + } + + if (useAddition) { + fullExpressionParts.add("+"); + currentResult += nextTerm.value(); + } else { + fullExpressionParts.add("-"); + currentResult -= nextTerm.value(); + } + fullExpressionParts.addAll(nextTerm.parts()); + operandsRemaining -= nextTerm.operandsUsed(); + } + return new Term(fullExpressionParts, currentResult, operandBudget - operandsRemaining); + } + + /** + * 生成一个“项”(Term)。一个项可以是简单的乘除法序列,也可以是带括号的子表达式。 + */ + private Term generateTerm(int operandsRemaining) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + if (operandsRemaining >= 2 && random.nextInt(10) < 3) { // 30%概率生成括号 + int subExpressionBudget = random.nextInt(2, operandsRemaining + 1); + Term subExpression = generateSafeExpression(subExpressionBudget); + + List parts = new ArrayList<>(); + parts.add("("); + parts.addAll(subExpression.parts()); + parts.add(")"); + return new Term(parts, subExpression.value(), subExpression.operandsUsed()); + } else { + return generateSimpleTerm(operandsRemaining); + } + } + + /** + * 生成一个仅包含乘除法的简单项。 + */ + private Term generateSimpleTerm(int operandsRemaining) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + List parts = new ArrayList<>(); + int termValue = random.nextInt(1, 21); + parts.add(String.valueOf(termValue)); + int operandsUsed = 1; + + if (operandsRemaining > 1 && random.nextBoolean()) { + if (random.nextBoolean()) { + parts.add("*"); + int multiplier = random.nextInt(1, 10); + parts.add(String.valueOf(multiplier)); + termValue *= multiplier; + } else { + parts.add("/"); + List divisors = getDivisors(termValue); + int divisor = divisors.get(random.nextInt(divisors.size())); + parts.add(String.valueOf(divisor)); + termValue /= divisor; + } + operandsUsed++; + } + return new Term(parts, termValue, operandsUsed); + } + + private List getDivisors(int number) { + List divisors = new ArrayList<>(); + for (int i = 1; i <= number; i++) { + if (number % i == 0) divisors.add(i); + } + return divisors; + } + + /** + * 生成四个选项 (1个正确,3个干扰项) + */ + private List generateOptions(int correctAnswer) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + Set options = new HashSet<>(); + options.add(String.valueOf(correctAnswer)); + + while (options.size() < 4) { + int delta = random.nextInt(1, 11); + int distractor = random.nextBoolean() ? correctAnswer + delta : correctAnswer - delta; + // 确保干扰项非负 + if (distractor >= 0) { + options.add(String.valueOf(distractor)); + } + } + List sortedOptions = new ArrayList<>(options); + Collections.shuffle(sortedOptions); + return sortedOptions; + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java b/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java new file mode 100644 index 0000000..4dc6be4 --- /dev/null +++ b/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java @@ -0,0 +1,58 @@ +package com.mathgenerator.generator; + +import com.mathgenerator.model.ChoiceQuestion; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 高中选择题生成器 (最终版 - 采用构造式添加)。 + * 通过在已生成的初中题基础上添加预设的三角函数项来构造题目。 + */ +public class SeniorHighSchoolGenerator extends JuniorHighSchoolGenerator { + + // 预设计算结果简单的三角函数项及其对应的值 + private static final Map TRIG_TERMS = Map.of( + "sin(0°)", 0.0, + "sin(30°)", 0.5, + "sin(90°)", 1.0, + "cos(0°)", 1.0, + "cos(60°)", 0.5, + "cos(90°)", 0.0, + "tan(45°)", 1.0 + ); + // 将Map的键转换为数组,方便随机选取 + 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(); + // 直接从初中题的答案反推其数值 + double juniorCorrectAnswer = Double.parseDouble(juniorHighQuestion.options().get(juniorHighQuestion.correctOptionIndex())); + + // 2. 随机选择一个预设的三角函数项 + String trigKey = TRIG_KEYS[ThreadLocalRandom.current().nextInt(TRIG_KEYS.length)]; + double trigValue = TRIG_TERMS.get(trigKey); + + // 3. 构造最终的高中题目 + String finalQuestionText; + double finalCorrectAnswer; + boolean useAddition = ThreadLocalRandom.current().nextBoolean(); // 随机决定是加还是减 + + if (useAddition) { + finalQuestionText = "(" + juniorQuestionText + ") + " + trigKey; + finalCorrectAnswer = juniorCorrectAnswer + trigValue; + } else { + finalQuestionText = "(" + juniorQuestionText + ") - " + trigKey; + finalCorrectAnswer = juniorCorrectAnswer - trigValue; + } + + // 4. 为最终的题目生成选项 + List options = generateDecimalOptions(finalCorrectAnswer); + int correctIndex = options.indexOf(formatNumber(finalCorrectAnswer)); + + return new ChoiceQuestion(finalQuestionText, options, correctIndex); + } +} \ No newline at end of file -- 2.34.1 From bf3b9c24349000c2ca11644bfa0414d1653a434d Mon Sep 17 00:00:00 2001 From: hnu202326010328 <2655155213@qq.com> Date: Wed, 8 Oct 2025 17:12:19 +0800 Subject: [PATCH 03/13] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=87=B3=20'src/main/java/com/mathgenerator/model'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mathgenerator/model/ChoiceQuestion.java | 12 ++++++++ .../java/com/mathgenerator/model/Level.java | 28 +++++++++++++++++++ .../java/com/mathgenerator/model/User.java | 10 +++++++ 3 files changed, 50 insertions(+) create mode 100644 src/main/java/com/mathgenerator/model/ChoiceQuestion.java create mode 100644 src/main/java/com/mathgenerator/model/Level.java create mode 100644 src/main/java/com/mathgenerator/model/User.java diff --git a/src/main/java/com/mathgenerator/model/ChoiceQuestion.java b/src/main/java/com/mathgenerator/model/ChoiceQuestion.java new file mode 100644 index 0000000..485b2ec --- /dev/null +++ b/src/main/java/com/mathgenerator/model/ChoiceQuestion.java @@ -0,0 +1,12 @@ +package com.mathgenerator.model; + +import java.util.List; + +/** + * 选择题数据模型。 + * @param questionText 题干 + * @param options 四个选项的列表 + * @param correctOptionIndex 正确答案在列表中的索引 (0-3) + */ +public record ChoiceQuestion(String questionText, List options, int correctOptionIndex) { +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/model/Level.java b/src/main/java/com/mathgenerator/model/Level.java new file mode 100644 index 0000000..8b27203 --- /dev/null +++ b/src/main/java/com/mathgenerator/model/Level.java @@ -0,0 +1,28 @@ +package com.mathgenerator.model; + +/** + * 学段枚举,用于类型安全地表示小学、初中和高中。 + */ +public enum Level { + PRIMARY("小学"), + JUNIOR_HIGH("初中"), + SENIOR_HIGH("高中"); + + private final String chineseName; + /** + * 枚举的构造函数。 + * + * @param chineseName 学段对应的中文名称。 + */ + Level(String chineseName) { + this.chineseName = chineseName; + } + /** + * 获取学段的中文名称。 + * + * @return 表示学段的中文名称字符串。 + */ + public String getChineseName() { + return chineseName; + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/model/User.java b/src/main/java/com/mathgenerator/model/User.java new file mode 100644 index 0000000..66ecbd3 --- /dev/null +++ b/src/main/java/com/mathgenerator/model/User.java @@ -0,0 +1,10 @@ +package com.mathgenerator.model; + +/** + * 用户数据记录 (Record),用于封装不可变的用户信息。 + * @param username 用户名 + * @param email 邮箱地址 (新增) + * @param password 密码 + */ +public record User(String username, String email, String password) { +} \ No newline at end of file -- 2.34.1 From a89b8983f9c602955b5a02fb6073108ca1c32e8a Mon Sep 17 00:00:00 2001 From: hnu202326010328 <2655155213@qq.com> Date: Wed, 8 Oct 2025 17:12:54 +0800 Subject: [PATCH 04/13] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=87=B3=20'src/main/java/com/mathgenerator/service'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mathgenerator/service/EmailConfig.java | 35 ++++ .../service/MixedDifficultyStrategy.java | 38 +++++ .../mathgenerator/service/PaperService.java | 68 ++++++++ .../mathgenerator/service/PaperStrategy.java | 18 ++ .../mathgenerator/service/UserService.java | 155 ++++++++++++++++++ 5 files changed, 314 insertions(+) create mode 100644 src/main/java/com/mathgenerator/service/EmailConfig.java create mode 100644 src/main/java/com/mathgenerator/service/MixedDifficultyStrategy.java create mode 100644 src/main/java/com/mathgenerator/service/PaperService.java create mode 100644 src/main/java/com/mathgenerator/service/PaperStrategy.java create mode 100644 src/main/java/com/mathgenerator/service/UserService.java diff --git a/src/main/java/com/mathgenerator/service/EmailConfig.java b/src/main/java/com/mathgenerator/service/EmailConfig.java new file mode 100644 index 0000000..44618f8 --- /dev/null +++ b/src/main/java/com/mathgenerator/service/EmailConfig.java @@ -0,0 +1,35 @@ +package com.mathgenerator.service; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +public class EmailConfig { + private static final Properties properties = new Properties(); + + static { + try (InputStream input = new FileInputStream("config.properties")) { + properties.load(input); + } catch (IOException ex) { + System.err.println("错误:无法加载 config.properties 文件!请确保该文件在项目根目录中。"); + ex.printStackTrace(); + } + } + + public static String getHost() { + return properties.getProperty("smtp.host"); + } + + public static int getPort() { + return Integer.parseInt(properties.getProperty("smtp.port")); + } + + public static String getUsername() { + return properties.getProperty("smtp.username"); + } + + public static String getPassword() { + return properties.getProperty("smtp.password"); + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/service/MixedDifficultyStrategy.java b/src/main/java/com/mathgenerator/service/MixedDifficultyStrategy.java new file mode 100644 index 0000000..cc7d78f --- /dev/null +++ b/src/main/java/com/mathgenerator/service/MixedDifficultyStrategy.java @@ -0,0 +1,38 @@ +package com.mathgenerator.service.strategy; + +import com.mathgenerator.generator.*; +import com.mathgenerator.model.Level; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 混合难度策略的具体实现。 + * (已更新,会根据主难度选择不同的小学基础生成器) + */ +public class MixedDifficultyStrategy implements PaperStrategy { + // 持有所有可能的生成器 + private final QuestionGenerator primaryGenerator = new PrimarySchoolGenerator(); + private final QuestionGenerator safePrimaryGenerator = new SafePrimarySchoolGenerator(); // 新增 + private final QuestionGenerator juniorHighGenerator = new JuniorHighSchoolGenerator(); + private final QuestionGenerator seniorHighGenerator = new SeniorHighSchoolGenerator(); + + @Override + public QuestionGenerator selectGenerator(Level mainLevel) { + double randomValue = ThreadLocalRandom.current().nextDouble(); + + return switch (mainLevel) { + // 当主难度是小学时,100%使用“安全”的生成器 + case PRIMARY -> safePrimaryGenerator; + case JUNIOR_HIGH -> { + // 初中试卷:70%初中难度,30%使用“不安全”的小学难度(允许负数) + if (randomValue < 0.7) yield juniorHighGenerator; + else yield primaryGenerator; + } + case SENIOR_HIGH -> { + // 高中试卷:60%高中,30%初中,10%使用“不安全”的小学难度 + if (randomValue < 0.6) yield seniorHighGenerator; + else if (randomValue < 0.9) yield juniorHighGenerator; + else yield primaryGenerator; + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/service/PaperService.java b/src/main/java/com/mathgenerator/service/PaperService.java new file mode 100644 index 0000000..3df7045 --- /dev/null +++ b/src/main/java/com/mathgenerator/service/PaperService.java @@ -0,0 +1,68 @@ +package com.mathgenerator.service; + +import com.mathgenerator.model.User; +import com.mathgenerator.model.Level; +import com.mathgenerator.model.ChoiceQuestion; // 导入新模型 +import com.mathgenerator.service.strategy.PaperStrategy; +import com.mathgenerator.storage.FileManager; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * 试卷服务,现在处理ChoiceQuestion对象。 + */ +public class PaperService { + private final FileManager fileManager; + private final PaperStrategy paperStrategy; + + public PaperService(FileManager fileManager, PaperStrategy paperStrategy) { + this.fileManager = fileManager; + this.paperStrategy = paperStrategy; + } + + /** + * 创建一份包含选择题的试卷。 + * @param user 当前用户 + * @param count 题目数量 + * @param currentLevel 当前难度 + * @return 生成的选择题列表 + */ + public List createPaper(User user, int count, Level currentLevel) { + // 查重集合现在存储题干字符串 + Set existingQuestionTexts = fileManager.loadExistingQuestions(user.username()); + List newPaper = new ArrayList<>(); + Set generatedInSession = new HashSet<>(); + + System.out.println("正在根据策略生成选择题,请稍候..."); + while (newPaper.size() < count) { + // 1. 生成的是ChoiceQuestion对象 + ChoiceQuestion question = paperStrategy.selectGenerator(currentLevel).generateSingleQuestion(); + String questionText = question.questionText(); // 提取题干用于查重 + + // 2. 使用题干进行查重 + if (!existingQuestionTexts.contains(questionText) && !generatedInSession.contains(questionText)) { + newPaper.add(question); + generatedInSession.add(questionText); + } + } + return newPaper; + } + + /** + * 将生成的试卷保存到文件。 + * @param username 用户名 + * @param paper 试卷题目列表 + */ + public void savePaper(String username, List paper) { + try { + String filePath = fileManager.savePaper(username, paper); + System.out.println("成功!" + paper.size() + "道数学题目已生成。"); + System.out.println("文件已保存至: " + filePath); + } catch (IOException e) { + System.err.println("错误:保存文件失败 - " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/service/PaperStrategy.java b/src/main/java/com/mathgenerator/service/PaperStrategy.java new file mode 100644 index 0000000..80d2ac4 --- /dev/null +++ b/src/main/java/com/mathgenerator/service/PaperStrategy.java @@ -0,0 +1,18 @@ +package com.mathgenerator.service.strategy; + +import com.mathgenerator.generator.QuestionGenerator; +import com.mathgenerator.model.Level; + +/** + * 试卷组合策略接口。 + * 封装了如何根据主难度来选择具体题目生成器的算法。 + */ +public interface PaperStrategy { + /** + * 根据用户选择的主难度,选择一个具体的题目生成器。 + * + * @param mainLevel 用户选择的主难度级别。 + * @return 一个根据策略选择出的QuestionGenerator实例。 + */ + QuestionGenerator selectGenerator(Level mainLevel); +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/service/UserService.java b/src/main/java/com/mathgenerator/service/UserService.java new file mode 100644 index 0000000..7a6375b --- /dev/null +++ b/src/main/java/com/mathgenerator/service/UserService.java @@ -0,0 +1,155 @@ +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 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; +import org.apache.commons.mail.Email; +import org.apache.commons.mail.EmailException; +import org.apache.commons.mail.SimpleEmail; + + +public class UserService { + private static final Path USER_FILE_PATH = Paths.get("users.json"); + // 密码策略: 6-10位, 必须包含大小写字母和数字 + private static final Pattern PASSWORD_PATTERN = + Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{6,10}$"); + private Map userDatabase; + private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + public UserService() { + this.userDatabase = loadUsersFromFile(); + } + + private Map loadUsersFromFile() { + try { + if (Files.exists(USER_FILE_PATH) && Files.size(USER_FILE_PATH) > 0) { + try (FileReader reader = new FileReader(USER_FILE_PATH.toFile())) { + Type type = new TypeToken>() {}.getType(); + Map loadedUsers = gson.fromJson(reader, type); + return loadedUsers != null ? new ConcurrentHashMap<>(loadedUsers) : new ConcurrentHashMap<>(); + } + } + } catch (IOException e) { + System.err.println("错误:加载用户文件失败 - " + e.getMessage()); + } + return new ConcurrentHashMap<>(); + } + + private void saveUsers() { + try (FileWriter writer = new FileWriter(USER_FILE_PATH.toFile())) { + gson.toJson(this.userDatabase, writer); + } catch (IOException e) { + System.err.println("错误:保存用户文件失败 - " + e.getMessage()); + } + } + + public Optional findUserByUsername(String username) { + return Optional.ofNullable(this.userDatabase.get(username)); + } + + public Optional login(String username, String password) { + return findUserByUsername(username) + .filter(user -> user.password().equals(password)); + } + + /** + * (已更新) 发送真实的邮件验证码。 + * @param email 用户的邮箱 + * @return 成功发送则返回生成的6位验证码, 失败则返回null + */ + public String sendVerificationCode(String email) { + String code = String.format("%06d", ThreadLocalRandom.current().nextInt(100000, 1000000)); + + try { + Email mail = new SimpleEmail(); + + // 1. 设置SMTP服务器信息 + mail.setHostName(EmailConfig.getHost()); + mail.setSmtpPort(EmailConfig.getPort()); + mail.setAuthentication(EmailConfig.getUsername(), EmailConfig.getPassword()); + mail.setSSLOnConnect(true); // 开启SSL加密 + + // 2. 设置邮件内容 + mail.setFrom(EmailConfig.getUsername()); // 发件人 + mail.setSubject("【数学学习软件】您的注册验证码"); // 邮件主题 + mail.setMsg("您好!\n\n感谢您注册数学学习软件。您的验证码是:" + code + "\n\n请在5分钟内使用。"); // 邮件正文 + mail.addTo(email); // 收件人 + + // 3. 发送邮件 + mail.send(); + + System.out.println("验证码邮件已成功发送至: " + email); + return code; + + } catch (EmailException e) { + System.err.println("错误:发送验证码邮件失败!请检查您的 config.properties 配置或网络连接。"); + e.printStackTrace(); + return null; // 发送失败 + } + } + + /** + * 注册新用户。 + * @return 成功返回true, 否则返回false + */ + public boolean register(String username, String email, String password) { + // 1. 基础校验:防止 null 或空白输入 + if (username == null || email == null || password == null || + username.trim().isEmpty() || email.trim().isEmpty() || password.trim().isEmpty()) { + return false; + } + + // 2. 检查用户名或邮箱是否已存在(使用 Objects.equals 安全比较) + boolean usernameExists = userDatabase.containsKey(username); + boolean emailExists = userDatabase.values().stream() + .anyMatch(u -> Objects.equals(u.email(), email)); + + if (usernameExists || emailExists) { + return false; // 用户名或邮箱已存在 + } + + // 3. 创建新用户并保存 + User newUser = new User(username, email, password); + userDatabase.put(username, newUser); + saveUsers(); + return true; + } + + /** + * 验证密码是否符合复杂度要求。 + * @param password 待验证的密码 + * @return true如果符合要求 + */ + public static boolean isPasswordValid(String password) { + return password != null && PASSWORD_PATTERN.matcher(password).matches(); + } + + /** + * 修改密码。 + * @return 成功返回true + */ + public boolean changePassword(String username, String oldPassword, String newPassword) { + return findUserByUsername(username) + .filter(user -> user.password().equals(oldPassword)) + .map(user -> { + User updatedUser = new User(user.username(), user.email(), newPassword); + userDatabase.put(username, updatedUser); + saveUsers(); + return true; + }).orElse(false); + } +} \ No newline at end of file -- 2.34.1 From b60526a20843b1ca7951470e8b33ad1c6f630fc2 Mon Sep 17 00:00:00 2001 From: hnu202326010328 <2655155213@qq.com> Date: Wed, 8 Oct 2025 17:13:17 +0800 Subject: [PATCH 05/13] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=87=B3=20'src/main/java/com/mathgenerator/storage'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mathgenerator/storage/FileManager.java | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/main/java/com/mathgenerator/storage/FileManager.java diff --git a/src/main/java/com/mathgenerator/storage/FileManager.java b/src/main/java/com/mathgenerator/storage/FileManager.java new file mode 100644 index 0000000..a1bb4a3 --- /dev/null +++ b/src/main/java/com/mathgenerator/storage/FileManager.java @@ -0,0 +1,89 @@ +package com.mathgenerator.storage; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.mathgenerator.model.ChoiceQuestion; // 导入新模型 + +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 负责文件读写,现已支持JSON格式的ChoiceQuestion对象。 + */ +public class FileManager { + private static final Path BASE_PATH = Paths.get("generated_papers"); + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"); + private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + /** + * 将生成的选择题试卷以JSON格式保存到文件。 + * @param username 用户名 + * @param paperContent 包含ChoiceQuestion对象的试卷列表 + * @return 保存成功后的文件路径 + */ + public String savePaper(String username, List paperContent) throws IOException { + Path userDir = BASE_PATH.resolve(username); + Files.createDirectories(userDir); + + String timestamp = LocalDateTime.now().format(FORMATTER); + String fileName = timestamp + ".json"; // 文件后缀改为 .json + Path filePath = userDir.resolve(fileName); + + try (FileWriter writer = new FileWriter(filePath.toFile())) { + gson.toJson(paperContent, writer); + } + return filePath.toString(); + } + + /** + * 加载指定用户的所有历史题目的题干文本,用于查重。 + * @param username 用户名 + * @return 包含所有历史题干文本的Set集合 + */ + public Set loadExistingQuestions(String username) { + Path userDir = BASE_PATH.resolve(username); + if (!Files.exists(userDir)) { + return new HashSet<>(); + } + + try (Stream stream = Files.walk(userDir)) { + return stream + .filter(file -> !Files.isDirectory(file) && file.toString().endsWith(".json")) // 只读取 .json 文件 + .flatMap(this::readQuestionsFromFile) // 使用方法引用 + .map(ChoiceQuestion::questionText) // 提取每个对象的题干文本 + .collect(Collectors.toSet()); + } catch (IOException e) { + System.err.println("错误:读取历史文件失败 - " + e.getMessage()); + return new HashSet<>(); + } + } + + /** + * 从单个JSON文件中读取并解析ChoiceQuestion对象列表。 + * @param file 要读取的单个试卷文件的路径对象 (Path)。 + * @return 一个包含该文件中所有ChoiceQuestion对象的流 (Stream)。 + */ + private Stream readQuestionsFromFile(Path file) { + try (FileReader reader = new FileReader(file.toFile())) { + Type listType = new TypeToken>() {}.getType(); + List questions = gson.fromJson(reader, listType); + return questions != null ? questions.stream() : Stream.empty(); + } catch (IOException e) { + System.err.println("错误:读取或解析文件 " + file + " 失败 - " + e.getMessage()); + return Stream.empty(); + } + } +} \ No newline at end of file -- 2.34.1 From 150b2e6fb5adf97ce8b977139c4f440b97e5dacc Mon Sep 17 00:00:00 2001 From: hnu202326010328 <2655155213@qq.com> Date: Wed, 8 Oct 2025 17:13:31 +0800 Subject: [PATCH 06/13] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=87=B3=20'src/main/java/com/mathgenerator'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/mathgenerator/MainApplication.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/main/java/com/mathgenerator/MainApplication.java diff --git a/src/main/java/com/mathgenerator/MainApplication.java b/src/main/java/com/mathgenerator/MainApplication.java new file mode 100644 index 0000000..cb6f216 --- /dev/null +++ b/src/main/java/com/mathgenerator/MainApplication.java @@ -0,0 +1,25 @@ +package com.mathgenerator; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; +import java.io.IOException; + +public class MainApplication extends Application { + + @Override + public void start(Stage primaryStage) throws IOException { + // 启动时加载登录界面 + Parent root = FXMLLoader.load(getClass().getResource("/com/mathgenerator/view/LoginView.fxml")); + primaryStage.setTitle("中小学数学学习软件"); + primaryStage.setScene(new Scene(root)); + primaryStage.setResizable(false); + primaryStage.show(); + } + + public static void main(String[] args) { + launch(args); + } +} \ No newline at end of file -- 2.34.1 From 673f3741774a4a459fd2f6ae5f2c6493d6d3ce35 Mon Sep 17 00:00:00 2001 From: hnu202326010328 <2655155213@qq.com> Date: Wed, 8 Oct 2025 17:15:40 +0800 Subject: [PATCH 07/13] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=87=B3=20'src/main/resources/com/mathgenerator/view'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../view/ChangePasswordView.fxml | 47 +++++++++++++ .../com/mathgenerator/view/MainMenuView.fxml | 50 ++++++++++++++ .../com/mathgenerator/view/QuizView.fxml | 68 +++++++++++++++++++ .../com/mathgenerator/view/RegisterView.fxml | 40 +++++++++++ .../com/mathgenerator/view/ScoreView.fxml | 44 ++++++++++++ 5 files changed, 249 insertions(+) create mode 100644 src/main/resources/com/mathgenerator/view/ChangePasswordView.fxml create mode 100644 src/main/resources/com/mathgenerator/view/MainMenuView.fxml create mode 100644 src/main/resources/com/mathgenerator/view/QuizView.fxml create mode 100644 src/main/resources/com/mathgenerator/view/RegisterView.fxml create mode 100644 src/main/resources/com/mathgenerator/view/ScoreView.fxml diff --git a/src/main/resources/com/mathgenerator/view/ChangePasswordView.fxml b/src/main/resources/com/mathgenerator/view/ChangePasswordView.fxml new file mode 100644 index 0000000..c6d967b --- /dev/null +++ b/src/main/resources/com/mathgenerator/view/ChangePasswordView.fxml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/com/mathgenerator/view/RegisterView.fxml b/src/main/resources/com/mathgenerator/view/RegisterView.fxml new file mode 100644 index 0000000..082d3fe --- /dev/null +++ b/src/main/resources/com/mathgenerator/view/RegisterView.fxml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + +