目前我们的项目在认证出错或者权限不足的时候响应回来的Json,默认是使用Security官方提供的响应的格式,但是这种响应的格式肯定是不符合我们项目的接口规范的。所以需要自定义异常处理
我们需要去实现AuthenticationEntryPoint(官方提供的认证失败处理器)类、AccessDeniedHandler(官方提供的授权失败处理器)类,然后配置给Security
由于我们前台和后台的异常处理是一样的,所以我们在framework包下创建异常处理类
在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));
}
}
在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));
}
}
把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();
}
}
第一步:打开redis,启动工程
第二步:向login接口发送用户名或者密码错误的post请求
第三步:向link/getAllLink接口发送不携带token的get请求
实际我们在开发过程中可能需要做很多的判断校验,如果出现了非法情况我们是期望响应对应的提示的。但是如果我们每次都自己手动去处理就会非常麻烦。我们可以选择直接抛出异常的方式,然后对异常进行统一处理。把异常中的信息封装成ResponseResult响应给前端
在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();
}
}
在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
}
}
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);
}
}
向login接口发送一个没有用户名只有密码的post,响应如下
可以看到,响应回来的信息是正确的
首相前端发送请求,controller层先判断是否携带用户名,如果没有携带,抛出SystemException异常,并把响应的枚举信息传给异常对象,然后全局异常类中的systemExceptionHandler处理器处理就会捕获到该异常,然后在这个位置去封装响应体返回
其他异常则是由exceptionHandler处理
这就是异常统一处理
请求方式 |
请求地址 |
请求头 |
POST |
/logout |
需要token请求头 |
响应格式
{
"code": 200,
"msg": "操作成功"
}
获取token解析出userId
删除redis中的用户信息
第一步: 把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();
}
}
首先测试logout是否真的实现了退出登录的效果,即删除了token在redis中的缓存,使得携带原token的请求失效
第一步,先登录 JSON格式的body可复制如下代码,登录成功
{
"userName":"sg",
"password":"1234"
}
第二步,拿着登录成功的token,去访问getAllLink接口,访问成功,到这里一切正常
第三步:携带该token向logout接口发送post请求,为什么要携带token呢,因为我们之前在SecurityConfig中配置过了,必须是已认证的状态,已认证的状态意味着必须是请求头携带token
postman结果如下,操作成功意味着退出登录成功
第四步:拿token再次访问getAllLink接口,发现已经不能访问
并且我们可以看到redis中也没有缓存的信息了
请求方式 |
请求地址 |
请求头 |
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": "操作成功"
}
第一步:实体类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 {
}
先实现查询根评论,子评论先不实现
第一步: 在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()));
}
}
测试如下
由于我们上面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;
}
}
测试通过
查询子评论
第一步: 把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;
}
}
第三步:测试
前后端联通中,效果符合预期
用户登录后可以对文章发表评论,也可以对已有的评论进行回复
请求方式 |
请求地址 |
请求头 |
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":"操作成功"
}
第一步: 在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;
}
}
第九步:测试
可以看到,评论和回复都响应正常数据库也新增成功
用户登录后可以对友链发表评论,也可以对已有的评论进行回复
请求方式 |
请求地址 |
请求头 |
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": "操作成功"
}
第一步: 把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);
}
}
前后端联调
每次点击打开友链页面都会弹出重新登录的弹窗,原因是友链接口被后端拦截了,前端是不携带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();
}
}