diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml new file mode 100644 index 0000000..4ea72a9 --- /dev/null +++ b/.idea/copilot.data.migration.agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask.xml b/.idea/copilot.data.migration.ask.xml new file mode 100644 index 0000000..7ef04e2 --- /dev/null +++ b/.idea/copilot.data.migration.ask.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml new file mode 100644 index 0000000..1f2ea11 --- /dev/null +++ b/.idea/copilot.data.migration.ask2agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml new file mode 100644 index 0000000..8648f94 --- /dev/null +++ b/.idea/copilot.data.migration.edit.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/libraries/lib.xml b/.idea/libraries/lib.xml new file mode 100644 index 0000000..7f1aa7e --- /dev/null +++ b/.idea/libraries/lib.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..31e1ebc --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..831f033 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 4a8b08a..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# mathStudyAPP - diff --git a/data/users.cfg b/data/users.cfg new file mode 100644 index 0000000..a790fcf --- /dev/null +++ b/data/users.cfg @@ -0,0 +1,12 @@ +王五3 123 HIGH wangwu3@example.com +王五2 123 HIGH wangwu2@example.com +123 Aa123456 PRIMARY 2571400460@qq.com +王五1 123 HIGH wangwu1@example.com +scb Aa123456 PRIMARY 3110858122@qq.com +张三3 123 PRIMARY zhangsan3@example.com +张三1 123 PRIMARY zhangsan1@example.com +张三2 123 PRIMARY zhangsan2@example.com +李四3 123 MIDDLE lishi3@example.com +sqf Qq123456 PRIMARY sqf090815@hnu.edu.cn +李四1 123 MIDDLE lishi1@example.com +李四2 123 MIDDLE lishi2@example.com diff --git a/doc/MyApp.jar b/doc/MyApp.jar new file mode 100644 index 0000000..01c6564 Binary files /dev/null and b/doc/MyApp.jar differ diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..c291673 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,30 @@ +# mathStudyAPP +## 📂 项目结构 +src/ +├─ Main.java +├─ controller/ +│ └─ FunctionController.java +├─ model/ +│ ├─ Login.java +│ ├─ LanguageSwitch.java +│ ├─ QuestionGenerator.java +│ ├─ Generator.java +│ ├─ LoadFile.java +│ ├─ Save.java +│ ├─ Create.java +│ ├─ ExpressionEvaluator.java +│ └─ Paper.java +└─ view/ + ├─ LoginFrame.java + ├─ RegisterFrame.java + ├─ MainMenuFrame.java + ├─ ExamSetupFrame.java + ├─ ExamFrame.java + └─ ResultFrame.java +lib/ +├─ jakarta.activation-2.0.1.jar +└─ jakarta.mail-2.0.1.jar + +--- + + diff --git a/lib/jakarta.activation-2.0.1.jar b/lib/jakarta.activation-2.0.1.jar new file mode 100644 index 0000000..521c7c4 Binary files /dev/null and b/lib/jakarta.activation-2.0.1.jar differ diff --git a/lib/jakarta.mail-2.0.1.jar b/lib/jakarta.mail-2.0.1.jar new file mode 100644 index 0000000..17e07cc Binary files /dev/null and b/lib/jakarta.mail-2.0.1.jar differ diff --git a/mathStudyApp.iml b/mathStudyApp.iml new file mode 100644 index 0000000..fb8e866 --- /dev/null +++ b/mathStudyApp.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/out/production/mathStudyApp/Main.class b/out/production/mathStudyApp/Main.class new file mode 100644 index 0000000..50bd932 Binary files /dev/null and b/out/production/mathStudyApp/Main.class differ diff --git a/out/production/mathStudyApp/controller/AssignController.class b/out/production/mathStudyApp/controller/AssignController.class new file mode 100644 index 0000000..c202f0e Binary files /dev/null and b/out/production/mathStudyApp/controller/AssignController.class differ diff --git a/out/production/mathStudyApp/controller/FunctionController.class b/out/production/mathStudyApp/controller/FunctionController.class new file mode 100644 index 0000000..621af2e Binary files /dev/null and b/out/production/mathStudyApp/controller/FunctionController.class differ diff --git a/out/production/mathStudyApp/model/Create.class b/out/production/mathStudyApp/model/Create.class new file mode 100644 index 0000000..002f264 Binary files /dev/null and b/out/production/mathStudyApp/model/Create.class differ diff --git a/out/production/mathStudyApp/model/ExpressionEvaluator.class b/out/production/mathStudyApp/model/ExpressionEvaluator.class new file mode 100644 index 0000000..ab78d34 Binary files /dev/null and b/out/production/mathStudyApp/model/ExpressionEvaluator.class differ diff --git a/out/production/mathStudyApp/model/Generator$1.class b/out/production/mathStudyApp/model/Generator$1.class new file mode 100644 index 0000000..2f8516d Binary files /dev/null and b/out/production/mathStudyApp/model/Generator$1.class differ diff --git a/out/production/mathStudyApp/model/Generator.class b/out/production/mathStudyApp/model/Generator.class new file mode 100644 index 0000000..58ca828 Binary files /dev/null and b/out/production/mathStudyApp/model/Generator.class differ diff --git a/out/production/mathStudyApp/model/LanguageSwitch$1.class b/out/production/mathStudyApp/model/LanguageSwitch$1.class new file mode 100644 index 0000000..f9b2a2b Binary files /dev/null and b/out/production/mathStudyApp/model/LanguageSwitch$1.class differ diff --git a/out/production/mathStudyApp/model/LanguageSwitch.class b/out/production/mathStudyApp/model/LanguageSwitch.class new file mode 100644 index 0000000..c3c3ca6 Binary files /dev/null and b/out/production/mathStudyApp/model/LanguageSwitch.class differ diff --git a/out/production/mathStudyApp/model/LoadFile.class b/out/production/mathStudyApp/model/LoadFile.class new file mode 100644 index 0000000..41b059e Binary files /dev/null and b/out/production/mathStudyApp/model/LoadFile.class differ diff --git a/out/production/mathStudyApp/model/Login$Account.class b/out/production/mathStudyApp/model/Login$Account.class new file mode 100644 index 0000000..aa24861 Binary files /dev/null and b/out/production/mathStudyApp/model/Login$Account.class differ diff --git a/out/production/mathStudyApp/model/Login$Level.class b/out/production/mathStudyApp/model/Login$Level.class new file mode 100644 index 0000000..a35c914 Binary files /dev/null and b/out/production/mathStudyApp/model/Login$Level.class differ diff --git a/out/production/mathStudyApp/model/Login.class b/out/production/mathStudyApp/model/Login.class new file mode 100644 index 0000000..e914c85 Binary files /dev/null and b/out/production/mathStudyApp/model/Login.class differ diff --git a/out/production/mathStudyApp/model/Paper.class b/out/production/mathStudyApp/model/Paper.class new file mode 100644 index 0000000..ee32776 Binary files /dev/null and b/out/production/mathStudyApp/model/Paper.class differ diff --git a/out/production/mathStudyApp/model/QuestionGenerator.class b/out/production/mathStudyApp/model/QuestionGenerator.class new file mode 100644 index 0000000..7ca3b27 Binary files /dev/null and b/out/production/mathStudyApp/model/QuestionGenerator.class differ diff --git a/out/production/mathStudyApp/model/Save.class b/out/production/mathStudyApp/model/Save.class new file mode 100644 index 0000000..e08888b Binary files /dev/null and b/out/production/mathStudyApp/model/Save.class differ diff --git a/out/production/mathStudyApp/view/ExamFrame.class b/out/production/mathStudyApp/view/ExamFrame.class new file mode 100644 index 0000000..e37f665 Binary files /dev/null and b/out/production/mathStudyApp/view/ExamFrame.class differ diff --git a/out/production/mathStudyApp/view/ExamSetupFrame.class b/out/production/mathStudyApp/view/ExamSetupFrame.class new file mode 100644 index 0000000..2850903 Binary files /dev/null and b/out/production/mathStudyApp/view/ExamSetupFrame.class differ diff --git a/out/production/mathStudyApp/view/LoginFrame.class b/out/production/mathStudyApp/view/LoginFrame.class new file mode 100644 index 0000000..b8a1070 Binary files /dev/null and b/out/production/mathStudyApp/view/LoginFrame.class differ diff --git a/out/production/mathStudyApp/view/MainMenuFrame$ChangePasswordDialog.class b/out/production/mathStudyApp/view/MainMenuFrame$ChangePasswordDialog.class new file mode 100644 index 0000000..1147586 Binary files /dev/null and b/out/production/mathStudyApp/view/MainMenuFrame$ChangePasswordDialog.class differ diff --git a/out/production/mathStudyApp/view/MainMenuFrame.class b/out/production/mathStudyApp/view/MainMenuFrame.class new file mode 100644 index 0000000..de0d464 Binary files /dev/null and b/out/production/mathStudyApp/view/MainMenuFrame.class differ diff --git a/out/production/mathStudyApp/view/RegisterFrame$1.class b/out/production/mathStudyApp/view/RegisterFrame$1.class new file mode 100644 index 0000000..9dd1b2a Binary files /dev/null and b/out/production/mathStudyApp/view/RegisterFrame$1.class differ diff --git a/out/production/mathStudyApp/view/RegisterFrame.class b/out/production/mathStudyApp/view/RegisterFrame.class new file mode 100644 index 0000000..eecd276 Binary files /dev/null and b/out/production/mathStudyApp/view/RegisterFrame.class differ diff --git a/out/production/mathStudyApp/view/ResultFrame.class b/out/production/mathStudyApp/view/ResultFrame.class new file mode 100644 index 0000000..bc6b282 Binary files /dev/null and b/out/production/mathStudyApp/view/ResultFrame.class differ diff --git a/src/Main.java b/src/Main.java new file mode 100644 index 0000000..07d6d26 --- /dev/null +++ b/src/Main.java @@ -0,0 +1,13 @@ +import javax.swing.SwingUtilities; + +import view.LoginFrame; + +/** + * 程序入口 + */ +public class Main { + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> new LoginFrame()); + } +} diff --git a/src/controller/FunctionController.java b/src/controller/FunctionController.java new file mode 100644 index 0000000..ef07cf9 --- /dev/null +++ b/src/controller/FunctionController.java @@ -0,0 +1,34 @@ +package controller; + +import javax.swing.JOptionPane; + +import model.Create; +import model.Login; +import model.Paper; +import view.ExamFrame; + +/** + * 题目生成控制器:接收题数与用户信息,调用 Create 生成 Paper 并打开 ExamFrame + */ +public class FunctionController { + + private Login.Account user; + + public FunctionController(Login.Account user) { + this.user = user; + } + + public void startExam(int n) { + if (n < 10 || n > 30) { + JOptionPane.showMessageDialog(null, "题目数量的有效输入范围是 10-30"); + return; + } + Paper paper = Create.create(n, user.level, user); + if (paper == null || paper.size() == 0) { + JOptionPane.showMessageDialog(null, "未能生成题目(可能因去重约束导致)。"); + return; + } + JOptionPane.showMessageDialog(null, "已生成 " + paper.size() + " 道题,开始答题。"); + new ExamFrame(paper, user); + } +} diff --git a/src/model/Create.java b/src/model/Create.java new file mode 100644 index 0000000..d39e04e --- /dev/null +++ b/src/model/Create.java @@ -0,0 +1,114 @@ +package model; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + +import javax.script.ScriptException; + +/** + * 负责生成试卷并创建选择题选项(四选一) + */ +public class Create { + + private static double evalExpression(String expr) throws ScriptException { + return ExpressionEvaluator.evaluate(expr); + } + + public static Paper create(int n, Login.Level currentLevel, Login.Account user) { + List existing = LoadFile.loadExistingQuestions(user.username); + List paper = generatePaper(n, currentLevel, existing); + if (paper.isEmpty()) return null; + + List optionsList = new ArrayList<>(); + int[] correctIdx = new int[paper.size()]; + + for (int i = 0; i < paper.size(); i++) { + String q = paper.get(i); + optionsList.add(generateOptions(q, correctIdx, i)); + } + + savePaper(user.username, paper); + + return new Paper(paper, optionsList, correctIdx); + } + + private static List generatePaper(int n, Login.Level level, List existing) { + Generator qg = new Generator(level, existing); + return qg.generatePaper(n); + } + + private static String[] generateOptions(String question, int[] correctIdx, int i) { + double correctVal; + try { + correctVal = evalExpression(question); + return generateNumericOptions(correctVal, correctIdx, i); + } catch (Exception e) { + return generateTextOptions(question, correctIdx, i); + } + } + + private static String[] generateTextOptions(String question, int[] correctIdx, int i) { + String[] opts = new String[]{question, question + " + 1", question + " - 1", "错误:" + question}; + shuffleArray(opts); + correctIdx[i] = findIndex(opts, question); + return opts; + } + + private static String[] generateNumericOptions(double correctVal, int[] correctIdx, int i) { + String correctStr = formatNumber(correctVal); + boolean isInt = Math.abs(correctVal - Math.round(correctVal)) < 1e-6; + Set optsSet = new LinkedHashSet<>(); + optsSet.add(correctStr); + Random r = new Random(); + + int attempts = 0; + while (optsSet.size() < 4 && attempts < 50) { + attempts++; + double delta = isInt ? r.nextInt(10) + 1 : (r.nextGaussian() * Math.max(1, Math.abs(correctVal) * 0.15)); + if (isInt && r.nextBoolean()) delta = -delta; + optsSet.add(formatNumber(correctVal + delta)); + } + + while (optsSet.size() < 4) { + double delta = isInt ? r.nextInt(10) - 5 : r.nextDouble() * 2 - 1; + optsSet.add(formatNumber(correctVal + delta)); + } + + String[] optsArr = optsSet.toArray(new String[0]); + shuffleArray(optsArr); + correctIdx[i] = findIndex(optsArr, correctStr); + return optsArr; + } + + private static void savePaper(String username, List paper) { + try { + Save.savePaper(username, new ArrayList<>(paper)); + } catch (RuntimeException re) { + System.err.println("保存试卷时出错: " + re.getMessage()); + } + } + + private static void shuffleArray(String[] arr) { + Random r = new Random(); + for (int i = arr.length - 1; i > 0; i--) { + int j = r.nextInt(i + 1); + String t = arr[i]; + arr[i] = arr[j]; + arr[j] = t; + } + } + + private static int findIndex(String[] arr, String target) { + for (int i = 0; i < arr.length; i++) if (arr[i].equals(target)) return i; + return 0; + } + + private static String formatNumber(double v) { + if (Math.abs(v - Math.round(v)) < 1e-6) return String.valueOf((long) Math.round(v)); + return String.format("%.3f", v); + } +} + diff --git a/src/model/ExpressionEvaluator.java b/src/model/ExpressionEvaluator.java new file mode 100644 index 0000000..0fc214d --- /dev/null +++ b/src/model/ExpressionEvaluator.java @@ -0,0 +1,131 @@ +package model; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.StringTokenizer; + + +public class ExpressionEvaluator { + + private static final Map PRECEDENCE = new HashMap<>(); + + static { + PRECEDENCE.put("+", 1); + PRECEDENCE.put("-", 1); + PRECEDENCE.put("*", 2); + PRECEDENCE.put("/", 2); + PRECEDENCE.put("^", 3); + } + + // 判断是否是运算符 + private static boolean isOperator(String token) { + return PRECEDENCE.containsKey(token); + } + + // 运算符优先级 + private static int precedence(String op) { + return PRECEDENCE.get(op); + } + + // 将中缀表达式转为逆波兰表达式(RPN) + private static List toRPN(String expr) { + List output = new ArrayList<>(); + Stack stack = new Stack<>(); + StringTokenizer tokenizer = new StringTokenizer(expr, "+-*/^() ", true); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken().trim(); + if (token.isEmpty()) { + continue; + } + + if (token.matches("[0-9.]+")) { // 数字 + output.add(token); + } else if (token.matches("[a-zA-Z]+")) { // 函数 + stack.push(token); + } else if (isOperator(token)) { // 运算符 + while (!stack.isEmpty() && isOperator(stack.peek()) + && precedence(stack.peek()) >= precedence(token)) { + output.add(stack.pop()); + } + stack.push(token); + } else if (token.equals("(")) { + stack.push(token); + } else if (token.equals(")")) { + while (!stack.isEmpty() && !stack.peek().equals("(")) { + output.add(stack.pop()); + } + if (!stack.isEmpty() && stack.peek().equals("(")) { + stack.pop(); + } + if (!stack.isEmpty() && stack.peek().matches("[a-zA-Z]+")) { + output.add(stack.pop()); + } + } + } + + while (!stack.isEmpty()) { + output.add(stack.pop()); + } + + return output; + } + + // 计算逆波兰表达式 + private static double evalRPN(List rpn) { + Stack stack = new Stack<>(); + for (String token : rpn) { + if (token.matches("[0-9.]+")) { + stack.push(Double.parseDouble(token)); + } else if (isOperator(token)) { + double b = stack.pop(); + double a = stack.pop(); + switch (token) { + case "+": + stack.push(a + b); + break; + case "-": + stack.push(a - b); + break; + case "*": + stack.push(a * b); + break; + case "/": + stack.push(a / b); + break; + case "^": + stack.push(Math.pow(a, b)); + break; + } + } else { // 函数 + double a = stack.pop(); + switch (token.toLowerCase()) { + case "sin": + stack.push(Math.sin(Math.toRadians(a))); + break; + case "cos": + stack.push(Math.cos(Math.toRadians(a))); + break; + case "tan": + stack.push(Math.tan(Math.toRadians(a))); + break; + case "sqrt": + stack.push(Math.sqrt(a)); + break; + default: + throw new RuntimeException("未知函数: " + token); + } + } + } + return stack.pop(); + } + + // 对外接口:传入表达式字符串,返回计算结果 + public static double evaluate(String expr) { + List rpn = toRPN(expr); + return evalRPN(rpn); + } + +} diff --git a/src/model/Generator.java b/src/model/Generator.java new file mode 100644 index 0000000..da2f306 --- /dev/null +++ b/src/model/Generator.java @@ -0,0 +1,162 @@ +package model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 具体题目生成器 + */ +public class Generator extends QuestionGenerator { + + public static final Random RAND = new Random(); + public static final int MAX_ATTEMPTS = 2000; + + public final Login.Level level; + public final Set existing; + + public Generator(Login.Level level, List existingQuestions) { + this.level = level; + this.existing = existingQuestions.stream().map(String::trim).collect(Collectors.toSet()); + } + + public List generatePaper(int n) { + Set generated = new LinkedHashSet<>(); + int attempts = 0; + while (generated.size() < n && attempts < MAX_ATTEMPTS) { + attempts++; + String q = generateOneQuestion(); + String key = normalize(q); + if (!existing.contains(key) && !generated.contains(key)) { + generated.add(q); + } + } + if (generated.size() < n) { + System.out.println( + "注意:无法生成足够的不重复题目,已生成 " + generated.size() + " 道题(请求 " + n + " 道)"); + } + return new ArrayList<>(generated); + } + + public String normalize(String s) { + return s.replaceAll("\\s+", "").toLowerCase(); + } + + public String generateOneQuestion() { + int operands = RAND.nextInt(5) + 1; // 1..5 + int operands_p = RAND.nextInt(4) + 2; // 2..5 + return switch (level) { + case PRIMARY -> genPrimary(operands_p); + case MIDDLE -> genMiddle(operands); + case HIGH -> genHigh(operands); + }; + } + + public String genPrimary(int operands) { + if (operands == 1) { + return String.valueOf(randInt(1, 100)); + } + List ops = Arrays.asList("+", "-", "*", "/"); + StringBuilder sb = new StringBuilder(); + boolean useParens = RAND.nextBoolean(); + if (useParens && operands >= 3 && RAND.nextBoolean()) { + sb.append("("); + sb.append(randInt(1, 100)).append(" ").append(randomChoice(ops)).append(" ") + .append(randInt(1, 100)); + sb.append(")"); + for (int i = 2; i < operands; i++) { + sb.append(" ").append(randomChoice(ops)).append(" ").append(randInt(1, 100)); + } + } else { + sb.append(randInt(1, 100)); + for (int i = 1; i < operands; i++) { + sb.append(" ").append(randomChoice(ops)).append(" ").append(randInt(1, 100)); + } + } + return sb.toString(); + } + + public String genMiddle(int operands) { + String expr = genPrimary(operands); + if (RAND.nextBoolean()) { + expr = applySquare(expr); + } else { + expr = applySqrt(expr); + } + return expr; + } + + public String genHigh(int operands) { + String expr = genPrimary(operands); + expr = applyTrig(expr); + return expr; + } + + public String applySquare(String expr) { + List spans = findNumberSpans(expr); + if (spans.isEmpty()) { + return expr + "^2"; + } + int[] s = spans.get(RAND.nextInt(spans.size())); + String before = expr.substring(0, s[0]); + String num = expr.substring(s[0], s[1]); + String after = expr.substring(s[1]); + return before + "(" + num + ")^2" + after; + } + + public String applySqrt(String expr) { + List spans = findNumberSpans(expr); + if (spans.isEmpty()) { + return "sqrt(" + expr + ")"; + } + int[] s = spans.get(RAND.nextInt(spans.size())); + String before = expr.substring(0, s[0]); + String num = expr.substring(s[0], s[1]); + String after = expr.substring(s[1]); + return before + "sqrt(" + num + ")" + after; + } + + public String applyTrig(String expr) { + List spans = findNumberSpans(expr); + String func = randomChoice(Arrays.asList("sin", "cos", "tan")); + if (spans.isEmpty()) { + return func + "(" + expr + ")"; + } + int[] s = spans.get(RAND.nextInt(spans.size())); + String before = expr.substring(0, s[0]); + String num = expr.substring(s[0], s[1]); + String after = expr.substring(s[1]); + return before + func + "(" + num + ")" + after; + } + + public List findNumberSpans(String expr) { + List spans = new ArrayList<>(); + char[] chs = expr.toCharArray(); + int i = 0, n = chs.length; + while (i < n) { + if (Character.isDigit(chs[i])) { + int j = i; + while (j < n && (Character.isDigit(chs[j]))) { + j++; + } + spans.add(new int[]{i, j}); + i = j; + } else { + i++; + } + } + return spans; + } + + public int randInt(int a, int b) { + return RAND.nextInt(b - a + 1) + a; + } + + public T randomChoice(List list) { + return list.get(RAND.nextInt(list.size())); + } +} diff --git a/src/model/LanguageSwitch.java b/src/model/LanguageSwitch.java new file mode 100644 index 0000000..a3c0501 --- /dev/null +++ b/src/model/LanguageSwitch.java @@ -0,0 +1,23 @@ +package model; + +public class LanguageSwitch { + + public static String levelToChinese(Login.Level l) { + return switch (l) { + case PRIMARY -> "小学"; + case MIDDLE -> "初中"; + case HIGH -> "高中"; + default -> "未知"; + }; + } + + public static Login.Level chineseToLevel(String s) { + s = s.trim(); + return switch (s) { + case "小学" -> Login.Level.PRIMARY; + case "初中" -> Login.Level.MIDDLE; + case "高中" -> Login.Level.HIGH; + default -> null; + }; + } +} diff --git a/src/model/LoadFile.java b/src/model/LoadFile.java new file mode 100644 index 0000000..cb8a7a5 --- /dev/null +++ b/src/model/LoadFile.java @@ -0,0 +1,60 @@ +package model; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + + +/** + * 读取用户文件夹下已有题目的所有题目文本(用于查重) + */ +public class LoadFile { + + public static List loadExistingQuestions(String username) { + List all = new ArrayList<>(); + Path userDir = Paths.get("data", username); + if (!Files.exists(userDir)) { + return all; + } + try (DirectoryStream ds = Files.newDirectoryStream(userDir, "*.txt")) { + for (Path p : ds) { + List lines = Files.readAllLines(p, StandardCharsets.UTF_8); + StringBuilder cur = new StringBuilder(); + for (String line : lines) { + if (line.matches("^\\s*\\d+\\..*")) { + if (!cur.isEmpty()) { + all.add(cur.toString().trim()); + } + cur.setLength(0); + cur.append(line.replaceFirst("^\\s*\\d+\\.", "").trim()); + } else { + if (line.trim().isEmpty()) { + if (!cur.isEmpty()) { + all.add(cur.toString().trim()); + cur.setLength(0); + } + } else { + if (!cur.isEmpty()) { + cur.append(" "); + } + cur.append(line.trim()); + } + } + } + if (!cur.isEmpty()) { + all.add(cur.toString().trim()); + } + } + } catch (IOException e) { + throw new RuntimeException("读取题目文件失败:" + e.getMessage(), e); + } + return all.stream().map(String::trim).filter(s -> !s.isEmpty()).distinct() + .collect(Collectors.toList()); + } +} diff --git a/src/model/Login.java b/src/model/Login.java new file mode 100644 index 0000000..54192f2 --- /dev/null +++ b/src/model/Login.java @@ -0,0 +1,177 @@ +package model; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + + +/** + * 用户管理(包含预设账户与简单文件持久化) + */ +public class Login { + + public enum Level {PRIMARY, MIDDLE, HIGH} + + public static class Account implements Serializable { + + public String username; + public String password; // 明文存储(课程项目允许),真实项目请哈希 + public Level level; + public String email; + + public Account(String u, String p, Level l, String email) { + this.username = u; + this.password = p; + this.level = l; + this.email = email; + } + } + + // 内存账户表 + private static final Map accounts = new HashMap<>(); + private static final Path USERS_FILE = Paths.get("data", "users.cfg"); // 简单序列化 + + static { + // 预设账号(与你的个人项目一致) + accounts.put("张三1", new Account("张三1", "123", Level.PRIMARY, "zhangsan1@example.com")); + accounts.put("张三2", new Account("张三2", "123", Level.PRIMARY, "zhangsan2@example.com")); + accounts.put("张三3", new Account("张三3", "123", Level.PRIMARY, "zhangsan3@example.com")); + + accounts.put("李四1", new Account("李四1", "123", Level.MIDDLE, "lishi1@example.com")); + accounts.put("李四2", new Account("李四2", "123", Level.MIDDLE, "lishi2@example.com")); + accounts.put("李四3", new Account("李四3", "123", Level.MIDDLE, "lishi3@example.com")); + + accounts.put("王五1", new Account("王五1", "123", Level.HIGH, "wangwu1@example.com")); + accounts.put("王五2", new Account("王五2", "123", Level.HIGH, "wangwu2@example.com")); + accounts.put("王五3", new Account("王五3", "123", Level.HIGH, "wangwu3@example.com")); + + // 加载自文件的额外用户 + loadFromFile(); + } + + // 登录验证(GUI 调用) + public static Account login(String username, String password) { + Account acc = accounts.get(username); + if (acc != null && acc.password.equals(password)) { + return acc; + } + return null; + } + + // 注册(GUI 调用),若用户名已存在返回 false + // 注册(GUI 调用),若用户名或邮箱已存在返回 false + public static synchronized boolean register(String username, String password, Level level, + String email) { + // 检查用户名是否重复 + if (accounts.containsKey(username)) { + return false; + } + + // 检查邮箱是否重复 + for (Account existing : accounts.values()) { + if (existing.email != null && existing.email.equalsIgnoreCase(email)) { + // 邮箱重复,不区分大小写 + return false; + } + } + + // 都不重复,则注册成功 + Account acc = new Account(username, password, level, email); + accounts.put(username, acc); + persistToFile(); + return true; + } + + + // 修改密码(需提供原密码),返回是否成功 + public static synchronized boolean changePassword(String username, String oldPwd, String newPwd) { + Account acc = accounts.get(username); + if (acc == null) { + return false; + } + if (!acc.password.equals(oldPwd)) { + return false; + } + acc.password = newPwd; + persistToFile(); + return true; + } + + // 文件持久化(非常简单的 CSV-like 方案) + private static void persistToFile() { + try { + Path dir = USERS_FILE.getParent(); + if (dir != null && !Files.exists(dir)) { + Files.createDirectories(dir); + } + try (BufferedWriter bw = Files.newBufferedWriter(USERS_FILE, StandardCharsets.UTF_8)) { + for (Account a : accounts.values()) { + // 格式: username\tpassword\tlevel\temail + bw.write( + a.username + "\t" + a.password + "\t" + a.level.name() + "\t" + (a.email == null ? "" + : a.email)); + bw.newLine(); + } + } + } catch (IOException e) { + System.err.println("保存用户文件失败: " + e.getMessage()); + } + } + + private static void loadFromFile() { + if (!Files.exists(USERS_FILE)) { + return; + } + try { + for (String line : Files.readAllLines(USERS_FILE, StandardCharsets.UTF_8)) { + if (line.trim().isEmpty()) { + continue; + } + String[] p = line.split("\t"); + if (p.length >= 3) { + String username = p[0], password = p[1]; + Level level = Level.valueOf(p[2]); + String email = p.length >= 4 ? p[3] : ""; + // 不覆盖预设同名账号(以文件为准覆盖预设) + accounts.put(username, new Account(username, password, level, email)); + } + } + } catch (IOException e) { + System.err.println("读取用户文件失败: " + e.getMessage()); + } catch (Exception ex) { + System.err.println("解析用户文件异常: " + ex.getMessage()); + } + } + + // 验证密码复杂度:6-10 位,必须含大写、小写和数字 + public static boolean validatePasswordRules(String pwd) { + if (pwd == null) { + return false; + } + if (pwd.length() < 6 || pwd.length() > 10) { + return false; + } + boolean hasUpper = false, hasLower = false, hasDigit = false; + for (char c : pwd.toCharArray()) { + if (Character.isUpperCase(c)) { + hasUpper = true; + } else if (Character.isLowerCase(c)) { + hasLower = true; + } else if (Character.isDigit(c)) { + hasDigit = true; + } + } + return hasUpper && hasLower && hasDigit; + } + + // 供界面显示用户列表(调试) + public static Map getAccounts() { + return accounts; + } +} diff --git a/src/model/Paper.java b/src/model/Paper.java new file mode 100644 index 0000000..f2e73a4 --- /dev/null +++ b/src/model/Paper.java @@ -0,0 +1,23 @@ +package model; + +import java.util.List; + +/** + * 试卷封装:每一道题为字符串(题干),每题有四个选项(字符串),且标记正确选项索引 + */ +public class Paper { + + public final List questions; + public final List options; // 每题 options[i] 长度为4 + public final int[] correctIndex; // 每题正确选项下标 0..3 + + public Paper(List questions, List options, int[] correctIndex) { + this.questions = questions; + this.options = options; + this.correctIndex = correctIndex; + } + + public int size() { + return questions.size(); + } +} diff --git a/src/model/QuestionGenerator.java b/src/model/QuestionGenerator.java new file mode 100644 index 0000000..8bdba3f --- /dev/null +++ b/src/model/QuestionGenerator.java @@ -0,0 +1,33 @@ +package model; + +import java.util.List; + +public abstract class QuestionGenerator { + + protected QuestionGenerator() { + } + + public abstract List generatePaper(int n); + + public abstract String normalize(String s); + + public abstract String generateOneQuestion(); + + public abstract String genPrimary(int operands); + + public abstract String genMiddle(int operands); + + public abstract String genHigh(int operands); + + public abstract String applySquare(String expr); + + public abstract String applySqrt(String expr); + + public abstract String applyTrig(String expr); + + public abstract java.util.List findNumberSpans(String expr); + + public abstract int randInt(int a, int b); + + public abstract T randomChoice(java.util.List list); +} diff --git a/src/model/Save.java b/src/model/Save.java new file mode 100644 index 0000000..8b5cd7d --- /dev/null +++ b/src/model/Save.java @@ -0,0 +1,37 @@ +package model; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +public class Save { + + public static String savePaper(String username, List paper) { + Path userDir = Paths.get("data", username); + try { + if (!Files.exists(userDir)) { + Files.createDirectories(userDir); + } + } catch (IOException e) { + throw new RuntimeException("无法创建用户文件夹:" + e.getMessage(), e); + } + String timestamp = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()); + Path file = userDir.resolve(timestamp + ".txt"); + try (BufferedWriter bw = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) { + for (int i = 0; i < paper.size(); i++) { + bw.write((i + 1) + ". " + paper.get(i)); + bw.newLine(); + bw.newLine(); + } + } catch (IOException e) { + throw new RuntimeException("保存文件失败:" + e.getMessage(), e); + } + return file.toString(); + } +} diff --git a/src/view/ExamFrame.java b/src/view/ExamFrame.java new file mode 100644 index 0000000..eda873e --- /dev/null +++ b/src/view/ExamFrame.java @@ -0,0 +1,172 @@ +package view; + +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.ButtonGroup; +import javax.swing.BorderFactory; +import javax.swing.SwingConstants; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.GridLayout; + +import model.Paper; +import model.Login; + +/** + * 答题界面(选择题,四选一) + */ +public class ExamFrame extends JFrame { + + private Paper paper; + private Login.Account user; + private int index = 0; + private int score = 0; + + private JLabel qLabel; + private JRadioButton[] radioBtns = new JRadioButton[4]; + private ButtonGroup group; + private JButton submitBtn; + private JButton quitBtn; + + private JPanel centerPanel; + private JPanel bottomPanel; + + public ExamFrame(Paper paper, Login.Account user) { + this.paper = paper; + this.user = user; + + setTitle("答题 - " + user.username); + setSize(700, 400); + setLocationRelativeTo(null); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + qLabel = new JLabel("", SwingConstants.LEFT); + qLabel.setFont(new Font("Serif", Font.PLAIN, 16)); + JPanel top = new JPanel(new BorderLayout()); + top.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + top.add(qLabel, BorderLayout.CENTER); + + centerPanel = new JPanel(new GridLayout(4, 1, 6, 6)); + group = new ButtonGroup(); + for (int i = 0; i < 4; i++) { + radioBtns[i] = new JRadioButton(); + group.add(radioBtns[i]); + centerPanel.add(radioBtns[i]); + } + + bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + submitBtn = new JButton("提交当前题"); + quitBtn = new JButton("结束并查看成绩"); + bottomPanel.add(quitBtn); + bottomPanel.add(submitBtn); + + add(top, BorderLayout.NORTH); + add(centerPanel, BorderLayout.CENTER); + add(bottomPanel, BorderLayout.SOUTH); + + submitBtn.addActionListener(e -> submitAnswer()); + quitBtn.addActionListener(e -> finishExam()); + + showQuestion(); + setVisible(true); + } + + /** + * 显示一道题 + */ + private void showQuestion() { + if (index >= paper.size()) { + showSubmitPage(); + return; + } + String q = paper.questions.get(index); + qLabel.setText("第 " + (index + 1) + " 题: " + q + ""); + String[] opts = paper.options.get(index); + + for (int i = 0; i < 4; i++) { + radioBtns[i].setText(opts[i]); + } + // 清除上一次选择 + group.clearSelection(); + } + + /** + * 提交当前题 + */ + private void submitAnswer() { + int selected = -1; + for (int i = 0; i < 4; i++) { + if (radioBtns[i].isSelected()) { + selected = i; + } + } + if (selected == -1) { + JOptionPane.showMessageDialog(this, "请选择一个选项后再提交"); + return; + } + // 判断对错 + if (selected == paper.correctIndex[index]) { + score++; + } + + index++; + if (index < paper.size()) { + showQuestion(); + } else { + // 做完所有题 → 显示提交确认页面 + showSubmitPage(); + } + } + + /** + * 做完所有题目后显示确认提交界面 + */ + private void showSubmitPage() { + // 移除中间题目区域和按钮 + getContentPane().remove(centerPanel); + getContentPane().remove(bottomPanel); + + JLabel finishLabel = new JLabel("已完成所有题目,是否确认提交?", SwingConstants.CENTER); + finishLabel.setFont(new Font("Serif", Font.BOLD, 18)); + add(finishLabel, BorderLayout.CENTER); + + JPanel confirmPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); + JButton confirmBtn = new JButton("确认提交"); + JButton cancelBtn = new JButton("返回最后一题"); + confirmPanel.add(cancelBtn); + confirmPanel.add(confirmBtn); + add(confirmPanel, BorderLayout.SOUTH); + + confirmBtn.addActionListener(e -> finishExam()); + cancelBtn.addActionListener(e -> { + // 返回最后一题继续查看/修改 + getContentPane().remove(finishLabel); + getContentPane().remove(confirmPanel); + add(centerPanel, BorderLayout.CENTER); + add(bottomPanel, BorderLayout.SOUTH); + index = paper.size() - 1; + showQuestion(); + revalidate(); + repaint(); + }); + + revalidate(); + repaint(); + } + + /** + * 完成考试 → 进入成绩界面 + */ + private void finishExam() { + int total = paper.size(); + double percent = total == 0 ? 0 : (100.0 * score / total); + dispose(); + new ResultFrame(user, score, total, percent); + } +} diff --git a/src/view/ExamSetupFrame.java b/src/view/ExamSetupFrame.java new file mode 100644 index 0000000..0da4e57 --- /dev/null +++ b/src/view/ExamSetupFrame.java @@ -0,0 +1,76 @@ +package view; + +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.BorderFactory; + +import java.awt.GridLayout; + +import controller.FunctionController; +import model.Login; +import model.LanguageSwitch; + +/** + * 出题设置:选择/切换难度(显示当前账户默认难度),输入题数(10-30) + */ +public class ExamSetupFrame extends JFrame { + + private JTextField numberField; + private JComboBox levelBox; + private FunctionController controller; + private Login.Account user; + + public ExamSetupFrame(Login.Account user) { + this.user = user; + this.controller = new FunctionController(user); + + setTitle("出题设置 - " + user.username); + setSize(380, 200); + setLocationRelativeTo(null); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + JPanel p = new JPanel(new GridLayout(4, 2, 8, 8)); + p.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + p.add(new JLabel("当前难度:")); + levelBox = new JComboBox<>(new String[]{"小学", "初中", "高中"}); + levelBox.setSelectedItem(LanguageSwitch.levelToChinese(user.level)); + p.add(levelBox); + + p.add(new JLabel("输入题目数量 (10-30):")); + numberField = new JTextField("10"); + p.add(numberField); + + JButton startBtn = new JButton("开始出题"); + JButton backBtn = new JButton("返回登录"); + p.add(startBtn); + p.add(backBtn); + + add(p); + + startBtn.addActionListener(e -> { + try { + int n = Integer.parseInt(numberField.getText().trim()); + // 更新用户难度为界面选择 + String lv = (String) levelBox.getSelectedItem(); + Login.Level newLv = LanguageSwitch.chineseToLevel(lv); + user.level = newLv; + controller.startExam(n); + dispose(); + } catch (NumberFormatException ex) { + JOptionPane.showMessageDialog(this, "请输入有效整数"); + } + }); + + backBtn.addActionListener(e -> { + dispose(); + new LoginFrame(); + }); + + setVisible(true); + } +} diff --git a/src/view/LoginFrame.java b/src/view/LoginFrame.java new file mode 100644 index 0000000..bda2dbb --- /dev/null +++ b/src/view/LoginFrame.java @@ -0,0 +1,78 @@ +package view; + +import java.awt.GridLayout; + +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.JPasswordField; +import javax.swing.BorderFactory; + +import model.Login; + +/** + * 登录窗口 + */ +public class LoginFrame extends JFrame { + + private JTextField usernameField; + private JPasswordField passwordField; + + public LoginFrame() { + setTitle("数学卷子生成器 - 登录"); + setSize(380, 220); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setLocationRelativeTo(null); + + JPanel p = new JPanel(new GridLayout(4, 2, 8, 8)); + p.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + p.add(new JLabel("用户名:")); + usernameField = new JTextField(); + p.add(usernameField); + + p.add(new JLabel("密码:")); + passwordField = new JPasswordField(); + p.add(passwordField); + + JButton loginBtn = new JButton("登录"); + JButton regBtn = new JButton("注册"); + JButton exitBtn = new JButton("退出"); + + p.add(loginBtn); + p.add(regBtn); + p.add(new JLabel()); + p.add(exitBtn); + + add(p); + + loginBtn.addActionListener(e -> { + String u = usernameField.getText().trim(); + String pwd = new String(passwordField.getPassword()).trim(); + if (u.isEmpty() || pwd.isEmpty()) { + JOptionPane.showMessageDialog(this, "请输入用户名和密码", "提示", + JOptionPane.INFORMATION_MESSAGE); + return; + } + Login.Account acc = Login.login(u, pwd); + if (acc != null) { + dispose(); + new MainMenuFrame(acc); // 登录后先进入主菜单 + } else { + JOptionPane.showMessageDialog(this, "用户名或密码错误", "登录失败", + JOptionPane.ERROR_MESSAGE); + } + + }); + + regBtn.addActionListener(e -> { + new RegisterFrame(this); + }); + + exitBtn.addActionListener(e -> System.exit(0)); + + setVisible(true); + } +} diff --git a/src/view/MainMenuFrame.java b/src/view/MainMenuFrame.java new file mode 100644 index 0000000..7fdc377 --- /dev/null +++ b/src/view/MainMenuFrame.java @@ -0,0 +1,97 @@ +package view; + +import java.awt.GridLayout; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPasswordField; +import javax.swing.BorderFactory; + +import model.Login; + + +/** + * 登录成功后的主菜单(可修改密码、开始出题、登出) 现在由 ExamSetupFrame 直接替代,但保留此类可拓展 + */ +public class MainMenuFrame extends JFrame { + + public MainMenuFrame(Login.Account user) { + setTitle("主菜单 - " + user.username); + setSize(400, 200); + setLocationRelativeTo(null); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + JPanel p = new JPanel(new GridLayout(3, 1, 10, 10)); + JButton changePwd = new JButton("修改密码"); + JButton start = new JButton("出题设置"); + JButton logout = new JButton("退出登录"); + p.add(start); + p.add(changePwd); + p.add(logout); + add(p); + + start.addActionListener(e -> { + new ExamSetupFrame(user); + dispose(); + }); + changePwd.addActionListener(e -> new ChangePasswordDialog(this, user)); + logout.addActionListener(e -> { + dispose(); + new LoginFrame(); + }); + + setVisible(true); + } + + // 内部类:修改密码对话框 + static class ChangePasswordDialog extends JDialog { + + public ChangePasswordDialog(JFrame owner, Login.Account user) { + super(owner, "修改密码", true); + setSize(350, 200); + setLocationRelativeTo(owner); + JPanel p = new JPanel(new GridLayout(4, 2, 6, 6)); + p.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6)); + p.add(new JLabel("旧密码:")); + JPasswordField oldp = new JPasswordField(); + p.add(oldp); + p.add(new JLabel("新密码:")); + JPasswordField newp = new JPasswordField(); + p.add(newp); + p.add(new JLabel("再次输入新密码:")); + JPasswordField newp2 = new JPasswordField(); + p.add(newp2); + JButton ok = new JButton("确定"); + JButton cancel = new JButton("取消"); + p.add(ok); + p.add(cancel); + add(p); + ok.addActionListener(a -> { + String oldPwd = new String(oldp.getPassword()).trim(); + String np = new String(newp.getPassword()).trim(); + String np2 = new String(newp2.getPassword()).trim(); + if (!np.equals(np2)) { + JOptionPane.showMessageDialog(this, "两次密码不一致"); + return; + } + if (!Login.validatePasswordRules(np)) { + JOptionPane.showMessageDialog(this, "密码不满足规则"); + return; + } + boolean okr = Login.changePassword(user.username, oldPwd, np); + if (okr) { + JOptionPane.showMessageDialog(this, "修改成功"); + dispose(); + } else { + JOptionPane.showMessageDialog(this, "旧密码错误"); + } + }); + cancel.addActionListener(a -> dispose()); + setVisible(true); + } + } +} diff --git a/src/view/RegisterFrame.java b/src/view/RegisterFrame.java new file mode 100644 index 0000000..a08c1a6 --- /dev/null +++ b/src/view/RegisterFrame.java @@ -0,0 +1,166 @@ +package view; + +import java.awt.GridLayout; +import java.util.Properties; +import java.util.Random; + +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPasswordField; +import javax.swing.JTextField; +import javax.swing.BorderFactory; + +import jakarta.mail.Authenticator; +import jakarta.mail.Message; +import jakarta.mail.PasswordAuthentication; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; + +import model.Login; +import model.LanguageSwitch; + +/** + * 注册界面(使用 QQ 邮箱发送验证码) + */ +public class RegisterFrame extends JDialog { + + private JTextField usernameField; + private JTextField emailField; + private JComboBox levelBox; + private JPasswordField pwdField; + private JPasswordField pwdField2; + private JTextField codeField; + private String lastCode; + + // QQ 邮箱配置 + private static final String FROM_EMAIL = "songqifeng.sqf@qq.com"; + private static final String AUTH_CODE = "gcyschltjgxedgjd"; // ⚠️ 在 QQ 邮箱里申请的授权码 + + public RegisterFrame(JFrame owner) { + super(owner, "注册新用户", true); + setSize(420, 320); + setLocationRelativeTo(owner); + + JPanel p = new JPanel(new GridLayout(7, 2, 6, 6)); + p.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + p.add(new JLabel("用户名:")); + usernameField = new JTextField(); + p.add(usernameField); + + p.add(new JLabel("邮箱(接收注册码):")); + emailField = new JTextField(); + p.add(emailField); + + p.add(new JLabel("年级:")); + levelBox = new JComboBox<>(new String[]{"小学", "初中", "高中"}); + p.add(levelBox); + + p.add(new JLabel("密码 (6-10位,含大小写与数字):")); + pwdField = new JPasswordField(); + p.add(pwdField); + + p.add(new JLabel("再次输入密码:")); + pwdField2 = new JPasswordField(); + p.add(pwdField2); + + JButton sendCodeBtn = new JButton("发送注册码"); + p.add(sendCodeBtn); + codeField = new JTextField(); + p.add(codeField); + + JButton regBtn = new JButton("注册"); + p.add(regBtn); + JButton cancelBtn = new JButton("取消"); + p.add(cancelBtn); + + add(p); + + sendCodeBtn.addActionListener(e -> { + String email = emailField.getText().trim(); + if (email.isEmpty() || !email.contains("@")) { + JOptionPane.showMessageDialog(this, "请输入有效邮箱"); + return; + } + lastCode = String.format("%04d", new Random().nextInt(10000)); + boolean sent = sendEmail(email, lastCode); + if (sent) { + JOptionPane.showMessageDialog(this, "注册码已发送,请检查邮箱。"); + } else { + JOptionPane.showMessageDialog(this, "发送邮件失败,请检查网络或邮箱配置。"); + } + }); + + regBtn.addActionListener(e -> { + String u = usernameField.getText().trim(); + String email = emailField.getText().trim(); + String pwd = new String(pwdField.getPassword()).trim(); + String pwd2 = new String(pwdField2.getPassword()).trim(); + String code = codeField.getText().trim(); + + if (u.isEmpty() || email.isEmpty() || pwd.isEmpty() || pwd2.isEmpty() || code.isEmpty()) { + JOptionPane.showMessageDialog(this, "请填写完整信息并输入注册码"); + return; + } + if (!code.equals(lastCode)) { + JOptionPane.showMessageDialog(this, "注册码错误,请重新输入"); + return; + } + if (!pwd.equals(pwd2)) { + JOptionPane.showMessageDialog(this, "两次密码不一致"); + return; + } + if (!Login.validatePasswordRules(pwd)) { + JOptionPane.showMessageDialog(this, "密码不满足要求:6-10位且包含大写、小写和数字"); + return; + } + String levelStr = (String) levelBox.getSelectedItem(); + Login.Level lv = LanguageSwitch.chineseToLevel(levelStr); + boolean ok = Login.register(u, pwd, lv, email); + if (!ok) { + JOptionPane.showMessageDialog(this, "用户名或邮箱已存在,请换一个用户名"); + return; + } + JOptionPane.showMessageDialog(this, "注册成功,请用新用户登录"); + dispose(); + }); + + cancelBtn.addActionListener(e -> dispose()); + + setVisible(true); + } + + private boolean sendEmail(String to, String code) { + try { + Properties props = new Properties(); + props.put("mail.smtp.host", "smtp.qq.com"); + props.put("mail.smtp.port", "465"); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.ssl.enable", "true"); // ← 开启 SSL + + Session session = Session.getInstance(props, new Authenticator() { + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(FROM_EMAIL, AUTH_CODE); + } + }); + + Message message = new MimeMessage(session); + message.setFrom(new InternetAddress(FROM_EMAIL)); + message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); + message.setSubject("数学卷子生成器 - 注册验证码"); + message.setText("您的注册码为: " + code + "\n有效期 5 分钟。"); + + Transport.send(message); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } +} diff --git a/src/view/ResultFrame.java b/src/view/ResultFrame.java new file mode 100644 index 0000000..a5186c4 --- /dev/null +++ b/src/view/ResultFrame.java @@ -0,0 +1,47 @@ +package view; + +import java.awt.FlowLayout; +import java.awt.GridLayout; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import model.Login; + +/** + * 显示成绩并提供“退出或继续做题”的选择 + */ +public class ResultFrame extends JFrame { + + public ResultFrame(Login.Account user, int score, int total, double percent) { + setTitle("成绩 - " + user.username); + setSize(360, 220); + setLocationRelativeTo(null); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + JPanel p = new JPanel(new GridLayout(5, 1, 6, 6)); + p.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + p.add(new JLabel("用户名: " + user.username)); + p.add(new JLabel("得分: " + score + " / " + total)); + p.add(new JLabel(String.format("百分比: %.2f%%", percent))); + + JPanel btns = new JPanel(new FlowLayout()); + JButton exit = new JButton("退出"); + JButton again = new JButton("继续做题"); + btns.add(again); + btns.add(exit); + p.add(btns); + + add(p); + exit.addActionListener(e -> System.exit(0)); + again.addActionListener(e -> { + dispose(); + new ExamSetupFrame(user); + }); + + setVisible(true); + } +}