本项目是码神之路所使用的博客系统,项目简单,需求明确,容易上手。
spring Boot 练手实战项目说明
项目讲解说明:
- 提供前端工程,只需要实现后端接口即可;
- 项目以单体架构入手,先快速开发,不考虑项目优化,降低开发负担;
- 开发完成后,开始优化项目,提升编程思维能力;
- 比如页面静态化,缓存,云存储,日志等;
- docker部署上线;
- 云服务器购买,域名购买,域名备案等。
技术栈:Spring Boot + Mybatis-Plus + Redis + MySQL
系统架构演变:
随着互联网的发展,网站应用的规模也在不断扩大,进而导致系统架构也在不断变化。从互联网兴起到现在,系统架构大致经历了以下几个过程:
- 单体应用架构
- 垂直应用架构
- 分布式应用架构
- SOA架构
- 微服务架构(主流)
- Service Mesh(服务网格化)
- 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 |
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
中用完的信息,会有内存泄露的风险,为什么?
一个线程会维护一个ThreadLocalMap
,ThreadLocalMap
中有一个key
,有一个value
,key
指向了ThreadLocal
,有引用指向该ThreadLocal
,value
就是存储的相应的值(在本例中,存储的是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
接口
实现类