Prechádzať zdrojové kódy

feat: "完善User和Admin相关逻辑"

Leonezhurui 2 rokov pred
rodič
commit
de93ebdc08
26 zmenil súbory, kde vykonal 921 pridanie a 261 odobranie
  1. 2 0
      example.csv
  2. BIN
      example.xlsx
  3. 19 0
      pom.xml
  4. 21 11
      src/main/java/com/njuzr/eaibackend/config/JWTAuthenticationFilter.java
  5. 22 0
      src/main/java/com/njuzr/eaibackend/config/MybatisConfig.java
  6. 24 0
      src/main/java/com/njuzr/eaibackend/config/RedisConfig.java
  7. 1 1
      src/main/java/com/njuzr/eaibackend/config/SecurityConfig.java
  8. 100 0
      src/main/java/com/njuzr/eaibackend/controller/AdminController.java
  9. 18 19
      src/main/java/com/njuzr/eaibackend/controller/AuthenticationController.java
  10. 52 52
      src/main/java/com/njuzr/eaibackend/controller/UserController.java
  11. 26 0
      src/main/java/com/njuzr/eaibackend/dto/AdminRegisterDTO.java
  12. 9 0
      src/main/java/com/njuzr/eaibackend/dto/UserRegisterDTO.java
  13. 29 0
      src/main/java/com/njuzr/eaibackend/dto/UserUpdateDTO.java
  14. 33 3
      src/main/java/com/njuzr/eaibackend/exception/GlobalExceptionHandler.java
  15. 10 2
      src/main/java/com/njuzr/eaibackend/exception/MyAuthenticationEntryPoint.java
  16. 9 0
      src/main/java/com/njuzr/eaibackend/mapper/UserMapper.java
  17. 116 31
      src/main/java/com/njuzr/eaibackend/service/EmailService.java
  18. 38 6
      src/main/java/com/njuzr/eaibackend/service/UserService.java
  19. 249 58
      src/main/java/com/njuzr/eaibackend/service/impl/UserServiceImpl.java
  20. 14 4
      src/main/java/com/njuzr/eaibackend/utils/JWTTokenUtil.java
  21. 0 69
      src/main/java/com/njuzr/eaibackend/utils/ObjectFieldCheckerUtil.java
  22. 32 0
      src/main/java/com/njuzr/eaibackend/utils/PageMapperUtil.java
  23. 82 0
      src/main/java/com/njuzr/eaibackend/utils/WebClientUtil.java
  24. 5 2
      src/main/resources/application.yaml
  25. 8 2
      src/main/resources/mapper/UserMapper.xml
  26. 2 1
      src/test/java/com/njuzr/eaibackend/service/EmailServiceTest.java

+ 2 - 0
example.csv

@@ -0,0 +1,2 @@
+序号, 姓名, 学号, 邮箱
+1, 研小朱, 522022320227, 522022320227@smail.nju.edu.cn

BIN
example.xlsx


+ 19 - 0
pom.xml

@@ -120,6 +120,25 @@
             <artifactId>spring-boot-starter-mail</artifactId>
         </dependency>
 
+        <!--Bean Validation依赖-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <!--其中包含WebClient依赖-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
+
+        <!--Redis依赖-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+
+
 
 
     </dependencies>

+ 21 - 11
src/main/java/com/njuzr/eaibackend/config/JWTAuthenticationFilter.java

@@ -1,5 +1,6 @@
 package com.njuzr.eaibackend.config;
 
+import com.njuzr.eaibackend.exception.MyAuthenticationEntryPoint;
 import com.njuzr.eaibackend.exception.MyException;
 import com.njuzr.eaibackend.service.MyUserDetailService;
 import com.njuzr.eaibackend.utils.JWTTokenUtil;
@@ -8,7 +9,9 @@ import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.authentication.CredentialsExpiredException;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.web.filter.OncePerRequestFilter;
@@ -27,6 +30,8 @@ public class JWTAuthenticationFilter extends OncePerRequestFilter {
     private final JWTTokenUtil jwtTokenUtil;
     private final MyUserDetailService userDetailService;
 
+    private final MyAuthenticationEntryPoint authenticationEntryPoint = new MyAuthenticationEntryPoint();
+
     public JWTAuthenticationFilter(JWTTokenUtil jwtTokenUtil, MyUserDetailService userDetailService) {
         this.jwtTokenUtil = jwtTokenUtil;
         this.userDetailService = userDetailService;
@@ -38,20 +43,25 @@ public class JWTAuthenticationFilter extends OncePerRequestFilter {
         String token = request.getHeader("Authorization");
         log.info("从Header中解析出Token:" + token);
 
-        if (token != null) {
-            if(!jwtTokenUtil.isTokenNotExpired(token))  throw new MyException(401, "用户token过期");
-            String username = jwtTokenUtil.parseToken(token);
-            log.info("解析出username:" + username);
-            UserDetails userDetails = userDetailService.loadUserByUsername(username);
-            log.info("解析出UserDetails:" + userDetails);
+        try {
+            if (token != null) {
+                String username = jwtTokenUtil.parseToken(token);
+                log.info("解析出username:" + username);
+                UserDetails userDetails = userDetailService.loadUserByUsername(username);
+                log.info("解析出UserDetails:" + userDetails);
+
+                // 设置principal和权限
+                UsernamePasswordAuthenticationToken authentication =
+                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
+                SecurityContextHolder.getContext().setAuthentication(authentication);
 
-            // 设置principal和权限
-            UsernamePasswordAuthenticationToken authentication =
-                    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
-            SecurityContextHolder.getContext().setAuthentication(authentication);
+            }
 
+            chain.doFilter(request, response);
+         } catch (CredentialsExpiredException e) {
+            // 将异常传递给AuthenticationEntryPoint处理(必要的步骤)
+            authenticationEntryPoint.commence(request, response, e);
         }
 
-        chain.doFilter(request, response);
     }
 }

+ 22 - 0
src/main/java/com/njuzr/eaibackend/config/MybatisConfig.java

@@ -0,0 +1,22 @@
+package com.njuzr.eaibackend.config;
+
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author: Leonezhurui
+ * @Date: 2024/2/27 - 15:29
+ * @Package: EAI-Backend
+ */
+
+@Component
+public class MybatisConfig {
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
+        return interceptor;
+    }
+}

+ 24 - 0
src/main/java/com/njuzr/eaibackend/config/RedisConfig.java

@@ -0,0 +1,24 @@
+package com.njuzr.eaibackend.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+
+/**
+ * @author: Leonezhurui
+ * @Date: 2024/2/27 - 21:20
+ * @Package: EAI-Backend
+ */
+
+@Configuration
+public class RedisConfig {
+
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory connectionFactory) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+        return template;
+    }
+}
+

+ 1 - 1
src/main/java/com/njuzr/eaibackend/config/SecurityConfig.java

