for merge #4

Merged
hnu202326010319 merged 1 commits from LiangJunYaoBranch into develop 4 months ago

@ -1,141 +0,0 @@
package com.mathquiz.model;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
/**
*
*
*/
public class ChoiceQuestion {
/**
* IDUUID
*/
private String questionId;
/**
* / /
*/
private Grade grade;
/**
* "2 + 3 × (4 - 1) = ?"
*/
private String questionContent;
/**
* 4
* ["11", "10", "13", "9"]
*/
private List<String> options;
/**
* "A""B""C" "D"
*/
private String correctAnswer;
// ---------------- 构造函数 ----------------
/**
* JSON
*/
public ChoiceQuestion() {}
/**
*
*/
public ChoiceQuestion(String questionId, Grade grade, String questionContent,
List<String> options, String correctAnswer) {
this.questionId = questionId;
this.grade = grade;
this.questionContent = questionContent;
this.options = options;
this.correctAnswer = correctAnswer;
}
// ---------------- 静态工厂方法 ----------------
/**
* UUID questionId
*
* @param grade
* @param questionContent
* @param options 4
* @param correctAnswer "A"-"D"
* @return ChoiceQuestion
*/
public static ChoiceQuestion of(Grade grade, String questionContent,
List<String> options, String correctAnswer) {
return new ChoiceQuestion(UUID.randomUUID().toString(), grade, questionContent, options, correctAnswer);
}
// ---------------- Getter & Setter ----------------
public String getQuestionId() {
return questionId;
}
public void setQuestionId(String questionId) {
this.questionId = questionId;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
public String getQuestionContent() {
return questionContent;
}
public void setQuestionContent(String questionContent) {
this.questionContent = questionContent;
}
public List<String> getOptions() {
return options;
}
public void setOptions(List<String> options) {
this.options = options;
}
public String getCorrectAnswer() {
return correctAnswer;
}
public void setCorrectAnswer(String correctAnswer) {
this.correctAnswer = correctAnswer;
}
// ---------------- Object 方法 ----------------
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ChoiceQuestion that = (ChoiceQuestion) o;
return Objects.equals(questionId, that.questionId);
}
@Override
public int hashCode() {
return Objects.hash(questionId);
}
@Override
public String toString() {
return "ChoiceQuestion{" +
"questionId='" + questionId + '\'' +
", grade=" + grade +
", questionContent='" + questionContent + '\'' +
", options=" + options +
", correctAnswer='" + correctAnswer + '\'' +
'}';
}
}

@ -1,27 +0,0 @@
package com.mathquiz.model;
/**
*
*/
public enum Grade {
PRIMARY("小学", 1),
JUNIOR("初中", 2),
SENIOR("高中", 3);
private final String displayName;
private final int level;
Grade(String displayName, int level) {
this.displayName = displayName;
this.level = level;
}
public String getDisplayName() {
return displayName;
}
public int getLevel() {
return level;
}
}

