博客系统


本项目是码神之路所使用的博客系统,项目简单,需求明确,容易上手。

spring Boot 练手实战项目说明

项目讲解说明:

  1. 提供前端工程,只需要实现后端接口即可;
  2. 项目以单体架构入手,先快速开发,不考虑项目优化,降低开发负担;
  3. 开发完成后,开始优化项目,提升编程思维能力;
  4. 比如页面静态化,缓存,云存储,日志等;
  5. docker部署上线;
  6. 云服务器购买,域名购买,域名备案等。

技术栈:Spring Boot + Mybatis-Plus + Redis + MySQL

系统架构演变:

随着互联网的发展,网站应用的规模也在不断扩大,进而导致系统架构也在不断变化。从互联网兴起到现在,系统架构大致经历了以下几个过程:

  1. 单体应用架构
  2. 垂直应用架构
  3. 分布式应用架构
  4. SOA架构
  5. 微服务架构(主流)
  6. Service Mesh(服务网格化)
  7. Servless(无服务)

1. 工程搭建

1.1 新建maven工程

<!-- 项目用到的依赖 -->

MashenBlog作为父工程,删掉其目录下的src文件,并构建子模块blog-api

1.2 配置

#server
server.port= 8888
spring.application.name=mszlu_blog
# datasource
spring.datasource.url=jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

#mybatis-plus
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.global-config.db-config.table-prefix=ms_
package com.prannt.mashenblog.config;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
@MapperScan("com.prannt.mashenblog.mapper")
public class MybatisPlusConfig {
    @Bean
    public PaginationInterceptor paginationInterceptor(){
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        return paginationInterceptor;
    }
}
package com.prannt.mashenblog.config;

import com.prannt.mashenblog.handler.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

// 配置跨域
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 跨域配置,不可设置为*,不安全, 前后端分离项目,可能域名不一致
        // 本地测试 端口不一致 也算跨域
        registry.addMapping("/**").allowedOrigins("http://localhost:8080");
    }

    // 配置登录拦截器,告诉springmvc应该拦截谁
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 拦截test接口,后续实际遇到需要拦截的接口时,再配置为真正的拦截接口
        registry.addInterceptor(loginInterceptor).addPathPatterns("/test");
    }
}

1.3 启动类

@SpringBootApplication
public class MashenblogApplication {
    public static void main(String[] args) {
        SpringApplication.run(MashenblogApplication.class, args);
    }
}

2.首页—文章列表

2.1 接口说明

接口url:/articles

请求方式:POST

请求参数:见下表

参数名称 参数类型 说明
page Integer 当前页
pageSize Integer 每页显示的数量

2.2 表结构

2.2.1 article

字段 类型 是否非空
id bigint(20) NO(Primary Key)
comment_counts int(11) YES
create_date bigint(20) YES
summary varchar(255) YES
title varchar(64) YES
view_counts int(11) YES
weight int(11) NO
author_id bigint(20) YES
body_id bigint(20) YES
category_id int(11) YES

2.2.2 article_body

字段 类型 是否非空
id bigint(20) NO(Primary Key)
content longtext YES
content_html longtext YES
article_id bigint(20) NO

2.2.3 article_tag

字段 类型 是否非空
id bigint(20) NO(Primary Key)
article_id bigint(20) NO
tag_id bigint(20) NO

2.2.4 category

字段 类型 是否非空
id bigint(20) NO(Primary Key)
avatar varchar(255) YES
category_name varchar(255) YES
description varchar(255) YES

2.2.5 comment

字段 类型 是否非空
id bigint(20) NO(Primary Key)
content varchar(255) NO
create_date bigint(20) NO
article_id int(11) NO
author_id bigint(20) NO
parent_id bigint(20) NO
to_uid bigint(20) NO
level varchar(1) NO

2.2.6 log

字段 类型 是否非空
id biging(20) NO(Primary Key)
create_date bigint(20) YES
ip varchar(15) YES
method varchar(100) YES
module varchar(10) YES
nickname varchar(10) YES
operation varchar(25) YES
params varchar(255) YES
time bigint(20) YES
userid bigint(20) YES

2.2.7 tag

字段 类型 是否非空
id bigint(20) NO(Primary Key)
avatar varchar(255) YES
tag_name varchar(255) YES

2.2.8 user

字段 类型 是否非空
id bigint(20) NO(Primary Key)
account varchar(64) YES
admin bit(1) YES
avatar varchar(255) YES
create_date bigint(20) YES
deleted bit(1) YES
email varchar(128) YES
last_login bigint(20) YES
mobile_phone_number varchar(20) YES
nickname varchar(255) YES
password varchar(64) YES
salt varchar(255) YES
status varchar(255) YES

2.3 entity

2.3.1 Article

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class Article implements Serializable {
    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;                // 主键id  
    private Integer commentCounts;  // 评论数量 
    private Long createDate;        // 创建时间 
    private String summary;         // 简介    
    private String title;           // 标题    
    private Integer viewCounts;     // 浏览数量 
    private Integer weight;         // 是否置顶 
    private Long authorId;          // 作者id
    private Long bodyId;            // 内容id
    private Long categoryId;     	// 类别id
}

2.3.2 ArticleBody

@Data
public class ArticleBody {
    private Long id;
    private String content;
    private String contentHtml;
    private Long articleId;
}

2.3.3 ArticleTag

@Data
public class ArticleTag {
    private Long id;
    private Long articleId;
    private Long tagId;
}

2.3.4 Category

@Data
public class Category {
    private Long id;
    private String avatar;
    private String categoryName;
    private String description;
}

2.3.5 Comment

@Data
public class Comment {
    private Long id;
    private String content;
    private Long createDate;
    private Long articleId;
    private Long authorId;
    private Long parentId;
    private Long toUid;
    private Integer level;
}

2.3.6 Tag

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class Tag implements Serializable {
    private static final long serialVersionUID = 1L;

    private Long id;
    private String avatar;
    private String tagName;
}

2.3.7 User

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class User implements Serializable {
    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)  // 数据库自增
    private Long id;			// 主键id
    private String account;		// 账号
    private Boolean admin;		// 是否管理员
    private String avatar;		// 头像
    private Long createDate;	// 注册时间
    private Boolean deleted;	// 是否删除
    private String email;		// 邮箱
    private Long lastLogin;		// 最后登录时间
    private String mobilePhoneNumber;// 手机号
    private String nickname;	// 昵称
    private String password;	// 密码
    private String salt;		// 加密盐
    private String status;		// 状态
}

