From 62c59450f5fb0559fcdf6f81fb5b5b41d27e40e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E5=8D=9A=E6=96=87?= <15549487+FX_YBW@user.noreply.gitee.com> Date: Mon, 6 Oct 2025 20:01:47 +0800 Subject: [PATCH 01/16] =?UTF-8?q?v1.0=20=E9=A2=98=E7=9B=AE=E5=A4=84?= =?UTF-8?q?=E7=90=86=E9=80=BB=E8=BE=91=E8=BF=98=E6=B2=A1=E5=BC=84=E5=A5=BD?= =?UTF-8?q?=EF=BC=8C=E6=9C=AA=E4=B8=8E=E5=89=8D=E7=AB=AF=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/uiDesigner.xml | 124 +++++++++++ pom.xml | 5 + .../com/ybw/mathapp/LoginAndRegister.java | 132 ++++++++++++ .../com/ybw/mathapp/config/EmailConfig.java | 18 ++ .../java/com/ybw/mathapp/entity/User.java | 95 +++++++++ .../com/ybw/mathapp/service/Caculate.java | 198 ++++++++++++++++++ .../ybw/mathapp/service/ChoiceGenerator.java | 191 +++++++++++++++++ .../com/ybw/mathapp/service/FileHandler.java | 69 ++++++ .../mathapp/service/JuniorHighGenerator.java | 187 +++++++++++++++++ .../service/PrimarySchoolGenerator.java | 121 +++++++++++ .../mathapp/service/QuestionDeduplicator.java | 91 ++++++++ .../mathapp/service/QuestionGenerator.java | 28 +++ .../mathapp/service/QuestionWithAnswer.java | 31 +++ .../mathapp/service/SeniorHighGenerator.java | 126 +++++++++++ .../ybw/mathapp/service/StartController.java | 132 ++++++++++++ .../com/ybw/mathapp/system/LogSystem.java | 39 ++++ .../com/ybw/mathapp/util/EmailService.java | 158 ++++++++++++++ .../com/ybw/mathapp/util/LoginFileUtils.java | 74 +++++++ src/main/java/module-info.java | 1 + .../META-INF/javamail.default.address.map | 2 + 20 files changed, 1822 insertions(+) create mode 100644 .idea/uiDesigner.xml create mode 100644 src/main/java/com/ybw/mathapp/LoginAndRegister.java create mode 100644 src/main/java/com/ybw/mathapp/config/EmailConfig.java create mode 100644 src/main/java/com/ybw/mathapp/entity/User.java create mode 100644 src/main/java/com/ybw/mathapp/service/Caculate.java create mode 100644 src/main/java/com/ybw/mathapp/service/ChoiceGenerator.java create mode 100644 src/main/java/com/ybw/mathapp/service/FileHandler.java create mode 100644 src/main/java/com/ybw/mathapp/service/JuniorHighGenerator.java create mode 100644 src/main/java/com/ybw/mathapp/service/PrimarySchoolGenerator.java create mode 100644 src/main/java/com/ybw/mathapp/service/QuestionDeduplicator.java create mode 100644 src/main/java/com/ybw/mathapp/service/QuestionGenerator.java create mode 100644 src/main/java/com/ybw/mathapp/service/QuestionWithAnswer.java create mode 100644 src/main/java/com/ybw/mathapp/service/SeniorHighGenerator.java create mode 100644 src/main/java/com/ybw/mathapp/service/StartController.java create mode 100644 src/main/java/com/ybw/mathapp/system/LogSystem.java create mode 100644 src/main/java/com/ybw/mathapp/util/EmailService.java create mode 100644 src/main/java/com/ybw/mathapp/util/LoginFileUtils.java create mode 100644 src/main/resources/META-INF/javamail.default.address.map diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9d7a90b..dd404b3 100644 --- a/pom.xml +++ b/pom.xml @@ -111,6 +111,11 @@ ${junit.version} test + + com.sun.mail + jakarta.mail + 2.0.1 + diff --git a/src/main/java/com/ybw/mathapp/LoginAndRegister.java b/src/main/java/com/ybw/mathapp/LoginAndRegister.java new file mode 100644 index 0000000..b4b83c8 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/LoginAndRegister.java @@ -0,0 +1,132 @@ +package com.ybw.mathapp; + +// UserService.java +import com.ybw.mathapp.entity.User; +import com.ybw.mathapp.util.EmailService; +import com.ybw.mathapp.util.LoginFileUtils; +import java.util.Scanner; +import java.util.regex.Pattern; + +public class LoginAndRegister { + private static Scanner scanner = new Scanner(System.in); + + // UserService.java 中的注册方法更新 + public static boolean register() { + System.out.println("\n=== 用户注册 ==="); + + // 输入邮箱 + System.out.print("请输入邮箱地址: "); + String email = scanner.nextLine().trim(); + + if (!isValidEmail(email)) { + return false; + } + + if (LoginFileUtils.isEmailRegistered(email)) { + System.out.println("该邮箱已注册,请直接登录!"); + return false; + } + + // 发送、验证验证码 + if (!sendAndVerifyCode(email)) { + return false; + } + + // 设置密码(其余代码保持不变) + System.out.print("请输入密码: "); + String password1 = scanner.nextLine(); + System.out.print("请再次输入密码: "); + String password2 = scanner.nextLine(); + if(!isVaildPassword(password1, password2)) { + return false; + } + + User user = new User(email, password1); + LoginFileUtils.saveUser(user); + System.out.println("注册成功!您可以使用邮箱和密码登录了。"); + return true; + } + + // 登录流程 + public static boolean login() { + System.out.println("\n=== 用户登录 ==="); + + System.out.print("请输入邮箱: "); + String email = scanner.nextLine().trim(); + + System.out.print("请输入密码: "); + String password = scanner.nextLine(); + + if (LoginFileUtils.validateUser(email, password)) { + System.out.println("登录成功!欢迎回来," + email); + return true; + } else { + System.out.println("邮箱或密码错误!"); + return false; + } + } + + // + /** + * 邮箱格式验证 + * @param email 待验证的邮箱地址 + * @return true表示邮箱格式正确,false表示邮箱格式错误 + */ + private static boolean isValidEmail(String email) { + if (email.isEmpty()) { + System.out.println("邮箱地址不能为空!"); + return false; + } + if (!(email.contains("@") && email.contains("."))) { + System.out.println("邮箱格式不正确!"); + return false; + } + return true; + } + + /** + * 密码格式验证 + * @param password1 第一次输入的密码 + * @param password2 第二次输入的密码 + * @return true表示符合要求,false表示不符合 + */ + public static boolean isVaildPassword(String password1, String password2) { + if (password1 == null || password1.length() < 6 || password1.length() > 10) { + return false; + } + + // 使用正则表达式验证:长度6-10,只包含字母数字,且包含大小写字母和数字 + String regex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{6,10}$"; + if (!Pattern.matches(regex, password1)) { + return false; + } + + System.out.print("请再次输入密码: "); + if (!password1.equals(password2)) { + System.out.println("两次输入的密码不一致!"); + return false; + } + return true; + } + + public static boolean sendAndVerifyCode(String email) { + // 发送真实邮件验证码 + String verificationCode = EmailService.generateVerificationCode(); + System.out.println("正在发送验证码邮件,请稍候..."); + + if (!EmailService.sendVerificationCode(email, verificationCode)) { + System.out.println("发送验证码失败,请检查邮箱配置或稍后重试!"); + return false; + } + + // 验证验证码 + System.out.print("请输入收到的验证码: "); + String inputCode = scanner.nextLine().trim(); + + if (!EmailService.verifyCode(email, inputCode)) { + System.out.println("验证码错误或已过期!"); + return false; + } + return true; + } +} diff --git a/src/main/java/com/ybw/mathapp/config/EmailConfig.java b/src/main/java/com/ybw/mathapp/config/EmailConfig.java new file mode 100644 index 0000000..8331ea5 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/config/EmailConfig.java @@ -0,0 +1,18 @@ +package com.ybw.mathapp.config; + +public class EmailConfig { + // 发件人邮箱配置(以QQ邮箱为例) + public static final String SMTP_HOST = "smtp.qq.com"; + public static final String SMTP_PORT = "587"; + public static final String SENDER_EMAIL = "1798231811@qq.com"; // 替换为你的邮箱 + public static final String SENDER_PASSWORD = "dzmfirotgnlceeae"; // 替换为你的授权码 + + // 如果使用Gmail + // public static final String SMTP_HOST = "smtp.gmail.com"; + // public static final String SMTP_PORT = "587"; + // public static final String SENDER_EMAIL = "your_email@gmail.com"; + // public static final String SENDER_PASSWORD = "your_app_password"; + + public static final String EMAIL_SUBJECT = "【用户注册】验证码"; + public static final int CODE_EXPIRY_MINUTES = 5; +} diff --git a/src/main/java/com/ybw/mathapp/entity/User.java b/src/main/java/com/ybw/mathapp/entity/User.java new file mode 100644 index 0000000..91e46c0 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/entity/User.java @@ -0,0 +1,95 @@ +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 email 邮箱,不能为空 用户名,不能为空 + * @param password 用户密码,不能为空 + */ + public User(String email, String password) { + this.password = password; + this.email = email; + } + + /** + * 获取用户密码。 + * + * @return 用户密码 + */ + public String getPassword() { + return password; + } + + /** + * 获取用户当前的学习级别。 + * + * @return 用户学习级别 + */ + public String getLevel() { + return level; + } + + /** + * 获取用户邮箱。 + * + * @return 用户邮箱 + */ + public String getEmail() { + return email; + } + + /** + * 设置用户的学习级别。 + * + * @param newLevel 新的学习级别,支持"小学"、"初中"、"高中" + */ + public void setLevel(String newLevel) { + level = newLevel; + } + + /** + * 保存邮箱+密码。 + * + * @return 邮箱+密码 + */ + @Override + public String toString() { + return email + "," + password; + } + + public static User fromString(String line) { + if (line == null || line.trim().isEmpty()) { + return null; + } + + String[] parts = line.split(",", 2); // 最多分割成2部分 + if (parts.length == 2) { + return new User(parts[0].trim(), parts[1].trim()); + } + return null; + } +} diff --git a/src/main/java/com/ybw/mathapp/service/Caculate.java b/src/main/java/com/ybw/mathapp/service/Caculate.java new file mode 100644 index 0000000..9592645 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/Caculate.java @@ -0,0 +1,198 @@ +package com.ybw.mathapp.service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +/** + * 用于计算包含四则运算、括号、平方、开根号、三角函数的表达式。 + * 注意:三角函数的输入通常认为是度数 (Degree)。 + */ +public class Caculate { + + /** + * 计算表达式的值。 + * @param parts 表达式分解后的列表,例如 ["(", "2", "+", "3", ")", "*", "4"] + * @return 计算结果 + */ + public double caculate(List parts) { + // 将中缀表达式转换为后缀表达式(逆波兰表示法) + List postfix = infixToPostfix(parts); + // 计算后缀表达式的值 + return evaluatePostfix(postfix); + } + + private List infixToPostfix(List infix) { + Map precedence = new HashMap<>(); + precedence.put("+", 1); + precedence.put("-", 1); + precedence.put("*", 2); + precedence.put("/", 2); + // "平方" 和 "开根号" 作为后缀运算符,优先级最高 + // "sin", "cos", "tan" 作为前缀函数,优先级也很高 + // 这里简化处理,将它们都视为高优先级,但需要特殊处理其结合性 + // 实际上,"平方", "开根号" 是后缀一元运算符 + // "sin", "cos", "tan" 是前缀一元函数 + // 标准的调度场算法需要扩展来处理一元运算符和函数 + // 为了简化,我们假设它们的优先级为 3 + precedence.put("平方", 3); + precedence.put("开根号", 3); + precedence.put("sin", 3); + precedence.put("cos", 3); + precedence.put("tan", 3); + + Stack operatorStack = new Stack<>(); + List postfix = new ArrayList<>(); + + for (String token : infix) { + if (isNumeric(token)) { + postfix.add(token); + } else if ("(".equals(token)) { + operatorStack.push(token); + } else if (")".equals(token)) { + while (!operatorStack.isEmpty() && !"(".equals(operatorStack.peek())) { + postfix.add(operatorStack.pop()); + } + if (!operatorStack.isEmpty()) { // Pop the '(' + operatorStack.pop(); + } + } else if (precedence.containsKey(token)) { + // 处理运算符和函数 + // 对于前缀函数 (sin, cos, tan),它们没有左操作数,直接入栈 + // 对于后缀运算符 (平方, 开根号),它们作用于前面的一个操作数 + // 这里简化处理,将它们都当作普通二元运算符入栈,然后在计算时特殊处理 + // 实际上,对于后缀运算符,应该立即处理它前面的一个操作数 + // 对于前缀函数,应该立即处理它后面的一个操作数 + // 这需要修改调度场算法或在 evaluatePostfix 中处理 + // 最好的方式是:在生成器生成时,将 "x平方" -> ["x", "平方"],将 "sin(x)" -> ["sin", "x"] + // 这样 "平方", "开根号" 就是后缀,"sin", "cos", "tan" 就是前缀 + // 但生成器可能生成 "开根号(x)" 或 "(x)平方" + // 这使得解析变得复杂 + // 我们尝试一种方法:在解析时,如果遇到 "开根号",它后面必须跟 "(" 和表达式 ")" + // 将 "开根号(...)" 视为一个整体 token + // 同理,"sin(...)", "cos(...)", "tan(...)" 也是如此 + // 或者,在调度场算法中,当遇到 "开根号", "平方" 时,它们是后缀,立即处理 + // 当遇到 "sin", "cos", "tan" 时,它们是前缀,立即处理 + // 这需要修改算法 + // 一个简化的处理方法是:假设 "开根号", "平方" 总是作用于紧随其后的表达式(可能用括号包围) + // "sin", "cos", "tan" 也是作用于紧随其后的表达式(通常用括号包围) + // 但这与标准的 "x平方" 或 "开根号(x)" 不同 + // 标准的 "x平方" -> ["x", "平方"] (后缀) + // 标准的 "sin(x)" -> ["sin", "x"] (前缀) + // 生成器生成的 "开根号(16)" -> ["开根号", "(", "16", ")"] + // 生成器生成的 "(2+3)平方" -> ["(", "2", "+", "3", ")", "平方"] + // 生成器生成的 "sin 30" -> ["sin", "30"] (如果生成器是这样分词的) + // 生成器生成的 "sin(30)" -> ["sin", "(", "30", ")"] (如果生成器是这样分词的) + // 这里我们假设 parts 已经是标准的后缀/前缀形式,或者调度场算法能处理 "开根号", "平方", "sin", "cos", "tan" 作为特殊运算符 + // 我们尝试一个近似方法:将 "开根号", "平方", "sin", "cos", "tan" 视为高优先级的特殊运算符 + // 在调度场算法中,遇到它们就立即处理(如果它们作用于前面或后面的操作数) + // 这在某些复杂嵌套情况下可能不准确,但对于大多数情况应该足够 + // 更好的方法是使用 Shunting-yard 的扩展版本,或者使用递归下降解析器 + + // 暂时按照标准调度场算法处理,但在 evaluatePostfix 中特殊处理 + // 将 "平方", "开根号", "sin", "cos", "tan" 放入运算符栈 + while (!operatorStack.isEmpty() && + precedence.containsKey(operatorStack.peek()) && + precedence.get(operatorStack.peek()) >= precedence.get(token)) { + postfix.add(operatorStack.pop()); + } + operatorStack.push(token); + } else { + // 其他token,例如数字的一部分(如果格式错误) + throw new IllegalArgumentException("Unknown token: " + token); + } + } + + while (!operatorStack.isEmpty()) { + postfix.add(operatorStack.pop()); + } + + return postfix; + } + + private double evaluatePostfix(List postfix) { + Stack stack = new Stack<>(); + for (String token : postfix) { + if (isNumeric(token)) { + stack.push(Double.parseDouble(token)); + } else { + double result = 0; + double operand = 0; // 用于一元运算符 + double operand2 = 0; // 用于二元运算符 + double operand1 = 0; // 用于二元运算符 + + switch (token) { + case "+": + operand2 = stack.pop(); + operand1 = stack.pop(); + stack.push(operand1 + operand2); + break; + case "-": + operand2 = stack.pop(); + operand1 = stack.pop(); + stack.push(operand1 - operand2); + break; + case "*": + operand2 = stack.pop(); + operand1 = stack.pop(); + stack.push(operand1 * operand2); + break; + case "/": + operand2 = stack.pop(); + operand1 = stack.pop(); + if (operand2 == 0) { + throw new ArithmeticException("Division by zero"); + } + stack.push(operand1 / operand2); + break; + case "平方": // 一元后缀运算符 + operand = stack.pop(); + stack.push(operand * operand); + break; + case "开根号": // 一元后缀运算符 + operand = stack.pop(); + if (operand < 0) { + throw new ArithmeticException("Square root of negative number"); + } + stack.push(Math.sqrt(operand)); + break; + case "sin": // 一元前缀函数 + operand = stack.pop(); + // 假设输入是度数 (Degree) + stack.push(Math.sin(Math.toRadians(operand))); + break; + case "cos": // 一元前缀函数 + operand = stack.pop(); + // 假设输入是度数 (Degree) + stack.push(Math.cos(Math.toRadians(operand))); + break; + case "tan": // 一元前缀函数 + operand = stack.pop(); + // 假设输入是度数 (Degree) + stack.push(Math.tan(Math.toRadians(operand))); + break; + default: + throw new IllegalArgumentException("Unknown operator in postfix: " + token); + } + } + } + if (stack.size() != 1) { + throw new IllegalStateException("Invalid expression evaluation - stack size: " + stack.size()); + } + return stack.peek(); + } + + private 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/ChoiceGenerator.java b/src/main/java/com/ybw/mathapp/service/ChoiceGenerator.java new file mode 100644 index 0000000..dae8fc3 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/ChoiceGenerator.java @@ -0,0 +1,191 @@ +package com.ybw.mathapp.service; + +// File: mathpuzzle/service/MultipleChoiceGenerator.java +import java.util.*; + +/** + * 选择题生成器,负责为给定的题目生成器生成的题目添加选项,并在本次生成中去重。 + * + *

