增加UI #2

Closed
hnu202326010207 wants to merge 0 commits from chengfangyu_branch into develop

@ -1,40 +0,0 @@
# Math Learning 应用 JAR 运行指南
## 环境准备
- Windows 10/11已安装 64 位 JDK 17
## 终端设置为 UTF-8
为避免中文字符显示异常,请在运行前切换到 UTF-8 编码。
### 命令提示符cmd
```cmd
chcp 65001
set JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8
```
### PowerShell
```powershell
chcp 65001
$env:JAVA_TOOL_OPTIONS = "-Dfile.encoding=UTF-8"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
```
## 运行 JAR 包
1. 打开对应终端,切换到 可执行JAR 所在目录:
2. 执行运行命令:
- **cmd**
```cmd
java -jar math-learning.jar
```
- **PowerShell**
```powershell
java -jar math-learning.jar
```
## 项目技术要点速览
- **语言与版本**Java 17采用 UTF-8 编码。
- **界面框架**JavaFX 21controls 与 fxml 模块)。
- **构建工具**Maven使用 `maven-jar-plugin``maven-shade-plugin` 打包并生成可执行胖 JAR。
- **模块架构**分层设计认证、试题生成、JavaFX UI 视图与场景)。
- **邮件支持**Jakarta Mail 2.0.1,基于 `email-config.properties` 的 SMTP 配置。
- **测试框架**JUnit 5jupiter-api 与 jupiter-engine

@ -1,150 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.personalproject</groupId>
<artifactId>math-learning</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Math Learning Application</name>
<description>A desktop application for math learning with exam functionality</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<javafx.version>21</javafx.version>
<javafx.maven.plugin.version>0.0.8</javafx.maven.plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<!-- Jakarta Mail for SMTP -->
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
<version>2.0.1</version>
</dependency>
<!-- Testing Dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<!-- JavaFX Maven Plugin -->
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>${javafx.maven.plugin.version}</version>
<configuration>
<mainClass>com.personalproject.ui.MathExamGUI</mainClass>
<!-- Specify required JavaFX modules -->
<options>
<option>--add-modules</option>
<option>javafx.controls,javafx.fxml,javafx.graphics</option>
</options>
</configuration>
<executions>
<execution>
<!-- Default execution for run goal -->
<id>default-cli</id>
<configuration>
<mainClass>com.personalproject.ui.MathExamGUI</mainClass>
</configuration>
</execution>
</executions>
</plugin>
<!-- Maven Surefire Plugin for testing -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<!-- Maven JAR Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<mainClass>com.personalproject.ui.MathExamGUI</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<!-- Maven Shade Plugin to create a fat JAR -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.personalproject.MathExamApplication</mainClass>
</transformer>
</transformers>
<!-- Include all JavaFX modules in the fat JAR -->
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

@ -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<DifficultyLevel, QuestionGenerator> 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<com.personalproject.auth.UserAccount> 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("密码修改失败。");
}
}
}

@ -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<String, UserAccount> accounts = new HashMap<>();
private final Map<String, String> registrationCodes = new HashMap<>();
/**
* .
*
* @param username .
* @param password .
* @return .
*/
public Optional<UserAccount> 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<UserAccount> getUser(String username) {
return Optional.ofNullable(accounts.get(username.trim()));
}
}

@ -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);
}
}

@ -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();
}
}

@ -15,13 +15,13 @@ public record UserAccount(
boolean isRegistered) {
/**
* 使.
* Creates a new user account with registration date set to now.
*
* @param username
* @param email
* @param password
* @param difficultyLevel
* @param isRegistered
* @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()) {

@ -142,14 +142,4 @@ public final class MathLearningController {
public boolean isValidEmail(String email) {
return MathLearningService.isValidEmail(email);
}
/**
* .
*
* @param username
* @return Optional
*/
public Optional<com.personalproject.auth.UserAccount> getUserAccount(String username) {
return mathLearningService.getUser(username);
}
}
}

@ -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;
}
}