2.4 Vo对象

2.4.1 ArticleVo

@Data
public class ArticleVo {
    private Long id;                // 主键id 
    private String title;           // 标题   
    private String summary;         // 简介   
    private Integer commentCounts;  // 评论数量 
    private Integer viewCounts;     // 浏览数量 
    private Integer weight;         // 是否置顶 
    private String createDate;      // 创建时间 
    private String author;          // 作者
    private ArticleBodyVo body;
    private List<TagVo> tags;       // 标签
    private CategoryVo category;
}

2.4.2 ArticleBodyVo

@Data
public class ArticleBodyVo {
    private String content;
}

2.4.3 ArticleTagVo

// TODO

2.4.4 CategoryVo

@Data
public class CategoryVo {
    private String id;
    private String avatar;
    private String categoryName;
    private String description;
}

2.4.5 CommentVo

@Data
public class CommentVo  {
    //防止前端 精度损失 把id转为string
//    @JsonSerialize(using = ToStringSerializer.class)
    private String id;
    private UserVo author;
    private String content;
    private List<CommentVo> childrens;
    private String createDate;
    private Integer level;
    private UserVo toUser;
}

2.4.6 TagVo

@Data
public class TagVo {
    private String id;
    private String tagName;
    private String avatar;
}

2.4.7 UserVo

@Data
public class UserVo {
    private String nickname;
    private String avatar;
    private String id;
}

2.4.8 LoginUserVo

@Data
public class LoginUserVo {
    private Long id;
    private String account;
    private String nickName;
    private String avatar;
}

2.5 Controller层

@RestController     // json数据交互
@RequestMapping("articles")
public class ArticleController {
    @Autowired
    private ArticleService articleService;

    // 首页文章列表
    @PostMapping
    public Result listArticle(@RequestBody PageParams pageParams){
        return articleService.listArticle(pageParams);
    }
}

2.6 Service层

// 接口
public interface ArticleService extends IService<Article> {
    Result listArticle(PageParams pageParams);  // 首页文章列表
}
// 实现类
@Service
@RequiredArgsConstructor
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
    private final ArticleMapper articleMapper;
    // 分页查询文章列表
    @Override
    public Result listArticle(PageParams params) {
        // 分页查询article表
        Page<Article> page = new Page<>(params.getPage(),params.getPageSize());
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
        // 是否置顶排序
        // 按创建时间倒序排列,相当于order by create_date desc
        queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);
        IPage<Article> articlePage = articleMapper.selectPage(page, queryWrapper);  // 分页查询
        List<Article> records = articlePage.getRecords();
        List<ArticleVo> articleVoList = copyList(records,true,true);  // 转成vo对象
        return Result.success(articleVoList);
    }
    
	private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor) {
		List<ArticleVo> articleVoList = new ArrayList<>();
		for (Article record : records) {
			articleVoList.add(copy(record,isTag,isAuthor,false,false));
		}
		return articleVoList;
    }
    
    // 具体的转换逻辑
    private ArticleVo copy(Article article,boolean isTag,boolean isAuthor,boolean isBody,boolean isCategory){
        ArticleVo articleVo = new ArticleVo();
        BeanUtils.copyProperties(article,articleVo);
        articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
        // 并不是所有接口都需要标签、作者信息
        if(isTag) {
            Long articleId = article.getId();  // 文章id
            articleVo.setTags(tagService.findTagsByArticleId(articleId));
        }
        if (isAuthor) {
            Long authorId = article.getAuthorId();
            articleVo.setAuthor(userService.findUserById(authorId).getNickname());
        }
        if (isBody) {
            Long bodyId = article.getBodyId();
            articleVo.setBody(findArticleBodyById(bodyId));
        }
        if (isCategory) {
            Long categoryId = article.getCategoryId();
            articleVo.setCategory(categoryService.findCategoryById(categoryId));
        }
        return articleVo;
    }
}

2.7 Mapper层

首页文章列表不需要写Mapper层代码,使用的是MybatisPlus提供好的方法。

2.8 完善

2.8.1 Tag

2.8.1.1 TagService
public interface TagService extends IService<Tag> {
    List<TagVo> findTagsByArticleId(Long articleId);    // 根据文章id查询标签
}
@Service
@RequiredArgsConstructor
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {
    private final TagMapper tagMapper;
    // 根据文章id查询标签
    @Override
    public List<TagVo> findTagsByArticleId(Long articleId) {
        // mybatisplus
        List<Tag> tags = tagMapper.findTagsByArticleId(articleId);
        return copyList(tags);
    }
}
2.8.1.2 TagMapper
public interface TagMapper extends BaseMapper<Tag> {
    List<Tag> findTagsByArticleId(Long articleId);  // 可以使用快捷键生成mapper.xml
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.prannt.mashenblog.mapper.TagMapper">
    <select id="findTagsByArticleId" parameterType="long" resultType="com.prannt.mashenblog.entity.Tag">
        select
               id,avatar,tag_name as tagName
        from
             tag
        where
              id
                  in
              (select tag_id from article_tag where article_id = #{articleId})
    </select>
</mapper>

2.8.2 User

2.8.2.1 UserService
public interface UserService extends IService<User> {
    User findUserById(Long id);   // 根据id查询用户
}
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    private final UserMapper userMapper;
    // 根据id查询用户
    @Override
    public User findUserById(Long id) {
        User user = userMapper.selectById(id);	// MP自带方法
        if (user == null) {
            user = new User();
            user.setNickname("prannt");
        }
        return user;
    }
}

3. 首页—最热标签

3.1 接口说明

接口url:/tags/hot

请求方式:GET

请求参数:无

3.2 编码

3.2.1 Controller

@RestController
@RequestMapping("/tags")
@RequiredArgsConstructor
public class TagController {
    private final TagService tagService;
    // 最热标签
    @GetMapping("/hot")
    public Result hot(){
        int limit = 6;  // 查询最热的6个标签
        return tagService.hots(limit);
    }
}

3.2.2 Service

接口

public interface TagService extends IService<Tag> {
    Result hots(int limit);     // 查询最热的6个标签
}

实现类

// 查询最热的6个标签
// 最热标签:article_id表中,标签下拥有的文章最多
@Override
public Result hots(int limit) {
	// 标签下拥有的文章最多就是最热标签
	// 查询 根据tag_id分组计数,从大到小排列,取前limit个
	// SELECT tag_id FROM article_tag GROUP BY tag_id ORDER BY COUNT(*) DESC LIMIT 2;
	List<Long> tagIds = tagMapper.findHotTagIds(limit);
	if (CollectionUtils.isEmpty(tagIds)) {
		return Result.success(Collections.emptyList());
	}
	// 需求的是:tagId和tagName,即Tag对象
	// select * from tag where id in (1,2,3,4);
	List<Tag> tagList = tagMapper.findTagsByTagIds(tagIds);
	return Result.success(tagList);
}

3.2.3 Mapper

public interface TagMapper extends BaseMapper<Tag> {
    List<Long> findHotTagIds(int limit);     // 查询最热的前limit条标签
    List<Tag> findTagsByTagIds(List<Long> tagIds);  // 根据标签id查询标签
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.prannt.mashenblog.mapper.TagMapper">
    <select id="findHotTagIds" parameterType="int" resultType="java.lang.Long">
        select
               tag_id
        from
             article_tag
        group by
                 tag_id
        order by
                 count(*)
                 desc
                 limit #{limit}
    </select>
    
