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