@ -2,11 +2,10 @@ package com.personalproject.model;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* .
* Represents an exam session for a user.
*/
public final class ExamSession {
@ -18,11 +17,11 @@ public final class ExamSession {
private int currentQuestionIndex;
/**
* .
* Creates a new exam session.
*
* @param username
* @param difficultyLevel
* @param questions
* @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<QuizQuestion> questions) {
@ -38,77 +37,80 @@ public final class ExamSession {
this.username = username;
this.difficultyLevel = difficultyLevel;
this.questions = List.copyOf(questions); // 题目的不可变副本
this.userAnswers = new ArrayList<>(Collections.nCopies(questions.size(), -1));
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
* @return The username
*/
public String getUsername() {
return username;
}
/**
* .
* Gets the difficulty level of the exam.
*
* @return
* @return The difficulty level
*/
public DifficultyLevel getDifficultyLevel() {
return difficultyLevel;
}
/**
* .
* Gets the list of questions in the exam.
*
* @return
* @return An unmodifiable list of questions
*/
public List<QuizQuestion> getQuestions() {
return questions;
}
/**
* .
* Gets the user's answers to the questions.
*
* @return -1
* @return A list of answer indices (-1 means no answer selected)
*/
public List<Integer> getUserAnswers() {
return List.copyOf(userAnswers); // 返回副本以防止被修改
return List.copyOf(userAnswers); // Return a copy to prevent modification
}
/**
* .
* Gets the current question index.
*
* @return
* @return The current question index
*/
public int getCurrentQuestionIndex() {
return currentQuestionIndex;
}
/**
* .
* Sets the user's answer for the current question.
*
* @param answerIndex
* @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");
}
int optionCount = questions.get(currentQuestionIndex).getOptions().size();
if (answerIndex < 0 || answerIndex >= optionCount) {
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 false
* @return true if successfully moved to next question, false if already at the last question
*/
public boolean goToNextQuestion() {
if (currentQuestionIndex < questions.size() - 1) {
@ -119,9 +121,9 @@ public final class ExamSession {
}
/**
* .
* Moves to the previous question.
*
* @return true false
* @return true if successfully moved to previous question, false if already at the first question
*/
public boolean goToPreviousQuestion() {
if (currentQuestionIndex > 0) {
@ -132,18 +134,18 @@ public final class ExamSession {
}
/**
* .
* Checks if the exam is complete (all questions answered or at the end).
*
* @return true false
* @return true if the exam is complete, false otherwise
*/
public boolean isComplete() {
return userAnswers.stream().allMatch(answer -> answer != -1);
return currentQuestionIndex >= questions.size() - 1;
}
/**
* .
* Gets the current question.
*
* @return
* @return The current quiz question
*/
public QuizQuestion getCurrentQuestion() {
if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) {
@ -153,10 +155,10 @@ public final class ExamSession {
}
/**
* .
* Gets the user's answer for a specific question.
*
* @param questionIndex
* @return -1
* @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()) {
@ -166,9 +168,9 @@ public final class ExamSession {
}
/**
* .
* Calculates the score as a percentage.
*
* @return 0-100
* @return The score as a percentage (0-100)
*/
public double calculateScore() {
int correctCount = 0;
@ -183,73 +185,11 @@ public final class ExamSession {
}
/**
* .
*
* @return
*/
public int getTotalQuestions() {
return questions.size();
}
/**
* .
*
* @param questionIndex
* @return true false
*/
public boolean hasAnswered(int questionIndex) {
if (questionIndex < 0 || questionIndex >= questions.size()) {
throw new IllegalArgumentException("Question index out of bounds");
}
return userAnswers.get(questionIndex) != -1;
}
/**
* .
* Gets the start time of the exam.
*
* @return
* @return The start time
*/
public LocalDateTime getStartTime() {
return startTime;
}
/**
* .
*
* @return
*/
public int getCorrectAnswersCount() {
int correctCount = 0;
for (int i = 0; i < questions.size(); i++) {
QuizQuestion question = questions.get(i);
int userAnswer = userAnswers.get(i);
if (userAnswer != -1 && question.isAnswerCorrect(userAnswer)) {
correctCount++;
}
}
return correctCount;
}
/**
* .
*
* @return
*/
public int getIncorrectAnswersCount() {
int totalAnswered = 0;
int correctCount = 0;
for (int i = 0; i < questions.size(); i++) {
int userAnswer = userAnswers.get(i);
if (userAnswer != -1) {
totalAnswered++;
QuizQuestion question = questions.get(i);
if (question.isAnswerCorrect(userAnswer)) {
correctCount++;
}
}
}
return totalAnswered - correctCount;
}
}
}

@ -3,7 +3,7 @@ package com.personalproject.model;
import java.util.List;
/**
* .
* Represents a quiz question with multiple choice options.
*/
public final class QuizQuestion {
@ -12,11 +12,11 @@ public final class QuizQuestion {
private final int correctAnswerIndex;
/**
* .
* Creates a new quiz question.
*
* @param questionText
* @param options
* @param correctAnswerIndex
* @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<String> options, int correctAnswerIndex) {
if (questionText == null || questionText.trim().isEmpty()) {
@ -30,44 +30,44 @@ public final class QuizQuestion {
}
this.questionText = questionText;
this.options = List.copyOf(options); // 不可变副本
this.options = List.copyOf(options); // Immutable copy
this.correctAnswerIndex = correctAnswerIndex;
}
/**
* .
* Gets the question text.
*
* @return
* @return The question text
*/
public String getQuestionText() {
return questionText;
}
/**
* .
* Gets the list of answer options.
*
* @return
* @return An unmodifiable list of answer options
*/
public List<String> getOptions() {
return options;
}
/**
* .
* Gets the index of the correct answer in the options list.
*
* @return
* @return The index of the correct answer
*/
public int getCorrectAnswerIndex() {
return correctAnswerIndex;
}
/**
* .
* Checks if the given answer index matches the correct answer.
*
* @param answerIndex
* @return true false
* @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;
}
}
}

@ -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<DifficultyLevel, QuestionGenerator> generators;
private final Random random = new Random();
private final QuestionGenerationService questionGenerationService;
/**
* .
*
* @param generatorMap
* @param questionGenerationService
*/
public ExamService(
Map<DifficultyLevel, QuestionGenerator> 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<String> 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<QuizQuestion> quizQuestions = new ArrayList<>();
for (String questionText : generatedQuestions) {
List<String> 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<QuizQuestion> questions) {
if (questions.size() < 10 || questions.size() > 30) {
throw new IllegalArgumentException("题目数量必须在10到30之间");
}
return new ExamSession(username, difficultyLevel, questions);
}
/**
* . - .
*
* @param questionText
* @return
*/
private List<String> generateOptions(String questionText) {
List<String> 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 0optionCount-1
*/
private int generateCorrectAnswerIndex(int optionCount) {
return random.nextInt(optionCount);
}
}

@ -3,17 +3,15 @@ package com.personalproject.service;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.function.DoubleUnaryOperator;
import java.util.regex.Pattern;
/**
* .
* 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<Character, Integer> PRECEDENCE = new HashMap<>();
private static final Map<String, DoubleUnaryOperator> FUNCTIONS = new HashMap<>();
static {
PRECEDENCE.put('+', 1);
@ -21,49 +19,44 @@ public final class MathExpressionEvaluator {
PRECEDENCE.put('*', 2);
PRECEDENCE.put('/', 2);
PRECEDENCE.put('^', 3);
FUNCTIONS.put("sin", angle -> Math.sin(Math.toRadians(angle)));
FUNCTIONS.put("cos", angle -> Math.cos(Math.toRadians(angle)));
FUNCTIONS.put("tan", angle -> Math.tan(Math.toRadians(angle)));
FUNCTIONS.put("sqrt", Math::sqrt);
}
private MathExpressionEvaluator() {
// 防止实例化此工具类
// Prevent instantiation of utility class
}
/**
* .
* Evaluates a mathematical expression string.
*
* @param expression
* @return
* @throws IllegalArgumentException
* @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+", ""); // 移除空白字符
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
* @return
* @param expression The expression to tokenize
* @return An array of tokens
*/
private static String[] tokenize(String expression) {
java.util.List<String> tokens = new java.util.ArrayList<>();
@ -74,21 +67,6 @@ public final class MathExpressionEvaluator {
if (Character.isDigit(c) || c == '.') {
currentNumber.append(c);
} else if (Character.isLetter(c)) {
if (currentNumber.length() > 0) {
tokens.add(currentNumber.toString());
currentNumber.setLength(0);
}
StringBuilder functionBuilder = new StringBuilder();
functionBuilder.append(c);
while (i + 1 < expression.length() && Character.isLetter(expression.charAt(i + 1))) {
functionBuilder.append(expression.charAt(++i));
}
String function = functionBuilder.toString();
if (!isFunction(function)) {
throw new IllegalArgumentException("Unsupported function: " + function);
}
tokens.add(function);
} else if (c == '(' || c == ')') {
if (currentNumber.length() > 0) {
tokens.add(currentNumber.toString());
@ -101,7 +79,7 @@ public final class MathExpressionEvaluator {
currentNumber.setLength(0);
}
// 处理一元负号
// Handle unary minus
if (c == '-' && (i == 0 || expression.charAt(i - 1) == '(')) {
currentNumber.append(c);
} else {
@ -120,24 +98,20 @@ public final class MathExpressionEvaluator {
}
/**
* .
* Checks if the character is an operator.
*
* @param c
* @return true false
* @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 == '^';
}
private static boolean isFunction(String token) {
return FUNCTIONS.containsKey(token);
}
/**
* 使.
* Converts infix notation to postfix notation using the Shunting Yard algorithm.
*
* @param tokens
* @return
* @param tokens The tokens in infix notation
* @return An array of tokens in postfix notation
*/
private static String[] infixToPostfix(String[] tokens) {
java.util.List<String> output = new java.util.ArrayList<>();
@ -146,8 +120,6 @@ public final class MathExpressionEvaluator {
for (String token : tokens) {
if (isNumber(token)) {
output.add(token);
} else if (isFunction(token)) {
operators.push(token);
} else if (token.equals("(")) {
operators.push(token);
} else if (token.equals(")")) {
@ -155,10 +127,7 @@ public final class MathExpressionEvaluator {
output.add(operators.pop());
}
if (!operators.isEmpty()) {
operators.pop(); // 移除 "("
}
if (!operators.isEmpty() && isFunction(operators.peek())) {
output.add(operators.pop());
operators.pop(); // Remove the "("
}
} else if (isOperator(token.charAt(0))) {
while (!operators.isEmpty()
@ -171,21 +140,17 @@ public final class MathExpressionEvaluator {
}
while (!operators.isEmpty()) {
String operator = operators.pop();
if (operator.equals("(") || operator.equals(")")) {
throw new IllegalArgumentException("Mismatched parentheses in expression");
}
output.add(operator);
output.add(operators.pop());
}
return output.toArray(new String[0]);
}
/**
* .
* Evaluates a postfix expression.
*
* @param postfix
* @return
* @param postfix The tokens in postfix notation
* @return The result of the evaluation
*/
private static double evaluatePostfix(String[] postfix) {
Stack<Double> values = new Stack<>();
@ -202,15 +167,6 @@ public final class MathExpressionEvaluator {
double a = values.pop();
double result = performOperation(a, b, token.charAt(0));
values.push(result);
} else if (isFunction(token)) {
if (values.isEmpty()) {
throw new IllegalArgumentException(
"Invalid expression: insufficient operands for function");
}
double value = values.pop();
values.push(applyFunction(token, value));
} else {
throw new IllegalArgumentException("Unknown token: " + token);
}
}
@ -222,12 +178,12 @@ public final class MathExpressionEvaluator {
}
/**
* .
* Performs the specified operation on the two operands.
*
* @param a
* @param b
* @param operator
* @return
* @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) {
@ -249,21 +205,13 @@ public final class MathExpressionEvaluator {
}
}
private static double applyFunction(String function, double value) {
DoubleUnaryOperator operator = FUNCTIONS.get(function);
if (operator == null) {
throw new IllegalArgumentException("Unknown function: " + function);
}
return operator.applyAsDouble(value);
}
/**
* .
* Checks if the token is a number.
*
* @param token
* @return true false
* @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();
}
}
}

@ -30,10 +30,10 @@ public final class MathLearningService {
public MathLearningService(
Map<DifficultyLevel, QuestionGenerator> generatorMap,
QuestionGenerationService questionGenerationService) {
this.storageService = new QuestionStorageService();
this.accountRepository = new AccountRepository();
this.registrationService = new RegistrationService(accountRepository);
this.examService = new ExamService(generatorMap, questionGenerationService, storageService);
this.examService = new ExamService(generatorMap, questionGenerationService);
this.storageService = new QuestionStorageService();
this.resultService = new ExamResultService();
}
@ -167,14 +167,4 @@ public final class MathLearningService {
public boolean userExists(String username) {
return registrationService.userExists(username);
}
/**
* .
*
* @param username
* @return Optional
*/
public Optional<com.personalproject.auth.UserAccount> getUser(String username) {
return registrationService.getUser(username);
}
}
}

@ -6,7 +6,6 @@ import com.personalproject.auth.PasswordValidator;
import com.personalproject.model.DifficultyLevel;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
@ -17,7 +16,6 @@ public final class RegistrationService {
private final AccountRepository accountRepository;
private final Map<String, String> pendingRegistrations;
private final Map<String, Integer> registrationAttempts;
private final Set<String> verifiedUsers;
/**
* .
@ -28,7 +26,6 @@ public final class RegistrationService {
this.accountRepository = accountRepository;
this.pendingRegistrations = new ConcurrentHashMap<>();
this.registrationAttempts = new ConcurrentHashMap<>();
this.verifiedUsers = ConcurrentHashMap.newKeySet();
}
/**
@ -41,36 +38,19 @@ public final class RegistrationService {
*/
public boolean initiateRegistration(String username, String email,
DifficultyLevel difficultyLevel) {
if (username == null || email == null || difficultyLevel == null) {
if (!EmailService.isValidEmail(email)) {
return false;
}
String normalizedUsername = username.trim();
String normalizedEmail = email.trim();
if (normalizedUsername.isEmpty() || normalizedEmail.isEmpty()) {
return false;
}
if (!EmailService.isValidEmail(normalizedEmail)) {
return false;
}
if (!accountRepository.registerUser(normalizedUsername, normalizedEmail, difficultyLevel)) {
if (!accountRepository.registerUser(username, email, difficultyLevel)) {
return false; // 用户名已存在或邮箱已注册
}
String registrationCode = EmailService.generateRegistrationCode();
pendingRegistrations.put(normalizedUsername, registrationCode);
registrationAttempts.put(normalizedUsername, 0);
verifiedUsers.remove(normalizedUsername);
boolean sent = EmailService.sendRegistrationCode(normalizedEmail, registrationCode);
if (!sent) {
pendingRegistrations.remove(normalizedUsername);
registrationAttempts.remove(normalizedUsername);
accountRepository.removeUnverifiedUser(normalizedUsername);
}
return sent;
pendingRegistrations.put(username, registrationCode);
registrationAttempts.put(username, 0);
return EmailService.sendRegistrationCode(email, registrationCode);
}
/**
@ -81,34 +61,25 @@ public final class RegistrationService {
* @return truefalse
*/
public boolean verifyRegistrationCode(String username, String registrationCode) {
if (username == null || registrationCode == null) {
return false;
}
String normalizedUsername = username.trim();
String trimmedCode = registrationCode.trim();
String storedCode = pendingRegistrations.get(normalizedUsername);
if (storedCode == null || !storedCode.equals(trimmedCode)) {
String storedCode = pendingRegistrations.get(username);
if (storedCode == null || !storedCode.equals(registrationCode)) {
// 跟踪失败尝试
int attempts = registrationAttempts.getOrDefault(normalizedUsername, 0);
int attempts = registrationAttempts.getOrDefault(username, 0);
attempts++;
registrationAttempts.put(normalizedUsername, attempts);
registrationAttempts.put(username, attempts);
if (attempts >= 3) {
// 如果失败次数过多,则删除用户
pendingRegistrations.remove(normalizedUsername);
registrationAttempts.remove(normalizedUsername);
verifiedUsers.remove(normalizedUsername);
accountRepository.removeUnverifiedUser(normalizedUsername);
pendingRegistrations.remove(username);
registrationAttempts.remove(username);
return false;
}
return false;
}
// 有效码,从待处理列表中移除
pendingRegistrations.remove(normalizedUsername);
registrationAttempts.remove(normalizedUsername);
verifiedUsers.add(normalizedUsername);
pendingRegistrations.remove(username);
registrationAttempts.remove(username);
return true;
}
@ -120,23 +91,11 @@ public final class RegistrationService {
* @return truefalse
*/
public boolean setPassword(String username, String password) {
if (username == null || password == null) {
return false;
}
String normalizedUsername = username.trim();
if (!PasswordValidator.isValidPassword(password)) {
return false;
}
if (!verifiedUsers.contains(normalizedUsername)) {
return false;
}
boolean updated = accountRepository.setPassword(normalizedUsername, password);
if (updated) {
verifiedUsers.remove(normalizedUsername);
}
return updated;
return accountRepository.setPassword(username, password);
}
/**
@ -148,14 +107,11 @@ public final class RegistrationService {
* @return truefalse
*/
public boolean changePassword(String username, String oldPassword, String newPassword) {
if (username == null || oldPassword == null || newPassword == null) {
return false;
}
if (!PasswordValidator.isValidPassword(newPassword)) {
return false;
}
return accountRepository.changePassword(username.trim(), oldPassword, newPassword);
return accountRepository.changePassword(username, oldPassword, newPassword);
}
/**
@ -167,10 +123,7 @@ public final class RegistrationService {
*/
public Optional<com.personalproject.auth.UserAccount> authenticate(String username,
String password) {
if (username == null || password == null) {
return Optional.empty();
}
return accountRepository.authenticate(username.trim(), password);
return accountRepository.authenticate(username, password);
}
/**
@ -180,22 +133,6 @@ public final class RegistrationService {
* @return truefalse
*/
public boolean userExists(String username) {
if (username == null) {
return false;
}
return accountRepository.userExists(username.trim());
}
/**
* .
*
* @param username
* @return Optional
*/
public Optional<com.personalproject.auth.UserAccount> getUser(String username) {
if (username == null) {
return Optional.empty();
}
return accountRepository.getUser(username.trim());
return accountRepository.userExists(username);
}
}
}

@ -87,20 +87,32 @@ public final class QuestionStorageService {
Files.createDirectories(resultsDirectory);
StringBuilder builder = new StringBuilder();
builder.append("考试试卷").append(System.lineSeparator());
builder.append("考试结果报告").append(System.lineSeparator());
builder.append("用户名: ").append(examSession.getUsername()).append(System.lineSeparator());
builder.append("难度: ").append(examSession.getDifficultyLevel().getDisplayName())
.append(System.lineSeparator());
builder.append("开始时间: ").append(examSession.getStartTime()).append(System.lineSeparator());
builder.append("题目数量: ").append(examSession.getQuestions().size())
.append(System.lineSeparator());
builder.append("得分: ").append(String.format("%.2f", examSession.calculateScore())).append("%")
.append(System.lineSeparator());
builder.append(System.lineSeparator());
// 只保存题目内容
// 添加逐题结果
for (int i = 0; i < examSession.getQuestions().size(); i++) {
var question = examSession.getQuestions().get(i);
int userAnswer = examSession.getUserAnswer(i);
boolean isCorrect = question.isAnswerCorrect(userAnswer);
builder.append("题目 ").append(i + 1).append(": ").append(question.getQuestionText())
.append(System.lineSeparator());
builder.append("您的答案: ").append(userAnswer == -1 ? "未回答" :
(userAnswer < question.getOptions().size() ? question.getOptions().get(userAnswer)
: "无效")).append(System.lineSeparator());
builder.append("正确答案: ").append(
question.getOptions().get(question.getCorrectAnswerIndex()))
.append(System.lineSeparator());
builder.append("结果: ").append(isCorrect ? "正确" : "错误").append(System.lineSeparator());
builder.append(System.lineSeparator());
}
@ -147,4 +159,4 @@ public final class QuestionStorageService {
"读取题目文件失败:" + path + ",原因:" + exception.getMessage() + ",将跳过该文件.");
}
}
}
}

@ -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<DifficultyLevel, QuestionGenerator> 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<com.personalproject.auth.UserAccount> 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<DifficultyLevel> 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<DifficultyLevel> 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<String> 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);
});
}
}

@ -1,19 +0,0 @@
package com.personalproject;
import com.personalproject.ui.MathExamGui;
/**
* . JavaFX GUI.
*/
public final class MathExamApplication {
/**
* JavaFX GUI.
*
* @param args 使.
*/
public static void main(String[] args) {
// 启动JavaFX应用程序
MathExamGui.main(args);
}
}

@ -1,324 +0,0 @@
package com.personalproject.auth;
import com.personalproject.model.DifficultyLevel;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
/**
* .
*/
public final class AccountRepository {
private static final Path ACCOUNTS_DIRECTORY = Paths.get("user_data", "accounts");
private static final String FILE_EXTENSION = ".properties";
private final Map<String, UserAccount> accounts = new ConcurrentHashMap<>();
/**
* .
*/
public AccountRepository() {
loadAccounts();
}
/**
* .
*
* @param username .
* @param password .
* @return .
*/
public Optional<UserAccount> authenticate(String username, String password) {
if (username == null || password == null) {
return Optional.empty();
}
String normalizedUsername = username.trim();
String normalizedPassword = password.trim();
UserAccount account = accounts.get(normalizedUsername);
if (account == null || !account.isRegistered()) {
return Optional.empty();
}
if (!account.password().equals(hashPassword(normalizedPassword))) {
return Optional.empty();
}
return Optional.of(account);
}
/**
* 使.
*
* @param username
* @param email
* @param difficultyLevel
* @return true false
*/
public synchronized boolean registerUser(String username, String email,
DifficultyLevel difficultyLevel) {
if (username == null || email == null || difficultyLevel == null) {
return false;
}
String normalizedUsername = username.trim();
String normalizedEmail = email.trim();
if (normalizedUsername.isEmpty() || normalizedEmail.isEmpty()) {
return false;
}
UserAccount existing = accounts.get(normalizedUsername);
if (existing != null && existing.isRegistered()) {
return false;
}
if (isEmailInUse(normalizedEmail, normalizedUsername)) {
return false;
}
LocalDateTime registrationDate =
existing != null ? existing.registrationDate() : LocalDateTime.now();
UserAccount account = new UserAccount(
normalizedUsername,
normalizedEmail,
"",
difficultyLevel,
registrationDate,
false);
accounts.put(normalizedUsername, account);
persistAccount(account);
return true;
}
/**
* .
*
* @param username
* @param password
* @return true false
*/
public synchronized boolean setPassword(String username, String password) {
if (username == null || password == null) {
return false;
}
String normalizedUsername = username.trim();
UserAccount account = accounts.get(normalizedUsername);
if (account == null || account.isRegistered()) {
return false;
}
UserAccount updatedAccount = new UserAccount(
account.username(),
account.email(),
hashPassword(password.trim()),
account.difficultyLevel(),
account.registrationDate(),
true);
accounts.put(normalizedUsername, updatedAccount);
persistAccount(updatedAccount);
return true;
}
/**
* .
*
* @param username
* @param oldPassword
* @param newPassword
* @return true false
*/
public synchronized boolean changePassword(String username, String oldPassword,
String newPassword) {
if (username == null || oldPassword == null || newPassword == null) {
return false;
}
String normalizedUsername = username.trim();
UserAccount account = accounts.get(normalizedUsername);
if (account == null || !account.isRegistered()) {
return false;
}
if (!account.password().equals(hashPassword(oldPassword.trim()))) {
return false;
}
UserAccount updatedAccount = new UserAccount(
account.username(),
account.email(),
hashPassword(newPassword.trim()),
account.difficultyLevel(),
account.registrationDate(),
true);
accounts.put(normalizedUsername, updatedAccount);
persistAccount(updatedAccount);
return true;
}
/**
* 便.
*
* @param username
*/
public synchronized void removeUnverifiedUser(String username) {
if (username == null) {
return;
}
String normalizedUsername = username.trim();
UserAccount account = accounts.get(normalizedUsername);
if (account != null && !account.isRegistered()) {
accounts.remove(normalizedUsername);
deleteAccountFile(normalizedUsername);
}
}
/**
* .
*
* @param username
* @return true false
*/
public boolean userExists(String username) {
if (username == null) {
return false;
}
return accounts.containsKey(username.trim());
}
/**
* .
*
* @param username
* @return Optional
*/
public Optional<UserAccount> getUser(String username) {
if (username == null) {
return Optional.empty();
}
return Optional.ofNullable(accounts.get(username.trim()));
}
private void loadAccounts() {
try {
if (!Files.exists(ACCOUNTS_DIRECTORY)) {
Files.createDirectories(ACCOUNTS_DIRECTORY);
return;
}
try (var paths = Files.list(ACCOUNTS_DIRECTORY)) {
paths
.filter(path -> path.getFileName().toString().endsWith(FILE_EXTENSION))
.forEach(this::loadAccountFromFile);
}
} catch (IOException exception) {
throw new IllegalStateException("加载账户数据失败", exception);
}
}
private void loadAccountFromFile(Path file) {
Properties properties = new Properties();
try (Reader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
properties.load(reader);
} catch (IOException exception) {
System.err.println("读取账户文件失败: " + file + ",原因: " + exception.getMessage());
return;
}
String username = properties.getProperty("username");
if (username == null || username.isBlank()) {
return;
}
String email = properties.getProperty("email", "");
String passwordHash = properties.getProperty("passwordHash", "");
String difficultyValue = properties.getProperty("difficulty", DifficultyLevel.PRIMARY.name());
String registrationDateValue = properties.getProperty("registrationDate");
String registeredValue = properties.getProperty("registered", "false");
try {
DifficultyLevel difficultyLevel = DifficultyLevel.valueOf(difficultyValue);
LocalDateTime registrationDate = registrationDateValue == null
? LocalDateTime.now()
: LocalDateTime.parse(registrationDateValue);
boolean registered = Boolean.parseBoolean(registeredValue);
UserAccount account = new UserAccount(
username,
email,
passwordHash,
difficultyLevel,
registrationDate,
registered);
accounts.put(username, account);
} catch (Exception exception) {
System.err.println("解析账户文件失败: " + file + ",原因: " + exception.getMessage());
}
}
private void persistAccount(UserAccount account) {
Properties properties = new Properties();
properties.setProperty("username", account.username());
properties.setProperty("email", account.email());
properties.setProperty("passwordHash", account.password());
properties.setProperty("difficulty", account.difficultyLevel().name());
properties.setProperty("registrationDate", account.registrationDate().toString());
properties.setProperty("registered", Boolean.toString(account.isRegistered()));
Path targetFile = accountFile(account.username());
try {
if (!Files.exists(ACCOUNTS_DIRECTORY)) {
Files.createDirectories(ACCOUNTS_DIRECTORY);
}
try (Writer writer = Files.newBufferedWriter(targetFile, StandardCharsets.UTF_8)) {
properties.store(writer, "User account data");
}
} catch (IOException exception) {
throw new IllegalStateException("保存账户数据失败: " + account.username(), exception);
}
}
private void deleteAccountFile(String username) {
Path file = accountFile(username);
try {
Files.deleteIfExists(file);
} catch (IOException exception) {
System.err.println("删除账户文件失败: " + file + ",原因: " + exception.getMessage());
}
}
private boolean isEmailInUse(String email, String currentUsername) {
return accounts.values().stream()
.anyMatch(account -> account.email().equalsIgnoreCase(email)
&& !account.username().equals(currentUsername));
}
private Path accountFile(String username) {
return ACCOUNTS_DIRECTORY.resolve(username + FILE_EXTENSION);
}
private String hashPassword(String password) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(password.getBytes(StandardCharsets.UTF_8));
return toHex(hashBytes);
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("当前运行环境不支持SHA-256算法", exception);
}
}
private String toHex(byte[] bytes) {
StringBuilder builder = new StringBuilder(bytes.length * 2);
for (byte value : bytes) {
builder.append(String.format("%02x", value));
}
return builder.toString();
}
}

@ -1,231 +0,0 @@
package com.personalproject.auth;
import jakarta.mail.Authenticator;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.PasswordAuthentication;
import jakarta.mail.Session;
import jakarta.mail.Transport;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
/**
* .
*/
public final class EmailService {
private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
private static final int CODE_LENGTH = 6;
private static final Random RANDOM = new Random();
private static final Path OUTBOX_DIRECTORY = Paths.get("user_data", "emails");
private static final String CONFIG_RESOURCE = "email-config.properties";
private static final DateTimeFormatter DATE_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
private static final Map<String, String> LAST_CODES = new ConcurrentHashMap<>();
private static final AtomicReference<MailConfiguration> CONFIG_CACHE = new AtomicReference<>();
private static final String DEFAULT_SUBJECT = "数学学习软件注册验证码";
private EmailService() {
// 防止实例化此工具类
}
/**
* .
*
* @return
*/
public static String generateRegistrationCode() {
StringBuilder code = new StringBuilder();
for (int i = 0; i < CODE_LENGTH; i++) {
code.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length())));
}
return code.toString();
}
/**
* .
*
* @param email
* @param registrationCode
* @return true
*/
public static boolean sendRegistrationCode(String email, String registrationCode) {
try {
MailConfiguration configuration = loadConfiguration();
sendEmail(email, registrationCode, configuration);
persistMessage(email, registrationCode);
LAST_CODES.put(email.trim().toLowerCase(), registrationCode);
return true;
} catch (Exception exception) {
System.err.println("发送验证码失败: " + exception.getMessage());
return false;
}
}
/**
* 便.
*
* @param email
* @return
*/
public static Optional<String> getLastSentRegistrationCode(String email) {
if (email == null) {
return Optional.empty();
}
return Optional.ofNullable(LAST_CODES.get(email.trim().toLowerCase()));
}
/**
* .
*
* @param email
* @return true false
*/
public static boolean isValidEmail(String email) {
if (email == null || email.trim().isEmpty()) {
return false;
}
String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@"
+ "(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
return email.matches(emailRegex);
}
private static MailConfiguration loadConfiguration() throws IOException {
MailConfiguration cached = CONFIG_CACHE.get();
if (cached != null) {
return cached;
}
synchronized (CONFIG_CACHE) {
cached = CONFIG_CACHE.get();
if (cached != null) {
return cached;
}
Properties properties = new Properties();
try (InputStream inputStream =
EmailService.class.getClassLoader().getResourceAsStream(CONFIG_RESOURCE)) {
if (inputStream == null) {
throw new IllegalStateException(
"类路径下缺少邮箱配置文件: " + CONFIG_RESOURCE
+ ",请在 src/main/resources 下提供该文件");
}
try (InputStreamReader reader =
new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
properties.load(reader);
}
}
String username = require(properties, "mail.username");
final String password = require(properties, "mail.password");
final String from = properties.getProperty("mail.from", username);
final String subject = properties.getProperty("mail.subject", DEFAULT_SUBJECT);
Properties smtpProperties = new Properties();
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
String key = entry.getKey().toString();
if (key.startsWith("mail.smtp.")) {
smtpProperties.put(key, entry.getValue());
}
}
if (!smtpProperties.containsKey("mail.smtp.host")) {
throw new IllegalStateException("邮箱配置缺少 mail.smtp.host");
}
if (!smtpProperties.containsKey("mail.smtp.port")) {
throw new IllegalStateException("邮箱配置缺少 mail.smtp.port");
}
smtpProperties.putIfAbsent("mail.smtp.auth", "true");
smtpProperties.putIfAbsent("mail.smtp.starttls.enable", "true");
MailConfiguration configuration =
new MailConfiguration(smtpProperties, username, password, from, subject);
CONFIG_CACHE.set(configuration);
return configuration;
}
}
private static void sendEmail(
String recipientEmail, String registrationCode, MailConfiguration configuration)
throws MessagingException {
Session session = Session.getInstance(configuration.smtpProperties(), new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(configuration.username(), configuration.password());
}
});
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(configuration.from()));
message.setRecipients(
Message.RecipientType.TO,
InternetAddress.parse(recipientEmail, false));
message.setSubject(configuration.subject());
String body = "您好,您的注册码为:" + registrationCode + System.lineSeparator()
+ "请在10分钟内使用该验证码完成注册。";
message.setText(body, StandardCharsets.UTF_8.name());
Transport.send(message);
}
private static void persistMessage(String email, String registrationCode) throws IOException {
if (!Files.exists(OUTBOX_DIRECTORY)) {
Files.createDirectories(OUTBOX_DIRECTORY);
}
String sanitizedEmail = sanitizeEmail(email);
String timestamp = DATE_TIME_FORMATTER.format(LocalDateTime.now());
final Path messageFile = OUTBOX_DIRECTORY.resolve(sanitizedEmail + "_" + timestamp + ".txt");
StringBuilder content = new StringBuilder();
content.append("收件人: ").append(email).append(System.lineSeparator());
content.append("注册码: ").append(registrationCode).append(System.lineSeparator());
content.append("发送时间: ").append(LocalDateTime.now()).append(System.lineSeparator());
Files.writeString(
messageFile,
content.toString(),
StandardCharsets.UTF_8,
StandardOpenOption.CREATE_NEW,
StandardOpenOption.WRITE);
}
private static String require(Properties properties, String key) {
String value = properties.getProperty(key);
if (value == null || value.trim().isEmpty()) {
throw new IllegalStateException("邮箱配置缺少必要字段: " + key);
}
return value.trim();
}
private static String sanitizeEmail(String email) {
return email.replaceAll("[^a-zA-Z0-9._-]", "_");
}
private record MailConfiguration(
Properties smtpProperties,
String username,
String password,
String from,
String subject) {
}
}

@ -1,37 +0,0 @@
package com.personalproject.auth;
import java.util.regex.Pattern;
/**
* .
*/
public final class PasswordValidator {
private static final Pattern PASSWORD_PATTERN =
Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{6,10}$");
private PasswordValidator() {
// 防止实例化此工具类
}
/**
* .
* - 6-10
* -
* -
* -
*
* @param password
* @return true false
*/
public static boolean isValidPassword(String password) {
if (password == null) {
return false;
}
String normalized = password.trim();
if (normalized.isEmpty()) {
return false;
}
return PASSWORD_PATTERN.matcher(normalized).matches();
}
}

@ -1,119 +0,0 @@
package com.personalproject.generator;
import com.personalproject.model.QuizQuestion;
import com.personalproject.service.MathExpressionEvaluator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
/**
* .
*/
public final class PrimaryQuestionGenerator implements QuestionGenerator, QuizQuestionGenerator {
private static final String[] OPERATORS = {"+", "-", "*", "/"};
private static final int OPTIONS_COUNT = 4;
@Override
public String generateQuestion(Random random) {
// 至少生成两个操作数,避免题目退化成单个数字
int operandCount = random.nextInt(4) + 2;
StringBuilder builder = new StringBuilder();
for (int index = 0; index < operandCount; index++) {
if (index > 0) {
String operator = OPERATORS[random.nextInt(OPERATORS.length)];
builder.append(' ').append(operator).append(' ');
}
int value = random.nextInt(100) + 1;
builder.append(value);
}
String expression = builder.toString();
if (operandCount > 1 && random.nextBoolean()) {
return '(' + expression + ')';
}
return expression;
}
@Override
public QuizQuestion generateQuizQuestion(Random random) {
// 生成数学表达式
int operandCount = random.nextInt(4) + 2;
StringBuilder builder = new StringBuilder();
for (int index = 0; index < operandCount; index++) {
if (index > 0) {
String operator = OPERATORS[random.nextInt(OPERATORS.length)];
builder.append(' ').append(operator).append(' ');
}
int value = random.nextInt(100) + 1;
builder.append(value);
}
String expression = builder.toString();
if (operandCount > 1 && random.nextBoolean()) {
expression = '(' + expression + ')';
}
// 直接计算正确答案
double correctAnswer;
try {
correctAnswer = MathExpressionEvaluator.evaluate(expression);
} catch (Exception e) {
// 如果计算失败则使用兜底值
correctAnswer = 0.0;
}
List<String> options = generateOptions(correctAnswer, random);
String correctOption = formatOption(correctAnswer);
int correctAnswerIndex = options.indexOf(correctOption);
if (correctAnswerIndex < 0) {
// 兜底逻辑:确保正确答案存在于选项中
options.set(0, correctOption);
correctAnswerIndex = 0;
}
return new QuizQuestion(expression, options, correctAnswerIndex);
}
/**
* .
*/
private List<String> generateOptions(double correctAnswer, Random random) {
String correctOption = formatOption(correctAnswer);
Set<String> optionSet = new LinkedHashSet<>();
optionSet.add(correctOption);
double scale = Math.max(Math.abs(correctAnswer) * 0.2, 1.0);
int attempts = 0;
while (optionSet.size() < OPTIONS_COUNT && attempts < 100) {
double delta = random.nextGaussian() * scale;
if (Math.abs(delta) < 0.5) {
double direction = random.nextBoolean() ? 1 : -1;
delta = direction * (0.5 + random.nextDouble()) * scale;
}
double candidate = correctAnswer + delta;
if (Double.isNaN(candidate) || Double.isInfinite(candidate)) {
attempts++;
continue;
}
optionSet.add(formatOption(candidate));
attempts++;
}
double step = Math.max(scale, 1.0);
while (optionSet.size() < OPTIONS_COUNT) {
double candidate = correctAnswer + step * optionSet.size();
optionSet.add(formatOption(candidate));
step += 1.0;
}
List<String> options = new ArrayList<>(optionSet);
Collections.shuffle(options, random);
return options;
}
private String formatOption(double value) {
return String.format("%.2f", value);
}
}

@ -1,18 +0,0 @@
package com.personalproject.generator;
import com.personalproject.model.QuizQuestion;
import java.util.Random;
/**
* .
*/
public interface QuizQuestionGenerator {
/**
* .
*
* @param random .
* @return .
*/
QuizQuestion generateQuizQuestion(Random random);
}

@ -1,158 +0,0 @@
package com.personalproject.service;
import com.personalproject.generator.QuestionGenerator;
import com.personalproject.model.DifficultyLevel;
import com.personalproject.model.ExamSession;
import com.personalproject.model.QuizQuestion;
import com.personalproject.storage.QuestionStorageService;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
/**
* .
*/
public final class ExamService {
private static final int OPTIONS_COUNT = 4;
private final Map<DifficultyLevel, QuestionGenerator> generators;
private final Random random = new Random();
private final QuestionGenerationService questionGenerationService;
private final QuestionStorageService questionStorageService;
/**
* .
*
* @param generatorMap
* @param questionGenerationService
* @param questionStorageService
*/
public ExamService(
Map<DifficultyLevel, QuestionGenerator> generatorMap,
QuestionGenerationService questionGenerationService,
QuestionStorageService questionStorageService) {
this.generators = new EnumMap<>(DifficultyLevel.class);
this.generators.putAll(generatorMap);
this.questionGenerationService = questionGenerationService;
this.questionStorageService = questionStorageService;
}
/**
* 使.
*
* @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之间");
}
// 根据难度级别生成题目
if (!generators.containsKey(difficultyLevel)) {
throw new IllegalArgumentException("找不到难度级别的生成器: " + difficultyLevel);
}
try {
Set<String> existingQuestions = questionStorageService.loadExistingQuestions(username);
List<String> uniqueQuestions = questionGenerationService.generateUniqueQuestions(
difficultyLevel, questionCount, existingQuestions);
List<QuizQuestion> quizQuestions = new ArrayList<>();
for (String questionText : uniqueQuestions) {
quizQuestions.add(buildQuizQuestion(questionText));
}
questionStorageService.saveQuestions(username, uniqueQuestions);
return new ExamSession(username, difficultyLevel, quizQuestions);
} catch (IOException exception) {
throw new IllegalStateException("生成考试题目失败", exception);
}
}
/**
* 使.
*
* @param username
* @param difficultyLevel
* @param questions
* @return
*/
public ExamSession createExamSession(
String username, DifficultyLevel difficultyLevel, List<QuizQuestion> questions) {
if (questions.size() < 10 || questions.size() > 30) {
throw new IllegalArgumentException("题目数量必须在10到30之间");
}
return new ExamSession(username, difficultyLevel, questions);
}
private QuizQuestion buildQuizQuestion(String questionText) {
try {
double correctAnswer = MathExpressionEvaluator.evaluate(questionText);
String formattedCorrectAnswer = formatAnswer(correctAnswer);
List<String> options = buildOptions(correctAnswer, formattedCorrectAnswer);
int correctAnswerIndex = options.indexOf(formattedCorrectAnswer);
if (correctAnswerIndex < 0) {
options.set(0, formattedCorrectAnswer);
correctAnswerIndex = 0;
}
return new QuizQuestion(questionText, options, correctAnswerIndex);
} catch (RuntimeException exception) {
List<String> fallbackOptions = new ArrayList<>();
for (int i = 1; i <= OPTIONS_COUNT; i++) {
fallbackOptions.add("选项" + i);
}
return new QuizQuestion(questionText, fallbackOptions, 0);
}
}
private List<String> buildOptions(double correctAnswer, String formattedCorrectAnswer) {
Set<String> optionSet = new LinkedHashSet<>();
optionSet.add(formattedCorrectAnswer);
int attempts = 0;
while (optionSet.size() < OPTIONS_COUNT && attempts < 100) {
double offset = generateOffset(correctAnswer);
double candidateValue = correctAnswer + offset;
if (Double.isNaN(candidateValue) || Double.isInfinite(candidateValue)) {
attempts++;
continue;
}
String candidate = formatAnswer(candidateValue);
if (!candidate.equals(formattedCorrectAnswer)) {
optionSet.add(candidate);
}
attempts++;
}
while (optionSet.size() < OPTIONS_COUNT) {
optionSet.add(formatAnswer(correctAnswer + optionSet.size() * 1.5));
}
List<String> options = new ArrayList<>(optionSet);
Collections.shuffle(options, random);
return options;
}
private double generateOffset(double base) {
double scale = Math.max(1.0, Math.abs(base) / 2.0);
double offset = random.nextGaussian() * scale;
if (Math.abs(offset) < 0.5) {
offset += offset >= 0 ? 1.5 : -1.5;
}
return offset;
}
private String formatAnswer(double value) {
return String.format("%.2f", value);
}
}

@ -1,54 +0,0 @@
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.service.QuestionGenerationService;
import com.personalproject.ui.scenes.LoginScene;
import java.util.EnumMap;
import java.util.Map;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
/**
* JavaFX . .
*/
public final class MathExamGui extends Application {
private MathLearningController controller;
@Override
public void start(Stage primaryStage) {
// 使用题目生成器初始化控制器
Map<DifficultyLevel, QuestionGenerator> 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);
// 配置主舞台
primaryStage.setTitle("数学学习软件");
// 从登录界面开始
LoginScene loginScene = new LoginScene(primaryStage, controller);
Scene scene = new Scene(loginScene, 600, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* JavaFX .
*
* @param args
*/
public static void main(String[] args) {
launch(args);
}
}

@ -1,162 +0,0 @@
package com.personalproject.ui.scenes;
import com.personalproject.controller.MathLearningController;
import com.personalproject.ui.views.MainMenuView;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
/**
* .
*/
public class LoginScene extends BorderPane {
private final Stage primaryStage;
private final MathLearningController controller;
private TextField usernameField;
private PasswordField passwordField;
private Button loginButton;
private Button registerButton;
/**
* LoginScene .
*
* @param primaryStage
* @param controller
*/
public LoginScene(Stage primaryStage, MathLearningController controller) {
this.primaryStage = primaryStage;
this.controller = controller;
initializeUi();
}
/**
* .
*/
private void initializeUi() {
// 创建主布局
VBox mainLayout = new VBox(15);
mainLayout.setAlignment(Pos.CENTER);
mainLayout.setPadding(new Insets(20));
// 标题
final Label titleLabel = new Label("数学学习软件");
titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24));
// 登录表单
GridPane loginForm = new GridPane();
loginForm.setHgap(10);
loginForm.setVgap(10);
loginForm.setAlignment(Pos.CENTER);
final Label usernameLabel = new Label("用户名:");
usernameField = new TextField();
usernameField.setPrefWidth(200);
final Label passwordLabel = new Label("密码:");
passwordField = new PasswordField();
passwordField.setPrefWidth(200);
loginForm.add(usernameLabel, 0, 0);
loginForm.add(usernameField, 1, 0);
loginForm.add(passwordLabel, 0, 1);
loginForm.add(passwordField, 1, 1);
// 按钮
HBox buttonBox = new HBox(10);
buttonBox.setAlignment(Pos.CENTER);
loginButton = new Button("登录");
registerButton = new Button("注册");
// 设置按钮样式
loginButton.setPrefWidth(100);
registerButton.setPrefWidth(100);
buttonBox.getChildren().addAll(loginButton, registerButton);
// 将组件添加到主布局
mainLayout.getChildren().addAll(titleLabel, loginForm, buttonBox);
// 将主布局放到边界面板中央
setCenter(mainLayout);
// 添加事件处理器
addEventHandlers();
}
/**
* .
*/
private void addEventHandlers() {
loginButton.setOnAction(e -> handleLogin());
registerButton.setOnAction(e -> handleRegistration());
// 允许使用回车键登录
setOnKeyPressed(event -> {
if (event.getCode().toString().equals("ENTER")) {
handleLogin();
}
});
}
/**
* .
*/
private void handleLogin() {
String username = usernameField.getText().trim();
String password = passwordField.getText();
if (username.isEmpty() || password.isEmpty()) {
showAlert(Alert.AlertType.WARNING, "警告", "请输入用户名和密码");
return;
}
// 验证用户
var userAccount = controller.authenticate(username, password);
if (userAccount.isPresent()) {
// 登录成功,跳转到主菜单
MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount.get());
primaryStage.getScene().setRoot(mainMenuView);
} else {
// 登录失败
showAlert(Alert.AlertType.ERROR, "登录失败", "用户名或密码错误");
}
}
/**
* .
*/
private void handleRegistration() {
// 切换到注册界面
RegistrationScene registrationScene = new RegistrationScene(primaryStage, controller);
primaryStage.getScene().setRoot(registrationScene);
}
/**
* .
*
* @param alertType
* @param title
* @param message
*/
private void showAlert(Alert.AlertType alertType, String title, String message) {
Alert alert = new Alert(alertType);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
}

@ -1,288 +0,0 @@
package com.personalproject.ui.scenes;
import com.personalproject.controller.MathLearningController;
import com.personalproject.model.DifficultyLevel;
import com.personalproject.ui.views.MainMenuView;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
/**
* .
*/
public class RegistrationScene extends BorderPane {
private final Stage primaryStage;
private final MathLearningController controller;
private TextField usernameField;
private TextField emailField;
private ComboBox<DifficultyLevel> difficultyComboBox;
private Button sendCodeButton;
private TextField registrationCodeField;
private Button verifyCodeButton;
private PasswordField passwordField;
private PasswordField confirmPasswordField;
private Button setPasswordButton;
private Button backButton;
private VBox registrationForm;
/**
* RegistrationScene .
*
* @param primaryStage
* @param controller
*/
public RegistrationScene(Stage primaryStage, MathLearningController controller) {
this.primaryStage = primaryStage;
this.controller = controller;
initializeUi();
}
/**
* .
*/
private void initializeUi() {
// 创建主布局
VBox mainLayout = new VBox(15);
mainLayout.setAlignment(Pos.TOP_CENTER);
mainLayout.setPadding(new Insets(20));
// 标题
final Label titleLabel = new Label("用户注册");
titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24));
// 注册表单
registrationForm = new VBox(15);
registrationForm.setAlignment(Pos.CENTER);
// 步骤1填写基础信息
GridPane basicInfoForm = new GridPane();
basicInfoForm.setHgap(10);
basicInfoForm.setVgap(10);
basicInfoForm.setAlignment(Pos.CENTER);
final Label usernameLabel = new Label("用户名:");
usernameField = new TextField();
usernameField.setPrefWidth(200);
final Label emailLabel = new Label("邮箱:");
emailField = new TextField();
emailField.setPrefWidth(200);
final Label difficultyLabel = new Label("默认难度:");
difficultyComboBox = new ComboBox<>();
difficultyComboBox.getItems()
.addAll(DifficultyLevel.PRIMARY, DifficultyLevel.MIDDLE, DifficultyLevel.HIGH);
difficultyComboBox.setValue(DifficultyLevel.PRIMARY);
difficultyComboBox.setPrefWidth(200);
basicInfoForm.add(usernameLabel, 0, 0);
basicInfoForm.add(usernameField, 1, 0);
basicInfoForm.add(emailLabel, 0, 1);
basicInfoForm.add(emailField, 1, 1);
basicInfoForm.add(difficultyLabel, 0, 2);
basicInfoForm.add(difficultyComboBox, 1, 2);
sendCodeButton = new Button("发送注册码");
sendCodeButton.setPrefWidth(120);
registrationForm.getChildren().addAll(basicInfoForm, sendCodeButton);
// 步骤2验证码验证初始隐藏
VBox verificationSection = new VBox(10);
verificationSection.setAlignment(Pos.CENTER);
verificationSection.setVisible(false);
verificationSection.setManaged(false);
final Label codeLabel = new Label("注册码:");
registrationCodeField = new TextField();
registrationCodeField.setPrefWidth(200);
verifyCodeButton = new Button("验证注册码");
verifyCodeButton.setPrefWidth(120);
verificationSection.getChildren().addAll(codeLabel, registrationCodeField, verifyCodeButton);
registrationForm.getChildren().add(verificationSection);
// 步骤3设置密码初始隐藏
VBox passwordSection = new VBox(10);
passwordSection.setAlignment(Pos.CENTER);
passwordSection.setVisible(false);
passwordSection.setManaged(false);
final Label passwordLabel = new Label("设置密码 (6-10位包含大小写字母和数字):");
passwordField = new PasswordField();
passwordField.setPrefWidth(200);
final Label confirmPasswordLabel = new Label("确认密码:");
confirmPasswordField = new PasswordField();
confirmPasswordField.setPrefWidth(200);
setPasswordButton = new Button("设置密码");
setPasswordButton.setPrefWidth(120);
passwordSection.getChildren().addAll(passwordLabel, passwordField, confirmPasswordLabel,
confirmPasswordField, setPasswordButton);
registrationForm.getChildren().add(passwordSection);
// 返回按钮
backButton = new Button("返回");
backButton.setPrefWidth(100);
// 将组件添加到主布局
mainLayout.getChildren().addAll(titleLabel, registrationForm, backButton);
// 使用可滚动容器保证内容在小窗口中完整显示
ScrollPane scrollPane = new ScrollPane(mainLayout);
scrollPane.setFitToWidth(true);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
setCenter(scrollPane);
// 添加事件处理器
addEventHandlers(sendCodeButton, verificationSection, verifyCodeButton, passwordSection);
}
/**
* .
*/
private void addEventHandlers(Button sendCodeButton, VBox verificationSection,
Button verifyCodeButton, VBox passwordSection) {
sendCodeButton.setOnAction(e -> handleSendCode(verificationSection));
verifyCodeButton.setOnAction(e -> handleVerifyCode(passwordSection));
setPasswordButton.setOnAction(e -> handleSetPassword());
backButton.setOnAction(e -> handleBack());
}
/**
* .
*/
private void handleSendCode(VBox verificationSection) {
final String username = usernameField.getText().trim();
final String email = emailField.getText().trim();
final DifficultyLevel difficultyLevel = difficultyComboBox.getValue();
if (username.isEmpty() || email.isEmpty()) {
showAlert(Alert.AlertType.WARNING, "警告", "请输入用户名和邮箱");
return;
}
if (!controller.isValidEmail(email)) {
showAlert(Alert.AlertType.ERROR, "邮箱格式错误", "请输入有效的邮箱地址");
return;
}
// 发起注册
boolean success = controller.initiateRegistration(username, email, difficultyLevel);
if (success) {
showAlert(Alert.AlertType.INFORMATION, "注册码已发送", "注册码已发送至您的邮箱,请查收。");
verificationSection.setVisible(true);
verificationSection.setManaged(true);
} else {
showAlert(Alert.AlertType.ERROR, "注册失败", "用户名或邮箱可能已存在,请重试。");
}
}
/**
* .
*/
private void handleVerifyCode(VBox passwordSection) {
final String username = usernameField.getText().trim();
final String registrationCode = registrationCodeField.getText().trim();
if (registrationCode.isEmpty()) {
showAlert(Alert.AlertType.WARNING, "警告", "请输入注册码");
return;
}
boolean verified = controller.verifyRegistrationCode(username, registrationCode);
if (verified) {
showAlert(Alert.AlertType.INFORMATION, "验证成功", "注册码验证成功!");
passwordSection.setVisible(true);
passwordSection.setManaged(true);
} else {
showAlert(Alert.AlertType.ERROR, "验证失败", "注册码验证失败,请检查后重试。");
}
}
/**
* .
*/
private void handleSetPassword() {
final String username = usernameField.getText().trim();
final String password = passwordField.getText();
final String confirmPassword = confirmPasswordField.getText();
if (password.isEmpty() || confirmPassword.isEmpty()) {
showAlert(Alert.AlertType.WARNING, "警告", "请输入并确认密码");
return;
}
if (!password.equals(confirmPassword)) {
showAlert(Alert.AlertType.ERROR, "密码不匹配", "两次输入的密码不一致");
return;
}
if (!controller.isValidPassword(password)) {
showAlert(Alert.AlertType.ERROR, "密码不符合要求",
"密码长度必须为6-10位且包含大小写字母和数字");
return;
}
boolean success = controller.setPassword(username, password);
if (success) {
var userAccountOptional = controller.getUserAccount(username);
if (userAccountOptional.isPresent()) {
showAlert(Alert.AlertType.INFORMATION, "注册成功", "注册成功!正在进入难度选择界面。");
MainMenuView mainMenuView = new MainMenuView(primaryStage, controller,
userAccountOptional.get());
if (primaryStage.getScene() != null) {
primaryStage.getScene().setRoot(mainMenuView);
}
} else {
showAlert(Alert.AlertType.WARNING, "注册提示",
"注册成功,但未能加载用户信息,请使用新密码登录。");
handleBack();
}
} else {
showAlert(Alert.AlertType.ERROR, "设置密码失败", "设置密码失败,请重试。");
}
}
/**
* .
*/
private void handleBack() {
LoginScene loginScene = new LoginScene(primaryStage, controller);
primaryStage.getScene().setRoot(loginScene);
}
/**
* .
*
* @param alertType
* @param title
* @param message
*/
private void showAlert(Alert.AlertType alertType, String title, String message) {
Alert alert = new Alert(alertType);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
}