该生成器会调用传入的 {@link QuestionGenerator} 生成原始题目, + * 计算正确答案,并生成指定数量的干扰项,最终形成选择题。 + * 干扰项的生成方式是基于正确答案添加一个随机的小误差。 + * 查重逻辑确保本次生成的题目列表中没有重复。 + * + * @author 你的名字 + * @version 1.0 + * @since 2025 + */ +public class ChoiceGenerator { + + private final QuestionGenerator generator; + private final Random random = new Random(); + private static final int OPTIONS_COUNT = 4; // 默认选项数量 + + /** + * 构造函数,指定题目生成器。 + * + * @param generator 用于生成题目的 {@link QuestionGenerator} 实例。 + */ + public ChoiceGenerator(QuestionGenerator generator) { + this.generator = generator; + } + + /** + * 生成指定数量的选择题。 + * 该方法会生成原始题目和答案,然后为每道题生成干扰项, + * 并将选项打乱顺序。同时,确保生成的题目在本次调用中不重复。 + * + * @param count 需要生成的选择题数量。 + * @return 包含选择题的列表,每个元素包含题目、选项和正确答案索引。 + */ + public List generateMultipleChoiceQuestions(int count) { + List mcQuestions = new ArrayList<>(); + Set generatedQuestionTexts = new HashSet<>(); // 用于本次生成过程中的查重 + Caculate calculator = new Caculate(); // 使用计算器实例 + + int attempts = 0; + int maxAttempts = count * 100; // 设置最大尝试次数,防止无限循环 + + while (mcQuestions.size() < count && attempts < maxAttempts) { + attempts++; + // 生成原始题目 + List rawQuestions = generator.generateQuestions(1); + if (rawQuestions.isEmpty()) { + continue; // 如果生成器返回空,跳过 + } + + String rawQuestion = rawQuestions.get(0); + String questionTextForDedup = rawQuestion.endsWith(" =") ? + rawQuestion.substring(0, rawQuestion.length() - 2) : rawQuestion; + + // 检查是否重复 + if (generatedQuestionTexts.contains(questionTextForDedup)) { + continue; // 如果重复,重新生成 + } + + // 计算正确答案 + double correctAnswer = calculateAnswer(rawQuestion, calculator); + if (Double.isNaN(correctAnswer) || Double.isInfinite(correctAnswer)) { + continue; // 如果计算出错,跳过这道题 + } + + // 生成选项 + List options = generateOptions(correctAnswer); + // 随机打乱选项 + Collections.shuffle(options); + // 找到正确答案在打乱后列表中的索引 + int correctIndex = options.indexOf(correctAnswer); + + // 添加到结果列表和查重集合 + mcQuestions.add(new MultipleChoiceQuestion(rawQuestion, options, correctIndex)); + generatedQuestionTexts.add(questionTextForDedup); + } + + if (mcQuestions.size() < count) { + System.out.println("警告:在尝试了 " + maxAttempts + " 次后,仅生成了 " + mcQuestions.size() + " 道不重复的选择题。"); + } + + return mcQuestions; + } + + /** + * 计算给定题目的答案。 + * @param question 题目字符串,例如 "2 + 3 * 4 =" + * @param calc 计算器实例 + * @return 计算得出的答案。 + */ + private double calculateAnswer(String question, Caculate calc) { + // 移除 " =" + String expression = question.substring(0, question.length() - 2).trim(); + // 将表达式字符串分割成部分 + // 这里需要根据 QuestionGenerator 生成的格式来决定如何分割 + // 如果 QuestionGenerator 生成的是 "开根号(16)" 或 "(2+3)平方",则按空格分割可能不够 + // 但通常,生成器会生成 "开根号 ( 16 )" 或 "开根号 16" 或 "( 2 + 3 ) 平方" 这样的格式 + // 按空格分割 ["开根号", "(", "16", ")"] 或 ["开根号", "16"] 或 ["(", "2", "+", "3", ")", "平方"] + // CaculatePrimary 需要能处理这些格式 + List parts = Arrays.asList(expression.split("\\s+")); // 按空格分割 + + try { + return calc.caculate(parts); + } catch (Exception e) { + System.err.println("计算表达式失败: " + expression + ", 错误: " + e.getMessage()); + return Double.NaN; // 或者抛出异常 + } + } + + /** + * 生成选项列表,包含一个正确答案和若干干扰项。 + * + * @param correctAnswer 正确答案。 + * @return 包含正确答案和干扰项的列表。 + */ + private List generateOptions(double correctAnswer) { + List options = new ArrayList<>(); + options.add(correctAnswer); // 添加正确答案 + + for (int i = 1; i < OPTIONS_COUNT; i++) { + // 生成干扰项:在正确答案基础上加一个随机误差 + // 误差范围可以根据需要调整,例如 +/- 10% 或固定范围 + double error = correctAnswer * (random.nextDouble() * 0.2 - 0.1); // +/- 10% 的误差 + // 为了避免干扰项过于接近或重复,可以添加一些逻辑 + double incorrectAnswer = correctAnswer + error; + // 确保干扰项不等于正确答案,且不重复 + while (Math.abs(incorrectAnswer - correctAnswer) < 0.001 || options.contains(incorrectAnswer)) { // 使用一个小的容差比较 + error = correctAnswer * (random.nextDouble() * 0.2 - 0.1); + incorrectAnswer = correctAnswer + error; + // 防止无限循环,如果误差太小,强制加一个最小值 + if (Math.abs(error) < 0.001) { + incorrectAnswer = correctAnswer + (random.nextBoolean() ? 0.01 : -0.01); + } + } + options.add(incorrectAnswer); + } + + return options; + } + + /** + * 用于封装一道选择题的内部类。 + */ + public static class MultipleChoiceQuestion { + private final String question; // 题目文本 + private final List options; // 选项列表 + private final int correctIndex; // 正确选项的索引 + + public MultipleChoiceQuestion(String question, List options, int correctIndex) { + this.question = question; + this.options = new ArrayList<>(options); // 创建副本以防外部修改 + this.correctIndex = correctIndex; + } + + public String getQuestion() { + return question; + } + + public List getOptions() { + return new ArrayList<>(this.options); // 返回副本 + } + + public int getCorrectIndex() { + return correctIndex; + } + + public Double getCorrectAnswer() { + if (correctIndex >= 0 && correctIndex < options.size()) { + return options.get(correctIndex); + } + return null; // 或抛出异常 + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Question: ").append(question).append("\n"); + for (int i = 0; i < options.size(); i++) { + sb.append((char)('A' + i)).append(". ").append(options.get(i)).append("\n"); + } + sb.append("Correct Answer: ").append((char)('A' + correctIndex)).append(" (").append(options.get(correctIndex)).append(")\n"); + return sb.toString(); + } + } +} diff --git a/src/main/java/com/ybw/mathapp/service/FileHandler.java b/src/main/java/com/ybw/mathapp/service/FileHandler.java new file mode 100644 index 0000000..8cd486a --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/FileHandler.java @@ -0,0 +1,69 @@ +package com.ybw.mathapp.service; + + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import mathpuzzle.entity.User; + +/** + * 文件处理器,负责创建用户目录和保存试卷文件。 + * + *

该类提供用户目录管理功能和试卷文件保存功能。 + * 每次生成的试卷会以时间戳命名保存到对应用户的目录中。 + * + * @author 杨博文 + * @version 1.0 + * @since 2025 + */ +public class FileHandler { + + /** + * 确保用户目录存在,如果不存在则创建。 + * + *

该方法检查用户对应的目录是否存在,如果不存在则创建该目录。 + * 用户目录以用户名命名,位于当前工作目录下。 + * + * @param user 需要创建目录的用户对象 + * @throws IOException 当目录创建过程中发生错误时抛出 + */ + public void ensureUserDirectory(User user) throws IOException { + String dirPath = "./" + user.getName(); + Path path = Paths.get(dirPath); + if (!Files.exists(path)) { + Files.createDirectories(path); + } + } + + /** + * 保存试卷到用户目录中。 + * + *

该方法将生成的题目列表保存到文件中,文件名包含时间戳信息, + * 以确保每次生成的试卷都有唯一的文件名。每个题目前添加题号, 题目之间用空行分隔。 + * + * @param user 试卷所属的用户对象 + * @param questions 需要保存的题目列表 + * @throws IOException 当文件写入过程中发生错误时抛出 + */ + public void savePaper(User user, List questions) throws IOException { + ensureUserDirectory(user); + // 生成文件名:年-月-日-时-分-秒.txt + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"); + String fileName = LocalDateTime.now().format(formatter) + ".txt"; + String filePath = "./" + user.getName() + "/" + fileName; + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) { + for (int i = 0; i < questions.size(); i++) { + writer.write((i + 1) + ". " + questions.get(i)); // 添加题号 + writer.newLine(); + writer.newLine(); // 每题之间空一行 + } + } + } +} \ 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..11bc7b1 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/JuniorHighGenerator.java @@ -0,0 +1,187 @@ +package com.ybw.mathapp.service; + +import static com.ybw.mathapp.service.PrimarySchoolGenerator.isNumeric; +import static com.ybw.mathapp.service.PrimarySchoolGenerator.isNumeric; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * 初中题目生成器,负责生成包含平方或开根号运算的初中级别数学题目。 + * + *

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

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

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

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

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

该方法在表达式指定位置添加平方运算,对一段子表达式进行平方运算。 + * + * @param parts 包含表达式各部分的列表 + * @param i 平方运算的起始位置 + * @return 添加了平方运算的表达式列表 + */ + public List generateSquare(List parts, int i) { + parts.set(i, "(" + parts.get(i)); + // 为避免随机数上限出现0,此处要单独判断一下左括号正好括住倒数第二个操作数的情况 + if (i == parts.size() - 3) { + parts.set(parts.size() - 1, parts.get(parts.size() - 1) + ")"); + } else { + while (true) { + int i2 = random.nextInt(parts.size() - 3 - i) + 2; + if (isNumeric(parts.get(i + i2))) { + parts.set(i + i2, parts.get(i + i2) + ")平方"); + break; + } + } + } + return parts; + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/service/PrimarySchoolGenerator.java b/src/main/java/com/ybw/mathapp/service/PrimarySchoolGenerator.java new file mode 100644 index 0000000..e70946f --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/PrimarySchoolGenerator.java @@ -0,0 +1,121 @@ +package com.ybw.mathapp.service; + +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() { + Caculate caculate = new Caculate(); + int operandCount = random.nextInt(4) + 2; // 2-5个操作数 + List parts = new ArrayList<>(); + while (true) { + // 生成基础操作 + parts = generateBase(operandCount, parts); + // 简单添加括号逻辑:随机加一个括号 + if (operandCount > 2 && random.nextBoolean()) { + // 遍历查找左括号的合理位置 + for (int i = 0; i < parts.size() - 2; i++) { + // 该位置要为操作数且随机添加括号 + if (isNumeric(parts.get(i)) && random.nextBoolean()) { + parts.add(i, "("); + i++; + // 为避免随机数上限出现0,此处要单独判断一下左括号正好括住倒数第二个操作数的情况 + if (i == parts.size() - 3) { + parts.add(")"); + } else { + while (true) { + int i2 = random.nextInt(parts.size() - 3 - i) + 2; + if (isNumeric(parts.get(i + i2))) { + parts.add(i + i2 + 1, ")"); + break; + } + } + } + break; + } + } + } + if (caculate.caculate(parts) >= 0) { + return String.join(" ", parts) + " ="; + } else { + parts.clear(); + } + } + } + + /** + * 判断给定字符串是否为数字。 + * + *

该方法检查字符串是否可以转换为数字格式。 + * + * @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; + } + } + + /** + * 生成基本的四则运算表达式部分。 + * + *

该方法生成指定数量的操作数和运算符,构成基础的数学表达式。 + * + * @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/QuestionDeduplicator.java b/src/main/java/com/ybw/mathapp/service/QuestionDeduplicator.java new file mode 100644 index 0000000..b4319ee --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/QuestionDeduplicator.java @@ -0,0 +1,91 @@ +package com.ybw.mathapp.service; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import mathpuzzle.entity.User; + +/** + * 题目查重器,负责加载历史题目并检查新题目是否重复。 + * + *

该类维护一个题目集合,用于检测当前生成的题目是否与用户的历史题目重复。 + * 通过加载用户目录下的所有历史试卷文件,提取题目内容进行去重检查。 + * + * @author 杨博文 + * @version 1.0 + * @since 2025 + */ +public class QuestionDeduplicator { + + /** + * 存储用户历史题目的集合。 + */ + private final Set existingQuestions = new HashSet<>(); + + /** + * 加载指定用户的所有历史题目。 + * + *

该方法遍历用户目录下的所有.txt文件,读取并解析题目内容, + * 将历史题目添加到去重集合中。此操作会清空之前的题目记录。 + * + * @param user 需要加载历史题目的用户对象 + */ + public void loadExistingQuestions(User user) { + existingQuestions.clear(); + String userDir = "./" + user.getName(); + File dir = new File(userDir); + + if (!dir.exists()) { + return; // 目录不存在,无历史题目 + } + + File[] files = dir.listFiles((d, name) -> name.endsWith(".txt")); + if (files == null) { + return; + } + + for (File file : files) { + try (BufferedReader br = new BufferedReader(new FileReader(file))) { + String line; + while ((line = br.readLine()) != null) { + // 只加载题目行,忽略题号和空行 + if (line.trim().isEmpty() || line.matches("\\d+\\. .*")) { + String questionContent = line.replaceFirst("\\d+\\. ", "").trim(); + if (!questionContent.isEmpty() && !questionContent.equals("=")) { + existingQuestions.add(questionContent); + } + } + } + } catch (IOException e) { + System.err.println("读取历史文件时出错: " + file.getName()); + } + } + } + + /** + * 检查指定题目是否为重复题目。 + * + *

该方法检查给定的题目是否已经存在于历史题目集合中。 + * + * @param question 待检查的题目内容 + * @return 如果题目重复则返回true,否则返回false + */ + public boolean isDuplicate(String question) { + return existingQuestions.contains(question); + } + + /** + * 将新题目添加到去重集合中。 + * + *

该方法将当前生成的题目添加到去重集合中,用于防止 + * 在同一次生成过程中出现重复题目。 + * + * @param question 需要添加的题目内容 + */ + public void addQuestion(String question) { + existingQuestions.add(question); + } +} \ 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..104466d --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/QuestionGenerator.java @@ -0,0 +1,28 @@ +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/QuestionWithAnswer.java b/src/main/java/com/ybw/mathapp/service/QuestionWithAnswer.java new file mode 100644 index 0000000..336a461 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/QuestionWithAnswer.java @@ -0,0 +1,31 @@ +package com.ybw.mathapp.service; + +// File: mathpuzzle/entity/QuestionWithAnswer.java +/** + * 用于封装一道题目及其正确答案。 + */ +public class QuestionWithAnswer { + private final String question; // 题目字符串,例如 "2 + 3 =" + private final double correctAnswer; // 计算得出的正确答案 + + public QuestionWithAnswer(String question, double correctAnswer) { + this.question = question; + this.correctAnswer = correctAnswer; + } + + public String getQuestion() { + return question; + } + + public double getCorrectAnswer() { + return correctAnswer; + } + + @Override + public String toString() { + return "QuestionWithAnswer{" + + "question='" + question + '\'' + + ", correctAnswer=" + correctAnswer + + '}'; + } +} 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..074e5d7 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/SeniorHighGenerator.java @@ -0,0 +1,126 @@ +package com.ybw.mathapp.service; + +import static mathpuzzle.service.PrimarySchoolGenerator.isNumeric; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * 高中题目生成器,负责生成包含三角函数运算的高中级别数学题目。 + * + *

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

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

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

该方法在表达式指定位置添加三角函数运算,可能只对单个操作数 + * 进行三角函数运算,或者对一段子表达式进行三角函数运算。 + * + * @param parts 包含表达式各部分的列表 + * @param i 三角函数运算的位置 + * @return 添加了三角函数运算的表达式列表 + */ + // 产生三角函数运算符 + public List generateTrig(List parts, int i) { + String trigOp = TRIG_FUNCS[random.nextInt(TRIG_FUNCS.length)]; + if (random.nextBoolean()) { + parts.set(i, trigOp + parts.get(i)); + } else { + parts.set(i, trigOp + "(" + parts.get(i)); + // 为避免随机数上限出现0,此处要单独判断一下左括号正好括住倒数第二个操作数的情况 + if (i == parts.size() - 3) { + parts.set(parts.size() - 1, parts.get(parts.size() - 1) + ")"); + } else { + while (true) { + int i2 = random.nextInt(parts.size() - 3 - i) + 2; + if (isNumeric(parts.get(i + i2))) { + parts.set(i + i2, parts.get(i + i2) + ")"); + break; + } + } + } + } + return parts; + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/service/StartController.java b/src/main/java/com/ybw/mathapp/service/StartController.java new file mode 100644 index 0000000..ef0ff3c --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/StartController.java @@ -0,0 +1,132 @@ +package com.ybw.mathapp.service; + +import com.ybw.mathapp.LoginAndRegister; +import com.ybw.mathapp.service.ChoiceGenerator.MultipleChoiceQuestion; +import com.ybw.mathapp.system.LogSystem; +import java.io.IOException; +import java.util.List; +import java.util.Scanner; + +public class StartController { + + private LogSystem logSystem = new LogSystem(); + private FileHandler fileHandler = new FileHandler(); + private double score; + + public double getScore() { + return score; + } + + public void setScore(double score) { + this.score = score; + } + + public void start() { + Scanner scanner = new Scanner(System.in); + while (true) { + // 此处应添加初始界面(登录、注册界面)函数 + if (registerButtonPressed()) { + // 此处添加加载注册界面函数 + if (!LoginAndRegister.register()) { + continue; + } + } else { + if (!LoginAndRegister.login()) { + continue; + } + } + while (true) { + // 此处添加小学、初中、高中选择界面函数 (修改密码按钮也应在此界面) + if (changePasswordButtonPressed()) { + ChangePassword(); + } else { + while (true) { + // 此处添加小学、初中、高中按钮点击响应函数,creatGenerate函数,返回值为QuestionGenerator + QuestionGenerator generator = null; + // 出题数目页面 + String input = " "; + try { + int count = Integer.parseInt(input); + if (count < 10 || count > 30) { + System.out.println("题目数量必须在10-30之间!"); + continue; + } + try { + handleMultipleChoiceGeneration(generator, count); // 调用生成题目函数 + } catch (IOException e) { + throw new RuntimeException(e); + } + + } catch (NumberFormatException e) { + System.out.println("请输入数字!"); + continue; + } + System.out.println(score); + score = 0; + } + } + } + } + } + + // ... (handleLevelSwitch 和 createGenerator 保持不变) + + // 处理选择题生成,进行本次生成内的去重工作 + private void handleMultipleChoiceGeneration(QuestionGenerator generator, int count) throws IOException { + + ChoiceGenerator mcGenerator = new ChoiceGenerator(generator); + + // 生成选择题列表,内部已处理去重 + List finalQuestions = mcGenerator.generateMultipleChoiceQuestions(count); + int rightCount = 0; + // 显示选择题 + for (int i = 0; i < finalQuestions.size(); i++) { + MultipleChoiceQuestion mcq = finalQuestions.get(i); + System.out.println((i + 1) + ". " + mcq.getQuestion()); + List options = mcq.getOptions(); + int correctAnswerIndex = mcq.getCorrectIndex(); + for (int j = 0; j < options.size(); j++) { + // 此处可以转换成ABCD + System.out.println(" " + (char)('A' + j) + ". " + options.get(j)); + } + // 显示选择题界面,等待用户选择 + while (true) { + if(nextButtonPressed()) { + if (isCorrectAnswer (input, correctAnswerIndex, options)) { + rightCount++; + System.out.println("正确!"); + } else { + System.out.println("答案错误!"); + } + break; + } + } + } + setScore(caculateScore(rightCount, finalQuestions.size())); + } + + public QuestionGenerator createGenerator(String level) { + switch (level) { + case "小学": + return new PrimarySchoolGenerator(); + case "初中": + return new JuniorHighGenerator(); + case "高中": + return new SeniorHighGenerator(); + default: + return null; + } + } + + public boolean isCorrectAnswer(String input, int correctAnswerIndex, List options) { + if(input.equals(String.valueOf(options.get(correctAnswerIndex)))) { + return true; + } else { + return false; + } + } + + public double caculateScore(int rightCount, int totalCount) { + return rightCount / (double) totalCount; + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/system/LogSystem.java b/src/main/java/com/ybw/mathapp/system/LogSystem.java new file mode 100644 index 0000000..c867a63 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/system/LogSystem.java @@ -0,0 +1,39 @@ +package com.ybw.mathapp.system; + +import java.util.HashMap; +import java.util.Scanner; +import com.ybw.mathapp.entity.User; + +public class LogSystem { + private final HashMap userHashMap = new HashMap<>(); + public void userHashMapInit() { + // 小学 + userHashMap.put("1798231811@qq.com", new User("1798231811@qq.com", "1234567")); + + } + + public User login() { + System.out.println("请输入用户名和密码,两者之间用空格隔开,用户名为邮箱账号"); + while(true) { + Scanner scanner = new Scanner(System.in); + String[] info = scanner.nextLine().split(" "); + if(info.length != 2) { + System.out.println("请输入正确格式"); + } else { + String name = info[0]; + String password = info[1]; + User user = userHashMap.get(name); + if (user == null) { + System.out.println("邮箱未注册"); + } + else if (!user.getPassword().equals(password)) { + System.out.println("请输入正确的用户名、密码"); + } + else { + System.out.println("当前选择为" + user.getLevel() + "出题"); + return user; + } + } + } + } +} 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..7de816d --- /dev/null +++ b/src/main/java/com/ybw/mathapp/util/EmailService.java @@ -0,0 +1,158 @@ +package com.ybw.mathapp.util; + +import com.ybw.mathapp.config.EmailConfig; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Random; +import jakarta.mail.Authenticator; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.PasswordAuthentication; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; + +public class EmailService { + private static Map verificationCodes = new HashMap<>(); + + // 验证码信息内部类 + private static class VerificationCodeInfo { + String code; + long timestamp; + + VerificationCodeInfo(String code, long timestamp) { + this.code = code; + this.timestamp = timestamp; + } + } + + // 生成6位随机验证码 + public static String generateVerificationCode() { + Random random = new Random(); + int code = 100000 + random.nextInt(900000); + return String.valueOf(code); + } + + // 发送真实邮件验证码 + public static boolean sendVerificationCode(String recipientEmail, String code) { + try { + // 创建邮件会话 + Properties props = new Properties(); + props.put("mail.smtp.host", EmailConfig.SMTP_HOST); + props.put("mail.smtp.port", EmailConfig.SMTP_PORT); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.starttls.enable", "true"); + props.put("mail.smtp.ssl.protocols", "TLSv1.2"); + + // 创建认证器 + Authenticator auth = new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication( + EmailConfig.SENDER_EMAIL, + EmailConfig.SENDER_PASSWORD + ); + } + }; + + Session session = Session.getInstance(props, auth); + + // 创建邮件消息 + Message message = new MimeMessage(session); + message.setFrom(new InternetAddress(EmailConfig.SENDER_EMAIL)); + message.setRecipients(Message.RecipientType.TO, + InternetAddress.parse(recipientEmail)); + message.setSubject(EmailConfig.EMAIL_SUBJECT); + + // 创建邮件内容 + String emailContent = createEmailContent(code); + message.setContent(emailContent, "text/html; charset=utf-8"); + + // 发送邮件 + Transport.send(message); + + // 存储验证码信息 + verificationCodes.put(recipientEmail, + new VerificationCodeInfo(code, System.currentTimeMillis())); + + System.out.println("验证码已发送到邮箱: " + recipientEmail); + return true; + + } catch (MessagingException e) { + System.err.println("发送邮件失败: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + // 创建HTML格式的邮件内容 + private static String createEmailContent(String code) { + return "" + + "" + + "" + + "" + + "" + + "" + + "" + + "

" + + "
" + + "

用户注册验证码

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

您好!

" + + "

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

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

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

" + + "

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

" + + "
" + + "" + + "
" + + "" + + ""; + } + + // 验证验证码 + public static boolean verifyCode(String email, String inputCode) { + VerificationCodeInfo codeInfo = verificationCodes.get(email); + if (codeInfo == null) { + return false; + } + + // 检查验证码是否过期 + long currentTime = System.currentTimeMillis(); + if (currentTime - codeInfo.timestamp > EmailConfig.CODE_EXPIRY_MINUTES * 60 * 1000) { + verificationCodes.remove(email); + return false; + } + + return codeInfo.code.equals(inputCode); + } + + // 清理过期的验证码(可选) + public static void cleanupExpiredCodes() { + long currentTime = System.currentTimeMillis(); + Iterator> iterator = + verificationCodes.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (currentTime - entry.getValue().timestamp > + EmailConfig.CODE_EXPIRY_MINUTES * 60 * 1000) { + iterator.remove(); + } + } + } +} 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..f99a56f --- /dev/null +++ b/src/main/java/com/ybw/mathapp/util/LoginFileUtils.java @@ -0,0 +1,74 @@ +package com.ybw.mathapp.util; + +import com.ybw.mathapp.entity.User; +import java.io.*; +import java.util.ArrayList; +import java.util.List; + +public class LoginFileUtils { + private static final String USER_FILE = "users.txt"; + + // 读取所有用户 + // FileUtils.java 中的 readUsers 方法(简化版) + public static List readUsers() { + List users = new ArrayList<>(); + File file = new File(USER_FILE); + + if (!file.exists()) { + try { + file.createNewFile(); + } catch (IOException e) { + System.err.println("创建用户文件失败: " + e.getMessage()); + } + return users; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + + User user = User.fromString(line); + if (user != null) { + users.add(user); + } + } + } catch (IOException e) { + System.err.println("读取用户文件失败: " + e.getMessage()); + } + return users; + } + + // 保存用户到文件 + public static void saveUser(User user) { + try (PrintWriter writer = new PrintWriter(new FileWriter(USER_FILE, true))) { + writer.println(user.toString()); + } catch (IOException e) { + System.err.println("保存用户信息失败: " + e.getMessage()); + } + } + + // 检查邮箱是否已注册 + public static boolean isEmailRegistered(String email) { + List users = readUsers(); + for (User user : users) { + if (user.getEmail().equalsIgnoreCase(email)) { + return true; + } + } + return false; + } + + // 验证用户登录 + public static boolean validateUser(String email, String password) { + List users = readUsers(); + for (User user : users) { + if (user.getEmail().equalsIgnoreCase(email) && + user.getPassword().equals(password)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 2b5b80f..6902ee7 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -10,6 +10,7 @@ module com.ybw.mathapp { requires org.kordamp.bootstrapfx.core; requires eu.hansolo.tilesfx; requires com.almasb.fxgl.all; + requires jakarta.mail; opens com.ybw.mathapp to javafx.fxml; exports com.ybw.mathapp; 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 -- 2.34.1 From e2e0d99656380f14feeadcfa5a792dff4bcb3aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=96=E5=85=AE=E5=86=89?= <15550273+wang-shengfa-11@user.noreply.gitee.com> Date: Mon, 6 Oct 2025 23:14:48 +0800 Subject: [PATCH 02/16] =?UTF-8?q?v1.0=20=E5=A4=A7=E6=A6=82=E7=9A=84?= =?UTF-8?q?=E6=A1=86=E6=9E=B6=E5=B7=B2=E7=BB=8F=E5=86=99=E5=A5=BD=EF=BC=8C?= =?UTF-8?q?=E6=9C=AA=E4=B8=8E=E5=90=8E=E7=AB=AF=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 4 +- src/main/java/com/wsf/mathapp/App.java | 97 ----------- src/main/java/com/wsf/mathapp/Main.java | 17 ++ .../com/wsf/mathapp/controller/Login.fxml | 23 --- .../mathapp/controller/LoginController.java | 75 -------- .../wsf/mathapp/controller/SceneManager.java | 69 ++++++++ .../com/wsf/mathapp/service/EmailService.java | 79 +++++++++ .../wsf/mathapp/service/QuestionService.java | 34 ++++ .../com/wsf/mathapp/service/UserService.java | 81 +++++++++ .../wsf/mathapp/view/LevelSelectionView.java | 77 +++++++++ .../java/com/wsf/mathapp/view/LoginView.java | 97 +++++++++++ .../com/wsf/mathapp/view/MainMenuView.java | 112 ++++++++++++ .../wsf/mathapp/view/QuestionCountView.java | 100 +++++++++++ .../java/com/wsf/mathapp/view/QuizView.java | 148 ++++++++++++++++ .../com/wsf/mathapp/view/RegisterView.java | 160 ++++++++++++++++++ .../java/com/wsf/mathapp/view/ResultView.java | 102 +++++++++++ .../java/com/ybw/mathapp/entity/User.java | 9 +- .../com/ybw/mathapp/service/FileHandler.java | 2 +- .../mathapp/service/QuestionDeduplicator.java | 2 +- 19 files changed, 1087 insertions(+), 201 deletions(-) delete mode 100644 src/main/java/com/wsf/mathapp/App.java create mode 100644 src/main/java/com/wsf/mathapp/Main.java delete mode 100644 src/main/java/com/wsf/mathapp/controller/Login.fxml delete mode 100644 src/main/java/com/wsf/mathapp/controller/LoginController.java create mode 100644 src/main/java/com/wsf/mathapp/controller/SceneManager.java create mode 100644 src/main/java/com/wsf/mathapp/service/EmailService.java create mode 100644 src/main/java/com/wsf/mathapp/service/QuestionService.java create mode 100644 src/main/java/com/wsf/mathapp/service/UserService.java create mode 100644 src/main/java/com/wsf/mathapp/view/LevelSelectionView.java create mode 100644 src/main/java/com/wsf/mathapp/view/LoginView.java create mode 100644 src/main/java/com/wsf/mathapp/view/MainMenuView.java create mode 100644 src/main/java/com/wsf/mathapp/view/QuestionCountView.java create mode 100644 src/main/java/com/wsf/mathapp/view/QuizView.java create mode 100644 src/main/java/com/wsf/mathapp/view/RegisterView.java create mode 100644 src/main/java/com/wsf/mathapp/view/ResultView.java diff --git a/pom.xml b/pom.xml index 56d7799..67d77d6 100644 --- a/pom.xml +++ b/pom.xml @@ -156,14 +156,14 @@ 0.0.8 - com.wsf.mathapp.App + com.wsf.mathapp.Main default-cli - com.wsf.mathapp.App + com.wsf.mathapp.Main diff --git a/src/main/java/com/wsf/mathapp/App.java b/src/main/java/com/wsf/mathapp/App.java deleted file mode 100644 index 8df2322..0000000 --- a/src/main/java/com/wsf/mathapp/App.java +++ /dev/null @@ -1,97 +0,0 @@ -// src/main/java/com/wsf/mathapp/App.java -package com.wsf.mathapp; - -import com.wsf.mathapp.controller.LoginController; -import javafx.application.Application; -import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; -import javafx.scene.Scene; -import javafx.stage.Stage; -import java.io.IOException; - -public class App extends Application { - - private Stage primaryStage; - - @Override - public void start(Stage primaryStage) throws IOException { - this.primaryStage = primaryStage; - this.primaryStage.setTitle("数学学习软件"); - - showLoginScene(); - } - - public void showLoginScene() throws IOException { - FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/wsf/mathapp/view/fxml/login.fxml")); - Parent root = loader.load(); - LoginController controller = loader.getController(); - controller.setApp(this); - - Scene scene = new Scene(root); - scene.getStylesheets().add(getClass().getResource("/com/wsf/mathapp/view/css/styles.css").toExternalForm()); - primaryStage.setScene(scene); - primaryStage.show(); - } - - public void showRegisterScene() throws IOException { - FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/wsf/mathapp/view/fxml/register.fxml")); - Parent root = loader.load(); - - Scene scene = new Scene(root); - scene.getStylesheets().add(getClass().getResource("/com/wsf/mathapp/view/css/styles.css").toExternalForm()); - primaryStage.setScene(scene); - } - - public void showSetPasswordScene(String email) throws IOException { - FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/wsf/mathapp/view/fxml/set_password.fxml")); - Parent root = loader.load(); - // Pass email to SetPasswordController if needed - // SetPasswordController controller = loader.getController(); - // controller.setEmail(email); - - Scene scene = new Scene(root); - scene.getStylesheets().add(getClass().getResource("/com/wsf/mathapp/view/css/styles.css").toExternalForm()); - primaryStage.setScene(scene); - } - - public void showGradeSelectionScene() throws IOException { - FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/wsf/mathapp/view/fxml/grade_selection.fxml")); - Parent root = loader.load(); - - Scene scene = new Scene(root); - scene.getStylesheets().add(getClass().getResource("/com/wsf/mathapp/view/css/styles.css").toExternalForm()); - primaryStage.setScene(scene); - } - - public void showQuestionScene(java.util.List questions) throws IOException { - FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/wsf/mathapp/view/fxml/question.fxml")); - Parent root = loader.load(); - com.wsf.mathapp.controller.QuestionController controller = loader.getController(); - controller.setApp(this); - controller.setQuestions(questions); - - Scene scene = new Scene(root); - scene.getStylesheets().add(getClass().getResource("/com/wsf/mathapp/view/css/styles.css").toExternalForm()); - primaryStage.setScene(scene); - } - - public void showResultScene(com.wsf.mathapp.model.QuizResult result) throws IOException { - FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/wsf/mathapp/view/fxml/result.fxml")); - Parent root = loader.load(); - com.wsf.mathapp.controller.ResultController controller = loader.getController(); - controller.setApp(this); - controller.setResult(result); - - Scene scene = new Scene(root); - scene.getStylesheets().add(getClass().getResource("/com/wsf/mathapp/view/css/styles.css").toExternalForm()); - primaryStage.setScene(scene); - } - - public Stage getPrimaryStage() { - return primaryStage; - } - - public static void main(String[] args) { - launch(); - } -} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/Main.java b/src/main/java/com/wsf/mathapp/Main.java new file mode 100644 index 0000000..21c2960 --- /dev/null +++ b/src/main/java/com/wsf/mathapp/Main.java @@ -0,0 +1,17 @@ +package com.wsf.mathapp; + +import com.wsf.mathapp.controller.SceneManager; +import javafx.application.Application; +import javafx.stage.Stage; + +public class Main extends Application { + @Override + public void start(Stage primaryStage) { + SceneManager sceneManager = new SceneManager(primaryStage); + sceneManager.showLoginView(); + } + + public static void main(String[] args) { + launch(args); + } +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/controller/Login.fxml b/src/main/java/com/wsf/mathapp/controller/Login.fxml deleted file mode 100644 index 14309c8..0000000 --- a/src/main/java/com/wsf/mathapp/controller/Login.fxml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - -