博客项目(前台功能实现)
创始人
2024-04-13 09:58:04
0

博客项目(前台接口实现)

文章目录

  • 博客项目(前台接口实现)
    • 1.前置知识
      • 1.1Controller
        • 1.1.1ResponseResult类
        • 1.1.2该类的方法
      • 1.2Service
      • 1.3ServiceImpl
      • 1.4Mapper
      • 1.5Vo的理解
      • 1.6可能会用到的相关插件
      • 1.7设置字面量
      • 1.8后端接口测试工具
    • 2.热门文章接口分析
      • 2.1热门文章接口位置
      • 2.2接口的路径
      • 2.3前端展示效果
      • 2.4接口响应的格式
      • 2.5需求以及分析数据表
      • 2.6代码实现
    • 3.文章分类接口分析
      • 3.1文章接口位置
      • 3.2接口路径
      • 3.3文章接口展示效果
      • 3.4接口响应的格式
      • 3.5需求以及分析数据表
      • 3.5代码实现
      • 3.6自定义Bean拷贝
    • 4.文章列表接口分析
      • 4.1文章列表接口位置
      • 4.2文章列表展示效果
      • 4.3接口相应的格式
      • 4.4需求以及分析数据表
      • 4.5代码实现
    • 5.文章详情接口分析
      • 5.1文章详情接口位置
      • 5.2接口相应的需求以及分析
      • 5.3代码实现
      • 5.4添加FastJson配置类
    • 6.友链查询
      • 6.1友链查询接口位置
      • 6.2接口相应的需求以及分析
      • 6.3代码实现
    • 7.登录/退出登录功能接口分析
      • 7.1登录/退出登录接口位置
      • 7.2接口相应的需求以及分析
      • 7.3代码实现
      • 7.4登录/退出登录总结
    • 8.评论列表
      • 8.1评论列表接口的位置
      • 8.2接口相应的需求以及分析
      • 8.3代码实现
    • 9.友链评论列表
    • 10.个人信息查询
      • 10.1个人信息查询接口位置
      • 10.2接口相应的需求以及分析
      • 10.3代码实现
    • 11.头像上传
      • 11.1头像上传接口位置
      • 11.2接口相应的需求以及分析
      • 11.3代码实现
    • 12.更新用户
      • 12.1更新用户接口位置
      • 12.2代码实现
    • 13.注册用户
      • 13.1注册用户的接口位置
      • 13.2接口相应的需求以及分析
      • 13.3代码实现
    • 14.AOP实现日志记录代码
      • 14.0复习知识
      • 14.1打印日志需求
      • 14.2步骤分析
      • 14.3代码实现
    • 15.更新博客浏览量
      • 15.1需求分析
      • 15.2代码实现
    • 16.总结

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 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 {@TableIdprivate Long id;//标题private String title;@TableField(exist = false)//分类的标题private String categoryName;//文章内容private String content;//文章摘要private String summary;//所属分类idprivate 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;//所属分类idprivate 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 {@AutowiredArticleService 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需求以及分析数据表

需求:

  1. 查询的必须是正式发布的文章
  2. 查询前十条数据
  3. 根据访问量从大到小排序
  4. 展示文章的标题和浏览量
  5. 不能把已删除的查询出来

文章表

在这里插入图片描述

分析:

  1. 可以根据文章表的字段,查询文章的状态,0表示已发布
  2. 查询前十条数据,需要查询到的文章分页展示,前10条
  3. 展示文章的标题和浏览量,可以将实体类封装对应的Vo返回给前端
  4. 删除标志,这里的删除是逻辑删除(也就是说删除了,其实底层没有删除,查询的时候看逻辑删除字段的状态查询数据)

2.6代码实现

 public ResponseResult hotArticleList() {//查询热门文章,封装成ResponseResult返回LambdaQueryWrapper
queryWrapper=new LambdaQueryWrapper
();//必须是正式文章queryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);//按照浏览量进行排序queryWrapper.orderByDesc(Article::getViewCount);//最多只查询10条Page
page = new Page<>(1, 10);page(page, queryWrapper);List
articles = page.getRecords();//bean拷贝ArrayList 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 {@Autowiredprivate CategoryService categoryService;@GetMapping("/getCategoryList")public ResponseResult getCategoryList(){return categoryService.getCategoryList();}
}

3.3文章接口展示效果

在这里插入图片描述

3.4接口响应的格式

在这里插入图片描述

3.5需求以及分析数据表

需求:

  1. 必须是正式的文章的分类
  2. 必须是正常状态的文章

数据表:

在这里插入图片描述

分析:

  1. 可以看到前端需要返回分类名和对应的分类id
  2. 我们可以先查询文章表,然后得到对应的分类id的集合,然后去查询分类表(为什么需要这样子做,而不是直接查询目录表,因为考虑到一个问题就是如果文章的状态是不对的,不正常的状态。所以应该先拿到正常的全部文章的id集合,然后在从目录表中查询相应的数据封装返回)

3.5代码实现

CategoryServiceImpl

@Service
public class CategoryServiceImpl extends ServiceImpl implements CategoryService {@Autowiredprivate ArticleService articleService;@Overridepublic ResponseResult getCategoryList() {//查询文章表,状态为已发布LambdaQueryWrapper
articleWrapper = new LambdaQueryWrapper
();articleWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);List
articleList = articleService.list(articleWrapper);//获取文章分类的id,并且去重Set categoryIds = articleList.stream().map(article -> article.getCategoryId()).collect(Collectors.toSet());//查询分列表List categories = listByIds(categoryIds);categories = categories.stream().filter(category -> SystemConstants.STATUS_NORMAL.equals(category.getStatus())).collect(Collectors.toList());//封装VoList 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 copyBean(Object source,Class clazz) {//创建目标对象V result = null;try {result = clazz.newInstance();//实现属性copyBeanUtils.copyProperties(source, result);} catch (Exception e) {e.printStackTrace();}//返回结果return result;}public static  List copyBeanList(List list,Class clazz){return list.stream().map(o -> copyBean(o, clazz)).collect(Collectors.toList());}
}

4.文章列表接口分析

4.1文章列表接口位置

文章列表接口肯定是前台展示的数据,应该在blog的前台模块中。
在这里插入图片描述

参数解释:

  1. 因为文章可能有很多需要分页
  2. 因为分类这也需要调用这个列表展示,所以需要分类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
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
page = new Page<>(pageNum, pageSize);page(page, lambdaQueryWrapper);//查询categoryName方式一/* List
articles = page.getRecords();for (Article article : articles) {Category category = categoryService.getById(article.getCategoryId());article.setCategoryName(category.getName());}*///查询categoryName方式二List
articles = page.getRecords();articles = articles.stream().map(article ->article.setCategoryName(categoryService.getById(article.getCategoryId()).getName())).collect(Collectors.toList());//封装查询结果List
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);//转换成VOArticleDetailVo 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 {@Overridepublic void addCorsMappings(CorsRegistry registry) {// 设置允许跨域的路径registry.addMapping("/**")// 设置允许跨域请求的域名.allowedOriginPatterns("*")// 是否允许cookie.allowCredentials(true)// 设置允许的请求方式.allowedMethods("GET", "POST", "DELETE", "PUT")// 设置允许的header属性.allowedHeaders("*")// 跨域允许时间.maxAge(3600);}@Bean//使用@Bean注入fastJsonHttpMessageConvertpublic 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;}@Overridepublic void configureMessageConverters(List> converters) {converters.add(fastJsonHttpMessageConverters());}}