@ -1,161 +0,0 @@
package com.personalproject.ui.views;
import com.personalproject.controller.MathLearningController;
import com.personalproject.model.ExamSession;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
/**
* .
*/
public class ExamResultsView extends BorderPane {
private final Stage primaryStage;
private final MathLearningController controller;
private final ExamSession examSession;
private Button continueButton;
private Button exitButton;
/**
* ExamResultsView .
*
* @param primaryStage
* @param controller
* @param examSession
*/
public ExamResultsView(Stage primaryStage, MathLearningController controller,
ExamSession examSession) {
this.primaryStage = primaryStage;
this.controller = controller;
this.examSession = examSession;
initializeUi();
}
/**
* .
*/
private void initializeUi() {
// 创建主布局
VBox mainLayout = new VBox(20);
mainLayout.setAlignment(Pos.CENTER);
mainLayout.setPadding(new Insets(20));
// 结果标题
Label titleLabel = new Label("考试结果");
titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24));
// 分数展示
double score = examSession.calculateScore();
Label scoreLabel = new Label(String.format("您的得分: %.2f", score));
scoreLabel.setFont(Font.font("System", FontWeight.BOLD, 18));
// 成绩明细
VBox breakdownBox = new VBox(10);
breakdownBox.setAlignment(Pos.CENTER);
Label totalQuestionsLabel = new Label("总题数: " + examSession.getTotalQuestions());
Label correctAnswersLabel = new Label("答对题数: " + examSession.getCorrectAnswersCount());
Label incorrectAnswersLabel = new Label("答错题数: " + examSession.getIncorrectAnswersCount());
breakdownBox.getChildren()
.addAll(totalQuestionsLabel, correctAnswersLabel, incorrectAnswersLabel);
// 按钮区域
HBox buttonBox = new HBox(15);
buttonBox.setAlignment(Pos.CENTER);
continueButton = new Button("继续考试");
exitButton = new Button("退出");
// 设置按钮尺寸
continueButton.setPrefSize(120, 40);
exitButton.setPrefSize(120, 40);
buttonBox.getChildren().addAll(continueButton, exitButton);
// 将组件添加到主布局
mainLayout.getChildren().addAll(titleLabel, scoreLabel, breakdownBox, buttonBox);
// 将主布局置于边界面板中央
setCenter(mainLayout);
// 添加事件处理器
addEventHandlers();
}
/**
* .
*/
private void addEventHandlers() {
continueButton.setOnAction(e -> handleContinue());
exitButton.setOnAction(e -> handleExit());
}
/**
* .
*/
private void handleContinue() {
// 返回主菜单以开始新考试
controller.getUserAccount(examSession.getUsername())
.ifPresentOrElse(
userAccount -> {
MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount);
primaryStage.getScene().setRoot(mainMenuView);
},
() -> {
// 如果找不到用户信息,则提示错误并返回登录界面
showAlert(Alert.AlertType.ERROR, "错误", "用户信息无法找到,请重新登录");
// 返回登录场景
com.personalproject.ui.scenes.LoginScene loginScene =
new com.personalproject.ui.scenes.LoginScene(primaryStage, controller);
primaryStage.getScene().setRoot(loginScene);
}
);
}
/**
* 退.
*/
private void handleExit() {
// 返回主菜单
controller.getUserAccount(examSession.getUsername())
.ifPresentOrElse(
userAccount -> {
MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount);
primaryStage.getScene().setRoot(mainMenuView);
},
() -> {
// 如果找不到用户信息,则提示错误并返回登录界面
showAlert(Alert.AlertType.ERROR, "错误", "用户信息无法找到,请重新登录");
// 返回登录场景
com.personalproject.ui.scenes.LoginScene loginScene =
new com.personalproject.ui.scenes.LoginScene(primaryStage, controller);
primaryStage.getScene().setRoot(loginScene);
}
);
}
/**
* .
*
* @param alertType
* @param title
* @param message
*/
private void showAlert(Alert.AlertType alertType, String title, String message) {
Alert alert = new Alert(alertType);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
}

