博客文档续更

十、 博客前台模块-异常处理

目前我们的项目在认证出错或者权限不足的时候响应回来的Json,默认是使用Security官方提供的响应的格式,但是这种响应的格式肯定是不符合我们项目的接口规范的。所以需要自定义异常处理

我们需要去实现AuthenticationEntryPoint(官方提供的认证失败处理器)类、AccessDeniedHandler(官方提供的授权失败处理器)类,然后配置给Security

博客文档续更_第1张图片

由于我们前台和后台的异常处理是一样的,所以我们在framework包下创建异常处理类

1. 认证的异常处理

 在keke-framework工程的src/main/java/com.keke目录新建handler.security.AuthenticationEntryPointImpl类,写入如下

package com.keke.handler.security;

import com.alibaba.fastjson.JSON;
import com.keke.domain.ResponseResult;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.utils.WebUtils;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
     @Override
     public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException authException) throws IOException, ServletException {
          authException.printStackTrace();
          ResponseResult result = null;
          //BadCredentialsException 这个是我们测试输入错误账号密码出现的异常
          if(authException instanceof BadCredentialsException){
              result = ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_ERROR.getCode(),authException.getMessage());
          } else if (authException instanceof InsufficientAuthenticationException) {
               //InsufficientAuthenticationException 这个是我们测试不携带token出现的异常
               result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
          }else {
               result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR,"认证或者授权失败");
          }
          //响应给前端
          WebUtils.renderString(httpServletResponse, JSON.toJSONString(result));
     }
}

2. 授权的异常处理

在keke-framework工程的src/main/java/com.keke目录新建handler.security.AccessDeniedHandlerImpl类,写入如下

package com.keke.handler.security;

import com.alibaba.fastjson.JSON;
import com.keke.domain.ResponseResult;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.utils.WebUtils;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
     @Override
     public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException accessDeniedException) throws IOException, ServletException {
          accessDeniedException.printStackTrace();
          ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NO_OPERATOR_AUTH);
          //响应给前端
          WebUtils.renderString(httpServletResponse, JSON.toJSONString(result));
     }
}

3. 认证授权异常处理配置到框架

把keke-blog工程的SecurityConfig类修改为如下

package com.keke.config;

import com.keke.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
//WebSecurityConfigurerAdapter是Security官方提供的类
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //注入我们在keke-blog工程写的JwtAuthenticationTokenFilter过滤器
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    AccessDeniedHandler accessDeniedHandler;



    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    //把官方的PasswordEncoder密码加密方式替换成BCryptPasswordEncoder
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
                //为方便测试认证过滤器,我们把查询友链的接口设置为需要登录才能访问。然后我们去访问的时候就能测试登录认证功能了
                .antMatchers("/link/getAllLink").authenticated()
                // 除上面外的所有请求全部不需要认证即可访问
                .anyRequest().permitAll();

        //配置我们自己写的认证和授权的异常处理
        http.exceptionHandling()
                        .authenticationEntryPoint(authenticationEntryPoint)
                        .accessDeniedHandler(accessDeniedHandler);



        http.logout().disable();
        //将自定义filter加入security过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //允许跨域
        http.cors();
    }

}

4. 测试自定义异常处理

第一步:打开redis,启动工程

第二步:向login接口发送用户名或者密码错误的post请求

博客文档续更_第2张图片

第三步:向link/getAllLink接口发送不携带token的get请求

博客文档续更_第3张图片

5. 统一异常处理

实际我们在开发过程中可能需要做很多的判断校验,如果出现了非法情况我们是期望响应对应的提示的。但是如果我们每次都自己手动去处理就会非常麻烦。我们可以选择直接抛出异常的方式,然后对异常进行统一处理。把异常中的信息封装成ResponseResult响应给前端

5.1 自定义异常

在keke-framework工程的src/main/java/com.keke目录新建exception.SystemException类,写入如下

package com.keke.exception;


import com.keke.enums.AppHttpCodeEnum;

//统一异常处理
public class SystemException extends RuntimeException{

    private int code;

    private String msg;

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }

    //定义一个构造方法,接收的参数是枚举类型,AppHttpCodeEnum是我们在huanf-framework工程定义的枚举类
    public SystemException(AppHttpCodeEnum httpCodeEnum) {
        super(httpCodeEnum.getMsg());
        //把某个枚举类里面的code和msg赋值给异常对象
        this.code = httpCodeEnum.getCode();
        this.msg = httpCodeEnum.getMsg();
    }
}

5.2 全局异常处理

在keke-framework的com.keke包下新建handler.exception.GlobalExceptionHandler 写入如下,登录和其他地方出现的异常都会被这里捕获,然后响应返回

package com.keke.handler.exception;

import com.keke.domain.ResponseResult;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.exception.SystemException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;


//@ControllerAdvice //对controller层的增强
//@ResponseBody

//或者用下面一个注解代替上面的两个注解
@RestControllerAdvice
//使用Lombok提供的Slf4j注解,实现日志功能
@Slf4j
//全局异常处理。最终都会在这个类进行处理异常
public class GlobalExceptionHandler {

    //SystemException是我们写的类。用户登录的异常交给这里处理
    @ExceptionHandler(SystemException.class)
    public ResponseResult systemExceptionHandler(SystemException e){

        //打印异常信息,方便我们追溯问题的原因。{}是占位符,具体值由e决定
        log.error("出现了异常! {}",e);

        //从异常对象中获取提示信息封装,然后返回。ResponseResult是我们写的类
        return ResponseResult.errorResult(e.getCode(),e.getMsg());
    }

    //其它异常交给这里处理
    @ExceptionHandler(Exception.class)
    public ResponseResult exceptionHandler(Exception e){

        //打印异常信息,方便我们追溯问题的原因。{}是占位符,具体值由e决定
        log.error("出现了异常! {}",e);

        //从异常对象中获取提示信息封装,然后返回。ResponseResult、AppHttpCodeEnum是我们写的类
        return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR.getCode(),e.getMessage());//枚举值是500
    }
}

5.3 Controller层逻辑

package com.keke.controller;


