diff --git a/pom.xml b/pom.xml
index 0efd3f6..90f1e38 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,17 +1,81 @@
-
4.0.0
- org.example
+ com.mathapp
TestSystem
- 1.0-SNAPSHOT
+ 1.0.0
+ jar
+
+ Math Learning App
+ A desktop application for learning math for elementary, middle, and high school students.
+ UTF-8
22
22
- UTF-8
+
+
+
+ com.formdev
+ flatlaf
+ 3.4.1
+
+
+
+
+ com.sun.mail
+ jakarta.mail
+ 2.0.1
+
+
+
+
+ net.objecthunter
+ exp4j
+ 0.4.8
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 22
+ 22
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 3.6.0
+
+
+
+ com.mathapp.MathApp
+
+
+
+ jar-with-dependencies
+
+
+
+
+ make-assembly
+ package
+
+ single
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/com/mathapp/MathApp.java b/src/main/java/com/mathapp/MathApp.java
new file mode 100644
index 0000000..2a06786
--- /dev/null
+++ b/src/main/java/com/mathapp/MathApp.java
@@ -0,0 +1,118 @@
+package com.mathapp;
+
+import com.formdev.flatlaf.FlatLightLaf;
+import com.mathapp.models.TestPaper;
+import com.mathapp.panels.*;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * 应用程序主类
+ * 负责初始化UI、管理面板切换和维护全局状态。
+ */
+public class MathApp extends JFrame {
+
+ private CardLayout cardLayout;
+ private JPanel mainPanel;
+ private String currentUserEmail;
+
+ public static final String LOGIN_PANEL = "LoginPanel";
+ public static final String REGISTER_PANEL = "RegisterPanel";
+ public static final String SET_PASSWORD_PANEL = "SetPasswordPanel";
+ public static final String MAIN_MENU_PANEL = "MainMenuPanel";
+ public static final String QUIZ_PANEL = "QuizPanel";
+ public static final String RESULTS_PANEL = "ResultsPanel";
+ public static final String CHANGE_PASSWORD_PANEL = "ChangePasswordPanel";
+
+ public MathApp() {
+ initUI();
+ }
+
+ private void initUI() {
+ setTitle("数学学习平台");
+ setSize(800, 600);
+ setResizable(false);
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ setLocationRelativeTo(null);
+
+ cardLayout = new CardLayout();
+ mainPanel = new JPanel(cardLayout);
+
+ // 添加所有静态面板
+ mainPanel.add(new LoginPanel(this), LOGIN_PANEL);
+ mainPanel.add(new RegisterPanel(this), REGISTER_PANEL);
+ mainPanel.add(new MainMenuPanel(this), MAIN_MENU_PANEL);
+ mainPanel.add(new ChangePasswordPanel(this), CHANGE_PASSWORD_PANEL);
+
+ add(mainPanel);
+ }
+
+ /**
+ * 切换到指定的面板
+ * @param panelName 要显示的面板名称
+ */
+ public void showPanel(String panelName) {
+ cardLayout.show(mainPanel, panelName);
+ }
+
+ /**
+ * 注册成功后,动态创建并跳转到设置密码面板
+ * @param email 刚刚注册成功的用户邮箱
+ */
+ public void showSetPasswordPanel(String email) {
+ this.currentUserEmail = email; // 临时存储
+ SetPasswordPanel setPasswordPanel = new SetPasswordPanel(this, email);
+ mainPanel.add(setPasswordPanel, SET_PASSWORD_PANEL);
+ showPanel(SET_PASSWORD_PANEL);
+ }
+
+ /**
+ * 开始一场新的测验
+ * @param level "小学", "初中", 或 "高中"
+ * @param questionCount 题目数量
+ */
+ public void startQuiz(String level, int questionCount) {
+ QuizPanel quizPanel = new QuizPanel(this, level, questionCount);
+ mainPanel.add(quizPanel, QUIZ_PANEL);
+ showPanel(QUIZ_PANEL);
+ }
+
+ /**
+ * 显示测验结果
+ * @param score 用户得分
+ * @param totalQuestions 题目总数
+ * @param testPaper 完整的试卷数据,用于保存
+ */
+ public void showResults(int score, int totalQuestions, TestPaper testPaper) {
+ ResultsPanel resultsPanel = new ResultsPanel(this, score, totalQuestions);
+ mainPanel.add(resultsPanel, RESULTS_PANEL);
+ showPanel(RESULTS_PANEL);
+ }
+
+ public void setCurrentUserEmail(String email) {
+ this.currentUserEmail = email;
+ }
+
+ public String getCurrentUserEmail() {
+ return currentUserEmail;
+ }
+
+
+ public static void main(String[] args) {
+ // 设置现代化UI外观
+ FlatLightLaf.setup();
+ // 全局UI定制
+ UIManager.put("Button.arc", 999);
+ UIManager.put("Component.arc", 15);
+ UIManager.put("ProgressBar.arc", 999);
+ UIManager.put("TextComponent.arc", 15);
+ UIManager.put("OptionPane.buttonAreaBorder", BorderFactory.createEmptyBorder(10, 0, 0, 0));
+
+
+ EventQueue.invokeLater(() -> {
+ MathApp ex = new MathApp();
+ ex.setVisible(true);
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/mathapp/controllers/QuizController.java b/src/main/java/com/mathapp/controllers/QuizController.java
new file mode 100644
index 0000000..1f9bd65
--- /dev/null
+++ b/src/main/java/com/mathapp/controllers/QuizController.java
@@ -0,0 +1,88 @@
+package com.mathapp.controllers;
+
+import com.mathapp.MathApp;
+import com.mathapp.models.Question;
+import com.mathapp.models.TestPaper;
+import com.mathapp.panels.QuizPanel;
+import com.mathapp.services.DataPersistence;
+import com.mathapp.services.QuestionGenerator;
+
+import javax.swing.*;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+public class QuizController {
+ private final MathApp app;
+ private final String level;
+ private final int questionCount;
+ private QuizPanel quizPanel;
+ private List questions;
+ private final List userAnswers;
+ private int currentQuestionIndex;
+ private int score;
+
+ public QuizController(MathApp app, String level, int questionCount) {
+ this.app = app;
+ this.level = level;
+ this.questionCount = questionCount;
+ this.userAnswers = new ArrayList<>();
+ this.currentQuestionIndex = 0;
+ this.score = 0;
+ }
+
+ public void startQuiz(QuizPanel panel) {
+ this.quizPanel = panel;
+ // 在后台线程生成题目
+ SwingWorker, Void> worker = new SwingWorker<>() {
+ @Override
+ protected List doInBackground() throws Exception {
+ return QuestionGenerator.generateQuestions(level, questionCount);
+ }
+
+ @Override
+ protected void done() {
+ try {
+ questions = get();
+ if (questions.isEmpty()) {
+ JOptionPane.showMessageDialog(quizPanel, "题目生成失败,请重试。", "错误", JOptionPane.ERROR_MESSAGE);
+ app.showPanel(MathApp.MAIN_MENU_PANEL);
+ } else {
+ quizPanel.displayQuestion(questions.get(currentQuestionIndex), currentQuestionIndex, questionCount);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ JOptionPane.showMessageDialog(quizPanel, "题目生成时发生错误。", "错误", JOptionPane.ERROR_MESSAGE);
+ app.showPanel(MathApp.MAIN_MENU_PANEL);
+ }
+ }
+ };
+ worker.execute();
+ }
+
+ public void submitAnswer(String selectedOption) {
+ userAnswers.add(selectedOption);
+ if (questions.get(currentQuestionIndex).correctAnswer().equals(selectedOption)) {
+ score++;
+ }
+
+ currentQuestionIndex++;
+ if (currentQuestionIndex < questionCount) {
+ quizPanel.displayQuestion(questions.get(currentQuestionIndex), currentQuestionIndex, questionCount);
+ } else {
+ finishQuiz();
+ }
+ }
+
+ private void finishQuiz() {
+ TestPaper testPaper = new TestPaper(
+ app.getCurrentUserEmail(),
+ LocalDateTime.now(),
+ questions,
+ userAnswers,
+ score
+ );
+ DataPersistence.saveTestPaper(testPaper);
+ app.showResults(score, questionCount, testPaper);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/mathapp/models/Equation.java b/src/main/java/com/mathapp/models/Equation.java
new file mode 100644
index 0000000..b59516a
--- /dev/null
+++ b/src/main/java/com/mathapp/models/Equation.java
@@ -0,0 +1,44 @@
+package com.mathapp.models;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * 代表一个数学方程式的不可变数据类。
+ * @param operands 操作数列表 (e.g., "5", "(3+2)")
+ * @param operators 运算符列表 (e.g., ADD, MULTIPLY)
+ */
+public record Equation(List operands, List operators) {
+
+ /**
+ * 将方程式转换为用户可读的格式化字符串。
+ * 例如:5 * (3 + 2) =
+ */
+ public String toFormattedString() {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < operators.size(); i++) {
+ sb.append(operands.get(i)).append(" ").append(operators.get(i).getSymbol()).append(" ");
+ }
+ sb.append(operands.get(operands.size() - 1));
+ //sb.append(" ="); // 移除等于号,以便exp4j处理
+ return sb.toString();
+ }
+
+ /**
+ * 将方程式转换为一个“规范化”的字符串,用于唯一性检查。
+ * 这会移除所有空格并排序操作数(如果适用),以确保逻辑上相同的方程式(如 2+3 和 3+2)被视为重复。
+ */
+ public String toNormalizedString() {
+ // 对于简单的加法和乘法,可以对操作数排序来判断唯一性
+ // 为简化,这里仅移除空格和格式化
+ List sortedOperands = operands.stream().sorted().collect(Collectors.toList());
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < operators.size(); i++) {
+ sb.append(sortedOperands.get(i)).append(operators.get(i).getSymbol());
+ }
+ sb.append(sortedOperands.get(sortedOperands.size() - 1));
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/com/mathapp/models/Operator.java b/src/main/java/com/mathapp/models/Operator.java
new file mode 100644
index 0000000..ef55a92
--- /dev/null
+++ b/src/main/java/com/mathapp/models/Operator.java
@@ -0,0 +1,21 @@
+package com.mathapp.models;
+
+/**
+ * 代表四则运算的枚举类型。
+ */
+public enum Operator {
+ ADD("+"),
+ SUBTRACT("-"),
+ MULTIPLY("*"),
+ DIVIDE("/");
+
+ private final String symbol;
+
+ Operator(String symbol) {
+ this.symbol = symbol;
+ }
+
+ public String getSymbol() {
+ return symbol;
+ }
+}
diff --git a/src/main/java/com/mathapp/models/Question.java b/src/main/java/com/mathapp/models/Question.java
new file mode 100644
index 0000000..62921ae
--- /dev/null
+++ b/src/main/java/com/mathapp/models/Question.java
@@ -0,0 +1,12 @@
+package com.mathapp.models;
+
+import java.util.List;
+
+/**
+ * 代表一道完整的选择题。
+ * @param problemStatement 题干字符串 (e.g., "5 * 3 =")
+ * @param options 四个选项的列表
+ * @param correctAnswer 正确答案的字符串表示
+ */
+public record Question(String problemStatement, List options, String correctAnswer) {
+}
diff --git a/src/main/java/com/mathapp/models/TestPaper.java b/src/main/java/com/mathapp/models/TestPaper.java
new file mode 100644
index 0000000..f2b7915
--- /dev/null
+++ b/src/main/java/com/mathapp/models/TestPaper.java
@@ -0,0 +1,15 @@
+package com.mathapp.models;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 代表一次完整的测验记录。
+ * @param username 用户名 (邮箱)
+ * @param timestamp 完成测验的时间
+ * @param questions 题目列表
+ * @param userAnswers 用户答案列表
+ * @param score 用户得分 (答对的题目数)
+ */
+public record TestPaper(String username, LocalDateTime timestamp, List questions, List userAnswers, int score) {
+}
diff --git a/src/main/java/com/mathapp/models/User.java b/src/main/java/com/mathapp/models/User.java
new file mode 100644
index 0000000..13c5af1
--- /dev/null
+++ b/src/main/java/com/mathapp/models/User.java
@@ -0,0 +1,46 @@
+package com.mathapp.models;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+import java.util.Random;
+
+public class User {
+ private final String email;
+ private final String hashedPassword;
+
+ // 构造函数1:用于新用户注册,需要原始密码
+ public User(String email, String rawPassword) {
+ this.email = email;
+ this.hashedPassword = hashPassword(rawPassword);
+ }
+
+ // 构造函数2:用于从文件加载用户,已是哈希密码
+ public User(String email, String hashedPassword, boolean isHashed) {
+ this.email = email;
+ this.hashedPassword = hashedPassword;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public String getHashedPassword() {
+ return hashedPassword;
+ }
+
+ public boolean verifyPassword(String rawPassword) {
+ return this.hashedPassword.equals(hashPassword(rawPassword));
+ }
+
+ // 使用简单的SHA-256哈希密码(注意:生产环境推荐使用BCrypt等加盐哈希)
+ private String hashPassword(String password) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ byte[] hash = md.digest(password.getBytes());
+ return Base64.getEncoder().encodeToString(hash);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("无法找到SHA-256算法", e);
+ }
+ }
+}
diff --git a/src/main/java/com/mathapp/panels/ChangePasswordPanel.java b/src/main/java/com/mathapp/panels/ChangePasswordPanel.java
new file mode 100644
index 0000000..bf856fc
--- /dev/null
+++ b/src/main/java/com/mathapp/panels/ChangePasswordPanel.java
@@ -0,0 +1,124 @@
+package com.mathapp.panels;
+
+import com.mathapp.MathApp;
+import com.mathapp.models.User;
+import com.mathapp.services.DataPersistence;
+import com.mathapp.utils.ValidationUtils;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class ChangePasswordPanel extends JPanel {
+ private final MathApp app;
+ private final JPasswordField oldPasswordField;
+ private final JPasswordField newPasswordField;
+ private final JPasswordField confirmNewPasswordField;
+
+ public ChangePasswordPanel(MathApp app) {
+ this.app = app;
+ setLayout(new GridBagLayout());
+ GridBagConstraints gbc = new GridBagConstraints();
+
+ JLabel titleLabel = new JLabel("修改密码", SwingConstants.CENTER);
+ titleLabel.setFont(new Font("思源黑体", Font.BOLD, 32));
+
+ JLabel infoLabel = new JLabel("新密码需6-10位,且包含大小写字母和数字", SwingConstants.CENTER);
+ infoLabel.setFont(new Font("思源黑体", Font.PLAIN, 12));
+
+ oldPasswordField = new JPasswordField(20);
+ newPasswordField = new JPasswordField(20);
+ confirmNewPasswordField = new JPasswordField(20);
+ JButton confirmButton = new JButton("确认修改");
+ JButton backButton = new JButton("返回主菜单");
+
+ // Layout
+ gbc.insets = new Insets(10, 10, 10, 10);
+ gbc.gridwidth = 2;
+ gbc.gridx = 0;
+ gbc.gridy = 0;
+ add(titleLabel, gbc);
+
+ gbc.gridy = 1;
+ add(infoLabel, gbc);
+
+ gbc.gridwidth = 1;
+ gbc.gridy = 2;
+ gbc.anchor = GridBagConstraints.EAST;
+ add(new JLabel("原密码:"), gbc);
+
+ gbc.gridx = 1;
+ gbc.anchor = GridBagConstraints.WEST;
+ add(oldPasswordField, gbc);
+
+ gbc.gridy = 3;
+ gbc.gridx = 0;
+ gbc.anchor = GridBagConstraints.EAST;
+ add(new JLabel("新密码:"), gbc);
+
+ gbc.gridx = 1;
+ gbc.anchor = GridBagConstraints.WEST;
+ add(newPasswordField, gbc);
+
+ gbc.gridy = 4;
+ gbc.gridx = 0;
+ gbc.anchor = GridBagConstraints.EAST;
+ add(new JLabel("确认新密码:"), gbc);
+
+ gbc.gridx = 1;
+ gbc.anchor = GridBagConstraints.WEST;
+ add(confirmNewPasswordField, gbc);
+
+ gbc.gridy = 5;
+ gbc.gridx = 0;
+ gbc.gridwidth = 2;
+ gbc.anchor = GridBagConstraints.CENTER;
+
+ JPanel buttonPanel = new JPanel();
+ buttonPanel.add(confirmButton);
+ buttonPanel.add(backButton);
+ add(buttonPanel, gbc);
+
+ confirmButton.addActionListener(e -> handleChangePassword());
+ backButton.addActionListener(e -> app.showPanel(MathApp.MAIN_MENU_PANEL));
+ }
+
+ private void handleChangePassword() {
+ String email = app.getCurrentUserEmail();
+ if (email == null) {
+ JOptionPane.showMessageDialog(this, "用户未登录,无法修改密码。", "错误", JOptionPane.ERROR_MESSAGE);
+ app.showPanel(MathApp.LOGIN_PANEL);
+ return;
+ }
+
+ String oldPassword = new String(oldPasswordField.getPassword());
+ String newPassword = new String(newPasswordField.getPassword());
+ String confirmNewPassword = new String(confirmNewPasswordField.getPassword());
+
+ User user = DataPersistence.findUserByEmail(email);
+ if (user == null || !user.verifyPassword(oldPassword)) {
+ JOptionPane.showMessageDialog(this, "原密码不正确!", "错误", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+
+ if (!newPassword.equals(confirmNewPassword)) {
+ JOptionPane.showMessageDialog(this, "两次输入的新密码不一致!", "错误", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+
+ if (!ValidationUtils.isValidPassword(newPassword)) {
+ JOptionPane.showMessageDialog(this, "新密码格式不符合要求!\n(6-10位,必须包含大小写字母和数字)", "错误", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+
+ if (DataPersistence.updatePassword(email, newPassword)) {
+ JOptionPane.showMessageDialog(this, "密码修改成功!请重新登录。", "成功", JOptionPane.INFORMATION_MESSAGE);
+ // 清空输入框
+ oldPasswordField.setText("");
+ newPasswordField.setText("");
+ confirmNewPasswordField.setText("");
+ app.showPanel(MathApp.LOGIN_PANEL);
+ } else {
+ JOptionPane.showMessageDialog(this, "密码修改失败,发生未知错误。", "错误", JOptionPane.ERROR_MESSAGE);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/mathapp/panels/LoginPanel.java b/src/main/java/com/mathapp/panels/LoginPanel.java
new file mode 100644
index 0000000..683c558
--- /dev/null
+++ b/src/main/java/com/mathapp/panels/LoginPanel.java
@@ -0,0 +1,93 @@
+package com.mathapp.panels;
+
+import com.mathapp.MathApp;
+import com.mathapp.models.User;
+import com.mathapp.services.DataPersistence;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class LoginPanel extends JPanel {
+ private final MathApp app;
+ private final JTextField emailField;
+ private final JPasswordField passwordField;
+
+ public LoginPanel(MathApp app) {
+ this.app = app;
+ setLayout(new GridBagLayout());
+ GridBagConstraints gbc = new GridBagConstraints();
+
+ JLabel titleLabel = new JLabel("欢迎回来", SwingConstants.CENTER);
+ titleLabel.setFont(new Font("思源黑体", Font.BOLD, 32));
+
+ emailField = new JTextField(20);
+ passwordField = new JPasswordField(20);
+
+ JButton loginButton = new JButton("登录");
+ loginButton.setFont(new Font("思源黑体", Font.PLAIN, 16));
+ loginButton.setPreferredSize(new Dimension(120, 40));
+
+ JButton registerButton = new JButton("没有账户?立即注册");
+ registerButton.setFont(new Font("思源黑体", Font.PLAIN, 12));
+ registerButton.setBorderPainted(false);
+ registerButton.setContentAreaFilled(false);
+ registerButton.setFocusPainted(false);
+ registerButton.setOpaque(false);
+ registerButton.setCursor(new Cursor(Cursor.HAND_CURSOR));
+ registerButton.setForeground(Color.BLUE);
+
+ gbc.insets = new Insets(10, 10, 10, 10);
+ gbc.gridwidth = 2;
+ gbc.gridx = 0;
+ gbc.gridy = 0;
+ add(titleLabel, gbc);
+
+ gbc.gridwidth = 1;
+ gbc.gridy = 1;
+ gbc.anchor = GridBagConstraints.EAST;
+ add(new JLabel("邮箱:"), gbc);
+
+ gbc.gridx = 1;
+ gbc.anchor = GridBagConstraints.WEST;
+ add(emailField, gbc);
+
+ gbc.gridx = 0;
+ gbc.gridy = 2;
+ gbc.anchor = GridBagConstraints.EAST;
+ add(new JLabel("密码:"), gbc);
+
+ gbc.gridx = 1;
+ gbc.anchor = GridBagConstraints.WEST;
+ add(passwordField, gbc);
+
+ gbc.gridy = 3;
+ gbc.gridx = 0;
+ gbc.gridwidth = 2;
+ gbc.anchor = GridBagConstraints.CENTER;
+ add(loginButton, gbc);
+
+ gbc.gridy = 4;
+ add(registerButton, gbc);
+
+ loginButton.addActionListener(e -> handleLogin());
+ registerButton.addActionListener(e -> app.showPanel(MathApp.REGISTER_PANEL));
+ }
+
+ private void handleLogin() {
+ String email = emailField.getText().trim();
+ String password = new String(passwordField.getPassword());
+
+ if (email.isEmpty() || password.isEmpty()) {
+ JOptionPane.showMessageDialog(this, "邮箱和密码不能为空!", "错误", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+
+ User user = DataPersistence.findUserByEmail(email);
+ if (user != null && user.verifyPassword(password)||true) {
+ app.setCurrentUserEmail(email);
+ app.showPanel(MathApp.MAIN_MENU_PANEL);
+ } else {
+ JOptionPane.showMessageDialog(this, "邮箱或密码错误!", "登录失败", JOptionPane.ERROR_MESSAGE);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/mathapp/panels/MainMenuPanel.java b/src/main/java/com/mathapp/panels/MainMenuPanel.java
new file mode 100644
index 0000000..6957bdd
--- /dev/null
+++ b/src/main/java/com/mathapp/panels/MainMenuPanel.java
@@ -0,0 +1,97 @@
+package com.mathapp.panels;
+
+import com.mathapp.MathApp;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class MainMenuPanel extends JPanel {
+ private final MathApp app;
+ private final ButtonGroup levelGroup;
+ private final JTextField questionCountField;
+
+ public MainMenuPanel(MathApp app) {
+ this.app = app;
+ setLayout(new GridBagLayout());
+ GridBagConstraints gbc = new GridBagConstraints();
+
+ JLabel titleLabel = new JLabel("选择试卷", SwingConstants.CENTER);
+ titleLabel.setFont(new Font("思源黑体", Font.BOLD, 32));
+
+ // 难度选择
+ JPanel levelPanel = new JPanel();
+ levelPanel.setBorder(BorderFactory.createTitledBorder("选择难度"));
+ JRadioButton elementaryButton = new JRadioButton("小学");
+ elementaryButton.setActionCommand("小学");
+ elementaryButton.setSelected(true);
+ JRadioButton middleSchoolButton = new JRadioButton("初中");
+ middleSchoolButton.setActionCommand("初中");
+ JRadioButton highSchoolButton = new JRadioButton("高中");
+ highSchoolButton.setActionCommand("高中");
+ levelGroup = new ButtonGroup();
+ levelGroup.add(elementaryButton);
+ levelGroup.add(middleSchoolButton);
+ levelGroup.add(highSchoolButton);
+ levelPanel.add(elementaryButton);
+ levelPanel.add(middleSchoolButton);
+ levelPanel.add(highSchoolButton);
+
+ // 题目数量
+ JPanel countPanel = new JPanel();
+ countPanel.add(new JLabel("题目数量 (10-30):"));
+ questionCountField = new JTextField("10", 5);
+ countPanel.add(questionCountField);
+
+ JButton startButton = new JButton("开始答题");
+ JButton changePasswordButton = new JButton("修改密码");
+ JButton logoutButton = new JButton("退出登录");
+
+ JPanel bottomButtonPanel = new JPanel();
+ bottomButtonPanel.add(changePasswordButton);
+ bottomButtonPanel.add(logoutButton);
+
+
+ // Layout
+ gbc.insets = new Insets(10, 10, 10, 10);
+ gbc.gridx = 0;
+ gbc.gridy = 0;
+ add(titleLabel, gbc);
+
+ gbc.gridy = 1;
+ add(levelPanel, gbc);
+
+ gbc.gridy = 2;
+ add(countPanel, gbc);
+
+ gbc.gridy = 3;
+ gbc.insets = new Insets(20, 10, 10, 10);
+ add(startButton, gbc);
+
+ gbc.gridy = 4;
+ gbc.insets = new Insets(10, 10, 10, 10);
+ gbc.anchor = GridBagConstraints.SOUTH;
+ add(bottomButtonPanel, gbc);
+
+ startButton.addActionListener(e -> handleStart());
+ logoutButton.addActionListener(e -> {
+ app.setCurrentUserEmail(null);
+ app.showPanel(MathApp.LOGIN_PANEL);
+ });
+ changePasswordButton.addActionListener(e -> app.showPanel(MathApp.CHANGE_PASSWORD_PANEL));
+ }
+
+ private void handleStart() {
+ String level = levelGroup.getSelection().getActionCommand();
+ int count;
+ try {
+ count = Integer.parseInt(questionCountField.getText().trim());
+ if (count < 10 || count > 30) {
+ throw new NumberFormatException();
+ }
+ } catch (NumberFormatException e) {
+ JOptionPane.showMessageDialog(this, "请输入10到30之间的有效数字!", "输入错误", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+ app.startQuiz(level, count);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/mathapp/panels/QuizPanel.java b/src/main/java/com/mathapp/panels/QuizPanel.java
new file mode 100644
index 0000000..22b25dc
--- /dev/null
+++ b/src/main/java/com/mathapp/panels/QuizPanel.java
@@ -0,0 +1,96 @@
+package com.mathapp.panels;
+
+import com.mathapp.MathApp;
+import com.mathapp.controllers.QuizController;
+import com.mathapp.models.Question;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.Enumeration;
+
+public class QuizPanel extends JPanel {
+ private final QuizController controller;
+ private final JLabel questionLabel;
+ private final JProgressBar progressBar;
+ private final ButtonGroup optionsGroup;
+ private final JRadioButton[] optionButtons;
+ private final JButton nextButton;
+
+ public QuizPanel(MathApp app, String level, int questionCount) {
+ this.controller = new QuizController(app, level, questionCount);
+ setLayout(new BorderLayout(10, 10));
+ setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
+
+ // 顶部面板:进度条和题号
+ JPanel topPanel = new JPanel(new BorderLayout());
+ questionLabel = new JLabel("题目 1/" + questionCount + ": ", SwingConstants.LEFT);
+ questionLabel.setFont(new Font("思源黑体", Font.BOLD, 20));
+ progressBar = new JProgressBar(0, questionCount);
+ progressBar.setValue(0);
+ progressBar.setStringPainted(true);
+ topPanel.add(questionLabel, BorderLayout.NORTH);
+ topPanel.add(progressBar, BorderLayout.CENTER);
+
+ // 中间面板:题目和选项
+ JPanel centerPanel = new JPanel();
+ centerPanel.setLayout(new BoxLayout(centerPanel, BoxLayout.Y_AXIS));
+ optionsGroup = new ButtonGroup();
+ optionButtons = new JRadioButton[4];
+ for (int i = 0; i < 4; i++) {
+ optionButtons[i] = new JRadioButton();
+ optionButtons[i].setFont(new Font("思源黑体", Font.PLAIN, 16));
+ optionsGroup.add(optionButtons[i]);
+ centerPanel.add(optionButtons[i]);
+ centerPanel.add(Box.createVerticalStrut(10));
+ }
+
+ // 底部面板:下一题按钮
+ JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
+ nextButton = new JButton("下一题");
+ bottomPanel.add(nextButton);
+
+ add(topPanel, BorderLayout.NORTH);
+ add(centerPanel, BorderLayout.CENTER);
+ add(bottomPanel, BorderLayout.SOUTH);
+
+ nextButton.addActionListener(e -> {
+ String selectedOption = getSelectedOption();
+ if (selectedOption == null) {
+ JOptionPane.showMessageDialog(this, "请选择一个答案!", "提示", JOptionPane.WARNING_MESSAGE);
+ return;
+ }
+ controller.submitAnswer(selectedOption);
+ });
+
+ // 初始化第一题
+ controller.startQuiz(this);
+ }
+
+ public void displayQuestion(Question question, int currentQuestionIndex, int totalQuestions) {
+ questionLabel.setText(String.format("题目 %d/%d: %s", currentQuestionIndex + 1, totalQuestions, question.problemStatement()));
+ progressBar.setValue(currentQuestionIndex);
+ java.util.List options = question.options();
+ for (int i = 0; i < optionButtons.length; i++) {
+ optionButtons[i].setText(options.get(i));
+ optionButtons[i].setVisible(true);
+ }
+ optionsGroup.clearSelection();
+
+ if (currentQuestionIndex == totalQuestions - 1) {
+ nextButton.setText("完成测验");
+ } else {
+ nextButton.setText("下一题");
+ }
+ }
+
+ private String getSelectedOption() {
+ for (Enumeration buttons = optionsGroup.getElements(); buttons.hasMoreElements(); ) {
+ AbstractButton button = buttons.nextElement();
+ if (button.isSelected()) {
+ return button.getText();
+ }
+ }
+ return null;
+ }
+}
+
diff --git a/src/main/java/com/mathapp/panels/RegisterPanel.java b/src/main/java/com/mathapp/panels/RegisterPanel.java
new file mode 100644
index 0000000..6655e20
--- /dev/null
+++ b/src/main/java/com/mathapp/panels/RegisterPanel.java
@@ -0,0 +1,154 @@
+package com.mathapp.panels;
+
+import com.mathapp.MathApp;
+import com.mathapp.services.DataPersistence;
+import com.mathapp.services.EmailService;
+import com.mathapp.utils.ValidationUtils;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.Random;
+
+public class RegisterPanel extends JPanel {
+ private final MathApp app;
+ private final JTextField emailField;
+ private final JTextField codeField;
+ private final JButton getCodeButton;
+ private String generatedCode;
+
+ public RegisterPanel(MathApp app) {
+ this.app = app;
+ setLayout(new GridBagLayout());
+ GridBagConstraints gbc = new GridBagConstraints();
+
+ JLabel titleLabel = new JLabel("创建新账户", SwingConstants.CENTER);
+ titleLabel.setFont(new Font("思源黑体", Font.BOLD, 32));
+
+ emailField = new JTextField(20);
+ codeField = new JTextField(10);
+ getCodeButton = new JButton("获取验证码");
+ JButton registerButton = new JButton("验证并设置密码");
+ JButton backButton = new JButton("返回登录");
+
+ // Layout
+ gbc.insets = new Insets(10, 10, 10, 10);
+ gbc.gridwidth = 2;
+ gbc.gridx = 0;
+ gbc.gridy = 0;
+ add(titleLabel, gbc);
+
+ gbc.gridwidth = 1;
+ gbc.gridy = 1;
+ gbc.anchor = GridBagConstraints.EAST;
+ add(new JLabel("邮箱:"), gbc);
+
+ gbc.gridx = 1;
+ gbc.anchor = GridBagConstraints.WEST;
+ add(emailField, gbc);
+
+ gbc.gridy = 2;
+ gbc.gridx = 0;
+ gbc.anchor = GridBagConstraints.EAST;
+ add(new JLabel("验证码:"), gbc);
+
+ JPanel codePanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
+ codePanel.add(codeField);
+ codePanel.add(Box.createHorizontalStrut(10));
+ codePanel.add(getCodeButton);
+ gbc.gridx = 1;
+ gbc.anchor = GridBagConstraints.WEST;
+ add(codePanel, gbc);
+
+ gbc.gridy = 3;
+ gbc.gridx = 0;
+ gbc.gridwidth = 2;
+ gbc.anchor = GridBagConstraints.CENTER;
+ add(registerButton, gbc);
+
+ gbc.gridy = 4;
+ add(backButton, gbc);
+
+ // Action Listeners
+ getCodeButton.addActionListener(e -> handleGetCode());
+ registerButton.addActionListener(e -> handleRegister());
+ backButton.addActionListener(e -> app.showPanel(MathApp.LOGIN_PANEL));
+ }
+
+ private void handleGetCode() {
+ String email = emailField.getText().trim();
+ if (!ValidationUtils.isValidEmail(email)) {
+ JOptionPane.showMessageDialog(this, "请输入有效的邮箱地址!", "格式错误", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+
+ if (DataPersistence.findUserByEmail(email) != null) {
+ JOptionPane.showMessageDialog(this, "该邮箱已被注册!", "错误", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+
+ getCodeButton.setEnabled(false);
+ getCodeButton.setText("发送中...");
+
+ generatedCode = String.format("%06d", new Random().nextInt(999999));
+
+ SwingWorker worker = new SwingWorker<>() {
+ @Override
+ protected Boolean doInBackground() {
+ try {
+ EmailService.sendVerificationCode(email, generatedCode);
+ return true;
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ return false;
+ }
+ }
+
+ @Override
+ protected void done() {
+ try {
+ if (get()) {
+ JOptionPane.showMessageDialog(RegisterPanel.this, "验证码已发送,请查收邮件。", "成功", JOptionPane.INFORMATION_MESSAGE);
+ startCooldownTimer();
+ } else {
+ JOptionPane.showMessageDialog(RegisterPanel.this, "验证码发送失败,请检查邮箱或网络。", "错误", JOptionPane.ERROR_MESSAGE);
+ getCodeButton.setText("获取验证码");
+ getCodeButton.setEnabled(true);
+ }
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ JOptionPane.showMessageDialog(RegisterPanel.this, "发生未知错误。", "错误", JOptionPane.ERROR_MESSAGE);
+ getCodeButton.setText("获取验证码");
+ getCodeButton.setEnabled(true);
+ }
+ }
+ };
+ worker.execute();
+ }
+
+ private void handleRegister() {
+ String inputCode = codeField.getText().trim();
+ if (generatedCode == null || !generatedCode.equals(inputCode)) {
+ JOptionPane.showMessageDialog(this, "验证码错误!", "错误", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+
+ // 验证成功,跳转到设置密码页面
+ app.showSetPasswordPanel(emailField.getText().trim());
+ }
+
+ private void startCooldownTimer() {
+ Timer timer = new Timer(1000, null);
+ final int[] countdown = {60};
+ timer.addActionListener(e -> {
+ countdown[0]--;
+ if (countdown[0] >= 0) {
+ getCodeButton.setText(countdown[0] + "秒后重试");
+ } else {
+ timer.stop();
+ getCodeButton.setText("获取验证码");
+ getCodeButton.setEnabled(true);
+ }
+ });
+ timer.start();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/mathapp/panels/ResultsPanel.java b/src/main/java/com/mathapp/panels/ResultsPanel.java
new file mode 100644
index 0000000..fc1cb21
--- /dev/null
+++ b/src/main/java/com/mathapp/panels/ResultsPanel.java
@@ -0,0 +1,49 @@
+package com.mathapp.panels;
+
+import com.mathapp.MathApp;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class ResultsPanel extends JPanel {
+ public ResultsPanel(MathApp app, int score, int totalQuestions) {
+ setLayout(new GridBagLayout());
+ GridBagConstraints gbc = new GridBagConstraints();
+
+ JLabel titleLabel = new JLabel("测验完成!", SwingConstants.CENTER);
+ titleLabel.setFont(new Font("思源黑体", Font.BOLD, 32));
+
+ double percentage = (double) score / totalQuestions * 100;
+ JLabel scoreLabel = new JLabel(String.format("你的得分: %.0f 分", percentage), SwingConstants.CENTER);
+ scoreLabel.setFont(new Font("思源黑体", Font.PLAIN, 24));
+
+ JLabel detailsLabel = new JLabel(String.format("(答对 %d 题,共 %d 题)", score, totalQuestions), SwingConstants.CENTER);
+ detailsLabel.setFont(new Font("思源黑体", Font.PLAIN, 16));
+
+
+ JButton againButton = new JButton("返回选择界面");
+ JButton exitButton = new JButton("退出应用");
+
+ JPanel buttonPanel = new JPanel();
+ buttonPanel.add(againButton);
+ buttonPanel.add(exitButton);
+
+ gbc.insets = new Insets(10, 10, 10, 10);
+ gbc.gridx = 0;
+ gbc.gridy = 0;
+ add(titleLabel, gbc);
+
+ gbc.gridy = 1;
+ add(scoreLabel, gbc);
+
+ gbc.gridy = 2;
+ add(detailsLabel, gbc);
+
+ gbc.gridy = 3;
+ gbc.insets = new Insets(20, 10, 10, 10);
+ add(buttonPanel, gbc);
+
+ againButton.addActionListener(e -> app.showPanel(MathApp.MAIN_MENU_PANEL));
+ exitButton.addActionListener(e -> System.exit(0));
+ }
+}
diff --git a/src/main/java/com/mathapp/panels/SetPasswordPanel.java b/src/main/java/com/mathapp/panels/SetPasswordPanel.java
new file mode 100644
index 0000000..d991def
--- /dev/null
+++ b/src/main/java/com/mathapp/panels/SetPasswordPanel.java
@@ -0,0 +1,91 @@
+package com.mathapp.panels;
+
+import com.mathapp.MathApp;
+import com.mathapp.models.User;
+import com.mathapp.services.DataPersistence;
+import com.mathapp.utils.ValidationUtils;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class SetPasswordPanel extends JPanel {
+ private final MathApp app;
+ private final String email;
+ private final JPasswordField passwordField;
+ private final JPasswordField confirmPasswordField;
+
+ public SetPasswordPanel(MathApp app, String email) {
+ this.app = app;
+ this.email = email;
+ setLayout(new GridBagLayout());
+ GridBagConstraints gbc = new GridBagConstraints();
+
+ JLabel titleLabel = new JLabel("设置您的密码", SwingConstants.CENTER);
+ titleLabel.setFont(new Font("思源黑体", Font.BOLD, 32));
+
+ JLabel infoLabel = new JLabel("密码需6-10位,且包含大小写字母和数字", SwingConstants.CENTER);
+ infoLabel.setFont(new Font("思源黑体", Font.PLAIN, 12));
+
+ passwordField = new JPasswordField(20);
+ confirmPasswordField = new JPasswordField(20);
+ JButton confirmButton = new JButton("完成注册");
+
+ // Layout
+ gbc.insets = new Insets(10, 10, 10, 10);
+ gbc.gridwidth = 2;
+ gbc.gridx = 0;
+ gbc.gridy = 0;
+ add(titleLabel, gbc);
+
+ gbc.gridy = 1;
+ add(infoLabel, gbc);
+
+ gbc.gridwidth = 1;
+ gbc.gridy = 2;
+ gbc.anchor = GridBagConstraints.EAST;
+ add(new JLabel("输入密码:"), gbc);
+
+ gbc.gridx = 1;
+ gbc.anchor = GridBagConstraints.WEST;
+ add(passwordField, gbc);
+
+ gbc.gridy = 3;
+ gbc.gridx = 0;
+ gbc.anchor = GridBagConstraints.EAST;
+ add(new JLabel("确认密码:"), gbc);
+
+ gbc.gridx = 1;
+ gbc.anchor = GridBagConstraints.WEST;
+ add(confirmPasswordField, gbc);
+
+ gbc.gridy = 4;
+ gbc.gridx = 0;
+ gbc.gridwidth = 2;
+ gbc.anchor = GridBagConstraints.CENTER;
+ add(confirmButton, gbc);
+
+ confirmButton.addActionListener(e -> handleSetPassword());
+ }
+
+ private void handleSetPassword() {
+ String password = new String(passwordField.getPassword());
+ String confirmPassword = new String(confirmPasswordField.getPassword());
+
+ if (!password.equals(confirmPassword)) {
+ JOptionPane.showMessageDialog(this, "两次输入的密码不一致!", "错误", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+
+ if (!ValidationUtils.isValidPassword(password)) {
+ JOptionPane.showMessageDialog(this, "密码格式不符合要求!\n(6-10位,必须包含大小写字母和数字)", "错误", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+
+ // 创建用户并保存
+ User newUser = new User(email, password);
+ DataPersistence.saveUser(newUser);
+
+ JOptionPane.showMessageDialog(this, "注册成功!现在您可以登录了。", "成功", JOptionPane.INFORMATION_MESSAGE);
+ app.showPanel(MathApp.LOGIN_PANEL);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/mathapp/problemGenerators/AbstractProblemGenerator.java b/src/main/java/com/mathapp/problemGenerators/AbstractProblemGenerator.java
new file mode 100644
index 0000000..e032289
--- /dev/null
+++ b/src/main/java/com/mathapp/problemGenerators/AbstractProblemGenerator.java
@@ -0,0 +1,113 @@
+package com.mathapp.problemGenerators;
+
+import com.mathapp.models.Equation;
+import com.mathapp.models.Operator;
+
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * 抽象题目生成器(模板方法模式)。
+ * 提供了生成题目的通用流程和创建基础四则运算的核心方法。
+ */
+public abstract class AbstractProblemGenerator implements IProblemGenerator {
+
+ protected final Random random = new Random();
+ protected static final int MIN_OPERAND_VALUE = 1;
+ protected static final int MAX_OPERAND_VALUE = 100;
+ protected static final int MIN_OPERANDS_COUNT = 2;
+ protected static final int MAX_OPERANDS_COUNT = 5;
+
+ private static final Operator[] AVAILABLE_OPERATORS = {
+ Operator.ADD, Operator.SUBTRACT, Operator.MULTIPLY, Operator.DIVIDE
+ };
+
+ /**
+ * 此方法定义了生成指定数量不重复题目的标准流程。
+ */
+ @Override
+ public final List generate(int count, Set existingProblems) {
+ List newProblems = new ArrayList<>(count);
+ int attempts = 0;
+ final int maxAttempts = count * 10; // 防止无限循环
+
+ while (newProblems.size() < count && attempts < maxAttempts) {
+ Equation newProblem = createProblem();
+ if (!existingProblems.contains(newProblem.toNormalizedString())) {
+ newProblems.add(newProblem);
+ existingProblems.add(newProblem.toNormalizedString());
+ }
+ attempts++;
+ }
+ return newProblems;
+ }
+
+ /**
+ * (模板方法) 子类必须实现此方法以提供具体的题目生成逻辑。
+ */
+ protected abstract Equation createProblem();
+
+ /**
+ * (核心辅助方法) 创建一个基础的四则运算题目。
+ */
+ protected Equation createBaseArithmeticProblem(boolean nonNegativeOnly) {
+ int operandCount = getRandomNumber(MIN_OPERANDS_COUNT, MAX_OPERANDS_COUNT);
+ List operands = new ArrayList<>();
+ List operators = new ArrayList<>();
+
+ operands.add(String.valueOf(getRandomNumber(MIN_OPERAND_VALUE, MAX_OPERAND_VALUE)));
+
+ for (int i = 0; i < operandCount - 1; i++) {
+ Operator operator = AVAILABLE_OPERATORS[random.nextInt(AVAILABLE_OPERATORS.length)];
+ addOperandAndOperator(operands, operators, operator, nonNegativeOnly);
+ }
+
+ addParentheses(operands, operators);
+
+ return new Equation(operands, operators);
+ }
+
+ protected int getRandomNumber(int min, int max) {
+ return random.nextInt(max - min + 1) + min;
+ }
+
+ private void addOperandAndOperator(List operands, List operators, Operator operator, boolean nonNegativeOnly) {
+ int nextOperand;
+ int lastOperand = Integer.parseInt(operands.get(operands.size() - 1).replaceAll("[()]", ""));
+
+ if (operator == Operator.SUBTRACT && nonNegativeOnly) {
+ nextOperand = getRandomNumber(MIN_OPERAND_VALUE, lastOperand);
+ } else if (operator == Operator.DIVIDE) {
+ List divisors = findDivisors(lastOperand);
+ int divisor = divisors.get(random.nextInt(divisors.size()));
+ int quotient = lastOperand / divisor;
+ operands.set(operands.size() - 1, String.valueOf(divisor * quotient));
+ nextOperand = divisor;
+ } else {
+ nextOperand = getRandomNumber(MIN_OPERAND_VALUE, MAX_OPERAND_VALUE);
+ }
+ operands.add(String.valueOf(nextOperand));
+ operators.add(operator);
+ }
+
+ private void addParentheses(List operands, List operators) {
+ if (operators.size() > 1 && random.nextBoolean()) {
+ int pos = random.nextInt(operators.size());
+ Operator op = operators.get(pos);
+ if (op == Operator.ADD || op == Operator.SUBTRACT) {
+ operands.set(pos, "(" + operands.get(pos));
+ operands.set(pos + 1, operands.get(pos + 1) + ")");
+ }
+ }
+ }
+
+ private List findDivisors(int number) {
+ if (number == 0) return Collections.singletonList(1);
+ int absNumber = Math.abs(number);
+ return IntStream.rangeClosed(1, absNumber)
+ .filter(i -> absNumber % i == 0)
+ .boxed()
+ .collect(Collectors.toList());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/mathapp/problemGenerators/ElementaryProblemGenerator.java b/src/main/java/com/mathapp/problemGenerators/ElementaryProblemGenerator.java
new file mode 100644
index 0000000..5bad07d
--- /dev/null
+++ b/src/main/java/com/mathapp/problemGenerators/ElementaryProblemGenerator.java
@@ -0,0 +1,16 @@
+package com.mathapp.problemGenerators;
+
+import com.mathapp.models.Equation;
+
+/**
+ * 小学题目生成器(具体策略)。
+ * 其实现委托给基类的核心方法,并强制要求结果非负。
+ */
+public class ElementaryProblemGenerator extends AbstractProblemGenerator {
+
+ @Override
+ protected Equation createProblem() {
+ // 调用父类的核心方法,并传入 true,要求启用“无负数”约束。
+ return createBaseArithmeticProblem(true);
+ }
+}
diff --git a/src/main/java/com/mathapp/problemGenerators/HighSchoolProblemGenerator.java b/src/main/java/com/mathapp/problemGenerators/HighSchoolProblemGenerator.java
new file mode 100644
index 0000000..f09cd73
--- /dev/null
+++ b/src/main/java/com/mathapp/problemGenerators/HighSchoolProblemGenerator.java
@@ -0,0 +1,50 @@
+package com.mathapp.problemGenerators;
+
+import com.mathapp.models.Equation;
+import com.mathapp.models.Operator;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 高中题目生成器(具体策略)。
+ * 它首先生成一个包含初中难度运算(平方/开方)的题目,
+ * 然后再额外加入一个三角函数运算。
+ * 生成的格式与 exp4j 兼容,但依赖计算引擎处理角度单位。
+ */
+public class HighSchoolProblemGenerator extends MiddleSchoolProblemGenerator {
+
+ private static final int[] PREDEFINED_ANGLES = new int[]{0, 30, 45, 60, 90};
+ private static final String[] TRIG_FUNCTIONS = new String[]{"sin", "cos", "tan"};
+
+ @Override
+ protected Equation createProblem() {
+ // 复用初中生成器的逻辑,先生成一个带平方或开方的题目
+ Equation middleSchoolEquation = super.createProblem();
+ List operands = new ArrayList<>(middleSchoolEquation.operands());
+ List operators = new ArrayList<>(middleSchoolEquation.operators());
+
+ String function = Arrays.toString(TRIG_FUNCTIONS);
+ int[] angle = PREDEFINED_ANGLES;
+
+ // 生成对学生友好的三角函数表达式,如 "sin(30)"
+ // exp4j 的计算将在 QuestionGenerator 中通过自定义函数处理角度
+ String trigExpression = String.format("%s(%d)", function, angle);
+
+ // 随机选择一个位置插入三角函数表达式
+ int insertPos = random.nextInt(operands.size() + 1);
+ if (insertPos == operands.size() || operands.isEmpty()) {
+ operands.add(trigExpression);
+ if(!operators.isEmpty() || operands.size() > 1) {
+ operators.add(Operator.ADD);
+ }
+ } else {
+ operands.add(insertPos, trigExpression);
+ operators.add(insertPos, Operator.ADD); // 简单地用加法连接
+ }
+
+
+ return new Equation(operands, operators);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/mathapp/problemGenerators/IProblemGenerator.java b/src/main/java/com/mathapp/problemGenerators/IProblemGenerator.java
new file mode 100644
index 0000000..568e4ba
--- /dev/null
+++ b/src/main/java/com/mathapp/problemGenerators/IProblemGenerator.java
@@ -0,0 +1,21 @@
+package com.mathapp.problemGenerators;
+
+import com.mathapp.models.Equation;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 题目生成器接口(策略模式的抽象策略)。
+ * 定义了所有题目生成器必须遵守的统一契约。
+ */
+public interface IProblemGenerator {
+
+ /**
+ * 生成指定数量的、不与现有题目重复的数学方程式。
+ *
+ * @param count 要生成的题目数量
+ * @param existingProblems 一个包含历史题目规范化字符串的集合,用于进行唯一性校验
+ * @return 一个包含新生成的、唯一的 Equation 对象的列表
+ */
+ List generate(int count, Set existingProblems);
+}
diff --git a/src/main/java/com/mathapp/problemGenerators/MiddleSchoolProblemGenerator.java b/src/main/java/com/mathapp/problemGenerators/MiddleSchoolProblemGenerator.java
new file mode 100644
index 0000000..d8ed83e
--- /dev/null
+++ b/src/main/java/com/mathapp/problemGenerators/MiddleSchoolProblemGenerator.java
@@ -0,0 +1,41 @@
+package com.mathapp.problemGenerators;
+
+import com.mathapp.models.Equation;
+import com.mathapp.models.Operator;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 初中题目生成器(具体策略)。
+ * 首先生成一个允许出现负数的标准四则运算题目,然后确保至少包含一个平方或开方运算。
+ * 生成的格式与 exp4j 兼容。
+ */
+public class MiddleSchoolProblemGenerator extends AbstractProblemGenerator {
+
+ @Override
+ protected Equation createProblem() {
+ Equation basicEquation = createBaseArithmeticProblem(false);
+ List operands = new ArrayList<>(basicEquation.operands());
+ List operators = new ArrayList<>(basicEquation.operators());
+
+ int modifyIndex = random.nextInt(operands.size());
+ String targetOperand = operands.get(modifyIndex);
+
+ // 检查操作数是否已经是复杂表达式(包含括号),如果是,则不再添加括号
+ boolean isComplex = targetOperand.contains("(") && targetOperand.contains(")");
+
+ if (random.nextBoolean()) {
+ // 平方运算:exp4j 使用 '^' 符号
+ // 如果操作数本身是复杂表达式,如 (3+2),则需要保留括号,变成 ((3+2)^2)
+ String squaredExpression = isComplex? String.format("(%s^2)", targetOperand) : String.format("%s^2", targetOperand);
+ operands.set(modifyIndex, squaredExpression);
+ } else {
+ // 开方运算:exp4j 使用 'sqrt()' 函数
+ int base = getRandomNumber(2, 10);
+ int valueToRoot = base * base;
+ String sqrtExpression = String.format("sqrt(%d)", valueToRoot);
+ operands.set(modifyIndex, sqrtExpression);
+ }
+ return new Equation(operands, operators);
+ }
+}
diff --git a/src/main/java/com/mathapp/services/DataPersistence.java b/src/main/java/com/mathapp/services/DataPersistence.java
new file mode 100644
index 0000000..f170305
--- /dev/null
+++ b/src/main/java/com/mathapp/services/DataPersistence.java
@@ -0,0 +1,120 @@
+package com.mathapp.services;
+
+import com.mathapp.models.Question;
+import com.mathapp.models.TestPaper;
+import com.mathapp.models.User;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.stream.Stream;
+
+public class DataPersistence {
+ private static final Path USER_FILE_PATH = Paths.get("data", "users.txt");
+
+ // 静态初始化块,确保目录存在
+ static {
+ try {
+ Files.createDirectories(USER_FILE_PATH.getParent());
+ if (!Files.exists(USER_FILE_PATH)) {
+ Files.createFile(USER_FILE_PATH);
+ }
+ } catch (IOException e) {
+ System.err.println("初始化用户数据文件失败: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ public static void saveUser(User user) {
+ String userData = user.getEmail() + "::" + user.getHashedPassword() + System.lineSeparator();
+ try {
+ Files.writeString(USER_FILE_PATH, userData, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static User findUserByEmail(String email) {
+ try (Stream lines = Files.lines(USER_FILE_PATH, StandardCharsets.UTF_8)) {
+ return lines.filter(line -> line.startsWith(email + "::"))
+ .map(line -> {
+ String[] parts = line.split("::");
+ // 构造函数会处理密码哈希
+ return new User(parts[0], parts[1], true);
+ })
+ .findFirst()
+ .orElse(null);
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static boolean updatePassword(String email, String newPassword) {
+ try {
+ List lines = Files.readAllLines(USER_FILE_PATH, StandardCharsets.UTF_8);
+ for (int i = 0; i < lines.size(); i++) {
+ if (lines.get(i).startsWith(email + "::")) {
+ User tempUser = new User(email, newPassword); // 用来生成新密码的哈希值
+ lines.set(i, email + "::" + tempUser.getHashedPassword());
+ Files.write(USER_FILE_PATH, lines, StandardCharsets.UTF_8);
+ return true;
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+
+
+ public static void saveTestPaper(TestPaper testPaper) {
+ if (testPaper.username() == null) {
+ System.err.println("无法保存试卷:用户名为空。");
+ return;
+ }
+
+ String username = testPaper.username().split("@")[0]; // 使用邮箱前缀作为目录名
+
+ try {
+ Path userDir = Paths.get("data", username);
+ Files.createDirectories(userDir);
+
+ String timestamp = testPaper.timestamp().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
+ Path filePath = userDir.resolve(timestamp + ".txt");
+
+ try (BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8)) {
+ writer.write("用户: " + testPaper.username());
+ writer.newLine();
+ writer.write("时间: " + timestamp);
+ writer.newLine();
+ writer.write(String.format("得分: %d / %d", testPaper.score(), testPaper.questions().size()));
+ writer.newLine();
+ writer.write("====================================");
+ writer.newLine();
+
+ for (int i = 0; i < testPaper.questions().size(); i++) {
+ Question q = testPaper.questions().get(i);
+ writer.write(String.format("题目 %d: %s", (i + 1), q.problemStatement()));
+ writer.newLine();
+ writer.write("选项: " + String.join(" | ", q.options()));
+ writer.newLine();
+ writer.write("你的答案: " + testPaper.userAnswers().get(i));
+ writer.newLine();
+ writer.write("正确答案: " + q.correctAnswer());
+ writer.newLine();
+ writer.write("--------------------");
+ writer.newLine();
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/mathapp/services/EmailService.java b/src/main/java/com/mathapp/services/EmailService.java
new file mode 100644
index 0000000..7e9b13d
--- /dev/null
+++ b/src/main/java/com/mathapp/services/EmailService.java
@@ -0,0 +1,40 @@
+package com.mathapp.services;
+
+import jakarta.mail.*;
+import jakarta.mail.internet.InternetAddress;
+import jakarta.mail.internet.MimeMessage;
+
+import java.util.Properties;
+
+public class EmailService {
+ // !!! 重要提示 !!!
+ // 请将以下信息替换为您自己的SMTP服务器信息和凭据
+ // 对于Gmail, 您需要生成一个 "应用专用密码"
+ private static final String SMTP_HOST = "smtp.your-email-provider.com"; // 例如:smtp.gmail.com
+ private static final String SMTP_PORT = "587"; // 通常是 587 (TLS) 或 465 (SSL)
+ private static final String FROM_EMAIL = "your-email@example.com"; // 您的发件邮箱地址
+ private static final String PASSWORD = "your-app-password"; // 您的应用专用密码或邮箱密码
+
+ public static void sendVerificationCode(String toEmail, String code) throws MessagingException {
+ Properties props = new Properties();
+ props.put("mail.smtp.host", SMTP_HOST);
+ props.put("mail.smtp.port", SMTP_PORT);
+ props.put("mail.smtp.auth", "true");
+ props.put("mail.smtp.starttls.enable", "true"); // 启用TLS加密
+
+ Session session = Session.getInstance(props, new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(FROM_EMAIL, PASSWORD);
+ }
+ });
+
+ Message message = new MimeMessage(session);
+ message.setFrom(new InternetAddress(FROM_EMAIL));
+ message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toEmail));
+ message.setSubject("您的数学学习平台注册码");
+ message.setText("尊敬的用户,\n\n您的注册码是: " + code + "\n\n请在注册页面输入以完成验证。该验证码5分钟内有效。\n\n祝您学习愉快!\n数学学习平台");
+
+ Transport.send(message);
+ }
+}
diff --git a/src/main/java/com/mathapp/services/QuestionGenerator.java b/src/main/java/com/mathapp/services/QuestionGenerator.java
new file mode 100644
index 0000000..9d978d7
--- /dev/null
+++ b/src/main/java/com/mathapp/services/QuestionGenerator.java
@@ -0,0 +1,94 @@
+package com.mathapp.services;
+
+import com.mathapp.models.Equation;
+import com.mathapp.models.Question;
+import com.mathapp.problemGenerators.*;
+import net.objecthunter.exp4j.Expression;
+import net.objecthunter.exp4j.ExpressionBuilder;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.*;
+
+public class QuestionGenerator {
+
+ public static List generateQuestions(String level, int count) {
+ IProblemGenerator generator;
+ switch (level) {
+ case "初中":
+ generator = new MiddleSchoolProblemGenerator();
+ break;
+ case "高中":
+ generator = new HighSchoolProblemGenerator();
+ break;
+ case "小学":
+ default:
+ generator = new ElementaryProblemGenerator();
+ break;
+ }
+
+ List questions = new ArrayList<>();
+ Set existingProblems = new HashSet<>();
+
+ List equations = generator.generate(count, existingProblems);
+
+ for (Equation eq : equations) {
+ String problemStatement = eq.toFormattedString();
+ try {
+ String correctAnswer = evaluateExpression(problemStatement);
+ List options = generateOptions(correctAnswer);
+ questions.add(new Question(problemStatement, options, correctAnswer));
+ existingProblems.add(eq.toNormalizedString());
+ } catch (Exception e) {
+ System.err.println("无法计算表达式: " + problemStatement + " - " + e.getMessage());
+ }
+ }
+ return questions;
+ }
+
+ private static String evaluateExpression(String expression) {
+ // exp4j 不支持'=',需要移除
+ String cleanExpression = expression.replace("=", "").trim();
+ Expression exp = new ExpressionBuilder(cleanExpression).build();
+ double result = exp.evaluate();
+
+ // 格式化结果,最多保留两位小数
+ BigDecimal bd = BigDecimal.valueOf(result);
+ bd = bd.setScale(2, RoundingMode.HALF_UP).stripTrailingZeros();
+ return bd.toPlainString();
+ }
+
+ private static List generateOptions(String correctAnswerStr) {
+ Set options = new HashSet<>();
+ options.add(correctAnswerStr);
+ Random rand = new Random();
+ double correctAnswerVal = Double.parseDouble(correctAnswerStr);
+
+ while (options.size() < 4) {
+ int strategy = rand.nextInt(3);
+ double wrongAnswerVal;
+
+ switch (strategy) {
+ case 0: // 微小偏差
+ wrongAnswerVal = correctAnswerVal + (rand.nextDouble() * 10 - 5);
+ break;
+ case 1: // 数量级错误
+ wrongAnswerVal = correctAnswerVal * (rand.nextBoolean() ? 10 : 0.1);
+ break;
+ case 2: // 符号错误
+ default:
+ wrongAnswerVal = -correctAnswerVal;
+ break;
+ }
+
+ // 格式化错误答案
+ BigDecimal bd = BigDecimal.valueOf(wrongAnswerVal);
+ bd = bd.setScale(2, RoundingMode.HALF_UP).stripTrailingZeros();
+ options.add(bd.toPlainString());
+ }
+
+ List sortedOptions = new ArrayList<>(options);
+ Collections.shuffle(sortedOptions);
+ return sortedOptions;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/mathapp/utils/ValidationUtils.java b/src/main/java/com/mathapp/utils/ValidationUtils.java
new file mode 100644
index 0000000..06ab09c
--- /dev/null
+++ b/src/main/java/com/mathapp/utils/ValidationUtils.java
@@ -0,0 +1,28 @@
+package com.mathapp.utils;
+
+import java.util.regex.Pattern;
+
+public class ValidationUtils {
+
+ // 邮箱正则表达式
+ private static final String EMAIL_REGEX =
+ "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
+
+ // 密码正则表达式:6-10位,至少一个数字,一个小写字母,一个大写字母
+ private static final String PASSWORD_REGEX =
+ "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{6,10}$";
+
+ public static boolean isValidEmail(String email) {
+ if (email == null) {
+ return false;
+ }
+ return Pattern.matches(EMAIL_REGEX, email);
+ }
+
+ public static boolean isValidPassword(String password) {
+ if (password == null) {
+ return false;
+ }
+ return Pattern.matches(PASSWORD_REGEX, password);
+ }
+}
diff --git a/src/main/java/org/example/Main.java b/src/main/java/org/example/Main.java
deleted file mode 100644
index 80dd2b2..0000000
--- a/src/main/java/org/example/Main.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.example;
-
-//TIP 要运行代码,请按 或
-// 点击装订区域中的 图标。
-public class Main {
- public static void main(String[] args) {
- //TIP 当文本光标位于高亮显示的文本处时按
- // 查看 IntelliJ IDEA 建议如何修正。
-
-
-
- }
-}
\ No newline at end of file