diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml index 95a8d96..d14ab4f 100644 --- a/.idea/checkstyle-idea.xml +++ b/.idea/checkstyle-idea.xml @@ -1,7 +1,7 @@ - 10.26.1 + 11.0.1 JavaOnly true - + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/default-avatar.png b/images/default-avatar.png new file mode 100644 index 0000000..e7fe7f4 Binary files /dev/null and b/images/default-avatar.png differ diff --git a/pom.xml b/pom.xml index 9d7a90b..67d77d6 100644 --- a/pom.xml +++ b/pom.xml @@ -4,42 +4,60 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - com.ybw + com.wsf MathApp 1.0-SNAPSHOT MathApp UTF-8 + 17 + 17 5.10.0 + 17.0.6 + org.openjfx javafx-controls - 17.0.6 + ${javafx.version} org.openjfx javafx-fxml - 17.0.6 + ${javafx.version} + + org.openjfx + javafx-base + ${javafx.version} + + + org.openjfx + javafx-graphics + ${javafx.version} + + + org.openjfx javafx-web - 17.0.6 + ${javafx.version} org.openjfx javafx-swing - 17.0.6 + ${javafx.version} org.openjfx javafx-media - 17.0.6 + ${javafx.version} + + org.controlsfx controlsfx @@ -99,6 +117,8 @@ + + org.junit.jupiter junit-jupiter-api @@ -111,6 +131,11 @@ ${junit.version} test + + com.sun.mail + jakarta.mail + 2.0.1 + @@ -122,24 +147,23 @@ 17 17 + org.openjfx javafx-maven-plugin 0.0.8 + + + com.wsf.mathapp.Main + + - default-cli - com.ybw.mathapp/com.ybw.mathapp.HelloApplication - app - app - app - true - true - true + com.wsf.mathapp.Main diff --git a/src/main/java/com/wsf/mathapp/Main.java b/src/main/java/com/wsf/mathapp/Main.java new file mode 100644 index 0000000..21c2960 --- /dev/null +++ b/src/main/java/com/wsf/mathapp/Main.java @@ -0,0 +1,17 @@ +package com.wsf.mathapp; + +import com.wsf.mathapp.controller.SceneManager; +import javafx.application.Application; +import javafx.stage.Stage; + +public class Main extends Application { + @Override + public void start(Stage primaryStage) { + SceneManager sceneManager = new SceneManager(primaryStage); + sceneManager.showLoginView(); + } + + public static void main(String[] args) { + launch(args); + } +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/controller/SceneManager.java b/src/main/java/com/wsf/mathapp/controller/SceneManager.java new file mode 100644 index 0000000..edbc218 --- /dev/null +++ b/src/main/java/com/wsf/mathapp/controller/SceneManager.java @@ -0,0 +1,84 @@ +package com.wsf.mathapp.controller; + +import com.wsf.mathapp.view.*; +import javafx.stage.Stage; + +public class SceneManager { + private final Stage primaryStage; + private final LoginView loginView; + private final RegisterView registerView; + private final MainMenuView mainMenuView; + private final LevelSelectionView levelSelectionView; + private final QuestionCountView questionCountView; + private final QuizView quizView; + private final ResultView resultView; + private String currentUserName; + + public SceneManager(Stage primaryStage) { + this.primaryStage = primaryStage; + this.primaryStage.setTitle("数学学习软件"); + this.primaryStage.setResizable(false); + + // 初始化所有视图 + this.loginView = new LoginView(this); + this.registerView = new RegisterView(this); + this.mainMenuView = new MainMenuView(this); + this.levelSelectionView = new LevelSelectionView(this); + this.questionCountView = new QuestionCountView(this); + this.quizView = new QuizView(this); + this.resultView = new ResultView(this); + } + + public void showLoginView() { + primaryStage.setScene(loginView.getScene()); + primaryStage.show(); + } + + public void showRegisterView() { + primaryStage.setScene(registerView.getScene()); + primaryStage.show(); + } + + public void showMainMenuView() { + // 在显示主菜单前更新用户名 + if (mainMenuView != null) { + mainMenuView.updateUsername(currentUserName); + } + if (mainMenuView != null) { + primaryStage.setScene(mainMenuView.getScene()); + } + } + + public void showLevelSelectionView() { + // 在显示级别选择界面前更新用户名 + if (levelSelectionView != null) { + levelSelectionView.updateUsername(currentUserName); + } + if (levelSelectionView != null) { + primaryStage.setScene(levelSelectionView.getScene()); + } + } + public void showQuestionCountView() { + primaryStage.setScene(questionCountView.getScene()); + } + + public void showQuizView() { + primaryStage.setScene(quizView.getScene()); + } + + public void showResultView(double score) { + resultView.setScore(score); + primaryStage.setScene(resultView.getScene()); + } + + // Getter methods for views + public LoginView getLoginView() { return loginView; } + public RegisterView getRegisterView() { return registerView; } + public MainMenuView getMainMenuView() { return mainMenuView; } + public LevelSelectionView getLevelSelectionView() { return levelSelectionView; } + public QuestionCountView getQuestionCountView() { return questionCountView; } + public QuizView getQuizView() { return quizView; } + public ResultView getResultView() { return resultView; } + public void setCurrentUserName(String currentUserName){ this.currentUserName = currentUserName;} + public String getCurrentUserName(){return this.currentUserName;} +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/service/QuestionService.java b/src/main/java/com/wsf/mathapp/service/QuestionService.java new file mode 100644 index 0000000..ec5c254 --- /dev/null +++ b/src/main/java/com/wsf/mathapp/service/QuestionService.java @@ -0,0 +1,21 @@ +package com.wsf.mathapp.service; + + +import com.ybw.mathapp.service.PrimarySchoolGenerator; +import com.ybw.mathapp.service.JuniorHighGenerator; +import com.ybw.mathapp.service.SeniorHighGenerator; +import com.ybw.mathapp.service.QuestionGenerator; + + +public class QuestionService { + + public static QuestionGenerator createGenerator(String level) { + return switch (level) { + case "小学" -> new PrimarySchoolGenerator(); + case "初中" -> new JuniorHighGenerator(); + case "高中" -> new SeniorHighGenerator(); + default -> null; + }; + } + +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/view/LevelSelectionView.java b/src/main/java/com/wsf/mathapp/view/LevelSelectionView.java new file mode 100644 index 0000000..94f675e --- /dev/null +++ b/src/main/java/com/wsf/mathapp/view/LevelSelectionView.java @@ -0,0 +1,190 @@ +package com.wsf.mathapp.view; + +import com.wsf.mathapp.controller.SceneManager; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; + +public class LevelSelectionView { + private Scene scene; + private final SceneManager sceneManager; + private String currentUsername; + + // 添加界面组件引用 + private Label usernameLabel; + private Text avatarText; + + public LevelSelectionView(SceneManager sceneManager) { + this.sceneManager = sceneManager; + this.currentUsername = sceneManager.getCurrentUserName(); + createScene(); + } + + private void createScene() { + // 创建主容器 + VBox mainContainer = new VBox(); + mainContainer.setPadding(new Insets(20)); + + // 创建顶部用户信息栏 + HBox userInfoBar = createUserInfoBar(); + + // 创建级别选择内容区域 + VBox selectionContent = createSelectionContent(); + + mainContainer.getChildren().addAll(userInfoBar, selectionContent); + + scene = new Scene(mainContainer, 450, 550); + } + + private HBox createUserInfoBar() { + HBox userInfoBar = new HBox(15); + userInfoBar.setAlignment(Pos.CENTER_LEFT); + userInfoBar.setPadding(new Insets(0, 0, 30, 0)); + userInfoBar.setStyle("-fx-border-color: #e0e0e0; -fx-border-width: 0 0 1 0; -fx-padding: 0 0 15 0;"); + + // 创建圆形头像容器 + VBox avatarContainer = new VBox(); + avatarContainer.setAlignment(Pos.CENTER); + avatarContainer.setPrefSize(50, 50); + + // 创建圆形头像背景 + Circle avatarCircle = new Circle(22); + avatarCircle.setFill(Color.web("#4CAF50")); // 统一的绿色背景 + avatarCircle.setStroke(Color.WHITE); + avatarCircle.setStrokeWidth(2); + + // 添加阴影效果 + avatarCircle.setStyle("-fx-effect: drop shadow(gaussian, rgba(0,0,0,0.2), 5, 0.3, 2, 2);"); + + // 添加首字母文本 + avatarText = new Text(getFirstLetter()); + avatarText.setFill(Color.WHITE); + avatarText.setFont(Font.font("Arial", FontWeight.BOLD, 16)); + + // 使用StackPane将文本放在圆形中心 + javafx.scene.layout.StackPane avatarStack = new javafx.scene.layout.StackPane(); + avatarStack.getChildren().addAll(avatarCircle, avatarText); + avatarStack.setPrefSize(44, 44); + avatarStack.setAlignment(Pos.CENTER); + + avatarContainer.getChildren().add(avatarStack); + + // 用户名标签 + usernameLabel = new Label(currentUsername != null ? currentUsername : "用户"); + usernameLabel.setFont(Font.font("Arial", FontWeight.BOLD, 16)); + usernameLabel.setStyle("-fx-text-fill: #2c3e50;"); + + // 间隔 + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + userInfoBar.getChildren().addAll(avatarContainer, usernameLabel, spacer); + + return userInfoBar; + } + + private VBox createSelectionContent() { + VBox selectionContent = new VBox(25); + selectionContent.setPadding(new Insets(30, 20, 20, 20)); + selectionContent.setAlignment(Pos.CENTER); + + Label titleLabel = new Label("选择题目级别"); + titleLabel.setFont(Font.font("Arial", FontWeight.BOLD, 26)); + titleLabel.setStyle("-fx-text-fill: #2c3e50;"); + + // 添加副标题 + Label subtitleLabel = new Label("请选择适合您的学习级别"); + subtitleLabel.setFont(Font.font("Arial", 14)); + subtitleLabel.setStyle("-fx-text-fill: #7f8c8d; -fx-padding: 0 0 10 0;"); + + Button primaryButton = createLevelButton("小学题目", "#4CAF50", "#45a049"); + Button juniorButton = createLevelButton("初中题目", "#2196F3", "#1976D2"); + Button seniorButton = createLevelButton("高中题目", "#9C27B0", "#7B1FA2"); + + Button backButton = new Button("返回主菜单"); + backButton.setStyle("-fx-background-color: #95a5a6; -fx-text-fill: white; -fx-font-size: 14px; -fx-background-radius: 8; -fx-padding: 8 20;"); + backButton.setPrefSize(180, 45); + backButton.setOnMouseEntered(e -> backButton.setStyle("-fx-background-color: #7f8c8d; -fx-text-fill: white; -fx-font-size: 14px; -fx-background-radius: 8; -fx-padding: 8 20;")); + backButton.setOnMouseExited(e -> backButton.setStyle("-fx-background-color: #95a5a6; -fx-text-fill: white; -fx-font-size: 14px; -fx-background-radius: 8; -fx-padding: 8 20;")); + + primaryButton.setOnAction(e -> { + String selectedLevel = "小学"; + sceneManager.getQuestionCountView().setLevel(selectedLevel); + sceneManager.showQuestionCountView(); + }); + + juniorButton.setOnAction(e -> { + String selectedLevel = "初中"; + sceneManager.getQuestionCountView().setLevel(selectedLevel); + sceneManager.showQuestionCountView(); + }); + + seniorButton.setOnAction(e -> { + String selectedLevel = "高中"; + sceneManager.getQuestionCountView().setLevel(selectedLevel); + sceneManager.showQuestionCountView(); + }); + + backButton.setOnAction(e -> { + sceneManager.showMainMenuView(); + }); + + selectionContent.getChildren().addAll( + titleLabel, subtitleLabel, primaryButton, juniorButton, seniorButton, backButton + ); + + return selectionContent; + } + + private Button createLevelButton(String text, String color, String hoverColor) { + Button button = new Button(text); + button.setStyle(String.format( + "-fx-background-color: %s; -fx-text-fill: white; -fx-font-size: 16px; -fx-font-weight: bold; " + + "-fx-background-radius: 12; -fx-padding: 12 30; -fx-effect: drop shadow(gaussian, rgba(0,0,0,0.2), 8, 0.3, 2, 2);", + color + )); + button.setPrefSize(220, 60); + button.setOnMouseEntered(e -> button.setStyle(String.format( + "-fx-background-color: %s; -fx-text-fill: white; -fx-font-size: 16px; -fx-font-weight: bold; " + + "-fx-background-radius: 12; -fx-padding: 12 30; -fx-effect: drop shadow(gaussian, rgba(0,0,0,0.3), 10, 0.4, 3, 3);", + hoverColor + ))); + button.setOnMouseExited(e -> button.setStyle(String.format( + "-fx-background-color: %s; -fx-text-fill: white; -fx-font-size: 16px; -fx-font-weight: bold; " + + "-fx-background-radius: 12; -fx-padding: 12 30; -fx-effect: drop shadow(gaussian, rgba(0,0,0,0.2), 8, 0.3, 2, 2);", + color + ))); + return button; + } + + private String getFirstLetter() { + return currentUsername != null && !currentUsername.isEmpty() ? + currentUsername.substring(0, 1).toUpperCase() : "U"; + } + + // 添加更新用户名的方法 + public void updateUsername(String username) { + this.currentUsername = username; + if (usernameLabel != null) { + usernameLabel.setText(username != null ? username : "用户"); + } + if (avatarText != null) { + avatarText.setText(getFirstLetter()); + } + } + + public Scene getScene() { + return scene; + } +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/view/LoginView.java b/src/main/java/com/wsf/mathapp/view/LoginView.java new file mode 100644 index 0000000..2e4c088 --- /dev/null +++ b/src/main/java/com/wsf/mathapp/view/LoginView.java @@ -0,0 +1,113 @@ +package com.wsf.mathapp.view; + +import com.wsf.mathapp.controller.SceneManager; +import com.ybw.mathapp.util.Login; +import com.ybw.mathapp.util.LoginFileUtils; +import com.ybw.mathapp.util.Register; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; + +public class LoginView { + private Scene scene; + private final SceneManager sceneManager; + + public LoginView(SceneManager sceneManager) { + this.sceneManager = sceneManager; + createScene(); + } + + private void createScene() { + VBox root = new VBox(20); + root.setPadding(new Insets(40)); + root.setAlignment(Pos.CENTER); + + Label titleLabel = new Label("数学学习软件"); + titleLabel.setFont(Font.font(24)); + + Label subtitleLabel = new Label("用户登录"); + subtitleLabel.setFont(Font.font(18)); + + // 修改提示文本 + TextField usernameOrEmailField = new TextField(); + usernameOrEmailField.setPromptText("请输入用户名或邮箱"); + usernameOrEmailField.setMaxWidth(300); + usernameOrEmailField.setPrefHeight(40); + + PasswordField passwordField = new PasswordField(); + passwordField.setPromptText("请输入密码"); + passwordField.setMaxWidth(300); + passwordField.setPrefHeight(40); + + Button loginButton = new Button("登录"); + loginButton.setStyle("-fx-background-color: #4CAF50; -fx-text-fill: white; -fx-font-size: 14px;"); + loginButton.setPrefSize(300, 40); + + Button registerButton = new Button("注册账号"); + registerButton.setStyle("-fx-background-color: #2196F3; -fx-text-fill: white; -fx-font-size: 14px;"); + registerButton.setPrefSize(300, 40); + + Label statusLabel = new Label(); + + loginButton.setOnAction(e -> { + String usernameOrEmail = usernameOrEmailField.getText().trim(); + String password = passwordField.getText(); + + if (usernameOrEmail.isEmpty() || password.isEmpty()) { + showError(statusLabel, "用户名/邮箱和密码不能为空!"); + return; + } + + // 调用后端的登录方法,支持用户名或邮箱登录 + boolean success = Login.login(usernameOrEmail, password); + System.out.println(usernameOrEmail); + System.out.println(password); + if (success) { + showSuccess(statusLabel); + String username; + if (LoginFileUtils.emailFindName(usernameOrEmail) == null){ + username = usernameOrEmail; + } + else { + username = LoginFileUtils.emailFindName(usernameOrEmail); + } + sceneManager.setCurrentUserName(username); + sceneManager.showMainMenuView(); + } else { + showError(statusLabel, "用户名/邮箱或密码错误!"); + } + }); + + registerButton.setOnAction(e -> { + sceneManager.showRegisterView(); + }); + + // 添加回车键登录支持 + usernameOrEmailField.setOnAction(e -> loginButton.fire()); + passwordField.setOnAction(e -> loginButton.fire()); + + root.getChildren().addAll( + titleLabel, subtitleLabel, usernameOrEmailField, passwordField, + loginButton, registerButton, statusLabel + ); + + scene = new Scene(root, 400, 500); + } + + private void showError(Label label, String message) { + label.setText(message); + label.setStyle("-fx-text-fill: red;"); + } + + private void showSuccess(Label label) { + label.setText("登录成功!"); + label.setStyle("-fx-text-fill: green;"); + } + + public Scene getScene() { + return scene; + } +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/view/MainMenuView.java b/src/main/java/com/wsf/mathapp/view/MainMenuView.java new file mode 100644 index 0000000..2582869 --- /dev/null +++ b/src/main/java/com/wsf/mathapp/view/MainMenuView.java @@ -0,0 +1,257 @@ +package com.wsf.mathapp.view; + +import com.wsf.mathapp.controller.SceneManager; +import com.ybw.mathapp.util.ChangePassword; +import com.ybw.mathapp.util.Login; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; + +public class MainMenuView { + private Scene scene; + private final SceneManager sceneManager; + private String currentUsername; + + // 添加界面组件引用,用于动态更新 + private Label usernameLabel; + private Text avatarText; + private Label welcomeLabel; + + public MainMenuView(SceneManager sceneManager) { + this.sceneManager = sceneManager; + this.currentUsername = sceneManager.getCurrentUserName(); + createScene(); + } + + private void createScene() { + // 创建主容器 + VBox mainContainer = new VBox(); + mainContainer.setPadding(new Insets(20)); + + // 创建顶部用户信息栏 + HBox userInfoBar = createUserInfoBar(); + + // 创建主菜单内容区域 + VBox menuContent = createMenuContent(); + + mainContainer.getChildren().addAll(userInfoBar, menuContent); + + scene = new Scene(mainContainer, 450, 550); + } + + private HBox createUserInfoBar() { + HBox userInfoBar = new HBox(15); + userInfoBar.setAlignment(Pos.CENTER_LEFT); + userInfoBar.setPadding(new Insets(0, 0, 30, 0)); + userInfoBar.setStyle("-fx-border-color: #e0e0e0; -fx-border-width: 0 0 1 0; -fx-padding: 0 0 15 0;"); + + // 创建圆形头像容器 + VBox avatarContainer = new VBox(); + avatarContainer.setAlignment(Pos.CENTER); + avatarContainer.setPrefSize(50, 50); + + // 创建圆形头像背景 + Circle avatarCircle = new Circle(22); + avatarCircle.setFill(Color.web("#4CAF50")); + avatarCircle.setStroke(Color.WHITE); + avatarCircle.setStrokeWidth(2); + + // 添加阴影效果 + avatarCircle.setStyle("-fx-effect: drop shadow(gaussian, rgba(0,0,0,0.2), 5, 0.3, 2, 2);"); + + // 添加首字母文本 + avatarText = new Text(getFirstLetter()); + avatarText.setFill(Color.WHITE); + avatarText.setFont(Font.font("Arial", FontWeight.BOLD, 16)); + + // 使用StackPane将文本放在圆形中心 + javafx.scene.layout.StackPane avatarStack = new javafx.scene.layout.StackPane(); + avatarStack.getChildren().addAll(avatarCircle, avatarText); + avatarStack.setPrefSize(44, 44); + avatarStack.setAlignment(Pos.CENTER); + + avatarContainer.getChildren().add(avatarStack); + + // 用户名标签 + usernameLabel = new Label(currentUsername != null ? currentUsername : "用户"); + usernameLabel.setFont(Font.font("Arial", FontWeight.BOLD, 16)); + usernameLabel.setStyle("-fx-text-fill: #2c3e50;"); + + // 间隔 + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + userInfoBar.getChildren().addAll(avatarContainer, usernameLabel, spacer); + + return userInfoBar; + } + + private VBox createMenuContent() { + VBox menuContent = new VBox(25); + menuContent.setPadding(new Insets(20)); + menuContent.setAlignment(Pos.CENTER); + + Label titleLabel = new Label("数学学习软件"); + titleLabel.setFont(Font.font(28)); + titleLabel.setStyle("-fx-text-fill: #2c3e50;"); + + // 修改欢迎标签,显示用户名 + welcomeLabel = new Label("欢迎," + (currentUsername != null ? currentUsername : "用户") + "!"); + welcomeLabel.setFont(Font.font(18)); + welcomeLabel.setStyle("-fx-text-fill: #7f8c8d;"); + + Button startButton = new Button("开始练习"); + startButton.setStyle("-fx-background-color: #4CAF50; -fx-text-fill: white; -fx-font-size: 16px; -fx-background-radius: 10;"); + startButton.setPrefSize(220, 55); + startButton.setOnMouseEntered(e -> startButton.setStyle("-fx-background-color: #45a049; -fx-text-fill: white; -fx-font-size: 16px; -fx-background-radius: 10;")); + startButton.setOnMouseExited(e -> startButton.setStyle("-fx-background-color: #4CAF50; -fx-text-fill: white; -fx-font-size: 16px; -fx-background-radius: 10;")); + + Button changePasswordButton = new Button("修改密码"); + changePasswordButton.setStyle("-fx-background-color: #2196F3; -fx-text-fill: white; -fx-font-size: 16px; -fx-background-radius: 10;"); + changePasswordButton.setPrefSize(220, 55); + changePasswordButton.setOnMouseEntered(e -> changePasswordButton.setStyle("-fx-background-color: #1976D2; -fx-text-fill: white; -fx-font-size: 16px; -fx-background-radius: 10;")); + changePasswordButton.setOnMouseExited(e -> changePasswordButton.setStyle("-fx-background-color: #2196F3; -fx-text-fill: white; -fx-font-size: 16px; -fx-background-radius: 10;")); + + Button logoutButton = new Button("退出登录"); + logoutButton.setStyle("-fx-background-color: #f44336; -fx-text-fill: white; -fx-font-size: 16px; -fx-background-radius: 10;"); + logoutButton.setPrefSize(220, 55); + logoutButton.setOnMouseEntered(e -> logoutButton.setStyle("-fx-background-color: #d32f2f; -fx-text-fill: white; -fx-font-size: 16px; -fx-background-radius: 10;")); + logoutButton.setOnMouseExited(e -> logoutButton.setStyle("-fx-background-color: #f44336; -fx-text-fill: white; -fx-font-size: 16px; -fx-background-radius: 10;")); + + startButton.setOnAction(e -> { + sceneManager.showLevelSelectionView(); + }); + + changePasswordButton.setOnAction(e -> { + showChangePasswordDialog(); + }); + + logoutButton.setOnAction(e -> { + sceneManager.showLoginView(); + }); + + menuContent.getChildren().addAll( + titleLabel, welcomeLabel, startButton, + changePasswordButton, logoutButton + ); + + return menuContent; + } + + private String getFirstLetter() { + return currentUsername != null && !currentUsername.isEmpty() ? + currentUsername.substring(0, 1).toUpperCase() : "U"; + } + + // 添加更新用户名的方法 + public void updateUsername(String username) { + this.currentUsername = username; + if (usernameLabel != null) { + usernameLabel.setText(username != null ? username : "用户"); + } + if (avatarText != null) { + avatarText.setText(getFirstLetter()); + } + if (welcomeLabel != null) { + welcomeLabel.setText("欢迎," + (username != null ? username : "用户") + "!"); + } + } + + private void showChangePasswordDialog() { + // 创建修改密码对话框 + javafx.scene.control.Dialog dialog = new javafx.scene.control.Dialog<>(); + dialog.setTitle("修改密码"); + dialog.setHeaderText("请输入密码信息"); + dialog.setGraphic(null); // 移除默认图标 + + // 创建表单 + javafx.scene.layout.GridPane grid = new javafx.scene.layout.GridPane(); + grid.setHgap(15); + grid.setVgap(15); + grid.setPadding(new Insets(20, 30, 10, 30)); + + javafx.scene.control.PasswordField oldPassword = new javafx.scene.control.PasswordField(); + oldPassword.setPromptText("请输入原密码"); + oldPassword.setPrefWidth(200); + + javafx.scene.control.PasswordField newPassword = new javafx.scene.control.PasswordField(); + newPassword.setPromptText("请输入新密码"); + newPassword.setPrefWidth(200); + + javafx.scene.control.PasswordField confirmPassword = new javafx.scene.control.PasswordField(); + confirmPassword.setPromptText("请确认新密码"); + confirmPassword.setPrefWidth(200); + + grid.add(new Label("原密码:"), 0, 0); + grid.add(oldPassword, 1, 0); + grid.add(new Label("新密码:"), 0, 1); + grid.add(newPassword, 1, 1); + grid.add(new Label("确认密码:"), 0, 2); + grid.add(confirmPassword, 1, 2); + + dialog.getDialogPane().setContent(grid); + + // 添加按钮 + dialog.getDialogPane().getButtonTypes().addAll( + javafx.scene.control.ButtonType.OK, + javafx.scene.control.ButtonType.CANCEL + ); + + dialog.setResultConverter(dialogButton -> { + if (dialogButton == javafx.scene.control.ButtonType.OK) { + // 这里可以添加修改密码的逻辑 + String oldPwd = oldPassword.getText(); + String newPwd = newPassword.getText(); + String confirmPwd = confirmPassword.getText(); + + if (oldPwd.isEmpty() || newPwd.isEmpty() || confirmPwd.isEmpty()) { + showAlert("错误", "请填写所有密码字段!"); + return null; + } + if (!Login.login(currentUsername,oldPwd)){ + showAlert("错误", "原密码错误!"); + return null; + } + if (!newPwd.equals(confirmPwd)) { + showAlert("错误", "两次输入的新密码不一致!"); + return null; + } + + // 调用修改密码的后端逻辑 + if(!ChangePassword.changePassword(currentUsername, confirmPwd)){ + showAlert("错误", "密码修改失败"); + } + else{ + showAlert("成功", "密码修改成功!"); + } + } + return null; + }); + + dialog.showAndWait(); + } + + private void showAlert(String title, String message) { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } + + public Scene getScene() { + return scene; + } +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/view/QuestionCountView.java b/src/main/java/com/wsf/mathapp/view/QuestionCountView.java new file mode 100644 index 0000000..5e100a4 --- /dev/null +++ b/src/main/java/com/wsf/mathapp/view/QuestionCountView.java @@ -0,0 +1,100 @@ +package com.wsf.mathapp.view; + +import com.wsf.mathapp.controller.SceneManager; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; + +public class QuestionCountView { + private Scene scene; + private final SceneManager sceneManager; + private String level; + + public QuestionCountView(SceneManager sceneManager) { + this.sceneManager = sceneManager; + createScene(); + } + + private void createScene() { + VBox root = new VBox(20); + root.setPadding(new Insets(40)); + root.setAlignment(Pos.CENTER); + + Label titleLabel = new Label("选择题目数量"); + titleLabel.setFont(Font.font(20)); + + Label levelLabel = new Label(); + levelLabel.setFont(Font.font(16)); + + TextField countField = new TextField(); + countField.setPromptText("请输入题目数量 (10-30)"); + countField.setMaxWidth(250); + countField.setPrefHeight(40); + + Button startButton = new Button("开始答题"); + startButton.setStyle("-fx-background-color: #4CAF50; -fx-text-fill: white; -fx-font-size: 14px;"); + startButton.setPrefSize(250, 45); + + Button backButton = new Button("返回"); + backButton.setStyle("-fx-background-color: #757575; -fx-text-fill: white;"); + backButton.setPrefSize(250, 40); + + Label statusLabel = new Label(); + + startButton.setOnAction(e -> { + try { + String countText = countField.getText().trim(); + if (countText.isEmpty()) { + showError(statusLabel, "请输入题目数量!"); + return; + } + + int count = Integer.parseInt(countText); + if (count < 10 || count > 30) { + showError(statusLabel, "题目数量必须在10-30之间!"); + return; + } + + // 设置题目数量和级别,然后开始答题 + sceneManager.getQuizView().setQuizParameters(level, count); + sceneManager.showQuizView(); + + } catch (NumberFormatException ex) { + showError(statusLabel, "请输入有效的数字!"); + } + }); + + backButton.setOnAction(e -> { + sceneManager.showLevelSelectionView(); + }); + + root.getChildren().addAll( + titleLabel, levelLabel, countField, startButton, backButton, statusLabel + ); + + scene = new Scene(root, 400, 400); + } + + public void setLevel(String level) { + this.level = level; + // 更新界面显示当前选择的级别 + if (scene != null) { + VBox root = (VBox) scene.getRoot(); + Label levelLabel = (Label) root.getChildren().get(1); + levelLabel.setText("当前级别: " + level); + levelLabel.setStyle("-fx-text-fill: #2196F3;"); + } + } + + private void showError(Label label, String message) { + label.setText(message); + label.setStyle("-fx-text-fill: red;"); + } + + public Scene getScene() { + return scene; + } +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/view/QuizView.java b/src/main/java/com/wsf/mathapp/view/QuizView.java new file mode 100644 index 0000000..8ad1ecd --- /dev/null +++ b/src/main/java/com/wsf/mathapp/view/QuizView.java @@ -0,0 +1,152 @@ +package com.wsf.mathapp.view; + +import com.wsf.mathapp.controller.SceneManager; +import com.wsf.mathapp.service.QuestionService; +import com.ybw.mathapp.entity.QuestionWithOptions; +import com.ybw.mathapp.service.MultipleChoiceGenerator; +import com.ybw.mathapp.service.QuestionGenerator; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; +import java.util.List; + +public class QuizView { + private Scene scene; + private final SceneManager sceneManager; + private MultipleChoiceGenerator multipleChoiceGenerator = null; + private String currentLevel; + private int questionCount; + private List questions; + private int correctAnswers = 0; + + private Label questionLabel; + private ToggleGroup optionsGroup; + private VBox optionsContainer; + private Button nextButton; + private Label progressLabel; + private Label questionNumberLabel; + + public QuizView(SceneManager sceneManager) { + this.sceneManager = sceneManager; + createScene(); + } + + private void createScene() { + VBox root = new VBox(20); + root.setPadding(new Insets(30)); + root.setAlignment(Pos.TOP_CENTER); + + progressLabel = new Label(); + progressLabel.setFont(Font.font(14)); + + questionNumberLabel = new Label(); + questionNumberLabel.setFont(Font.font(16)); + questionNumberLabel.setStyle("-fx-text-fill: #666;"); + + questionLabel = new Label(); + questionLabel.setFont(Font.font(18)); + questionLabel.setWrapText(true); + questionLabel.setStyle("-fx-padding: 10 0 20 0;"); + + optionsGroup = new ToggleGroup(); + optionsContainer = new VBox(15); + optionsContainer.setAlignment(Pos.CENTER_LEFT); + optionsContainer.setPadding(new Insets(10)); + + nextButton = new Button("下一题"); + nextButton.setStyle("-fx-background-color: #4CAF50; -fx-text-fill: white; -fx-font-size: 14px;"); + nextButton.setPrefSize(150, 40); + nextButton.setDisable(true); + + root.getChildren().addAll(progressLabel, questionNumberLabel, questionLabel, optionsContainer, nextButton); + + scene = new Scene(root, 600, 500); + } + + public void setQuizParameters(String level, int count) { + this.currentLevel = level; + this.questionCount = count; + this.correctAnswers = 0; + + // 生成题目 + generateQuestions(); + showQuestion(0); + } + + private void generateQuestions() { + QuestionGenerator questionGenerator = QuestionService.createGenerator(currentLevel); + multipleChoiceGenerator = new MultipleChoiceGenerator(questionGenerator,currentLevel); + questions = multipleChoiceGenerator.generateMultipleChoiceQuestions(questionCount); + } + + private void showQuestion(int index) { + if (index >= questions.size()) { + // 所有题目已回答,显示结果 + double score = (double) correctAnswers / questions.size(); + sceneManager.showResultView(score); + return; + } + QuestionWithOptions question = questions.get(index); + + // 更新进度和题号 + progressLabel.setText("进度: " + (index + 1) + "/" + questions.size()); + questionNumberLabel.setText("第 " + (index + 1) + " 题"); + + // 设置题目内容 + String questionText = question.getQuestionText(); + // 移除末尾的 "=" + if (questionText.endsWith(" =")) { + questionText = questionText.substring(0, questionText.length() - 2); + } + questionLabel.setText(questionText + " = ?"); + + // 清空选项容器 + optionsContainer.getChildren().clear(); + optionsGroup = new ToggleGroup(); + + // 添加选项 + List options = question.getOptions(); + for (int i = 0; i < options.size(); i++) { + RadioButton radioButton = new RadioButton((char)('A' + i) + ". " + options.get(i)); + radioButton.setToggleGroup(optionsGroup); + radioButton.setFont(Font.font(14)); + radioButton.setWrapText(true); + radioButton.setPrefWidth(500); + optionsContainer.getChildren().add(radioButton); + } + + // 设置下一题按钮 + nextButton.setDisable(true); + nextButton.setText(index == questions.size() - 1 ? "提交答案" : "下一题"); + nextButton.setOnAction(e -> { + checkAnswer(question); + showQuestion(index + 1); + }); + + // 启用下一题按钮当选择了一个选项时 + optionsGroup.selectedToggleProperty().addListener((obs, oldVal, newVal) -> { + nextButton.setDisable(newVal == null); + }); + } + + private void checkAnswer(QuestionWithOptions question) { + RadioButton selectedRadioButton = (RadioButton) optionsGroup.getSelectedToggle(); + if (selectedRadioButton != null) { + int selectedIndex = optionsContainer.getChildren().indexOf(selectedRadioButton); + if (selectedIndex == question.getCorrectAnswerIndex()) { + correctAnswers++; + } + } + } + + public Scene getScene() { + return scene; + } + + public MultipleChoiceGenerator getMultipleChoiceGenerator() { + return multipleChoiceGenerator; + } +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/view/RegisterView.java b/src/main/java/com/wsf/mathapp/view/RegisterView.java new file mode 100644 index 0000000..f1ed416 --- /dev/null +++ b/src/main/java/com/wsf/mathapp/view/RegisterView.java @@ -0,0 +1,229 @@ +package com.wsf.mathapp.view; + +import com.wsf.mathapp.controller.SceneManager; +import com.ybw.mathapp.util.EmailService; +import com.ybw.mathapp.util.Register; +import java.util.regex.Pattern; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; +import com.ybw.mathapp.util.LoginFileUtils; + +public class RegisterView { + private Scene scene; + private final SceneManager sceneManager; + + public RegisterView(SceneManager sceneManager) { + this.sceneManager = sceneManager; + createScene(); + } + + private void createScene() { + VBox root = new VBox(15); + root.setPadding(new Insets(30)); + root.setAlignment(Pos.CENTER); + + Label titleLabel = new Label("用户注册"); + titleLabel.setFont(Font.font(20)); + + // 添加用户名输入框 + TextField usernameField = new TextField(); + usernameField.setPromptText("请输入用户名(3-20位字母、数字、下划线)"); + usernameField.setMaxWidth(300); + usernameField.setPrefHeight(35); + + TextField emailField = new TextField(); + emailField.setPromptText("请输入邮箱"); + emailField.setMaxWidth(300); + emailField.setPrefHeight(35); + + Button sendCodeButton = new Button("发送验证码"); + sendCodeButton.setStyle("-fx-background-color: #FF9800; -fx-text-fill: white;"); + sendCodeButton.setPrefSize(120, 35); + + TextField codeField = new TextField(); + codeField.setPromptText("请输入验证码"); + codeField.setMaxWidth(300); + codeField.setPrefHeight(35); + + PasswordField passwordField = new PasswordField(); + passwordField.setPromptText("请输入密码(6-10位,含大小写字母和数字)"); + passwordField.setMaxWidth(300); + passwordField.setPrefHeight(35); + + PasswordField confirmPasswordField = new PasswordField(); + confirmPasswordField.setPromptText("请再次输入密码"); + confirmPasswordField.setMaxWidth(300); + confirmPasswordField.setPrefHeight(35); + + Button registerButton = new Button("注册"); + registerButton.setStyle("-fx-background-color: #4CAF50; -fx-text-fill: white; -fx-font-size: 14px;"); + registerButton.setPrefSize(300, 40); + + Button backButton = new Button("返回登录"); + backButton.setStyle("-fx-background-color: #757575; -fx-text-fill: white;"); + backButton.setPrefSize(300, 35); + + Label statusLabel = new Label(); + + sendCodeButton.setOnAction(e -> { + String username = usernameField.getText().trim(); + String email = emailField.getText().trim(); + + // 验证用户名 + if (!isValidUsername(username)) { + showError(statusLabel, "用户名只包含英文字母和数字)"); + return; + } + + // 检查用户名是否已存在 + if (LoginFileUtils.isNameRegistered(username)) { + showError(statusLabel, "用户名已存在,请选择其他用户名!"); + return; + } + + if (!EmailService.isValidEmail(email)) { + showError(statusLabel, "请输入有效的邮箱地址!"); + return; + } + + if (LoginFileUtils.isEmailRegistered(email)){ + showError(statusLabel, "该邮箱已注册,请直接登录!"); + return; + } + + // 生成并发送验证码 + boolean sent = EmailService.sendCode(email); + if (sent) { + showSuccess(statusLabel, "验证码已发送到您的邮箱!"); + // 禁用发送按钮60秒 + sendCodeButton.setDisable(true); + startCountdown(sendCodeButton); + } else { + showError(statusLabel, "发送验证码失败,请稍后重试!"); + } + }); + + registerButton.setOnAction(e -> { + String username = usernameField.getText().trim(); + String email = emailField.getText().trim(); + String code = codeField.getText().trim(); + String password = passwordField.getText(); + String confirmPassword = confirmPasswordField.getText(); + + // 验证用户名 + if (!isValidUsername(username)) { + showError(statusLabel, "用户名只包含英文字母和数字"); + return; + } + + // 检查用户名是否已存在 + if (LoginFileUtils.isNameRegistered((username))){ + showError(statusLabel, "用户名已存在,请选择其他用户名!"); + return; + } + + if (!EmailService.isValidEmail(email)) { + showError(statusLabel, "请输入有效的邮箱地址!"); + return; + } + + if (LoginFileUtils.isEmailRegistered(email)){ + showError(statusLabel, "该邮箱已注册,请直接登录!"); + return; + } + + if (username.isEmpty() || email.isEmpty() || code.isEmpty() || password.isEmpty()) { + showError(statusLabel, "请填写所有字段!"); + return; + } + + // 验证验证码 + if (!EmailService.verifyCode(email, code)) { + showError(statusLabel, "验证码错误或已过期!"); + return; + } + + if (!Register.isVaildPassword(password)) { + showError(statusLabel, "密码要6-10位,包含大,小写字母和数字"); + return; + } + + if (!Register.isEqualPassword(password,confirmPassword)){ + showError(statusLabel, "两次密码不一致"); + return; + } + + if (Register.register(username, email, password)) { + showSuccess(statusLabel, "注册成功!用户名: " + username); + sceneManager.showLoginView(); + } else { + showError(statusLabel, "注册失败,请检查信息!"); + } + }); + + backButton.setOnAction(e -> { + sceneManager.showLoginView(); + }); + + root.getChildren().addAll( + titleLabel, + usernameField, // 添加用户名输入框 + emailField, + sendCodeButton, + codeField, + passwordField, + confirmPasswordField, + registerButton, + backButton, + statusLabel + ); + + scene = new Scene(root, 400, 550); // 增加高度以适应新字段 + } + + private void startCountdown(Button button) { + new Thread(() -> { + try { + for (int i = 60; i > 0; i--) { + int finalI = i; + javafx.application.Platform.runLater(() -> { + button.setText(finalI + "秒后重发"); + }); + Thread.sleep(1000); + } + javafx.application.Platform.runLater(() -> { + button.setText("发送验证码"); + button.setDisable(false); + }); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + }).start(); + } + + private void showError(Label label, String message) { + label.setText(message); + label.setStyle("-fx-text-fill: red;"); + } + + private void showSuccess(Label label, String message) { + label.setText(message); + label.setStyle("-fx-text-fill: green;"); + } + + public Scene getScene() { + return scene; + } + public static boolean isValidUsername(String username) { + if (username == null || username.trim().isEmpty()) { + return false; + } + // 用户名规则:只能包含英文字母和数字 + String usernameRegex = "^[a-zA-Z0-9]{3,100}$"; + return Pattern.matches(usernameRegex, username.trim()); + } +} diff --git a/src/main/java/com/wsf/mathapp/view/ResultView.java b/src/main/java/com/wsf/mathapp/view/ResultView.java new file mode 100644 index 0000000..5276044 --- /dev/null +++ b/src/main/java/com/wsf/mathapp/view/ResultView.java @@ -0,0 +1,211 @@ +package com.wsf.mathapp.view; + +import com.wsf.mathapp.controller.SceneManager; +import java.util.Objects; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; + +public class ResultView { + private Scene scene; + private final SceneManager sceneManager; + private double score; + private String currentUsername; + + public ResultView(SceneManager sceneManager) { + this.sceneManager = sceneManager; + this.currentUsername = "用户"; // 默认用户名 + createScene(); + } + + private void createScene() { + // 创建主容器 + VBox mainContainer = new VBox(); + mainContainer.setPadding(new Insets(20)); + + // 创建顶部用户信息栏 + HBox userInfoBar = createUserInfoBar(); + + // 创建结果内容区域 + VBox resultContent = createResultContent(); + + mainContainer.getChildren().addAll(userInfoBar, resultContent); + + scene = new Scene(mainContainer, 500, 550); // 增加宽度和高度以适应新布局 + } + + private HBox createUserInfoBar() { + HBox userInfoBar = new HBox(10); + userInfoBar.setAlignment(Pos.CENTER_LEFT); + userInfoBar.setPadding(new Insets(0, 0, 20, 0)); + + // 创建默认头像(使用系统图标或文本) + ImageView avatar = createDefaultAvatar(); + + // 用户名标签 + Label usernameLabel = new Label(currentUsername); + usernameLabel.setFont(Font.font(16)); + usernameLabel.setStyle("-fx-text-fill: #333;"); + + // 添加间隔,让用户信息靠左,其他内容居中 + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + userInfoBar.getChildren().addAll(avatar, usernameLabel, spacer); + + return userInfoBar; + } + + private ImageView createDefaultAvatar() { + // 尝试加载默认头像图片,如果失败则使用文本头像 + ImageView avatar = new ImageView(); + avatar.setFitWidth(40); + avatar.setFitHeight(40); + + try { + // 尝试加载默认头像图片 + Image defaultAvatar = new Image( + Objects.requireNonNull(getClass().getResourceAsStream("/images/default-avatar.png"))); + avatar.setImage(defaultAvatar); + } catch (Exception e) { + // 如果图片加载失败,创建一个圆形文本头像 + avatar.setImage(createTextAvatar()); + } + + // 设置头像样式 + avatar.setStyle( + "-fx-background-color: #4CAF50; " + + "-fx-background-radius: 20; " + + "-fx-border-radius: 20; " + + "-fx-border-color: #ddd; " + + "-fx-border-width: 2;" + ); + + return avatar; + } + + private Image createTextAvatar() { + // 创建一个简单的文本头像(使用首字母) + String firstLetter = !currentUsername.isEmpty() ? + currentUsername.substring(0, 1).toUpperCase() : "U"; + + // 这里可以创建一个包含文本的图像,但为了简单起见,我们使用CSS样式 + // 在实际项目中,你可以使用Canvas绘制或预加载图片 + return null; // 返回null,我们将使用CSS样式 + } + + private VBox createResultContent() { + VBox resultContent = new VBox(30); + resultContent.setPadding(new Insets(20)); + resultContent.setAlignment(Pos.CENTER); + + Label titleLabel = new Label("答题完成"); + titleLabel.setFont(Font.font(24)); + + Label scoreLabel = new Label(); + scoreLabel.setFont(Font.font(48)); + + Label messageLabel = new Label(); + messageLabel.setFont(Font.font(18)); + + Button restartButton = new Button("再次练习"); + restartButton.setStyle("-fx-background-color: #4CAF50; -fx-text-fill: white; -fx-font-size: 16px;"); + restartButton.setPrefSize(200, 50); + + Button mainMenuButton = new Button("返回主菜单"); + mainMenuButton.setStyle("-fx-background-color: #2196F3; -fx-text-fill: white; -fx-font-size: 16px;"); + mainMenuButton.setPrefSize(200, 50); + + Button exitButton = new Button("退出系统"); + exitButton.setStyle("-fx-background-color: #757575; -fx-text-fill: white; -fx-font-size: 16px;"); + exitButton.setPrefSize(200, 50); + + restartButton.setOnAction(e -> { + sceneManager.showLevelSelectionView(); + }); + + mainMenuButton.setOnAction(e -> { + sceneManager.showMainMenuView(); + }); + + exitButton.setOnAction(e -> { + System.exit(0); + }); + + resultContent.getChildren().addAll( + titleLabel, scoreLabel, messageLabel, + restartButton, mainMenuButton, exitButton + ); + + return resultContent; + } + + public void setScore(double score) { + this.score = score; + updateScoreDisplay(); + } + + public void setCurrentUsername(String username) { + this.currentUsername = username != null ? username : "用户"; + updateUserInfo(); + } + + private void updateUserInfo() { + if (scene != null) { + VBox mainContainer = (VBox) scene.getRoot(); + HBox userInfoBar = (HBox) mainContainer.getChildren().get(0); + + // 更新用户名标签 + Label usernameLabel = (Label) userInfoBar.getChildren().get(1); + usernameLabel.setText(currentUsername); + + // 更新头像(如果需要显示首字母) + updateAvatar(); + } + } + + private void updateAvatar() { + // 如果需要根据用户名更新头像样式,可以在这里实现 + // 例如根据用户名首字母设置不同的背景颜色 + } + + private void updateScoreDisplay() { + if (scene != null) { + VBox mainContainer = (VBox) scene.getRoot(); + VBox resultContent = (VBox) mainContainer.getChildren().get(1); + Label scoreLabel = (Label) resultContent.getChildren().get(1); + Label messageLabel = (Label) resultContent.getChildren().get(2); + + int percentage = (int) (score * 100); + scoreLabel.setText(percentage + "分"); + + // 根据分数设置不同的颜色和评语 + if (score >= 0.9) { + scoreLabel.setStyle("-fx-text-fill: #4CAF50;"); + messageLabel.setText("优秀!表现非常出色!"); + } else if (score >= 0.7) { + scoreLabel.setStyle("-fx-text-fill: #FF9800;"); + messageLabel.setText("良好!继续加油!"); + } else if (score >= 0.6) { + scoreLabel.setStyle("-fx-text-fill: #FFC107;"); + messageLabel.setText("及格!还有进步空间!"); + } else { + scoreLabel.setStyle("-fx-text-fill: #f44336;"); + messageLabel.setText("需要多加练习!"); + } + } + } + + public Scene getScene() { + return scene; + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/HelloApplication.java b/src/main/java/com/ybw/mathapp/HelloApplication.java deleted file mode 100644 index 6ee41bb..0000000 --- a/src/main/java/com/ybw/mathapp/HelloApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.ybw.mathapp; - -import javafx.application.Application; -import javafx.fxml.FXMLLoader; -import javafx.scene.Scene; -import javafx.stage.Stage; - -import java.io.IOException; - -public class HelloApplication extends Application { - - @Override - public void start(Stage stage) throws IOException { - FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml")); - Scene scene = new Scene(fxmlLoader.load(), 320, 240); - stage.setTitle("Hello!"); - stage.setScene(scene); - stage.show(); - } - - public static void main(String[] args) { - launch(); - } -} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/HelloController.java b/src/main/java/com/ybw/mathapp/HelloController.java deleted file mode 100644 index 262fb4d..0000000 --- a/src/main/java/com/ybw/mathapp/HelloController.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.ybw.mathapp; - -import javafx.fxml.FXML; -import javafx.scene.control.Label; - -public class HelloController { - - @FXML - private Label welcomeText; - - @FXML - protected void onHelloButtonClick() { - welcomeText.setText("Welcome to JavaFX Application!"); - } -} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/LoginAndRegister.java b/src/main/java/com/ybw/mathapp/LoginAndRegister.java new file mode 100644 index 0000000..d71d548 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/LoginAndRegister.java @@ -0,0 +1,132 @@ +package com.ybw.mathapp; + +// UserService.java +import com.ybw.mathapp.entity.User; +import com.ybw.mathapp.service.EmailService; +import com.ybw.mathapp.util.LoginFileUtils; +import java.util.Scanner; +import java.util.regex.Pattern; + +public class LoginAndRegister { + private static Scanner scanner = new Scanner(System.in); + + // UserService.java 中的注册方法更新 + public static boolean register() { + System.out.println("\n=== 用户注册 ==="); + + // 输入邮箱 + System.out.print("请输入邮箱地址: "); + String email = scanner.nextLine().trim(); + + if (!isValidEmail(email)) { + return false; + } + + if (LoginFileUtils.isEmailRegistered(email)) { + System.out.println("该邮箱已注册,请直接登录!"); + return false; + } + + // 发送、验证验证码 + if (!sendAndVerifyCode(email)) { + return false; + } + + // 设置密码(其余代码保持不变) + System.out.print("请输入密码: "); + String password1 = scanner.nextLine(); + System.out.print("请再次输入密码: "); + String password2 = scanner.nextLine(); + if(!isVaildPassword(password1, password2)) { + return false; + } + + User user = new User(email, password1); + LoginFileUtils.saveUser(user); + System.out.println("注册成功!您可以使用邮箱和密码登录了。"); + return true; + } + + // 登录流程 + public static boolean login() { + System.out.println("\n=== 用户登录 ==="); + + System.out.print("请输入邮箱: "); + String email = scanner.nextLine().trim(); + + System.out.print("请输入密码: "); + String password = scanner.nextLine(); + + if (LoginFileUtils.validateUser(email, password)) { + System.out.println("登录成功!欢迎回来," + email); + return true; + } else { + System.out.println("邮箱或密码错误!"); + return false; + } + } + + // + /** + * 邮箱格式验证 + * @param email 待验证的邮箱地址 + * @return true表示邮箱格式正确,false表示邮箱格式错误 + */ + private static boolean isValidEmail(String email) { + if (email.isEmpty()) { + System.out.println("邮箱地址不能为空!"); + return false; + } + if (!(email.contains("@") && email.contains("."))) { + System.out.println("邮箱格式不正确!"); + return false; + } + return true; + } + + /** + * 密码格式验证 + * @param password1 第一次输入的密码 + * @param password2 第二次输入的密码 + * @return true表示符合要求,false表示不符合 + */ + public static boolean isVaildPassword(String password1, String password2) { + if (password1 == null || password1.length() < 6 || password1.length() > 10) { + return false; + } + + // 使用正则表达式验证:长度6-10,只包含字母数字,且包含大小写字母和数字 + String regex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{6,10}$"; + if (!Pattern.matches(regex, password1)) { + return false; + } + + System.out.print("请再次输入密码: "); + if (!password1.equals(password2)) { + System.out.println("两次输入的密码不一致!"); + return false; + } + return true; + } + + public static boolean sendAndVerifyCode(String email) { + // 发送真实邮件验证码 + String verificationCode = EmailService.generateVerificationCode(); + System.out.println("正在发送验证码邮件,请稍候..."); + + if (!EmailService.sendVerificationCode(email, verificationCode)) { + System.out.println("发送验证码失败,请检查邮箱配置或稍后重试!"); + return false; + } + + // 验证验证码 + System.out.print("请输入收到的验证码: "); + String inputCode = scanner.nextLine().trim(); + + if (!EmailService.verifyCode(email, inputCode)) { + System.out.println("验证码错误或已过期!"); + return false; + } + return true; + } +} diff --git a/src/main/java/com/ybw/mathapp/Main.java b/src/main/java/com/ybw/mathapp/Main.java new file mode 100644 index 0000000..3e360f8 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/Main.java @@ -0,0 +1,36 @@ +package com.ybw.mathapp; + +import com.ybw.mathapp.entity.QuestionWithOptions; +import com.ybw.mathapp.service.JuniorHighGenerator; +import com.ybw.mathapp.service.MultipleChoiceGenerator; +import com.ybw.mathapp.service.PrimarySchoolGenerator; +import java.util.List; + +public class Main { + public static void main(String[] args) { + // 生成小学选择题 + System.out.println("--- Primary School MCQs ---"); + PrimarySchoolGenerator primaryGen = new PrimarySchoolGenerator(); + MultipleChoiceGenerator primaryMC = new MultipleChoiceGenerator(primaryGen, "小学"); // 传入级别 + List primaryMCQs = primaryMC.generateMultipleChoiceQuestions(10); + primaryMCQs.forEach(q -> System.out.println(q + "\n")); + + // 生成初中选择题 + System.out.println("--- Junior High MCQs ---"); + JuniorHighGenerator juniorGen = new JuniorHighGenerator(); + MultipleChoiceGenerator juniorMC = new MultipleChoiceGenerator(juniorGen, "初中"); // 传入级别 + List juniorMCQs = juniorMC.generateMultipleChoiceQuestions(10); + juniorMCQs.forEach(q -> System.out.println(q + "\n")); + + System.out.println(); + /* + // 生成高中选择题 + System.out.println("--- Senior High MCQs ---"); + SeniorHighGenerator seniorGen = new SeniorHighGenerator(); + MultipleChoiceGenerator seniorMC = new MultipleChoiceGenerator(seniorGen, "高中"); // 传入级别 + List seniorMCQs = seniorMC.generateMultipleChoiceQuestions(2, 4); + seniorMCQs.forEach(q -> System.out.println(q + "\n")); + */ + + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/config/EmailConfig.java b/src/main/java/com/ybw/mathapp/config/EmailConfig.java new file mode 100644 index 0000000..8331ea5 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/config/EmailConfig.java @@ -0,0 +1,18 @@ +package com.ybw.mathapp.config; + +public class EmailConfig { + // 发件人邮箱配置(以QQ邮箱为例) + public static final String SMTP_HOST = "smtp.qq.com"; + public static final String SMTP_PORT = "587"; + public static final String SENDER_EMAIL = "1798231811@qq.com"; // 替换为你的邮箱 + public static final String SENDER_PASSWORD = "dzmfirotgnlceeae"; // 替换为你的授权码 + + // 如果使用Gmail + // public static final String SMTP_HOST = "smtp.gmail.com"; + // public static final String SMTP_PORT = "587"; + // public static final String SENDER_EMAIL = "your_email@gmail.com"; + // public static final String SENDER_PASSWORD = "your_app_password"; + + public static final String EMAIL_SUBJECT = "【用户注册】验证码"; + public static final int CODE_EXPIRY_MINUTES = 5; +} diff --git a/src/main/java/com/ybw/mathapp/entity/QuestionWithOptions.java b/src/main/java/com/ybw/mathapp/entity/QuestionWithOptions.java new file mode 100644 index 0000000..fe1c47a --- /dev/null +++ b/src/main/java/com/ybw/mathapp/entity/QuestionWithOptions.java @@ -0,0 +1,56 @@ +package com.ybw.mathapp.entity; + +import java.util.List; + +/** + * 选择题对象,包含题干、选项和正确答案索引。 + * + * @author 你的名字 + * @since 2025 + */ +public class QuestionWithOptions { + private String questionText; // 题干 + private List options; // 选项列表 + private int correctAnswerIndex; // 正确答案的索引 (0-based) + + public QuestionWithOptions(String questionText, List options, int correctAnswerIndex) { + this.questionText = questionText; + this.options = options; + this.correctAnswerIndex = correctAnswerIndex; + } + + public String getQuestionText() { + return questionText; + } + + public void setQuestionText(String questionText) { + this.questionText = questionText; + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } + + public int getCorrectAnswerIndex() { + return correctAnswerIndex; + } + + public void setCorrectAnswerIndex(int correctAnswerIndex) { + this.correctAnswerIndex = correctAnswerIndex; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Question: ").append(questionText).append("\n"); + for (int i = 0; i < options.size(); i++) { + sb.append("Option ").append((char)('A' + i)).append(": ").append(options.get(i)).append("\n"); + } + sb.append("Correct Answer: ").append((char)('A' + correctAnswerIndex)); + return sb.toString(); + } +} diff --git a/src/main/java/com/ybw/mathapp/entity/User.java b/src/main/java/com/ybw/mathapp/entity/User.java new file mode 100644 index 0000000..fa07684 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/entity/User.java @@ -0,0 +1,100 @@ +package com.ybw.mathapp.entity; + +/** + * 用户实体类,表示系统中的用户信息。 + * + *

该类包含用户的基本信息,如用户名、密码和学习级别。 + * 用户级别可以是小学、初中或高中。 + * + * @author 杨博文 + * @version 1.0 + * @since 2025 + */ +public class User { + + public String getName() { + return name; + } + + /** 用户名,不可修改。 */ + private final String name; + + /** 邮箱,不可修改。 */ + private final String email; + + /** 用户密码,不可修改。 */ + private final String password; + + /** 用户当前的学习级别,可以修改。 */ + private String level; + + /** + * 构造一个新的用户对象。 + * + * @param email 邮箱,不能为空 用户名,不能为空 + * @param password 用户密码,不能为空 + */ + public User(String name, String email, String password) { + this.name = name; + this.password = password; + this.email = email; + } + + /** + * 获取用户密码。 + * + * @return 用户密码 + */ + public String getPassword() { + return password; + } + + /** + * 获取用户当前的学习级别。 + * + * @return 用户学习级别 + */ + public String getLevel() { + return level; + } + + /** + * 获取用户邮箱。 + * + * @return 用户邮箱 + */ + public String getEmail() { + return email; + } + + /** + * 设置用户的学习级别。 + * + * @param newLevel 新的学习级别,支持"小学"、"初中"、"高中" + */ + public void setLevel(String newLevel) { + level = newLevel; + } + + /** + * 保存邮箱+密码。 + * + * @return 邮箱+密码 + */ + @Override + public String toString() { + return name + "," + email + "," + password; + } + + public static User fromString(String line) { + if (line == null || line.trim().isEmpty()) { + return null; + } + + String[] parts = line.split(",", 3); // 最多分割成3部分 + if (parts.length == 3) { + return new User(parts[0].trim(), parts[1].trim(), parts[2].trim()); + } + return null; + } +} diff --git a/src/main/java/com/ybw/mathapp/service/AdvancedCaculate.java b/src/main/java/com/ybw/mathapp/service/AdvancedCaculate.java new file mode 100644 index 0000000..d69d11b --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/AdvancedCaculate.java @@ -0,0 +1,180 @@ +package com.ybw.mathapp.service; + +import java.util.*; + +/** + * 扩展的计算类,支持基础四则运算、括号、平方、开根号、三角函数 (sin, cos, tan)。 + * 括号优先级最高。 + * 高级运算符 (平方, 开根号, sin, cos, tan) 优先级高于四则运算。 + * 平方是后置运算符,开根号和三角函数是前置运算符。 + * 例如: "3 + 开根号 16 平方" 解释为 "3 + (sqrt(16))^2" = "3 + 4^2" = "3 + 16" = 19 + * "开根号 ( 4 + 5 ) 平方" 解释为 "(sqrt(4+5))^2" = "sqrt(9)^2" = "3^2" = 9 + * "sin 30 + 5" 解释为 "sin(30) + 5" (假设30是度数) + * 修正:在计算开根号时,若操作数为负数,则抛出 ArithmeticException。 + */ +public class AdvancedCaculate { + + private static final Set OPERATORS = new HashSet<>(Arrays.asList("+", "-", "*", "/")); + private static final Set ADVANCED_OPERATORS = new HashSet<>(Arrays.asList("平方", "开根号", "sin", "cos", "tan")); + private static final Map PRECEDENCE = new HashMap<>(); + + static { + PRECEDENCE.put("+", 1); + PRECEDENCE.put("-", 1); + PRECEDENCE.put("*", 2); + PRECEDENCE.put("/", 2); + // 高级运算符优先级更高 + PRECEDENCE.put("平方", 3); // 后置 + PRECEDENCE.put("开根号", 3); // 前置 + PRECEDENCE.put("sin", 3); // 前置 + PRECEDENCE.put("cos", 3); // 前置 + PRECEDENCE.put("tan", 3); // 前置 + } + + /** + * 计算给定表达式的值。 + * + * @param expressionTokens 表达式分词列表,例如 ["3", "+", "开根号", "16", "平方"] + * @return 计算结果 + * @throws IllegalArgumentException 如果表达式无效 + * @throws ArithmeticException 如果计算过程中出现错误(如除零、负数开根号) + */ + public static double calculate(List expressionTokens) { + Stack numberStack = new Stack<>(); + Stack operatorStack = new Stack<>(); + + for (int i = 0; i < expressionTokens.size(); i++) { + String token = expressionTokens.get(i); + + if (isNumeric(token)) { + numberStack.push(Double.parseDouble(token)); + } else if (token.equals("(")) { + operatorStack.push(token); + } else if (token.equals(")")) { + while (!operatorStack.isEmpty() && !operatorStack.peek().equals("(")) { + processOperator(numberStack, operatorStack.pop()); + } + if (!operatorStack.isEmpty()) { // Pop the "(" + operatorStack.pop(); + } + } else if (ADVANCED_OPERATORS.contains(token)) { + // 前置运算符 (开根号, sin, cos, tan) 直接入栈 + // 后置运算符 (平方) 需要等待其操作数先计算出来 + // 在标准调度场算法中,后置运算符通常在遇到时立即处理其栈顶的操作数 + // 这里我们可以在遇到 "平方" 时,立即对 numberStack 的顶部元素进行平方操作 + if ("平方".equals(token)) { + if (numberStack.isEmpty()) { + throw new IllegalArgumentException("Invalid expression: '平方' lacks an operand."); + } + double operand = numberStack.pop(); + numberStack.push(Math.pow(operand, 2)); + } else { // "开根号", "sin", "cos", "tan" 是前置运算符 + operatorStack.push(token); + } + } else if (OPERATORS.contains(token)) { + // 处理四则运算符,遵循优先级 + while (!operatorStack.isEmpty() && + !operatorStack.peek().equals("(") && + PRECEDENCE.get(token) <= PRECEDENCE.getOrDefault(operatorStack.peek(), 0)) { + processOperator(numberStack, operatorStack.pop()); + } + operatorStack.push(token); + } else { + throw new IllegalArgumentException("Unknown token: " + token); + } + } + + // 处理栈中剩余的所有运算符 + while (!operatorStack.isEmpty()) { + String op = operatorStack.pop(); + if (op.equals("(") || op.equals(")")) { + throw new IllegalArgumentException("Mismatched parentheses in expression."); + } + processOperator(numberStack, op); + } + + if (numberStack.size() != 1 || !operatorStack.isEmpty()) { + throw new IllegalArgumentException("Invalid expression: " + String.join(" ", expressionTokens)); + } + + return numberStack.pop(); + } + + /** + * 执行一次运算操作。 + * + * @param numberStack 数字栈 + * @param operator 运算符 + */ + private static void processOperator(Stack numberStack, String operator) { + if (numberStack.size() < 1) { + throw new IllegalArgumentException("Invalid expression: operator '" + operator + "' lacks operand(s)."); + } + + if (ADVANCED_OPERATORS.contains(operator)) { + double operand = numberStack.pop(); + switch (operator) { + case "开根号": + if (operand < 0) { + // 抛出异常,让调用者(MultipleChoiceGenerator)处理 + throw new ArithmeticException("Cannot take square root of negative number: " + operand); + } + numberStack.push(Math.sqrt(operand)); + break; + case "sin": + numberStack.push(Math.sin(Math.toRadians(operand))); // 假设输入是度数 + break; + case "cos": + numberStack.push(Math.cos(Math.toRadians(operand))); + break; + case "tan": + // tan(90 + n*180) 会趋向无穷,这里不特别处理,让其返回 Infinity 或 -Infinity + numberStack.push(Math.tan(Math.toRadians(operand))); + break; + default: + throw new IllegalArgumentException("Unknown advanced operator: " + operator); + } + } else if (OPERATORS.contains(operator)) { + if (numberStack.size() < 2) { + throw new IllegalArgumentException("Invalid expression: operator '" + operator + "' lacks operand(s)."); + } + double b = numberStack.pop(); + double a = numberStack.pop(); + double result; + switch (operator) { + case "+": + result = a + b; + break; + case "-": + result = a - b; + break; + case "*": + result = a * b; + break; + case "/": + if (b == 0) { + throw new ArithmeticException("Division by zero"); + } + result = a / b; + break; + default: + throw new IllegalArgumentException("Unknown operator: " + operator); + } + numberStack.push(result); + } else { + throw new IllegalArgumentException("Unexpected operator in process: " + operator); + } + } + + public static boolean isNumeric(String str) { + if (str == null || str.isEmpty()) { + return false; + } + try { + Double.parseDouble(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/service/EmailService.java b/src/main/java/com/ybw/mathapp/service/EmailService.java new file mode 100644 index 0000000..4728a47 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/EmailService.java @@ -0,0 +1,149 @@ +package com.ybw.mathapp.service; + +import com.ybw.mathapp.config.EmailConfig; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Random; +//import javax.mail.*; +//import javax.mail.internet.*; + +public class EmailService { + private static Map verificationCodes = new HashMap<>(); + + private static class VerificationCodeInfo { + String code; + long timestamp; + + VerificationCodeInfo(String code, long timestamp) { + this.code = code; + this.timestamp = timestamp; + } + } + + public static String generateVerificationCode() { + Random random = new Random(); + int code = 100000 + random.nextInt(900000); + return String.valueOf(code); + } + + public static boolean sendVerificationCode(String recipientEmail, String code) { + /* + try { + // 创建邮件会话 + Properties props = new Properties(); + props.put("mail.smtp.host", EmailConfig.SMTP_HOST); + props.put("mail.smtp.port", EmailConfig.SMTP_PORT); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.starttls.enable", "true"); + + // 创建认证器 + Authenticator auth = new Authenticator() { + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication( + EmailConfig.SENDER_EMAIL, + EmailConfig.SENDER_PASSWORD + ); + } + }; + + Session session = Session.getInstance(props, auth); + + // 创建邮件消息 + Message message = new MimeMessage(session); + message.setFrom(new InternetAddress(EmailConfig.SENDER_EMAIL)); + message.setRecipients(Message.RecipientType.TO, + InternetAddress.parse(recipientEmail)); + message.setSubject(EmailConfig.EMAIL_SUBJECT); + + // 创建邮件内容 + String emailContent = createEmailContent(code); + message.setContent(emailContent, "text/html; charset=utf-8"); + + // 发送邮件 + Transport.send(message); + + // 存储验证码信息 + verificationCodes.put(recipientEmail, + new VerificationCodeInfo(code, System.currentTimeMillis())); + + System.out.println("验证码已发送到邮箱: " + recipientEmail); + return true; + + } catch (Exception e) { + System.err.println("发送邮件失败: " + e.getMessage()); + // 在开发环境中,直接返回true用于测试 + verificationCodes.put(recipientEmail, + new VerificationCodeInfo(code, System.currentTimeMillis())); + return true; + } + */ + return true; + } + + private static String createEmailContent(String code) { + return "" + + "" + + "" + + "" + + "" + + "" + + "" + + "

" + + "
" + + "

数学学习软件 - 注册验证码

" + + "
" + + "
" + + "

您好!

" + + "

您正在注册数学学习软件账户,验证码如下:

" + + "
" + code + "
" + + "

验证码有效期为 " + EmailConfig.CODE_EXPIRY_MINUTES + " 分钟,请勿泄露给他人。

" + + "

如果这不是您本人的操作,请忽略此邮件。

" + + "
" + + "" + + "
" + + "" + + ""; + } + + public static boolean verifyCode(String email, String inputCode) { + VerificationCodeInfo codeInfo = verificationCodes.get(email); + if (codeInfo == null) { + return false; + } + + // 检查验证码是否过期 + long currentTime = System.currentTimeMillis(); + if (currentTime - codeInfo.timestamp > EmailConfig.CODE_EXPIRY_MINUTES * 60 * 1000) { + verificationCodes.remove(email); + return false; + } + + return codeInfo.code.equals(inputCode); + } + + public static void cleanupExpiredCodes() { + long currentTime = System.currentTimeMillis(); + Iterator> iterator = + verificationCodes.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (currentTime - entry.getValue().timestamp > + EmailConfig.CODE_EXPIRY_MINUTES * 60 * 1000) { + iterator.remove(); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/service/JuniorHighGenerator.java b/src/main/java/com/ybw/mathapp/service/JuniorHighGenerator.java new file mode 100644 index 0000000..853218c --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/JuniorHighGenerator.java @@ -0,0 +1,186 @@ +package com.ybw.mathapp.service; + +import static com.ybw.mathapp.service.AdvancedCaculate.isNumeric; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * 初中题目生成器,负责生成包含平方或开根号运算的初中级别数学题目。 + * + *

该生成器确保每道题目都包含至少一个高级运算符(平方或开根号), + * 题目结构包含基本的四则运算和高级运算的组合。 + * + * @author 杨博文 + * @version 1.0 + * @since 2025 + */ +public class JuniorHighGenerator implements QuestionGenerator { + + /** + * 高级运算符数组,包含"平方"和"开根号"。 + */ + private static final String[] ADVANCED_OPS = {"平方", "开根号"}; + + /** + * 基本运算符数组,包含四则运算符号。 + */ + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + + /** + * 随机数生成器,用于生成随机题目。 + */ + private final Random random = new Random(); + + @Override + public List generateQuestions(int count) { + List questions = new ArrayList<>(); + for (int i = 0; i < count; i++) { + String question = generateSingleQuestion(); + questions.add(question); + } + return questions; + } + + /** + * 生成单个初中级别的数学题目。 + * + *

该方法确保生成的题目包含至少一个高级运算符(平方或开根号), + * 并根据操作数数量采用不同的生成策略。 + * + * @return 生成的数学题目字符串 + */ + private String generateSingleQuestion() { + List parts = new ArrayList<>(); + int operandCount = random.nextInt(5) + 1; + parts = generateBase(operandCount, parts); + // hasAdvancedOp用以检测下面的循环是否加入了高级运算符,如果没有就启动保底 + boolean hasAdvancedOp = false; + if (operandCount == 1) { + if ("平方".equals(ADVANCED_OPS[random.nextInt(ADVANCED_OPS.length)])) { + parts.add("平方"); + } else { + parts.add(0, "开根号"); + } + hasAdvancedOp = true; + } else { + // 遍历查找左括号的合理位置 + for (int i = 0; i < parts.size() - 2; i++) { + // 该位置要为操作数且随机添加括号 + if (isNumeric(parts.get(i)) && random.nextBoolean()) { + // 随机数看取出来的是不是开根号运算符 + if ("开根号".equals(ADVANCED_OPS[random.nextInt(ADVANCED_OPS.length)])) { + parts = generateRoot(parts, i); + } else { // 如果不是开根号就是平方运算 + parts = generateSquare(parts, i); + } + hasAdvancedOp = true; + break; + } + } + } + // 启动保底强制加入一个高级运算符 + if (!hasAdvancedOp) { + parts = forceAddAdvancedOp(parts); + } + return String.join(" ", parts) + " ="; + } + + /** + * 生成基本的四则运算表达式部分。 + * + *

该方法生成指定数量的操作数和运算符,构成基础的数学表达式。 + * + * @param operandCount 操作数的数量 + * @param parts 用于存储表达式各部分的列表 + * @return 包含基本运算表达式的列表 + */ + public List generateBase(int operandCount, List parts) { + for (int i = 0; i < operandCount; i++) { + int num = random.nextInt(100) + 1; + parts.add(String.valueOf(num)); + if (i < operandCount - 1) { + parts.add(OPERATORS[random.nextInt(OPERATORS.length)]); + } + } + return parts; + } + + /** + * 强制在表达式中添加一个高级运算符作为保底机制。 + * + *

当随机生成过程中没有添加高级运算符时,使用此方法确保 + * 每道题目都包含至少一个高级运算符。 + * + * @param parts 包含表达式各部分的列表 + * @return 添加了高级运算符的表达式列表 + */ + public List forceAddAdvancedOp(List parts) { + String advancedOp = ADVANCED_OPS[random.nextInt(ADVANCED_OPS.length)]; + if ("平方".equals(advancedOp)) { + parts.add("平方"); + } else { // 开根号 + parts.set(0, "开根号 ( " + parts.get(0)); + parts.set(parts.size() - 1, parts.get(parts.size() - 1) + " )"); + } + return parts; + } + + /** + * 在指定位置生成开根号运算。 + * + *

该方法在表达式指定位置添加开根号运算,可能只对单个操作数 + * 进行开根号,或者对一段子表达式进行开根号。 + * + * @param parts 包含表达式各部分的列表 + * @param i 开根号运算的起始位置 + * @return 添加了开根号运算的表达式列表 + */ + public List generateRoot(List parts, int i) { + if (random.nextBoolean()) { + parts.set(i, "开根号 ( " + parts.get(i) + " )"); + } else { + parts.set(i, "开根号 ( " + parts.get(i)); + // 为避免随机数上限出现0,此处要单独判断一下左括号正好括住倒数第二个操作数的情况 + if (i == parts.size() - 3) { + parts.set(parts.size() - 1, parts.get(parts.size() - 1) + " )"); + } else { + while (true) { + int i2 = random.nextInt(parts.size() - 3 - i) + 2; + if (isNumeric(parts.get(i + i2))) { + parts.set(i + i2, parts.get(i + i2) + " )"); + break; + } + } + } + } + return parts; + } + + /** + * 在指定位置生成平方运算。 + * + *

该方法在表达式指定位置添加平方运算,对一段子表达式进行平方运算。 + * + * @param parts 包含表达式各部分的列表 + * @param i 平方运算的起始位置 + * @return 添加了平方运算的表达式列表 + */ + public List generateSquare(List parts, int i) { + parts.set(i, "(" + parts.get(i)); + // 为避免随机数上限出现0,此处要单独判断一下左括号正好括住倒数第二个操作数的情况 + if (i == parts.size() - 3) { + parts.set(parts.size() - 1, parts.get(parts.size() - 1) + " )"); + } else { + while (true) { + int i2 = random.nextInt(parts.size() - 3 - i) + 2; + if (isNumeric(parts.get(i + i2))) { + parts.set(i + i2, parts.get(i + i2) + " ) 平方"); + break; + } + } + } + return parts; + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/service/MultipleChoiceGenerator.java b/src/main/java/com/ybw/mathapp/service/MultipleChoiceGenerator.java new file mode 100644 index 0000000..8c5c6ff --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/MultipleChoiceGenerator.java @@ -0,0 +1,199 @@ +package com.ybw.mathapp.service; + +import com.ybw.mathapp.entity.QuestionWithOptions; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + +/** + * 通用选择题生成器,将基础题目生成器的结果转换为带选项的选择题。 + * 确保生成的题目列表中不包含重复的题干。 + * 特别处理:1. 初中题避免负数开根号;2. 小学题选项避免负数。 + * + * @author 你的名字 + * @since 2025 + */ +public class MultipleChoiceGenerator { + + private final QuestionGenerator baseGenerator; + private final Random random = new Random(); + private static final DecimalFormat df = new DecimalFormat("#0.00"); + private final String level; // 记录当前题目类型,用于特殊处理 + + public MultipleChoiceGenerator(QuestionGenerator baseGenerator, String level) { + this.baseGenerator = baseGenerator; + this.level = level; + } + + /** + * 生成指定数量的选择题,确保题干不重复。 + * + * @param count 题目数量 + * @return 选择题列表 (去重后) + */ + public List generateMultipleChoiceQuestions(int count) { + List mcQuestions = new ArrayList<>(); + Set seenQuestionTexts = new HashSet<>(); // 用于存储已生成的题干,保证唯一性 + + while (mcQuestions.size() < count) { + String baseQuestion = generateUniqueBaseQuestion(seenQuestionTexts); + if (baseQuestion == null) { + // 如果无法生成不重复的基础题目,可能需要退出或处理 + // 例如,如果基础生成器的可能组合用尽了 + break; // 或者抛出异常 + } + + QuestionWithOptions mcq = generateSingleMCQ(baseQuestion); + if (mcq != null) { + mcQuestions.add(mcq); + seenQuestionTexts.add(baseQuestion); // 添加成功生成的题干 + } + // 如果 generateSingleMCQ 返回 null,说明计算或生成选项失败,循环会继续尝试下一个基础题目 + } + return mcQuestions; + } + + /** + * 生成一个唯一的、未处理过的基础题目。 + */ + private String generateUniqueBaseQuestion(Set seenQuestionTexts) { + int attempts = 0; + int maxAttempts = 1000; // 防止无限循环,如果生成太多重复题则退出 + while (attempts < maxAttempts) { + List baseQuestionList = baseGenerator.generateQuestions(1); + String baseQuestion = baseQuestionList.get(0); + if (!seenQuestionTexts.contains(baseQuestion)) { + return baseQuestion; + } + attempts++; + } + return null; // 达到最大尝试次数仍未找到唯一题目 + } + + /** + * 从单个基础题目生成选择题对象。 + */ + private QuestionWithOptions generateSingleMCQ(String baseQuestion) { + try { + String expression = baseQuestion.substring(0, baseQuestion.lastIndexOf(" =")).trim(); + List tokens = tokenizeExpression(expression); + double correctAnswer = AdvancedCaculate.calculate(tokens); + + List options = generateOptions(correctAnswer); + if (options == null) { + // 无法生成足够的选项 + return null; + } + + Collections.shuffle(options); + int correctIndex = options.indexOf(df.format(correctAnswer)); + + return new QuestionWithOptions(baseQuestion, options, correctIndex); + + } catch (ArithmeticException | IllegalArgumentException e) { + // System.out.println("计算或表达式错误,跳过题目: " + baseQuestion + ", Error: " + e.getMessage()); + return null; // 返回 null 表示生成失败 + } + } + + /** + * 生成选项列表 (正确答案 + 错误答案)。 + */ + private List generateOptions(double correctAnswer) { + Set wrongAnswers = new HashSet<>(); + int attempts = 0; + int maxAttempts = 100; // 防止无限循环 + int numWrongOptions = 3; // 假设总共4个选项,需要3个错误答案 + + while (wrongAnswers.size() < numWrongOptions && attempts < maxAttempts) { + int offset = random.nextInt(20) + 1; + if (random.nextBoolean()) { + offset = -offset; + } + double wrongAnswer = correctAnswer + offset; + + if (Math.abs(df.format(wrongAnswer).compareTo(df.format(correctAnswer))) != 0) { + if (!level.equals("小学") || wrongAnswer >= 0) { + wrongAnswers.add(wrongAnswer); + } + } + attempts++; + } + + if (wrongAnswers.size() < numWrongOptions) { + // System.out.println("Warning: Could not generate enough distinct wrong answers for: " + correctAnswer); + return null; // 无法生成足够选项 + } + + List allAnswers = new ArrayList<>(); + allAnswers.add(correctAnswer); + allAnswers.addAll(wrongAnswers); + + List options = new ArrayList<>(); + for (Double ans : allAnswers) { + options.add(df.format(ans)); + } + return options; + } + + // ... (其他类成员和方法保持不变) + + // --- 表达式分词逻辑 --- + // 将 "3 + 开根号 ( 4 ) 平方 - sin 30" 分割成 ["3", "+", "开根号", "(", "4", ")", "平方", "-", "sin", "30"] + private List tokenizeExpression(String expression) { + List tokens = new ArrayList<>(); + int i = 0; + while (i < expression.length()) { + String token = _findNextToken(expression, i); + if (token != null) { + tokens.add(token); + i += token.length(); + } else { + // 如果找不到匹配的 token,可能是单个字符或未知格式 + tokens.add(String.valueOf(expression.charAt(i))); + i++; + } + } + return tokens; + } + + /** + * 查找从指定位置开始的下一个 token。 + * @param expression 表达式字符串 + * @param startPos 起始查找位置 + * @return 找到的 token,如果未找到则返回 null + */ + private String _findNextToken(String expression, int startPos) { + String[] advancedOps = {"平方", "开根号", "sin", "cos", "tan"}; + char c = expression.charAt(startPos); + + if (Character.isWhitespace(c)) { + return null; // 空格由调用者处理 + } + if (c == '(' || c == ')') { + return String.valueOf(c); + } + if (Character.isDigit(c) || c == '.') { + // 查找连续的数字或小数点 + int j = startPos; + while (j < expression.length() && (Character.isDigit(expression.charAt(j)) || expression.charAt(j) == '.')) { + j++; + } + return expression.substring(startPos, j); + } + + // 检查多字符运算符 + for (String op : advancedOps) { + if (expression.startsWith(op, startPos)) { + return op; + } + } + + // 如果不是多字符运算符,则认为是单字符运算符 (+, -, *, / 等) + return String.valueOf(c); + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/service/PrimarySchoolGenerator.java b/src/main/java/com/ybw/mathapp/service/PrimarySchoolGenerator.java new file mode 100644 index 0000000..9374b69 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/PrimarySchoolGenerator.java @@ -0,0 +1,102 @@ +package com.ybw.mathapp.service; + +import static com.ybw.mathapp.service.AdvancedCaculate.isNumeric; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * 小学题目生成器,负责生成包含四则运算和括号的小学级别数学题目。 + * + *

该生成器专门用于生成适合小学生的数学题目,题目仅包含加减乘除四则运算 + * 和括号,确保计算结果为非负数。 + * + * @author 杨博文 + * @version 1.0 + * @since 2025 + */ +public class PrimarySchoolGenerator implements QuestionGenerator { + + /** 运算符数组,包含四则运算符号。 */ + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + + /** 随机数生成器,用于生成随机题目。 */ + private final Random random = new Random(); + + @Override + public List generateQuestions(int count) { + List questions = new ArrayList<>(); + for (int i = 0; i < count; i++) { + String question = generateSingleQuestion(); + questions.add(question); + } + return questions; + } + + /** + * 生成单个小级别的数学题目. + * + *

该方法生成包含2-5个操作数的四则运算表达式,可能包含括号, + * 并确保计算结果为非负数。如果计算结果为负数,则重新生成。 + * + * @return 生成的小学数学题目字符串 + */ + private String generateSingleQuestion() { + int operandCount = random.nextInt(4) + 2; // 2-5个操作数 + List parts = new ArrayList<>(); + while (true) { + // 生成基础操作 + parts = generateBase(operandCount, parts); + // 简单添加括号逻辑:随机加一个括号 + if (operandCount > 2 && random.nextBoolean()) { + // 遍历查找左括号的合理位置 + for (int i = 0; i < parts.size() - 2; i++) { + // 该位置要为操作数且随机添加括号 + if (isNumeric(parts.get(i)) && random.nextBoolean()) { + parts.add(i, "("); + i++; + // 为避免随机数上限出现0,此处要单独判断一下左括号正好括住倒数第二个操作数的情况 + if (i == parts.size() - 3) { + parts.add(")"); + } else { + while (true) { + int i2 = random.nextInt(parts.size() - 3 - i) + 2; + if (isNumeric(parts.get(i + i2))) { + parts.add(i + i2 + 1, ")"); + break; + } + } + } + break; + } + } + } + if (AdvancedCaculate.calculate(parts) >= 0) { + return String.join(" ", parts) + " ="; + } else { + parts.clear(); + } + } + } + + /** + * 生成基本的四则运算表达式部分。 + * + *

该方法生成指定数量的操作数和运算符,构成基础的数学表达式。 + * + * @param operandCount 操作数的数量 + * @param parts 用于存储表达式各部分的列表 + * @return 包含基本运算表达式的列表 + */ + public List generateBase(int operandCount, List parts) { + for (int i = 0; i < operandCount; i++) { + int num = random.nextInt(100) + 1; + parts.add(String.valueOf(num)); + if (i < operandCount - 1) { + parts.add(OPERATORS[random.nextInt(OPERATORS.length)]); + } + } + return parts; + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/service/QuestionGenerator.java b/src/main/java/com/ybw/mathapp/service/QuestionGenerator.java new file mode 100644 index 0000000..91c9436 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/QuestionGenerator.java @@ -0,0 +1,29 @@ +package com.ybw.mathapp.service; + +import java.util.List; + +/** + * 题目生成器接口,定义了题目生成器的标准方法。 + * + *

所有具体的题目生成器都应该实现此接口,提供统一的题目生成功能。 + * 不同级别的题目生成器(如小学、初中、高中)可以根据各自的特点 + * 实现不同的生成算法。 + * + * @author 杨博文 + * @version 1.0 + * @since 2025 + */ +public interface QuestionGenerator { + + /** + * 生成指定数量的数学题目。 + * + *

该方法根据实现类的特定规则生成指定数量的数学题目。 + * 生成的题目应该符合对应教育级别的难度要求。 + * + * @param count 需要生成的题目数量 + * @return 包含生成题目的字符串列表 + */ + List generateQuestions(int count); + +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/service/SeniorHighGenerator.java b/src/main/java/com/ybw/mathapp/service/SeniorHighGenerator.java new file mode 100644 index 0000000..62ab5bb --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/SeniorHighGenerator.java @@ -0,0 +1,127 @@ +package com.ybw.mathapp.service; + +import static com.ybw.mathapp.service.AdvancedCaculate.isNumeric; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * 高中题目生成器,负责生成包含三角函数运算的高中级别数学题目。 + * + *

该生成器确保每道题目都包含至少一个三角函数运算符(sin、cos或tan), + * 题目结构包含基本的四则运算和三角函数运算的组合。 + * + * @author 杨博文 + * @version 1.0 + * @since 2025 + */ +public class SeniorHighGenerator implements QuestionGenerator { + + /** 三角函数运算符数组,包含"sin"、"cos"和"tan"。 */ + private static final String[] TRIG_FUNCS = {"sin", "cos", "tan"}; + + /** 基本运算符数组,包含四则运算符号。 */ + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + + /** 随机数生成器,用于生成随机题目。 */ + private final Random random = new Random(); + + @Override + public List generateQuestions(int count) { + List questions = new ArrayList<>(); + for (int i = 0; i < count; i++) { + String question = generateSingleQuestion(); + questions.add(question); + } + return questions; + } + + /** + * 生成单个高中级别的数学题目。 + * + *

该方法确保生成的题目包含至少一个三角函数运算符, + * 并根据操作数数量采用不同的生成策略。 + * + * @return 生成的数学题目字符串 + */ + private String generateSingleQuestion() { + List parts = new ArrayList<>(); + int operandCount = random.nextInt(5) + 1; + parts = generateBase(operandCount, parts); + String advancedOp; + if (operandCount == 1) { + advancedOp = TRIG_FUNCS[random.nextInt(TRIG_FUNCS.length)]; + parts.set(0, advancedOp + " " + parts.get(0)); + } else { + // 遍历查找左括号的合理位置 + for (int i = 0; i < parts.size(); i++) { + // 最后一次循环保底生成高中三角函数 + if (i == parts.size() - 1) { + advancedOp = TRIG_FUNCS[random.nextInt(TRIG_FUNCS.length)]; + parts.set(i, advancedOp + " " + parts.get(i)); + } else if (isNumeric(parts.get(i)) && random.nextBoolean()) { // 随机数看是否为操作数且随即进入生成程序 + // 进入随机生成tan\sin\cos的程序 + parts = generateTrig(parts, i); + break; + } + } + } + return String.join(" ", parts) + " ="; + } + + /** + * 生成基本的四则运算表达式部分。 + * + *

该方法生成指定数量的操作数和运算符,构成基础的数学表达式。 + * + * @param operandCount 操作数的数量 + * @param parts 用于存储表达式各部分的列表 + * @return 包含基本运算表达式的列表 + */ + // 产生基本操作 + public List generateBase(int operandCount, List parts) { + for (int i = 0; i < operandCount; i++) { + int num = random.nextInt(100) + 1; + parts.add(String.valueOf(num)); + if (i < operandCount - 1) { + parts.add(OPERATORS[random.nextInt(OPERATORS.length)]); + } + } + return parts; + } + + /** + * 在指定位置生成三角函数运算。 + * + *

该方法在表达式指定位置添加三角函数运算,可能只对单个操作数 + * 进行三角函数运算,或者对一段子表达式进行三角函数运算。 + * + * @param parts 包含表达式各部分的列表 + * @param i 三角函数运算的位置 + * @return 添加了三角函数运算的表达式列表 + */ + // 产生三角函数运算符 + public List generateTrig(List parts, int i) { + String trigOp = TRIG_FUNCS[random.nextInt(TRIG_FUNCS.length)]; + if (random.nextBoolean()) { + parts.set(i, trigOp + " " + parts.get(i)); + } else { + parts.set(i, trigOp + " ( " + parts.get(i)); + // 为避免随机数上限出现0,此处要单独判断一下左括号正好括住倒数第二个操作数的情况 + if (i == parts.size() - 3) { + parts.set(parts.size() - 1, parts.get(parts.size() - 1) + " )"); + } else { + while (true) { + int i2 = random.nextInt(parts.size() - 3 - i) + 2; + if (isNumeric(parts.get(i + i2))) { + parts.set(i + i2, parts.get(i + i2) + " )"); + break; + } + } + } + } + return parts; + } + +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/service/StartController.java b/src/main/java/com/ybw/mathapp/service/StartController.java new file mode 100644 index 0000000..00b99cc --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/StartController.java @@ -0,0 +1,166 @@ +package com.ybw.mathapp.service; + +import com.ybw.mathapp.entity.User; +import java.io.IOException; +import java.util.List; +import java.util.Scanner; + +public class StartController { + + private double score; + + public double getScore() { + return score; + } + + public void setScore(double score) { + this.score = score; + } + + public void start() { + Scanner scanner = new Scanner(System.in); + while (true) { + System.out.println("=== 数学学习软件 ==="); + System.out.println("1. 登录"); + System.out.println("2. 注册"); + System.out.print("请选择: "); + + String choice = scanner.nextLine(); + if ("1".equals(choice)) { + if (login()) { + showMainMenu(scanner); + } + } else if ("2".equals(choice)) { + if (register()) { + showMainMenu(scanner); + } + } else { + System.out.println("无效选择,请重新输入!"); + } + } + } + + private boolean login() { + System.out.println("\n=== 用户登录 ==="); + Scanner scanner = new Scanner(System.in); + + System.out.print("请输入邮箱: "); + String email = scanner.nextLine().trim(); + + System.out.print("请输入密码: "); + String password = scanner.nextLine(); + + // 这里应该调用真实的登录逻辑 + System.out.println("登录功能待实现"); + return true; + } + + private boolean register() { + System.out.println("\n=== 用户注册 ==="); + System.out.println("注册功能待实现"); + return true; + } + + private void showMainMenu(Scanner scanner) { + while (true) { + System.out.println("\n=== 主菜单 ==="); + System.out.println("1. 开始练习"); + System.out.println("2. 修改密码"); + System.out.println("3. 退出登录"); + System.out.print("请选择: "); + + String choice = scanner.nextLine(); + if ("1".equals(choice)) { + showLevelSelection(scanner); + } else if ("2".equals(choice)) { + changePassword(scanner); + } else if ("3".equals(choice)) { + System.out.println("退出登录成功!"); + return; + } else { + System.out.println("无效选择,请重新输入!"); + } + } + } + + private void showLevelSelection(Scanner scanner) { + System.out.println("\n=== 选择题目级别 ==="); + System.out.println("1. 小学"); + System.out.println("2. 初中"); + System.out.println("3. 高中"); + System.out.println("4. 返回"); + System.out.print("请选择: "); + + String choice = scanner.nextLine(); + String level = null; + switch (choice) { + case "1": level = "小学"; break; + case "2": level = "初中"; break; + case "3": level = "高中"; break; + case "4": return; + default: + System.out.println("无效选择!"); + return; + } + + showQuestionCount(scanner, level); + } + + private void showQuestionCount(Scanner scanner, String level) { + System.out.println("\n=== 选择题目数量 ==="); + System.out.print("请输入题目数量 (10-30): "); + + try { + int count = Integer.parseInt(scanner.nextLine()); + if (count < 10 || count > 30) { + System.out.println("题目数量必须在10-30之间!"); + return; + } + + startQuiz(level, count); + + } catch (NumberFormatException e) { + System.out.println("请输入有效的数字!"); + } + } + + private void startQuiz(String level, int count) { + System.out.println("\n开始 " + level + " 级别答题,共 " + count + " 题"); + // 这里应该调用题目生成和答题逻辑 + System.out.println("答题功能待实现"); + + // 模拟答题结果 + this.score = 0.8; // 模拟80%的正确率 + System.out.println("答题完成!得分: " + (score * 100) + "%"); + } + + private void changePassword(Scanner scanner) { + System.out.println("\n=== 修改密码 ==="); + System.out.println("修改密码功能待实现"); + } + + public QuestionGenerator createGenerator(String level) { + switch (level) { + case "小学": + return new PrimarySchoolGenerator(); + case "初中": + return new JuniorHighGenerator(); + case "高中": + return new SeniorHighGenerator(); + default: + return null; + } + } + + public boolean isCorrectAnswer(String input, int correctAnswerIndex, List options) { + if (input.equals(String.valueOf(options.get(correctAnswerIndex)))) { + return true; + } else { + return false; + } + } + + public double caculateScore(int rightCount, int totalCount) { + return rightCount / (double) totalCount; + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/util/ChangePassword.java b/src/main/java/com/ybw/mathapp/util/ChangePassword.java new file mode 100644 index 0000000..72af3df --- /dev/null +++ b/src/main/java/com/ybw/mathapp/util/ChangePassword.java @@ -0,0 +1,91 @@ +package com.ybw.mathapp.util; + +import static com.ybw.mathapp.util.LoginFileUtils.USER_FILE; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class ChangePassword { + static final List lines = new ArrayList<>(); + static String userLine = null; + static int userLineNumber = -1; + + // 前端接口-修改密码 + public static boolean changePassword(String name, String newPassword) { + File file = new File(USER_FILE); + if (!file.exists()) { + System.out.println("用户文件不存在: " + USER_FILE); + return false; + } + + // 1. 读取文件,查找用户 + if(!findUserLine(name, file)) { + return false; + } + + if (userLine == null || userLineNumber == -1) { + // 用户未找到 + System.out.println("用户 '" + name + "' 不存在,修改失败。"); + return false; + } + + // 2. 更新找到的用户行中的密码 + String[] parts = userLine.split(","); + if (parts.length != 3) { + return false; + } + parts[2] = newPassword; // 假设密码是第三个字段 + String updatedLine = String.join(",", parts); + + lines.set(userLineNumber, updatedLine); // 替换列表中的旧行 + + // 3. 将更新后的内容写回文件 + if(!writeBack(lines, file)) { + return false; + } + + return true; + } + + public static boolean writeBack(List lines, File file) { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + for (String l : lines) { + writer.write(l); + writer.newLine(); + } + } catch (IOException e) { + System.err.println("写入文件时出错: " + e.getMessage()); + return false; // 如果写回失败,认为修改未成功 + } + return true; + } + + public static boolean findUserLine(String name, File file) { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + int currentLineNum = 0; + while ((line = reader.readLine()) != null) { + lines.add(line); + String[] parts = line.split(","); + // 假设格式为: username,email,password + if (parts.length >= 3 && parts[0].equals(name)) { + userLine = line; // 找到用户行 + userLineNumber = currentLineNum; + break; // 找到后可以退出循环 + } + currentLineNum++; + } + } catch (IOException e) { + System.err.println("读取文件时出错: " + e.getMessage()); + return false; + } + return true; + } + +} diff --git a/src/main/java/com/ybw/mathapp/util/EmailService.java b/src/main/java/com/ybw/mathapp/util/EmailService.java new file mode 100644 index 0000000..eba77a8 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/util/EmailService.java @@ -0,0 +1,189 @@ +package com.ybw.mathapp.util; + +import com.ybw.mathapp.config.EmailConfig; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Random; +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; + +public class EmailService { + private static Map verificationCodes = new HashMap<>(); + + // 验证码信息内部类 + private static class VerificationCodeInfo { + String code; + long timestamp; + + VerificationCodeInfo(String code, long timestamp) { + this.code = code; + this.timestamp = timestamp; + } + } + + // 生成6位随机验证码 + public static String generateVerificationCode() { + Random random = new Random(); + int code = 100000 + random.nextInt(900000); + return String.valueOf(code); + } + + // 发送真实邮件验证码 + public static boolean sendVerificationCode(String recipientEmail, String code) { + try { + // 创建邮件会话 + Properties props = new Properties(); + props.put("mail.smtp.host", EmailConfig.SMTP_HOST); + props.put("mail.smtp.port", EmailConfig.SMTP_PORT); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.starttls.enable", "true"); + props.put("mail.smtp.ssl.protocols", "TLSv1.2"); + + // 创建认证器 + Authenticator auth = new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication( + EmailConfig.SENDER_EMAIL, + EmailConfig.SENDER_PASSWORD + ); + } + }; + + Session session = Session.getInstance(props, auth); + + // 创建邮件消息 + Message message = new MimeMessage(session); + message.setFrom(new InternetAddress(EmailConfig.SENDER_EMAIL)); + message.setRecipients(Message.RecipientType.TO, + InternetAddress.parse(recipientEmail)); + message.setSubject(EmailConfig.EMAIL_SUBJECT); + + // 创建邮件内容 + String emailContent = createEmailContent(code); + message.setContent(emailContent, "text/html; charset=utf-8"); + + // 发送邮件 + Transport.send(message); + + // 存储验证码信息 + verificationCodes.put(recipientEmail, + new VerificationCodeInfo(code, System.currentTimeMillis())); + + System.out.println("验证码已发送到邮箱: " + recipientEmail); + return true; + + } catch (MessagingException e) { + System.err.println("发送邮件失败: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + // 创建HTML格式的邮件内容 + private static String createEmailContent(String code) { + return "" + + "" + + "" + + "" + + "" + + "" + + "" + + "

" + + "
" + + "

用户注册验证码

" + + "
" + + "
" + + "

您好!

" + + "

您正在注册账户,验证码如下:

" + + "
" + code + "
" + + "

验证码有效期为 " + EmailConfig.CODE_EXPIRY_MINUTES + " 分钟,请勿泄露给他人。

" + + "

如果这不是您本人的操作,请忽略此邮件。

" + + "
" + + "" + + "
" + + "" + + ""; + } + + // 前端接口-验证验证码 + public static boolean verifyCode(String email, String inputCode) { + VerificationCodeInfo codeInfo = verificationCodes.get(email); + if (codeInfo == null) { + return false; + } + + // 检查验证码是否过期 + long currentTime = System.currentTimeMillis(); + if (currentTime - codeInfo.timestamp > EmailConfig.CODE_EXPIRY_MINUTES * 60 * 1000) { + verificationCodes.remove(email); + return false; + } + + return codeInfo.code.equals(inputCode); + } + + // 清理过期的验证码(可选) + public static void cleanupExpiredCodes() { + long currentTime = System.currentTimeMillis(); + Iterator> iterator = + verificationCodes.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (currentTime - entry.getValue().timestamp > + EmailConfig.CODE_EXPIRY_MINUTES * 60 * 1000) { + iterator.remove(); + } + } + } + + /** + * 前端接口-邮箱格式验证 + * @param email 待验证的邮箱地址 + * @return true表示邮箱格式正确,false表示邮箱格式错误 + */ + public static boolean isValidEmail(String email) { + if (email.isEmpty()) { + System.out.println("邮箱地址不能为空!"); + return false; + } + if (!(email.contains("@") && email.contains("."))) { + System.out.println("邮箱格式不正确!"); + return false; + } + return true; + } + + // 前端接口-发送验证码 + public static boolean sendCode(String email) { + // 发送真实邮件验证码 + String verificationCode = EmailService.generateVerificationCode(); + // 验证验证码是否成功发送 + if (!EmailService.sendVerificationCode(email, verificationCode)) { + return false; + } + return true; + } + + + +} diff --git a/src/main/java/com/ybw/mathapp/util/Login.java b/src/main/java/com/ybw/mathapp/util/Login.java new file mode 100644 index 0000000..83f18c0 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/util/Login.java @@ -0,0 +1,16 @@ +package com.ybw.mathapp.util; + +public class Login { + + // 前端接口-登录成功or失败 + public static boolean login(String name, String password) { + if (LoginFileUtils.validateUser(name, password)) { + System.out.println("登录成功!欢迎回来," + name); + return true; + } else { + System.out.println("邮箱或密码错误!"); + return false; + } + } + +} diff --git a/src/main/java/com/ybw/mathapp/util/LoginFileUtils.java b/src/main/java/com/ybw/mathapp/util/LoginFileUtils.java new file mode 100644 index 0000000..830fad2 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/util/LoginFileUtils.java @@ -0,0 +1,95 @@ +package com.ybw.mathapp.util; + +import com.ybw.mathapp.entity.User; +import java.io.*; +import java.util.ArrayList; +import java.util.List; + +public class LoginFileUtils { + static final String USER_FILE = "users.txt"; + + // 读取所有用户 + public static List readUsers() { + List users = new ArrayList<>(); + File file = new File(USER_FILE); + + if (!file.exists()) { + try { + file.createNewFile(); + } catch (IOException e) { + System.err.println("创建用户文件失败: " + e.getMessage()); + } + return users; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + + User user = User.fromString(line); + if (user != null) { + users.add(user); + } + } + } catch (IOException e) { + System.err.println("读取用户文件失败: " + e.getMessage()); + } + return users; + } + + // 保存用户到文件 + public static void saveUser(User user) { + try (PrintWriter writer = new PrintWriter(new FileWriter(USER_FILE, true))) { + writer.println(user.toString()); + } catch (IOException e) { + System.err.println("保存用户信息失败: " + e.getMessage()); + } + } + + // 前端接口-检查邮箱是否已注册 + public static boolean isEmailRegistered(String email) { + List users = readUsers(); + for (User user : users) { + if (user.getEmail().equalsIgnoreCase(email)) { + return true; + } + } + return false; + } + + // 前端接口-检查用户名是否已注册 + public static boolean isNameRegistered(String name) { + List users = readUsers(); + for (User user : users) { + if (user.getName().equalsIgnoreCase(name)) { + return true; + } + } + return false; + } + + // 前端接口-验证用户登录 + public static boolean validateUser(String emailOrName, String password) { + List users = readUsers(); + for (User user : users) { + if ((user.getEmail().equalsIgnoreCase(emailOrName) + || user.getName().equalsIgnoreCase(emailOrName) ) + && user.getPassword().equals(password)) { + return true; + } + } + return false; + } + // 前端接口-通过邮箱查找用户名 + public static String emailFindName (String email) { + List users = readUsers(); + for (User user : users) { + if (user.getEmail().equalsIgnoreCase(email)) { + return user.getName(); + } + } + return null; + } +} diff --git a/src/main/java/com/ybw/mathapp/util/Register.java b/src/main/java/com/ybw/mathapp/util/Register.java new file mode 100644 index 0000000..5085316 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/util/Register.java @@ -0,0 +1,44 @@ +package com.ybw.mathapp.util; + +import com.ybw.mathapp.entity.User; +import java.util.regex.Pattern; + +public class Register { + + // 前端接口-完成注册 + public static boolean register(String name, String email, String password1) { + User user = new User(name, email, password1); + LoginFileUtils.saveUser(user); + System.out.println("注册成功!您可以使用邮箱和密码登录了。"); + return true; + } + + /** + * 前端接口-密码格式验证 + * @param password1 第一次输入的密码 + * @return true表示符合要求,false表示不符合 + */ + public static boolean isVaildPassword(String password1) { + // 使用正则表达式验证:长度6-10,只包含字母数字,且包含大小写字母和数字 + String regex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{6,10}$"; + if (!Pattern.matches(regex, password1)) { + return false; + } + return true; + } + + /** + * 前端接口-两次密码匹配 + * @param password1 第一次输入的密码 + * @param password2 第二次输入的密码 + * @return true表示符合要求,false表示不符合 + */ + public static boolean isEqualPassword(String password1, String password2) { + if (!password1.equals(password2)) { + System.out.println("两次输入的密码不一致!"); + return false; + } + return true; + } + +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 2b5b80f..1b7863b 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,16 +1,10 @@ module com.ybw.mathapp { requires javafx.controls; requires javafx.fxml; - requires javafx.web; + requires javafx.base; + requires javafx.graphics; + requires jakarta.mail; - requires org.controlsfx.controls; - requires com.dlsc.formsfx; - requires net.synedra.validatorfx; - requires org.kordamp.ikonli.javafx; - requires org.kordamp.bootstrapfx.core; - requires eu.hansolo.tilesfx; - requires com.almasb.fxgl.all; - - opens com.ybw.mathapp to javafx.fxml; - exports com.ybw.mathapp; + opens com.wsf.mathapp to javafx.fxml; + exports com.wsf.mathapp; } \ No newline at end of file diff --git a/src/main/resources/META-INF/javamail.default.address.map b/src/main/resources/META-INF/javamail.default.address.map new file mode 100644 index 0000000..e3baea0 --- /dev/null +++ b/src/main/resources/META-INF/javamail.default.address.map @@ -0,0 +1,2 @@ +rfc822=smtp +smtp=smtp \ No newline at end of file diff --git a/src/main/resources/com/ybw/mathapp/hello-view.fxml b/src/main/resources/com/ybw/mathapp/hello-view.fxml deleted file mode 100644 index 11142e0..0000000 --- a/src/main/resources/com/ybw/mathapp/hello-view.fxml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - -