options) {
+ if (options == null) {
+ throw new IllegalArgumentException("选项列表不能为 null");
+ }
+ this.options = options;
+ }
+
+ /**
+ * 获取正确答案的索引。
+ *
+ * @return 正确答案的索引 (0-based)
+ */
+ public int getCorrectAnswerIndex() {
+ return correctAnswerIndex;
+ }
+
+ /**
+ * 设置正确答案的索引。
+ *
+ * @param correctAnswerIndex 要设置的正确答案索引 (0-based)
+ * @throws IllegalArgumentException 如果索引超出选项范围
+ */
+ public void setCorrectAnswerIndex(int correctAnswerIndex) {
+ if (correctAnswerIndex < 0 || correctAnswerIndex >= options.size()) {
+ throw new IllegalArgumentException("正确答案索引超出选项范围");
+ }
+ this.correctAnswerIndex = correctAnswerIndex;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Question: ").append(questionText).append("\n");
+ for (int i = 0; i < options.size(); i++) {
+ sb.append("Option ").append((char) ('A' + i)).append(": ").append(options.get(i))
+ .append("\n");
+ }
+ sb.append("Correct Answer: ").append((char) ('A' + correctAnswerIndex));
+ return sb.toString();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ybw/mathapp/entity/User.java b/src/main/java/com/ybw/mathapp/entity/User.java
new file mode 100644
index 0000000..b886577
--- /dev/null
+++ b/src/main/java/com/ybw/mathapp/entity/User.java
@@ -0,0 +1,134 @@
+package com.ybw.mathapp.entity;
+
+/**
+ * 用户实体类,表示系统中的用户信息。
+ *
+ * 该类包含用户的基本信息,如用户名、密码和学习级别。
+ * 用户级别可以是小学、初中或高中。
+ *
+ * @author 杨博文
+ * @version 1.0
+ * @since 2025
+ */
+public class User {
+
+ /**
+ * 用户名,不可修改。
+ */
+ private final String name;
+
+ /**
+ * 邮箱,不可修改。
+ */
+ private final String email;
+
+ /**
+ * 用户密码,不可修改。
+ */
+ private final String password;
+
+ /**
+ * 用户当前的学习级别,可以修改。
+ */
+ private String level;
+
+ /**
+ * 构造一个新的用户对象。
+ *
+ * @param name 用户名,不能为空
+ * @param email 邮箱,不能为空
+ * @param password 用户密码,不能为空
+ * @throws IllegalArgumentException 如果 name、email 或 password 为 null 或空字符串
+ */
+ public User(String name, String email, String password) {
+ if (name == null || name.trim().isEmpty()) {
+ throw new IllegalArgumentException("用户名不能为空");
+ }
+ if (email == null || email.trim().isEmpty()) {
+ throw new IllegalArgumentException("邮箱不能为空");
+ }
+ if (password == null || password.trim().isEmpty()) {
+ throw new IllegalArgumentException("密码不能为空");
+ }
+ this.name = name;
+ this.email = email;
+ this.password = password;
+ }
+
+ /**
+ * 从字符串创建用户对象。
+ *
+ *
字符串格式应为 "name,email,password"。
+ *
+ * @param line 包含用户信息的字符串
+ * @return 解析成功的用户对象,如果解析失败则返回 null
+ */
+ public static User fromString(String line) {
+ if (line == null || line.trim().isEmpty()) {
+ return null;
+ }
+
+ String[] parts = line.split(",", 3); // 最多分割成3部分
+ if (parts.length == 3) {
+ return new User(parts[0].trim(), parts[1].trim(), parts[2].trim());
+ }
+ return null;
+ }
+
+ /**
+ * 获取用户名。
+ *
+ * @return 用户名
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * 获取用户密码。
+ *
+ * @return 用户密码
+ */
+ public String getPassword() {
+ return password;
+ }
+
+ /**
+ * 获取用户当前的学习级别。
+ *
+ * @return 用户学习级别
+ */
+ public String getLevel() {
+ return level;
+ }
+
+ /**
+ * 设置用户的学习级别。
+ *
+ * @param newLevel 新的学习级别,支持"小学"、"初中"、"高中"
+ */
+ public void setLevel(String newLevel) {
+ level = newLevel;
+ }
+
+ /**
+ * 获取用户邮箱。
+ *
+ * @return 用户邮箱
+ */
+ public String getEmail() {
+ return email;
+ }
+
+ /**
+ * 返回用户信息的字符串表示。
+ *
+ *
格式为 "name,email,password"。
+ *
+ * @return 包含用户名、邮箱和密码的字符串
+ */
+ @Override
+ public String toString() {
+ return name + "," + email + "," + password;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ybw/mathapp/service/AdvancedCaculate.java b/src/main/java/com/ybw/mathapp/service/AdvancedCaculate.java
new file mode 100644
index 0000000..2f41901
--- /dev/null
+++ b/src/main/java/com/ybw/mathapp/service/AdvancedCaculate.java
@@ -0,0 +1,248 @@
+package com.ybw.mathapp.service;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+
+/**
+ * 扩展的计算类,支持基础四则运算、括号、平方、开根号、三角函数 (sin, cos, tan)。
+ *
+ *
运算符优先级定义如下:
+ *
+ * - 括号 {@code ( )} 优先级最高。
+ * - 高级运算符 (平方, 开根号, sin, cos, tan) 优先级高于四则运算。
+ * - 平方是后置运算符,开根号和三角函数是前置运算符。
+ *
+ *
+ * 例如:
+ *
+ * - {@code "3 + 开根号 16 平方"} 解释为 {@code "3 + (sqrt(16))^2"} = 19
+ * - {@code "开根号 ( 4 + 5 ) 平方"} 解释为 {@code "(sqrt(4+5))^2"} = 9
+ * - {@code "sin 30 + 5"} 解释为 {@code "sin(30) + 5"} (假设30是度数)
+ *
+ *
+ * 注意: 在计算开根号时,若操作数为负数,则抛出 ArithmeticException。
+ *
+ * @author 杨博文
+ * @since 2025
+ */
+public class AdvancedCaculate {
+
+ private static final Set BASIC_OPERATORS = new HashSet<>(
+ Arrays.asList("+", "-", "*", "/"));
+ private static final Set ADVANCED_OPERATORS = new HashSet<>(
+ Arrays.asList("平方", "开根号", "sin", "cos", "tan"));
+ private static final Map PRECEDENCE = new HashMap<>();
+
+ static {
+ PRECEDENCE.put("+", 1);
+ PRECEDENCE.put("-", 1);
+ PRECEDENCE.put("*", 2);
+ PRECEDENCE.put("/", 2);
+ // 高级运算符优先级更高
+ PRECEDENCE.put("平方", 3); // 后置
+ PRECEDENCE.put("开根号", 3); // 前置
+ PRECEDENCE.put("sin", 3); // 前置
+ PRECEDENCE.put("cos", 3); // 前置
+ PRECEDENCE.put("tan", 3); // 前置
+ }
+
+ /**
+ * 计算给定表达式的值。
+ *
+ * 表达式分词列表例如: ["3", "+", "开根号", "16", "平方"]
+ *
+ * @param expressionTokens 表达式的分词列表
+ * @return 计算结果
+ * @throws IllegalArgumentException 如果表达式无效(例如括号不匹配、操作数不足)
+ * @throws ArithmeticException 如果计算过程中出现错误(如除零、负数开根号)
+ */
+ public static double calculate(List expressionTokens) {
+ Stack numberStack = new Stack<>();
+ Stack operatorStack = new Stack<>();
+
+ for (String token : expressionTokens) {
+ if (isNumeric(token)) {
+ numberStack.push(Double.parseDouble(token));
+ } else if (token.equals("(")) {
+ operatorStack.push(token);
+ } else if (token.equals(")")) {
+ handleClosingParenthesis(numberStack, operatorStack);
+ } else if (ADVANCED_OPERATORS.contains(token)) {
+ handleAdvancedOperator(token, numberStack, operatorStack);
+ } else if (BASIC_OPERATORS.contains(token)) {
+ handleBasicOperator(token, numberStack, operatorStack);
+ } else {
+ throw new IllegalArgumentException("Unknown token: " + token);
+ }
+ }
+
+ // 处理栈中剩余的所有运算符
+ while (!operatorStack.isEmpty()) {
+ String op = operatorStack.pop();
+ if (op.equals("(") || op.equals(")")) {
+ throw new IllegalArgumentException("Mismatched parentheses in expression.");
+ }
+ processOperator(numberStack, op);
+ }
+
+ if (numberStack.size() != 1 || !operatorStack.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Invalid expression: " + String.join(" ", expressionTokens));
+ }
+ return numberStack.pop();
+ }
+
+ /**
+ * 处理遇到右括号 ')' 的情况
+ */
+ private static void handleClosingParenthesis(Stack numberStack,
+ Stack operatorStack) {
+ while (!operatorStack.isEmpty() && !operatorStack.peek().equals("(")) {
+ processOperator(numberStack, operatorStack.pop());
+ }
+ if (!operatorStack.isEmpty()) { // Pop the "("
+ operatorStack.pop();
+ } else {
+ throw new IllegalArgumentException("Mismatched parentheses in expression.");
+ }
+ }
+
+ /**
+ * 处理高级运算符(开根号, sin, cos, tan, 平方)
+ */
+ private static void handleAdvancedOperator(String token, Stack numberStack,
+ Stack operatorStack) {
+ if ("平方".equals(token)) {
+ if (numberStack.isEmpty()) {
+ throw new IllegalArgumentException("Invalid expression: '平方' lacks an operand.");
+ }
+ double operand = numberStack.pop();
+ numberStack.push(Math.pow(operand, 2));
+ } else { // "开根号", "sin", "cos", "tan" 是前置运算符
+ operatorStack.push(token);
+ }
+ }
+
+ /**
+ * 处理基础四则运算符(+, -, *, /)
+ */
+ private static void handleBasicOperator(String token, Stack numberStack,
+ Stack operatorStack) {
+ // 处理四则运算符,遵循优先级
+ while (!operatorStack.isEmpty()
+ && !operatorStack.peek().equals("(")
+ && PRECEDENCE.get(token) <= PRECEDENCE.getOrDefault(operatorStack.peek(), 0)) {
+ processOperator(numberStack, operatorStack.pop());
+ }
+ operatorStack.push(token);
+ }
+
+ /**
+ * 执行一次运算操作。
+ *
+ * @param numberStack 数字栈
+ * @param operator 运算符
+ * @throws IllegalArgumentException 如果运算符缺少操作数或为未知运算符
+ * @throws ArithmeticException 如果计算过程中出现错误(如除零、负数开根号)
+ */
+ private static void processOperator(Stack numberStack, String operator) {
+ if (numberStack.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Invalid expression: operator '" + operator + "' lacks operand(s).");
+ }
+
+ if (ADVANCED_OPERATORS.contains(operator)) {
+ processAdvancedOperator(numberStack, operator);
+ } else if (BASIC_OPERATORS.contains(operator)) {
+ processBasicOperator(numberStack, operator);
+ } else {
+ throw new IllegalArgumentException("Unexpected operator in process: " + operator);
+ }
+ }
+
+ /**
+ * 执行高级运算操作(开根号, sin, cos, tan)
+ */
+ private static void processAdvancedOperator(Stack numberStack, String operator) {
+ double operand = numberStack.pop();
+ double result;
+ switch (operator) {
+ case "开根号":
+ if (operand < 0) {
+ // 抛出异常,让调用者(MultipleChoiceGenerator)处理
+ throw new ArithmeticException("Cannot take square root of negative number: " + operand);
+ }
+ result = Math.sqrt(operand);
+ break;
+ case "sin":
+ result = Math.sin(Math.toRadians(operand)); // 假设输入是度数
+ break;
+ case "cos":
+ result = Math.cos(Math.toRadians(operand));
+ break;
+ case "tan":
+ // tan(90 + n*180) 会趋向无穷,这里不特别处理,让其返回 Infinity 或 -Infinity
+ result = Math.tan(Math.toRadians(operand));
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown advanced operator: " + operator);
+ }
+ numberStack.push(result);
+ }
+
+ /**
+ * 执行基础四则运算操作(+, -, *, /)
+ */
+ private static void processBasicOperator(Stack numberStack, String operator) {
+ if (numberStack.size() < 2) {
+ throw new IllegalArgumentException(
+ "Invalid expression: operator '" + operator + "' lacks operand(s).");
+ }
+ double b = numberStack.pop();
+ double a = numberStack.pop();
+ double result;
+ switch (operator) {
+ case "+":
+ result = a + b;
+ break;
+ case "-":
+ result = a - b;
+ break;
+ case "*":
+ result = a * b;
+ break;
+ case "/":
+ if (b == 0) {
+ throw new ArithmeticException("Division by zero");
+ }
+ result = a / b;
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown operator: " + operator);
+ }
+ numberStack.push(result);
+ }
+
+ /**
+ * 判断给定字符串是否为数字。
+ *
+ * @param str 待判断的字符串
+ * @return 如果是数字则返回 true,否则返回 false
+ */
+ public static boolean isNumeric(String str) {
+ if (str == null || str.isEmpty()) {
+ return false;
+ }
+ try {
+ Double.parseDouble(str);
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ybw/mathapp/service/JuniorHighGenerator.java b/src/main/java/com/ybw/mathapp/service/JuniorHighGenerator.java
new file mode 100644
index 0000000..a14790e
--- /dev/null
+++ b/src/main/java/com/ybw/mathapp/service/JuniorHighGenerator.java
@@ -0,0 +1,214 @@
+package com.ybw.mathapp.service;
+
+import static com.ybw.mathapp.service.AdvancedCaculate.isNumeric;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * 初中题目生成器,负责生成包含平方或开根号运算的初中级别数学题目。
+ *
+ * 该生成器确保每道题目都包含至少一个高级运算符(平方或开根号),
+ * 题目结构包含基本的四则运算和高级运算的组合。
+ *
+ * @author 杨博文
+ * @version 1.1
+ * @since 2025
+ */
+public class JuniorHighGenerator implements QuestionGenerator {
+
+ /**
+ * 高级运算符数组,包含"平方"和"开根号"。
+ */
+ private static final String[] ADVANCED_OPS = {"平方", "开根号"};
+
+ /**
+ * 基本运算符数组,包含四则运算符号。
+ */
+ private static final String[] OPERATORS = {"+", "-", "*", "/"};
+
+ /**
+ * 随机数生成器,用于生成随机题目。
+ */
+ private final Random random = new Random();
+
+ @Override
+ public List generateQuestions(int count) {
+ List questions = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ String question = generateSingleQuestion();
+ questions.add(question);
+ }
+ return questions;
+ }
+
+ /**
+ * 生成单个初中级别的数学题目。
+ *
+ * 该方法确保生成的题目包含至少一个高级运算符(平方或开根号),
+ * 并根据操作数数量采用不同的生成策略。
+ *
+ * @return 生成的数学题目字符串
+ */
+ private String generateSingleQuestion() {
+ List parts = new ArrayList<>();
+ int operandCount = random.nextInt(5) + 1;
+ parts = generateBase(operandCount, parts);
+
+ boolean hasAdvancedOp = false;
+
+ if (operandCount == 1) {
+ // 对于单个操作数,直接添加高级运算符
+ if ("平方".equals(ADVANCED_OPS[random.nextInt(ADVANCED_OPS.length)])) {
+ parts.add("平方");
+ } else {
+ // 为单个操作数的开根号添加括号
+ parts.set(0, "开根号 ( " + parts.get(0) + " )");
+ }
+ hasAdvancedOp = true;
+ } else {
+ // 遍历所有可能的操作数位置 (索引为偶数)
+ // 修复:循环条件确保最后一个操作数也能被检查
+ for (int i = 0; i < parts.size(); i += 2) { // 只检查操作数索引 (0, 2, 4, ...)
+ // 该位置要为操作数且随机添加括号
+ if (isNumeric(parts.get(i)) && random.nextBoolean()) {
+ // 随机选择高级运算符
+ if ("开根号".equals(ADVANCED_OPS[random.nextInt(ADVANCED_OPS.length)])) {
+ parts = generateRoot(parts, i);
+ } else { // 平方运算
+ parts = generateSquare(parts, i);
+ }
+ hasAdvancedOp = true;
+ break; // 添加成功后退出循环
+ }
+ }
+ }
+
+ // 启动保底强制加入一个高级运算符
+ if (!hasAdvancedOp) {
+ parts = forceAddAdvancedOp(parts);
+ }
+ return String.join(" ", parts) + " =";
+ }
+
+ /**
+ * 生成基本的四则运算表达式部分。
+ *
+ * 该方法生成指定数量的操作数和运算符,构成基础的数学表达式。
+ *
+ * @param operandCount 操作数的数量
+ * @param parts 用于存储表达式各部分的列表
+ * @return 包含基本运算表达式的列表
+ */
+ public List generateBase(int operandCount, List parts) {
+ for (int i = 0; i < operandCount; i++) {
+ int num = random.nextInt(100) + 1;
+ parts.add(String.valueOf(num));
+ if (i < operandCount - 1) {
+ parts.add(OPERATORS[random.nextInt(OPERATORS.length)]);
+ }
+ }
+ return parts;
+ }
+
+ /**
+ * 强制在表达式中添加一个高级运算符作为保底机制。
+ *
+ * 当随机生成过程中没有添加高级运算符时,使用此方法确保
+ * 每道题目都包含至少一个高级运算符。
+ *
+ * @param parts 包含表达式各部分的列表
+ * @return 添加了高级运算符的表达式列表
+ */
+ public List forceAddAdvancedOp(List parts) {
+ String advancedOp = ADVANCED_OPS[random.nextInt(ADVANCED_OPS.length)];
+ if ("平方".equals(advancedOp)) {
+ // 对整个表达式进行平方
+ parts.add(0, "("); // 在开头添加左括号
+ parts.add(") 平方"); // 在末尾添加右括号和"平方"
+ } else { // 开根号
+ // 对整个表达式进行开根号
+ parts.add(0, "开根号 ( "); // 在开头添加"开根号 ( "
+ parts.add(" )"); // 在末尾添加" )"
+ }
+ return parts;
+ }
+
+ /**
+ * 在指定位置生成开根号运算。
+ *
+ * 该方法在表达式指定位置添加开根号运算,可能只对单个操作数
+ * 进行开根号,或者对一段子表达式进行开根号。
+ *
+ * @param parts 包含表达式各部分的列表
+ * @param i 开根号运算的起始位置 (操作数索引)
+ * @return 添加了开根号运算的表达式列表
+ */
+ public List generateRoot(List parts, int i) {
+ if (random.nextBoolean()) {
+ // 对单个操作数进行开根号
+ parts.set(i, "开根号 ( " + parts.get(i) + " )");
+ } else {
+ // 对子表达式进行开根号
+ parts.set(i, "开根号 ( " + parts.get(i)); // 在起始操作数前添加左括号和"开根号 ("
+ int endIndex = findMatchingEndIndex(parts, i); // 查找匹配的结束操作数索引
+ String currentEnd = parts.get(endIndex);
+ parts.set(endIndex, currentEnd + " )"); // 在结束操作数后添加右括号
+ }
+ return parts;
+ }
+
+ /**
+ * 在指定位置生成平方运算。
+ *
+ * 该方法在表达式指定位置添加平方运算,对一段子表达式进行平方运算。
+ *
+ * @param parts 包含表达式各部分的列表
+ * @param i 平方运算的起始位置 (操作数索引)
+ * @return 添加了平方运算的表达式列表
+ */
+ public List generateSquare(List parts, int i) {
+ parts.set(i, "(" + parts.get(i)); // 在起始操作数前添加左括号
+ int endIndex = findMatchingEndIndex(parts, i); // 查找匹配的结束操作数索引
+ String currentEnd = parts.get(endIndex);
+ parts.set(endIndex, currentEnd + " ) 平方"); // 在结束操作数后添加右括号和"平方"
+ return parts;
+ }
+
+ /**
+ * 辅助方法:查找子表达式的结束操作数索引。 从起始操作数索引 i 开始,随机决定子表达式的结束位置 (必须是操作数索引)。 为了简化,这里选择从 i+2
+ * 开始到最后一个操作数之间的随机一个操作数索引。
+ *
+ * @param parts 表达式各部分列表
+ * @param i 起始操作数索引
+ * @return 结束操作数索引
+ */
+ private int findMatchingEndIndex(List parts, int i) {
+ // 获取最后一个操作数的索引
+ int lastOperandIndex = parts.size() - 1;
+ if (lastOperandIndex % 2 != 0) {
+ lastOperandIndex--; // 如果列表长度是偶数,最后一个元素是运算符,需要减1得到最后一个操作数索引
+ }
+
+ // 确保范围是有效的操作数索引
+ if (i >= lastOperandIndex) {
+ return lastOperandIndex; // 如果i已经是最后一个或超出,返回最后一个
+ }
+
+ // 在 [i+2, lastOperandIndex] 范围内选择一个操作数索引 (步长为2)
+ // 确保至少包含一个运算符,所以从i+2开始
+ List possibleEndIndices = new ArrayList<>();
+ for (int idx = i + 2; idx <= lastOperandIndex; idx += 2) {
+ possibleEndIndices.add(idx);
+ }
+
+ if (!possibleEndIndices.isEmpty()) {
+ // 随机选择一个可能的结束索引
+ return possibleEndIndices.get(random.nextInt(possibleEndIndices.size()));
+ } else {
+ // 理论上不应该到达这里,如果到达则返回起始索引
+ return i;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ybw/mathapp/service/MultipleChoiceGenerator.java b/src/main/java/com/ybw/mathapp/service/MultipleChoiceGenerator.java
new file mode 100644
index 0000000..d84b603
--- /dev/null
+++ b/src/main/java/com/ybw/mathapp/service/MultipleChoiceGenerator.java
@@ -0,0 +1,252 @@
+package com.ybw.mathapp.service;
+
+import com.ybw.mathapp.entity.QuestionWithOptions;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+
+/**
+ * 通用选择题生成器,将基础题目生成器的结果转换为带选项的选择题。
+ *
+ * 该类确保生成的题目列表中不包含重复的题干。
+ * 特殊处理规则:
+ *
+ * - 初中题:在计算过程中避免负数开根号(由 {@link AdvancedCaculate} 抛出异常,此处理捕获并跳过)。
+ * - 小学题:生成的选项避免出现负数。
+ *
+ *
+ * @author 杨博文
+ * @since 2025
+ */
+public class MultipleChoiceGenerator {
+
+ /**
+ * 用于格式化数字的格式化器,保留两位小数。
+ */
+ private static final DecimalFormat df = new DecimalFormat("#0.00");
+ /**
+ * 基础题目生成器。
+ */
+ private final QuestionGenerator baseGenerator;
+ /**
+ * 随机数生成器。
+ */
+ private final Random random = new Random();
+ /**
+ * 当前题目类型,用于特殊处理逻辑。
+ */
+ private final String level;
+
+ /**
+ * 构造一个新的选择题生成器。
+ *
+ * @param baseGenerator 基础题目生成器
+ * @param level 题目所属的级别 ("小学", "初中", "高中")
+ */
+ public MultipleChoiceGenerator(QuestionGenerator baseGenerator, String level) {
+ this.baseGenerator = baseGenerator;
+ this.level = level;
+ }
+
+ /**
+ * 生成指定数量的选择题,确保题干不重复。
+ *
+ * @param count 要生成的题目数量
+ * @return 生成的选择题列表(已去重)
+ */
+ public List generateMultipleChoiceQuestions(int count) {
+ List mcQuestions = new ArrayList<>();
+ Set seenQuestionTexts = new HashSet<>(); // 用于存储已生成的题干,保证唯一性
+
+ while (mcQuestions.size() < count) {
+ String baseQuestion = generateUniqueBaseQuestion(seenQuestionTexts);
+ if (baseQuestion == null) {
+ // 如果无法生成不重复的基础题目,可能基础生成器的可能组合已用尽
+ break; // 退出循环
+ }
+ QuestionWithOptions mcq = generateSingleMcq(baseQuestion);
+ if (mcq != null) {
+ mcQuestions.add(mcq);
+ seenQuestionTexts.add(baseQuestion); // 添加成功生成的题干
+ }
+ // 如果 generateSingleMCQ 返回 null,说明计算或生成选项失败,循环会继续尝试下一个基础题目
+ }
+ return mcQuestions;
+ }
+
+ /**
+ * 生成一个唯一的、未处理过的基础题目。
+ *
+ * @param seenQuestionTexts 已生成题干的集合
+ * @return 一个唯一的题干字符串,如果在限定尝试次数内无法找到则返回 null
+ */
+ private String generateUniqueBaseQuestion(Set seenQuestionTexts) {
+ int attempts = 0;
+ int maxAttempts = 1000; // 防止无限循环,如果生成太多重复题则退出
+ while (attempts < maxAttempts) {
+ List baseQuestionList = baseGenerator.generateQuestions(1);
+ String baseQuestion = baseQuestionList.get(0);
+ if (!seenQuestionTexts.contains(baseQuestion)) {
+ return baseQuestion;
+ }
+ attempts++;
+ }
+ return null; // 达到最大尝试次数仍未找到唯一题目
+ }
+
+ /**
+ * 从单个基础题目生成选择题对象。
+ *
+ * @param baseQuestion 基础题干字符串,例如 "3 + 5 = ?"
+ * @return 生成的选择题对象,如果计算或生成选项失败则返回 null
+ */
+ private QuestionWithOptions generateSingleMcq(String baseQuestion) {
+ try {
+ // 从基础题干中提取表达式部分,例如 "3 + 5 = ?" -> "3 + 5"
+ String expression = baseQuestion.substring(0, baseQuestion.lastIndexOf(" =")).trim();
+ List tokens = tokenizeExpression(expression);
+ // 计算正确答案
+ double correctAnswer = AdvancedCaculate.calculate(tokens);
+
+ // 生成选项列表
+ List options = generateOptions(correctAnswer);
+ if (options == null) {
+ // 无法生成足够的有效选项
+ return null;
+ }
+
+ // 随机打乱选项顺序
+ Collections.shuffle(options);
+ // 找到正确答案在打乱后列表中的索引
+ int correctIndex = options.indexOf(df.format(correctAnswer));
+
+ return new QuestionWithOptions(baseQuestion, options, correctIndex);
+
+ } catch (ArithmeticException | IllegalArgumentException e) {
+ // 计算或表达式格式错误,跳过此题
+ // System.out.println("计算或表达式错误,跳过题目: " + baseQuestion + ", Error: " + e.getMessage());
+ return null; // 返回 null 表示生成失败
+ }
+ }
+
+ /**
+ * 生成选项列表 (正确答案 + 错误答案)。
+ *
+ * 对于小学级别,错误答案不会包含负数。
+ *
+ * @param correctAnswer 正确答案
+ * @return 包含正确答案和错误答案的字符串列表,如果无法生成足够选项则返回 null
+ */
+ private List generateOptions(double correctAnswer) {
+ Set wrongAnswers = new HashSet<>();
+ int attempts = 0;
+ int maxAttempts = 100; // 防止无限循环
+ int numWrongOptions = 3; // 假设总共4个选项,需要3个错误答案
+
+ while (wrongAnswers.size() < numWrongOptions && attempts < maxAttempts) {
+ int offset = random.nextInt(20) + 1; // 生成 1-20 的偏移量
+ if (random.nextBoolean()) {
+ offset = -offset; // 随机正负
+ }
+ double wrongAnswer = correctAnswer + offset;
+
+ // 确保错误答案与正确答案不同,并且对于小学题不为负数
+ if (Math.abs(df.format(wrongAnswer).compareTo(df.format(correctAnswer))) != 0) {
+ if (!level.equals("小学") || wrongAnswer >= 0) {
+ wrongAnswers.add(wrongAnswer);
+ }
+ }
+ attempts++;
+ }
+
+ if (wrongAnswers.size() < numWrongOptions) {
+ return null; // 无法生成足够选项
+ }
+
+ // 将正确答案和错误答案合并
+ List allAnswers = new ArrayList<>();
+ allAnswers.add(correctAnswer);
+ allAnswers.addAll(wrongAnswers);
+
+ // 格式化所有答案为字符串
+ List options = new ArrayList<>();
+ for (Double ans : allAnswers) {
+ options.add(df.format(ans));
+ }
+ return options;
+ }
+
+ // ... (其他类成员和方法保持不变)
+
+ // --- 表达式分词逻辑 ---
+ // 将 "3 + 开根号 ( 4 ) 平方 - sin 30" 分割成 ["3", "+", "开根号", "(", "4", ")", "平方", "-", "sin", "30"]
+
+ /**
+ * 将表达式字符串分割成标记列表。
+ *
+ * @param expression 表达式字符串
+ * @return 标记列表
+ */
+ private List tokenizeExpression(String expression) {
+ List tokens = new ArrayList<>();
+ int i = 0;
+ while (i < expression.length()) {
+ String token = findNextToken(expression, i);
+ if (token != null) {
+ tokens.add(token);
+ i += token.length();
+ } else {
+ i++;
+ } /* else {
+ // 如果找不到匹配的 token,可能是单个字符或未知格式
+ tokens.add(String.valueOf(expression.charAt(i)));
+ i++;
+ }
+ */
+ }
+ return tokens;
+ }
+
+ /**
+ * 查找从指定位置开始的下一个 token。
+ *
+ * @param expression 表达式字符串
+ * @param startPos 起始查找位置
+ * @return 找到的 token,如果未找到则返回 null
+ */
+ private String findNextToken(String expression, int startPos) {
+ char c = expression.charAt(startPos);
+
+ if (Character.isWhitespace(c)) {
+ return null; // 空格由调用者处理
+ }
+ if (c == '(' || c == ')') {
+ return String.valueOf(c);
+ }
+ if (Character.isDigit(c) || c == '.') {
+ // 查找连续的数字或小数点
+ int j = startPos;
+ while (j < expression.length() && (Character.isDigit(expression.charAt(j))
+ || expression.charAt(j) == '.')) {
+ j++;
+ }
+ return expression.substring(startPos, j);
+ }
+
+ String[] advancedOps = {"平方", "开根号", "sin", "cos", "tan"};
+ // 检查多字符运算符
+ for (String op : advancedOps) {
+ if (expression.startsWith(op, startPos)) {
+ return op;
+ }
+ }
+
+ // 如果不是多字符运算符,则认为是单字符运算符 (+, -, *, / 等)
+ return String.valueOf(c);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ybw/mathapp/service/PrimarySchoolGenerator.java b/src/main/java/com/ybw/mathapp/service/PrimarySchoolGenerator.java
new file mode 100644
index 0000000..3b2bb81
--- /dev/null
+++ b/src/main/java/com/ybw/mathapp/service/PrimarySchoolGenerator.java
@@ -0,0 +1,106 @@
+package com.ybw.mathapp.service;
+
+import static com.ybw.mathapp.service.AdvancedCaculate.isNumeric;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * 小学题目生成器,负责生成包含四则运算和括号的小学级别数学题目。
+ *
+ * 该生成器专门用于生成适合小学生的数学题目,题目仅包含加减乘除四则运算
+ * 和括号,确保计算结果为非负数。
+ *
+ * @author 杨博文
+ * @version 1.0
+ * @since 2025
+ */
+public class PrimarySchoolGenerator implements QuestionGenerator {
+
+ /** 运算符数组,包含四则运算符号。 */
+ private static final String[] OPERATORS = {"+", "-", "*", "/"};
+
+ /** 随机数生成器,用于生成随机题目。 */
+ private final Random random = new Random();
+
+ @Override
+ public List generateQuestions(int count) {
+ List questions = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ String question = generateSingleQuestion();
+ questions.add(question);
+ }
+ return questions;
+ }
+
+ /**
+ * 生成单个小级别的数学题目.
+ *
+ * 该方法生成包含2-5个操作数的四则运算表达式,可能包含括号,
+ * 并确保计算结果为非负数。如果计算结果为负数,则重新生成。
+ *
+ * @return 生成的小学数学题目字符串
+ */
+ private String generateSingleQuestion() {
+ int operandCount = random.nextInt(4) + 2; // 2-5个操作数
+ List parts = new ArrayList<>();
+ while (true) {
+ // 生成基础操作
+ parts = generateBase(operandCount, parts);
+ // 简单添加括号逻辑:随机加一个括号
+ if (operandCount > 2 && random.nextBoolean()) {
+ // 遍历查找左括号的合理位置
+ for (int i = 0; i < parts.size() - 2; i++) {
+ // 该位置要为操作数且随机添加括号
+ if (isNumeric(parts.get(i)) && random.nextBoolean()) {
+ parts.add(i, "(");
+ i++;
+ // 为避免随机数上限出现0,此处要单独判断一下左括号正好括住倒数第二个操作数的情况
+ if (i == parts.size() - 3) {
+ parts.add(")");
+ } else {
+ while (true) {
+ int i2 = random.nextInt(parts.size() - 3 - i) + 2;
+ if (isNumeric(parts.get(i + i2))) {
+ parts.add(i + i2 + 1, ")");
+ break;
+ }
+ }
+ }
+ break;
+ }
+ }
+ }
+ try {
+ if (AdvancedCaculate.calculate(parts) >= 0) {
+ return String.join(" ", parts) + " =";
+ } else {
+ parts.clear();
+ }
+ } catch (ArithmeticException | IllegalArgumentException e) {
+ parts.clear();
+ }
+ }
+ }
+
+ /**
+ * 生成基本的四则运算表达式部分。
+ *
+ * 该方法生成指定数量的操作数和运算符,构成基础的数学表达式。
+ *
+ * @param operandCount 操作数的数量
+ * @param parts 用于存储表达式各部分的列表
+ * @return 包含基本运算表达式的列表
+ */
+ public List generateBase(int operandCount, List parts) {
+ for (int i = 0; i < operandCount; i++) {
+ int num = random.nextInt(100) + 1;
+ parts.add(String.valueOf(num));
+ if (i < operandCount - 1) {
+ parts.add(OPERATORS[random.nextInt(OPERATORS.length)]);
+ }
+ }
+ return parts;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ybw/mathapp/service/QuestionGenerator.java b/src/main/java/com/ybw/mathapp/service/QuestionGenerator.java
new file mode 100644
index 0000000..91c9436
--- /dev/null
+++ b/src/main/java/com/ybw/mathapp/service/QuestionGenerator.java
@@ -0,0 +1,29 @@
+package com.ybw.mathapp.service;
+
+import java.util.List;
+
+/**
+ * 题目生成器接口,定义了题目生成器的标准方法。
+ *
+ * 所有具体的题目生成器都应该实现此接口,提供统一的题目生成功能。
+ * 不同级别的题目生成器(如小学、初中、高中)可以根据各自的特点
+ * 实现不同的生成算法。
+ *
+ * @author 杨博文
+ * @version 1.0
+ * @since 2025
+ */
+public interface QuestionGenerator {
+
+ /**
+ * 生成指定数量的数学题目。
+ *
+ *
该方法根据实现类的特定规则生成指定数量的数学题目。
+ * 生成的题目应该符合对应教育级别的难度要求。
+ *
+ * @param count 需要生成的题目数量
+ * @return 包含生成题目的字符串列表
+ */
+ List generateQuestions(int count);
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/ybw/mathapp/service/SeniorHighGenerator.java b/src/main/java/com/ybw/mathapp/service/SeniorHighGenerator.java
new file mode 100644
index 0000000..62ab5bb
--- /dev/null
+++ b/src/main/java/com/ybw/mathapp/service/SeniorHighGenerator.java
@@ -0,0 +1,127 @@
+package com.ybw.mathapp.service;
+
+import static com.ybw.mathapp.service.AdvancedCaculate.isNumeric;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * 高中题目生成器,负责生成包含三角函数运算的高中级别数学题目。
+ *
+ * 该生成器确保每道题目都包含至少一个三角函数运算符(sin、cos或tan),
+ * 题目结构包含基本的四则运算和三角函数运算的组合。
+ *
+ * @author 杨博文
+ * @version 1.0
+ * @since 2025
+ */
+public class SeniorHighGenerator implements QuestionGenerator {
+
+ /** 三角函数运算符数组,包含"sin"、"cos"和"tan"。 */
+ private static final String[] TRIG_FUNCS = {"sin", "cos", "tan"};
+
+ /** 基本运算符数组,包含四则运算符号。 */
+ private static final String[] OPERATORS = {"+", "-", "*", "/"};
+
+ /** 随机数生成器,用于生成随机题目。 */
+ private final Random random = new Random();
+
+ @Override
+ public List generateQuestions(int count) {
+ List questions = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ String question = generateSingleQuestion();
+ questions.add(question);
+ }
+ return questions;
+ }
+
+ /**
+ * 生成单个高中级别的数学题目。
+ *
+ * 该方法确保生成的题目包含至少一个三角函数运算符,
+ * 并根据操作数数量采用不同的生成策略。
+ *
+ * @return 生成的数学题目字符串
+ */
+ private String generateSingleQuestion() {
+ List parts = new ArrayList<>();
+ int operandCount = random.nextInt(5) + 1;
+ parts = generateBase(operandCount, parts);
+ String advancedOp;
+ if (operandCount == 1) {
+ advancedOp = TRIG_FUNCS[random.nextInt(TRIG_FUNCS.length)];
+ parts.set(0, advancedOp + " " + parts.get(0));
+ } else {
+ // 遍历查找左括号的合理位置
+ for (int i = 0; i < parts.size(); i++) {
+ // 最后一次循环保底生成高中三角函数
+ if (i == parts.size() - 1) {
+ advancedOp = TRIG_FUNCS[random.nextInt(TRIG_FUNCS.length)];
+ parts.set(i, advancedOp + " " + parts.get(i));
+ } else if (isNumeric(parts.get(i)) && random.nextBoolean()) { // 随机数看是否为操作数且随即进入生成程序
+ // 进入随机生成tan\sin\cos的程序
+ parts = generateTrig(parts, i);
+ break;
+ }
+ }
+ }
+ return String.join(" ", parts) + " =";
+ }
+
+ /**
+ * 生成基本的四则运算表达式部分。
+ *
+ * 该方法生成指定数量的操作数和运算符,构成基础的数学表达式。
+ *
+ * @param operandCount 操作数的数量
+ * @param parts 用于存储表达式各部分的列表
+ * @return 包含基本运算表达式的列表
+ */
+ // 产生基本操作
+ public List generateBase(int operandCount, List parts) {
+ for (int i = 0; i < operandCount; i++) {
+ int num = random.nextInt(100) + 1;
+ parts.add(String.valueOf(num));
+ if (i < operandCount - 1) {
+ parts.add(OPERATORS[random.nextInt(OPERATORS.length)]);
+ }
+ }
+ return parts;
+ }
+
+ /**
+ * 在指定位置生成三角函数运算。
+ *
+ * 该方法在表达式指定位置添加三角函数运算,可能只对单个操作数
+ * 进行三角函数运算,或者对一段子表达式进行三角函数运算。
+ *
+ * @param parts 包含表达式各部分的列表
+ * @param i 三角函数运算的位置
+ * @return 添加了三角函数运算的表达式列表
+ */
+ // 产生三角函数运算符
+ public List generateTrig(List parts, int i) {
+ String trigOp = TRIG_FUNCS[random.nextInt(TRIG_FUNCS.length)];
+ if (random.nextBoolean()) {
+ parts.set(i, trigOp + " " + parts.get(i));
+ } else {
+ parts.set(i, trigOp + " ( " + parts.get(i));
+ // 为避免随机数上限出现0,此处要单独判断一下左括号正好括住倒数第二个操作数的情况
+ if (i == parts.size() - 3) {
+ parts.set(parts.size() - 1, parts.get(parts.size() - 1) + " )");
+ } else {
+ while (true) {
+ int i2 = random.nextInt(parts.size() - 3 - i) + 2;
+ if (isNumeric(parts.get(i + i2))) {
+ parts.set(i + i2, parts.get(i + i2) + " )");
+ break;
+ }
+ }
+ }
+ }
+ return parts;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/ybw/mathapp/util/ChangePassword.java b/src/main/java/com/ybw/mathapp/util/ChangePassword.java
new file mode 100644
index 0000000..e4872cb
--- /dev/null
+++ b/src/main/java/com/ybw/mathapp/util/ChangePassword.java
@@ -0,0 +1,139 @@
+package com.ybw.mathapp.util;
+
+import static com.ybw.mathapp.util.LoginFileUtils.USER_FILE;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 修改用户密码的工具类。
+ *
+ * 该类提供了一个静态方法来更新用户文件中指定用户的密码。
+ * 它通过读取整个文件,找到目标用户,修改其密码,然后将整个内容写回文件来实现。
+ *
+ * @author 杨博文
+ * @since 2025
+ */
+public class ChangePassword {
+
+ /**
+ * 用于缓存文件所有行内容的列表。
+ */
+ private static final List lines = new ArrayList<>();
+ /**
+ * 存储找到的用户信息行。
+ */
+ private static String userLine = null;
+ /**
+ * 存储找到的用户行在文件中的行号(从0开始)。
+ */
+ private static int userLineNumber = -1;
+
+ /**
+ * 修改指定用户的密码。
+ *
+ * 该方法会读取用户文件,查找与给定用户名匹配的行,更新该行的密码字段,
+ * 然后将更新后的内容写回文件。
+ *
+ * @param name 要修改密码的用户名
+ * @param newPassword 新密码
+ * @return 如果修改成功返回 true,否则返回 false
+ */
+ public static boolean changePassword(String name, String newPassword) {
+ File file = new File(USER_FILE);
+ if (!file.exists()) {
+ System.out.println("用户文件不存在: " + USER_FILE);
+ return false;
+ }
+
+ // 1. 读取文件,查找用户
+ lines.clear(); // 清空上一次的缓存
+ userLine = null;
+ userLineNumber = -1;
+ if (!findUserLine(name, file)) {
+ return false;
+ }
+
+ if (userLine == null || userLineNumber == -1) {
+ // 用户未找到
+ System.out.println("用户 '" + name + "' 不存在,修改失败。");
+ return false;
+ }
+
+ // 2. 更新找到的用户行中的密码
+ String[] parts = userLine.split(",");
+ if (parts.length != 3) {
+ System.err.println("用户文件中用户 '" + name + "' 的数据格式不正确,无法修改密码。");
+ return false;
+ }
+ parts[2] = newPassword; // 假设密码是第三个字段
+ String updatedLine = String.join(",", parts);
+
+ lines.set(userLineNumber, updatedLine); // 替换列表中的旧行
+
+ // 3. 将更新后的内容写回文件
+ if (!writeBack(lines, file)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * 将给定的行列表写回指定的文件。
+ *
+ * @param lines 要写入的行列表
+ * @param file 目标文件
+ * @return 如果写入成功返回 true,否则返回 false
+ */
+ private static boolean writeBack(List lines, File file) {
+ try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
+ for (String line : lines) {
+ writer.write(line);
+ writer.newLine();
+ }
+ } catch (IOException e) {
+ System.err.println("写入文件时出错: " + e.getMessage());
+ return false; // 如果写回失败,认为修改未成功
+ }
+ return true;
+ }
+
+ /**
+ * 在指定文件中查找包含给定用户名的行。
+ *
+ * 此方法会将文件的所有行读入 {@code lines} 列表,并设置
+ * {@code userLine} 和 {@code userLineNumber}。
+ *
+ * @param name 要查找的用户名
+ * @param file 要搜索的文件
+ * @return 如果读取文件过程无异常则返回 true,否则返回 false
+ */
+ private static boolean findUserLine(String name, File file) {
+ try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
+ String line;
+ int currentLineNum = 0;
+ while ((line = reader.readLine()) != null) {
+ lines.add(line);
+ String[] parts = line.split(",");
+ // 假设格式为: username,email,password
+ if (parts.length >= 3 && parts[0].trim().equals(name.trim())) {
+ userLine = line; // 找到用户行
+ userLineNumber = currentLineNum;
+ // break; // 找到后可以退出循环,但为了读取所有行到 lines,不在此处 break
+ }
+ currentLineNum++;
+ }
+ } catch (IOException e) {
+ System.err.println("读取文件时出错: " + e.getMessage());
+ return false;
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ybw/mathapp/util/EmailService.java b/src/main/java/com/ybw/mathapp/util/EmailService.java
new file mode 100644
index 0000000..956d5de
--- /dev/null
+++ b/src/main/java/com/ybw/mathapp/util/EmailService.java
@@ -0,0 +1,250 @@
+package com.ybw.mathapp.util;
+
+import com.ybw.mathapp.config.EmailConfig;
+import jakarta.mail.Authenticator;
+import jakarta.mail.Message;
+import jakarta.mail.MessagingException;
+import jakarta.mail.PasswordAuthentication;
+import jakarta.mail.Session;
+import jakarta.mail.Transport;
+import jakarta.mail.internet.InternetAddress;
+import jakarta.mail.internet.MimeMessage;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Random;
+
+/**
+ * 邮件服务类,用于发送验证码和验证验证码。
+ *
+ *
该类提供生成验证码、发送邮件、验证验证码以及清理过期验证码的功能。
+ * 验证码的有效期由 {@link EmailConfig} 配置。
+ *
+ * @author 杨博文
+ * @since 2025
+ */
+public class EmailService {
+
+ /**
+ * 存储邮箱地址与验证码信息的映射。
+ */
+ private static final Map verificationCodes = new HashMap<>();
+
+ /**
+ * 生成一个6位的随机数字验证码。
+ *
+ * @return 6位数字验证码字符串
+ */
+ public static String generateVerificationCode() {
+ Random random = new Random();
+ int code = 100000 + random.nextInt(900000);
+ return String.valueOf(code);
+ }
+
+ /**
+ * 发送验证码邮件到指定邮箱。
+ *
+ * 此方法使用 {@link EmailConfig} 中的SMTP配置来发送邮件。
+ * 发送成功后,会将验证码和时间戳存储到内存中。
+ *
+ * @param recipientEmail 接收验证码的邮箱地址
+ * @param code 要发送的验证码
+ * @return 如果邮件发送成功返回 true,否则返回 false
+ */
+ public static boolean sendVerificationCode(String recipientEmail, String code) {
+ try {
+ // 创建邮件会话
+ Properties props = new Properties();
+ props.put("mail.smtp.host", EmailConfig.SMTP_HOST);
+ props.put("mail.smtp.port", EmailConfig.SMTP_PORT);
+ props.put("mail.smtp.auth", "true");
+ props.put("mail.smtp.starttls.enable", "true");
+ props.put("mail.smtp.ssl.protocols", "TLSv1.2");
+
+ // 创建认证器
+ Authenticator auth = new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(
+ EmailConfig.SENDER_EMAIL,
+ EmailConfig.SENDER_PASSWORD
+ );
+ }
+ };
+
+ Session session = Session.getInstance(props, auth);
+
+ // 创建邮件消息
+ Message message = new MimeMessage(session);
+ message.setFrom(new InternetAddress(EmailConfig.SENDER_EMAIL));
+ message.setRecipients(Message.RecipientType.TO,
+ InternetAddress.parse(recipientEmail));
+ message.setSubject(EmailConfig.EMAIL_SUBJECT);
+
+ // 创建邮件内容
+ String emailContent = createEmailContent(code);
+ message.setContent(emailContent, "text/html; charset=utf-8");
+
+ // 发送邮件
+ Transport.send(message);
+
+ // 存储验证码信息
+ verificationCodes.put(recipientEmail,
+ new VerificationCodeInfo(code, System.currentTimeMillis()));
+
+ System.out.println("验证码已发送到邮箱: " + recipientEmail);
+ return true;
+
+ } catch (MessagingException e) {
+ System.err.println("发送邮件失败: " + e.getMessage());
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ /**
+ * 创建HTML格式的邮件内容。
+ *
+ * @param code 要嵌入到邮件中的验证码
+ * @return HTML格式的邮件正文字符串
+ */
+ private static String createEmailContent(String code) {
+ return ""
+ + ""
+ + "
"
+ + ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + "
"
+ + "
您好!
"
+ + "
您正在注册账户,验证码如下:
"
+ + "
" + code + "
"
+ + "
验证码有效期为 " + EmailConfig.CODE_EXPIRY_MINUTES + " 分钟,请勿泄露给他人。
"
+ + "
如果这不是您本人的操作,请忽略此邮件。
"
+ + "
"
+ + ""
+ + "
"
+ + ""
+ + "";
+ }
+
+ /**
+ * 验证用户输入的验证码是否正确且未过期。
+ *
+ * @param email 发送验证码的邮箱地址
+ * @param inputCode 用户输入的验证码
+ * @return 如果验证码正确且未过期返回 true,否则返回 false
+ */
+ public static boolean verifyCode(String email, String inputCode) {
+ VerificationCodeInfo codeInfo = verificationCodes.get(email);
+ if (codeInfo == null) {
+ return false;
+ }
+
+ // 检查验证码是否过期
+ long currentTime = System.currentTimeMillis();
+ if (currentTime - codeInfo.timestamp > EmailConfig.CODE_EXPIRY_MINUTES * 60 * 1000) {
+ verificationCodes.remove(email);
+ return false;
+ }
+
+ return codeInfo.code.equals(inputCode);
+ }
+
+ /**
+ * 清理内存中所有已过期的验证码。
+ */
+ public static void cleanupExpiredCodes() {
+ long currentTime = System.currentTimeMillis();
+ Iterator> iterator =
+ verificationCodes.entrySet().iterator();
+
+ while (iterator.hasNext()) {
+ Map.Entry entry = iterator.next();
+ if (currentTime - entry.getValue().timestamp
+ > EmailConfig.CODE_EXPIRY_MINUTES * 60 * 1000) {
+ iterator.remove();
+ }
+ }
+ }
+
+ /**
+ * 验证邮箱地址格式是否正确(简单验证)。
+ *
+ * @param email 待验证的邮箱地址
+ * @return 如果邮箱格式正确返回 true,否则返回 false
+ */
+ public static boolean isValidEmail(String email) {
+ if (email == null || email.isEmpty()) {
+ System.out.println("邮箱地址不能为空!");
+ return false;
+ }
+ if (!(email.contains("@") && email.contains("."))) {
+ System.out.println("邮箱格式不正确!");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * 前端接口:发送验证码到指定邮箱。
+ *
+ * 此方法会生成验证码并尝试发送邮件。
+ *
+ * @param email 接收验证码的邮箱地址
+ * @return 如果验证码生成并发送邮件成功返回 true,否则返回 false
+ */
+ public static boolean sendCode(String email) {
+ // 生成验证码
+ String verificationCode = EmailService.generateVerificationCode();
+ // 尝试发送邮件
+ if (!EmailService.sendVerificationCode(email, verificationCode)) {
+ // 如果发送失败,sendVerificationCode 已经打印了错误信息
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * 存储验证码及其生成时间的内部类。
+ */
+ private static class VerificationCodeInfo {
+
+ /**
+ * 验证码字符串。
+ */
+ String code;
+ /**
+ * 验证码生成的时间戳(毫秒)。
+ */
+ long timestamp;
+
+ /**
+ * 构造一个新的验证码信息对象。
+ *
+ * @param code 验证码
+ * @param timestamp 生成时间戳
+ */
+ VerificationCodeInfo(String code, long timestamp) {
+ this.code = code;
+ this.timestamp = timestamp;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ybw/mathapp/util/Login.java b/src/main/java/com/ybw/mathapp/util/Login.java
new file mode 100644
index 0000000..66a1a7d
--- /dev/null
+++ b/src/main/java/com/ybw/mathapp/util/Login.java
@@ -0,0 +1,32 @@
+package com.ybw.mathapp.util;
+
+/**
+ * 用户登录处理类。
+ *
+ *
该类提供一个静态方法用于验证用户凭据并处理登录逻辑。
+ *
+ * @author 杨博文
+ * @since 2025
+ */
+public class Login {
+
+ /**
+ * 尝试使用给定的用户名和密码进行登录。
+ *
+ *
此方法通过 {@link LoginFileUtils#validateUser(String, String)} 验证用户凭据。
+ * 登录成功或失败时会打印相应的控制台消息。
+ *
+ * @param name 用户名
+ * @param password 用户密码
+ * @return 如果登录成功返回 true,否则返回 false
+ */
+ public static boolean login(String name, String password) {
+ if (LoginFileUtils.validateUser(name, password)) {
+ System.out.println("登录成功!欢迎回来," + name);
+ return true;
+ } else {
+ System.out.println("用户名或密码错误!");
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ybw/mathapp/util/LoginFileUtils.java b/src/main/java/com/ybw/mathapp/util/LoginFileUtils.java
new file mode 100644
index 0000000..e22d705
--- /dev/null
+++ b/src/main/java/com/ybw/mathapp/util/LoginFileUtils.java
@@ -0,0 +1,155 @@
+package com.ybw.mathapp.util;
+
+import com.ybw.mathapp.entity.User;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 用户文件操作工具类。
+ *
+ *
该类负责与用户信息文件 ({@code users.txt}) 进行交互,
+ * 提供读取用户、保存用户、检查用户是否存在以及验证用户登录等功能。
+ *
+ * @author 杨博文
+ * @since 2025
+ */
+public class LoginFileUtils {
+
+ /**
+ * 用户信息文件的路径。
+ */
+ public static final String USER_FILE = "users.txt";
+
+ /**
+ * 从用户文件中读取所有用户信息。
+ *
+ *
如果文件不存在,将尝试创建一个新文件并返回一个空列表。
+ *
+ * @return 包含所有用户对象的列表
+ */
+ public static List readUsers() {
+ List users = new ArrayList<>();
+ File file = new File(USER_FILE);
+
+ if (!file.exists()) {
+ try {
+ file.createNewFile();
+ } catch (IOException e) {
+ System.err.println("创建用户文件失败: " + e.getMessage());
+ }
+ return users;
+ }
+
+ try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (line.isEmpty()) {
+ continue;
+ }
+
+ User user = User.fromString(line);
+ if (user != null) {
+ users.add(user);
+ }
+ }
+ } catch (IOException e) {
+ System.err.println("读取用户文件失败: " + e.getMessage());
+ }
+ return users;
+ }
+
+ /**
+ * 将一个新用户信息追加保存到用户文件中。
+ *
+ * @param user 要保存的用户对象
+ */
+ public static void saveUser(User user) {
+ try (PrintWriter writer = new PrintWriter(new FileWriter(USER_FILE, true))) {
+ writer.println(user.toString());
+ } catch (IOException e) {
+ System.err.println("保存用户信息失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 检查指定的邮箱是否已经注册。
+ *
+ * 比较时不区分大小写。
+ *
+ * @param email 要检查的邮箱地址
+ * @return 如果邮箱已注册返回 true,否则返回 false
+ */
+ public static boolean isEmailRegistered(String email) {
+ List users = readUsers();
+ for (User user : users) {
+ if (user.getEmail().equalsIgnoreCase(email)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 检查指定的用户名是否已经注册。
+ *
+ * 比较时不区分大小写。
+ *
+ * @param name 要检查的用户名
+ * @return 如果用户名已注册返回 true,否则返回 false
+ */
+ public static boolean isNameRegistered(String name) {
+ List users = readUsers();
+ for (User user : users) {
+ if (user.getName().equalsIgnoreCase(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 验证用户登录凭据。
+ *
+ * 支持使用邮箱或用户名进行登录。比较邮箱/用户名时不区分大小写。
+ *
+ * @param emailOrName 用户输入的邮箱或用户名
+ * @param password 用户输入的密码
+ * @return 如果凭据有效返回 true,否则返回 false
+ */
+ public static boolean validateUser(String emailOrName, String password) {
+ List users = readUsers();
+ for (User user : users) {
+ if ((user.getEmail().equalsIgnoreCase(emailOrName)
+ || user.getName().equalsIgnoreCase(emailOrName))
+ && user.getPassword().equals(password)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 根据邮箱地址查找对应的用户名。
+ *
+ * 比较邮箱时不区分大小写。
+ *
+ * @param email 要查找的邮箱地址
+ * @return 如果找到用户则返回其用户名,否则返回 null
+ */
+ public static String emailFindName(String email) {
+ List users = readUsers();
+ for (User user : users) {
+ if (user.getEmail().equalsIgnoreCase(email)) {
+ return user.getName();
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ybw/mathapp/util/Register.java b/src/main/java/com/ybw/mathapp/util/Register.java
new file mode 100644
index 0000000..02d4e5f
--- /dev/null
+++ b/src/main/java/com/ybw/mathapp/util/Register.java
@@ -0,0 +1,70 @@
+package com.ybw.mathapp.util;
+
+import com.ybw.mathapp.entity.User;
+import java.util.regex.Pattern;
+
+
+/**
+ * 用户注册处理类。
+ *
+ * 该类提供用户注册、密码格式验证和两次密码匹配验证等功能。
+ *
+ * @author 杨博文
+ * @since 2025
+ */
+public class Register {
+
+ /**
+ * 完成用户注册流程。
+ *
+ *
此方法会创建一个新的用户对象并将其保存到用户文件中。
+ *
+ * @param name 用户名
+ * @param email 用户邮箱
+ * @param password1 用户密码
+ * @return 注册成功返回 true(当前实现总是返回 true,即使保存可能失败)
+ */
+ public static boolean register(String name, String email, String password1) {
+ User user = new User(name, email, password1);
+ LoginFileUtils.saveUser(user);
+ System.out.println("注册成功!您可以使用邮箱和密码登录了。");
+ return true;
+ }
+
+ /**
+ * 验证密码格式是否符合要求。
+ *
+ *
密码要求:
+ *
+ * - 长度为 6 到 10 个字符
+ * - 只能包含字母和数字
+ * - 必须同时包含至少一个大写字母、一个小写字母和一个数字
+ *
+ *
+ * @param password1 待验证的密码
+ * @return 如果密码格式符合要求返回 true,否则返回 false
+ */
+ public static boolean isVaildPassword(String password1) {
+ // 使用正则表达式验证:长度6-10,只包含字母数字,且包含大小写字母和数字
+ String regex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{6,10}$";
+ if (!Pattern.matches(regex, password1)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * 验证两次输入的密码是否一致。
+ *
+ * @param password1 第一次输入的密码
+ * @param password2 第二次输入的密码
+ * @return 如果两次密码相同返回 true,否则返回 false 并打印错误信息
+ */
+ public static boolean isEqualPassword(String password1, String password2) {
+ if (!password1.equals(password2)) {
+ System.out.println("两次输入的密码不一致!");
+ return false;
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/META-INF/javamail.default.address.map b/src/main/resources/META-INF/javamail.default.address.map
new file mode 100644
index 0000000..e3baea0
--- /dev/null
+++ b/src/main/resources/META-INF/javamail.default.address.map
@@ -0,0 +1,2 @@
+rfc822=smtp
+smtp=smtp
\ No newline at end of file