添加了修改密码 忘记密码 邀请码的生成与验证

Change-Id: I88ffc40e64943c7c9fcd411c763e788e6e49c834
diff --git a/src/main/java/com/example/g8backend/controller/ForgotPasswordController.java b/src/main/java/com/example/g8backend/controller/ForgotPasswordController.java
new file mode 100644
index 0000000..1d4cf58
--- /dev/null
+++ b/src/main/java/com/example/g8backend/controller/ForgotPasswordController.java
@@ -0,0 +1,27 @@
+package com.example.g8backend.controller;
+
+import com.example.g8backend.dto.ForgotPasswordDTO;
+import com.example.g8backend.dto.ResetPasswordDTO;
+import com.example.g8backend.service.IForgotPasswordService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/forgot-password")
+public class ForgotPasswordController {
+    @Autowired
+    private IForgotPasswordService forgotPasswordService;
+
+    @PostMapping("/send-code")
+    public ResponseEntity<?> sendCode(@RequestBody ForgotPasswordDTO dto) {
+        forgotPasswordService.sendCodeToEmail(dto.getUsername());
+        return ResponseEntity.ok("验证码已发送到注册邮箱");
+    }
+
+    @PostMapping("/reset")
+    public ResponseEntity<?> resetPassword(@RequestBody ResetPasswordDTO dto) {
+        forgotPasswordService.resetPassword(dto.getUsername(), dto.getCode(), dto.getNewPassword());
+        return ResponseEntity.ok("密码重置成功");
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/controller/UserSecurityController.java b/src/main/java/com/example/g8backend/controller/UserSecurityController.java
new file mode 100644
index 0000000..34bbb96
--- /dev/null
+++ b/src/main/java/com/example/g8backend/controller/UserSecurityController.java
@@ -0,0 +1,24 @@
+package com.example.g8backend.controller;
+
+import com.example.g8backend.dto.PasswordChangeDTO;
+import com.example.g8backend.service.IUserSecurityService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/user/security")
+public class UserSecurityController {
+    @Autowired
+    private IUserSecurityService userSecurityService;
+
+    @PutMapping("/change-password")
+    public ResponseEntity<?> changePassword(@RequestBody PasswordChangeDTO dto) {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        Long userId = (Long) authentication.getPrincipal();
+        userSecurityService.changePassword(userId, dto.getOldPassword(), dto.getNewPassword());
+        return ResponseEntity.ok("密码修改成功");
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/dto/ForgotPasswordDTO.java b/src/main/java/com/example/g8backend/dto/ForgotPasswordDTO.java
new file mode 100644
index 0000000..d9eb3ce
--- /dev/null
+++ b/src/main/java/com/example/g8backend/dto/ForgotPasswordDTO.java
@@ -0,0 +1,8 @@
+package com.example.g8backend.dto;
+
+import lombok.Data;
+
+@Data
+public class ForgotPasswordDTO {
+    private String username;
+}
diff --git a/src/main/java/com/example/g8backend/dto/PasswordChangeDTO.java b/src/main/java/com/example/g8backend/dto/PasswordChangeDTO.java
new file mode 100644
index 0000000..7a96ccf
--- /dev/null
+++ b/src/main/java/com/example/g8backend/dto/PasswordChangeDTO.java
@@ -0,0 +1,9 @@
+package com.example.g8backend.dto;
+
+import lombok.Data;
+
+@Data
+public class PasswordChangeDTO {
+    private String oldPassword;
+    private String newPassword;
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/dto/ResetPasswordDTO.java b/src/main/java/com/example/g8backend/dto/ResetPasswordDTO.java
new file mode 100644
index 0000000..2fa008c
--- /dev/null
+++ b/src/main/java/com/example/g8backend/dto/ResetPasswordDTO.java
@@ -0,0 +1,10 @@
+package com.example.g8backend.dto;
+
+import lombok.Data;
+
+@Data
+public class ResetPasswordDTO {
+    private String username;
+    private String code;
+    private String newPassword;
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/service/IForgotPasswordService.java b/src/main/java/com/example/g8backend/service/IForgotPasswordService.java
new file mode 100644
index 0000000..af865ae
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/IForgotPasswordService.java
@@ -0,0 +1,6 @@
+package com.example.g8backend.service;
+
+public interface IForgotPasswordService {
+    void sendCodeToEmail(String username);
+    boolean resetPassword(String username, String code, String newPassword);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/service/IUserSecurityService.java b/src/main/java/com/example/g8backend/service/IUserSecurityService.java
new file mode 100644
index 0000000..0ac40dc
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/IUserSecurityService.java
@@ -0,0 +1,5 @@
+package com.example.g8backend.service;
+
+public interface IUserSecurityService {
+    boolean changePassword(Long userId, String oldPassword, String newPassword);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/service/impl/ForgotPasswordServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/ForgotPasswordServiceImpl.java
new file mode 100644
index 0000000..f75617b
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/ForgotPasswordServiceImpl.java
@@ -0,0 +1,66 @@
+package com.example.g8backend.service.impl;
+
+import com.example.g8backend.entity.User;
+import com.example.g8backend.mapper.UserMapper;
+import com.example.g8backend.service.IForgotPasswordService;
+import com.example.g8backend.util.mailUtil;  // 导入 mailUtil
+import jakarta.annotation.Resource;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+@Service
+public class ForgotPasswordServiceImpl implements IForgotPasswordService {
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private RedisTemplate<String, String> redisTemplate;
+
+    @Resource
+    private PasswordEncoder passwordEncoder;
+
+    @Resource
+    private mailUtil mailUtil;  // 注入 mailUtil
+
+    @Override
+    public void sendCodeToEmail(String username) {
+        User user = userMapper.getUserByName(username);
+        if (user == null || user.getEmail() == null) throw new RuntimeException("用户不存在或未绑定邮箱");
+
+        // 生成验证码
+        String code = String.valueOf(100000 + new Random().nextInt(900000));
+
+        // 将验证码存储到 Redis
+        redisTemplate.opsForValue().set("reset_code:" + username, code, 10, TimeUnit.MINUTES);
+
+        // 使用 mailUtil 发送邮件
+        String subject = "重置密码验证码";
+        String message = "您的验证码为:" + code + ",10分钟内有效。";
+        mailUtil.sendMail(user.getEmail(), subject, message);  // 发送邮件
+    }
+
+    @Override
+    public boolean resetPassword(String username, String code, String newPassword) {
+        String key = "reset_code:" + username;
+        String realCode = redisTemplate.opsForValue().get(key);
+
+        if (realCode == null || !realCode.equals(code)) throw new RuntimeException("验证码错误或已过期");
+
+        User user = userMapper.getUserByName(username);
+        if (user == null) throw new RuntimeException("用户不存在");
+
+        // 更新密码
+        user.setPassword(passwordEncoder.encode(newPassword));
+
+        // 删除 Redis 中存储的验证码
+        redisTemplate.delete(key);
+
+        // 更新用户信息
+        return userMapper.updateById(user) > 0;
+    }
+}
diff --git a/src/main/java/com/example/g8backend/service/impl/UserSecurityServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/UserSecurityServiceImpl.java
new file mode 100644
index 0000000..e49551f
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/UserSecurityServiceImpl.java
@@ -0,0 +1,27 @@
+package com.example.g8backend.service.impl;
+
+import com.example.g8backend.entity.User;
+import com.example.g8backend.mapper.UserMapper;
+import com.example.g8backend.service.IUserSecurityService;
+import jakarta.annotation.Resource;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UserSecurityServiceImpl implements IUserSecurityService {
+    @Resource
+    private UserMapper userMapper;
+    @Resource
+    private PasswordEncoder passwordEncoder;
+
+    @Override
+    public boolean changePassword(Long userId, String oldPassword, String newPassword) {
+        User user = userMapper.selectById(userId);
+        if (user == null) throw new RuntimeException("用户不存在");
+        if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
+            throw new RuntimeException("原密码错误");
+        }
+        user.setPassword(passwordEncoder.encode(newPassword));
+        return userMapper.updateById(user) > 0;
+    }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 22f3728..4a4d894 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -3,6 +3,7 @@
 spring.datasource.url=jdbc:mysql://localhost:3306/g8backend
 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
 spring.sql.init.mode=always
+logging.level.root=DEBUG
 
 mybatis-plus.mapper-locations=classpath*:/mapper/**/*.xml
 
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index 5e5dd43..c603a39 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -84,4 +84,5 @@
     is_read BOOLEAN DEFAULT false,
     FOREIGN KEY (sender_id) REFERENCES users(user_id),
     FOREIGN KEY (receiver_id) REFERENCES users(user_id)
-);
\ No newline at end of file
+);
+
diff --git a/src/test/java/com/example/g8backend/service/ForgotPasswordServiceImplTest.java b/src/test/java/com/example/g8backend/service/ForgotPasswordServiceImplTest.java
new file mode 100644
index 0000000..05535fd
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/ForgotPasswordServiceImplTest.java
@@ -0,0 +1,77 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.entity.User;
+import com.example.g8backend.mapper.UserMapper;
+import com.example.g8backend.service.impl.ForgotPasswordServiceImpl;
+import com.example.g8backend.util.mailUtil; // Import mailUtil
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+public class ForgotPasswordServiceImplTest {
+
+    @Mock
+    private UserMapper userMapper;
+    @Mock
+    private RedisTemplate<String, String> redisTemplate;
+    @Mock
+    private PasswordEncoder passwordEncoder;
+    @Mock
+    private mailUtil mailUtil;  // Mock mailUtil
+
+    @InjectMocks
+    private ForgotPasswordServiceImpl forgotPasswordService;
+
+    @Mock
+    private ValueOperations<String, String> valueOperations; // Mock Redis ValueOperations
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+        when(redisTemplate.opsForValue()).thenReturn(valueOperations); // Mock Redis operations
+    }
+
+    @Test
+    void sendCodeToEmail_Success() {
+        User user = new User();
+        user.setUserName("test");
+        user.setEmail("test@example.com");
+        when(userMapper.getUserByName("test")).thenReturn(user);
+
+        // Call the method under test
+        forgotPasswordService.sendCodeToEmail("test");
+
+        // Verify that mailUtil.sendMail was called once with the expected arguments
+        verify(mailUtil, times(1)).sendMail(eq("test@example.com"), eq("重置密码验证码"), contains("您的验证码"));
+        verify(redisTemplate, times(1)).opsForValue();
+    }
+
+    @Test
+    void resetPassword_Success() {
+        User user = new User();
+        user.setUserName("test");
+        user.setPassword("old");
+        when(userMapper.getUserByName("test")).thenReturn(user);
+        when(redisTemplate.opsForValue().get("reset_code:test")).thenReturn("123456");
+        when(passwordEncoder.encode("newpass")).thenReturn("encodedNew");
+        when(userMapper.updateById(any(User.class))).thenReturn(1);
+
+        boolean result = forgotPasswordService.resetPassword("test", "123456", "newpass");
+        assertTrue(result);
+        verify(redisTemplate, times(1)).delete("reset_code:test");
+    }
+
+    @Test
+    void resetPassword_WrongCode() {
+        when(redisTemplate.opsForValue().get("reset_code:test")).thenReturn("654321");
+        assertThrows(RuntimeException.class, () -> forgotPasswordService.resetPassword("test", "123456", "newpass"));
+    }
+}
diff --git a/src/test/java/com/example/g8backend/service/UserSecurityServiceImplTest.java b/src/test/java/com/example/g8backend/service/UserSecurityServiceImplTest.java
new file mode 100644
index 0000000..f04fbc5
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/UserSecurityServiceImplTest.java
@@ -0,0 +1,55 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.entity.User;
+import com.example.g8backend.mapper.UserMapper;
+import com.example.g8backend.service.impl.UserSecurityServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+public class UserSecurityServiceImplTest {
+
+    @Mock
+    private UserMapper userMapper;
+    @Mock
+    private PasswordEncoder passwordEncoder;
+
+    @InjectMocks
+    private UserSecurityServiceImpl userSecurityService;
+
+    private User user;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+        user = new User();
+        user.setUserId(1L);
+        user.setPassword("encodedOld");
+    }
+
+    @Test
+    void changePassword_Success() {
+        when(userMapper.selectById(1L)).thenReturn(user);
+        when(passwordEncoder.matches("old", "encodedOld")).thenReturn(true);
+        when(passwordEncoder.encode("new")).thenReturn("encodedNew");
+        when(userMapper.updateById(any(User.class))).thenReturn(1);
+
+        boolean result = userSecurityService.changePassword(1L, "old", "new");
+        assertTrue(result);
+        verify(userMapper).updateById(any(User.class));
+    }
+
+    @Test
+    void changePassword_WrongOldPassword() {
+        when(userMapper.selectById(1L)).thenReturn(user);
+        when(passwordEncoder.matches("wrong", "encodedOld")).thenReturn(false);
+
+        assertThrows(RuntimeException.class, () -> userSecurityService.changePassword(1L, "wrong", "new"));
+    }
+}
\ No newline at end of file