pull/1/head
宋奇峰 2 months ago
parent 2a068523af
commit 41ecae7f5d

@ -1,2 +0,0 @@
# mathStudyAPP

@ -0,0 +1,20 @@
1. 37 + 35
2. 82 * 28
3. (16 / 56) + 42
4. 6 * 52 + 35
5. (99 + 26) + 37
6. 40 * 63
7. 52 - 61 - 16 - 70 * 51
8. 65 + 83
9. 84 / 64
10. 24 - 47 + 21 + 1 - 33

@ -0,0 +1,10 @@
王五3 123 HIGH wangwu3@example.com
王五2 123 HIGH wangwu2@example.com
王五1 123 HIGH wangwu1@example.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 Aa123456 PRIMARY sqf090815@hnu.edu.cn
李四1 123 MIDDLE lishi1@example.com
李四2 123 MIDDLE lishi2@example.com

@ -0,0 +1,27 @@
# mathStudyAPP
## 📂 项目结构
src/
├─ Main.java
├─ controller/
│ ├─ AssignController.java
│ └─ FunctionController.java
├─ model/
│ ├─ Login.java
│ ├─ LanguageSwitch.java
│ ├─ QuestionGenerator.java
│ ├─ Generator.java
│ ├─ LoadFile.java
│ ├─ Save.java
│ ├─ Create.java
│ └─ Paper.java
└─ view/
├─ LoginFrame.java
├─ RegisterFrame.java
├─ MainMenuFrame.java
├─ ExamSetupFrame.java
├─ ExamFrame.java
└─ ResultFrame.java
---

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="lib" level="project" />
<orderEntry type="library" name="jakarta.activation-2.0.1 (2)" level="project" />
<orderEntry type="library" name="jakarta.mail-2.0.1 (2)" level="project" />
</component>
</module>

@ -0,0 +1,12 @@
import javax.swing.SwingUtilities;
import view.LoginFrame;
/**
*
*/
public class Main {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new LoginFrame());
}
}

@ -0,0 +1,18 @@
package controller;
import model.Login;
import model.LanguageSwitch;
import view.ExamSetupFrame;
import javax.swing.JOptionPane;
/**
*
*/
public class AssignController {
public void loginSuccess(Login.Account user) {
JOptionPane.showMessageDialog(null,
"登录成功。当前选择为 " + LanguageSwitch.levelToChinese(user.level) + " 出题");
new ExamSetupFrame(user);
}
}

