入门实战---个人博客

个人博客,项目简单,需求明确,容易上手,非常适合做为练手级项目。

项目讲解说明:

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

项目使用技术 :

springboot + mybatisplus+redis+mysql

目录

一. 工程搭建

二.  首页展示

1.文章列表

2. 首页---最热标签

3  首页--最热文章

4. 最新文章显示

 5. 文章归档显示

三. 登录

1. 登录功能实现

 2. 获取用户信息

 3. 退出登录

 四. 用户注册

4.1  注册功能

4.2 登录拦截器

4.3  ThreadLocal----保存用户信息

总结:

ThreadLocal(本地的线程)到底有什么用

五. 文章详情

5.1 文章详情实现

5.2  使用线程池 更新阅读次数

六.  评论列表

七.  评论功能

八.  发布文章 

8.1  获取所有文章类别

8.2 所有文章标签

8.3 发布文章

 8.4 AOP日志

九. 图片上传

十. 导航显示

10.1 查询所有文章分类

 10.2 分类文章列表显示

 10.3  获取所有标签

10.4  获取标签文章列表 

          10.5   归档文章列表

十一.统一缓存处理(优化)



一. 工程搭建

1.1 前端用vscode打开并运行(或者直接运行)

1.先安装node

2.打开项目根目录,地址栏中输入cmd

 3.在命令窗口中输入npm run dev 运行程序

 

npm run dev

运行结果

 1.2 创建maven工程

