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