博客项目(前台功能实现)

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

文章目录

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需求以及分析数据表

需求:

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

文章表

在这里插入图片描述

分析:

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

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需求以及分析数据表

需求:

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

数据表:

在这里插入图片描述

分析:

  1. 可以看到前端需要返回分类名和对应的分类id
  2. 我们可以先查询文章表,然后得到对应的分类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的前台模块中。
在这里插入图片描述

参数解释:

  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<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.总结

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值