    <select id="findTagsByTagIds" parameterType="list" resultType="com.prannt.mashenblog.entity.Tag">
        select
               id,tag_name as tagName
        from
             tag
        where
              id
                  in
              <foreach collection="list" item="tagId" separator="," open="(" close=")">
                #{tagId}
              </foreach>
    </select>
</mapper>

4. 统一异常处理

不管是controller层还是service,mapper层,都有可能报异常,如果是预料中的异常,可以直接捕获处理,如果是意料之外的异常,需要统一进行处理,进行记录,并给用户提示相对比较友好的信息。

package com.prannt.mashenblog.handler;

@ControllerAdvice   // 对加了@controller注解的方法进行拦截处理,是AOP的实现
public class AllExceptionHandler {
    @ExceptionHandler(Exception.class)  // 对所有异常进行处理
    @ResponseBody   // 返回json数据
    public Result doException(Exception e){
        e.printStackTrace();
        return Result.fail(-999,"系统异常");
    }
}

5.首页-最热文章

5.1 接口说明

接口url:/articles/hot

请求方式:POST

5.2 Controller

@RestController     // json数据交互
@RequestMapping("articles")
public class ArticleController {
    @Autowired
    private ArticleService articleService;
    // 首页最热文章
    @PostMapping("hot")
    public Result hotArticle(){
        int limit = 5;  // 最热文章取前5条
        return articleService.hotArticle(limit);
    }
}

5.3 Service

接口

public interface ArticleService extends IService<Article> {
    Result hotArticle(int limit);   // 首页最热文章
}

实现类

@Service
@RequiredArgsConstructor
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
    private final ArticleMapper articleMapper;
    private final TagService tagService;
    private final UserService userService;
    private final ArticleBodyMapper articleBodyMapper;
    private final CategoryService categoryService;
    private final ThreadService threadService;

    // 首页最热文章
    @Override
    public Result hotArticle(int limit) {
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.orderByDesc(Article::getViewCounts);       // 对浏览量进行倒序排序
        queryWrapper.select(Article::getId,Article::getTitle);  // 只选择id和title
        queryWrapper.last("limit " + limit);             // 注意:limit后有空格
        // 以上三行代码等同于:select id,title from article order by view_counts desc limit 5;
        List<Article> articles = articleMapper.selectList(queryWrapper);
        return Result.success(copyList(articles,false,false));  // 转成vo对象
    }

    private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor) {
        List<ArticleVo> articleVoList = new ArrayList<>();
        for (Article record : records) {
            articleVoList.add(copy(record,isTag,isAuthor,false,false));
        }
        return articleVoList;
    }
    // 具体的转换逻辑
    private ArticleVo copy(Article article,boolean isTag,boolean isAuthor,boolean isBody,boolean isCategory){
        ArticleVo articleVo = new ArticleVo();
        BeanUtils.copyProperties(article,articleVo);
        articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
        // 并不是所有接口都需要标签、作者信息
        if(isTag) {
            Long articleId = article.getId();  // 文章id
            articleVo.setTags(tagService.findTagsByArticleId(articleId));
        }
        if (isAuthor) {
            Long authorId = article.getAuthorId();
            articleVo.setAuthor(userService.findUserById(authorId).getNickname());
        }
        if (isBody) {
            Long bodyId = article.getBodyId();
            articleVo.setBody(findArticleBodyById(bodyId));
        }
        if (isCategory) {
            Long categoryId = article.getCategoryId();
            articleVo.setCategory(categoryService.findCategoryById(categoryId));
        }
        return articleVo;
    }
}

6. 首页—最新文章

6.1 接口说明

接口url:/articles/new

请求方式:POST

6.2 Controller

@RestController     // json数据交互
@RequestMapping("articles")
public class ArticleController {
    @Autowired
    private ArticleService articleService;
    // 首页最新文章
    @PostMapping("new")
    public Result newArticles(){
        int limit = 5;  // 最新文章取前5条
        return articleService.newArticles(limit);
    }
}

6.3 Service

接口

public interface ArticleService extends IService<Article> {
    Result newArticles(int limit);  // 首页最新文章
}

实现类

@Service
@RequiredArgsConstructor
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
    private final ArticleMapper articleMapper;
    // 首页最新文章
    @Override
    public Result newArticles(int limit) {
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.orderByDesc(Article::getCreateDate);       // 对浏览量进行倒序排序
        queryWrapper.select(Article::getId,Article::getTitle);  // 只选择id和title
        queryWrapper.last("limit " + limit);             // 注意:limit后有空格
        // 以上三行代码等同于:select id,title from article order by create_date desc limit 5;
        List<Article> articles = articleMapper.selectList(queryWrapper);
        return Result.success(copyList(articles,false,false));  // 转成vo对象
    }
}

7. 首页—文章归档

7.1 接口说明

接口url:/articles/listArchives

请求方式:POST

7.2 Controller