导入依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <!-- 排除 默认使用的logback  -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.qiniu</groupId>
            <artifactId>qiniu-java-sdk</artifactId>
            <version>[7.7.0, 7.7.99]</version>
        </dependency>
        <!-- log4j2 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.2</version>
        </dependency>
        <!--        md5加密的依赖包-->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/joda-time/joda-time -->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.10.10</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
    </dependencies>
   <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.6.7</version>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.xml</include>
                    <include>**/*.properties</include>
                </includes>
            </resource>
        </resources>
    </build>

1.3 application配置文件配置

#server
server.port= 8888
#应用名称
spring.application.name=hnjm_blog
# datasource
spring.datasource.url=jdbc:mysql://localhost:3306/hnjm_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
#标识表名均为ms_前缀
mybatis-plus.global-config.db-config.table-prefix=ms_
mybatis-plus.type-aliases-package=com.hnjm.pojo
mybatis-plus.mapper-locations=classpath:com/hnjm/mapper/*.xml
#驼峰
mybatis-plus.configuration.map-underscore-to-camel-case=true

注意:需要修改数据库密码、数据库名、应用名称

1.4  Mybatis Plus配置

创建项目结构:

在config中创建配置类,设置分页查询(一般项目都会用到,所以提前配置好),注意@MapperScan(“com.hnjm.mapper”)注解。配置类一定要加@Configuration。

@Configuration
//扫包,将此包下的接口生成代理实现类,并且注册到spring容器中
@MapperScan("com.hnjm.mapper")
public class MyBatisPlusConfig {
    //分页插件
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }
}

1.5  WebMVCConfig配置类

解决不同端口之间的跨域问题
配置类一定要加@Configuration。

@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
  

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

}

1.6  启动类

@SpringBootApplication
@MapperScan("com.hnjm.mapper")
public class BlogApiApplication {

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

}

注意:@MapperScan("com.hnjm.mapper") 扫描所有接口文件 和接口文件上@Mapper作用一样

二.  首页展示

1.文章列表

1.1 接口说明

接口url:/articles

请求方式:POST

请求参数:

参数名称参数类型说明
pageint当前页数
pageSizeint每页显示的数量

返回数据:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id": 1,
            "title": "springboot介绍以及入门案例",
            "summary": "通过Spring Boot实现的服务,只需要依靠一个Java类,把它打包成jar,并通过`java -jar`命令就可以运行起来。\r\n\r\n这一切相较于传统Spring应用来说,已经变得非常的轻便、简单。",
            "commentCounts": 2,
            "viewCounts": 54,
            "weight": 1,
            "createDate": "2609-06-26 15:58",
            "author": "12",
            "body": null,
            "tags": [
                {
                    "id": 5,
                    "avatar": null,
                    "tagName": "444"
                },
                {
                    "id": 7,
                    "avatar": null,
                    "tagName": "22"
                },
                {
                    "id": 8,
                    "avatar": null,
                    "tagName": "11"
                }
            ],
            "categorys": null
        },
        {
            "id": 9,
            "title": "Vue.js 是什么",
            "summary": "Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。",
            "commentCounts": 0,
            "viewCounts": 3,
            "weight": 0,
            "createDate": "2609-06-27 11:25",
            "author": "12",
            "body": null,
            "tags": [
                {
                    "id": 7,
                    "avatar": null,
                    "tagName": "22"
                }
            ],
            "categorys": null
        },
        {
            "id": 10,
            "title": "Element相关",
            "summary": "本节将介绍如何在项目中使用 Element。",
            "commentCounts": 0,
            "viewCounts": 3,
            "weight": 0,
            "createDate": "2609-06-27 11:25",
            "author": "12",
            "body": null,
            "tags": [
                {
                    "id": 5,
                    "avatar": null,
                    "tagName": "444"
                },
                {
                    "id": 6,
                    "avatar": null,
                    "tagName": "33"
                },
                {
                    "id": 7,
                    "avatar": null,
                    "tagName": "22"
                },
                {
                    "id": 8,
                    "avatar": null,
                    "tagName": "11"
                }
            ],
            "categorys": null
        }
    ]
}

1.2 编码实现

1.2.1 控制类

@RestController//JSON数据交互
@RequestMapping("articles")
public class ArticleController {

    @Autowired
    ArticleService articleService;

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

1.2.2 创建实体类

根据数据表结构创建持久化类

(1)Article.java

@Data
public class Article {
    //初始化两个静态变量
    public static final int Article_TOP = 1;
    public static final int Article_Common = 0;
    private Long id;
    private String title;
    private String summary;
    private Integer commentCounts;
    private Integer viewCounts;
    //作者id
    private Long authorId;
    //内容id
    private Long bodyId;
    //类别id
    private Long categoryId;
    //置顶
    private Integer weight = Article_Common;
    //创建时间
    private Long createDate;
}

说明:@Data:为该类提供set、get、和tostring()方法

(2)Tag.java

@Data
public class Tag {

    private Long id;

    private String avatar;

    private String tagName;

}

(3)Category.java

import lombok.Data;

@Data
public class Category {

    private Long id;

    private String avatar;

    private String categoryName;

    private String description;
}

 (5)SysUser.java

import lombok.Data;

@Data
public class SysUser {

    private Long id;

    private String account;

    private Integer admin;

    private String avatar;

    private Long createDate;

    private Integer deleted;

    private String email;

    private Long lastLogin;

    private String mobilePhoneNumber;

    private String nickname;

    private String password;

    private String salt;

    private String status;
}

(5)Result类

在Vo包中创建 Resoult类,定义了两个静态方法,分别表示请求成功、请求失败。

vo:为返回前端实际数据而创建的类

@Data
@AllArgsConstructor//全构造方法
@NoArgsConstructor
public class Result {

    private boolean success;

    private Integer code;

    private String msg;

    private Object data;


    public static Result success(Object data) {
        return new Result(true,200,"success",data);
    }
    public static Result fail(Integer code, String msg) {
        return new Result(false,code,msg,null);
    }
}

(6)ArticleVo.java

@Data
public class ArticleVo {//vo是和页面交互的数据
    @JsonSerialize(using = ToStringSerializer.class)
    private Long 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;

}

说明:java中long数据能表示的范围比js中number大,在跟前端交互时,这样也就意味着部分数值在js中存不下(变成不准确的值)。
解决办法可以这样:
使用fastjson的ToStringSerializer注解,让系统序列化时,保留相关精度。

(7)TagVo.java

@Data
public class TagVo {

    private Long id;

    private String tagName;

    private String avatar;
}

(8)CategoryVo.java

@Data
public class CategoryVo {

    private Long id;

    private String avatar;

    private String categoryName;

    private String description;
}

(9)在param文件中创建PageParams.java

根据接口文档中,前端传给后端接口的json数据,我们都封装为param类,方便操作。
PageParams 类定义了分页查询的page和pageSize,分别对应分页查询中的start和size。 

@Data
public class PageParams {

    private int page = 1;

    private int pageSize = 5;

    private Long categoryId;

    private Long tagId;

    private String year;

    private String month;

    public String getMonth(){
        if (this.month != null && this.month.length() == 1){
            return "0"+this.month;
        }
        return this.month;
    }
}

 1.2.3 Mapper接口

此处定义了三个mapper接口,分别为ArticleMapper、SysUserMapper、TagMapper。

public interface ArticleMapper extends BaseMapper<Article> {
}
public interface TagMapper extends BaseMapper<Tag> {
    List<Tag> findTagsByArticleId(Long articleId);
}
public interface SysUserMapper extends BaseMapper<SysUser> {
} 

说明:

因为tag和article有一张对应的表,所以要查询article对应的tag时,需要设计到多表的查询,但是,mybatisplus不支持多表查询,所以需要自己写mapper.xml文件。
TagMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!--MyBatis配置文件-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.hnjm.mapper.TagMapper">

    <select id="findTagsByArticleId" parameterType="long" resultType="Tag">
        select id,avatar,tag_name as tagName from ms_tag
        <where>
            id in
            (select tag_id from ms_article_tag where article_id = #{articleId})
        </where>
    </select>
</mapper>

注意:mapper.xml文件所在目录要和mapper对应,本次工程都在com.hnjm.mapper下。
可以在application.properties中mybatis-plus开启驼峰命名
mybatis-plus.configuration.map-underscore-to-camel-case=true
这样SQL语句就不需要as别名。

1.2.4 Service

该阶段定义了三个service接口类。

public interface ArticleService {
    //分页查询文章列表
    Result listArticle(PageParams pageParams);
}
public interface SysUserService {
    //根据文章id获取用户信息
    SysUser findUserById(Long id);
}
public interface TagService {
    //通过文章id查询ui赢得标签,有一张表专门映射文章id和标签id
    List<TagVo> findTagsByArticleId(Long articleId);
}

serviceImpl类

(1)ArticleServiceImpl
该实现类目前实现了文章分页查询。

知识点:
1)Page类定义分页对象;
2)LambdaQueryWrapper定义查询wrapper;
3)selectPage()函数返回的是Page对象,通过getRecords获得Article对象列表。
4)copy和copyList函数实现Article到ArticleVo的转换
5)BeanUtils.copyProperties(article,articleVo),可以实现对象之间的复制,相同属性名复制,不同属性名为null。
 

@Service
public class ArticleServiceImpl implements ArticleService{
   @Autowired
   ArticleMapper articleMapper;
   @Autowired
   TagService tagService;
   @Autowired
   SysUserService sysUserService;
   //分页查询
   @Override
   public Result listArticle(PageParams pageParams) {
       //分页
       Page<Article> page = new Page<>(pageParams.getPage(),pageParams.getPageSize());

       LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
       //先置顶排序由属性weight决定,后按照时间排序
       wrapper.orderByDesc(Article::getWeight,Article::getCreateDate);
       Page<Article> articlePage = articleMapper.selectPage(page, wrapper);
       //文章列表
       List<Article> records = articlePage.getRecords();
       //因为页面展示出来的数据不一定和数据库数据一致,所以我们要做一个拷贝
       //将查出的数据复制到articleVo中实现解耦,vo和页面数据交换
       List<ArticleVo> articleVo = copyList(records,true,true);

       return Result.success(articleVo);
   }

   //copyList实现
   private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor) {
       ArrayList<ArticleVo> articleVos = new ArrayList<>();
       for(Article article:records){
           articleVos.add(copy(article,isTag,isAuthor));
       }
       return articleVos;
   }
   //这个方法是主要点是BeanUtils,又Spring提供的,专门用来拷贝的,Article和articlevo相同属性的拷贝过来返回
   private ArticleVo copy(Article article,boolean isTag,boolean isAuthor){
       ArticleVo articleVo = new ArticleVo();
       BeanUtils.copyProperties(article,articleVo);
       //joda包中的DataTime.toString方法将Article的Long日期属性转为ArticleVo中的字符串日期属性
       articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
       //是否显示标签和作者
       if(isTag){
           articleVo.setTags(tagService.findTagsByArticleId(article.getId()));
       }
       if(isAuthor){
           articleVo.setAuthor(sysUserService.findUserById(article.getAuthorId()).getNickname());
       }
       return articleVo;
   }
}

(2) TagMapperServiceImpl
此处也会涉及对象之间的复制,原理同ArticleServiceImpl,这里实现的是Tag类复制为TagVo类。

@Service
public class TagServiceImpl implements TagService{
   @Autowired
   TagMapper tagMapper;
   @Override
   public List<TagVo> findTagsByArticleId(Long articleId) {
       List<Tag> tags = tagMapper.findTagsByArticleId(articleId);
       return copyList(tags);
   }
   //copyList实现
   private List<TagVo> copyList(List<Tag> tags) {
       ArrayList<TagVo> tagVos = new ArrayList<>();
       for(Tag tag : tags){
           tagVos.add(copy(tag));
       }
       return tagVos;
   }
   private TagVo copy(Tag tag){
       TagVo tagVo = new TagVo();
       //BeanUtils,copyProperties用于类之间的复制,相同字段复制,不同字段为null
       BeanUtils.copyProperties(tag,tagVo);
       return tagVo;
   }

}

(3)SysUserServiceImpl.java:根据id查找用户信息


@Service
public class SysUserServiceImpl implements SysUserService{
   @Autowired
   SysUserMapper sysUserMapper;
   @Override
   public SysUser findUserById(Long id) {
       return sysUserMapper.selectById(id);
   }
}

运行结果

 也可以在postman 中测试

 

2. 首页---最热标签

2.1 接口文档

接口url:/tags/hot

请求方式:GET

请求参数:无

返回数据:

思路分析:

思路:
1)首先在ms_article_tag表操作,通过tag_id分组并排序获得前几名,返回一个tag_id列表。
2)然后根据tag_id列表查询ms_tag表中对应的id和tagName将查询结果返回为TagVo对象(作者使用的是返回为Tag对象,但是由于前端展示的都是vo类,所以我们转换为TagVo)。

 2.2 编码实现

2.2.1 controller

@RestController
@RequestMapping("/tags")
public class TagController {
    @Autowired
    TagService tagService;

    //tags/hot相应最热标签tag对象
    @RequestMapping("/hot")
    public Result hot() {
        int limit = 6;//最热六个
        return tagService.hots(limit);
    }
}

2.2.2 TagMapper.java  

public interface TagMapper extends BaseMapper<Tag> {
    //根据id 查找tag列表
    List<Tag> findTagsByArticleId(Long articleId);
    //查询最热标签前limit条
    List<Long> findHotsTagId(int limit);
     //通过最热标签tagid查询最热tags
    List<TagVo> findTagsByIds(@Param("tagIds") List<Long> tagIds);
}

TagMapper对应的Mapper文件
重点:
1)findHotsTagId涉及分组并排序
2)findTagsByIds涉及到foreach标签TagMapper.xml

 <!--    查询最热标签id,提取前limit个-->
    <select id="findHotsTagId" parameterType="int" resultType="long">
        select tag_id from ms_article_tag
        group by tag_id
        order by count(*) limit #{limit}
    </select>

    <!--    根据最热标签id查询对应tag对象-->
    <select id="findTagsByIds" resultType="com.hnjm.vo.TagVo" parameterType="list">
        select id, tag_name from ms_tag
        where id in
        <foreach collection="tagIds" item="tagId" separator="," open="(" close=")">
            #{tagId}
        </foreach>
    </select>

2.2.3  TagService.java

Result hots(int limit);

  TagServiceImp.java

 //查询若干个最热标签功能
    public Result hots(int limit){
        List<Long> hotsTagId = tagMapper.findHotsTagId(limit);
        //判断hotsTagId是否为空
        if(CollectionUtils.isEmpty(hotsTagId)){
            return Result.success(Collections.emptyList());
        }
        List<TagVo> tagsList = tagMapper.findTagsByIds(hotsTagId);
        return Result.success(tagsList);
    }

2.3   统一异常处理

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

//对加了@Controller的方法进行拦截处理,AOP实现
@ControllerAdvice
public class AllExceptionHandler {
    //进行异常处理,处理Exception.class异常
    @ExceptionHandler(Exception.class)
    //返回json数据
    @ResponseBody
    public Result doExceptionHandler(Exception e) {
        return Result.fail(-999,"系统异常,抱歉!");
    }
}

说明:

1)@ResponseBody  :返回jsojn数据
2)@ControllerAdvice:对加了@Controller的方法进行拦截处理,AOP实现
3)@ExceptionHandler(Exception.class)://进行异常处理,处理Exception.class异常

运行结果

3  首页--最热文章

3.1  接口文档

接口url:/articles/hot

请求方式:POST

返回数据:

 3.2 编码实现

思路:原理同最热标签查询。根据view_count排序,选择出最热文章。

(1)Articlecontroller

    //显示最热文章
    @PostMapping("/hot")
    public Result hotArticle(){
        int limit = 3;
        return articleService.hotArticle(limit);
    }

(2)ArticleService

Result hotArticle(int limit);

(3)ArticleServiceImpl

//根据view_counts字段查询最热文章
    @Override
    public Result hotArticle(int limit) {
        LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
        wrapper.orderByDesc(Article::getViewCounts)
                .select(Article::getId,Article::getTitle)
                .last("limit "+limit);
        List<Article> articles = articleMapper.selectList(wrapper);
        return Result.success(copyList(articles,false,false));
    }

  .last("limit "+limit); limit后面一定要加空格

运行结果

4. 最新文章显示

4.1 接口说明

接口url:/articles/new

请求方式:POST

返回数据

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id": 1,
            "title": "springboot介绍以及入门案例",
        },
        {
            "id": 9,
            "title": "Vue.js 是什么",
        },
        {
            "id": 10,
            "title": "Element相关",
            
        }
    ]
}

4.2  编程实现

思路:原理和最热文章完全相同,只是通过create_date字段排序,选择出最新

(1)ArticleController

//显示最新文章
    @PostMapping("/new")
    public Result newArticle(){
        int limit = 3;
        return articleService.newArticle(limit);
    }

(2)ArticleService

    Result newArticle(int limit);

(3)ArticleServiceImpl

/最新文章
    @Override
    public Result newArticle(int limit) {
        LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
        wrapper.orderByDesc(Article::getCreateDate)
                .select(Article::getId,Article::getTitle)
                .last("limit "+limit);
        List<Article> articles = articleMapper.selectList(wrapper);
        return Result.success(copyList(articles,false,false));
    }

运行结果

 5. 文章归档显示

5.1 接口说明

接口url:/articles/listArchives

请求方式:POST

返回数据:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "year": "2021",
            "month": "6",
            "count": 2
        }
            
    ]
}

5.2  编程实现

思路:

根据接口文档分析,归档查询涉及到数据库内部函数Year、Month,所以MybatisPlus不能实现,需要通过mapper.xml文件实现。
文章归档返回的对象为Archives(档案)类,将这个类放入vo包中。
(1)  Archives.java

@Data
public class Archives {
    private Integer year;
    private Integer month;
    private Long count;

}

(2) ArticleController中添加方法

//文章归档
    @PostMapping("/listArchives")
    public Result listArchives(){
        return articleService.listArchives();
    }

(3)ArticleMapper.java 中添加方法

List<Archives> listArchives();

对应的ArticleMapper.xml文件中添加方法

<?xml version="1.0" encoding="UTF-8" ?>
<!--MyBatis配置文件-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hnjm.mapper.ArticleMapper">
    <!--FROM_UNIXTIME 格式化MYSQL时间戳函数
      先将bigint类型转换为时间数据类型 from_unixtime(1488470400000/1000) 
    -->
    <select id="listArchives" resultType="com.hnjm.vo.Archives">
        select YEAR(FROM_UNIXTIME(create_date/1000)) as year,
          MONTH(FROM_UNIXTIME(create_date/1000)) as month,
          count(*) as count
          from ms_article
          group by year,month
    </select>

说明:数据库中create_date 为bigint 13位,直接year()不行,需要先转date型后year()。ROM_UNIXTIME 格式化MYSQL时间戳函数

将bigint类型转换为时间数据类型 from_unixtime(1488470400000/1000)

(4)  ArticleService与ArticleServiceImpl

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

运行结果:

三. 登录

1. 登录功能实现

1.1  接口说明

接口url:/login

请求方式:POST

请求参数:

参数名称参数类型说明
accountstring账号
passwordstring密码

 返回数据:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": "token"
}

1.2  编程实现

(1)工具类:在util中添加JWT工具类

JWT

登录使用JWT技术。

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

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

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

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

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

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

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

public class JWTUtils {
    private static final String jwtToken = "mszlu!@#$$";

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

    public static Map<String, Object> checkToken(String token){
        try {
            Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
            return (Map<String, Object>) parse.getBody();
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;

    }

}

(2)LoginParam (根据接口文档中参数定义)

import lombok.Data;

@Data
public class LoginParam {
    private String account;
    private  String password;
    private String  nickname;
}

(3)LoginController

@RestController
@RequestMapping("/login")
public class LoginController {

    @Autowired
    private LoginService loginService;
    //登录
    @PostMapping
    public Result login(@RequestBody LoginParam loginParam){
        return loginService.login(loginParam);
    }
}

(3)LoginService(登录由login去处理)

  Result login(LoginParam loginParam);

(4)需要开启redis服务器,并在配置文件中加载

 在application.properties配置文件中加上

#redis配置
spring.redis.host=localhost
spring.redis.port=6379

 (5)ErrorCode.class
因为每次登录都有错误验证,所以在vo包中定义了一个ErrorCode类
ErrorCode.class

public enum ErrorCode {

    PARAMS_ERROR(10001,"参数有误"),
    ACCOUNT_PWD_NOT_EXIST(10002,"用户密码不存在喔!"),
    TOKEN_ERROR(10003,"Token不合法"),
    ACCOUNT_EXIST(10004,"账号已存在"),
    NO_PERMISSION(70001,"无访问权限"),
    SESSION_TIME_OUT(90001,"会话超时"),
    NO_LOGIN(90002,"未登录"),;


    private int code;
    private String msg;

    ErrorCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private RedisTemplate<String,String> redisTemplate;
    //md5加密使用到的盐
    private static final String salt="mszlu!@#";
    @Override
    public Result login(LoginParam loginParam) {
        /*
         * 1、检查参数是否合法
         * 2、根据用户名和密码检查ms_sys_user表中对应的account、password字段
         * 3、如果不存在,登录失败
         * 4、如果存在,使用jwt,生成token,返回给前端
         * 5、token放在redis中,redis映射token和user信息
         * 设置过期时间,先认证token是否合法,再认证redis是否存在
         * */
        String account = loginParam.getAccount();
        String password = loginParam.getPassword();

        //用户名或者密码为空
        if(StringUtils.isBlank(account)||StringUtils.isBlank(password)){
            //提前写好的错误编码类,方便使用
            return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
        }

        //这里使用的DigestUtils是来自于commons-codec包的,需要外部导入依赖
        //密码加盐,因为数据库中的密码是经过盐加密的
        String pwd = DigestUtils.md5Hex(password+salt);
        System.out.println(pwd);
        //验证时需要通过sysUserService查询到对应用户并返回部分用户信息
        SysUser user = sysUserService.findUser(account, pwd);
        //用户不存在
        if(user == null){//错误信息调用错误码
            return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());
        }
        //用户密码正确,给用户返回一个JWT (token)
        String token = JWTUtils.createToken(user.getId());
        //存入redis,要确保已开启redis
        redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(user),1, TimeUnit.DAYS);
        return Result.success(token);
    }
}