import com.keke.domain.ResponseResult;
import com.keke.domain.entity.User;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.exception.SystemException;
import com.keke.service.BlogLoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BlogLoginController {

     @Autowired
     private BlogLoginService blogLoginService;

     @PostMapping("/login")
     public ResponseResult login(@RequestBody User user){
          if(!StringUtils.hasText(user.getUserName())){
               //提示必须要传用户名
               throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);
          }
          return blogLoginService.login(user);
     }
}

5.3 测试

向login接口发送一个没有用户名只有密码的post,响应如下

博客文档续更_第4张图片

可以看到,响应回来的信息是正确的

5.4 总结

首相前端发送请求,controller层先判断是否携带用户名,如果没有携带,抛出SystemException异常,并把响应的枚举信息传给异常对象,然后全局异常类中的systemExceptionHandler处理器处理就会捕获到该异常,然后在这个位置去封装响应体返回

其他异常则是由exceptionHandler处理

这就是异常统一处理

十一、博客前台模块-退出登录

1. 接口分析

请求方式

请求地址

请求头

POST

/logout

需要token请求头

响应格式

{
    "code": 200,
    "msg": "操作成功"
}

2. 思路分析

获取token解析出userId

删除redis中的用户信息

3. 代码实现

第一步: 把keke-blog工程的BlogLoginController类修改为如下,新增了退出登录的接口

package com.keke.controller;


import com.keke.domain.ResponseResult;
import com.keke.domain.entity.User;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.exception.SystemException;
import com.keke.service.BlogLoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BlogLoginController {

     @Autowired
     private BlogLoginService blogLoginService;

     @PostMapping("/login")
     public ResponseResult login(@RequestBody User user){
          if(!StringUtils.hasText(user.getUserName())){
               //提示必须要传用户名
               throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);
          }
          return blogLoginService.login(user);
     }

     @PostMapping("/logout")
     public ResponseResult logout(){
          return blogLoginService.logout();
     }
}

第二步: 把keke-framework工程的BlogLoginService接口修改为如下,新增了退出登录的方法

package com.keke.service;

import com.keke.domain.ResponseResult;
import com.keke.domain.entity.User;

public interface BlogLoginService {
     ResponseResult login(User user);

     ResponseResult logout();

}

第三步: 把keke-framework工程的BlogLoginServiceImpl类修改为如下,新增了退出登录的核心代码

package com.keke.service.impl;

import com.keke.domain.ResponseResult;
import com.keke.domain.entity.LoginUser;
import com.keke.domain.entity.User;
import com.keke.domain.vo.BlogLoginUserVo;
import com.keke.domain.vo.UserInfoVo;
import com.keke.service.BlogLoginService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.JwtUtil;
import com.keke.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.Objects;


@Service
public class BlogLoginServiceImpl implements BlogLoginService {

     @Autowired
     private AuthenticationManager authenticationManager;

     @Autowired
     private RedisCache redisCache;

     @Override
     public ResponseResult login(User user) {
          UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
          Authentication authenticate = authenticationManager.authenticate(authenticationToken);
          //authenticationManager会默认调用UserDetailsService从内存中进行用户认证,我们实际需求是从数据库,因此我们要重新创建一个UserDetailsService的实现类
          //判断是否认证通过
          if(Objects.isNull(authenticate)){
               throw new RuntimeException("用户名或者密码错误");
          }
          //获取Userid,生成token
          LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
          String userId = loginUser.getUser().getId().toString();
          String jwt = JwtUtil.createJWT(userId);
          //把用户信息存入redis
          redisCache.setCacheObject("bloglogin:" + userId,loginUser);
          //把token和userInfo封装返回,因为响应回去的data有这两个属性,所以要封装Vo
          UserInfoVo userInfoVo = BeanCopyUtils.copyBean(loginUser.getUser(), UserInfoVo.class);
          BlogLoginUserVo blogLoginUserVo = new BlogLoginUserVo(jwt,userInfoVo);
          return ResponseResult.okResult(blogLoginUserVo);
     }

     @Override
     public ResponseResult logout() {
          //获取token解析获取userId
          Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
          LoginUser loginUser = (LoginUser) authentication.getPrincipal();
          Long userId = loginUser.getUser().getId();
          //删除redis中的信息(根据key删除)
          redisCache.deleteObject("bloglogin:" + userId);
          return ResponseResult.okResult();
     }
}

第四步: 把keke-blog工程的SecurityConfig类修改为如下,增加了需要有登录状态才能执行退出登录,否则就报'401 需要登录后操作'

package com.keke.config;

import com.keke.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
//WebSecurityConfigurerAdapter是Security官方提供的类
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //注入我们在keke-blog工程写的JwtAuthenticationTokenFilter过滤器
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    AccessDeniedHandler accessDeniedHandler;



    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    //把官方的PasswordEncoder密码加密方式替换成BCryptPasswordEncoder
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
//这里新增必须要是登录状态才能访问退出登录的接口,即是认证过的状态
                .antMatchers("/logout").authenticated()
                //为方便测试认证过滤器,我们把查询友链的接口设置为需要登录才能访问。然后我们去访问的时候就能测试登录认证功能了
                .antMatchers("/link/getAllLink").authenticated()
                // 除上面外的所有请求全部不需要认证即可访问
                .anyRequest().permitAll();

        //配置我们自己写的认证和授权的异常处理
        http.exceptionHandling()
                        .authenticationEntryPoint(authenticationEntryPoint)
                        .accessDeniedHandler(accessDeniedHandler);



        http.logout().disable();
        //将自定义filter加入security过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //允许跨域
        http.cors();
    }

}

4. 测试

首先测试logout是否真的实现了退出登录的效果,即删除了token在redis中的缓存,使得携带原token的请求失效

第一步,先登录 JSON格式的body可复制如下代码,登录成功

{
    "userName":"sg",
    "password":"1234"
}

博客文档续更_第5张图片

第二步,拿着登录成功的token,去访问getAllLink接口,访问成功,到这里一切正常

博客文档续更_第6张图片

第三步:携带该token向logout接口发送post请求,为什么要携带token呢,因为我们之前在SecurityConfig中配置过了,必须是已认证的状态,已认证的状态意味着必须是请求头携带token