@RestController     // json数据交互
@RequestMapping("articles")
public class ArticleController {
    @Autowired
    private ArticleService articleService;
    // 文章归档的前端可在component/card/cardArchive.vue下更改
    // 首页文章归档
    @PostMapping("listArchives")
    public Result listArchives(){
        return articleService.listArchives();
    }
}

7.3 Service

接口

public interface ArticleService extends IService<Article> {
    Result listArchives();     // 首页文章归档
}

实现类

@Service
@RequiredArgsConstructor
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
    private final ArticleMapper articleMapper;

    // 首页文章归档
    @Override
    public Result listArchives() {
        List<Archives> archivesList = articleMapper.listArchives();
        return Result.success(archivesList);
    }
}

7.4 Mapper

接口

@Mapper
public interface ArticleMapper extends BaseMapper<Article> {
    List<Archives> listArchives();
}

xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.prannt.mashenblog.mapper.ArticleMapper">

    <select id="listArchives" resultType="com.prannt.mashenblog.dos.Archives">
        select
               year(create_date) as year,
               month(create_date) as month,
               count(*) as count
        from
             article
        group by
                 year,month
    </select>
</mapper>

8. 登录

8.1 接口说明

接口url:/login

请求方式:POST

请求参数:

参数名称 参数类型 说明
account String 账号
password String 密码

8.2 JWT

登录使用JWT技术。

jwt 可以生成 一个加密的token,做为用户登录的令牌,当用户登录成功之后,发放给客户端。

请求需要登录的资源或者接口的时候,将token携带,后端验证token是否合法。

jwt 有三部分组成:A.B.C

A:Header,{“type”:”JWT”,”alg”:”HS256”} 固定

B:playload,存放信息,比如,用户id,过期时间等等,可以被解密,不能存放敏感信息

C: 签证,A和B加上秘钥 加密而成,只要秘钥不丢失,可以认为是安全的。

jwt 验证,主要就是验证C部分 是否合法。

依赖包:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

工具类:

public class JWTUtils {
    private static final String jwtToken = "123456Mszlu!@#$$";  // 密钥

    public static String createToken(Long userId){
        Map<String,Object> claims = new HashMap<>();
        claims.put("userId",userId);    // B部分
        JwtBuilder jwtBuilder = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法,秘钥为jwtToken A部分
                .setClaims(claims) // body数据,要唯一,自行设置   // B部分
                .setIssuedAt(new Date()) // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000));// 一天的有效时间
        String token = jwtBuilder.compact();
        return token;
    }

    // 检查token是否合法
    public static Map<String, Object> checkToken(String token){
        try {
            Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token); // 提供密钥解析token
            return (Map<String, Object>) parse.getBody();
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

8.3 Controller

@RestController
@RequestMapping("/login")
@RequiredArgsConstructor
public class LoginController {
    private final LoginService loginService;
    @PostMapping
    public Result login(@RequestBody LoginParam loginParam){    // 获取头部信息
        // 登录  验证用户 访问用户表
        return loginService.login(loginParam);
    }
}

8.4 Servcie

接口

public interface LoginService {
    Result login(LoginParam loginParam);    // 登录功能
}
public interface UserService extends IService<User> {
    User findUser(String account, String password); // 根据账户、密码查询用户
}

实现类

@Service
@RequiredArgsConstructor
public class LoginServiceImpl implements LoginService {
    private final UserService userService;
    private final RedisTemplate<String,String> redisTemplate;

    private static final String salt = "mszlu!@#";  // 盐

    // 登录功能
    @Override
    public Result login(LoginParam loginParam) {
        // 1.检查参数是否合法
        String account = loginParam.getAccount();
        String password = loginParam.getPassword();
        if (StringUtils.isEmpty(account) || StringUtils.isEmpty(password)) {
            return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
        }
        // 2.根据用户名和密码去user表中查询是否存在
        // 密码加密
        password = DigestUtils.md5Hex(password + salt);
        User user = userService.findUser(account,password);
        // 3.如果不存在,则登录失败
        if (user == null)
            return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_EXIST.getMsg());
        // 4.如果存在,使用jwt生成token,返回给前端
        String token = JWTUtils.createToken(user.getId());
        // 5.把token放入redis中,redis存储 token:user的对应关系,设置过期时间
        // (登录认证时,先认证token字符串是否合法,然后去redis中认证是否存在)
        redisTemplate.opsForValue().set("TOKEN_" + token, JSON.toJSONString(user),1, TimeUnit.DAYS);
        return Result.success(token);
    }
}
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    private final UserMapper userMapper;
    private final TokenService tokenService;
    // 根据账户、密码查询用户
    @Override
    public User findUser(String account, String password) {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getAccount,account);
        queryWrapper.eq(User::getPassword,password);
        queryWrapper.select(User::getAccount,User::getId,User::getAvatar,User::getNickname);
        queryWrapper.last("limit 1");
        return userMapper.selectOne(queryWrapper);
    }
}

9. 获取用户信息

9.1 接口说明

接口url:/users/currentUser

请求方式:GET

请求参数:

参数名称 参数类型 说明
Authorization String 头部信息(TOKEN)

9.2 Controller

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    @GetMapping("/currentUser")
    public Result currentUser(@RequestHeader("Authorization") String token){
        return userService.findUserByToken(token);  // 根据token查询用户信息
    }
}

9.3 Service层

接口

public interface UserService extends IService<User> {
    Result findUserByToken(String token);     // 根据token查询用户信息
}
public interface TokenService {
    User checkToken(String token);  // 校验token,成功则返回对应用户信息
}

实现类

@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    private final UserMapper userMapper;
    private final TokenService tokenService;
    // 根据token查询用户信息
    @Override
    public Result findUserByToken(String token) {
        User user = tokenService.checkToken(token); // 校验成功,返回对应用户信息
        if (user == null) {
            return Result.fail(ErrorCode.TOKEN_ERROR.getCode(), ErrorCode.TOKEN_ERROR.getMsg());
        }
        LoginUserVo loginUserVo = new LoginUserVo();
        loginUserVo.setId(user.getId());
        loginUserVo.setNickName(user.getNickname());
        loginUserVo.setAvatar(user.getAvatar());
        loginUserVo.setAccount(user.getAccount());
        return Result.success(loginUserVo);
    }
}
@Service
public class TokenServiceImpl implements TokenService {
    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    // 校验token,成功则返回对应用户信息
    @Override
    public User checkToken(String token) {
        // 1.token合法性校验
        // 1.1是否为空
        if (StringUtils.isEmpty(token)) {
            return null;
        }
        // 1.2解析是否成功
        Map<String, Object> stringObjectMap = JWTUtils.checkToken(token);
        if (stringObjectMap == null) {
            return null;
        }
        String userJson = redisTemplate.opsForValue().get("TOKEN_" + token);    // 解析成功
        // 1.3redis是否存在
        if (StringUtils.isEmpty(userJson)) {
            return null;
        }
        // 2.如果校验失败,返回错误(错误全部写在UserServiceImpl中)
        // 3.如果成功,返回对应的结果
        User user = JSON.parseObject(userJson, User.class);
        return user;
        // 因为只需要返回4条数据,所以把User包装一下(LoginUserVo)
    }
}

10. 退出登录

10.1 接口说明

接口url:/logout

请求方式:GET

请求参数:

参数名称 参数类型 说明
Authorization String 头部信息(TOKEN)

10.2 Controller

@RestController
@RequestMapping("/logout")
public class LogoutController {
    @Autowired
    private LogoutService logoutService;

    // 退出登录
    @GetMapping
    public Result logout(@RequestHeader("Authorization") String token){ // 获取头部信息
        return logoutService.logout(token);
    }
}

10.3 Service

接口

@Transactional
public interface LogoutService {
    Result logout(String token);    // 退出登录
}

实现类

@Service
@RequiredArgsConstructor
public class LogoutServiceImpl implements LogoutService {
    private final RedisTemplate<String,String> redisTemplate;
    private final UserService userService;

    private static final String salt = "mszlu!@#";  // 盐

    // 退出登录
    @Override
    public Result logout(String token) {
        redisTemplate.delete("TOKEN_" + token);
        return Result.success(null);
    }
}

11. 注册

11.1 接口说明

接口url:/register

请求方式:POST

参数名称 参数类型 说明
account String 账号
password String 密码
nickname String 昵称

11.2 Controller

@RestController
@RequestMapping("register")
@RequiredArgsConstructor
public class RegisterController {
    private final LogoutService logoutService;
    @PostMapping
    public Result register(@RequestBody LoginParam loginParam){
        // SSO单点登录,后期如果把登录、注册、退出功能提出去(单独的服务),可以独立提供
        return logoutService.register(loginParam);
    }
}

11.3 Service

接口

@Transactional
public interface LogoutService {
    Result register(LoginParam loginParam);     // 注册功能
}
public interface UserService extends IService<User> {
    User findUserByAccount(String account);     // 根据账户查找用户
    void saveUser(User user);                   // 保存用户
}

实现类

@Service
@RequiredArgsConstructor
public class LogoutServiceImpl implements LogoutService {
    private final RedisTemplate<String,String> redisTemplate;
    private final UserService userService;

    private static final String salt = "mszlu!@#";  // 盐

    @Override
    public Result register(LoginParam loginParam) {
        // 1.判断参数是否合法
        String account = loginParam.getAccount();
        String password = loginParam.getPassword();
        String nickname = loginParam.getNickname();
        if (StringUtils.isEmpty(account) || StringUtils.isEmpty(password) || StringUtils.isEmpty(nickname)) {
            return Result.fail(ErrorCode.PARAMS_ERROR.getCode(), ErrorCode.PARAMS_ERROR.getMsg());
        }
        // 2.判断账户是否存在,若存在,则返回账户已经被注册
        User user = userService.findUserByAccount(account);
        if (user != null) {
            return Result.fail(ErrorCode.ACCOUNT_EXIST.getCode(), ErrorCode.ACCOUNT_EXIST.getMsg());
        }
        // 3.账户如果不存在,则注册用户
        user = new User();
        user.setNickname(nickname);
        user.setAccount(account);
        user.setPassword(DigestUtils.md5Hex(password + salt));
        user.setCreateDate(System.currentTimeMillis());
        user.setLastLogin(System.currentTimeMillis());
        user.setAvatar("https://gitee.com/prannt/blogphoto/raw/master/img/35.jpg");
        user.setAdmin(true);
        user.setDeleted(false);
        user.setSalt("");
        user.setStatus("");
        user.setEmail("");
        userService.saveUser(user);
        // 4.生成token
        String token = JWTUtils.createToken(user.getId());
        // 5.存入redis并返回
        redisTemplate.opsForValue().set("TOKEN_" + token, JSON.toJSONString(user),1, TimeUnit.DAYS);
        // 6.注意加上事务,一旦中间的任何过程出现问题,注册的用户需要回滚
        return Result.success(token);
    }
}
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    private final UserMapper userMapper;
    private final TokenService tokenService;
    // 根据账户查找用户
    @Override
    public User findUserByAccount(String account) {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getAccount,account);
        queryWrapper.last("limit 1");   // 确保只查询1条数据,也能提升效率
        return userMapper.selectOne(queryWrapper);
    }
    
    // 保存用户
    @Override
    public void saveUser(User user){
        // 保存用户时,id会自动生成
        // 这个地方默认生成的id是分布式id,采用雪花算法
        userMapper.insert(user);
    }
}

12. 登录拦截器

每次访问需要登录的资源的时候,都需要在代码中进行判断,一旦登录的逻辑有所改变,代码都得进行变动,非常不合适。

那么可不可以统一进行登录判断呢?

可以,使用拦截器进行登录拦截,遇到需要登录才能访问的接口时,如果未登录,拦截器直接返回,并跳转登录页面。

12.1 拦截器实现

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private TokenService tokenService;

    // 在执行controller方法(Handler)之前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.需要判断请求的接口路径是否为HandlerMethod(controller方法)
        if (!(handler instanceof HandlerMethod)) {
            //handler可能是RequestResourceHandler(资源handler), springboot程序访问静态资源默认去classpath下的static目录去查询
            return true;
        }
        // 2.判断token是否为空,如果为空, 则表示未登录
        String token = request.getHeader("Authorization");
        // 打印日志,方便查看请求信息
        log.info("================= request start ===========================");
        String requestURI = request.getRequestURI();
        log.info("request uri:{}",requestURI);
        log.info("request method:{}",request.getMethod());
        log.info("token:{}", token);
        log.info("================= request end ===========================");


        if (StringUtils.isEmpty(token)) {
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        // 3.如果token不为空,登录验证 TokenService checkToken
        User user = tokenService.checkToken(token);
        if (user == null) {
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        // 4.如果认证成功 放行即可
        // 如果希望在controller直接获取用户信息,怎么获取?即在TestController中获取User信息
        UserThreadLocal.put(user);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 如果不删除ThreadLocal中用完的信息,会有内存泄露的风险
        UserThreadLocal.remove();
    }
}