@ -1,157 +0,0 @@
package com.personalproject.ui.views;
import com.personalproject.auth.UserAccount;
import com.personalproject.controller.MathLearningController;
import com.personalproject.model.DifficultyLevel;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.Spinner;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
/**
* .
*/
public class ExamSelectionView extends BorderPane {
private final Stage primaryStage;
private final MathLearningController controller;
private final UserAccount userAccount;
private ComboBox<DifficultyLevel> difficultyComboBox;
private Spinner<Integer> questionCountSpinner;
private Button startExamButton;
private Button backButton;
/**
* ExamSelectionView .
*
* @param primaryStage
* @param controller
* @param userAccount
*/
public ExamSelectionView(Stage primaryStage, MathLearningController controller,
UserAccount userAccount) {
this.primaryStage = primaryStage;
this.controller = controller;
this.userAccount = userAccount;
initializeUi();
}
/**
* .
*/
private void initializeUi() {
// 创建主布局
VBox mainLayout = new VBox(20);
mainLayout.setAlignment(Pos.CENTER);
mainLayout.setPadding(new Insets(20));
// 标题
final Label titleLabel = new Label("考试设置");
titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24));
// 考试设置表单
GridPane examSettingsForm = new GridPane();
examSettingsForm.setHgap(15);
examSettingsForm.setVgap(15);
examSettingsForm.setAlignment(Pos.CENTER);
final Label difficultyLabel = new Label("选择难度:");
difficultyComboBox = new ComboBox<>();
difficultyComboBox.getItems()
.addAll(DifficultyLevel.PRIMARY, DifficultyLevel.MIDDLE, DifficultyLevel.HIGH);
difficultyComboBox.setValue(userAccount.difficultyLevel()); // 默认选中用户的难度
difficultyComboBox.setPrefWidth(200);
final Label questionCountLabel = new Label("题目数量 (10-30):");
questionCountSpinner = new Spinner<>(10, 30, 10); // 最小值、最大值、初始值
questionCountSpinner.setPrefWidth(200);
examSettingsForm.add(difficultyLabel, 0, 0);
examSettingsForm.add(difficultyComboBox, 1, 0);
examSettingsForm.add(questionCountLabel, 0, 1);
examSettingsForm.add(questionCountSpinner, 1, 1);
// 按钮区域
HBox buttonBox = new HBox(15);
buttonBox.setAlignment(Pos.CENTER);
startExamButton = new Button("开始考试");
backButton = new Button("返回");
// 设置按钮尺寸
startExamButton.setPrefSize(120, 40);
backButton.setPrefSize(120, 40);
buttonBox.getChildren().addAll(startExamButton, backButton);
// 将组件添加到主布局
mainLayout.getChildren().addAll(titleLabel, examSettingsForm, buttonBox);
// 将主布局置于边界面板中央
setCenter(mainLayout);
// 添加事件处理器
addEventHandlers();
}
/**
* .
*/
private void addEventHandlers() {
startExamButton.setOnAction(e -> handleStartExam());
backButton.setOnAction(e -> handleBack());
}
/**
* .
*/
private void handleStartExam() {
DifficultyLevel selectedDifficulty = difficultyComboBox.getValue();
int questionCount = questionCountSpinner.getValue();
if (questionCount < 10 || questionCount > 30) {
showAlert(Alert.AlertType.WARNING, "无效输入", "题目数量必须在10到30之间");
return;
}
// 创建并启动考试会话
com.personalproject.model.ExamSession examSession = controller.createExamSession(
userAccount.username(), selectedDifficulty, questionCount);
ExamView examView = new ExamView(primaryStage, controller, examSession);
primaryStage.getScene().setRoot(examView);
}
/**
* .
*/
private void handleBack() {
MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount);
primaryStage.getScene().setRoot(mainMenuView);
}
/**
* .
*
* @param alertType
* @param title
* @param message
*/
private void showAlert(Alert.AlertType alertType, String title, String message) {
Alert alert = new Alert(alertType);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
}