博客文档续更_第7张图片

postman结果如下,操作成功意味着退出登录成功

博客文档续更_第8张图片

第四步:拿token再次访问getAllLink接口,发现已经不能访问

博客文档续更_第9张图片

并且我们可以看到redis中也没有缓存的信息了

博客文档续更_第10张图片

十二、博客前台模块-评论列表

1. 评论表的字段

博客文档续更_第11张图片

博客文档续更_第12张图片

2. 接口分析

请求方式

请求地址

请求头

GET

/comment/commentList

不需要token请求头(未登录也能看到评论信息)

请求格式为query格式,参数如下

articleId:文章id
pageNum:页码
pageSize:每页条数

响应格式如下

{
    "code": 200,
    "data": {
        "rows": [
            {
                "articleId": "1",
                "children": [
                    {
                        "articleId": "1",
                        "content": "评论内容(子评论)",
                        "createBy": "1",
                        "createTime": "2022-01-30 10:06:21",
                        "id": "20",
                        "rootId": "1",
                        "toCommentId": "1",
                        "toCommentUserId": "1",
                        "toCommentUserName": "这条评论(子评论)回复的是哪个人",
                        "username": "发这条评论(子评论)的人"
                    }
                ],
                "content": "评论内容(根评论)",
                "createBy": "1",
                "createTime": "2022-01-29 07:59:22",
                "id": "1",
                "rootId": "-1",
                "toCommentId": "-1",
                "toCommentUserId": "-1",
                "username": "发这条评论(根评论)的人"
            }
        ],
        "total": "15"
    },
    "msg": "操作成功"
}

3. 准备代码

第一步:实体类Comment创建在keke-framework的com.keke.domain.entity下

package com.keke.domain.entity;

import java.util.Date;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableName;

/**
 * 评论表(Comment)表实体类
 *
 * @author makejava
 * @since 2023-10-12 20:20:14
 */
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("ke_comment")
public class Comment {
    
    private Long id;
    //评论类型(0代表文章评论,1代表友链评论)
    private String type;
    //文章id
    private Long articleId;
    //根评论id
    private Long rootId;
    //评论内容
    private String content;
    //所回复的目标评论的userid
    private Long toCommentUserId;
    //回复目标评论id
    private Long toCommentId;
    
    private Long createBy;
    
    private Date createTime;
    
    private Long updateBy;
    
    private Date updateTime;
    //删除标志(0代表未删除,1代表已删除)
    private Integer delFlag;

}

第二步:创建CommentMapper

package com.keke.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.keke.domain.entity.Comment;


/**
 * 评论表(Comment)表数据库访问层
 *
 * @author makejava
 * @since 2023-10-12 20:20:41
 */
public interface CommentMapper extends BaseMapper {

}

第三步:创建CommentService

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.entity.Comment;


/**
 * 评论表(Comment)表服务接口
 *
 * @author makejava
 * @since 2023-10-12 20:20:41
 */
public interface CommentService extends IService {

}

第四步:创建CommentServiceImpl

package com.keke.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.domain.entity.Comment;
import com.keke.mapper.CommentMapper;
import com.keke.service.CommentService;
import org.springframework.stereotype.Service;

/**
 * 评论表(Comment)表服务实现类
 *
 * @author makejava
 * @since 2023-10-12 20:20:41
 */
@Service("commentService")
public class CommentServiceImpl extends ServiceImpl implements CommentService {

}

4. 代码实现-不考虑子评论

先实现查询根评论,子评论先不实现

第一步: 在keke-framework工程的domain.entity目录新建User类,写入如下

package com.keke.domain.entity;

import java.util.Date;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableName;

/**
 * 用户表(User)表实体类
 *
 * @author makejava
 * @since 2023-10-11 20:26:58
 */
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("sys_user")
public class User {
    //主键
    private Long id;
    //用户名
    private String userName;
    //昵称
    private String nickName;
    //密码
    private String password;
    //用户类型:0代表普通用户,1代表管理员
    private String type;
    //账号状态(0正常 1停用)
    private String status;
    //邮箱
    private String email;
    //手机号
    private String phonenumber;
    //用户性别(0男,1女,2未知)
    private String sex;
    //头像
    private String avatar;
    //创建人的用户id
    private Long createBy;
    //创建时间
    private Date createTime;
    //更新人
    private Long updateBy;
    //更新时间
    private Date updateTime;
    //删除标志(0代表未删除,1代表已删除)
    private Integer delFlag;

}

第二步: 在keke-framework工程的service目录新建UserService接口,写入如下

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.entity.User;


/**
 * 用户表(User)表服务接口
 *
 * @author makejava
 * @since 2023-10-13 09:08:38
 */
public interface UserService extends IService {

}

 第三步: 在keke-framework工程的service.impl目录新建UserServiceImpl类,写入如下

package com.keke.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.domain.entity.User;
import com.keke.mapper.UserMapper;
import com.keke.service.UserService;
import org.springframework.stereotype.Service;

/**
 * 用户表(User)表服务实现类
 *
 * @author makejava
 * @since 2023-10-13 10:12:51
 */
@Service("userService")
public class UserServiceImpl extends ServiceImpl implements UserService {

}

第四步: 在keke-framework工程的vo目录新建CommentVo类,写入如下

package com.keke.domain.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;


@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommentVo {

     private Long id;

     //文章id
     private Long articleId;
     //根评论id
     private Long rootId;
     //评论内容
     private String content;
     //发根评论的userid
     private Long toCommentUserId;
     //发根评论的userName
     private String toCommentUserName;
     //回复目标评论id
     private Long toCommentId;
     //当前评论的创建人id
     private Long createBy;

     private Date createTime;

     //评论是谁发的
     private String username;

}

第五步: 把keke-framework工程的SystemCanstants类修改为如下,增加了判定为根评论的常量

package com.keke.constants;


//字面值(代码中的固定值)处理,把字面值都在这里定义成常量
public class SystemConstants {

    /**
     *  文章是草稿
     */
    public static final int ARTICLE_STATUS_DRAFT = 1;
    
    /**
     *  文章是正常发布状态
     */
    public static final int ARTICLE_STATUS_NORMAL = 0;
    
    /**
     * 文章列表当前查询页数
     */
    public static final int ARTICLE_STATUS_CURRENT = 1;

    /**
     * 文章列表每页显示的数据条数
     */
    public static final int ARTICLE_STATUS_SIZE = 10;

    /**
     * 分类表的分类状态是正常状态
     */
    public static final String STATUS_NORMAL = "0";

    /**
     * 友联审核通过
     */
    public static final String Link_STATUS_NORMAL = "0";

    /**
     * 评论区的某条评论是根评论
     */
    public static final String COMMENT_ROOT = "-1";
}

第六步: 在keke-blog工程的controller目录新建CommentController类,写入如下

package com.keke.controller;

import com.keke.domain.ResponseResult;
import com.keke.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/comment")
public class CommentController {

     @Autowired
     private CommentService commentService;

     @GetMapping("/commentList")
     public ResponseResult commentList(Long articleId,Integer pageNum,Integer pageSize){
          return commentService.commentList(articleId,pageNum,pageSize);
     }
}

第七步: 在keke-framework工程的service目录新建CommentService接口,写入如下

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.Comment;


/**
 * 评论表(Comment)表服务接口
 *
 * @author makejava
 * @since 2023-10-12 20:20:41
 */
public interface CommentService extends IService {

     ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize);

}

 第八步: 在keke-framework工程的service目录新建impl.CommentServiceImpl类,写入如下

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.constants.SystemConstants;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.Article;
import com.keke.domain.entity.Comment;
import com.keke.domain.vo.CommentVo;
import com.keke.domain.vo.PageVo;
import com.keke.mapper.CommentMapper;
import com.keke.service.ArticleService;
import com.keke.service.CommentService;
import com.keke.utils.BeanCopyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 评论表(Comment)表服务实现类
 *
 * @author makejava
 * @since 2023-10-12 20:20:41
 */
@Service("commentService")
public class CommentServiceImpl extends ServiceImpl implements CommentService {

     @Autowired
     private ArticleService articleService;

     @Override
     public ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize) {
          //查询对应文章的根评论
          LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //对articleId进行判断
          lambdaQueryWrapper.eq(Comment::getArticleId,articleId);
          //查询根评论
          lambdaQueryWrapper.eq(Comment::getRootId, SystemConstants.COMMENT_ROOT);
          //分页查询
          Page page = new Page<>(pageNum,pageSize);
          page(page,lambdaQueryWrapper);
          List comments = page.getRecords();
          List commentVos = BeanCopyUtils.copyBeanList(comments, CommentVo.class);
          return ResponseResult.okResult(new PageVo(commentVos,page.getTotal()));
     }
}

测试如下

博客文档续更_第13张图片

优化

由于我们上面BeanCopy虽然copy了大量的字段,但是username(昵称)和toCommentUserName(根评论的昵称)这两个字段无法进行copy,需要我们手动进行处理

将 CommentServiceImpl修改如下

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.constants.SystemConstants;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.Article;
import com.keke.domain.entity.Comment;
import com.keke.domain.vo.CommentVo;
import com.keke.domain.vo.PageVo;
import com.keke.mapper.CommentMapper;
import com.keke.service.ArticleService;
import com.keke.service.CommentService;
import com.keke.service.UserService;
import com.keke.utils.BeanCopyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

/**
 * 评论表(Comment)表服务实现类
 *
 * @author makejava
 * @since 2023-10-12 20:20:41
 */
@Service("commentService")
public class CommentServiceImpl extends ServiceImpl implements CommentService {


     @Autowired
     private UserService userService;

     @Override
     public ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize) {
          //查询对应文章的根评论
          LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //对articleId进行判断
          lambdaQueryWrapper.eq(Comment::getArticleId,articleId);
          //查询根评论
          lambdaQueryWrapper.eq(Comment::getRootId, SystemConstants.COMMENT_ROOT);
          //分页查询
          Page page = new Page<>(pageNum,pageSize);
          page(page,lambdaQueryWrapper);
          List comments = page.getRecords();
          List commentVos = toCommentVoList(comments);
          return ResponseResult.okResult(new PageVo(commentVos,page.getTotal()));
     }

     private List toCommentVoList(List comments){
          List commentVos = BeanCopyUtils.copyBeanList(comments, CommentVo.class);

          for (CommentVo commentVo : commentVos) {
               //首先获取Comment的创始人id,通过create_by字段,然后获取到创始人,再获取到昵称
               String nickName = userService.getById(commentVo.getCreateBy()).getNickName();
               commentVo.setUsername(nickName);
               //这里要加判断,如果to_comment_user_id不为-1才表示这个评论是有根评论的,才可以查
               if(commentVo.getToCommentId()!=-1) {
                    //根据to_comment_user_id获取根评论创始人的userid,然后查到其昵称
                    String toCommentUserName = userService.getById(commentVo.getToCommentUserId()).getNickName();
                    commentVo.setToCommentUserName(toCommentUserName);
               }
          }
          return commentVos;
     }
}

测试通过

博客文档续更_第14张图片

5. 实现-考虑子评论

查询子评论

第一步: 把CommentVo类修改为如下,增加了子评论字段

package com.keke.domain.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;
import java.util.List;


@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommentVo {
     private Long id;
     //文章id
     private Long articleId;
     //根评论id
     private Long rootId;
     //评论内容
     private String content;
     //发根评论的userid
     private Long toCommentUserId;
     //发根评论的userName
     private String toCommentUserName;
     //回复目标评论id
     private Long toCommentId;
     //当前评论的创建人id
     private Long createBy;
     private Date createTime;
     //评论是谁发的,注意这里是昵称
     private String username;
     //子评论们
     private List children;
}

第二步: 把CommentServiceImpl修改为如下,增加了查询子评论、子评论按照时间排序

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.constants.SystemConstants;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.Article;
import com.keke.domain.entity.Comment;
import com.keke.domain.vo.CommentVo;
import com.keke.domain.vo.PageVo;
import com.keke.mapper.CommentMapper;
import com.keke.service.ArticleService;
import com.keke.service.CommentService;
import com.keke.service.UserService;
import com.keke.utils.BeanCopyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