12.2 使拦截器生效

@Configuration
public class WebConfig  implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //跨域配置,不可设置为*,不安全, 前后端分离项目,可能域名不一致
        //本地测试 端口不一致 也算跨域
        registry.addMapping("/**").allowedOrigins("http://localhost:8080");
    }

    // 配置登录拦截器,告诉springmvc应该拦截谁
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 拦截test接口,后续实际遇到需要拦截的接口时,再配置为真正的拦截接口
        registry.addInterceptor(loginInterceptor).addPathPatterns("/test");
    }
}

13. ThreadLocal保存用户信息

Redis中只存放了token,而我们希望直接获取用户信息。

使用ThreadLocal替代Session的好处:可以在同一线程中很方便的获取用户信息,不需要频繁的传递session对象。

具体实现流程:

  • 在登录业务代码中,当用户登录成功时,生成一个登录凭证存储到redis中,将凭证中的字符串保存在cookie中返回给客户端。
  • 使用一个拦截器拦截请求,从cookie中获取凭证字符串与redis中的凭证进行匹配,获取用户信息,将用户信息存储到ThreadLocal中,在本次请求中持有用户信息,即可在后续操作中使用到用户信息。
package com.prannt.mashenblog.utils;

public class UserThreadLocal {
    private UserThreadLocal(){}     // 设置为私有的,不能从外部new出来
    // ThreadLocal用于做线程变量隔离
    private static final ThreadLocal<User> LOCAL = new ThreadLocal<>();
    // 放入user
    public static void put(User user){
        LOCAL.set(user);
    }

    // 取出user
    public static User get(){
        return LOCAL.get();
    }

    // 删除user
    public static void remove(){
        LOCAL.remove();
    }
}
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private TokenService tokenService;

    // 在执行controller方法(Handler)之前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.需要判断请求的接口路径是否为HandlerMethod(controller方法)
        if (!(handler instanceof HandlerMethod)) {
            //handler可能是RequestResourceHandler(资源handler), springboot程序访问静态资源默认去classpath下的static目录去查询
            return true;
        }
        // 2.判断token是否为空,如果为空, 则表示未登录
        String token = request.getHeader("Authorization");
        // 打印日志,方便查看请求信息
        log.info("================= request start ===========================");
        String requestURI = request.getRequestURI();
        log.info("request uri:{}",requestURI);
        log.info("request method:{}",request.getMethod());
        log.info("token:{}", token);
        log.info("================= request end ===========================");


        if (StringUtils.isEmpty(token)) {
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        // 3.如果token不为空,登录验证 TokenService checkToken
        User user = tokenService.checkToken(token);
        if (user == null) {
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        // 4.如果认证成功 放行即可
        // 如果希望在controller直接获取用户信息,怎么获取?即在TestController中获取User信息
        UserThreadLocal.put(user);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 如果不删除ThreadLocal中用完的信息,会有内存泄露的风险
        UserThreadLocal.remove();
    }
}
@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {
    @GetMapping
    public Result test(){
        User user = UserThreadLocal.get();
        log.info("打印user信息:{}",user);
        return Result.success(null);
    }
}

测试: http://localhost:8888/test ,在Headers中加上登录得到的token

14. ThreadLocal内存泄漏

如果不删除ThreadLocal中用完的信息,会有内存泄露的风险,为什么?

一个线程会维护一个ThreadLocalMapThreadLocalMap中有一个key,有一个valuekey指向了ThreadLocal,有引用指向该ThreadLocalvalue就是存储的相应的值(在本例中,存储的是user对象)。

在上图中,实线代表强引用,虚线代表弱引用(key所在的引用是弱引用)。

强引用:使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足时,JVM宁愿抛出OOM异常,使程序异常终止,也不会回收该对象。如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间回收该对象。

弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在Java中,用java.lang.ref.WeakReference来表示。

ThreadLocal和线程处于同一生命周期,当ThreadLocal被垃圾回收掉之后,key就没有了,但是线程还存在,即value还存在(一个Map,key被回收掉,value还存在着)。value将永远存在于线程当中,如果有大量的这种行为,就很有可能造成内存泄漏。

15. 文章详情

15.1 接口说明

接口url:/articles/view/{id}

请求方式:POST

请求参数:

参数名称 参数类型 说明
id long 文章id(路径参数)

15.2 Controller

@RestController     // json数据交互
@RequestMapping("articles")
public class ArticleController {
    // 文章详情
    @PostMapping("view/{id}")
    public Result findArticleById(@PathVariable("id") Long articleId){
        return articleService.findArticleById(articleId);
    }
}

15.3 Service

接口

public interface ArticleService extends IService<Article> {
    Result findArticleById(Long articleId);       // 查看文章详情
}
public interface CategoryService {
    CategoryVo findCategoryById(Long categoryId);  // 根据id查询类别
}

实现类