在SysUserService中添加方法

//根据账号、密码查找
    SysUser findUser(String account, String password);

SysUserServiceImpl

    @Override
    public SysUser findUser(String account, String password) {
            LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper();
            queryWrapper.eq(SysUser::getAccount,account);
            queryWrapper.eq(SysUser::getPassword,password);
            queryWrapper.select(SysUser::getId,SysUser::getAccount,SysUser::getAvatar,SysUser::getNickname);
            queryWrapper.last("limit 1");//加快速度
            SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
            return sysUser;
        }

最后在postman中测试,切记不要在前端测试,因为显示用户名信息没有写完,会死掉的

 

 2. 获取用户信息

2.1 接口说明

接口url:/users/currentUser

请求方式:GET

请求参数:

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

返回数据

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": {
        "id":1,
        "account":"1",
        "nickaname":"1",
        "avatar":"ss"
    }
}

2.2  编码实现

思路:登陆后用户的token会存放在浏览器本地,当用户登陆时会在请求头携带token,token中含有用户的id,用户的信息实际存放在redis中。

(1)UserController

@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    SysUserService sysUserService;

    //用户信息展示请求
    //token放在请求头中,所以要获取token需要@RequestHeader("Authorization")注解
    @GetMapping("/currentUser")
    public Result currentUser(@RequestHeader("Authorization") String token){
        return sysUserService.findUserByToken(token);
    }
}

(2)SysUserService

Result findUserByToken(String token);
SysUser checkToken(String token);//从reddis中获取用户信息

(3)LoginUserVo(根据接口文档中定义类)

   该类用于返回用户信息,由于页面展示用户部分信息

import lombok.Data;

@Data
public class LoginUserVo {
    private  Long id;

    private String account;

    private String nickname;

    private String avatar; //头像
}

(4) SysUserServiceImpl

    @Service
public class SysUserServiceImpl implements SysUserService {
    @Autowired
    private  SysUserMapper sysUserMapper;
    @Override
    public SysUser findUserById(Long id) {
        return sysUserMapper.selectById(id);
    }

    @Override
    public SysUser findUser(String account, String password) {
        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper();
        queryWrapper.eq(SysUser::getAccount, account);
        queryWrapper.eq(SysUser::getPassword, password);
        queryWrapper.select(SysUser::getId, SysUser::getAccount, SysUser::getAvatar, SysUser::getNickname);
        queryWrapper.last("limit 1");//加快速度
        SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
        return sysUser;
    }
    @Autowired
    private RedisTemplate<String,String> redisTemplate;
    @Override
    public Result findUserByToken(String token) {
        /*
         *1、token合法校验,是否为空,解析是否成功, Redis是否存在
         *如果校验失败,返回错误
         * 如果成功,返回对应的结果 LogininUserVo
         *  */
        SysUser user=checkToken(token);
        LoginUserVo loginUserVo = new LoginUserVo();
        loginUserVo.setAccount(user.getAccount());
        loginUserVo.setId(user.getId());
        loginUserVo.setAvatar(user.getAvatar());
        loginUserVo.setNickname(user.getNickname());
        return Result.success(loginUserVo);
    }
    public SysUser checkToken(String token) {
        if(StringUtils.isBlank(token)){//如果对象为空
            return null;
        }
        Map<String, Object> map = JWTUtils.checkToken(token);//解析token
        if(map == null){
            return null;
        }
        String  userJson= redisTemplate.opsForValue().get("TOKEN_"+token);
        if(StringUtils.isBlank(userJson)){
            return null;
        }
        //token解析成功,并且redis存在
        //JSON.parseObject将json对象转为SysUser对象
        SysUser sysUser=JSON.parseObject(userJson,SysUser.class);//将json对象转换为use
        return sysUser;
    }

}

运行结果

 3. 退出登录

3.1 接口文档

接口url:/logout

请求方式:GET

请求参数:

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

返回数据:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": null
}

 3.2 编码实现

思路:退出登录就是删除redis中的toke

(1)LoginOutController

@RestController
@RequestMapping("/logout")
public class LoginOutController {

    @Autowired
    private LoginService loginService;
    @GetMapping
    public Result logout(@RequestHeader("Authorization") String token){
        return loginService.logout(token);
    }
}

(2)LoginService和LoginServiceImpl

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

运行结果:

 四. 用户注册

4.1  注册功能

4.1.1  接口说明

接口url:/register

请求方式:POST

请求参数:

参数名称参数类型说明
accountstring账号
passwordstring密码
nicknamestring昵称

返回数据:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": "token"
}

4.1.2  编码实现

(1)RegisterController

@RestController
@RequestMapping("/register")
public class RegisterController {
    @Autowired
    LoginService loginService;
    //注册功能,返回数据为token
    @PostMapping
    public Result register(@RequestBody LoginParam loginParam){
        return loginService.register(loginParam);
    }
}

 (2)注册功能在LoginService实现

   LoginService与LoginServiceImpl

Result register(LoginParam loginParam);

 思路:
1)判断参数是否合法;
2)判断账户是否已经存在;
3)若合法并且不存在,则创建新用户;
4)生成token;
5)token即用户信息存入redis;
6)注意:在SysServiceImpl中设置事务,一旦出现问题,就回滚;
7)返回给前端token.

LoginServiceImpl

   @Override
    public Result register(LoginParam loginParam) {
        /*1、判断参数是否合法
         * 2、判断账户是否存在,存在则返回已经注册
         * 3、不存在,注册用户
         * 4、生成token
         * 5、存入redis,并返回
         * 6、注意加上事务,一旦中间过程一旦出现问题,注册的用户 需要回滚
         * */
        String account = loginParam.getAccount();
        String password = loginParam.getPassword();
        String nickname = loginParam.getNickname();
        //用户参数为空
        if(StringUtils.isBlank(account)||StringUtils.isBlank(password)||StringUtils.isBlank(nickname)){
            return Result.fail(ErrorCode.PARAMS_ERROR.getCode(), ErrorCode.PARAMS_ERROR.getMsg());
        }
        SysUser sysUser = sysUserService.findUserByAccount(account);
        //用户已经存在
        if(sysUser!=null){
            return Result.fail(ErrorCode.ACCOUNT_EXIST.getCode(), "账号已经被注册");
        }
        //创建用户,ID默认为自增
        sysUser =new SysUser();
        sysUser.setAccount(account);                                   //账户名
        sysUser.setNickname(nickname);                                  //昵称
        sysUser.setPassword(DigestUtils.md5Hex(password+salt));  //密码加盐md5
        sysUser.setCreateDate(System.currentTimeMillis());              //创建时间
        sysUser.setLastLogin(System.currentTimeMillis());               //最后登录时间
        sysUser.setAvatar("/static/img/logo.b3a48c0.png");              //头像
        sysUser.setAdmin(1);                                             //管理员权限
        sysUser.setDeleted(0);                                             //假删除
        sysUser.setSalt("");                                                //盐
        sysUser.setStatus("");                                              //状态
        sysUser.setEmail("");                                               //邮箱
        this.sysUserService.save(sysUser);

        //生成token
        String token = JWTUtils.createToken(sysUser.getId());
        //注册成功 使用Jwt生成Token ,返回Token 并 存到redis
        redisTemplate.opsForValue().set("TOKEN_"+token,JSON.toJSONString(sysUser),1,TimeUnit.DAYS);
        return Result.success(token);
    }

上面代码中涉及到两个方法
sysUserService.findUserByAccount(account)查询用户
sysUserService.save(sysUser)创建用户

 @Override
    public SysUser findUserByAccount(String account) {
        LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysUser::getAccount,account)
                .last("limit 1");
        return this.sysUserMapper.selectOne(wrapper);
    }
    @Override
    public void save(SysUser sysUser) {
        //保存用户id会自动生成
        //默认生成分布式id,采用雪花算法
        //mybatis-plus
        sysUserMapper.insert(sysUser);
    }

运行结果:

4.2 登录拦截器

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

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

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

4.1  定义拦截器LoginInterceptor

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    public SysUserService sysUserService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        /*1、需要判断请求的接口上是否是HandleMethod即controller方法
         * 2、判断token是否为空,为空未登录
         * 3、不为空,登陆验证(通过SysUserService中的checkToken方法)
         * 4、如果认证成功,则放行
         * */
        if(!(handler instanceof HandlerMethod)){
            //拦截器是拦截的controller中的方法,controller的方法其实就是一个Handler
            //handler可能是RequestResourceHandle(访问资源handle),即可能是访问静态资源的方法
            //解释:controller对应HandlerMethod,所以拦截器只拦截HandlerMethod
            return true;
        }
        //获取token
        String token = request.getHeader("Authorization");
        //日志问题,需要导入lombok下的@slf4
        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===================");

        //token为空,不放行
        if(StringUtils.isBlank(token)){
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
            //设置返回消息格式
            response.setContentType("application/json;charset=utf8");
            //返回json信息
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        //token不为空,去做认证

        SysUser sysUser =sysUserService.checkToken(token) ;
        //用户不存在,即认证失败
        if(sysUser == null){
            System.out.println("没有该用户");
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
            response.setContentType("application/json;charset=utf8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        //登陆验证成功,放行
        return true;
    }
}

使拦截器生效,必须要在WebMVCConfig添加配置

  @Autowired
    private LoginInterceptor loginInterceptor;
@Override
    public void addInterceptors(InterceptorRegistry registry) {
        //配置拦截接口,此处配置为test,用于测试
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/test");
    }

创建测试类

@RestController
@RequestMapping("/test")
public class TestController {
    @RequestMapping
    public Result test(){
        return Result.success(null);
    }
}

4.3  ThreadLocal----保存用户信息

引入ThreadLocal的目的是为实现在Controller层获得用户信息。

ThreadLocal简介:多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。

实现步骤分析:
  1)在登录拦截放行前将用户信息存入ThreadLocal中;
  2)在Controller中获取用户信息;
  3)在Controller方法执行完后删除用户信息;

实现过程
(1)在util包中创建UserThreadLocal类


public class UserThreadLocal {
    //    声明为私有,即每个线程有自己的ThreadLocal变量
    private UserThreadLocal() {
    }
    //    实例化一个ThreadLocal的类,即启用
    private static final ThreadLocal<SysUser> LOCAL = new ThreadLocal<>();
    //存入用户信息
    public static void put(SysUser sysUser) {
        LOCAL.set(sysUser);
    }
    //获取用户信息
    public static SysUser get() {
        return LOCAL.get();
    }
    //删除用户信息
    public static void remove() {
        LOCAL.remove();
    }
}

说明:UserThreadLocal的set方法用于存入用户信息

           UserThreadLocal的get方法用于获取用户信息

(2)在登录拦截器LoginInterceptor中应用,即登陆时set存入用户信息。

(3)在test方法中测试结果

 

 (4) 在拦截器中添加方法

//controller方法处理完之后的操作,要将ThreadLocal释放掉,否则会内存泄漏
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserThreadLocal.remove();
    }

总结:

ThreadLocal(本地的线程)到底有什么用

比如我们发出一个请求,当你启动某一个进程的时候,你让他和你对应的进程进行绑定的话,会深入的绑定到一起(以达到绑定用户信息的目的)。
为什么在那个后面一定要删除,因为一旦内存泄漏是很严重的

五. 文章详情

5.1 文章详情实现

5.1.1   接口说明

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

请求方式:POST

请求参数:

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

返回数据:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": {
        "id": 1,
        "title": "springboot介绍以及入门案例",
        "summary": "介绍",
        "commentCounts": 0,
        "viewCounts": 60,
        "weight": 0,
        "createDate": "2609-06-26 15:58",
        "author": "李四",
        "body": {
            "content": "111"
        },
        "tags": [
            {
                "id": 5,
                "tagName": "springboot"
            },
            {
                "id": 7,
                "tagName": "springmvc"
            },
            {
                "id": 8,
                "tagName": "11"
            }
        ],
        "category": {
            "id": 2,
            "avatar": "/category/back.png",
            "categoryName": "后端"
        }
    }
}

5.1.2   编程实现

思路:

点击文章标题进入到文章内容显示页面,此时文章内容增加了分类(category)和内容(body)两个部分,所以ArticleVo代码要增加两个字段。

(1)根据数据articlebody创建类ArticleBody

@Data
public class ArticleBody {

    private Long id;
    private String content;
    private String contentHtml;
    private Long articleId;
}

(2)根据返回数据类型设置ArticleBodyVo

import lombok.Data;

@Data
public class ArticleBodyVo {
    private String content;
}

(3)根据表Category.java

@Data
public class Category {
    private Long id;

    private String avatar;

    private String categoryName;

    private String description;
}

 CateGoryVo.java

@Data
public class CategoryVo {
    private Long id;

    private String avatar;

    private String categoryName;

}

(4)ArticleVo中添加两个属性

 (5) 在Artictroller中添加方法

  //文章详情
    @PostMapping("/view/{id}")
    public Result findArticleById(@PathVariable("id") Long id){
        return articleService.findArticleById(id);
    }

(6)在ArticleService和ArticleServiceImpl中添加方法

Result findArticleById(Long id);

 @Override
        public Result findArticleById(Long id) {
            /*
             * 1、根据id获得article对象
             * 2、根据bodyId和categoryId去做关联查询
             * */
            Article article = articleMapper.selectById(id);
            return Result.success(copy(article,true,true,true,true));
        }
         //copyList实现
        private List<ArticleVo> copyList(List<Article> records, boolean isTag, boolean isAuthor) {
            ArrayList<ArticleVo> articleVos = new ArrayList<>();
            for(Article article:records){
                articleVos.add(copy(article,isTag,isAuthor,false,false));
            }
            return articleVos;
        }
        //copyList重载
        private List<ArticleVo> copyList(List<Article> records, boolean isTag, boolean isAuthor,boolean isBody,boolean isCategory) {
            ArrayList<ArticleVo> articleVos = new ArrayList<>();
            for(Article article:records){
                articleVos.add(copy(article,isTag,isAuthor,isBody,isCategory));
            }
            return articleVos;
        }
        //这个方法是主要点是BeanUtils,又Spring提供的,专门用来拷贝的,Article和articlevo相同属性的拷贝过来返回
        private ArticleVo copy(Article article,boolean isTag,boolean isAuthor,boolean isBody,boolean isCategory){
            ArticleVo articleVo = new ArticleVo();
            BeanUtils.copyProperties(article,articleVo);
            //joda包中的DataTime.toString方法将Article的Long日期属性转为ArticleVo中的字符串日期属性
            articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
            //是否显示标签和作者
            if(isTag){
                articleVo.setTags(tagService.findTagsByArticleId(article.getId()));
            }
            if(isAuthor){
                articleVo.setAuthor(sysUserService.findUserById(article.getAuthorId()).getNickname());
            }
            if (isBody){
                ArticleBodyVo articleBody = findArticleBody(article.getId());
                articleVo.setBody(articleBody);
            }
            if(isCategory){
                CategoryVo categoryVo=categoryService.findCategoryById(article.getCategoryId());
                articleVo.setCategory(categoryVo);
            }
            return articleVo;
        }
        //根据文章id查找文章内容
        private ArticleBodyVo findArticleBody(Long articleId) {
            LambdaQueryWrapper<ArticleBody> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(ArticleBody::getArticleId,articleId);
            ArticleBody articleBody = articleBodyMapper.selectOne(queryWrapper);
            ArticleBodyVo articleBodyVo = new ArticleBodyVo();
            articleBodyVo.setContent(articleBody.getContent());
            return articleBodyVo;
        }

重点说明:copy函数改写,之前的copy函数只有三个参数,此时还要加两个参数内容和分类

(7) 添加 ArticleBodyMapper.java

public interface ArticleBodyMapper extends BaseMapper<ArticleBody> {

}

(8)创建CategoryMapper,CategoryService和CategoryServiceImpl

public interface CategoryMapper extends BaseMapper<Category> {
}
public interface CategoryService {
    CategoryVo findCategoryById(Long categoryId);
}
@Service
public class CategoryServiceImpl implements CategoryService {

    @Autowired
    CategoryMapper categoryMapper;
    @Override
    public CategoryVo findCategoryById(Long categoryId) {
        Category category = categoryMapper.selectById(categoryId);
        CategoryVo categoryVo = new CategoryVo();
        BeanUtils.copyProperties(category,categoryVo);
        return categoryVo;
    }
}

运行结果:

问题总结:好多出现查不到文章详情,id号后几位变成0,由于数据库中id都是使用的Long,并且所有的Long在数据库中数值位数都设置为20,当数据以json形式传给前端时,前端JavaScript解析20位数的Long会出现溢出,因为JavaScript无法解析过长的数值。

原理:
后端使用64位存储长整数(long),最大支持9223372036854775807
前端的JavaScript使用53位来存放,最大支持9007199254740992,超过最大值的数,可能会出现问题(得到的溢出后的值)
解决方法:降低数据库中Long表示的Id字段的位数,由20降为15,并且将id的数值也修改为位数小于或等于15位的数值。

解决方法:

 将数据库中长度修改为15

 总结:文章详情展示这个模块业务逻辑比较简单,只是涉及了太多实体类和VO类,在这里画一个图用于理解它们之间的关系

5.2  使用线程池 更新阅读次数

查完文章了,如果新增阅读数,有没有问题呢?
当然有啊!本应该直接返回数据,这时候做了一个更新操作,更新时间时加写锁,阻塞其他的读操作,更新功能就会比较低,
而且更新增加了此次接口的耗时,一旦更新出问题,直接影响其他操作:看文章呀什么的
那要怎么样去优化呢?,---->这时就想到了线程池
可以把更新操作扔到线程池里面,就不会影响了,和主线程就不相关了

(1)在service包中创建ThreadService接口

//多线程实现文章阅读数++
@Component
public class ThreadService {
    //Async注解用于表示方法需要异步调用
    @Async("taskExecutor")
    public void updateArticleViewCount(ArticleMapper articleMapper, Article article){
        int viewCounts = article.getViewCounts();
        Article articleUpdate = new Article();
        articleUpdate.setViewCounts(viewCounts+1);

        LambdaUpdateWrapper<Article> updateWrapper = new LambdaUpdateWrapper<>();
        //更新的条件时id和viewCount要对应,即由id和viewCount确定唯一的文章
        updateWrapper.eq(Article::getId,article.getId())
                .eq(Article::getViewCounts,article.getViewCounts());

        articleMapper.update(articleUpdate,updateWrapper);
        //异步操作实现
        try {
            Thread.sleep(2000);
            System.out.println("更新完成!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

(2)设置线程池配置文件ThreadPoolConfig

@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.setKeepAliveSeconds(60);
        // 设置线程名称
        executor.setThreadNamePrefix("Lum博客");
        //等待所有任务及结束后关闭线程
        executor.setWaitForTasksToCompleteOnShutdown(true);
        //执行初始化
        executor.initialize();

        return executor;

    }
}

 (3)在ArticleServiceImpl中查看文章详情方法内加入更新viewCount操作

 运行结果:需要刷新后才能看到结果,可以在前端中优化代码

六.  评论列表

6.1 接口说明

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

请求方式:GET

请求参数:

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

 返回数据:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id": 53,
            "author": {
                "nickname": "李四",
                "avatar": "http://localhost:8080/static/img/logo.b3a48c0.png",
                "id": 1
            },
            "content": "写的好",
            "childrens": [
                {
                    "id": 54,
                    "author": {
                        "nickname": "李四",
                        "avatar": "http://localhost:8080/static/img/logo.b3a48c0.png",
                        "id": 1
                    },
                    "content": "111",
                    "childrens": [],
                    "createDate": "1973-11-26 08:52",
                    "level": 2,
                    "toUser": {
                        "nickname": "李四",
                        "avatar": "http://localhost:8080/static/img/logo.b3a48c0.png",
                        "id": 1
                    }
                }
            ],
            "createDate": "1973-11-27 09:53",
            "level": 1,
            "toUser": null
        }
    ]
}

 思路:

根据接口文档分析,需要返回comment、author信息。

 6.2 编码实现

 (1)创建实体类Comment,前端需要类CommentVo和UserVo

在pojo包中创建Comment.java

@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;
}

在vo包中创建CommentVo.java

@Data
public class CommentVo {
    private Long id;
    //评论作者
    private UserVo author;
    private String content;
    private List<CommentVo> childrens;
    private String createDate;
    private Integer level;
    //当为二级评论时,表示被评论人
    private UserVo toUser;

}

UserVo.java

import lombok.Data;

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

 (2)  评论显示模块  CommentController

@RestController
@RequestMapping("comments")
public class CommentController {
    @Autowired
    CommentService commentService;
    @GetMapping("article/{id}")
    public Result comments(@PathVariable("id") Long articleId){
        return commentService.commonsByArticleId(articleId);
    }
}

 (3)在SysUserService和SysUserServiceImpl中提供 查询用户信息的服务:

//根据作者id查找用户信息
    UserVo findUserVoById(Long authorId);
@Override
    public UserVo findUserVoById(Long id) {
        SysUser sysUser = sysUserMapper.selectById(id);
        if (sysUser == null){
            sysUser = new SysUser();
            sysUser.setId(1L);
            sysUser.setAvatar("/static/img/logo.b3a48c0.png");
            sysUser.setNickname("hnjm");
        }
        UserVo userVo = new UserVo();
        userVo.setAvatar(sysUser.getAvatar());
        userVo.setNickname(sysUser.getNickname());
        userVo.setId(sysUser.getId());
        return userVo;
    }

(3) 在mapper文件中创建CommentMapper.java

public interface CommentMapper extends BaseMapper<Comment> {
}

 (4)在service中创建CommentService和CommentServiceImpl

public interface CommentService {
    Result commonsByArticleId(Long articleId);
}

思路:

 1、根据文章id查询评论列表,在comment表中查询
 2、根据作者id查询作者信息
 3、如果level=1,查询是否有子评论
 4、如果有,根据评论id查询子评论

@Service
public class CommentServiceImpl implements CommentService {
    @Autowired
    CommentMapper commentMapper;
    @Autowired
    SysUserService sysUserService;

    @Override
    public Result commonsByArticleId(Long articleId) {
        /*
         * 1、根据文章id查询评论列表,在comment表中查询
         * 2、根据作者id查询作者信息
         * 3、如果level=1,查询是否有子评论
         * 4、如果有,根据评论id查询子评论
         * */
        LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Comment::getArticleId,articleId)
                .eq(Comment::getLevel,1);
        List<Comment> commentList = commentMapper.selectList(queryWrapper);
        List<CommentVo> commentVos = copyList(commentList);
        return Result.success(commentVos);
    }

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

    private CommentVo copy(Comment comment) {
        CommentVo commentVo = new CommentVo();
        //将相同属性进行copy
        BeanUtils.copyProperties(comment,commentVo);
        //时间格式化
        commentVo.setCreateDate(new DateTime(comment.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
        //作者信息
        commentVo.setAuthor(sysUserService.findUserVoById(comment.getAuthorId()));
        //子评论
        if(comment.getLevel()==1){
            Long id = comment.getId();
            List<CommentVo> commentVoList = findCommentByParentId(id);
            commentVo.setChildrens(commentVoList);
        }
        //toUser:向谁评论
        if(comment.getId()>1){
            Long toUid = comment.getToUid();
            UserVo userVo = sysUserService.findUserVoById(toUid);
            commentVo.setToUser(userVo);
        }
        return commentVo;
    }

    private List<CommentVo> findCommentByParentId(Long id) {
        LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Comment::getParentId,id)
                .eq(Comment::getLevel,2);
        List<Comment> comments = commentMapper.selectList(queryWrapper);
        return copyList(comments);
    }
}

结果:

 总结:

1、在根据作者id查找用户信息时要注意异常情况,没有登录情况是可以查看平路的,所有代码一定写上。

 2、所有类的int类型都要用Integer类型

在article中commentCounts默认值为null(Integer),但是articleVo中commentCounts默认值为0(int),如果进行复制,则int类型被赋予了null,一定会报错,因为int类型没有null这个属性值。

七.  评论功能

7.1 接口说明

接口url:/comments/create/change

请求方式:POST

请求参数:

参数名称参数类型说明
articleIdlong文章id
contentstring评论内容
parentlong父评论id
toUserIdlong被评论的用户id

返回数据:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": null
}

 7.2 编程实现

思路:需要设置拦截器,用户登录才可以评论,否则不可以。

         通过comment()方法发布评论,获取前端的评论及用户数据,存入相应表中

(1)   加入到登录拦截器中(WebMVCConfig中修改)

(2)在CommentController中创建comment方法

首先在param中创建评论参数对象CommentParam.java

@Data
public class CommentParam {

    private Long articleId;

    private String content;

    private Long parent;

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

(3)在CommentService和CommentServiceImpl中添加方法

Result comment(CommentParam commentParam);
//发表评论
    //发表评论,是要将用户进行的评论存入到comment表中,所以要创建comment对象,而不是commentVo对象
    @Override
    public Result comment(CommentParam commentParam) {
        SysUser user= UserThreadLocal.get();
        Comment comment = new Comment();
        comment.setArticleId(commentParam.getArticleId());
        System.out.println("ser"+user);
        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);
    }

运行结果: 

总结:

八.  发布文章 

写文章需要 三个接口:

  1. 获取所有文章类别

  2. 获取所有标签

  3. 发布文章

8.1  获取所有文章类别

8.1.1 接口说明

接口url:/categorys

请求方式:GET

请求参数:

参数名称参数类型说明

返回数据:

{
    "success":true,
 	"code":200,
    "msg":"success",
    "data":
    [
        {"id":1,"avatar":"/category/front.png","categoryName":"前端"},	
        {"id":2,"avatar":"/category/back.png","categoryName":"后端"},
        {"id":3,"avatar":"/category/lift.jpg","categoryName":"生活"},
        {"id":4,"avatar":"/category/database.png","categoryName":"数据库"},
        {"id":5,"avatar":"/category/language.png","categoryName":"编程语言"}
    ]
}

8.1.2 编码实现

(1)CategoryController

@RestController
@RequestMapping("/categorys")
public class CategoryController {
    @Autowired
    CategoryService categoryService;
    //查询所有文章分类
    @GetMapping
    public Result findAll(){
        return categoryService.findAll();
    }
}

(2)CategoryService和CategoryServiceImpl

    //查询所有分类
    Result findAll();
    //获取所有文章分类
    @Override
    public Result findAll() {

        List<Category> categories = categoryMapper.selectList(null);
        return Result.success(copyList(categories));
    }

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

    private CategoryVo copy(Category category) {
        CategoryVo categoryVo = new CategoryVo();
        BeanUtils.copyProperties(category,categoryVo);
        return categoryVo;
    }

8.2 所有文章标签

8.2.1 接口说明:

url:/tags

请求方式:GET

请求参数:

参数名称参数类型说明

返回数据:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id": 5,
            "tagName": "springboot"
        },
        {
            "id": 6,
            "tagName": "spring"
        },
        {
            "id": 7,
            "tagName": "springmvc"
        },
        {
            "id": 8,
            "tagName": "11"
        }
    ]
}

 8.2.2 编程实现

(1)TagController

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

(2)TagService和TagServiceImpl

   Result findAll();
public Result findAll() {
        List<Tag> tags = tagMapper.selectList(null);
        return Result.success(copyList(tags));
    }

    //copyList实现
    private List<TagVo> copyList(List<Tag> tags) {
        ArrayList<TagVo> tagVos = new ArrayList<>();
        for(Tag tag : tags){
            tagVos.add(copy(tag));
        }
        return tagVos;
    }
    private TagVo copy(Tag tag){
        TagVo tagVo = new TagVo();
        //BeanUtils,copyProperties用于类之间的复制,相同字段复制,不同字段为null
        BeanUtils.copyProperties(tag,tagVo);
        return tagVo;
    }

8.3 发布文章

8.3.1 接口说明

接口url:/articles/publish

请求方式:POST

请求参数:

参数名称参数类型说明
titlestring文章标题
idlong文章id(编辑有值)
bodyobject({content: "ww", contentHtml: "

ww

↵"})
文章内容
category{id: 2, avatar: "/category/back.png", categoryName: "后端"}文章类别
summarystring文章概述
tags[{id: 5}, {id: 6}]文章标签

返回数据:

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

8.3.2  编程实现 

(1)根据接口参数在param中添加  ArticleBodyParam.java和ArticleParam.java

@Data
public class ArticleBodyParam {

    private String content;

    private String contentHtml;

}
@Data
public class ArticleParam {

    private Long id;

    private ArticleBodyParam body;

    private CategoryVo category;

    private String summary;

    private List<TagVo> tags;

    private String title;
}

(2)在ArticleController中添加方法

    //发布文章
    @PostMapping("publish")
    public Result publish(@RequestBody ArticleParam articleParam){
        return articleService.publish(articleParam);
    }

(3)在ArticleService和ArticleServiceImpl中添加方法

    Result publish(ArticleParam articleParam);

创建实体类 ArticleTag

@Data
public class ArticleTag {

    private Long id;

    private Long articleId;

    private Long tagId;
}

 ArticleServiceImpl中添加方法

    //发布文章
    @Override
    public Result publish(ArticleParam articleParam) {
        /**
         * 1.发布文章 目的构建Article对象
         * 2. 作者id 当前登陆用户
         * 3. 标签 将标签加入关联表中
         * 4. body内容存储 article bodyId
         * @param articleParam
         * @return
         * 此接口要加入登陆拦截中
         */
        //获得用户信息,前提,将该请接口加入到拦截其中

        SysUser sysUser = UserThreadLocal.get();
        System.out.println("----------用户信息");
        System.out.println(sysUser);
        System.out.println("----------");
        Article article = new Article();
        article.setAuthorId(sysUser.getId());
        article.setWeight(Article.Article_Common);
        article.setViewCounts(0);
        article.setTitle(articleParam.getTitle());
        article.setSummary(articleParam.getSummary());
        article.setCommentCounts(0);
        article.setCreateDate(System.currentTimeMillis());
        article.setCategoryId(articleParam.getCategory().getId());

        System.out.println("----------");
        System.out.println(article);
        System.out.println("----------");

        //插入之后会自动生成id
        articleMapper.insert(article);

        //tag插入数据
        List<TagVo> tags = articleParam.getTags();
        if(tags != null){
            for(TagVo tag : tags){
                Long articleId = article.getId();
                ArticleTag articleTag = new ArticleTag();
                articleTag.setTagId((tag.getId()));
                articleTag.setArticleId(articleId);
                articleTagMapper.insert(articleTag);
            }
        }
        //articleBody插入数据
        ArticleBody articleBody = new ArticleBody();
        articleBody.setArticleId(article.getId());
        articleBody.setContent(articleParam.getBody().getContent());
        articleBody.setContentHtml(articleParam.getBody().getContentHtml());
        articleBodyMapper.insert(articleBody);
        //设置文章bodyId
        article.setBodyId(articleBody.getId());
        //更新article bodyId字段
        articleMapper.updateById(article);
        //将id转换成string放入map
        Map<String,String> map = new HashMap<>();
        map.put("id",article.getId().toString());
        return Result.success(map);
    }

ArticleTagMapper.java

public interface ArticleTagMapper extends BaseMapper<ArticleTag> {
}

 最后在登录拦截器中,需要加入发布文章的配置:

运行结果为:

 8.4 AOP日志

思路:通过注解实现AOP。
Springboot 自定义注解+AOP

(1)在util中添加两个工具类 IpUtils.java   

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.http.HttpServletRequest;

/**
 * 获取Ip
 *
 */
@Slf4j
public class IpUtils {

    /**
     * 获取IP地址
     * <p>
     * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
     * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
     */
    public static String getIpAddr(HttpServletRequest request) {
        String ip = null, unknown = "unknown", seperator = ",";
        int maxLength = 15;
        try {
            ip = request.getHeader("x-forwarded-for");
            if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (StringUtils.isEmpty(ip) || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_CLIENT_IP");
            }
            if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_X_FORWARDED_FOR");
            }
            if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }
        } catch (Exception e) {
            log.error("IpUtils ERROR ", e);
        }

        // 使用代理,则获取第一个IP地址
        if (StringUtils.isEmpty(ip) && ip.length() > maxLength) {
            int idx = ip.indexOf(seperator);
            if (idx > 0) {
                ip = ip.substring(0, idx);
            }
        }

        return ip;
    }

    /**
     * 获取ip地址
     *
     * @return
     */
    public static String getIpAddr() {
        HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
        return getIpAddr(request);
    }
}

 HttpContextUtils.java

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * HttpServletRequest
 *
 */
public class HttpContextUtils {

    public static HttpServletRequest getHttpServletRequest() {
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }

}

(2) 在aop包中添加自定义注解类  目的:实现日志输出

//Type 代表可以放在类上面 Method 代表可以放在方法上
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {

    String module() default "";

    String operator() default "";
}

 (3)AOP面向切面LogAspect.java

@Component
@Aspect //切面 定义了通知和切点的关系
@Slf4j
public class LogAspect {

    @Pointcut("@annotation(com.hnjm.aop.LogAnnotation)")
    public void pt(){}

    //环绕通知
    @Around("pt()")
    public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
        long beginTime = System.currentTimeMillis();
        //执行方法
        Object result = joinPoint.proceed();
        //执行时长(毫秒)
        long time = System.currentTimeMillis() - beginTime;
        //保存日志
        recordLog(joinPoint, time);
        return result;
    }

    private void recordLog(ProceedingJoinPoint joinPoint, long time) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
        log.info("=====================log start================================");
        log.info("module:{}",logAnnotation.module());
        log.info("operation:{}",logAnnotation.operator());

        //请求的方法名
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = signature.getName();
        log.info("request method:{}",className + "." + methodName + "()");

//        //请求的参数
        Object[] args = joinPoint.getArgs();
        String params = JSON.toJSONString(args[0]);
        log.info("params:{}",params);

        //获取request 设置IP地址
        HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
        log.info("ip:{}", IpUtils.getIpAddr(request));


        log.info("excute time : {} ms",time);
        log.info("=====================log end================================");
    }
}

说明:
定义切入点:此处为自定义的注解
@Pointcut("@annotation(com.komorebi.common.aop.LogAnnotation)")
public void pt(){}
环绕通知:@Around(“pt()”)
return joinPoint.proceed(): 这个是从切点的环绕增强里面脱离出来
joinPoint.getSignature:获取被增强目标对象
getMethod:获得方法对象
getAnnotation:获得注解

在要打印日志得的方法上填上注解

//加上此注解 代表要对此接口记录日志

@LogAnnotation(module="文章",operator="获取文章列表")

 运行结果

九. 图片上传

接口url:/upload

请求方式:POST

请求参数:

参数名称参数类型说明
imagefile上传的文件名称

返回数据:

{
    "success":true,
 	"code":200,
    "msg":"success",
    "data":"https://static.mszlu.com/aa.png"
}

思路:用七牛云第三方插件

(1)加入依赖

<dependency>
  <groupId>com.qiniu</groupId>
  <artifactId>qiniu-java-sdk</artifactId>
  <version>[7.7.0, 7.7.99]</version>
</dependency>

 (2)在util中加入工具类


@Component
public class QiniuUtils {
/*
* 1、网址不要错,不要https
* 2、后面要加/
* 3、空间要公有
* 4、bucket = "hnjm"改自己的
* */
    public static  final String url = "http://rbx3b4a91.hb-bkt.clouddn.com/";

    @Value("${qiniu.accessKey}")
    private  String accessKey;
    @Value("${qiniu.accessSecretKey}")
    private  String accessSecretKey;

    public  boolean upload(MultipartFile file,String fileName){

        //构造一个带指定 Region 对象的配置类
        Configuration cfg = new Configuration(Region.huabei());
        //...其他参数参考类注释
        UploadManager uploadManager = new UploadManager(cfg);
        //...生成上传凭证,然后准备上传
        String bucket = "hnjm";
        //默认不指定key的情况下,以文件内容的hash值作为文件名
        try {
            byte[] uploadBytes = file.getBytes();
            Auth auth = Auth.create(accessKey, accessSecretKey);
            String upToken = auth.uploadToken(bucket);
                Response response = uploadManager.put(uploadBytes, fileName, upToken);
                //解析上传成功的结果
                DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);
                return true;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        return false;
    }

    public void deleteAll() throws QiniuException {
        String bucket = "hnjm";
        Auth auth = Auth.create(accessKey, accessSecretKey);
        //构造一个带指定 Region 对象的配置类
        Configuration cfg = new Configuration(Region.huabei());
        BucketManager bucketManager = new BucketManager(auth,cfg);
        FileListing fileListing = bucketManager.listFiles(bucket, "/", "", 1000, "");
        FileInfo[] items = fileListing.items;
        for (FileInfo info : items){

        }
    }
}

注意:

  1、网址不要错,不要https
  2、后面要加/
  3、空间要公有
  4、bucket = "hnjm"改自己的

(3)在application.properties中加入七牛云配置

# 上传文件总的最大值
spring.servlet.multipart.max-request-size=40MB
# 单个文件的最大值
spring.servlet.multipart.max-file-size=20MB
qiniu.accessKey=HjVpEDDATdplRkB9RQ7fUyP0y5aKuLbx2Yf9LLxY
qiniu.accessSecretKey=e-QIA_rN4LLdenB-3wuRwR1FAdm9z1aAXVxMDkan

注意:密钥是自己申请的

(4)创建控制类UploadController

@RestController
@RequestMapping("upload")
public class UploadController {
    @Autowired
    private QiniuUtils qiniuUtils;

    @PostMapping
    public Result upload(@RequestParam("image") MultipartFile file){
        //原始文件
        String fileName = UUID.randomUUID().toString() + "." + StringUtils.substringAfterLast(file.getOriginalFilename(), ".");
        boolean upload = qiniuUtils.upload(file, fileName);
        if (upload){
            return Result.success(QiniuUtils.url + fileName);
        }
        return Result.fail(20001,"上传失败");
    }
}

运行结果

十. 导航显示

10.1 查询所有文章分类

接口说明

接口url:/categorys/detail

请求方式:GET

请求参数:

参数名称参数类型说明

返回数据:

{
    "success": true, 
    "code": 200, 
    "msg": "success", 
    "data": [
        {
            "id": 1, 
            "avatar": "/static/category/front.png", 
            "categoryName": "前端", 
            "description": "前端是什么,大前端"
        }, 
        {
            "id": 2, 
            "avatar": "/static/category/back.png", 
            "categoryName": "后端", 
            "description": "后端最牛叉"
        }, 
        {
            "id": 3, 
            "avatar": "/static/category/lift.jpg", 
            "categoryName": "生活", 
            "description": "生活趣事"
        }, 
        {
            "id": 4, 
            "avatar": "/static/category/database.png", 
            "categoryName": "数据库", 
            "description": "没数据库,啥也不管用"
        }, 
        {
            "id": 5, 
            "avatar": "/static/category/language.png", 
            "categoryName": "编程语言", 
            "description": "好多语言,该学哪个?"
        }
    ]
}

 思路:展示所有的分类信息,并且点击进入相应的分类后,可以显示对应分类的文章

(1)在CategoryController中添加方法

 //文章分类页面
    @GetMapping("/detail")
    public Result findAllDetail(){
        return categoryService.findAllDetail();
    }
    

 (2)在CategoryService和CategoryServiceImpl中添加方法

   //查询分类页面
    Result findAllDetail();
@Override
    public Result findAllDetail() {
        List<Category> categories = categoryMapper.selectList(new LambdaQueryWrapper<>());
        //页面交互的对象
        return Result.success(copyList(categories));
    }

运行结果:

 10.2 分类文章列表显示

接口说明:

接口url:/category/detail/{id}

请求方式:GET

请求参数:

参数名称参数类型说明
id分类id路径参数

返回数据:

{
    "success": true, 
    "code": 200, 
    "msg": "success", 
    "data": 
        {
            "id": 1, 
            "avatar": "/static/category/front.png", 
            "categoryName": "前端", 
            "description": "前端是什么,大前端"
        }
}

(1)在CategoryController中添加方法

    //查询某个分类对应的文章
    @GetMapping("/detail/{id}")
    public Result categoryDetailById(@PathVariable("id") Long id){
        return categoryService.categoriesDetailById(id);
    }

(2)在CategoryService和CategoryServiceImpl中添加方法

//查询某个id分类显示
    Result categoriesDetailById(Long id);
    public Result categoriesDetailById(Long id) {
        Category category = categoryMapper.selectById(id);
        CategoryVo categoryVo = copy(category);
        return Result.success(categoryVo);
    }

 在因为要获取文章,所以我们只需要在原有的获取文章列表方法中,增加一个查询条件即可。
ArticleServiceImpl

         //查询文章的参数 加上分类id,判断不为空 加上分类条件
            if (pageParams.getCategoryId() != null) {//后面加上
                wrapper.eq(Article::getCategoryId,pageParams.getCategoryId());
            }
            

  

 运行结果

 10.3  获取所有标签

接口url:/tags/detail

请求方式:GET

请求参数:

参数名称参数类型说明

返回数据:

{
    "success": true, 
    "code": 200, 
    "msg": "success", 
    "data": [
        {
            "id": 5, 
            "tagName": "springboot", 
            "avatar": "/static/tag/java.png"
        }, 
        {
            "id": 6, 
            "tagName": "spring", 
            "avatar": "/static/tag/java.png"
        }, 
        {
            "id": 7, 
            "tagName": "springmvc", 
            "avatar": "/static/tag/java.png"
        }, 
        {
            "id": 8, 
            "tagName": "11", 
            "avatar": "/static/tag/css.png"
        }
    ]
}

(1)在TagController中添加方法

    //获取所有标签
    @GetMapping("detail")
    public Result findAllDetail(){
        return tagService.findAllDetail();
    }

(2)在TagService和TagServiceImpl中添加方法

Result findAllDetail();
 @Override
    public Result findAllDetail() {
        LambdaQueryWrapper<Tag> queryWrapper = new LambdaQueryWrapper<>();
        List<Tag> tags = this.tagMapper.selectList(queryWrapper);
        return Result.success(copyList(tags));
    }

 运行结果:

10.4  获取标签文章列表 

接口url:/tags/detail/{id}

请求方式:GET

请求参数:

参数名称参数类型说明
id标签id路径参数

返回数据:

{
    "success": true, 
    "code": 200, 
    "msg": "success", 
    "data": 
        {
            "id": 5, 
            "tagName": "springboot", 
            "avatar": "/static/tag/java.png"
        }
}

 (1)在TagController中添加方法

    @GetMapping("detail/{id}")
    public Result findDetailById(@PathVariable("id") Long id){
        return tagService.findDetailById(id);
    }

(2)在TagService和TagServiceImpl中添加方法

Result findDetailById(Long id);
//获取标签内的文章列表
    @Override
    public Result findDetailById(Long id) {
        Tag tag = tagMapper.selectById(id);
        TagVo copy = copy(tag);
        return Result.success(copy);
    }

(3)在因为要获取文章,所以我们只需要在原有的获取文章列表方法中,增加一个查询条件即可。ArticleServiceImpl中listArticle()方法中添加

 //文章标签页面,按照分类TagId展示文章
            List<Long> articleIdList = new ArrayList<>();
            if(pageParams.getTagId()!=null){
                //加入标签 条件查询
                //article表中没有tag字段,因为一篇文章有多个标签
                //映射为一张新表article_tag :article_id 1:n tag_id
                //1、查询标签id对应的文章id列表
                LambdaQueryWrapper<ArticleTag> articleTagLambdaQueryWrapper = new LambdaQueryWrapper<>();
                articleTagLambdaQueryWrapper.eq(ArticleTag::getTagId,pageParams.getTagId());
                List<ArticleTag> articleTags = articleTagMapper.selectList(articleTagLambdaQueryWrapper);
                for (ArticleTag articleTag : articleTags){
                    articleIdList.add(articleTag.getArticleId());
                }
                if(articleIdList.size()>0){
                    wrapper.in(Article::getId,articleIdList);
                }
            }

运行结果:

10.5   归档文章列表

接口url:/articles

请求方式:POST

请求参数:

参数名称参数类型说明
yearstring
monthstring

返回数据:

{
    "success": true, 
    "code": 200, 
    "msg": "success", 
    "data": [文章列表,数据同之前的文章列表接口]
        
}

 (1)在PageParam中添加属性(前面添加过略过)

由于文章归档涉及到年,月的属性,所以PageParams类需要添加一些属性

@Data
public class PageParams {

    private int page = 1;

    private int pageSize = 5;

    private Long categoryId;

    private Long tagId;

    private String year;

    private String month;

    public String getMonth() {
        if (this.month != null && this.month.length() == 1) {
            return "0" + this.month;
        }
        return this.month;
    }
}

因为涉及到年月的计算,mybatis_plus不能实现,所以只能使用sql语句实现,又因为文章归档归根结底也是文章查询,只是添加了一些查询条件。
所以将文章查询整体修改为mapper.xml形式,即将以前通过mybatis_plus实现的文章查询注释掉。
(2)ArticleMapper.java中添加方法

    IPage<Article> listArticle(Page<Article> page,
                               Long categoryId,
                               Long tagId,
                               String year,
                               String month);

(3)在ArticleMapper.xml中编写sql

     <resultMap id="articleMap" type="Article">
        <id column="id" property="id" />
        <result column="author_id" property="authorId"/>
        <result column="comment_counts" property="commentCounts"/>
        <result column="create_date" property="createDate"/>
        <result column="summary" property="summary"/>
        <result column="title" property="title"/>
        <result column="view_counts" property="viewCounts"/>
        <result column="weight" property="weight"/>
        <result column="body_id" property="bodyId"/>
        <result column="category_id" property="categoryId"/>
    </resultMap>
    <!--    文章显示-->
    <select id="listArticle"  resultMap="articleMap">
        select * from ms_article
        <where>
            1 = 1
            <if test="categoryId != null">
                and  category_id = #{categoryId}
            </if>
            <if test="year != null and year.length>0 and month != null and month.length>0">
                and ( FROM_UNIXTIME(create_date/1000,'%Y') = #{year} and FROM_UNIXTIME(create_date/1000,'%m') = #{month} )
            </if>
            <if test="tagId != null">
                and id in (select article_id from ms_article_tag where tag_id=#{tagId})
            </if>
        </where>
        order by create_date desc
    </select>

 (3)在ArticleImpl中listArticle()方法注销掉,重新添加

 //分页查询
        @Override
        public Result listArticle(PageParams pageParams) {
            Page<Article> page = new Page<>(pageParams.getPage(),pageParams.getPageSize());
            IPage<Article> articleIPage = this.articleMapper.listArticle(page,pageParams.getCategoryId(),pageParams.getTagId(),pageParams.getYear(),pageParams.getMonth());
            return Result.success(copyList(articleIPage.getRecords(),true,true));
        }

运行结果:

十一.统一缓存处理(优化)

内存的访问速度 远远大于 磁盘的访问速度 (1000倍起)!!!!!!

首页加载的东西每次都会去访问数据库,我们可以把它们都加入到缓存中,加快存取速度。
如何在不改变原有代码的基础上,加入缓存呢!AOP

(1)在aop中创建接口Cache

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {
    //缓存过期时间
    long expire() default 1*60*1000;
    //名称
    String name() default "";
}

(2) 在aop中创建接口Cache切片程序

@Aspect
@Component
@Slf4j
public class CacheAspect {

    @Autowired
    RedisTemplate<String,String> redisTemplate;
    //切入点为注解Cache
    @Pointcut("@annotation(com.hnjm.aop.Cache)")
    public void pt(){}

    @Around("pt()")
    public Object around(ProceedingJoinPoint joinPoint){
        try{
            Signature signature = joinPoint.getSignature();
            //获得类名
            String className = joinPoint.getTarget().getClass().getSimpleName();
            //获得方法名
            String methodName = signature.getName();
            //存取方法参数类型
            Class[] parameterTypes = new Class[joinPoint.getArgs().length];
            //拿到参数
            Object[] args = joinPoint.getArgs();
            //将所有参数拼接成字符串
            String params = "";
            for(int i=0; i<args.length; i++){
                if(args[i] != null){
                    params += JSON.toJSONString(args[i]);
                    parameterTypes[i] = args[i].getClass();
                }else{
                    parameterTypes[i] = null;
                }
            }
            if(StringUtils.isNotEmpty(params)){
                //md5参数加密,用于设置redis key
                params = DigestUtils.md5Hex(params);
            }
            //通过parameterTypes拿到对应的方法
            Method method = joinPoint.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);
            //获取cache注解
            Cache annotation = method.getAnnotation(Cache.class);
            //获取过期时间
            long expire = annotation.expire();
            //缓存名称
            String name = annotation.name();

            //创建redis Key,保证key的唯一性
            String redisKey = name+"::"+className+"::"+methodName+"::"+params;
            //1、先从redis中获取要查询的信息
            String redisValue = redisTemplate.opsForValue().get(redisKey);
            //如果redis中有
            if(StringUtils.isNotEmpty(redisValue)){
                log.info("走了缓存---,{},{}",className,methodName);
                return JSON.parseObject(redisValue,Result.class);
            }
            //2、redis中没有,访问查询方法,然后将结果存入redis
            //proceed()即代表执行了Controller中的方法,
            // 如果有返回值就返回,如果没有就不用返回,在这里有返回值,为文章信息
            Object proceed = joinPoint.proceed();
            //JSON.toJSONString将对象转为json字符串
            //JSON.parseObject将json字符串转为对象
            redisTemplate.opsForValue().set(redisKey,JSON.toJSONString(proceed), Duration.ofMillis(expire));
            log.info("存入缓存---{},{}",className,methodName);
            return proceed;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return Result.fail(-999,"系统错误");
    }
}

(3)在想要添加缓存的方法上添加Cache注解

@Cache(expire = 5*60*1000,name="listArticle")

 //首页文章列表
    //Result是统一结果返回
    @RequestMapping
    //加上此注解 代表要对此接口记录日志
    @LogAnnotation(module="文章",operator="获取文章列表")
    @Cache(expire = 5*60*1000,name="listArticle")
    public Result articles(@RequestBody PageParams pageParams) {
        //ArticleVo 页面接收的数据
        return  articleService.listArticle(pageParams);
    }

运行结果

  • 14
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值