/**
 * 评论表(Comment)表服务实现类
 *
 * @author makejava
 * @since 2023-10-12 20:20:41
 */
@Service("commentService")
public class CommentServiceImpl extends ServiceImpl implements CommentService {


     @Autowired
     private UserService userService;

     @Override
     public ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize) {
          //查询对应文章的根评论
          LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //对articleId进行判断
          lambdaQueryWrapper.eq(Comment::getArticleId,articleId);
          //查询根评论
          lambdaQueryWrapper.eq(Comment::getRootId, SystemConstants.COMMENT_ROOT);
          //分页查询
          Page page = new Page<>(pageNum,pageSize);
          page(page,lambdaQueryWrapper);
          List comments = page.getRecords();



          List commentVos = toCommentVoList(comments);
          //查询所有根评论对应的子评论集合,并且赋值给对应的属性
          for (CommentVo commentVo : commentVos) {
               //查询对应的子评论
               List children = getChildren(commentVo.getId());
               //赋值
               commentVo.setChildren(children);
          }
          return ResponseResult.okResult(new PageVo(commentVos,page.getTotal()));
     }

     private List toCommentVoList(List comments){
          List commentVos = BeanCopyUtils.copyBeanList(comments, CommentVo.class);

          for (CommentVo commentVo : commentVos) {
               //首先获取Comment的创始人id,通过create_by字段,然后获取到创始人,再获取到昵称
               String nickName = userService.getById(commentVo.getCreateBy()).getNickName();
               commentVo.setUsername(nickName);
               //这里要加判断,如果to_comment_user_id不为-1才表示这个评论是有根评论的,才可以查
               if(commentVo.getToCommentId()!=-1) {
                    //根据to_comment_user_id获取根评论创始人的userid,然后查到其昵称
                    String toCommentUserName = userService.getById(commentVo.getToCommentUserId()).getNickName();
                    commentVo.setToCommentUserName(toCommentUserName);
               }
          }
          return commentVos;
     }

     /**
      * 根据根评论id查询所对应的子评论的集合
      * @param id
      * @return
      */
     private List getChildren(Long id) {
          LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //传过来的id,然后查询所有评论表中,根评论是该id的评论
          lambdaQueryWrapper.eq(Comment::getRootId,id);
          //对评论的时间进行升序排序,发表的早的显示在前面,符合逻辑
          lambdaQueryWrapper.orderByAsc(Comment::getCreateTime);
          List comments = list(lambdaQueryWrapper);
          //调用称我们之前封装好的方法
          List children = toCommentVoList(comments);
          //返回
          return children;
     }
}

 第三步:测试

postman中博客文档续更_第15张图片

前后端联通中,效果符合预期

博客文档续更_第16张图片

十三、博客前台模块-发文章评论

1. 接口分析

用户登录后可以对文章发表评论,也可以对已有的评论进行回复

请求方式

请求地址

请求头

POST

/comment

需要token头

请求体:

回复了文章,type为0表示文章评论

{    
    "articleId":1,
    "type":0,
    "rootId":-1,
    "toCommentId":-1,
    "toCommentUserId":-1,
    "content":"评论了文章"
}

回复了文章的评论,type为0表示文章评论

{    
    "articleId":1,
    "type":0,
    "rootId":-1,
    "toCommentId":-1,
    "toCommentUserId":-1,
    "content":"回复了文章的某条评论"
}

响应体:

{
	"code":200,
	"msg":"操作成功"
}

2. 代码实现

第一步: 在keke-framework工程的utils目录新建SecurityUtils类,写入如下

package com.keke.utils;

import com.keke.domain.entity.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;


//在'发送评论'功能那里会用到的工具类
public class SecurityUtils {

    /**
     * 获取用户的userid
     **/
    public static LoginUser getLoginUser() {
        return (LoginUser) getAuthentication().getPrincipal();
    }

    /**
     * 获取Authentication
     */
    public static Authentication getAuthentication() {
        return SecurityContextHolder.getContext().getAuthentication();
    }

    /**
     * 指定userid为1的用户就是网站管理员
     * @return
     */
    public static Boolean isAdmin(){
        Long id = getLoginUser().getUser().getId();
        return id != null && 1L == id;
    }

    public static Long getUserId() {
        return getLoginUser().getUser().getId();
    }
}

这个类创建的原因是在添加评论的时候,comment字段里面的创建人和创建时间,更新人更新时间无法赋值过来,而创建人userid前端传过来肯定不安全,所以我们只能从token中解析出来,然后进行赋值操作

这一步可以封装成一个工具类,因为我们之后肯定要经常进行从token中解析出userid来获取用户信息的操作

第二步: 在keke-framework工程的handler目录新建mybatisplus.MyMetaObjectHandler类,写入如下

package com.keke.handler.mybatisplus;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.keke.utils.SecurityUtils;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.util.Date;


@Component
//这个类是用来配置mybatis的字段自动填充。用于'发送评论'功能,由于我们在评论表无法对下面这四个字段进行插入数据(原因是前端在发送评论时,没有在
//请求体提供下面四个参数,所以后端在往数据库插入数据时,下面四个字段是空值),所有就需要这个类来帮助我们往下面这四个字段自动的插入值,
//只要我们更新了评论表的字段,那么无法插入值的字段就自动有值了
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    //只要对数据库执行了插入语句,那么就会执行到这个方法
    public void insertFill(MetaObject metaObject) {
        Long userId = null;
        try {
            //获取用户id
            userId = SecurityUtils.getUserId();
        } catch (Exception e) {
            e.printStackTrace();
            userId = -1L;//如果异常了,就说明该用户还没注册,我们就把该用户的userid字段赋值d为-1
        }
        //自动把下面四个字段新增了值。
        this.setFieldValByName("createTime", new Date(), metaObject);
        this.setFieldValByName("createBy",userId , metaObject);
        this.setFieldValByName("updateTime", new Date(), metaObject);
        this.setFieldValByName("updateBy", userId, metaObject);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        this.setFieldValByName("updateTime", new Date(), metaObject);
        this.setFieldValByName(" ", SecurityUtils.getUserId(), metaObject);
    }
}