@Service
@RequiredArgsConstructor
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
    private final ArticleMapper articleMapper;
    private final TagService tagService;
    private final UserService userService;
    private final ArticleBodyMapper articleBodyMapper;
    private final CategoryService categoryService;

    // 查看文章详情
    @Override
    public Result findArticleById(Long articleId) {
        // 1.根据id查询文章信息
        Article article = articleMapper.selectById(articleId);
        // 2.根据bodyId和category做关联查询
        ArticleVo articleVo = copy(article,true,true,true,true);
        threadService.updateArticleViewCount(articleMapper,article);
        return Result.success(articleVo);
    }

    // 根据id查询文章内容
    private ArticleBodyVo findArticleBodyById(Long bodyId) { 
        ArticleBody articleBody = articleBodyMapper.selectById(bodyId);
        ArticleBodyVo articleBodyVo = new ArticleBodyVo();
        articleBodyVo.setContent(articleBody.getContent());
        return articleBodyVo;
    }

    // copyList重载方法
    private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor,boolean isBody,boolean isCategory) {
        List<ArticleVo> articleVoList = new ArrayList<>();
        for (Article record : records) {
            articleVoList.add(copy(record,isTag,isAuthor,isBody,isCategory));
        }
        return articleVoList;
    }
    // 具体的转换逻辑
    private ArticleVo copy(Article article,boolean isTag,boolean isAuthor,boolean isBody,boolean isCategory){
        ArticleVo articleVo = new ArticleVo();
        BeanUtils.copyProperties(article,articleVo);
        articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
        // 并不是所有接口都需要标签、作者信息
        if(isTag) {
            Long articleId = article.getId();  // 文章id
            articleVo.setTags(tagService.findTagsByArticleId(articleId));
        }
        if (isAuthor) {
            Long authorId = article.getAuthorId();
            articleVo.setAuthor(userService.findUserById(authorId).getNickname());
        }
        if (isBody) {
            Long bodyId = article.getBodyId();
            articleVo.setBody(findArticleBodyById(bodyId));
        }
        if (isCategory) {
            Long categoryId = article.getCategoryId();
            articleVo.setCategory(categoryService.findCategoryById(categoryId));
        }
        return articleVo;
    }
}
@Service
public class CategoryServiceImpl implements CategoryService {
    @Autowired
    private CategoryMapper categoryMapper;

    // 根据id查询类别
    @Override
    public CategoryVo findCategoryById(Long categoryId) {
        Category category = categoryMapper.selectById(categoryId);
        CategoryVo categoryVo = new CategoryVo();
        BeanUtils.copyProperties(category,categoryVo);
        return categoryVo;
    }
}

16. 线程池的使用

@Service
@RequiredArgsConstructor
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
    private final ThreadService threadService;
    // 查看文章详情
    @Override
    public Result findArticleById(Long articleId) {
        // 1.根据id查询文章信息
        Article article = articleMapper.selectById(articleId);
        // 2.根据bodyId和category做关联查询
        ArticleVo articleVo = copy(article,true,true,true,true);
        // 查看完文章了,新增阅读数,有没有问题呢?
        // 查看完文章之后,本应该直接返回数据,但这时候做了一个更新操作。更新时在数据库中加写锁,阻塞其他的读操作,性能低下(无法解决)
        // 更新操作增加了此次接口的耗时(可以优化)
        // 一旦更新出问题,不能影响查看文章的操作(使用线程池技术)
        // 可以把更新操作放到线程池中去执行,和线程就不相关了
        threadService.updateArticleViewCount(articleMapper,article);
        return Result.success(articleVo);
    }
}
@Component
public class ThreadService {
    // 期望此操作在线程池,执行不会影响原有的主线程
    @Async("taskExecutor")
    public void updateArticleViewCount(ArticleMapper articleMapper, Article article) {
        Integer viewCounts = article.getViewCounts();
        Article articleUpdate = new Article();
        articleUpdate.setViewCounts(viewCounts + 1);
        LambdaUpdateWrapper<Article> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(Article::getId,article.getId());
        updateWrapper.eq(Article::getViewCounts,viewCounts);    // 为了在多线程的环境下保证线程安全(乐观锁,CAS)
        // 相当于 update article set view_count = 100 where view_count = 99 and id = xxx
        articleMapper.update(articleUpdate,updateWrapper);
    }
}
@Configuration
@EnableAsync    // 开启多线程
public class ThreadPoolConfig {
    @Bean("taskExecutor")
    public Executor asyncServiceExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        executor.setCorePoolSize(5);
        // 设置最大线程数
        executor.setMaxPoolSize(20);
        // 配置队列大小
        executor.setQueueCapacity(Integer.MAX_VALUE);
        // 设置默认线程名称
        executor.setThreadNamePrefix("prannt的个人博客");
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(60);
        // 等待所有任务结束后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        // 执行初始化
        executor.initialize();
        return executor;
    }
}

17. 评论列表

17.1 接口说明

接口url:/comments/article/{id}

请求方式:GET

请求参数:

参数名称 参数类型 说明
id long 文章id(路径参数)

17.2 Controller

@RestController
@RequestMapping("comments")
@RequiredArgsConstructor
public class CommentController {
    private final CommentService commentService;

    @GetMapping("article/{id}")
    public Result comments(@PathVariable("id") Long id){
        return commentService.commentsByArticleId(id);
    }
}

17.3 Service

接口

public interface CommentService {
    Result commentsByArticleId(Long id);  // 根据文章id查询所有的评论列表
}
public interface UserService extends IService<User> {
    UserVo findUserVoById(Long id);       // 根据id获取UserVo对象
}

实现类

@Service
@RequiredArgsConstructor
public class CommentServiceImpl implements CommentService {
    private final CommentMapper commentMapper;
    private final UserService userService;

    // 根据文章id查询所有的评论列表
    @Override
    public Result commentsByArticleId(Long id) {
        /*
         * 1.根据文章id查询评论列表(从comment表中查询)
         * 2.根据author_id查询作者信息
         * 3.判断:如果level = 1,要去查询他有没有子评论
         * 4.如果有子评论,根据parent_id进行查询
         */
        LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Comment::getArticleId,id);
        queryWrapper.eq(Comment::getLevel,1);
        List<Comment> comments = commentMapper.selectList(queryWrapper);
        List<CommentVo> commentVoList = copyList(comments);
        return Result.success(commentVoList);
    }

    @Override
    public Result comment(CommentParam commentParam) {
        return null;
    }

    private List<CommentVo> copyList(List<Comment> comments) {
        List<CommentVo> commentVoList = new ArrayList<>();
        for (Comment comment : comments) {
            commentVoList.add(copy(comment));
        }
        return commentVoList;
    }

    private CommentVo copy(Comment comment) {
        CommentVo commentVo = new CommentVo();
        BeanUtils.copyProperties(comment,commentVo);    // 拷贝的是相同属性
        // 作者信息
        Long authorId = comment.getAuthorId();
        UserVo userVo = this.userService.findUserVoById(authorId);   // 根据id查询UserVo对象
        commentVo.setAuthor(userVo);
        // 子评论
        Integer level = comment.getLevel();
        if (1 == level) {
            Long id = comment.getId();
            List<CommentVo> commentVoList = findCommentByParentId(id);
            commentVo.setChildrens(commentVoList);
        }
        // to User 给谁评论
        if (level > 1) {
            Long toUid = comment.getToUid();
            UserVo toUserVo = this.userService.findUserVoById(toUid);
            commentVo.setToUser(toUserVo);
        }
        return commentVo;
    }

    private List<CommentVo> findCommentByParentId(Long id) {
        LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper();
        queryWrapper.eq(Comment::getParentId,id);
        queryWrapper.eq(Comment::getLevel,2);
        return copyList(commentMapper.selectList(queryWrapper));
    }
}
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    private final UserMapper userMapper;

    @Override
    public UserVo findUserVoById(Long id) {
        User user = userMapper.selectById(id);
        if (user == null) {
            user = new User();
            user.setId(1L);
            user.setNickname("prannt");
        }
        UserVo userVo = new UserVo();
        BeanUtils.copyProperties(user,userVo);
        return userVo;
    }
}

