稳定版本 #12

Merged
hnu202326010318 merged 7 commits from develop into main 3 months ago

@ -0,0 +1,90 @@
package com.mathgenerator.controller;
import com.mathgenerator.model.User;
import com.mathgenerator.service.UserService;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.stage.Stage;
import java.io.IOException;
public class ChangePasswordController {
private final UserService userService = new UserService();
private User currentUser;
@FXML private PasswordField oldPasswordField;
@FXML private PasswordField newPasswordField;
@FXML private PasswordField confirmNewPasswordField;
@FXML private Button confirmButton;
@FXML private Button backButton;
@FXML private Label statusLabel;
/**
*
*/
public void initData(User user) {
this.currentUser = user;
}
@FXML
private void handleConfirmAction(ActionEvent event) {
// 1. 获取输入
String oldPassword = oldPasswordField.getText();
String newPassword = newPasswordField.getText();
String confirmNewPassword = confirmNewPasswordField.getText();
// 2. 输入校验
if (oldPassword.isEmpty() || newPassword.isEmpty() || confirmNewPassword.isEmpty()) {
statusLabel.setText("所有密码字段都不能为空!");
return;
}
if (!newPassword.equals(confirmNewPassword)) {
statusLabel.setText("两次输入的新密码不匹配!");
return;
}
if (!UserService.isPasswordValid(newPassword)) {
statusLabel.setText("新密码格式错误必须为6-10位且包含大小写字母和数字。");
return;
}
// 3. 调用后端服务修改密码
boolean success = userService.changePassword(
currentUser.username(),
oldPassword,
newPassword
);
// 4. 更新UI反馈
if (success) {
statusLabel.setText("密码修改成功!请返回主菜单。");
confirmButton.setDisable(true); // 防止重复点击
} else {
statusLabel.setText("修改失败:当前密码错误。");
}
}
/**
*
*/
@FXML
private void handleBackAction(ActionEvent event) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/MainMenuView.fxml"));
Parent root = loader.load();
MainMenuController controller = loader.getController();
controller.initData(currentUser); // 将用户信息传回主菜单
Stage stage = (Stage) backButton.getScene().getWindow();
stage.setScene(new Scene(root));
stage.setTitle("主菜单");
} catch (IOException e) {
e.printStackTrace();
}
}
}

