diff --git a/src/main/java/mathlearning/App.java b/src/main/java/mathlearning/App.java new file mode 100644 index 0000000..0e2ff2d --- /dev/null +++ b/src/main/java/mathlearning/App.java @@ -0,0 +1,24 @@ +package mathlearning; + +import mathlearning.service.UserService; +import mathlearning.ui.LoginFrame; + +import javax.swing.*; + +public class App { + public static void main(String[] args) { + // 设置UI风格 + try { + UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); + } catch (Exception e) { + e.printStackTrace(); + } + + // 启动应用程序 + SwingUtilities.invokeLater(() -> { + UserService userService = new UserService(); + LoginFrame loginFrame = new LoginFrame(userService); + loginFrame.setVisible(true); + }); + } +} diff --git a/src/main/java/mathlearning/model/User.java b/src/main/java/mathlearning/model/User.java new file mode 100644 index 0000000..57b7887 --- /dev/null +++ b/src/main/java/mathlearning/model/User.java @@ -0,0 +1,41 @@ +package mathlearning.model; + +import java.time.LocalDateTime; + +public class User { + private String username; + private String email; + private String passwordHash; + private LocalDateTime registrationDate; + private String verificationCode; + private boolean verified; + + public User() {} + + public User(String email, String passwordHash, String verificationCode) { + this.email = email; + this.passwordHash = passwordHash; + this.verificationCode = verificationCode; + this.verified = false; + this.registrationDate = LocalDateTime.now(); + } + + // Getters and setters + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public String getPasswordHash() { return passwordHash; } + public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; } + + public String getVerificationCode() { return verificationCode; } + public void setVerificationCode(String verificationCode) { this.verificationCode = verificationCode; } + + public boolean isVerified() { return verified; } + public void setVerified(boolean verified) { this.verified = verified; } + + public LocalDateTime getRegistrationDate() { return registrationDate; } + public void setRegistrationDate(LocalDateTime registrationDate) { this.registrationDate = registrationDate; } + + public String getUsername() { return username; } + public void setUsername(String username) {this.username = username; } +} diff --git a/src/main/java/mathlearning/service/EmailService.java b/src/main/java/mathlearning/service/EmailService.java new file mode 100644 index 0000000..86f5b0d --- /dev/null +++ b/src/main/java/mathlearning/service/EmailService.java @@ -0,0 +1,57 @@ +package mathlearning.service; + +import javax.mail.*; +import javax.mail.internet.*; +import java.util.Properties; + +public class EmailService { + private final String host; + private final String port; + private final String username; + private final String password; + private final boolean auth; + + public EmailService(String host, String port, String username, String password, boolean auth) { + this.host = host; + this.port = port; + this.username = username; + this.password = password; + this.auth = auth; + } + + public boolean sendVerificationCode(String toEmail, String verificationCode) { + try { + Properties props = new Properties(); + props.put("mail.smtp.host", host); + props.put("mail.smtp.port", port); + props.put("mail.smtp.auth", auth); + props.put("mail.smtp.starttls.enable", "true"); + + Session session = Session.getInstance(props, new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + + Message message = new MimeMessage(session); + message.setFrom(new InternetAddress(username)); + message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toEmail)); + message.setSubject("数学学习软件 - 注册验证码"); + + String emailContent = "尊敬的用户:\n\n" + + "您的注册验证码是:" + verificationCode + "\n\n" + + "该验证码有效期为10分钟。\n\n" + + "如果您没有注册本软件,请忽略此邮件。\n\n" + + "数学学习软件团队"; + + message.setText(emailContent); + + Transport.send(message); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } +} diff --git a/src/main/java/mathlearning/service/UserService.java b/src/main/java/mathlearning/service/UserService.java new file mode 100644 index 0000000..f239562 --- /dev/null +++ b/src/main/java/mathlearning/service/UserService.java @@ -0,0 +1,175 @@ +package mathlearning.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import mathlearning.model.User; +import at.favre.lib.crypto.bcrypt.BCrypt; + +import java.io.File; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class UserService { + private static final String USERS_FILE = "data/users.json"; + private static final long VERIFICATION_CODE_EXPIRY_MINUTES = 10; + + private Map users; + private Map verificationCodeTimestamps; + private ObjectMapper objectMapper; + + public UserService() { + this.users = new ConcurrentHashMap<>(); + this.verificationCodeTimestamps = new ConcurrentHashMap<>(); + this.objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + loadUsers(); + } + + private void loadUsers() { + try { + File file = new File(USERS_FILE); + if (file.exists() && file.length() > 0) { + User[] userArray = objectMapper.readValue(file, User[].class); + for (User user : userArray) { + users.put(user.getEmail(), user); + } + System.out.println("成功加载 " + userArray.length + " 个用户"); + } else { + saveUsers(); + System.out.println("创建新的用户数据文件"); + } + } catch (IOException e) { + System.err.println("加载用户数据失败: " + e.getMessage()); + backupAndRecreateFile(); + } + } + + private void backupAndRecreateFile() { + try { + File file = new File(USERS_FILE); + if (file.exists()) { + File backup = new File(USERS_FILE + ".bak." + System.currentTimeMillis()); + file.renameTo(backup); + System.err.println("损坏的文件已备份为: " + backup.getName()); + } + saveUsers(); + System.out.println("已重新创建用户数据文件"); + } catch (Exception e) { + System.err.println("备份文件失败: " + e.getMessage()); + } + } + + private void saveUsers() { + try { + File file = new File(USERS_FILE); + file.getParentFile().mkdirs(); + objectMapper.writerWithDefaultPrettyPrinter().writeValue(file, users.values().toArray(new User[0])); + } catch (IOException e) { + System.err.println("保存用户数据失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + public boolean registerUser(String email, String verificationCode) { + if (users.containsKey(email)) { + return false; // 用户已存在 + } + + // 生成密码占位符,实际密码在验证后设置 + String tempPasswordHash = BCrypt.withDefaults().hashToString(12, "temp".toCharArray()); + User user = new User(email, tempPasswordHash, verificationCode); + users.put(email, user); + verificationCodeTimestamps.put(email, LocalDateTime.now()); + saveUsers(); + return true; + } + + public boolean verifyUser(String username, String email, String verificationCode, String password) { + User user = users.get(email); + if (user == null || !user.getVerificationCode().equals(verificationCode)) { + return false; + } + + // 检查验证码是否过期 + LocalDateTime codeTime = verificationCodeTimestamps.get(email); + if (codeTime == null || codeTime.plusMinutes(VERIFICATION_CODE_EXPIRY_MINUTES).isBefore(LocalDateTime.now())) { + return false; + } + + // 验证密码格式 + if (!validatePassword(password)) { + return false; + } + + // 设置实际密码 + String passwordHash = BCrypt.withDefaults().hashToString(12, password.toCharArray()); + user.setPasswordHash(passwordHash); + user.setUsername(username); + user.setVerified(true); + verificationCodeTimestamps.remove(email); + saveUsers(); + return true; + } + + public boolean login(String email, String password) { + User user = users.get(email); + if (user == null || !user.isVerified()) { + return false; + } + + BCrypt.Result result = BCrypt.verifyer().verify(password.toCharArray(), user.getPasswordHash()); + return result.verified; + } + + public boolean changePassword(String email, String oldPassword, String newPassword) { + User user = users.get(email); + if (user == null) { + return false; + } + + // 验证旧密码 + BCrypt.Result result = BCrypt.verifyer().verify(oldPassword.toCharArray(), user.getPasswordHash()); + if (!result.verified) { + return false; + } + + // 验证新密码格式 + if (!validatePassword(newPassword)) { + return false; + } + + // 更新密码 + String newPasswordHash = BCrypt.withDefaults().hashToString(12, newPassword.toCharArray()); + user.setPasswordHash(newPasswordHash); + saveUsers(); + return true; + } + + public boolean validatePassword(String password) { + if (password.length() < 6 || password.length() > 10) { + return false; + } + + boolean hasUpper = false; + boolean hasLower = false; + boolean hasDigit = false; + + for (char c : password.toCharArray()) { + if (Character.isUpperCase(c)) hasUpper = true; + if (Character.isLowerCase(c)) hasLower = true; + if (Character.isDigit(c)) hasDigit = true; + } + + return hasUpper && hasLower && hasDigit; + } + + public boolean userExists(String email) { + return users.containsKey(email); + } + + public boolean isValidEmail(String email) { + return email.matches("^[A-Za-z0-9+_.-]+@(.+)$"); + } +} \ No newline at end of file diff --git a/src/main/java/mathlearning/ui/LoginFrame.java b/src/main/java/mathlearning/ui/LoginFrame.java new file mode 100644 index 0000000..a9579d0 --- /dev/null +++ b/src/main/java/mathlearning/ui/LoginFrame.java @@ -0,0 +1,111 @@ +package mathlearning.ui; + +import mathlearning.model.User; +import mathlearning.service.UserService; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class LoginFrame extends JFrame{ + private UserService userService; + private JTextField emailField; + private JPasswordField passwordField; + + public LoginFrame(UserService userService) { + this.userService = userService; + initializeUI(); + } + + private void initializeUI() { + setTitle("数学学习软件 - 登录"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setSize(400, 300); + setLocationRelativeTo(null); + setResizable(false); + + // 主面板 + JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + + // 标题 + JLabel titleLabel = new JLabel("用户登录", JLabel.CENTER); + titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 24)); + mainPanel.add(titleLabel, BorderLayout.NORTH); + + // 表单面板 + JPanel formPanel = new JPanel(new GridLayout(3, 2, 10, 10)); + + JLabel emailLabel = new JLabel("邮箱:"); + emailLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + emailField = new JTextField(); + + JLabel passwordLabel = new JLabel("密码:"); + passwordLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + passwordField = new JPasswordField(); + + formPanel.add(emailLabel); + formPanel.add(emailField); + formPanel.add(passwordLabel); + formPanel.add(passwordField); + + mainPanel.add(formPanel, BorderLayout.CENTER); + + // 按钮面板 + JPanel buttonPanel = new JPanel(new FlowLayout()); + + JButton loginButton = new JButton("登录"); + loginButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + loginButton.addActionListener(new LoginButtonListener()); + + JButton registerButton = new JButton("注册"); + registerButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + registerButton.addActionListener(e -> openRegisterFrame()); + + buttonPanel.add(loginButton); + buttonPanel.add(registerButton); + + mainPanel.add(buttonPanel, BorderLayout.SOUTH); + + add(mainPanel); + } + + private class LoginButtonListener implements ActionListener { + @Override + public void actionPerformed(ActionEvent e) { + String email = emailField.getText().trim(); + String password = new String(passwordField.getPassword()); + + if (email.isEmpty() || password.isEmpty()) { + JOptionPane.showMessageDialog(LoginFrame.this, + "请输入邮箱和密码", "错误", JOptionPane.ERROR_MESSAGE); + return; + } + + if (userService.login(email, password)) { + JOptionPane.showMessageDialog(LoginFrame.this, + "登录成功!", "成功", JOptionPane.INFORMATION_MESSAGE); + // 这里可以打开主界面 + openMainFrame(email); + } else { + JOptionPane.showMessageDialog(LoginFrame.this, + "邮箱或密码错误", "登录失败", JOptionPane.ERROR_MESSAGE); + } + } + } + + private void openRegisterFrame() { + RegisterFrame registerFrame = new RegisterFrame(userService, this); + registerFrame.setVisible(true); + this.setVisible(false); + } + + private void openMainFrame(String email) { + + // 这里先简单显示一个消息,后续可以扩展为主界面 + JOptionPane.showMessageDialog(this, + "欢迎 " + email + "!\n登录成功,主界面功能待实现。", + "登录成功", JOptionPane.INFORMATION_MESSAGE); + } +} diff --git a/src/main/java/mathlearning/ui/RegisterFrame.java b/src/main/java/mathlearning/ui/RegisterFrame.java new file mode 100644 index 0000000..ebc97c7 --- /dev/null +++ b/src/main/java/mathlearning/ui/RegisterFrame.java @@ -0,0 +1,230 @@ +package mathlearning.ui; + +import mathlearning.service.EmailService; +import mathlearning.service.UserService; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class RegisterFrame extends JFrame{ + private UserService userService; + private EmailService emailService; + private LoginFrame loginFrame; + + private JTextField nameField; + private JTextField emailField; + private JButton sendCodeButton; + private JTextField codeField; + private JPasswordField passwordField; + private JPasswordField confirmPasswordField; + private JButton registerButton; + private JButton backButton; + + private String verificationCode; + + public RegisterFrame(UserService userService, LoginFrame loginFrame) { + this.userService = userService; + this.loginFrame = loginFrame; + + // 配置邮箱服务(需要替换为真实的邮箱配置) + this.emailService = new EmailService( + "smtp.qq.com", + "587", + "2793415226@qq.com", // 替换为你的QQ邮箱 + "rmiomlakglpjddhb", // 替换为你的授权码 + true + ); + + initializeUI(); + } + + private void initializeUI() { + setTitle("数学学习软件 - 注册"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setSize(450, 400); + setLocationRelativeTo(null); + setResizable(false); + + // 主面板 + JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + + // 标题 + JLabel titleLabel = new JLabel("用户注册", JLabel.CENTER); + titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 24)); + mainPanel.add(titleLabel, BorderLayout.NORTH); + + // 表单面板 + JPanel formPanel = new JPanel(new GridLayout(5, 2, 10, 10)); + + JLabel nameLabel = new JLabel("用户名:"); + nameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + nameField = new JTextField(); + + JLabel emailLabel = new JLabel("邮箱:"); + emailLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + emailField = new JTextField(); + + JLabel codeLabel = new JLabel("验证码:"); + codeLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + + JPanel codePanel = new JPanel(new BorderLayout()); + codeField = new JTextField(); + sendCodeButton = new JButton("发送验证码"); + sendCodeButton.addActionListener(new SendCodeListener()); + + codePanel.add(codeField, BorderLayout.CENTER); + codePanel.add(sendCodeButton, BorderLayout.EAST); + + JLabel passwordLabel = new JLabel("密码:"); + passwordLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + passwordField = new JPasswordField(); + + JLabel confirmPasswordLabel = new JLabel("确认密码:"); + confirmPasswordLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + confirmPasswordField = new JPasswordField(); + + formPanel.add(nameLabel); + formPanel.add(nameField); + formPanel.add(emailLabel); + formPanel.add(emailField); + formPanel.add(codeLabel); + formPanel.add(codePanel); + formPanel.add(passwordLabel); + formPanel.add(passwordField); + formPanel.add(confirmPasswordLabel); + formPanel.add(confirmPasswordField); + + mainPanel.add(formPanel, BorderLayout.CENTER); + + // 按钮面板 + JPanel buttonPanel = new JPanel(new FlowLayout()); + + registerButton = new JButton("注册"); + registerButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + registerButton.addActionListener(new RegisterButtonListener()); + + backButton = new JButton("返回登录"); + backButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + backButton.addActionListener(e -> goBackToLogin()); + + buttonPanel.add(registerButton); + buttonPanel.add(backButton); + + mainPanel.add(buttonPanel, BorderLayout.SOUTH); + + add(mainPanel); + } + + private class SendCodeListener implements ActionListener { + @Override + public void actionPerformed(ActionEvent e) { + String email = emailField.getText().trim(); + + if (email.isEmpty()) { + JOptionPane.showMessageDialog(RegisterFrame.this, + "请输入邮箱地址", "错误", JOptionPane.ERROR_MESSAGE); + return; + } + + if (!userService.isValidEmail(email)) { + JOptionPane.showMessageDialog(RegisterFrame.this, + "邮箱格式不正确", "错误", JOptionPane.ERROR_MESSAGE); + return; + } + + if (userService.userExists(email)) { + JOptionPane.showMessageDialog(RegisterFrame.this, + "该邮箱已注册", "错误", JOptionPane.ERROR_MESSAGE); + return; + } + + // 生成6位验证码 + verificationCode = String.valueOf((int)((Math.random() * 9 + 1) * 100000)); + + // 发送验证码邮件 + boolean sent = emailService.sendVerificationCode(email, verificationCode); + + if (sent) { + JOptionPane.showMessageDialog(RegisterFrame.this, + "验证码已发送到您的邮箱,请查收", "成功", JOptionPane.INFORMATION_MESSAGE); + + // 禁用发送按钮60秒 + sendCodeButton.setEnabled(false); + new Thread(() -> { + try { + for (int i = 60; i > 0; i--) { + final int current = i; + SwingUtilities.invokeLater(() -> + sendCodeButton.setText(current + "秒后重发")); + Thread.sleep(1000); + } + SwingUtilities.invokeLater(() -> { + sendCodeButton.setText("发送验证码"); + sendCodeButton.setEnabled(true); + }); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + }).start(); + + } else { + JOptionPane.showMessageDialog(RegisterFrame.this, + "验证码发送失败,请检查邮箱地址或网络连接", "错误", JOptionPane.ERROR_MESSAGE); + } + } + } + + private class RegisterButtonListener implements ActionListener { + @Override + public void actionPerformed(ActionEvent e) { + String username = nameField.getText().trim(); + String email = emailField.getText().trim(); + String code = codeField.getText().trim(); + String password = new String(passwordField.getPassword()); + String confirmPassword = new String(confirmPasswordField.getPassword()); + + // 验证输入 + if (email.isEmpty() || code.isEmpty() || password.isEmpty() || username.isEmpty()) { + JOptionPane.showMessageDialog(RegisterFrame.this, + "请填写所有字段", "错误", JOptionPane.ERROR_MESSAGE); + return; + } + + if (!password.equals(confirmPassword)) { + JOptionPane.showMessageDialog(RegisterFrame.this, + "两次输入的密码不一致", "错误", JOptionPane.ERROR_MESSAGE); + return; + } + + if (!userService.validatePassword(password)) { + JOptionPane.showMessageDialog(RegisterFrame.this, + "密码必须为6-10位,且包含大小写字母和数字", "错误", JOptionPane.ERROR_MESSAGE); + return; + } + + // 先注册用户(临时状态) + if (userService.registerUser(email, verificationCode)) { + // 验证用户并设置密码 + if (userService.verifyUser(username, email, code, password)) { + JOptionPane.showMessageDialog(RegisterFrame.this, + "注册成功!", "成功", JOptionPane.INFORMATION_MESSAGE); + goBackToLogin(); + } else { + JOptionPane.showMessageDialog(RegisterFrame.this, + "验证码错误或已过期", "错误", JOptionPane.ERROR_MESSAGE); + } + } else { + JOptionPane.showMessageDialog(RegisterFrame.this, + "注册失败,用户可能已存在", "错误", JOptionPane.ERROR_MESSAGE); + } + } + } + + private void goBackToLogin() { + loginFrame.setVisible(true); + this.dispose(); + } +}