mp在进行save操作时候会插入数据库信息,而创建人和创建时间,更新人和更新时间这些字段我们不愿意每次都自己进行手动赋值,我们这里配置mp的处理器,自动帮我们封装这个操作

第三步: 把keke-framework工程的Comment类修改为如下,增加了具体的自动填充规则

package com.keke.domain.entity;

import java.util.Date;
import java.io.Serializable;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableName;

/**
 * 评论表(Comment)表实体类
 *
 * @author makejava
 * @since 2023-10-12 20:20:14
 */
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("ke_comment")
public class Comment {
    
    private Long id;
    //评论类型(0代表文章评论,1代表友链评论)
    private String type;
    //文章id
    private Long articleId;
    //根评论id
    private Long rootId;
    //评论内容
    private String content;
    //所回复的目标评论的userid
    private Long toCommentUserId;
    //回复目标评论id
    private Long toCommentId;

    //由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充
    //所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值
    @TableField(fill = FieldFill.INSERT)
    private Long createBy;

    //由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充
    //所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;

    //由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充
    //所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值
    @TableField(fill = FieldFill.INSERT)
    private Long updateBy;

    //由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充
    //所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值
    @TableField(fill = FieldFill.INSERT)
    private Date updateTime;
    //删除标志(0代表未删除,1代表已删除)
    private Integer delFlag;

}

第四步: 把keke-framework工程的AppHttpCodeEnum类修改为如下,增加了一个枚举变量来限制用户要发送的评论内容不能为空

package com.keke.enums;


public enum AppHttpCodeEnum {
    // 成功
    SUCCESS(200,"操作成功"),
    // 登录
    NEED_LOGIN(401,"需要登录后操作"),
    NO_OPERATOR_AUTH(403,"无权限操作"),
    SYSTEM_ERROR(500,"出现错误"),
    USERNAME_EXIST(501,"用户名已存在"),
    PHONENUMBER_EXIST(502,"手机号已存在"), EMAIL_EXIST(503, "邮箱已存在"),
    REQUIRE_USERNAME(504, "必需填写用户名"),
    LOGIN_ERROR(505,"用户名或密码错误"),
    
    CONTENT_NOT_NULL(506, "发送的评论内容不能为空");
    int code;
    String msg;

    AppHttpCodeEnum(int code, String errorMessage){
        this.code = code;
        this.msg = errorMessage;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

第五步: 把keke-blog工程的CommentController修改为如下,增加了发送评论的请求路径 

package com.keke.controller;

import com.keke.domain.ResponseResult;
import com.keke.domain.dto.CommentDto;
import com.keke.domain.entity.Comment;
import com.keke.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/comment")
public class CommentController {

     @Autowired
     private CommentService commentService;

     //展示评论列表
     @GetMapping("/commentList")
     public ResponseResult commentList(Long articleId,Integer pageNum,Integer pageSize){
          return commentService.commentList(articleId,pageNum,pageSize);
     }

     //发表评论
     @PostMapping
     //标准点的话,这里应该用一个dto去接受,dto是后端接受前端传送的Json所封装称的对象
     public ResponseResult addComment(@RequestBody CommentDto commentDto){
          return commentService.addComment(commentDto);
     }
}

 第六步:在keke-framework中新增domain/dto/Commentdto

package com.keke.domain.dto;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import java.util.Date;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommentDto {
     //评论类型(0代表文章评论,1代表友链评论)
     private String type;
     //文章id
     private Long articleId;
     //根评论id
     private Long rootId;
     //评论内容
     private String content;
     //所回复的目标评论的userid
     private Long toCommentUserId;
     //回复目标评论id
     private Long toCommentId;
}

第七步:把keke-framework工程的CommentService接口修改为如下,增加了发送评论的接口

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.CommentDto;
import com.keke.domain.entity.Comment;


/**
 * 评论表(Comment)表服务接口
 *
 * @author makejava
 * @since 2023-10-12 20:20:41
 */
public interface CommentService extends IService {

     ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize);

     ResponseResult addComment(CommentDto commentDto);
}

第八步: 把keke-framework工程的CommentServiceImpl类,增加了发送评论的代码实现

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.constants.SystemConstants;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.CommentDto;
import com.keke.domain.entity.Article;
import com.keke.domain.entity.Comment;
import com.keke.domain.vo.CommentVo;
import com.keke.domain.vo.PageVo;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.exception.SystemException;
import com.keke.mapper.CommentMapper;
import com.keke.service.ArticleService;
import com.keke.service.CommentService;
import com.keke.service.UserService;
import com.keke.utils.BeanCopyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.List;

/**
 * 评论表(Comment)表服务实现类
 *
 * @author makejava
 * @since 2023-10-12 20:20:41
 */
@Service("commentService")
public class CommentServiceImpl extends ServiceImpl implements CommentService {


     @Autowired
     private UserService userService;

     @Override
     public ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize) {
          //查询对应文章的根评论
          LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //对articleId进行判断
          lambdaQueryWrapper.eq(Comment::getArticleId,articleId);
          //查询根评论
          lambdaQueryWrapper.eq(Comment::getRootId, SystemConstants.COMMENT_ROOT);
          //分页查询
          Page page = new Page<>(pageNum,pageSize);
          page(page,lambdaQueryWrapper);
          List comments = page.getRecords();



          List commentVos = toCommentVoList(comments);
          //查询所有根评论对应的子评论集合,并且赋值给对应的属性
          for (CommentVo commentVo : commentVos) {
               //查询对应的子评论
               List children = getChildren(commentVo.getId());
               //赋值
               commentVo.setChildren(children);
          }
          return ResponseResult.okResult(new PageVo(commentVos,page.getTotal()));
     }

