From 4c887386182e0d83866e33265a26aa902b7baf01 Mon Sep 17 00:00:00 2001 From: hnu202326010207 <2893666874@qq.com> Date: Fri, 26 Sep 2025 20:32:10 +0800 Subject: [PATCH 1/3] Initial commit --- README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..f2b9cdd --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# math-learing + -- 2.34.1 From 26a79f09399b5179ab91a1a33df0e6155cb3baad Mon Sep 17 00:00:00 2001 From: xiaoh <2893666874@qq.com> Date: Mon, 6 Oct 2025 23:14:51 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E7=AC=AC=E4=B8=80=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 - .../personalproject/MathExamApplication.java | 330 ++++++++++++++++++ .../auth/AccountRepository.java | 140 ++++++++ .../personalproject/auth/EmailService.java | 62 ++++ .../auth/PasswordValidator.java | 30 ++ src/com/personalproject/auth/UserAccount.java | 40 +++ .../controller/MathLearningController.java | 145 ++++++++ .../HighSchoolQuestionGenerator.java | 37 ++ .../MiddleSchoolQuestionGenerator.java | 39 +++ .../generator/PrimaryQuestionGenerator.java | 31 ++ .../generator/QuestionGenerator.java | 17 + .../model/DifficultyLevel.java | 49 +++ .../personalproject/model/ExamSession.java | 195 +++++++++++ .../personalproject/model/QuizQuestion.java | 73 ++++ .../service/ExamResultService.java | 83 +++++ .../personalproject/service/ExamService.java | 140 ++++++++ .../service/MathExpressionEvaluator.java | 217 ++++++++++++ .../service/MathLearningService.java | 170 +++++++++ .../service/QuestionGenerationService.java | 71 ++++ .../service/RegistrationService.java | 138 ++++++++ .../storage/QuestionStorageService.java | 162 +++++++++ 21 files changed, 2169 insertions(+), 2 deletions(-) delete mode 100644 README.md create mode 100644 src/com/personalproject/MathExamApplication.java create mode 100644 src/com/personalproject/auth/AccountRepository.java create mode 100644 src/com/personalproject/auth/EmailService.java create mode 100644 src/com/personalproject/auth/PasswordValidator.java create mode 100644 src/com/personalproject/auth/UserAccount.java create mode 100644 src/com/personalproject/controller/MathLearningController.java create mode 100644 src/com/personalproject/generator/HighSchoolQuestionGenerator.java create mode 100644 src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java create mode 100644 src/com/personalproject/generator/PrimaryQuestionGenerator.java create mode 100644 src/com/personalproject/generator/QuestionGenerator.java create mode 100644 src/com/personalproject/model/DifficultyLevel.java create mode 100644 src/com/personalproject/model/ExamSession.java create mode 100644 src/com/personalproject/model/QuizQuestion.java create mode 100644 src/com/personalproject/service/ExamResultService.java create mode 100644 src/com/personalproject/service/ExamService.java create mode 100644 src/com/personalproject/service/MathExpressionEvaluator.java create mode 100644 src/com/personalproject/service/MathLearningService.java create mode 100644 src/com/personalproject/service/QuestionGenerationService.java create mode 100644 src/com/personalproject/service/RegistrationService.java create mode 100644 src/com/personalproject/storage/QuestionStorageService.java diff --git a/README.md b/README.md deleted file mode 100644 index f2b9cdd..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# math-learing - diff --git a/src/com/personalproject/MathExamApplication.java b/src/com/personalproject/MathExamApplication.java new file mode 100644 index 0000000..35faa4c --- /dev/null +++ b/src/com/personalproject/MathExamApplication.java @@ -0,0 +1,330 @@ +package com.personalproject; + +import com.personalproject.controller.MathLearningController; +import com.personalproject.generator.HighSchoolQuestionGenerator; +import com.personalproject.generator.MiddleSchoolQuestionGenerator; +import com.personalproject.generator.PrimaryQuestionGenerator; +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; +import com.personalproject.service.QuestionGenerationService; +import java.util.EnumMap; +import java.util.Map; +import java.util.Optional; +import java.util.Scanner; + +/** + * 数学学习软件主应用程序入口. 采用MVC架构模式,此为主类,控制器层处理业务逻辑. + */ +public final class MathExamApplication { + + private final MathLearningController controller; + + /** + * 构造函数:初始化难度与题目生成器的映射关系,并创建控制器. + */ + public MathExamApplication() { + Map generatorMap = new EnumMap<>(DifficultyLevel.class); + generatorMap.put(DifficultyLevel.PRIMARY, new PrimaryQuestionGenerator()); + generatorMap.put(DifficultyLevel.MIDDLE, new MiddleSchoolQuestionGenerator()); + generatorMap.put(DifficultyLevel.HIGH, new HighSchoolQuestionGenerator()); + QuestionGenerationService questionGenerationService = new QuestionGenerationService( + generatorMap); + this.controller = new MathLearningController(generatorMap, questionGenerationService); + } + + /** + * 程序入口:创建应用实例并启动交互流程. + * + * @param args 命令行参数,当前未使用. + */ + public static void main(String[] args) { + MathExamApplication application = new MathExamApplication(); + application.run(); + } + + /** + * 主循环:保持读取输入并驱动登录及考试流程. + */ + private void run() { + Scanner scanner = new Scanner(System.in); + while (true) { + System.out.println("=== 欢迎使用数学学习软件 ==="); + System.out.println("1. 登录"); + System.out.println("2. 注册"); + System.out.println("3. 退出"); + System.out.print("请选择操作 (1-3): "); + + String choice = scanner.nextLine().trim(); + + switch (choice) { + case "1" -> handleLogin(scanner); + case "2" -> handleRegistration(scanner); + case "3" -> { + System.out.println("感谢使用,再见!"); + return; + } + default -> System.out.println("无效选择,请重新输入。"); + } + } + } + + /** + * 处理用户登录流程. + * + * @param scanner 读取控制台输入的扫描器 + */ + private void handleLogin(Scanner scanner) { + System.out.print("请输入用户名: "); + String username = scanner.nextLine().trim(); + + System.out.print("请输入密码: "); + String password = scanner.nextLine().trim(); + + Optional userAccount = + controller.authenticate(username, password); + + if (userAccount.isPresent()) { + System.out.println("登录成功!欢迎, " + userAccount.get().username()); + handleUserSession(scanner, userAccount.get()); + } else { + System.out.println("登录失败:用户名或密码错误。"); + } + } + + /** + * 处理用户注册流程. + * + * @param scanner 读取控制台输入的扫描器 + */ + private void handleRegistration(Scanner scanner) { + System.out.print("请输入用户名: "); + final String username = scanner.nextLine().trim(); + + System.out.print("请输入邮箱: "); + final String email = scanner.nextLine().trim(); + + if (!controller.isValidEmail(email)) { + System.out.println("邮箱格式不正确。"); + return; + } + + System.out.println("请选择难度级别:"); + System.out.println("1. 小学"); + System.out.println("2. 初中"); + System.out.println("3. 高中"); + System.out.print("请选择 (1-3): "); + + int levelChoice = -1; + try { + levelChoice = Integer.parseInt(scanner.nextLine().trim()); + } catch (NumberFormatException e) { + System.out.println("输入格式不正确。"); + return; + } + + DifficultyLevel difficultyLevel; + switch (levelChoice) { + case 1 -> difficultyLevel = DifficultyLevel.PRIMARY; + case 2 -> difficultyLevel = DifficultyLevel.MIDDLE; + case 3 -> difficultyLevel = DifficultyLevel.HIGH; + default -> { + System.out.println("无效的选择。"); + return; + } + } + + // 发送注册码 + if (controller.initiateRegistration(username, email, difficultyLevel)) { + System.out.println("注册码已发送至您的邮箱,请查收。"); + + System.out.print("请输入收到的注册码: "); + String registrationCode = scanner.nextLine().trim(); + + if (controller.verifyRegistrationCode(username, registrationCode)) { + System.out.println("注册码验证成功!"); + + // 设置密码 + while (true) { + System.out.print("请设置密码 (6-10位,包含大小写字母和数字): "); + String password = scanner.nextLine().trim(); + + if (!controller.isValidPassword(password)) { + System.out.println("密码不符合要求,请重新设置。"); + continue; + } + + if (controller.setPassword(username, password)) { + System.out.println("注册成功!请登录。"); + break; + } else { + System.out.println("设置密码失败,请重试。"); + } + } + } else { + System.out.println("注册码验证失败,请检查后重试。"); + } + } else { + System.out.println("注册失败:用户名或邮箱可能已存在。"); + } + } + + /** + * 处理用户会话,包括考试等操作. + * + * @param scanner 读取控制台输入的扫描器 + * @param userAccount 当前登录的用户账户 + */ + private void handleUserSession(Scanner scanner, + com.personalproject.auth.UserAccount userAccount) { + while (true) { + System.out.println("\n=== 用户菜单 ==="); + System.out.println("1. 开始考试"); + System.out.println("2. 修改密码"); + System.out.println("3. 退出账号"); + System.out.print("请选择操作 (1-3): "); + + String choice = scanner.nextLine().trim(); + + switch (choice) { + case "1" -> handleExamSession(scanner, userAccount); + case "2" -> handleChangePassword(scanner, userAccount); + case "3" -> { + System.out.println("已退出账号。"); + return; + } + default -> System.out.println("无效选择,请重新输入。"); + } + } + } + + /** + * 处理考试会话. + * + * @param scanner 读取控制台输入的扫描器 + * @param userAccount 当前登录的用户账户 + */ + private void handleExamSession(Scanner scanner, + com.personalproject.auth.UserAccount userAccount) { + System.out.println("当前选择难度: " + userAccount.difficultyLevel().getDisplayName()); + + System.out.print("请输入生成题目数量 (10-30): "); + int questionCount = -1; + try { + questionCount = Integer.parseInt(scanner.nextLine().trim()); + } catch (NumberFormatException e) { + System.out.println("输入格式不正确。"); + return; + } + + if (questionCount < 10 || questionCount > 30) { + System.out.println("题目数量必须在10到30之间。"); + return; + } + + // 创建考试会话 + ExamSession examSession = + controller.createExamSession(userAccount.username(), userAccount.difficultyLevel(), + questionCount); + + // 进行考试 + conductExam(scanner, examSession); + + // 保存结果 + controller.saveExamResults(examSession); + + System.out.printf("考试结束!您的得分: %.2f%%\n", examSession.calculateScore()); + + // 询问用户是否继续或退出 + System.out.println("1. 继续考试"); + System.out.println("2. 退出"); + System.out.print("请选择 (1-2): "); + + String choice = scanner.nextLine().trim(); + if (choice.equals("1")) { + handleExamSession(scanner, userAccount); + } + } + + /** + * 通过导航问题进行考试. + * + * @param scanner 用户输入的扫描器 + * @param examSession 要进行的考试会话 + */ + private void conductExam(Scanner scanner, ExamSession examSession) { + while (!examSession.isComplete()) { + var currentQuestion = examSession.getCurrentQuestion(); + System.out.printf("\n第 %d 题: %s\n", examSession.getCurrentQuestionIndex() + 1, + currentQuestion.getQuestionText()); + + // 打印选项 + for (int i = 0; i < currentQuestion.getOptions().size(); i++) { + System.out.printf("%d. %s\n", i + 1, currentQuestion.getOptions().get(i)); + } + + System.out.print( + "请选择答案 (1-" + currentQuestion.getOptions().size() + ", 0 返回上一题): "); + int choice = -1; + try { + choice = Integer.parseInt(scanner.nextLine().trim()); + } catch (NumberFormatException e) { + System.out.println("输入格式不正确,请重新输入。"); + continue; + } + + if (choice == 0) { + // 如果可能,转到上一个问题 + if (!examSession.goToPreviousQuestion()) { + System.out.println("已经是第一题了。"); + } + continue; + } + + if (choice < 1 || choice > currentQuestion.getOptions().size()) { + System.out.println("无效的选择,请重新输入。"); + continue; + } + + // 设置答案(从基于1的索引调整为基于0的索引) + examSession.setAnswer(choice - 1); + + // 转到下一题 + if (!examSession.goToNextQuestion()) { + // 如果无法转到下一题,意味着已到达末尾 + break; + } + } + } + + /** + * 处理用户的密码更改. + * + * @param scanner 读取控制台输入的扫描器 + * @param userAccount 当前登录的用户账户 + */ + private void handleChangePassword(Scanner scanner, + com.personalproject.auth.UserAccount userAccount) { + System.out.print("请输入当前密码: "); + String oldPassword = scanner.nextLine().trim(); + + if (!userAccount.password().equals(oldPassword)) { + System.out.println("当前密码错误。"); + return; + } + + System.out.print("请输入新密码 (6-10位,包含大小写字母和数字): "); + String newPassword = scanner.nextLine().trim(); + + if (!controller.isValidPassword(newPassword)) { + System.out.println("新密码不符合要求。"); + return; + } + + if (controller.changePassword(userAccount.username(), oldPassword, newPassword)) { + System.out.println("密码修改成功!"); + } else { + System.out.println("密码修改失败。"); + } + } +} \ No newline at end of file diff --git a/src/com/personalproject/auth/AccountRepository.java b/src/com/personalproject/auth/AccountRepository.java new file mode 100644 index 0000000..c5a00a2 --- /dev/null +++ b/src/com/personalproject/auth/AccountRepository.java @@ -0,0 +1,140 @@ +package com.personalproject.auth; + +import com.personalproject.model.DifficultyLevel; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * 存放用户账号并提供认证和注册查询能力. + */ +public final class AccountRepository { + + private final Map accounts = new HashMap<>(); + private final Map registrationCodes = new HashMap<>(); + + /** + * 根据用户名与密码查找匹配的账号. + * + * @param username 用户名. + * @param password 密码. + * @return 匹配成功时返回账号信息,否则返回空结果. + */ + public Optional authenticate(String username, String password) { + if (username == null || password == null) { + return Optional.empty(); + } + UserAccount account = accounts.get(username.trim()); + if (account == null || !account.isRegistered()) { + return Optional.empty(); + } + if (!account.password().equals(password.trim())) { + return Optional.empty(); + } + return Optional.of(account); + } + + /** + * Registers a new user account with email. + * + * @param username The username + * @param email The email address + * @param difficultyLevel The selected difficulty level + * @return true if registration was successful, false if username already exists + */ + public boolean registerUser(String username, String email, DifficultyLevel difficultyLevel) { + String trimmedUsername = username.trim(); + String trimmedEmail = email.trim(); + + if (accounts.containsKey(trimmedUsername)) { + return false; // Username already exists + } + + // Check if email is already used by another account + for (UserAccount account : accounts.values()) { + if (account.email().equals(trimmedEmail) && account.isRegistered()) { + return false; // Email already registered + } + } + + UserAccount newAccount = new UserAccount( + trimmedUsername, + trimmedEmail, + "", // Empty password initially + difficultyLevel, + LocalDateTime.now(), + false); // Not registered until password is set + accounts.put(trimmedUsername, newAccount); + return true; + } + + /** + * Sets the password for a user after registration. + * + * @param username The username + * @param password The password to set + * @return true if successful, false if user doesn't exist + */ + public boolean setPassword(String username, String password) { + UserAccount account = accounts.get(username.trim()); + if (account == null) { + return false; + } + + UserAccount updatedAccount = new UserAccount( + account.username(), + account.email(), + password, + account.difficultyLevel(), + account.registrationDate(), + true); // Now registered + accounts.put(username.trim(), updatedAccount); + return true; + } + + /** + * Changes the password for an existing user. + * + * @param username The username + * @param oldPassword The current password + * @param newPassword The new password + * @return true if successful, false if old password is incorrect or user doesn't exist + */ + public boolean changePassword(String username, String oldPassword, String newPassword) { + UserAccount account = accounts.get(username.trim()); + if (account == null || !account.password().equals(oldPassword) || !account.isRegistered()) { + return false; + } + + UserAccount updatedAccount = new UserAccount( + account.username(), + account.email(), + newPassword, + account.difficultyLevel(), + account.registrationDate(), + true); + accounts.put(username.trim(), updatedAccount); + return true; + } + + /** + * Checks if a user exists in the system. + * + * @param username The username to check + * @return true if user exists, false otherwise + */ + public boolean userExists(String username) { + return accounts.containsKey(username.trim()); + } + + /** + * Gets a user account by username. + * + * @param username The username + * @return Optional containing the user account if found + */ + public Optional getUser(String username) { + return Optional.ofNullable(accounts.get(username.trim())); + } +} diff --git a/src/com/personalproject/auth/EmailService.java b/src/com/personalproject/auth/EmailService.java new file mode 100644 index 0000000..d790c83 --- /dev/null +++ b/src/com/personalproject/auth/EmailService.java @@ -0,0 +1,62 @@ +package com.personalproject.auth; + +import java.util.Random; + +/** + * Interface for sending emails with registration codes. + */ +public final class EmailService { + + private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + private static final int CODE_LENGTH = 6; + private static final Random RANDOM = new Random(); + + private EmailService() { + // Prevent instantiation of utility class + } + + /** + * Generates a random registration code. + * + * @return A randomly generated registration code + */ + public static String generateRegistrationCode() { + StringBuilder code = new StringBuilder(); + for (int i = 0; i < CODE_LENGTH; i++) { + code.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length()))); + } + return code.toString(); + } + + /** + * Sends a registration code to the specified email address. In a real implementation, this would + * connect to an email server. + * + * @param email The email address to send the code to + * @param registrationCode The registration code to send + * @return true if successfully sent (in this mock implementation, always true) + */ + public static boolean sendRegistrationCode(String email, String registrationCode) { + // In a real implementation, this would connect to an email server + // For the mock implementation, we'll just print to console + System.out.println("Sending registration code " + registrationCode + " to " + email); + return true; + } + + /** + * Validates if an email address has a valid format. + * + * @param email The email address to validate + * @return true if the email has valid format, false otherwise + */ + public static boolean isValidEmail(String email) { + if (email == null || email.trim().isEmpty()) { + return false; + } + + // Simple email validation using regex + String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@" + + "(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"; + return email.matches(emailRegex); + } +} \ No newline at end of file diff --git a/src/com/personalproject/auth/PasswordValidator.java b/src/com/personalproject/auth/PasswordValidator.java new file mode 100644 index 0000000..acece6f --- /dev/null +++ b/src/com/personalproject/auth/PasswordValidator.java @@ -0,0 +1,30 @@ +package com.personalproject.auth; + +import java.util.regex.Pattern; + +/** + * Utility class for password validation. + */ +public final class PasswordValidator { + + private static final Pattern PASSWORD_PATTERN = + Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{6,10}$"); + + private PasswordValidator() { + // Prevent instantiation of utility class + } + + /** + * Validates if a password meets the requirements: - 6-10 characters - Contains at least one + * uppercase letter - Contains at least one lowercase letter - Contains at least one digit. + * + * @param password The password to validate + * @return true if password meets requirements, false otherwise + */ + public static boolean isValidPassword(String password) { + if (password == null) { + return false; + } + return PASSWORD_PATTERN.matcher(password).matches(); + } +} \ No newline at end of file diff --git a/src/com/personalproject/auth/UserAccount.java b/src/com/personalproject/auth/UserAccount.java new file mode 100644 index 0000000..80d5acf --- /dev/null +++ b/src/com/personalproject/auth/UserAccount.java @@ -0,0 +1,40 @@ +package com.personalproject.auth; + +import com.personalproject.model.DifficultyLevel; +import java.time.LocalDateTime; + +/** + * 不可变的账号定义. + */ +public record UserAccount( + String username, + String email, + String password, + DifficultyLevel difficultyLevel, + LocalDateTime registrationDate, + boolean isRegistered) { + + /** + * Creates a new user account with registration date set to now. + * + * @param username The username + * @param email The email address + * @param password The password + * @param difficultyLevel The selected difficulty level + * @param isRegistered Whether the user has completed registration + */ + public UserAccount { + if (username == null || username.trim().isEmpty()) { + throw new IllegalArgumentException("Username cannot be null or empty"); + } + if (email == null || email.trim().isEmpty()) { + throw new IllegalArgumentException("Email cannot be null or empty"); + } + if (password == null) { + throw new IllegalArgumentException("Password cannot be null"); + } + if (difficultyLevel == null) { + throw new IllegalArgumentException("Difficulty level cannot be null"); + } + } +} diff --git a/src/com/personalproject/controller/MathLearningController.java b/src/com/personalproject/controller/MathLearningController.java new file mode 100644 index 0000000..c847b94 --- /dev/null +++ b/src/com/personalproject/controller/MathLearningController.java @@ -0,0 +1,145 @@ +package com.personalproject.controller; + +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; +import com.personalproject.service.ExamResultService; +import com.personalproject.service.MathLearningService; +import com.personalproject.service.QuestionGenerationService; +import java.util.Map; +import java.util.Optional; + +/** + * 遵循MVC模式处理应用程序逻辑的控制器类. + */ +public final class MathLearningController { + + private final MathLearningService mathLearningService; + + /** + * 创建具有所需服务的新控制器. + * + * @param generatorMap 难度级别到题目生成器的映射 + * @param questionGenerationService 题目生成服务 + */ + public MathLearningController( + Map generatorMap, + QuestionGenerationService questionGenerationService) { + this.mathLearningService = new MathLearningService(generatorMap, questionGenerationService); + } + + /** + * 启动用户注册. + * + * @param username 所需用户名 + * @param email 用户的电子邮箱地址 + * @param difficultyLevel 选择的难度级别 + * @return 注册成功启动则返回true,否则返回false + */ + public boolean initiateRegistration(String username, String email, + DifficultyLevel difficultyLevel) { + return mathLearningService.initiateRegistration(username, email, difficultyLevel); + } + + /** + * 验证注册码. + * + * @param username 用户名 + * @param registrationCode 注册码 + * @return 码有效则返回true,否则返回false + */ + public boolean verifyRegistrationCode(String username, String registrationCode) { + return mathLearningService.verifyRegistrationCode(username, registrationCode); + } + + /** + * 注册后设置用户密码. + * + * @param username 用户名 + * @param password 要设置的密码 + * @return 密码设置成功则返回true,否则返回false + */ + public boolean setPassword(String username, String password) { + return mathLearningService.setPassword(username, password); + } + + /** + * 验证用户. + * + * @param username 用户名 + * @param password 密码 + * @return 验证成功则返回包含用户账户的Optional + */ + public Optional authenticate(String username, + String password) { + return mathLearningService.authenticate(username, password); + } + + /** + * 为用户创建考试会话. + * + * @param username 用户名 + * @param difficultyLevel 难度级别 + * @param questionCount 题目数量 + * @return 创建的考试会话 + */ + public ExamSession createExamSession(String username, DifficultyLevel difficultyLevel, + int questionCount) { + return mathLearningService.createExamSession(username, difficultyLevel, questionCount); + } + + /** + * 保存考试结果. + * + * @param examSession 要保存的考试会话 + */ + public void saveExamResults(ExamSession examSession) { + mathLearningService.saveExamResults(examSession); + } + + /** + * 处理考试结果并确定下一步操作. + * + * @param examSession 完成的考试会话 + * @param continueWithSameLevel 是否继续相同难度 + * @param newDifficultyLevel 更改时的新难度(如果不更改则为null) + * @return 要执行的下一步操作 + */ + public ExamResultService.ExamContinuationAction processExamResult( + ExamSession examSession, boolean continueWithSameLevel, DifficultyLevel newDifficultyLevel) { + return mathLearningService.processExamResult(examSession, continueWithSameLevel, + newDifficultyLevel); + } + + /** + * 更改用户密码. + * + * @param username 用户名 + * @param oldPassword 旧密码 + * @param newPassword 新密码 + * @return 密码更改成功则返回true,否则返回false + */ + public boolean changePassword(String username, String oldPassword, String newPassword) { + return mathLearningService.changePassword(username, oldPassword, newPassword); + } + + /** + * 验证密码. + * + * @param password 要验证的密码 + * @return 密码有效则返回true,否则返回false + */ + public boolean isValidPassword(String password) { + return MathLearningService.isValidPassword(password); + } + + /** + * 验证电子邮箱地址. + * + * @param email 要验证的邮箱 + * @return 邮箱有效则返回true,否则返回false + */ + public boolean isValidEmail(String email) { + return MathLearningService.isValidEmail(email); + } +} \ No newline at end of file diff --git a/src/com/personalproject/generator/HighSchoolQuestionGenerator.java b/src/com/personalproject/generator/HighSchoolQuestionGenerator.java new file mode 100644 index 0000000..d22bf53 --- /dev/null +++ b/src/com/personalproject/generator/HighSchoolQuestionGenerator.java @@ -0,0 +1,37 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 生成至少包含一种三角函数的高中难度题目表达式. + */ +public final class HighSchoolQuestionGenerator implements QuestionGenerator { + + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + private static final String[] TRIG_FUNCTIONS = {"sin", "cos", "tan"}; + + @Override + public String generateQuestion(Random random) { + int operandCount = random.nextInt(5) + 1; + String[] operands = new String[operandCount]; + for (int index = 0; index < operandCount; index++) { + operands[index] = String.valueOf(random.nextInt(100) + 1); + } + int specialIndex = random.nextInt(operandCount); + String function = TRIG_FUNCTIONS[random.nextInt(TRIG_FUNCTIONS.length)]; + operands[specialIndex] = function + '(' + operands[specialIndex] + ')'; + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < operandCount; index++) { + if (index > 0) { + String operator = OPERATORS[random.nextInt(OPERATORS.length)]; + builder.append(' ').append(operator).append(' '); + } + builder.append(operands[index]); + } + String expression = builder.toString(); + if (operandCount > 1 && random.nextBoolean()) { + return '(' + expression + ')'; + } + return expression; + } +} diff --git a/src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java b/src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java new file mode 100644 index 0000000..ad46678 --- /dev/null +++ b/src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java @@ -0,0 +1,39 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 生成包含平方或开根号运算的初中难度题目表达式. + */ +public final class MiddleSchoolQuestionGenerator implements QuestionGenerator { + + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + + @Override + public String generateQuestion(Random random) { + int operandCount = random.nextInt(5) + 1; + String[] operands = new String[operandCount]; + for (int index = 0; index < operandCount; index++) { + operands[index] = String.valueOf(random.nextInt(100) + 1); + } + int specialIndex = random.nextInt(operandCount); + if (random.nextBoolean()) { + operands[specialIndex] = '(' + operands[specialIndex] + ")^2"; + } else { + operands[specialIndex] = "sqrt(" + operands[specialIndex] + ')'; + } + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < operandCount; index++) { + if (index > 0) { + String operator = OPERATORS[random.nextInt(OPERATORS.length)]; + builder.append(' ').append(operator).append(' '); + } + builder.append(operands[index]); + } + String expression = builder.toString(); + if (operandCount > 1 && random.nextBoolean()) { + return '(' + expression + ')'; + } + return expression; + } +} diff --git a/src/com/personalproject/generator/PrimaryQuestionGenerator.java b/src/com/personalproject/generator/PrimaryQuestionGenerator.java new file mode 100644 index 0000000..9073153 --- /dev/null +++ b/src/com/personalproject/generator/PrimaryQuestionGenerator.java @@ -0,0 +1,31 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 生成包含基础四则运算的小学难度题目表达式. + */ +public final class PrimaryQuestionGenerator implements QuestionGenerator { + + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + + @Override + public String generateQuestion(Random random) { + // 至少生成两个操作数,避免题目退化成单个数字 + int operandCount = random.nextInt(4) + 2; + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < operandCount; index++) { + if (index > 0) { + String operator = OPERATORS[random.nextInt(OPERATORS.length)]; + builder.append(' ').append(operator).append(' '); + } + int value = random.nextInt(100) + 1; + builder.append(value); + } + String expression = builder.toString(); + if (operandCount > 1 && random.nextBoolean()) { + return '(' + expression + ')'; + } + return expression; + } +} diff --git a/src/com/personalproject/generator/QuestionGenerator.java b/src/com/personalproject/generator/QuestionGenerator.java new file mode 100644 index 0000000..a2a5ea7 --- /dev/null +++ b/src/com/personalproject/generator/QuestionGenerator.java @@ -0,0 +1,17 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 负责生成单条数学题目的表达式. + */ +public interface QuestionGenerator { + + /** + * 基于提供的随机数生成器构造一道题目的表达式. + * + * @param random 用于生成随机数的实例. + * @return 生成的题目表达式. + */ + String generateQuestion(Random random); +} diff --git a/src/com/personalproject/model/DifficultyLevel.java b/src/com/personalproject/model/DifficultyLevel.java new file mode 100644 index 0000000..bc654bb --- /dev/null +++ b/src/com/personalproject/model/DifficultyLevel.java @@ -0,0 +1,49 @@ +package com.personalproject.model; + +import java.util.Optional; + +/** + * 系统支持的出题难度级别. + */ +public enum DifficultyLevel { + PRIMARY("小学"), + MIDDLE("初中"), + HIGH("高中"); + + private final String displayName; + + DifficultyLevel(String displayName) { + this.displayName = displayName; + } + + /** + * 获取当前难度对应的展示名称. + * + * @return 难度的展示名称. + */ + public String getDisplayName() { + return displayName; + } + + /** + * 根据展示名称查找对应的难度枚举值. + * + * @param name 展示名称. + * @return 匹配到的难度枚举,可空返回. + */ + public static Optional fromDisplayName(String name) { + if (name == null) { + return Optional.empty(); + } + String trimmed = name.trim(); + if (trimmed.isEmpty()) { + return Optional.empty(); + } + for (DifficultyLevel level : values()) { + if (level.displayName.equals(trimmed)) { + return Optional.of(level); + } + } + return Optional.empty(); + } +} diff --git a/src/com/personalproject/model/ExamSession.java b/src/com/personalproject/model/ExamSession.java new file mode 100644 index 0000000..6437ba5 --- /dev/null +++ b/src/com/personalproject/model/ExamSession.java @@ -0,0 +1,195 @@ +package com.personalproject.model; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents an exam session for a user. + */ +public final class ExamSession { + + private final String username; + private final DifficultyLevel difficultyLevel; + private final List questions; + private final List userAnswers; + private final LocalDateTime startTime; + private int currentQuestionIndex; + + /** + * Creates a new exam session. + * + * @param username The username of the test taker + * @param difficultyLevel The difficulty level of the exam + * @param questions The list of questions for the exam + */ + public ExamSession(String username, DifficultyLevel difficultyLevel, + List questions) { + if (username == null || username.trim().isEmpty()) { + throw new IllegalArgumentException("Username cannot be null or empty"); + } + if (difficultyLevel == null) { + throw new IllegalArgumentException("Difficulty level cannot be null"); + } + if (questions == null || questions.isEmpty()) { + throw new IllegalArgumentException("Questions list cannot be null or empty"); + } + + this.username = username; + this.difficultyLevel = difficultyLevel; + this.questions = List.copyOf(questions); // Immutable copy of questions + this.userAnswers = new ArrayList<>(); + // Initialize user answers with -1 (no answer selected) + for (int i = 0; i < questions.size(); i++) { + userAnswers.add(-1); + } + this.startTime = LocalDateTime.now(); + this.currentQuestionIndex = 0; + } + + /** + * Gets the username of the test taker. + * + * @return The username + */ + public String getUsername() { + return username; + } + + /** + * Gets the difficulty level of the exam. + * + * @return The difficulty level + */ + public DifficultyLevel getDifficultyLevel() { + return difficultyLevel; + } + + /** + * Gets the list of questions in the exam. + * + * @return An unmodifiable list of questions + */ + public List getQuestions() { + return questions; + } + + /** + * Gets the user's answers to the questions. + * + * @return A list of answer indices (-1 means no answer selected) + */ + public List getUserAnswers() { + return List.copyOf(userAnswers); // Return a copy to prevent modification + } + + /** + * Gets the current question index. + * + * @return The current question index + */ + public int getCurrentQuestionIndex() { + return currentQuestionIndex; + } + + /** + * Sets the user's answer for the current question. + * + * @param answerIndex The index of the selected answer + */ + public void setAnswer(int answerIndex) { + if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) { + throw new IllegalStateException("No valid question at current index"); + } + if (answerIndex < 0 || answerIndex > questions.get(currentQuestionIndex).getOptions().size()) { + throw new IllegalArgumentException("Invalid answer index"); + } + userAnswers.set(currentQuestionIndex, answerIndex); + } + + /** + * Moves to the next question. + * + * @return true if successfully moved to next question, false if already at the last question + */ + public boolean goToNextQuestion() { + if (currentQuestionIndex < questions.size() - 1) { + currentQuestionIndex++; + return true; + } + return false; + } + + /** + * Moves to the previous question. + * + * @return true if successfully moved to previous question, false if already at the first question + */ + public boolean goToPreviousQuestion() { + if (currentQuestionIndex > 0) { + currentQuestionIndex--; + return true; + } + return false; + } + + /** + * Checks if the exam is complete (all questions answered or at the end). + * + * @return true if the exam is complete, false otherwise + */ + public boolean isComplete() { + return currentQuestionIndex >= questions.size() - 1; + } + + /** + * Gets the current question. + * + * @return The current quiz question + */ + public QuizQuestion getCurrentQuestion() { + if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) { + throw new IllegalStateException("No valid question at current index"); + } + return questions.get(currentQuestionIndex); + } + + /** + * Gets the user's answer for a specific question. + * + * @param questionIndex The index of the question + * @return The index of the user's answer (or -1 if no answer selected) + */ + public int getUserAnswer(int questionIndex) { + if (questionIndex < 0 || questionIndex >= questions.size()) { + throw new IllegalArgumentException("Question index out of bounds"); + } + return userAnswers.get(questionIndex); + } + + /** + * Calculates the score as a percentage. + * + * @return The score as a percentage (0-100) + */ + public double calculateScore() { + int correctCount = 0; + for (int i = 0; i < questions.size(); i++) { + QuizQuestion question = questions.get(i); + int userAnswer = userAnswers.get(i); + if (userAnswer != -1 && question.isAnswerCorrect(userAnswer)) { + correctCount++; + } + } + return questions.isEmpty() ? 0.0 : (double) correctCount / questions.size() * 100.0; + } + + /** + * Gets the start time of the exam. + * + * @return The start time + */ + public LocalDateTime getStartTime() { + return startTime; + } +} \ No newline at end of file diff --git a/src/com/personalproject/model/QuizQuestion.java b/src/com/personalproject/model/QuizQuestion.java new file mode 100644 index 0000000..e66f14c --- /dev/null +++ b/src/com/personalproject/model/QuizQuestion.java @@ -0,0 +1,73 @@ +package com.personalproject.model; + +import java.util.List; + +/** + * Represents a quiz question with multiple choice options. + */ +public final class QuizQuestion { + + private final String questionText; + private final List options; + private final int correctAnswerIndex; + + /** + * Creates a new quiz question. + * + * @param questionText The text of the question + * @param options The list of answer options + * @param correctAnswerIndex The index of the correct answer in the options list + */ + public QuizQuestion(String questionText, List options, int correctAnswerIndex) { + if (questionText == null || questionText.trim().isEmpty()) { + throw new IllegalArgumentException("Question text cannot be null or empty"); + } + if (options == null || options.size() < 2) { + throw new IllegalArgumentException("Options must contain at least 2 choices"); + } + if (correctAnswerIndex < 0 || correctAnswerIndex >= options.size()) { + throw new IllegalArgumentException("Correct answer index out of bounds"); + } + + this.questionText = questionText; + this.options = List.copyOf(options); // Immutable copy + this.correctAnswerIndex = correctAnswerIndex; + } + + /** + * Gets the question text. + * + * @return The question text + */ + public String getQuestionText() { + return questionText; + } + + /** + * Gets the list of answer options. + * + * @return An unmodifiable list of answer options + */ + public List getOptions() { + return options; + } + + /** + * Gets the index of the correct answer in the options list. + * + * @return The index of the correct answer + */ + public int getCorrectAnswerIndex() { + return correctAnswerIndex; + } + + /** + * Checks if the given answer index matches the correct answer. + * + * @param answerIndex The index of the user's answer + * @return true if the answer is correct, false otherwise + */ + public boolean isAnswerCorrect(int answerIndex) { + return answerIndex == correctAnswerIndex; + } +} \ No newline at end of file diff --git a/src/com/personalproject/service/ExamResultService.java b/src/com/personalproject/service/ExamResultService.java new file mode 100644 index 0000000..55a79f6 --- /dev/null +++ b/src/com/personalproject/service/ExamResultService.java @@ -0,0 +1,83 @@ +package com.personalproject.service; + +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; + +/** + * 用于处理考试后选项(如继续或退出)的服务. + */ +public final class ExamResultService { + + /** + * 评估用户在完成考试后的选择. + * + * @param examSession 完成的考试会话 + * @param continueWithSameLevel 是否使用相同难度级别继续 + * @param newDifficultyLevel 更改时的新难度级别(如果不更改则为null) + * @return 指示下一步操作的操作 + */ + public ExamContinuationAction processExamResult( + ExamSession examSession, boolean continueWithSameLevel, DifficultyLevel newDifficultyLevel) { + + if (continueWithSameLevel) { + return new ExamContinuationAction( + true, examSession.getDifficultyLevel(), (int) examSession.getQuestions().size()); + } else if (newDifficultyLevel != null) { + return new ExamContinuationAction(true, newDifficultyLevel, + (int) examSession.getQuestions().size()); + } else { + return new ExamContinuationAction(false, null, 0); + } + } + + /** + * 表示完成考试后要采取的操作. + */ + public static final class ExamContinuationAction { + + private final boolean shouldContinue; + private final DifficultyLevel nextDifficultyLevel; + private final int nextQuestionCount; + + /** + * 创建新的考试延续操作. + * + * @param shouldContinue 用户是否想继续参加另一次考试 + * @param nextDifficultyLevel 下次考试的难度级别(如果不继续则为null) + * @param nextQuestionCount 下次考试的题目数量(如果不继续则为0) + */ + public ExamContinuationAction( + boolean shouldContinue, DifficultyLevel nextDifficultyLevel, int nextQuestionCount) { + this.shouldContinue = shouldContinue; + this.nextDifficultyLevel = nextDifficultyLevel; + this.nextQuestionCount = nextQuestionCount; + } + + /** + * 获取用户是否想继续参加另一次考试. + * + * @return 如果继续则为true,如果退出则为false + */ + public boolean shouldContinue() { + return shouldContinue; + } + + /** + * 获取下次考试的难度级别. + * + * @return 下次难度级别(如果不继续则为null) + */ + public DifficultyLevel getNextDifficultyLevel() { + return nextDifficultyLevel; + } + + /** + * 获取下次考试的题目数量. + * + * @return 下次题目数量(如果不继续则为0) + */ + public int getNextQuestionCount() { + return nextQuestionCount; + } + } +} \ No newline at end of file diff --git a/src/com/personalproject/service/ExamService.java b/src/com/personalproject/service/ExamService.java new file mode 100644 index 0000000..392a83a --- /dev/null +++ b/src/com/personalproject/service/ExamService.java @@ -0,0 +1,140 @@ +package com.personalproject.service; + +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; +import com.personalproject.model.QuizQuestion; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +/** + * 用于管理考试会话和生成测验题目的服务类. + */ +public final class ExamService { + + private static final int OPTIONS_COUNT = 4; + private final Map generators; + private final Random random = new Random(); + private final QuestionGenerationService questionGenerationService; + + /** + * 创建新的考试服务. + * + * @param generatorMap 难度级别到题目生成器的映射 + * @param questionGenerationService 题目生成服务 + */ + public ExamService( + Map generatorMap, + QuestionGenerationService questionGenerationService) { + this.generators = new EnumMap<>(DifficultyLevel.class); + this.generators.putAll(generatorMap); + this.questionGenerationService = questionGenerationService; + } + + /** + * 使用指定参数创建新的考试会话. + * + * @param username 考生用户名 + * @param difficultyLevel 考试难度级别 + * @param questionCount 考试中包含的题目数量 + * @return 新的考试会话 + */ + public ExamSession createExamSession( + String username, DifficultyLevel difficultyLevel, int questionCount) { + if (questionCount < 10 || questionCount > 30) { + throw new IllegalArgumentException("题目数量必须在10到30之间"); + } + + // 根据难度级别生成题目 + List generatedQuestions = new ArrayList<>(); + QuestionGenerator generator = generators.get(difficultyLevel); + if (generator == null) { + throw new IllegalArgumentException("找不到难度级别的生成器: " + difficultyLevel); + } + + for (int i = 0; i < questionCount; i++) { + String question = generator.generateQuestion(random); + generatedQuestions.add(question); + } + + // 将字符串题目转换为带有选项和答案的QuizQuestion对象 + List quizQuestions = new ArrayList<>(); + for (String questionText : generatedQuestions) { + List options = generateOptions(questionText); + int correctAnswerIndex = generateCorrectAnswerIndex(options.size()); + QuizQuestion quizQuestion = new QuizQuestion(questionText, options, correctAnswerIndex); + quizQuestions.add(quizQuestion); + } + + return new ExamSession(username, difficultyLevel, quizQuestions); + } + + /** + * 使用预定义题目创建考试会话(用于测试). + * + * @param username 考生用户名 + * @param difficultyLevel 考试难度级别 + * @param questions 预定义题目列表 + * @return 新的考试会话 + */ + public ExamSession createExamSession( + String username, DifficultyLevel difficultyLevel, List questions) { + if (questions.size() < 10 || questions.size() > 30) { + throw new IllegalArgumentException("题目数量必须在10到30之间"); + } + return new ExamSession(username, difficultyLevel, questions); + } + + /** + * 为给定题目生成多项选择选项. 这是一个模拟实现 - 在实际系统中,选项会根据题目生成. + * + * @param questionText 题目文本 + * @return 答案选项列表 + */ + private List generateOptions(String questionText) { + List options = new ArrayList<>(); + // 为每个题目生成4个选项 + try { + // 尝试将数学表达式作为正确答案进行评估 + double correctAnswer = MathExpressionEvaluator.evaluate(questionText); + + // 创建正确答案选项 + options.add(String.format("%.2f", correctAnswer)); + + // 生成3个错误选项 + for (int i = 0; i < OPTIONS_COUNT - 1; i++) { + double incorrectAnswer = correctAnswer + (random.nextGaussian() * 10); // 添加一些随机偏移 + if (Math.abs(incorrectAnswer - correctAnswer) < 0.1) { // 确保不同 + incorrectAnswer += 1.5; + } + options.add(String.format("%.2f", incorrectAnswer)); + } + } catch (Exception e) { + // 如果评估失败,创建虚拟选项 + for (int i = 0; i < OPTIONS_COUNT; i++) { + options.add("选项 " + (i + 1)); + } + } + + // 随机打乱选项以随机化正确答案的位置 + java.util.Collections.shuffle(options, random); + + // 找到打乱后的正确答案索引 + // 对于此模拟实现,我们将返回第一个选项(索引0)作为正确答案 + // 实际实现将跟踪正确答案 + return options; + } + + /** + * 为给定选项数量生成随机正确答案索引. + * + * @param optionCount 选项数量 + * @return 0到optionCount-1之间的随机索引 + */ + private int generateCorrectAnswerIndex(int optionCount) { + return random.nextInt(optionCount); + } +} \ No newline at end of file diff --git a/src/com/personalproject/service/MathExpressionEvaluator.java b/src/com/personalproject/service/MathExpressionEvaluator.java new file mode 100644 index 0000000..6e32ed1 --- /dev/null +++ b/src/com/personalproject/service/MathExpressionEvaluator.java @@ -0,0 +1,217 @@ +package com.personalproject.service; + +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; +import java.util.regex.Pattern; + +/** + * A mathematical expression evaluator that can handle basic arithmetic operations. + */ +public final class MathExpressionEvaluator { + + private static final Pattern NUMBER_PATTERN = Pattern.compile("-?\\d+(\\.\\d+)?"); + private static final Map PRECEDENCE = new HashMap<>(); + + static { + PRECEDENCE.put('+', 1); + PRECEDENCE.put('-', 1); + PRECEDENCE.put('*', 2); + PRECEDENCE.put('/', 2); + PRECEDENCE.put('^', 3); + } + + private MathExpressionEvaluator() { + // Prevent instantiation of utility class + } + + /** + * Evaluates a mathematical expression string. + * + * @param expression The mathematical expression to evaluate + * @return The result of the evaluation + * @throws IllegalArgumentException If the expression is invalid + */ + public static double evaluate(String expression) { + if (expression == null) { + throw new IllegalArgumentException("Expression cannot be null"); + } + + expression = expression.replaceAll("\\s+", ""); // Remove whitespace + if (expression.isEmpty()) { + throw new IllegalArgumentException("Expression cannot be empty"); + } + + // Tokenize the expression + String[] tokens = tokenize(expression); + + // Convert infix to postfix notation using Shunting Yard algorithm + String[] postfix = infixToPostfix(tokens); + + // Evaluate the postfix expression + return evaluatePostfix(postfix); + } + + /** + * Tokenizes the expression into numbers and operators. + * + * @param expression The expression to tokenize + * @return An array of tokens + */ + private static String[] tokenize(String expression) { + java.util.List tokens = new java.util.ArrayList<>(); + StringBuilder currentNumber = new StringBuilder(); + + for (int i = 0; i < expression.length(); i++) { + char c = expression.charAt(i); + + if (Character.isDigit(c) || c == '.') { + currentNumber.append(c); + } else if (c == '(' || c == ')') { + if (currentNumber.length() > 0) { + tokens.add(currentNumber.toString()); + currentNumber.setLength(0); + } + tokens.add(String.valueOf(c)); + } else if (isOperator(c)) { + if (currentNumber.length() > 0) { + tokens.add(currentNumber.toString()); + currentNumber.setLength(0); + } + + // Handle unary minus + if (c == '-' && (i == 0 || expression.charAt(i - 1) == '(')) { + currentNumber.append(c); + } else { + tokens.add(String.valueOf(c)); + } + } else { + throw new IllegalArgumentException("Invalid character in expression: " + c); + } + } + + if (currentNumber.length() > 0) { + tokens.add(currentNumber.toString()); + } + + return tokens.toArray(new String[0]); + } + + /** + * Checks if the character is an operator. + * + * @param c The character to check + * @return true if the character is an operator, false otherwise + */ + private static boolean isOperator(char c) { + return c == '+' || c == '-' || c == '*' || c == '/' || c == '^'; + } + + /** + * Converts infix notation to postfix notation using the Shunting Yard algorithm. + * + * @param tokens The tokens in infix notation + * @return An array of tokens in postfix notation + */ + private static String[] infixToPostfix(String[] tokens) { + java.util.List output = new java.util.ArrayList<>(); + Stack operators = new Stack<>(); + + for (String token : tokens) { + if (isNumber(token)) { + output.add(token); + } else if (token.equals("(")) { + operators.push(token); + } else if (token.equals(")")) { + while (!operators.isEmpty() && !operators.peek().equals("(")) { + output.add(operators.pop()); + } + if (!operators.isEmpty()) { + operators.pop(); // Remove the "(" + } + } else if (isOperator(token.charAt(0))) { + while (!operators.isEmpty() + && isOperator(operators.peek().charAt(0)) + && PRECEDENCE.get(operators.peek().charAt(0)) >= PRECEDENCE.get(token.charAt(0))) { + output.add(operators.pop()); + } + operators.push(token); + } + } + + while (!operators.isEmpty()) { + output.add(operators.pop()); + } + + return output.toArray(new String[0]); + } + + /** + * Evaluates a postfix expression. + * + * @param postfix The tokens in postfix notation + * @return The result of the evaluation + */ + private static double evaluatePostfix(String[] postfix) { + Stack values = new Stack<>(); + + for (String token : postfix) { + if (isNumber(token)) { + values.push(Double.parseDouble(token)); + } else if (isOperator(token.charAt(0))) { + if (values.size() < 2) { + throw new IllegalArgumentException("Invalid expression: insufficient operands"); + } + + double b = values.pop(); + double a = values.pop(); + double result = performOperation(a, b, token.charAt(0)); + values.push(result); + } + } + + if (values.size() != 1) { + throw new IllegalArgumentException("Invalid expression: too many operands"); + } + + return values.pop(); + } + + /** + * Performs the specified operation on the two operands. + * + * @param a The first operand + * @param b The second operand + * @param operator The operator to apply + * @return The result of the operation + */ + private static double performOperation(double a, double b, char operator) { + switch (operator) { + case '+': + return a + b; + case '-': + return a - b; + case '*': + return a * b; + case '/': + if (b == 0) { + throw new ArithmeticException("Division by zero"); + } + return a / b; + case '^': + return Math.pow(a, b); + default: + throw new IllegalArgumentException("Unknown operator: " + operator); + } + } + + /** + * Checks if the token is a number. + * + * @param token The token to check + * @return true if the token is a number, false otherwise + */ + private static boolean isNumber(String token) { + return NUMBER_PATTERN.matcher(token).matches(); + } +} \ No newline at end of file diff --git a/src/com/personalproject/service/MathLearningService.java b/src/com/personalproject/service/MathLearningService.java new file mode 100644 index 0000000..e404ddb --- /dev/null +++ b/src/com/personalproject/service/MathLearningService.java @@ -0,0 +1,170 @@ +package com.personalproject.service; + +import com.personalproject.auth.AccountRepository; +import com.personalproject.auth.EmailService; +import com.personalproject.auth.PasswordValidator; +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; +import com.personalproject.storage.QuestionStorageService; +import java.util.Map; +import java.util.Optional; + +/** + * 为主JavaFX UI提供所有功能的服务类. 此类协调各种服务以为UI提供统一的API. + */ +public final class MathLearningService { + + private final AccountRepository accountRepository; + private final RegistrationService registrationService; + private final ExamService examService; + private final QuestionStorageService storageService; + private final ExamResultService resultService; + + /** + * 创建一个新的数学学习服务. + * + * @param generatorMap 难度级别到题目生成器的映射 + * @param questionGenerationService 题目生成服务 + */ + public MathLearningService( + Map generatorMap, + QuestionGenerationService questionGenerationService) { + this.accountRepository = new AccountRepository(); + this.registrationService = new RegistrationService(accountRepository); + this.examService = new ExamService(generatorMap, questionGenerationService); + this.storageService = new QuestionStorageService(); + this.resultService = new ExamResultService(); + } + + // 注册方法 + + /** + * 通过向提供的电子邮件发送注册码来启动注册过程. + * + * @param username 所需用户名 + * @param email 要发送注册码的电子邮件地址 + * @param difficultyLevel 选择的难度级别 + * @return 如果注册成功启动则返回true,如果电子邮件无效或用户名已存在则返回false + */ + public boolean initiateRegistration(String username, String email, + DifficultyLevel difficultyLevel) { + return registrationService.initiateRegistration(username, email, difficultyLevel); + } + + /** + * 验证用户输入的注册码. + * + * @param username 用户名 + * @param registrationCode 通过电子邮件收到的注册码 + * @return 如果注册码有效则返回true,否则返回false + */ + public boolean verifyRegistrationCode(String username, String registrationCode) { + return registrationService.verifyRegistrationCode(username, registrationCode); + } + + /** + * 在成功验证注册码后为用户设置密码. + * + * @param username 用户名 + * @param password 要设置的密码 + * @return 如果密码设置成功则返回true,如果验证失败或用户不存在则返回false + */ + public boolean setPassword(String username, String password) { + return registrationService.setPassword(username, password); + } + + /** + * 使用用户名和密码验证用户. + * + * @param username 用户名 + * @param password 密码 + * @return 如果验证成功则返回包含用户账户的Optional + */ + public Optional authenticate(String username, + String password) { + return registrationService.authenticate(username, password); + } + + /** + * 检查密码是否符合验证要求. + * + * @param password 要验证的密码 + * @return 如果密码有效则返回true,否则返回false + */ + public static boolean isValidPassword(String password) { + return PasswordValidator.isValidPassword(password); + } + + /** + * 更改现有用户的密码. + * + * @param username 用户名 + * @param oldPassword 当前密码 + * @param newPassword 新密码 + * @return 如果密码更改成功则返回true,如果验证失败或验证失败则返回false + */ + public boolean changePassword(String username, String oldPassword, String newPassword) { + return registrationService.changePassword(username, oldPassword, newPassword); + } + + /** + * 检查电子邮件地址是否具有有效格式. + * + * @param email 要验证的电子邮件地址 + * @return 如果电子邮件格式有效则返回true,否则返回false + */ + public static boolean isValidEmail(String email) { + return EmailService.isValidEmail(email); + } + + /** + * 为指定用户和难度级别创建新的考试会话. + * + * @param username 应试者用户名 + * @param difficultyLevel 考试难度级别 + * @param questionCount 考试中包含的题目数量 + * @return 新的考试会话 + */ + public ExamSession createExamSession(String username, DifficultyLevel difficultyLevel, + int questionCount) { + return examService.createExamSession(username, difficultyLevel, questionCount); + } + + /** + * 将考试结果保存到存储中. + * + * @param examSession 完成的考试会话 + * @return 保存结果文件的路径 + */ + public java.nio.file.Path saveExamResults(ExamSession examSession) { + try { + return storageService.saveExamResults(examSession); + } catch (java.io.IOException e) { + throw new RuntimeException("保存考试结果失败", e); + } + } + + /** + * 处理用户完成考试后的选择. + * + * @param examSession 完成的考试会话 + * @param continueWithSameLevel 是否继续使用相同难度级别 + * @param newDifficultyLevel 更改时的新难度级别(如果不更改则为null) + * @return 表示下一步操作的动作 + */ + public ExamResultService.ExamContinuationAction processExamResult( + ExamSession examSession, boolean continueWithSameLevel, DifficultyLevel newDifficultyLevel) { + return resultService.processExamResult(examSession, continueWithSameLevel, newDifficultyLevel); + } + + /** + * 检查用户是否存在于系统中. + * + * @param username 要检查的用户名 + * @return 如果用户存在则返回true,否则返回false + */ + public boolean userExists(String username) { + return registrationService.userExists(username); + } +} \ No newline at end of file diff --git a/src/com/personalproject/service/QuestionGenerationService.java b/src/com/personalproject/service/QuestionGenerationService.java new file mode 100644 index 0000000..b536a7d --- /dev/null +++ b/src/com/personalproject/service/QuestionGenerationService.java @@ -0,0 +1,71 @@ +package com.personalproject.service; + +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +/** + * 负责批量生成题目并避免与历史题库重复. + */ +public final class QuestionGenerationService { + + private static final int MAX_ATTEMPTS = 10_000; + private final Map generators; + private final Random random = new SecureRandom(); + + /** + * 构造函数:复制难度与生成器的映射,保留内部安全副本. + * + * @param generatorMap 难度到题目生成器的外部映射. + */ + public QuestionGenerationService(Map generatorMap) { + generators = new EnumMap<>(DifficultyLevel.class); + generators.putAll(generatorMap); + } + + /** + * 根据指定难度批量生成不与历史题目重复的新题目. + * + * @param level 目标题目难度. + * @param count 需要生成的题目数量. + * @param existingQuestions 已存在的题目集合,用于查重. + * @return 生成的题目列表. + * @throws IllegalArgumentException 当难度未配置时抛出. + * @throws IllegalStateException 当达到最大尝试次数仍无法生成足够题目时抛出. + */ + public List generateUniqueQuestions( + DifficultyLevel level, int count, Set existingQuestions) { + QuestionGenerator generator = generators.get(level); + if (generator == null) { + throw new IllegalArgumentException("Unsupported difficulty level: " + level); + } + Set produced = new HashSet<>(); + List results = new ArrayList<>(); + int attempts = 0; + while (results.size() < count) { + if (attempts >= MAX_ATTEMPTS) { + throw new IllegalStateException("Unable to generate enough unique questions."); + } + attempts++; + String question = generator.generateQuestion(random).trim(); + if (question.isEmpty()) { + continue; + } + if (existingQuestions.contains(question)) { + continue; + } + if (!produced.add(question)) { + continue; + } + results.add(question); + } + return results; + } +} diff --git a/src/com/personalproject/service/RegistrationService.java b/src/com/personalproject/service/RegistrationService.java new file mode 100644 index 0000000..100b58d --- /dev/null +++ b/src/com/personalproject/service/RegistrationService.java @@ -0,0 +1,138 @@ +package com.personalproject.service; + +import com.personalproject.auth.AccountRepository; +import com.personalproject.auth.EmailService; +import com.personalproject.auth.PasswordValidator; +import com.personalproject.model.DifficultyLevel; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 用于处理用户注册和身份验证过程的服务类. + */ +public final class RegistrationService { + + private final AccountRepository accountRepository; + private final Map pendingRegistrations; + private final Map registrationAttempts; + + /** + * 创建新的注册服务. + * + * @param accountRepository 使用的账户存储库 + */ + public RegistrationService(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + this.pendingRegistrations = new ConcurrentHashMap<>(); + this.registrationAttempts = new ConcurrentHashMap<>(); + } + + /** + * 通过向提供的电子邮件发送注册码来启动注册过程. + * + * @param username 所需用户名 + * @param email 要发送注册码的电子邮件地址 + * @param difficultyLevel 选择的难度级别 + * @return 注册成功启动则返回true,如果电子邮件无效或用户名已存在则返回false + */ + public boolean initiateRegistration(String username, String email, + DifficultyLevel difficultyLevel) { + if (!EmailService.isValidEmail(email)) { + return false; + } + + if (!accountRepository.registerUser(username, email, difficultyLevel)) { + return false; // 用户名已存在或邮箱已注册 + } + + String registrationCode = EmailService.generateRegistrationCode(); + pendingRegistrations.put(username, registrationCode); + registrationAttempts.put(username, 0); + + return EmailService.sendRegistrationCode(email, registrationCode); + } + + /** + * 通过验证注册码完成注册. + * + * @param username 用户名 + * @param registrationCode 通过电子邮件收到的注册码 + * @return 注册码有效则返回true,否则返回false + */ + public boolean verifyRegistrationCode(String username, String registrationCode) { + String storedCode = pendingRegistrations.get(username); + if (storedCode == null || !storedCode.equals(registrationCode)) { + // 跟踪失败尝试 + int attempts = registrationAttempts.getOrDefault(username, 0); + attempts++; + registrationAttempts.put(username, attempts); + + if (attempts >= 3) { + // 如果失败次数过多,则删除用户 + pendingRegistrations.remove(username); + registrationAttempts.remove(username); + return false; + } + return false; + } + + // 有效码,从待处理列表中移除 + pendingRegistrations.remove(username); + registrationAttempts.remove(username); + return true; + } + + /** + * 在成功验证注册码后为用户设置密码. + * + * @param username 用户名 + * @param password 要设置的密码 + * @return 密码设置成功则返回true,如果验证失败或用户不存在则返回false + */ + public boolean setPassword(String username, String password) { + if (!PasswordValidator.isValidPassword(password)) { + return false; + } + + return accountRepository.setPassword(username, password); + } + + /** + * 更改现有用户的密码. + * + * @param username 用户名 + * @param oldPassword 当前密码 + * @param newPassword 新密码 + * @return 密码更改成功则返回true,如果验证失败或身份验证失败则返回false + */ + public boolean changePassword(String username, String oldPassword, String newPassword) { + if (!PasswordValidator.isValidPassword(newPassword)) { + return false; + } + + return accountRepository.changePassword(username, oldPassword, newPassword); + } + + /** + * 使用用户名和密码验证用户. + * + * @param username 用户名 + * @param password 密码 + * @return 验证成功则返回包含用户账户的Optional + */ + public Optional authenticate(String username, + String password) { + return accountRepository.authenticate(username, password); + } + + /** + * 检查用户是否存在于系统中. + * + * @param username 要检查的用户名 + * @return 用户存在则返回true,否则返回false + */ + public boolean userExists(String username) { + return accountRepository.userExists(username); + } +} \ No newline at end of file diff --git a/src/com/personalproject/storage/QuestionStorageService.java b/src/com/personalproject/storage/QuestionStorageService.java new file mode 100644 index 0000000..4fc9870 --- /dev/null +++ b/src/com/personalproject/storage/QuestionStorageService.java @@ -0,0 +1,162 @@ +package com.personalproject.storage; + +import com.personalproject.model.ExamSession; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +/** + * 负责保存生成的题目并维护查重信息. + */ +public final class QuestionStorageService { + + private static final String BASE_DIRECTORY = "user_data"; + private static final String QUESTIONS_SUBDIR = "questions"; + private static final String RESULTS_SUBDIR = "results"; + private static final DateTimeFormatter FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"); + + /** + * 加载指定用户历史生成的题目,用于后续查重. + * + * @param username 用户名. + * @return 历史题目的去重集合. + * @throws IOException 当读取文件时发生 I/O 错误. + */ + public Set loadExistingQuestions(String username) throws IOException { + Path accountDirectory = getQuestionsDirectory(username); + Set questions = new HashSet<>(); + if (!Files.exists(accountDirectory)) { + return questions; + } + try (Stream paths = Files.list(accountDirectory)) { + paths + .filter(path -> path.getFileName().toString().endsWith(".txt")) + .sorted() + .forEach(path -> readQuestionsFromFile(path, questions)); + } + return questions; + } + + /** + * 将新生成的题目保存到用户目录,并返回生成的文件路径. + * + * @param username 用户名. + * @param questions 待保存的题目列表. + * @return 保存题目的文件路径. + * @throws IOException 当写入文件失败时抛出. + */ + public Path saveQuestions(String username, List questions) throws IOException { + Path accountDirectory = getQuestionsDirectory(username); + Files.createDirectories(accountDirectory); + String fileName = FORMATTER.format(LocalDateTime.now()) + ".txt"; + Path outputFile = accountDirectory.resolve(fileName); + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < questions.size(); index++) { + String question = questions.get(index); + builder + .append(index + 1) + .append(". ") + .append(question) + .append(System.lineSeparator()) + .append(System.lineSeparator()); + } + Files.writeString( + outputFile, builder.toString(), StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW); + return outputFile; + } + + /** + * 保存用户的考试结果. + * + * @param examSession 包含结果的考试会话 + * @return 保存结果文件的路径 + * @throws IOException 如果写入文件失败 + */ + public Path saveExamResults(ExamSession examSession) throws IOException { + Path resultsDirectory = getResultsDirectory(examSession.getUsername()); + Files.createDirectories(resultsDirectory); + + StringBuilder builder = new StringBuilder(); + builder.append("考试结果报告").append(System.lineSeparator()); + builder.append("用户名: ").append(examSession.getUsername()).append(System.lineSeparator()); + builder.append("难度: ").append(examSession.getDifficultyLevel().getDisplayName()) + .append(System.lineSeparator()); + builder.append("开始时间: ").append(examSession.getStartTime()).append(System.lineSeparator()); + builder.append("题目数量: ").append(examSession.getQuestions().size()) + .append(System.lineSeparator()); + builder.append("得分: ").append(String.format("%.2f", examSession.calculateScore())).append("%") + .append(System.lineSeparator()); + builder.append(System.lineSeparator()); + + // 添加逐题结果 + for (int i = 0; i < examSession.getQuestions().size(); i++) { + var question = examSession.getQuestions().get(i); + int userAnswer = examSession.getUserAnswer(i); + boolean isCorrect = question.isAnswerCorrect(userAnswer); + + builder.append("题目 ").append(i + 1).append(": ").append(question.getQuestionText()) + .append(System.lineSeparator()); + builder.append("您的答案: ").append(userAnswer == -1 ? "未回答" : + (userAnswer < question.getOptions().size() ? question.getOptions().get(userAnswer) + : "无效")).append(System.lineSeparator()); + builder.append("正确答案: ").append( + question.getOptions().get(question.getCorrectAnswerIndex())) + .append(System.lineSeparator()); + builder.append("结果: ").append(isCorrect ? "正确" : "错误").append(System.lineSeparator()); + builder.append(System.lineSeparator()); + } + + String fileName = "exam_result_" + FORMATTER.format(LocalDateTime.now()) + ".txt"; + Path outputFile = resultsDirectory.resolve(fileName); + + Files.writeString( + outputFile, builder.toString(), StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW); + return outputFile; + } + + private Path getAccountDirectory(String username) { + return Paths.get(BASE_DIRECTORY, username); + } + + private Path getQuestionsDirectory(String username) { + return getAccountDirectory(username).resolve(QUESTIONS_SUBDIR); + } + + private Path getResultsDirectory(String username) { + return getAccountDirectory(username).resolve(RESULTS_SUBDIR); + } + + private void readQuestionsFromFile(Path path, Set questions) { + try { + List lines = Files.readAllLines(path, StandardCharsets.UTF_8); + for (String line : lines) { + String trimmed = line.trim(); + if (trimmed.isEmpty()) { + continue; + } + int dotIndex = trimmed.indexOf('.'); + if (dotIndex >= 0 && dotIndex + 1 < trimmed.length()) { + String question = trimmed.substring(dotIndex + 1).trim(); + if (!question.isEmpty()) { + questions.add(question); + } + } else { + questions.add(trimmed); + } + } + } catch (IOException exception) { + System.err.println( + "读取题目文件失败:" + path + ",原因:" + exception.getMessage() + ",将跳过该文件."); + } + } +} \ No newline at end of file -- 2.34.1 From 5b16613177062cbea069f620e7fdae2dfbe8f358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E8=8A=B3=E5=AE=87?= <15528541+cheng-fangyu@user.noreply.gitee.com> Date: Tue, 7 Oct 2025 21:51:22 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E5=A2=9E=E5=8A=A0UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../personalproject/MathExamApplication.java | 330 +++++++++ .../auth/AccountRepository.java | 140 ++++ .../personalproject/auth/EmailService.java | 62 ++ .../auth/PasswordValidator.java | 30 + src/com/personalproject/auth/UserAccount.java | 40 ++ .../controller/MathLearningController.java | 145 ++++ .../HighSchoolQuestionGenerator.java | 37 + .../MiddleSchoolQuestionGenerator.java | 39 ++ .../generator/PrimaryQuestionGenerator.java | 31 + .../generator/QuestionGenerator.java | 17 + .../model/DifficultyLevel.java | 49 ++ .../personalproject/model/ExamSession.java | 195 ++++++ .../personalproject/model/QuizQuestion.java | 73 ++ .../service/ExamResultService.java | 83 +++ .../personalproject/service/ExamService.java | 140 ++++ .../service/MathExpressionEvaluator.java | 217 ++++++ .../service/MathLearningService.java | 170 +++++ .../service/QuestionGenerationService.java | 71 ++ .../service/RegistrationService.java | 138 ++++ .../storage/QuestionStorageService.java | 162 +++++ src/com/personalproject/ui/DesktopApp.java | 658 ++++++++++++++++++ 21 files changed, 2827 insertions(+) create mode 100644 src/com/personalproject/MathExamApplication.java create mode 100644 src/com/personalproject/auth/AccountRepository.java create mode 100644 src/com/personalproject/auth/EmailService.java create mode 100644 src/com/personalproject/auth/PasswordValidator.java create mode 100644 src/com/personalproject/auth/UserAccount.java create mode 100644 src/com/personalproject/controller/MathLearningController.java create mode 100644 src/com/personalproject/generator/HighSchoolQuestionGenerator.java create mode 100644 src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java create mode 100644 src/com/personalproject/generator/PrimaryQuestionGenerator.java create mode 100644 src/com/personalproject/generator/QuestionGenerator.java create mode 100644 src/com/personalproject/model/DifficultyLevel.java create mode 100644 src/com/personalproject/model/ExamSession.java create mode 100644 src/com/personalproject/model/QuizQuestion.java create mode 100644 src/com/personalproject/service/ExamResultService.java create mode 100644 src/com/personalproject/service/ExamService.java create mode 100644 src/com/personalproject/service/MathExpressionEvaluator.java create mode 100644 src/com/personalproject/service/MathLearningService.java create mode 100644 src/com/personalproject/service/QuestionGenerationService.java create mode 100644 src/com/personalproject/service/RegistrationService.java create mode 100644 src/com/personalproject/storage/QuestionStorageService.java create mode 100644 src/com/personalproject/ui/DesktopApp.java diff --git a/src/com/personalproject/MathExamApplication.java b/src/com/personalproject/MathExamApplication.java new file mode 100644 index 0000000..35faa4c --- /dev/null +++ b/src/com/personalproject/MathExamApplication.java @@ -0,0 +1,330 @@ +package com.personalproject; + +import com.personalproject.controller.MathLearningController; +import com.personalproject.generator.HighSchoolQuestionGenerator; +import com.personalproject.generator.MiddleSchoolQuestionGenerator; +import com.personalproject.generator.PrimaryQuestionGenerator; +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; +import com.personalproject.service.QuestionGenerationService; +import java.util.EnumMap; +import java.util.Map; +import java.util.Optional; +import java.util.Scanner; + +/** + * 数学学习软件主应用程序入口. 采用MVC架构模式,此为主类,控制器层处理业务逻辑. + */ +public final class MathExamApplication { + + private final MathLearningController controller; + + /** + * 构造函数:初始化难度与题目生成器的映射关系,并创建控制器. + */ + public MathExamApplication() { + Map generatorMap = new EnumMap<>(DifficultyLevel.class); + generatorMap.put(DifficultyLevel.PRIMARY, new PrimaryQuestionGenerator()); + generatorMap.put(DifficultyLevel.MIDDLE, new MiddleSchoolQuestionGenerator()); + generatorMap.put(DifficultyLevel.HIGH, new HighSchoolQuestionGenerator()); + QuestionGenerationService questionGenerationService = new QuestionGenerationService( + generatorMap); + this.controller = new MathLearningController(generatorMap, questionGenerationService); + } + + /** + * 程序入口:创建应用实例并启动交互流程. + * + * @param args 命令行参数,当前未使用. + */ + public static void main(String[] args) { + MathExamApplication application = new MathExamApplication(); + application.run(); + } + + /** + * 主循环:保持读取输入并驱动登录及考试流程. + */ + private void run() { + Scanner scanner = new Scanner(System.in); + while (true) { + System.out.println("=== 欢迎使用数学学习软件 ==="); + System.out.println("1. 登录"); + System.out.println("2. 注册"); + System.out.println("3. 退出"); + System.out.print("请选择操作 (1-3): "); + + String choice = scanner.nextLine().trim(); + + switch (choice) { + case "1" -> handleLogin(scanner); + case "2" -> handleRegistration(scanner); + case "3" -> { + System.out.println("感谢使用,再见!"); + return; + } + default -> System.out.println("无效选择,请重新输入。"); + } + } + } + + /** + * 处理用户登录流程. + * + * @param scanner 读取控制台输入的扫描器 + */ + private void handleLogin(Scanner scanner) { + System.out.print("请输入用户名: "); + String username = scanner.nextLine().trim(); + + System.out.print("请输入密码: "); + String password = scanner.nextLine().trim(); + + Optional userAccount = + controller.authenticate(username, password); + + if (userAccount.isPresent()) { + System.out.println("登录成功!欢迎, " + userAccount.get().username()); + handleUserSession(scanner, userAccount.get()); + } else { + System.out.println("登录失败:用户名或密码错误。"); + } + } + + /** + * 处理用户注册流程. + * + * @param scanner 读取控制台输入的扫描器 + */ + private void handleRegistration(Scanner scanner) { + System.out.print("请输入用户名: "); + final String username = scanner.nextLine().trim(); + + System.out.print("请输入邮箱: "); + final String email = scanner.nextLine().trim(); + + if (!controller.isValidEmail(email)) { + System.out.println("邮箱格式不正确。"); + return; + } + + System.out.println("请选择难度级别:"); + System.out.println("1. 小学"); + System.out.println("2. 初中"); + System.out.println("3. 高中"); + System.out.print("请选择 (1-3): "); + + int levelChoice = -1; + try { + levelChoice = Integer.parseInt(scanner.nextLine().trim()); + } catch (NumberFormatException e) { + System.out.println("输入格式不正确。"); + return; + } + + DifficultyLevel difficultyLevel; + switch (levelChoice) { + case 1 -> difficultyLevel = DifficultyLevel.PRIMARY; + case 2 -> difficultyLevel = DifficultyLevel.MIDDLE; + case 3 -> difficultyLevel = DifficultyLevel.HIGH; + default -> { + System.out.println("无效的选择。"); + return; + } + } + + // 发送注册码 + if (controller.initiateRegistration(username, email, difficultyLevel)) { + System.out.println("注册码已发送至您的邮箱,请查收。"); + + System.out.print("请输入收到的注册码: "); + String registrationCode = scanner.nextLine().trim(); + + if (controller.verifyRegistrationCode(username, registrationCode)) { + System.out.println("注册码验证成功!"); + + // 设置密码 + while (true) { + System.out.print("请设置密码 (6-10位,包含大小写字母和数字): "); + String password = scanner.nextLine().trim(); + + if (!controller.isValidPassword(password)) { + System.out.println("密码不符合要求,请重新设置。"); + continue; + } + + if (controller.setPassword(username, password)) { + System.out.println("注册成功!请登录。"); + break; + } else { + System.out.println("设置密码失败,请重试。"); + } + } + } else { + System.out.println("注册码验证失败,请检查后重试。"); + } + } else { + System.out.println("注册失败:用户名或邮箱可能已存在。"); + } + } + + /** + * 处理用户会话,包括考试等操作. + * + * @param scanner 读取控制台输入的扫描器 + * @param userAccount 当前登录的用户账户 + */ + private void handleUserSession(Scanner scanner, + com.personalproject.auth.UserAccount userAccount) { + while (true) { + System.out.println("\n=== 用户菜单 ==="); + System.out.println("1. 开始考试"); + System.out.println("2. 修改密码"); + System.out.println("3. 退出账号"); + System.out.print("请选择操作 (1-3): "); + + String choice = scanner.nextLine().trim(); + + switch (choice) { + case "1" -> handleExamSession(scanner, userAccount); + case "2" -> handleChangePassword(scanner, userAccount); + case "3" -> { + System.out.println("已退出账号。"); + return; + } + default -> System.out.println("无效选择,请重新输入。"); + } + } + } + + /** + * 处理考试会话. + * + * @param scanner 读取控制台输入的扫描器 + * @param userAccount 当前登录的用户账户 + */ + private void handleExamSession(Scanner scanner, + com.personalproject.auth.UserAccount userAccount) { + System.out.println("当前选择难度: " + userAccount.difficultyLevel().getDisplayName()); + + System.out.print("请输入生成题目数量 (10-30): "); + int questionCount = -1; + try { + questionCount = Integer.parseInt(scanner.nextLine().trim()); + } catch (NumberFormatException e) { + System.out.println("输入格式不正确。"); + return; + } + + if (questionCount < 10 || questionCount > 30) { + System.out.println("题目数量必须在10到30之间。"); + return; + } + + // 创建考试会话 + ExamSession examSession = + controller.createExamSession(userAccount.username(), userAccount.difficultyLevel(), + questionCount); + + // 进行考试 + conductExam(scanner, examSession); + + // 保存结果 + controller.saveExamResults(examSession); + + System.out.printf("考试结束!您的得分: %.2f%%\n", examSession.calculateScore()); + + // 询问用户是否继续或退出 + System.out.println("1. 继续考试"); + System.out.println("2. 退出"); + System.out.print("请选择 (1-2): "); + + String choice = scanner.nextLine().trim(); + if (choice.equals("1")) { + handleExamSession(scanner, userAccount); + } + } + + /** + * 通过导航问题进行考试. + * + * @param scanner 用户输入的扫描器 + * @param examSession 要进行的考试会话 + */ + private void conductExam(Scanner scanner, ExamSession examSession) { + while (!examSession.isComplete()) { + var currentQuestion = examSession.getCurrentQuestion(); + System.out.printf("\n第 %d 题: %s\n", examSession.getCurrentQuestionIndex() + 1, + currentQuestion.getQuestionText()); + + // 打印选项 + for (int i = 0; i < currentQuestion.getOptions().size(); i++) { + System.out.printf("%d. %s\n", i + 1, currentQuestion.getOptions().get(i)); + } + + System.out.print( + "请选择答案 (1-" + currentQuestion.getOptions().size() + ", 0 返回上一题): "); + int choice = -1; + try { + choice = Integer.parseInt(scanner.nextLine().trim()); + } catch (NumberFormatException e) { + System.out.println("输入格式不正确,请重新输入。"); + continue; + } + + if (choice == 0) { + // 如果可能,转到上一个问题 + if (!examSession.goToPreviousQuestion()) { + System.out.println("已经是第一题了。"); + } + continue; + } + + if (choice < 1 || choice > currentQuestion.getOptions().size()) { + System.out.println("无效的选择,请重新输入。"); + continue; + } + + // 设置答案(从基于1的索引调整为基于0的索引) + examSession.setAnswer(choice - 1); + + // 转到下一题 + if (!examSession.goToNextQuestion()) { + // 如果无法转到下一题,意味着已到达末尾 + break; + } + } + } + + /** + * 处理用户的密码更改. + * + * @param scanner 读取控制台输入的扫描器 + * @param userAccount 当前登录的用户账户 + */ + private void handleChangePassword(Scanner scanner, + com.personalproject.auth.UserAccount userAccount) { + System.out.print("请输入当前密码: "); + String oldPassword = scanner.nextLine().trim(); + + if (!userAccount.password().equals(oldPassword)) { + System.out.println("当前密码错误。"); + return; + } + + System.out.print("请输入新密码 (6-10位,包含大小写字母和数字): "); + String newPassword = scanner.nextLine().trim(); + + if (!controller.isValidPassword(newPassword)) { + System.out.println("新密码不符合要求。"); + return; + } + + if (controller.changePassword(userAccount.username(), oldPassword, newPassword)) { + System.out.println("密码修改成功!"); + } else { + System.out.println("密码修改失败。"); + } + } +} \ No newline at end of file diff --git a/src/com/personalproject/auth/AccountRepository.java b/src/com/personalproject/auth/AccountRepository.java new file mode 100644 index 0000000..c5a00a2 --- /dev/null +++ b/src/com/personalproject/auth/AccountRepository.java @@ -0,0 +1,140 @@ +package com.personalproject.auth; + +import com.personalproject.model.DifficultyLevel; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * 存放用户账号并提供认证和注册查询能力. + */ +public final class AccountRepository { + + private final Map accounts = new HashMap<>(); + private final Map registrationCodes = new HashMap<>(); + + /** + * 根据用户名与密码查找匹配的账号. + * + * @param username 用户名. + * @param password 密码. + * @return 匹配成功时返回账号信息,否则返回空结果. + */ + public Optional authenticate(String username, String password) { + if (username == null || password == null) { + return Optional.empty(); + } + UserAccount account = accounts.get(username.trim()); + if (account == null || !account.isRegistered()) { + return Optional.empty(); + } + if (!account.password().equals(password.trim())) { + return Optional.empty(); + } + return Optional.of(account); + } + + /** + * Registers a new user account with email. + * + * @param username The username + * @param email The email address + * @param difficultyLevel The selected difficulty level + * @return true if registration was successful, false if username already exists + */ + public boolean registerUser(String username, String email, DifficultyLevel difficultyLevel) { + String trimmedUsername = username.trim(); + String trimmedEmail = email.trim(); + + if (accounts.containsKey(trimmedUsername)) { + return false; // Username already exists + } + + // Check if email is already used by another account + for (UserAccount account : accounts.values()) { + if (account.email().equals(trimmedEmail) && account.isRegistered()) { + return false; // Email already registered + } + } + + UserAccount newAccount = new UserAccount( + trimmedUsername, + trimmedEmail, + "", // Empty password initially + difficultyLevel, + LocalDateTime.now(), + false); // Not registered until password is set + accounts.put(trimmedUsername, newAccount); + return true; + } + + /** + * Sets the password for a user after registration. + * + * @param username The username + * @param password The password to set + * @return true if successful, false if user doesn't exist + */ + public boolean setPassword(String username, String password) { + UserAccount account = accounts.get(username.trim()); + if (account == null) { + return false; + } + + UserAccount updatedAccount = new UserAccount( + account.username(), + account.email(), + password, + account.difficultyLevel(), + account.registrationDate(), + true); // Now registered + accounts.put(username.trim(), updatedAccount); + return true; + } + + /** + * Changes the password for an existing user. + * + * @param username The username + * @param oldPassword The current password + * @param newPassword The new password + * @return true if successful, false if old password is incorrect or user doesn't exist + */ + public boolean changePassword(String username, String oldPassword, String newPassword) { + UserAccount account = accounts.get(username.trim()); + if (account == null || !account.password().equals(oldPassword) || !account.isRegistered()) { + return false; + } + + UserAccount updatedAccount = new UserAccount( + account.username(), + account.email(), + newPassword, + account.difficultyLevel(), + account.registrationDate(), + true); + accounts.put(username.trim(), updatedAccount); + return true; + } + + /** + * Checks if a user exists in the system. + * + * @param username The username to check + * @return true if user exists, false otherwise + */ + public boolean userExists(String username) { + return accounts.containsKey(username.trim()); + } + + /** + * Gets a user account by username. + * + * @param username The username + * @return Optional containing the user account if found + */ + public Optional getUser(String username) { + return Optional.ofNullable(accounts.get(username.trim())); + } +} diff --git a/src/com/personalproject/auth/EmailService.java b/src/com/personalproject/auth/EmailService.java new file mode 100644 index 0000000..d790c83 --- /dev/null +++ b/src/com/personalproject/auth/EmailService.java @@ -0,0 +1,62 @@ +package com.personalproject.auth; + +import java.util.Random; + +/** + * Interface for sending emails with registration codes. + */ +public final class EmailService { + + private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + private static final int CODE_LENGTH = 6; + private static final Random RANDOM = new Random(); + + private EmailService() { + // Prevent instantiation of utility class + } + + /** + * Generates a random registration code. + * + * @return A randomly generated registration code + */ + public static String generateRegistrationCode() { + StringBuilder code = new StringBuilder(); + for (int i = 0; i < CODE_LENGTH; i++) { + code.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length()))); + } + return code.toString(); + } + + /** + * Sends a registration code to the specified email address. In a real implementation, this would + * connect to an email server. + * + * @param email The email address to send the code to + * @param registrationCode The registration code to send + * @return true if successfully sent (in this mock implementation, always true) + */ + public static boolean sendRegistrationCode(String email, String registrationCode) { + // In a real implementation, this would connect to an email server + // For the mock implementation, we'll just print to console + System.out.println("Sending registration code " + registrationCode + " to " + email); + return true; + } + + /** + * Validates if an email address has a valid format. + * + * @param email The email address to validate + * @return true if the email has valid format, false otherwise + */ + public static boolean isValidEmail(String email) { + if (email == null || email.trim().isEmpty()) { + return false; + } + + // Simple email validation using regex + String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@" + + "(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"; + return email.matches(emailRegex); + } +} \ No newline at end of file diff --git a/src/com/personalproject/auth/PasswordValidator.java b/src/com/personalproject/auth/PasswordValidator.java new file mode 100644 index 0000000..acece6f --- /dev/null +++ b/src/com/personalproject/auth/PasswordValidator.java @@ -0,0 +1,30 @@ +package com.personalproject.auth; + +import java.util.regex.Pattern; + +/** + * Utility class for password validation. + */ +public final class PasswordValidator { + + private static final Pattern PASSWORD_PATTERN = + Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{6,10}$"); + + private PasswordValidator() { + // Prevent instantiation of utility class + } + + /** + * Validates if a password meets the requirements: - 6-10 characters - Contains at least one + * uppercase letter - Contains at least one lowercase letter - Contains at least one digit. + * + * @param password The password to validate + * @return true if password meets requirements, false otherwise + */ + public static boolean isValidPassword(String password) { + if (password == null) { + return false; + } + return PASSWORD_PATTERN.matcher(password).matches(); + } +} \ No newline at end of file diff --git a/src/com/personalproject/auth/UserAccount.java b/src/com/personalproject/auth/UserAccount.java new file mode 100644 index 0000000..80d5acf --- /dev/null +++ b/src/com/personalproject/auth/UserAccount.java @@ -0,0 +1,40 @@ +package com.personalproject.auth; + +import com.personalproject.model.DifficultyLevel; +import java.time.LocalDateTime; + +/** + * 不可变的账号定义. + */ +public record UserAccount( + String username, + String email, + String password, + DifficultyLevel difficultyLevel, + LocalDateTime registrationDate, + boolean isRegistered) { + + /** + * Creates a new user account with registration date set to now. + * + * @param username The username + * @param email The email address + * @param password The password + * @param difficultyLevel The selected difficulty level + * @param isRegistered Whether the user has completed registration + */ + public UserAccount { + if (username == null || username.trim().isEmpty()) { + throw new IllegalArgumentException("Username cannot be null or empty"); + } + if (email == null || email.trim().isEmpty()) { + throw new IllegalArgumentException("Email cannot be null or empty"); + } + if (password == null) { + throw new IllegalArgumentException("Password cannot be null"); + } + if (difficultyLevel == null) { + throw new IllegalArgumentException("Difficulty level cannot be null"); + } + } +} diff --git a/src/com/personalproject/controller/MathLearningController.java b/src/com/personalproject/controller/MathLearningController.java new file mode 100644 index 0000000..c847b94 --- /dev/null +++ b/src/com/personalproject/controller/MathLearningController.java @@ -0,0 +1,145 @@ +package com.personalproject.controller; + +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; +import com.personalproject.service.ExamResultService; +import com.personalproject.service.MathLearningService; +import com.personalproject.service.QuestionGenerationService; +import java.util.Map; +import java.util.Optional; + +/** + * 遵循MVC模式处理应用程序逻辑的控制器类. + */ +public final class MathLearningController { + + private final MathLearningService mathLearningService; + + /** + * 创建具有所需服务的新控制器. + * + * @param generatorMap 难度级别到题目生成器的映射 + * @param questionGenerationService 题目生成服务 + */ + public MathLearningController( + Map generatorMap, + QuestionGenerationService questionGenerationService) { + this.mathLearningService = new MathLearningService(generatorMap, questionGenerationService); + } + + /** + * 启动用户注册. + * + * @param username 所需用户名 + * @param email 用户的电子邮箱地址 + * @param difficultyLevel 选择的难度级别 + * @return 注册成功启动则返回true,否则返回false + */ + public boolean initiateRegistration(String username, String email, + DifficultyLevel difficultyLevel) { + return mathLearningService.initiateRegistration(username, email, difficultyLevel); + } + + /** + * 验证注册码. + * + * @param username 用户名 + * @param registrationCode 注册码 + * @return 码有效则返回true,否则返回false + */ + public boolean verifyRegistrationCode(String username, String registrationCode) { + return mathLearningService.verifyRegistrationCode(username, registrationCode); + } + + /** + * 注册后设置用户密码. + * + * @param username 用户名 + * @param password 要设置的密码 + * @return 密码设置成功则返回true,否则返回false + */ + public boolean setPassword(String username, String password) { + return mathLearningService.setPassword(username, password); + } + + /** + * 验证用户. + * + * @param username 用户名 + * @param password 密码 + * @return 验证成功则返回包含用户账户的Optional + */ + public Optional authenticate(String username, + String password) { + return mathLearningService.authenticate(username, password); + } + + /** + * 为用户创建考试会话. + * + * @param username 用户名 + * @param difficultyLevel 难度级别 + * @param questionCount 题目数量 + * @return 创建的考试会话 + */ + public ExamSession createExamSession(String username, DifficultyLevel difficultyLevel, + int questionCount) { + return mathLearningService.createExamSession(username, difficultyLevel, questionCount); + } + + /** + * 保存考试结果. + * + * @param examSession 要保存的考试会话 + */ + public void saveExamResults(ExamSession examSession) { + mathLearningService.saveExamResults(examSession); + } + + /** + * 处理考试结果并确定下一步操作. + * + * @param examSession 完成的考试会话 + * @param continueWithSameLevel 是否继续相同难度 + * @param newDifficultyLevel 更改时的新难度(如果不更改则为null) + * @return 要执行的下一步操作 + */ + public ExamResultService.ExamContinuationAction processExamResult( + ExamSession examSession, boolean continueWithSameLevel, DifficultyLevel newDifficultyLevel) { + return mathLearningService.processExamResult(examSession, continueWithSameLevel, + newDifficultyLevel); + } + + /** + * 更改用户密码. + * + * @param username 用户名 + * @param oldPassword 旧密码 + * @param newPassword 新密码 + * @return 密码更改成功则返回true,否则返回false + */ + public boolean changePassword(String username, String oldPassword, String newPassword) { + return mathLearningService.changePassword(username, oldPassword, newPassword); + } + + /** + * 验证密码. + * + * @param password 要验证的密码 + * @return 密码有效则返回true,否则返回false + */ + public boolean isValidPassword(String password) { + return MathLearningService.isValidPassword(password); + } + + /** + * 验证电子邮箱地址. + * + * @param email 要验证的邮箱 + * @return 邮箱有效则返回true,否则返回false + */ + public boolean isValidEmail(String email) { + return MathLearningService.isValidEmail(email); + } +} \ No newline at end of file diff --git a/src/com/personalproject/generator/HighSchoolQuestionGenerator.java b/src/com/personalproject/generator/HighSchoolQuestionGenerator.java new file mode 100644 index 0000000..d22bf53 --- /dev/null +++ b/src/com/personalproject/generator/HighSchoolQuestionGenerator.java @@ -0,0 +1,37 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 生成至少包含一种三角函数的高中难度题目表达式. + */ +public final class HighSchoolQuestionGenerator implements QuestionGenerator { + + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + private static final String[] TRIG_FUNCTIONS = {"sin", "cos", "tan"}; + + @Override + public String generateQuestion(Random random) { + int operandCount = random.nextInt(5) + 1; + String[] operands = new String[operandCount]; + for (int index = 0; index < operandCount; index++) { + operands[index] = String.valueOf(random.nextInt(100) + 1); + } + int specialIndex = random.nextInt(operandCount); + String function = TRIG_FUNCTIONS[random.nextInt(TRIG_FUNCTIONS.length)]; + operands[specialIndex] = function + '(' + operands[specialIndex] + ')'; + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < operandCount; index++) { + if (index > 0) { + String operator = OPERATORS[random.nextInt(OPERATORS.length)]; + builder.append(' ').append(operator).append(' '); + } + builder.append(operands[index]); + } + String expression = builder.toString(); + if (operandCount > 1 && random.nextBoolean()) { + return '(' + expression + ')'; + } + return expression; + } +} diff --git a/src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java b/src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java new file mode 100644 index 0000000..ad46678 --- /dev/null +++ b/src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java @@ -0,0 +1,39 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 生成包含平方或开根号运算的初中难度题目表达式. + */ +public final class MiddleSchoolQuestionGenerator implements QuestionGenerator { + + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + + @Override + public String generateQuestion(Random random) { + int operandCount = random.nextInt(5) + 1; + String[] operands = new String[operandCount]; + for (int index = 0; index < operandCount; index++) { + operands[index] = String.valueOf(random.nextInt(100) + 1); + } + int specialIndex = random.nextInt(operandCount); + if (random.nextBoolean()) { + operands[specialIndex] = '(' + operands[specialIndex] + ")^2"; + } else { + operands[specialIndex] = "sqrt(" + operands[specialIndex] + ')'; + } + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < operandCount; index++) { + if (index > 0) { + String operator = OPERATORS[random.nextInt(OPERATORS.length)]; + builder.append(' ').append(operator).append(' '); + } + builder.append(operands[index]); + } + String expression = builder.toString(); + if (operandCount > 1 && random.nextBoolean()) { + return '(' + expression + ')'; + } + return expression; + } +} diff --git a/src/com/personalproject/generator/PrimaryQuestionGenerator.java b/src/com/personalproject/generator/PrimaryQuestionGenerator.java new file mode 100644 index 0000000..9073153 --- /dev/null +++ b/src/com/personalproject/generator/PrimaryQuestionGenerator.java @@ -0,0 +1,31 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 生成包含基础四则运算的小学难度题目表达式. + */ +public final class PrimaryQuestionGenerator implements QuestionGenerator { + + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + + @Override + public String generateQuestion(Random random) { + // 至少生成两个操作数,避免题目退化成单个数字 + int operandCount = random.nextInt(4) + 2; + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < operandCount; index++) { + if (index > 0) { + String operator = OPERATORS[random.nextInt(OPERATORS.length)]; + builder.append(' ').append(operator).append(' '); + } + int value = random.nextInt(100) + 1; + builder.append(value); + } + String expression = builder.toString(); + if (operandCount > 1 && random.nextBoolean()) { + return '(' + expression + ')'; + } + return expression; + } +} diff --git a/src/com/personalproject/generator/QuestionGenerator.java b/src/com/personalproject/generator/QuestionGenerator.java new file mode 100644 index 0000000..a2a5ea7 --- /dev/null +++ b/src/com/personalproject/generator/QuestionGenerator.java @@ -0,0 +1,17 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 负责生成单条数学题目的表达式. + */ +public interface QuestionGenerator { + + /** + * 基于提供的随机数生成器构造一道题目的表达式. + * + * @param random 用于生成随机数的实例. + * @return 生成的题目表达式. + */ + String generateQuestion(Random random); +} diff --git a/src/com/personalproject/model/DifficultyLevel.java b/src/com/personalproject/model/DifficultyLevel.java new file mode 100644 index 0000000..bc654bb --- /dev/null +++ b/src/com/personalproject/model/DifficultyLevel.java @@ -0,0 +1,49 @@ +package com.personalproject.model; + +import java.util.Optional; + +/** + * 系统支持的出题难度级别. + */ +public enum DifficultyLevel { + PRIMARY("小学"), + MIDDLE("初中"), + HIGH("高中"); + + private final String displayName; + + DifficultyLevel(String displayName) { + this.displayName = displayName; + } + + /** + * 获取当前难度对应的展示名称. + * + * @return 难度的展示名称. + */ + public String getDisplayName() { + return displayName; + } + + /** + * 根据展示名称查找对应的难度枚举值. + * + * @param name 展示名称. + * @return 匹配到的难度枚举,可空返回. + */ + public static Optional fromDisplayName(String name) { + if (name == null) { + return Optional.empty(); + } + String trimmed = name.trim(); + if (trimmed.isEmpty()) { + return Optional.empty(); + } + for (DifficultyLevel level : values()) { + if (level.displayName.equals(trimmed)) { + return Optional.of(level); + } + } + return Optional.empty(); + } +} diff --git a/src/com/personalproject/model/ExamSession.java b/src/com/personalproject/model/ExamSession.java new file mode 100644 index 0000000..6437ba5 --- /dev/null +++ b/src/com/personalproject/model/ExamSession.java @@ -0,0 +1,195 @@ +package com.personalproject.model; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents an exam session for a user. + */ +public final class ExamSession { + + private final String username; + private final DifficultyLevel difficultyLevel; + private final List questions; + private final List userAnswers; + private final LocalDateTime startTime; + private int currentQuestionIndex; + + /** + * Creates a new exam session. + * + * @param username The username of the test taker + * @param difficultyLevel The difficulty level of the exam + * @param questions The list of questions for the exam + */ + public ExamSession(String username, DifficultyLevel difficultyLevel, + List questions) { + if (username == null || username.trim().isEmpty()) { + throw new IllegalArgumentException("Username cannot be null or empty"); + } + if (difficultyLevel == null) { + throw new IllegalArgumentException("Difficulty level cannot be null"); + } + if (questions == null || questions.isEmpty()) { + throw new IllegalArgumentException("Questions list cannot be null or empty"); + } + + this.username = username; + this.difficultyLevel = difficultyLevel; + this.questions = List.copyOf(questions); // Immutable copy of questions + this.userAnswers = new ArrayList<>(); + // Initialize user answers with -1 (no answer selected) + for (int i = 0; i < questions.size(); i++) { + userAnswers.add(-1); + } + this.startTime = LocalDateTime.now(); + this.currentQuestionIndex = 0; + } + + /** + * Gets the username of the test taker. + * + * @return The username + */ + public String getUsername() { + return username; + } + + /** + * Gets the difficulty level of the exam. + * + * @return The difficulty level + */ + public DifficultyLevel getDifficultyLevel() { + return difficultyLevel; + } + + /** + * Gets the list of questions in the exam. + * + * @return An unmodifiable list of questions + */ + public List getQuestions() { + return questions; + } + + /** + * Gets the user's answers to the questions. + * + * @return A list of answer indices (-1 means no answer selected) + */ + public List getUserAnswers() { + return List.copyOf(userAnswers); // Return a copy to prevent modification + } + + /** + * Gets the current question index. + * + * @return The current question index + */ + public int getCurrentQuestionIndex() { + return currentQuestionIndex; + } + + /** + * Sets the user's answer for the current question. + * + * @param answerIndex The index of the selected answer + */ + public void setAnswer(int answerIndex) { + if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) { + throw new IllegalStateException("No valid question at current index"); + } + if (answerIndex < 0 || answerIndex > questions.get(currentQuestionIndex).getOptions().size()) { + throw new IllegalArgumentException("Invalid answer index"); + } + userAnswers.set(currentQuestionIndex, answerIndex); + } + + /** + * Moves to the next question. + * + * @return true if successfully moved to next question, false if already at the last question + */ + public boolean goToNextQuestion() { + if (currentQuestionIndex < questions.size() - 1) { + currentQuestionIndex++; + return true; + } + return false; + } + + /** + * Moves to the previous question. + * + * @return true if successfully moved to previous question, false if already at the first question + */ + public boolean goToPreviousQuestion() { + if (currentQuestionIndex > 0) { + currentQuestionIndex--; + return true; + } + return false; + } + + /** + * Checks if the exam is complete (all questions answered or at the end). + * + * @return true if the exam is complete, false otherwise + */ + public boolean isComplete() { + return currentQuestionIndex >= questions.size() - 1; + } + + /** + * Gets the current question. + * + * @return The current quiz question + */ + public QuizQuestion getCurrentQuestion() { + if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) { + throw new IllegalStateException("No valid question at current index"); + } + return questions.get(currentQuestionIndex); + } + + /** + * Gets the user's answer for a specific question. + * + * @param questionIndex The index of the question + * @return The index of the user's answer (or -1 if no answer selected) + */ + public int getUserAnswer(int questionIndex) { + if (questionIndex < 0 || questionIndex >= questions.size()) { + throw new IllegalArgumentException("Question index out of bounds"); + } + return userAnswers.get(questionIndex); + } + + /** + * Calculates the score as a percentage. + * + * @return The score as a percentage (0-100) + */ + public double calculateScore() { + int correctCount = 0; + for (int i = 0; i < questions.size(); i++) { + QuizQuestion question = questions.get(i); + int userAnswer = userAnswers.get(i); + if (userAnswer != -1 && question.isAnswerCorrect(userAnswer)) { + correctCount++; + } + } + return questions.isEmpty() ? 0.0 : (double) correctCount / questions.size() * 100.0; + } + + /** + * Gets the start time of the exam. + * + * @return The start time + */ + public LocalDateTime getStartTime() { + return startTime; + } +} \ No newline at end of file diff --git a/src/com/personalproject/model/QuizQuestion.java b/src/com/personalproject/model/QuizQuestion.java new file mode 100644 index 0000000..e66f14c --- /dev/null +++ b/src/com/personalproject/model/QuizQuestion.java @@ -0,0 +1,73 @@ +package com.personalproject.model; + +import java.util.List; + +/** + * Represents a quiz question with multiple choice options. + */ +public final class QuizQuestion { + + private final String questionText; + private final List options; + private final int correctAnswerIndex; + + /** + * Creates a new quiz question. + * + * @param questionText The text of the question + * @param options The list of answer options + * @param correctAnswerIndex The index of the correct answer in the options list + */ + public QuizQuestion(String questionText, List options, int correctAnswerIndex) { + if (questionText == null || questionText.trim().isEmpty()) { + throw new IllegalArgumentException("Question text cannot be null or empty"); + } + if (options == null || options.size() < 2) { + throw new IllegalArgumentException("Options must contain at least 2 choices"); + } + if (correctAnswerIndex < 0 || correctAnswerIndex >= options.size()) { + throw new IllegalArgumentException("Correct answer index out of bounds"); + } + + this.questionText = questionText; + this.options = List.copyOf(options); // Immutable copy + this.correctAnswerIndex = correctAnswerIndex; + } + + /** + * Gets the question text. + * + * @return The question text + */ + public String getQuestionText() { + return questionText; + } + + /** + * Gets the list of answer options. + * + * @return An unmodifiable list of answer options + */ + public List getOptions() { + return options; + } + + /** + * Gets the index of the correct answer in the options list. + * + * @return The index of the correct answer + */ + public int getCorrectAnswerIndex() { + return correctAnswerIndex; + } + + /** + * Checks if the given answer index matches the correct answer. + * + * @param answerIndex The index of the user's answer + * @return true if the answer is correct, false otherwise + */ + public boolean isAnswerCorrect(int answerIndex) { + return answerIndex == correctAnswerIndex; + } +} \ No newline at end of file diff --git a/src/com/personalproject/service/ExamResultService.java b/src/com/personalproject/service/ExamResultService.java new file mode 100644 index 0000000..55a79f6 --- /dev/null +++ b/src/com/personalproject/service/ExamResultService.java @@ -0,0 +1,83 @@ +package com.personalproject.service; + +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; + +/** + * 用于处理考试后选项(如继续或退出)的服务. + */ +public final class ExamResultService { + + /** + * 评估用户在完成考试后的选择. + * + * @param examSession 完成的考试会话 + * @param continueWithSameLevel 是否使用相同难度级别继续 + * @param newDifficultyLevel 更改时的新难度级别(如果不更改则为null) + * @return 指示下一步操作的操作 + */ + public ExamContinuationAction processExamResult( + ExamSession examSession, boolean continueWithSameLevel, DifficultyLevel newDifficultyLevel) { + + if (continueWithSameLevel) { + return new ExamContinuationAction( + true, examSession.getDifficultyLevel(), (int) examSession.getQuestions().size()); + } else if (newDifficultyLevel != null) { + return new ExamContinuationAction(true, newDifficultyLevel, + (int) examSession.getQuestions().size()); + } else { + return new ExamContinuationAction(false, null, 0); + } + } + + /** + * 表示完成考试后要采取的操作. + */ + public static final class ExamContinuationAction { + + private final boolean shouldContinue; + private final DifficultyLevel nextDifficultyLevel; + private final int nextQuestionCount; + + /** + * 创建新的考试延续操作. + * + * @param shouldContinue 用户是否想继续参加另一次考试 + * @param nextDifficultyLevel 下次考试的难度级别(如果不继续则为null) + * @param nextQuestionCount 下次考试的题目数量(如果不继续则为0) + */ + public ExamContinuationAction( + boolean shouldContinue, DifficultyLevel nextDifficultyLevel, int nextQuestionCount) { + this.shouldContinue = shouldContinue; + this.nextDifficultyLevel = nextDifficultyLevel; + this.nextQuestionCount = nextQuestionCount; + } + + /** + * 获取用户是否想继续参加另一次考试. + * + * @return 如果继续则为true,如果退出则为false + */ + public boolean shouldContinue() { + return shouldContinue; + } + + /** + * 获取下次考试的难度级别. + * + * @return 下次难度级别(如果不继续则为null) + */ + public DifficultyLevel getNextDifficultyLevel() { + return nextDifficultyLevel; + } + + /** + * 获取下次考试的题目数量. + * + * @return 下次题目数量(如果不继续则为0) + */ + public int getNextQuestionCount() { + return nextQuestionCount; + } + } +} \ No newline at end of file diff --git a/src/com/personalproject/service/ExamService.java b/src/com/personalproject/service/ExamService.java new file mode 100644 index 0000000..392a83a --- /dev/null +++ b/src/com/personalproject/service/ExamService.java @@ -0,0 +1,140 @@ +package com.personalproject.service; + +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; +import com.personalproject.model.QuizQuestion; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +/** + * 用于管理考试会话和生成测验题目的服务类. + */ +public final class ExamService { + + private static final int OPTIONS_COUNT = 4; + private final Map generators; + private final Random random = new Random(); + private final QuestionGenerationService questionGenerationService; + + /** + * 创建新的考试服务. + * + * @param generatorMap 难度级别到题目生成器的映射 + * @param questionGenerationService 题目生成服务 + */ + public ExamService( + Map generatorMap, + QuestionGenerationService questionGenerationService) { + this.generators = new EnumMap<>(DifficultyLevel.class); + this.generators.putAll(generatorMap); + this.questionGenerationService = questionGenerationService; + } + + /** + * 使用指定参数创建新的考试会话. + * + * @param username 考生用户名 + * @param difficultyLevel 考试难度级别 + * @param questionCount 考试中包含的题目数量 + * @return 新的考试会话 + */ + public ExamSession createExamSession( + String username, DifficultyLevel difficultyLevel, int questionCount) { + if (questionCount < 10 || questionCount > 30) { + throw new IllegalArgumentException("题目数量必须在10到30之间"); + } + + // 根据难度级别生成题目 + List generatedQuestions = new ArrayList<>(); + QuestionGenerator generator = generators.get(difficultyLevel); + if (generator == null) { + throw new IllegalArgumentException("找不到难度级别的生成器: " + difficultyLevel); + } + + for (int i = 0; i < questionCount; i++) { + String question = generator.generateQuestion(random); + generatedQuestions.add(question); + } + + // 将字符串题目转换为带有选项和答案的QuizQuestion对象 + List quizQuestions = new ArrayList<>(); + for (String questionText : generatedQuestions) { + List options = generateOptions(questionText); + int correctAnswerIndex = generateCorrectAnswerIndex(options.size()); + QuizQuestion quizQuestion = new QuizQuestion(questionText, options, correctAnswerIndex); + quizQuestions.add(quizQuestion); + } + + return new ExamSession(username, difficultyLevel, quizQuestions); + } + + /** + * 使用预定义题目创建考试会话(用于测试). + * + * @param username 考生用户名 + * @param difficultyLevel 考试难度级别 + * @param questions 预定义题目列表 + * @return 新的考试会话 + */ + public ExamSession createExamSession( + String username, DifficultyLevel difficultyLevel, List questions) { + if (questions.size() < 10 || questions.size() > 30) { + throw new IllegalArgumentException("题目数量必须在10到30之间"); + } + return new ExamSession(username, difficultyLevel, questions); + } + + /** + * 为给定题目生成多项选择选项. 这是一个模拟实现 - 在实际系统中,选项会根据题目生成. + * + * @param questionText 题目文本 + * @return 答案选项列表 + */ + private List generateOptions(String questionText) { + List options = new ArrayList<>(); + // 为每个题目生成4个选项 + try { + // 尝试将数学表达式作为正确答案进行评估 + double correctAnswer = MathExpressionEvaluator.evaluate(questionText); + + // 创建正确答案选项 + options.add(String.format("%.2f", correctAnswer)); + + // 生成3个错误选项 + for (int i = 0; i < OPTIONS_COUNT - 1; i++) { + double incorrectAnswer = correctAnswer + (random.nextGaussian() * 10); // 添加一些随机偏移 + if (Math.abs(incorrectAnswer - correctAnswer) < 0.1) { // 确保不同 + incorrectAnswer += 1.5; + } + options.add(String.format("%.2f", incorrectAnswer)); + } + } catch (Exception e) { + // 如果评估失败,创建虚拟选项 + for (int i = 0; i < OPTIONS_COUNT; i++) { + options.add("选项 " + (i + 1)); + } + } + + // 随机打乱选项以随机化正确答案的位置 + java.util.Collections.shuffle(options, random); + + // 找到打乱后的正确答案索引 + // 对于此模拟实现,我们将返回第一个选项(索引0)作为正确答案 + // 实际实现将跟踪正确答案 + return options; + } + + /** + * 为给定选项数量生成随机正确答案索引. + * + * @param optionCount 选项数量 + * @return 0到optionCount-1之间的随机索引 + */ + private int generateCorrectAnswerIndex(int optionCount) { + return random.nextInt(optionCount); + } +} \ No newline at end of file diff --git a/src/com/personalproject/service/MathExpressionEvaluator.java b/src/com/personalproject/service/MathExpressionEvaluator.java new file mode 100644 index 0000000..6e32ed1 --- /dev/null +++ b/src/com/personalproject/service/MathExpressionEvaluator.java @@ -0,0 +1,217 @@ +package com.personalproject.service; + +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; +import java.util.regex.Pattern; + +/** + * A mathematical expression evaluator that can handle basic arithmetic operations. + */ +public final class MathExpressionEvaluator { + + private static final Pattern NUMBER_PATTERN = Pattern.compile("-?\\d+(\\.\\d+)?"); + private static final Map PRECEDENCE = new HashMap<>(); + + static { + PRECEDENCE.put('+', 1); + PRECEDENCE.put('-', 1); + PRECEDENCE.put('*', 2); + PRECEDENCE.put('/', 2); + PRECEDENCE.put('^', 3); + } + + private MathExpressionEvaluator() { + // Prevent instantiation of utility class + } + + /** + * Evaluates a mathematical expression string. + * + * @param expression The mathematical expression to evaluate + * @return The result of the evaluation + * @throws IllegalArgumentException If the expression is invalid + */ + public static double evaluate(String expression) { + if (expression == null) { + throw new IllegalArgumentException("Expression cannot be null"); + } + + expression = expression.replaceAll("\\s+", ""); // Remove whitespace + if (expression.isEmpty()) { + throw new IllegalArgumentException("Expression cannot be empty"); + } + + // Tokenize the expression + String[] tokens = tokenize(expression); + + // Convert infix to postfix notation using Shunting Yard algorithm + String[] postfix = infixToPostfix(tokens); + + // Evaluate the postfix expression + return evaluatePostfix(postfix); + } + + /** + * Tokenizes the expression into numbers and operators. + * + * @param expression The expression to tokenize + * @return An array of tokens + */ + private static String[] tokenize(String expression) { + java.util.List tokens = new java.util.ArrayList<>(); + StringBuilder currentNumber = new StringBuilder(); + + for (int i = 0; i < expression.length(); i++) { + char c = expression.charAt(i); + + if (Character.isDigit(c) || c == '.') { + currentNumber.append(c); + } else if (c == '(' || c == ')') { + if (currentNumber.length() > 0) { + tokens.add(currentNumber.toString()); + currentNumber.setLength(0); + } + tokens.add(String.valueOf(c)); + } else if (isOperator(c)) { + if (currentNumber.length() > 0) { + tokens.add(currentNumber.toString()); + currentNumber.setLength(0); + } + + // Handle unary minus + if (c == '-' && (i == 0 || expression.charAt(i - 1) == '(')) { + currentNumber.append(c); + } else { + tokens.add(String.valueOf(c)); + } + } else { + throw new IllegalArgumentException("Invalid character in expression: " + c); + } + } + + if (currentNumber.length() > 0) { + tokens.add(currentNumber.toString()); + } + + return tokens.toArray(new String[0]); + } + + /** + * Checks if the character is an operator. + * + * @param c The character to check + * @return true if the character is an operator, false otherwise + */ + private static boolean isOperator(char c) { + return c == '+' || c == '-' || c == '*' || c == '/' || c == '^'; + } + + /** + * Converts infix notation to postfix notation using the Shunting Yard algorithm. + * + * @param tokens The tokens in infix notation + * @return An array of tokens in postfix notation + */ + private static String[] infixToPostfix(String[] tokens) { + java.util.List output = new java.util.ArrayList<>(); + Stack operators = new Stack<>(); + + for (String token : tokens) { + if (isNumber(token)) { + output.add(token); + } else if (token.equals("(")) { + operators.push(token); + } else if (token.equals(")")) { + while (!operators.isEmpty() && !operators.peek().equals("(")) { + output.add(operators.pop()); + } + if (!operators.isEmpty()) { + operators.pop(); // Remove the "(" + } + } else if (isOperator(token.charAt(0))) { + while (!operators.isEmpty() + && isOperator(operators.peek().charAt(0)) + && PRECEDENCE.get(operators.peek().charAt(0)) >= PRECEDENCE.get(token.charAt(0))) { + output.add(operators.pop()); + } + operators.push(token); + } + } + + while (!operators.isEmpty()) { + output.add(operators.pop()); + } + + return output.toArray(new String[0]); + } + + /** + * Evaluates a postfix expression. + * + * @param postfix The tokens in postfix notation + * @return The result of the evaluation + */ + private static double evaluatePostfix(String[] postfix) { + Stack values = new Stack<>(); + + for (String token : postfix) { + if (isNumber(token)) { + values.push(Double.parseDouble(token)); + } else if (isOperator(token.charAt(0))) { + if (values.size() < 2) { + throw new IllegalArgumentException("Invalid expression: insufficient operands"); + } + + double b = values.pop(); + double a = values.pop(); + double result = performOperation(a, b, token.charAt(0)); + values.push(result); + } + } + + if (values.size() != 1) { + throw new IllegalArgumentException("Invalid expression: too many operands"); + } + + return values.pop(); + } + + /** + * Performs the specified operation on the two operands. + * + * @param a The first operand + * @param b The second operand + * @param operator The operator to apply + * @return The result of the operation + */ + private static double performOperation(double a, double b, char operator) { + switch (operator) { + case '+': + return a + b; + case '-': + return a - b; + case '*': + return a * b; + case '/': + if (b == 0) { + throw new ArithmeticException("Division by zero"); + } + return a / b; + case '^': + return Math.pow(a, b); + default: + throw new IllegalArgumentException("Unknown operator: " + operator); + } + } + + /** + * Checks if the token is a number. + * + * @param token The token to check + * @return true if the token is a number, false otherwise + */ + private static boolean isNumber(String token) { + return NUMBER_PATTERN.matcher(token).matches(); + } +} \ No newline at end of file diff --git a/src/com/personalproject/service/MathLearningService.java b/src/com/personalproject/service/MathLearningService.java new file mode 100644 index 0000000..e404ddb --- /dev/null +++ b/src/com/personalproject/service/MathLearningService.java @@ -0,0 +1,170 @@ +package com.personalproject.service; + +import com.personalproject.auth.AccountRepository; +import com.personalproject.auth.EmailService; +import com.personalproject.auth.PasswordValidator; +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.model.ExamSession; +import com.personalproject.storage.QuestionStorageService; +import java.util.Map; +import java.util.Optional; + +/** + * 为主JavaFX UI提供所有功能的服务类. 此类协调各种服务以为UI提供统一的API. + */ +public final class MathLearningService { + + private final AccountRepository accountRepository; + private final RegistrationService registrationService; + private final ExamService examService; + private final QuestionStorageService storageService; + private final ExamResultService resultService; + + /** + * 创建一个新的数学学习服务. + * + * @param generatorMap 难度级别到题目生成器的映射 + * @param questionGenerationService 题目生成服务 + */ + public MathLearningService( + Map generatorMap, + QuestionGenerationService questionGenerationService) { + this.accountRepository = new AccountRepository(); + this.registrationService = new RegistrationService(accountRepository); + this.examService = new ExamService(generatorMap, questionGenerationService); + this.storageService = new QuestionStorageService(); + this.resultService = new ExamResultService(); + } + + // 注册方法 + + /** + * 通过向提供的电子邮件发送注册码来启动注册过程. + * + * @param username 所需用户名 + * @param email 要发送注册码的电子邮件地址 + * @param difficultyLevel 选择的难度级别 + * @return 如果注册成功启动则返回true,如果电子邮件无效或用户名已存在则返回false + */ + public boolean initiateRegistration(String username, String email, + DifficultyLevel difficultyLevel) { + return registrationService.initiateRegistration(username, email, difficultyLevel); + } + + /** + * 验证用户输入的注册码. + * + * @param username 用户名 + * @param registrationCode 通过电子邮件收到的注册码 + * @return 如果注册码有效则返回true,否则返回false + */ + public boolean verifyRegistrationCode(String username, String registrationCode) { + return registrationService.verifyRegistrationCode(username, registrationCode); + } + + /** + * 在成功验证注册码后为用户设置密码. + * + * @param username 用户名 + * @param password 要设置的密码 + * @return 如果密码设置成功则返回true,如果验证失败或用户不存在则返回false + */ + public boolean setPassword(String username, String password) { + return registrationService.setPassword(username, password); + } + + /** + * 使用用户名和密码验证用户. + * + * @param username 用户名 + * @param password 密码 + * @return 如果验证成功则返回包含用户账户的Optional + */ + public Optional authenticate(String username, + String password) { + return registrationService.authenticate(username, password); + } + + /** + * 检查密码是否符合验证要求. + * + * @param password 要验证的密码 + * @return 如果密码有效则返回true,否则返回false + */ + public static boolean isValidPassword(String password) { + return PasswordValidator.isValidPassword(password); + } + + /** + * 更改现有用户的密码. + * + * @param username 用户名 + * @param oldPassword 当前密码 + * @param newPassword 新密码 + * @return 如果密码更改成功则返回true,如果验证失败或验证失败则返回false + */ + public boolean changePassword(String username, String oldPassword, String newPassword) { + return registrationService.changePassword(username, oldPassword, newPassword); + } + + /** + * 检查电子邮件地址是否具有有效格式. + * + * @param email 要验证的电子邮件地址 + * @return 如果电子邮件格式有效则返回true,否则返回false + */ + public static boolean isValidEmail(String email) { + return EmailService.isValidEmail(email); + } + + /** + * 为指定用户和难度级别创建新的考试会话. + * + * @param username 应试者用户名 + * @param difficultyLevel 考试难度级别 + * @param questionCount 考试中包含的题目数量 + * @return 新的考试会话 + */ + public ExamSession createExamSession(String username, DifficultyLevel difficultyLevel, + int questionCount) { + return examService.createExamSession(username, difficultyLevel, questionCount); + } + + /** + * 将考试结果保存到存储中. + * + * @param examSession 完成的考试会话 + * @return 保存结果文件的路径 + */ + public java.nio.file.Path saveExamResults(ExamSession examSession) { + try { + return storageService.saveExamResults(examSession); + } catch (java.io.IOException e) { + throw new RuntimeException("保存考试结果失败", e); + } + } + + /** + * 处理用户完成考试后的选择. + * + * @param examSession 完成的考试会话 + * @param continueWithSameLevel 是否继续使用相同难度级别 + * @param newDifficultyLevel 更改时的新难度级别(如果不更改则为null) + * @return 表示下一步操作的动作 + */ + public ExamResultService.ExamContinuationAction processExamResult( + ExamSession examSession, boolean continueWithSameLevel, DifficultyLevel newDifficultyLevel) { + return resultService.processExamResult(examSession, continueWithSameLevel, newDifficultyLevel); + } + + /** + * 检查用户是否存在于系统中. + * + * @param username 要检查的用户名 + * @return 如果用户存在则返回true,否则返回false + */ + public boolean userExists(String username) { + return registrationService.userExists(username); + } +} \ No newline at end of file diff --git a/src/com/personalproject/service/QuestionGenerationService.java b/src/com/personalproject/service/QuestionGenerationService.java new file mode 100644 index 0000000..b536a7d --- /dev/null +++ b/src/com/personalproject/service/QuestionGenerationService.java @@ -0,0 +1,71 @@ +package com.personalproject.service; + +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +/** + * 负责批量生成题目并避免与历史题库重复. + */ +public final class QuestionGenerationService { + + private static final int MAX_ATTEMPTS = 10_000; + private final Map generators; + private final Random random = new SecureRandom(); + + /** + * 构造函数:复制难度与生成器的映射,保留内部安全副本. + * + * @param generatorMap 难度到题目生成器的外部映射. + */ + public QuestionGenerationService(Map generatorMap) { + generators = new EnumMap<>(DifficultyLevel.class); + generators.putAll(generatorMap); + } + + /** + * 根据指定难度批量生成不与历史题目重复的新题目. + * + * @param level 目标题目难度. + * @param count 需要生成的题目数量. + * @param existingQuestions 已存在的题目集合,用于查重. + * @return 生成的题目列表. + * @throws IllegalArgumentException 当难度未配置时抛出. + * @throws IllegalStateException 当达到最大尝试次数仍无法生成足够题目时抛出. + */ + public List generateUniqueQuestions( + DifficultyLevel level, int count, Set existingQuestions) { + QuestionGenerator generator = generators.get(level); + if (generator == null) { + throw new IllegalArgumentException("Unsupported difficulty level: " + level); + } + Set produced = new HashSet<>(); + List results = new ArrayList<>(); + int attempts = 0; + while (results.size() < count) { + if (attempts >= MAX_ATTEMPTS) { + throw new IllegalStateException("Unable to generate enough unique questions."); + } + attempts++; + String question = generator.generateQuestion(random).trim(); + if (question.isEmpty()) { + continue; + } + if (existingQuestions.contains(question)) { + continue; + } + if (!produced.add(question)) { + continue; + } + results.add(question); + } + return results; + } +} diff --git a/src/com/personalproject/service/RegistrationService.java b/src/com/personalproject/service/RegistrationService.java new file mode 100644 index 0000000..100b58d --- /dev/null +++ b/src/com/personalproject/service/RegistrationService.java @@ -0,0 +1,138 @@ +package com.personalproject.service; + +import com.personalproject.auth.AccountRepository; +import com.personalproject.auth.EmailService; +import com.personalproject.auth.PasswordValidator; +import com.personalproject.model.DifficultyLevel; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 用于处理用户注册和身份验证过程的服务类. + */ +public final class RegistrationService { + + private final AccountRepository accountRepository; + private final Map pendingRegistrations; + private final Map registrationAttempts; + + /** + * 创建新的注册服务. + * + * @param accountRepository 使用的账户存储库 + */ + public RegistrationService(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + this.pendingRegistrations = new ConcurrentHashMap<>(); + this.registrationAttempts = new ConcurrentHashMap<>(); + } + + /** + * 通过向提供的电子邮件发送注册码来启动注册过程. + * + * @param username 所需用户名 + * @param email 要发送注册码的电子邮件地址 + * @param difficultyLevel 选择的难度级别 + * @return 注册成功启动则返回true,如果电子邮件无效或用户名已存在则返回false + */ + public boolean initiateRegistration(String username, String email, + DifficultyLevel difficultyLevel) { + if (!EmailService.isValidEmail(email)) { + return false; + } + + if (!accountRepository.registerUser(username, email, difficultyLevel)) { + return false; // 用户名已存在或邮箱已注册 + } + + String registrationCode = EmailService.generateRegistrationCode(); + pendingRegistrations.put(username, registrationCode); + registrationAttempts.put(username, 0); + + return EmailService.sendRegistrationCode(email, registrationCode); + } + + /** + * 通过验证注册码完成注册. + * + * @param username 用户名 + * @param registrationCode 通过电子邮件收到的注册码 + * @return 注册码有效则返回true,否则返回false + */ + public boolean verifyRegistrationCode(String username, String registrationCode) { + String storedCode = pendingRegistrations.get(username); + if (storedCode == null || !storedCode.equals(registrationCode)) { + // 跟踪失败尝试 + int attempts = registrationAttempts.getOrDefault(username, 0); + attempts++; + registrationAttempts.put(username, attempts); + + if (attempts >= 3) { + // 如果失败次数过多,则删除用户 + pendingRegistrations.remove(username); + registrationAttempts.remove(username); + return false; + } + return false; + } + + // 有效码,从待处理列表中移除 + pendingRegistrations.remove(username); + registrationAttempts.remove(username); + return true; + } + + /** + * 在成功验证注册码后为用户设置密码. + * + * @param username 用户名 + * @param password 要设置的密码 + * @return 密码设置成功则返回true,如果验证失败或用户不存在则返回false + */ + public boolean setPassword(String username, String password) { + if (!PasswordValidator.isValidPassword(password)) { + return false; + } + + return accountRepository.setPassword(username, password); + } + + /** + * 更改现有用户的密码. + * + * @param username 用户名 + * @param oldPassword 当前密码 + * @param newPassword 新密码 + * @return 密码更改成功则返回true,如果验证失败或身份验证失败则返回false + */ + public boolean changePassword(String username, String oldPassword, String newPassword) { + if (!PasswordValidator.isValidPassword(newPassword)) { + return false; + } + + return accountRepository.changePassword(username, oldPassword, newPassword); + } + + /** + * 使用用户名和密码验证用户. + * + * @param username 用户名 + * @param password 密码 + * @return 验证成功则返回包含用户账户的Optional + */ + public Optional authenticate(String username, + String password) { + return accountRepository.authenticate(username, password); + } + + /** + * 检查用户是否存在于系统中. + * + * @param username 要检查的用户名 + * @return 用户存在则返回true,否则返回false + */ + public boolean userExists(String username) { + return accountRepository.userExists(username); + } +} \ No newline at end of file diff --git a/src/com/personalproject/storage/QuestionStorageService.java b/src/com/personalproject/storage/QuestionStorageService.java new file mode 100644 index 0000000..4fc9870 --- /dev/null +++ b/src/com/personalproject/storage/QuestionStorageService.java @@ -0,0 +1,162 @@ +package com.personalproject.storage; + +import com.personalproject.model.ExamSession; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +/** + * 负责保存生成的题目并维护查重信息. + */ +public final class QuestionStorageService { + + private static final String BASE_DIRECTORY = "user_data"; + private static final String QUESTIONS_SUBDIR = "questions"; + private static final String RESULTS_SUBDIR = "results"; + private static final DateTimeFormatter FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"); + + /** + * 加载指定用户历史生成的题目,用于后续查重. + * + * @param username 用户名. + * @return 历史题目的去重集合. + * @throws IOException 当读取文件时发生 I/O 错误. + */ + public Set loadExistingQuestions(String username) throws IOException { + Path accountDirectory = getQuestionsDirectory(username); + Set questions = new HashSet<>(); + if (!Files.exists(accountDirectory)) { + return questions; + } + try (Stream paths = Files.list(accountDirectory)) { + paths + .filter(path -> path.getFileName().toString().endsWith(".txt")) + .sorted() + .forEach(path -> readQuestionsFromFile(path, questions)); + } + return questions; + } + + /** + * 将新生成的题目保存到用户目录,并返回生成的文件路径. + * + * @param username 用户名. + * @param questions 待保存的题目列表. + * @return 保存题目的文件路径. + * @throws IOException 当写入文件失败时抛出. + */ + public Path saveQuestions(String username, List questions) throws IOException { + Path accountDirectory = getQuestionsDirectory(username); + Files.createDirectories(accountDirectory); + String fileName = FORMATTER.format(LocalDateTime.now()) + ".txt"; + Path outputFile = accountDirectory.resolve(fileName); + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < questions.size(); index++) { + String question = questions.get(index); + builder + .append(index + 1) + .append(". ") + .append(question) + .append(System.lineSeparator()) + .append(System.lineSeparator()); + } + Files.writeString( + outputFile, builder.toString(), StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW); + return outputFile; + } + + /** + * 保存用户的考试结果. + * + * @param examSession 包含结果的考试会话 + * @return 保存结果文件的路径 + * @throws IOException 如果写入文件失败 + */ + public Path saveExamResults(ExamSession examSession) throws IOException { + Path resultsDirectory = getResultsDirectory(examSession.getUsername()); + Files.createDirectories(resultsDirectory); + + StringBuilder builder = new StringBuilder(); + builder.append("考试结果报告").append(System.lineSeparator()); + builder.append("用户名: ").append(examSession.getUsername()).append(System.lineSeparator()); + builder.append("难度: ").append(examSession.getDifficultyLevel().getDisplayName()) + .append(System.lineSeparator()); + builder.append("开始时间: ").append(examSession.getStartTime()).append(System.lineSeparator()); + builder.append("题目数量: ").append(examSession.getQuestions().size()) + .append(System.lineSeparator()); + builder.append("得分: ").append(String.format("%.2f", examSession.calculateScore())).append("%") + .append(System.lineSeparator()); + builder.append(System.lineSeparator()); + + // 添加逐题结果 + for (int i = 0; i < examSession.getQuestions().size(); i++) { + var question = examSession.getQuestions().get(i); + int userAnswer = examSession.getUserAnswer(i); + boolean isCorrect = question.isAnswerCorrect(userAnswer); + + builder.append("题目 ").append(i + 1).append(": ").append(question.getQuestionText()) + .append(System.lineSeparator()); + builder.append("您的答案: ").append(userAnswer == -1 ? "未回答" : + (userAnswer < question.getOptions().size() ? question.getOptions().get(userAnswer) + : "无效")).append(System.lineSeparator()); + builder.append("正确答案: ").append( + question.getOptions().get(question.getCorrectAnswerIndex())) + .append(System.lineSeparator()); + builder.append("结果: ").append(isCorrect ? "正确" : "错误").append(System.lineSeparator()); + builder.append(System.lineSeparator()); + } + + String fileName = "exam_result_" + FORMATTER.format(LocalDateTime.now()) + ".txt"; + Path outputFile = resultsDirectory.resolve(fileName); + + Files.writeString( + outputFile, builder.toString(), StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW); + return outputFile; + } + + private Path getAccountDirectory(String username) { + return Paths.get(BASE_DIRECTORY, username); + } + + private Path getQuestionsDirectory(String username) { + return getAccountDirectory(username).resolve(QUESTIONS_SUBDIR); + } + + private Path getResultsDirectory(String username) { + return getAccountDirectory(username).resolve(RESULTS_SUBDIR); + } + + private void readQuestionsFromFile(Path path, Set questions) { + try { + List lines = Files.readAllLines(path, StandardCharsets.UTF_8); + for (String line : lines) { + String trimmed = line.trim(); + if (trimmed.isEmpty()) { + continue; + } + int dotIndex = trimmed.indexOf('.'); + if (dotIndex >= 0 && dotIndex + 1 < trimmed.length()) { + String question = trimmed.substring(dotIndex + 1).trim(); + if (!question.isEmpty()) { + questions.add(question); + } + } else { + questions.add(trimmed); + } + } + } catch (IOException exception) { + System.err.println( + "读取题目文件失败:" + path + ",原因:" + exception.getMessage() + ",将跳过该文件."); + } + } +} \ No newline at end of file diff --git a/src/com/personalproject/ui/DesktopApp.java b/src/com/personalproject/ui/DesktopApp.java new file mode 100644 index 0000000..61e1e26 --- /dev/null +++ b/src/com/personalproject/ui/DesktopApp.java @@ -0,0 +1,658 @@ +package com.personalproject.ui; + +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.model.QuizQuestion; +import com.personalproject.service.QuestionGenerationService; +import java.awt.BorderLayout; +import java.awt.CardLayout; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPasswordField; +import javax.swing.JRadioButton; +import javax.swing.JScrollPane; +import javax.swing.JSpinner; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.SpinnerNumberModel; + +/** + * A Swing desktop application that drives the math learning backend. + */ +public final class DesktopApp extends JFrame { + + private static final String CARD_LOGIN = "login"; + private static final String CARD_REGISTER = "register"; + private static final String CARD_VERIFY = "verify"; + private static final String CARD_SET_PASSWORD = "set_password"; + private static final String CARD_CHANGE_PASSWORD = "change_password"; + private static final String CARD_SELECT = "select"; + private static final String CARD_EXAM = "exam"; + private static final String CARD_SCORE = "score"; + + private final MathLearningController controller; + private final CardLayout cardLayout; + private final JPanel cards; + + private String currentUsername = ""; + private ExamSession currentExamSession; + private int currentSelectedAnswerIndex = -1; + + public DesktopApp() { + super("数学学习桌面应用"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setMinimumSize(new Dimension(820, 640)); + + 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); + + this.cardLayout = new CardLayout(); + this.cards = new JPanel(cardLayout); + + cards.add(buildLoginPanel(), CARD_LOGIN); + cards.add(buildRegisterPanel(), CARD_REGISTER); + cards.add(buildVerifyPanel(), CARD_VERIFY); + cards.add(buildSetPasswordPanel(), CARD_SET_PASSWORD); + cards.add(buildChangePasswordPanel(), CARD_CHANGE_PASSWORD); + cards.add(buildSelectPanel(), CARD_SELECT); + cards.add(buildExamPanel(), CARD_EXAM); + cards.add(buildScorePanel(), CARD_SCORE); + + getContentPane().setLayout(new BorderLayout()); + getContentPane().add(cards, BorderLayout.CENTER); + cardLayout.show(cards, CARD_LOGIN); + } + + private JPanel buildLoginPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24)); + GridBagConstraints gc = baseGbc(); + + JLabel title = new JLabel("登录"); + title.setFont(title.getFont().deriveFont(20f)); + gc.gridwidth = 2; + panel.add(title, gc); + + gc.gridy++; + gc.gridwidth = 1; + panel.add(new JLabel("用户名:"), gc); + JTextField usernameField = new JTextField(18); + gc.gridx = 1; + panel.add(usernameField, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("密码:"), gc); + JPasswordField passwordField = new JPasswordField(18); + gc.gridx = 1; + panel.add(passwordField, gc); + + gc.gridy++; + gc.gridx = 0; + JButton loginBtn = new JButton("登录"); + panel.add(loginBtn, gc); + gc.gridx = 1; + JButton toRegisterBtn = new JButton("注册"); + panel.add(toRegisterBtn, gc); + + gc.gridy++; + gc.gridx = 0; + JButton toChangePwd = new JButton("修改密码"); + panel.add(toChangePwd, gc); + + loginBtn.addActionListener(e -> { + String username = usernameField.getText().trim(); + String password = String.valueOf(passwordField.getPassword()); + if (username.isEmpty() || password.isEmpty()) { + showError("请输入用户名和密码"); + return; + } + Optional account = controller.authenticate(username, + password); + if (account.isPresent()) { + currentUsername = username; + cardLayout.show(cards, CARD_SELECT); + } else { + showError("登录失败:用户名或密码错误,或未完成注册"); + } + }); + + toRegisterBtn.addActionListener(e -> cardLayout.show(cards, CARD_REGISTER)); + toChangePwd.addActionListener(e -> cardLayout.show(cards, CARD_CHANGE_PASSWORD)); + + return panel; + } + + private JPanel buildRegisterPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24)); + GridBagConstraints gc = baseGbc(); + + JLabel title = new JLabel("用户注册"); + title.setFont(title.getFont().deriveFont(20f)); + gc.gridwidth = 2; + panel.add(title, gc); + + gc.gridy++; + gc.gridwidth = 1; + panel.add(new JLabel("用户名:"), gc); + JTextField usernameField = new JTextField(18); + gc.gridx = 1; + panel.add(usernameField, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("邮箱:"), gc); + JTextField emailField = new JTextField(18); + gc.gridx = 1; + panel.add(emailField, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("难度:"), gc); + JComboBox levelBox = new JComboBox<>(DifficultyLevel.values()); + gc.gridx = 1; + panel.add(levelBox, gc); + + gc.gridy++; + gc.gridx = 0; + JButton sendCodeBtn = new JButton("发送注册码"); + panel.add(sendCodeBtn, gc); + gc.gridx = 1; + JButton backBtn = new JButton("返回登录"); + panel.add(backBtn, gc); + + sendCodeBtn.addActionListener(e -> { + String username = usernameField.getText().trim(); + String email = emailField.getText().trim(); + DifficultyLevel level = (DifficultyLevel) levelBox.getSelectedItem(); + if (username.isEmpty() || email.isEmpty() || level == null) { + showError("请填写完整信息"); + return; + } + if (!controller.isValidEmail(email)) { + showError("邮箱格式不正确"); + return; + } + boolean ok = controller.initiateRegistration(username, email, level); + if (ok) { + currentUsername = username; + JOptionPane.showMessageDialog(this, "注册码已发送到邮箱(示例中输出到控制台)"); + cardLayout.show(cards, CARD_VERIFY); + } else { + showError("注册启动失败:用户名已存在或邮箱已注册"); + } + }); + + backBtn.addActionListener(e -> cardLayout.show(cards, CARD_LOGIN)); + return panel; + } + + private JPanel buildVerifyPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24)); + GridBagConstraints gc = baseGbc(); + + JLabel title = new JLabel("输入邮箱收到的注册码"); + title.setFont(title.getFont().deriveFont(18f)); + gc.gridwidth = 2; + panel.add(title, gc); + + gc.gridy++; + gc.gridwidth = 1; + panel.add(new JLabel("用户名:"), gc); + JTextField usernameField = new JTextField(18); + gc.gridx = 1; + panel.add(usernameField, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("注册码:"), gc); + JTextField codeField = new JTextField(18); + gc.gridx = 1; + panel.add(codeField, gc); + + gc.gridy++; + gc.gridx = 0; + JButton verifyBtn = new JButton("验证"); + panel.add(verifyBtn, gc); + gc.gridx = 1; + JButton backBtn = new JButton("返回"); + panel.add(backBtn, gc); + + verifyBtn.addActionListener(e -> { + String username = usernameField.getText().trim(); + String code = codeField.getText().trim(); + if (username.isEmpty() || code.isEmpty()) { + showError("请输入用户名与注册码"); + return; + } + boolean ok = controller.verifyRegistrationCode(username, code); + if (ok) { + currentUsername = username; + JOptionPane.showMessageDialog(this, "验证成功,请设置密码"); + cardLayout.show(cards, CARD_SET_PASSWORD); + } else { + showError("验证失败:注册码错误或超出尝试次数"); + } + }); + + backBtn.addActionListener(e -> cardLayout.show(cards, CARD_REGISTER)); + return panel; + } + + private JPanel buildSetPasswordPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24)); + GridBagConstraints gc = baseGbc(); + + JLabel title = new JLabel("设置密码(6-10位,含大小写字母和数字)"); + title.setFont(title.getFont().deriveFont(18f)); + gc.gridwidth = 2; + panel.add(title, gc); + + gc.gridy++; + gc.gridwidth = 1; + panel.add(new JLabel("用户名:"), gc); + JTextField usernameField = new JTextField(18); + gc.gridx = 1; + panel.add(usernameField, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("密码:"), gc); + JPasswordField pwd1 = new JPasswordField(18); + gc.gridx = 1; + panel.add(pwd1, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("确认密码:"), gc); + JPasswordField pwd2 = new JPasswordField(18); + gc.gridx = 1; + panel.add(pwd2, gc); + + gc.gridy++; + gc.gridx = 0; + JButton setBtn = new JButton("设置密码"); + panel.add(setBtn, gc); + gc.gridx = 1; + JButton backBtn = new JButton("返回"); + panel.add(backBtn, gc); + + setBtn.addActionListener(e -> { + String username = usernameField.getText().trim(); + String p1 = String.valueOf(pwd1.getPassword()); + String p2 = String.valueOf(pwd2.getPassword()); + if (username.isEmpty() || p1.isEmpty() || p2.isEmpty()) { + showError("请填写完整信息"); + return; + } + if (!p1.equals(p2)) { + showError("两次密码不一致"); + return; + } + if (!controller.isValidPassword(p1)) { + showError("密码不符合要求:6-10位,含大小写字母和数字"); + return; + } + boolean ok = controller.setPassword(username, p1); + if (ok) { + currentUsername = username; + JOptionPane.showMessageDialog(this, "密码设置成功,请登录"); + cardLayout.show(cards, CARD_LOGIN); + } else { + showError("设置失败:请确认已完成注册或用户存在"); + } + }); + + backBtn.addActionListener(e -> cardLayout.show(cards, CARD_VERIFY)); + return panel; + } + + private JPanel buildChangePasswordPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24)); + GridBagConstraints gc = baseGbc(); + + JLabel title = new JLabel("修改密码"); + title.setFont(title.getFont().deriveFont(18f)); + gc.gridwidth = 2; + panel.add(title, gc); + + gc.gridy++; + gc.gridwidth = 1; + panel.add(new JLabel("用户名:"), gc); + JTextField usernameField = new JTextField(18); + gc.gridx = 1; + panel.add(usernameField, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("原密码:"), gc); + JPasswordField oldPwd = new JPasswordField(18); + gc.gridx = 1; + panel.add(oldPwd, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("新密码:"), gc); + JPasswordField newPwd1 = new JPasswordField(18); + gc.gridx = 1; + panel.add(newPwd1, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("确认新密码:"), gc); + JPasswordField newPwd2 = new JPasswordField(18); + gc.gridx = 1; + panel.add(newPwd2, gc); + + gc.gridy++; + gc.gridx = 0; + JButton changeBtn = new JButton("修改"); + panel.add(changeBtn, gc); + gc.gridx = 1; + JButton backBtn = new JButton("返回登录"); + panel.add(backBtn, gc); + + changeBtn.addActionListener(e -> { + String username = usernameField.getText().trim(); + String old = String.valueOf(oldPwd.getPassword()); + String n1 = String.valueOf(newPwd1.getPassword()); + String n2 = String.valueOf(newPwd2.getPassword()); + if (username.isEmpty() || old.isEmpty() || n1.isEmpty() || n2.isEmpty()) { + showError("请填写完整信息"); + return; + } + if (!n1.equals(n2)) { + showError("两次新密码不一致"); + return; + } + if (!controller.isValidPassword(n1)) { + showError("新密码不符合要求:6-10位,含大小写字母和数字"); + return; + } + boolean ok = controller.changePassword(username, old, n1); + if (ok) { + JOptionPane.showMessageDialog(this, "密码修改成功,请使用新密码登录"); + cardLayout.show(cards, CARD_LOGIN); + } else { + showError("修改失败:原密码错误或未完成注册"); + } + }); + + backBtn.addActionListener(e -> cardLayout.show(cards, CARD_LOGIN)); + return panel; + } + + private JPanel buildSelectPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24)); + GridBagConstraints gc = baseGbc(); + + JLabel title = new JLabel("选择难度与题目数量"); + title.setFont(title.getFont().deriveFont(18f)); + gc.gridwidth = 2; + panel.add(title, gc); + + gc.gridy++; + gc.gridwidth = 1; + panel.add(new JLabel("难度:"), gc); + JComboBox levelBox = new JComboBox<>(DifficultyLevel.values()); + gc.gridx = 1; + panel.add(levelBox, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("题目数量 (10-30):"), gc); + JSpinner countSpinner = new JSpinner(new SpinnerNumberModel(10, 10, 30, 1)); + gc.gridx = 1; + panel.add(countSpinner, gc); + + gc.gridy++; + gc.gridx = 0; + JButton startBtn = new JButton("开始做题"); + panel.add(startBtn, gc); + gc.gridx = 1; + JButton logoutBtn = new JButton("退出登录"); + panel.add(logoutBtn, gc); + + startBtn.addActionListener(e -> { + if (currentUsername == null || currentUsername.isEmpty()) { + showError("请先登录"); + cardLayout.show(cards, CARD_LOGIN); + return; + } + DifficultyLevel level = (DifficultyLevel) levelBox.getSelectedItem(); + int count = (Integer) countSpinner.getValue(); + try { + currentExamSession = controller.createExamSession(currentUsername, level, count); + currentSelectedAnswerIndex = -1; + loadExamQuestionIntoUI(); + cardLayout.show(cards, CARD_EXAM); + } catch (IllegalArgumentException ex) { + showError("无法创建考试:" + ex.getMessage()); + } + }); + + logoutBtn.addActionListener(e -> { + currentUsername = ""; + cardLayout.show(cards, CARD_LOGIN); + }); + return panel; + } + + private JPanel buildExamPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24)); + GridBagConstraints gc = baseGbc(); + + JLabel title = new JLabel("答题"); + title.setFont(title.getFont().deriveFont(18f)); + gc.gridwidth = 2; + panel.add(title, gc); + + gc.gridy++; + gc.gridwidth = 2; + JTextArea questionArea = new JTextArea(5, 50); + questionArea.setEditable(false); + questionArea.setLineWrap(true); + questionArea.setWrapStyleWord(true); + panel.add(new JScrollPane(questionArea), gc); + + gc.gridy++; + gc.gridwidth = 2; + JPanel optionsPanel = new JPanel(new GridBagLayout()); + optionsPanel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + panel.add(optionsPanel, gc); + + ButtonGroup group = new ButtonGroup(); + JRadioButton[] optionButtons = new JRadioButton[4]; + for (int i = 0; i < optionButtons.length; i++) { + optionButtons[i] = new JRadioButton(); + group.add(optionButtons[i]); + } + + // place options + for (int i = 0; i < optionButtons.length; i++) { + GridBagConstraints og = new GridBagConstraints(); + og.gridx = 0; + og.gridy = i; + og.anchor = GridBagConstraints.WEST; + og.insets = new Insets(6, 6, 6, 6); + optionsPanel.add(optionButtons[i], og); + } + + gc.gridy++; + gc.gridwidth = 1; + JButton submitBtn = new JButton("提交/下一题"); + panel.add(submitBtn, gc); + gc.gridx = 1; + JButton backToSelect = new JButton("中止并返回"); + panel.add(backToSelect, gc); + + submitBtn.addActionListener(e -> { + if (currentExamSession == null) { + showError("没有正在进行的考试"); + return; + } + int selected = currentSelectedAnswerIndex; + if (selected < 0) { + showError("请先选择一个选项"); + return; + } + try { + currentExamSession.setAnswer(selected); + } catch (RuntimeException ex) { + showError("设置答案失败:" + ex.getMessage()); + return; + } + boolean hasNext = currentExamSession.goToNextQuestion(); + if (hasNext) { + currentSelectedAnswerIndex = -1; + group.clearSelection(); + loadExamQuestionIntoUI(questionArea, optionButtons); + } else { + showScoreAndSave(); + cardLayout.show(cards, CARD_SCORE); + } + }); + + backToSelect.addActionListener(e -> cardLayout.show(cards, CARD_SELECT)); + + // initial load hookup via helper method when starting exam + panel.putClientProperty("questionArea", questionArea); + panel.putClientProperty("optionButtons", optionButtons); + return panel; + } + + private JPanel buildScorePanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24)); + GridBagConstraints gc = baseGbc(); + + JLabel title = new JLabel("成绩"); + title.setFont(title.getFont().deriveFont(18f)); + gc.gridwidth = 2; + panel.add(title, gc); + + gc.gridy++; + gc.gridwidth = 2; + JLabel scoreLabel = new JLabel("得分: 0.00%"); + panel.add(scoreLabel, gc); + + gc.gridy++; + gc.gridwidth = 1; + JButton continueBtn = new JButton("继续做题"); + panel.add(continueBtn, gc); + gc.gridx = 1; + JButton exitBtn = new JButton("退出到登录"); + panel.add(exitBtn, gc); + + continueBtn.addActionListener(e -> cardLayout.show(cards, CARD_SELECT)); + exitBtn.addActionListener(e -> { + currentUsername = ""; + cardLayout.show(cards, CARD_LOGIN); + }); + + panel.putClientProperty("scoreLabel", scoreLabel); + return panel; + } + + private void loadExamQuestionIntoUI() { + JPanel examPanel = (JPanel) cards.getComponent(6); // CARD_EXAM position in add order + JTextArea questionArea = (JTextArea) examPanel.getClientProperty("questionArea"); + @SuppressWarnings("unchecked") + JRadioButton[] optionButtons = (JRadioButton[]) examPanel.getClientProperty("optionButtons"); + loadExamQuestionIntoUI(questionArea, optionButtons); + } + + private void loadExamQuestionIntoUI(JTextArea questionArea, JRadioButton[] optionButtons) { + if (currentExamSession == null) { + return; + } + QuizQuestion question = currentExamSession.getCurrentQuestion(); + questionArea.setText(question.getQuestionText()); + List options = question.getOptions(); + for (int i = 0; i < optionButtons.length; i++) { + if (i < options.size()) { + optionButtons[i].setText(options.get(i)); + int idx = i; + for (var al : optionButtons[i].getActionListeners()) { + optionButtons[i].removeActionListener(al); + } + optionButtons[i].addActionListener(e -> currentSelectedAnswerIndex = idx); + optionButtons[i].setVisible(true); + } else { + optionButtons[i].setVisible(false); + } + } + } + + private void showScoreAndSave() { + if (currentExamSession == null) { + return; + } + double score = currentExamSession.calculateScore(); + try { + controller.saveExamResults(currentExamSession); + JOptionPane.showMessageDialog(this, + String.format("本次得分:%.2f%%\n结果已保存。", score)); + } catch (RuntimeException ex) { + JOptionPane.showMessageDialog(this, + String.format("本次得分:%.2f%%\n保存结果失败:%s", score, ex.getMessage())); + } + JPanel scorePanel = (JPanel) cards.getComponent(7); // CARD_SCORE + JLabel scoreLabel = (JLabel) scorePanel.getClientProperty("scoreLabel"); + scoreLabel.setText(String.format("得分: %.2f%%", score)); + } + + private static GridBagConstraints baseGbc() { + GridBagConstraints gc = new GridBagConstraints(); + gc.gridx = 0; + gc.gridy = 0; + gc.insets = new Insets(8, 8, 8, 8); + gc.anchor = GridBagConstraints.WEST; + return gc; + } + + private void showError(String message) { + JOptionPane.showMessageDialog(this, message, "错误", JOptionPane.ERROR_MESSAGE); + } + + public static void main(String[] args) { + EventQueue.invokeLater(() -> { + DesktopApp app = new DesktopApp(); + app.setLocationRelativeTo(null); + app.setVisible(true); + }); + } +} + + -- 2.34.1