@@ -50,7 +50,7 @@ public class SecurityConfig {
     public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
         http
                 .authorizeHttpRequests(authorize -> authorize
-                        .requestMatchers("/api/user/login", "/api/user/register").permitAll() // 允许公开访问的路径
+                        .requestMatchers("/api/user/login", "/api/user/register", "/api/user/verifyCode").permitAll() // 允许公开访问的路径
                         .anyRequest().authenticated() // 其他所有请求需要认证
                 )
                 .exceptionHandling(exception -> exception

+ 100 - 0
src/main/java/com/njuzr/eaibackend/controller/AdminController.java

@@ -0,0 +1,100 @@
+package com.njuzr.eaibackend.controller;
+
+import com.njuzr.eaibackend.dto.AdminRegisterDTO;
+import com.njuzr.eaibackend.dto.UserDTO;
+import com.njuzr.eaibackend.dto.UserRegisterDTO;
+import com.njuzr.eaibackend.service.UserService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * @author: Leonezhurui
+ * @Date: 2024/2/23 - 14:22
+ * @Package: EAI-Backend
+ */
+
+@Slf4j
+@RestController
+@RequestMapping("/api/admin")
+public class AdminController {
+
+    private final UserService userService;
+
+    @Autowired
+    public AdminController(UserService userService) {
+        this.userService = userService;
+    }
+
+    /**
+     * 管理员创建用户(学生和老师)
+     * @param adminRegisterDTO
+     * @return
+     */
+    @PreAuthorize("hasRole('ROLE_ADMIN')")
+    @PostMapping("/createUser")
+    public MyResponse createUser(@Validated @RequestBody AdminRegisterDTO adminRegisterDTO) {
+        userService.adminCreateUser(adminRegisterDTO);
+        return MyResponse.success("创建成功");
+    }
+
+    /**
+     * 管理更新用户信息,除id之外都可以修改
+     * @param userDTO
+     * @return
+     */
+    @PreAuthorize("hasRole('ROLE_ADMIN')")
+    @PostMapping("/updateUser")
+    public MyResponse updateUser(@RequestParam("id") Long id, @RequestBody UserDTO userDTO) {
+        return MyResponse.success(userService.updateUser(id, userDTO));
+    }
+
+    @PreAuthorize("hasRole('ROLE_ADMIN')")
+    @PutMapping("/resetPassword")
+    public MyResponse resetPassword(@RequestParam("id") Long id) {
+        userService.resetPassword(id);
+        return MyResponse.success("重置密码成功");
+    }
+
+
+    /**
+     * (管理员)通过上传excel/csv文件,批量创建用户,文件必须含有字段信息:姓名、学号、邮箱
+     * @return
+     */
+    @PostMapping("/batch-upload")
+    @PreAuthorize("hasRole('ROLE_ADMIN')")
+    public MyResponse batchCreateUsers(@RequestParam("file") MultipartFile file) {
+        String fileName = file.getOriginalFilename();
+
+        // 根据文件类型进行不同的处理
+        try {
+            if(fileName.endsWith(".xls") || fileName.endsWith(".xlsx")) {
+                userService.batchCreateUsersFromExcel(file);
+            } else if (fileName.endsWith(".csv")) {
+                userService.batchCreateUsersFromCsv(file);
+            }else {
+                return MyResponse.error(400, "文件类型不支持");
+            }
+        } catch (Exception e) {
+            log.error("上传文件失败,错误如下:"+e.getMessage());
+            return MyResponse.error(400, "上传文件失败:"+e.getMessage());
+        }
+
+        return MyResponse.success("批量创建用户成功");
+    }
+
+    /**
+     * (管理员)根据ID删除某个特定的用户
+     * @param id
+     * @return
+     */
+    @DeleteMapping()
+    @PreAuthorize("hasRole('ROLE_ADMIN')")
+    public MyResponse deleteById(@RequestParam Long id) {
+        userService.deleteById(id);
+        return MyResponse.success("删除成功");
+    }
+}

+ 18 - 19
src/main/java/com/njuzr/eaibackend/controller/AuthenticationController.java

@@ -12,11 +12,15 @@ import com.njuzr.eaibackend.vo.UserVO;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -45,10 +49,14 @@ public class AuthenticationController {
         this.jwtTokenUtil = jwtTokenUtil;
     }
 
+    /**
+     * 用户登陆学号(officialNumber)和密码(password)进行登陆
+     * @param userLoginDTO officialNumber、password
+     * @return 如果成功登陆,则返回登陆用户的身份信息和Token;如果失败,则返回具体原因。
+     */
     @PostMapping("/login")
     public MyResponse login(@RequestBody UserLoginDTO userLoginDTO) {
         try {
-            // 用户登陆学号和密码进行登陆
             Authentication authentication = authenticationManager.authenticate(
                     new UsernamePasswordAuthenticationToken(
                             userLoginDTO.getOfficialNumber(),
@@ -57,33 +65,24 @@ public class AuthenticationController {
             );
 
             SecurityContextHolder.getContext().setAuthentication(authentication);
+
             MyUserDetails userDetails = (MyUserDetails) authentication.getPrincipal();
 
-            log.info("用户认证通过,用户认证信息如下:"+userDetails.toString());
+            log.info("用户认证通过,用户认证信息如下:" + userDetails.toString());
 
             String jwt = jwtTokenUtil.generateToken(userDetails.getOfficialNumber());
-            log.info("生成jwt成功,token信息:"+jwt);
+            log.info("生成jwt成功,token信息:" + jwt);
 
             UserLoginVO userLoginVO = ModelMapperUtil.map(userDetails, UserLoginVO.class);
             userLoginVO.setToken(jwt);
             return MyResponse.success(userLoginVO);
-        } catch (AuthenticationException e) {
-            return MyResponse.error(HttpStatus.UNAUTHORIZED.value(), "Error: Authentication failed"+e.getMessage());
-        }
-    }
 
-
-    @PostMapping("/register")
-    public MyResponse createUser(@RequestBody UserRegisterDTO userRegisterDTO) {
-        log.info("createUser - 用户数据:"+userRegisterDTO.toString()); // 解析前端传递的数据
-
-        try {
-            UserVO userVO = userService.createUser(userRegisterDTO);
-            return MyResponse.success(userVO);
-        } catch (MyException e) {
-            return MyResponse.error(501, e.getMessage());
-        } catch (Exception e) {
-            return MyResponse.error(501, "创建用户失败");
+        } catch (BadCredentialsException e) { // 密码出错抛出的异常
+            log.error("登陆失败,密码错误");
+            return MyResponse.error(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"用户名或密码错误");
+        } catch (UsernameNotFoundException e) {// 找不到用户抛出的异常
+            log.error("登陆失败,找不到该用户");
+            return MyResponse.error(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"用户名或密码错误");
         }
     }
 

+ 52 - 52
src/main/java/com/njuzr/eaibackend/controller/UserController.java

@@ -1,19 +1,22 @@
 package com.njuzr.eaibackend.controller;
 
-import com.njuzr.eaibackend.dto.UserDTO;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.njuzr.eaibackend.dto.UserRegisterDTO;
+import com.njuzr.eaibackend.dto.UserUpdateDTO;
+import com.njuzr.eaibackend.enums.Role;
 import com.njuzr.eaibackend.exception.MyException;
+import com.njuzr.eaibackend.po.User;
 import com.njuzr.eaibackend.service.UserService;
-import com.njuzr.eaibackend.utils.ModelMapperUtil;
-import com.njuzr.eaibackend.utils.ObjectFieldCheckerUtil;
 import com.njuzr.eaibackend.vo.UserVO;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
-import org.springframework.web.multipart.MultipartFile;
 
 /**
  * @author: Leonezhurui
@@ -34,6 +37,23 @@ public class UserController {
     }
 
 
+    /**
+     * 学生注册账号,
+     * @param userRegisterDTO
+     * @return
+     */
+    @PostMapping("/register")
+    public MyResponse createUser(@Validated @RequestBody UserRegisterDTO userRegisterDTO) {
+        UserVO userVO = userService.createUser(userRegisterDTO);
+        return MyResponse.success(userVO);
+    }
+
+    @GetMapping("/verifyCode")
+    public MyResponse sendVerifyCode(@RequestParam(value = "officialEmail") String officialEmail) {
+        userService.sendVerifyCode(officialEmail);
+        return MyResponse.success("发送成功");
+    }
+
     /**
      * (学生、老师)更新自己的用户信息,固定信息不可更改,例如真实姓名、邮箱、学号、角色等
      * @param id
@@ -42,65 +62,45 @@ public class UserController {
      */
     @PreAuthorize("hasRole('ROLE_STUDENT') or hasRole('ROLE_TEACHER')")
     @PutMapping
-    public MyResponse updateUser(@AuthenticationPrincipal(expression = "id") Long id, @RequestBody UserDTO userDTO) {
+    public MyResponse updateUser(@AuthenticationPrincipal(expression = "id") Long id, @RequestBody UserUpdateDTO userDTO) {
         log.info("UserDTO解析成功,对象如下:"+userDTO);
-        UserVO userVO;
-        if (ObjectFieldCheckerUtil.areAllFieldsNullOrAbsent(userDTO, "id", "name", "officialEmail", "officialNumber", "role", "createTime")) {
-            userVO = userService.updateUser(id, userDTO);
-            return MyResponse.success(userVO);
+        UserVO userVO = userService.updateUser(id, userDTO);
+        return MyResponse.success(userVO);
+    }
+
+    @PreAuthorize("hasRole('ROLE_STUDENT') or hasRole('ROLE_TEACHER')")
+    @PutMapping("/updatePassword")
+    public MyResponse updatePassword(
+            @AuthenticationPrincipal(expression = "id") Long id,
+            @RequestParam("newPassword") String newPassword,
+            @RequestParam("oldPassword") String oldPassword) {
+        try {
+            userService.updatePassword(id, newPassword, oldPassword);
+            return MyResponse.success("用户密码更新成功");
+        } catch (MyException e) {
+            return MyResponse.error(e.getErrCode(), e.getMessage());
         }
-        return MyResponse.error(400, "请求体中包含了不能更新的字段");
     }
 
     /**
      * (管理员或教师)可以查找具体某个学生
      * @param id
-     * @param userDetails
      * @return
      */
-    @GetMapping("/{id}")
+    @GetMapping()
     @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_TEACHER')")
-    public MyResponse findUserById(@PathVariable Long id, @AuthenticationPrincipal UserDetails userDetails) {
-        log.info("用户认证成功,解析出的UserDetails信息如下:"+userDetails.getAuthorities());
-        UserVO userVO = userService.findById(id);
-        log.info("生成的userVO信息如下:"+userVO);
-        return MyResponse.success(userVO);
-    }
+    public MyResponse searchUsers(
+            @RequestParam(value = "page", defaultValue = "1") int currentPage,
+            @RequestParam(value = "size", defaultValue = "10") int pageSize,
+            @RequestParam(value = "id", required = false) Long id,
+            @RequestParam(value = "name",required = false) String name,
+            @RequestParam(value = "officialNumber",required = false) String officialNumber,
+            @RequestParam(value = "role",required = false) Role role) {
+        Page<User> page = new Page<>(currentPage, pageSize);
 
-    /**
-     * (管理员)通过上传excel/csv文件,批量创建用户,文件必须含有字段信息:姓名、学号、邮箱
-     * @return
-     */
-    @PostMapping("/batch-upload")
-    @PreAuthorize("hasRole('ROLE_ADMIN')")
-    public MyResponse batchCreateUsers(@RequestParam("file")MultipartFile file) {
-        String fileName = file.getOriginalFilename();
-
-        // 根据文件类型进行不同的处理
-        try {
-            if(fileName.endsWith(".xls") || fileName.endsWith(".xlsx")) {
-                userService.batchCreateUsersFromExcel(file);
-            } else if (fileName.endsWith(".csv")) {
-                userService.batchCreateUsersFromCsv(file);
-            }else {
-                return MyResponse.error(400, "文件类型不支持");
-            }
-        } catch (Exception e) {
-            log.error("上传文件失败,错误如下:"+e.getMessage());
-            return MyResponse.error(400, "上传文件失败:"+e.getMessage());
-        }
-
-        return MyResponse.success("上传成功");
+        // TODO 可能会有操作记录的功能
+        IPage<UserVO> userVO = userService.searchUsers(page, id, name, officialNumber, role);
+        return MyResponse.success(userVO);
     }
 
-    /**
-     * (管理员)根据ID删除某个特定的用户
-     * @param userId
-     * @return
-     */
-    @DeleteMapping()
-    @PreAuthorize("hasRole('ROLE_ADMIN')")
-    public MyResponse deleteById(@RequestParam Long userId) {
-        return MyResponse.success("删除成功");
-    }
 }

+ 26 - 0
src/main/java/com/njuzr/eaibackend/dto/AdminRegisterDTO.java

@@ -0,0 +1,26 @@
+package com.njuzr.eaibackend.dto;
+
+import com.njuzr.eaibackend.enums.Role;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * @author: Leonezhurui
+ * @Date: 2024/2/23 - 14:36
+ * @Package: EAI-Backend
+ */
+
+@Data
+public class AdminRegisterDTO {
+    @NotNull(message = "name不能为空")
+    private String name; // 真实姓名(必填)
+
+    @NotNull(message = "officialEmail不能为空")
+    private String officialEmail; // 南大官邮,xxx@smail.nju.edu.cn(必填)
+
+    @NotNull(message = "officialNumber不能为空")
+    private String officialNumber; // 南大学号、工号(必填)
+
+    @NotNull(message = "role不能为空")
+    private Role role; // 用户角色:学生、教师、管理员(必填)
+}

+ 9 - 0
src/main/java/com/njuzr/eaibackend/dto/UserRegisterDTO.java

@@ -2,6 +2,7 @@ package com.njuzr.eaibackend.dto;
 
 import com.njuzr.eaibackend.enums.Role;
 import com.njuzr.eaibackend.po.base.BaseUser;
+import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
@@ -15,13 +16,21 @@ import lombok.ToString;
 
 @Data
 public class UserRegisterDTO { // 用户注册,前传后
+    @NotNull(message = "name不能为空")
     private String name; // 真实姓名(必填)
 
+    @NotNull(message = "password不能为空")
     private String password;  // 密码(加密存储)(必填)
 
+    @NotNull(message = "officialEmail不能为空")
     private String officialEmail; // 南大官邮,xxx@smail.nju.edu.cn(必填)
 
+    @NotNull(message = "验证码不能为空")
+    private String verifyCode;
+
+    @NotNull(message = "officialNumber不能为空")
     private String officialNumber; // 南大学号、工号(必填)
 
+    @NotNull(message = "role不能为空")
     private Role role; // 用户角色:学生、教师、管理员(必填)
 }

+ 29 - 0
src/main/java/com/njuzr/eaibackend/dto/UserUpdateDTO.java

@@ -0,0 +1,29 @@
+package com.njuzr.eaibackend.dto;
+
+import com.njuzr.eaibackend.enums.Gender;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author: Leonezhurui
+ * @Date: 2024/2/24 - 10:21
+ * @Package: EAI-Backend
+ */
+
+@Data
+public class UserUpdateDTO {
+    private String userAvatar; // 头像URL(有默认值、选填)
+
+    private Date lastLoginTime; // 上一次登陆时间(可选)
+
+    private String englishName; // 英文名(选填)
+
+    private Gender gender; // 性别(选填)
+
+    private Date birthday; // 生日(选填)
+
+    private String phone; // 联系方式(选填)
+
+    private String contentEmail; // 联系邮箱(选填)
+}

+ 33 - 3
src/main/java/com/njuzr/eaibackend/exception/GlobalExceptionHandler.java

@@ -3,16 +3,20 @@ package com.njuzr.eaibackend.exception;
 import com.njuzr.eaibackend.controller.MyResponse;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
 
-import java.nio.file.AccessDeniedException;
 
 /**
  * @author: Leonezhurui
  * @Date: 2024/2/21 - 08:35
  * @Package: EAI-Backend
- * @Descrpition: 定义Controller层异常处理机制
+ * @Descrpition: 定义全局异常处理,是异常处理的最后一道防线;在业务逻辑中未处理的异常,会统一在这里被捕捉;
+ * 如果需要捕捉特定异常,则需要在业务代码中指定。
  */
 
 @Slf4j
@@ -24,7 +28,33 @@ public class GlobalExceptionHandler {
     @ExceptionHandler(org.springframework.security.access.AccessDeniedException.class)
     public MyResponse handleAccessDeniedException(org.springframework.security.access.AccessDeniedException e) {
         log.error("全局异常处理器捕获,访问被拒绝,错误如下:"+e.getMessage());
-        return MyResponse.error(HttpStatus.FORBIDDEN.value(), "访问被拒绝:" + e.getMessage());
+        return MyResponse.error(HttpStatus.FORBIDDEN.value(), "权限不够:" + e.getMessage());
+    }
+
+    /**
+     * 处理@NotNull标注的字段为空的情况
+     * @param e
+     * @return
+     */
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    @ResponseBody
+    public MyResponse handleMethodArgumentNotValid(MethodArgumentNotValidException e) {
+        log.error("全局异常处理器捕获,@NotNull标注的字段为null,错误如下:"+e.getMessage());
+        return MyResponse.error(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"传递信息缺失");
+    }
+
+    @ExceptionHandler(HttpMessageNotReadableException.class)
+    public MyResponse handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
+        log.error("全局异常处理器捕获,请求体转换出错,错误如下"+":"+e.getMessage());
+        return MyResponse.error(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"传递信息错误");
+
+    }
+
+
+    @ExceptionHandler(MyException.class)
+    public MyResponse handleGlobalException(MyException e) {
+        log.error("全局异常处理器捕获,错误如下:"+e.getMessage());
+        return MyResponse.error(e.getErrCode(), e.getMessage());
     }
 
     @ExceptionHandler(Exception.class)

+ 10 - 2
src/main/java/com/njuzr/eaibackend/exception/MyAuthenticationEntryPoint.java

@@ -5,6 +5,7 @@ import com.njuzr.eaibackend.controller.MyResponse;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.authentication.CredentialsExpiredException;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.web.AuthenticationEntryPoint;
 import org.springframework.stereotype.Component;
@@ -27,10 +28,17 @@ public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
     @Override
     public void commence(HttpServletRequest request, HttpServletResponse response,
                          AuthenticationException authException) throws IOException {
-        log.error("认证出错,用户未提供认证信息");
+
         response.setContentType("application/json;charset=UTF-8");
         response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
-        MyResponse myResponse = MyResponse.error(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: " + authException.getMessage());
+        MyResponse myResponse;
+        if (authException instanceof CredentialsExpiredException) { // token过期的异常处理
+            log.error("认证出错,用户token已过期");
+            myResponse = MyResponse.error(HttpServletResponse.SC_UNAUTHORIZED, "token已过期:" + authException.getMessage());
+        } else {
+            log.error("认证出错,用户未提供认证信息");
+            myResponse = MyResponse.error(HttpServletResponse.SC_UNAUTHORIZED, "用户未授权:" + authException.getMessage());
+        }
         response.getWriter().write(mapper.writeValueAsString(myResponse));
     }
 }

+ 9 - 0
src/main/java/com/njuzr/eaibackend/mapper/UserMapper.java

@@ -1,10 +1,14 @@
 package com.njuzr.eaibackend.mapper;
 
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.njuzr.eaibackend.po.User;
 import org.apache.ibatis.annotations.Insert;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Select;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.util.List;
 
@@ -24,6 +28,7 @@ public interface UserMapper extends BaseMapper<User> {
      */
     User selectByOfficialNumber(String officialNumber);
 
+
     /**
      * 用户信息更新
      * @param user
@@ -31,10 +36,14 @@ public interface UserMapper extends BaseMapper<User> {
      */
     int updateUser(User user);
 
+
+    int updatePassword(Long id, String newPassword);
+
     /**
      * 批量导入用户
      * @param users 用户列表
      * @return 成功操作的数量
      */
+    @Transactional
     int batchInsert(List<User> users);
 }

+ 116 - 31
src/main/java/com/njuzr/eaibackend/service/EmailService.java

@@ -1,10 +1,15 @@
 package com.njuzr.eaibackend.service;
 
+import com.njuzr.eaibackend.exception.MyException;
+import com.njuzr.eaibackend.utils.WebClientUtil;
 import jakarta.mail.MessagingException;
 import jakarta.mail.internet.MimeMessage;
+import lombok.AllArgsConstructor;
+import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
 import org.springframework.mail.SimpleMailMessage;
 import org.springframework.mail.javamail.JavaMailSender;
 import org.springframework.mail.javamail.JavaMailSenderImpl;
@@ -20,43 +25,123 @@ import org.springframework.stereotype.Service;
 @Slf4j
 @Service
 public class EmailService {
-    private final JavaMailSender javaMailSender = new JavaMailSenderImpl();
+    private final WebClientUtil webClientUtil = new WebClientUtil("https://message.seec.seecoder.cn/message/mail");
 
-    @Value("${spring.mail.username}")
-    private String sourceEmail;
+    private void sendEmail(MyRequestObject requestObject) {
+        MyResponseObject response = webClientUtil.post("", requestObject, MyResponseObject.class);
+        if (response.code == 1) {
+            log.info("WebClient请求成功,邮件发送成功!");
+        } else {
+            log.error("WebClient请求失败,邮件发送失败~");
+            throw new MyException(HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()+":"+"邮件发送失败");
+        }
+    }
 
     /**
-     * FIXME 邮箱服务暂时不可用,需要Seecoder的公邮和stmp秘钥
+     * 创建账号后发送初始密码
      * @param to
      * @param username
      * @param password
-     * @return
-     * @throws MessagingException
      */
-    public int sendPasswordEmail(String to, String username, String password) throws MessagingException {
-        try {
-            MimeMessage message = javaMailSender.createMimeMessage();
-            MimeMessageHelper helper = new MimeMessageHelper(message, true);
-
-            helper.setFrom(sourceEmail);
-            helper.setTo(to);
-            helper.setSubject("Your New Account");
-
-            String htmlContent = "<h1>欢迎来到EAI天地!</h1>" +
-                    "<p>亲爱的<b>" + username + "</b>同学,</p>" +
-                    "<p>你的EAI账号已经创建,初始密码是<b>" + password + "</b></p>" +
-                    "<p>请在登陆后尽快修改密码!</p>" +
-                    "<p><a href='https://example.com/login'>登陆的虫洞~</a></p>" +
-                    "<p>祝你的专业写作能力不断提升,<br>Seecoder EAI开发团队</p>";
-
-            helper.setText(htmlContent, true);
-
-            javaMailSender.send(message);
-            log.info("邮件发送成功!==> 收件人:{}", username);
-            return 1;
-        } catch (Exception e) {
-            log.error("邮件发送失败!==> {}",e.getMessage());
-            return 0;
-        }
+    public void sendInitialPasswordEmail(String to, String username, String password) {
+        sendEmail(new MyRequestObject(
+                new String[]{to},
+                "Your New Account",
+                "<h1>欢迎来到EAI天地!</h1>" +
+                        "<p>亲爱的<b>" + username + "</b>同学,</p>" +
+                        "<p>你的EAI账号已经创建,初始密码是<b>" + password + "</b></p>" +
+                        "<p>请在登陆后尽快修改密码!</p>" +
+                        "<p><a href='https://example.com/login'>登陆的虫洞~</a></p>" +
+                        "<p>祝你的专业写作能力不断提升,<br>Seecoder EAI开发团队</p>"
+        ));
+    }
+
+    /**
+     * 创建账号后发送初始密码
+     * @param to
+     * @param username
+     * @param password
+     */
+    public void sendResetPasswordEmail(String to, String username, String password) {
+        sendEmail(new MyRequestObject(
+                new String[]{to},
+                "Your Reset Password",
+                "<h1>欢迎来到EAI天地!</h1>" +
+                        "<p>亲爱的<b>" + username + "</b>同学,</p>" +
+                        "<p>你的密码已被重置,重置后的密码是<b>" + password + "</b></p>" +
+                        "<p><a href='https://example.com/login'>登陆的虫洞~</a></p>" +
+                        "<p>祝你的专业写作能力不断提升,<br>Seecoder EAI开发团队</p>"
+        ));
+    }
+
+
+    public void sendVerifyCodeEmail(String to, String code, int timeout) {
+        sendEmail(new MyRequestObject(
+                new String[]{to},
+                "Your Verify Code",
+                "<h1>欢迎来到EAI天地!</h1>" +
+                        "<p>亲爱的同学,</p>" +
+                        "<p>你的邮箱验证是<b>" + code + "</b></p>" +
+                        "<p>请尽快完成注册流程,邮箱验证码有效时间为<b>"+timeout+"分钟</b></p>" +
+                        "<p>祝你的专业写作能力不断提升,<br>Seecoder EAI开发团队</p>"
+        ));
+    }
+
+
+    @Data
+    @AllArgsConstructor
+    static class MyRequestObject {
+        private String[] receivers;
+        private String subject;
+        private String text;
     }
+
+    @Data
+    static class MyResponseObject {
+        private int code;
+        private String msg;
+        private Object data;
+    }
+
+
+//    private final JavaMailSender javaMailSender = new JavaMailSenderImpl();
+//
+//    @Value("${spring.mail.username}")
+//    private String sourceEmail;
+
+//    /**
+//     * FIXME 邮箱服务暂时不可用,需要Seecoder的公邮和stmp秘钥
+//     * @param to
+//     * @param username
+//     * @param password
+//     * @return
+//     * @throws MessagingException
+//     */
+//    public int sendPasswordEmail(String to, String username, String password) throws MessagingException {
+//        try {
+//            MimeMessage message = javaMailSender.createMimeMessage();
+//            MimeMessageHelper helper = new MimeMessageHelper(message, true);
+//
+//            helper.setFrom(sourceEmail);
+//            helper.setTo(to);
+//            helper.setSubject("Your New Account");
+//
+//            String htmlContent = "<h1>欢迎来到EAI天地!</h1>" +
+//                    "<p>亲爱的<b>" + username + "</b>同学,</p>" +
+//                    "<p>你的EAI账号已经创建,初始密码是<b>" + password + "</b></p>" +
+//                    "<p>请在登陆后尽快修改密码!</p>" +
+//                    "<p><a href='https://example.com/login'>登陆的虫洞~</a></p>" +
+//                    "<p>祝你的专业写作能力不断提升,<br>Seecoder EAI开发团队</p>";
+//
+//            helper.setText(htmlContent, true);
+//
+//            javaMailSender.send(message);
+//            log.info("邮件发送成功!==> 收件人:{}", username);
+//            return 1;
+//        } catch (Exception e) {
+//            log.error("邮件发送失败!==> {}",e.getMessage());
+//            return 0;
+//        }
+//    }
+
 }

+ 38 - 6
src/main/java/com/njuzr/eaibackend/service/UserService.java

@@ -1,14 +1,16 @@
 package com.njuzr.eaibackend.service;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.njuzr.eaibackend.dto.AdminRegisterDTO;
 import com.njuzr.eaibackend.dto.UserDTO;
 import com.njuzr.eaibackend.dto.UserRegisterDTO;
+import com.njuzr.eaibackend.dto.UserUpdateDTO;
+import com.njuzr.eaibackend.enums.Role;
+import com.njuzr.eaibackend.po.User;
 import com.njuzr.eaibackend.vo.UserVO;
-import com.opencsv.exceptions.CsvValidationException;
-import org.springframework.security.core.userdetails.User;
 import org.springframework.web.multipart.MultipartFile;
 
-import java.io.IOException;
-import java.util.List;
 
 /**
  * @author: Leonezhurui
@@ -25,21 +27,51 @@ public interface UserService {
      */
     UserVO createUser(UserRegisterDTO userDTO);
 
+    void sendVerifyCode(String officialEmail);
+
+    void adminCreateUser(AdminRegisterDTO adminRegisterDTO);
+
     /**
      * 通过用户Id找到用户
      * @param id
      * @return UserVO
      */
-    UserVO findById(Long id);
+    IPage<UserVO> searchUsers(Page<User> page, Long id, String name, String officialNumber, Role role);
 
     /**
-     * 更新用户信息
+     * 用户更新用户信息
      * @param id
      * @param userDTO
      * @return UserVO
      */
+    UserVO updateUser(Long id, UserUpdateDTO userDTO);
+
+    /**
+     * 管理员更新用户信息
+     * @param id
+     * @param userDTO
+     * @return
+     */
     UserVO updateUser(Long id, UserDTO userDTO);
 
+    /**
+     * 用户更新密码
+     * @param id
+     * @param newPassword
+     * @param oldPassword
+     */
+    void updatePassword(Long id, String newPassword, String oldPassword);
+
+    /**
+     * 管理员重置密码
+     * @param id
+     */
+    void resetPassword(Long id);
+
+
+    void deleteById(Long id);
+
+
     /**
      * 从Excel文件中读取数据,并批量创建用户
      * @param file

+ 249 - 58
src/main/java/com/njuzr/eaibackend/service/impl/UserServiceImpl.java

@@ -1,27 +1,34 @@
 package com.njuzr.eaibackend.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
 import com.baomidou.mybatisplus.extension.activerecord.Model;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.njuzr.eaibackend.dto.AdminRegisterDTO;
 import com.njuzr.eaibackend.dto.UserDTO;
 import com.njuzr.eaibackend.dto.UserRegisterDTO;
+import com.njuzr.eaibackend.dto.UserUpdateDTO;
+import com.njuzr.eaibackend.enums.Role;
 import com.njuzr.eaibackend.exception.MyException;
 import com.njuzr.eaibackend.mapper.UserMapper;
 import com.njuzr.eaibackend.po.User;
 import com.njuzr.eaibackend.service.EmailService;
 import com.njuzr.eaibackend.service.UserService;
 import com.njuzr.eaibackend.utils.ModelMapperUtil;
+import com.njuzr.eaibackend.utils.PageMapperUtil;
 import com.njuzr.eaibackend.vo.UserVO;
 import com.opencsv.CSVReader;
 import com.opencsv.exceptions.CsvValidationException;
 import jakarta.mail.MessagingException;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.poi.ss.usermodel.Cell;
-import org.apache.poi.ss.usermodel.Row;
-import org.apache.poi.ss.usermodel.Sheet;
-import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.ss.usermodel.*;
 import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.http.HttpStatus;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
@@ -29,10 +36,9 @@ import org.springframework.web.multipart.MultipartFile;
 
 import java.io.IOException;
 import java.io.InputStreamReader;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.UUID;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 /**
  * @author: Leonezhurui
@@ -46,48 +52,124 @@ public class UserServiceImpl implements UserService {
 
     private final UserMapper userMapper;
 
+    private final RedisTemplate<String, Object> redisTemplate;
+
     private final EmailService emailService = new EmailService();
 
     private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
 
     @Autowired
-    public UserServiceImpl(UserMapper userMapper) {
+    public UserServiceImpl(UserMapper userMapper, RedisTemplate<String, Object> redisTemplate) {
         this.userMapper = userMapper;
+        this.redisTemplate = redisTemplate;
     }
 
 
     @Override
-    public UserVO findById(Long id) {
-        try {
-            User target = userMapper.selectById(id);
-            return ModelMapperUtil.map(target, UserVO.class);
-        }catch (Exception e){
-            log.error("findById -- 数据库查找错误:"+e.getMessage());
-            throw new MyException(501, "用户查找失败,id错误");
+    public IPage<UserVO> searchUsers(Page<User> page, Long id, String name, String officialNumber, Role role) {
+        QueryWrapper<User> queryWrapper = new QueryWrapper<>(); // 设置查询条件
+
+        if(role == Role.ADMIN) // 禁止访问查询ADMIN用户
+            throw new MyException(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase()+":"+"被拒绝");
+
+        queryWrapper
+                .eq(Objects.nonNull(id), "id", id)
+                .like(StringUtils.isNotBlank(name), "name", name)
+                .eq(StringUtils.isNotBlank(officialNumber), "official_number", officialNumber)
+                .eq(Objects.nonNull(role), "role", role);
+
+        IPage<User> targets = userMapper.selectPage(page,queryWrapper); // selectPage是内置的方法
+
+        // targets.total如果为0,则说明没有符合条件的用户,但是不报错,由前端自行处理异常。
+
+        return PageMapperUtil.convert(targets, record -> ModelMapperUtil.map(record, UserVO.class));
+    }
+
+    @Override
+    public UserVO createUser(UserRegisterDTO userDTO) throws MyException{
+        Role role = userDTO.getRole();
+        if (role != Role.STUDENT) { // 只允许注册学生账号
+            throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"权限不够");
+        }
+
+        // 从Redis中获取验证码
+        final String key = "verifyCode:" + userDTO.getOfficialEmail();
+        String storedCode = (String) redisTemplate.opsForValue().get(key);
+
+        if (storedCode == null)
+            throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"验证码已过期");
+
+        // 验证码校验
+        if (storedCode.equals(userDTO.getVerifyCode())) {
+            User targetUser = ModelMapperUtil.map(userDTO, User.class);
+
+            targetUser.setPassword(new BCryptPasswordEncoder().encode(targetUser.getPassword())); // 密码加密存储
+            targetUser.setCreateTime(new Date()); // 设置创建时间
+
+            if (isUserNotExists(targetUser.getOfficialNumber())) {
+                int status = userMapper.insert(targetUser);
+                if (status == 0) {
+                    log.error("createUser -- 数据库插入错误:");
+                    throw new MyException(501, "数据库插入错误");
+                }
+            }
+
+            log.info("User创建完毕,User对象如下:"+ targetUser);
+
+            redisTemplate.delete(key); //注册成功后删除验证码
+
+            return ModelMapperUtil.map(targetUser, UserVO.class);
+
+        } else {
+            throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"验证码错误");
         }
     }
 
     @Override
-    public UserVO createUser(UserRegisterDTO userDTO) {
-        User targetUser = ModelMapperUtil.map(userDTO, User.class);
+    public void sendVerifyCode(String officialEmail) {
+        // 生成验证码
+        String code = UUID.randomUUID().toString().substring(0, 6);
+        // 存储验证码到Redis,设置5分钟过期
+        int timeout = 5;
+        final String key = "verifyCode:" + officialEmail;
+        redisTemplate.opsForValue().set(key, code, timeout, TimeUnit.MINUTES);
+
+        emailService.sendVerifyCodeEmail(officialEmail, code, timeout);
+    }
 
-        targetUser.setPassword(new BCryptPasswordEncoder().encode(targetUser.getPassword())); // 密码加密存储
-        targetUser.setCreateTime(new Date()); // 设置创建时间
+    /**
+     * 管理员创建用户,给出name、officialEmail、officialNumber、role,随机生成密码,将密码通过邮件服务发送到用户邮箱
+     * @param adminRegisterDTO
+     * @return 1表示成功,0表示失败
+     */
+    @Override
+    public void adminCreateUser(AdminRegisterDTO adminRegisterDTO) {
+        Role role = adminRegisterDTO.getRole();
+        if (role != Role.STUDENT && role != Role.TEACHER) { // 只能注册学生或老师账号
+            throw new MyException(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase()+":"+"被拒绝");
+        }
 
-        try {
-            int status = userMapper.insert(targetUser);
-            if (status == 0) {
+        User targetUser = ModelMapperUtil.map(adminRegisterDTO, User.class);
+
+        // 随机生成密码
+        String randomPassword = UUID.randomUUID().toString();
+        targetUser.setPassword(passwordEncoder.encode(randomPassword));
+
+        targetUser.setCreateTime(new Date());
+
+        // 检查用户是否存在
+        if (isUserNotExists(targetUser.getOfficialNumber())) {
+            int code =  userMapper.insert(targetUser);
+            if (code == 0) {
                 log.error("createUser -- 数据库插入错误:");
                 throw new MyException(501, "数据库插入错误");
             }
-        } catch (Exception e) {
-            log.error("createUser -- 数据库插入错误:"+e.getMessage());
-            throw new MyException(501, "数据库插入错误");
+            log.info("User创建完毕,User对象如下:"+ targetUser);
         }
 
-        log.info("User创建完毕,User对象如下:"+ targetUser);
+        // 发送初始密码
+        emailService.sendInitialPasswordEmail(targetUser.getOfficialEmail(), targetUser.getName(), randomPassword);
 
-        return ModelMapperUtil.map(targetUser, UserVO.class);
     }
 
     /**
@@ -95,6 +177,12 @@ public class UserServiceImpl implements UserService {
      * @param userDTO
      * @return
      */
+    @Override
+    public UserVO updateUser(Long id, UserUpdateDTO userDTO) {
+        UserDTO opeUser = ModelMapperUtil.map(userDTO, UserDTO.class);
+        return updateUser(id, opeUser);
+    }
+
     @Override
     public UserVO updateUser(Long id, UserDTO userDTO) {
         User opeUser = ModelMapperUtil.map(userDTO, User.class);
@@ -106,11 +194,59 @@ public class UserServiceImpl implements UserService {
             }
         }catch (Exception e) {
             log.error("数据库更新错误,错误如下:"+e.getMessage());
-            throw new MyException(500, "数据库更新失败");
+            throw new MyException(HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()+":"+"数据库更新失败");
         }
         return null;
     }
 
+    @Override
+    public void updatePassword(Long id, String newPassword, String oldPassword) throws MyException{
+        User user = userMapper.selectById(id);
+        if (user == null) {
+            log.error("用户不存在");
+            throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"找不到用户");
+        }
+
+        if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
+            log.error("用户就密码不正确");
+            throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"旧密码不正确");
+        }
+
+        String encodedNewPassword = passwordEncoder.encode(newPassword);
+        int code = userMapper.updatePassword(id, encodedNewPassword);
+        if (code == 1) {
+            log.info("用户更新成功");
+        }
+    }
+
+    /**
+     * 管理员重置密码,并将重置密码发送到指定邮箱
+     * @param id
+     */
+    @Override
+    public void resetPassword(Long id) {
+        User targetUser = userMapper.selectById(id);
+        if(targetUser == null) throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"用户不存在");
+        String targetName = targetUser.getName();
+        String targetEmail = targetUser.getOfficialEmail();
+        String randomPassword = UUID.randomUUID().toString();
+        int code = userMapper.updatePassword(id, passwordEncoder.encode(randomPassword));
+        if (code == 1) {
+            log.info("用户更新成功");
+        }
+        emailService.sendResetPasswordEmail(targetEmail, targetName, randomPassword);
+    }
+
+
+    @Override
+    public void deleteById(Long id) {
+        if (isUserExists(id)) {
+            int res = userMapper.deleteById(id);
+            if (res == 0)
+                throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"删除失败");
+        }
+    }
+
     /**
      * 解析文件,批量创建用户。其中,文件必须包含姓名、邮箱、学号。
      * @param file
@@ -125,27 +261,24 @@ public class UserServiceImpl implements UserService {
         int numberCol = findColumnIndex(headerRow, "学号");
         int emailCol = findColumnIndex(headerRow, "邮箱");
 
+        if(nameCol < 0 || numberCol < 0 || emailCol < 0)
+            throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"缺失必要字段信息");
+
         List<User> users = new ArrayList<>();
 
         for (Row row : sheet) {
             if (row.getRowNum() == 0) continue; // 跳过表头
 
             User user = new User();
-            user.setName(row.getCell(nameCol).getStringCellValue());
-            user.setOfficialNumber(row.getCell(numberCol).getStringCellValue());
-            user.setOfficialEmail(row.getCell(emailCol).getStringCellValue());
-
-            // 随机生成密码,并使用BCryptPasswordEncoder加密密码
-            String randomPassword = UUID.randomUUID().toString();
-            String encodedPassword = passwordEncoder.encode(randomPassword);
-            user.setPassword(encodedPassword);
+            user.setName(stringifyCellValue(row.getCell(nameCol)));
+            user.setOfficialNumber(stringifyCellValue(row.getCell(numberCol)));
+            user.setOfficialEmail(stringifyCellValue(row.getCell(emailCol)));
+            user.setRole(Role.STUDENT); // 批量创建只创建学生账号
+            user.setCreateTime(new Date());
 
             users.add(user);
         }
 
-        int res = userMapper.batchInsert(users);
-        if (res == 0) throw new MyException(500, "批量创建失败");
-
         processUsers(users);
     }
 
@@ -157,28 +290,66 @@ public class UserServiceImpl implements UserService {
         int numberCol = findColumnIndex(header, "学号");
         int emailCol = findColumnIndex(header, "邮箱");
 
+        if(nameCol < 0 || numberCol < 0 || emailCol < 0)
+            throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"缺失必要字段信息");
+
         List<User> users = new ArrayList<>();
         String[] nextRecord;
         while ((nextRecord = csvReader.readNext()) != null) {
-            User user = new User();
-            user.setName(nextRecord[nameCol]);
-            user.setOfficialNumber(nextRecord[numberCol]);
-            user.setOfficialEmail(nextRecord[emailCol]);
-
-            // 随机生成密码,并使用BCryptPasswordEncoder加密密码
-            String randomPassword = UUID.randomUUID().toString();
-            String encodedPassword = passwordEncoder.encode(randomPassword);
-            user.setPassword(encodedPassword);
+            String officialNumber = String.valueOf(nextRecord[numberCol]);
+            if (isUserNotExists(officialNumber)) {
+                User user = new User();
+                user.setName(String.valueOf(nextRecord[nameCol]));
+                user.setOfficialNumber(officialNumber);
+                user.setOfficialEmail(String.valueOf(nextRecord[emailCol]));
+                user.setRole(Role.STUDENT); // 批量创建只创建学生账号
+                user.setCreateTime(new Date());
+
+                users.add(user);
+            }
 
-            users.add(user);
         }
 
-        int res = userMapper.batchInsert(users);
-        if (res == 0) throw new MyException(500, "批量创建失败");
-
+        // 统一生成随机密码,批量创建用户,创建成功后,将初始密码发送至邮箱
         processUsers(users);
+
+    }
+
+
+    /**
+     * 查看用户是否不存在,如果用户不存在,则返回true
+     * @param officialNumber
+     * @return
+     */
+    private boolean isUserNotExists(String officialNumber) throws MyException{
+        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("official_number", officialNumber);
+        User existingUser = userMapper.selectOne(queryWrapper);
+        if (existingUser != null) {
+            log.error("用户已存在,学号{}重复", officialNumber);
+            throw new MyException(400, "用户已存在,请检查学号信息");
+        }
+        return true;
     }
 
+    /**
+     * 检查用户是否存在,如果存在则返回true;不存在则报错
+     * @param id
+     * @return
+     * @throws MyException
+     */
+    private boolean isUserExists(Long id) throws MyException{
+        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("id", id);
+        User existingUser = userMapper.selectOne(queryWrapper);
+        if (existingUser == null) {
+            log.error("用户不存在,ID:{}", id);
+            throw new MyException(400, "用户不存在,请检查ID信息");
+        }
+        return true;
+    }
+
+
     /**
      * 在指定Excel Cell中找到具体列名的索引值
      * @param headerRow
@@ -194,6 +365,25 @@ public class UserServiceImpl implements UserService {
         return -1;
     }
 
+    private String stringifyCellValue(Cell cell) {
+        String cellValue;
+        if (cell.getCellType() == CellType.STRING) {
+            cellValue = cell.getStringCellValue();
+        } else if (cell.getCellType() == CellType.NUMERIC) {
+            // 对于数值类型,可以将其转换为字符串
+            // 注意:这里直接使用了Double.toString,你可能需要根据实际情况调整格式化方式
+            cellValue = Double.toString(cell.getNumericCellValue());
+        } else if (cell.getCellType() == CellType.BOOLEAN) {
+            // 对于布尔类型,也可以转换为字符串
+            cellValue = Boolean.toString(cell.getBooleanCellValue());
+        } else {
+            // 其他类型,根据需要处理或转换为字符串
+            // 例如,对于公式类型,可以计算公式的值
+            throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"上传失败:请检查excel单元格的类型");
+        }
+        return cellValue;
+    }
+
     /**
      * 在csv文件的header中找到具体列的索引值
      * @param header
@@ -209,12 +399,12 @@ public class UserServiceImpl implements UserService {
         return -1;
     }
 
+
     /**
-     * TODO 通过邮箱服务,将初始密码发送给指定邮箱
+     * 统一生成随机密码,批量创建用户,创建成功后,将初始密码发送至邮箱
      * @param users
-     * @throws MessagingException
      */
-    private void processUsers(List<User> users) throws MessagingException {
+    private void processUsers(List<User> users) {
         List<String> passwordStage = new ArrayList<>();
         for(User user: users) {
             // 随机生成密码,并使用BCryptPasswordEncoder加密密码
@@ -224,10 +414,11 @@ public class UserServiceImpl implements UserService {
             user.setPassword(encodedPassword);
         }
         int res = userMapper.batchInsert(users);
-        if (res == 0) throw new MyException(500, "批量创建失败");
+        if (res == 0)
+            throw new MyException(HttpStatus.INTERNAL_SERVER_ERROR.value(),HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()+":"+"批量创建失败");
 
         for (int i = 0; i < users.size(); i++) {
-            emailService.sendPasswordEmail(users.get(i).getOfficialEmail(), users.get(i).getName(), passwordStage.get(i));
+            emailService.sendInitialPasswordEmail(users.get(i).getOfficialEmail(), users.get(i).getName(), passwordStage.get(i));
         }
     }
 

+ 14 - 4
src/main/java/com/njuzr/eaibackend/utils/JWTTokenUtil.java

@@ -4,6 +4,8 @@ import io.jsonwebtoken.*;
 import io.jsonwebtoken.security.Keys;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.CredentialsExpiredException;
 import org.springframework.stereotype.Component;
 
 import java.time.Instant;
@@ -59,10 +61,18 @@ public class JWTTokenUtil {
      * @return Jws<Claims>
      */
     public Jws<Claims> parseClaim(String token) {
-        return Jwts.parser()
-                .verifyWith(Keys.hmacShaKeyFor(secret.getBytes()))
-                .build()
-                .parseSignedClaims(token);
+        try {
+            return Jwts.parser()
+                    .verifyWith(Keys.hmacShaKeyFor(secret.getBytes()))
+                    .build()
+                    .parseSignedClaims(token);
+        } catch (ExpiredJwtException e) {
+            // 令牌过期
+            throw new CredentialsExpiredException("Token has expired", e);
+        } catch (JwtException e) {
+            // 其他JWT解析错误
+            throw new AuthenticationServiceException("Error parsing JWT", e);
+        }
     }
 
     public String parseToken(String token) {

+ 0 - 69
src/main/java/com/njuzr/eaibackend/utils/ObjectFieldCheckerUtil.java

@@ -1,69 +0,0 @@
-package com.njuzr.eaibackend.utils;
-
-import lombok.extern.slf4j.Slf4j;
-
-import java.lang.reflect.Field;
-
-/**
- * @author: Leonezhurui
- * @Date: 2024/2/21 - 11:06
- * @Package: EAI-Backend
- */
-
-@Slf4j
-public class ObjectFieldCheckerUtil {
-
-    /**
-     * 检查指定的字段是否全部为 null 或不存在。
-     *
-     * @param object 对象实例
-     * @param fieldNames 字段名数组
-     * @return 如果所有指定字段都为 null 或不存在,则返回 true;如果任一字段存在且不为 null,则返回 false。
-     */
-    public static boolean areAllFieldsNullOrAbsent(Object object, String... fieldNames) {
-        if (object == null) {
-            return true;
-        }
-
-        for (String fieldName : fieldNames) {
-            try {
-                Field field = getFieldFromClassOrSuperclass(object.getClass(), fieldName);
-                if (field != null) {
-                    field.setAccessible(true); // 确保可以访问私有和受保护字段
-                    if (field.get(object) != null) {
-                        return false;
-                    }
-                }
-            } catch (IllegalAccessException e) {
-                // IllegalAccessException: 如果此 Field 对象正在执行 Java 语言访问控制,并且底层字段不可访问
-                log.error("对象字段出错,错误如下:"+e.getMessage());
-                throw new RuntimeException("无法访问字段: " + e.getMessage());
-            }
-        }
-
-        // 所有字段都为 null 或不存在
-        return true;
-    }
-
-    /**
-     * 遍历当前类及其所有父类,直到找到所需的字段或到达类层次结构的顶端
-     * @param clazz
-     * @param fieldName
-     * @return
-     */
-    public static Field getFieldFromClassOrSuperclass(Class<?> clazz, String fieldName) {
-        Class<?> currentClass = clazz;
-        while (currentClass != null) {
-            try {
-                // 尝试在当前类中获取字段
-                return currentClass.getDeclaredField(fieldName);
-            } catch (NoSuchFieldException e) {
-                // 如果当前类中没有该字段,移动到父类继续查找
-                currentClass = currentClass.getSuperclass();
-            }
-        }
-        // 如果在整个类层次结构中都没有找到该字段,则返回 null 或抛出异常
-        return null;
-    }
-
-}

+ 32 - 0
src/main/java/com/njuzr/eaibackend/utils/PageMapperUtil.java

@@ -0,0 +1,32 @@
+package com.njuzr.eaibackend.utils;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * @author: Leonezhurui
+ * @Date: 2024/2/27 - 14:49
+ * @Package: EAI-Backend
+ */
+
+public class PageMapperUtil {
+    /**
+     * 将分页结果中的每个对象从一种类型转换为另一种类型。
+     * @param sourcePage 源分页结果
+     * @param converter 转换函数
+     * @param <T> 源对象类型
+     * @param <V> 目标VO类型
+     * @return 转换后的分页结果
+     */
+    public static <T, V> Page<V> convert(IPage<T> sourcePage, Function<T, V> converter) {
+        Page<V> targetPage = new Page<>();
+        targetPage.setRecords(sourcePage.getRecords().stream().map(converter).collect(Collectors.toList()));
+        targetPage.setTotal(sourcePage.getTotal());
+        targetPage.setCurrent(sourcePage.getCurrent());
+        targetPage.setSize(sourcePage.getSize());
+        return targetPage;
+    }
+}

+ 82 - 0
src/main/java/com/njuzr/eaibackend/utils/WebClientUtil.java

@@ -0,0 +1,82 @@
+package com.njuzr.eaibackend.utils;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.WebClientResponseException;
+
+/**
+ * @author: Leonezhurui
+ * @Date: 2024/2/25 - 16:52
+ * @Package: EAI-Backend
+ */
+
+@Slf4j
+public class WebClientUtil {
+    private final WebClient webClient;
+
+    // 构造函数,使用baseUrl初始化WebClient
+    public WebClientUtil(String baseUrl) {
+        this.webClient = WebClient.builder()
+                .baseUrl(baseUrl)
+                .build();
+    }
+
+    // GET请求方法
+    public <T> T get(String uri, Class<T> responseType) {
+        try {
+            return this.webClient.get()
+                    .uri(uri)
+                    .retrieve()
+                    .bodyToMono(responseType)
+                    .block(); // 转换为阻塞调用
+        } catch (WebClientResponseException e) {
+            log.error("WebClient发送GET请求失败:" + e.getMessage());
+            throw new RuntimeException("Failed to get response: " + e.getMessage(), e);
+        }
+    }
+
+    // POST请求方法
+    public <T, R> T post(String uri, R request, Class<T> responseType) {
+        try {
+            return this.webClient.post()
+                    .uri(uri)
+                    .bodyValue(request)
+                    .retrieve()
+                    .bodyToMono(responseType)
+                    .block(); // 转换为阻塞调用
+        } catch (WebClientResponseException e) {
+            log.error("WebClient发送POST请求失败:" + e.getMessage());
+            throw new RuntimeException("Failed to post data: " + e.getMessage(), e);
+        }
+    }
+
+    // PUT请求方法
+    public <T, R> T put(String uri, R request, Class<T> responseType) {
+        try {
+            return this.webClient.put()
+                    .uri(uri)
+                    .bodyValue(request)
+                    .retrieve()
+                    .bodyToMono(responseType)
+                    .block(); // 转换为阻塞调用
+        } catch (WebClientResponseException e) {
+            log.error("WebClient发送PUT请求失败:" + e.getMessage());
+            throw new RuntimeException("Failed to put data: " + e.getMessage(), e);
+        }
+    }
+
+    // DELETE请求方法
+    public <T> T delete(String uri, Class<T> responseType) {
+        try {
+            return this.webClient.delete()
+                    .uri(uri)
+                    .retrieve()
+                    .bodyToMono(responseType)
+                    .block(); // 转换为阻塞调用
+        } catch (WebClientResponseException e) {
+            log.error("WebClient发送DELETE请求失败:" + e.getMessage());
+            throw new RuntimeException("Failed to delete resource: " + e.getMessage(), e);
+        }
+    }
+
+}

+ 5 - 2
src/main/resources/application.yaml

@@ -1,4 +1,7 @@
 spring:
+  jackson:
+    deserialization:
+      fail-on-unknown-properties: true
   datasource:
     driver-class-name: com.mysql.cj.jdbc.Driver
     username: root
@@ -7,8 +10,8 @@ spring:
   mail:
     host: smtp.163.com
     port: 25
-    username: xxx # Seecoder的公共邮箱
-    password: xxx # 邮箱开启stmp的秘钥
+    username: seecoder2022@163.com # Seecoder的公共邮箱
+    password: WEIHUIOOJYUFMZGV # 邮箱开启stmp的秘钥
     default-encoding: UTF-8
 
 

+ 8 - 2
src/main/resources/mapper/UserMapper.xml

@@ -26,9 +26,15 @@
     </update>
 
     <insert id="batchInsert" parameterType="list">
-        INSERT INTO users (name, official_number, official_email) VALUES
+        INSERT INTO users (name, password, official_number, official_email, role, create_time) VALUES
         <foreach collection="list" item="user" index="index" separator=",">
-            (#{user.name}, #{user.officialNumber}, #{user.officialEmail})
+            (#{user.name}, #{user.password}, #{user.officialNumber}, #{user.officialEmail}, #{user.role}, #{user.createTime})
         </foreach>
     </insert>
+
+    <update id="updatePassword">
+        UPDATE users
+        SET password=#{newPassword}
+        WHERE id=#{id}
+    </update>
 </mapper>

+ 2 - 1
src/test/java/com/njuzr/eaibackend/service/EmailServiceTest.java

@@ -6,6 +6,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
 
 /**
  * @author: Leonezhurui
@@ -25,7 +26,7 @@ public class EmailServiceTest {
 
     @Test
     public void testEmailSender() throws MessagingException {
-        int res = emailService.sendPasswordEmail("Leonezhurui@gmail.com", "小苏", "123456");
+        int res = emailService.sendEmail("Leonezhurui@gmail.com", "小苏", "123456");
         assertEquals(1, res);
 
     }