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