commit #4

Merged
plks47r9b merged 8 commits from develop into main 4 months ago

8
.idea/.gitignore vendored

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AskMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EditMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

@ -0,0 +1,19 @@
<component name="libraryTable">
<library name="lib">
<CLASSES>
<root url="jar://$PROJECT_DIR$/lib/javax.ejb.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/javax.servlet.jsp.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/javax.transaction.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/jakarta.mail-2.0.1.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/javax.resource.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/javax.servlet.jsp.jstl.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/javax.jms.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/javax.persistence.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/javax.annotation.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/javax.servlet.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/jakarta.activation-2.0.1.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="openjdk-23" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/mathStudyAPP.iml" filepath="$PROJECT_DIR$/mathStudyAPP.iml" />
</modules>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

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

@ -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

Binary file not shown.

@ -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
---

Binary file not shown.

Binary file not shown.

@ -0,0 +1,12 @@
<?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" />
</component>
</module>

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

@ -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);
}
}

@ -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<String> existing = LoadFile.loadExistingQuestions(user.username);
List<String> paper = generatePaper(n, currentLevel, existing);
if (paper.isEmpty()) return null;
List<String[]> 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<String> generatePaper(int n, Login.Level level, List<String> 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<String> 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<String> 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);
}
}

@ -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<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<>();
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,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<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,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;
};
}
}

@ -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<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,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<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
// 注册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<String, Account> getAccounts() {
return accounts;
}
}

@ -0,0 +1,23 @@
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,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<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,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("<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,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<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,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);
}
}

@ -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);
}
}
}

@ -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<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,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);
}
}
Loading…
Cancel
Save