18. 评论功能

18.1 接口说明

接口url:/comments/create/change

请求方式:POST

请求参数:

参数名称 参数类型 说明
articleId long 文章id
content String 评论内容
parent long 父评论id
toUserId long 被评论的用户id

18.2 加入拦截器

@Configuration
public class WebConfig  implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    // 配置登录拦截器,告诉springmvc应该拦截谁
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/test")
            	 // 只有登录的用户才能评论
                .addPathPatterns("/comments/create/change");
    }
}

18.3 Controller

@RestController
@RequestMapping("comments")
@RequiredArgsConstructor
public class CommentController {
    private final CommentService commentService;

    @PostMapping("create/change")
    public Result comment(@RequestBody CommentParam commentParam){
        return commentService.comment(commentParam);
    }
}

18.4 Service

接口

public interface CommentService {
    Result comment(CommentParam commentParam);  // 写评论
}

实现类

@Service
@RequiredArgsConstructor
public class CommentServiceImpl implements CommentService {
    private final CommentMapper commentMapper;
    private final UserService userService;

    @Override
    public Result comment(CommentParam commentParam) {
        User user = UserThreadLocal.get();
        Comment comment = new Comment();
        comment.setArticleId(commentParam.getArticleId());
        comment.setAuthorId(user.getId());
        comment.setContent(commentParam.getContent());
        comment.setCreateDate(System.currentTimeMillis());
        Long parent = commentParam.getParent();
        if (parent == null || parent == 0) {
            comment.setLevel(1);
        }else{
            comment.setLevel(2);
        }
        comment.setParentId(parent == null ? 0 : parent);
        Long toUserId = commentParam.getToUserId();
        comment.setToUid(toUserId == null ? 0 : toUserId);
        this.commentMapper.insert(comment);
        return Result.success(null);
    }
}
@Data
public class CommentVo {
    @JsonSerialize(using = ToStringSerializer.class)    // 防止前端精度损失,把id转为string
    private Long id;
    private UserVo author;
    private String content;
    private List<CommentVo> childrens;
    private String createDate;
    private Integer level;
    private UserVo toUser;
}

分布式id比较长,传到前端会有精度损失,必须转为String类型进行传输,就不会有问题了

19. 写文章

19.1 所有文章分类

19.1.1 接口说明

接口url:/categorys

请求方式:GET

请求参数:

19.1.2 Controller

@RestController // 返回json格式数据
@RequestMapping("/categorys")
@RequiredArgsConstructor
public class CategoryController {
    private final CategoryService categoryService;

    @GetMapping
    public Result categories(){
        return categoryService.findAll();
    }
}

19.1.3 Service

接口

public interface CategoryService {
    Result findAll();   // 为写文章提供所有的类别查询
}

实现类

@Service
public class CategoryServiceImpl implements CategoryService {
    @Autowired
    private CategoryMapper categoryMapper;

    // 为写文章提供所有的类别查询
    @Override
    public Result findAll() {
        List<Category> categories = categoryMapper.selectList(new LambdaQueryWrapper<>());
        // 和页面交互的对象
        return Result.success(copyList(categories));
    }

    private List<CategoryVo> copyList(List<Category> categoryList) {
        List<CategoryVo> categoryVoList = new ArrayList<>();
        for (Category category : categoryList) {
            categoryVoList.add(copy(category));
        }
        return categoryVoList;
    }

    private CategoryVo copy(Category category) {
        CategoryVo categoryVo = new CategoryVo();
        BeanUtils.copyProperties(category,categoryVo);
        //id不一致要重新设立
        categoryVo.setId(String.valueOf(category.getId()));
        return categoryVo;
    }
}

19.2 所有文章标签

19.2.1 接口说明

接口url:/tags

请求方式:GET

19.2.2 Controller

@RestController
@RequestMapping("/tags")
@RequiredArgsConstructor
public class TagController {
    private final TagService tagService;

    // 查询所有文章标签
    @GetMapping
    public Result findAll(){
        return tagService.findAll();
    }
}

19.2.3 Service

接口

public interface TagService extends IService<Tag> {
    Result findAll();     // 查询所有文章标签
}

实现类

@Service
@RequiredArgsConstructor
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {
    private final TagMapper tagMapper;

    // 查询所有文章标签
    @Override
    public Result findAll() {
        List<Tag> tags = tagMapper.selectList(new LambdaQueryWrapper<>());
        return Result.success(copyList(tags));
    }

    private List<TagVo> copyList(List<Tag> tagList) {
        List<TagVo> tagVoList = new ArrayList<>();
        for (Tag tag : tagList) {
            tagVoList.add(copy(tag));
        }
        return tagVoList;
    }

    public TagVo copy(Tag tag){
        TagVo tagVo = new TagVo();
        BeanUtils.copyProperties(tag,tagVo);
        return tagVo;
    }
}

19.3 发布文章

19.3.1 接口说明

接口url:/articles/publish

请求方式:POST

请求参数:

参数名称 参数类型 说明
titile String 文章标题
id Long 文章id(编辑有值)
body object({content: “ww”, contentHtml: “ww”}) 文章内容
category {id: 2, avatar: “/category/back.png”, categoryName: “后端”} 文章类别
summary String 文章概述
tags [{id: 5}, {id: 6}] 文章标签

返回数据:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": {"id":12232323}
}

19.3.2 Controller

19.3.3 Service

接口

实现类


文章作者: Prannt
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Prannt !
评论
  目录