diff --git a/controller/AppController.java b/controller/AppController.java new file mode 100644 index 0000000..ee55db9 --- /dev/null +++ b/controller/AppController.java @@ -0,0 +1,316 @@ +package controller; + +import model.*; +import service.*; +import view.*; + +import javax.swing.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + + +public class AppController { + // 模型和服务 + private final UserManager userManager; + private final ExamManager examManager; + private final EmailService emailService; + + // 视图 + private final MainFrame mainFrame; + private final LoginView loginView; + private final RegisterView registerView; + private final PasswordSetView passwordSetView; + private final MainMenuView mainMenuView; + private final ExamView examView; + private final ScoreView scoreView; + + // 应用状态 + private User currentUser; + private String emailForRegistration; + private String usernameForRegistration; + private final Map registrationCodes = new HashMap<>(); + private List currentExam; + private int currentQuestionIndex; + private int score; + private int[] userAnswers; // 新增:用于存储用户每一题的答案 + + public AppController() { + this.mainFrame = new MainFrame(); + this.userManager = new UserManager(); + this.examManager = new ExamManager(); + this.emailService = new EmailService(); + + this.loginView = new LoginView(); + this.registerView = new RegisterView(); + this.passwordSetView = new PasswordSetView(); + this.mainMenuView = new MainMenuView(); + this.examView = new ExamView(); + this.scoreView = new ScoreView(); + + mainFrame.addPanel(loginView, "LOGIN"); + mainFrame.addPanel(registerView, "REGISTER"); + mainFrame.addPanel(passwordSetView, "PASSWORD_SET"); + mainFrame.addPanel(mainMenuView, "MAIN_MENU"); + mainFrame.addPanel(examView, "EXAM"); + mainFrame.addPanel(scoreView, "SCORE"); + + attachListeners(); + mainFrame.showPanel("LOGIN"); + } + + /** + * 附加所有UI组件的事件监听器。 + */ + private void attachListeners() { + // --- 登录逻辑 --- + loginView.getLoginButton().addActionListener(e -> handleLogin()); + loginView.getRegisterButton().addActionListener(e -> mainFrame.showPanel("REGISTER")); + + // --- 注册逻辑 --- + registerView.getGetCodeButton().addActionListener(e -> handleGetCode()); + registerView.getRegisterButton().addActionListener(e -> handleVerifyCode()); + registerView.getBackButton().addActionListener(e -> mainFrame.showPanel("LOGIN")); + + // --- 密码设置逻辑 --- + passwordSetView.getSubmitButton().addActionListener(e -> handleSetPassword()); + + // --- 主菜单逻辑 --- + mainMenuView.getStartExamButton().addActionListener(e -> handleStartExam()); + mainMenuView.getChangePasswordButton().addActionListener(e -> handleChangePassword()); + mainMenuView.getLogoutButton().addActionListener(e -> handleLogout()); + + // --- 考试逻辑 (已重构) --- + examView.getPrevButton().addActionListener(e -> handlePreviousQuestion()); + examView.getNextButton().addActionListener(e -> handleNextQuestion()); + examView.getFinishButton().addActionListener(e -> handleFinishExam()); + + // --- 分数界面逻辑 --- + scoreView.getContinueButton().addActionListener(e -> { + String welcomeName = currentUser.getUsername() != null && !currentUser.getUsername().isEmpty() ? currentUser.getUsername() : currentUser.getEmail(); + mainMenuView.setWelcomeMessage("欢迎, " + welcomeName); + mainFrame.showPanel("MAIN_MENU"); + }); + scoreView.getExitButton().addActionListener(e -> handleLogout()); + } + + // --- 核心处理方法 --- + + private void handleLogin() { + User user = userManager.authenticate(loginView.getIdentifier(), loginView.getPassword()); + if (user != null) { + currentUser = user; + String welcomeName = user.getUsername() != null && !user.getUsername().isEmpty() ? user.getUsername() : user.getEmail(); + mainMenuView.setWelcomeMessage("欢迎, " + welcomeName); + mainFrame.showPanel("MAIN_MENU"); + } else { + JOptionPane.showMessageDialog(mainFrame, "邮箱/用户名或密码错误", "登录失败", JOptionPane.ERROR_MESSAGE); + } + loginView.clearFields(); + } + + private void handleGetCode() { + String email = registerView.getEmail(); + String username = registerView.getUsername(); + + if (!ValidationService.isValidEmail(email)) { + JOptionPane.showMessageDialog(mainFrame, "请输入有效的邮箱格式!", "格式错误", JOptionPane.ERROR_MESSAGE); + return; + } + if (username.trim().isEmpty() || username.contains("|") || username.contains(" ")) { + JOptionPane.showMessageDialog(mainFrame, "用户名不能为空,且不能包含'|'或空格!", "格式错误", JOptionPane.ERROR_MESSAGE); + return; + } + if (userManager.emailExists(email)) { + JOptionPane.showMessageDialog(mainFrame, "该邮箱已被注册!", "注册失败", JOptionPane.ERROR_MESSAGE); + return; + } + if (userManager.usernameExists(username)) { + JOptionPane.showMessageDialog(mainFrame, "该用户名已被使用!", "注册失败", JOptionPane.ERROR_MESSAGE); + return; + } + + emailForRegistration = email; + usernameForRegistration = username; + String code = String.format("%06d", new Random().nextInt(1000000)); + registrationCodes.put(emailForRegistration, code); + + final JButton getCodeButton = registerView.getGetCodeButton(); + getCodeButton.setEnabled(false); + getCodeButton.setText("发送中..."); + + SwingWorker worker = new SwingWorker<>() { + private Exception mailException = null; + + @Override + protected Void doInBackground() { + try { + emailService.sendVerificationCode(emailForRegistration, code); + } catch (Exception e) { + this.mailException = e; + } + return null; + } + + @Override + protected void done() { + getCodeButton.setEnabled(true); + getCodeButton.setText("发送验证邮件"); + + if (mailException == null) { + JOptionPane.showMessageDialog(mainFrame, "一封包含验证码的【电子邮件】已发送至您的邮箱,请登录邮箱查收。", "发送成功", JOptionPane.INFORMATION_MESSAGE); + } else { + String errorMessage = "邮件发送失败,请检查:\n1. 网络连接是否正常。\n2. config.properties 中的邮箱和授权码是否正确。\n\n错误详情: " + mailException.getMessage(); + JOptionPane.showMessageDialog(mainFrame, errorMessage, "发送失败", JOptionPane.ERROR_MESSAGE); + mailException.printStackTrace(); + } + } + }; + worker.execute(); + } + + private void handleVerifyCode() { + String code = registerView.getCode(); + if (code.equals(registrationCodes.get(emailForRegistration))) { + mainFrame.showPanel("PASSWORD_SET"); + } else { + JOptionPane.showMessageDialog(mainFrame, "注册码错误!", "验证失败", JOptionPane.ERROR_MESSAGE); + } + } + + private void handleSetPassword() { + String pass1 = passwordSetView.getPassword(); + String pass2 = passwordSetView.getConfirmPassword(); + + if (!pass1.equals(pass2)) { + JOptionPane.showMessageDialog(mainFrame, "两次输入的密码不一致!", "错误", JOptionPane.ERROR_MESSAGE); + return; + } + if (!ValidationService.isPasswordStrong(pass1)) { + JOptionPane.showMessageDialog(mainFrame, "密码必须为6-10位,且包含大小写字母和数字。", "密码格式错误", JOptionPane.ERROR_MESSAGE); + return; + } + + userManager.registerUser(emailForRegistration, usernameForRegistration, pass1, UserType.PRIMARY); + JOptionPane.showMessageDialog(mainFrame, "注册成功!请返回登录。", "成功", JOptionPane.INFORMATION_MESSAGE); + mainFrame.showPanel("LOGIN"); + } + + private void handleStartExam() { + UserType type = mainMenuView.getSelectedType(); + int count = mainMenuView.getQuestionCount(); + + if(currentUser.getUserType() != type) { + currentUser.setUserType(type); + userManager.updateUser(currentUser); + } + + currentExam = examManager.generateExam(currentUser, type, count); + if (currentExam.size() < count) { + JOptionPane.showMessageDialog(mainFrame, "抱歉,未能生成足够数量的不重复题目,实际生成 " + currentExam.size() + " 道。", "提示", JOptionPane.WARNING_MESSAGE); + } + if (currentExam.isEmpty()) return; + + currentQuestionIndex = 0; + score = 0; + userAnswers = new int[currentExam.size()]; + Arrays.fill(userAnswers, -1); // -1 代表未作答 + + examView.setupForExam(currentExam.size()); + displayCurrentQuestion(); + mainFrame.showPanel("EXAM"); + } + + private void displayCurrentQuestion() { + Question q = currentExam.get(currentQuestionIndex); + int savedAnswer = userAnswers[currentQuestionIndex]; + examView.displayQuestion(q, currentQuestionIndex + 1, currentExam.size(), savedAnswer); + examView.updateNavigationButtons(currentQuestionIndex, currentExam.size()); + } + + private void handlePreviousQuestion() { + saveCurrentAnswer(); + if (currentQuestionIndex > 0) { + currentQuestionIndex--; + displayCurrentQuestion(); + } + } + + private void handleNextQuestion() { + saveCurrentAnswer(); + if (currentQuestionIndex < currentExam.size() - 1) { + currentQuestionIndex++; + displayCurrentQuestion(); + } + } + + private void saveCurrentAnswer() { + int selectedIndex = examView.getSelectedIndex(); + userAnswers[currentQuestionIndex] = selectedIndex; + } + + private void handleFinishExam() { + saveCurrentAnswer(); + + int choice = JOptionPane.showConfirmDialog(mainFrame, + "您确定要交卷吗?\n未完成的题目将按 0 分计算。", + "确认交卷", + JOptionPane.YES_NO_OPTION); + + if (choice == JOptionPane.YES_OPTION) { + calculateScore(); + int finalScore = (int) Math.round((double) score / currentExam.size() * 100); + scoreView.setScore(finalScore, score, currentExam.size()); + mainFrame.showPanel("SCORE"); + } + } + + private void calculateScore() { + score = 0; + for (int i = 0; i < currentExam.size(); i++) { + if (userAnswers[i] != -1 && userAnswers[i] == currentExam.get(i).getCorrectOptionIndex()) { + score++; + } + } + } + + private void handleChangePassword() { + ChangePasswordView dialog = new ChangePasswordView(mainFrame); + + dialog.getConfirmButton().addActionListener(e -> { + String oldPass = dialog.getOldPassword(); + String newPass = dialog.getNewPassword(); + String confirmPass = dialog.getConfirmPassword(); + + if (userManager.authenticate(currentUser.getEmail(), oldPass) == null) { + JOptionPane.showMessageDialog(dialog, "原密码错误!", "验证失败", JOptionPane.ERROR_MESSAGE); + return; + } + if (!newPass.equals(confirmPass)) { + JOptionPane.showMessageDialog(dialog, "两次输入的新密码不一致!", "错误", JOptionPane.ERROR_MESSAGE); + return; + } + if (!ValidationService.isPasswordStrong(newPass)) { + JOptionPane.showMessageDialog(dialog, "新密码必须为6-10位,且包含大小写字母和数字。", "密码格式错误", JOptionPane.ERROR_MESSAGE); + return; + } + + currentUser.setHashedPassword(newPass); + userManager.updateUser(currentUser); + + JOptionPane.showMessageDialog(dialog, "密码修改成功!", "成功", JOptionPane.INFORMATION_MESSAGE); + dialog.dispose(); + }); + + dialog.getCancelButton().addActionListener(e -> dialog.dispose()); + dialog.setVisible(true); + } + + private void handleLogout() { + currentUser = null; + loginView.clearFields(); + mainFrame.showPanel("LOGIN"); + } +} \ No newline at end of file