@ -0,0 +1,33 @@
package controller;
import model.Create;
import model.Login;
import model.Paper;
import view.ExamFrame;
import javax.swing.JOptionPane;
/**
* 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);
}
}

@ -0,0 +1,157 @@
package model;
import java.util.*;
import javax.script.*;
/**
*
*/
public class Create {
// 把数学表达式转换为 JS 可执行表达式的简单转换器
private static String toJsExpr(String expr) {
String s = expr;
s = s.replaceAll("\\^2", ".pow2"); // 临时占位
s = s.replaceAll("sqrt\\(", "Math.sqrt(");
s = s.replaceAll("sin\\(", "Math.sin(Math.toRadians(");
s = s.replaceAll("cos\\(", "Math.cos(Math.toRadians(");
s = s.replaceAll("tan\\(", "Math.tan(Math.toRadians(");
// 修复 sin(Math.toRadians(x) 末尾多一层 ) -> we need to close paren
// Simple approach: for each "Math.sin(Math.toRadians(", there will be following number or parenthesis, we close later with "))"
// Replace our .pow2 placeholder into Math.pow(x,2)
// Handle pow2: we earlier converted x^2 -> x.pow2, so replace (\d+|...) .pow2 to Math.pow(that,2)
// For simplicity, replace pattern "([0-9)]+)\\.pow2" with "Math.pow($1,2)" — approximate
s = s.replaceAll("(\\d+)\\.pow2", "Math.pow($1,2)");
// If there are parentheses before .pow2: ") .pow2" unlikely; accept some limitations.
// For sin/cos/tan we changed 'sin(' -> 'Math.sin(Math.toRadians(' so must add two closing ) after the argument.
// To keep it simple, replace "Math.sin(Math.toRadians(" -> "Math.sin(Math.toRadians(" and later we will ensure parentheses in evaluation by appending "))" when needed.
// We'll also replace any leftover "^2" directly
s = s.replaceAll("\\^2", "");
// Finally, replace any multiple spaces
s = s.replaceAll("\\s+", "");
return s;
}
// 评估表达式数值double若失败抛异常
// 评估表达式数值double若失败抛异常
private static double evalExpression(String expr) throws ScriptException {
return ExpressionEvaluator.evaluate(expr);
}
/**
* Paper questions, options, correctIndex
*/
public static Paper create(int n, Login.Level currentLevel, Login.Account user) {
List<String> existing = LoadFile.loadExistingQuestions(user.username);
Generator qg = new Generator(currentLevel, existing);
List<String> paper = qg.generatePaper(n);
if (paper.isEmpty()) {
return null;
}
// 为每道题生成 4 个选项:一个正确值(三位有效数)加上 3 个干扰项
List<String[]> optionsList = new ArrayList<>();
int[] correctIdx = new int[paper.size()];
for (int i = 0; i < paper.size(); i++) {
String q = paper.get(i);
double correctVal;
try {
correctVal = evalExpression(q);
} catch (Exception ex) {
// 如果无法精确计算(如复杂表达式),我们退而使用字符串匹配:将题目文本作为正确选项,生成三个变体文本
String[] opts = new String[4];
opts[0] = q;
opts[1] = q + " + 1";
opts[2] = q + " - 1";
opts[3] = "错误:" + q;
shuffleArray(opts);
int idx = findIndex(opts, q);
optionsList.add(opts);
correctIdx[i] = idx;
continue;
}
// 格式化正确值为保留3位小数若为整数则不显示小数
String correctStr = formatNumber(correctVal);
// 判断是否为整数答案
boolean isIntAnswer = Math.abs(correctVal - Math.round(correctVal)) < 1e-6;
// 产生三个干扰值
Set<String> optsSet = new LinkedHashSet<>();
optsSet.add(correctStr);
Random r = new Random();
int attempts = 0;
while (optsSet.size() < 4 && attempts < 50) {
attempts++;
double delta;
if (isIntAnswer) {
// 整数题:干扰项也是整数(偏差 ±1~±10
delta = r.nextInt(10) + 1;
if (r.nextBoolean()) delta = -delta;
} else {
// 小数题:随机偏差比例
delta = (Math.abs(correctVal) < 1e-6)
? (r.nextDouble() * 5 + 1)
: (r.nextGaussian() * Math.max(1, Math.abs(correctVal) * 0.15));
}
double cand = correctVal + delta;
String s = formatNumber(cand);
optsSet.add(s);
}
// 不足 4 个时补齐
while (optsSet.size() < 4) {
double cand = isIntAnswer
? correctVal + (r.nextInt(10) - 5)
: correctVal + (r.nextDouble() * 2 - 1);
optsSet.add(formatNumber(cand));
}
String[] optsArr = optsSet.toArray(new String[0]);
// 随机打乱并记录正确项位置
shuffleArray(optsArr);
int idx = findIndex(optsArr, correctStr);
optionsList.add(optsArr);
correctIdx[i] = idx;
}
// 保存试卷原文本(题干)到文件(按你的 Save 实现)
try {
// 为保存,我们保存原题干列表(文本)
java.util.List<String> raw = new ArrayList<>();
for (String s : paper) raw.add(s);
Save.savePaper(user.username, raw);
} catch (RuntimeException re) {
// 抛给 UI 处理(但仍返回 Paper
System.err.println("保存试卷时出错: " + re.getMessage());
}
return new Paper(paper, optionsList, correctIdx);
}
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));
} else {
return String.format("%.3f", v);
}
}
}

