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