From 26a79f09399b5179ab91a1a33df0e6155cb3baad Mon Sep 17 00:00:00 2001 From: xiaoh <2893666874@qq.com> Date: Mon, 6 Oct 2025 23:14:51 +0800 Subject: [PATCH 1/9] =?UTF-8?q?=E7=AC=AC=E4=B8=80=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 - .../personalproject/MathExamApplication.java | 330 ++++++++++++++++++ .../auth/AccountRepository.java | 140 ++++++++ .../personalproject/auth/EmailService.java | 62 ++++ .../auth/PasswordValidator.java | 30 ++ src/com/personalproject/auth/UserAccount.java | 40 +++ .../controller/MathLearningController.java | 145 ++++++++ .../HighSchoolQuestionGenerator.java | 37 ++ .../MiddleSchoolQuestionGenerator.java | 39 +++ .../generator/PrimaryQuestionGenerator.java | 31 ++ .../generator/QuestionGenerator.java | 17 + .../model/DifficultyLevel.java | 49 +++ .../personalproject/model/ExamSession.java | 195 +++++++++++ .../personalproject/model/QuizQuestion.java | 73 ++++ .../service/ExamResultService.java | 83 +++++ .../personalproject/service/ExamService.java | 140 ++++++++ .../service/MathExpressionEvaluator.java | 217 ++++++++++++ .../service/MathLearningService.java | 170 +++++++++ .../service/QuestionGenerationService.java | 71 ++++ .../service/RegistrationService.java | 138 ++++++++ .../storage/QuestionStorageService.java | 162 +++++++++ 21 files changed, 2169 insertions(+), 2 deletions(-) delete mode 100644 README.md create mode 100644 src/com/personalproject/MathExamApplication.java create mode 100644 src/com/personalproject/auth/AccountRepository.java create mode 100644 src/com/personalproject/auth/EmailService.java create mode 100644 src/com/personalproject/auth/PasswordValidator.java create mode 100644 src/com/personalproject/auth/UserAccount.java create mode 100644 src/com/personalproject/controller/MathLearningController.java create mode 100644 src/com/personalproject/generator/HighSchoolQuestionGenerator.java create mode 100644 src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java create mode 100644 src/com/personalproject/generator/PrimaryQuestionGenerator.java create mode 100644 src/com/personalproject/generator/QuestionGenerator.java create mode 100644 src/com/personalproject/model/DifficultyLevel.java create mode 100644 src/com/personalproject/model/ExamSession.java create mode 100644 src/com/personalproject/model/QuizQuestion.java create mode 100644 src/com/personalproject/service/ExamResultService.java create mode 100644 src/com/personalproject/service/ExamService.java create mode 100644 src/com/personalproject/service/MathExpressionEvaluator.java create mode 100644 src/com/personalproject/service/MathLearningService.java create mode 100644 src/com/personalproject/service/QuestionGenerationService.java create mode 100644 src/com/personalproject/service/RegistrationService.java create mode 100644 src/com/personalproject/storage/QuestionStorageService.java diff --git a/README.md b/README.md deleted file mode 100644 index f2b9cdd..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# math-learing - diff --git a/src/com/personalproject/MathExamApplication.java b/src/com/personalproject/MathExamApplication.java new file mode 100644 index 0000000..35faa4c --- /dev/null +++ b/src/com/personalproject/MathExamApplication.java @@ -0,0 +1,330 @@ +package com.personalproject; + +import com.personalproject.controller.MathLearningController; +import com.personalproject.generator.HighSchoolQuestionGenerator; +import com.personalproject.generator.MiddleSchoolQuestionGenerator; +import com.personalproject.generator.PrimaryQuestionGenerator; +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; +import com.personalproject.service.QuestionGenerationService; +import java.util.EnumMap; +import java.util.Map; +import java.util.Optional; +import java.util.Scanner; + +/** + * 数学学习软件主应用程序入口. 采用MVC架构模式,此为主类,控制器层处理业务逻辑. + */ +public final class MathExamApplication { + + private final MathLearningController controller; + + /** + * 构造函数:初始化难度与题目生成器的映射关系,并创建控制器. + */ + public MathExamApplication() { + Map generatorMap = new EnumMap<>(DifficultyLevel.class); + generatorMap.put(DifficultyLevel.PRIMARY, new PrimaryQuestionGenerator()); + generatorMap.put(DifficultyLevel.MIDDLE, new MiddleSchoolQuestionGenerator()); + generatorMap.put(DifficultyLevel.HIGH, new HighSchoolQuestionGenerator()); + QuestionGenerationService questionGenerationService = new QuestionGenerationService( + generatorMap); + this.controller = new MathLearningController(generatorMap, questionGenerationService); + } + + /** + * 程序入口:创建应用实例并启动交互流程. + * + * @param args 命令行参数,当前未使用. + */ + public static void main(String[] args) { + MathExamApplication application = new MathExamApplication(); + application.run(); + } + + /** + * 主循环:保持读取输入并驱动登录及考试流程. + */ + private void run() { + Scanner scanner = new Scanner(System.in); + while (true) { + System.out.println("=== 欢迎使用数学学习软件 ==="); + System.out.println("1. 登录"); + System.out.println("2. 注册"); + System.out.println("3. 退出"); + System.out.print("请选择操作 (1-3): "); + + String choice = scanner.nextLine().trim(); + + switch (choice) { + case "1" -> handleLogin(scanner); + case "2" -> handleRegistration(scanner); + case "3" -> { + System.out.println("感谢使用,再见!"); + return; + } + default -> System.out.println("无效选择,请重新输入。"); + } + } + } + + /** + * 处理用户登录流程. + * + * @param scanner 读取控制台输入的扫描器 + */ + private void handleLogin(Scanner scanner) { + System.out.print("请输入用户名: "); + String username = scanner.nextLine().trim(); + + System.out.print("请输入密码: "); + String password = scanner.nextLine().trim(); + + Optional userAccount = + controller.authenticate(username, password); + + if (userAccount.isPresent()) { + System.out.println("登录成功!欢迎, " + userAccount.get().username()); + handleUserSession(scanner, userAccount.get()); + } else { + System.out.println("登录失败:用户名或密码错误。"); + } + } + + /** + * 处理用户注册流程. + * + * @param scanner 读取控制台输入的扫描器 + */ + private void handleRegistration(Scanner scanner) { + System.out.print("请输入用户名: "); + final String username = scanner.nextLine().trim(); + + System.out.print("请输入邮箱: "); + final String email = scanner.nextLine().trim(); + + if (!controller.isValidEmail(email)) { + System.out.println("邮箱格式不正确。"); + return; + } + + System.out.println("请选择难度级别:"); + System.out.println("1. 小学"); + System.out.println("2. 初中"); + System.out.println("3. 高中"); + System.out.print("请选择 (1-3): "); + + int levelChoice = -1; + try { + levelChoice = Integer.parseInt(scanner.nextLine().trim()); + } catch (NumberFormatException e) { + System.out.println("输入格式不正确。"); + return; + } + + DifficultyLevel difficultyLevel; + switch (levelChoice) { + case 1 -> difficultyLevel = DifficultyLevel.PRIMARY; + case 2 -> difficultyLevel = DifficultyLevel.MIDDLE; + case 3 -> difficultyLevel = DifficultyLevel.HIGH; + default -> { + System.out.println("无效的选择。"); + return; + } + } + + // 发送注册码 + if (controller.initiateRegistration(username, email, difficultyLevel)) { + System.out.println("注册码已发送至您的邮箱,请查收。"); + + System.out.print("请输入收到的注册码: "); + String registrationCode = scanner.nextLine().trim(); + + if (controller.verifyRegistrationCode(username, registrationCode)) { + System.out.println("注册码验证成功!"); + + // 设置密码 + while (true) { + System.out.print("请设置密码 (6-10位,包含大小写字母和数字): "); + String password = scanner.nextLine().trim(); + + if (!controller.isValidPassword(password)) { + System.out.println("密码不符合要求,请重新设置。"); + continue; + } + + if (controller.setPassword(username, password)) { + System.out.println("注册成功!请登录。"); + break; + } else { + System.out.println("设置密码失败,请重试。"); + } + } + } else { + System.out.println("注册码验证失败,请检查后重试。"); + } + } else { + System.out.println("注册失败:用户名或邮箱可能已存在。"); + } + } + + /** + * 处理用户会话,包括考试等操作. + * + * @param scanner 读取控制台输入的扫描器 + * @param userAccount 当前登录的用户账户 + */ + private void handleUserSession(Scanner scanner, + com.personalproject.auth.UserAccount userAccount) { + while (true) { + System.out.println("\n=== 用户菜单 ==="); + System.out.println("1. 开始考试"); + System.out.println("2. 修改密码"); + System.out.println("3. 退出账号"); + System.out.print("请选择操作 (1-3): "); + + String choice = scanner.nextLine().trim(); + + switch (choice) { + case "1" -> handleExamSession(scanner, userAccount); + case "2" -> handleChangePassword(scanner, userAccount); + case "3" -> { + System.out.println("已退出账号。"); + return; + } + default -> System.out.println("无效选择,请重新输入。"); + } + } + } + + /** + * 处理考试会话. + * + * @param scanner 读取控制台输入的扫描器 + * @param userAccount 当前登录的用户账户 + */ + private void handleExamSession(Scanner scanner, + com.personalproject.auth.UserAccount userAccount) { + System.out.println("当前选择难度: " + userAccount.difficultyLevel().getDisplayName()); + + System.out.print("请输入生成题目数量 (10-30): "); + int questionCount = -1; + try { + questionCount = Integer.parseInt(scanner.nextLine().trim()); + } catch (NumberFormatException e) { + System.out.println("输入格式不正确。"); + return; + } + + if (questionCount < 10 || questionCount > 30) { + System.out.println("题目数量必须在10到30之间。"); + return; + } + + // 创建考试会话 + ExamSession examSession = + controller.createExamSession(userAccount.username(), userAccount.difficultyLevel(), + questionCount); + + // 进行考试 + conductExam(scanner, examSession); + + // 保存结果 + controller.saveExamResults(examSession); + + System.out.printf("考试结束!您的得分: %.2f%%\n", examSession.calculateScore()); + + // 询问用户是否继续或退出 + System.out.println("1. 继续考试"); + System.out.println("2. 退出"); + System.out.print("请选择 (1-2): "); + + String choice = scanner.nextLine().trim(); + if (choice.equals("1")) { + handleExamSession(scanner, userAccount); + } + } + + /** + * 通过导航问题进行考试. + * + * @param scanner 用户输入的扫描器 + * @param examSession 要进行的考试会话 + */ + private void conductExam(Scanner scanner, ExamSession examSession) { + while (!examSession.isComplete()) { + var currentQuestion = examSession.getCurrentQuestion(); + System.out.printf("\n第 %d 题: %s\n", examSession.getCurrentQuestionIndex() + 1, + currentQuestion.getQuestionText()); + + // 打印选项 + for (int i = 0; i < currentQuestion.getOptions().size(); i++) { + System.out.printf("%d. %s\n", i + 1, currentQuestion.getOptions().get(i)); + } + + System.out.print( + "请选择答案 (1-" + currentQuestion.getOptions().size() + ", 0 返回上一题): "); + int choice = -1; + try { + choice = Integer.parseInt(scanner.nextLine().trim()); + } catch (NumberFormatException e) { + System.out.println("输入格式不正确,请重新输入。"); + continue; + } + + if (choice == 0) { + // 如果可能,转到上一个问题 + if (!examSession.goToPreviousQuestion()) { + System.out.println("已经是第一题了。"); + } + continue; + } + + if (choice < 1 || choice > currentQuestion.getOptions().size()) { + System.out.println("无效的选择,请重新输入。"); + continue; + } + + // 设置答案(从基于1的索引调整为基于0的索引) + examSession.setAnswer(choice - 1); + + // 转到下一题 + if (!examSession.goToNextQuestion()) { + // 如果无法转到下一题,意味着已到达末尾 + break; + } + } + } + + /** + * 处理用户的密码更改. + * + * @param scanner 读取控制台输入的扫描器 + * @param userAccount 当前登录的用户账户 + */ + private void handleChangePassword(Scanner scanner, + com.personalproject.auth.UserAccount userAccount) { + System.out.print("请输入当前密码: "); + String oldPassword = scanner.nextLine().trim(); + + if (!userAccount.password().equals(oldPassword)) { + System.out.println("当前密码错误。"); + return; + } + + System.out.print("请输入新密码 (6-10位,包含大小写字母和数字): "); + String newPassword = scanner.nextLine().trim(); + + if (!controller.isValidPassword(newPassword)) { + System.out.println("新密码不符合要求。"); + return; + } + + if (controller.changePassword(userAccount.username(), oldPassword, newPassword)) { + System.out.println("密码修改成功!"); + } else { + System.out.println("密码修改失败。"); + } + } +} \ No newline at end of file diff --git a/src/com/personalproject/auth/AccountRepository.java b/src/com/personalproject/auth/AccountRepository.java new file mode 100644 index 0000000..c5a00a2 --- /dev/null +++ b/src/com/personalproject/auth/AccountRepository.java @@ -0,0 +1,140 @@ +package com.personalproject.auth; + +import com.personalproject.model.DifficultyLevel; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * 存放用户账号并提供认证和注册查询能力. + */ +public final class AccountRepository { + + private final Map accounts = new HashMap<>(); + private final Map registrationCodes = new HashMap<>(); + + /** + * 根据用户名与密码查找匹配的账号. + * + * @param username 用户名. + * @param password 密码. + * @return 匹配成功时返回账号信息,否则返回空结果. + */ + public Optional authenticate(String username, String password) { + if (username == null || password == null) { + return Optional.empty(); + } + UserAccount account = accounts.get(username.trim()); + if (account == null || !account.isRegistered()) { + return Optional.empty(); + } + if (!account.password().equals(password.trim())) { + return Optional.empty(); + } + return Optional.of(account); + } + + /** + * Registers a new user account with email. + * + * @param username The username + * @param email The email address + * @param difficultyLevel The selected difficulty level + * @return true if registration was successful, false if username already exists + */ + public boolean registerUser(String username, String email, DifficultyLevel difficultyLevel) { + String trimmedUsername = username.trim(); + String trimmedEmail = email.trim(); + + if (accounts.containsKey(trimmedUsername)) { + return false; // Username already exists + } + + // Check if email is already used by another account + for (UserAccount account : accounts.values()) { + if (account.email().equals(trimmedEmail) && account.isRegistered()) { + return false; // Email already registered + } + } + + UserAccount newAccount = new UserAccount( + trimmedUsername, + trimmedEmail, + "", // Empty password initially + difficultyLevel, + LocalDateTime.now(), + false); // Not registered until password is set + accounts.put(trimmedUsername, newAccount); + return true; + } + + /** + * Sets the password for a user after registration. + * + * @param username The username + * @param password The password to set + * @return true if successful, false if user doesn't exist + */ + public boolean setPassword(String username, String password) { + UserAccount account = accounts.get(username.trim()); + if (account == null) { + return false; + } + + UserAccount updatedAccount = new UserAccount( + account.username(), + account.email(), + password, + account.difficultyLevel(), + account.registrationDate(), + true); // Now registered + accounts.put(username.trim(), updatedAccount); + return true; + } + + /** + * Changes the password for an existing user. + * + * @param username The username + * @param oldPassword The current password + * @param newPassword The new password + * @return true if successful, false if old password is incorrect or user doesn't exist + */ + public boolean changePassword(String username, String oldPassword, String newPassword) { + UserAccount account = accounts.get(username.trim()); + if (account == null || !account.password().equals(oldPassword) || !account.isRegistered()) { + return false; + } + + UserAccount updatedAccount = new UserAccount( + account.username(), + account.email(), + newPassword, + account.difficultyLevel(), + account.registrationDate(), + true); + accounts.put(username.trim(), updatedAccount); + return true; + } + + /** + * Checks if a user exists in the system. + * + * @param username The username to check + * @return true if user exists, false otherwise + */ + public boolean userExists(String username) { + return accounts.containsKey(username.trim()); + } + + /** + * Gets a user account by username. + * + * @param username The username + * @return Optional containing the user account if found + */ + public Optional getUser(String username) { + return Optional.ofNullable(accounts.get(username.trim())); + } +} diff --git a/src/com/personalproject/auth/EmailService.java b/src/com/personalproject/auth/EmailService.java new file mode 100644 index 0000000..d790c83 --- /dev/null +++ b/src/com/personalproject/auth/EmailService.java @@ -0,0 +1,62 @@ +package com.personalproject.auth; + +import java.util.Random; + +/** + * Interface for sending emails with registration codes. + */ +public final class EmailService { + + private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + private static final int CODE_LENGTH = 6; + private static final Random RANDOM = new Random(); + + private EmailService() { + // Prevent instantiation of utility class + } + + /** + * Generates a random registration code. + * + * @return A randomly generated registration code + */ + public static String generateRegistrationCode() { + StringBuilder code = new StringBuilder(); + for (int i = 0; i < CODE_LENGTH; i++) { + code.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length()))); + } + return code.toString(); + } + + /** + * Sends a registration code to the specified email address. In a real implementation, this would + * connect to an email server. + * + * @param email The email address to send the code to + * @param registrationCode The registration code to send + * @return true if successfully sent (in this mock implementation, always true) + */ + public static boolean sendRegistrationCode(String email, String registrationCode) { + // In a real implementation, this would connect to an email server + // For the mock implementation, we'll just print to console + System.out.println("Sending registration code " + registrationCode + " to " + email); + return true; + } + + /** + * Validates if an email address has a valid format. + * + * @param email The email address to validate + * @return true if the email has valid format, false otherwise + */ + public static boolean isValidEmail(String email) { + if (email == null || email.trim().isEmpty()) { + return false; + } + + // Simple email validation using regex + String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@" + + "(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"; + return email.matches(emailRegex); + } +} \ No newline at end of file diff --git a/src/com/personalproject/auth/PasswordValidator.java b/src/com/personalproject/auth/PasswordValidator.java new file mode 100644 index 0000000..acece6f --- /dev/null +++ b/src/com/personalproject/auth/PasswordValidator.java @@ -0,0 +1,30 @@ +package com.personalproject.auth; + +import java.util.regex.Pattern; + +/** + * Utility class for password validation. + */ +public final class PasswordValidator { + + private static final Pattern PASSWORD_PATTERN = + Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{6,10}$"); + + private PasswordValidator() { + // Prevent instantiation of utility class + } + + /** + * Validates if a password meets the requirements: - 6-10 characters - Contains at least one + * uppercase letter - Contains at least one lowercase letter - Contains at least one digit. + * + * @param password The password to validate + * @return true if password meets requirements, false otherwise + */ + public static boolean isValidPassword(String password) { + if (password == null) { + return false; + } + return PASSWORD_PATTERN.matcher(password).matches(); + } +} \ No newline at end of file diff --git a/src/com/personalproject/auth/UserAccount.java b/src/com/personalproject/auth/UserAccount.java new file mode 100644 index 0000000..80d5acf --- /dev/null +++ b/src/com/personalproject/auth/UserAccount.java @@ -0,0 +1,40 @@ +package com.personalproject.auth; + +import com.personalproject.model.DifficultyLevel; +import java.time.LocalDateTime; + +/** + * 不可变的账号定义. + */ +public record UserAccount( + String username, + String email, + String password, + DifficultyLevel difficultyLevel, + LocalDateTime registrationDate, + boolean isRegistered) { + + /** + * Creates a new user account with registration date set to now. + * + * @param username The username + * @param email The email address + * @param password The password + * @param difficultyLevel The selected difficulty level + * @param isRegistered Whether the user has completed registration + */ + public UserAccount { + if (username == null || username.trim().isEmpty()) { + throw new IllegalArgumentException("Username cannot be null or empty"); + } + if (email == null || email.trim().isEmpty()) { + throw new IllegalArgumentException("Email cannot be null or empty"); + } + if (password == null) { + throw new IllegalArgumentException("Password cannot be null"); + } + if (difficultyLevel == null) { + throw new IllegalArgumentException("Difficulty level cannot be null"); + } + } +} diff --git a/src/com/personalproject/controller/MathLearningController.java b/src/com/personalproject/controller/MathLearningController.java new file mode 100644 index 0000000..c847b94 --- /dev/null +++ b/src/com/personalproject/controller/MathLearningController.java @@ -0,0 +1,145 @@ +package com.personalproject.controller; + +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; +import com.personalproject.service.ExamResultService; +import com.personalproject.service.MathLearningService; +import com.personalproject.service.QuestionGenerationService; +import java.util.Map; +import java.util.Optional; + +/** + * 遵循MVC模式处理应用程序逻辑的控制器类. + */ +public final class MathLearningController { + + private final MathLearningService mathLearningService; + + /** + * 创建具有所需服务的新控制器. + * + * @param generatorMap 难度级别到题目生成器的映射 + * @param questionGenerationService 题目生成服务 + */ + public MathLearningController( + Map generatorMap, + QuestionGenerationService questionGenerationService) { + this.mathLearningService = new MathLearningService(generatorMap, questionGenerationService); + } + + /** + * 启动用户注册. + * + * @param username 所需用户名 + * @param email 用户的电子邮箱地址 + * @param difficultyLevel 选择的难度级别 + * @return 注册成功启动则返回true,否则返回false + */ + public boolean initiateRegistration(String username, String email, + DifficultyLevel difficultyLevel) { + return mathLearningService.initiateRegistration(username, email, difficultyLevel); + } + + /** + * 验证注册码. + * + * @param username 用户名 + * @param registrationCode 注册码 + * @return 码有效则返回true,否则返回false + */ + public boolean verifyRegistrationCode(String username, String registrationCode) { + return mathLearningService.verifyRegistrationCode(username, registrationCode); + } + + /** + * 注册后设置用户密码. + * + * @param username 用户名 + * @param password 要设置的密码 + * @return 密码设置成功则返回true,否则返回false + */ + public boolean setPassword(String username, String password) { + return mathLearningService.setPassword(username, password); + } + + /** + * 验证用户. + * + * @param username 用户名 + * @param password 密码 + * @return 验证成功则返回包含用户账户的Optional + */ + public Optional authenticate(String username, + String password) { + return mathLearningService.authenticate(username, password); + } + + /** + * 为用户创建考试会话. + * + * @param username 用户名 + * @param difficultyLevel 难度级别 + * @param questionCount 题目数量 + * @return 创建的考试会话 + */ + public ExamSession createExamSession(String username, DifficultyLevel difficultyLevel, + int questionCount) { + return mathLearningService.createExamSession(username, difficultyLevel, questionCount); + } + + /** + * 保存考试结果. + * + * @param examSession 要保存的考试会话 + */ + public void saveExamResults(ExamSession examSession) { + mathLearningService.saveExamResults(examSession); + } + + /** + * 处理考试结果并确定下一步操作. + * + * @param examSession 完成的考试会话 + * @param continueWithSameLevel 是否继续相同难度 + * @param newDifficultyLevel 更改时的新难度(如果不更改则为null) + * @return 要执行的下一步操作 + */ + public ExamResultService.ExamContinuationAction processExamResult( + ExamSession examSession, boolean continueWithSameLevel, DifficultyLevel newDifficultyLevel) { + return mathLearningService.processExamResult(examSession, continueWithSameLevel, + newDifficultyLevel); + } + + /** + * 更改用户密码. + * + * @param username 用户名 + * @param oldPassword 旧密码 + * @param newPassword 新密码 + * @return 密码更改成功则返回true,否则返回false + */ + public boolean changePassword(String username, String oldPassword, String newPassword) { + return mathLearningService.changePassword(username, oldPassword, newPassword); + } + + /** + * 验证密码. + * + * @param password 要验证的密码 + * @return 密码有效则返回true,否则返回false + */ + public boolean isValidPassword(String password) { + return MathLearningService.isValidPassword(password); + } + + /** + * 验证电子邮箱地址. + * + * @param email 要验证的邮箱 + * @return 邮箱有效则返回true,否则返回false + */ + public boolean isValidEmail(String email) { + return MathLearningService.isValidEmail(email); + } +} \ No newline at end of file diff --git a/src/com/personalproject/generator/HighSchoolQuestionGenerator.java b/src/com/personalproject/generator/HighSchoolQuestionGenerator.java new file mode 100644 index 0000000..d22bf53 --- /dev/null +++ b/src/com/personalproject/generator/HighSchoolQuestionGenerator.java @@ -0,0 +1,37 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 生成至少包含一种三角函数的高中难度题目表达式. + */ +public final class HighSchoolQuestionGenerator implements QuestionGenerator { + + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + private static final String[] TRIG_FUNCTIONS = {"sin", "cos", "tan"}; + + @Override + public String generateQuestion(Random random) { + int operandCount = random.nextInt(5) + 1; + String[] operands = new String[operandCount]; + for (int index = 0; index < operandCount; index++) { + operands[index] = String.valueOf(random.nextInt(100) + 1); + } + int specialIndex = random.nextInt(operandCount); + String function = TRIG_FUNCTIONS[random.nextInt(TRIG_FUNCTIONS.length)]; + operands[specialIndex] = function + '(' + operands[specialIndex] + ')'; + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < operandCount; index++) { + if (index > 0) { + String operator = OPERATORS[random.nextInt(OPERATORS.length)]; + builder.append(' ').append(operator).append(' '); + } + builder.append(operands[index]); + } + String expression = builder.toString(); + if (operandCount > 1 && random.nextBoolean()) { + return '(' + expression + ')'; + } + return expression; + } +} diff --git a/src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java b/src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java new file mode 100644 index 0000000..ad46678 --- /dev/null +++ b/src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java @@ -0,0 +1,39 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 生成包含平方或开根号运算的初中难度题目表达式. + */ +public final class MiddleSchoolQuestionGenerator implements QuestionGenerator { + + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + + @Override + public String generateQuestion(Random random) { + int operandCount = random.nextInt(5) + 1; + String[] operands = new String[operandCount]; + for (int index = 0; index < operandCount; index++) { + operands[index] = String.valueOf(random.nextInt(100) + 1); + } + int specialIndex = random.nextInt(operandCount); + if (random.nextBoolean()) { + operands[specialIndex] = '(' + operands[specialIndex] + ")^2"; + } else { + operands[specialIndex] = "sqrt(" + operands[specialIndex] + ')'; + } + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < operandCount; index++) { + if (index > 0) { + String operator = OPERATORS[random.nextInt(OPERATORS.length)]; + builder.append(' ').append(operator).append(' '); + } + builder.append(operands[index]); + } + String expression = builder.toString(); + if (operandCount > 1 && random.nextBoolean()) { + return '(' + expression + ')'; + } + return expression; + } +} diff --git a/src/com/personalproject/generator/PrimaryQuestionGenerator.java b/src/com/personalproject/generator/PrimaryQuestionGenerator.java new file mode 100644 index 0000000..9073153 --- /dev/null +++ b/src/com/personalproject/generator/PrimaryQuestionGenerator.java @@ -0,0 +1,31 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 生成包含基础四则运算的小学难度题目表达式. + */ +public final class PrimaryQuestionGenerator implements QuestionGenerator { + + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + + @Override + public String generateQuestion(Random random) { + // 至少生成两个操作数,避免题目退化成单个数字 + int operandCount = random.nextInt(4) + 2; + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < operandCount; index++) { + if (index > 0) { + String operator = OPERATORS[random.nextInt(OPERATORS.length)]; + builder.append(' ').append(operator).append(' '); + } + int value = random.nextInt(100) + 1; + builder.append(value); + } + String expression = builder.toString(); + if (operandCount > 1 && random.nextBoolean()) { + return '(' + expression + ')'; + } + return expression; + } +} diff --git a/src/com/personalproject/generator/QuestionGenerator.java b/src/com/personalproject/generator/QuestionGenerator.java new file mode 100644 index 0000000..a2a5ea7 --- /dev/null +++ b/src/com/personalproject/generator/QuestionGenerator.java @@ -0,0 +1,17 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 负责生成单条数学题目的表达式. + */ +public interface QuestionGenerator { + + /** + * 基于提供的随机数生成器构造一道题目的表达式. + * + * @param random 用于生成随机数的实例. + * @return 生成的题目表达式. + */ + String generateQuestion(Random random); +} diff --git a/src/com/personalproject/model/DifficultyLevel.java b/src/com/personalproject/model/DifficultyLevel.java new file mode 100644 index 0000000..bc654bb --- /dev/null +++ b/src/com/personalproject/model/DifficultyLevel.java @@ -0,0 +1,49 @@ +package com.personalproject.model; + +import java.util.Optional; + +/** + * 系统支持的出题难度级别. + */ +public enum DifficultyLevel { + PRIMARY("小学"), + MIDDLE("初中"), + HIGH("高中"); + + private final String displayName; + + DifficultyLevel(String displayName) { + this.displayName = displayName; + } + + /** + * 获取当前难度对应的展示名称. + * + * @return 难度的展示名称. + */ + public String getDisplayName() { + return displayName; + } + + /** + * 根据展示名称查找对应的难度枚举值. + * + * @param name 展示名称. + * @return 匹配到的难度枚举,可空返回. + */ + public static Optional fromDisplayName(String name) { + if (name == null) { + return Optional.empty(); + } + String trimmed = name.trim(); + if (trimmed.isEmpty()) { + return Optional.empty(); + } + for (DifficultyLevel level : values()) { + if (level.displayName.equals(trimmed)) { + return Optional.of(level); + } + } + return Optional.empty(); + } +} diff --git a/src/com/personalproject/model/ExamSession.java b/src/com/personalproject/model/ExamSession.java new file mode 100644 index 0000000..6437ba5 --- /dev/null +++ b/src/com/personalproject/model/ExamSession.java @@ -0,0 +1,195 @@ +package com.personalproject.model; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents an exam session for a user. + */ +public final class ExamSession { + + private final String username; + private final DifficultyLevel difficultyLevel; + private final List questions; + private final List userAnswers; + private final LocalDateTime startTime; + private int currentQuestionIndex; + + /** + * Creates a new exam session. + * + * @param username The username of the test taker + * @param difficultyLevel The difficulty level of the exam + * @param questions The list of questions for the exam + */ + public ExamSession(String username, DifficultyLevel difficultyLevel, + List questions) { + if (username == null || username.trim().isEmpty()) { + throw new IllegalArgumentException("Username cannot be null or empty"); + } + if (difficultyLevel == null) { + throw new IllegalArgumentException("Difficulty level cannot be null"); + } + if (questions == null || questions.isEmpty()) { + throw new IllegalArgumentException("Questions list cannot be null or empty"); + } + + this.username = username; + this.difficultyLevel = difficultyLevel; + this.questions = List.copyOf(questions); // Immutable copy of questions + this.userAnswers = new ArrayList<>(); + // Initialize user answers with -1 (no answer selected) + for (int i = 0; i < questions.size(); i++) { + userAnswers.add(-1); + } + this.startTime = LocalDateTime.now(); + this.currentQuestionIndex = 0; + } + + /** + * Gets the username of the test taker. + * + * @return The username + */ + public String getUsername() { + return username; + } + + /** + * Gets the difficulty level of the exam. + * + * @return The difficulty level + */ + public DifficultyLevel getDifficultyLevel() { + return difficultyLevel; + } + + /** + * Gets the list of questions in the exam. + * + * @return An unmodifiable list of questions + */ + public List getQuestions() { + return questions; + } + + /** + * Gets the user's answers to the questions. + * + * @return A list of answer indices (-1 means no answer selected) + */ + public List getUserAnswers() { + return List.copyOf(userAnswers); // Return a copy to prevent modification + } + + /** + * Gets the current question index. + * + * @return The current question index + */ + public int getCurrentQuestionIndex() { + return currentQuestionIndex; + } + + /** + * Sets the user's answer for the current question. + * + * @param answerIndex The index of the selected answer + */ + public void setAnswer(int answerIndex) { + if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) { + throw new IllegalStateException("No valid question at current index"); + } + if (answerIndex < 0 || answerIndex > questions.get(currentQuestionIndex).getOptions().size()) { + throw new IllegalArgumentException("Invalid answer index"); + } + userAnswers.set(currentQuestionIndex, answerIndex); + } + + /** + * Moves to the next question. + * + * @return true if successfully moved to next question, false if already at the last question + */ + public boolean goToNextQuestion() { + if (currentQuestionIndex < questions.size() - 1) { + currentQuestionIndex++; + return true; + } + return false; + } + + /** + * Moves to the previous question. + * + * @return true if successfully moved to previous question, false if already at the first question + */ + public boolean goToPreviousQuestion() { + if (currentQuestionIndex > 0) { + currentQuestionIndex--; + return true; + } + return false; + } + + /** + * Checks if the exam is complete (all questions answered or at the end). + * + * @return true if the exam is complete, false otherwise + */ + public boolean isComplete() { + return currentQuestionIndex >= questions.size() - 1; + } + + /** + * Gets the current question. + * + * @return The current quiz question + */ + public QuizQuestion getCurrentQuestion() { + if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) { + throw new IllegalStateException("No valid question at current index"); + } + return questions.get(currentQuestionIndex); + } + + /** + * Gets the user's answer for a specific question. + * + * @param questionIndex The index of the question + * @return The index of the user's answer (or -1 if no answer selected) + */ + public int getUserAnswer(int questionIndex) { + if (questionIndex < 0 || questionIndex >= questions.size()) { + throw new IllegalArgumentException("Question index out of bounds"); + } + return userAnswers.get(questionIndex); + } + + /** + * Calculates the score as a percentage. + * + * @return The score as a percentage (0-100) + */ + public double calculateScore() { + int correctCount = 0; + for (int i = 0; i < questions.size(); i++) { + QuizQuestion question = questions.get(i); + int userAnswer = userAnswers.get(i); + if (userAnswer != -1 && question.isAnswerCorrect(userAnswer)) { + correctCount++; + } + } + return questions.isEmpty() ? 0.0 : (double) correctCount / questions.size() * 100.0; + } + + /** + * Gets the start time of the exam. + * + * @return The start time + */ + public LocalDateTime getStartTime() { + return startTime; + } +} \ No newline at end of file diff --git a/src/com/personalproject/model/QuizQuestion.java b/src/com/personalproject/model/QuizQuestion.java new file mode 100644 index 0000000..e66f14c --- /dev/null +++ b/src/com/personalproject/model/QuizQuestion.java @@ -0,0 +1,73 @@ +package com.personalproject.model; + +import java.util.List; + +/** + * Represents a quiz question with multiple choice options. + */ +public final class QuizQuestion { + + private final String questionText; + private final List options; + private final int correctAnswerIndex; + + /** + * Creates a new quiz question. + * + * @param questionText The text of the question + * @param options The list of answer options + * @param correctAnswerIndex The index of the correct answer in the options list + */ + public QuizQuestion(String questionText, List options, int correctAnswerIndex) { + if (questionText == null || questionText.trim().isEmpty()) { + throw new IllegalArgumentException("Question text cannot be null or empty"); + } + if (options == null || options.size() < 2) { + throw new IllegalArgumentException("Options must contain at least 2 choices"); + } + if (correctAnswerIndex < 0 || correctAnswerIndex >= options.size()) { + throw new IllegalArgumentException("Correct answer index out of bounds"); + } + + this.questionText = questionText; + this.options = List.copyOf(options); // Immutable copy + this.correctAnswerIndex = correctAnswerIndex; + } + + /** + * Gets the question text. + * + * @return The question text + */ + public String getQuestionText() { + return questionText; + } + + /** + * Gets the list of answer options. + * + * @return An unmodifiable list of answer options + */ + public List getOptions() { + return options; + } + + /** + * Gets the index of the correct answer in the options list. + * + * @return The index of the correct answer + */ + public int getCorrectAnswerIndex() { + return correctAnswerIndex; + } + + /** + * Checks if the given answer index matches the correct answer. + * + * @param answerIndex The index of the user's answer + * @return true if the answer is correct, false otherwise + */ + public boolean isAnswerCorrect(int answerIndex) { + return answerIndex == correctAnswerIndex; + } +} \ No newline at end of file diff --git a/src/com/personalproject/service/ExamResultService.java b/src/com/personalproject/service/ExamResultService.java new file mode 100644 index 0000000..55a79f6 --- /dev/null +++ b/src/com/personalproject/service/ExamResultService.java @@ -0,0 +1,83 @@ +package com.personalproject.service; + +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; + +/** + * 用于处理考试后选项(如继续或退出)的服务. + */ +public final class ExamResultService { + + /** + * 评估用户在完成考试后的选择. + * + * @param examSession 完成的考试会话 + * @param continueWithSameLevel 是否使用相同难度级别继续 + * @param newDifficultyLevel 更改时的新难度级别(如果不更改则为null) + * @return 指示下一步操作的操作 + */ + public ExamContinuationAction processExamResult( + ExamSession examSession, boolean continueWithSameLevel, DifficultyLevel newDifficultyLevel) { + + if (continueWithSameLevel) { + return new ExamContinuationAction( + true, examSession.getDifficultyLevel(), (int) examSession.getQuestions().size()); + } else if (newDifficultyLevel != null) { + return new ExamContinuationAction(true, newDifficultyLevel, + (int) examSession.getQuestions().size()); + } else { + return new ExamContinuationAction(false, null, 0); + } + } + + /** + * 表示完成考试后要采取的操作. + */ + public static final class ExamContinuationAction { + + private final boolean shouldContinue; + private final DifficultyLevel nextDifficultyLevel; + private final int nextQuestionCount; + + /** + * 创建新的考试延续操作. + * + * @param shouldContinue 用户是否想继续参加另一次考试 + * @param nextDifficultyLevel 下次考试的难度级别(如果不继续则为null) + * @param nextQuestionCount 下次考试的题目数量(如果不继续则为0) + */ + public ExamContinuationAction( + boolean shouldContinue, DifficultyLevel nextDifficultyLevel, int nextQuestionCount) { + this.shouldContinue = shouldContinue; + this.nextDifficultyLevel = nextDifficultyLevel; + this.nextQuestionCount = nextQuestionCount; + } + + /** + * 获取用户是否想继续参加另一次考试. + * + * @return 如果继续则为true,如果退出则为false + */ + public boolean shouldContinue() { + return shouldContinue; + } + + /** + * 获取下次考试的难度级别. + * + * @return 下次难度级别(如果不继续则为null) + */ + public DifficultyLevel getNextDifficultyLevel() { + return nextDifficultyLevel; + } + + /** + * 获取下次考试的题目数量. + * + * @return 下次题目数量(如果不继续则为0) + */ + public int getNextQuestionCount() { + return nextQuestionCount; + } + } +} \ No newline at end of file diff --git a/src/com/personalproject/service/ExamService.java b/src/com/personalproject/service/ExamService.java new file mode 100644 index 0000000..392a83a --- /dev/null +++ b/src/com/personalproject/service/ExamService.java @@ -0,0 +1,140 @@ +package com.personalproject.service; + +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; +import com.personalproject.model.QuizQuestion; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +/** + * 用于管理考试会话和生成测验题目的服务类. + */ +public final class ExamService { + + private static final int OPTIONS_COUNT = 4; + private final Map generators; + private final Random random = new Random(); + private final QuestionGenerationService questionGenerationService; + + /** + * 创建新的考试服务. + * + * @param generatorMap 难度级别到题目生成器的映射 + * @param questionGenerationService 题目生成服务 + */ + public ExamService( + Map generatorMap, + QuestionGenerationService questionGenerationService) { + this.generators = new EnumMap<>(DifficultyLevel.class); + this.generators.putAll(generatorMap); + this.questionGenerationService = questionGenerationService; + } + + /** + * 使用指定参数创建新的考试会话. + * + * @param username 考生用户名 + * @param difficultyLevel 考试难度级别 + * @param questionCount 考试中包含的题目数量 + * @return 新的考试会话 + */ + public ExamSession createExamSession( + String username, DifficultyLevel difficultyLevel, int questionCount) { + if (questionCount < 10 || questionCount > 30) { + throw new IllegalArgumentException("题目数量必须在10到30之间"); + } + + // 根据难度级别生成题目 + List generatedQuestions = new ArrayList<>(); + QuestionGenerator generator = generators.get(difficultyLevel); + if (generator == null) { + throw new IllegalArgumentException("找不到难度级别的生成器: " + difficultyLevel); + } + + for (int i = 0; i < questionCount; i++) { + String question = generator.generateQuestion(random); + generatedQuestions.add(question); + } + + // 将字符串题目转换为带有选项和答案的QuizQuestion对象 + List quizQuestions = new ArrayList<>(); + for (String questionText : generatedQuestions) { + List options = generateOptions(questionText); + int correctAnswerIndex = generateCorrectAnswerIndex(options.size()); + QuizQuestion quizQuestion = new QuizQuestion(questionText, options, correctAnswerIndex); + quizQuestions.add(quizQuestion); + } + + return new ExamSession(username, difficultyLevel, quizQuestions); + } + + /** + * 使用预定义题目创建考试会话(用于测试). + * + * @param username 考生用户名 + * @param difficultyLevel 考试难度级别 + * @param questions 预定义题目列表 + * @return 新的考试会话 + */ + public ExamSession createExamSession( + String username, DifficultyLevel difficultyLevel, List questions) { + if (questions.size() < 10 || questions.size() > 30) { + throw new IllegalArgumentException("题目数量必须在10到30之间"); + } + return new ExamSession(username, difficultyLevel, questions); + } + + /** + * 为给定题目生成多项选择选项. 这是一个模拟实现 - 在实际系统中,选项会根据题目生成. + * + * @param questionText 题目文本 + * @return 答案选项列表 + */ + private List generateOptions(String questionText) { + List options = new ArrayList<>(); + // 为每个题目生成4个选项 + try { + // 尝试将数学表达式作为正确答案进行评估 + double correctAnswer = MathExpressionEvaluator.evaluate(questionText); + + // 创建正确答案选项 + options.add(String.format("%.2f", correctAnswer)); + + // 生成3个错误选项 + for (int i = 0; i < OPTIONS_COUNT - 1; i++) { + double incorrectAnswer = correctAnswer + (random.nextGaussian() * 10); // 添加一些随机偏移 + if (Math.abs(incorrectAnswer - correctAnswer) < 0.1) { // 确保不同 + incorrectAnswer += 1.5; + } + options.add(String.format("%.2f", incorrectAnswer)); + } + } catch (Exception e) { + // 如果评估失败,创建虚拟选项 + for (int i = 0; i < OPTIONS_COUNT; i++) { + options.add("选项 " + (i + 1)); + } + } + + // 随机打乱选项以随机化正确答案的位置 + java.util.Collections.shuffle(options, random); + + // 找到打乱后的正确答案索引 + // 对于此模拟实现,我们将返回第一个选项(索引0)作为正确答案 + // 实际实现将跟踪正确答案 + return options; + } + + /** + * 为给定选项数量生成随机正确答案索引. + * + * @param optionCount 选项数量 + * @return 0到optionCount-1之间的随机索引 + */ + private int generateCorrectAnswerIndex(int optionCount) { + return random.nextInt(optionCount); + } +} \ No newline at end of file diff --git a/src/com/personalproject/service/MathExpressionEvaluator.java b/src/com/personalproject/service/MathExpressionEvaluator.java new file mode 100644 index 0000000..6e32ed1 --- /dev/null +++ b/src/com/personalproject/service/MathExpressionEvaluator.java @@ -0,0 +1,217 @@ +package com.personalproject.service; + +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; +import java.util.regex.Pattern; + +/** + * A mathematical expression evaluator that can handle basic arithmetic operations. + */ +public final class MathExpressionEvaluator { + + private static final Pattern NUMBER_PATTERN = Pattern.compile("-?\\d+(\\.\\d+)?"); + private static final Map PRECEDENCE = new HashMap<>(); + + static { + PRECEDENCE.put('+', 1); + PRECEDENCE.put('-', 1); + PRECEDENCE.put('*', 2); + PRECEDENCE.put('/', 2); + PRECEDENCE.put('^', 3); + } + + private MathExpressionEvaluator() { + // Prevent instantiation of utility class + } + + /** + * Evaluates a mathematical expression string. + * + * @param expression The mathematical expression to evaluate + * @return The result of the evaluation + * @throws IllegalArgumentException If the expression is invalid + */ + public static double evaluate(String expression) { + if (expression == null) { + throw new IllegalArgumentException("Expression cannot be null"); + } + + expression = expression.replaceAll("\\s+", ""); // Remove whitespace + if (expression.isEmpty()) { + throw new IllegalArgumentException("Expression cannot be empty"); + } + + // Tokenize the expression + String[] tokens = tokenize(expression); + + // Convert infix to postfix notation using Shunting Yard algorithm + String[] postfix = infixToPostfix(tokens); + + // Evaluate the postfix expression + return evaluatePostfix(postfix); + } + + /** + * Tokenizes the expression into numbers and operators. + * + * @param expression The expression to tokenize + * @return An array of tokens + */ + private static String[] tokenize(String expression) { + java.util.List tokens = new java.util.ArrayList<>(); + StringBuilder currentNumber = new StringBuilder(); + + for (int i = 0; i < expression.length(); i++) { + char c = expression.charAt(i); + + if (Character.isDigit(c) || c == '.') { + currentNumber.append(c); + } else if (c == '(' || c == ')') { + if (currentNumber.length() > 0) { + tokens.add(currentNumber.toString()); + currentNumber.setLength(0); + } + tokens.add(String.valueOf(c)); + } else if (isOperator(c)) { + if (currentNumber.length() > 0) { + tokens.add(currentNumber.toString()); + currentNumber.setLength(0); + } + + // Handle unary minus + if (c == '-' && (i == 0 || expression.charAt(i - 1) == '(')) { + currentNumber.append(c); + } else { + tokens.add(String.valueOf(c)); + } + } else { + throw new IllegalArgumentException("Invalid character in expression: " + c); + } + } + + if (currentNumber.length() > 0) { + tokens.add(currentNumber.toString()); + } + + return tokens.toArray(new String[0]); + } + + /** + * Checks if the character is an operator. + * + * @param c The character to check + * @return true if the character is an operator, false otherwise + */ + private static boolean isOperator(char c) { + return c == '+' || c == '-' || c == '*' || c == '/' || c == '^'; + } + + /** + * Converts infix notation to postfix notation using the Shunting Yard algorithm. + * + * @param tokens The tokens in infix notation + * @return An array of tokens in postfix notation + */ + private static String[] infixToPostfix(String[] tokens) { + java.util.List output = new java.util.ArrayList<>(); + Stack operators = new Stack<>(); + + for (String token : tokens) { + if (isNumber(token)) { + output.add(token); + } else if (token.equals("(")) { + operators.push(token); + } else if (token.equals(")")) { + while (!operators.isEmpty() && !operators.peek().equals("(")) { + output.add(operators.pop()); + } + if (!operators.isEmpty()) { + operators.pop(); // Remove the "(" + } + } else if (isOperator(token.charAt(0))) { + while (!operators.isEmpty() + && isOperator(operators.peek().charAt(0)) + && PRECEDENCE.get(operators.peek().charAt(0)) >= PRECEDENCE.get(token.charAt(0))) { + output.add(operators.pop()); + } + operators.push(token); + } + } + + while (!operators.isEmpty()) { + output.add(operators.pop()); + } + + return output.toArray(new String[0]); + } + + /** + * Evaluates a postfix expression. + * + * @param postfix The tokens in postfix notation + * @return The result of the evaluation + */ + private static double evaluatePostfix(String[] postfix) { + Stack values = new Stack<>(); + + for (String token : postfix) { + if (isNumber(token)) { + values.push(Double.parseDouble(token)); + } else if (isOperator(token.charAt(0))) { + if (values.size() < 2) { + throw new IllegalArgumentException("Invalid expression: insufficient operands"); + } + + double b = values.pop(); + double a = values.pop(); + double result = performOperation(a, b, token.charAt(0)); + values.push(result); + } + } + + if (values.size() != 1) { + throw new IllegalArgumentException("Invalid expression: too many operands"); + } + + return values.pop(); + } + + /** + * Performs the specified operation on the two operands. + * + * @param a The first operand + * @param b The second operand + * @param operator The operator to apply + * @return The result of the operation + */ + private static double performOperation(double a, double b, char operator) { + switch (operator) { + case '+': + return a + b; + case '-': + return a - b; + case '*': + return a * b; + case '/': + if (b == 0) { + throw new ArithmeticException("Division by zero"); + } + return a / b; + case '^': + return Math.pow(a, b); + default: + throw new IllegalArgumentException("Unknown operator: " + operator); + } + } + + /** + * Checks if the token is a number. + * + * @param token The token to check + * @return true if the token is a number, false otherwise + */ + private static boolean isNumber(String token) { + return NUMBER_PATTERN.matcher(token).matches(); + } +} \ No newline at end of file diff --git a/src/com/personalproject/service/MathLearningService.java b/src/com/personalproject/service/MathLearningService.java new file mode 100644 index 0000000..e404ddb --- /dev/null +++ b/src/com/personalproject/service/MathLearningService.java @@ -0,0 +1,170 @@ +package com.personalproject.service; + +import com.personalproject.auth.AccountRepository; +import com.personalproject.auth.EmailService; +import com.personalproject.auth.PasswordValidator; +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; +import com.personalproject.storage.QuestionStorageService; +import java.util.Map; +import java.util.Optional; + +/** + * 为主JavaFX UI提供所有功能的服务类. 此类协调各种服务以为UI提供统一的API. + */ +public final class MathLearningService { + + private final AccountRepository accountRepository; + private final RegistrationService registrationService; + private final ExamService examService; + private final QuestionStorageService storageService; + private final ExamResultService resultService; + + /** + * 创建一个新的数学学习服务. + * + * @param generatorMap 难度级别到题目生成器的映射 + * @param questionGenerationService 题目生成服务 + */ + public MathLearningService( + Map generatorMap, + QuestionGenerationService questionGenerationService) { + this.accountRepository = new AccountRepository(); + this.registrationService = new RegistrationService(accountRepository); + this.examService = new ExamService(generatorMap, questionGenerationService); + this.storageService = new QuestionStorageService(); + this.resultService = new ExamResultService(); + } + + // 注册方法 + + /** + * 通过向提供的电子邮件发送注册码来启动注册过程. + * + * @param username 所需用户名 + * @param email 要发送注册码的电子邮件地址 + * @param difficultyLevel 选择的难度级别 + * @return 如果注册成功启动则返回true,如果电子邮件无效或用户名已存在则返回false + */ + public boolean initiateRegistration(String username, String email, + DifficultyLevel difficultyLevel) { + return registrationService.initiateRegistration(username, email, difficultyLevel); + } + + /** + * 验证用户输入的注册码. + * + * @param username 用户名 + * @param registrationCode 通过电子邮件收到的注册码 + * @return 如果注册码有效则返回true,否则返回false + */ + public boolean verifyRegistrationCode(String username, String registrationCode) { + return registrationService.verifyRegistrationCode(username, registrationCode); + } + + /** + * 在成功验证注册码后为用户设置密码. + * + * @param username 用户名 + * @param password 要设置的密码 + * @return 如果密码设置成功则返回true,如果验证失败或用户不存在则返回false + */ + public boolean setPassword(String username, String password) { + return registrationService.setPassword(username, password); + } + + /** + * 使用用户名和密码验证用户. + * + * @param username 用户名 + * @param password 密码 + * @return 如果验证成功则返回包含用户账户的Optional + */ + public Optional authenticate(String username, + String password) { + return registrationService.authenticate(username, password); + } + + /** + * 检查密码是否符合验证要求. + * + * @param password 要验证的密码 + * @return 如果密码有效则返回true,否则返回false + */ + public static boolean isValidPassword(String password) { + return PasswordValidator.isValidPassword(password); + } + + /** + * 更改现有用户的密码. + * + * @param username 用户名 + * @param oldPassword 当前密码 + * @param newPassword 新密码 + * @return 如果密码更改成功则返回true,如果验证失败或验证失败则返回false + */ + public boolean changePassword(String username, String oldPassword, String newPassword) { + return registrationService.changePassword(username, oldPassword, newPassword); + } + + /** + * 检查电子邮件地址是否具有有效格式. + * + * @param email 要验证的电子邮件地址 + * @return 如果电子邮件格式有效则返回true,否则返回false + */ + public static boolean isValidEmail(String email) { + return EmailService.isValidEmail(email); + } + + /** + * 为指定用户和难度级别创建新的考试会话. + * + * @param username 应试者用户名 + * @param difficultyLevel 考试难度级别 + * @param questionCount 考试中包含的题目数量 + * @return 新的考试会话 + */ + public ExamSession createExamSession(String username, DifficultyLevel difficultyLevel, + int questionCount) { + return examService.createExamSession(username, difficultyLevel, questionCount); + } + + /** + * 将考试结果保存到存储中. + * + * @param examSession 完成的考试会话 + * @return 保存结果文件的路径 + */ + public java.nio.file.Path saveExamResults(ExamSession examSession) { + try { + return storageService.saveExamResults(examSession); + } catch (java.io.IOException e) { + throw new RuntimeException("保存考试结果失败", e); + } + } + + /** + * 处理用户完成考试后的选择. + * + * @param examSession 完成的考试会话 + * @param continueWithSameLevel 是否继续使用相同难度级别 + * @param newDifficultyLevel 更改时的新难度级别(如果不更改则为null) + * @return 表示下一步操作的动作 + */ + public ExamResultService.ExamContinuationAction processExamResult( + ExamSession examSession, boolean continueWithSameLevel, DifficultyLevel newDifficultyLevel) { + return resultService.processExamResult(examSession, continueWithSameLevel, newDifficultyLevel); + } + + /** + * 检查用户是否存在于系统中. + * + * @param username 要检查的用户名 + * @return 如果用户存在则返回true,否则返回false + */ + public boolean userExists(String username) { + return registrationService.userExists(username); + } +} \ No newline at end of file diff --git a/src/com/personalproject/service/QuestionGenerationService.java b/src/com/personalproject/service/QuestionGenerationService.java new file mode 100644 index 0000000..b536a7d --- /dev/null +++ b/src/com/personalproject/service/QuestionGenerationService.java @@ -0,0 +1,71 @@ +package com.personalproject.service; + +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +/** + * 负责批量生成题目并避免与历史题库重复. + */ +public final class QuestionGenerationService { + + private static final int MAX_ATTEMPTS = 10_000; + private final Map generators; + private final Random random = new SecureRandom(); + + /** + * 构造函数:复制难度与生成器的映射,保留内部安全副本. + * + * @param generatorMap 难度到题目生成器的外部映射. + */ + public QuestionGenerationService(Map generatorMap) { + generators = new EnumMap<>(DifficultyLevel.class); + generators.putAll(generatorMap); + } + + /** + * 根据指定难度批量生成不与历史题目重复的新题目. + * + * @param level 目标题目难度. + * @param count 需要生成的题目数量. + * @param existingQuestions 已存在的题目集合,用于查重. + * @return 生成的题目列表. + * @throws IllegalArgumentException 当难度未配置时抛出. + * @throws IllegalStateException 当达到最大尝试次数仍无法生成足够题目时抛出. + */ + public List generateUniqueQuestions( + DifficultyLevel level, int count, Set existingQuestions) { + QuestionGenerator generator = generators.get(level); + if (generator == null) { + throw new IllegalArgumentException("Unsupported difficulty level: " + level); + } + Set produced = new HashSet<>(); + List results = new ArrayList<>(); + int attempts = 0; + while (results.size() < count) { + if (attempts >= MAX_ATTEMPTS) { + throw new IllegalStateException("Unable to generate enough unique questions."); + } + attempts++; + String question = generator.generateQuestion(random).trim(); + if (question.isEmpty()) { + continue; + } + if (existingQuestions.contains(question)) { + continue; + } + if (!produced.add(question)) { + continue; + } + results.add(question); + } + return results; + } +} diff --git a/src/com/personalproject/service/RegistrationService.java b/src/com/personalproject/service/RegistrationService.java new file mode 100644 index 0000000..100b58d --- /dev/null +++ b/src/com/personalproject/service/RegistrationService.java @@ -0,0 +1,138 @@ +package com.personalproject.service; + +import com.personalproject.auth.AccountRepository; +import com.personalproject.auth.EmailService; +import com.personalproject.auth.PasswordValidator; +import com.personalproject.model.DifficultyLevel; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 用于处理用户注册和身份验证过程的服务类. + */ +public final class RegistrationService { + + private final AccountRepository accountRepository; + private final Map pendingRegistrations; + private final Map registrationAttempts; + + /** + * 创建新的注册服务. + * + * @param accountRepository 使用的账户存储库 + */ + public RegistrationService(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + this.pendingRegistrations = new ConcurrentHashMap<>(); + this.registrationAttempts = new ConcurrentHashMap<>(); + } + + /** + * 通过向提供的电子邮件发送注册码来启动注册过程. + * + * @param username 所需用户名 + * @param email 要发送注册码的电子邮件地址 + * @param difficultyLevel 选择的难度级别 + * @return 注册成功启动则返回true,如果电子邮件无效或用户名已存在则返回false + */ + public boolean initiateRegistration(String username, String email, + DifficultyLevel difficultyLevel) { + if (!EmailService.isValidEmail(email)) { + return false; + } + + if (!accountRepository.registerUser(username, email, difficultyLevel)) { + return false; // 用户名已存在或邮箱已注册 + } + + String registrationCode = EmailService.generateRegistrationCode(); + pendingRegistrations.put(username, registrationCode); + registrationAttempts.put(username, 0); + + return EmailService.sendRegistrationCode(email, registrationCode); + } + + /** + * 通过验证注册码完成注册. + * + * @param username 用户名 + * @param registrationCode 通过电子邮件收到的注册码 + * @return 注册码有效则返回true,否则返回false + */ + public boolean verifyRegistrationCode(String username, String registrationCode) { + String storedCode = pendingRegistrations.get(username); + if (storedCode == null || !storedCode.equals(registrationCode)) { + // 跟踪失败尝试 + int attempts = registrationAttempts.getOrDefault(username, 0); + attempts++; + registrationAttempts.put(username, attempts); + + if (attempts >= 3) { + // 如果失败次数过多,则删除用户 + pendingRegistrations.remove(username); + registrationAttempts.remove(username); + return false; + } + return false; + } + + // 有效码,从待处理列表中移除 + pendingRegistrations.remove(username); + registrationAttempts.remove(username); + return true; + } + + /** + * 在成功验证注册码后为用户设置密码. + * + * @param username 用户名 + * @param password 要设置的密码 + * @return 密码设置成功则返回true,如果验证失败或用户不存在则返回false + */ + public boolean setPassword(String username, String password) { + if (!PasswordValidator.isValidPassword(password)) { + return false; + } + + return accountRepository.setPassword(username, password); + } + + /** + * 更改现有用户的密码. + * + * @param username 用户名 + * @param oldPassword 当前密码 + * @param newPassword 新密码 + * @return 密码更改成功则返回true,如果验证失败或身份验证失败则返回false + */ + public boolean changePassword(String username, String oldPassword, String newPassword) { + if (!PasswordValidator.isValidPassword(newPassword)) { + return false; + } + + return accountRepository.changePassword(username, oldPassword, newPassword); + } + + /** + * 使用用户名和密码验证用户. + * + * @param username 用户名 + * @param password 密码 + * @return 验证成功则返回包含用户账户的Optional + */ + public Optional authenticate(String username, + String password) { + return accountRepository.authenticate(username, password); + } + + /** + * 检查用户是否存在于系统中. + * + * @param username 要检查的用户名 + * @return 用户存在则返回true,否则返回false + */ + public boolean userExists(String username) { + return accountRepository.userExists(username); + } +} \ No newline at end of file diff --git a/src/com/personalproject/storage/QuestionStorageService.java b/src/com/personalproject/storage/QuestionStorageService.java new file mode 100644 index 0000000..4fc9870 --- /dev/null +++ b/src/com/personalproject/storage/QuestionStorageService.java @@ -0,0 +1,162 @@ +package com.personalproject.storage; + +import com.personalproject.model.ExamSession; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +/** + * 负责保存生成的题目并维护查重信息. + */ +public final class QuestionStorageService { + + private static final String BASE_DIRECTORY = "user_data"; + private static final String QUESTIONS_SUBDIR = "questions"; + private static final String RESULTS_SUBDIR = "results"; + private static final DateTimeFormatter FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"); + + /** + * 加载指定用户历史生成的题目,用于后续查重. + * + * @param username 用户名. + * @return 历史题目的去重集合. + * @throws IOException 当读取文件时发生 I/O 错误. + */ + public Set loadExistingQuestions(String username) throws IOException { + Path accountDirectory = getQuestionsDirectory(username); + Set questions = new HashSet<>(); + if (!Files.exists(accountDirectory)) { + return questions; + } + try (Stream paths = Files.list(accountDirectory)) { + paths + .filter(path -> path.getFileName().toString().endsWith(".txt")) + .sorted() + .forEach(path -> readQuestionsFromFile(path, questions)); + } + return questions; + } + + /** + * 将新生成的题目保存到用户目录,并返回生成的文件路径. + * + * @param username 用户名. + * @param questions 待保存的题目列表. + * @return 保存题目的文件路径. + * @throws IOException 当写入文件失败时抛出. + */ + public Path saveQuestions(String username, List questions) throws IOException { + Path accountDirectory = getQuestionsDirectory(username); + Files.createDirectories(accountDirectory); + String fileName = FORMATTER.format(LocalDateTime.now()) + ".txt"; + Path outputFile = accountDirectory.resolve(fileName); + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < questions.size(); index++) { + String question = questions.get(index); + builder + .append(index + 1) + .append(". ") + .append(question) + .append(System.lineSeparator()) + .append(System.lineSeparator()); + } + Files.writeString( + outputFile, builder.toString(), StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW); + return outputFile; + } + + /** + * 保存用户的考试结果. + * + * @param examSession 包含结果的考试会话 + * @return 保存结果文件的路径 + * @throws IOException 如果写入文件失败 + */ + public Path saveExamResults(ExamSession examSession) throws IOException { + Path resultsDirectory = getResultsDirectory(examSession.getUsername()); + Files.createDirectories(resultsDirectory); + + StringBuilder builder = new StringBuilder(); + builder.append("考试结果报告").append(System.lineSeparator()); + builder.append("用户名: ").append(examSession.getUsername()).append(System.lineSeparator()); + builder.append("难度: ").append(examSession.getDifficultyLevel().getDisplayName()) + .append(System.lineSeparator()); + builder.append("开始时间: ").append(examSession.getStartTime()).append(System.lineSeparator()); + builder.append("题目数量: ").append(examSession.getQuestions().size()) + .append(System.lineSeparator()); + builder.append("得分: ").append(String.format("%.2f", examSession.calculateScore())).append("%") + .append(System.lineSeparator()); + builder.append(System.lineSeparator()); + + // 添加逐题结果 + for (int i = 0; i < examSession.getQuestions().size(); i++) { + var question = examSession.getQuestions().get(i); + int userAnswer = examSession.getUserAnswer(i); + boolean isCorrect = question.isAnswerCorrect(userAnswer); + + builder.append("题目 ").append(i + 1).append(": ").append(question.getQuestionText()) + .append(System.lineSeparator()); + builder.append("您的答案: ").append(userAnswer == -1 ? "未回答" : + (userAnswer < question.getOptions().size() ? question.getOptions().get(userAnswer) + : "无效")).append(System.lineSeparator()); + builder.append("正确答案: ").append( + question.getOptions().get(question.getCorrectAnswerIndex())) + .append(System.lineSeparator()); + builder.append("结果: ").append(isCorrect ? "正确" : "错误").append(System.lineSeparator()); + builder.append(System.lineSeparator()); + } + + String fileName = "exam_result_" + FORMATTER.format(LocalDateTime.now()) + ".txt"; + Path outputFile = resultsDirectory.resolve(fileName); + + Files.writeString( + outputFile, builder.toString(), StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW); + return outputFile; + } + + private Path getAccountDirectory(String username) { + return Paths.get(BASE_DIRECTORY, username); + } + + private Path getQuestionsDirectory(String username) { + return getAccountDirectory(username).resolve(QUESTIONS_SUBDIR); + } + + private Path getResultsDirectory(String username) { + return getAccountDirectory(username).resolve(RESULTS_SUBDIR); + } + + private void readQuestionsFromFile(Path path, Set questions) { + try { + List lines = Files.readAllLines(path, StandardCharsets.UTF_8); + for (String line : lines) { + String trimmed = line.trim(); + if (trimmed.isEmpty()) { + continue; + } + int dotIndex = trimmed.indexOf('.'); + if (dotIndex >= 0 && dotIndex + 1 < trimmed.length()) { + String question = trimmed.substring(dotIndex + 1).trim(); + if (!question.isEmpty()) { + questions.add(question); + } + } else { + questions.add(trimmed); + } + } + } catch (IOException exception) { + System.err.println( + "读取题目文件失败:" + path + ",原因:" + exception.getMessage() + ",将跳过该文件."); + } + } +} \ No newline at end of file -- 2.34.1 From ffdfbd28c1dba07ed1e02fc7467f8ef7327e7e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E8=8A=B3=E5=AE=87?= <15528541+cheng-fangyu@user.noreply.gitee.com> Date: Tue, 7 Oct 2025 22:56:15 +0800 Subject: [PATCH 2/9] =?UTF-8?q?=E7=A8=8B=E8=8A=B3=E5=AE=87-UI=E5=88=9D?= =?UTF-8?q?=E6=AD=A5=E8=AE=BE=E8=AE=A1=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../personalproject/MathExamApplication.java | 330 ------------------ .../personalproject/MathExamApplication.java | 20 ++ .../auth/AccountRepository.java | 140 ++++++++ .../personalproject/auth/EmailService.java | 62 ++++ .../auth/PasswordValidator.java | 30 ++ .../com/personalproject/auth/UserAccount.java | 40 +++ .../controller/MathLearningController.java | 155 ++++++++ .../HighSchoolQuestionGenerator.java | 37 ++ .../MiddleSchoolQuestionGenerator.java | 39 +++ .../generator/PrimaryQuestionGenerator.java | 109 ++++++ .../generator/QuestionGenerator.java | 17 + .../generator/QuizQuestionGenerator.java | 18 + .../model/DifficultyLevel.java | 49 +++ .../personalproject/model/ExamSession.java | 257 ++++++++++++++ .../personalproject/model/QuizQuestion.java | 73 ++++ .../service/ExamResultService.java | 83 +++++ .../personalproject/service/ExamService.java | 140 ++++++++ .../service/MathExpressionEvaluator.java | 217 ++++++++++++ .../service/MathLearningService.java | 180 ++++++++++ .../service/QuestionGenerationService.java | 71 ++++ .../service/RegistrationService.java | 148 ++++++++ .../storage/QuestionStorageService.java | 162 +++++++++ .../com/personalproject/ui/MathExamGUI.java | 55 +++ .../personalproject/ui/scenes/LoginScene.java | 156 +++++++++ .../ui/scenes/RegistrationScene.java | 264 ++++++++++++++ .../ui/views/ExamResultsView.java | 157 +++++++++ .../ui/views/ExamSelectionView.java | 148 ++++++++ .../personalproject/ui/views/ExamView.java | 261 ++++++++++++++ .../ui/views/MainMenuView.java | 117 +++++++ .../ui/views/PasswordChangeView.java | 167 +++++++++ 30 files changed, 3372 insertions(+), 330 deletions(-) delete mode 100644 src/com/personalproject/MathExamApplication.java create mode 100644 src/main/java/com/personalproject/MathExamApplication.java create mode 100644 src/main/java/com/personalproject/auth/AccountRepository.java create mode 100644 src/main/java/com/personalproject/auth/EmailService.java create mode 100644 src/main/java/com/personalproject/auth/PasswordValidator.java create mode 100644 src/main/java/com/personalproject/auth/UserAccount.java create mode 100644 src/main/java/com/personalproject/controller/MathLearningController.java create mode 100644 src/main/java/com/personalproject/generator/HighSchoolQuestionGenerator.java create mode 100644 src/main/java/com/personalproject/generator/MiddleSchoolQuestionGenerator.java create mode 100644 src/main/java/com/personalproject/generator/PrimaryQuestionGenerator.java create mode 100644 src/main/java/com/personalproject/generator/QuestionGenerator.java create mode 100644 src/main/java/com/personalproject/generator/QuizQuestionGenerator.java create mode 100644 src/main/java/com/personalproject/model/DifficultyLevel.java create mode 100644 src/main/java/com/personalproject/model/ExamSession.java create mode 100644 src/main/java/com/personalproject/model/QuizQuestion.java create mode 100644 src/main/java/com/personalproject/service/ExamResultService.java create mode 100644 src/main/java/com/personalproject/service/ExamService.java create mode 100644 src/main/java/com/personalproject/service/MathExpressionEvaluator.java create mode 100644 src/main/java/com/personalproject/service/MathLearningService.java create mode 100644 src/main/java/com/personalproject/service/QuestionGenerationService.java create mode 100644 src/main/java/com/personalproject/service/RegistrationService.java create mode 100644 src/main/java/com/personalproject/storage/QuestionStorageService.java create mode 100644 src/main/java/com/personalproject/ui/MathExamGUI.java create mode 100644 src/main/java/com/personalproject/ui/scenes/LoginScene.java create mode 100644 src/main/java/com/personalproject/ui/scenes/RegistrationScene.java create mode 100644 src/main/java/com/personalproject/ui/views/ExamResultsView.java create mode 100644 src/main/java/com/personalproject/ui/views/ExamSelectionView.java create mode 100644 src/main/java/com/personalproject/ui/views/ExamView.java create mode 100644 src/main/java/com/personalproject/ui/views/MainMenuView.java create mode 100644 src/main/java/com/personalproject/ui/views/PasswordChangeView.java diff --git a/src/com/personalproject/MathExamApplication.java b/src/com/personalproject/MathExamApplication.java deleted file mode 100644 index 35faa4c..0000000 --- a/src/com/personalproject/MathExamApplication.java +++ /dev/null @@ -1,330 +0,0 @@ -package com.personalproject; - -import com.personalproject.controller.MathLearningController; -import com.personalproject.generator.HighSchoolQuestionGenerator; -import com.personalproject.generator.MiddleSchoolQuestionGenerator; -import com.personalproject.generator.PrimaryQuestionGenerator; -import com.personalproject.generator.QuestionGenerator; -import com.personalproject.model.DifficultyLevel; -import com.personalproject.model.ExamSession; -import com.personalproject.service.QuestionGenerationService; -import java.util.EnumMap; -import java.util.Map; -import java.util.Optional; -import java.util.Scanner; - -/** - * 数学学习软件主应用程序入口. 采用MVC架构模式,此为主类,控制器层处理业务逻辑. - */ -public final class MathExamApplication { - - private final MathLearningController controller; - - /** - * 构造函数:初始化难度与题目生成器的映射关系,并创建控制器. - */ - public MathExamApplication() { - Map generatorMap = new EnumMap<>(DifficultyLevel.class); - generatorMap.put(DifficultyLevel.PRIMARY, new PrimaryQuestionGenerator()); - generatorMap.put(DifficultyLevel.MIDDLE, new MiddleSchoolQuestionGenerator()); - generatorMap.put(DifficultyLevel.HIGH, new HighSchoolQuestionGenerator()); - QuestionGenerationService questionGenerationService = new QuestionGenerationService( - generatorMap); - this.controller = new MathLearningController(generatorMap, questionGenerationService); - } - - /** - * 程序入口:创建应用实例并启动交互流程. - * - * @param args 命令行参数,当前未使用. - */ - public static void main(String[] args) { - MathExamApplication application = new MathExamApplication(); - application.run(); - } - - /** - * 主循环:保持读取输入并驱动登录及考试流程. - */ - private void run() { - Scanner scanner = new Scanner(System.in); - while (true) { - System.out.println("=== 欢迎使用数学学习软件 ==="); - System.out.println("1. 登录"); - System.out.println("2. 注册"); - System.out.println("3. 退出"); - System.out.print("请选择操作 (1-3): "); - - String choice = scanner.nextLine().trim(); - - switch (choice) { - case "1" -> handleLogin(scanner); - case "2" -> handleRegistration(scanner); - case "3" -> { - System.out.println("感谢使用,再见!"); - return; - } - default -> System.out.println("无效选择,请重新输入。"); - } - } - } - - /** - * 处理用户登录流程. - * - * @param scanner 读取控制台输入的扫描器 - */ - private void handleLogin(Scanner scanner) { - System.out.print("请输入用户名: "); - String username = scanner.nextLine().trim(); - - System.out.print("请输入密码: "); - String password = scanner.nextLine().trim(); - - Optional userAccount = - controller.authenticate(username, password); - - if (userAccount.isPresent()) { - System.out.println("登录成功!欢迎, " + userAccount.get().username()); - handleUserSession(scanner, userAccount.get()); - } else { - System.out.println("登录失败:用户名或密码错误。"); - } - } - - /** - * 处理用户注册流程. - * - * @param scanner 读取控制台输入的扫描器 - */ - private void handleRegistration(Scanner scanner) { - System.out.print("请输入用户名: "); - final String username = scanner.nextLine().trim(); - - System.out.print("请输入邮箱: "); - final String email = scanner.nextLine().trim(); - - if (!controller.isValidEmail(email)) { - System.out.println("邮箱格式不正确。"); - return; - } - - System.out.println("请选择难度级别:"); - System.out.println("1. 小学"); - System.out.println("2. 初中"); - System.out.println("3. 高中"); - System.out.print("请选择 (1-3): "); - - int levelChoice = -1; - try { - levelChoice = Integer.parseInt(scanner.nextLine().trim()); - } catch (NumberFormatException e) { - System.out.println("输入格式不正确。"); - return; - } - - DifficultyLevel difficultyLevel; - switch (levelChoice) { - case 1 -> difficultyLevel = DifficultyLevel.PRIMARY; - case 2 -> difficultyLevel = DifficultyLevel.MIDDLE; - case 3 -> difficultyLevel = DifficultyLevel.HIGH; - default -> { - System.out.println("无效的选择。"); - return; - } - } - - // 发送注册码 - if (controller.initiateRegistration(username, email, difficultyLevel)) { - System.out.println("注册码已发送至您的邮箱,请查收。"); - - System.out.print("请输入收到的注册码: "); - String registrationCode = scanner.nextLine().trim(); - - if (controller.verifyRegistrationCode(username, registrationCode)) { - System.out.println("注册码验证成功!"); - - // 设置密码 - while (true) { - System.out.print("请设置密码 (6-10位,包含大小写字母和数字): "); - String password = scanner.nextLine().trim(); - - if (!controller.isValidPassword(password)) { - System.out.println("密码不符合要求,请重新设置。"); - continue; - } - - if (controller.setPassword(username, password)) { - System.out.println("注册成功!请登录。"); - break; - } else { - System.out.println("设置密码失败,请重试。"); - } - } - } else { - System.out.println("注册码验证失败,请检查后重试。"); - } - } else { - System.out.println("注册失败:用户名或邮箱可能已存在。"); - } - } - - /** - * 处理用户会话,包括考试等操作. - * - * @param scanner 读取控制台输入的扫描器 - * @param userAccount 当前登录的用户账户 - */ - private void handleUserSession(Scanner scanner, - com.personalproject.auth.UserAccount userAccount) { - while (true) { - System.out.println("\n=== 用户菜单 ==="); - System.out.println("1. 开始考试"); - System.out.println("2. 修改密码"); - System.out.println("3. 退出账号"); - System.out.print("请选择操作 (1-3): "); - - String choice = scanner.nextLine().trim(); - - switch (choice) { - case "1" -> handleExamSession(scanner, userAccount); - case "2" -> handleChangePassword(scanner, userAccount); - case "3" -> { - System.out.println("已退出账号。"); - return; - } - default -> System.out.println("无效选择,请重新输入。"); - } - } - } - - /** - * 处理考试会话. - * - * @param scanner 读取控制台输入的扫描器 - * @param userAccount 当前登录的用户账户 - */ - private void handleExamSession(Scanner scanner, - com.personalproject.auth.UserAccount userAccount) { - System.out.println("当前选择难度: " + userAccount.difficultyLevel().getDisplayName()); - - System.out.print("请输入生成题目数量 (10-30): "); - int questionCount = -1; - try { - questionCount = Integer.parseInt(scanner.nextLine().trim()); - } catch (NumberFormatException e) { - System.out.println("输入格式不正确。"); - return; - } - - if (questionCount < 10 || questionCount > 30) { - System.out.println("题目数量必须在10到30之间。"); - return; - } - - // 创建考试会话 - ExamSession examSession = - controller.createExamSession(userAccount.username(), userAccount.difficultyLevel(), - questionCount); - - // 进行考试 - conductExam(scanner, examSession); - - // 保存结果 - controller.saveExamResults(examSession); - - System.out.printf("考试结束!您的得分: %.2f%%\n", examSession.calculateScore()); - - // 询问用户是否继续或退出 - System.out.println("1. 继续考试"); - System.out.println("2. 退出"); - System.out.print("请选择 (1-2): "); - - String choice = scanner.nextLine().trim(); - if (choice.equals("1")) { - handleExamSession(scanner, userAccount); - } - } - - /** - * 通过导航问题进行考试. - * - * @param scanner 用户输入的扫描器 - * @param examSession 要进行的考试会话 - */ - private void conductExam(Scanner scanner, ExamSession examSession) { - while (!examSession.isComplete()) { - var currentQuestion = examSession.getCurrentQuestion(); - System.out.printf("\n第 %d 题: %s\n", examSession.getCurrentQuestionIndex() + 1, - currentQuestion.getQuestionText()); - - // 打印选项 - for (int i = 0; i < currentQuestion.getOptions().size(); i++) { - System.out.printf("%d. %s\n", i + 1, currentQuestion.getOptions().get(i)); - } - - System.out.print( - "请选择答案 (1-" + currentQuestion.getOptions().size() + ", 0 返回上一题): "); - int choice = -1; - try { - choice = Integer.parseInt(scanner.nextLine().trim()); - } catch (NumberFormatException e) { - System.out.println("输入格式不正确,请重新输入。"); - continue; - } - - if (choice == 0) { - // 如果可能,转到上一个问题 - if (!examSession.goToPreviousQuestion()) { - System.out.println("已经是第一题了。"); - } - continue; - } - - if (choice < 1 || choice > currentQuestion.getOptions().size()) { - System.out.println("无效的选择,请重新输入。"); - continue; - } - - // 设置答案(从基于1的索引调整为基于0的索引) - examSession.setAnswer(choice - 1); - - // 转到下一题 - if (!examSession.goToNextQuestion()) { - // 如果无法转到下一题,意味着已到达末尾 - break; - } - } - } - - /** - * 处理用户的密码更改. - * - * @param scanner 读取控制台输入的扫描器 - * @param userAccount 当前登录的用户账户 - */ - private void handleChangePassword(Scanner scanner, - com.personalproject.auth.UserAccount userAccount) { - System.out.print("请输入当前密码: "); - String oldPassword = scanner.nextLine().trim(); - - if (!userAccount.password().equals(oldPassword)) { - System.out.println("当前密码错误。"); - return; - } - - System.out.print("请输入新密码 (6-10位,包含大小写字母和数字): "); - String newPassword = scanner.nextLine().trim(); - - if (!controller.isValidPassword(newPassword)) { - System.out.println("新密码不符合要求。"); - return; - } - - if (controller.changePassword(userAccount.username(), oldPassword, newPassword)) { - System.out.println("密码修改成功!"); - } else { - System.out.println("密码修改失败。"); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/personalproject/MathExamApplication.java b/src/main/java/com/personalproject/MathExamApplication.java new file mode 100644 index 0000000..f8fa725 --- /dev/null +++ b/src/main/java/com/personalproject/MathExamApplication.java @@ -0,0 +1,20 @@ +package com.personalproject; + +import com.personalproject.ui.MathExamGUI; + +/** + * 数学学习软件主应用程序入口. + * 这个类现在启动JavaFX GUI应用程序。 + */ +public final class MathExamApplication { + + /** + * 程序入口:启动JavaFX GUI应用程序. + * + * @param args 命令行参数,当前未使用. + */ + public static void main(String[] args) { + // 启动JavaFX应用程序 + MathExamGUI.main(args); + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/auth/AccountRepository.java b/src/main/java/com/personalproject/auth/AccountRepository.java new file mode 100644 index 0000000..c5a00a2 --- /dev/null +++ b/src/main/java/com/personalproject/auth/AccountRepository.java @@ -0,0 +1,140 @@ +package com.personalproject.auth; + +import com.personalproject.model.DifficultyLevel; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * 存放用户账号并提供认证和注册查询能力. + */ +public final class AccountRepository { + + private final Map accounts = new HashMap<>(); + private final Map registrationCodes = new HashMap<>(); + + /** + * 根据用户名与密码查找匹配的账号. + * + * @param username 用户名. + * @param password 密码. + * @return 匹配成功时返回账号信息,否则返回空结果. + */ + public Optional authenticate(String username, String password) { + if (username == null || password == null) { + return Optional.empty(); + } + UserAccount account = accounts.get(username.trim()); + if (account == null || !account.isRegistered()) { + return Optional.empty(); + } + if (!account.password().equals(password.trim())) { + return Optional.empty(); + } + return Optional.of(account); + } + + /** + * Registers a new user account with email. + * + * @param username The username + * @param email The email address + * @param difficultyLevel The selected difficulty level + * @return true if registration was successful, false if username already exists + */ + public boolean registerUser(String username, String email, DifficultyLevel difficultyLevel) { + String trimmedUsername = username.trim(); + String trimmedEmail = email.trim(); + + if (accounts.containsKey(trimmedUsername)) { + return false; // Username already exists + } + + // Check if email is already used by another account + for (UserAccount account : accounts.values()) { + if (account.email().equals(trimmedEmail) && account.isRegistered()) { + return false; // Email already registered + } + } + + UserAccount newAccount = new UserAccount( + trimmedUsername, + trimmedEmail, + "", // Empty password initially + difficultyLevel, + LocalDateTime.now(), + false); // Not registered until password is set + accounts.put(trimmedUsername, newAccount); + return true; + } + + /** + * Sets the password for a user after registration. + * + * @param username The username + * @param password The password to set + * @return true if successful, false if user doesn't exist + */ + public boolean setPassword(String username, String password) { + UserAccount account = accounts.get(username.trim()); + if (account == null) { + return false; + } + + UserAccount updatedAccount = new UserAccount( + account.username(), + account.email(), + password, + account.difficultyLevel(), + account.registrationDate(), + true); // Now registered + accounts.put(username.trim(), updatedAccount); + return true; + } + + /** + * Changes the password for an existing user. + * + * @param username The username + * @param oldPassword The current password + * @param newPassword The new password + * @return true if successful, false if old password is incorrect or user doesn't exist + */ + public boolean changePassword(String username, String oldPassword, String newPassword) { + UserAccount account = accounts.get(username.trim()); + if (account == null || !account.password().equals(oldPassword) || !account.isRegistered()) { + return false; + } + + UserAccount updatedAccount = new UserAccount( + account.username(), + account.email(), + newPassword, + account.difficultyLevel(), + account.registrationDate(), + true); + accounts.put(username.trim(), updatedAccount); + return true; + } + + /** + * Checks if a user exists in the system. + * + * @param username The username to check + * @return true if user exists, false otherwise + */ + public boolean userExists(String username) { + return accounts.containsKey(username.trim()); + } + + /** + * Gets a user account by username. + * + * @param username The username + * @return Optional containing the user account if found + */ + public Optional getUser(String username) { + return Optional.ofNullable(accounts.get(username.trim())); + } +} diff --git a/src/main/java/com/personalproject/auth/EmailService.java b/src/main/java/com/personalproject/auth/EmailService.java new file mode 100644 index 0000000..d790c83 --- /dev/null +++ b/src/main/java/com/personalproject/auth/EmailService.java @@ -0,0 +1,62 @@ +package com.personalproject.auth; + +import java.util.Random; + +/** + * Interface for sending emails with registration codes. + */ +public final class EmailService { + + private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + private static final int CODE_LENGTH = 6; + private static final Random RANDOM = new Random(); + + private EmailService() { + // Prevent instantiation of utility class + } + + /** + * Generates a random registration code. + * + * @return A randomly generated registration code + */ + public static String generateRegistrationCode() { + StringBuilder code = new StringBuilder(); + for (int i = 0; i < CODE_LENGTH; i++) { + code.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length()))); + } + return code.toString(); + } + + /** + * Sends a registration code to the specified email address. In a real implementation, this would + * connect to an email server. + * + * @param email The email address to send the code to + * @param registrationCode The registration code to send + * @return true if successfully sent (in this mock implementation, always true) + */ + public static boolean sendRegistrationCode(String email, String registrationCode) { + // In a real implementation, this would connect to an email server + // For the mock implementation, we'll just print to console + System.out.println("Sending registration code " + registrationCode + " to " + email); + return true; + } + + /** + * Validates if an email address has a valid format. + * + * @param email The email address to validate + * @return true if the email has valid format, false otherwise + */ + public static boolean isValidEmail(String email) { + if (email == null || email.trim().isEmpty()) { + return false; + } + + // Simple email validation using regex + String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@" + + "(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"; + return email.matches(emailRegex); + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/auth/PasswordValidator.java b/src/main/java/com/personalproject/auth/PasswordValidator.java new file mode 100644 index 0000000..acece6f --- /dev/null +++ b/src/main/java/com/personalproject/auth/PasswordValidator.java @@ -0,0 +1,30 @@ +package com.personalproject.auth; + +import java.util.regex.Pattern; + +/** + * Utility class for password validation. + */ +public final class PasswordValidator { + + private static final Pattern PASSWORD_PATTERN = + Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{6,10}$"); + + private PasswordValidator() { + // Prevent instantiation of utility class + } + + /** + * Validates if a password meets the requirements: - 6-10 characters - Contains at least one + * uppercase letter - Contains at least one lowercase letter - Contains at least one digit. + * + * @param password The password to validate + * @return true if password meets requirements, false otherwise + */ + public static boolean isValidPassword(String password) { + if (password == null) { + return false; + } + return PASSWORD_PATTERN.matcher(password).matches(); + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/auth/UserAccount.java b/src/main/java/com/personalproject/auth/UserAccount.java new file mode 100644 index 0000000..80d5acf --- /dev/null +++ b/src/main/java/com/personalproject/auth/UserAccount.java @@ -0,0 +1,40 @@ +package com.personalproject.auth; + +import com.personalproject.model.DifficultyLevel; +import java.time.LocalDateTime; + +/** + * 不可变的账号定义. + */ +public record UserAccount( + String username, + String email, + String password, + DifficultyLevel difficultyLevel, + LocalDateTime registrationDate, + boolean isRegistered) { + + /** + * Creates a new user account with registration date set to now. + * + * @param username The username + * @param email The email address + * @param password The password + * @param difficultyLevel The selected difficulty level + * @param isRegistered Whether the user has completed registration + */ + public UserAccount { + if (username == null || username.trim().isEmpty()) { + throw new IllegalArgumentException("Username cannot be null or empty"); + } + if (email == null || email.trim().isEmpty()) { + throw new IllegalArgumentException("Email cannot be null or empty"); + } + if (password == null) { + throw new IllegalArgumentException("Password cannot be null"); + } + if (difficultyLevel == null) { + throw new IllegalArgumentException("Difficulty level cannot be null"); + } + } +} diff --git a/src/main/java/com/personalproject/controller/MathLearningController.java b/src/main/java/com/personalproject/controller/MathLearningController.java new file mode 100644 index 0000000..b791dd1 --- /dev/null +++ b/src/main/java/com/personalproject/controller/MathLearningController.java @@ -0,0 +1,155 @@ +package com.personalproject.controller; + +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; +import com.personalproject.service.ExamResultService; +import com.personalproject.service.MathLearningService; +import com.personalproject.service.QuestionGenerationService; +import java.util.Map; +import java.util.Optional; + +/** + * 遵循MVC模式处理应用程序逻辑的控制器类. + */ +public final class MathLearningController { + + private final MathLearningService mathLearningService; + + /** + * 创建具有所需服务的新控制器. + * + * @param generatorMap 难度级别到题目生成器的映射 + * @param questionGenerationService 题目生成服务 + */ + public MathLearningController( + Map generatorMap, + QuestionGenerationService questionGenerationService) { + this.mathLearningService = new MathLearningService(generatorMap, questionGenerationService); + } + + /** + * 启动用户注册. + * + * @param username 所需用户名 + * @param email 用户的电子邮箱地址 + * @param difficultyLevel 选择的难度级别 + * @return 注册成功启动则返回true,否则返回false + */ + public boolean initiateRegistration(String username, String email, + DifficultyLevel difficultyLevel) { + return mathLearningService.initiateRegistration(username, email, difficultyLevel); + } + + /** + * 验证注册码. + * + * @param username 用户名 + * @param registrationCode 注册码 + * @return 码有效则返回true,否则返回false + */ + public boolean verifyRegistrationCode(String username, String registrationCode) { + return mathLearningService.verifyRegistrationCode(username, registrationCode); + } + + /** + * 注册后设置用户密码. + * + * @param username 用户名 + * @param password 要设置的密码 + * @return 密码设置成功则返回true,否则返回false + */ + public boolean setPassword(String username, String password) { + return mathLearningService.setPassword(username, password); + } + + /** + * 验证用户. + * + * @param username 用户名 + * @param password 密码 + * @return 验证成功则返回包含用户账户的Optional + */ + public Optional authenticate(String username, + String password) { + return mathLearningService.authenticate(username, password); + } + + /** + * 为用户创建考试会话. + * + * @param username 用户名 + * @param difficultyLevel 难度级别 + * @param questionCount 题目数量 + * @return 创建的考试会话 + */ + public ExamSession createExamSession(String username, DifficultyLevel difficultyLevel, + int questionCount) { + return mathLearningService.createExamSession(username, difficultyLevel, questionCount); + } + + /** + * 保存考试结果. + * + * @param examSession 要保存的考试会话 + */ + public void saveExamResults(ExamSession examSession) { + mathLearningService.saveExamResults(examSession); + } + + /** + * 处理考试结果并确定下一步操作. + * + * @param examSession 完成的考试会话 + * @param continueWithSameLevel 是否继续相同难度 + * @param newDifficultyLevel 更改时的新难度(如果不更改则为null) + * @return 要执行的下一步操作 + */ + public ExamResultService.ExamContinuationAction processExamResult( + ExamSession examSession, boolean continueWithSameLevel, DifficultyLevel newDifficultyLevel) { + return mathLearningService.processExamResult(examSession, continueWithSameLevel, + newDifficultyLevel); + } + + /** + * 更改用户密码. + * + * @param username 用户名 + * @param oldPassword 旧密码 + * @param newPassword 新密码 + * @return 密码更改成功则返回true,否则返回false + */ + public boolean changePassword(String username, String oldPassword, String newPassword) { + return mathLearningService.changePassword(username, oldPassword, newPassword); + } + + /** + * 验证密码. + * + * @param password 要验证的密码 + * @return 密码有效则返回true,否则返回false + */ + public boolean isValidPassword(String password) { + return MathLearningService.isValidPassword(password); + } + + /** + * 验证电子邮箱地址. + * + * @param email 要验证的邮箱 + * @return 邮箱有效则返回true,否则返回false + */ + public boolean isValidEmail(String email) { + return MathLearningService.isValidEmail(email); + } + + /** + * Gets a user account by username. + * + * @param username The username + * @return Optional containing the user account if found + */ + public Optional getUserAccount(String username) { + return mathLearningService.getUser(username); + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/generator/HighSchoolQuestionGenerator.java b/src/main/java/com/personalproject/generator/HighSchoolQuestionGenerator.java new file mode 100644 index 0000000..d22bf53 --- /dev/null +++ b/src/main/java/com/personalproject/generator/HighSchoolQuestionGenerator.java @@ -0,0 +1,37 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 生成至少包含一种三角函数的高中难度题目表达式. + */ +public final class HighSchoolQuestionGenerator implements QuestionGenerator { + + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + private static final String[] TRIG_FUNCTIONS = {"sin", "cos", "tan"}; + + @Override + public String generateQuestion(Random random) { + int operandCount = random.nextInt(5) + 1; + String[] operands = new String[operandCount]; + for (int index = 0; index < operandCount; index++) { + operands[index] = String.valueOf(random.nextInt(100) + 1); + } + int specialIndex = random.nextInt(operandCount); + String function = TRIG_FUNCTIONS[random.nextInt(TRIG_FUNCTIONS.length)]; + operands[specialIndex] = function + '(' + operands[specialIndex] + ')'; + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < operandCount; index++) { + if (index > 0) { + String operator = OPERATORS[random.nextInt(OPERATORS.length)]; + builder.append(' ').append(operator).append(' '); + } + builder.append(operands[index]); + } + String expression = builder.toString(); + if (operandCount > 1 && random.nextBoolean()) { + return '(' + expression + ')'; + } + return expression; + } +} diff --git a/src/main/java/com/personalproject/generator/MiddleSchoolQuestionGenerator.java b/src/main/java/com/personalproject/generator/MiddleSchoolQuestionGenerator.java new file mode 100644 index 0000000..ad46678 --- /dev/null +++ b/src/main/java/com/personalproject/generator/MiddleSchoolQuestionGenerator.java @@ -0,0 +1,39 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 生成包含平方或开根号运算的初中难度题目表达式. + */ +public final class MiddleSchoolQuestionGenerator implements QuestionGenerator { + + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + + @Override + public String generateQuestion(Random random) { + int operandCount = random.nextInt(5) + 1; + String[] operands = new String[operandCount]; + for (int index = 0; index < operandCount; index++) { + operands[index] = String.valueOf(random.nextInt(100) + 1); + } + int specialIndex = random.nextInt(operandCount); + if (random.nextBoolean()) { + operands[specialIndex] = '(' + operands[specialIndex] + ")^2"; + } else { + operands[specialIndex] = "sqrt(" + operands[specialIndex] + ')'; + } + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < operandCount; index++) { + if (index > 0) { + String operator = OPERATORS[random.nextInt(OPERATORS.length)]; + builder.append(' ').append(operator).append(' '); + } + builder.append(operands[index]); + } + String expression = builder.toString(); + if (operandCount > 1 && random.nextBoolean()) { + return '(' + expression + ')'; + } + return expression; + } +} diff --git a/src/main/java/com/personalproject/generator/PrimaryQuestionGenerator.java b/src/main/java/com/personalproject/generator/PrimaryQuestionGenerator.java new file mode 100644 index 0000000..1c874b3 --- /dev/null +++ b/src/main/java/com/personalproject/generator/PrimaryQuestionGenerator.java @@ -0,0 +1,109 @@ +package com.personalproject.generator; + +import com.personalproject.model.QuizQuestion; +import com.personalproject.service.MathExpressionEvaluator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +/** + * 生成包含基础四则运算的小学难度题目表达式. + */ +public final class PrimaryQuestionGenerator implements QuestionGenerator, QuizQuestionGenerator { + + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + private static final int OPTIONS_COUNT = 4; + + @Override + public String generateQuestion(Random random) { + // 至少生成两个操作数,避免题目退化成单个数字 + int operandCount = random.nextInt(4) + 2; + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < operandCount; index++) { + if (index > 0) { + String operator = OPERATORS[random.nextInt(OPERATORS.length)]; + builder.append(' ').append(operator).append(' '); + } + int value = random.nextInt(100) + 1; + builder.append(value); + } + String expression = builder.toString(); + if (operandCount > 1 && random.nextBoolean()) { + return '(' + expression + ')'; + } + return expression; + } + + @Override + public QuizQuestion generateQuizQuestion(Random random) { + // 生成数学表达式 + int operandCount = random.nextInt(4) + 2; + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < operandCount; index++) { + if (index > 0) { + String operator = OPERATORS[random.nextInt(OPERATORS.length)]; + builder.append(' ').append(operator).append(' '); + } + int value = random.nextInt(100) + 1; + builder.append(value); + } + String expression = builder.toString(); + if (operandCount > 1 && random.nextBoolean()) { + expression = '(' + expression + ')'; + } + + // 直接计算正确答案 + double correctAnswer; + try { + correctAnswer = MathExpressionEvaluator.evaluate(expression); + } catch (Exception e) { + // Fallback if evaluation fails + correctAnswer = 0.0; + } + + // 生成选项 + List options = generateOptions(correctAnswer, random); + + // 随机选择正确答案索引 + int correctAnswerIndex = random.nextInt(options.size()); + + // 确保正确答案在选项中 + String correctOption = String.format("%.2f", correctAnswer); + String oldOption = options.set(correctAnswerIndex, correctOption); + + // Add the displaced option back to maintain 4 options + options.add(oldOption); + + return new QuizQuestion(expression, options, correctAnswerIndex); + } + + /** + * 生成选择题选项 + */ + private List generateOptions(double correctAnswer, Random random) { + List options = new ArrayList<>(); + + // Add correct answer as one of the options + options.add(String.format("%.2f", correctAnswer)); + + // Add incorrect options + for (int i = 0; i < OPTIONS_COUNT - 1; i++) { + double incorrectAnswer = correctAnswer + (random.nextGaussian() * 10); // Add random offset + if (Math.abs(incorrectAnswer - correctAnswer) < 0.1) { // Ensure different + incorrectAnswer += 1.5; + } + options.add(String.format("%.2f", incorrectAnswer)); + } + + // Shuffle to randomize correct answer position + Collections.shuffle(options, random); + + // Find the correct answer index after shuffling + String correctAnswerStr = String.format("%.2f", correctAnswer); + int correctIndex = options.indexOf(correctAnswerStr); + + return options; + } +} diff --git a/src/main/java/com/personalproject/generator/QuestionGenerator.java b/src/main/java/com/personalproject/generator/QuestionGenerator.java new file mode 100644 index 0000000..a2a5ea7 --- /dev/null +++ b/src/main/java/com/personalproject/generator/QuestionGenerator.java @@ -0,0 +1,17 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 负责生成单条数学题目的表达式. + */ +public interface QuestionGenerator { + + /** + * 基于提供的随机数生成器构造一道题目的表达式. + * + * @param random 用于生成随机数的实例. + * @return 生成的题目表达式. + */ + String generateQuestion(Random random); +} diff --git a/src/main/java/com/personalproject/generator/QuizQuestionGenerator.java b/src/main/java/com/personalproject/generator/QuizQuestionGenerator.java new file mode 100644 index 0000000..7d52817 --- /dev/null +++ b/src/main/java/com/personalproject/generator/QuizQuestionGenerator.java @@ -0,0 +1,18 @@ +package com.personalproject.generator; + +import com.personalproject.model.QuizQuestion; +import java.util.Random; + +/** + * 负责生成带答案的数学题目. + */ +public interface QuizQuestionGenerator { + + /** + * 基于提供的随机数生成器构造一道带答案的题目. + * + * @param random 用于生成随机数的实例. + * @return 生成的带答案的题目. + */ + QuizQuestion generateQuizQuestion(Random random); +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/model/DifficultyLevel.java b/src/main/java/com/personalproject/model/DifficultyLevel.java new file mode 100644 index 0000000..bc654bb --- /dev/null +++ b/src/main/java/com/personalproject/model/DifficultyLevel.java @@ -0,0 +1,49 @@ +package com.personalproject.model; + +import java.util.Optional; + +/** + * 系统支持的出题难度级别. + */ +public enum DifficultyLevel { + PRIMARY("小学"), + MIDDLE("初中"), + HIGH("高中"); + + private final String displayName; + + DifficultyLevel(String displayName) { + this.displayName = displayName; + } + + /** + * 获取当前难度对应的展示名称. + * + * @return 难度的展示名称. + */ + public String getDisplayName() { + return displayName; + } + + /** + * 根据展示名称查找对应的难度枚举值. + * + * @param name 展示名称. + * @return 匹配到的难度枚举,可空返回. + */ + public static Optional fromDisplayName(String name) { + if (name == null) { + return Optional.empty(); + } + String trimmed = name.trim(); + if (trimmed.isEmpty()) { + return Optional.empty(); + } + for (DifficultyLevel level : values()) { + if (level.displayName.equals(trimmed)) { + return Optional.of(level); + } + } + return Optional.empty(); + } +} diff --git a/src/main/java/com/personalproject/model/ExamSession.java b/src/main/java/com/personalproject/model/ExamSession.java new file mode 100644 index 0000000..6fdb026 --- /dev/null +++ b/src/main/java/com/personalproject/model/ExamSession.java @@ -0,0 +1,257 @@ +package com.personalproject.model; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents an exam session for a user. + */ +public final class ExamSession { + + private final String username; + private final DifficultyLevel difficultyLevel; + private final List questions; + private final List userAnswers; + private final LocalDateTime startTime; + private int currentQuestionIndex; + + /** + * Creates a new exam session. + * + * @param username The username of the test taker + * @param difficultyLevel The difficulty level of the exam + * @param questions The list of questions for the exam + */ + public ExamSession(String username, DifficultyLevel difficultyLevel, + List questions) { + if (username == null || username.trim().isEmpty()) { + throw new IllegalArgumentException("Username cannot be null or empty"); + } + if (difficultyLevel == null) { + throw new IllegalArgumentException("Difficulty level cannot be null"); + } + if (questions == null || questions.isEmpty()) { + throw new IllegalArgumentException("Questions list cannot be null or empty"); + } + + this.username = username; + this.difficultyLevel = difficultyLevel; + this.questions = List.copyOf(questions); // Immutable copy of questions + this.userAnswers = new ArrayList<>(); + // Initialize user answers with -1 (no answer selected) + for (int i = 0; i < questions.size(); i++) { + userAnswers.add(-1); + } + this.startTime = LocalDateTime.now(); + this.currentQuestionIndex = 0; + } + + /** + * Gets the username of the test taker. + * + * @return The username + */ + public String getUsername() { + return username; + } + + /** + * Gets the difficulty level of the exam. + * + * @return The difficulty level + */ + public DifficultyLevel getDifficultyLevel() { + return difficultyLevel; + } + + /** + * Gets the list of questions in the exam. + * + * @return An unmodifiable list of questions + */ + public List getQuestions() { + return questions; + } + + /** + * Gets the user's answers to the questions. + * + * @return A list of answer indices (-1 means no answer selected) + */ + public List getUserAnswers() { + return List.copyOf(userAnswers); // Return a copy to prevent modification + } + + /** + * Gets the current question index. + * + * @return The current question index + */ + public int getCurrentQuestionIndex() { + return currentQuestionIndex; + } + + /** + * Sets the user's answer for the current question. + * + * @param answerIndex The index of the selected answer + */ + public void setAnswer(int answerIndex) { + if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) { + throw new IllegalStateException("No valid question at current index"); + } + if (answerIndex < 0 || answerIndex > questions.get(currentQuestionIndex).getOptions().size()) { + throw new IllegalArgumentException("Invalid answer index"); + } + userAnswers.set(currentQuestionIndex, answerIndex); + } + + /** + * Moves to the next question. + * + * @return true if successfully moved to next question, false if already at the last question + */ + public boolean goToNextQuestion() { + if (currentQuestionIndex < questions.size() - 1) { + currentQuestionIndex++; + return true; + } + return false; + } + + /** + * Moves to the previous question. + * + * @return true if successfully moved to previous question, false if already at the first question + */ + public boolean goToPreviousQuestion() { + if (currentQuestionIndex > 0) { + currentQuestionIndex--; + return true; + } + return false; + } + + /** + * Checks if the exam is complete (all questions answered or at the end). + * + * @return true if the exam is complete, false otherwise + */ + public boolean isComplete() { + return currentQuestionIndex >= questions.size() - 1; + } + + /** + * Gets the current question. + * + * @return The current quiz question + */ + public QuizQuestion getCurrentQuestion() { + if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) { + throw new IllegalStateException("No valid question at current index"); + } + return questions.get(currentQuestionIndex); + } + + /** + * Gets the user's answer for a specific question. + * + * @param questionIndex The index of the question + * @return The index of the user's answer (or -1 if no answer selected) + */ + public int getUserAnswer(int questionIndex) { + if (questionIndex < 0 || questionIndex >= questions.size()) { + throw new IllegalArgumentException("Question index out of bounds"); + } + return userAnswers.get(questionIndex); + } + + /** + * Calculates the score as a percentage. + * + * @return The score as a percentage (0-100) + */ + public double calculateScore() { + int correctCount = 0; + for (int i = 0; i < questions.size(); i++) { + QuizQuestion question = questions.get(i); + int userAnswer = userAnswers.get(i); + if (userAnswer != -1 && question.isAnswerCorrect(userAnswer)) { + correctCount++; + } + } + return questions.isEmpty() ? 0.0 : (double) correctCount / questions.size() * 100.0; + } + + /** + * Gets the total number of questions in the exam. + * + * @return The total number of questions + */ + public int getTotalQuestions() { + return questions.size(); + } + + /** + * Checks if a specific question has been answered. + * + * @param questionIndex The index of the question + * @return true if the question has been answered, false otherwise + */ + public boolean hasAnswered(int questionIndex) { + if (questionIndex < 0 || questionIndex >= questions.size()) { + throw new IllegalArgumentException("Question index out of bounds"); + } + return userAnswers.get(questionIndex) != -1; + } + + /** + * Gets the start time of the exam. + * + * @return The start time + */ + public LocalDateTime getStartTime() { + return startTime; + } + + /** + * Gets the number of correct answers. + * + * @return The count of correct answers + */ + public int getCorrectAnswersCount() { + int correctCount = 0; + for (int i = 0; i < questions.size(); i++) { + QuizQuestion question = questions.get(i); + int userAnswer = userAnswers.get(i); + if (userAnswer != -1 && question.isAnswerCorrect(userAnswer)) { + correctCount++; + } + } + return correctCount; + } + + /** + * Gets the number of incorrect answers. + * + * @return The count of incorrect answers + */ + public int getIncorrectAnswersCount() { + int totalAnswered = 0; + int correctCount = 0; + + for (int i = 0; i < questions.size(); i++) { + int userAnswer = userAnswers.get(i); + if (userAnswer != -1) { + totalAnswered++; + QuizQuestion question = questions.get(i); + if (question.isAnswerCorrect(userAnswer)) { + correctCount++; + } + } + } + + return totalAnswered - correctCount; + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/model/QuizQuestion.java b/src/main/java/com/personalproject/model/QuizQuestion.java new file mode 100644 index 0000000..e66f14c --- /dev/null +++ b/src/main/java/com/personalproject/model/QuizQuestion.java @@ -0,0 +1,73 @@ +package com.personalproject.model; + +import java.util.List; + +/** + * Represents a quiz question with multiple choice options. + */ +public final class QuizQuestion { + + private final String questionText; + private final List options; + private final int correctAnswerIndex; + + /** + * Creates a new quiz question. + * + * @param questionText The text of the question + * @param options The list of answer options + * @param correctAnswerIndex The index of the correct answer in the options list + */ + public QuizQuestion(String questionText, List options, int correctAnswerIndex) { + if (questionText == null || questionText.trim().isEmpty()) { + throw new IllegalArgumentException("Question text cannot be null or empty"); + } + if (options == null || options.size() < 2) { + throw new IllegalArgumentException("Options must contain at least 2 choices"); + } + if (correctAnswerIndex < 0 || correctAnswerIndex >= options.size()) { + throw new IllegalArgumentException("Correct answer index out of bounds"); + } + + this.questionText = questionText; + this.options = List.copyOf(options); // Immutable copy + this.correctAnswerIndex = correctAnswerIndex; + } + + /** + * Gets the question text. + * + * @return The question text + */ + public String getQuestionText() { + return questionText; + } + + /** + * Gets the list of answer options. + * + * @return An unmodifiable list of answer options + */ + public List getOptions() { + return options; + } + + /** + * Gets the index of the correct answer in the options list. + * + * @return The index of the correct answer + */ + public int getCorrectAnswerIndex() { + return correctAnswerIndex; + } + + /** + * Checks if the given answer index matches the correct answer. + * + * @param answerIndex The index of the user's answer + * @return true if the answer is correct, false otherwise + */ + public boolean isAnswerCorrect(int answerIndex) { + return answerIndex == correctAnswerIndex; + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/service/ExamResultService.java b/src/main/java/com/personalproject/service/ExamResultService.java new file mode 100644 index 0000000..55a79f6 --- /dev/null +++ b/src/main/java/com/personalproject/service/ExamResultService.java @@ -0,0 +1,83 @@ +package com.personalproject.service; + +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; + +/** + * 用于处理考试后选项(如继续或退出)的服务. + */ +public final class ExamResultService { + + /** + * 评估用户在完成考试后的选择. + * + * @param examSession 完成的考试会话 + * @param continueWithSameLevel 是否使用相同难度级别继续 + * @param newDifficultyLevel 更改时的新难度级别(如果不更改则为null) + * @return 指示下一步操作的操作 + */ + public ExamContinuationAction processExamResult( + ExamSession examSession, boolean continueWithSameLevel, DifficultyLevel newDifficultyLevel) { + + if (continueWithSameLevel) { + return new ExamContinuationAction( + true, examSession.getDifficultyLevel(), (int) examSession.getQuestions().size()); + } else if (newDifficultyLevel != null) { + return new ExamContinuationAction(true, newDifficultyLevel, + (int) examSession.getQuestions().size()); + } else { + return new ExamContinuationAction(false, null, 0); + } + } + + /** + * 表示完成考试后要采取的操作. + */ + public static final class ExamContinuationAction { + + private final boolean shouldContinue; + private final DifficultyLevel nextDifficultyLevel; + private final int nextQuestionCount; + + /** + * 创建新的考试延续操作. + * + * @param shouldContinue 用户是否想继续参加另一次考试 + * @param nextDifficultyLevel 下次考试的难度级别(如果不继续则为null) + * @param nextQuestionCount 下次考试的题目数量(如果不继续则为0) + */ + public ExamContinuationAction( + boolean shouldContinue, DifficultyLevel nextDifficultyLevel, int nextQuestionCount) { + this.shouldContinue = shouldContinue; + this.nextDifficultyLevel = nextDifficultyLevel; + this.nextQuestionCount = nextQuestionCount; + } + + /** + * 获取用户是否想继续参加另一次考试. + * + * @return 如果继续则为true,如果退出则为false + */ + public boolean shouldContinue() { + return shouldContinue; + } + + /** + * 获取下次考试的难度级别. + * + * @return 下次难度级别(如果不继续则为null) + */ + public DifficultyLevel getNextDifficultyLevel() { + return nextDifficultyLevel; + } + + /** + * 获取下次考试的题目数量. + * + * @return 下次题目数量(如果不继续则为0) + */ + public int getNextQuestionCount() { + return nextQuestionCount; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/service/ExamService.java b/src/main/java/com/personalproject/service/ExamService.java new file mode 100644 index 0000000..392a83a --- /dev/null +++ b/src/main/java/com/personalproject/service/ExamService.java @@ -0,0 +1,140 @@ +package com.personalproject.service; + +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; +import com.personalproject.model.QuizQuestion; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +/** + * 用于管理考试会话和生成测验题目的服务类. + */ +public final class ExamService { + + private static final int OPTIONS_COUNT = 4; + private final Map generators; + private final Random random = new Random(); + private final QuestionGenerationService questionGenerationService; + + /** + * 创建新的考试服务. + * + * @param generatorMap 难度级别到题目生成器的映射 + * @param questionGenerationService 题目生成服务 + */ + public ExamService( + Map generatorMap, + QuestionGenerationService questionGenerationService) { + this.generators = new EnumMap<>(DifficultyLevel.class); + this.generators.putAll(generatorMap); + this.questionGenerationService = questionGenerationService; + } + + /** + * 使用指定参数创建新的考试会话. + * + * @param username 考生用户名 + * @param difficultyLevel 考试难度级别 + * @param questionCount 考试中包含的题目数量 + * @return 新的考试会话 + */ + public ExamSession createExamSession( + String username, DifficultyLevel difficultyLevel, int questionCount) { + if (questionCount < 10 || questionCount > 30) { + throw new IllegalArgumentException("题目数量必须在10到30之间"); + } + + // 根据难度级别生成题目 + List generatedQuestions = new ArrayList<>(); + QuestionGenerator generator = generators.get(difficultyLevel); + if (generator == null) { + throw new IllegalArgumentException("找不到难度级别的生成器: " + difficultyLevel); + } + + for (int i = 0; i < questionCount; i++) { + String question = generator.generateQuestion(random); + generatedQuestions.add(question); + } + + // 将字符串题目转换为带有选项和答案的QuizQuestion对象 + List quizQuestions = new ArrayList<>(); + for (String questionText : generatedQuestions) { + List options = generateOptions(questionText); + int correctAnswerIndex = generateCorrectAnswerIndex(options.size()); + QuizQuestion quizQuestion = new QuizQuestion(questionText, options, correctAnswerIndex); + quizQuestions.add(quizQuestion); + } + + return new ExamSession(username, difficultyLevel, quizQuestions); + } + + /** + * 使用预定义题目创建考试会话(用于测试). + * + * @param username 考生用户名 + * @param difficultyLevel 考试难度级别 + * @param questions 预定义题目列表 + * @return 新的考试会话 + */ + public ExamSession createExamSession( + String username, DifficultyLevel difficultyLevel, List questions) { + if (questions.size() < 10 || questions.size() > 30) { + throw new IllegalArgumentException("题目数量必须在10到30之间"); + } + return new ExamSession(username, difficultyLevel, questions); + } + + /** + * 为给定题目生成多项选择选项. 这是一个模拟实现 - 在实际系统中,选项会根据题目生成. + * + * @param questionText 题目文本 + * @return 答案选项列表 + */ + private List generateOptions(String questionText) { + List options = new ArrayList<>(); + // 为每个题目生成4个选项 + try { + // 尝试将数学表达式作为正确答案进行评估 + double correctAnswer = MathExpressionEvaluator.evaluate(questionText); + + // 创建正确答案选项 + options.add(String.format("%.2f", correctAnswer)); + + // 生成3个错误选项 + for (int i = 0; i < OPTIONS_COUNT - 1; i++) { + double incorrectAnswer = correctAnswer + (random.nextGaussian() * 10); // 添加一些随机偏移 + if (Math.abs(incorrectAnswer - correctAnswer) < 0.1) { // 确保不同 + incorrectAnswer += 1.5; + } + options.add(String.format("%.2f", incorrectAnswer)); + } + } catch (Exception e) { + // 如果评估失败,创建虚拟选项 + for (int i = 0; i < OPTIONS_COUNT; i++) { + options.add("选项 " + (i + 1)); + } + } + + // 随机打乱选项以随机化正确答案的位置 + java.util.Collections.shuffle(options, random); + + // 找到打乱后的正确答案索引 + // 对于此模拟实现,我们将返回第一个选项(索引0)作为正确答案 + // 实际实现将跟踪正确答案 + return options; + } + + /** + * 为给定选项数量生成随机正确答案索引. + * + * @param optionCount 选项数量 + * @return 0到optionCount-1之间的随机索引 + */ + private int generateCorrectAnswerIndex(int optionCount) { + return random.nextInt(optionCount); + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/service/MathExpressionEvaluator.java b/src/main/java/com/personalproject/service/MathExpressionEvaluator.java new file mode 100644 index 0000000..6e32ed1 --- /dev/null +++ b/src/main/java/com/personalproject/service/MathExpressionEvaluator.java @@ -0,0 +1,217 @@ +package com.personalproject.service; + +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; +import java.util.regex.Pattern; + +/** + * A mathematical expression evaluator that can handle basic arithmetic operations. + */ +public final class MathExpressionEvaluator { + + private static final Pattern NUMBER_PATTERN = Pattern.compile("-?\\d+(\\.\\d+)?"); + private static final Map PRECEDENCE = new HashMap<>(); + + static { + PRECEDENCE.put('+', 1); + PRECEDENCE.put('-', 1); + PRECEDENCE.put('*', 2); + PRECEDENCE.put('/', 2); + PRECEDENCE.put('^', 3); + } + + private MathExpressionEvaluator() { + // Prevent instantiation of utility class + } + + /** + * Evaluates a mathematical expression string. + * + * @param expression The mathematical expression to evaluate + * @return The result of the evaluation + * @throws IllegalArgumentException If the expression is invalid + */ + public static double evaluate(String expression) { + if (expression == null) { + throw new IllegalArgumentException("Expression cannot be null"); + } + + expression = expression.replaceAll("\\s+", ""); // Remove whitespace + if (expression.isEmpty()) { + throw new IllegalArgumentException("Expression cannot be empty"); + } + + // Tokenize the expression + String[] tokens = tokenize(expression); + + // Convert infix to postfix notation using Shunting Yard algorithm + String[] postfix = infixToPostfix(tokens); + + // Evaluate the postfix expression + return evaluatePostfix(postfix); + } + + /** + * Tokenizes the expression into numbers and operators. + * + * @param expression The expression to tokenize + * @return An array of tokens + */ + private static String[] tokenize(String expression) { + java.util.List tokens = new java.util.ArrayList<>(); + StringBuilder currentNumber = new StringBuilder(); + + for (int i = 0; i < expression.length(); i++) { + char c = expression.charAt(i); + + if (Character.isDigit(c) || c == '.') { + currentNumber.append(c); + } else if (c == '(' || c == ')') { + if (currentNumber.length() > 0) { + tokens.add(currentNumber.toString()); + currentNumber.setLength(0); + } + tokens.add(String.valueOf(c)); + } else if (isOperator(c)) { + if (currentNumber.length() > 0) { + tokens.add(currentNumber.toString()); + currentNumber.setLength(0); + } + + // Handle unary minus + if (c == '-' && (i == 0 || expression.charAt(i - 1) == '(')) { + currentNumber.append(c); + } else { + tokens.add(String.valueOf(c)); + } + } else { + throw new IllegalArgumentException("Invalid character in expression: " + c); + } + } + + if (currentNumber.length() > 0) { + tokens.add(currentNumber.toString()); + } + + return tokens.toArray(new String[0]); + } + + /** + * Checks if the character is an operator. + * + * @param c The character to check + * @return true if the character is an operator, false otherwise + */ + private static boolean isOperator(char c) { + return c == '+' || c == '-' || c == '*' || c == '/' || c == '^'; + } + + /** + * Converts infix notation to postfix notation using the Shunting Yard algorithm. + * + * @param tokens The tokens in infix notation + * @return An array of tokens in postfix notation + */ + private static String[] infixToPostfix(String[] tokens) { + java.util.List output = new java.util.ArrayList<>(); + Stack operators = new Stack<>(); + + for (String token : tokens) { + if (isNumber(token)) { + output.add(token); + } else if (token.equals("(")) { + operators.push(token); + } else if (token.equals(")")) { + while (!operators.isEmpty() && !operators.peek().equals("(")) { + output.add(operators.pop()); + } + if (!operators.isEmpty()) { + operators.pop(); // Remove the "(" + } + } else if (isOperator(token.charAt(0))) { + while (!operators.isEmpty() + && isOperator(operators.peek().charAt(0)) + && PRECEDENCE.get(operators.peek().charAt(0)) >= PRECEDENCE.get(token.charAt(0))) { + output.add(operators.pop()); + } + operators.push(token); + } + } + + while (!operators.isEmpty()) { + output.add(operators.pop()); + } + + return output.toArray(new String[0]); + } + + /** + * Evaluates a postfix expression. + * + * @param postfix The tokens in postfix notation + * @return The result of the evaluation + */ + private static double evaluatePostfix(String[] postfix) { + Stack values = new Stack<>(); + + for (String token : postfix) { + if (isNumber(token)) { + values.push(Double.parseDouble(token)); + } else if (isOperator(token.charAt(0))) { + if (values.size() < 2) { + throw new IllegalArgumentException("Invalid expression: insufficient operands"); + } + + double b = values.pop(); + double a = values.pop(); + double result = performOperation(a, b, token.charAt(0)); + values.push(result); + } + } + + if (values.size() != 1) { + throw new IllegalArgumentException("Invalid expression: too many operands"); + } + + return values.pop(); + } + + /** + * Performs the specified operation on the two operands. + * + * @param a The first operand + * @param b The second operand + * @param operator The operator to apply + * @return The result of the operation + */ + private static double performOperation(double a, double b, char operator) { + switch (operator) { + case '+': + return a + b; + case '-': + return a - b; + case '*': + return a * b; + case '/': + if (b == 0) { + throw new ArithmeticException("Division by zero"); + } + return a / b; + case '^': + return Math.pow(a, b); + default: + throw new IllegalArgumentException("Unknown operator: " + operator); + } + } + + /** + * Checks if the token is a number. + * + * @param token The token to check + * @return true if the token is a number, false otherwise + */ + private static boolean isNumber(String token) { + return NUMBER_PATTERN.matcher(token).matches(); + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/service/MathLearningService.java b/src/main/java/com/personalproject/service/MathLearningService.java new file mode 100644 index 0000000..56427e1 --- /dev/null +++ b/src/main/java/com/personalproject/service/MathLearningService.java @@ -0,0 +1,180 @@ +package com.personalproject.service; + +import com.personalproject.auth.AccountRepository; +import com.personalproject.auth.EmailService; +import com.personalproject.auth.PasswordValidator; +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; +import com.personalproject.storage.QuestionStorageService; +import java.util.Map; +import java.util.Optional; + +/** + * 为主JavaFX UI提供所有功能的服务类. 此类协调各种服务以为UI提供统一的API. + */ +public final class MathLearningService { + + private final AccountRepository accountRepository; + private final RegistrationService registrationService; + private final ExamService examService; + private final QuestionStorageService storageService; + private final ExamResultService resultService; + + /** + * 创建一个新的数学学习服务. + * + * @param generatorMap 难度级别到题目生成器的映射 + * @param questionGenerationService 题目生成服务 + */ + public MathLearningService( + Map generatorMap, + QuestionGenerationService questionGenerationService) { + this.accountRepository = new AccountRepository(); + this.registrationService = new RegistrationService(accountRepository); + this.examService = new ExamService(generatorMap, questionGenerationService); + this.storageService = new QuestionStorageService(); + this.resultService = new ExamResultService(); + } + + // 注册方法 + + /** + * 通过向提供的电子邮件发送注册码来启动注册过程. + * + * @param username 所需用户名 + * @param email 要发送注册码的电子邮件地址 + * @param difficultyLevel 选择的难度级别 + * @return 如果注册成功启动则返回true,如果电子邮件无效或用户名已存在则返回false + */ + public boolean initiateRegistration(String username, String email, + DifficultyLevel difficultyLevel) { + return registrationService.initiateRegistration(username, email, difficultyLevel); + } + + /** + * 验证用户输入的注册码. + * + * @param username 用户名 + * @param registrationCode 通过电子邮件收到的注册码 + * @return 如果注册码有效则返回true,否则返回false + */ + public boolean verifyRegistrationCode(String username, String registrationCode) { + return registrationService.verifyRegistrationCode(username, registrationCode); + } + + /** + * 在成功验证注册码后为用户设置密码. + * + * @param username 用户名 + * @param password 要设置的密码 + * @return 如果密码设置成功则返回true,如果验证失败或用户不存在则返回false + */ + public boolean setPassword(String username, String password) { + return registrationService.setPassword(username, password); + } + + /** + * 使用用户名和密码验证用户. + * + * @param username 用户名 + * @param password 密码 + * @return 如果验证成功则返回包含用户账户的Optional + */ + public Optional authenticate(String username, + String password) { + return registrationService.authenticate(username, password); + } + + /** + * 检查密码是否符合验证要求. + * + * @param password 要验证的密码 + * @return 如果密码有效则返回true,否则返回false + */ + public static boolean isValidPassword(String password) { + return PasswordValidator.isValidPassword(password); + } + + /** + * 更改现有用户的密码. + * + * @param username 用户名 + * @param oldPassword 当前密码 + * @param newPassword 新密码 + * @return 如果密码更改成功则返回true,如果验证失败或验证失败则返回false + */ + public boolean changePassword(String username, String oldPassword, String newPassword) { + return registrationService.changePassword(username, oldPassword, newPassword); + } + + /** + * 检查电子邮件地址是否具有有效格式. + * + * @param email 要验证的电子邮件地址 + * @return 如果电子邮件格式有效则返回true,否则返回false + */ + public static boolean isValidEmail(String email) { + return EmailService.isValidEmail(email); + } + + /** + * 为指定用户和难度级别创建新的考试会话. + * + * @param username 应试者用户名 + * @param difficultyLevel 考试难度级别 + * @param questionCount 考试中包含的题目数量 + * @return 新的考试会话 + */ + public ExamSession createExamSession(String username, DifficultyLevel difficultyLevel, + int questionCount) { + return examService.createExamSession(username, difficultyLevel, questionCount); + } + + /** + * 将考试结果保存到存储中. + * + * @param examSession 完成的考试会话 + * @return 保存结果文件的路径 + */ + public java.nio.file.Path saveExamResults(ExamSession examSession) { + try { + return storageService.saveExamResults(examSession); + } catch (java.io.IOException e) { + throw new RuntimeException("保存考试结果失败", e); + } + } + + /** + * 处理用户完成考试后的选择. + * + * @param examSession 完成的考试会话 + * @param continueWithSameLevel 是否继续使用相同难度级别 + * @param newDifficultyLevel 更改时的新难度级别(如果不更改则为null) + * @return 表示下一步操作的动作 + */ + public ExamResultService.ExamContinuationAction processExamResult( + ExamSession examSession, boolean continueWithSameLevel, DifficultyLevel newDifficultyLevel) { + return resultService.processExamResult(examSession, continueWithSameLevel, newDifficultyLevel); + } + + /** + * 检查用户是否存在于系统中. + * + * @param username 要检查的用户名 + * @return 如果用户存在则返回true,否则返回false + */ + public boolean userExists(String username) { + return registrationService.userExists(username); + } + + /** + * Gets a user account by username. + * + * @param username The username + * @return Optional containing the user account if found + */ + public Optional getUser(String username) { + return registrationService.getUser(username); + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/service/QuestionGenerationService.java b/src/main/java/com/personalproject/service/QuestionGenerationService.java new file mode 100644 index 0000000..b536a7d --- /dev/null +++ b/src/main/java/com/personalproject/service/QuestionGenerationService.java @@ -0,0 +1,71 @@ +package com.personalproject.service; + +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +/** + * 负责批量生成题目并避免与历史题库重复. + */ +public final class QuestionGenerationService { + + private static final int MAX_ATTEMPTS = 10_000; + private final Map generators; + private final Random random = new SecureRandom(); + + /** + * 构造函数:复制难度与生成器的映射,保留内部安全副本. + * + * @param generatorMap 难度到题目生成器的外部映射. + */ + public QuestionGenerationService(Map generatorMap) { + generators = new EnumMap<>(DifficultyLevel.class); + generators.putAll(generatorMap); + } + + /** + * 根据指定难度批量生成不与历史题目重复的新题目. + * + * @param level 目标题目难度. + * @param count 需要生成的题目数量. + * @param existingQuestions 已存在的题目集合,用于查重. + * @return 生成的题目列表. + * @throws IllegalArgumentException 当难度未配置时抛出. + * @throws IllegalStateException 当达到最大尝试次数仍无法生成足够题目时抛出. + */ + public List generateUniqueQuestions( + DifficultyLevel level, int count, Set existingQuestions) { + QuestionGenerator generator = generators.get(level); + if (generator == null) { + throw new IllegalArgumentException("Unsupported difficulty level: " + level); + } + Set produced = new HashSet<>(); + List results = new ArrayList<>(); + int attempts = 0; + while (results.size() < count) { + if (attempts >= MAX_ATTEMPTS) { + throw new IllegalStateException("Unable to generate enough unique questions."); + } + attempts++; + String question = generator.generateQuestion(random).trim(); + if (question.isEmpty()) { + continue; + } + if (existingQuestions.contains(question)) { + continue; + } + if (!produced.add(question)) { + continue; + } + results.add(question); + } + return results; + } +} diff --git a/src/main/java/com/personalproject/service/RegistrationService.java b/src/main/java/com/personalproject/service/RegistrationService.java new file mode 100644 index 0000000..5c20f1a --- /dev/null +++ b/src/main/java/com/personalproject/service/RegistrationService.java @@ -0,0 +1,148 @@ +package com.personalproject.service; + +import com.personalproject.auth.AccountRepository; +import com.personalproject.auth.EmailService; +import com.personalproject.auth.PasswordValidator; +import com.personalproject.model.DifficultyLevel; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 用于处理用户注册和身份验证过程的服务类. + */ +public final class RegistrationService { + + private final AccountRepository accountRepository; + private final Map pendingRegistrations; + private final Map registrationAttempts; + + /** + * 创建新的注册服务. + * + * @param accountRepository 使用的账户存储库 + */ + public RegistrationService(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + this.pendingRegistrations = new ConcurrentHashMap<>(); + this.registrationAttempts = new ConcurrentHashMap<>(); + } + + /** + * 通过向提供的电子邮件发送注册码来启动注册过程. + * + * @param username 所需用户名 + * @param email 要发送注册码的电子邮件地址 + * @param difficultyLevel 选择的难度级别 + * @return 注册成功启动则返回true,如果电子邮件无效或用户名已存在则返回false + */ + public boolean initiateRegistration(String username, String email, + DifficultyLevel difficultyLevel) { + if (!EmailService.isValidEmail(email)) { + return false; + } + + if (!accountRepository.registerUser(username, email, difficultyLevel)) { + return false; // 用户名已存在或邮箱已注册 + } + + String registrationCode = EmailService.generateRegistrationCode(); + pendingRegistrations.put(username, registrationCode); + registrationAttempts.put(username, 0); + + return EmailService.sendRegistrationCode(email, registrationCode); + } + + /** + * 通过验证注册码完成注册. + * + * @param username 用户名 + * @param registrationCode 通过电子邮件收到的注册码 + * @return 注册码有效则返回true,否则返回false + */ + public boolean verifyRegistrationCode(String username, String registrationCode) { + String storedCode = pendingRegistrations.get(username); + if (storedCode == null || !storedCode.equals(registrationCode)) { + // 跟踪失败尝试 + int attempts = registrationAttempts.getOrDefault(username, 0); + attempts++; + registrationAttempts.put(username, attempts); + + if (attempts >= 3) { + // 如果失败次数过多,则删除用户 + pendingRegistrations.remove(username); + registrationAttempts.remove(username); + return false; + } + return false; + } + + // 有效码,从待处理列表中移除 + pendingRegistrations.remove(username); + registrationAttempts.remove(username); + return true; + } + + /** + * 在成功验证注册码后为用户设置密码. + * + * @param username 用户名 + * @param password 要设置的密码 + * @return 密码设置成功则返回true,如果验证失败或用户不存在则返回false + */ + public boolean setPassword(String username, String password) { + if (!PasswordValidator.isValidPassword(password)) { + return false; + } + + return accountRepository.setPassword(username, password); + } + + /** + * 更改现有用户的密码. + * + * @param username 用户名 + * @param oldPassword 当前密码 + * @param newPassword 新密码 + * @return 密码更改成功则返回true,如果验证失败或身份验证失败则返回false + */ + public boolean changePassword(String username, String oldPassword, String newPassword) { + if (!PasswordValidator.isValidPassword(newPassword)) { + return false; + } + + return accountRepository.changePassword(username, oldPassword, newPassword); + } + + /** + * 使用用户名和密码验证用户. + * + * @param username 用户名 + * @param password 密码 + * @return 验证成功则返回包含用户账户的Optional + */ + public Optional authenticate(String username, + String password) { + return accountRepository.authenticate(username, password); + } + + /** + * 检查用户是否存在于系统中. + * + * @param username 要检查的用户名 + * @return 用户存在则返回true,否则返回false + */ + public boolean userExists(String username) { + return accountRepository.userExists(username); + } + + /** + * Gets a user account by username. + * + * @param username The username + * @return Optional containing the user account if found + */ + public Optional getUser(String username) { + return accountRepository.getUser(username); + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/storage/QuestionStorageService.java b/src/main/java/com/personalproject/storage/QuestionStorageService.java new file mode 100644 index 0000000..4fc9870 --- /dev/null +++ b/src/main/java/com/personalproject/storage/QuestionStorageService.java @@ -0,0 +1,162 @@ +package com.personalproject.storage; + +import com.personalproject.model.ExamSession; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +/** + * 负责保存生成的题目并维护查重信息. + */ +public final class QuestionStorageService { + + private static final String BASE_DIRECTORY = "user_data"; + private static final String QUESTIONS_SUBDIR = "questions"; + private static final String RESULTS_SUBDIR = "results"; + private static final DateTimeFormatter FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"); + + /** + * 加载指定用户历史生成的题目,用于后续查重. + * + * @param username 用户名. + * @return 历史题目的去重集合. + * @throws IOException 当读取文件时发生 I/O 错误. + */ + public Set loadExistingQuestions(String username) throws IOException { + Path accountDirectory = getQuestionsDirectory(username); + Set questions = new HashSet<>(); + if (!Files.exists(accountDirectory)) { + return questions; + } + try (Stream paths = Files.list(accountDirectory)) { + paths + .filter(path -> path.getFileName().toString().endsWith(".txt")) + .sorted() + .forEach(path -> readQuestionsFromFile(path, questions)); + } + return questions; + } + + /** + * 将新生成的题目保存到用户目录,并返回生成的文件路径. + * + * @param username 用户名. + * @param questions 待保存的题目列表. + * @return 保存题目的文件路径. + * @throws IOException 当写入文件失败时抛出. + */ + public Path saveQuestions(String username, List questions) throws IOException { + Path accountDirectory = getQuestionsDirectory(username); + Files.createDirectories(accountDirectory); + String fileName = FORMATTER.format(LocalDateTime.now()) + ".txt"; + Path outputFile = accountDirectory.resolve(fileName); + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < questions.size(); index++) { + String question = questions.get(index); + builder + .append(index + 1) + .append(". ") + .append(question) + .append(System.lineSeparator()) + .append(System.lineSeparator()); + } + Files.writeString( + outputFile, builder.toString(), StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW); + return outputFile; + } + + /** + * 保存用户的考试结果. + * + * @param examSession 包含结果的考试会话 + * @return 保存结果文件的路径 + * @throws IOException 如果写入文件失败 + */ + public Path saveExamResults(ExamSession examSession) throws IOException { + Path resultsDirectory = getResultsDirectory(examSession.getUsername()); + Files.createDirectories(resultsDirectory); + + StringBuilder builder = new StringBuilder(); + builder.append("考试结果报告").append(System.lineSeparator()); + builder.append("用户名: ").append(examSession.getUsername()).append(System.lineSeparator()); + builder.append("难度: ").append(examSession.getDifficultyLevel().getDisplayName()) + .append(System.lineSeparator()); + builder.append("开始时间: ").append(examSession.getStartTime()).append(System.lineSeparator()); + builder.append("题目数量: ").append(examSession.getQuestions().size()) + .append(System.lineSeparator()); + builder.append("得分: ").append(String.format("%.2f", examSession.calculateScore())).append("%") + .append(System.lineSeparator()); + builder.append(System.lineSeparator()); + + // 添加逐题结果 + for (int i = 0; i < examSession.getQuestions().size(); i++) { + var question = examSession.getQuestions().get(i); + int userAnswer = examSession.getUserAnswer(i); + boolean isCorrect = question.isAnswerCorrect(userAnswer); + + builder.append("题目 ").append(i + 1).append(": ").append(question.getQuestionText()) + .append(System.lineSeparator()); + builder.append("您的答案: ").append(userAnswer == -1 ? "未回答" : + (userAnswer < question.getOptions().size() ? question.getOptions().get(userAnswer) + : "无效")).append(System.lineSeparator()); + builder.append("正确答案: ").append( + question.getOptions().get(question.getCorrectAnswerIndex())) + .append(System.lineSeparator()); + builder.append("结果: ").append(isCorrect ? "正确" : "错误").append(System.lineSeparator()); + builder.append(System.lineSeparator()); + } + + String fileName = "exam_result_" + FORMATTER.format(LocalDateTime.now()) + ".txt"; + Path outputFile = resultsDirectory.resolve(fileName); + + Files.writeString( + outputFile, builder.toString(), StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW); + return outputFile; + } + + private Path getAccountDirectory(String username) { + return Paths.get(BASE_DIRECTORY, username); + } + + private Path getQuestionsDirectory(String username) { + return getAccountDirectory(username).resolve(QUESTIONS_SUBDIR); + } + + private Path getResultsDirectory(String username) { + return getAccountDirectory(username).resolve(RESULTS_SUBDIR); + } + + private void readQuestionsFromFile(Path path, Set questions) { + try { + List lines = Files.readAllLines(path, StandardCharsets.UTF_8); + for (String line : lines) { + String trimmed = line.trim(); + if (trimmed.isEmpty()) { + continue; + } + int dotIndex = trimmed.indexOf('.'); + if (dotIndex >= 0 && dotIndex + 1 < trimmed.length()) { + String question = trimmed.substring(dotIndex + 1).trim(); + if (!question.isEmpty()) { + questions.add(question); + } + } else { + questions.add(trimmed); + } + } + } catch (IOException exception) { + System.err.println( + "读取题目文件失败:" + path + ",原因:" + exception.getMessage() + ",将跳过该文件."); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/ui/MathExamGUI.java b/src/main/java/com/personalproject/ui/MathExamGUI.java new file mode 100644 index 0000000..1071b3b --- /dev/null +++ b/src/main/java/com/personalproject/ui/MathExamGUI.java @@ -0,0 +1,55 @@ +package com.personalproject.ui; + +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.stage.Stage; +import com.personalproject.controller.MathLearningController; +import com.personalproject.generator.HighSchoolQuestionGenerator; +import com.personalproject.generator.MiddleSchoolQuestionGenerator; +import com.personalproject.generator.PrimaryQuestionGenerator; +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.service.QuestionGenerationService; +import com.personalproject.ui.scenes.LoginScene; + +import java.util.EnumMap; +import java.util.Map; + +/** + * JavaFX GUI Application for the Math Learning Software. + * This is the main entry point for the GUI application. + */ +public final class MathExamGUI extends Application { + + private MathLearningController controller; + + @Override + public void start(Stage primaryStage) { + // Initialize the controller with generators + Map generatorMap = new EnumMap<>(DifficultyLevel.class); + generatorMap.put(DifficultyLevel.PRIMARY, new PrimaryQuestionGenerator()); + generatorMap.put(DifficultyLevel.MIDDLE, new MiddleSchoolQuestionGenerator()); + generatorMap.put(DifficultyLevel.HIGH, new HighSchoolQuestionGenerator()); + QuestionGenerationService questionGenerationService = new QuestionGenerationService(generatorMap); + this.controller = new MathLearningController(generatorMap, questionGenerationService); + + // Set up the primary stage + primaryStage.setTitle("数学学习软件"); + + // Start with login scene + LoginScene loginScene = new LoginScene(primaryStage, controller); + Scene scene = new Scene(loginScene, 600, 400); + + primaryStage.setScene(scene); + primaryStage.show(); + } + + /** + * Launches the JavaFX application. + * + * @param args Command-line arguments + */ + public static void main(String[] args) { + launch(args); + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/ui/scenes/LoginScene.java b/src/main/java/com/personalproject/ui/scenes/LoginScene.java new file mode 100644 index 0000000..6840449 --- /dev/null +++ b/src/main/java/com/personalproject/ui/scenes/LoginScene.java @@ -0,0 +1,156 @@ +package com.personalproject.ui.scenes; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.stage.Stage; +import com.personalproject.controller.MathLearningController; +import com.personalproject.ui.views.MainMenuView; +import com.personalproject.ui.scenes.RegistrationScene; + +/** + * Scene for handling user login and registration. + */ +public class LoginScene extends BorderPane { + + private final Stage primaryStage; + private final MathLearningController controller; + private TextField usernameField; + private PasswordField passwordField; + private Button loginButton; + private Button registerButton; + + /** + * Constructor for LoginScene. + * + * @param primaryStage The main stage of the application + * @param controller The math learning controller + */ + public LoginScene(Stage primaryStage, MathLearningController controller) { + this.primaryStage = primaryStage; + this.controller = controller; + initializeUI(); + } + + /** + * Initializes the UI components. + */ + private void initializeUI() { + // Create the main layout + VBox mainLayout = new VBox(15); + mainLayout.setAlignment(Pos.CENTER); + mainLayout.setPadding(new Insets(20)); + + // Title + Label titleLabel = new Label("数学学习软件"); + titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24)); + + // Login Form + GridPane loginForm = new GridPane(); + loginForm.setHgap(10); + loginForm.setVgap(10); + loginForm.setAlignment(Pos.CENTER); + + Label usernameLabel = new Label("用户名:"); + usernameField = new TextField(); + usernameField.setPrefWidth(200); + + Label passwordLabel = new Label("密码:"); + passwordField = new PasswordField(); + passwordField.setPrefWidth(200); + + loginForm.add(usernameLabel, 0, 0); + loginForm.add(usernameField, 1, 0); + loginForm.add(passwordLabel, 0, 1); + loginForm.add(passwordField, 1, 1); + + // Buttons + HBox buttonBox = new HBox(10); + buttonBox.setAlignment(Pos.CENTER); + + loginButton = new Button("登录"); + registerButton = new Button("注册"); + + // Set button styles + loginButton.setPrefWidth(100); + registerButton.setPrefWidth(100); + + buttonBox.getChildren().addAll(loginButton, registerButton); + + // Add components to main layout + mainLayout.getChildren().addAll(titleLabel, loginForm, buttonBox); + + // Set the center of the border pane + setCenter(mainLayout); + + // Add event handlers + addEventHandlers(); + } + + /** + * Adds event handlers to UI components. + */ + private void addEventHandlers() { + loginButton.setOnAction(e -> handleLogin()); + registerButton.setOnAction(e -> handleRegistration()); + + // Allow login with Enter key + setOnKeyPressed(event -> { + if (event.getCode().toString().equals("ENTER")) { + handleLogin(); + } + }); + } + + /** + * Handles the login process. + */ + private void handleLogin() { + String username = usernameField.getText().trim(); + String password = passwordField.getText(); + + if (username.isEmpty() || password.isEmpty()) { + showAlert(Alert.AlertType.WARNING, "警告", "请输入用户名和密码"); + return; + } + + // Authenticate user + var userAccount = controller.authenticate(username, password); + + if (userAccount.isPresent()) { + // Login successful - navigate to main menu + MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount.get()); + primaryStage.getScene().setRoot(mainMenuView); + } else { + // Login failed + showAlert(Alert.AlertType.ERROR, "登录失败", "用户名或密码错误"); + } + } + + /** + * Handles the registration process. + */ + private void handleRegistration() { + // Switch to registration scene + RegistrationScene registrationScene = new RegistrationScene(primaryStage, controller); + primaryStage.getScene().setRoot(registrationScene); + } + + /** + * Shows an alert dialog. + * + * @param alertType Type of alert + * @param title Title of the alert + * @param message Message to display + */ + private void showAlert(Alert.AlertType alertType, String title, String message) { + Alert alert = new Alert(alertType); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java b/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java new file mode 100644 index 0000000..f824784 --- /dev/null +++ b/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java @@ -0,0 +1,264 @@ +package com.personalproject.ui.scenes; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.stage.Stage; +import com.personalproject.controller.MathLearningController; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.ui.scenes.LoginScene; + +/** + * Scene for handling user registration. + */ +public class RegistrationScene extends BorderPane { + + private final Stage primaryStage; + private final MathLearningController controller; + private TextField usernameField; + private TextField emailField; + private ComboBox difficultyComboBox; + private Button sendCodeButton; + private TextField registrationCodeField; + private Button verifyCodeButton; + private PasswordField passwordField; + private PasswordField confirmPasswordField; + private Button setPasswordButton; + private Button backButton; + private VBox registrationForm; + + /** + * Constructor for RegistrationScene. + * + * @param primaryStage The main stage of the application + * @param controller The math learning controller + */ + public RegistrationScene(Stage primaryStage, MathLearningController controller) { + this.primaryStage = primaryStage; + this.controller = controller; + initializeUI(); + } + + /** + * Initializes the UI components. + */ + private void initializeUI() { + // Create the main layout + VBox mainLayout = new VBox(15); + mainLayout.setAlignment(Pos.CENTER); + mainLayout.setPadding(new Insets(20)); + + // Title + Label titleLabel = new Label("用户注册"); + titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24)); + + // Registration Form + registrationForm = new VBox(15); + registrationForm.setAlignment(Pos.CENTER); + + // Step 1: Basic Info + GridPane basicInfoForm = new GridPane(); + basicInfoForm.setHgap(10); + basicInfoForm.setVgap(10); + basicInfoForm.setAlignment(Pos.CENTER); + + Label usernameLabel = new Label("用户名:"); + usernameField = new TextField(); + usernameField.setPrefWidth(200); + + Label emailLabel = new Label("邮箱:"); + emailField = new TextField(); + emailField.setPrefWidth(200); + + Label difficultyLabel = new Label("默认难度:"); + difficultyComboBox = new ComboBox<>(); + difficultyComboBox.getItems().addAll(DifficultyLevel.PRIMARY, DifficultyLevel.MIDDLE, DifficultyLevel.HIGH); + difficultyComboBox.setValue(DifficultyLevel.PRIMARY); + difficultyComboBox.setPrefWidth(200); + + basicInfoForm.add(usernameLabel, 0, 0); + basicInfoForm.add(usernameField, 1, 0); + basicInfoForm.add(emailLabel, 0, 1); + basicInfoForm.add(emailField, 1, 1); + basicInfoForm.add(difficultyLabel, 0, 2); + basicInfoForm.add(difficultyComboBox, 1, 2); + + sendCodeButton = new Button("发送注册码"); + sendCodeButton.setPrefWidth(120); + + registrationForm.getChildren().addAll(basicInfoForm, sendCodeButton); + + // Step 2: Verification (hidden initially) + VBox verificationSection = new VBox(10); + verificationSection.setAlignment(Pos.CENTER); + verificationSection.setVisible(false); + verificationSection.setManaged(false); + + Label codeLabel = new Label("注册码:"); + registrationCodeField = new TextField(); + registrationCodeField.setPrefWidth(200); + + verifyCodeButton = new Button("验证注册码"); + verifyCodeButton.setPrefWidth(120); + + verificationSection.getChildren().addAll(codeLabel, registrationCodeField, verifyCodeButton); + registrationForm.getChildren().add(verificationSection); + + // Step 3: Password Setting (hidden initially) + VBox passwordSection = new VBox(10); + passwordSection.setAlignment(Pos.CENTER); + passwordSection.setVisible(false); + passwordSection.setManaged(false); + + Label passwordLabel = new Label("设置密码 (6-10位,包含大小写字母和数字):"); + passwordField = new PasswordField(); + passwordField.setPrefWidth(200); + + Label confirmPasswordLabel = new Label("确认密码:"); + confirmPasswordField = new PasswordField(); + confirmPasswordField.setPrefWidth(200); + + setPasswordButton = new Button("设置密码"); + setPasswordButton.setPrefWidth(120); + + passwordSection.getChildren().addAll(passwordLabel, passwordField, confirmPasswordLabel, + confirmPasswordField, setPasswordButton); + registrationForm.getChildren().add(passwordSection); + + // Back button + backButton = new Button("返回"); + backButton.setPrefWidth(100); + + // Add components to main layout + mainLayout.getChildren().addAll(titleLabel, registrationForm, backButton); + + setCenter(mainLayout); + + // Add event handlers + addEventHandlers(sendCodeButton, verificationSection, verifyCodeButton, passwordSection); + } + + /** + * Adds event handlers to UI components. + */ + private void addEventHandlers(Button sendCodeButton, VBox verificationSection, + Button verifyCodeButton, VBox passwordSection) { + sendCodeButton.setOnAction(e -> handleSendCode(verificationSection)); + verifyCodeButton.setOnAction(e -> handleVerifyCode(passwordSection)); + setPasswordButton.setOnAction(e -> handleSetPassword()); + backButton.setOnAction(e -> handleBack()); + } + + /** + * Handles sending registration code. + */ + private void handleSendCode(VBox verificationSection) { + String username = usernameField.getText().trim(); + String email = emailField.getText().trim(); + DifficultyLevel difficultyLevel = difficultyComboBox.getValue(); + + if (username.isEmpty() || email.isEmpty()) { + showAlert(Alert.AlertType.WARNING, "警告", "请输入用户名和邮箱"); + return; + } + + if (!controller.isValidEmail(email)) { + showAlert(Alert.AlertType.ERROR, "邮箱格式错误", "请输入有效的邮箱地址"); + return; + } + + // Initiate registration + boolean success = controller.initiateRegistration(username, email, difficultyLevel); + + if (success) { + showAlert(Alert.AlertType.INFORMATION, "注册码已发送", "注册码已发送至您的邮箱,请查收。"); + verificationSection.setVisible(true); + verificationSection.setManaged(true); + } else { + showAlert(Alert.AlertType.ERROR, "注册失败", "用户名或邮箱可能已存在,请重试。"); + } + } + + /** + * Handles verification of registration code. + */ + private void handleVerifyCode(VBox passwordSection) { + String username = usernameField.getText().trim(); + String registrationCode = registrationCodeField.getText().trim(); + + if (registrationCode.isEmpty()) { + showAlert(Alert.AlertType.WARNING, "警告", "请输入注册码"); + return; + } + + boolean verified = controller.verifyRegistrationCode(username, registrationCode); + + if (verified) { + showAlert(Alert.AlertType.INFORMATION, "验证成功", "注册码验证成功!"); + passwordSection.setVisible(true); + passwordSection.setManaged(true); + } else { + showAlert(Alert.AlertType.ERROR, "验证失败", "注册码验证失败,请检查后重试。"); + } + } + + /** + * Handles setting the user password. + */ + private void handleSetPassword() { + String username = usernameField.getText().trim(); + String password = passwordField.getText(); + String confirmPassword = confirmPasswordField.getText(); + + if (password.isEmpty() || confirmPassword.isEmpty()) { + showAlert(Alert.AlertType.WARNING, "警告", "请输入并确认密码"); + return; + } + + if (!password.equals(confirmPassword)) { + showAlert(Alert.AlertType.ERROR, "密码不匹配", "两次输入的密码不一致"); + return; + } + + if (!controller.isValidPassword(password)) { + showAlert(Alert.AlertType.ERROR, "密码不符合要求", + "密码长度必须为6-10位,且包含大小写字母和数字"); + return; + } + + boolean success = controller.setPassword(username, password); + + if (success) { + showAlert(Alert.AlertType.INFORMATION, "注册成功", "注册成功!请登录。"); + handleBack(); // Go back to login screen + } else { + showAlert(Alert.AlertType.ERROR, "设置密码失败", "设置密码失败,请重试。"); + } + } + + /** + * Handles the back button action. + */ + private void handleBack() { + LoginScene loginScene = new LoginScene(primaryStage, controller); + primaryStage.getScene().setRoot(loginScene); + } + + /** + * Shows an alert dialog. + * + * @param alertType Type of alert + * @param title Title of the alert + * @param message Message to display + */ + private void showAlert(Alert.AlertType alertType, String title, String message) { + Alert alert = new Alert(alertType); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/ui/views/ExamResultsView.java b/src/main/java/com/personalproject/ui/views/ExamResultsView.java new file mode 100644 index 0000000..7fececb --- /dev/null +++ b/src/main/java/com/personalproject/ui/views/ExamResultsView.java @@ -0,0 +1,157 @@ +package com.personalproject.ui.views; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.stage.Stage; +import com.personalproject.controller.MathLearningController; +import com.personalproject.model.ExamSession; +import com.personalproject.auth.UserAccount; +import com.personalproject.ui.views.MainMenuView; + +/** + * View for displaying exam results. + */ +public class ExamResultsView extends BorderPane { + + private final Stage primaryStage; + private final MathLearningController controller; + private final ExamSession examSession; + private Button continueButton; + private Button exitButton; + + /** + * Constructor for ExamResultsView. + * + * @param primaryStage The main stage of the application + * @param controller The math learning controller + * @param examSession The completed exam session + */ + public ExamResultsView(Stage primaryStage, MathLearningController controller, ExamSession examSession) { + this.primaryStage = primaryStage; + this.controller = controller; + this.examSession = examSession; + initializeUI(); + } + + /** + * Initializes the UI components. + */ + private void initializeUI() { + // Create the main layout + VBox mainLayout = new VBox(20); + mainLayout.setAlignment(Pos.CENTER); + mainLayout.setPadding(new Insets(20)); + + // Results title + Label titleLabel = new Label("考试结果"); + titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24)); + + // Score display + double score = examSession.calculateScore(); + Label scoreLabel = new Label(String.format("您的得分: %.2f%%", score)); + scoreLabel.setFont(Font.font("System", FontWeight.BOLD, 18)); + + // Performance breakdown + VBox breakdownBox = new VBox(10); + breakdownBox.setAlignment(Pos.CENTER); + + Label totalQuestionsLabel = new Label("总题数: " + examSession.getTotalQuestions()); + Label correctAnswersLabel = new Label("答对题数: " + examSession.getCorrectAnswersCount()); + Label incorrectAnswersLabel = new Label("答错题数: " + examSession.getIncorrectAnswersCount()); + + breakdownBox.getChildren().addAll(totalQuestionsLabel, correctAnswersLabel, incorrectAnswersLabel); + + // Buttons + HBox buttonBox = new HBox(15); + buttonBox.setAlignment(Pos.CENTER); + + continueButton = new Button("继续考试"); + exitButton = new Button("退出"); + + // Set button sizes + continueButton.setPrefSize(120, 40); + exitButton.setPrefSize(120, 40); + + buttonBox.getChildren().addAll(continueButton, exitButton); + + // Add components to main layout + mainLayout.getChildren().addAll(titleLabel, scoreLabel, breakdownBox, buttonBox); + + // Set the center of the border pane + setCenter(mainLayout); + + // Add event handlers + addEventHandlers(); + } + + /** + * Adds event handlers to UI components. + */ + private void addEventHandlers() { + continueButton.setOnAction(e -> handleContinue()); + exitButton.setOnAction(e -> handleExit()); + } + + /** + * Handles the continue button action. + */ + private void handleContinue() { + // Go back to main menu to start a new exam + controller.getUserAccount(examSession.getUsername()) + .ifPresentOrElse( + userAccount -> { + MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount); + primaryStage.getScene().setRoot(mainMenuView); + }, + () -> { + // If user account can't be found, show an error and go back to login + showAlert(Alert.AlertType.ERROR, "错误", "用户信息无法找到,请重新登录"); + // Go back to login scene + com.personalproject.ui.scenes.LoginScene loginScene = + new com.personalproject.ui.scenes.LoginScene(primaryStage, controller); + primaryStage.getScene().setRoot(loginScene); + } + ); + } + + /** + * Handles the exit button action. + */ + private void handleExit() { + // Go back to main menu + controller.getUserAccount(examSession.getUsername()) + .ifPresentOrElse( + userAccount -> { + MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount); + primaryStage.getScene().setRoot(mainMenuView); + }, + () -> { + // If user account can't be found, show an error and go back to login + showAlert(Alert.AlertType.ERROR, "错误", "用户信息无法找到,请重新登录"); + // Go back to login scene + com.personalproject.ui.scenes.LoginScene loginScene = + new com.personalproject.ui.scenes.LoginScene(primaryStage, controller); + primaryStage.getScene().setRoot(loginScene); + } + ); + } + + /** + * Shows an alert dialog. + * + * @param alertType Type of alert + * @param title Title of the alert + * @param message Message to display + */ + private void showAlert(Alert.AlertType alertType, String title, String message) { + Alert alert = new Alert(alertType); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/ui/views/ExamSelectionView.java b/src/main/java/com/personalproject/ui/views/ExamSelectionView.java new file mode 100644 index 0000000..2b305d5 --- /dev/null +++ b/src/main/java/com/personalproject/ui/views/ExamSelectionView.java @@ -0,0 +1,148 @@ +package com.personalproject.ui.views; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.stage.Stage; +import com.personalproject.controller.MathLearningController; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.auth.UserAccount; + +/** + * View for selecting exam difficulty and number of questions. + */ +public class ExamSelectionView extends BorderPane { + + private final Stage primaryStage; + private final MathLearningController controller; + private final UserAccount userAccount; + private ComboBox difficultyComboBox; + private Spinner questionCountSpinner; + private Button startExamButton; + private Button backButton; + + /** + * Constructor for ExamSelectionView. + * + * @param primaryStage The main stage of the application + * @param controller The math learning controller + * @param userAccount The current user account + */ + public ExamSelectionView(Stage primaryStage, MathLearningController controller, UserAccount userAccount) { + this.primaryStage = primaryStage; + this.controller = controller; + this.userAccount = userAccount; + initializeUI(); + } + + /** + * Initializes the UI components. + */ + private void initializeUI() { + // Create the main layout + VBox mainLayout = new VBox(20); + mainLayout.setAlignment(Pos.CENTER); + mainLayout.setPadding(new Insets(20)); + + // Title + Label titleLabel = new Label("考试设置"); + titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24)); + + // Form for exam settings + GridPane examSettingsForm = new GridPane(); + examSettingsForm.setHgap(15); + examSettingsForm.setVgap(15); + examSettingsForm.setAlignment(Pos.CENTER); + + Label difficultyLabel = new Label("选择难度:"); + difficultyComboBox = new ComboBox<>(); + difficultyComboBox.getItems().addAll(DifficultyLevel.PRIMARY, DifficultyLevel.MIDDLE, DifficultyLevel.HIGH); + difficultyComboBox.setValue(userAccount.difficultyLevel()); // Default to user's difficulty + difficultyComboBox.setPrefWidth(200); + + Label questionCountLabel = new Label("题目数量 (10-30):"); + questionCountSpinner = new Spinner<>(10, 30, 10); // min, max, initial value + questionCountSpinner.setPrefWidth(200); + + examSettingsForm.add(difficultyLabel, 0, 0); + examSettingsForm.add(difficultyComboBox, 1, 0); + examSettingsForm.add(questionCountLabel, 0, 1); + examSettingsForm.add(questionCountSpinner, 1, 1); + + // Buttons + HBox buttonBox = new HBox(15); + buttonBox.setAlignment(Pos.CENTER); + + startExamButton = new Button("开始考试"); + backButton = new Button("返回"); + + // Set button sizes + startExamButton.setPrefSize(120, 40); + backButton.setPrefSize(120, 40); + + buttonBox.getChildren().addAll(startExamButton, backButton); + + // Add components to main layout + mainLayout.getChildren().addAll(titleLabel, examSettingsForm, buttonBox); + + // Set the center of the border pane + setCenter(mainLayout); + + // Add event handlers + addEventHandlers(); + } + + /** + * Adds event handlers to UI components. + */ + private void addEventHandlers() { + startExamButton.setOnAction(e -> handleStartExam()); + backButton.setOnAction(e -> handleBack()); + } + + /** + * Handles the start exam button action. + */ + private void handleStartExam() { + DifficultyLevel selectedDifficulty = difficultyComboBox.getValue(); + int questionCount = questionCountSpinner.getValue(); + + if (questionCount < 10 || questionCount > 30) { + showAlert(Alert.AlertType.WARNING, "无效输入", "题目数量必须在10到30之间"); + return; + } + + // Create and start exam session + com.personalproject.model.ExamSession examSession = controller.createExamSession( + userAccount.username(), selectedDifficulty, questionCount); + + ExamView examView = new ExamView(primaryStage, controller, examSession); + primaryStage.getScene().setRoot(examView); + } + + /** + * Handles the back button action. + */ + private void handleBack() { + MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount); + primaryStage.getScene().setRoot(mainMenuView); + } + + /** + * Shows an alert dialog. + * + * @param alertType Type of alert + * @param title Title of the alert + * @param message Message to display + */ + private void showAlert(Alert.AlertType alertType, String title, String message) { + Alert alert = new Alert(alertType); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/ui/views/ExamView.java b/src/main/java/com/personalproject/ui/views/ExamView.java new file mode 100644 index 0000000..8b30cd3 --- /dev/null +++ b/src/main/java/com/personalproject/ui/views/ExamView.java @@ -0,0 +1,261 @@ +package com.personalproject.ui.views; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.stage.Stage; +import com.personalproject.controller.MathLearningController; +import com.personalproject.model.ExamSession; +import com.personalproject.model.QuizQuestion; +import com.personalproject.ui.views.ExamResultsView; + +/** + * View for taking the exam with questions and answer options. + */ +public class ExamView extends BorderPane { + + private final Stage primaryStage; + private final MathLearningController controller; + private final ExamSession examSession; + private Label questionNumberLabel; + private Label questionTextLabel; + private ToggleGroup answerToggleGroup; + private VBox optionsBox; + private Button nextButton; + private Button previousButton; + private Button finishButton; + private HBox buttonBox; + + /** + * Constructor for ExamView. + * + * @param primaryStage The main stage of the application + * @param controller The math learning controller + * @param examSession The current exam session + */ + public ExamView(Stage primaryStage, MathLearningController controller, ExamSession examSession) { + this.primaryStage = primaryStage; + this.controller = controller; + this.examSession = examSession; + initializeUI(); + } + + /** + * Initializes the UI components. + */ + private void initializeUI() { + // Create the main layout + VBox mainLayout = new VBox(20); + mainLayout.setAlignment(Pos.CENTER); + mainLayout.setPadding(new Insets(20)); + + // Question number + questionNumberLabel = new Label(); + questionNumberLabel.setFont(Font.font("System", FontWeight.BOLD, 16)); + + // Question text + questionTextLabel = new Label(); + questionTextLabel.setWrapText(true); + questionTextLabel.setFont(Font.font("System", FontWeight.NORMAL, 14)); + questionTextLabel.setMaxWidth(500); + + // Options + optionsBox = new VBox(10); + optionsBox.setPadding(new Insets(10)); + + // Buttons + buttonBox = new HBox(15); + buttonBox.setAlignment(Pos.CENTER); + + previousButton = new Button("上一题"); + nextButton = new Button("下一题"); + finishButton = new Button("完成考试"); + + // Set button sizes + previousButton.setPrefSize(100, 35); + nextButton.setPrefSize(100, 35); + finishButton.setPrefSize(120, 35); + + buttonBox.getChildren().addAll(previousButton, nextButton, finishButton); + + // Add components to main layout + mainLayout.getChildren().addAll(questionNumberLabel, questionTextLabel, optionsBox, buttonBox); + + // Set the center of the border pane + setCenter(mainLayout); + + // Load the first question + loadCurrentQuestion(); + + // Add event handlers + addEventHandlers(); + } + + /** + * Loads the current question into the UI. + */ + private void loadCurrentQuestion() { + try { + // Check if exam is complete before loading next question + if (examSession.isComplete()) { + // If exam is complete, the finish button should be enabled + updateButtonStates(); + return; + } + + QuizQuestion currentQuestion = examSession.getCurrentQuestion(); + int currentIndex = examSession.getCurrentQuestionIndex(); + + if (currentQuestion == null) { + showAlert(Alert.AlertType.ERROR, "错误", "当前题目为空,请重新开始考试"); + return; + } + + // Update question number and text + questionNumberLabel.setText("第 " + (currentIndex + 1) + " 题"); + questionTextLabel.setText(currentQuestion.getQuestionText()); + + // Clear previous options + optionsBox.getChildren().clear(); + + // Create new options + answerToggleGroup = new ToggleGroup(); + + for (int i = 0; i < currentQuestion.getOptions().size(); i++) { + String option = currentQuestion.getOptions().get(i); + RadioButton optionButton = new RadioButton((i + 1) + ". " + option); + optionButton.setToggleGroup(answerToggleGroup); + optionButton.setUserData(i); // Store option index + + // If this question already has an answer, select it + if (examSession.hasAnswered(currentIndex) && + examSession.getUserAnswer(currentIndex) == i) { + optionButton.setSelected(true); + } + + optionsBox.getChildren().add(optionButton); + } + + // Update button states + updateButtonStates(); + } catch (Exception e) { + showAlert(Alert.AlertType.ERROR, "错误", "加载题目时发生错误: " + e.getMessage()); + } + } + + /** + * Updates the state of navigation buttons based on current position. + */ + private void updateButtonStates() { + try { + int currentIndex = examSession.getCurrentQuestionIndex(); + int totalQuestions = examSession.getTotalQuestions(); + + // Handle potential edge cases + if (totalQuestions <= 0) { + // If there are no questions, disable all navigation + previousButton.setDisable(true); + nextButton.setDisable(true); + finishButton.setDisable(false); // Allow finishing exam + return; + } + + // Previous button state + previousButton.setDisable(currentIndex < 0 || currentIndex == 0); + + // Next button state + nextButton.setDisable(currentIndex < 0 || currentIndex >= totalQuestions - 1); + + // Finish button state - enabled when exam is complete or at the last question + boolean isExamComplete = examSession.isComplete(); + boolean isAtLastQuestion = (currentIndex >= totalQuestions - 1); + finishButton.setDisable(!(isExamComplete || isAtLastQuestion)); + } catch (Exception e) { + // In case of any error, disable navigation buttons to prevent further issues + previousButton.setDisable(true); + nextButton.setDisable(true); + finishButton.setDisable(false); // Still allow finishing + showAlert(Alert.AlertType.ERROR, "错误", "更新按钮状态时发生错误: " + e.getMessage()); + } + } + + /** + * Adds event handlers to UI components. + */ + private void addEventHandlers() { + nextButton.setOnAction(e -> handleNextQuestion()); + previousButton.setOnAction(e -> handlePreviousQuestion()); + finishButton.setOnAction(e -> handleFinishExam()); + + // Add change listener to save answer when an option is selected + answerToggleGroup.selectedToggleProperty().addListener((obs, oldSelection, newSelection) -> { + if (newSelection != null) { + int selectedIndex = (Integer) newSelection.getUserData(); + examSession.setAnswer(selectedIndex); + } + }); + } + + /** + * Handles the next question button action. + */ + private void handleNextQuestion() { + try { + if (examSession.goToNextQuestion()) { + loadCurrentQuestion(); + } else { + // If we can't go to next question, we might be at the end + // Check if exam is complete and update button states accordingly + updateButtonStates(); + } + } catch (Exception e) { + showAlert(Alert.AlertType.ERROR, "错误", "导航到下一题时发生错误: " + e.getMessage()); + } + } + + /** + * Handles the previous question button action. + */ + private void handlePreviousQuestion() { + try { + if (examSession.goToPreviousQuestion()) { + loadCurrentQuestion(); + } else { + // If we can't go to previous question, we might be at the beginning + updateButtonStates(); + } + } catch (Exception e) { + showAlert(Alert.AlertType.ERROR, "错误", "导航到上一题时发生错误: " + e.getMessage()); + } + } + + /** + * Shows an alert dialog. + * + * @param alertType Type of alert + * @param title Title of the alert + * @param message Message to display + */ + private void showAlert(Alert.AlertType alertType, String title, String message) { + Alert alert = new Alert(alertType); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } + + /** + * Handles the finish exam button action. + */ + private void handleFinishExam() { + // Save exam results + controller.saveExamResults(examSession); + + // Show results + ExamResultsView resultsView = new ExamResultsView(primaryStage, controller, examSession); + primaryStage.getScene().setRoot(resultsView); + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/ui/views/MainMenuView.java b/src/main/java/com/personalproject/ui/views/MainMenuView.java new file mode 100644 index 0000000..d3138c0 --- /dev/null +++ b/src/main/java/com/personalproject/ui/views/MainMenuView.java @@ -0,0 +1,117 @@ +package com.personalproject.ui.views; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.stage.Stage; +import com.personalproject.controller.MathLearningController; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; +import com.personalproject.auth.UserAccount; +import com.personalproject.ui.scenes.LoginScene; + +/** + * View for the main menu where users can start exams or change settings. + */ +public class MainMenuView extends BorderPane { + + private final Stage primaryStage; + private final MathLearningController controller; + private final UserAccount userAccount; + private Button startExamButton; + private Button changePasswordButton; + private Button logoutButton; + + /** + * Constructor for MainMenuView. + * + * @param primaryStage The main stage of the application + * @param controller The math learning controller + * @param userAccount The current user account + */ + public MainMenuView(Stage primaryStage, MathLearningController controller, UserAccount userAccount) { + this.primaryStage = primaryStage; + this.controller = controller; + this.userAccount = userAccount; + initializeUI(); + } + + /** + * Initializes the UI components. + */ + private void initializeUI() { + // Create the main layout + VBox mainLayout = new VBox(20); + mainLayout.setAlignment(Pos.CENTER); + mainLayout.setPadding(new Insets(20)); + + // Welcome message + Label welcomeLabel = new Label("欢迎, " + userAccount.username()); + welcomeLabel.setFont(Font.font("System", FontWeight.BOLD, 18)); + + // Difficulty info + Label difficultyLabel = new Label("当前难度: " + userAccount.difficultyLevel().getDisplayName()); + difficultyLabel.setFont(Font.font("System", FontWeight.NORMAL, 14)); + + // Buttons + VBox buttonBox = new VBox(15); + buttonBox.setAlignment(Pos.CENTER); + + startExamButton = new Button("开始考试"); + changePasswordButton = new Button("修改密码"); + logoutButton = new Button("退出登录"); + + // Set button sizes + startExamButton.setPrefSize(150, 40); + changePasswordButton.setPrefSize(150, 40); + logoutButton.setPrefSize(150, 40); + + buttonBox.getChildren().addAll(startExamButton, changePasswordButton, logoutButton); + + // Add components to main layout + mainLayout.getChildren().addAll(welcomeLabel, difficultyLabel, buttonBox); + + // Set the center of the border pane + setCenter(mainLayout); + + // Add event handlers + addEventHandlers(); + } + + /** + * Adds event handlers to UI components. + */ + private void addEventHandlers() { + startExamButton.setOnAction(e -> handleStartExam()); + changePasswordButton.setOnAction(e -> handleChangePassword()); + logoutButton.setOnAction(e -> handleLogout()); + } + + /** + * Handles the start exam button action. + */ + private void handleStartExam() { + ExamSelectionView examSelectionView = new ExamSelectionView(primaryStage, controller, userAccount); + primaryStage.getScene().setRoot(examSelectionView); + } + + /** + * Handles the change password button action. + */ + private void handleChangePassword() { + PasswordChangeView passwordChangeView = new PasswordChangeView(primaryStage, controller, userAccount); + primaryStage.getScene().setRoot(passwordChangeView); + } + + /** + * Handles the logout button action. + */ + private void handleLogout() { + // Go back to login screen + LoginScene loginScene = new LoginScene(primaryStage, controller); + primaryStage.getScene().setRoot(loginScene); + } +} \ No newline at end of file diff --git a/src/main/java/com/personalproject/ui/views/PasswordChangeView.java b/src/main/java/com/personalproject/ui/views/PasswordChangeView.java new file mode 100644 index 0000000..9f5b98a --- /dev/null +++ b/src/main/java/com/personalproject/ui/views/PasswordChangeView.java @@ -0,0 +1,167 @@ +package com.personalproject.ui.views; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.stage.Stage; +import com.personalproject.controller.MathLearningController; +import com.personalproject.auth.UserAccount; +import com.personalproject.ui.views.MainMenuView; + +/** + * View for changing user password. + */ +public class PasswordChangeView extends BorderPane { + + private final Stage primaryStage; + private final MathLearningController controller; + private final UserAccount userAccount; + private PasswordField oldPasswordField; + private PasswordField newPasswordField; + private PasswordField confirmNewPasswordField; + private Button changePasswordButton; + private Button backButton; + + /** + * Constructor for PasswordChangeView. + * + * @param primaryStage The main stage of the application + * @param controller The math learning controller + * @param userAccount The current user account + */ + public PasswordChangeView(Stage primaryStage, MathLearningController controller, UserAccount userAccount) { + this.primaryStage = primaryStage; + this.controller = controller; + this.userAccount = userAccount; + initializeUI(); + } + + /** + * Initializes the UI components. + */ + private void initializeUI() { + // Create the main layout + VBox mainLayout = new VBox(20); + mainLayout.setAlignment(Pos.CENTER); + mainLayout.setPadding(new Insets(20)); + + // Title + Label titleLabel = new Label("修改密码"); + titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24)); + + // Form for password change + GridPane passwordForm = new GridPane(); + passwordForm.setHgap(15); + passwordForm.setVgap(15); + passwordForm.setAlignment(Pos.CENTER); + + Label oldPasswordLabel = new Label("当前密码:"); + oldPasswordField = new PasswordField(); + oldPasswordField.setPrefWidth(200); + + Label newPasswordLabel = new Label("新密码 (6-10位,包含大小写字母和数字):"); + newPasswordField = new PasswordField(); + newPasswordField.setPrefWidth(200); + + Label confirmNewPasswordLabel = new Label("确认新密码:"); + confirmNewPasswordField = new PasswordField(); + confirmNewPasswordField.setPrefWidth(200); + + passwordForm.add(oldPasswordLabel, 0, 0); + passwordForm.add(oldPasswordField, 1, 0); + passwordForm.add(newPasswordLabel, 0, 1); + passwordForm.add(newPasswordField, 1, 1); + passwordForm.add(confirmNewPasswordLabel, 0, 2); + passwordForm.add(confirmNewPasswordField, 1, 2); + + // Buttons + HBox buttonBox = new HBox(15); + buttonBox.setAlignment(Pos.CENTER); + + changePasswordButton = new Button("修改密码"); + backButton = new Button("返回"); + + // Set button sizes + changePasswordButton.setPrefSize(120, 40); + backButton.setPrefSize(120, 40); + + buttonBox.getChildren().addAll(changePasswordButton, backButton); + + // Add components to main layout + mainLayout.getChildren().addAll(titleLabel, passwordForm, buttonBox); + + // Set the center of the border pane + setCenter(mainLayout); + + // Add event handlers + addEventHandlers(); + } + + /** + * Adds event handlers to UI components. + */ + private void addEventHandlers() { + changePasswordButton.setOnAction(e -> handleChangePassword()); + backButton.setOnAction(e -> handleBack()); + } + + /** + * Handles the change password button action. + */ + private void handleChangePassword() { + String oldPassword = oldPasswordField.getText(); + String newPassword = newPasswordField.getText(); + String confirmNewPassword = confirmNewPasswordField.getText(); + + if (oldPassword.isEmpty() || newPassword.isEmpty() || confirmNewPassword.isEmpty()) { + showAlert(Alert.AlertType.WARNING, "警告", "请填写所有字段"); + return; + } + + if (!newPassword.equals(confirmNewPassword)) { + showAlert(Alert.AlertType.ERROR, "密码不匹配", "新密码和确认密码不一致"); + return; + } + + if (!controller.isValidPassword(newPassword)) { + showAlert(Alert.AlertType.ERROR, "密码不符合要求", + "密码长度必须为6-10位,且包含大小写字母和数字"); + return; + } + + boolean success = controller.changePassword(userAccount.username(), oldPassword, newPassword); + + if (success) { + showAlert(Alert.AlertType.INFORMATION, "修改成功", "密码修改成功!"); + handleBack(); // Go back to main menu + } else { + showAlert(Alert.AlertType.ERROR, "修改失败", "当前密码错误或修改失败"); + } + } + + /** + * Handles the back button action. + */ + private void handleBack() { + MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount); + primaryStage.getScene().setRoot(mainMenuView); + } + + /** + * Shows an alert dialog. + * + * @param alertType Type of alert + * @param title Title of the alert + * @param message Message to display + */ + private void showAlert(Alert.AlertType alertType, String title, String message) { + Alert alert = new Alert(alertType); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } +} \ No newline at end of file -- 2.34.1 From e355c5b526857cee48bf4f21523334031b71d4bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E8=8A=B3=E5=AE=87?= <15528541+cheng-fangyu@user.noreply.gitee.com> Date: Wed, 8 Oct 2025 22:40:43 +0800 Subject: [PATCH 3/9] =?UTF-8?q?=E7=A8=8B=E8=8A=B3=E5=AE=87-Pom=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=8E=A8=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 pom.xml diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c62513a --- /dev/null +++ b/pom.xml @@ -0,0 +1,143 @@ + + + 4.0.0 + + com.personalproject + math-learning + 1.0-SNAPSHOT + jar + + Math Learning Application + A desktop application for math learning with exam functionality + + + 17 + 17 + UTF-8 + 21 + 0.0.8 + + + + + + org.openjfx + javafx-controls + ${javafx.version} + + + org.openjfx + javafx-fxml + ${javafx.version} + + + + + org.junit.jupiter + junit-jupiter-api + 5.9.2 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.9.2 + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + + + + + + org.openjfx + javafx-maven-plugin + ${javafx.maven.plugin.version} + + com.personalproject.ui.MathExamGUI + + + + + + + + + + default-cli + + com.personalproject.ui.MathExamGUI + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0 + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + com.personalproject.ui.MathExamGUI + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.4.1 + + + package + + shade + + + + + com.personalproject.ui.MathExamGUI + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + \ No newline at end of file -- 2.34.1 From 383162505935bd2313070813715825cd6f40899b Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 9 Oct 2025 19:44:29 +0800 Subject: [PATCH 4/9] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E4=B8=BA=E4=B8=AD=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/AccountRepository.java | 52 +++---- .../personalproject/auth/EmailService.java | 31 ++-- .../auth/PasswordValidator.java | 17 ++- src/com/personalproject/auth/UserAccount.java | 12 +- .../controller/MathLearningController.java | 12 +- .../generator/PrimaryQuestionGenerator.java | 80 ++++++++++- .../personalproject/model/ExamSession.java | 134 +++++++++++++----- .../personalproject/model/QuizQuestion.java | 32 ++--- .../service/MathExpressionEvaluator.java | 66 ++++----- .../service/MathLearningService.java | 12 +- .../service/RegistrationService.java | 12 +- .../auth/AccountRepository.java | 52 +++---- .../personalproject/auth/EmailService.java | 31 ++-- .../auth/PasswordValidator.java | 17 ++- .../com/personalproject/auth/UserAccount.java | 12 +- .../controller/MathLearningController.java | 8 +- .../generator/PrimaryQuestionGenerator.java | 16 +-- .../personalproject/model/ExamSession.java | 90 ++++++------ .../personalproject/model/QuizQuestion.java | 32 ++--- .../service/MathExpressionEvaluator.java | 66 ++++----- .../service/MathLearningService.java | 8 +- .../service/RegistrationService.java | 8 +- .../com/personalproject/ui/MathExamGUI.java | 16 +-- .../personalproject/ui/scenes/LoginScene.java | 52 +++---- .../ui/scenes/RegistrationScene.java | 52 +++---- .../ui/views/ExamResultsView.java | 58 ++++---- .../ui/views/ExamSelectionView.java | 50 +++---- .../personalproject/ui/views/ExamView.java | 100 ++++++------- .../ui/views/MainMenuView.java | 40 +++--- .../ui/views/PasswordChangeView.java | 46 +++--- 30 files changed, 694 insertions(+), 520 deletions(-) diff --git a/src/com/personalproject/auth/AccountRepository.java b/src/com/personalproject/auth/AccountRepository.java index c5a00a2..5bb193d 100644 --- a/src/com/personalproject/auth/AccountRepository.java +++ b/src/com/personalproject/auth/AccountRepository.java @@ -36,45 +36,45 @@ public final class AccountRepository { } /** - * Registers a new user account with email. + * 使用电子邮箱注册新用户账号。 * - * @param username The username - * @param email The email address - * @param difficultyLevel The selected difficulty level - * @return true if registration was successful, false if username already exists + * @param username 用户名 + * @param email 邮箱地址 + * @param difficultyLevel 选择的难度级别 + * @return 若注册成功则返回 true,若用户名已存在则返回 false */ public boolean registerUser(String username, String email, DifficultyLevel difficultyLevel) { String trimmedUsername = username.trim(); String trimmedEmail = email.trim(); if (accounts.containsKey(trimmedUsername)) { - return false; // Username already exists + return false; // 用户名已存在 } - // Check if email is already used by another account + // 检查邮箱是否已被其他账号使用 for (UserAccount account : accounts.values()) { if (account.email().equals(trimmedEmail) && account.isRegistered()) { - return false; // Email already registered + return false; // 邮箱已注册 } } UserAccount newAccount = new UserAccount( trimmedUsername, trimmedEmail, - "", // Empty password initially + "", // 初始为空密码 difficultyLevel, LocalDateTime.now(), - false); // Not registered until password is set + false); // 在设置密码前视为未注册 accounts.put(trimmedUsername, newAccount); return true; } /** - * Sets the password for a user after registration. + * 在注册后为用户设置密码。 * - * @param username The username - * @param password The password to set - * @return true if successful, false if user doesn't exist + * @param username 用户名 + * @param password 要设置的密码 + * @return 设置成功返回 true,若用户不存在则返回 false */ public boolean setPassword(String username, String password) { UserAccount account = accounts.get(username.trim()); @@ -88,18 +88,18 @@ public final class AccountRepository { password, account.difficultyLevel(), account.registrationDate(), - true); // Now registered + true); // 设置密码后标记为已注册 accounts.put(username.trim(), updatedAccount); return true; } /** - * Changes the password for an existing user. + * 修改现有用户的密码。 * - * @param username The username - * @param oldPassword The current password - * @param newPassword The new password - * @return true if successful, false if old password is incorrect or user doesn't exist + * @param username 用户名 + * @param oldPassword 当前密码 + * @param newPassword 新密码 + * @return 修改成功返回 true,若旧密码错误或用户不存在则返回 false */ public boolean changePassword(String username, String oldPassword, String newPassword) { UserAccount account = accounts.get(username.trim()); @@ -119,20 +119,20 @@ public final class AccountRepository { } /** - * Checks if a user exists in the system. + * 检查用户是否存在。 * - * @param username The username to check - * @return true if user exists, false otherwise + * @param username 待检查的用户名 + * @return 若存在则返回 true,否则返回 false */ public boolean userExists(String username) { return accounts.containsKey(username.trim()); } /** - * Gets a user account by username. + * 按用户名获取用户账户。 * - * @param username The username - * @return Optional containing the user account if found + * @param username 用户名 + * @return 若找到则返回包含用户账户的 Optional */ public Optional getUser(String username) { return Optional.ofNullable(accounts.get(username.trim())); diff --git a/src/com/personalproject/auth/EmailService.java b/src/com/personalproject/auth/EmailService.java index d790c83..6ae2b1c 100644 --- a/src/com/personalproject/auth/EmailService.java +++ b/src/com/personalproject/auth/EmailService.java @@ -3,7 +3,7 @@ package com.personalproject.auth; import java.util.Random; /** - * Interface for sending emails with registration codes. + * 用于发送带有注册码的电子邮件的工具类。 */ public final class EmailService { @@ -12,13 +12,13 @@ public final class EmailService { private static final Random RANDOM = new Random(); private EmailService() { - // Prevent instantiation of utility class + // 防止实例化此工具类 } /** - * Generates a random registration code. + * 生成随机注册码。 * - * @return A randomly generated registration code + * @return 随机生成的注册码 */ public static String generateRegistrationCode() { StringBuilder code = new StringBuilder(); @@ -29,34 +29,33 @@ public final class EmailService { } /** - * Sends a registration code to the specified email address. In a real implementation, this would - * connect to an email server. + * 将注册码发送到指定邮箱。真实实现中会连接邮件服务器。 * - * @param email The email address to send the code to - * @param registrationCode The registration code to send - * @return true if successfully sent (in this mock implementation, always true) + * @param email 收件人邮箱 + * @param registrationCode 要发送的注册码 + * @return 发送成功返回 true(在此模拟实现中始终为 true) */ public static boolean sendRegistrationCode(String email, String registrationCode) { - // In a real implementation, this would connect to an email server - // For the mock implementation, we'll just print to console + // 真实实现会连接邮件服务器 + // 模拟实现仅在控制台打印 System.out.println("Sending registration code " + registrationCode + " to " + email); return true; } /** - * Validates if an email address has a valid format. + * 校验电子邮件地址的格式是否有效。 * - * @param email The email address to validate - * @return true if the email has valid format, false otherwise + * @param email 待校验的邮箱地址 + * @return 若格式有效则返回 true,否则返回 false */ public static boolean isValidEmail(String email) { if (email == null || email.trim().isEmpty()) { return false; } - // Simple email validation using regex + // 使用正则表达式进行简单邮箱校验 String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@" + "(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"; return email.matches(emailRegex); } -} \ No newline at end of file +} diff --git a/src/com/personalproject/auth/PasswordValidator.java b/src/com/personalproject/auth/PasswordValidator.java index acece6f..6b4ab91 100644 --- a/src/com/personalproject/auth/PasswordValidator.java +++ b/src/com/personalproject/auth/PasswordValidator.java @@ -3,7 +3,7 @@ package com.personalproject.auth; import java.util.regex.Pattern; /** - * Utility class for password validation. + * 用于验证密码规则的工具类。 */ public final class PasswordValidator { @@ -11,15 +11,18 @@ public final class PasswordValidator { Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{6,10}$"); private PasswordValidator() { - // Prevent instantiation of utility class + // 防止实例化此工具类 } /** - * Validates if a password meets the requirements: - 6-10 characters - Contains at least one - * uppercase letter - Contains at least one lowercase letter - Contains at least one digit. + * 校验密码是否满足以下要求: + * - 长度 6-10 位 + * - 至少包含一个大写字母 + * - 至少包含一个小写字母 + * - 至少包含一个数字 * - * @param password The password to validate - * @return true if password meets requirements, false otherwise + * @param password 待校验的密码 + * @return 若满足要求则返回 true,否则返回 false */ public static boolean isValidPassword(String password) { if (password == null) { @@ -27,4 +30,4 @@ public final class PasswordValidator { } return PASSWORD_PATTERN.matcher(password).matches(); } -} \ No newline at end of file +} diff --git a/src/com/personalproject/auth/UserAccount.java b/src/com/personalproject/auth/UserAccount.java index 80d5acf..07543a0 100644 --- a/src/com/personalproject/auth/UserAccount.java +++ b/src/com/personalproject/auth/UserAccount.java @@ -15,13 +15,13 @@ public record UserAccount( boolean isRegistered) { /** - * Creates a new user account with registration date set to now. + * 创建新的用户账户,并使用当前时间作为注册时间。 * - * @param username The username - * @param email The email address - * @param password The password - * @param difficultyLevel The selected difficulty level - * @param isRegistered Whether the user has completed registration + * @param username 用户名 + * @param email 邮箱地址 + * @param password 密码 + * @param difficultyLevel 选择的难度级别 + * @param isRegistered 用户是否已完成注册 */ public UserAccount { if (username == null || username.trim().isEmpty()) { diff --git a/src/com/personalproject/controller/MathLearningController.java b/src/com/personalproject/controller/MathLearningController.java index c847b94..4edd694 100644 --- a/src/com/personalproject/controller/MathLearningController.java +++ b/src/com/personalproject/controller/MathLearningController.java @@ -142,4 +142,14 @@ public final class MathLearningController { public boolean isValidEmail(String email) { return MathLearningService.isValidEmail(email); } -} \ No newline at end of file + + /** + * 按用户名获取用户账户。 + * + * @param username 用户名 + * @return 若找到则返回包含用户账户的 Optional + */ + public Optional getUserAccount(String username) { + return mathLearningService.getUser(username); + } +} diff --git a/src/com/personalproject/generator/PrimaryQuestionGenerator.java b/src/com/personalproject/generator/PrimaryQuestionGenerator.java index 9073153..622bd0f 100644 --- a/src/com/personalproject/generator/PrimaryQuestionGenerator.java +++ b/src/com/personalproject/generator/PrimaryQuestionGenerator.java @@ -1,13 +1,20 @@ package com.personalproject.generator; +import com.personalproject.model.QuizQuestion; +import com.personalproject.service.MathExpressionEvaluator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Random; /** * 生成包含基础四则运算的小学难度题目表达式. */ -public final class PrimaryQuestionGenerator implements QuestionGenerator { +public final class PrimaryQuestionGenerator implements QuestionGenerator, QuizQuestionGenerator { private static final String[] OPERATORS = {"+", "-", "*", "/"}; + private static final int OPTIONS_COUNT = 4; @Override public String generateQuestion(Random random) { @@ -28,4 +35,75 @@ public final class PrimaryQuestionGenerator implements QuestionGenerator { } return expression; } + + @Override + public QuizQuestion generateQuizQuestion(Random random) { + // 生成数学表达式 + int operandCount = random.nextInt(4) + 2; + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < operandCount; index++) { + if (index > 0) { + String operator = OPERATORS[random.nextInt(OPERATORS.length)]; + builder.append(' ').append(operator).append(' '); + } + int value = random.nextInt(100) + 1; + builder.append(value); + } + String expression = builder.toString(); + if (operandCount > 1 && random.nextBoolean()) { + expression = '(' + expression + ')'; + } + + // 直接计算正确答案 + double correctAnswer; + try { + correctAnswer = MathExpressionEvaluator.evaluate(expression); + } catch (Exception e) { + // 如果计算失败则使用兜底值 + correctAnswer = 0.0; + } + + // 生成选项 + List options = generateOptions(correctAnswer, random); + + // 随机选择正确答案索引 + int correctAnswerIndex = random.nextInt(options.size()); + + // 确保正确答案在选项中 + String correctOption = String.format("%.2f", correctAnswer); + String oldOption = options.set(correctAnswerIndex, correctOption); + + // 将被替换的选项加回去以保持 4 个选项 + options.add(oldOption); + + return new QuizQuestion(expression, options, correctAnswerIndex); + } + + /** + * 生成选择题选项 + */ + private List generateOptions(double correctAnswer, Random random) { + List options = new ArrayList<>(); + + // 将正确答案加入选项 + options.add(String.format("%.2f", correctAnswer)); + + // 加入错误选项 + for (int i = 0; i < OPTIONS_COUNT - 1; i++) { + double incorrectAnswer = correctAnswer + (random.nextGaussian() * 10); // 添加随机偏移 + if (Math.abs(incorrectAnswer - correctAnswer) < 0.1) { // 确保与正确答案不同 + incorrectAnswer += 1.5; + } + options.add(String.format("%.2f", incorrectAnswer)); + } + + // 打乱顺序以随机化正确答案位置 + Collections.shuffle(options, random); + + // 在打乱后查找正确答案的索引 + String correctAnswerStr = String.format("%.2f", correctAnswer); + int correctIndex = options.indexOf(correctAnswerStr); + + return options; + } } diff --git a/src/com/personalproject/model/ExamSession.java b/src/com/personalproject/model/ExamSession.java index 6437ba5..6b8f918 100644 --- a/src/com/personalproject/model/ExamSession.java +++ b/src/com/personalproject/model/ExamSession.java @@ -5,7 +5,7 @@ import java.util.ArrayList; import java.util.List; /** - * Represents an exam session for a user. + * 表示用户的考试会话。 */ public final class ExamSession { @@ -17,11 +17,11 @@ public final class ExamSession { private int currentQuestionIndex; /** - * Creates a new exam session. + * 创建新的考试会话。 * - * @param username The username of the test taker - * @param difficultyLevel The difficulty level of the exam - * @param questions The list of questions for the exam + * @param username 考生的用户名 + * @param difficultyLevel 考试的难度级别 + * @param questions 考试题目列表 */ public ExamSession(String username, DifficultyLevel difficultyLevel, List questions) { @@ -37,9 +37,9 @@ public final class ExamSession { this.username = username; this.difficultyLevel = difficultyLevel; - this.questions = List.copyOf(questions); // Immutable copy of questions + this.questions = List.copyOf(questions); // 题目的不可变副本 this.userAnswers = new ArrayList<>(); - // Initialize user answers with -1 (no answer selected) + // 使用 -1 初始化用户答案(表示未选择) for (int i = 0; i < questions.size(); i++) { userAnswers.add(-1); } @@ -48,54 +48,54 @@ public final class ExamSession { } /** - * Gets the username of the test taker. + * 获取考生的用户名。 * - * @return The username + * @return 用户名 */ public String getUsername() { return username; } /** - * Gets the difficulty level of the exam. + * 获取考试的难度级别。 * - * @return The difficulty level + * @return 考试难度级别 */ public DifficultyLevel getDifficultyLevel() { return difficultyLevel; } /** - * Gets the list of questions in the exam. + * 获取考试中的题目列表。 * - * @return An unmodifiable list of questions + * @return 不可修改的题目列表 */ public List getQuestions() { return questions; } /** - * Gets the user's answers to the questions. + * 获取用户对各题的作答。 * - * @return A list of answer indices (-1 means no answer selected) + * @return 答案索引列表(-1 表示未选择) */ public List getUserAnswers() { - return List.copyOf(userAnswers); // Return a copy to prevent modification + return List.copyOf(userAnswers); // 返回副本以防止被修改 } /** - * Gets the current question index. + * 获取当前题目的索引。 * - * @return The current question index + * @return 当前题目索引 */ public int getCurrentQuestionIndex() { return currentQuestionIndex; } /** - * Sets the user's answer for the current question. + * 为当前题目记录用户的答案。 * - * @param answerIndex The index of the selected answer + * @param answerIndex 选中答案的索引 */ public void setAnswer(int answerIndex) { if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) { @@ -108,9 +108,9 @@ public final class ExamSession { } /** - * Moves to the next question. + * 跳转到下一题。 * - * @return true if successfully moved to next question, false if already at the last question + * @return 若成功跳转则返回 true,若已是最后一题则返回 false */ public boolean goToNextQuestion() { if (currentQuestionIndex < questions.size() - 1) { @@ -121,9 +121,9 @@ public final class ExamSession { } /** - * Moves to the previous question. + * 返回上一题。 * - * @return true if successfully moved to previous question, false if already at the first question + * @return 若成功返回则返回 true,若已是第一题则返回 false */ public boolean goToPreviousQuestion() { if (currentQuestionIndex > 0) { @@ -134,18 +134,18 @@ public final class ExamSession { } /** - * Checks if the exam is complete (all questions answered or at the end). + * 检查考试是否完成(所有题目已作答或到达最后一题)。 * - * @return true if the exam is complete, false otherwise + * @return 若考试已完成则返回 true,否则返回 false */ public boolean isComplete() { return currentQuestionIndex >= questions.size() - 1; } /** - * Gets the current question. + * 获取当前题目。 * - * @return The current quiz question + * @return 当前的测验题 */ public QuizQuestion getCurrentQuestion() { if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) { @@ -155,10 +155,10 @@ public final class ExamSession { } /** - * Gets the user's answer for a specific question. + * 获取指定题目的用户答案。 * - * @param questionIndex The index of the question - * @return The index of the user's answer (or -1 if no answer selected) + * @param questionIndex 题目索引 + * @return 用户答案的索引(未选择时为 -1) */ public int getUserAnswer(int questionIndex) { if (questionIndex < 0 || questionIndex >= questions.size()) { @@ -168,9 +168,9 @@ public final class ExamSession { } /** - * Calculates the score as a percentage. + * 计算得分百分比。 * - * @return The score as a percentage (0-100) + * @return 0-100 范围内的得分百分比 */ public double calculateScore() { int correctCount = 0; @@ -185,11 +185,73 @@ public final class ExamSession { } /** - * Gets the start time of the exam. + * 获取考试中的题目总数。 * - * @return The start time + * @return 题目总数 + */ + public int getTotalQuestions() { + return questions.size(); + } + + /** + * 检查指定题目是否已作答。 + * + * @param questionIndex 题目索引 + * @return 若已作答则返回 true,否则返回 false + */ + public boolean hasAnswered(int questionIndex) { + if (questionIndex < 0 || questionIndex >= questions.size()) { + throw new IllegalArgumentException("Question index out of bounds"); + } + return userAnswers.get(questionIndex) != -1; + } + + /** + * 获取考试开始时间。 + * + * @return 开始时间 */ public LocalDateTime getStartTime() { return startTime; } -} \ No newline at end of file + + /** + * 获取答对的题目数量。 + * + * @return 正确题目数量 + */ + public int getCorrectAnswersCount() { + int correctCount = 0; + for (int i = 0; i < questions.size(); i++) { + QuizQuestion question = questions.get(i); + int userAnswer = userAnswers.get(i); + if (userAnswer != -1 && question.isAnswerCorrect(userAnswer)) { + correctCount++; + } + } + return correctCount; + } + + /** + * 获取答错的题目数量。 + * + * @return 错误题目数量 + */ + public int getIncorrectAnswersCount() { + int totalAnswered = 0; + int correctCount = 0; + + for (int i = 0; i < questions.size(); i++) { + int userAnswer = userAnswers.get(i); + if (userAnswer != -1) { + totalAnswered++; + QuizQuestion question = questions.get(i); + if (question.isAnswerCorrect(userAnswer)) { + correctCount++; + } + } + } + + return totalAnswered - correctCount; + } +} diff --git a/src/com/personalproject/model/QuizQuestion.java b/src/com/personalproject/model/QuizQuestion.java index e66f14c..52c8f6b 100644 --- a/src/com/personalproject/model/QuizQuestion.java +++ b/src/com/personalproject/model/QuizQuestion.java @@ -3,7 +3,7 @@ package com.personalproject.model; import java.util.List; /** - * Represents a quiz question with multiple choice options. + * 表示一个带有多项选择的测验题目。 */ public final class QuizQuestion { @@ -12,11 +12,11 @@ public final class QuizQuestion { private final int correctAnswerIndex; /** - * Creates a new quiz question. + * 创建新的测验题目。 * - * @param questionText The text of the question - * @param options The list of answer options - * @param correctAnswerIndex The index of the correct answer in the options list + * @param questionText 题目文本 + * @param options 答案选项列表 + * @param correctAnswerIndex 正确答案在选项列表中的索引 */ public QuizQuestion(String questionText, List options, int correctAnswerIndex) { if (questionText == null || questionText.trim().isEmpty()) { @@ -30,44 +30,44 @@ public final class QuizQuestion { } this.questionText = questionText; - this.options = List.copyOf(options); // Immutable copy + this.options = List.copyOf(options); // 不可变副本 this.correctAnswerIndex = correctAnswerIndex; } /** - * Gets the question text. + * 获取题目文本。 * - * @return The question text + * @return 题目文本 */ public String getQuestionText() { return questionText; } /** - * Gets the list of answer options. + * 获取答案选项列表。 * - * @return An unmodifiable list of answer options + * @return 不可修改的答案选项列表 */ public List getOptions() { return options; } /** - * Gets the index of the correct answer in the options list. + * 获取正确答案在选项列表中的索引。 * - * @return The index of the correct answer + * @return 正确答案的索引 */ public int getCorrectAnswerIndex() { return correctAnswerIndex; } /** - * Checks if the given answer index matches the correct answer. + * 检查给定的答案索引是否正确。 * - * @param answerIndex The index of the user's answer - * @return true if the answer is correct, false otherwise + * @param answerIndex 用户答案的索引 + * @return 若答案正确则返回 true,否则返回 false */ public boolean isAnswerCorrect(int answerIndex) { return answerIndex == correctAnswerIndex; } -} \ No newline at end of file +} diff --git a/src/com/personalproject/service/MathExpressionEvaluator.java b/src/com/personalproject/service/MathExpressionEvaluator.java index 6e32ed1..c4ee580 100644 --- a/src/com/personalproject/service/MathExpressionEvaluator.java +++ b/src/com/personalproject/service/MathExpressionEvaluator.java @@ -6,7 +6,7 @@ import java.util.Stack; import java.util.regex.Pattern; /** - * A mathematical expression evaluator that can handle basic arithmetic operations. + * 可处理基本算术运算的数学表达式求值器。 */ public final class MathExpressionEvaluator { @@ -22,41 +22,41 @@ public final class MathExpressionEvaluator { } private MathExpressionEvaluator() { - // Prevent instantiation of utility class + // 防止实例化此工具类 } /** - * Evaluates a mathematical expression string. + * 计算数学表达式字符串的结果。 * - * @param expression The mathematical expression to evaluate - * @return The result of the evaluation - * @throws IllegalArgumentException If the expression is invalid + * @param expression 要计算的数学表达式 + * @return 计算结果 + * @throws IllegalArgumentException 如果表达式无效 */ public static double evaluate(String expression) { if (expression == null) { throw new IllegalArgumentException("Expression cannot be null"); } - expression = expression.replaceAll("\\s+", ""); // Remove whitespace + expression = expression.replaceAll("\\s+", ""); // 移除空白字符 if (expression.isEmpty()) { throw new IllegalArgumentException("Expression cannot be empty"); } - // Tokenize the expression + // 将表达式拆分为记号 String[] tokens = tokenize(expression); - // Convert infix to postfix notation using Shunting Yard algorithm + // 使用调度场算法将中缀表达式转换为后缀表达式 String[] postfix = infixToPostfix(tokens); - // Evaluate the postfix expression + // 计算后缀表达式 return evaluatePostfix(postfix); } /** - * Tokenizes the expression into numbers and operators. + * 将表达式拆分为数字与运算符的记号。 * - * @param expression The expression to tokenize - * @return An array of tokens + * @param expression 待拆分的表达式 + * @return 记号数组 */ private static String[] tokenize(String expression) { java.util.List tokens = new java.util.ArrayList<>(); @@ -79,7 +79,7 @@ public final class MathExpressionEvaluator { currentNumber.setLength(0); } - // Handle unary minus + // 处理一元负号 if (c == '-' && (i == 0 || expression.charAt(i - 1) == '(')) { currentNumber.append(c); } else { @@ -98,20 +98,20 @@ public final class MathExpressionEvaluator { } /** - * Checks if the character is an operator. + * 检查字符是否为运算符。 * - * @param c The character to check - * @return true if the character is an operator, false otherwise + * @param c 待检查的字符 + * @return 若字符是运算符则返回 true,否则返回 false */ private static boolean isOperator(char c) { return c == '+' || c == '-' || c == '*' || c == '/' || c == '^'; } /** - * Converts infix notation to postfix notation using the Shunting Yard algorithm. + * 使用调度场算法将中缀表达式转换为后缀表达式。 * - * @param tokens The tokens in infix notation - * @return An array of tokens in postfix notation + * @param tokens 中缀表达式的记号数组 + * @return 后缀表达式的记号数组 */ private static String[] infixToPostfix(String[] tokens) { java.util.List output = new java.util.ArrayList<>(); @@ -127,7 +127,7 @@ public final class MathExpressionEvaluator { output.add(operators.pop()); } if (!operators.isEmpty()) { - operators.pop(); // Remove the "(" + operators.pop(); // 移除 "(" } } else if (isOperator(token.charAt(0))) { while (!operators.isEmpty() @@ -147,10 +147,10 @@ public final class MathExpressionEvaluator { } /** - * Evaluates a postfix expression. + * 计算后缀表达式的值。 * - * @param postfix The tokens in postfix notation - * @return The result of the evaluation + * @param postfix 后缀表达式的记号数组 + * @return 计算结果 */ private static double evaluatePostfix(String[] postfix) { Stack values = new Stack<>(); @@ -178,12 +178,12 @@ public final class MathExpressionEvaluator { } /** - * Performs the specified operation on the two operands. + * 对两个操作数执行指定运算。 * - * @param a The first operand - * @param b The second operand - * @param operator The operator to apply - * @return The result of the operation + * @param a 第一个操作数 + * @param b 第二个操作数 + * @param operator 要应用的运算符 + * @return 运算结果 */ private static double performOperation(double a, double b, char operator) { switch (operator) { @@ -206,12 +206,12 @@ public final class MathExpressionEvaluator { } /** - * Checks if the token is a number. + * 检查记号是否为数字。 * - * @param token The token to check - * @return true if the token is a number, false otherwise + * @param token 待检查的记号 + * @return 若为数字则返回 true,否则返回 false */ private static boolean isNumber(String token) { return NUMBER_PATTERN.matcher(token).matches(); } -} \ No newline at end of file +} diff --git a/src/com/personalproject/service/MathLearningService.java b/src/com/personalproject/service/MathLearningService.java index e404ddb..7f7f62b 100644 --- a/src/com/personalproject/service/MathLearningService.java +++ b/src/com/personalproject/service/MathLearningService.java @@ -167,4 +167,14 @@ public final class MathLearningService { public boolean userExists(String username) { return registrationService.userExists(username); } -} \ No newline at end of file + + /** + * 按用户名获取用户账户。 + * + * @param username 用户名 + * @return 若找到则返回包含用户账户的 Optional + */ + public Optional getUser(String username) { + return registrationService.getUser(username); + } +} diff --git a/src/com/personalproject/service/RegistrationService.java b/src/com/personalproject/service/RegistrationService.java index 100b58d..024f2bf 100644 --- a/src/com/personalproject/service/RegistrationService.java +++ b/src/com/personalproject/service/RegistrationService.java @@ -135,4 +135,14 @@ public final class RegistrationService { public boolean userExists(String username) { return accountRepository.userExists(username); } -} \ No newline at end of file + + /** + * 按用户名获取用户账户。 + * + * @param username 用户名 + * @return 若找到则返回包含用户账户的 Optional + */ + public Optional getUser(String username) { + return accountRepository.getUser(username); + } +} diff --git a/src/main/java/com/personalproject/auth/AccountRepository.java b/src/main/java/com/personalproject/auth/AccountRepository.java index c5a00a2..5bb193d 100644 --- a/src/main/java/com/personalproject/auth/AccountRepository.java +++ b/src/main/java/com/personalproject/auth/AccountRepository.java @@ -36,45 +36,45 @@ public final class AccountRepository { } /** - * Registers a new user account with email. + * 使用电子邮箱注册新用户账号。 * - * @param username The username - * @param email The email address - * @param difficultyLevel The selected difficulty level - * @return true if registration was successful, false if username already exists + * @param username 用户名 + * @param email 邮箱地址 + * @param difficultyLevel 选择的难度级别 + * @return 若注册成功则返回 true,若用户名已存在则返回 false */ public boolean registerUser(String username, String email, DifficultyLevel difficultyLevel) { String trimmedUsername = username.trim(); String trimmedEmail = email.trim(); if (accounts.containsKey(trimmedUsername)) { - return false; // Username already exists + return false; // 用户名已存在 } - // Check if email is already used by another account + // 检查邮箱是否已被其他账号使用 for (UserAccount account : accounts.values()) { if (account.email().equals(trimmedEmail) && account.isRegistered()) { - return false; // Email already registered + return false; // 邮箱已注册 } } UserAccount newAccount = new UserAccount( trimmedUsername, trimmedEmail, - "", // Empty password initially + "", // 初始为空密码 difficultyLevel, LocalDateTime.now(), - false); // Not registered until password is set + false); // 在设置密码前视为未注册 accounts.put(trimmedUsername, newAccount); return true; } /** - * Sets the password for a user after registration. + * 在注册后为用户设置密码。 * - * @param username The username - * @param password The password to set - * @return true if successful, false if user doesn't exist + * @param username 用户名 + * @param password 要设置的密码 + * @return 设置成功返回 true,若用户不存在则返回 false */ public boolean setPassword(String username, String password) { UserAccount account = accounts.get(username.trim()); @@ -88,18 +88,18 @@ public final class AccountRepository { password, account.difficultyLevel(), account.registrationDate(), - true); // Now registered + true); // 设置密码后标记为已注册 accounts.put(username.trim(), updatedAccount); return true; } /** - * Changes the password for an existing user. + * 修改现有用户的密码。 * - * @param username The username - * @param oldPassword The current password - * @param newPassword The new password - * @return true if successful, false if old password is incorrect or user doesn't exist + * @param username 用户名 + * @param oldPassword 当前密码 + * @param newPassword 新密码 + * @return 修改成功返回 true,若旧密码错误或用户不存在则返回 false */ public boolean changePassword(String username, String oldPassword, String newPassword) { UserAccount account = accounts.get(username.trim()); @@ -119,20 +119,20 @@ public final class AccountRepository { } /** - * Checks if a user exists in the system. + * 检查用户是否存在。 * - * @param username The username to check - * @return true if user exists, false otherwise + * @param username 待检查的用户名 + * @return 若存在则返回 true,否则返回 false */ public boolean userExists(String username) { return accounts.containsKey(username.trim()); } /** - * Gets a user account by username. + * 按用户名获取用户账户。 * - * @param username The username - * @return Optional containing the user account if found + * @param username 用户名 + * @return 若找到则返回包含用户账户的 Optional */ public Optional getUser(String username) { return Optional.ofNullable(accounts.get(username.trim())); diff --git a/src/main/java/com/personalproject/auth/EmailService.java b/src/main/java/com/personalproject/auth/EmailService.java index d790c83..6ae2b1c 100644 --- a/src/main/java/com/personalproject/auth/EmailService.java +++ b/src/main/java/com/personalproject/auth/EmailService.java @@ -3,7 +3,7 @@ package com.personalproject.auth; import java.util.Random; /** - * Interface for sending emails with registration codes. + * 用于发送带有注册码的电子邮件的工具类。 */ public final class EmailService { @@ -12,13 +12,13 @@ public final class EmailService { private static final Random RANDOM = new Random(); private EmailService() { - // Prevent instantiation of utility class + // 防止实例化此工具类 } /** - * Generates a random registration code. + * 生成随机注册码。 * - * @return A randomly generated registration code + * @return 随机生成的注册码 */ public static String generateRegistrationCode() { StringBuilder code = new StringBuilder(); @@ -29,34 +29,33 @@ public final class EmailService { } /** - * Sends a registration code to the specified email address. In a real implementation, this would - * connect to an email server. + * 将注册码发送到指定邮箱。真实实现中会连接邮件服务器。 * - * @param email The email address to send the code to - * @param registrationCode The registration code to send - * @return true if successfully sent (in this mock implementation, always true) + * @param email 收件人邮箱 + * @param registrationCode 要发送的注册码 + * @return 发送成功返回 true(在此模拟实现中始终为 true) */ public static boolean sendRegistrationCode(String email, String registrationCode) { - // In a real implementation, this would connect to an email server - // For the mock implementation, we'll just print to console + // 真实实现会连接邮件服务器 + // 模拟实现仅在控制台打印 System.out.println("Sending registration code " + registrationCode + " to " + email); return true; } /** - * Validates if an email address has a valid format. + * 校验电子邮件地址的格式是否有效。 * - * @param email The email address to validate - * @return true if the email has valid format, false otherwise + * @param email 待校验的邮箱地址 + * @return 若格式有效则返回 true,否则返回 false */ public static boolean isValidEmail(String email) { if (email == null || email.trim().isEmpty()) { return false; } - // Simple email validation using regex + // 使用正则表达式进行简单邮箱校验 String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@" + "(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"; return email.matches(emailRegex); } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/auth/PasswordValidator.java b/src/main/java/com/personalproject/auth/PasswordValidator.java index acece6f..6b4ab91 100644 --- a/src/main/java/com/personalproject/auth/PasswordValidator.java +++ b/src/main/java/com/personalproject/auth/PasswordValidator.java @@ -3,7 +3,7 @@ package com.personalproject.auth; import java.util.regex.Pattern; /** - * Utility class for password validation. + * 用于验证密码规则的工具类。 */ public final class PasswordValidator { @@ -11,15 +11,18 @@ public final class PasswordValidator { Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{6,10}$"); private PasswordValidator() { - // Prevent instantiation of utility class + // 防止实例化此工具类 } /** - * Validates if a password meets the requirements: - 6-10 characters - Contains at least one - * uppercase letter - Contains at least one lowercase letter - Contains at least one digit. + * 校验密码是否满足以下要求: + * - 长度 6-10 位 + * - 至少包含一个大写字母 + * - 至少包含一个小写字母 + * - 至少包含一个数字 * - * @param password The password to validate - * @return true if password meets requirements, false otherwise + * @param password 待校验的密码 + * @return 若满足要求则返回 true,否则返回 false */ public static boolean isValidPassword(String password) { if (password == null) { @@ -27,4 +30,4 @@ public final class PasswordValidator { } return PASSWORD_PATTERN.matcher(password).matches(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/auth/UserAccount.java b/src/main/java/com/personalproject/auth/UserAccount.java index 80d5acf..07543a0 100644 --- a/src/main/java/com/personalproject/auth/UserAccount.java +++ b/src/main/java/com/personalproject/auth/UserAccount.java @@ -15,13 +15,13 @@ public record UserAccount( boolean isRegistered) { /** - * Creates a new user account with registration date set to now. + * 创建新的用户账户,并使用当前时间作为注册时间。 * - * @param username The username - * @param email The email address - * @param password The password - * @param difficultyLevel The selected difficulty level - * @param isRegistered Whether the user has completed registration + * @param username 用户名 + * @param email 邮箱地址 + * @param password 密码 + * @param difficultyLevel 选择的难度级别 + * @param isRegistered 用户是否已完成注册 */ public UserAccount { if (username == null || username.trim().isEmpty()) { diff --git a/src/main/java/com/personalproject/controller/MathLearningController.java b/src/main/java/com/personalproject/controller/MathLearningController.java index b791dd1..4edd694 100644 --- a/src/main/java/com/personalproject/controller/MathLearningController.java +++ b/src/main/java/com/personalproject/controller/MathLearningController.java @@ -144,12 +144,12 @@ public final class MathLearningController { } /** - * Gets a user account by username. + * 按用户名获取用户账户。 * - * @param username The username - * @return Optional containing the user account if found + * @param username 用户名 + * @return 若找到则返回包含用户账户的 Optional */ public Optional getUserAccount(String username) { return mathLearningService.getUser(username); } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/generator/PrimaryQuestionGenerator.java b/src/main/java/com/personalproject/generator/PrimaryQuestionGenerator.java index 1c874b3..622bd0f 100644 --- a/src/main/java/com/personalproject/generator/PrimaryQuestionGenerator.java +++ b/src/main/java/com/personalproject/generator/PrimaryQuestionGenerator.java @@ -59,7 +59,7 @@ public final class PrimaryQuestionGenerator implements QuestionGenerator, QuizQu try { correctAnswer = MathExpressionEvaluator.evaluate(expression); } catch (Exception e) { - // Fallback if evaluation fails + // 如果计算失败则使用兜底值 correctAnswer = 0.0; } @@ -73,7 +73,7 @@ public final class PrimaryQuestionGenerator implements QuestionGenerator, QuizQu String correctOption = String.format("%.2f", correctAnswer); String oldOption = options.set(correctAnswerIndex, correctOption); - // Add the displaced option back to maintain 4 options + // 将被替换的选项加回去以保持 4 个选项 options.add(oldOption); return new QuizQuestion(expression, options, correctAnswerIndex); @@ -85,22 +85,22 @@ public final class PrimaryQuestionGenerator implements QuestionGenerator, QuizQu private List generateOptions(double correctAnswer, Random random) { List options = new ArrayList<>(); - // Add correct answer as one of the options + // 将正确答案加入选项 options.add(String.format("%.2f", correctAnswer)); - // Add incorrect options + // 加入错误选项 for (int i = 0; i < OPTIONS_COUNT - 1; i++) { - double incorrectAnswer = correctAnswer + (random.nextGaussian() * 10); // Add random offset - if (Math.abs(incorrectAnswer - correctAnswer) < 0.1) { // Ensure different + double incorrectAnswer = correctAnswer + (random.nextGaussian() * 10); // 添加随机偏移 + if (Math.abs(incorrectAnswer - correctAnswer) < 0.1) { // 确保与正确答案不同 incorrectAnswer += 1.5; } options.add(String.format("%.2f", incorrectAnswer)); } - // Shuffle to randomize correct answer position + // 打乱顺序以随机化正确答案位置 Collections.shuffle(options, random); - // Find the correct answer index after shuffling + // 在打乱后查找正确答案的索引 String correctAnswerStr = String.format("%.2f", correctAnswer); int correctIndex = options.indexOf(correctAnswerStr); diff --git a/src/main/java/com/personalproject/model/ExamSession.java b/src/main/java/com/personalproject/model/ExamSession.java index 6fdb026..6b8f918 100644 --- a/src/main/java/com/personalproject/model/ExamSession.java +++ b/src/main/java/com/personalproject/model/ExamSession.java @@ -5,7 +5,7 @@ import java.util.ArrayList; import java.util.List; /** - * Represents an exam session for a user. + * 表示用户的考试会话。 */ public final class ExamSession { @@ -17,11 +17,11 @@ public final class ExamSession { private int currentQuestionIndex; /** - * Creates a new exam session. + * 创建新的考试会话。 * - * @param username The username of the test taker - * @param difficultyLevel The difficulty level of the exam - * @param questions The list of questions for the exam + * @param username 考生的用户名 + * @param difficultyLevel 考试的难度级别 + * @param questions 考试题目列表 */ public ExamSession(String username, DifficultyLevel difficultyLevel, List questions) { @@ -37,9 +37,9 @@ public final class ExamSession { this.username = username; this.difficultyLevel = difficultyLevel; - this.questions = List.copyOf(questions); // Immutable copy of questions + this.questions = List.copyOf(questions); // 题目的不可变副本 this.userAnswers = new ArrayList<>(); - // Initialize user answers with -1 (no answer selected) + // 使用 -1 初始化用户答案(表示未选择) for (int i = 0; i < questions.size(); i++) { userAnswers.add(-1); } @@ -48,54 +48,54 @@ public final class ExamSession { } /** - * Gets the username of the test taker. + * 获取考生的用户名。 * - * @return The username + * @return 用户名 */ public String getUsername() { return username; } /** - * Gets the difficulty level of the exam. + * 获取考试的难度级别。 * - * @return The difficulty level + * @return 考试难度级别 */ public DifficultyLevel getDifficultyLevel() { return difficultyLevel; } /** - * Gets the list of questions in the exam. + * 获取考试中的题目列表。 * - * @return An unmodifiable list of questions + * @return 不可修改的题目列表 */ public List getQuestions() { return questions; } /** - * Gets the user's answers to the questions. + * 获取用户对各题的作答。 * - * @return A list of answer indices (-1 means no answer selected) + * @return 答案索引列表(-1 表示未选择) */ public List getUserAnswers() { - return List.copyOf(userAnswers); // Return a copy to prevent modification + return List.copyOf(userAnswers); // 返回副本以防止被修改 } /** - * Gets the current question index. + * 获取当前题目的索引。 * - * @return The current question index + * @return 当前题目索引 */ public int getCurrentQuestionIndex() { return currentQuestionIndex; } /** - * Sets the user's answer for the current question. + * 为当前题目记录用户的答案。 * - * @param answerIndex The index of the selected answer + * @param answerIndex 选中答案的索引 */ public void setAnswer(int answerIndex) { if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) { @@ -108,9 +108,9 @@ public final class ExamSession { } /** - * Moves to the next question. + * 跳转到下一题。 * - * @return true if successfully moved to next question, false if already at the last question + * @return 若成功跳转则返回 true,若已是最后一题则返回 false */ public boolean goToNextQuestion() { if (currentQuestionIndex < questions.size() - 1) { @@ -121,9 +121,9 @@ public final class ExamSession { } /** - * Moves to the previous question. + * 返回上一题。 * - * @return true if successfully moved to previous question, false if already at the first question + * @return 若成功返回则返回 true,若已是第一题则返回 false */ public boolean goToPreviousQuestion() { if (currentQuestionIndex > 0) { @@ -134,18 +134,18 @@ public final class ExamSession { } /** - * Checks if the exam is complete (all questions answered or at the end). + * 检查考试是否完成(所有题目已作答或到达最后一题)。 * - * @return true if the exam is complete, false otherwise + * @return 若考试已完成则返回 true,否则返回 false */ public boolean isComplete() { return currentQuestionIndex >= questions.size() - 1; } /** - * Gets the current question. + * 获取当前题目。 * - * @return The current quiz question + * @return 当前的测验题 */ public QuizQuestion getCurrentQuestion() { if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) { @@ -155,10 +155,10 @@ public final class ExamSession { } /** - * Gets the user's answer for a specific question. + * 获取指定题目的用户答案。 * - * @param questionIndex The index of the question - * @return The index of the user's answer (or -1 if no answer selected) + * @param questionIndex 题目索引 + * @return 用户答案的索引(未选择时为 -1) */ public int getUserAnswer(int questionIndex) { if (questionIndex < 0 || questionIndex >= questions.size()) { @@ -168,9 +168,9 @@ public final class ExamSession { } /** - * Calculates the score as a percentage. + * 计算得分百分比。 * - * @return The score as a percentage (0-100) + * @return 0-100 范围内的得分百分比 */ public double calculateScore() { int correctCount = 0; @@ -185,19 +185,19 @@ public final class ExamSession { } /** - * Gets the total number of questions in the exam. + * 获取考试中的题目总数。 * - * @return The total number of questions + * @return 题目总数 */ public int getTotalQuestions() { return questions.size(); } /** - * Checks if a specific question has been answered. + * 检查指定题目是否已作答。 * - * @param questionIndex The index of the question - * @return true if the question has been answered, false otherwise + * @param questionIndex 题目索引 + * @return 若已作答则返回 true,否则返回 false */ public boolean hasAnswered(int questionIndex) { if (questionIndex < 0 || questionIndex >= questions.size()) { @@ -207,18 +207,18 @@ public final class ExamSession { } /** - * Gets the start time of the exam. + * 获取考试开始时间。 * - * @return The start time + * @return 开始时间 */ public LocalDateTime getStartTime() { return startTime; } /** - * Gets the number of correct answers. + * 获取答对的题目数量。 * - * @return The count of correct answers + * @return 正确题目数量 */ public int getCorrectAnswersCount() { int correctCount = 0; @@ -233,9 +233,9 @@ public final class ExamSession { } /** - * Gets the number of incorrect answers. + * 获取答错的题目数量。 * - * @return The count of incorrect answers + * @return 错误题目数量 */ public int getIncorrectAnswersCount() { int totalAnswered = 0; @@ -254,4 +254,4 @@ public final class ExamSession { return totalAnswered - correctCount; } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/model/QuizQuestion.java b/src/main/java/com/personalproject/model/QuizQuestion.java index e66f14c..52c8f6b 100644 --- a/src/main/java/com/personalproject/model/QuizQuestion.java +++ b/src/main/java/com/personalproject/model/QuizQuestion.java @@ -3,7 +3,7 @@ package com.personalproject.model; import java.util.List; /** - * Represents a quiz question with multiple choice options. + * 表示一个带有多项选择的测验题目。 */ public final class QuizQuestion { @@ -12,11 +12,11 @@ public final class QuizQuestion { private final int correctAnswerIndex; /** - * Creates a new quiz question. + * 创建新的测验题目。 * - * @param questionText The text of the question - * @param options The list of answer options - * @param correctAnswerIndex The index of the correct answer in the options list + * @param questionText 题目文本 + * @param options 答案选项列表 + * @param correctAnswerIndex 正确答案在选项列表中的索引 */ public QuizQuestion(String questionText, List options, int correctAnswerIndex) { if (questionText == null || questionText.trim().isEmpty()) { @@ -30,44 +30,44 @@ public final class QuizQuestion { } this.questionText = questionText; - this.options = List.copyOf(options); // Immutable copy + this.options = List.copyOf(options); // 不可变副本 this.correctAnswerIndex = correctAnswerIndex; } /** - * Gets the question text. + * 获取题目文本。 * - * @return The question text + * @return 题目文本 */ public String getQuestionText() { return questionText; } /** - * Gets the list of answer options. + * 获取答案选项列表。 * - * @return An unmodifiable list of answer options + * @return 不可修改的答案选项列表 */ public List getOptions() { return options; } /** - * Gets the index of the correct answer in the options list. + * 获取正确答案在选项列表中的索引。 * - * @return The index of the correct answer + * @return 正确答案的索引 */ public int getCorrectAnswerIndex() { return correctAnswerIndex; } /** - * Checks if the given answer index matches the correct answer. + * 检查给定的答案索引是否正确。 * - * @param answerIndex The index of the user's answer - * @return true if the answer is correct, false otherwise + * @param answerIndex 用户答案的索引 + * @return 若答案正确则返回 true,否则返回 false */ public boolean isAnswerCorrect(int answerIndex) { return answerIndex == correctAnswerIndex; } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/service/MathExpressionEvaluator.java b/src/main/java/com/personalproject/service/MathExpressionEvaluator.java index 6e32ed1..c4ee580 100644 --- a/src/main/java/com/personalproject/service/MathExpressionEvaluator.java +++ b/src/main/java/com/personalproject/service/MathExpressionEvaluator.java @@ -6,7 +6,7 @@ import java.util.Stack; import java.util.regex.Pattern; /** - * A mathematical expression evaluator that can handle basic arithmetic operations. + * 可处理基本算术运算的数学表达式求值器。 */ public final class MathExpressionEvaluator { @@ -22,41 +22,41 @@ public final class MathExpressionEvaluator { } private MathExpressionEvaluator() { - // Prevent instantiation of utility class + // 防止实例化此工具类 } /** - * Evaluates a mathematical expression string. + * 计算数学表达式字符串的结果。 * - * @param expression The mathematical expression to evaluate - * @return The result of the evaluation - * @throws IllegalArgumentException If the expression is invalid + * @param expression 要计算的数学表达式 + * @return 计算结果 + * @throws IllegalArgumentException 如果表达式无效 */ public static double evaluate(String expression) { if (expression == null) { throw new IllegalArgumentException("Expression cannot be null"); } - expression = expression.replaceAll("\\s+", ""); // Remove whitespace + expression = expression.replaceAll("\\s+", ""); // 移除空白字符 if (expression.isEmpty()) { throw new IllegalArgumentException("Expression cannot be empty"); } - // Tokenize the expression + // 将表达式拆分为记号 String[] tokens = tokenize(expression); - // Convert infix to postfix notation using Shunting Yard algorithm + // 使用调度场算法将中缀表达式转换为后缀表达式 String[] postfix = infixToPostfix(tokens); - // Evaluate the postfix expression + // 计算后缀表达式 return evaluatePostfix(postfix); } /** - * Tokenizes the expression into numbers and operators. + * 将表达式拆分为数字与运算符的记号。 * - * @param expression The expression to tokenize - * @return An array of tokens + * @param expression 待拆分的表达式 + * @return 记号数组 */ private static String[] tokenize(String expression) { java.util.List tokens = new java.util.ArrayList<>(); @@ -79,7 +79,7 @@ public final class MathExpressionEvaluator { currentNumber.setLength(0); } - // Handle unary minus + // 处理一元负号 if (c == '-' && (i == 0 || expression.charAt(i - 1) == '(')) { currentNumber.append(c); } else { @@ -98,20 +98,20 @@ public final class MathExpressionEvaluator { } /** - * Checks if the character is an operator. + * 检查字符是否为运算符。 * - * @param c The character to check - * @return true if the character is an operator, false otherwise + * @param c 待检查的字符 + * @return 若字符是运算符则返回 true,否则返回 false */ private static boolean isOperator(char c) { return c == '+' || c == '-' || c == '*' || c == '/' || c == '^'; } /** - * Converts infix notation to postfix notation using the Shunting Yard algorithm. + * 使用调度场算法将中缀表达式转换为后缀表达式。 * - * @param tokens The tokens in infix notation - * @return An array of tokens in postfix notation + * @param tokens 中缀表达式的记号数组 + * @return 后缀表达式的记号数组 */ private static String[] infixToPostfix(String[] tokens) { java.util.List output = new java.util.ArrayList<>(); @@ -127,7 +127,7 @@ public final class MathExpressionEvaluator { output.add(operators.pop()); } if (!operators.isEmpty()) { - operators.pop(); // Remove the "(" + operators.pop(); // 移除 "(" } } else if (isOperator(token.charAt(0))) { while (!operators.isEmpty() @@ -147,10 +147,10 @@ public final class MathExpressionEvaluator { } /** - * Evaluates a postfix expression. + * 计算后缀表达式的值。 * - * @param postfix The tokens in postfix notation - * @return The result of the evaluation + * @param postfix 后缀表达式的记号数组 + * @return 计算结果 */ private static double evaluatePostfix(String[] postfix) { Stack values = new Stack<>(); @@ -178,12 +178,12 @@ public final class MathExpressionEvaluator { } /** - * Performs the specified operation on the two operands. + * 对两个操作数执行指定运算。 * - * @param a The first operand - * @param b The second operand - * @param operator The operator to apply - * @return The result of the operation + * @param a 第一个操作数 + * @param b 第二个操作数 + * @param operator 要应用的运算符 + * @return 运算结果 */ private static double performOperation(double a, double b, char operator) { switch (operator) { @@ -206,12 +206,12 @@ public final class MathExpressionEvaluator { } /** - * Checks if the token is a number. + * 检查记号是否为数字。 * - * @param token The token to check - * @return true if the token is a number, false otherwise + * @param token 待检查的记号 + * @return 若为数字则返回 true,否则返回 false */ private static boolean isNumber(String token) { return NUMBER_PATTERN.matcher(token).matches(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/service/MathLearningService.java b/src/main/java/com/personalproject/service/MathLearningService.java index 56427e1..7f7f62b 100644 --- a/src/main/java/com/personalproject/service/MathLearningService.java +++ b/src/main/java/com/personalproject/service/MathLearningService.java @@ -169,12 +169,12 @@ public final class MathLearningService { } /** - * Gets a user account by username. + * 按用户名获取用户账户。 * - * @param username The username - * @return Optional containing the user account if found + * @param username 用户名 + * @return 若找到则返回包含用户账户的 Optional */ public Optional getUser(String username) { return registrationService.getUser(username); } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/service/RegistrationService.java b/src/main/java/com/personalproject/service/RegistrationService.java index 5c20f1a..024f2bf 100644 --- a/src/main/java/com/personalproject/service/RegistrationService.java +++ b/src/main/java/com/personalproject/service/RegistrationService.java @@ -137,12 +137,12 @@ public final class RegistrationService { } /** - * Gets a user account by username. + * 按用户名获取用户账户。 * - * @param username The username - * @return Optional containing the user account if found + * @param username 用户名 + * @return 若找到则返回包含用户账户的 Optional */ public Optional getUser(String username) { return accountRepository.getUser(username); } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/ui/MathExamGUI.java b/src/main/java/com/personalproject/ui/MathExamGUI.java index 1071b3b..272bf36 100644 --- a/src/main/java/com/personalproject/ui/MathExamGUI.java +++ b/src/main/java/com/personalproject/ui/MathExamGUI.java @@ -16,8 +16,8 @@ import java.util.EnumMap; import java.util.Map; /** - * JavaFX GUI Application for the Math Learning Software. - * This is the main entry point for the GUI application. + * 数学学习软件的 JavaFX 图形界面应用程序。 + * 这是图形界面的主要入口点。 */ public final class MathExamGUI extends Application { @@ -25,7 +25,7 @@ public final class MathExamGUI extends Application { @Override public void start(Stage primaryStage) { - // Initialize the controller with generators + // 使用题目生成器初始化控制器 Map generatorMap = new EnumMap<>(DifficultyLevel.class); generatorMap.put(DifficultyLevel.PRIMARY, new PrimaryQuestionGenerator()); generatorMap.put(DifficultyLevel.MIDDLE, new MiddleSchoolQuestionGenerator()); @@ -33,10 +33,10 @@ public final class MathExamGUI extends Application { QuestionGenerationService questionGenerationService = new QuestionGenerationService(generatorMap); this.controller = new MathLearningController(generatorMap, questionGenerationService); - // Set up the primary stage + // 配置主舞台 primaryStage.setTitle("数学学习软件"); - // Start with login scene + // 从登录界面开始 LoginScene loginScene = new LoginScene(primaryStage, controller); Scene scene = new Scene(loginScene, 600, 400); @@ -45,11 +45,11 @@ public final class MathExamGUI extends Application { } /** - * Launches the JavaFX application. + * 启动 JavaFX 应用程序。 * - * @param args Command-line arguments + * @param args 命令行参数 */ public static void main(String[] args) { launch(args); } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/ui/scenes/LoginScene.java b/src/main/java/com/personalproject/ui/scenes/LoginScene.java index 6840449..4d4d6be 100644 --- a/src/main/java/com/personalproject/ui/scenes/LoginScene.java +++ b/src/main/java/com/personalproject/ui/scenes/LoginScene.java @@ -12,7 +12,7 @@ import com.personalproject.ui.views.MainMenuView; import com.personalproject.ui.scenes.RegistrationScene; /** - * Scene for handling user login and registration. + * 负责处理用户登录与注册的场景。 */ public class LoginScene extends BorderPane { @@ -24,10 +24,10 @@ public class LoginScene extends BorderPane { private Button registerButton; /** - * Constructor for LoginScene. + * LoginScene 的构造函数。 * - * @param primaryStage The main stage of the application - * @param controller The math learning controller + * @param primaryStage 应用程序的主舞台 + * @param controller 数学学习控制器 */ public LoginScene(Stage primaryStage, MathLearningController controller) { this.primaryStage = primaryStage; @@ -36,19 +36,19 @@ public class LoginScene extends BorderPane { } /** - * Initializes the UI components. + * 初始化界面组件。 */ private void initializeUI() { - // Create the main layout + // 创建主布局 VBox mainLayout = new VBox(15); mainLayout.setAlignment(Pos.CENTER); mainLayout.setPadding(new Insets(20)); - // Title + // 标题 Label titleLabel = new Label("数学学习软件"); titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24)); - // Login Form + // 登录表单 GridPane loginForm = new GridPane(); loginForm.setHgap(10); loginForm.setVgap(10); @@ -67,37 +67,37 @@ public class LoginScene extends BorderPane { loginForm.add(passwordLabel, 0, 1); loginForm.add(passwordField, 1, 1); - // Buttons + // 按钮 HBox buttonBox = new HBox(10); buttonBox.setAlignment(Pos.CENTER); loginButton = new Button("登录"); registerButton = new Button("注册"); - // Set button styles + // 设置按钮样式 loginButton.setPrefWidth(100); registerButton.setPrefWidth(100); buttonBox.getChildren().addAll(loginButton, registerButton); - // Add components to main layout + // 将组件添加到主布局 mainLayout.getChildren().addAll(titleLabel, loginForm, buttonBox); - // Set the center of the border pane + // 将主布局放到边界面板中央 setCenter(mainLayout); - // Add event handlers + // 添加事件处理器 addEventHandlers(); } /** - * Adds event handlers to UI components. + * 为界面组件添加事件处理器。 */ private void addEventHandlers() { loginButton.setOnAction(e -> handleLogin()); registerButton.setOnAction(e -> handleRegistration()); - // Allow login with Enter key + // 允许使用回车键登录 setOnKeyPressed(event -> { if (event.getCode().toString().equals("ENTER")) { handleLogin(); @@ -106,7 +106,7 @@ public class LoginScene extends BorderPane { } /** - * Handles the login process. + * 处理登录流程。 */ private void handleLogin() { String username = usernameField.getText().trim(); @@ -117,34 +117,34 @@ public class LoginScene extends BorderPane { return; } - // Authenticate user + // 验证用户 var userAccount = controller.authenticate(username, password); if (userAccount.isPresent()) { - // Login successful - navigate to main menu + // 登录成功,跳转到主菜单 MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount.get()); primaryStage.getScene().setRoot(mainMenuView); } else { - // Login failed + // 登录失败 showAlert(Alert.AlertType.ERROR, "登录失败", "用户名或密码错误"); } } /** - * Handles the registration process. + * 处理注册流程。 */ private void handleRegistration() { - // Switch to registration scene + // 切换到注册界面 RegistrationScene registrationScene = new RegistrationScene(primaryStage, controller); primaryStage.getScene().setRoot(registrationScene); } /** - * Shows an alert dialog. + * 显示提示对话框。 * - * @param alertType Type of alert - * @param title Title of the alert - * @param message Message to display + * @param alertType 提示类型 + * @param title 对话框标题 + * @param message 显示的消息 */ private void showAlert(Alert.AlertType alertType, String title, String message) { Alert alert = new Alert(alertType); @@ -153,4 +153,4 @@ public class LoginScene extends BorderPane { alert.setContentText(message); alert.showAndWait(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java b/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java index f824784..c180c73 100644 --- a/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java +++ b/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java @@ -12,7 +12,7 @@ import com.personalproject.model.DifficultyLevel; import com.personalproject.ui.scenes.LoginScene; /** - * Scene for handling user registration. + * 负责处理用户注册的场景。 */ public class RegistrationScene extends BorderPane { @@ -31,10 +31,10 @@ public class RegistrationScene extends BorderPane { private VBox registrationForm; /** - * Constructor for RegistrationScene. + * RegistrationScene 的构造函数。 * - * @param primaryStage The main stage of the application - * @param controller The math learning controller + * @param primaryStage 应用程序的主舞台 + * @param controller 数学学习控制器 */ public RegistrationScene(Stage primaryStage, MathLearningController controller) { this.primaryStage = primaryStage; @@ -43,23 +43,23 @@ public class RegistrationScene extends BorderPane { } /** - * Initializes the UI components. + * 初始化界面组件。 */ private void initializeUI() { - // Create the main layout + // 创建主布局 VBox mainLayout = new VBox(15); mainLayout.setAlignment(Pos.CENTER); mainLayout.setPadding(new Insets(20)); - // Title + // 标题 Label titleLabel = new Label("用户注册"); titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24)); - // Registration Form + // 注册表单 registrationForm = new VBox(15); registrationForm.setAlignment(Pos.CENTER); - // Step 1: Basic Info + // 步骤1:填写基础信息 GridPane basicInfoForm = new GridPane(); basicInfoForm.setHgap(10); basicInfoForm.setVgap(10); @@ -91,7 +91,7 @@ public class RegistrationScene extends BorderPane { registrationForm.getChildren().addAll(basicInfoForm, sendCodeButton); - // Step 2: Verification (hidden initially) + // 步骤2:验证码验证(初始隐藏) VBox verificationSection = new VBox(10); verificationSection.setAlignment(Pos.CENTER); verificationSection.setVisible(false); @@ -107,7 +107,7 @@ public class RegistrationScene extends BorderPane { verificationSection.getChildren().addAll(codeLabel, registrationCodeField, verifyCodeButton); registrationForm.getChildren().add(verificationSection); - // Step 3: Password Setting (hidden initially) + // 步骤3:设置密码(初始隐藏) VBox passwordSection = new VBox(10); passwordSection.setAlignment(Pos.CENTER); passwordSection.setVisible(false); @@ -128,21 +128,21 @@ public class RegistrationScene extends BorderPane { confirmPasswordField, setPasswordButton); registrationForm.getChildren().add(passwordSection); - // Back button + // 返回按钮 backButton = new Button("返回"); backButton.setPrefWidth(100); - // Add components to main layout + // 将组件添加到主布局 mainLayout.getChildren().addAll(titleLabel, registrationForm, backButton); setCenter(mainLayout); - // Add event handlers + // 添加事件处理器 addEventHandlers(sendCodeButton, verificationSection, verifyCodeButton, passwordSection); } /** - * Adds event handlers to UI components. + * 为界面组件添加事件处理器。 */ private void addEventHandlers(Button sendCodeButton, VBox verificationSection, Button verifyCodeButton, VBox passwordSection) { @@ -153,7 +153,7 @@ public class RegistrationScene extends BorderPane { } /** - * Handles sending registration code. + * 处理发送注册码的逻辑。 */ private void handleSendCode(VBox verificationSection) { String username = usernameField.getText().trim(); @@ -170,7 +170,7 @@ public class RegistrationScene extends BorderPane { return; } - // Initiate registration + // 发起注册 boolean success = controller.initiateRegistration(username, email, difficultyLevel); if (success) { @@ -183,7 +183,7 @@ public class RegistrationScene extends BorderPane { } /** - * Handles verification of registration code. + * 处理注册码验证。 */ private void handleVerifyCode(VBox passwordSection) { String username = usernameField.getText().trim(); @@ -206,7 +206,7 @@ public class RegistrationScene extends BorderPane { } /** - * Handles setting the user password. + * 处理设置用户密码的逻辑。 */ private void handleSetPassword() { String username = usernameField.getText().trim(); @@ -233,14 +233,14 @@ public class RegistrationScene extends BorderPane { if (success) { showAlert(Alert.AlertType.INFORMATION, "注册成功", "注册成功!请登录。"); - handleBack(); // Go back to login screen + handleBack(); // 返回登录界面 } else { showAlert(Alert.AlertType.ERROR, "设置密码失败", "设置密码失败,请重试。"); } } /** - * Handles the back button action. + * 处理返回按钮的操作。 */ private void handleBack() { LoginScene loginScene = new LoginScene(primaryStage, controller); @@ -248,11 +248,11 @@ public class RegistrationScene extends BorderPane { } /** - * Shows an alert dialog. + * 显示提示对话框。 * - * @param alertType Type of alert - * @param title Title of the alert - * @param message Message to display + * @param alertType 提示类型 + * @param title 对话框标题 + * @param message 显示的消息 */ private void showAlert(Alert.AlertType alertType, String title, String message) { Alert alert = new Alert(alertType); @@ -261,4 +261,4 @@ public class RegistrationScene extends BorderPane { alert.setContentText(message); alert.showAndWait(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/ui/views/ExamResultsView.java b/src/main/java/com/personalproject/ui/views/ExamResultsView.java index 7fececb..64d0e91 100644 --- a/src/main/java/com/personalproject/ui/views/ExamResultsView.java +++ b/src/main/java/com/personalproject/ui/views/ExamResultsView.java @@ -13,7 +13,7 @@ import com.personalproject.auth.UserAccount; import com.personalproject.ui.views.MainMenuView; /** - * View for displaying exam results. + * 用于展示考试结果的界面。 */ public class ExamResultsView extends BorderPane { @@ -24,11 +24,11 @@ public class ExamResultsView extends BorderPane { private Button exitButton; /** - * Constructor for ExamResultsView. + * ExamResultsView 的构造函数。 * - * @param primaryStage The main stage of the application - * @param controller The math learning controller - * @param examSession The completed exam session + * @param primaryStage 应用程序的主舞台 + * @param controller 数学学习控制器 + * @param examSession 已完成的考试会话 */ public ExamResultsView(Stage primaryStage, MathLearningController controller, ExamSession examSession) { this.primaryStage = primaryStage; @@ -38,24 +38,24 @@ public class ExamResultsView extends BorderPane { } /** - * Initializes the UI components. + * 初始化界面组件。 */ private void initializeUI() { - // Create the main layout + // 创建主布局 VBox mainLayout = new VBox(20); mainLayout.setAlignment(Pos.CENTER); mainLayout.setPadding(new Insets(20)); - // Results title + // 结果标题 Label titleLabel = new Label("考试结果"); titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24)); - // Score display + // 分数展示 double score = examSession.calculateScore(); Label scoreLabel = new Label(String.format("您的得分: %.2f%%", score)); scoreLabel.setFont(Font.font("System", FontWeight.BOLD, 18)); - // Performance breakdown + // 成绩明细 VBox breakdownBox = new VBox(10); breakdownBox.setAlignment(Pos.CENTER); @@ -65,31 +65,31 @@ public class ExamResultsView extends BorderPane { breakdownBox.getChildren().addAll(totalQuestionsLabel, correctAnswersLabel, incorrectAnswersLabel); - // Buttons + // 按钮区域 HBox buttonBox = new HBox(15); buttonBox.setAlignment(Pos.CENTER); continueButton = new Button("继续考试"); exitButton = new Button("退出"); - // Set button sizes + // 设置按钮尺寸 continueButton.setPrefSize(120, 40); exitButton.setPrefSize(120, 40); buttonBox.getChildren().addAll(continueButton, exitButton); - // Add components to main layout + // 将组件添加到主布局 mainLayout.getChildren().addAll(titleLabel, scoreLabel, breakdownBox, buttonBox); - // Set the center of the border pane + // 将主布局置于边界面板中央 setCenter(mainLayout); - // Add event handlers + // 添加事件处理器 addEventHandlers(); } /** - * Adds event handlers to UI components. + * 为界面组件添加事件处理器。 */ private void addEventHandlers() { continueButton.setOnAction(e -> handleContinue()); @@ -97,10 +97,10 @@ public class ExamResultsView extends BorderPane { } /** - * Handles the continue button action. + * 处理继续考试按钮的操作。 */ private void handleContinue() { - // Go back to main menu to start a new exam + // 返回主菜单以开始新考试 controller.getUserAccount(examSession.getUsername()) .ifPresentOrElse( userAccount -> { @@ -108,9 +108,9 @@ public class ExamResultsView extends BorderPane { primaryStage.getScene().setRoot(mainMenuView); }, () -> { - // If user account can't be found, show an error and go back to login + // 如果找不到用户信息,则提示错误并返回登录界面 showAlert(Alert.AlertType.ERROR, "错误", "用户信息无法找到,请重新登录"); - // Go back to login scene + // 返回登录场景 com.personalproject.ui.scenes.LoginScene loginScene = new com.personalproject.ui.scenes.LoginScene(primaryStage, controller); primaryStage.getScene().setRoot(loginScene); @@ -119,10 +119,10 @@ public class ExamResultsView extends BorderPane { } /** - * Handles the exit button action. + * 处理退出按钮的操作。 */ private void handleExit() { - // Go back to main menu + // 返回主菜单 controller.getUserAccount(examSession.getUsername()) .ifPresentOrElse( userAccount -> { @@ -130,9 +130,9 @@ public class ExamResultsView extends BorderPane { primaryStage.getScene().setRoot(mainMenuView); }, () -> { - // If user account can't be found, show an error and go back to login + // 如果找不到用户信息,则提示错误并返回登录界面 showAlert(Alert.AlertType.ERROR, "错误", "用户信息无法找到,请重新登录"); - // Go back to login scene + // 返回登录场景 com.personalproject.ui.scenes.LoginScene loginScene = new com.personalproject.ui.scenes.LoginScene(primaryStage, controller); primaryStage.getScene().setRoot(loginScene); @@ -141,11 +141,11 @@ public class ExamResultsView extends BorderPane { } /** - * Shows an alert dialog. + * 显示提示对话框。 * - * @param alertType Type of alert - * @param title Title of the alert - * @param message Message to display + * @param alertType 提示类型 + * @param title 对话框标题 + * @param message 显示的消息 */ private void showAlert(Alert.AlertType alertType, String title, String message) { Alert alert = new Alert(alertType); @@ -154,4 +154,4 @@ public class ExamResultsView extends BorderPane { alert.setContentText(message); alert.showAndWait(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/ui/views/ExamSelectionView.java b/src/main/java/com/personalproject/ui/views/ExamSelectionView.java index 2b305d5..839b54f 100644 --- a/src/main/java/com/personalproject/ui/views/ExamSelectionView.java +++ b/src/main/java/com/personalproject/ui/views/ExamSelectionView.java @@ -12,7 +12,7 @@ import com.personalproject.model.DifficultyLevel; import com.personalproject.auth.UserAccount; /** - * View for selecting exam difficulty and number of questions. + * 用于选择考试难度和题目数量的界面。 */ public class ExamSelectionView extends BorderPane { @@ -25,11 +25,11 @@ public class ExamSelectionView extends BorderPane { private Button backButton; /** - * Constructor for ExamSelectionView. + * ExamSelectionView 的构造函数。 * - * @param primaryStage The main stage of the application - * @param controller The math learning controller - * @param userAccount The current user account + * @param primaryStage 应用程序的主舞台 + * @param controller 数学学习控制器 + * @param userAccount 当前用户账户 */ public ExamSelectionView(Stage primaryStage, MathLearningController controller, UserAccount userAccount) { this.primaryStage = primaryStage; @@ -39,19 +39,19 @@ public class ExamSelectionView extends BorderPane { } /** - * Initializes the UI components. + * 初始化界面组件。 */ private void initializeUI() { - // Create the main layout + // 创建主布局 VBox mainLayout = new VBox(20); mainLayout.setAlignment(Pos.CENTER); mainLayout.setPadding(new Insets(20)); - // Title + // 标题 Label titleLabel = new Label("考试设置"); titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24)); - // Form for exam settings + // 考试设置表单 GridPane examSettingsForm = new GridPane(); examSettingsForm.setHgap(15); examSettingsForm.setVgap(15); @@ -60,11 +60,11 @@ public class ExamSelectionView extends BorderPane { Label difficultyLabel = new Label("选择难度:"); difficultyComboBox = new ComboBox<>(); difficultyComboBox.getItems().addAll(DifficultyLevel.PRIMARY, DifficultyLevel.MIDDLE, DifficultyLevel.HIGH); - difficultyComboBox.setValue(userAccount.difficultyLevel()); // Default to user's difficulty + difficultyComboBox.setValue(userAccount.difficultyLevel()); // 默认选中用户的难度 difficultyComboBox.setPrefWidth(200); Label questionCountLabel = new Label("题目数量 (10-30):"); - questionCountSpinner = new Spinner<>(10, 30, 10); // min, max, initial value + questionCountSpinner = new Spinner<>(10, 30, 10); // 最小值、最大值、初始值 questionCountSpinner.setPrefWidth(200); examSettingsForm.add(difficultyLabel, 0, 0); @@ -72,31 +72,31 @@ public class ExamSelectionView extends BorderPane { examSettingsForm.add(questionCountLabel, 0, 1); examSettingsForm.add(questionCountSpinner, 1, 1); - // Buttons + // 按钮区域 HBox buttonBox = new HBox(15); buttonBox.setAlignment(Pos.CENTER); startExamButton = new Button("开始考试"); backButton = new Button("返回"); - // Set button sizes + // 设置按钮尺寸 startExamButton.setPrefSize(120, 40); backButton.setPrefSize(120, 40); buttonBox.getChildren().addAll(startExamButton, backButton); - // Add components to main layout + // 将组件添加到主布局 mainLayout.getChildren().addAll(titleLabel, examSettingsForm, buttonBox); - // Set the center of the border pane + // 将主布局置于边界面板中央 setCenter(mainLayout); - // Add event handlers + // 添加事件处理器 addEventHandlers(); } /** - * Adds event handlers to UI components. + * 为界面组件添加事件处理器。 */ private void addEventHandlers() { startExamButton.setOnAction(e -> handleStartExam()); @@ -104,7 +104,7 @@ public class ExamSelectionView extends BorderPane { } /** - * Handles the start exam button action. + * 处理开始考试按钮的操作。 */ private void handleStartExam() { DifficultyLevel selectedDifficulty = difficultyComboBox.getValue(); @@ -115,7 +115,7 @@ public class ExamSelectionView extends BorderPane { return; } - // Create and start exam session + // 创建并启动考试会话 com.personalproject.model.ExamSession examSession = controller.createExamSession( userAccount.username(), selectedDifficulty, questionCount); @@ -124,7 +124,7 @@ public class ExamSelectionView extends BorderPane { } /** - * Handles the back button action. + * 处理返回按钮的操作。 */ private void handleBack() { MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount); @@ -132,11 +132,11 @@ public class ExamSelectionView extends BorderPane { } /** - * Shows an alert dialog. + * 显示提示对话框。 * - * @param alertType Type of alert - * @param title Title of the alert - * @param message Message to display + * @param alertType 提示类型 + * @param title 对话框标题 + * @param message 显示的消息 */ private void showAlert(Alert.AlertType alertType, String title, String message) { Alert alert = new Alert(alertType); @@ -145,4 +145,4 @@ public class ExamSelectionView extends BorderPane { alert.setContentText(message); alert.showAndWait(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/ui/views/ExamView.java b/src/main/java/com/personalproject/ui/views/ExamView.java index 8b30cd3..67c0a13 100644 --- a/src/main/java/com/personalproject/ui/views/ExamView.java +++ b/src/main/java/com/personalproject/ui/views/ExamView.java @@ -13,7 +13,7 @@ import com.personalproject.model.QuizQuestion; import com.personalproject.ui.views.ExamResultsView; /** - * View for taking the exam with questions and answer options. + * 提供答题界面以及题目与选项的呈现。 */ public class ExamView extends BorderPane { @@ -30,11 +30,11 @@ public class ExamView extends BorderPane { private HBox buttonBox; /** - * Constructor for ExamView. + * ExamView 的构造函数。 * - * @param primaryStage The main stage of the application - * @param controller The math learning controller - * @param examSession The current exam session + * @param primaryStage 应用程序的主舞台 + * @param controller 数学学习控制器 + * @param examSession 当前的考试会话 */ public ExamView(Stage primaryStage, MathLearningController controller, ExamSession examSession) { this.primaryStage = primaryStage; @@ -44,29 +44,29 @@ public class ExamView extends BorderPane { } /** - * Initializes the UI components. + * 初始化界面组件。 */ private void initializeUI() { - // Create the main layout + // 创建主布局 VBox mainLayout = new VBox(20); mainLayout.setAlignment(Pos.CENTER); mainLayout.setPadding(new Insets(20)); - // Question number + // 题号 questionNumberLabel = new Label(); questionNumberLabel.setFont(Font.font("System", FontWeight.BOLD, 16)); - // Question text + // 题目文本 questionTextLabel = new Label(); questionTextLabel.setWrapText(true); questionTextLabel.setFont(Font.font("System", FontWeight.NORMAL, 14)); questionTextLabel.setMaxWidth(500); - // Options + // 选项容器 optionsBox = new VBox(10); optionsBox.setPadding(new Insets(10)); - // Buttons + // 按钮区域 buttonBox = new HBox(15); buttonBox.setAlignment(Pos.CENTER); @@ -74,34 +74,34 @@ public class ExamView extends BorderPane { nextButton = new Button("下一题"); finishButton = new Button("完成考试"); - // Set button sizes + // 设置按钮尺寸 previousButton.setPrefSize(100, 35); nextButton.setPrefSize(100, 35); finishButton.setPrefSize(120, 35); buttonBox.getChildren().addAll(previousButton, nextButton, finishButton); - // Add components to main layout + // 将组件添加到主布局 mainLayout.getChildren().addAll(questionNumberLabel, questionTextLabel, optionsBox, buttonBox); - // Set the center of the border pane + // 将主布局置于边界面板中央 setCenter(mainLayout); - // Load the first question + // 加载第一题 loadCurrentQuestion(); - // Add event handlers + // 添加事件处理器 addEventHandlers(); } /** - * Loads the current question into the UI. + * 将当前题目加载到界面。 */ private void loadCurrentQuestion() { try { - // Check if exam is complete before loading next question + // 在加载下一题之前检查考试是否已完成 if (examSession.isComplete()) { - // If exam is complete, the finish button should be enabled + // 如果考试已完成,则启用“完成考试”按钮 updateButtonStates(); return; } @@ -114,23 +114,23 @@ public class ExamView extends BorderPane { return; } - // Update question number and text + // 更新题号与题目文本 questionNumberLabel.setText("第 " + (currentIndex + 1) + " 题"); questionTextLabel.setText(currentQuestion.getQuestionText()); - // Clear previous options + // 清空上一题的选项 optionsBox.getChildren().clear(); - // Create new options + // 创建新的选项组件 answerToggleGroup = new ToggleGroup(); for (int i = 0; i < currentQuestion.getOptions().size(); i++) { String option = currentQuestion.getOptions().get(i); RadioButton optionButton = new RadioButton((i + 1) + ". " + option); optionButton.setToggleGroup(answerToggleGroup); - optionButton.setUserData(i); // Store option index - - // If this question already has an answer, select it + optionButton.setUserData(i); // 存储选项索引 + + // 如果该题已有答案则自动选中 if (examSession.hasAnswered(currentIndex) && examSession.getUserAnswer(currentIndex) == i) { optionButton.setSelected(true); @@ -139,7 +139,7 @@ public class ExamView extends BorderPane { optionsBox.getChildren().add(optionButton); } - // Update button states + // 更新按钮状态 updateButtonStates(); } catch (Exception e) { showAlert(Alert.AlertType.ERROR, "错误", "加载题目时发生错误: " + e.getMessage()); @@ -147,50 +147,50 @@ public class ExamView extends BorderPane { } /** - * Updates the state of navigation buttons based on current position. + * 根据当前位置更新导航按钮的状态。 */ private void updateButtonStates() { try { int currentIndex = examSession.getCurrentQuestionIndex(); int totalQuestions = examSession.getTotalQuestions(); - // Handle potential edge cases + // 处理潜在极端情况 if (totalQuestions <= 0) { - // If there are no questions, disable all navigation + // 如果没有题目,则禁用所有导航按钮 previousButton.setDisable(true); nextButton.setDisable(true); - finishButton.setDisable(false); // Allow finishing exam + finishButton.setDisable(false); // 仍允许完成考试 return; } - // Previous button state + // “上一题”按钮状态 previousButton.setDisable(currentIndex < 0 || currentIndex == 0); - // Next button state + // “下一题”按钮状态 nextButton.setDisable(currentIndex < 0 || currentIndex >= totalQuestions - 1); - // Finish button state - enabled when exam is complete or at the last question + // “完成考试”按钮状态——在考试完成或到达最后一题时启用 boolean isExamComplete = examSession.isComplete(); boolean isAtLastQuestion = (currentIndex >= totalQuestions - 1); finishButton.setDisable(!(isExamComplete || isAtLastQuestion)); } catch (Exception e) { - // In case of any error, disable navigation buttons to prevent further issues + // 若出现异常,禁用导航按钮以避免进一步问题 previousButton.setDisable(true); nextButton.setDisable(true); - finishButton.setDisable(false); // Still allow finishing + finishButton.setDisable(false); // 仍允许完成考试 showAlert(Alert.AlertType.ERROR, "错误", "更新按钮状态时发生错误: " + e.getMessage()); } } /** - * Adds event handlers to UI components. + * 为界面组件添加事件处理器。 */ private void addEventHandlers() { nextButton.setOnAction(e -> handleNextQuestion()); previousButton.setOnAction(e -> handlePreviousQuestion()); finishButton.setOnAction(e -> handleFinishExam()); - // Add change listener to save answer when an option is selected + // 添加变更监听器,在选项被选择时保存答案 answerToggleGroup.selectedToggleProperty().addListener((obs, oldSelection, newSelection) -> { if (newSelection != null) { int selectedIndex = (Integer) newSelection.getUserData(); @@ -200,15 +200,15 @@ public class ExamView extends BorderPane { } /** - * Handles the next question button action. + * 处理“下一题”按钮的操作。 */ private void handleNextQuestion() { try { if (examSession.goToNextQuestion()) { loadCurrentQuestion(); } else { - // If we can't go to next question, we might be at the end - // Check if exam is complete and update button states accordingly + // 若无法跳转到下一题,可能已经到达末尾 + // 检查考试是否完成并据此更新按钮状态 updateButtonStates(); } } catch (Exception e) { @@ -217,14 +217,14 @@ public class ExamView extends BorderPane { } /** - * Handles the previous question button action. + * 处理“上一题”按钮的操作。 */ private void handlePreviousQuestion() { try { if (examSession.goToPreviousQuestion()) { loadCurrentQuestion(); } else { - // If we can't go to previous question, we might be at the beginning + // 若无法返回上一题,可能已经位于开头 updateButtonStates(); } } catch (Exception e) { @@ -233,11 +233,11 @@ public class ExamView extends BorderPane { } /** - * Shows an alert dialog. + * 显示提示对话框。 * - * @param alertType Type of alert - * @param title Title of the alert - * @param message Message to display + * @param alertType 提示类型 + * @param title 对话框标题 + * @param message 显示的消息 */ private void showAlert(Alert.AlertType alertType, String title, String message) { Alert alert = new Alert(alertType); @@ -248,14 +248,14 @@ public class ExamView extends BorderPane { } /** - * Handles the finish exam button action. + * 处理“完成考试”按钮的操作。 */ private void handleFinishExam() { - // Save exam results + // 保存考试结果 controller.saveExamResults(examSession); - // Show results + // 展示考试结果 ExamResultsView resultsView = new ExamResultsView(primaryStage, controller, examSession); primaryStage.getScene().setRoot(resultsView); } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/ui/views/MainMenuView.java b/src/main/java/com/personalproject/ui/views/MainMenuView.java index d3138c0..619ca88 100644 --- a/src/main/java/com/personalproject/ui/views/MainMenuView.java +++ b/src/main/java/com/personalproject/ui/views/MainMenuView.java @@ -14,7 +14,7 @@ import com.personalproject.auth.UserAccount; import com.personalproject.ui.scenes.LoginScene; /** - * View for the main menu where users can start exams or change settings. + * 用户可以开始考试或更改设置的主菜单界面。 */ public class MainMenuView extends BorderPane { @@ -26,11 +26,11 @@ public class MainMenuView extends BorderPane { private Button logoutButton; /** - * Constructor for MainMenuView. + * MainMenuView 的构造函数。 * - * @param primaryStage The main stage of the application - * @param controller The math learning controller - * @param userAccount The current user account + * @param primaryStage 应用程序的主舞台 + * @param controller 数学学习控制器 + * @param userAccount 当前用户账户 */ public MainMenuView(Stage primaryStage, MathLearningController controller, UserAccount userAccount) { this.primaryStage = primaryStage; @@ -40,23 +40,23 @@ public class MainMenuView extends BorderPane { } /** - * Initializes the UI components. + * 初始化界面组件。 */ private void initializeUI() { - // Create the main layout + // 创建主布局 VBox mainLayout = new VBox(20); mainLayout.setAlignment(Pos.CENTER); mainLayout.setPadding(new Insets(20)); - // Welcome message + // 欢迎信息 Label welcomeLabel = new Label("欢迎, " + userAccount.username()); welcomeLabel.setFont(Font.font("System", FontWeight.BOLD, 18)); - // Difficulty info + // 难度信息 Label difficultyLabel = new Label("当前难度: " + userAccount.difficultyLevel().getDisplayName()); difficultyLabel.setFont(Font.font("System", FontWeight.NORMAL, 14)); - // Buttons + // 按钮区域 VBox buttonBox = new VBox(15); buttonBox.setAlignment(Pos.CENTER); @@ -64,25 +64,25 @@ public class MainMenuView extends BorderPane { changePasswordButton = new Button("修改密码"); logoutButton = new Button("退出登录"); - // Set button sizes + // 设置按钮尺寸 startExamButton.setPrefSize(150, 40); changePasswordButton.setPrefSize(150, 40); logoutButton.setPrefSize(150, 40); buttonBox.getChildren().addAll(startExamButton, changePasswordButton, logoutButton); - // Add components to main layout + // 将组件添加到主布局 mainLayout.getChildren().addAll(welcomeLabel, difficultyLabel, buttonBox); - // Set the center of the border pane + // 将主布局置于边界面板中央 setCenter(mainLayout); - // Add event handlers + // 添加事件处理器 addEventHandlers(); } /** - * Adds event handlers to UI components. + * 为界面组件添加事件处理器。 */ private void addEventHandlers() { startExamButton.setOnAction(e -> handleStartExam()); @@ -91,7 +91,7 @@ public class MainMenuView extends BorderPane { } /** - * Handles the start exam button action. + * 处理开始考试按钮的操作。 */ private void handleStartExam() { ExamSelectionView examSelectionView = new ExamSelectionView(primaryStage, controller, userAccount); @@ -99,7 +99,7 @@ public class MainMenuView extends BorderPane { } /** - * Handles the change password button action. + * 处理修改密码按钮的操作。 */ private void handleChangePassword() { PasswordChangeView passwordChangeView = new PasswordChangeView(primaryStage, controller, userAccount); @@ -107,11 +107,11 @@ public class MainMenuView extends BorderPane { } /** - * Handles the logout button action. + * 处理退出登录按钮的操作。 */ private void handleLogout() { - // Go back to login screen + // 返回登录界面 LoginScene loginScene = new LoginScene(primaryStage, controller); primaryStage.getScene().setRoot(loginScene); } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/ui/views/PasswordChangeView.java b/src/main/java/com/personalproject/ui/views/PasswordChangeView.java index 9f5b98a..ef28098 100644 --- a/src/main/java/com/personalproject/ui/views/PasswordChangeView.java +++ b/src/main/java/com/personalproject/ui/views/PasswordChangeView.java @@ -12,7 +12,7 @@ import com.personalproject.auth.UserAccount; import com.personalproject.ui.views.MainMenuView; /** - * View for changing user password. + * 用户修改密码的界面。 */ public class PasswordChangeView extends BorderPane { @@ -26,11 +26,11 @@ public class PasswordChangeView extends BorderPane { private Button backButton; /** - * Constructor for PasswordChangeView. + * PasswordChangeView 的构造函数。 * - * @param primaryStage The main stage of the application - * @param controller The math learning controller - * @param userAccount The current user account + * @param primaryStage 应用程序的主舞台 + * @param controller 数学学习控制器 + * @param userAccount 当前用户账户 */ public PasswordChangeView(Stage primaryStage, MathLearningController controller, UserAccount userAccount) { this.primaryStage = primaryStage; @@ -40,19 +40,19 @@ public class PasswordChangeView extends BorderPane { } /** - * Initializes the UI components. + * 初始化界面组件。 */ private void initializeUI() { - // Create the main layout + // 创建主布局 VBox mainLayout = new VBox(20); mainLayout.setAlignment(Pos.CENTER); mainLayout.setPadding(new Insets(20)); - // Title + // 标题 Label titleLabel = new Label("修改密码"); titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24)); - // Form for password change + // 修改密码表单 GridPane passwordForm = new GridPane(); passwordForm.setHgap(15); passwordForm.setVgap(15); @@ -77,31 +77,31 @@ public class PasswordChangeView extends BorderPane { passwordForm.add(confirmNewPasswordLabel, 0, 2); passwordForm.add(confirmNewPasswordField, 1, 2); - // Buttons + // 按钮区域 HBox buttonBox = new HBox(15); buttonBox.setAlignment(Pos.CENTER); changePasswordButton = new Button("修改密码"); backButton = new Button("返回"); - // Set button sizes + // 设置按钮尺寸 changePasswordButton.setPrefSize(120, 40); backButton.setPrefSize(120, 40); buttonBox.getChildren().addAll(changePasswordButton, backButton); - // Add components to main layout + // 将组件添加到主布局 mainLayout.getChildren().addAll(titleLabel, passwordForm, buttonBox); - // Set the center of the border pane + // 将主布局置于边界面板中央 setCenter(mainLayout); - // Add event handlers + // 添加事件处理器 addEventHandlers(); } /** - * Adds event handlers to UI components. + * 为界面组件添加事件处理器。 */ private void addEventHandlers() { changePasswordButton.setOnAction(e -> handleChangePassword()); @@ -109,7 +109,7 @@ public class PasswordChangeView extends BorderPane { } /** - * Handles the change password button action. + * 处理修改密码按钮的操作。 */ private void handleChangePassword() { String oldPassword = oldPasswordField.getText(); @@ -136,14 +136,14 @@ public class PasswordChangeView extends BorderPane { if (success) { showAlert(Alert.AlertType.INFORMATION, "修改成功", "密码修改成功!"); - handleBack(); // Go back to main menu + handleBack(); // 返回主菜单 } else { showAlert(Alert.AlertType.ERROR, "修改失败", "当前密码错误或修改失败"); } } /** - * Handles the back button action. + * 处理返回按钮的操作。 */ private void handleBack() { MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount); @@ -151,11 +151,11 @@ public class PasswordChangeView extends BorderPane { } /** - * Shows an alert dialog. + * 显示提示对话框。 * - * @param alertType Type of alert - * @param title Title of the alert - * @param message Message to display + * @param alertType 提示类型 + * @param title 对话框标题 + * @param message 显示的消息 */ private void showAlert(Alert.AlertType alertType, String title, String message) { Alert alert = new Alert(alertType); @@ -164,4 +164,4 @@ public class PasswordChangeView extends BorderPane { alert.setContentText(message); alert.showAndWait(); } -} \ No newline at end of file +} -- 2.34.1 From ecce393c825a3b8c7a77136ffdf311e611c7062d Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 9 Oct 2025 21:56:39 +0800 Subject: [PATCH 5/9] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 9 +- .../auth/AccountRepository.java | 140 ---------- .../personalproject/auth/EmailService.java | 61 ----- .../auth/PasswordValidator.java | 33 --- src/com/personalproject/auth/UserAccount.java | 40 --- .../controller/MathLearningController.java | 155 ----------- .../HighSchoolQuestionGenerator.java | 37 --- .../MiddleSchoolQuestionGenerator.java | 39 --- .../generator/PrimaryQuestionGenerator.java | 109 -------- .../generator/QuestionGenerator.java | 17 -- .../model/DifficultyLevel.java | 49 ---- .../personalproject/model/ExamSession.java | 257 ------------------ .../personalproject/model/QuizQuestion.java | 73 ----- .../service/ExamResultService.java | 83 ------ .../personalproject/service/ExamService.java | 140 ---------- .../service/MathExpressionEvaluator.java | 217 --------------- .../service/MathLearningService.java | 180 ------------ .../service/QuestionGenerationService.java | 71 ----- .../service/RegistrationService.java | 148 ---------- .../storage/QuestionStorageService.java | 162 ----------- .../auth/AccountRepository.java | 246 ++++++++++++++--- .../personalproject/auth/EmailService.java | 183 ++++++++++++- .../auth/PasswordValidator.java | 6 +- .../generator/PrimaryQuestionGenerator.java | 65 +++-- .../personalproject/model/ExamSession.java | 12 +- .../personalproject/service/ExamService.java | 128 +++++---- .../service/MathExpressionEvaluator.java | 53 +++- .../service/MathLearningService.java | 4 +- .../service/RegistrationService.java | 91 +++++-- .../storage/QuestionStorageService.java | 18 +- .../personalproject/ui/views/ExamView.java | 5 +- src/main/resources/email-config.properties | 20 ++ 32 files changed, 670 insertions(+), 2181 deletions(-) delete mode 100644 src/com/personalproject/auth/AccountRepository.java delete mode 100644 src/com/personalproject/auth/EmailService.java delete mode 100644 src/com/personalproject/auth/PasswordValidator.java delete mode 100644 src/com/personalproject/auth/UserAccount.java delete mode 100644 src/com/personalproject/controller/MathLearningController.java delete mode 100644 src/com/personalproject/generator/HighSchoolQuestionGenerator.java delete mode 100644 src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java delete mode 100644 src/com/personalproject/generator/PrimaryQuestionGenerator.java delete mode 100644 src/com/personalproject/generator/QuestionGenerator.java delete mode 100644 src/com/personalproject/model/DifficultyLevel.java delete mode 100644 src/com/personalproject/model/ExamSession.java delete mode 100644 src/com/personalproject/model/QuizQuestion.java delete mode 100644 src/com/personalproject/service/ExamResultService.java delete mode 100644 src/com/personalproject/service/ExamService.java delete mode 100644 src/com/personalproject/service/MathExpressionEvaluator.java delete mode 100644 src/com/personalproject/service/MathLearningService.java delete mode 100644 src/com/personalproject/service/QuestionGenerationService.java delete mode 100644 src/com/personalproject/service/RegistrationService.java delete mode 100644 src/com/personalproject/storage/QuestionStorageService.java create mode 100644 src/main/resources/email-config.properties diff --git a/pom.xml b/pom.xml index c62513a..70e9913 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,13 @@ ${javafx.version} + + + com.sun.mail + jakarta.mail + 2.0.1 + + org.junit.jupiter @@ -140,4 +147,4 @@ - \ No newline at end of file + diff --git a/src/com/personalproject/auth/AccountRepository.java b/src/com/personalproject/auth/AccountRepository.java deleted file mode 100644 index 5bb193d..0000000 --- a/src/com/personalproject/auth/AccountRepository.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.personalproject.auth; - -import com.personalproject.model.DifficultyLevel; -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -/** - * 存放用户账号并提供认证和注册查询能力. - */ -public final class AccountRepository { - - private final Map accounts = new HashMap<>(); - private final Map registrationCodes = new HashMap<>(); - - /** - * 根据用户名与密码查找匹配的账号. - * - * @param username 用户名. - * @param password 密码. - * @return 匹配成功时返回账号信息,否则返回空结果. - */ - public Optional authenticate(String username, String password) { - if (username == null || password == null) { - return Optional.empty(); - } - UserAccount account = accounts.get(username.trim()); - if (account == null || !account.isRegistered()) { - return Optional.empty(); - } - if (!account.password().equals(password.trim())) { - return Optional.empty(); - } - return Optional.of(account); - } - - /** - * 使用电子邮箱注册新用户账号。 - * - * @param username 用户名 - * @param email 邮箱地址 - * @param difficultyLevel 选择的难度级别 - * @return 若注册成功则返回 true,若用户名已存在则返回 false - */ - public boolean registerUser(String username, String email, DifficultyLevel difficultyLevel) { - String trimmedUsername = username.trim(); - String trimmedEmail = email.trim(); - - if (accounts.containsKey(trimmedUsername)) { - return false; // 用户名已存在 - } - - // 检查邮箱是否已被其他账号使用 - for (UserAccount account : accounts.values()) { - if (account.email().equals(trimmedEmail) && account.isRegistered()) { - return false; // 邮箱已注册 - } - } - - UserAccount newAccount = new UserAccount( - trimmedUsername, - trimmedEmail, - "", // 初始为空密码 - difficultyLevel, - LocalDateTime.now(), - false); // 在设置密码前视为未注册 - accounts.put(trimmedUsername, newAccount); - return true; - } - - /** - * 在注册后为用户设置密码。 - * - * @param username 用户名 - * @param password 要设置的密码 - * @return 设置成功返回 true,若用户不存在则返回 false - */ - public boolean setPassword(String username, String password) { - UserAccount account = accounts.get(username.trim()); - if (account == null) { - return false; - } - - UserAccount updatedAccount = new UserAccount( - account.username(), - account.email(), - password, - account.difficultyLevel(), - account.registrationDate(), - true); // 设置密码后标记为已注册 - accounts.put(username.trim(), updatedAccount); - return true; - } - - /** - * 修改现有用户的密码。 - * - * @param username 用户名 - * @param oldPassword 当前密码 - * @param newPassword 新密码 - * @return 修改成功返回 true,若旧密码错误或用户不存在则返回 false - */ - public boolean changePassword(String username, String oldPassword, String newPassword) { - UserAccount account = accounts.get(username.trim()); - if (account == null || !account.password().equals(oldPassword) || !account.isRegistered()) { - return false; - } - - UserAccount updatedAccount = new UserAccount( - account.username(), - account.email(), - newPassword, - account.difficultyLevel(), - account.registrationDate(), - true); - accounts.put(username.trim(), updatedAccount); - return true; - } - - /** - * 检查用户是否存在。 - * - * @param username 待检查的用户名 - * @return 若存在则返回 true,否则返回 false - */ - public boolean userExists(String username) { - return accounts.containsKey(username.trim()); - } - - /** - * 按用户名获取用户账户。 - * - * @param username 用户名 - * @return 若找到则返回包含用户账户的 Optional - */ - public Optional getUser(String username) { - return Optional.ofNullable(accounts.get(username.trim())); - } -} diff --git a/src/com/personalproject/auth/EmailService.java b/src/com/personalproject/auth/EmailService.java deleted file mode 100644 index 6ae2b1c..0000000 --- a/src/com/personalproject/auth/EmailService.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.personalproject.auth; - -import java.util.Random; - -/** - * 用于发送带有注册码的电子邮件的工具类。 - */ -public final class EmailService { - - private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - private static final int CODE_LENGTH = 6; - private static final Random RANDOM = new Random(); - - private EmailService() { - // 防止实例化此工具类 - } - - /** - * 生成随机注册码。 - * - * @return 随机生成的注册码 - */ - public static String generateRegistrationCode() { - StringBuilder code = new StringBuilder(); - for (int i = 0; i < CODE_LENGTH; i++) { - code.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length()))); - } - return code.toString(); - } - - /** - * 将注册码发送到指定邮箱。真实实现中会连接邮件服务器。 - * - * @param email 收件人邮箱 - * @param registrationCode 要发送的注册码 - * @return 发送成功返回 true(在此模拟实现中始终为 true) - */ - public static boolean sendRegistrationCode(String email, String registrationCode) { - // 真实实现会连接邮件服务器 - // 模拟实现仅在控制台打印 - System.out.println("Sending registration code " + registrationCode + " to " + email); - return true; - } - - /** - * 校验电子邮件地址的格式是否有效。 - * - * @param email 待校验的邮箱地址 - * @return 若格式有效则返回 true,否则返回 false - */ - public static boolean isValidEmail(String email) { - if (email == null || email.trim().isEmpty()) { - return false; - } - - // 使用正则表达式进行简单邮箱校验 - String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@" - + "(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"; - return email.matches(emailRegex); - } -} diff --git a/src/com/personalproject/auth/PasswordValidator.java b/src/com/personalproject/auth/PasswordValidator.java deleted file mode 100644 index 6b4ab91..0000000 --- a/src/com/personalproject/auth/PasswordValidator.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.personalproject.auth; - -import java.util.regex.Pattern; - -/** - * 用于验证密码规则的工具类。 - */ -public final class PasswordValidator { - - private static final Pattern PASSWORD_PATTERN = - Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{6,10}$"); - - private PasswordValidator() { - // 防止实例化此工具类 - } - - /** - * 校验密码是否满足以下要求: - * - 长度 6-10 位 - * - 至少包含一个大写字母 - * - 至少包含一个小写字母 - * - 至少包含一个数字 - * - * @param password 待校验的密码 - * @return 若满足要求则返回 true,否则返回 false - */ - public static boolean isValidPassword(String password) { - if (password == null) { - return false; - } - return PASSWORD_PATTERN.matcher(password).matches(); - } -} diff --git a/src/com/personalproject/auth/UserAccount.java b/src/com/personalproject/auth/UserAccount.java deleted file mode 100644 index 07543a0..0000000 --- a/src/com/personalproject/auth/UserAccount.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.personalproject.auth; - -import com.personalproject.model.DifficultyLevel; -import java.time.LocalDateTime; - -/** - * 不可变的账号定义. - */ -public record UserAccount( - String username, - String email, - String password, - DifficultyLevel difficultyLevel, - LocalDateTime registrationDate, - boolean isRegistered) { - - /** - * 创建新的用户账户,并使用当前时间作为注册时间。 - * - * @param username 用户名 - * @param email 邮箱地址 - * @param password 密码 - * @param difficultyLevel 选择的难度级别 - * @param isRegistered 用户是否已完成注册 - */ - public UserAccount { - if (username == null || username.trim().isEmpty()) { - throw new IllegalArgumentException("Username cannot be null or empty"); - } - if (email == null || email.trim().isEmpty()) { - throw new IllegalArgumentException("Email cannot be null or empty"); - } - if (password == null) { - throw new IllegalArgumentException("Password cannot be null"); - } - if (difficultyLevel == null) { - throw new IllegalArgumentException("Difficulty level cannot be null"); - } - } -} diff --git a/src/com/personalproject/controller/MathLearningController.java b/src/com/personalproject/controller/MathLearningController.java deleted file mode 100644 index 4edd694..0000000 --- a/src/com/personalproject/controller/MathLearningController.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.personalproject.controller; - -import com.personalproject.generator.QuestionGenerator; -import com.personalproject.model.DifficultyLevel; -import com.personalproject.model.ExamSession; -import com.personalproject.service.ExamResultService; -import com.personalproject.service.MathLearningService; -import com.personalproject.service.QuestionGenerationService; -import java.util.Map; -import java.util.Optional; - -/** - * 遵循MVC模式处理应用程序逻辑的控制器类. - */ -public final class MathLearningController { - - private final MathLearningService mathLearningService; - - /** - * 创建具有所需服务的新控制器. - * - * @param generatorMap 难度级别到题目生成器的映射 - * @param questionGenerationService 题目生成服务 - */ - public MathLearningController( - Map generatorMap, - QuestionGenerationService questionGenerationService) { - this.mathLearningService = new MathLearningService(generatorMap, questionGenerationService); - } - - /** - * 启动用户注册. - * - * @param username 所需用户名 - * @param email 用户的电子邮箱地址 - * @param difficultyLevel 选择的难度级别 - * @return 注册成功启动则返回true,否则返回false - */ - public boolean initiateRegistration(String username, String email, - DifficultyLevel difficultyLevel) { - return mathLearningService.initiateRegistration(username, email, difficultyLevel); - } - - /** - * 验证注册码. - * - * @param username 用户名 - * @param registrationCode 注册码 - * @return 码有效则返回true,否则返回false - */ - public boolean verifyRegistrationCode(String username, String registrationCode) { - return mathLearningService.verifyRegistrationCode(username, registrationCode); - } - - /** - * 注册后设置用户密码. - * - * @param username 用户名 - * @param password 要设置的密码 - * @return 密码设置成功则返回true,否则返回false - */ - public boolean setPassword(String username, String password) { - return mathLearningService.setPassword(username, password); - } - - /** - * 验证用户. - * - * @param username 用户名 - * @param password 密码 - * @return 验证成功则返回包含用户账户的Optional - */ - public Optional authenticate(String username, - String password) { - return mathLearningService.authenticate(username, password); - } - - /** - * 为用户创建考试会话. - * - * @param username 用户名 - * @param difficultyLevel 难度级别 - * @param questionCount 题目数量 - * @return 创建的考试会话 - */ - public ExamSession createExamSession(String username, DifficultyLevel difficultyLevel, - int questionCount) { - return mathLearningService.createExamSession(username, difficultyLevel, questionCount); - } - - /** - * 保存考试结果. - * - * @param examSession 要保存的考试会话 - */ - public void saveExamResults(ExamSession examSession) { - mathLearningService.saveExamResults(examSession); - } - - /** - * 处理考试结果并确定下一步操作. - * - * @param examSession 完成的考试会话 - * @param continueWithSameLevel 是否继续相同难度 - * @param newDifficultyLevel 更改时的新难度(如果不更改则为null) - * @return 要执行的下一步操作 - */ - public ExamResultService.ExamContinuationAction processExamResult( - ExamSession examSession, boolean continueWithSameLevel, DifficultyLevel newDifficultyLevel) { - return mathLearningService.processExamResult(examSession, continueWithSameLevel, - newDifficultyLevel); - } - - /** - * 更改用户密码. - * - * @param username 用户名 - * @param oldPassword 旧密码 - * @param newPassword 新密码 - * @return 密码更改成功则返回true,否则返回false - */ - public boolean changePassword(String username, String oldPassword, String newPassword) { - return mathLearningService.changePassword(username, oldPassword, newPassword); - } - - /** - * 验证密码. - * - * @param password 要验证的密码 - * @return 密码有效则返回true,否则返回false - */ - public boolean isValidPassword(String password) { - return MathLearningService.isValidPassword(password); - } - - /** - * 验证电子邮箱地址. - * - * @param email 要验证的邮箱 - * @return 邮箱有效则返回true,否则返回false - */ - public boolean isValidEmail(String email) { - return MathLearningService.isValidEmail(email); - } - - /** - * 按用户名获取用户账户。 - * - * @param username 用户名 - * @return 若找到则返回包含用户账户的 Optional - */ - public Optional getUserAccount(String username) { - return mathLearningService.getUser(username); - } -} diff --git a/src/com/personalproject/generator/HighSchoolQuestionGenerator.java b/src/com/personalproject/generator/HighSchoolQuestionGenerator.java deleted file mode 100644 index d22bf53..0000000 --- a/src/com/personalproject/generator/HighSchoolQuestionGenerator.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.personalproject.generator; - -import java.util.Random; - -/** - * 生成至少包含一种三角函数的高中难度题目表达式. - */ -public final class HighSchoolQuestionGenerator implements QuestionGenerator { - - private static final String[] OPERATORS = {"+", "-", "*", "/"}; - private static final String[] TRIG_FUNCTIONS = {"sin", "cos", "tan"}; - - @Override - public String generateQuestion(Random random) { - int operandCount = random.nextInt(5) + 1; - String[] operands = new String[operandCount]; - for (int index = 0; index < operandCount; index++) { - operands[index] = String.valueOf(random.nextInt(100) + 1); - } - int specialIndex = random.nextInt(operandCount); - String function = TRIG_FUNCTIONS[random.nextInt(TRIG_FUNCTIONS.length)]; - operands[specialIndex] = function + '(' + operands[specialIndex] + ')'; - StringBuilder builder = new StringBuilder(); - for (int index = 0; index < operandCount; index++) { - if (index > 0) { - String operator = OPERATORS[random.nextInt(OPERATORS.length)]; - builder.append(' ').append(operator).append(' '); - } - builder.append(operands[index]); - } - String expression = builder.toString(); - if (operandCount > 1 && random.nextBoolean()) { - return '(' + expression + ')'; - } - return expression; - } -} diff --git a/src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java b/src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java deleted file mode 100644 index ad46678..0000000 --- a/src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.personalproject.generator; - -import java.util.Random; - -/** - * 生成包含平方或开根号运算的初中难度题目表达式. - */ -public final class MiddleSchoolQuestionGenerator implements QuestionGenerator { - - private static final String[] OPERATORS = {"+", "-", "*", "/"}; - - @Override - public String generateQuestion(Random random) { - int operandCount = random.nextInt(5) + 1; - String[] operands = new String[operandCount]; - for (int index = 0; index < operandCount; index++) { - operands[index] = String.valueOf(random.nextInt(100) + 1); - } - int specialIndex = random.nextInt(operandCount); - if (random.nextBoolean()) { - operands[specialIndex] = '(' + operands[specialIndex] + ")^2"; - } else { - operands[specialIndex] = "sqrt(" + operands[specialIndex] + ')'; - } - StringBuilder builder = new StringBuilder(); - for (int index = 0; index < operandCount; index++) { - if (index > 0) { - String operator = OPERATORS[random.nextInt(OPERATORS.length)]; - builder.append(' ').append(operator).append(' '); - } - builder.append(operands[index]); - } - String expression = builder.toString(); - if (operandCount > 1 && random.nextBoolean()) { - return '(' + expression + ')'; - } - return expression; - } -} diff --git a/src/com/personalproject/generator/PrimaryQuestionGenerator.java b/src/com/personalproject/generator/PrimaryQuestionGenerator.java deleted file mode 100644 index 622bd0f..0000000 --- a/src/com/personalproject/generator/PrimaryQuestionGenerator.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.personalproject.generator; - -import com.personalproject.model.QuizQuestion; -import com.personalproject.service.MathExpressionEvaluator; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Random; - -/** - * 生成包含基础四则运算的小学难度题目表达式. - */ -public final class PrimaryQuestionGenerator implements QuestionGenerator, QuizQuestionGenerator { - - private static final String[] OPERATORS = {"+", "-", "*", "/"}; - private static final int OPTIONS_COUNT = 4; - - @Override - public String generateQuestion(Random random) { - // 至少生成两个操作数,避免题目退化成单个数字 - int operandCount = random.nextInt(4) + 2; - StringBuilder builder = new StringBuilder(); - for (int index = 0; index < operandCount; index++) { - if (index > 0) { - String operator = OPERATORS[random.nextInt(OPERATORS.length)]; - builder.append(' ').append(operator).append(' '); - } - int value = random.nextInt(100) + 1; - builder.append(value); - } - String expression = builder.toString(); - if (operandCount > 1 && random.nextBoolean()) { - return '(' + expression + ')'; - } - return expression; - } - - @Override - public QuizQuestion generateQuizQuestion(Random random) { - // 生成数学表达式 - int operandCount = random.nextInt(4) + 2; - StringBuilder builder = new StringBuilder(); - for (int index = 0; index < operandCount; index++) { - if (index > 0) { - String operator = OPERATORS[random.nextInt(OPERATORS.length)]; - builder.append(' ').append(operator).append(' '); - } - int value = random.nextInt(100) + 1; - builder.append(value); - } - String expression = builder.toString(); - if (operandCount > 1 && random.nextBoolean()) { - expression = '(' + expression + ')'; - } - - // 直接计算正确答案 - double correctAnswer; - try { - correctAnswer = MathExpressionEvaluator.evaluate(expression); - } catch (Exception e) { - // 如果计算失败则使用兜底值 - correctAnswer = 0.0; - } - - // 生成选项 - List options = generateOptions(correctAnswer, random); - - // 随机选择正确答案索引 - int correctAnswerIndex = random.nextInt(options.size()); - - // 确保正确答案在选项中 - String correctOption = String.format("%.2f", correctAnswer); - String oldOption = options.set(correctAnswerIndex, correctOption); - - // 将被替换的选项加回去以保持 4 个选项 - options.add(oldOption); - - return new QuizQuestion(expression, options, correctAnswerIndex); - } - - /** - * 生成选择题选项 - */ - private List generateOptions(double correctAnswer, Random random) { - List options = new ArrayList<>(); - - // 将正确答案加入选项 - options.add(String.format("%.2f", correctAnswer)); - - // 加入错误选项 - for (int i = 0; i < OPTIONS_COUNT - 1; i++) { - double incorrectAnswer = correctAnswer + (random.nextGaussian() * 10); // 添加随机偏移 - if (Math.abs(incorrectAnswer - correctAnswer) < 0.1) { // 确保与正确答案不同 - incorrectAnswer += 1.5; - } - options.add(String.format("%.2f", incorrectAnswer)); - } - - // 打乱顺序以随机化正确答案位置 - Collections.shuffle(options, random); - - // 在打乱后查找正确答案的索引 - String correctAnswerStr = String.format("%.2f", correctAnswer); - int correctIndex = options.indexOf(correctAnswerStr); - - return options; - } -} diff --git a/src/com/personalproject/generator/QuestionGenerator.java b/src/com/personalproject/generator/QuestionGenerator.java deleted file mode 100644 index a2a5ea7..0000000 --- a/src/com/personalproject/generator/QuestionGenerator.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.personalproject.generator; - -import java.util.Random; - -/** - * 负责生成单条数学题目的表达式. - */ -public interface QuestionGenerator { - - /** - * 基于提供的随机数生成器构造一道题目的表达式. - * - * @param random 用于生成随机数的实例. - * @return 生成的题目表达式. - */ - String generateQuestion(Random random); -} diff --git a/src/com/personalproject/model/DifficultyLevel.java b/src/com/personalproject/model/DifficultyLevel.java deleted file mode 100644 index bc654bb..0000000 --- a/src/com/personalproject/model/DifficultyLevel.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.personalproject.model; - -import java.util.Optional; - -/** - * 系统支持的出题难度级别. - */ -public enum DifficultyLevel { - PRIMARY("小学"), - MIDDLE("初中"), - HIGH("高中"); - - private final String displayName; - - DifficultyLevel(String displayName) { - this.displayName = displayName; - } - - /** - * 获取当前难度对应的展示名称. - * - * @return 难度的展示名称. - */ - public String getDisplayName() { - return displayName; - } - - /** - * 根据展示名称查找对应的难度枚举值. - * - * @param name 展示名称. - * @return 匹配到的难度枚举,可空返回. - */ - public static Optional fromDisplayName(String name) { - if (name == null) { - return Optional.empty(); - } - String trimmed = name.trim(); - if (trimmed.isEmpty()) { - return Optional.empty(); - } - for (DifficultyLevel level : values()) { - if (level.displayName.equals(trimmed)) { - return Optional.of(level); - } - } - return Optional.empty(); - } -} diff --git a/src/com/personalproject/model/ExamSession.java b/src/com/personalproject/model/ExamSession.java deleted file mode 100644 index 6b8f918..0000000 --- a/src/com/personalproject/model/ExamSession.java +++ /dev/null @@ -1,257 +0,0 @@ -package com.personalproject.model; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -/** - * 表示用户的考试会话。 - */ -public final class ExamSession { - - private final String username; - private final DifficultyLevel difficultyLevel; - private final List questions; - private final List userAnswers; - private final LocalDateTime startTime; - private int currentQuestionIndex; - - /** - * 创建新的考试会话。 - * - * @param username 考生的用户名 - * @param difficultyLevel 考试的难度级别 - * @param questions 考试题目列表 - */ - public ExamSession(String username, DifficultyLevel difficultyLevel, - List questions) { - if (username == null || username.trim().isEmpty()) { - throw new IllegalArgumentException("Username cannot be null or empty"); - } - if (difficultyLevel == null) { - throw new IllegalArgumentException("Difficulty level cannot be null"); - } - if (questions == null || questions.isEmpty()) { - throw new IllegalArgumentException("Questions list cannot be null or empty"); - } - - this.username = username; - this.difficultyLevel = difficultyLevel; - this.questions = List.copyOf(questions); // 题目的不可变副本 - this.userAnswers = new ArrayList<>(); - // 使用 -1 初始化用户答案(表示未选择) - for (int i = 0; i < questions.size(); i++) { - userAnswers.add(-1); - } - this.startTime = LocalDateTime.now(); - this.currentQuestionIndex = 0; - } - - /** - * 获取考生的用户名。 - * - * @return 用户名 - */ - public String getUsername() { - return username; - } - - /** - * 获取考试的难度级别。 - * - * @return 考试难度级别 - */ - public DifficultyLevel getDifficultyLevel() { - return difficultyLevel; - } - - /** - * 获取考试中的题目列表。 - * - * @return 不可修改的题目列表 - */ - public List getQuestions() { - return questions; - } - - /** - * 获取用户对各题的作答。 - * - * @return 答案索引列表(-1 表示未选择) - */ - public List getUserAnswers() { - return List.copyOf(userAnswers); // 返回副本以防止被修改 - } - - /** - * 获取当前题目的索引。 - * - * @return 当前题目索引 - */ - public int getCurrentQuestionIndex() { - return currentQuestionIndex; - } - - /** - * 为当前题目记录用户的答案。 - * - * @param answerIndex 选中答案的索引 - */ - public void setAnswer(int answerIndex) { - if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) { - throw new IllegalStateException("No valid question at current index"); - } - if (answerIndex < 0 || answerIndex > questions.get(currentQuestionIndex).getOptions().size()) { - throw new IllegalArgumentException("Invalid answer index"); - } - userAnswers.set(currentQuestionIndex, answerIndex); - } - - /** - * 跳转到下一题。 - * - * @return 若成功跳转则返回 true,若已是最后一题则返回 false - */ - public boolean goToNextQuestion() { - if (currentQuestionIndex < questions.size() - 1) { - currentQuestionIndex++; - return true; - } - return false; - } - - /** - * 返回上一题。 - * - * @return 若成功返回则返回 true,若已是第一题则返回 false - */ - public boolean goToPreviousQuestion() { - if (currentQuestionIndex > 0) { - currentQuestionIndex--; - return true; - } - return false; - } - - /** - * 检查考试是否完成(所有题目已作答或到达最后一题)。 - * - * @return 若考试已完成则返回 true,否则返回 false - */ - public boolean isComplete() { - return currentQuestionIndex >= questions.size() - 1; - } - - /** - * 获取当前题目。 - * - * @return 当前的测验题 - */ - public QuizQuestion getCurrentQuestion() { - if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) { - throw new IllegalStateException("No valid question at current index"); - } - return questions.get(currentQuestionIndex); - } - - /** - * 获取指定题目的用户答案。 - * - * @param questionIndex 题目索引 - * @return 用户答案的索引(未选择时为 -1) - */ - public int getUserAnswer(int questionIndex) { - if (questionIndex < 0 || questionIndex >= questions.size()) { - throw new IllegalArgumentException("Question index out of bounds"); - } - return userAnswers.get(questionIndex); - } - - /** - * 计算得分百分比。 - * - * @return 0-100 范围内的得分百分比 - */ - public double calculateScore() { - int correctCount = 0; - for (int i = 0; i < questions.size(); i++) { - QuizQuestion question = questions.get(i); - int userAnswer = userAnswers.get(i); - if (userAnswer != -1 && question.isAnswerCorrect(userAnswer)) { - correctCount++; - } - } - return questions.isEmpty() ? 0.0 : (double) correctCount / questions.size() * 100.0; - } - - /** - * 获取考试中的题目总数。 - * - * @return 题目总数 - */ - public int getTotalQuestions() { - return questions.size(); - } - - /** - * 检查指定题目是否已作答。 - * - * @param questionIndex 题目索引 - * @return 若已作答则返回 true,否则返回 false - */ - public boolean hasAnswered(int questionIndex) { - if (questionIndex < 0 || questionIndex >= questions.size()) { - throw new IllegalArgumentException("Question index out of bounds"); - } - return userAnswers.get(questionIndex) != -1; - } - - /** - * 获取考试开始时间。 - * - * @return 开始时间 - */ - public LocalDateTime getStartTime() { - return startTime; - } - - /** - * 获取答对的题目数量。 - * - * @return 正确题目数量 - */ - public int getCorrectAnswersCount() { - int correctCount = 0; - for (int i = 0; i < questions.size(); i++) { - QuizQuestion question = questions.get(i); - int userAnswer = userAnswers.get(i); - if (userAnswer != -1 && question.isAnswerCorrect(userAnswer)) { - correctCount++; - } - } - return correctCount; - } - - /** - * 获取答错的题目数量。 - * - * @return 错误题目数量 - */ - public int getIncorrectAnswersCount() { - int totalAnswered = 0; - int correctCount = 0; - - for (int i = 0; i < questions.size(); i++) { - int userAnswer = userAnswers.get(i); - if (userAnswer != -1) { - totalAnswered++; - QuizQuestion question = questions.get(i); - if (question.isAnswerCorrect(userAnswer)) { - correctCount++; - } - } - } - - return totalAnswered - correctCount; - } -} diff --git a/src/com/personalproject/model/QuizQuestion.java b/src/com/personalproject/model/QuizQuestion.java deleted file mode 100644 index 52c8f6b..0000000 --- a/src/com/personalproject/model/QuizQuestion.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.personalproject.model; - -import java.util.List; - -/** - * 表示一个带有多项选择的测验题目。 - */ -public final class QuizQuestion { - - private final String questionText; - private final List options; - private final int correctAnswerIndex; - - /** - * 创建新的测验题目。 - * - * @param questionText 题目文本 - * @param options 答案选项列表 - * @param correctAnswerIndex 正确答案在选项列表中的索引 - */ - public QuizQuestion(String questionText, List options, int correctAnswerIndex) { - if (questionText == null || questionText.trim().isEmpty()) { - throw new IllegalArgumentException("Question text cannot be null or empty"); - } - if (options == null || options.size() < 2) { - throw new IllegalArgumentException("Options must contain at least 2 choices"); - } - if (correctAnswerIndex < 0 || correctAnswerIndex >= options.size()) { - throw new IllegalArgumentException("Correct answer index out of bounds"); - } - - this.questionText = questionText; - this.options = List.copyOf(options); // 不可变副本 - this.correctAnswerIndex = correctAnswerIndex; - } - - /** - * 获取题目文本。 - * - * @return 题目文本 - */ - public String getQuestionText() { - return questionText; - } - - /** - * 获取答案选项列表。 - * - * @return 不可修改的答案选项列表 - */ - public List getOptions() { - return options; - } - - /** - * 获取正确答案在选项列表中的索引。 - * - * @return 正确答案的索引 - */ - public int getCorrectAnswerIndex() { - return correctAnswerIndex; - } - - /** - * 检查给定的答案索引是否正确。 - * - * @param answerIndex 用户答案的索引 - * @return 若答案正确则返回 true,否则返回 false - */ - public boolean isAnswerCorrect(int answerIndex) { - return answerIndex == correctAnswerIndex; - } -} diff --git a/src/com/personalproject/service/ExamResultService.java b/src/com/personalproject/service/ExamResultService.java deleted file mode 100644 index 55a79f6..0000000 --- a/src/com/personalproject/service/ExamResultService.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.personalproject.service; - -import com.personalproject.model.DifficultyLevel; -import com.personalproject.model.ExamSession; - -/** - * 用于处理考试后选项(如继续或退出)的服务. - */ -public final class ExamResultService { - - /** - * 评估用户在完成考试后的选择. - * - * @param examSession 完成的考试会话 - * @param continueWithSameLevel 是否使用相同难度级别继续 - * @param newDifficultyLevel 更改时的新难度级别(如果不更改则为null) - * @return 指示下一步操作的操作 - */ - public ExamContinuationAction processExamResult( - ExamSession examSession, boolean continueWithSameLevel, DifficultyLevel newDifficultyLevel) { - - if (continueWithSameLevel) { - return new ExamContinuationAction( - true, examSession.getDifficultyLevel(), (int) examSession.getQuestions().size()); - } else if (newDifficultyLevel != null) { - return new ExamContinuationAction(true, newDifficultyLevel, - (int) examSession.getQuestions().size()); - } else { - return new ExamContinuationAction(false, null, 0); - } - } - - /** - * 表示完成考试后要采取的操作. - */ - public static final class ExamContinuationAction { - - private final boolean shouldContinue; - private final DifficultyLevel nextDifficultyLevel; - private final int nextQuestionCount; - - /** - * 创建新的考试延续操作. - * - * @param shouldContinue 用户是否想继续参加另一次考试 - * @param nextDifficultyLevel 下次考试的难度级别(如果不继续则为null) - * @param nextQuestionCount 下次考试的题目数量(如果不继续则为0) - */ - public ExamContinuationAction( - boolean shouldContinue, DifficultyLevel nextDifficultyLevel, int nextQuestionCount) { - this.shouldContinue = shouldContinue; - this.nextDifficultyLevel = nextDifficultyLevel; - this.nextQuestionCount = nextQuestionCount; - } - - /** - * 获取用户是否想继续参加另一次考试. - * - * @return 如果继续则为true,如果退出则为false - */ - public boolean shouldContinue() { - return shouldContinue; - } - - /** - * 获取下次考试的难度级别. - * - * @return 下次难度级别(如果不继续则为null) - */ - public DifficultyLevel getNextDifficultyLevel() { - return nextDifficultyLevel; - } - - /** - * 获取下次考试的题目数量. - * - * @return 下次题目数量(如果不继续则为0) - */ - public int getNextQuestionCount() { - return nextQuestionCount; - } - } -} \ No newline at end of file diff --git a/src/com/personalproject/service/ExamService.java b/src/com/personalproject/service/ExamService.java deleted file mode 100644 index 392a83a..0000000 --- a/src/com/personalproject/service/ExamService.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.personalproject.service; - -import com.personalproject.generator.QuestionGenerator; -import com.personalproject.model.DifficultyLevel; -import com.personalproject.model.ExamSession; -import com.personalproject.model.QuizQuestion; -import java.util.ArrayList; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; -import java.util.Random; - -/** - * 用于管理考试会话和生成测验题目的服务类. - */ -public final class ExamService { - - private static final int OPTIONS_COUNT = 4; - private final Map generators; - private final Random random = new Random(); - private final QuestionGenerationService questionGenerationService; - - /** - * 创建新的考试服务. - * - * @param generatorMap 难度级别到题目生成器的映射 - * @param questionGenerationService 题目生成服务 - */ - public ExamService( - Map generatorMap, - QuestionGenerationService questionGenerationService) { - this.generators = new EnumMap<>(DifficultyLevel.class); - this.generators.putAll(generatorMap); - this.questionGenerationService = questionGenerationService; - } - - /** - * 使用指定参数创建新的考试会话. - * - * @param username 考生用户名 - * @param difficultyLevel 考试难度级别 - * @param questionCount 考试中包含的题目数量 - * @return 新的考试会话 - */ - public ExamSession createExamSession( - String username, DifficultyLevel difficultyLevel, int questionCount) { - if (questionCount < 10 || questionCount > 30) { - throw new IllegalArgumentException("题目数量必须在10到30之间"); - } - - // 根据难度级别生成题目 - List generatedQuestions = new ArrayList<>(); - QuestionGenerator generator = generators.get(difficultyLevel); - if (generator == null) { - throw new IllegalArgumentException("找不到难度级别的生成器: " + difficultyLevel); - } - - for (int i = 0; i < questionCount; i++) { - String question = generator.generateQuestion(random); - generatedQuestions.add(question); - } - - // 将字符串题目转换为带有选项和答案的QuizQuestion对象 - List quizQuestions = new ArrayList<>(); - for (String questionText : generatedQuestions) { - List options = generateOptions(questionText); - int correctAnswerIndex = generateCorrectAnswerIndex(options.size()); - QuizQuestion quizQuestion = new QuizQuestion(questionText, options, correctAnswerIndex); - quizQuestions.add(quizQuestion); - } - - return new ExamSession(username, difficultyLevel, quizQuestions); - } - - /** - * 使用预定义题目创建考试会话(用于测试). - * - * @param username 考生用户名 - * @param difficultyLevel 考试难度级别 - * @param questions 预定义题目列表 - * @return 新的考试会话 - */ - public ExamSession createExamSession( - String username, DifficultyLevel difficultyLevel, List questions) { - if (questions.size() < 10 || questions.size() > 30) { - throw new IllegalArgumentException("题目数量必须在10到30之间"); - } - return new ExamSession(username, difficultyLevel, questions); - } - - /** - * 为给定题目生成多项选择选项. 这是一个模拟实现 - 在实际系统中,选项会根据题目生成. - * - * @param questionText 题目文本 - * @return 答案选项列表 - */ - private List generateOptions(String questionText) { - List options = new ArrayList<>(); - // 为每个题目生成4个选项 - try { - // 尝试将数学表达式作为正确答案进行评估 - double correctAnswer = MathExpressionEvaluator.evaluate(questionText); - - // 创建正确答案选项 - options.add(String.format("%.2f", correctAnswer)); - - // 生成3个错误选项 - for (int i = 0; i < OPTIONS_COUNT - 1; i++) { - double incorrectAnswer = correctAnswer + (random.nextGaussian() * 10); // 添加一些随机偏移 - if (Math.abs(incorrectAnswer - correctAnswer) < 0.1) { // 确保不同 - incorrectAnswer += 1.5; - } - options.add(String.format("%.2f", incorrectAnswer)); - } - } catch (Exception e) { - // 如果评估失败,创建虚拟选项 - for (int i = 0; i < OPTIONS_COUNT; i++) { - options.add("选项 " + (i + 1)); - } - } - - // 随机打乱选项以随机化正确答案的位置 - java.util.Collections.shuffle(options, random); - - // 找到打乱后的正确答案索引 - // 对于此模拟实现,我们将返回第一个选项(索引0)作为正确答案 - // 实际实现将跟踪正确答案 - return options; - } - - /** - * 为给定选项数量生成随机正确答案索引. - * - * @param optionCount 选项数量 - * @return 0到optionCount-1之间的随机索引 - */ - private int generateCorrectAnswerIndex(int optionCount) { - return random.nextInt(optionCount); - } -} \ No newline at end of file diff --git a/src/com/personalproject/service/MathExpressionEvaluator.java b/src/com/personalproject/service/MathExpressionEvaluator.java deleted file mode 100644 index c4ee580..0000000 --- a/src/com/personalproject/service/MathExpressionEvaluator.java +++ /dev/null @@ -1,217 +0,0 @@ -package com.personalproject.service; - -import java.util.HashMap; -import java.util.Map; -import java.util.Stack; -import java.util.regex.Pattern; - -/** - * 可处理基本算术运算的数学表达式求值器。 - */ -public final class MathExpressionEvaluator { - - private static final Pattern NUMBER_PATTERN = Pattern.compile("-?\\d+(\\.\\d+)?"); - private static final Map PRECEDENCE = new HashMap<>(); - - static { - PRECEDENCE.put('+', 1); - PRECEDENCE.put('-', 1); - PRECEDENCE.put('*', 2); - PRECEDENCE.put('/', 2); - PRECEDENCE.put('^', 3); - } - - private MathExpressionEvaluator() { - // 防止实例化此工具类 - } - - /** - * 计算数学表达式字符串的结果。 - * - * @param expression 要计算的数学表达式 - * @return 计算结果 - * @throws IllegalArgumentException 如果表达式无效 - */ - public static double evaluate(String expression) { - if (expression == null) { - throw new IllegalArgumentException("Expression cannot be null"); - } - - expression = expression.replaceAll("\\s+", ""); // 移除空白字符 - if (expression.isEmpty()) { - throw new IllegalArgumentException("Expression cannot be empty"); - } - - // 将表达式拆分为记号 - String[] tokens = tokenize(expression); - - // 使用调度场算法将中缀表达式转换为后缀表达式 - String[] postfix = infixToPostfix(tokens); - - // 计算后缀表达式 - return evaluatePostfix(postfix); - } - - /** - * 将表达式拆分为数字与运算符的记号。 - * - * @param expression 待拆分的表达式 - * @return 记号数组 - */ - private static String[] tokenize(String expression) { - java.util.List tokens = new java.util.ArrayList<>(); - StringBuilder currentNumber = new StringBuilder(); - - for (int i = 0; i < expression.length(); i++) { - char c = expression.charAt(i); - - if (Character.isDigit(c) || c == '.') { - currentNumber.append(c); - } else if (c == '(' || c == ')') { - if (currentNumber.length() > 0) { - tokens.add(currentNumber.toString()); - currentNumber.setLength(0); - } - tokens.add(String.valueOf(c)); - } else if (isOperator(c)) { - if (currentNumber.length() > 0) { - tokens.add(currentNumber.toString()); - currentNumber.setLength(0); - } - - // 处理一元负号 - if (c == '-' && (i == 0 || expression.charAt(i - 1) == '(')) { - currentNumber.append(c); - } else { - tokens.add(String.valueOf(c)); - } - } else { - throw new IllegalArgumentException("Invalid character in expression: " + c); - } - } - - if (currentNumber.length() > 0) { - tokens.add(currentNumber.toString()); - } - - return tokens.toArray(new String[0]); - } - - /** - * 检查字符是否为运算符。 - * - * @param c 待检查的字符 - * @return 若字符是运算符则返回 true,否则返回 false - */ - private static boolean isOperator(char c) { - return c == '+' || c == '-' || c == '*' || c == '/' || c == '^'; - } - - /** - * 使用调度场算法将中缀表达式转换为后缀表达式。 - * - * @param tokens 中缀表达式的记号数组 - * @return 后缀表达式的记号数组 - */ - private static String[] infixToPostfix(String[] tokens) { - java.util.List output = new java.util.ArrayList<>(); - Stack operators = new Stack<>(); - - for (String token : tokens) { - if (isNumber(token)) { - output.add(token); - } else if (token.equals("(")) { - operators.push(token); - } else if (token.equals(")")) { - while (!operators.isEmpty() && !operators.peek().equals("(")) { - output.add(operators.pop()); - } - if (!operators.isEmpty()) { - operators.pop(); // 移除 "(" - } - } else if (isOperator(token.charAt(0))) { - while (!operators.isEmpty() - && isOperator(operators.peek().charAt(0)) - && PRECEDENCE.get(operators.peek().charAt(0)) >= PRECEDENCE.get(token.charAt(0))) { - output.add(operators.pop()); - } - operators.push(token); - } - } - - while (!operators.isEmpty()) { - output.add(operators.pop()); - } - - return output.toArray(new String[0]); - } - - /** - * 计算后缀表达式的值。 - * - * @param postfix 后缀表达式的记号数组 - * @return 计算结果 - */ - private static double evaluatePostfix(String[] postfix) { - Stack values = new Stack<>(); - - for (String token : postfix) { - if (isNumber(token)) { - values.push(Double.parseDouble(token)); - } else if (isOperator(token.charAt(0))) { - if (values.size() < 2) { - throw new IllegalArgumentException("Invalid expression: insufficient operands"); - } - - double b = values.pop(); - double a = values.pop(); - double result = performOperation(a, b, token.charAt(0)); - values.push(result); - } - } - - if (values.size() != 1) { - throw new IllegalArgumentException("Invalid expression: too many operands"); - } - - return values.pop(); - } - - /** - * 对两个操作数执行指定运算。 - * - * @param a 第一个操作数 - * @param b 第二个操作数 - * @param operator 要应用的运算符 - * @return 运算结果 - */ - private static double performOperation(double a, double b, char operator) { - switch (operator) { - case '+': - return a + b; - case '-': - return a - b; - case '*': - return a * b; - case '/': - if (b == 0) { - throw new ArithmeticException("Division by zero"); - } - return a / b; - case '^': - return Math.pow(a, b); - default: - throw new IllegalArgumentException("Unknown operator: " + operator); - } - } - - /** - * 检查记号是否为数字。 - * - * @param token 待检查的记号 - * @return 若为数字则返回 true,否则返回 false - */ - private static boolean isNumber(String token) { - return NUMBER_PATTERN.matcher(token).matches(); - } -} diff --git a/src/com/personalproject/service/MathLearningService.java b/src/com/personalproject/service/MathLearningService.java deleted file mode 100644 index 7f7f62b..0000000 --- a/src/com/personalproject/service/MathLearningService.java +++ /dev/null @@ -1,180 +0,0 @@ -package com.personalproject.service; - -import com.personalproject.auth.AccountRepository; -import com.personalproject.auth.EmailService; -import com.personalproject.auth.PasswordValidator; -import com.personalproject.generator.QuestionGenerator; -import com.personalproject.model.DifficultyLevel; -import com.personalproject.model.ExamSession; -import com.personalproject.storage.QuestionStorageService; -import java.util.Map; -import java.util.Optional; - -/** - * 为主JavaFX UI提供所有功能的服务类. 此类协调各种服务以为UI提供统一的API. - */ -public final class MathLearningService { - - private final AccountRepository accountRepository; - private final RegistrationService registrationService; - private final ExamService examService; - private final QuestionStorageService storageService; - private final ExamResultService resultService; - - /** - * 创建一个新的数学学习服务. - * - * @param generatorMap 难度级别到题目生成器的映射 - * @param questionGenerationService 题目生成服务 - */ - public MathLearningService( - Map generatorMap, - QuestionGenerationService questionGenerationService) { - this.accountRepository = new AccountRepository(); - this.registrationService = new RegistrationService(accountRepository); - this.examService = new ExamService(generatorMap, questionGenerationService); - this.storageService = new QuestionStorageService(); - this.resultService = new ExamResultService(); - } - - // 注册方法 - - /** - * 通过向提供的电子邮件发送注册码来启动注册过程. - * - * @param username 所需用户名 - * @param email 要发送注册码的电子邮件地址 - * @param difficultyLevel 选择的难度级别 - * @return 如果注册成功启动则返回true,如果电子邮件无效或用户名已存在则返回false - */ - public boolean initiateRegistration(String username, String email, - DifficultyLevel difficultyLevel) { - return registrationService.initiateRegistration(username, email, difficultyLevel); - } - - /** - * 验证用户输入的注册码. - * - * @param username 用户名 - * @param registrationCode 通过电子邮件收到的注册码 - * @return 如果注册码有效则返回true,否则返回false - */ - public boolean verifyRegistrationCode(String username, String registrationCode) { - return registrationService.verifyRegistrationCode(username, registrationCode); - } - - /** - * 在成功验证注册码后为用户设置密码. - * - * @param username 用户名 - * @param password 要设置的密码 - * @return 如果密码设置成功则返回true,如果验证失败或用户不存在则返回false - */ - public boolean setPassword(String username, String password) { - return registrationService.setPassword(username, password); - } - - /** - * 使用用户名和密码验证用户. - * - * @param username 用户名 - * @param password 密码 - * @return 如果验证成功则返回包含用户账户的Optional - */ - public Optional authenticate(String username, - String password) { - return registrationService.authenticate(username, password); - } - - /** - * 检查密码是否符合验证要求. - * - * @param password 要验证的密码 - * @return 如果密码有效则返回true,否则返回false - */ - public static boolean isValidPassword(String password) { - return PasswordValidator.isValidPassword(password); - } - - /** - * 更改现有用户的密码. - * - * @param username 用户名 - * @param oldPassword 当前密码 - * @param newPassword 新密码 - * @return 如果密码更改成功则返回true,如果验证失败或验证失败则返回false - */ - public boolean changePassword(String username, String oldPassword, String newPassword) { - return registrationService.changePassword(username, oldPassword, newPassword); - } - - /** - * 检查电子邮件地址是否具有有效格式. - * - * @param email 要验证的电子邮件地址 - * @return 如果电子邮件格式有效则返回true,否则返回false - */ - public static boolean isValidEmail(String email) { - return EmailService.isValidEmail(email); - } - - /** - * 为指定用户和难度级别创建新的考试会话. - * - * @param username 应试者用户名 - * @param difficultyLevel 考试难度级别 - * @param questionCount 考试中包含的题目数量 - * @return 新的考试会话 - */ - public ExamSession createExamSession(String username, DifficultyLevel difficultyLevel, - int questionCount) { - return examService.createExamSession(username, difficultyLevel, questionCount); - } - - /** - * 将考试结果保存到存储中. - * - * @param examSession 完成的考试会话 - * @return 保存结果文件的路径 - */ - public java.nio.file.Path saveExamResults(ExamSession examSession) { - try { - return storageService.saveExamResults(examSession); - } catch (java.io.IOException e) { - throw new RuntimeException("保存考试结果失败", e); - } - } - - /** - * 处理用户完成考试后的选择. - * - * @param examSession 完成的考试会话 - * @param continueWithSameLevel 是否继续使用相同难度级别 - * @param newDifficultyLevel 更改时的新难度级别(如果不更改则为null) - * @return 表示下一步操作的动作 - */ - public ExamResultService.ExamContinuationAction processExamResult( - ExamSession examSession, boolean continueWithSameLevel, DifficultyLevel newDifficultyLevel) { - return resultService.processExamResult(examSession, continueWithSameLevel, newDifficultyLevel); - } - - /** - * 检查用户是否存在于系统中. - * - * @param username 要检查的用户名 - * @return 如果用户存在则返回true,否则返回false - */ - public boolean userExists(String username) { - return registrationService.userExists(username); - } - - /** - * 按用户名获取用户账户。 - * - * @param username 用户名 - * @return 若找到则返回包含用户账户的 Optional - */ - public Optional getUser(String username) { - return registrationService.getUser(username); - } -} diff --git a/src/com/personalproject/service/QuestionGenerationService.java b/src/com/personalproject/service/QuestionGenerationService.java deleted file mode 100644 index b536a7d..0000000 --- a/src/com/personalproject/service/QuestionGenerationService.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.personalproject.service; - -import com.personalproject.generator.QuestionGenerator; -import com.personalproject.model.DifficultyLevel; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.EnumMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; - -/** - * 负责批量生成题目并避免与历史题库重复. - */ -public final class QuestionGenerationService { - - private static final int MAX_ATTEMPTS = 10_000; - private final Map generators; - private final Random random = new SecureRandom(); - - /** - * 构造函数:复制难度与生成器的映射,保留内部安全副本. - * - * @param generatorMap 难度到题目生成器的外部映射. - */ - public QuestionGenerationService(Map generatorMap) { - generators = new EnumMap<>(DifficultyLevel.class); - generators.putAll(generatorMap); - } - - /** - * 根据指定难度批量生成不与历史题目重复的新题目. - * - * @param level 目标题目难度. - * @param count 需要生成的题目数量. - * @param existingQuestions 已存在的题目集合,用于查重. - * @return 生成的题目列表. - * @throws IllegalArgumentException 当难度未配置时抛出. - * @throws IllegalStateException 当达到最大尝试次数仍无法生成足够题目时抛出. - */ - public List generateUniqueQuestions( - DifficultyLevel level, int count, Set existingQuestions) { - QuestionGenerator generator = generators.get(level); - if (generator == null) { - throw new IllegalArgumentException("Unsupported difficulty level: " + level); - } - Set produced = new HashSet<>(); - List results = new ArrayList<>(); - int attempts = 0; - while (results.size() < count) { - if (attempts >= MAX_ATTEMPTS) { - throw new IllegalStateException("Unable to generate enough unique questions."); - } - attempts++; - String question = generator.generateQuestion(random).trim(); - if (question.isEmpty()) { - continue; - } - if (existingQuestions.contains(question)) { - continue; - } - if (!produced.add(question)) { - continue; - } - results.add(question); - } - return results; - } -} diff --git a/src/com/personalproject/service/RegistrationService.java b/src/com/personalproject/service/RegistrationService.java deleted file mode 100644 index 024f2bf..0000000 --- a/src/com/personalproject/service/RegistrationService.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.personalproject.service; - -import com.personalproject.auth.AccountRepository; -import com.personalproject.auth.EmailService; -import com.personalproject.auth.PasswordValidator; -import com.personalproject.model.DifficultyLevel; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -/** - * 用于处理用户注册和身份验证过程的服务类. - */ -public final class RegistrationService { - - private final AccountRepository accountRepository; - private final Map pendingRegistrations; - private final Map registrationAttempts; - - /** - * 创建新的注册服务. - * - * @param accountRepository 使用的账户存储库 - */ - public RegistrationService(AccountRepository accountRepository) { - this.accountRepository = accountRepository; - this.pendingRegistrations = new ConcurrentHashMap<>(); - this.registrationAttempts = new ConcurrentHashMap<>(); - } - - /** - * 通过向提供的电子邮件发送注册码来启动注册过程. - * - * @param username 所需用户名 - * @param email 要发送注册码的电子邮件地址 - * @param difficultyLevel 选择的难度级别 - * @return 注册成功启动则返回true,如果电子邮件无效或用户名已存在则返回false - */ - public boolean initiateRegistration(String username, String email, - DifficultyLevel difficultyLevel) { - if (!EmailService.isValidEmail(email)) { - return false; - } - - if (!accountRepository.registerUser(username, email, difficultyLevel)) { - return false; // 用户名已存在或邮箱已注册 - } - - String registrationCode = EmailService.generateRegistrationCode(); - pendingRegistrations.put(username, registrationCode); - registrationAttempts.put(username, 0); - - return EmailService.sendRegistrationCode(email, registrationCode); - } - - /** - * 通过验证注册码完成注册. - * - * @param username 用户名 - * @param registrationCode 通过电子邮件收到的注册码 - * @return 注册码有效则返回true,否则返回false - */ - public boolean verifyRegistrationCode(String username, String registrationCode) { - String storedCode = pendingRegistrations.get(username); - if (storedCode == null || !storedCode.equals(registrationCode)) { - // 跟踪失败尝试 - int attempts = registrationAttempts.getOrDefault(username, 0); - attempts++; - registrationAttempts.put(username, attempts); - - if (attempts >= 3) { - // 如果失败次数过多,则删除用户 - pendingRegistrations.remove(username); - registrationAttempts.remove(username); - return false; - } - return false; - } - - // 有效码,从待处理列表中移除 - pendingRegistrations.remove(username); - registrationAttempts.remove(username); - return true; - } - - /** - * 在成功验证注册码后为用户设置密码. - * - * @param username 用户名 - * @param password 要设置的密码 - * @return 密码设置成功则返回true,如果验证失败或用户不存在则返回false - */ - public boolean setPassword(String username, String password) { - if (!PasswordValidator.isValidPassword(password)) { - return false; - } - - return accountRepository.setPassword(username, password); - } - - /** - * 更改现有用户的密码. - * - * @param username 用户名 - * @param oldPassword 当前密码 - * @param newPassword 新密码 - * @return 密码更改成功则返回true,如果验证失败或身份验证失败则返回false - */ - public boolean changePassword(String username, String oldPassword, String newPassword) { - if (!PasswordValidator.isValidPassword(newPassword)) { - return false; - } - - return accountRepository.changePassword(username, oldPassword, newPassword); - } - - /** - * 使用用户名和密码验证用户. - * - * @param username 用户名 - * @param password 密码 - * @return 验证成功则返回包含用户账户的Optional - */ - public Optional authenticate(String username, - String password) { - return accountRepository.authenticate(username, password); - } - - /** - * 检查用户是否存在于系统中. - * - * @param username 要检查的用户名 - * @return 用户存在则返回true,否则返回false - */ - public boolean userExists(String username) { - return accountRepository.userExists(username); - } - - /** - * 按用户名获取用户账户。 - * - * @param username 用户名 - * @return 若找到则返回包含用户账户的 Optional - */ - public Optional getUser(String username) { - return accountRepository.getUser(username); - } -} diff --git a/src/com/personalproject/storage/QuestionStorageService.java b/src/com/personalproject/storage/QuestionStorageService.java deleted file mode 100644 index 4fc9870..0000000 --- a/src/com/personalproject/storage/QuestionStorageService.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.personalproject.storage; - -import com.personalproject.model.ExamSession; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Stream; - -/** - * 负责保存生成的题目并维护查重信息. - */ -public final class QuestionStorageService { - - private static final String BASE_DIRECTORY = "user_data"; - private static final String QUESTIONS_SUBDIR = "questions"; - private static final String RESULTS_SUBDIR = "results"; - private static final DateTimeFormatter FORMATTER = - DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"); - - /** - * 加载指定用户历史生成的题目,用于后续查重. - * - * @param username 用户名. - * @return 历史题目的去重集合. - * @throws IOException 当读取文件时发生 I/O 错误. - */ - public Set loadExistingQuestions(String username) throws IOException { - Path accountDirectory = getQuestionsDirectory(username); - Set questions = new HashSet<>(); - if (!Files.exists(accountDirectory)) { - return questions; - } - try (Stream paths = Files.list(accountDirectory)) { - paths - .filter(path -> path.getFileName().toString().endsWith(".txt")) - .sorted() - .forEach(path -> readQuestionsFromFile(path, questions)); - } - return questions; - } - - /** - * 将新生成的题目保存到用户目录,并返回生成的文件路径. - * - * @param username 用户名. - * @param questions 待保存的题目列表. - * @return 保存题目的文件路径. - * @throws IOException 当写入文件失败时抛出. - */ - public Path saveQuestions(String username, List questions) throws IOException { - Path accountDirectory = getQuestionsDirectory(username); - Files.createDirectories(accountDirectory); - String fileName = FORMATTER.format(LocalDateTime.now()) + ".txt"; - Path outputFile = accountDirectory.resolve(fileName); - StringBuilder builder = new StringBuilder(); - for (int index = 0; index < questions.size(); index++) { - String question = questions.get(index); - builder - .append(index + 1) - .append(". ") - .append(question) - .append(System.lineSeparator()) - .append(System.lineSeparator()); - } - Files.writeString( - outputFile, builder.toString(), StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW); - return outputFile; - } - - /** - * 保存用户的考试结果. - * - * @param examSession 包含结果的考试会话 - * @return 保存结果文件的路径 - * @throws IOException 如果写入文件失败 - */ - public Path saveExamResults(ExamSession examSession) throws IOException { - Path resultsDirectory = getResultsDirectory(examSession.getUsername()); - Files.createDirectories(resultsDirectory); - - StringBuilder builder = new StringBuilder(); - builder.append("考试结果报告").append(System.lineSeparator()); - builder.append("用户名: ").append(examSession.getUsername()).append(System.lineSeparator()); - builder.append("难度: ").append(examSession.getDifficultyLevel().getDisplayName()) - .append(System.lineSeparator()); - builder.append("开始时间: ").append(examSession.getStartTime()).append(System.lineSeparator()); - builder.append("题目数量: ").append(examSession.getQuestions().size()) - .append(System.lineSeparator()); - builder.append("得分: ").append(String.format("%.2f", examSession.calculateScore())).append("%") - .append(System.lineSeparator()); - builder.append(System.lineSeparator()); - - // 添加逐题结果 - for (int i = 0; i < examSession.getQuestions().size(); i++) { - var question = examSession.getQuestions().get(i); - int userAnswer = examSession.getUserAnswer(i); - boolean isCorrect = question.isAnswerCorrect(userAnswer); - - builder.append("题目 ").append(i + 1).append(": ").append(question.getQuestionText()) - .append(System.lineSeparator()); - builder.append("您的答案: ").append(userAnswer == -1 ? "未回答" : - (userAnswer < question.getOptions().size() ? question.getOptions().get(userAnswer) - : "无效")).append(System.lineSeparator()); - builder.append("正确答案: ").append( - question.getOptions().get(question.getCorrectAnswerIndex())) - .append(System.lineSeparator()); - builder.append("结果: ").append(isCorrect ? "正确" : "错误").append(System.lineSeparator()); - builder.append(System.lineSeparator()); - } - - String fileName = "exam_result_" + FORMATTER.format(LocalDateTime.now()) + ".txt"; - Path outputFile = resultsDirectory.resolve(fileName); - - Files.writeString( - outputFile, builder.toString(), StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW); - return outputFile; - } - - private Path getAccountDirectory(String username) { - return Paths.get(BASE_DIRECTORY, username); - } - - private Path getQuestionsDirectory(String username) { - return getAccountDirectory(username).resolve(QUESTIONS_SUBDIR); - } - - private Path getResultsDirectory(String username) { - return getAccountDirectory(username).resolve(RESULTS_SUBDIR); - } - - private void readQuestionsFromFile(Path path, Set questions) { - try { - List lines = Files.readAllLines(path, StandardCharsets.UTF_8); - for (String line : lines) { - String trimmed = line.trim(); - if (trimmed.isEmpty()) { - continue; - } - int dotIndex = trimmed.indexOf('.'); - if (dotIndex >= 0 && dotIndex + 1 < trimmed.length()) { - String question = trimmed.substring(dotIndex + 1).trim(); - if (!question.isEmpty()) { - questions.add(question); - } - } else { - questions.add(trimmed); - } - } - } catch (IOException exception) { - System.err.println( - "读取题目文件失败:" + path + ",原因:" + exception.getMessage() + ",将跳过该文件."); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/personalproject/auth/AccountRepository.java b/src/main/java/com/personalproject/auth/AccountRepository.java index 5bb193d..2a2f665 100644 --- a/src/main/java/com/personalproject/auth/AccountRepository.java +++ b/src/main/java/com/personalproject/auth/AccountRepository.java @@ -1,18 +1,34 @@ package com.personalproject.auth; import com.personalproject.model.DifficultyLevel; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.time.LocalDateTime; -import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; /** * 存放用户账号并提供认证和注册查询能力. */ public final class AccountRepository { - private final Map accounts = new HashMap<>(); - private final Map registrationCodes = new HashMap<>(); + private static final Path ACCOUNTS_DIRECTORY = Paths.get("user_data", "accounts"); + private static final String FILE_EXTENSION = ".properties"; + + private final Map accounts = new ConcurrentHashMap<>(); + + public AccountRepository() { + loadAccounts(); + } /** * 根据用户名与密码查找匹配的账号. @@ -25,11 +41,13 @@ public final class AccountRepository { if (username == null || password == null) { return Optional.empty(); } - UserAccount account = accounts.get(username.trim()); + String normalizedUsername = username.trim(); + String normalizedPassword = password.trim(); + UserAccount account = accounts.get(normalizedUsername); if (account == null || !account.isRegistered()) { return Optional.empty(); } - if (!account.password().equals(password.trim())) { + if (!account.password().equals(hashPassword(normalizedPassword))) { return Optional.empty(); } return Optional.of(account); @@ -43,29 +61,38 @@ public final class AccountRepository { * @param difficultyLevel 选择的难度级别 * @return 若注册成功则返回 true,若用户名已存在则返回 false */ - public boolean registerUser(String username, String email, DifficultyLevel difficultyLevel) { - String trimmedUsername = username.trim(); - String trimmedEmail = email.trim(); + public synchronized boolean registerUser(String username, String email, + DifficultyLevel difficultyLevel) { + if (username == null || email == null || difficultyLevel == null) { + return false; + } - if (accounts.containsKey(trimmedUsername)) { - return false; // 用户名已存在 + String normalizedUsername = username.trim(); + String normalizedEmail = email.trim(); + if (normalizedUsername.isEmpty() || normalizedEmail.isEmpty()) { + return false; } - // 检查邮箱是否已被其他账号使用 - for (UserAccount account : accounts.values()) { - if (account.email().equals(trimmedEmail) && account.isRegistered()) { - return false; // 邮箱已注册 - } + UserAccount existing = accounts.get(normalizedUsername); + if (existing != null && existing.isRegistered()) { + return false; } - UserAccount newAccount = new UserAccount( - trimmedUsername, - trimmedEmail, - "", // 初始为空密码 + if (isEmailInUse(normalizedEmail, normalizedUsername)) { + return false; + } + + LocalDateTime registrationDate = existing != null ? existing.registrationDate() : LocalDateTime.now(); + UserAccount account = new UserAccount( + normalizedUsername, + normalizedEmail, + "", difficultyLevel, - LocalDateTime.now(), - false); // 在设置密码前视为未注册 - accounts.put(trimmedUsername, newAccount); + registrationDate, + false); + + accounts.put(normalizedUsername, account); + persistAccount(account); return true; } @@ -76,20 +103,25 @@ public final class AccountRepository { * @param password 要设置的密码 * @return 设置成功返回 true,若用户不存在则返回 false */ - public boolean setPassword(String username, String password) { - UserAccount account = accounts.get(username.trim()); - if (account == null) { + public synchronized boolean setPassword(String username, String password) { + if (username == null || password == null) { + return false; + } + String normalizedUsername = username.trim(); + UserAccount account = accounts.get(normalizedUsername); + if (account == null || account.isRegistered()) { return false; } UserAccount updatedAccount = new UserAccount( account.username(), account.email(), - password, + hashPassword(password.trim()), account.difficultyLevel(), account.registrationDate(), - true); // 设置密码后标记为已注册 - accounts.put(username.trim(), updatedAccount); + true); + accounts.put(normalizedUsername, updatedAccount); + persistAccount(updatedAccount); return true; } @@ -101,23 +133,50 @@ public final class AccountRepository { * @param newPassword 新密码 * @return 修改成功返回 true,若旧密码错误或用户不存在则返回 false */ - public boolean changePassword(String username, String oldPassword, String newPassword) { - UserAccount account = accounts.get(username.trim()); - if (account == null || !account.password().equals(oldPassword) || !account.isRegistered()) { + public synchronized boolean changePassword(String username, String oldPassword, + String newPassword) { + if (username == null || oldPassword == null || newPassword == null) { + return false; + } + String normalizedUsername = username.trim(); + UserAccount account = accounts.get(normalizedUsername); + if (account == null || !account.isRegistered()) { + return false; + } + + if (!account.password().equals(hashPassword(oldPassword.trim()))) { return false; } UserAccount updatedAccount = new UserAccount( account.username(), account.email(), - newPassword, + hashPassword(newPassword.trim()), account.difficultyLevel(), account.registrationDate(), true); - accounts.put(username.trim(), updatedAccount); + accounts.put(normalizedUsername, updatedAccount); + persistAccount(updatedAccount); return true; } + /** + * 移除未完成注册的用户,便于重新注册。 + * + * @param username 待移除的用户名 + */ + public synchronized void removeUnverifiedUser(String username) { + if (username == null) { + return; + } + String normalizedUsername = username.trim(); + UserAccount account = accounts.get(normalizedUsername); + if (account != null && !account.isRegistered()) { + accounts.remove(normalizedUsername); + deleteAccountFile(normalizedUsername); + } + } + /** * 检查用户是否存在。 * @@ -125,6 +184,9 @@ public final class AccountRepository { * @return 若存在则返回 true,否则返回 false */ public boolean userExists(String username) { + if (username == null) { + return false; + } return accounts.containsKey(username.trim()); } @@ -135,6 +197,124 @@ public final class AccountRepository { * @return 若找到则返回包含用户账户的 Optional */ public Optional getUser(String username) { + if (username == null) { + return Optional.empty(); + } return Optional.ofNullable(accounts.get(username.trim())); } + + private void loadAccounts() { + try { + if (!Files.exists(ACCOUNTS_DIRECTORY)) { + Files.createDirectories(ACCOUNTS_DIRECTORY); + return; + } + + try (var paths = Files.list(ACCOUNTS_DIRECTORY)) { + paths + .filter(path -> path.getFileName().toString().endsWith(FILE_EXTENSION)) + .forEach(this::loadAccountFromFile); + } + } catch (IOException exception) { + throw new IllegalStateException("加载账户数据失败", exception); + } + } + + private void loadAccountFromFile(Path file) { + Properties properties = new Properties(); + try (Reader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { + properties.load(reader); + } catch (IOException exception) { + System.err.println("读取账户文件失败: " + file + ",原因: " + exception.getMessage()); + return; + } + + String username = properties.getProperty("username"); + if (username == null || username.isBlank()) { + return; + } + + String email = properties.getProperty("email", ""); + String passwordHash = properties.getProperty("passwordHash", ""); + String difficultyValue = properties.getProperty("difficulty", DifficultyLevel.PRIMARY.name()); + String registrationDateValue = properties.getProperty("registrationDate"); + String registeredValue = properties.getProperty("registered", "false"); + + try { + DifficultyLevel difficultyLevel = DifficultyLevel.valueOf(difficultyValue); + LocalDateTime registrationDate = registrationDateValue == null + ? LocalDateTime.now() + : LocalDateTime.parse(registrationDateValue); + boolean registered = Boolean.parseBoolean(registeredValue); + UserAccount account = new UserAccount( + username, + email, + passwordHash, + difficultyLevel, + registrationDate, + registered); + accounts.put(username, account); + } catch (Exception exception) { + System.err.println("解析账户文件失败: " + file + ",原因: " + exception.getMessage()); + } + } + + private void persistAccount(UserAccount account) { + Properties properties = new Properties(); + properties.setProperty("username", account.username()); + properties.setProperty("email", account.email()); + properties.setProperty("passwordHash", account.password()); + properties.setProperty("difficulty", account.difficultyLevel().name()); + properties.setProperty("registrationDate", account.registrationDate().toString()); + properties.setProperty("registered", Boolean.toString(account.isRegistered())); + + Path targetFile = accountFile(account.username()); + try { + if (!Files.exists(ACCOUNTS_DIRECTORY)) { + Files.createDirectories(ACCOUNTS_DIRECTORY); + } + try (Writer writer = Files.newBufferedWriter(targetFile, StandardCharsets.UTF_8)) { + properties.store(writer, "User account data"); + } + } catch (IOException exception) { + throw new IllegalStateException("保存账户数据失败: " + account.username(), exception); + } + } + + private void deleteAccountFile(String username) { + Path file = accountFile(username); + try { + Files.deleteIfExists(file); + } catch (IOException exception) { + System.err.println("删除账户文件失败: " + file + ",原因: " + exception.getMessage()); + } + } + + private boolean isEmailInUse(String email, String currentUsername) { + return accounts.values().stream() + .anyMatch(account -> account.email().equalsIgnoreCase(email) + && !account.username().equals(currentUsername)); + } + + private Path accountFile(String username) { + return ACCOUNTS_DIRECTORY.resolve(username + FILE_EXTENSION); + } + + private String hashPassword(String password) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashBytes = digest.digest(password.getBytes(StandardCharsets.UTF_8)); + return toHex(hashBytes); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("当前运行环境不支持SHA-256算法", exception); + } + } + + private String toHex(byte[] bytes) { + StringBuilder builder = new StringBuilder(bytes.length * 2); + for (byte value : bytes) { + builder.append(String.format("%02x", value)); + } + return builder.toString(); + } } diff --git a/src/main/java/com/personalproject/auth/EmailService.java b/src/main/java/com/personalproject/auth/EmailService.java index 6ae2b1c..4d357bf 100644 --- a/src/main/java/com/personalproject/auth/EmailService.java +++ b/src/main/java/com/personalproject/auth/EmailService.java @@ -1,6 +1,29 @@ package com.personalproject.auth; +import jakarta.mail.Authenticator; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.PasswordAuthentication; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; /** * 用于发送带有注册码的电子邮件的工具类。 @@ -10,6 +33,13 @@ public final class EmailService { private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; private static final int CODE_LENGTH = 6; private static final Random RANDOM = new Random(); + private static final Path OUTBOX_DIRECTORY = Paths.get("user_data", "emails"); + private static final String CONFIG_RESOURCE = "email-config.properties"; + private static final DateTimeFormatter DATE_TIME_FORMATTER = + DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"); + private static final Map LAST_CODES = new ConcurrentHashMap<>(); + private static final AtomicReference CONFIG_CACHE = new AtomicReference<>(); + private static final String DEFAULT_SUBJECT = "数学学习软件注册验证码"; private EmailService() { // 防止实例化此工具类 @@ -29,17 +59,36 @@ public final class EmailService { } /** - * 将注册码发送到指定邮箱。真实实现中会连接邮件服务器。 + * 将注册码发送到指定邮箱。 * * @param email 收件人邮箱 * @param registrationCode 要发送的注册码 - * @return 发送成功返回 true(在此模拟实现中始终为 true) + * @return 发送成功返回 true */ public static boolean sendRegistrationCode(String email, String registrationCode) { - // 真实实现会连接邮件服务器 - // 模拟实现仅在控制台打印 - System.out.println("Sending registration code " + registrationCode + " to " + email); - return true; + try { + MailConfiguration configuration = loadConfiguration(); + sendEmail(email, registrationCode, configuration); + persistMessage(email, registrationCode); + LAST_CODES.put(email.trim().toLowerCase(), registrationCode); + return true; + } catch (Exception exception) { + System.err.println("发送验证码失败: " + exception.getMessage()); + return false; + } + } + + /** + * 获取最近一次发送到指定邮箱的验证码(便于调试或测试)。 + * + * @param email 收件人邮箱 + * @return 若存在则返回验证码 + */ + public static Optional getLastSentRegistrationCode(String email) { + if (email == null) { + return Optional.empty(); + } + return Optional.ofNullable(LAST_CODES.get(email.trim().toLowerCase())); } /** @@ -53,9 +102,129 @@ public final class EmailService { return false; } - // 使用正则表达式进行简单邮箱校验 String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@" + "(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"; return email.matches(emailRegex); } + + private static MailConfiguration loadConfiguration() throws IOException { + MailConfiguration cached = CONFIG_CACHE.get(); + if (cached != null) { + return cached; + } + + synchronized (CONFIG_CACHE) { + cached = CONFIG_CACHE.get(); + if (cached != null) { + return cached; + } + + Properties properties = new Properties(); + try (InputStream inputStream = + EmailService.class.getClassLoader().getResourceAsStream(CONFIG_RESOURCE)) { + if (inputStream == null) { + throw new IllegalStateException( + "类路径下缺少邮箱配置文件: " + CONFIG_RESOURCE + + ",请在 src/main/resources 下提供该文件"); + } + try (InputStreamReader reader = + new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { + properties.load(reader); + } + } + + String username = require(properties, "mail.username"); + String password = require(properties, "mail.password"); + String from = properties.getProperty("mail.from", username); + String subject = properties.getProperty("mail.subject", DEFAULT_SUBJECT); + + Properties smtpProperties = new Properties(); + for (Map.Entry entry : properties.entrySet()) { + String key = entry.getKey().toString(); + if (key.startsWith("mail.smtp.")) { + smtpProperties.put(key, entry.getValue()); + } + } + + if (!smtpProperties.containsKey("mail.smtp.host")) { + throw new IllegalStateException("邮箱配置缺少 mail.smtp.host"); + } + if (!smtpProperties.containsKey("mail.smtp.port")) { + throw new IllegalStateException("邮箱配置缺少 mail.smtp.port"); + } + + smtpProperties.putIfAbsent("mail.smtp.auth", "true"); + smtpProperties.putIfAbsent("mail.smtp.starttls.enable", "true"); + + MailConfiguration configuration = + new MailConfiguration(smtpProperties, username, password, from, subject); + CONFIG_CACHE.set(configuration); + return configuration; + } + } + + private static void sendEmail( + String recipientEmail, String registrationCode, MailConfiguration configuration) + throws MessagingException { + Session session = Session.getInstance(configuration.smtpProperties(), new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(configuration.username(), configuration.password()); + } + }); + + MimeMessage message = new MimeMessage(session); + message.setFrom(new InternetAddress(configuration.from())); + message.setRecipients( + Message.RecipientType.TO, + InternetAddress.parse(recipientEmail, false)); + message.setSubject(configuration.subject()); + + String body = "您好,您的注册码为:" + registrationCode + System.lineSeparator() + + "请在10分钟内使用该验证码完成注册。"; + message.setText(body, StandardCharsets.UTF_8.name()); + + Transport.send(message); + } + + private static void persistMessage(String email, String registrationCode) throws IOException { + if (!Files.exists(OUTBOX_DIRECTORY)) { + Files.createDirectories(OUTBOX_DIRECTORY); + } + + String sanitizedEmail = sanitizeEmail(email); + String timestamp = DATE_TIME_FORMATTER.format(LocalDateTime.now()); + Path messageFile = OUTBOX_DIRECTORY.resolve(sanitizedEmail + "_" + timestamp + ".txt"); + StringBuilder content = new StringBuilder(); + content.append("收件人: ").append(email).append(System.lineSeparator()); + content.append("注册码: ").append(registrationCode).append(System.lineSeparator()); + content.append("发送时间: ").append(LocalDateTime.now()).append(System.lineSeparator()); + + Files.writeString( + messageFile, + content.toString(), + StandardCharsets.UTF_8, + StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE); + } + + private static String require(Properties properties, String key) { + String value = properties.getProperty(key); + if (value == null || value.trim().isEmpty()) { + throw new IllegalStateException("邮箱配置缺少必要字段: " + key); + } + return value.trim(); + } + + private static String sanitizeEmail(String email) { + return email.replaceAll("[^a-zA-Z0-9._-]", "_"); + } + + private record MailConfiguration( + Properties smtpProperties, + String username, + String password, + String from, + String subject) { + } } diff --git a/src/main/java/com/personalproject/auth/PasswordValidator.java b/src/main/java/com/personalproject/auth/PasswordValidator.java index 6b4ab91..53740d8 100644 --- a/src/main/java/com/personalproject/auth/PasswordValidator.java +++ b/src/main/java/com/personalproject/auth/PasswordValidator.java @@ -28,6 +28,10 @@ public final class PasswordValidator { if (password == null) { return false; } - return PASSWORD_PATTERN.matcher(password).matches(); + String normalized = password.trim(); + if (normalized.isEmpty()) { + return false; + } + return PASSWORD_PATTERN.matcher(normalized).matches(); } } diff --git a/src/main/java/com/personalproject/generator/PrimaryQuestionGenerator.java b/src/main/java/com/personalproject/generator/PrimaryQuestionGenerator.java index 622bd0f..8041330 100644 --- a/src/main/java/com/personalproject/generator/PrimaryQuestionGenerator.java +++ b/src/main/java/com/personalproject/generator/PrimaryQuestionGenerator.java @@ -5,8 +5,10 @@ import com.personalproject.service.MathExpressionEvaluator; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Random; +import java.util.Set; /** * 生成包含基础四则运算的小学难度题目表达式. @@ -63,18 +65,14 @@ public final class PrimaryQuestionGenerator implements QuestionGenerator, QuizQu correctAnswer = 0.0; } - // 生成选项 List options = generateOptions(correctAnswer, random); - - // 随机选择正确答案索引 - int correctAnswerIndex = random.nextInt(options.size()); - - // 确保正确答案在选项中 - String correctOption = String.format("%.2f", correctAnswer); - String oldOption = options.set(correctAnswerIndex, correctOption); - - // 将被替换的选项加回去以保持 4 个选项 - options.add(oldOption); + String correctOption = formatOption(correctAnswer); + int correctAnswerIndex = options.indexOf(correctOption); + if (correctAnswerIndex < 0) { + // 兜底逻辑:确保正确答案存在于选项中 + options.set(0, correctOption); + correctAnswerIndex = 0; + } return new QuizQuestion(expression, options, correctAnswerIndex); } @@ -83,27 +81,40 @@ public final class PrimaryQuestionGenerator implements QuestionGenerator, QuizQu * 生成选择题选项 */ private List generateOptions(double correctAnswer, Random random) { - List options = new ArrayList<>(); - - // 将正确答案加入选项 - options.add(String.format("%.2f", correctAnswer)); + String correctOption = formatOption(correctAnswer); + Set optionSet = new LinkedHashSet<>(); + optionSet.add(correctOption); - // 加入错误选项 - for (int i = 0; i < OPTIONS_COUNT - 1; i++) { - double incorrectAnswer = correctAnswer + (random.nextGaussian() * 10); // 添加随机偏移 - if (Math.abs(incorrectAnswer - correctAnswer) < 0.1) { // 确保与正确答案不同 - incorrectAnswer += 1.5; + double scale = Math.max(Math.abs(correctAnswer) * 0.2, 1.0); + int attempts = 0; + while (optionSet.size() < OPTIONS_COUNT && attempts < 100) { + double delta = random.nextGaussian() * scale; + if (Math.abs(delta) < 0.5) { + double direction = random.nextBoolean() ? 1 : -1; + delta = direction * (0.5 + random.nextDouble()) * scale; + } + double candidate = correctAnswer + delta; + if (Double.isNaN(candidate) || Double.isInfinite(candidate)) { + attempts++; + continue; } - options.add(String.format("%.2f", incorrectAnswer)); + optionSet.add(formatOption(candidate)); + attempts++; } - // 打乱顺序以随机化正确答案位置 + double step = Math.max(scale, 1.0); + while (optionSet.size() < OPTIONS_COUNT) { + double candidate = correctAnswer + step * optionSet.size(); + optionSet.add(formatOption(candidate)); + step += 1.0; + } + + List options = new ArrayList<>(optionSet); Collections.shuffle(options, random); - - // 在打乱后查找正确答案的索引 - String correctAnswerStr = String.format("%.2f", correctAnswer); - int correctIndex = options.indexOf(correctAnswerStr); - return options; } + + private String formatOption(double value) { + return String.format("%.2f", value); + } } diff --git a/src/main/java/com/personalproject/model/ExamSession.java b/src/main/java/com/personalproject/model/ExamSession.java index 6b8f918..a57a808 100644 --- a/src/main/java/com/personalproject/model/ExamSession.java +++ b/src/main/java/com/personalproject/model/ExamSession.java @@ -2,6 +2,7 @@ package com.personalproject.model; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -38,11 +39,7 @@ public final class ExamSession { this.username = username; this.difficultyLevel = difficultyLevel; this.questions = List.copyOf(questions); // 题目的不可变副本 - this.userAnswers = new ArrayList<>(); - // 使用 -1 初始化用户答案(表示未选择) - for (int i = 0; i < questions.size(); i++) { - userAnswers.add(-1); - } + this.userAnswers = new ArrayList<>(Collections.nCopies(questions.size(), -1)); this.startTime = LocalDateTime.now(); this.currentQuestionIndex = 0; } @@ -101,7 +98,8 @@ public final class ExamSession { if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) { throw new IllegalStateException("No valid question at current index"); } - if (answerIndex < 0 || answerIndex > questions.get(currentQuestionIndex).getOptions().size()) { + int optionCount = questions.get(currentQuestionIndex).getOptions().size(); + if (answerIndex < 0 || answerIndex >= optionCount) { throw new IllegalArgumentException("Invalid answer index"); } userAnswers.set(currentQuestionIndex, answerIndex); @@ -139,7 +137,7 @@ public final class ExamSession { * @return 若考试已完成则返回 true,否则返回 false */ public boolean isComplete() { - return currentQuestionIndex >= questions.size() - 1; + return userAnswers.stream().allMatch(answer -> answer != -1); } /** diff --git a/src/main/java/com/personalproject/service/ExamService.java b/src/main/java/com/personalproject/service/ExamService.java index 392a83a..f72dcd7 100644 --- a/src/main/java/com/personalproject/service/ExamService.java +++ b/src/main/java/com/personalproject/service/ExamService.java @@ -4,11 +4,16 @@ import com.personalproject.generator.QuestionGenerator; import com.personalproject.model.DifficultyLevel; import com.personalproject.model.ExamSession; import com.personalproject.model.QuizQuestion; +import com.personalproject.storage.QuestionStorageService; +import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.EnumMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Random; +import java.util.Set; /** * 用于管理考试会话和生成测验题目的服务类. @@ -19,19 +24,23 @@ public final class ExamService { private final Map generators; private final Random random = new Random(); private final QuestionGenerationService questionGenerationService; + private final QuestionStorageService questionStorageService; /** * 创建新的考试服务. * * @param generatorMap 难度级别到题目生成器的映射 * @param questionGenerationService 题目生成服务 + * @param questionStorageService 题目存储服务 */ public ExamService( Map generatorMap, - QuestionGenerationService questionGenerationService) { + QuestionGenerationService questionGenerationService, + QuestionStorageService questionStorageService) { this.generators = new EnumMap<>(DifficultyLevel.class); this.generators.putAll(generatorMap); this.questionGenerationService = questionGenerationService; + this.questionStorageService = questionStorageService; } /** @@ -49,27 +58,25 @@ public final class ExamService { } // 根据难度级别生成题目 - List generatedQuestions = new ArrayList<>(); - QuestionGenerator generator = generators.get(difficultyLevel); - if (generator == null) { + if (!generators.containsKey(difficultyLevel)) { throw new IllegalArgumentException("找不到难度级别的生成器: " + difficultyLevel); } - for (int i = 0; i < questionCount; i++) { - String question = generator.generateQuestion(random); - generatedQuestions.add(question); - } + try { + Set existingQuestions = questionStorageService.loadExistingQuestions(username); + List uniqueQuestions = questionGenerationService.generateUniqueQuestions( + difficultyLevel, questionCount, existingQuestions); - // 将字符串题目转换为带有选项和答案的QuizQuestion对象 - List quizQuestions = new ArrayList<>(); - for (String questionText : generatedQuestions) { - List options = generateOptions(questionText); - int correctAnswerIndex = generateCorrectAnswerIndex(options.size()); - QuizQuestion quizQuestion = new QuizQuestion(questionText, options, correctAnswerIndex); - quizQuestions.add(quizQuestion); - } + List quizQuestions = new ArrayList<>(); + for (String questionText : uniqueQuestions) { + quizQuestions.add(buildQuizQuestion(questionText)); + } - return new ExamSession(username, difficultyLevel, quizQuestions); + questionStorageService.saveQuestions(username, uniqueQuestions); + return new ExamSession(username, difficultyLevel, quizQuestions); + } catch (IOException exception) { + throw new IllegalStateException("生成考试题目失败", exception); + } } /** @@ -88,53 +95,64 @@ public final class ExamService { return new ExamSession(username, difficultyLevel, questions); } - /** - * 为给定题目生成多项选择选项. 这是一个模拟实现 - 在实际系统中,选项会根据题目生成. - * - * @param questionText 题目文本 - * @return 答案选项列表 - */ - private List generateOptions(String questionText) { - List options = new ArrayList<>(); - // 为每个题目生成4个选项 + private QuizQuestion buildQuizQuestion(String questionText) { try { - // 尝试将数学表达式作为正确答案进行评估 double correctAnswer = MathExpressionEvaluator.evaluate(questionText); + String formattedCorrectAnswer = formatAnswer(correctAnswer); + List options = buildOptions(correctAnswer, formattedCorrectAnswer); + int correctAnswerIndex = options.indexOf(formattedCorrectAnswer); + if (correctAnswerIndex < 0) { + options.set(0, formattedCorrectAnswer); + correctAnswerIndex = 0; + } + return new QuizQuestion(questionText, options, correctAnswerIndex); + } catch (RuntimeException exception) { + List fallbackOptions = new ArrayList<>(); + for (int i = 1; i <= OPTIONS_COUNT; i++) { + fallbackOptions.add("选项" + i); + } + return new QuizQuestion(questionText, fallbackOptions, 0); + } + } - // 创建正确答案选项 - options.add(String.format("%.2f", correctAnswer)); - - // 生成3个错误选项 - for (int i = 0; i < OPTIONS_COUNT - 1; i++) { - double incorrectAnswer = correctAnswer + (random.nextGaussian() * 10); // 添加一些随机偏移 - if (Math.abs(incorrectAnswer - correctAnswer) < 0.1) { // 确保不同 - incorrectAnswer += 1.5; - } - options.add(String.format("%.2f", incorrectAnswer)); + private List buildOptions(double correctAnswer, String formattedCorrectAnswer) { + Set optionSet = new LinkedHashSet<>(); + optionSet.add(formattedCorrectAnswer); + + int attempts = 0; + while (optionSet.size() < OPTIONS_COUNT && attempts < 100) { + double offset = generateOffset(correctAnswer); + double candidateValue = correctAnswer + offset; + if (Double.isNaN(candidateValue) || Double.isInfinite(candidateValue)) { + attempts++; + continue; } - } catch (Exception e) { - // 如果评估失败,创建虚拟选项 - for (int i = 0; i < OPTIONS_COUNT; i++) { - options.add("选项 " + (i + 1)); + String candidate = formatAnswer(candidateValue); + if (!candidate.equals(formattedCorrectAnswer)) { + optionSet.add(candidate); } + attempts++; } - // 随机打乱选项以随机化正确答案的位置 - java.util.Collections.shuffle(options, random); + while (optionSet.size() < OPTIONS_COUNT) { + optionSet.add(formatAnswer(correctAnswer + optionSet.size() * 1.5)); + } - // 找到打乱后的正确答案索引 - // 对于此模拟实现,我们将返回第一个选项(索引0)作为正确答案 - // 实际实现将跟踪正确答案 + List options = new ArrayList<>(optionSet); + Collections.shuffle(options, random); return options; } - /** - * 为给定选项数量生成随机正确答案索引. - * - * @param optionCount 选项数量 - * @return 0到optionCount-1之间的随机索引 - */ - private int generateCorrectAnswerIndex(int optionCount) { - return random.nextInt(optionCount); + private double generateOffset(double base) { + double scale = Math.max(1.0, Math.abs(base) / 2.0); + double offset = random.nextGaussian() * scale; + if (Math.abs(offset) < 0.5) { + offset += offset >= 0 ? 1.5 : -1.5; + } + return offset; + } + + private String formatAnswer(double value) { + return String.format("%.2f", value); } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/service/MathExpressionEvaluator.java b/src/main/java/com/personalproject/service/MathExpressionEvaluator.java index c4ee580..ff77f94 100644 --- a/src/main/java/com/personalproject/service/MathExpressionEvaluator.java +++ b/src/main/java/com/personalproject/service/MathExpressionEvaluator.java @@ -3,6 +3,7 @@ package com.personalproject.service; import java.util.HashMap; import java.util.Map; import java.util.Stack; +import java.util.function.DoubleUnaryOperator; import java.util.regex.Pattern; /** @@ -12,6 +13,7 @@ public final class MathExpressionEvaluator { private static final Pattern NUMBER_PATTERN = Pattern.compile("-?\\d+(\\.\\d+)?"); private static final Map PRECEDENCE = new HashMap<>(); + private static final Map FUNCTIONS = new HashMap<>(); static { PRECEDENCE.put('+', 1); @@ -19,6 +21,11 @@ public final class MathExpressionEvaluator { PRECEDENCE.put('*', 2); PRECEDENCE.put('/', 2); PRECEDENCE.put('^', 3); + + FUNCTIONS.put("sin", angle -> Math.sin(Math.toRadians(angle))); + FUNCTIONS.put("cos", angle -> Math.cos(Math.toRadians(angle))); + FUNCTIONS.put("tan", angle -> Math.tan(Math.toRadians(angle))); + FUNCTIONS.put("sqrt", Math::sqrt); } private MathExpressionEvaluator() { @@ -67,6 +74,21 @@ public final class MathExpressionEvaluator { if (Character.isDigit(c) || c == '.') { currentNumber.append(c); + } else if (Character.isLetter(c)) { + if (currentNumber.length() > 0) { + tokens.add(currentNumber.toString()); + currentNumber.setLength(0); + } + StringBuilder functionBuilder = new StringBuilder(); + functionBuilder.append(c); + while (i + 1 < expression.length() && Character.isLetter(expression.charAt(i + 1))) { + functionBuilder.append(expression.charAt(++i)); + } + String function = functionBuilder.toString(); + if (!isFunction(function)) { + throw new IllegalArgumentException("Unsupported function: " + function); + } + tokens.add(function); } else if (c == '(' || c == ')') { if (currentNumber.length() > 0) { tokens.add(currentNumber.toString()); @@ -107,6 +129,10 @@ public final class MathExpressionEvaluator { return c == '+' || c == '-' || c == '*' || c == '/' || c == '^'; } + private static boolean isFunction(String token) { + return FUNCTIONS.containsKey(token); + } + /** * 使用调度场算法将中缀表达式转换为后缀表达式。 * @@ -120,6 +146,8 @@ public final class MathExpressionEvaluator { for (String token : tokens) { if (isNumber(token)) { output.add(token); + } else if (isFunction(token)) { + operators.push(token); } else if (token.equals("(")) { operators.push(token); } else if (token.equals(")")) { @@ -129,6 +157,9 @@ public final class MathExpressionEvaluator { if (!operators.isEmpty()) { operators.pop(); // 移除 "(" } + if (!operators.isEmpty() && isFunction(operators.peek())) { + output.add(operators.pop()); + } } else if (isOperator(token.charAt(0))) { while (!operators.isEmpty() && isOperator(operators.peek().charAt(0)) @@ -140,7 +171,11 @@ public final class MathExpressionEvaluator { } while (!operators.isEmpty()) { - output.add(operators.pop()); + String operator = operators.pop(); + if (operator.equals("(") || operator.equals(")")) { + throw new IllegalArgumentException("Mismatched parentheses in expression"); + } + output.add(operator); } return output.toArray(new String[0]); @@ -167,6 +202,14 @@ public final class MathExpressionEvaluator { double a = values.pop(); double result = performOperation(a, b, token.charAt(0)); values.push(result); + } else if (isFunction(token)) { + if (values.isEmpty()) { + throw new IllegalArgumentException("Invalid expression: insufficient operands for function"); + } + double value = values.pop(); + values.push(applyFunction(token, value)); + } else { + throw new IllegalArgumentException("Unknown token: " + token); } } @@ -205,6 +248,14 @@ public final class MathExpressionEvaluator { } } + private static double applyFunction(String function, double value) { + DoubleUnaryOperator operator = FUNCTIONS.get(function); + if (operator == null) { + throw new IllegalArgumentException("Unknown function: " + function); + } + return operator.applyAsDouble(value); + } + /** * 检查记号是否为数字。 * diff --git a/src/main/java/com/personalproject/service/MathLearningService.java b/src/main/java/com/personalproject/service/MathLearningService.java index 7f7f62b..768d98e 100644 --- a/src/main/java/com/personalproject/service/MathLearningService.java +++ b/src/main/java/com/personalproject/service/MathLearningService.java @@ -30,10 +30,10 @@ public final class MathLearningService { public MathLearningService( Map generatorMap, QuestionGenerationService questionGenerationService) { + this.storageService = new QuestionStorageService(); this.accountRepository = new AccountRepository(); this.registrationService = new RegistrationService(accountRepository); - this.examService = new ExamService(generatorMap, questionGenerationService); - this.storageService = new QuestionStorageService(); + this.examService = new ExamService(generatorMap, questionGenerationService, storageService); this.resultService = new ExamResultService(); } diff --git a/src/main/java/com/personalproject/service/RegistrationService.java b/src/main/java/com/personalproject/service/RegistrationService.java index 024f2bf..099152c 100644 --- a/src/main/java/com/personalproject/service/RegistrationService.java +++ b/src/main/java/com/personalproject/service/RegistrationService.java @@ -6,6 +6,7 @@ import com.personalproject.auth.PasswordValidator; import com.personalproject.model.DifficultyLevel; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** @@ -16,6 +17,7 @@ public final class RegistrationService { private final AccountRepository accountRepository; private final Map pendingRegistrations; private final Map registrationAttempts; + private final Set verifiedUsers; /** * 创建新的注册服务. @@ -26,6 +28,7 @@ public final class RegistrationService { this.accountRepository = accountRepository; this.pendingRegistrations = new ConcurrentHashMap<>(); this.registrationAttempts = new ConcurrentHashMap<>(); + this.verifiedUsers = ConcurrentHashMap.newKeySet(); } /** @@ -38,19 +41,36 @@ public final class RegistrationService { */ public boolean initiateRegistration(String username, String email, DifficultyLevel difficultyLevel) { - if (!EmailService.isValidEmail(email)) { + if (username == null || email == null || difficultyLevel == null) { return false; } - if (!accountRepository.registerUser(username, email, difficultyLevel)) { + String normalizedUsername = username.trim(); + String normalizedEmail = email.trim(); + if (normalizedUsername.isEmpty() || normalizedEmail.isEmpty()) { + return false; + } + + if (!EmailService.isValidEmail(normalizedEmail)) { + return false; + } + + if (!accountRepository.registerUser(normalizedUsername, normalizedEmail, difficultyLevel)) { return false; // 用户名已存在或邮箱已注册 } String registrationCode = EmailService.generateRegistrationCode(); - pendingRegistrations.put(username, registrationCode); - registrationAttempts.put(username, 0); - - return EmailService.sendRegistrationCode(email, registrationCode); + pendingRegistrations.put(normalizedUsername, registrationCode); + registrationAttempts.put(normalizedUsername, 0); + verifiedUsers.remove(normalizedUsername); + + boolean sent = EmailService.sendRegistrationCode(normalizedEmail, registrationCode); + if (!sent) { + pendingRegistrations.remove(normalizedUsername); + registrationAttempts.remove(normalizedUsername); + accountRepository.removeUnverifiedUser(normalizedUsername); + } + return sent; } /** @@ -61,25 +81,34 @@ public final class RegistrationService { * @return 注册码有效则返回true,否则返回false */ public boolean verifyRegistrationCode(String username, String registrationCode) { - String storedCode = pendingRegistrations.get(username); - if (storedCode == null || !storedCode.equals(registrationCode)) { + if (username == null || registrationCode == null) { + return false; + } + String normalizedUsername = username.trim(); + String trimmedCode = registrationCode.trim(); + + String storedCode = pendingRegistrations.get(normalizedUsername); + if (storedCode == null || !storedCode.equals(trimmedCode)) { // 跟踪失败尝试 - int attempts = registrationAttempts.getOrDefault(username, 0); + int attempts = registrationAttempts.getOrDefault(normalizedUsername, 0); attempts++; - registrationAttempts.put(username, attempts); + registrationAttempts.put(normalizedUsername, attempts); if (attempts >= 3) { // 如果失败次数过多,则删除用户 - pendingRegistrations.remove(username); - registrationAttempts.remove(username); + pendingRegistrations.remove(normalizedUsername); + registrationAttempts.remove(normalizedUsername); + verifiedUsers.remove(normalizedUsername); + accountRepository.removeUnverifiedUser(normalizedUsername); return false; } return false; } // 有效码,从待处理列表中移除 - pendingRegistrations.remove(username); - registrationAttempts.remove(username); + pendingRegistrations.remove(normalizedUsername); + registrationAttempts.remove(normalizedUsername); + verifiedUsers.add(normalizedUsername); return true; } @@ -91,11 +120,23 @@ public final class RegistrationService { * @return 密码设置成功则返回true,如果验证失败或用户不存在则返回false */ public boolean setPassword(String username, String password) { + if (username == null || password == null) { + return false; + } + String normalizedUsername = username.trim(); if (!PasswordValidator.isValidPassword(password)) { return false; } - return accountRepository.setPassword(username, password); + if (!verifiedUsers.contains(normalizedUsername)) { + return false; + } + + boolean updated = accountRepository.setPassword(normalizedUsername, password); + if (updated) { + verifiedUsers.remove(normalizedUsername); + } + return updated; } /** @@ -107,11 +148,14 @@ public final class RegistrationService { * @return 密码更改成功则返回true,如果验证失败或身份验证失败则返回false */ public boolean changePassword(String username, String oldPassword, String newPassword) { + if (username == null || oldPassword == null || newPassword == null) { + return false; + } if (!PasswordValidator.isValidPassword(newPassword)) { return false; } - return accountRepository.changePassword(username, oldPassword, newPassword); + return accountRepository.changePassword(username.trim(), oldPassword, newPassword); } /** @@ -123,7 +167,10 @@ public final class RegistrationService { */ public Optional authenticate(String username, String password) { - return accountRepository.authenticate(username, password); + if (username == null || password == null) { + return Optional.empty(); + } + return accountRepository.authenticate(username.trim(), password); } /** @@ -133,7 +180,10 @@ public final class RegistrationService { * @return 用户存在则返回true,否则返回false */ public boolean userExists(String username) { - return accountRepository.userExists(username); + if (username == null) { + return false; + } + return accountRepository.userExists(username.trim()); } /** @@ -143,6 +193,9 @@ public final class RegistrationService { * @return 若找到则返回包含用户账户的 Optional */ public Optional getUser(String username) { - return accountRepository.getUser(username); + if (username == null) { + return Optional.empty(); + } + return accountRepository.getUser(username.trim()); } } diff --git a/src/main/java/com/personalproject/storage/QuestionStorageService.java b/src/main/java/com/personalproject/storage/QuestionStorageService.java index 4fc9870..dc483c9 100644 --- a/src/main/java/com/personalproject/storage/QuestionStorageService.java +++ b/src/main/java/com/personalproject/storage/QuestionStorageService.java @@ -87,32 +87,20 @@ public final class QuestionStorageService { Files.createDirectories(resultsDirectory); StringBuilder builder = new StringBuilder(); - builder.append("考试结果报告").append(System.lineSeparator()); + builder.append("考试试卷").append(System.lineSeparator()); builder.append("用户名: ").append(examSession.getUsername()).append(System.lineSeparator()); builder.append("难度: ").append(examSession.getDifficultyLevel().getDisplayName()) .append(System.lineSeparator()); builder.append("开始时间: ").append(examSession.getStartTime()).append(System.lineSeparator()); builder.append("题目数量: ").append(examSession.getQuestions().size()) .append(System.lineSeparator()); - builder.append("得分: ").append(String.format("%.2f", examSession.calculateScore())).append("%") - .append(System.lineSeparator()); builder.append(System.lineSeparator()); - // 添加逐题结果 + // 只保存题目内容 for (int i = 0; i < examSession.getQuestions().size(); i++) { var question = examSession.getQuestions().get(i); - int userAnswer = examSession.getUserAnswer(i); - boolean isCorrect = question.isAnswerCorrect(userAnswer); - builder.append("题目 ").append(i + 1).append(": ").append(question.getQuestionText()) .append(System.lineSeparator()); - builder.append("您的答案: ").append(userAnswer == -1 ? "未回答" : - (userAnswer < question.getOptions().size() ? question.getOptions().get(userAnswer) - : "无效")).append(System.lineSeparator()); - builder.append("正确答案: ").append( - question.getOptions().get(question.getCorrectAnswerIndex())) - .append(System.lineSeparator()); - builder.append("结果: ").append(isCorrect ? "正确" : "错误").append(System.lineSeparator()); builder.append(System.lineSeparator()); } @@ -159,4 +147,4 @@ public final class QuestionStorageService { "读取题目文件失败:" + path + ",原因:" + exception.getMessage() + ",将跳过该文件."); } } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/ui/views/ExamView.java b/src/main/java/com/personalproject/ui/views/ExamView.java index 67c0a13..02f2363 100644 --- a/src/main/java/com/personalproject/ui/views/ExamView.java +++ b/src/main/java/com/personalproject/ui/views/ExamView.java @@ -65,6 +65,7 @@ public class ExamView extends BorderPane { // 选项容器 optionsBox = new VBox(10); optionsBox.setPadding(new Insets(10)); + answerToggleGroup = new ToggleGroup(); // 按钮区域 buttonBox = new HBox(15); @@ -119,11 +120,11 @@ public class ExamView extends BorderPane { questionTextLabel.setText(currentQuestion.getQuestionText()); // 清空上一题的选项 + answerToggleGroup.selectToggle(null); + answerToggleGroup.getToggles().clear(); optionsBox.getChildren().clear(); // 创建新的选项组件 - answerToggleGroup = new ToggleGroup(); - for (int i = 0; i < currentQuestion.getOptions().size(); i++) { String option = currentQuestion.getOptions().get(i); RadioButton optionButton = new RadioButton((i + 1) + ". " + option); diff --git a/src/main/resources/email-config.properties b/src/main/resources/email-config.properties new file mode 100644 index 0000000..71ce04e --- /dev/null +++ b/src/main/resources/email-config.properties @@ -0,0 +1,20 @@ + +# 主机不变 +mail.smtp.host=smtp.126.com + +# 关键修改:端口改为 465 +mail.smtp.port=465 + +# 关键修改:禁用 STARTTLS +mail.smtp.starttls.enable=false + +# 关键修改:启用 SSL +mail.smtp.ssl.enable=true + +# 以下不变 +mail.smtp.auth=true +mail.username=soloyouth@126.com +mail.password=ZYsjxwDXFBsWeQcX +mail.from=soloyouth@126.com +mail.subject=数学学习软件注册验证码 +mail.debug=true \ No newline at end of file -- 2.34.1 From 1cdadce70d20e9fa2b795d186f0a96d957afcf22 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 11 Oct 2025 22:13:25 +0800 Subject: [PATCH 6/9] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 8 +- .../personalproject/MathExamApplication.java | 9 +- .../auth/AccountRepository.java | 18 ++- .../personalproject/auth/EmailService.java | 18 +-- .../auth/PasswordValidator.java | 4 +- .../com/personalproject/auth/UserAccount.java | 2 +- .../controller/MathLearningController.java | 2 +- .../generator/PrimaryQuestionGenerator.java | 3 +- .../personalproject/model/ExamSession.java | 42 +++--- .../personalproject/model/QuizQuestion.java | 12 +- .../personalproject/service/ExamService.java | 2 +- .../service/MathExpressionEvaluator.java | 19 +-- .../service/MathLearningService.java | 2 +- .../service/RegistrationService.java | 2 +- .../ui/{MathExamGUI.java => MathExamGui.java} | 21 ++- .../personalproject/ui/scenes/LoginScene.java | 72 +++++----- .../ui/scenes/RegistrationScene.java | 127 +++++++++--------- .../ui/views/ExamResultsView.java | 74 +++++----- .../ui/views/ExamSelectionView.java | 85 ++++++------ .../personalproject/ui/views/ExamView.java | 101 +++++++------- .../ui/views/MainMenuView.java | 66 ++++----- .../ui/views/PasswordChangeView.java | 80 ++++++----- src/main/resources/email-config.properties | 2 +- 23 files changed, 407 insertions(+), 364 deletions(-) rename src/main/java/com/personalproject/ui/{MathExamGUI.java => MathExamGui.java} (87%) diff --git a/pom.xml b/pom.xml index 70e9913..1c15ac9 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ javafx-maven-plugin ${javafx.maven.plugin.version} - com.personalproject.ui.MathExamGUI + com.personalproject.ui.MathExamGui @@ -86,7 +86,7 @@ default-cli - com.personalproject.ui.MathExamGUI + com.personalproject.ui.MathExamGui @@ -107,7 +107,7 @@ - com.personalproject.ui.MathExamGUI + com.personalproject.ui.MathExamGui @@ -127,7 +127,7 @@ - com.personalproject.ui.MathExamGUI + com.personalproject.ui.MathExamGui diff --git a/src/main/java/com/personalproject/MathExamApplication.java b/src/main/java/com/personalproject/MathExamApplication.java index f8fa725..3298df1 100644 --- a/src/main/java/com/personalproject/MathExamApplication.java +++ b/src/main/java/com/personalproject/MathExamApplication.java @@ -1,10 +1,9 @@ package com.personalproject; -import com.personalproject.ui.MathExamGUI; +import com.personalproject.ui.MathExamGui; /** - * 数学学习软件主应用程序入口. - * 这个类现在启动JavaFX GUI应用程序。 + * 数学学习软件主应用程序入口. 这个类现在启动JavaFX GUI应用程序. */ public final class MathExamApplication { @@ -15,6 +14,6 @@ public final class MathExamApplication { */ public static void main(String[] args) { // 启动JavaFX应用程序 - MathExamGUI.main(args); + MathExamGui.main(args); } -} \ No newline at end of file +} diff --git a/src/main/java/com/personalproject/auth/AccountRepository.java b/src/main/java/com/personalproject/auth/AccountRepository.java index 2a2f665..e30df5b 100644 --- a/src/main/java/com/personalproject/auth/AccountRepository.java +++ b/src/main/java/com/personalproject/auth/AccountRepository.java @@ -26,6 +26,9 @@ public final class AccountRepository { private final Map accounts = new ConcurrentHashMap<>(); + /** + * 创建账号仓库并立即加载磁盘中的账号数据. + */ public AccountRepository() { loadAccounts(); } @@ -54,7 +57,7 @@ public final class AccountRepository { } /** - * 使用电子邮箱注册新用户账号。 + * 使用电子邮箱注册新用户账号. * * @param username 用户名 * @param email 邮箱地址 @@ -82,7 +85,8 @@ public final class AccountRepository { return false; } - LocalDateTime registrationDate = existing != null ? existing.registrationDate() : LocalDateTime.now(); + LocalDateTime registrationDate = + existing != null ? existing.registrationDate() : LocalDateTime.now(); UserAccount account = new UserAccount( normalizedUsername, normalizedEmail, @@ -97,7 +101,7 @@ public final class AccountRepository { } /** - * 在注册后为用户设置密码。 + * 在注册后为用户设置密码. * * @param username 用户名 * @param password 要设置的密码 @@ -126,7 +130,7 @@ public final class AccountRepository { } /** - * 修改现有用户的密码。 + * 修改现有用户的密码. * * @param username 用户名 * @param oldPassword 当前密码 @@ -161,7 +165,7 @@ public final class AccountRepository { } /** - * 移除未完成注册的用户,便于重新注册。 + * 移除未完成注册的用户,便于重新注册. * * @param username 待移除的用户名 */ @@ -178,7 +182,7 @@ public final class AccountRepository { } /** - * 检查用户是否存在。 + * 检查用户是否存在. * * @param username 待检查的用户名 * @return 若存在则返回 true,否则返回 false @@ -191,7 +195,7 @@ public final class AccountRepository { } /** - * 按用户名获取用户账户。 + * 按用户名获取用户账户. * * @param username 用户名 * @return 若找到则返回包含用户账户的 Optional diff --git a/src/main/java/com/personalproject/auth/EmailService.java b/src/main/java/com/personalproject/auth/EmailService.java index 4d357bf..1f3b0f2 100644 --- a/src/main/java/com/personalproject/auth/EmailService.java +++ b/src/main/java/com/personalproject/auth/EmailService.java @@ -26,7 +26,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; /** - * 用于发送带有注册码的电子邮件的工具类。 + * 用于发送带有注册码的电子邮件的工具类. */ public final class EmailService { @@ -46,7 +46,7 @@ public final class EmailService { } /** - * 生成随机注册码。 + * 生成随机注册码. * * @return 随机生成的注册码 */ @@ -59,7 +59,7 @@ public final class EmailService { } /** - * 将注册码发送到指定邮箱。 + * 将注册码发送到指定邮箱. * * @param email 收件人邮箱 * @param registrationCode 要发送的注册码 @@ -79,7 +79,7 @@ public final class EmailService { } /** - * 获取最近一次发送到指定邮箱的验证码(便于调试或测试)。 + * 获取最近一次发送到指定邮箱的验证码(便于调试或测试). * * @param email 收件人邮箱 * @return 若存在则返回验证码 @@ -92,7 +92,7 @@ public final class EmailService { } /** - * 校验电子邮件地址的格式是否有效。 + * 校验电子邮件地址的格式是否有效. * * @param email 待校验的邮箱地址 * @return 若格式有效则返回 true,否则返回 false @@ -134,9 +134,9 @@ public final class EmailService { } String username = require(properties, "mail.username"); - String password = require(properties, "mail.password"); - String from = properties.getProperty("mail.from", username); - String subject = properties.getProperty("mail.subject", DEFAULT_SUBJECT); + final String password = require(properties, "mail.password"); + final String from = properties.getProperty("mail.from", username); + final String subject = properties.getProperty("mail.subject", DEFAULT_SUBJECT); Properties smtpProperties = new Properties(); for (Map.Entry entry : properties.entrySet()) { @@ -194,7 +194,7 @@ public final class EmailService { String sanitizedEmail = sanitizeEmail(email); String timestamp = DATE_TIME_FORMATTER.format(LocalDateTime.now()); - Path messageFile = OUTBOX_DIRECTORY.resolve(sanitizedEmail + "_" + timestamp + ".txt"); + final Path messageFile = OUTBOX_DIRECTORY.resolve(sanitizedEmail + "_" + timestamp + ".txt"); StringBuilder content = new StringBuilder(); content.append("收件人: ").append(email).append(System.lineSeparator()); content.append("注册码: ").append(registrationCode).append(System.lineSeparator()); diff --git a/src/main/java/com/personalproject/auth/PasswordValidator.java b/src/main/java/com/personalproject/auth/PasswordValidator.java index 53740d8..e8196af 100644 --- a/src/main/java/com/personalproject/auth/PasswordValidator.java +++ b/src/main/java/com/personalproject/auth/PasswordValidator.java @@ -3,7 +3,7 @@ package com.personalproject.auth; import java.util.regex.Pattern; /** - * 用于验证密码规则的工具类。 + * 用于验证密码规则的工具类. */ public final class PasswordValidator { @@ -15,7 +15,7 @@ public final class PasswordValidator { } /** - * 校验密码是否满足以下要求: + * 校验密码是否满足以下要求. * - 长度 6-10 位 * - 至少包含一个大写字母 * - 至少包含一个小写字母 diff --git a/src/main/java/com/personalproject/auth/UserAccount.java b/src/main/java/com/personalproject/auth/UserAccount.java index 07543a0..3900693 100644 --- a/src/main/java/com/personalproject/auth/UserAccount.java +++ b/src/main/java/com/personalproject/auth/UserAccount.java @@ -15,7 +15,7 @@ public record UserAccount( boolean isRegistered) { /** - * 创建新的用户账户,并使用当前时间作为注册时间。 + * 创建新的用户账户,并使用当前时间作为注册时间. * * @param username 用户名 * @param email 邮箱地址 diff --git a/src/main/java/com/personalproject/controller/MathLearningController.java b/src/main/java/com/personalproject/controller/MathLearningController.java index 4edd694..0a5fe61 100644 --- a/src/main/java/com/personalproject/controller/MathLearningController.java +++ b/src/main/java/com/personalproject/controller/MathLearningController.java @@ -144,7 +144,7 @@ public final class MathLearningController { } /** - * 按用户名获取用户账户。 + * 按用户名获取用户账户. * * @param username 用户名 * @return 若找到则返回包含用户账户的 Optional diff --git a/src/main/java/com/personalproject/generator/PrimaryQuestionGenerator.java b/src/main/java/com/personalproject/generator/PrimaryQuestionGenerator.java index 8041330..6c1536b 100644 --- a/src/main/java/com/personalproject/generator/PrimaryQuestionGenerator.java +++ b/src/main/java/com/personalproject/generator/PrimaryQuestionGenerator.java @@ -2,7 +2,6 @@ package com.personalproject.generator; import com.personalproject.model.QuizQuestion; import com.personalproject.service.MathExpressionEvaluator; - import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; @@ -78,7 +77,7 @@ public final class PrimaryQuestionGenerator implements QuestionGenerator, QuizQu } /** - * 生成选择题选项 + * 生成选择题选项. */ private List generateOptions(double correctAnswer, Random random) { String correctOption = formatOption(correctAnswer); diff --git a/src/main/java/com/personalproject/model/ExamSession.java b/src/main/java/com/personalproject/model/ExamSession.java index a57a808..bce8b58 100644 --- a/src/main/java/com/personalproject/model/ExamSession.java +++ b/src/main/java/com/personalproject/model/ExamSession.java @@ -6,7 +6,7 @@ import java.util.Collections; import java.util.List; /** - * 表示用户的考试会话。 + * 表示用户的考试会话. */ public final class ExamSession { @@ -18,7 +18,7 @@ public final class ExamSession { private int currentQuestionIndex; /** - * 创建新的考试会话。 + * 创建新的考试会话. * * @param username 考生的用户名 * @param difficultyLevel 考试的难度级别 @@ -45,7 +45,7 @@ public final class ExamSession { } /** - * 获取考生的用户名。 + * 获取考生的用户名. * * @return 用户名 */ @@ -54,7 +54,7 @@ public final class ExamSession { } /** - * 获取考试的难度级别。 + * 获取考试的难度级别. * * @return 考试难度级别 */ @@ -63,7 +63,7 @@ public final class ExamSession { } /** - * 获取考试中的题目列表。 + * 获取考试中的题目列表. * * @return 不可修改的题目列表 */ @@ -72,7 +72,7 @@ public final class ExamSession { } /** - * 获取用户对各题的作答。 + * 获取用户对各题的作答. * * @return 答案索引列表(-1 表示未选择) */ @@ -81,7 +81,7 @@ public final class ExamSession { } /** - * 获取当前题目的索引。 + * 获取当前题目的索引. * * @return 当前题目索引 */ @@ -90,7 +90,7 @@ public final class ExamSession { } /** - * 为当前题目记录用户的答案。 + * 为当前题目记录用户的答案. * * @param answerIndex 选中答案的索引 */ @@ -106,7 +106,7 @@ public final class ExamSession { } /** - * 跳转到下一题。 + * 跳转到下一题. * * @return 若成功跳转则返回 true,若已是最后一题则返回 false */ @@ -119,7 +119,7 @@ public final class ExamSession { } /** - * 返回上一题。 + * 返回上一题. * * @return 若成功返回则返回 true,若已是第一题则返回 false */ @@ -132,7 +132,7 @@ public final class ExamSession { } /** - * 检查考试是否完成(所有题目已作答或到达最后一题)。 + * 检查考试是否完成(所有题目已作答或到达最后一题). * * @return 若考试已完成则返回 true,否则返回 false */ @@ -141,7 +141,7 @@ public final class ExamSession { } /** - * 获取当前题目。 + * 获取当前题目. * * @return 当前的测验题 */ @@ -153,7 +153,7 @@ public final class ExamSession { } /** - * 获取指定题目的用户答案。 + * 获取指定题目的用户答案. * * @param questionIndex 题目索引 * @return 用户答案的索引(未选择时为 -1) @@ -166,7 +166,7 @@ public final class ExamSession { } /** - * 计算得分百分比。 + * 计算得分百分比. * * @return 0-100 范围内的得分百分比 */ @@ -183,7 +183,7 @@ public final class ExamSession { } /** - * 获取考试中的题目总数。 + * 获取考试中的题目总数. * * @return 题目总数 */ @@ -192,7 +192,7 @@ public final class ExamSession { } /** - * 检查指定题目是否已作答。 + * 检查指定题目是否已作答. * * @param questionIndex 题目索引 * @return 若已作答则返回 true,否则返回 false @@ -205,7 +205,7 @@ public final class ExamSession { } /** - * 获取考试开始时间。 + * 获取考试开始时间. * * @return 开始时间 */ @@ -214,7 +214,7 @@ public final class ExamSession { } /** - * 获取答对的题目数量。 + * 获取答对的题目数量. * * @return 正确题目数量 */ @@ -231,14 +231,14 @@ public final class ExamSession { } /** - * 获取答错的题目数量。 + * 获取答错的题目数量. * * @return 错误题目数量 */ public int getIncorrectAnswersCount() { int totalAnswered = 0; int correctCount = 0; - + for (int i = 0; i < questions.size(); i++) { int userAnswer = userAnswers.get(i); if (userAnswer != -1) { @@ -249,7 +249,7 @@ public final class ExamSession { } } } - + return totalAnswered - correctCount; } } diff --git a/src/main/java/com/personalproject/model/QuizQuestion.java b/src/main/java/com/personalproject/model/QuizQuestion.java index 52c8f6b..cb6c8c9 100644 --- a/src/main/java/com/personalproject/model/QuizQuestion.java +++ b/src/main/java/com/personalproject/model/QuizQuestion.java @@ -3,7 +3,7 @@ package com.personalproject.model; import java.util.List; /** - * 表示一个带有多项选择的测验题目。 + * 表示一个带有多项选择的测验题目. */ public final class QuizQuestion { @@ -12,7 +12,7 @@ public final class QuizQuestion { private final int correctAnswerIndex; /** - * 创建新的测验题目。 + * 创建新的测验题目. * * @param questionText 题目文本 * @param options 答案选项列表 @@ -35,7 +35,7 @@ public final class QuizQuestion { } /** - * 获取题目文本。 + * 获取题目文本. * * @return 题目文本 */ @@ -44,7 +44,7 @@ public final class QuizQuestion { } /** - * 获取答案选项列表。 + * 获取答案选项列表. * * @return 不可修改的答案选项列表 */ @@ -53,7 +53,7 @@ public final class QuizQuestion { } /** - * 获取正确答案在选项列表中的索引。 + * 获取正确答案在选项列表中的索引. * * @return 正确答案的索引 */ @@ -62,7 +62,7 @@ public final class QuizQuestion { } /** - * 检查给定的答案索引是否正确。 + * 检查给定的答案索引是否正确. * * @param answerIndex 用户答案的索引 * @return 若答案正确则返回 true,否则返回 false diff --git a/src/main/java/com/personalproject/service/ExamService.java b/src/main/java/com/personalproject/service/ExamService.java index f72dcd7..643c311 100644 --- a/src/main/java/com/personalproject/service/ExamService.java +++ b/src/main/java/com/personalproject/service/ExamService.java @@ -31,7 +31,7 @@ public final class ExamService { * * @param generatorMap 难度级别到题目生成器的映射 * @param questionGenerationService 题目生成服务 - * @param questionStorageService 题目存储服务 + * @param questionStorageService 题目存储服务 */ public ExamService( Map generatorMap, diff --git a/src/main/java/com/personalproject/service/MathExpressionEvaluator.java b/src/main/java/com/personalproject/service/MathExpressionEvaluator.java index ff77f94..49b919d 100644 --- a/src/main/java/com/personalproject/service/MathExpressionEvaluator.java +++ b/src/main/java/com/personalproject/service/MathExpressionEvaluator.java @@ -7,7 +7,7 @@ import java.util.function.DoubleUnaryOperator; import java.util.regex.Pattern; /** - * 可处理基本算术运算的数学表达式求值器。 + * 可处理基本算术运算的数学表达式求值器. */ public final class MathExpressionEvaluator { @@ -33,7 +33,7 @@ public final class MathExpressionEvaluator { } /** - * 计算数学表达式字符串的结果。 + * 计算数学表达式字符串的结果. * * @param expression 要计算的数学表达式 * @return 计算结果 @@ -60,7 +60,7 @@ public final class MathExpressionEvaluator { } /** - * 将表达式拆分为数字与运算符的记号。 + * 将表达式拆分为数字与运算符的记号. * * @param expression 待拆分的表达式 * @return 记号数组 @@ -120,7 +120,7 @@ public final class MathExpressionEvaluator { } /** - * 检查字符是否为运算符。 + * 检查字符是否为运算符. * * @param c 待检查的字符 * @return 若字符是运算符则返回 true,否则返回 false @@ -134,7 +134,7 @@ public final class MathExpressionEvaluator { } /** - * 使用调度场算法将中缀表达式转换为后缀表达式。 + * 使用调度场算法将中缀表达式转换为后缀表达式. * * @param tokens 中缀表达式的记号数组 * @return 后缀表达式的记号数组 @@ -182,7 +182,7 @@ public final class MathExpressionEvaluator { } /** - * 计算后缀表达式的值。 + * 计算后缀表达式的值. * * @param postfix 后缀表达式的记号数组 * @return 计算结果 @@ -204,7 +204,8 @@ public final class MathExpressionEvaluator { values.push(result); } else if (isFunction(token)) { if (values.isEmpty()) { - throw new IllegalArgumentException("Invalid expression: insufficient operands for function"); + throw new IllegalArgumentException( + "Invalid expression: insufficient operands for function"); } double value = values.pop(); values.push(applyFunction(token, value)); @@ -221,7 +222,7 @@ public final class MathExpressionEvaluator { } /** - * 对两个操作数执行指定运算。 + * 对两个操作数执行指定运算. * * @param a 第一个操作数 * @param b 第二个操作数 @@ -257,7 +258,7 @@ public final class MathExpressionEvaluator { } /** - * 检查记号是否为数字。 + * 检查记号是否为数字. * * @param token 待检查的记号 * @return 若为数字则返回 true,否则返回 false diff --git a/src/main/java/com/personalproject/service/MathLearningService.java b/src/main/java/com/personalproject/service/MathLearningService.java index 768d98e..71a0fd8 100644 --- a/src/main/java/com/personalproject/service/MathLearningService.java +++ b/src/main/java/com/personalproject/service/MathLearningService.java @@ -169,7 +169,7 @@ public final class MathLearningService { } /** - * 按用户名获取用户账户。 + * 按用户名获取用户账户. * * @param username 用户名 * @return 若找到则返回包含用户账户的 Optional diff --git a/src/main/java/com/personalproject/service/RegistrationService.java b/src/main/java/com/personalproject/service/RegistrationService.java index 099152c..ee2bc82 100644 --- a/src/main/java/com/personalproject/service/RegistrationService.java +++ b/src/main/java/com/personalproject/service/RegistrationService.java @@ -187,7 +187,7 @@ public final class RegistrationService { } /** - * 按用户名获取用户账户。 + * 按用户名获取用户账户. * * @param username 用户名 * @return 若找到则返回包含用户账户的 Optional diff --git a/src/main/java/com/personalproject/ui/MathExamGUI.java b/src/main/java/com/personalproject/ui/MathExamGui.java similarity index 87% rename from src/main/java/com/personalproject/ui/MathExamGUI.java rename to src/main/java/com/personalproject/ui/MathExamGui.java index 272bf36..2603eaf 100644 --- a/src/main/java/com/personalproject/ui/MathExamGUI.java +++ b/src/main/java/com/personalproject/ui/MathExamGui.java @@ -1,8 +1,5 @@ package com.personalproject.ui; -import javafx.application.Application; -import javafx.scene.Scene; -import javafx.stage.Stage; import com.personalproject.controller.MathLearningController; import com.personalproject.generator.HighSchoolQuestionGenerator; import com.personalproject.generator.MiddleSchoolQuestionGenerator; @@ -11,15 +8,16 @@ import com.personalproject.generator.QuestionGenerator; import com.personalproject.model.DifficultyLevel; import com.personalproject.service.QuestionGenerationService; import com.personalproject.ui.scenes.LoginScene; - import java.util.EnumMap; import java.util.Map; +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.stage.Stage; /** - * 数学学习软件的 JavaFX 图形界面应用程序。 - * 这是图形界面的主要入口点。 + * 数学学习软件的 JavaFX 图形界面应用程序. 这是图形界面的主要入口点. */ -public final class MathExamGUI extends Application { +public final class MathExamGui extends Application { private MathLearningController controller; @@ -30,22 +28,23 @@ public final class MathExamGUI extends Application { generatorMap.put(DifficultyLevel.PRIMARY, new PrimaryQuestionGenerator()); generatorMap.put(DifficultyLevel.MIDDLE, new MiddleSchoolQuestionGenerator()); generatorMap.put(DifficultyLevel.HIGH, new HighSchoolQuestionGenerator()); - QuestionGenerationService questionGenerationService = new QuestionGenerationService(generatorMap); + QuestionGenerationService questionGenerationService = new QuestionGenerationService( + generatorMap); this.controller = new MathLearningController(generatorMap, questionGenerationService); // 配置主舞台 primaryStage.setTitle("数学学习软件"); - + // 从登录界面开始 LoginScene loginScene = new LoginScene(primaryStage, controller); Scene scene = new Scene(loginScene, 600, 400); - + primaryStage.setScene(scene); primaryStage.show(); } /** - * 启动 JavaFX 应用程序。 + * 启动 JavaFX 应用程序. * * @param args 命令行参数 */ diff --git a/src/main/java/com/personalproject/ui/scenes/LoginScene.java b/src/main/java/com/personalproject/ui/scenes/LoginScene.java index 4d4d6be..b12f741 100644 --- a/src/main/java/com/personalproject/ui/scenes/LoginScene.java +++ b/src/main/java/com/personalproject/ui/scenes/LoginScene.java @@ -1,18 +1,24 @@ package com.personalproject.ui.scenes; +import com.personalproject.controller.MathLearningController; +import com.personalproject.ui.views.MainMenuView; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.control.*; -import javafx.scene.layout.*; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import javafx.stage.Stage; -import com.personalproject.controller.MathLearningController; -import com.personalproject.ui.views.MainMenuView; -import com.personalproject.ui.scenes.RegistrationScene; /** - * 负责处理用户登录与注册的场景。 + * 负责处理用户登录与注册的场景. */ public class LoginScene extends BorderPane { @@ -24,79 +30,79 @@ public class LoginScene extends BorderPane { private Button registerButton; /** - * LoginScene 的构造函数。 + * LoginScene 的构造函数. * * @param primaryStage 应用程序的主舞台 - * @param controller 数学学习控制器 + * @param controller 数学学习控制器 */ public LoginScene(Stage primaryStage, MathLearningController controller) { this.primaryStage = primaryStage; this.controller = controller; - initializeUI(); + initializeUi(); } /** - * 初始化界面组件。 + * 初始化界面组件. */ - private void initializeUI() { + private void initializeUi() { // 创建主布局 VBox mainLayout = new VBox(15); mainLayout.setAlignment(Pos.CENTER); mainLayout.setPadding(new Insets(20)); - + // 标题 - Label titleLabel = new Label("数学学习软件"); + final Label titleLabel = new Label("数学学习软件"); titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24)); - + // 登录表单 GridPane loginForm = new GridPane(); loginForm.setHgap(10); loginForm.setVgap(10); loginForm.setAlignment(Pos.CENTER); - - Label usernameLabel = new Label("用户名:"); + + final Label usernameLabel = new Label("用户名:"); usernameField = new TextField(); usernameField.setPrefWidth(200); - - Label passwordLabel = new Label("密码:"); + + final Label passwordLabel = new Label("密码:"); passwordField = new PasswordField(); passwordField.setPrefWidth(200); - + loginForm.add(usernameLabel, 0, 0); loginForm.add(usernameField, 1, 0); loginForm.add(passwordLabel, 0, 1); loginForm.add(passwordField, 1, 1); - + // 按钮 HBox buttonBox = new HBox(10); buttonBox.setAlignment(Pos.CENTER); - + loginButton = new Button("登录"); registerButton = new Button("注册"); - + // 设置按钮样式 loginButton.setPrefWidth(100); registerButton.setPrefWidth(100); - + buttonBox.getChildren().addAll(loginButton, registerButton); - + // 将组件添加到主布局 mainLayout.getChildren().addAll(titleLabel, loginForm, buttonBox); - + // 将主布局放到边界面板中央 setCenter(mainLayout); - + // 添加事件处理器 addEventHandlers(); } /** - * 为界面组件添加事件处理器。 + * 为界面组件添加事件处理器. */ private void addEventHandlers() { loginButton.setOnAction(e -> handleLogin()); registerButton.setOnAction(e -> handleRegistration()); - + // 允许使用回车键登录 setOnKeyPressed(event -> { if (event.getCode().toString().equals("ENTER")) { @@ -106,7 +112,7 @@ public class LoginScene extends BorderPane { } /** - * 处理登录流程。 + * 处理登录流程. */ private void handleLogin() { String username = usernameField.getText().trim(); @@ -131,7 +137,7 @@ public class LoginScene extends BorderPane { } /** - * 处理注册流程。 + * 处理注册流程. */ private void handleRegistration() { // 切换到注册界面 @@ -140,11 +146,11 @@ public class LoginScene extends BorderPane { } /** - * 显示提示对话框。 + * 显示提示对话框. * * @param alertType 提示类型 - * @param title 对话框标题 - * @param message 显示的消息 + * @param title 对话框标题 + * @param message 显示的消息 */ private void showAlert(Alert.AlertType alertType, String title, String message) { Alert alert = new Alert(alertType); diff --git a/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java b/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java index c180c73..3b4334c 100644 --- a/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java +++ b/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java @@ -1,18 +1,24 @@ package com.personalproject.ui.scenes; +import com.personalproject.controller.MathLearningController; +import com.personalproject.model.DifficultyLevel; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.control.*; -import javafx.scene.layout.*; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import javafx.stage.Stage; -import com.personalproject.controller.MathLearningController; -import com.personalproject.model.DifficultyLevel; -import com.personalproject.ui.scenes.LoginScene; /** - * 负责处理用户注册的场景。 + * 负责处理用户注册的场景. */ public class RegistrationScene extends BorderPane { @@ -31,120 +37,121 @@ public class RegistrationScene extends BorderPane { private VBox registrationForm; /** - * RegistrationScene 的构造函数。 + * RegistrationScene 的构造函数. * * @param primaryStage 应用程序的主舞台 - * @param controller 数学学习控制器 + * @param controller 数学学习控制器 */ public RegistrationScene(Stage primaryStage, MathLearningController controller) { this.primaryStage = primaryStage; this.controller = controller; - initializeUI(); + initializeUi(); } /** - * 初始化界面组件。 + * 初始化界面组件. */ - private void initializeUI() { + private void initializeUi() { // 创建主布局 VBox mainLayout = new VBox(15); mainLayout.setAlignment(Pos.CENTER); mainLayout.setPadding(new Insets(20)); - + // 标题 - Label titleLabel = new Label("用户注册"); + final Label titleLabel = new Label("用户注册"); titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24)); - + // 注册表单 registrationForm = new VBox(15); registrationForm.setAlignment(Pos.CENTER); - + // 步骤1:填写基础信息 GridPane basicInfoForm = new GridPane(); basicInfoForm.setHgap(10); basicInfoForm.setVgap(10); basicInfoForm.setAlignment(Pos.CENTER); - - Label usernameLabel = new Label("用户名:"); + + final Label usernameLabel = new Label("用户名:"); usernameField = new TextField(); usernameField.setPrefWidth(200); - - Label emailLabel = new Label("邮箱:"); + + final Label emailLabel = new Label("邮箱:"); emailField = new TextField(); emailField.setPrefWidth(200); - - Label difficultyLabel = new Label("默认难度:"); + + final Label difficultyLabel = new Label("默认难度:"); difficultyComboBox = new ComboBox<>(); - difficultyComboBox.getItems().addAll(DifficultyLevel.PRIMARY, DifficultyLevel.MIDDLE, DifficultyLevel.HIGH); + difficultyComboBox.getItems() + .addAll(DifficultyLevel.PRIMARY, DifficultyLevel.MIDDLE, DifficultyLevel.HIGH); difficultyComboBox.setValue(DifficultyLevel.PRIMARY); difficultyComboBox.setPrefWidth(200); - + basicInfoForm.add(usernameLabel, 0, 0); basicInfoForm.add(usernameField, 1, 0); basicInfoForm.add(emailLabel, 0, 1); basicInfoForm.add(emailField, 1, 1); basicInfoForm.add(difficultyLabel, 0, 2); basicInfoForm.add(difficultyComboBox, 1, 2); - + sendCodeButton = new Button("发送注册码"); sendCodeButton.setPrefWidth(120); - + registrationForm.getChildren().addAll(basicInfoForm, sendCodeButton); - + // 步骤2:验证码验证(初始隐藏) VBox verificationSection = new VBox(10); verificationSection.setAlignment(Pos.CENTER); verificationSection.setVisible(false); verificationSection.setManaged(false); - - Label codeLabel = new Label("注册码:"); + + final Label codeLabel = new Label("注册码:"); registrationCodeField = new TextField(); registrationCodeField.setPrefWidth(200); - + verifyCodeButton = new Button("验证注册码"); verifyCodeButton.setPrefWidth(120); - + verificationSection.getChildren().addAll(codeLabel, registrationCodeField, verifyCodeButton); registrationForm.getChildren().add(verificationSection); - + // 步骤3:设置密码(初始隐藏) VBox passwordSection = new VBox(10); passwordSection.setAlignment(Pos.CENTER); passwordSection.setVisible(false); passwordSection.setManaged(false); - - Label passwordLabel = new Label("设置密码 (6-10位,包含大小写字母和数字):"); + + final Label passwordLabel = new Label("设置密码 (6-10位,包含大小写字母和数字):"); passwordField = new PasswordField(); passwordField.setPrefWidth(200); - - Label confirmPasswordLabel = new Label("确认密码:"); + + final Label confirmPasswordLabel = new Label("确认密码:"); confirmPasswordField = new PasswordField(); confirmPasswordField.setPrefWidth(200); - + setPasswordButton = new Button("设置密码"); setPasswordButton.setPrefWidth(120); - - passwordSection.getChildren().addAll(passwordLabel, passwordField, confirmPasswordLabel, + + passwordSection.getChildren().addAll(passwordLabel, passwordField, confirmPasswordLabel, confirmPasswordField, setPasswordButton); registrationForm.getChildren().add(passwordSection); - + // 返回按钮 backButton = new Button("返回"); backButton.setPrefWidth(100); - + // 将组件添加到主布局 mainLayout.getChildren().addAll(titleLabel, registrationForm, backButton); - + setCenter(mainLayout); - + // 添加事件处理器 addEventHandlers(sendCodeButton, verificationSection, verifyCodeButton, passwordSection); } /** - * 为界面组件添加事件处理器。 + * 为界面组件添加事件处理器. */ - private void addEventHandlers(Button sendCodeButton, VBox verificationSection, + private void addEventHandlers(Button sendCodeButton, VBox verificationSection, Button verifyCodeButton, VBox passwordSection) { sendCodeButton.setOnAction(e -> handleSendCode(verificationSection)); verifyCodeButton.setOnAction(e -> handleVerifyCode(passwordSection)); @@ -153,12 +160,12 @@ public class RegistrationScene extends BorderPane { } /** - * 处理发送注册码的逻辑。 + * 处理发送注册码的逻辑. */ private void handleSendCode(VBox verificationSection) { - String username = usernameField.getText().trim(); - String email = emailField.getText().trim(); - DifficultyLevel difficultyLevel = difficultyComboBox.getValue(); + final String username = usernameField.getText().trim(); + final String email = emailField.getText().trim(); + final DifficultyLevel difficultyLevel = difficultyComboBox.getValue(); if (username.isEmpty() || email.isEmpty()) { showAlert(Alert.AlertType.WARNING, "警告", "请输入用户名和邮箱"); @@ -183,11 +190,11 @@ public class RegistrationScene extends BorderPane { } /** - * 处理注册码验证。 + * 处理注册码验证. */ private void handleVerifyCode(VBox passwordSection) { - String username = usernameField.getText().trim(); - String registrationCode = registrationCodeField.getText().trim(); + final String username = usernameField.getText().trim(); + final String registrationCode = registrationCodeField.getText().trim(); if (registrationCode.isEmpty()) { showAlert(Alert.AlertType.WARNING, "警告", "请输入注册码"); @@ -206,12 +213,12 @@ public class RegistrationScene extends BorderPane { } /** - * 处理设置用户密码的逻辑。 + * 处理设置用户密码的逻辑. */ private void handleSetPassword() { - String username = usernameField.getText().trim(); - String password = passwordField.getText(); - String confirmPassword = confirmPasswordField.getText(); + final String username = usernameField.getText().trim(); + final String password = passwordField.getText(); + final String confirmPassword = confirmPasswordField.getText(); if (password.isEmpty() || confirmPassword.isEmpty()) { showAlert(Alert.AlertType.WARNING, "警告", "请输入并确认密码"); @@ -224,7 +231,7 @@ public class RegistrationScene extends BorderPane { } if (!controller.isValidPassword(password)) { - showAlert(Alert.AlertType.ERROR, "密码不符合要求", + showAlert(Alert.AlertType.ERROR, "密码不符合要求", "密码长度必须为6-10位,且包含大小写字母和数字"); return; } @@ -240,7 +247,7 @@ public class RegistrationScene extends BorderPane { } /** - * 处理返回按钮的操作。 + * 处理返回按钮的操作. */ private void handleBack() { LoginScene loginScene = new LoginScene(primaryStage, controller); @@ -248,11 +255,11 @@ public class RegistrationScene extends BorderPane { } /** - * 显示提示对话框。 + * 显示提示对话框. * * @param alertType 提示类型 - * @param title 对话框标题 - * @param message 显示的消息 + * @param title 对话框标题 + * @param message 显示的消息 */ private void showAlert(Alert.AlertType alertType, String title, String message) { Alert alert = new Alert(alertType); diff --git a/src/main/java/com/personalproject/ui/views/ExamResultsView.java b/src/main/java/com/personalproject/ui/views/ExamResultsView.java index 64d0e91..0b71fe7 100644 --- a/src/main/java/com/personalproject/ui/views/ExamResultsView.java +++ b/src/main/java/com/personalproject/ui/views/ExamResultsView.java @@ -1,19 +1,21 @@ package com.personalproject.ui.views; +import com.personalproject.controller.MathLearningController; +import com.personalproject.model.ExamSession; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.control.*; -import javafx.scene.layout.*; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import javafx.stage.Stage; -import com.personalproject.controller.MathLearningController; -import com.personalproject.model.ExamSession; -import com.personalproject.auth.UserAccount; -import com.personalproject.ui.views.MainMenuView; /** - * 用于展示考试结果的界面。 + * 用于展示考试结果的界面. */ public class ExamResultsView extends BorderPane { @@ -24,72 +26,74 @@ public class ExamResultsView extends BorderPane { private Button exitButton; /** - * ExamResultsView 的构造函数。 + * ExamResultsView 的构造函数. * * @param primaryStage 应用程序的主舞台 - * @param controller 数学学习控制器 - * @param examSession 已完成的考试会话 + * @param controller 数学学习控制器 + * @param examSession 已完成的考试会话 */ - public ExamResultsView(Stage primaryStage, MathLearningController controller, ExamSession examSession) { + public ExamResultsView(Stage primaryStage, MathLearningController controller, + ExamSession examSession) { this.primaryStage = primaryStage; this.controller = controller; this.examSession = examSession; - initializeUI(); + initializeUi(); } /** - * 初始化界面组件。 + * 初始化界面组件. */ - private void initializeUI() { + private void initializeUi() { // 创建主布局 VBox mainLayout = new VBox(20); mainLayout.setAlignment(Pos.CENTER); mainLayout.setPadding(new Insets(20)); - + // 结果标题 Label titleLabel = new Label("考试结果"); titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24)); - + // 分数展示 double score = examSession.calculateScore(); Label scoreLabel = new Label(String.format("您的得分: %.2f%%", score)); scoreLabel.setFont(Font.font("System", FontWeight.BOLD, 18)); - + // 成绩明细 VBox breakdownBox = new VBox(10); breakdownBox.setAlignment(Pos.CENTER); - + Label totalQuestionsLabel = new Label("总题数: " + examSession.getTotalQuestions()); Label correctAnswersLabel = new Label("答对题数: " + examSession.getCorrectAnswersCount()); Label incorrectAnswersLabel = new Label("答错题数: " + examSession.getIncorrectAnswersCount()); - - breakdownBox.getChildren().addAll(totalQuestionsLabel, correctAnswersLabel, incorrectAnswersLabel); - + + breakdownBox.getChildren() + .addAll(totalQuestionsLabel, correctAnswersLabel, incorrectAnswersLabel); + // 按钮区域 HBox buttonBox = new HBox(15); buttonBox.setAlignment(Pos.CENTER); - + continueButton = new Button("继续考试"); exitButton = new Button("退出"); - + // 设置按钮尺寸 continueButton.setPrefSize(120, 40); exitButton.setPrefSize(120, 40); - + buttonBox.getChildren().addAll(continueButton, exitButton); - + // 将组件添加到主布局 mainLayout.getChildren().addAll(titleLabel, scoreLabel, breakdownBox, buttonBox); - + // 将主布局置于边界面板中央 setCenter(mainLayout); - + // 添加事件处理器 addEventHandlers(); } /** - * 为界面组件添加事件处理器。 + * 为界面组件添加事件处理器. */ private void addEventHandlers() { continueButton.setOnAction(e -> handleContinue()); @@ -97,7 +101,7 @@ public class ExamResultsView extends BorderPane { } /** - * 处理继续考试按钮的操作。 + * 处理继续考试按钮的操作. */ private void handleContinue() { // 返回主菜单以开始新考试 @@ -111,7 +115,7 @@ public class ExamResultsView extends BorderPane { // 如果找不到用户信息,则提示错误并返回登录界面 showAlert(Alert.AlertType.ERROR, "错误", "用户信息无法找到,请重新登录"); // 返回登录场景 - com.personalproject.ui.scenes.LoginScene loginScene = + com.personalproject.ui.scenes.LoginScene loginScene = new com.personalproject.ui.scenes.LoginScene(primaryStage, controller); primaryStage.getScene().setRoot(loginScene); } @@ -119,7 +123,7 @@ public class ExamResultsView extends BorderPane { } /** - * 处理退出按钮的操作。 + * 处理退出按钮的操作. */ private void handleExit() { // 返回主菜单 @@ -133,7 +137,7 @@ public class ExamResultsView extends BorderPane { // 如果找不到用户信息,则提示错误并返回登录界面 showAlert(Alert.AlertType.ERROR, "错误", "用户信息无法找到,请重新登录"); // 返回登录场景 - com.personalproject.ui.scenes.LoginScene loginScene = + com.personalproject.ui.scenes.LoginScene loginScene = new com.personalproject.ui.scenes.LoginScene(primaryStage, controller); primaryStage.getScene().setRoot(loginScene); } @@ -141,11 +145,11 @@ public class ExamResultsView extends BorderPane { } /** - * 显示提示对话框。 + * 显示提示对话框. * * @param alertType 提示类型 - * @param title 对话框标题 - * @param message 显示的消息 + * @param title 对话框标题 + * @param message 显示的消息 */ private void showAlert(Alert.AlertType alertType, String title, String message) { Alert alert = new Alert(alertType); diff --git a/src/main/java/com/personalproject/ui/views/ExamSelectionView.java b/src/main/java/com/personalproject/ui/views/ExamSelectionView.java index 839b54f..2a2d640 100644 --- a/src/main/java/com/personalproject/ui/views/ExamSelectionView.java +++ b/src/main/java/com/personalproject/ui/views/ExamSelectionView.java @@ -1,18 +1,25 @@ package com.personalproject.ui.views; +import com.personalproject.auth.UserAccount; +import com.personalproject.controller.MathLearningController; +import com.personalproject.model.DifficultyLevel; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.control.*; -import javafx.scene.layout.*; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.Spinner; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import javafx.stage.Stage; -import com.personalproject.controller.MathLearningController; -import com.personalproject.model.DifficultyLevel; -import com.personalproject.auth.UserAccount; /** - * 用于选择考试难度和题目数量的界面。 + * 用于选择考试难度和题目数量的界面. */ public class ExamSelectionView extends BorderPane { @@ -25,78 +32,80 @@ public class ExamSelectionView extends BorderPane { private Button backButton; /** - * ExamSelectionView 的构造函数。 + * ExamSelectionView 的构造函数. * * @param primaryStage 应用程序的主舞台 - * @param controller 数学学习控制器 - * @param userAccount 当前用户账户 + * @param controller 数学学习控制器 + * @param userAccount 当前用户账户 */ - public ExamSelectionView(Stage primaryStage, MathLearningController controller, UserAccount userAccount) { + public ExamSelectionView(Stage primaryStage, MathLearningController controller, + UserAccount userAccount) { this.primaryStage = primaryStage; this.controller = controller; this.userAccount = userAccount; - initializeUI(); + initializeUi(); } /** - * 初始化界面组件。 + * 初始化界面组件. */ - private void initializeUI() { + private void initializeUi() { // 创建主布局 VBox mainLayout = new VBox(20); mainLayout.setAlignment(Pos.CENTER); mainLayout.setPadding(new Insets(20)); - + // 标题 - Label titleLabel = new Label("考试设置"); + final Label titleLabel = new Label("考试设置"); titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24)); - + // 考试设置表单 GridPane examSettingsForm = new GridPane(); examSettingsForm.setHgap(15); examSettingsForm.setVgap(15); examSettingsForm.setAlignment(Pos.CENTER); - - Label difficultyLabel = new Label("选择难度:"); + + final Label difficultyLabel = new Label("选择难度:"); difficultyComboBox = new ComboBox<>(); - difficultyComboBox.getItems().addAll(DifficultyLevel.PRIMARY, DifficultyLevel.MIDDLE, DifficultyLevel.HIGH); + difficultyComboBox.getItems() + .addAll(DifficultyLevel.PRIMARY, DifficultyLevel.MIDDLE, DifficultyLevel.HIGH); difficultyComboBox.setValue(userAccount.difficultyLevel()); // 默认选中用户的难度 difficultyComboBox.setPrefWidth(200); - - Label questionCountLabel = new Label("题目数量 (10-30):"); + + final Label questionCountLabel = new Label("题目数量 (10-30):"); questionCountSpinner = new Spinner<>(10, 30, 10); // 最小值、最大值、初始值 questionCountSpinner.setPrefWidth(200); - + examSettingsForm.add(difficultyLabel, 0, 0); examSettingsForm.add(difficultyComboBox, 1, 0); examSettingsForm.add(questionCountLabel, 0, 1); examSettingsForm.add(questionCountSpinner, 1, 1); - + // 按钮区域 HBox buttonBox = new HBox(15); buttonBox.setAlignment(Pos.CENTER); - + startExamButton = new Button("开始考试"); backButton = new Button("返回"); - + // 设置按钮尺寸 startExamButton.setPrefSize(120, 40); backButton.setPrefSize(120, 40); - + buttonBox.getChildren().addAll(startExamButton, backButton); - + // 将组件添加到主布局 mainLayout.getChildren().addAll(titleLabel, examSettingsForm, buttonBox); - + // 将主布局置于边界面板中央 setCenter(mainLayout); - + // 添加事件处理器 addEventHandlers(); } /** - * 为界面组件添加事件处理器。 + * 为界面组件添加事件处理器. */ private void addEventHandlers() { startExamButton.setOnAction(e -> handleStartExam()); @@ -104,27 +113,27 @@ public class ExamSelectionView extends BorderPane { } /** - * 处理开始考试按钮的操作。 + * 处理开始考试按钮的操作. */ private void handleStartExam() { DifficultyLevel selectedDifficulty = difficultyComboBox.getValue(); int questionCount = questionCountSpinner.getValue(); - + if (questionCount < 10 || questionCount > 30) { showAlert(Alert.AlertType.WARNING, "无效输入", "题目数量必须在10到30之间"); return; } - + // 创建并启动考试会话 com.personalproject.model.ExamSession examSession = controller.createExamSession( userAccount.username(), selectedDifficulty, questionCount); - + ExamView examView = new ExamView(primaryStage, controller, examSession); primaryStage.getScene().setRoot(examView); } /** - * 处理返回按钮的操作。 + * 处理返回按钮的操作. */ private void handleBack() { MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount); @@ -132,11 +141,11 @@ public class ExamSelectionView extends BorderPane { } /** - * 显示提示对话框。 + * 显示提示对话框. * * @param alertType 提示类型 - * @param title 对话框标题 - * @param message 显示的消息 + * @param title 对话框标题 + * @param message 显示的消息 */ private void showAlert(Alert.AlertType alertType, String title, String message) { Alert alert = new Alert(alertType); diff --git a/src/main/java/com/personalproject/ui/views/ExamView.java b/src/main/java/com/personalproject/ui/views/ExamView.java index 02f2363..2ff4b17 100644 --- a/src/main/java/com/personalproject/ui/views/ExamView.java +++ b/src/main/java/com/personalproject/ui/views/ExamView.java @@ -1,19 +1,24 @@ package com.personalproject.ui.views; +import com.personalproject.controller.MathLearningController; +import com.personalproject.model.ExamSession; +import com.personalproject.model.QuizQuestion; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.control.*; -import javafx.scene.layout.*; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import javafx.stage.Stage; -import com.personalproject.controller.MathLearningController; -import com.personalproject.model.ExamSession; -import com.personalproject.model.QuizQuestion; -import com.personalproject.ui.views.ExamResultsView; /** - * 提供答题界面以及题目与选项的呈现。 + * 提供答题界面以及题目与选项的呈现. */ public class ExamView extends BorderPane { @@ -30,73 +35,73 @@ public class ExamView extends BorderPane { private HBox buttonBox; /** - * ExamView 的构造函数。 + * ExamView 的构造函数. * * @param primaryStage 应用程序的主舞台 - * @param controller 数学学习控制器 - * @param examSession 当前的考试会话 + * @param controller 数学学习控制器 + * @param examSession 当前的考试会话 */ public ExamView(Stage primaryStage, MathLearningController controller, ExamSession examSession) { this.primaryStage = primaryStage; this.controller = controller; this.examSession = examSession; - initializeUI(); + initializeUi(); } /** - * 初始化界面组件。 + * 初始化界面组件. */ - private void initializeUI() { + private void initializeUi() { // 创建主布局 VBox mainLayout = new VBox(20); mainLayout.setAlignment(Pos.CENTER); mainLayout.setPadding(new Insets(20)); - + // 题号 questionNumberLabel = new Label(); questionNumberLabel.setFont(Font.font("System", FontWeight.BOLD, 16)); - + // 题目文本 questionTextLabel = new Label(); questionTextLabel.setWrapText(true); questionTextLabel.setFont(Font.font("System", FontWeight.NORMAL, 14)); questionTextLabel.setMaxWidth(500); - + // 选项容器 optionsBox = new VBox(10); optionsBox.setPadding(new Insets(10)); answerToggleGroup = new ToggleGroup(); - + // 按钮区域 buttonBox = new HBox(15); buttonBox.setAlignment(Pos.CENTER); - + previousButton = new Button("上一题"); nextButton = new Button("下一题"); finishButton = new Button("完成考试"); - + // 设置按钮尺寸 previousButton.setPrefSize(100, 35); nextButton.setPrefSize(100, 35); finishButton.setPrefSize(120, 35); - + buttonBox.getChildren().addAll(previousButton, nextButton, finishButton); - + // 将组件添加到主布局 mainLayout.getChildren().addAll(questionNumberLabel, questionTextLabel, optionsBox, buttonBox); - + // 将主布局置于边界面板中央 setCenter(mainLayout); - + // 加载第一题 loadCurrentQuestion(); - + // 添加事件处理器 addEventHandlers(); } /** - * 将当前题目加载到界面。 + * 将当前题目加载到界面. */ private void loadCurrentQuestion() { try { @@ -106,24 +111,24 @@ public class ExamView extends BorderPane { updateButtonStates(); return; } - + QuizQuestion currentQuestion = examSession.getCurrentQuestion(); int currentIndex = examSession.getCurrentQuestionIndex(); - + if (currentQuestion == null) { showAlert(Alert.AlertType.ERROR, "错误", "当前题目为空,请重新开始考试"); return; } - + // 更新题号与题目文本 questionNumberLabel.setText("第 " + (currentIndex + 1) + " 题"); questionTextLabel.setText(currentQuestion.getQuestionText()); - + // 清空上一题的选项 answerToggleGroup.selectToggle(null); answerToggleGroup.getToggles().clear(); optionsBox.getChildren().clear(); - + // 创建新的选项组件 for (int i = 0; i < currentQuestion.getOptions().size(); i++) { String option = currentQuestion.getOptions().get(i); @@ -132,14 +137,14 @@ public class ExamView extends BorderPane { optionButton.setUserData(i); // 存储选项索引 // 如果该题已有答案则自动选中 - if (examSession.hasAnswered(currentIndex) && - examSession.getUserAnswer(currentIndex) == i) { + if (examSession.hasAnswered(currentIndex) + && examSession.getUserAnswer(currentIndex) == i) { optionButton.setSelected(true); } - + optionsBox.getChildren().add(optionButton); } - + // 更新按钮状态 updateButtonStates(); } catch (Exception e) { @@ -148,13 +153,13 @@ public class ExamView extends BorderPane { } /** - * 根据当前位置更新导航按钮的状态。 + * 根据当前位置更新导航按钮的状态. */ private void updateButtonStates() { try { int currentIndex = examSession.getCurrentQuestionIndex(); int totalQuestions = examSession.getTotalQuestions(); - + // 处理潜在极端情况 if (totalQuestions <= 0) { // 如果没有题目,则禁用所有导航按钮 @@ -163,13 +168,13 @@ public class ExamView extends BorderPane { finishButton.setDisable(false); // 仍允许完成考试 return; } - + // “上一题”按钮状态 previousButton.setDisable(currentIndex < 0 || currentIndex == 0); - + // “下一题”按钮状态 nextButton.setDisable(currentIndex < 0 || currentIndex >= totalQuestions - 1); - + // “完成考试”按钮状态——在考试完成或到达最后一题时启用 boolean isExamComplete = examSession.isComplete(); boolean isAtLastQuestion = (currentIndex >= totalQuestions - 1); @@ -184,13 +189,13 @@ public class ExamView extends BorderPane { } /** - * 为界面组件添加事件处理器。 + * 为界面组件添加事件处理器. */ private void addEventHandlers() { nextButton.setOnAction(e -> handleNextQuestion()); previousButton.setOnAction(e -> handlePreviousQuestion()); finishButton.setOnAction(e -> handleFinishExam()); - + // 添加变更监听器,在选项被选择时保存答案 answerToggleGroup.selectedToggleProperty().addListener((obs, oldSelection, newSelection) -> { if (newSelection != null) { @@ -201,7 +206,7 @@ public class ExamView extends BorderPane { } /** - * 处理“下一题”按钮的操作。 + * 处理“下一题”按钮的操作. */ private void handleNextQuestion() { try { @@ -218,7 +223,7 @@ public class ExamView extends BorderPane { } /** - * 处理“上一题”按钮的操作。 + * 处理“上一题”按钮的操作. */ private void handlePreviousQuestion() { try { @@ -234,11 +239,11 @@ public class ExamView extends BorderPane { } /** - * 显示提示对话框。 + * 显示提示对话框. * * @param alertType 提示类型 - * @param title 对话框标题 - * @param message 显示的消息 + * @param title 对话框标题 + * @param message 显示的消息 */ private void showAlert(Alert.AlertType alertType, String title, String message) { Alert alert = new Alert(alertType); @@ -249,12 +254,12 @@ public class ExamView extends BorderPane { } /** - * 处理“完成考试”按钮的操作。 + * 处理“完成考试”按钮的操作. */ private void handleFinishExam() { // 保存考试结果 controller.saveExamResults(examSession); - + // 展示考试结果 ExamResultsView resultsView = new ExamResultsView(primaryStage, controller, examSession); primaryStage.getScene().setRoot(resultsView); diff --git a/src/main/java/com/personalproject/ui/views/MainMenuView.java b/src/main/java/com/personalproject/ui/views/MainMenuView.java index 619ca88..a39ed84 100644 --- a/src/main/java/com/personalproject/ui/views/MainMenuView.java +++ b/src/main/java/com/personalproject/ui/views/MainMenuView.java @@ -1,20 +1,20 @@ package com.personalproject.ui.views; +import com.personalproject.auth.UserAccount; +import com.personalproject.controller.MathLearningController; +import com.personalproject.ui.scenes.LoginScene; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.control.*; -import javafx.scene.layout.*; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import javafx.stage.Stage; -import com.personalproject.controller.MathLearningController; -import com.personalproject.model.DifficultyLevel; -import com.personalproject.model.ExamSession; -import com.personalproject.auth.UserAccount; -import com.personalproject.ui.scenes.LoginScene; /** - * 用户可以开始考试或更改设置的主菜单界面。 + * 用户可以开始考试或更改设置的主菜单界面. */ public class MainMenuView extends BorderPane { @@ -26,63 +26,65 @@ public class MainMenuView extends BorderPane { private Button logoutButton; /** - * MainMenuView 的构造函数。 + * MainMenuView 的构造函数. * * @param primaryStage 应用程序的主舞台 - * @param controller 数学学习控制器 - * @param userAccount 当前用户账户 + * @param controller 数学学习控制器 + * @param userAccount 当前用户账户 */ - public MainMenuView(Stage primaryStage, MathLearningController controller, UserAccount userAccount) { + public MainMenuView(Stage primaryStage, MathLearningController controller, + UserAccount userAccount) { this.primaryStage = primaryStage; this.controller = controller; this.userAccount = userAccount; - initializeUI(); + initializeUi(); } /** - * 初始化界面组件。 + * 初始化界面组件. */ - private void initializeUI() { + private void initializeUi() { // 创建主布局 VBox mainLayout = new VBox(20); mainLayout.setAlignment(Pos.CENTER); mainLayout.setPadding(new Insets(20)); - + // 欢迎信息 Label welcomeLabel = new Label("欢迎, " + userAccount.username()); welcomeLabel.setFont(Font.font("System", FontWeight.BOLD, 18)); - + // 难度信息 - Label difficultyLabel = new Label("当前难度: " + userAccount.difficultyLevel().getDisplayName()); + Label difficultyLabel = new Label( + "当前难度: " + userAccount.difficultyLevel().getDisplayName()); difficultyLabel.setFont(Font.font("System", FontWeight.NORMAL, 14)); - + // 按钮区域 VBox buttonBox = new VBox(15); buttonBox.setAlignment(Pos.CENTER); - + startExamButton = new Button("开始考试"); changePasswordButton = new Button("修改密码"); logoutButton = new Button("退出登录"); - + // 设置按钮尺寸 startExamButton.setPrefSize(150, 40); changePasswordButton.setPrefSize(150, 40); logoutButton.setPrefSize(150, 40); - + buttonBox.getChildren().addAll(startExamButton, changePasswordButton, logoutButton); - + // 将组件添加到主布局 mainLayout.getChildren().addAll(welcomeLabel, difficultyLabel, buttonBox); - + // 将主布局置于边界面板中央 setCenter(mainLayout); - + // 添加事件处理器 addEventHandlers(); } /** - * 为界面组件添加事件处理器。 + * 为界面组件添加事件处理器. */ private void addEventHandlers() { startExamButton.setOnAction(e -> handleStartExam()); @@ -91,23 +93,25 @@ public class MainMenuView extends BorderPane { } /** - * 处理开始考试按钮的操作。 + * 处理开始考试按钮的操作. */ private void handleStartExam() { - ExamSelectionView examSelectionView = new ExamSelectionView(primaryStage, controller, userAccount); + ExamSelectionView examSelectionView = new ExamSelectionView(primaryStage, controller, + userAccount); primaryStage.getScene().setRoot(examSelectionView); } /** - * 处理修改密码按钮的操作。 + * 处理修改密码按钮的操作. */ private void handleChangePassword() { - PasswordChangeView passwordChangeView = new PasswordChangeView(primaryStage, controller, userAccount); + PasswordChangeView passwordChangeView = new PasswordChangeView(primaryStage, controller, + userAccount); primaryStage.getScene().setRoot(passwordChangeView); } /** - * 处理退出登录按钮的操作。 + * 处理退出登录按钮的操作. */ private void handleLogout() { // 返回登录界面 diff --git a/src/main/java/com/personalproject/ui/views/PasswordChangeView.java b/src/main/java/com/personalproject/ui/views/PasswordChangeView.java index ef28098..4a55267 100644 --- a/src/main/java/com/personalproject/ui/views/PasswordChangeView.java +++ b/src/main/java/com/personalproject/ui/views/PasswordChangeView.java @@ -1,18 +1,23 @@ package com.personalproject.ui.views; +import com.personalproject.auth.UserAccount; +import com.personalproject.controller.MathLearningController; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.control.*; -import javafx.scene.layout.*; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import javafx.stage.Stage; -import com.personalproject.controller.MathLearningController; -import com.personalproject.auth.UserAccount; -import com.personalproject.ui.views.MainMenuView; /** - * 用户修改密码的界面。 + * 用户修改密码的界面. */ public class PasswordChangeView extends BorderPane { @@ -26,82 +31,83 @@ public class PasswordChangeView extends BorderPane { private Button backButton; /** - * PasswordChangeView 的构造函数。 + * PasswordChangeView 的构造函数. * * @param primaryStage 应用程序的主舞台 - * @param controller 数学学习控制器 - * @param userAccount 当前用户账户 + * @param controller 数学学习控制器 + * @param userAccount 当前用户账户 */ - public PasswordChangeView(Stage primaryStage, MathLearningController controller, UserAccount userAccount) { + public PasswordChangeView(Stage primaryStage, MathLearningController controller, + UserAccount userAccount) { this.primaryStage = primaryStage; this.controller = controller; this.userAccount = userAccount; - initializeUI(); + initializeUi(); } /** - * 初始化界面组件。 + * 初始化界面组件. */ - private void initializeUI() { + private void initializeUi() { // 创建主布局 VBox mainLayout = new VBox(20); mainLayout.setAlignment(Pos.CENTER); mainLayout.setPadding(new Insets(20)); - + // 标题 - Label titleLabel = new Label("修改密码"); + final Label titleLabel = new Label("修改密码"); titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24)); - + // 修改密码表单 GridPane passwordForm = new GridPane(); passwordForm.setHgap(15); passwordForm.setVgap(15); passwordForm.setAlignment(Pos.CENTER); - - Label oldPasswordLabel = new Label("当前密码:"); + + final Label oldPasswordLabel = new Label("当前密码:"); oldPasswordField = new PasswordField(); oldPasswordField.setPrefWidth(200); - - Label newPasswordLabel = new Label("新密码 (6-10位,包含大小写字母和数字):"); + + final Label newPasswordLabel = new Label("新密码 (6-10位,包含大小写字母和数字):"); newPasswordField = new PasswordField(); newPasswordField.setPrefWidth(200); - - Label confirmNewPasswordLabel = new Label("确认新密码:"); + + final Label confirmNewPasswordLabel = new Label("确认新密码:"); confirmNewPasswordField = new PasswordField(); confirmNewPasswordField.setPrefWidth(200); - + passwordForm.add(oldPasswordLabel, 0, 0); passwordForm.add(oldPasswordField, 1, 0); passwordForm.add(newPasswordLabel, 0, 1); passwordForm.add(newPasswordField, 1, 1); passwordForm.add(confirmNewPasswordLabel, 0, 2); passwordForm.add(confirmNewPasswordField, 1, 2); - + // 按钮区域 HBox buttonBox = new HBox(15); buttonBox.setAlignment(Pos.CENTER); - + changePasswordButton = new Button("修改密码"); backButton = new Button("返回"); - + // 设置按钮尺寸 changePasswordButton.setPrefSize(120, 40); backButton.setPrefSize(120, 40); - + buttonBox.getChildren().addAll(changePasswordButton, backButton); - + // 将组件添加到主布局 mainLayout.getChildren().addAll(titleLabel, passwordForm, buttonBox); - + // 将主布局置于边界面板中央 setCenter(mainLayout); - + // 添加事件处理器 addEventHandlers(); } /** - * 为界面组件添加事件处理器。 + * 为界面组件添加事件处理器. */ private void addEventHandlers() { changePasswordButton.setOnAction(e -> handleChangePassword()); @@ -109,7 +115,7 @@ public class PasswordChangeView extends BorderPane { } /** - * 处理修改密码按钮的操作。 + * 处理修改密码按钮的操作. */ private void handleChangePassword() { String oldPassword = oldPasswordField.getText(); @@ -127,7 +133,7 @@ public class PasswordChangeView extends BorderPane { } if (!controller.isValidPassword(newPassword)) { - showAlert(Alert.AlertType.ERROR, "密码不符合要求", + showAlert(Alert.AlertType.ERROR, "密码不符合要求", "密码长度必须为6-10位,且包含大小写字母和数字"); return; } @@ -143,7 +149,7 @@ public class PasswordChangeView extends BorderPane { } /** - * 处理返回按钮的操作。 + * 处理返回按钮的操作. */ private void handleBack() { MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount); @@ -151,11 +157,11 @@ public class PasswordChangeView extends BorderPane { } /** - * 显示提示对话框。 + * 显示提示对话框. * * @param alertType 提示类型 - * @param title 对话框标题 - * @param message 显示的消息 + * @param title 对话框标题 + * @param message 显示的消息 */ private void showAlert(Alert.AlertType alertType, String title, String message) { Alert alert = new Alert(alertType); diff --git a/src/main/resources/email-config.properties b/src/main/resources/email-config.properties index 71ce04e..db546dc 100644 --- a/src/main/resources/email-config.properties +++ b/src/main/resources/email-config.properties @@ -17,4 +17,4 @@ mail.username=soloyouth@126.com mail.password=ZYsjxwDXFBsWeQcX mail.from=soloyouth@126.com mail.subject=数学学习软件注册验证码 -mail.debug=true \ No newline at end of file +mail.debug=true -- 2.34.1 From 19ea50d3fabbbad91ad1e88b8d6f9646383c4c0b Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 11 Oct 2025 22:23:12 +0800 Subject: [PATCH 7/9] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E8=B7=B3=E8=BD=AC=E9=80=BB=E8=BE=91,=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E5=90=8E=E7=9B=B4=E6=8E=A5=E8=BF=9B=E5=85=A5=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E5=90=8E=E7=9A=84=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/scenes/RegistrationScene.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java b/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java index 3b4334c..5df646e 100644 --- a/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java +++ b/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java @@ -2,6 +2,7 @@ package com.personalproject.ui.scenes; import com.personalproject.controller.MathLearningController; import com.personalproject.model.DifficultyLevel; +import com.personalproject.ui.views.MainMenuView; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Alert; @@ -239,8 +240,18 @@ public class RegistrationScene extends BorderPane { boolean success = controller.setPassword(username, password); if (success) { - showAlert(Alert.AlertType.INFORMATION, "注册成功", "注册成功!请登录。"); - handleBack(); // 返回登录界面 + var userAccountOptional = controller.getUserAccount(username); + if (userAccountOptional.isPresent()) { + showAlert(Alert.AlertType.INFORMATION, "注册成功", "注册成功!正在进入主菜单。"); + MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, + userAccountOptional.get()); + if (primaryStage.getScene() != null) { + primaryStage.getScene().setRoot(mainMenuView); + } + } else { + showAlert(Alert.AlertType.WARNING, "注册提示", "注册成功,但未能加载用户信息,请使用新密码登录。"); + handleBack(); + } } else { showAlert(Alert.AlertType.ERROR, "设置密码失败", "设置密码失败,请重试。"); } -- 2.34.1 From 9f24f3a71883e4e16dc5a0e45888a7cca1ed3148 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 12 Oct 2025 08:31:23 +0800 Subject: [PATCH 8/9] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E8=A6=81=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../personalproject/auth/EmailService.java | 1 + .../ui/scenes/RegistrationScene.java | 5 +- .../ui/views/ExamResultsView.java | 2 +- .../ui/views/MainMenuView.java | 74 +++++++++++++++---- 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/personalproject/auth/EmailService.java b/src/main/java/com/personalproject/auth/EmailService.java index 1f3b0f2..f12a81d 100644 --- a/src/main/java/com/personalproject/auth/EmailService.java +++ b/src/main/java/com/personalproject/auth/EmailService.java @@ -226,5 +226,6 @@ public final class EmailService { String password, String from, String subject) { + } } diff --git a/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java b/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java index 5df646e..2d63930 100644 --- a/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java +++ b/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java @@ -242,14 +242,15 @@ public class RegistrationScene extends BorderPane { if (success) { var userAccountOptional = controller.getUserAccount(username); if (userAccountOptional.isPresent()) { - showAlert(Alert.AlertType.INFORMATION, "注册成功", "注册成功!正在进入主菜单。"); + showAlert(Alert.AlertType.INFORMATION, "注册成功", "注册成功!正在进入难度选择界面。"); MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccountOptional.get()); if (primaryStage.getScene() != null) { primaryStage.getScene().setRoot(mainMenuView); } } else { - showAlert(Alert.AlertType.WARNING, "注册提示", "注册成功,但未能加载用户信息,请使用新密码登录。"); + showAlert(Alert.AlertType.WARNING, "注册提示", + "注册成功,但未能加载用户信息,请使用新密码登录。"); handleBack(); } } else { diff --git a/src/main/java/com/personalproject/ui/views/ExamResultsView.java b/src/main/java/com/personalproject/ui/views/ExamResultsView.java index 0b71fe7..18de3c2 100644 --- a/src/main/java/com/personalproject/ui/views/ExamResultsView.java +++ b/src/main/java/com/personalproject/ui/views/ExamResultsView.java @@ -55,7 +55,7 @@ public class ExamResultsView extends BorderPane { // 分数展示 double score = examSession.calculateScore(); - Label scoreLabel = new Label(String.format("您的得分: %.2f%%", score)); + Label scoreLabel = new Label(String.format("您的得分: %.2f", score)); scoreLabel.setFont(Font.font("System", FontWeight.BOLD, 18)); // 成绩明细 diff --git a/src/main/java/com/personalproject/ui/views/MainMenuView.java b/src/main/java/com/personalproject/ui/views/MainMenuView.java index a39ed84..7866b73 100644 --- a/src/main/java/com/personalproject/ui/views/MainMenuView.java +++ b/src/main/java/com/personalproject/ui/views/MainMenuView.java @@ -2,11 +2,15 @@ package com.personalproject.ui.views; import com.personalproject.auth.UserAccount; import com.personalproject.controller.MathLearningController; +import com.personalproject.model.DifficultyLevel; import com.personalproject.ui.scenes.LoginScene; +import java.util.Optional; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.TextInputDialog; import javafx.scene.layout.BorderPane; import javafx.scene.layout.VBox; import javafx.scene.text.Font; @@ -21,7 +25,6 @@ public class MainMenuView extends BorderPane { private final Stage primaryStage; private final MathLearningController controller; private final UserAccount userAccount; - private Button startExamButton; private Button changePasswordButton; private Button logoutButton; @@ -58,23 +61,32 @@ public class MainMenuView extends BorderPane { "当前难度: " + userAccount.difficultyLevel().getDisplayName()); difficultyLabel.setFont(Font.font("System", FontWeight.NORMAL, 14)); + Label promptLabel = new Label("请选择考试难度开始答题"); + promptLabel.setFont(Font.font("System", FontWeight.NORMAL, 14)); + + VBox difficultyBox = new VBox(10); + difficultyBox.setAlignment(Pos.CENTER); + difficultyBox.getChildren().addAll( + createDifficultyButton(DifficultyLevel.PRIMARY), + createDifficultyButton(DifficultyLevel.MIDDLE), + createDifficultyButton(DifficultyLevel.HIGH)); + // 按钮区域 VBox buttonBox = new VBox(15); buttonBox.setAlignment(Pos.CENTER); - startExamButton = new Button("开始考试"); changePasswordButton = new Button("修改密码"); logoutButton = new Button("退出登录"); // 设置按钮尺寸 - startExamButton.setPrefSize(150, 40); changePasswordButton.setPrefSize(150, 40); logoutButton.setPrefSize(150, 40); - buttonBox.getChildren().addAll(startExamButton, changePasswordButton, logoutButton); + buttonBox.getChildren().addAll(changePasswordButton, logoutButton); // 将组件添加到主布局 - mainLayout.getChildren().addAll(welcomeLabel, difficultyLabel, buttonBox); + mainLayout.getChildren().addAll(welcomeLabel, difficultyLabel, promptLabel, difficultyBox, + buttonBox); // 将主布局置于边界面板中央 setCenter(mainLayout); @@ -87,18 +99,46 @@ public class MainMenuView extends BorderPane { * 为界面组件添加事件处理器. */ private void addEventHandlers() { - startExamButton.setOnAction(e -> handleStartExam()); changePasswordButton.setOnAction(e -> handleChangePassword()); logoutButton.setOnAction(e -> handleLogout()); } - /** - * 处理开始考试按钮的操作. - */ - private void handleStartExam() { - ExamSelectionView examSelectionView = new ExamSelectionView(primaryStage, controller, - userAccount); - primaryStage.getScene().setRoot(examSelectionView); + private Button createDifficultyButton(DifficultyLevel level) { + Button button = new Button(level.getDisplayName()); + button.setPrefSize(150, 40); + button.setOnAction(e -> handleDifficultySelection(level)); + return button; + } + + private void handleDifficultySelection(DifficultyLevel level) { + TextInputDialog dialog = new TextInputDialog("10"); + dialog.setTitle("题目数量"); + dialog.setHeaderText("请选择题目数量"); + dialog.setContentText("请输入题目数量 (10-30):"); + + Optional result = dialog.showAndWait(); + if (result.isEmpty()) { + return; + } + + int questionCount; + try { + questionCount = Integer.parseInt(result.get().trim()); + } catch (NumberFormatException ex) { + showAlert(Alert.AlertType.ERROR, "无效输入", "请输入有效的数字。"); + return; + } + + if (questionCount < 10 || questionCount > 30) { + showAlert(Alert.AlertType.WARNING, "题目数量范围错误", "题目数量必须在10到30之间。"); + return; + } + + com.personalproject.model.ExamSession examSession = controller.createExamSession( + userAccount.username(), level, questionCount); + + ExamView examView = new ExamView(primaryStage, controller, examSession); + primaryStage.getScene().setRoot(examView); } /** @@ -110,6 +150,14 @@ public class MainMenuView extends BorderPane { primaryStage.getScene().setRoot(passwordChangeView); } + private void showAlert(Alert.AlertType alertType, String title, String message) { + Alert alert = new Alert(alertType); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } + /** * 处理退出登录按钮的操作. */ -- 2.34.1 From 0bb8a3552e4604085fb78668401e1888243a7b22 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 12 Oct 2025 14:21:35 +0800 Subject: [PATCH 9/9] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/personalproject/ui/scenes/RegistrationScene.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java b/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java index 2d63930..8141bf9 100644 --- a/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java +++ b/src/main/java/com/personalproject/ui/scenes/RegistrationScene.java @@ -10,6 +10,7 @@ import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.PasswordField; +import javafx.scene.control.ScrollPane; import javafx.scene.control.TextField; import javafx.scene.layout.BorderPane; import javafx.scene.layout.GridPane; @@ -55,7 +56,7 @@ public class RegistrationScene extends BorderPane { private void initializeUi() { // 创建主布局 VBox mainLayout = new VBox(15); - mainLayout.setAlignment(Pos.CENTER); + mainLayout.setAlignment(Pos.TOP_CENTER); mainLayout.setPadding(new Insets(20)); // 标题 @@ -143,7 +144,11 @@ public class RegistrationScene extends BorderPane { // 将组件添加到主布局 mainLayout.getChildren().addAll(titleLabel, registrationForm, backButton); - setCenter(mainLayout); + // 使用可滚动容器保证内容在小窗口中完整显示 + ScrollPane scrollPane = new ScrollPane(mainLayout); + scrollPane.setFitToWidth(true); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + setCenter(scrollPane); // 添加事件处理器 addEventHandlers(sendCodeButton, verificationSection, verifyCodeButton, passwordSection); -- 2.34.1