博客项目(前台接口实现)
文章目录
1.前置知识
1.1Controller
-
在controller层需要调用到service层的代码,做相关业务的处理。
-
需要统一返回相同的响应格式,不管在前端还是后端,我们在项目中需要统一返回响应的格式。
-
我们可以定义一个统一返回响应格式的类,这个类前后台都可以用得到,我们定义在公共模块中
1.1.1ResponseResult类
统一响应类,响应给前端。
package com.jierlung.domain;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.jierlung.enums.AppHttpCodeEnum;
import java.io.Serializable;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> implements Serializable {
private Integer code;
private String msg;
private T data;
public ResponseResult() {
this.code = AppHttpCodeEnum.SUCCESS.getCode();
this.msg = AppHttpCodeEnum.SUCCESS.getMsg();
}
public ResponseResult(Integer code, T data) {
this.code = code;
this.data = data;
}
public ResponseResult(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public ResponseResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public static ResponseResult errorResult(int code, String msg) {
ResponseResult result = new ResponseResult();
return result.error(code, msg);
}
public static ResponseResult okResult() {
ResponseResult result = new ResponseResult();
return result;
}
public static ResponseResult okResult(int code, String msg) {
ResponseResult result = new ResponseResult();
return result.ok(code, null, msg);
}
public static ResponseResult okResult(Object data) {
ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getMsg());
if(data!=null) {
result.setData(data);
}
return result;
}
public static ResponseResult errorResult(AppHttpCodeEnum enums){
return setAppHttpCodeEnum(enums,enums.getMsg());
}
public static ResponseResult errorResult(AppHttpCodeEnum enums, String msg){
return setAppHttpCodeEnum(enums,msg);
}
public static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums){
return okResult(enums.getCode(),enums.getMsg());
}
private static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums, String msg){
return okResult(enums.getCode(),msg);
}
public ResponseResult<?> error(Integer code, String msg) {
this.code = code;
this.msg = msg;
return this;
}
public ResponseResult<?> ok(Integer code, T data) {
this.code = code;
this.data = data;
return this;
}
public ResponseResult<?> ok(Integer code, T data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
return this;
}
public ResponseResult<?> ok(T data) {
this.data = data;
return this;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
1.1.2该类的方法
1.2Service
我们在Service接口中去继承IService方法
1.3ServiceImpl
在ServiceImpl写具体的业务逻辑。
1.4Mapper
我们在Mapper接口中去继承Mybatis-plus的BaseMapper的里面的方法。
1.5Vo的理解
java开发规范手册解释
其实Vo就是返回给前端展示的数据,比如说我现在表里面查询出来10个字段,而需要前端展示的字段只有5个,难道全部返回给前端吗,显然不合理。
我们需要定义一个Vo对应表的实体类,类中就是展示给前端的字段数据。
例如:
实体类Article(文章表)
package com.jierlung.domain.entity;
import java.util.Date;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* 文章表(Article)表实体类
*
* @author makejava
* @since 2022-10-10 15:27:26
*/
@SuppressWarnings("serial")
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sg_article")
@Accessors(chain = true)
public class Article {
@TableId
private Long id;
//标题
private String title;
@TableField(exist = false)
//分类的标题
private String categoryName;
//文章内容
private String content;
//文章摘要
private String summary;
//所属分类id
private Long categoryId;
//缩略图
private String thumbnail;
//是否置顶(0否,1是)
private String isTop;
//状态(0已发布,1草稿)
private String status;
//访问量
private Long viewCount;
//是否允许评论 1是,0否
private String isComment;
private Long createBy;
private Date createTime;
private Long updateBy;
private Date updateTime;
//删除标志(0代表未删除,1代表已删除)
private Integer delFlag;
}
而根据展示的内容我们可以去掉不必要的字段创建对应的Vo:articleListVo
package com.jierlung.domain.vo;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class articleListVo {
private Long id;
//标题
private String title;
//文章摘要
private String summary;
//所属分类id
private String categoryName;
//缩略图
private String thumbnail;
//访问量
private Long viewCount;
private Date createTime;
}
然后可以通过Bean拷贝的形式对Vo进行赋值,比如我查询出来一个List集合的article,然后通过拷贝的形式,去给Vo赋值,然后将Vo对象返回给前端。
后面的接口实现中,不在重复对Vo的编写,读者可以根据返还给前端的数据展示相应的字段,编写Vo
1.6可能会用到的相关插件
可以根据对应表自动生成对应的实体类和mappe、Service、ServiceImpl、Controller。
1.7设置字面量
可以设置一个类,表示字面量,比如说文章的状态是0,查询的条件如果直接写0,不太方便理解,写字面量的形式更加方便。
1.8后端接口测试工具
ApiPost7或者postman都可以,操作过程差不多。
例如:查询热门文章列表
2.热门文章接口分析
2.1热门文章接口位置
热门文章接口肯定是前台展示的数据,应该在blog的前台模块中
2.2接口的路径
前端代码的接口路径:
后端:
@RestController
@RequestMapping("/article")
public class ArticleController {
@Autowired
ArticleService articleService;
@GetMapping("/articleList")
public ResponseResult articleList(Integer pageNum,Integer pageSize,Long categoryId){
return articleService.articleList(pageNum,pageSize,categoryId);
}
}
2.3前端展示效果
2.4接口响应的格式
可以看到需要返回一个code和msg,和一个data
2.5需求以及分析数据表
需求:
- 查询的必须是正式发布的文章
- 查询前十条数据
- 根据访问量从大到小排序
- 展示文章的标题和浏览量
- 不能把已删除的查询出来
文章表
分析:
- 可以根据文章表的字段,查询文章的状态,0表示已发布
- 查询前十条数据,需要查询到的文章分页展示,前10条
- 展示文章的标题和浏览量,可以将实体类封装对应的Vo返回给前端
- 删除标志,这里的删除是逻辑删除(也就是说删除了,其实底层没有删除,查询的时候看逻辑删除字段的状态查询数据)
2.6代码实现
public ResponseResult hotArticleList() {
//查询热门文章,封装成ResponseResult返回
LambdaQueryWrapper<Article> queryWrapper=new LambdaQueryWrapper<Article>();
//必须是正式文章
queryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);
//按照浏览量进行排序
queryWrapper.orderByDesc(Article::getViewCount);
//最多只查询10条
Page<Article> page = new Page<>(1, 10);
page(page, queryWrapper);
List<Article> articles = page.getRecords();
//bean拷贝
ArrayList<HotArticleVo> articleVos = new ArrayList<>();
for (Article article:articles){
HotArticleVo vo = new HotArticleVo();
BeanUtils.copyProperties(article,vo);
articleVos.add(vo);
}
return ResponseResult.okResult(articleVos);
}
步骤解析:
第一步:构建article的查询对象,封装一系列的查询条件,例如:降序,必须是正式文章…
第二步:构建分页查询对象,只展示前十条数据
第三步:得到查询后并且经过条件过滤后的的分页对象,将其转化为list集合
第四步:封装vo(因为返回给前端的数据没有那么多,其他的数据没必要返回)
因为:
前端只需要这些数据,我们可以定义vo的方式去对应返回给前端的字段,定义一定要从本来实体类出发,简而言之就是去掉不需要的字段。
第五步:通过bean拷贝的方式去从article实体类去拷贝到articleVo实体类上,最后面统一封装返回。
第六步测试:
总结一下:首先看对应的涉及到的表,例如这是对应这article表,看需要什么字段,你就给我返回什么字段,在编写相应的逻辑就可以了。
3.文章分类接口分析
3.1文章接口位置
文章分类的接口肯定是前台展示的数据,应该在blog的前台模块中
3.2接口路径
前端返回的接口路径
后端:
@RestController
@RequestMapping("/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@GetMapping("/getCategoryList")
public ResponseResult getCategoryList(){
return categoryService.getCategoryList();
}
}
3.3文章接口展示效果
3.4接口响应的格式
3.5需求以及分析数据表
需求:
- 必须是正式的文章的分类
- 必须是正常状态的文章
数据表:
分析:
- 可以看到前端需要返回分类名和对应的分类id
- 我们可以先查询文章表,然后得到对应的分类id的集合,然后去查询分类表(为什么需要这样子做,而不是直接查询目录表,因为考虑到一个问题就是如果文章的状态是不对的,不正常的状态。所以应该先拿到正常的全部文章的id集合,然后在从目录表中查询相应的数据封装返回)
3.5代码实现
CategoryServiceImpl
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
@Autowired
private ArticleService articleService;
@Override
public ResponseResult getCategoryList() {
//查询文章表,状态为已发布
LambdaQueryWrapper<Article> articleWrapper = new LambdaQueryWrapper<Article>();
articleWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);
List<Article> articleList = articleService.list(articleWrapper);
//获取文章分类的id,并且去重
Set<Long> categoryIds = articleList.stream()
.map(article -> article.getCategoryId())
.collect(Collectors.toSet());
//查询分列表
List<Category> categories = listByIds(categoryIds);
categories = categories.stream()
.filter(category -> SystemConstants.STATUS_NORMAL.equals(category.getStatus()))
.collect(Collectors.toList());
//封装Vo
List<CategoryVo> categoryVoList = BeanCopyUtils.copyBeanList(categories, CategoryVo.class);
return ResponseResult.okResult(categoryVoList);
}
}
步骤分析:
第一步:我拿到正常的article集合,然后通过stream流的形式,我得到一个文章的id的list
第二步:调用mybatis-plus的自带的listByIds方法得到category的集合,通过stream流的方式去过滤一些条件,最后面封装vo,拷贝到vo返回即可。
第三步:测试
3.6自定义Bean拷贝
也就是说方法中可以进行单个对象和集合对象的拷贝
public class BeanCopyUtils {
private BeanCopyUtils() {
}
public static <V> V copyBean(Object source,Class<V> clazz) {
//创建目标对象
V result = null;
try {
result = clazz.newInstance();
//实现属性copy
BeanUtils.copyProperties(source, result);
} catch (Exception e) {
e.printStackTrace();
}
//返回结果
return result;
}
public static <O,V> List<V> copyBeanList(List<O> list,Class<V> clazz){
return list.stream()
.map(o -> copyBean(o, clazz))
.collect(Collectors.toList());
}
}
4.文章列表接口分析
4.1文章列表接口位置
文章列表接口肯定是前台展示的数据,应该在blog的前台模块中。
参数解释:
- 因为文章可能有很多需要分页
- 因为分类这也需要调用这个列表展示,所以需要分类id,如果不传那就是首页,如果传了那就是点击了分类。
4.2文章列表展示效果
4.3接口相应的格式
4.4需求以及分析数据表
分析:在这我需要查询到article的信息,而json数据里面有categoryName,我需要从category表中去查到名字去赋值到article表中,而关联信息就是传入的categoryId。
也就是说:我查到的文章列表的数据,我通过比较如果我的categoryId的aritlce表和category表,如果相同的话就是一篇文章,我就在article实体类赋值我的categoryName
首先分析:
在article实体类上添加字段:
4.5代码实现
public ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId) {
//查询条件
LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
// 如果有categoryId 就要 查询时和传入的相同
lambdaQueryWrapper.eq(Objects.nonNull(categoryId)&&categoryId>0, Article::getCategoryId,categoryId);
//状态是正式发布的
lambdaQueryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);
//对isTop进行排序
lambdaQueryWrapper.orderByDesc(Article::getIsTop);
//分页查询
Page<Article> page = new Page<>(pageNum, pageSize);
page(page, lambdaQueryWrapper);
//查询categoryName方式一
/* List<Article> articles = page.getRecords();
for (Article article : articles) {
Category category = categoryService.getById(article.getCategoryId());
article.setCategoryName(category.getName());
}*/
//查询categoryName方式二
List<Article> articles = page.getRecords();
articles = articles.stream()
.map(article ->
article.setCategoryName(categoryService.getById(article.getCategoryId()).getName()))
.collect(Collectors.toList());
//封装查询结果
List<Article> articleListVos = BeanCopyUtils.copyBeanList(page.getRecords(), Article.class);
PageVo pageVo = new PageVo(articleListVos,page.getTotal());
return ResponseResult.okResult(pageVo);
}
5.文章详情接口分析
5.1文章详情接口位置
点击阅读全文即可跳转到文章详情界面
参数说明:
需要文章的id
5.2接口相应的需求以及分析
5.3代码实现
public ResponseResult getArticleDetail(Long id) {
//根据id查询文章
Article article = getById(id);
//转换成VO
ArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class);
//根据分类id去查询分类名
Long categoryId = articleDetailVo.getCategoryId();
Category category = categoryService.getById(categoryId);
if(category != null){
articleDetailVo.setCategoryName(category.getName());
}
//封装相应返回
return ResponseResult.okResult(articleDetailVo);
}
步骤分析:
第一步:首先根据文章id得到相应的文章
第二步:转化为vo对象,根据分类的id得到category
第三步:判断如果不为空,设置vo的categoryName,封装返回
5.4添加FastJson配置类
引入依赖,创建配置类,统一处理日期。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
@Bean//使用@Bean注入fastJsonHttpMessageConvert
public HttpMessageConverter fastJsonHttpMessageConverters() {
//1.需要定义一个Convert转换消息的对象
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
SerializeConfig.globalInstance.put(Long.class, ToStringSerializer.instance);
fastJsonConfig.setSerializeConfig(SerializeConfig.globalInstance);
fastConverter.setFastJsonConfig(fastJsonConfig);
HttpMessageConverter<?> converter = fastConverter;
return converter;
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(fastJsonHttpMessageConverters());
}
}
6.友链查询
6.1友链查询接口位置
6.2接口相应的需求以及分析
对应的数据表
返回的数据格式
6.3代码实现
@Service("linkService")
public class LinkServiceImpl extends ServiceImpl<LinkMapper, Link> implements LinkService {
@Override
public ResponseResult getAllLink() {
//查询所有审核通过的
LambdaQueryWrapper<Link> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Link::getStatus, SystemConstants.LINK_STATUS_NORMAL);
List<Link> links = list(queryWrapper);
//转换成vo
List<LinkVo> linkVos = BeanCopyUtils.copyBeanList(links, LinkVo.class);
//封装返回
return ResponseResult.okResult(linkVos);
}
}
步骤分析:
第一步:因为查询的字段刚好数据库表相对应,构建查询对象。
第二步:封装Vo返回即可。
7.登录/退出登录功能接口分析
7.1登录/退出登录接口位置
7.2接口相应的需求以及分析
对应的数据表
返回的数据格式
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxZjhmYWVjM2U3YzI0YTE4OGUyMjQyMzNjMDNlMmQyZiIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTY2ODQ2ODQwOSwiZXhwIjoxNjY4NDcyMDA5fQ.gbmWqXLKv0LoRI5yv5qEy8jQwdyay1tDLzQbmvAU1Mk",
"userInfoVo": {
"avatar": "https://11111",
"email": "22222@qq.com",
"id": "1",
"nickName": "sg888",
"sex": "1"
}
},
"msg": "操作成功"
}
7.3代码实现
UserDetailServiceImpl
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询用户信息
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(User::getUserName, username);
User user = userMapper.selectOne(lambdaQueryWrapper);
if(Objects.isNull(user)){
throw new RuntimeException("用户不存在!");
}
//返回用户信息
//TODO 查询权限信息封装
return new LoginUser(user);
}
}
自定义登录的实现类,去实现UserDetailsService的接口里面的loadUserByUsername方法,再次登录的时候,Spring Security会自动调用覆写的方法,方法里面自定义从数据库查询用户的相关信息,返回一个UserDetails的对象。
LoginUser
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
SecurityConfig
我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。
我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
BlogLoginServiceImpl
package com.jierlung.service.Impl;
import com.jierlung.domain.ResponseResult;
import com.jierlung.domain.entity.LoginUser;
import com.jierlung.domain.entity.User;
import com.jierlung.domain.vo.BlogUserLoginVo;
import com.jierlung.domain.vo.UserInfoVo;
import com.jierlung.service.BlogLoginService;
import com.jierlung.utils.BeanCopyUtils;
import com.jierlung.utils.JwtUtil;
import com.jierlung.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);
if (Objects.isNull(authenticate)) {
throw new RuntimeException("用户名或密码错误");
}
//获取用户id,生成token
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
//把用户信息存入redis当中
redisCache.setCacheObject("bloglogin:" + userId, loginUser);
//Bean拷贝
UserInfoVo userInfoVo = BeanCopyUtils.copyBean(loginUser.getUser(), UserInfoVo.class);
BlogUserLoginVo blogUserLoginVo = new BlogUserLoginVo(jwt, userInfoVo);
return ResponseResult.okResult(blogUserLoginVo);
}
@Override
public ResponseResult logout() {
//获取token 解析获取userid
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
//获取userid
Long userId = loginUser.getUser().getId();
//删除redis中的用户信息
redisCache.deleteObject("bloglogin:"+userId);
return ResponseResult.okResult();
}
}
JwtAuthenticationTokenFilter
package com.jierlung.filter;
import com.alibaba.fastjson.JSON;
import com.jierlung.domain.ResponseResult;
import com.jierlung.domain.entity.LoginUser;
import com.jierlung.enums.AppHttpCodeEnum;
import com.jierlung.utils.JwtUtil;
import com.jierlung.utils.RedisCache;
import com.jierlung.utils.WebUtils;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, IOException {
//获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//放行
filterChain.doFilter(request, response);
return;
}
//解析获取userid
Claims claims=null;
try {
claims = JwtUtil.parseJWT(token);
} catch (Exception e) {
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
WebUtils.renderString(response, JSON.toJSONString(result));
return;
}
String userId = claims.getSubject();
LoginUser loginUser = redisCache.getCacheObject("bloglogin:" + userId);
//存入securityContextHolder
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
流程:首先会进入过滤器,会检查是否携带了token,如果携带就解析放行,如果没有token就会去到自定义实现类去查数据库,生成token返回,数据存入到redis中。
7.4登录/退出登录总结
登录认证需要有spring security的知识,如果还没学到,请去学习相关内容,而具体流程可以看我这篇文章:Spring Security安全认证流程
8.评论列表
8.1评论列表接口的位置
8.2接口相应的需求以及分析
相关数据表
8.3代码实现
@Service("CommentService")
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {
@Autowired
private UserService userService;
@Override
public ResponseResult commentList(String commentType, Long articleId, Integer pageNum, Integer pageSize) {
//查询所有文章列表
//查询条件
LambdaQueryWrapper<Comment> lambdaQueryWrapper = new LambdaQueryWrapper<>();
//对文章id进行判断
lambdaQueryWrapper.eq(SystemConstants.ARTICLE_COMMENT.equals(commentType),Comment::getArticleId, articleId);
//根评论的id为-1
lambdaQueryWrapper.eq(Comment::getRootId, -1);
//评论类型
lambdaQueryWrapper.eq(Comment::getType, commentType);
//分页查询
Page<Comment> page = new Page<>(pageNum, pageSize);
page(page, lambdaQueryWrapper);
List<CommentVo> commentVoList = toCommentVoList(page.getRecords());
//查询所有根评论对应的子评论
for (CommentVo commentVo : commentVoList) {
//查询对应的子评论
List<CommentVo> children=getChildren(commentVo.getId());
commentVo.setChilden(children);
}
return ResponseResult.okResult(new PageVo(commentVoList, page.getTotal()));
}
@Override
public ResponseResult addComment(Comment comment) {
if(!StringUtils.hasText(comment.getContent())){
throw new SystemException(AppHttpCodeEnum.CONTENT_NOT_NULL);
}
save(comment);
return ResponseResult.okResult();
}
private List<CommentVo> getChildren(Long id) {
LambdaQueryWrapper<Comment> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(Comment::getRootId, id);
lambdaQueryWrapper.orderByAsc(Comment::getCreateTime);
List<Comment> comments = list(lambdaQueryWrapper);
List<CommentVo> commentVos = toCommentVoList(comments);
return commentVos;
}
private List<CommentVo> toCommentVoList(List<Comment> list) {
List<CommentVo> commentVos = BeanCopyUtils.copyBeanList(list, CommentVo.class);
//遍历commentVo
for (CommentVo commentVo : commentVos) {
//查询getCreateBy,对应的用户
String nickName = userService.getById(commentVo.getCreateBy()).getNickName();
commentVo.setUsername(nickName);
//通过对toCommentUserId查询用户的昵称并赋值
//如果toCommentUserId不为-1才进行查询
if(commentVo.getToCommentUserId()!=-1){
String toCommentUserName = userService.getById(commentVo.getToCommentUserId()).getNickName();
commentVo.setToCommentUserName(toCommentUserName);
}
}
return commentVos;
}
}
关键步骤解析:
第一步:首先我拿到所以评论的集合(暂时不考虑子评论情况),注意判断条件中,友链中也有评论,所以需要去判断类型。
第二步:拿到分页的评论的对象,转化为list集合,通过Bean拷贝的方式,将其转化为Vo对象,注意这里有三个字段是Vo中没有的,MP在赋值这里为空,我们需要手动去加上这三个字段,分别是
username字段和toCommentUserName字段我们可以在Bean拷贝时,手动赋值,因为username我们可以根据查出来的评论的createby查到该用户,在调用该用户的方法去为评论的Vo赋值。
toCommentUserName字段我们可以通过toCommentUserId不等于-1,就证明不是根评论,我们就可以查到评论该评论的评论者的用户(子评论人)然后在为Vo赋值。
第三步:我已经拿到完整的评论Vo对象,我该查询子评论,遍历评论Vo对象,编写方法,如果子评论的rootId一样的话,收集到该集合,在重复步骤二。
第四步:封装返回对象。
9.友链评论列表
方法和上面一模一样,在这复用之前的方法,在方法里面加上评论的类型。
10.个人信息查询
10.1个人信息查询接口位置
10.2接口相应的需求以及分析
相关数据表
10.3代码实现
@Override
public ResponseResult userInfo() {
//获取用户当前的id
Long userId = SecurityUtils.getUserId();
//根据用户id查询用用户信息
User user = getById(userId);
//封装拷贝
UserInfoVo vo = BeanCopyUtils.copyBean(user, UserInfoVo.class);
return ResponseResult.okResult(vo);
}
步骤解析:
因为登录了,就产生了token,直接拿到token,去查询用户信息即可,最后面封装返回。
11.头像上传
在这使用七牛云来保存图片
也就说,上传到七牛云,前端通过读取千牛云的图片,不通过Web服务器来存储。
11.1头像上传接口位置
11.2接口相应的需求以及分析
响应数据
11.3代码实现
配置在yml文件中配置oss
@ConfigurationProperties(prefix = "oss")
public class UploadServiceImpl implements UploadService {
@Override
public ResponseResult uploadImg(MultipartFile img) {
String originalFilename = img.getOriginalFilename();
//判断类型
if(!originalFilename.endsWith(".png")){
throw new SystemException(AppHttpCodeEnum.FILE_TYPE_ERROR);
}
//如果判断通过上传文件到oss
String filePath = PathUtils.generateFilePath(originalFilename);
String url = uploadOss(img,filePath);
return ResponseResult.okResult(url);
}
private String accessKey;
private String secretKey;
private String bucket;
private String uploadOss(MultipartFile imgFile, String filePath){
//构造一个带指定 Region 对象的配置类
Configuration cfg = new Configuration(Region.autoRegion());
//...其他参数参考类注释
UploadManager uploadManager = new UploadManager(cfg);
//...生成上传凭证,然后准备上传
// String accessKey = "your access key";
// String secretKey = "your secret key";
// String bucket = "sg-blog";
//默认不指定key的情况下,以文件内容的hash值作为文件名
String key = filePath;
try {
InputStream inputStream = imgFile.getInputStream();
Auth auth = Auth.create(accessKey, secretKey);
String upToken = auth.uploadToken(bucket);
try {
Response response = uploadManager.put(inputStream,key,upToken,null, null);
//解析上传成功的结果
DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
System.out.println(putRet.key);
System.out.println(putRet.hash);
return "http://rl4fyr0mn.hn-bkt.clouddn.com/"+key;
} catch (QiniuException ex) {
Response r = ex.response;
System.err.println(r.toString());
try {
System.err.println(r.bodyString());
} catch (QiniuException ex2) {
//ignore
}
}
} catch (Exception ex) {
//ignore
}
return "www";
}
}
12.更新用户
12.1更新用户接口位置
12.2代码实现
@Override
public ResponseResult updateUserInfo(User user) {
updateById(user);
return ResponseResult.okResult();
}
13.注册用户
13.1注册用户的接口位置
13.2接口相应的需求以及分析
13.3代码实现
@Override
public ResponseResult register(User user) {
//对数据进行非空判断
if(!StringUtils.hasText(user.getUserName())){
throw new SystemException(AppHttpCodeEnum.USERNAME_NOT_NULL);
}
if(!StringUtils.hasText(user.getPassword())){
throw new SystemException(AppHttpCodeEnum.PASSWORD_NOT_NULL);
}
if(!StringUtils.hasText(user.getEmail())){
throw new SystemException(AppHttpCodeEnum.EMAIL_NOT_NULL);
}
if(!StringUtils.hasText(user.getNickName())){
throw new SystemException(AppHttpCodeEnum.NICKNAME_NOT_NULL);
}
//对数据进行是否存在的判断
if(userNameExist(user.getUserName())){
throw new SystemException(AppHttpCodeEnum.USERNAME_EXIST);
}
if(nickNameExist(user.getNickName())){
throw new SystemException(AppHttpCodeEnum.NICKNAME_EXIST);
}
//...
//对密码进行加密
String encodePassword = passwordEncoder.encode(user.getPassword());
user.setPassword(encodePassword);
//存入数据库
save(user);
return ResponseResult.okResult();
}
14.AOP实现日志记录代码
14.0复习知识
相关注解
@Component 将当前类注入到Spring容器内
@Aspect :表明是一个切面类
@Before :前置通知,在方法执行之前执行
@After :后置通知,在方法执行之后执行
@AfterRuturning :返回通知,在方法返回结果之后执行
@AfterThrowing :异常通知,在方法抛出异常之后执行
@Around :环绕通知,围绕着方法执行
@Pointcut :切入点,PointCut(切入点)表达式有很多种,其中execution用于使用切面的连接点。
上面所提到的五种通知方法中,除了环绕通知外,其他的四个通知注解中,加或者不加参数JoinPoint都可以,如果有用到JoinPoint的地方就加,用不到就可以不加。
JoinPoint:里包含了类名、被切面的方法名,参数等属性。
环绕通知:参数必须为ProceedingJoinPoint,pjp.proceed相应于执行被切面的方法。
返回通知:可以加returning = “XXX”,XXX即为被切入方法的返回值,本例中是controller类中方法的返回值。
异常通知:可以加throwing = “XXX”,供读取异常信息。
返回通知和异常通知只会执行一个,如果产生异常,返回通知就不执行,后置通知一定会执行
环绕通知一般单独使用,环绕通知可以替代上面的四种通知,后面单独介绍。
相关概念
Joinpoint(连接点):所谓连接点是指那些被拦截到的点,在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点,通俗的说就是被增强类中的所有方法
PointCut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义,通俗的说就是被增强类中的被增强的方法,因为被增强类中并不是所有的方法都被代理了
Advice(通知/增强):所谓通知是指拦截到 Joinpoint (被增强的方法)之后所要做的事情就是通知,通俗的说就是对被增强的方法进行增强的代码
通知的类型:前置通知,返回通知,异常通知,后置通知,环绕通知
前置通知:在被代理方法执行之前执行
返回通知:在被代理方法执行之后执行
异常通知:在被代理方法执行出错的时候执行
后置通知:无论怎样都会执行
注意:返回通知和异常通知只能有一个会被执行,因为发生异常执行异常通知,然后就不会继续向下执行,自然后置通知也就不会被执行,反之亦然。
Aspect(切面):是切入点和通知(引介)的结合,通俗的说就是建立切入点和通知方法在创建时的对应关系
14.1打印日志需求
需要通过日志的接口信息,便于后期的调试排查,并可能有很多接口都需要进行日志的记录。
14.2步骤分析
第一步:编写一个自定义注解
第二步:在目标的Controller类方法中添加一个方法
第三步:定义切面类
14.3代码实现
SystemLog
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SystemLog {
String businessName();
}
LogAspect
@Component
@Aspect
@Slf4j
public class LogAspect {
@Pointcut("@annotation(com.sangeng.annotation.SystemLog)")
public void pt(){
}
@Around("pt()")
public Object printLog(ProceedingJoinPoint joinPoint) throws Throwable {
Object ret;
try {
handleBefore(joinPoint);
ret = joinPoint.proceed();
handleAfter(ret);
} finally {
// 结束后换行
log.info("=======End=======" + System.lineSeparator());
}
return ret;
}
private void handleAfter(Object ret) {
// 打印出参
log.info("Response : {}", JSON.toJSONString(ret));
}
private void handleBefore(ProceedingJoinPoint joinPoint) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
//获取被增强方法上的注解对象
SystemLog systemLog = getSystemLog(joinPoint);
log.info("=======Start=======");
// 打印请求 URL
log.info("URL : {}",request.getRequestURL());
// 打印描述信息
log.info("BusinessName : {}",systemLog.businessName());
// 打印 Http method
log.info("HTTP Method : {}",request.getMethod());
// 打印调用 controller 的全路径以及执行方法
log.info("Class Method : {}.{}",joinPoint.getSignature().getDeclaringTypeName(),((MethodSignature) joinPoint.getSignature()).getName());
// 打印请求的 IP
log.info("IP : {}",request.getRemoteHost());
// 打印请求入参
log.info("Request Args : {}", JSON.toJSONString(joinPoint.getArgs()) );
}
private SystemLog getSystemLog(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
SystemLog systemLog = methodSignature.getMethod().getAnnotation(SystemLog.class);
return systemLog;
}
}
15.更新博客浏览量
15.1需求分析
15.2代码实现
首先在应用启动时,把mysql的博客浏览量存储到redis中
在应用启动时,需要去继承CommandLineRunner,启动时执行的代码
@Component
public class ViewCountRunner implements CommandLineRunner {
@Resource
private ArticleMapper articleMapper;
@Autowired
private RedisCache redisCache;
@Override
public void run(String... args) throws Exception {
//查询博客信息 id viewCount
List<Article> articles = articleMapper.selectList(null);
Map<String, Integer> viewCountMap = articles.stream()
.collect(Collectors.toMap(article -> article.getId().toString(), article -> {
return article.getViewCount().intValue();//
}));
//存储到redis中
redisCache.setCacheMap("article:viewCount",viewCountMap);
}
}
定义接口,点击浏览文章时,该文章的浏览器+1
添加定时任务,给定的时间,同步数据到mysql中去,使得数据保持一致
关于crone表达式,可以自行去网上查找相关内容学习
@Component
public class UpdateViewCountJob {
@Autowired
private RedisCache redisCache;
@Autowired
private ArticleService articleService;
@Scheduled(cron = "0/55 * * * * ?")
public void updateViewCount(){
//获取redis中的浏览量
Map<String, Integer> viewCountMap = redisCache.getCacheMap("article:viewCount");
List<Article> articles = viewCountMap.entrySet()
.stream()
.map(entry -> new Article(Long.valueOf(entry.getKey()), entry.getValue().longValue()))
.collect(Collectors.toList());
//更新到数据库中
articleService.updateBatchById(articles);
}
}
修改查询文章的代码,冲redis中去查询数据,获得实时的数据。
@Override
public ResponseResult getArticleDetail(Long id) {
//根据id查询文章
Article article = getById(id);
//从redis中获取viewCount
Integer viewCount = redisCache.getCacheMapValue("article:viewCount", id.toString());
article.setViewCount(viewCount.longValue());
//转换成VO
ArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class);
//根据分类id查询分类名
Long categoryId = articleDetailVo.getCategoryId();
Category category = categoryService.getById(categoryId);
if(category!=null){
articleDetailVo.setCategoryName(category.getName());
}
//封装响应返回
return ResponseResult.okResult(articleDetailVo);
}
16.总结
前台部分到此结束,学会自行去分析,根据接口去写代码,一步步去分析业务逻辑。