     @Override
     public ResponseResult addComment(CommentDto commentDto) {
          //注意前端在调用这个发送评论接口时,在请求体是没有向我们传入createTime、createId、updateTime、updateID字段,所以
          //我们这里往后端插入数据时,就会导致上面那行的四个字段没有值
          //为了解决这个问题,我们在keke-framework工程新增了MyMetaObjectHandler类、修改了Comment类。详细可自己定位去看一下代码

          //限制发送评论不能为空
          if(!StringUtils.hasText(commentDto.getContent())){
               throw new SystemException(AppHttpCodeEnum.CONTENT_NOT_NULL);
          }

          //解决了四个字段没有值的情况,就可以直接调用mybatisplus提供的save方法往数据库插入数据(用户发送的评论的各个字段)了
          Comment comment = BeanCopyUtils.copyBean(commentDto, Comment.class);
          save(comment);
          //封装返回
          return ResponseResult.okResult();
     }

     private List toCommentVoList(List comments){
          List commentVos = BeanCopyUtils.copyBeanList(comments, CommentVo.class);

          for (CommentVo commentVo : commentVos) {
               //首先获取Comment的创始人id,通过create_by字段,然后获取到创始人,再获取到昵称
               String nickName = userService.getById(commentVo.getCreateBy()).getNickName();
               commentVo.setUsername(nickName);
               //这里要加判断,如果to_comment_user_id不为-1才表示这个评论是有根评论的,才可以查
               if(commentVo.getToCommentId()!=-1) {
                    //根据to_comment_user_id获取根评论创始人的userid,然后查到其昵称
                    String toCommentUserName = userService.getById(commentVo.getToCommentUserId()).getNickName();
                    commentVo.setToCommentUserName(toCommentUserName);
               }
          }
          return commentVos;
     }

     /**
      * 根据根评论id查询所对应的子评论的集合
      * @param id
      * @return
      */
     private List getChildren(Long id) {
          LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //传过来的id,然后查询所有评论表中,根评论是该id的评论
          lambdaQueryWrapper.eq(Comment::getRootId,id);
          //对评论的时间进行升序排序,发表的早的显示在前面,符合逻辑
          lambdaQueryWrapper.orderByAsc(Comment::getCreateTime);
          List comments = list(lambdaQueryWrapper);
          //调用称我们之前封装好的方法
          List children = toCommentVoList(comments);
          //返回
          return children;
     }
}

第九步:测试

博客文档续更_第17张图片

博客文档续更_第18张图片

博客文档续更_第19张图片

博客文档续更_第20张图片

博客文档续更_第21张图片

博客文档续更_第22张图片

可以看到,评论和回复都响应正常数据库也新增成功

十四、友联评论列表 + 评论优化

1. 接口分析

用户登录后可以对友链发表评论,也可以对已有的评论进行回复

请求方式

请求地址

请求头

GET

/comment/linkCommentList

不需要token请求头

Query格式请求参数:

pageNum: 页码
pageSize: 每页条数

响应格式

{
    "code": 200,
    "data": {
        "rows": [
            {
                "articleId": "1",
                "children": [
                    {
                        "articleId": "1",
                        "content": "回复友链评论3",
                        "createBy": "1",
                        "createTime": "2022-01-30 10:08:50",
                        "id": "23",
                        "rootId": "22",
                        "toCommentId": "22",
                        "toCommentUserId": "1",
                        "toCommentUserName": "sg333",
                        "username": "sg333"
                    }
                ],
                "content": "友链评论2",
                "createBy": "1",
                "createTime": "2022-01-30 10:08:28",
                "id": "22",
                "rootId": "-1",
                "toCommentId": "-1",
                "toCommentUserId": "-1",
                "username": "sg333"
            }
        ],
        "total": "1"
    },
    "msg": "操作成功"
}

2. 代码实现

第一步: 把keke-framework工程的SystemCanstants类修改为如下,增加了区分文章、友链的评论类型
 

package com.keke.constants;


//字面值(代码中的固定值)处理,把字面值都在这里定义成常量
public class SystemConstants {

    /**
     *  文章是草稿
     */
    public static final int ARTICLE_STATUS_DRAFT = 1;
    
    /**
     *  文章是正常发布状态
     */
    public static final int ARTICLE_STATUS_NORMAL = 0;
    
    /**
     * 文章列表当前查询页数
     */
    public static final int ARTICLE_STATUS_CURRENT = 1;

    /**
     * 文章列表每页显示的数据条数
     */
    public static final int ARTICLE_STATUS_SIZE = 10;

    /**
     * 分类表的分类状态是正常状态
     */
    public static final String STATUS_NORMAL = "0";

    /**
     * 友联审核通过
     */
    public static final String Link_STATUS_NORMAL = "0";

    /**
     * 评论区的某条评论是根评论
     */
    public static final String COMMENT_ROOT = "-1";

    /**
     * 文章评论
     */
    public static final String ARTICLE_COMMENT = "0";

    /**
     * 文章评论
     */
    public static final String LINK_COMMENT = "1";
}

第二步: 把keke-framework工程的CommentService接口修改为如下,为CommentList方法增加了commentType参数类型 

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.CommentDto;
import com.keke.domain.entity.Comment;


/**
 * 评论表(Comment)表服务接口
 *
 * @author makejava
 * @since 2023-10-12 20:20:41
 */
public interface CommentService extends IService {

     ResponseResult commentList(String commentType,Long articleId, Integer pageNum, Integer pageSize);

     ResponseResult addComment(CommentDto commentDto);
}

第三步: 把keke-framework工程的CommentServiceImpl类修改为如下,稍微修改了commentList方法

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.constants.SystemConstants;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.CommentDto;
import com.keke.domain.entity.Article;
import com.keke.domain.entity.Comment;
import com.keke.domain.vo.CommentVo;
import com.keke.domain.vo.PageVo;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.exception.SystemException;
import com.keke.mapper.CommentMapper;
import com.keke.service.ArticleService;
import com.keke.service.CommentService;
import com.keke.service.UserService;
import com.keke.utils.BeanCopyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.List;

/**
 * 评论表(Comment)表服务实现类
 *
 * @author makejava
 * @since 2023-10-12 20:20:41
 */
@Service("commentService")
public class CommentServiceImpl extends ServiceImpl implements CommentService {


     @Autowired
     private UserService userService;