6.友链查询

6.1友链查询接口位置

在这里插入图片描述

在这里插入图片描述

6.2接口相应的需求以及分析

对应的数据表

在这里插入图片描述

返回的数据格式

在这里插入图片描述在这里插入图片描述

6.3代码实现

@Service("linkService")
public class LinkServiceImpl extends ServiceImpl implements LinkService {@Overridepublic ResponseResult getAllLink() {//查询所有审核通过的LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(Link::getStatus, SystemConstants.LINK_STATUS_NORMAL);List links = list(queryWrapper);//转换成voList 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 {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据用户名查询用户信息LambdaQueryWrapper 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;@Overridepublic Collection getAuthorities() {return null;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUserName();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}

在这里插入图片描述

SecurityConfig

我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。

我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。

我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic 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 {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCache redisCache;@Overridepublic 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,生成tokenLoginUser 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);}@Overridepublic ResponseResult logout() {//获取token 解析获取useridAuthentication authentication = SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser = (LoginUser) authentication.getPrincipal();//获取useridLong 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 {@Autowiredprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, IOException {//获取tokenString token = request.getHeader("token");if (!StringUtils.hasText(token)) {//放行filterChain.doFilter(request, response);return;}//解析获取useridClaims 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);//存入securityContextHolderUsernamePasswordAuthenticationToken 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 implements CommentService {@Autowiredprivate UserService userService;@Overridepublic ResponseResult commentList(String commentType, Long articleId, Integer pageNum, Integer pageSize) {//查询所有文章列表//查询条件LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();//对文章id进行判断lambdaQueryWrapper.eq(SystemConstants.ARTICLE_COMMENT.equals(commentType),Comment::getArticleId, articleId);//根评论的id为-1lambdaQueryWrapper.eq(Comment::getRootId, -1);//评论类型lambdaQueryWrapper.eq(Comment::getType, commentType);//分页查询Page page = new Page<>(pageNum, pageSize);page(page, lambdaQueryWrapper);List commentVoList = toCommentVoList(page.getRecords());//查询所有根评论对应的子评论for (CommentVo commentVo : commentVoList) {//查询对应的子评论List children=getChildren(commentVo.getId());commentVo.setChilden(children);}return ResponseResult.okResult(new PageVo(commentVoList, page.getTotal()));}@Overridepublic ResponseResult addComment(Comment comment) {if(!StringUtils.hasText(comment.getContent())){throw  new SystemException(AppHttpCodeEnum.CONTENT_NOT_NULL);}save(comment);return ResponseResult.okResult();}private List getChildren(Long id) {LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();lambdaQueryWrapper.eq(Comment::getRootId, id);lambdaQueryWrapper.orderByAsc(Comment::getCreateTime);List comments = list(lambdaQueryWrapper);List commentVos = toCommentVoList(comments);return commentVos;}private List toCommentVoList(List list) {List commentVos = BeanCopyUtils.copyBeanList(list, CommentVo.class);//遍历commentVofor (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代码实现

 @Overridepublic ResponseResult userInfo() {//获取用户当前的idLong 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 {@Overridepublic ResponseResult uploadImg(MultipartFile img) {String originalFilename = img.getOriginalFilename();//判断类型if(!originalFilename.endsWith(".png")){throw new SystemException(AppHttpCodeEnum.FILE_TYPE_ERROR);}//如果判断通过上传文件到ossString 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代码实现

@Overridepublic 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=======");// 打印请求 URLlog.info("URL            : {}",request.getRequestURL());// 打印描述信息log.info("BusinessName   : {}",systemLog.businessName());// 打印 Http methodlog.info("HTTP Method    : {}",request.getMethod());// 打印调用 controller 的全路径以及执行方法log.info("Class Method   : {}.{}",joinPoint.getSignature().getDeclaringTypeName(),((MethodSignature) joinPoint.getSignature()).getName());// 打印请求的 IPlog.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 {@Resourceprivate ArticleMapper articleMapper;@Autowiredprivate RedisCache redisCache;@Overridepublic void run(String... args) throws Exception {//查询博客信息  id  viewCountList
articles = articleMapper.selectList(null);Map 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 {@Autowiredprivate RedisCache redisCache;@Autowiredprivate ArticleService articleService;@Scheduled(cron = "0/55 * * * * ?")public void updateViewCount(){//获取redis中的浏览量Map viewCountMap = redisCache.getCacheMap("article:viewCount");List
articles = viewCountMap.entrySet().stream().map(entry -> new Article(Long.valueOf(entry.getKey()), entry.getValue().longValue())).collect(Collectors.toList());//更新到数据库中articleService.updateBatchById(articles);} }

修改查询文章的代码,冲redis中去查询数据,获得实时的数据。

 @Overridepublic ResponseResult getArticleDetail(Long id) {//根据id查询文章Article article = getById(id);//从redis中获取viewCountInteger viewCount = redisCache.getCacheMapValue("article:viewCount", id.toString());article.setViewCount(viewCount.longValue());//转换成VOArticleDetailVo 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.总结

前台部分到此结束,学会自行去分析,根据接口去写代码,一步步去分析业务逻辑。

相关内容

热门资讯

下调!住房出售,最新政策来了! 12月30日,财政部、税务总局发布《关于个人销售住房增值税政策的公告》(下称《公告》),明确个人将购...
原创 欣... 《电鳗财经》电鳗号/文 欣旺达子公司因动力电池质量纠纷被诉,索赔金额高达数亿元的消息引发行业震动。...
华蓝集团:关联交易按制度审议与... 证券之星消息,华蓝集团(301027)12月30日在投资者关系平台上答复投资者关心的问题。 投资者提...
郑州银行发布诉讼事项进展 被告... 12月31日,郑州银行发布《关于诉讼事项进展的公告》称,2025年7月,郑州银行中原路支行与郑州金威...
2026年嘉兴离婚律师权威推荐... 2026年嘉兴离婚律师权威推荐:北京国樽(嘉兴)律师事务所领衔,专业离婚律师/婚姻律师/诉讼离婚律师...
厦门出台《厦门历史文化名城保护... 集美学村建筑群 12月30日,市人大常委会表决通过《厦门历史文化名城保护条例》《厦门经济特区绿色金融...
李某平、杨某福借助黑客技术侵入... 近日,云南公安机关网安部门协同作战,成功斩断一条利用黑客技术窃取公民个人信息的黑色产业链,抓获犯罪嫌...
2026年“两新”政策方案发布... 央广网北京12月31日消息(记者周尧)据中央广播电视总台中国之声《新闻和报纸摘要》报道,国家发展改革...
市人大常委会会议表决通过4件法... 充分发挥职能服务良好开局 市人大常委会会议表决通过4件法规、人事任免事项等,黄莉新主持全体会议并讲话...
大烨智能收到立案告知书,律师征... 雷达财经雷助吧出品 文|阑珊 编|深海 12月26日,大烨智能发布《关于收到中国证券监督管理委员会立...