From 8a8a03839ca442552daf378a1fea2fbb5bb79a4b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=A8=E9=BB=98=E6=B6=B5?=
<15530826+wgll926@user.noreply.gitee.com>
Date: Thu, 9 Oct 2025 18:27:22 +0800
Subject: [PATCH] =?UTF-8?q?=E8=8E=B7=E5=8F=96=E9=82=AE=E7=AE=B1=E9=AA=8C?=
=?UTF-8?q?=E8=AF=81=E7=A0=81=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pom.xml | 10 ++
.../mathsystemtogether/EmailCodeService.java | 125 ++++++++++++++++++
.../mathsystemtogether/ExamController.java | 76 +++++++++++
.../mathsystemtogether/HelloApplication.java | 2 +-
src/main/java/module-info.java | 2 +
.../example/mathsystemtogether/exam-view.fxml | 19 +++
.../mathsystemtogether/mail.properties | 9 ++
7 files changed, 242 insertions(+), 1 deletion(-)
create mode 100644 src/main/java/com/example/mathsystemtogether/EmailCodeService.java
create mode 100644 src/main/resources/com/example/mathsystemtogether/mail.properties
diff --git a/pom.xml b/pom.xml
index 96bff82..579e6ad 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,6 +38,16 @@
${junit.version}
test
+
+ com.sun.mail
+ jakarta.mail
+ 2.0.1
+
+
+ jakarta.activation
+ jakarta.activation-api
+ 2.1.3
+
diff --git a/src/main/java/com/example/mathsystemtogether/EmailCodeService.java b/src/main/java/com/example/mathsystemtogether/EmailCodeService.java
new file mode 100644
index 0000000..0bdf163
--- /dev/null
+++ b/src/main/java/com/example/mathsystemtogether/EmailCodeService.java
@@ -0,0 +1,125 @@
+package com.example.mathsystemtogether;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Random;
+import java.util.concurrent.ConcurrentHashMap;
+import jakarta.mail.Authenticator;
+import jakarta.mail.Message;
+import jakarta.mail.MessagingException;
+import jakarta.mail.PasswordAuthentication;
+import jakarta.mail.Session;
+import jakarta.mail.Transport;
+import jakarta.mail.internet.InternetAddress;
+import jakarta.mail.internet.MimeMessage;
+
+/**
+ * 简单的内存版邮箱验证码服务(单进程适用)。
+ * 如需真实发信,可将 sendEmail 方法接入 SMTP 或第三方服务。
+ */
+public class EmailCodeService {
+
+ private static class CodeRecord {
+ final String code;
+ final Instant expireAt;
+
+ CodeRecord(String code, Instant expireAt) {
+ this.code = code;
+ this.expireAt = expireAt;
+ }
+ }
+
+ private final Map emailToCode = new ConcurrentHashMap<>();
+ private final Map emailToLastSend = new ConcurrentHashMap<>();
+ private final Random random = new Random();
+
+ private final Duration codeTtl = Duration.ofMinutes(5);
+ private final Duration rateLimit = Duration.ofSeconds(60);
+
+ public void sendCode(String email) {
+ Instant now = Instant.now();
+ Instant last = emailToLastSend.get(email);
+ if (last != null && Duration.between(last, now).compareTo(rateLimit) < 0) {
+ long waitSeconds = rateLimit.minus(Duration.between(last, now)).getSeconds();
+ throw new IllegalStateException("请求过于频繁,请于" + waitSeconds + "秒后重试");
+ }
+
+ String code = generateCode();
+ emailToCode.put(email, new CodeRecord(code, now.plus(codeTtl)));
+ emailToLastSend.put(email, now);
+
+ // 使用 SMTP 发送邮件(QQ邮箱)
+ sendEmail(email, code);
+ }
+
+ public boolean verifyCode(String email, String code) {
+ CodeRecord record = emailToCode.get(email);
+ if (record == null) return false;
+ if (Instant.now().isAfter(record.expireAt)) {
+ emailToCode.remove(email);
+ return false;
+ }
+ boolean ok = record.code.equals(code);
+ if (ok) {
+ emailToCode.remove(email); // 一次性
+ }
+ return ok;
+ }
+
+ private String generateCode() {
+ return String.valueOf(100000 + random.nextInt(900000));
+ }
+
+ private void sendEmail(String email, String code) {
+ // 从资源文件读取发件配置
+ Properties conf = new Properties();
+ try {
+ conf.load(EmailCodeService.class.getResourceAsStream("/com/example/mathsystemtogether/mail.properties"));
+ } catch (Exception e) {
+ System.out.println("[EmailCodeService] 读取 mail.properties 失败,改为控制台输出验证码:" + code);
+ return;
+ }
+
+ String from = conf.getProperty("mail.from", "");
+ String authCode = conf.getProperty("mail.authCode", "");
+ String host = conf.getProperty("mail.host", "smtp.qq.com");
+ String port = conf.getProperty("mail.port", "465");
+ boolean ssl = Boolean.parseBoolean(conf.getProperty("mail.ssl", "true"));
+ String subject = conf.getProperty("mail.subject", "您的验证码");
+ String bodyPrefix = conf.getProperty("mail.bodyPrefix", "验证码:");
+ String bodySuffix = conf.getProperty("mail.bodySuffix", ",5分钟内有效。");
+
+ if (from.isEmpty() || authCode.isEmpty()) {
+ System.out.println("[EmailCodeService] 未配置 mail.from 或 mail.authCode,改为控制台输出验证码:" + code);
+ return;
+ }
+
+ Properties props = new Properties();
+ props.put("mail.smtp.host", host);
+ props.put("mail.smtp.port", port);
+ props.put("mail.smtp.auth", "true");
+ props.put("mail.smtp.ssl.enable", String.valueOf(ssl));
+
+ Session session = Session.getInstance(props, new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(from, authCode);
+ }
+ });
+
+ try {
+ Message message = new MimeMessage(session);
+ message.setFrom(new InternetAddress(from));
+ message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(email));
+ message.setSubject(subject);
+ message.setText(bodyPrefix + code + bodySuffix);
+ Transport.send(message);
+ } catch (MessagingException e) {
+ throw new IllegalStateException("邮件发送失败:" + e.getMessage(), e);
+ }
+ }
+}
+
+
diff --git a/src/main/java/com/example/mathsystemtogether/ExamController.java b/src/main/java/com/example/mathsystemtogether/ExamController.java
index d591fc2..7625583 100644
--- a/src/main/java/com/example/mathsystemtogether/ExamController.java
+++ b/src/main/java/com/example/mathsystemtogether/ExamController.java
@@ -20,6 +20,11 @@ public class ExamController {
@FXML private TextField usernameField;
@FXML private PasswordField passwordField;
@FXML private Button loginButton;
+ @FXML private TextField emailField;
+ @FXML private TextField emailCodeField;
+ @FXML private Button sendCodeButton;
+ @FXML private Button verifyCodeButton;
+ @FXML private Label emailStatusLabel;
@FXML private Label loginStatusLabel;
// 考试设置界面控件
@@ -39,6 +44,8 @@ public class ExamController {
private Map userAnswers = new HashMap<>();
private ChoiceQuestionGenerator questionGenerator;
private final Map userMap = new HashMap<>();
+ private final EmailCodeService emailCodeService = new EmailCodeService();
+ private boolean emailVerified = false;
// 常量定义
private static final int MIN_QUESTIONS = 5;
@@ -50,6 +57,9 @@ public class ExamController {
setupLevelComboBox();
examSetupPanel.setVisible(false);
questionCountField.setText("10");
+ if (emailStatusLabel != null) {
+ emailStatusLabel.setText("");
+ }
}
private void initAccounts() {
@@ -93,6 +103,11 @@ public class ExamController {
examSetupPanel.setVisible(true);
loginStatusLabel.setText("登录成功!");
loginStatusLabel.setStyle("-fx-text-fill: green;");
+ emailVerified = false;
+ if (emailStatusLabel != null) {
+ emailStatusLabel.setText("请进行邮箱验证后再开始考试");
+ emailStatusLabel.setStyle("-fx-text-fill: #8B0000;");
+ }
} else {
loginStatusLabel.setText("用户名或密码错误");
loginStatusLabel.setStyle("-fx-text-fill: red;");
@@ -110,6 +125,10 @@ public class ExamController {
passwordField.clear();
loginStatusLabel.setText("");
statusLabel.setText("");
+ emailVerified = false;
+ if (emailField != null) emailField.clear();
+ if (emailCodeField != null) emailCodeField.clear();
+ if (emailStatusLabel != null) emailStatusLabel.setText("");
}
@FXML
@@ -132,6 +151,12 @@ public class ExamController {
String selectedLevel = levelComboBox.getValue();
ChoiceQuestionGenerator.Level level = ChoiceQuestionGenerator.Level.valueOf(selectedLevel);
+ if (!emailVerified) {
+ statusLabel.setText("请先完成邮箱验证码验证");
+ statusLabel.setStyle("-fx-text-fill: red;");
+ return;
+ }
+
// 生成考试题目
questionGenerator = new ChoiceQuestionGenerator();
examQuestions = questionGenerator.generateQuestions(level, count);
@@ -150,6 +175,57 @@ public class ExamController {
statusLabel.setStyle("-fx-text-fill: red;");
}
}
+
+ @FXML
+ private void handleSendEmailCode() {
+ String email = emailField != null ? emailField.getText().trim() : "";
+ if (email.isEmpty()) {
+ if (emailStatusLabel != null) {
+ emailStatusLabel.setText("请输入邮箱");
+ emailStatusLabel.setStyle("-fx-text-fill: red;");
+ }
+ return;
+ }
+ try {
+ emailCodeService.sendCode(email);
+ if (emailStatusLabel != null) {
+ emailStatusLabel.setText("验证码已发送,请在5分钟内查收(控制台可见)");
+ emailStatusLabel.setStyle("-fx-text-fill: green;");
+ }
+ } catch (Exception ex) {
+ if (emailStatusLabel != null) {
+ emailStatusLabel.setText("发送失败:" + ex.getMessage());
+ emailStatusLabel.setStyle("-fx-text-fill: red;");
+ }
+ }
+ }
+
+ @FXML
+ private void handleVerifyEmailCode() {
+ String email = emailField != null ? emailField.getText().trim() : "";
+ String code = emailCodeField != null ? emailCodeField.getText().trim() : "";
+ if (email.isEmpty() || code.isEmpty()) {
+ if (emailStatusLabel != null) {
+ emailStatusLabel.setText("请输入邮箱和验证码");
+ emailStatusLabel.setStyle("-fx-text-fill: red;");
+ }
+ return;
+ }
+ boolean ok = emailCodeService.verifyCode(email, code);
+ if (ok) {
+ emailVerified = true;
+ if (emailStatusLabel != null) {
+ emailStatusLabel.setText("邮箱验证成功,可开始考试");
+ emailStatusLabel.setStyle("-fx-text-fill: green;");
+ }
+ } else {
+ emailVerified = false;
+ if (emailStatusLabel != null) {
+ emailStatusLabel.setText("验证码错误或已过期");
+ emailStatusLabel.setStyle("-fx-text-fill: red;");
+ }
+ }
+ }
private void startExam() {
try {
diff --git a/src/main/java/com/example/mathsystemtogether/HelloApplication.java b/src/main/java/com/example/mathsystemtogether/HelloApplication.java
index 9cf2187..91183f8 100644
--- a/src/main/java/com/example/mathsystemtogether/HelloApplication.java
+++ b/src/main/java/com/example/mathsystemtogether/HelloApplication.java
@@ -11,7 +11,7 @@ public class HelloApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("exam-view.fxml"));
- Scene scene = new Scene(fxmlLoader.load(), 900, 800);
+ Scene scene = new Scene(fxmlLoader.load(), 900, 900);
stage.setTitle("数学考试系统");
stage.setResizable(true);
stage.setScene(scene);
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 30fc570..9c6a55e 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -1,6 +1,8 @@
module com.example.mathsystemtogether {
requires javafx.controls;
requires javafx.fxml;
+ requires jakarta.mail;
+ requires jakarta.activation;
opens com.example.mathsystemtogether to javafx.fxml;
diff --git a/src/main/resources/com/example/mathsystemtogether/exam-view.fxml b/src/main/resources/com/example/mathsystemtogether/exam-view.fxml
index 86e5ffb..46b5366 100644
--- a/src/main/resources/com/example/mathsystemtogether/exam-view.fxml
+++ b/src/main/resources/com/example/mathsystemtogether/exam-view.fxml
@@ -55,6 +55,25 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/com/example/mathsystemtogether/mail.properties b/src/main/resources/com/example/mathsystemtogether/mail.properties
new file mode 100644
index 0000000..b82b7ad
--- /dev/null
+++ b/src/main/resources/com/example/mathsystemtogether/mail.properties
@@ -0,0 +1,9 @@
+mail.from=1961004835@qq.com
+mail.authCode=nixyzbpbcrekhgfe
+mail.host=smtp.qq.com
+mail.port=465
+mail.ssl=true
+mail.subject=您的验证码
+mail.bodyPrefix=验证码:
+mail.bodySuffix=,5分钟内有效。
+