     @Override
     public ResponseResult commentList(String commentType,Long articleId, Integer pageNum, Integer pageSize) {
          //查询对应文章的根评论
          LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //对articleId进行判断
          //这里要新增一条对评论类型的判断,如果是0的话才能进行文章评论的封装
          lambdaQueryWrapper.eq(SystemConstants.ARTICLE_COMMENT.equals(commentType),Comment::getArticleId,articleId);
          //查询根评论
          lambdaQueryWrapper.eq(Comment::getRootId, SystemConstants.COMMENT_ROOT);
          //评论类型进行判断
          lambdaQueryWrapper.eq(Comment::getType,commentType);
          //分页查询
          Page page = new Page<>(pageNum,pageSize);
          page(page,lambdaQueryWrapper);
          List comments = page.getRecords();


          List commentVos = toCommentVoList(comments);
          //查询所有根评论对应的子评论集合,并且赋值给对应的属性
          for (CommentVo commentVo : commentVos) {
               //查询对应的子评论
               List children = getChildren(commentVo.getId());
               //赋值
               commentVo.setChildren(children);
          }
          return ResponseResult.okResult(new PageVo(commentVos,page.getTotal()));
     }

     @Override
     public ResponseResult addComment(CommentDto commentDto) {
          //注意前端在调用这个发送评论接口时,在请求体是没有向我们传入createTime、createId、updateTime、updateID字段,所以
          //我们这里往后端插入数据时,就会导致上面那行的四个字段没有值
          //为了解决这个问题,我们在keke-framework工程新增了MyMetaObjectHandler类、修改了Comment类。详细可自己定位去看一下代码

          //限制发送评论不能为空
          if(!StringUtils.hasText(commentDto.getContent())){
               throw new SystemException(AppHttpCodeEnum.CONTENT_NOT_NULL);
          }

          //解决了四个字段没有值的情况,就可以直接调用mybatisplus提供的save方法往数据库插入数据(用户发送的评论的各个字段)了
          Comment comment = BeanCopyUtils.copyBean(commentDto, Comment.class);
          save(comment);
          //封装返回
          return ResponseResult.okResult();
     }

     private List toCommentVoList(List comments){
          List commentVos = BeanCopyUtils.copyBeanList(comments, CommentVo.class);

          for (CommentVo commentVo : commentVos) {
               //首先获取Comment的创始人id,通过create_by字段,然后获取到创始人,再获取到昵称
               String nickName = userService.getById(commentVo.getCreateBy()).getNickName();
               commentVo.setUsername(nickName);
               //这里要加判断,如果to_comment_user_id不为-1才表示这个评论是有根评论的,才可以查
               if(commentVo.getToCommentId()!=-1) {
                    //根据to_comment_user_id获取根评论创始人的userid,然后查到其昵称
                    String toCommentUserName = userService.getById(commentVo.getToCommentUserId()).getNickName();
                    commentVo.setToCommentUserName(toCommentUserName);
               }
          }
          return commentVos;
     }

     /**
      * 根据根评论id查询所对应的子评论的集合
      * @param id
      * @return
      */
     private List getChildren(Long id) {
          LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //传过来的id,然后查询所有评论表中,根评论是该id的评论
          lambdaQueryWrapper.eq(Comment::getRootId,id);
          //对评论的时间进行升序排序,发表的早的显示在前面,符合逻辑
          lambdaQueryWrapper.orderByAsc(Comment::getCreateTime);
          List comments = list(lambdaQueryWrapper);
          //调用称我们之前封装好的方法
          List children = toCommentVoList(comments);
          //返回
          return children;
     }
}

 第四步: 把keke-blog工程的CommentController类修改为如下,增加了linkCommentList方法

package com.keke.controller;

import com.keke.constants.SystemConstants;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.CommentDto;
import com.keke.domain.entity.Comment;
import com.keke.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/comment")
public class CommentController {

     @Autowired
     private CommentService commentService;

     //展示评论列表
     @GetMapping("/commentList")
     public ResponseResult commentList(Long articleId,Integer pageNum,Integer pageSize){
          //调用文章评论的接口,但增加传入文章类型的判断,从而实现友链和文章评论的列表分页查询
          return commentService.commentList(SystemConstants.ARTICLE_COMMENT,articleId,pageNum,pageSize);
     }

     //发表评论
     @PostMapping
     //标准点的话,这里应该用一个dto去接受,dto是后端接受前端传送的Json所封装称的对象
     public ResponseResult addComment(@RequestBody CommentDto commentDto){
          return commentService.addComment(commentDto);
     }

     //友链评论
     @GetMapping("/linkCommentList")
     public ResponseResult linkCommentList(Integer pageNum,Integer pageSize){
          //调用文章评论的接口,但增加传入文章类型的判断,从而实现友链和文章评论的列表分页查询
          return commentService.commentList(SystemConstants.LINK_COMMENT,null,pageNum,pageSize);
     }


}

 3. 测试

前后端联调

博客文档续更_第23张图片

4. 友链页面的登录bug

每次点击打开友链页面都会弹出重新登录的弹窗,原因是友链接口被后端拦截了,前端是不携带token来访问友链接口,原本沟通好是不需要认证就可以访问友链接口的,而后端却写着要认证

 

package com.keke.config;

import com.keke.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
//WebSecurityConfigurerAdapter是Security官方提供的类
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //注入我们在keke-blog工程写的JwtAuthenticationTokenFilter过滤器
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    AccessDeniedHandler accessDeniedHandler;



    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    //把官方的PasswordEncoder密码加密方式替换成BCryptPasswordEncoder
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
//这里新增必须要是登录状态才能访问退出登录的接口,即是认证过的状态
                .antMatchers("/logout").authenticated()
                //为方便测试认证过滤器,我们把查询友链的接口设置为需要登录才能访问。然后我们去访问的时候就能测试登录认证功能了
//                .antMatchers("/link/getAllLink").authenticated()
                // 除上面外的所有请求全部不需要认证即可访问
                .anyRequest().permitAll();

        //配置我们自己写的认证和授权的异常处理
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);



        http.logout().disable();
        //将自定义filter加入security过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //允许跨域
        http.cors();
    }

}

你可能感兴趣的:(KekeBlog,java,spring,boot,intellij-idea,spring,maven)