@ -0,0 +1,105 @@
package model;
import java.util.*;
public class ExpressionEvaluator {
private static final Map<String, Integer> 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<String> toRPN(String expr) {
List<String> output = new ArrayList<>();
Stack<String> stack = new Stack<>();
// 正则拆分 token数字、函数、符号、括号
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<String> rpn) {
Stack<Double> 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<String> rpn = toRPN(expr);
return evalRPN(rpn);
}
}

@ -0,0 +1,142 @@
package model;
import java.util.*;
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<String> existing;
public Generator(Login.Level level, List<String> existingQuestions) {
this.level = level;
this.existing = existingQuestions.stream().map(String::trim).collect(Collectors.toSet());
}
public List<String> generatePaper(int n) {
Set<String> 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<String> 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<int[]> 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<int[]> 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<int[]> 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<int[]> findNumberSpans(String expr) {
List<int[]> 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> T randomChoice(List<T> list) {
return list.get(RAND.nextInt(list.size()));
}
}

@ -0,0 +1,22 @@
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;
};
}
}

@ -0,0 +1,49 @@
package model;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.nio.file.*;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.util.stream.Collectors;
/**
*
*/
public class LoadFile {
public static List<String> loadExistingQuestions(String username) {
List<String> all = new ArrayList<>();
Path userDir = Paths.get("data", username);
if (!Files.exists(userDir)) {
return all;
}
try (DirectoryStream<Path> ds = Files.newDirectoryStream(userDir, "*.txt")) {
for (Path p : ds) {
List<String> 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());
}
}

@ -0,0 +1,160 @@
package model;
import java.util.Map;
import java.util.HashMap;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
/**
*
*/
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<String, Account> 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
public static synchronized boolean register(String username, String password, Level level,
String email) {
if (accounts.containsKey(username)) {
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<String, Account> getAccounts() {
return accounts;
}
}

@ -0,0 +1,22 @@
package model;
import java.util.List;
/**
*
*/
public class Paper {
public final List<String> questions;
public final List<String[]> options; // 每题 options[i] 长度为4
public final int[] correctIndex; // 每题正确选项下标 0..3
public Paper(List<String> questions, List<String[]> options, int[] correctIndex) {
this.questions = questions;
this.options = options;
this.correctIndex = correctIndex;
}
public int size() {
return questions.size();
}
}

@ -0,0 +1,33 @@
package model;
import java.util.List;
public abstract class QuestionGenerator {
protected QuestionGenerator() {
}
public abstract List<String> 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<int[]> findNumberSpans(String expr);
public abstract int randInt(int a, int b);
public abstract <T> T randomChoice(java.util.List<T> list);
}

@ -0,0 +1,32 @@
package model;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.nio.charset.StandardCharsets;
public class Save {
public static String savePaper(String username, List<String> 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();
}
}

@ -0,0 +1,144 @@
package view;
import javax.swing.*;
import java.awt.*;
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("<html><b>第 " + (index+1) + " 题:</b> " + q + "</html>");
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);
}
}

@ -0,0 +1,63 @@
package view;
import javax.swing.*;
import java.awt.*;
import controller.FunctionController;
import model.Login;
import model.LanguageSwitch;
/**
* /10-30
*/
public class ExamSetupFrame extends JFrame {
private JTextField numberField;
private JComboBox<String> 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);
}
}

@ -0,0 +1,68 @@
package view;
import javax.swing.*;
import java.awt.*;
import model.Login;
import controller.AssignController;
/**
*
*/
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);
}
}

@ -0,0 +1,59 @@
package view;
import javax.swing.*;
import java.awt.*;
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);
}
}
}

@ -0,0 +1,149 @@
package view;
import javax.swing.*;
import java.awt.*;
import java.util.Properties;
import java.util.Random;
import jakarta.mail.*;
import jakarta.mail.internet.*;
import model.Login;
import model.LanguageSwitch;
/**
* 使 QQ
*/
public class RegisterFrame extends JDialog {
private JTextField usernameField;
private JTextField emailField;
private JComboBox<String> 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;
}
}
}

@ -0,0 +1,39 @@
package view;
import javax.swing.*;
import java.awt.*;
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);
}
}
Loading…
Cancel
Save