@ -15,7 +15,7 @@ import javafx.stage.Stage;
import java.io.IOException;
import java.util.Optional;
import com.mathgenerator.util.ValidationUtils;
public class LoginController {
// 依赖注入后端服务
@ -46,7 +46,13 @@ public class LoginController {
String username = usernameField.getText();
String password = passwordField.getText();
if (username.isEmpty() || password.isEmpty()) {
// --- 2. 使用工具类进行校验 ---
if (!ValidationUtils.isUsernameValid(username)) {
statusLabel.setText("登录失败:用户名不能为空且不能包含空格。");
return;
}
if (password.isEmpty()) {
statusLabel.setText("用户名和密码不能为空!");
return;
}

@ -48,11 +48,26 @@ public class MainMenuController {
@FXML
private void handleChangePasswordAction(ActionEvent event) {
// TODO: 跳转到修改密码界面
statusLabel.setText("修改密码功能待实现。");
}
try {
// 1\. 加载 FXML 文件
FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/ChangePasswordView.fxml"));
Parent root = loader.load();
// 2\. 获取新界面的控制器
ChangePasswordController controller = loader.getController();
@FXML
// 3\. 调用控制器的方法,传递当前用户信息
controller.initData(currentUser);
// 4\. 显示新场景
Stage stage = (Stage) logoutButton.getScene().getWindow();
stage.setScene(new Scene(root));
stage.setTitle("修改密码");
} catch (IOException e) {
e.printStackTrace();
}
}
@FXML
private void handleLogoutAction(ActionEvent event) {
// 跳转回登录界面
loadScene("/com/mathgenerator/view/LoginView.fxml");

@ -62,7 +62,7 @@ public class QuizController {
}
/**
*
* (ABCD)
*/
private void displayCurrentQuestion() {
ChoiceQuestion currentQuestion = questions.get(currentQuestionIndex);
@ -72,8 +72,10 @@ public class QuizController {
questionTextLabel.setText(currentQuestion.questionText());
List<RadioButton> radioButtons = List.of(option1, option2, option3, option4);
String[] prefixes = {"A. ", "B. ", "C. ", "D. "}; // 定义选项前缀
for (int i = 0; i < radioButtons.size(); i++) {
radioButtons.get(i).setText(currentQuestion.options().get(i));
// 将前缀和选项文本结合起来
radioButtons.get(i).setText(prefixes[i] + currentQuestion.options().get(i));
}
optionsGroup.selectToggle(null); // 清除上一次的选择

@ -12,7 +12,7 @@ import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
import java.io.IOException;
import com.mathgenerator.util.ValidationUtils;
public class RegisterController {
private final UserService userService = new UserService();
@ -50,41 +50,52 @@ public class RegisterController {
@FXML
private void handleRegisterAction(ActionEvent event) {
// 1. 字段校验
if (usernameField.getText().isEmpty() || emailField.getText().isEmpty() ||
verificationCodeField.getText().isEmpty() || passwordField.getText().isEmpty()) {
statusLabel.setText("所有字段都不能为空!");
return;
}
if (!passwordField.getText().equals(confirmPasswordField.getText())) {
statusLabel.setText("两次输入的密码不匹配!");
// 1. 字段校验 (已简化,不再校验密码)
String username = usernameField.getText();
String email = emailField.getText();
if (!ValidationUtils.isUsernameValid(username) || !ValidationUtils.isEmailValid(email) ||
verificationCodeField.getText().isEmpty()) {
statusLabel.setText("所有字段都不能为空且格式正确!");
return;
}
if (this.sentCode == null || !this.sentCode.equals(verificationCodeField.getText())) {
statusLabel.setText("验证码错误!");
return;
}
if (!UserService.isPasswordValid(passwordField.getText())) {
statusLabel.setText("密码格式错误必须为6-10位且包含大小写字母和数字。");
return;
}
// 2. 调用后端服务进行注册
boolean success = userService.register(
usernameField.getText(),
emailField.getText(),
passwordField.getText()
);
// 2. 调用后端服务进行无密码注册
boolean success = userService.register(username, email);
// 3. 根据结果更新UI
// 3. 根据结果更新UI或跳转
if (success) {
statusLabel.setText("注册成功!请返回登录。");
registerButton.setDisable(true);
statusLabel.setText("注册成功!请设置您的密码。");
// 成功后,加载设置密码界面,并传递用户名
loadSetPasswordScene(username);
} else {
statusLabel.setText("注册失败:用户名或邮箱已被占用。");
}
}
/**
* ()
*/
private void loadSetPasswordScene(String username) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/SetPasswordView.fxml"));
Parent root = loader.load();
SetPasswordController controller = loader.getController();
controller.initData(username); // 将用户名传递给新界面的控制器
Stage stage = (Stage) registerButton.getScene().getWindow();
stage.setScene(new Scene(root));
stage.setTitle("设置密码");
} catch (IOException e) {
e.printStackTrace();
}
}
@FXML
private void handleBackToLoginAction(ActionEvent event) {
loadScene("/com/mathgenerator/view/LoginView.fxml");

@ -0,0 +1,80 @@
package com.mathgenerator.controller;
import com.mathgenerator.model.User;
import com.mathgenerator.service.UserService;
import com.mathgenerator.util.ValidationUtils;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.stage.Stage;
import java.io.IOException;
public class SetPasswordController {
private final UserService userService = new UserService();
private String username;
@FXML private Label promptLabel;
@FXML private PasswordField newPasswordField;
@FXML private PasswordField confirmPasswordField;
@FXML private Button confirmButton;
@FXML private Label statusLabel;
/**
*
*/
public void initData(String username) {
this.username = username;
promptLabel.setText("为您的账户 " + username + " 设置密码");
}
@FXML
private void handleConfirmAction(ActionEvent event) {
String newPassword = newPasswordField.getText();
String confirmPassword = confirmPasswordField.getText();
if (!newPassword.equals(confirmPassword)) {
statusLabel.setText("两次输入的密码不匹配!");
return;
}
if (!ValidationUtils.isPasswordValid(newPassword)) {
statusLabel.setText("新密码格式错误必须为6-10位且包含大小写字母和数字。");
return;
}
// 调用后端服务设置密码
boolean success = userService.setPassword(this.username, newPassword);
if (success) {
statusLabel.setText("密码设置成功!正在进入主菜单...");
// 密码设置成功后,获取完整的用户信息并直接跳转到主菜单
userService.findUserByUsername(this.username).ifPresent(this::loadMainMenu);
} else {
statusLabel.setText("密码设置失败,请稍后重试或重新注册。");
}
}
/**
* ()
*/
private void loadMainMenu(User user) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/MainMenuView.fxml"));
Parent root = loader.load();
MainMenuController controller = loader.getController();
controller.initData(user); // 将完整的User对象传递给主菜单
Stage stage = (Stage) confirmButton.getScene().getWindow();
stage.setScene(new Scene(root));
stage.setTitle("主菜单");
} catch (IOException e) {
e.printStackTrace();
}
}
}

@ -172,25 +172,6 @@ public class PrimarySchoolGenerator implements QuestionGenerator {
parts.add(startIndex, "(");
}
// 在 PrimarySchoolGenerator.java 中添加这个方法
/**
*
* 便
* @return
*/
public String generateBasicQuestionText() {
ThreadLocalRandom random = ThreadLocalRandom.current();
int operandCount = random.nextInt(2, 5);
List<String> parts = new ArrayList<>();
parts.add(String.valueOf(getOperand()));
for (int i = 1; i < operandCount; i++) {
parts.add(getRandomOperator());
parts.add(String.valueOf(getOperand()));
}
if (operandCount > 2 && random.nextBoolean()) {
addParentheses(parts);
}
return String.join(" ", parts);
}
}

@ -35,18 +35,20 @@ public class UserService {
}
private Map<String, User> loadUsersFromFile() {
try {
if (Files.exists(USER_FILE_PATH) && Files.size(USER_FILE_PATH) > 0) {
try (FileReader reader = new FileReader(USER_FILE_PATH.toFile())) {
Type type = new TypeToken<Map<String, User>>() {}.getType();
Map<String, User> loadedUsers = gson.fromJson(reader, type);
return loadedUsers != null ? new ConcurrentHashMap<>(loadedUsers) : new ConcurrentHashMap<>();
}
}
// 如果文件不存在直接返回一个空的Map不再创建默认用户
if (!Files.exists(USER_FILE_PATH)) {
return new ConcurrentHashMap<>();
}
try (FileReader reader = new FileReader(USER_FILE_PATH.toFile())) {
Type type = new TypeToken<Map<String, User>>() {}.getType();
Map<String, User> loadedUsers = gson.fromJson(reader, type);
// 如果文件为空或格式错误也返回一个空的Map
return loadedUsers != null ? new ConcurrentHashMap<>(loadedUsers) : new ConcurrentHashMap<>();
} catch (IOException e) {
System.err.println("错误:加载用户文件失败 - " + e.getMessage());
return new ConcurrentHashMap<>();
}
return new ConcurrentHashMap<>();
}
private void saveUsers() {
@ -103,32 +105,49 @@ public class UserService {
}
/**
*
* @return true, false
* ()
* @param username
* @param email
* @return true, false
*/
public boolean register(String username, String email, String password) {
// 1. 基础校验:防止 null 或空白输入
if (username == null || email == null || password == null ||
username.trim().isEmpty() || email.trim().isEmpty() || password.trim().isEmpty()) {
return false;
public boolean register(String username, String email) {
if (userDatabase.containsKey(username)) {
return false; // 用户名已存在
}
// 2. 检查用户名或邮箱是否已存在(使用 Objects.equals 安全比较)
boolean usernameExists = userDatabase.containsKey(username);
boolean emailExists = userDatabase.values().stream()
.anyMatch(u -> Objects.equals(u.email(), email));
if (usernameExists || emailExists) {
return false; // 用户名或邮箱已存在
// 检查数据库中已存在的用户的email是否与新email相同
// 使用 email.equals(u.email()) 可以安全地处理 u.email() 为 null 的情况
if (userDatabase.values().stream()
.anyMatch(u -> email.equals(u.email()))) {
return false; // 邮箱已存在
}
// 3. 创建新用户并保存
User newUser = new User(username, email, password);
// --- 核心修正在这里 ---
// 创建用户时,密码字段设为 null表示该用户处于“待设置密码”状态
User newUser = new User(username, email, null);
userDatabase.put(username, newUser);
saveUsers();
return true;
}
/**
* ()
* @param username
* @param password
* @return true, false
*/
public boolean setPassword(String username, String password) {
return findUserByUsername(username)
.map(user -> {
// 只有当用户当前密码为 null 时才允许设置
if (user.password() == null) {
User updatedUser = new User(user.username(), user.email(), password);
userDatabase.put(username, updatedUser);
saveUsers();
return true;
}
return false; // 用户已经有密码,不能通过此方法设置
}).orElse(false);
}
/**
*
* @param password

@ -0,0 +1,52 @@
package com.mathgenerator.util;
import java.util.regex.Pattern;
/**
*
*
*/
public final class ValidationUtils {
// 密码策略: 6-10位, 必须包含大小写字母和数字
private static final Pattern PASSWORD_PATTERN =
Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{6,10}$");
// 用户名策略: 不包含任何空白字符
private static final Pattern USERNAME_NO_WHITESPACE_PATTERN =
Pattern.compile("^\\S+$");
// 私有构造函数,防止这个工具类被实例化
private ValidationUtils() {}
/**
*
*
* @param username
* @return true, false
*/
public static boolean isUsernameValid(String username) {
if (username == null || username.isEmpty()) {
return false;
}
return USERNAME_NO_WHITESPACE_PATTERN.matcher(username).matches();
}
/**
*
* @param password
* @return true
*/
public static boolean isPasswordValid(String password) {
return password != null && PASSWORD_PATTERN.matcher(password).matches();
}
/**
* ()
* @param email
* @return true
*/
public static boolean isEmailValid(String email) {
return email != null && !email.isEmpty() && email.contains("@");
}
}

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="450.0" spacing="15.0" style="-fx-background-color: #f4f4f4;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.mathgenerator.controller.ChangePasswordController">
<children>
<Label text="修改密码">
<font>
<Font name="System Bold" size="24.0" />
</font>
</Label>
<PasswordField fx:id="oldPasswordField" maxWidth="300.0" promptText="当前密码" />
<PasswordField fx:id="newPasswordField" maxWidth="300.0" promptText="新密码 (6-10位, 含大小写字母和数字)" />
<PasswordField fx:id="confirmNewPasswordField" maxWidth="300.0" promptText="确认新密码" />
<Button fx:id="confirmButton" mnemonicParsing="false" onAction="#handleConfirmAction" prefWidth="120.0" text="确认修改" />
<Button fx:id="backButton" mnemonicParsing="false" onAction="#handleBackAction" style="-fx-background-color: #6c757d;" text="返回主菜单" textFill="WHITE" />
<Label fx:id="statusLabel" textFill="RED" wrapText="true">
<VBox.margin>
<Insets top="10.0" />
</VBox.margin>
</Label>
</children>
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
</VBox>

@ -24,8 +24,6 @@
<Button fx:id="sendCodeButton" mnemonicParsing="false" onAction="#handleSendCodeAction" text="发送验证码" />
</children>
</HBox>
<PasswordField fx:id="passwordField" maxWidth="300.0" promptText="设置密码 (6-10位, 含大小写字母和数字)" />
<PasswordField fx:id="confirmPasswordField" maxWidth="300.0" promptText="确认密码" />
<Button fx:id="registerButton" mnemonicParsing="false" onAction="#handleRegisterAction" prefWidth="120.0" text="确认注册" />
<Button fx:id="backToLoginButton" mnemonicParsing="false" onAction="#handleBackToLoginAction" style="-fx-background-color: #6c757d;" text="返回登录" textFill="WHITE" />
<Label fx:id="statusLabel" textFill="RED" wrapText="true">

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="350.0" prefWidth="450.0" spacing="15.0" style="-fx-background-color: #f4f4f4;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.mathgenerator.controller.SetPasswordController">
<children>
<Label text="设置您的初始密码">
<font>
<Font name="System Bold" size="24.0" />
</font>
</Label>
<Label fx:id="promptLabel" text="为您的账户 [用户名] 设置密码" />
<PasswordField fx:id="newPasswordField" maxWidth="300.0" promptText="新密码 (6-10位, 含大小写字母和数字)" />
<PasswordField fx:id="confirmPasswordField" maxWidth="300.0" promptText="确认新密码" />
<Button fx:id="confirmButton" mnemonicParsing="false" onAction="#handleConfirmAction" prefWidth="120.0" text="确认并进入" />
<Label fx:id="statusLabel" textFill="RED" wrapText="true">
<VBox.margin>
<Insets top="10.0" />
</VBox.margin>
</Label>
</children>
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
</VBox>
Loading…
Cancel
Save