@ -1,52 +0,0 @@
package com.mathquiz.model;
/**
*
*/
public class User {
private String name;
private String email;
private String encryptedPwd;
private Grade grade;
public User() {}
public User(String email, String encryptedPwd) {
this.email = email;
this.encryptedPwd = encryptedPwd;
}
// Getter & Setter
public java.lang.String getName() {
return name;
}
public void setName(java.lang.String name) {
this.name = name;
}
public java.lang.String getEmail() {
return email;
}
public void setEmail(java.lang.String email) {
this.email = email;
}
public java.lang.String getEncryptedPwd() {
return encryptedPwd;
}
public void setEncryptedPwd(java.lang.String encryptedPwd) {
this.encryptedPwd = encryptedPwd;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
}

@ -0,0 +1,58 @@
package com.mathquiz.ui;
import com.mathquiz.model.Grade;
import com.mathquiz.service.QuizService;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.TextInputDialog;
import javafx.scene.layout.VBox;
public class GradeSelectPanel extends VBox {
public GradeSelectPanel(MainWindow mainWindow) {
setPadding(new Insets(20));
setSpacing(20);
getChildren().addAll(
new Button("小学") {{
setOnAction(e -> startQuiz(mainWindow, Grade.PRIMARY));
}},
new Button("初中") {{
setOnAction(e -> startQuiz(mainWindow, Grade.JUNIOR));
}},
new Button("高中") {{
setOnAction(e -> startQuiz(mainWindow, Grade.SENIOR));
}},
new Button("修改密码") {{
setOnAction(e -> mainWindow.showPasswordModifyPanel());
}}
);
}
private void startQuiz(MainWindow mainWindow, Grade grade) {
TextInputDialog dialog = new TextInputDialog("20");
dialog.setTitle("题目数量");
dialog.setHeaderText("请输入题目数量10-30");
dialog.setContentText("数量:");
dialog.showAndWait().ifPresent(input -> {
try {
int count = Integer.parseInt(input);
if (count < 10 || count > 30) {
throw new NumberFormatException();
}
// 创建 QuizService未来可注入
QuizService quizService = new QuizService(grade, mainWindow.getFileIOService());
var questions = quizService.generateQuestions(
mainWindow.getCurrentUser().getEmail(), count
);
mainWindow.showQuizPanel(questions, quizService);
} catch (NumberFormatException e) {
new Alert(Alert.AlertType.ERROR, "请输入10-30之间的整数").showAndWait();
}
});
}
}

@ -0,0 +1,44 @@
package com.mathquiz.ui;
import com.mathquiz.model.User;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
public class LoginPanel extends VBox {
private final TextField emailField = new TextField();
private final PasswordField pwdField = new PasswordField();
public LoginPanel(MainWindow mainWindow) {
setPadding(new Insets(20));
setSpacing(10);
getChildren().addAll(
new Label("登录"),
new Label("邮箱:"),
emailField,
new Label("密码:"),
pwdField,
new Button("登录") {{
setOnAction(e -> loginAction(mainWindow));
}},
new Hyperlink("没有账号?去注册") {{
setOnAction(e -> mainWindow.showRegisterPanel());
}}
);
}
private void loginAction(MainWindow mainWindow) {
String email = emailField.getText().trim();
String password = pwdField.getText();
User user = mainWindow.getUserService().login(email, password);
if (user != null) {
mainWindow.setCurrentUser(user);
mainWindow.showGradeSelectPanel();
} else {
new Alert(Alert.AlertType.ERROR, "邮箱或密码错误").showAndWait();
}
}
}

@ -0,0 +1,77 @@
package com.mathquiz.ui;
import com.mathquiz.model.User;
import com.mathquiz.service.*;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
/**
*
* UI
*/
public class MainWindow extends BorderPane {
// 服务层引用(便于后期替换实现)
private final UserService userService;
private final FileIOService fileIOService;
private User currentUser;
public MainWindow(Stage primaryStage) {
// 初始化服务(未来可替换为 DI 容器)
this.fileIOService = new FileIOService();
this.userService = new UserService(fileIOService);
// 默认显示登录页
showLoginPanel();
}
// ---------------- 公共方法:面板切换 ----------------
public void showPanel(javafx.scene.layout.Pane panel) {
this.setCenter(panel);
}
public void showLoginPanel() {
showPanel(new LoginPanel(this));
}
public void showRegisterPanel() {
showPanel(new RegisterPanel(this));
}
public void showPasswordModifyPanel() {
if (currentUser != null) {
showPanel(new PasswordModifyPanel(this, currentUser.getEmail()));
}
}
public void showGradeSelectPanel() {
showPanel(new GradeSelectPanel(this));
}
public void showQuizPanel(java.util.List<com.mathquiz.model.ChoiceQuestion> questions, QuizService quizService) {
showPanel(new QuizPanel(this, questions, quizService));
}
public void showResultPanel(int score, Runnable onContinue) {
showPanel(new ResultPanel(this, score, onContinue));
}
// ---------------- Getter ----------------
public UserService getUserService() {
return userService;
}
public FileIOService getFileIOService() {
return fileIOService;
}
public User getCurrentUser() {
return currentUser;
}
public void setCurrentUser(User user) {
this.currentUser = user;
}
}

@ -0,0 +1,46 @@
package com.mathquiz.ui;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
public class PasswordModifyPanel extends VBox {
private final String email;
private final PasswordField oldPwdField = new PasswordField();
private final PasswordField newPwd1Field = new PasswordField();
private final PasswordField newPwd2Field = new PasswordField();
public PasswordModifyPanel(MainWindow mainWindow, String email) {
this.email = email;
setPadding(new Insets(20));
setSpacing(10);
getChildren().addAll(
new Label("修改密码"),
new Label("原密码:"),
oldPwdField,
new Label("新密码6-10位含大小写+数字):"),
newPwd1Field,
new Label("确认新密码:"),
newPwd2Field,
new Button("确认修改") {{
setOnAction(e -> changePassword(mainWindow));
}},
new Button("返回") {{
setOnAction(e -> mainWindow.showGradeSelectPanel());
}}
);
}
private void changePassword(MainWindow mainWindow) {
boolean success = mainWindow.getUserService()
.changePassword(email, oldPwdField.getText(), newPwd1Field.getText(), newPwd2Field.getText());
if (success) {
new Alert(Alert.AlertType.INFORMATION, "密码修改成功!").showAndWait();
mainWindow.showGradeSelectPanel();
} else {
new Alert(Alert.AlertType.ERROR, "原密码错误或新密码不符合要求").showAndWait();
}
}
}

@ -0,0 +1,67 @@
package com.mathquiz.ui;
import com.mathquiz.model.ChoiceQuestion;
import com.mathquiz.service.QuizService;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import java.util.ArrayList;
import java.util.List;
public class QuizPanel extends VBox {
private final List<ChoiceQuestion> questions;
private final List<String> userAnswers = new ArrayList<>();
private final QuizService quizService;
private final MainWindow mainWindow;
private int currentIndex = 0;
public QuizPanel(MainWindow mainWindow, List<ChoiceQuestion> questions, QuizService quizService) {
this.mainWindow = mainWindow;
this.questions = questions;
this.quizService = quizService;
setPadding(new Insets(20));
showQuestion(currentIndex);
}
private void showQuestion(int index) {
getChildren().clear();
ChoiceQuestion q = questions.get(index);
getChildren().add(new Label("第 " + (index + 1) + " 题 / " + questions.size()));
getChildren().add(new Label(q.getQuestionContent()));
ToggleGroup group = new ToggleGroup();
for (int i = 0; i < 4; i++) {
RadioButton rb = new RadioButton(
(char)('A' + i) + ". " + q.getOptions().get(i)
);
rb.setToggleGroup(group);
getChildren().add(rb);
}
Button submitBtn = new Button("提交");
submitBtn.setOnAction(e -> {
RadioButton selected = (RadioButton) group.getSelectedToggle();
if (selected != null) {
String answer = selected.getText().substring(0, 1); // "A"
userAnswers.add(answer);
if (index + 1 < questions.size()) {
showQuestion(index + 1);
} else {
int score = quizService.calculateScore(questions, userAnswers);
Runnable onContinue = () -> {
quizService.savePaper(mainWindow.getCurrentUser().getEmail(), questions);
};
mainWindow.showResultPanel(score, onContinue);
}
} else {
new Alert(Alert.AlertType.WARNING, "请选择一个选项").showAndWait();
}
});
getChildren().add(submitBtn);
}
}

@ -0,0 +1,80 @@
package com.mathquiz.ui;
import com.mathquiz.util.EmailUtil;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
/**
*
*/
public class RegisterPanel extends VBox {
private final TextField emailField = new TextField();
private final TextField codeField = new TextField();
private final PasswordField pwd1Field = new PasswordField();
private final PasswordField pwd2Field = new PasswordField();
public RegisterPanel(MainWindow mainWindow) {
setPadding(new Insets(20));
setSpacing(10);
getChildren().addAll(
new Label("注册"),
new Label("邮箱:"),
emailField,
new Button("发送注册码") {{
setOnAction(e -> sendCodeAction(mainWindow));
}},
new Label("注册码:"),
codeField,
new Label("密码6-10位含大小写+数字):"),
pwd1Field,
new Label("确认密码:"),
pwd2Field,
new Button("完成注册") {{
setOnAction(e -> registerAction(mainWindow));
}},
new Hyperlink("已有账号?去登录") {{
setOnAction(e -> mainWindow.showLoginPanel());
}}
);
}
private void sendCodeAction(MainWindow mainWindow) {
String email = emailField.getText().trim();
if (email.isEmpty() || !EmailUtil.isValidEmail(email)) {
showAlert("请输入有效的邮箱地址");
return;
}
// 调用服务层
boolean sent = mainWindow.getUserService().sendRegistrationCode(email);
if (sent) {
showAlert("注册码已发送(模拟)");
}
}
private void registerAction(MainWindow mainWindow) {
String email = emailField.getText().trim();
String code = codeField.getText().trim();
String pwd1 = pwd1Field.getText();
String pwd2 = pwd2Field.getText();
if (!mainWindow.getUserService().verifyCode(email, code)) {
showAlert("注册码错误");
return;
}
boolean success = mainWindow.getUserService().setPassword(email, pwd1, pwd2);
if (success) {
showAlert("注册成功!");
mainWindow.showGradeSelectPanel();
} else {
showAlert("密码不符合要求6-10位含大小写字母和数字");
}
}
private void showAlert(String message) {
new Alert(Alert.AlertType.INFORMATION, message).showAndWait();
}
}

@ -0,0 +1,32 @@
package com.mathquiz.ui;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
public class ResultPanel extends VBox {
public ResultPanel(MainWindow mainWindow, int score, Runnable onContinue) {
setPadding(new Insets(20));
setSpacing(20);
getChildren().addAll(
new Label("答题结束!"),
new Label("您的得分: " + score + " 分"),
new Button("继续做题") {{
setOnAction(e -> {
onContinue.run(); // 保存试卷
mainWindow.showGradeSelectPanel();
});
}},
new Button("退出") {{
setOnAction(e -> {
mainWindow.setCurrentUser(null);
mainWindow.showLoginPanel();
});
}}
);
}
}

@ -1,109 +0,0 @@
package com.util;
/**
*
*
*
*
* 使JavaMail
*/
public class EmailUtil {
/**
*
* @param toEmail
* @param registrationCode
* @return true
*/
public static boolean sendRegistrationCode(String toEmail, String registrationCode) {
// TODO: 暂不实现邮件发送功能
// 原因:项目需求是直接在界面显示注册码,不需要发邮件
System.out.println("【模拟】发送注册码邮件");
System.out.println("收件人: " + toEmail);
System.out.println("注册码: " + registrationCode);
return true;
}
/**
*
* @param toEmail
* @param newPassword
* @return true
*/
public static boolean sendPasswordReset(String toEmail, String newPassword) {
// TODO: 将来如果需要"找回密码"功能,可以在这里实现
System.out.println("【模拟】发送密码重置邮件");
System.out.println("收件人: " + toEmail);
System.out.println("新密码: " + newPassword);
return true;
}
/**
*
* @param email
* @return true
*/
public static boolean isValidEmail(String email) {
if (email == null || email.trim().isEmpty()) {
return false;
}
// 简单的邮箱格式验证
String emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
return email.matches(emailRegex);
}
}
/*
*
*
* 1. pom.xml
* <dependency>
* <groupId>javax.mail</groupId>
* <artifactId>javax.mail-api</artifactId>
* <version>1.6.2</version>
* </dependency>
* <dependency>
* <groupId>com.sun.mail</groupId>
* <artifactId>javax.mail</artifactId>
* <version>1.6.2</version>
* </dependency>
*
* 2.
*
* import javax.mail.*;
* import javax.mail.internet.*;
* import java.util.Properties;
*
* public static boolean sendEmail(String toEmail, String subject, String content) {
* try {
* Properties props = new Properties();
* props.put("mail.smtp.auth", "true");
* props.put("mail.smtp.starttls.enable", "true");
* props.put("mail.smtp.host", "smtp.qq.com");
* props.put("mail.smtp.port", "587");
*
* Session session = Session.getInstance(props, new Authenticator() {
* protected PasswordAuthentication getPasswordAuthentication() {
* return new PasswordAuthentication("your-email@qq.com", "your-password");
* }
* });
*
* Message message = new MimeMessage(session);
* message.setFrom(new InternetAddress("your-email@qq.com"));
* message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toEmail));
* message.setSubject(subject);
* message.setText(content);
*
* Transport.send(message);
* return true;
* } catch (Exception e) {
* e.printStackTrace();
* return false;
* }
* }
*/

@ -1,111 +0,0 @@
package com.util;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
/**
*
*
*/
public class FileUtils {
/**
*
* @param filePath
* @return
* @throws IOException
*/
public static String readFileToString(String filePath) throws IOException {
return Files.readString(Paths.get(filePath), StandardCharsets.UTF_8);
}
/**
*
* @param filePath
* @param content
* @throws IOException
*/
public static void writeStringToFile(String filePath, String content) throws IOException {
Files.writeString(Paths.get(filePath), content, StandardCharsets.UTF_8);
}
/**
*
* @param dirPath
* @throws IOException
*/
public static void createDirectoryIfNotExists(String dirPath) throws IOException {
Path path = Paths.get(dirPath);
if (!Files.exists(path)) {
Files.createDirectories(path);
}
}
/**
*
* @param filePath
* @return true
*/
public static boolean exists(String filePath) {
return Files.exists(Paths.get(filePath));
}
/**
*
* @param filePath
* @return true
*/
public static boolean deleteFile(String filePath) {
try {
return Files.deleteIfExists(Paths.get(filePath));
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
/**
*
* @param dirPath
* @return
*/
public static File[] listFiles(String dirPath) {
File dir = new File(dirPath);
if (dir.exists() && dir.isDirectory()) {
return dir.listFiles();
}
return new File[0];
}
/**
*
* @param filePath
* @param content
* @throws IOException
*/
public static void appendToFile(String filePath, String content) throws IOException {
Files.writeString(Paths.get(filePath), content, StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}
/**
*
* @param sourcePath
* @param targetPath
* @throws IOException
*/
public static void copyFile(String sourcePath, String targetPath) throws IOException {
Files.copy(Paths.get(sourcePath), Paths.get(targetPath), StandardCopyOption.REPLACE_EXISTING);
}
/**
*
* @param filePath
* @return
* @throws IOException
*/
public static long getFileSize(String filePath) throws IOException {
return Files.size(Paths.get(filePath));
}
}

@ -1,102 +0,0 @@
package com.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
*
*
*/
public class PasswordValidator {
/**
* 6-10
* @param password
* @return true
*/
public static boolean isValid(String password) {
if (password == null || password.length() < 6 || password.length() > 10) {
return false;
}
boolean hasLetter = password.matches("^(?=.*[a-z])(?=.*[A-Z]).*$");
boolean hasDigit = password.matches(".*\\d.*");
return hasLetter && hasDigit;
}
/**
* 使SHA-256
* @param password
* @return 16
*/
public static String encrypt(String password) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(password.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256算法不可用", e);
}
}
/**
*
* @param plainPassword
* @param encryptedPassword
* @return true
*/
public static boolean matches(String plainPassword, String encryptedPassword) {
return encrypt(plainPassword).equals(encryptedPassword);
}
/**
* 6-10
* @return
*/
public static String generateRegistrationCode() {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
int length = 6 + (int) (Math.random() * 5); // 6-10位
StringBuilder code = new StringBuilder();
// 确保至少有一个字母
code.append(chars.charAt((int) (Math.random() * 52)));
// 确保至少有一个数字
code.append(chars.charAt(52 + (int) (Math.random() * 10)));
// 填充剩余字符
for (int i = 2; i < length; i++) {
code.append(chars.charAt((int) (Math.random() * chars.length())));
}
// 打乱字符顺序
return shuffleString(code.toString());
}
/**
*
* @param str
* @return
*/
private static String shuffleString(String str) {
char[] chars = str.toCharArray();
for (int i = chars.length - 1; i > 0; i--) {
int j = (int) (Math.random() * (i + 1));
char temp = chars[i];
chars[i] = chars[j];
chars[j] = temp;
}
return new String(chars);
}
}

@ -1,69 +0,0 @@
package com.util;
import java.util.Collections;
import java.util.List;
import java.util.Random;
//各种随机数生成
public class RandomUtils {
private static final Random random = new Random();
//生成[min, max]范围内的随机整数(包含边界)
public static int nextInt(int min, int max) {
if (min > max) {
throw new IllegalArgumentException("min不能大于max");
}
return min + random.nextInt(max - min + 1);
}
//从数组中随机选择一个元素(模板类)
public static <T> T randomChoice(T[] array) {
if (array == null || array.length == 0) {
throw new IllegalArgumentException("数组不能为空");
}
return array[random.nextInt(array.length)];
}
//从列表中随机选择一个元素
public static <T> T randomChoice(List<T> list) {
if (list == null || list.isEmpty()) {
throw new IllegalArgumentException("列表不能为空");
}
return list.get(random.nextInt(list.size()));
}
/**
*
* @param list
*/
public static <T> void shuffle(List<T> list) {
Collections.shuffle(list, random);
}
//生成指定范围内的随机双精度浮点数
public static double nextDouble(double min, double max) {
if (min > max) {
throw new IllegalArgumentException("min不能大于max");
}
return min + (max - min) * random.nextDouble();
}
//生成随机布尔值
public static boolean nextBoolean() {
return random.nextBoolean();
}
//按概率返回true题目生成概率
//示例probability(0.7) 有70%概率返回true
public static boolean probability(double probability) {
if (probability < 0.0 || probability > 1.0) {
throw new IllegalArgumentException("概率必须在0.0-1.0之间");
}
return random.nextDouble() < probability;
}
}
Loading…
Cancel
Save