@ -1,267 +0,0 @@
package com.personalproject.ui.views;
import com.personalproject.controller.MathLearningController;
import com.personalproject.model.ExamSession;
import com.personalproject.model.QuizQuestion;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
/**
* .
*/
public class ExamView extends BorderPane {
private final Stage primaryStage;
private final MathLearningController controller;
private final ExamSession examSession;
private Label questionNumberLabel;
private Label questionTextLabel;
private ToggleGroup answerToggleGroup;
private VBox optionsBox;
private Button nextButton;
private Button previousButton;
private Button finishButton;
private HBox buttonBox;
/**
* ExamView .
*
* @param primaryStage
* @param controller
* @param examSession
*/
public ExamView(Stage primaryStage, MathLearningController controller, ExamSession examSession) {
this.primaryStage = primaryStage;
this.controller = controller;
this.examSession = examSession;
initializeUi();
}
/**
* .
*/
private void initializeUi() {
// 创建主布局
VBox mainLayout = new VBox(20);
mainLayout.setAlignment(Pos.CENTER);
mainLayout.setPadding(new Insets(20));
// 题号
questionNumberLabel = new Label();
questionNumberLabel.setFont(Font.font("System", FontWeight.BOLD, 16));
// 题目文本
questionTextLabel = new Label();
questionTextLabel.setWrapText(true);
questionTextLabel.setFont(Font.font("System", FontWeight.NORMAL, 14));
questionTextLabel.setMaxWidth(500);
// 选项容器
optionsBox = new VBox(10);
optionsBox.setPadding(new Insets(10));
answerToggleGroup = new ToggleGroup();
// 按钮区域
buttonBox = new HBox(15);
buttonBox.setAlignment(Pos.CENTER);
previousButton = new Button("上一题");
nextButton = new Button("下一题");
finishButton = new Button("完成考试");
// 设置按钮尺寸
previousButton.setPrefSize(100, 35);
nextButton.setPrefSize(100, 35);
finishButton.setPrefSize(120, 35);
buttonBox.getChildren().addAll(previousButton, nextButton, finishButton);
// 将组件添加到主布局
mainLayout.getChildren().addAll(questionNumberLabel, questionTextLabel, optionsBox, buttonBox);
// 将主布局置于边界面板中央
setCenter(mainLayout);
// 加载第一题
loadCurrentQuestion();
// 添加事件处理器
addEventHandlers();
}
/**
* .
*/
private void loadCurrentQuestion() {
try {
// 在加载下一题之前检查考试是否已完成
if (examSession.isComplete()) {
// 如果考试已完成,则启用“完成考试”按钮
updateButtonStates();
return;
}
QuizQuestion currentQuestion = examSession.getCurrentQuestion();
int currentIndex = examSession.getCurrentQuestionIndex();
if (currentQuestion == null) {
showAlert(Alert.AlertType.ERROR, "错误", "当前题目为空,请重新开始考试");
return;
}
// 更新题号与题目文本
questionNumberLabel.setText("第 " + (currentIndex + 1) + " 题");
questionTextLabel.setText(currentQuestion.getQuestionText());
// 清空上一题的选项
answerToggleGroup.selectToggle(null);
answerToggleGroup.getToggles().clear();
optionsBox.getChildren().clear();
// 创建新的选项组件
for (int i = 0; i < currentQuestion.getOptions().size(); i++) {
String option = currentQuestion.getOptions().get(i);
RadioButton optionButton = new RadioButton((i + 1) + ". " + option);
optionButton.setToggleGroup(answerToggleGroup);
optionButton.setUserData(i); // 存储选项索引
// 如果该题已有答案则自动选中
if (examSession.hasAnswered(currentIndex)
&& examSession.getUserAnswer(currentIndex) == i) {
optionButton.setSelected(true);
}
optionsBox.getChildren().add(optionButton);
}
// 更新按钮状态
updateButtonStates();
} catch (Exception e) {
showAlert(Alert.AlertType.ERROR, "错误", "加载题目时发生错误: " + e.getMessage());
}
}
/**
* .
*/
private void updateButtonStates() {
try {
int currentIndex = examSession.getCurrentQuestionIndex();
int totalQuestions = examSession.getTotalQuestions();
// 处理潜在极端情况
if (totalQuestions <= 0) {
// 如果没有题目,则禁用所有导航按钮
previousButton.setDisable(true);
nextButton.setDisable(true);
finishButton.setDisable(false); // 仍允许完成考试
return;
}
// “上一题”按钮状态
previousButton.setDisable(currentIndex < 0 || currentIndex == 0);
// “下一题”按钮状态
nextButton.setDisable(currentIndex < 0 || currentIndex >= totalQuestions - 1);
// “完成考试”按钮状态——在考试完成或到达最后一题时启用
boolean isExamComplete = examSession.isComplete();
boolean isAtLastQuestion = (currentIndex >= totalQuestions - 1);
finishButton.setDisable(!(isExamComplete || isAtLastQuestion));
} catch (Exception e) {
// 若出现异常,禁用导航按钮以避免进一步问题
previousButton.setDisable(true);
nextButton.setDisable(true);
finishButton.setDisable(false); // 仍允许完成考试
showAlert(Alert.AlertType.ERROR, "错误", "更新按钮状态时发生错误: " + e.getMessage());
}
}
/**
* .
*/
private void addEventHandlers() {
nextButton.setOnAction(e -> handleNextQuestion());
previousButton.setOnAction(e -> handlePreviousQuestion());
finishButton.setOnAction(e -> handleFinishExam());
// 添加变更监听器,在选项被选择时保存答案
answerToggleGroup.selectedToggleProperty().addListener((obs, oldSelection, newSelection) -> {
if (newSelection != null) {
int selectedIndex = (Integer) newSelection.getUserData();
examSession.setAnswer(selectedIndex);
}
});
}
/**
* .
*/
private void handleNextQuestion() {
try {
if (examSession.goToNextQuestion()) {
loadCurrentQuestion();
} else {
// 若无法跳转到下一题,可能已经到达末尾
// 检查考试是否完成并据此更新按钮状态
updateButtonStates();
}
} catch (Exception e) {
showAlert(Alert.AlertType.ERROR, "错误", "导航到下一题时发生错误: " + e.getMessage());
}
}
/**
* .
*/
private void handlePreviousQuestion() {
try {
if (examSession.goToPreviousQuestion()) {
loadCurrentQuestion();
} else {
// 若无法返回上一题,可能已经位于开头
updateButtonStates();
}
} catch (Exception e) {
showAlert(Alert.AlertType.ERROR, "错误", "导航到上一题时发生错误: " + e.getMessage());
}
}
/**
* .
*
* @param alertType
* @param title
* @param message
*/
private void showAlert(Alert.AlertType alertType, String title, String message) {
Alert alert = new Alert(alertType);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
/**
* .
*/
private void handleFinishExam() {
// 保存考试结果
controller.saveExamResults(examSession);
// 展示考试结果
ExamResultsView resultsView = new ExamResultsView(primaryStage, controller, examSession);
primaryStage.getScene().setRoot(resultsView);
}
}

@ -1,169 +0,0 @@
package com.personalproject.ui.views;
import com.personalproject.auth.UserAccount;
import com.personalproject.controller.MathLearningController;
import com.personalproject.model.DifficultyLevel;
import com.personalproject.ui.scenes.LoginScene;
import java.util.Optional;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextInputDialog;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
/**
* .
*/
public class MainMenuView extends BorderPane {
private final Stage primaryStage;
private final MathLearningController controller;
private final UserAccount userAccount;
private Button changePasswordButton;
private Button logoutButton;
/**
* MainMenuView .
*
* @param primaryStage
* @param controller
* @param userAccount
*/
public MainMenuView(Stage primaryStage, MathLearningController controller,
UserAccount userAccount) {
this.primaryStage = primaryStage;
this.controller = controller;
this.userAccount = userAccount;
initializeUi();
}
/**
* .
*/
private void initializeUi() {
// 创建主布局
VBox mainLayout = new VBox(20);
mainLayout.setAlignment(Pos.CENTER);
mainLayout.setPadding(new Insets(20));
// 欢迎信息
Label welcomeLabel = new Label("欢迎, " + userAccount.username());
welcomeLabel.setFont(Font.font("System", FontWeight.BOLD, 18));
// 难度信息
Label difficultyLabel = new Label(
"当前难度: " + userAccount.difficultyLevel().getDisplayName());
difficultyLabel.setFont(Font.font("System", FontWeight.NORMAL, 14));
Label promptLabel = new Label("请选择考试难度开始答题");
promptLabel.setFont(Font.font("System", FontWeight.NORMAL, 14));
VBox difficultyBox = new VBox(10);
difficultyBox.setAlignment(Pos.CENTER);
difficultyBox.getChildren().addAll(
createDifficultyButton(DifficultyLevel.PRIMARY),
createDifficultyButton(DifficultyLevel.MIDDLE),
createDifficultyButton(DifficultyLevel.HIGH));
// 按钮区域
VBox buttonBox = new VBox(15);
buttonBox.setAlignment(Pos.CENTER);
changePasswordButton = new Button("修改密码");
logoutButton = new Button("退出登录");
// 设置按钮尺寸
changePasswordButton.setPrefSize(150, 40);
logoutButton.setPrefSize(150, 40);
buttonBox.getChildren().addAll(changePasswordButton, logoutButton);
// 将组件添加到主布局
mainLayout.getChildren().addAll(welcomeLabel, difficultyLabel, promptLabel, difficultyBox,
buttonBox);
// 将主布局置于边界面板中央
setCenter(mainLayout);
// 添加事件处理器
addEventHandlers();
}
/**
* .
*/
private void addEventHandlers() {
changePasswordButton.setOnAction(e -> handleChangePassword());
logoutButton.setOnAction(e -> handleLogout());
}
private Button createDifficultyButton(DifficultyLevel level) {
Button button = new Button(level.getDisplayName());
button.setPrefSize(150, 40);
button.setOnAction(e -> handleDifficultySelection(level));
return button;
}
private void handleDifficultySelection(DifficultyLevel level) {
TextInputDialog dialog = new TextInputDialog("10");
dialog.setTitle("题目数量");
dialog.setHeaderText("请选择题目数量");
dialog.setContentText("请输入题目数量 (10-30):");
Optional<String> result = dialog.showAndWait();
if (result.isEmpty()) {
return;
}
int questionCount;
try {
questionCount = Integer.parseInt(result.get().trim());
} catch (NumberFormatException ex) {
showAlert(Alert.AlertType.ERROR, "无效输入", "请输入有效的数字。");
return;
}
if (questionCount < 10 || questionCount > 30) {
showAlert(Alert.AlertType.WARNING, "题目数量范围错误", "题目数量必须在10到30之间。");
return;
}
com.personalproject.model.ExamSession examSession = controller.createExamSession(
userAccount.username(), level, questionCount);
ExamView examView = new ExamView(primaryStage, controller, examSession);
primaryStage.getScene().setRoot(examView);
}
/**
* .
*/
private void handleChangePassword() {
PasswordChangeView passwordChangeView = new PasswordChangeView(primaryStage, controller,
userAccount);
primaryStage.getScene().setRoot(passwordChangeView);
}
private void showAlert(Alert.AlertType alertType, String title, String message) {
Alert alert = new Alert(alertType);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
/**
* 退.
*/
private void handleLogout() {
// 返回登录界面
LoginScene loginScene = new LoginScene(primaryStage, controller);
primaryStage.getScene().setRoot(loginScene);
}
}

@ -1,173 +0,0 @@
package com.personalproject.ui.views;
import com.personalproject.auth.UserAccount;
import com.personalproject.controller.MathLearningController;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
/**
* .
*/
public class PasswordChangeView extends BorderPane {
private final Stage primaryStage;
private final MathLearningController controller;
private final UserAccount userAccount;
private PasswordField oldPasswordField;
private PasswordField newPasswordField;
private PasswordField confirmNewPasswordField;
private Button changePasswordButton;
private Button backButton;
/**
* PasswordChangeView .
*
* @param primaryStage
* @param controller
* @param userAccount
*/
public PasswordChangeView(Stage primaryStage, MathLearningController controller,
UserAccount userAccount) {
this.primaryStage = primaryStage;
this.controller = controller;
this.userAccount = userAccount;
initializeUi();
}
/**
* .
*/
private void initializeUi() {
// 创建主布局
VBox mainLayout = new VBox(20);
mainLayout.setAlignment(Pos.CENTER);
mainLayout.setPadding(new Insets(20));
// 标题
final Label titleLabel = new Label("修改密码");
titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24));
// 修改密码表单
GridPane passwordForm = new GridPane();
passwordForm.setHgap(15);
passwordForm.setVgap(15);
passwordForm.setAlignment(Pos.CENTER);
final Label oldPasswordLabel = new Label("当前密码:");
oldPasswordField = new PasswordField();
oldPasswordField.setPrefWidth(200);
final Label newPasswordLabel = new Label("新密码 (6-10位包含大小写字母和数字):");
newPasswordField = new PasswordField();
newPasswordField.setPrefWidth(200);
final Label confirmNewPasswordLabel = new Label("确认新密码:");
confirmNewPasswordField = new PasswordField();
confirmNewPasswordField.setPrefWidth(200);
passwordForm.add(oldPasswordLabel, 0, 0);
passwordForm.add(oldPasswordField, 1, 0);
passwordForm.add(newPasswordLabel, 0, 1);
passwordForm.add(newPasswordField, 1, 1);
passwordForm.add(confirmNewPasswordLabel, 0, 2);
passwordForm.add(confirmNewPasswordField, 1, 2);
// 按钮区域
HBox buttonBox = new HBox(15);
buttonBox.setAlignment(Pos.CENTER);
changePasswordButton = new Button("修改密码");
backButton = new Button("返回");
// 设置按钮尺寸
changePasswordButton.setPrefSize(120, 40);
backButton.setPrefSize(120, 40);
buttonBox.getChildren().addAll(changePasswordButton, backButton);
// 将组件添加到主布局
mainLayout.getChildren().addAll(titleLabel, passwordForm, buttonBox);
// 将主布局置于边界面板中央
setCenter(mainLayout);
// 添加事件处理器
addEventHandlers();
}
/**
* .
*/
private void addEventHandlers() {
changePasswordButton.setOnAction(e -> handleChangePassword());
backButton.setOnAction(e -> handleBack());
}
/**
* .
*/
private void handleChangePassword() {
String oldPassword = oldPasswordField.getText();
String newPassword = newPasswordField.getText();
String confirmNewPassword = confirmNewPasswordField.getText();
if (oldPassword.isEmpty() || newPassword.isEmpty() || confirmNewPassword.isEmpty()) {
showAlert(Alert.AlertType.WARNING, "警告", "请填写所有字段");
return;
}
if (!newPassword.equals(confirmNewPassword)) {
showAlert(Alert.AlertType.ERROR, "密码不匹配", "新密码和确认密码不一致");
return;
}
if (!controller.isValidPassword(newPassword)) {
showAlert(Alert.AlertType.ERROR, "密码不符合要求",
"密码长度必须为6-10位且包含大小写字母和数字");
return;
}
boolean success = controller.changePassword(userAccount.username(), oldPassword, newPassword);
if (success) {
showAlert(Alert.AlertType.INFORMATION, "修改成功", "密码修改成功!");
handleBack(); // 返回主菜单
} else {
showAlert(Alert.AlertType.ERROR, "修改失败", "当前密码错误或修改失败");
}
}
/**
* .
*/
private void handleBack() {
MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount);
primaryStage.getScene().setRoot(mainMenuView);
}
/**
* .
*
* @param alertType
* @param title
* @param message
*/
private void showAlert(Alert.AlertType alertType, String title, String message) {
Alert alert = new Alert(alertType);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
}

@ -1,20 +0,0 @@
# 主机不变
mail.smtp.host=smtp.126.com
# 关键修改:端口改为 465
mail.smtp.port=465
# 关键修改:禁用 STARTTLS
mail.smtp.starttls.enable=false
# 关键修改:启用 SSL
mail.smtp.ssl.enable=true
# 以下不变
mail.smtp.auth=true
mail.username=soloyouth@126.com
mail.password=ZYsjxwDXFBsWeQcX
mail.from=soloyouth@126.com
mail.subject=数学学习软件注册验证码
mail.debug=true
Loading…
Cancel
Save