SpringBoot+vue 练手项目-- 个人博客系统

dSpringBoot+vue练手项目---博客系统

项目使用技术 :

springboot + mybatisplus+redis+mysql+jwt

项目讲解:https://www.bilibili.com/video/BV1Gb4y1d7zb?p=1

1. 工程搭建

前端的工程地址:

链接:https://pan.baidu.com/s/1cg_11ctsbbq_WM9BnpcOaQ
提取码:nrun

npm install
npm run build
npm run dev

1.1 新建maven工程

pom.xml(blog-parent)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.jihu</groupId>
    <artifactId>blog-parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>blog-api</module>
    </modules>
    <!--声明pom代表他是一个父工程-->
    <packaging>pom</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
        <relativePath/>
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>
        
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.2</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/joda-time/joda-time -->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.10.10</version>
        </dependency>
    </dependencies>
</dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

pom.xml(blog-api)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>blog-parent</artifactId>
        <groupId>com.jihu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>blog-api</artifactId>

    <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>

        <!-- 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>

        <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>
    </dependencies>


</project>

1.2 application.yml

server:
  port: 8888

spring:
  application:
    name: jihu

#数据库的配置
  datasource:
    url: jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

#mybatis-plus
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  #打印日志 如sql语句
  global-config:
    db-config:
      table-prefix: ms_  #标识表的前缀为ms_
#指定mapper文件的位置
mybatis-plus:
  config-location: classpath:mapper/*.xml

1.3 配置 分页 和跨域

分页

package com.jihu.blog.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

}

跨域

package com.jihu.blog.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
    //实现跨域请求
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET","HEAD","POST","PUT","DELETE","OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}

1.4启动类

package com.jihu.blog;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

}

2.首页-文章列表

2.1 接口说明

接口url:/articles

请求方式:POST

请求参数:

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

返回数据:

{
    "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
        }
    ]
}

2.2 编码

Spring基于注解的开发
每个注解的作用

2.2.1 表结构

文章表

CREATE TABLE `blog`.`ms_article`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `comment_counts` int(0) NULL DEFAULT NULL COMMENT '评论数量',
  `create_date` bigint(0) NULL DEFAULT NULL COMMENT '创建时间',
  `summary` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '简介',
  `title` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '标题',
  `view_counts` int(0) NULL DEFAULT NULL COMMENT '浏览数量',
  `weight` int(0) NOT NULL COMMENT '是否置顶',
  `author_id` bigint(0) NULL DEFAULT NULL COMMENT '作者id',
  `body_id` bigint(0) NULL DEFAULT NULL COMMENT '内容id',
  `category_id` int(0) NULL DEFAULT NULL COMMENT '类别id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

标签表

id,文章id,标签id,通过文章id可以间接查到标签id

CREATE TABLE `blog`.`ms_tag`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `article_id` bigint(0) NOT NULL,
  `tag_id` bigint(0) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `article_id`(`article_id`) USING BTREE,
  INDEX `tag_id`(`tag_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

用户表

CREATE TABLE `blog`.`ms_sys_user`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `account` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '账号',
  `admin` bit(1) NULL DEFAULT NULL COMMENT '是否管理员',
  `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像',
  `create_date` bigint(0) NULL DEFAULT NULL COMMENT '注册时间',
  `deleted` bit(1) NULL DEFAULT NULL COMMENT '是否删除',
  `email` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
  `last_login` bigint(0) NULL DEFAULT NULL COMMENT '最后登录时间',
  `mobile_phone_number` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号',
  `nickname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称',
  `password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
  `salt` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '加密盐',
  `status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '状态',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

2.2.2 entity层

Article 文章实体类
package com.jihu.blog.dao.pojo;

import lombok.Data;

@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 int commentCounts;

    private int viewCounts;

    /**
     * 作者id
     */
    private Long authorId;
    /**
     * 内容id
     */
    private Long bodyId;
    /**
     *类别id
     */
    private Long categoryId;

    /**
     * 置顶
     */
    private int weight = Article_Common;


    /**
     * 创建时间
     */
    private Long createDate;
}

SysUser 用户实体类
package com.jihu.blog.dao.pojo;

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;
}
Tag 标签实体类
package com.jihu.blog.dao.pojo;

import lombok.Data;

@Data
public class Tag {

    private Long id;

    private String avatar;

    private String tagName;

}

2.2.3 Controller层

ArticleController
package com.jihu.blog.controller;

import com.jihu.blog.service.ArticleService;
import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.PageParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("articles")
public class ArticleController {

    @Autowired
    private ArticleService articleService;

    //首页  文章列表
    @PostMapping
    public Result listArticle(@RequestBody PageParams pageParams){
        //ArticleVo 页面接收的数据
        return articleService.listArticle(pageParams);

    }

}

2.2.4 Service层

ArticleService
package com.jihu.blog.service;

import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.PageParams;

public interface ArticleService {

    Result listArticle(PageParams pageParams);
}

ArticleServiceImpl
package com.jihu.blog.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jihu.blog.dao.mapper.ArticleMapper;
import com.jihu.blog.dao.mapper.TagMapper;
import com.jihu.blog.dao.pojo.Article;
import com.jihu.blog.service.ArticleService;
import com.jihu.blog.service.SysUserService;
import com.jihu.blog.service.TagService;
import com.jihu.blog.vo.ArticleVo;
import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.PageParams;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class ArticleServiceImpl implements ArticleService {

    @Autowired
    private ArticleMapper articleMapper;

    @Autowired
    private TagService tagService;

    @Autowired
    private SysUserService sysUserService;


    @Override
    public Result listArticle(PageParams pageParams) {

        Page<Article> page = new Page<>(pageParams.getPage(), pageParams.getPageSize());
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();

        //是否置顶进行排序
        queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);

        Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);
        List<Article> records = articlePage.getRecords();
        //能直接返回吗  肯定不行  所以需要进行如下转换
       List<ArticleVo> articleVoList = copyList(records,true,true);
        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));
        }
        return articleVoList;
    }

    private ArticleVo copy(Article article,boolean isTag,boolean isAuthor){
        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();
            articleVo.setTags(tagService.findTagsByrticleId(articleId));
        }
        if (isAuthor){
            Long authorId = article.getAuthorId();
            articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname());
        }
        
        return articleVo;
    }
  
}
SysUserService
package com.jihu.blog.service;
import com.jihu.blog.dao.pojo.SysUser;
public interface SysUserService {
    SysUser findUserById(Long id);
}

SysUserServiceImpl
package com.jihu.blog.service.impl;

import com.jihu.blog.dao.mapper.SysUserMapper;
import com.jihu.blog.dao.pojo.SysUser;
import com.jihu.blog.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class SysUserServiceImpl implements SysUserService {

    @Autowired
    private SysUserMapper sysUserMapper;

    @Override
    public SysUser findUserById(Long id) {
        SysUser sysUser = sysUserMapper.selectById(id);
        //防止空指针出现 加一个判断
        if (sysUser == null){
            sysUser = new SysUser();
            sysUser.setNickname("马神之路");
        }
        return sysUser;
    }
}

TagService
package com.jihu.blog.service;
import com.jihu.blog.vo.TagVo;
import java.util.List;
public interface TagService {
    List<TagVo> findTagsByrticleId(Long articleId);
}

TagServiceImpl
package com.jihu.blog.service.impl;

import com.jihu.blog.dao.mapper.TagMapper;
import com.jihu.blog.dao.pojo.Tag;
import com.jihu.blog.service.TagService;
import com.jihu.blog.vo.TagVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class TagServiceImpl implements TagService {

    @Autowired
    private TagMapper tagMapper;

    @Override
    public List<TagVo> findTagsByrticleId(Long articleId) {
        //mybatisplus  无法进行多表查询
        List<Tag> tags = tagMapper.findTagsByrticleId(articleId);

        return copyList(tags);
    }

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

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

2.2.5 Mapper层

ArticleMapper
package com.jihu.blog.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.pojo.Article;
//BaseMapper mybatisplus中提供的可以让我们很方便的查询这张表
public interface ArticleMapper  extends BaseMapper<Article> {

}
SysUserMapper
package com.jihu.blog.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.pojo.SysUser;
public interface SysUserMapper extends BaseMapper<SysUser> {
}
TagMapper
package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.pojo.Tag;

import java.util.List;

public interface TagMapper extends BaseMapper<Tag> {

    /**
     * 根据文章id 查询标签列表
     * @param articleId
     * @return
     */
    List<Tag> findTagsByrticleId(Long articleId);
}
         
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.jihu.blog.dao.mapper.TagMapper">

    <sql id="all">
        id,avatar,tag_name as tagName
    </sql>

<!--        List<Tag> findTagsByArticleId(Long articleId);
在这个文件中,id代表方法名,parameterType表示输入变量的名字,resultType表示泛型的类型-->

    <select id="findTagsByrticleId" parameterType="long" resultType="com.jihu.blog.dao.pojo.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})
    </select>
</mapper>

2.2.6 Vo层

Result(统一最后的结果)
package com.jihu.blog.vo;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Result {
    private boolean success;
    private int code ;
    private String msg;
    private  Object data;

    public static Result success(Object data) {
        return new Result(true, 200, "success", data);
    }

    public static Result fail(int code,String msg) {
        return new Result(false, code, msg, null);
    }
}
ArticleVo 建立与前端交互的Vo文件
package com.jihu.blog.vo;

import lombok.Data;

import java.util.List;

//建立与前端交互的Vo文件
@Data
public class ArticleVo {
    private Long id;

    private String title;

    private String summary;

    private int commentCounts;

    private int viewCounts;

    private int weight;
    /**
     * 创建时间
     */
    private String createDate;

    private String author;

//    private ArticleBodyVo body;

    private List<TagVo> tags;

//    private List<CategoryVo> categorys;
}
新建TagVo
package com.jihu.blog.vo;

import lombok.Data;

@Data
public class TagVo {
    private Long id;
    private String tagName;
}
新建PageParams
package com.jihu.blog.vo.params;
import lombok.Data;
@Data
public class PageParams {
    private  int Page =1;  //当前页数
    private  int PageSize =10;  //每页显示的数量
}	

2.2.7 测试:

image-20220404162338575

3.首页- 最热标签

3.1接口说明

接口url:/tags/hot

请求方式:GET

请求参数:无

id: 标签名称 ,我们期望点击标签关于文章的所有列表都显示出来

返回数据:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id":1,
            "tagName":"4444"
        }
    ]
}

3.2编码

3.2.1Controller层

package com.jihu.blog.controller;

import com.jihu.blog.service.TagService;
import com.jihu.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("tags")
public class TagsController {

    @Autowired
    private TagService tagService;

    @GetMapping("hot")
    public Result hot(){
        int limit = 6;
        return tagService.hots(limit);
    }
}

3.2.2 Service层

建立service接口

TagService
package com.jihu.blog.service;
import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.TagVo;
import java.util.List;
public interface TagService {
    Result hots(int limit);
}

建立serviceimpl实现类

TagServiceImpl
@Service
public class TagServiceImpl implements TagService {

    @Autowired
    private TagMapper tagMapper;
    
	@Override
    public Result hots(int limit) {
        /*
          1.标签所拥有的文章数量最多  即为最热标签
          2. 查询  根据tag_id 分组  计数,从大到小  排列  取前 limit个
         */
        List<Long> tagIds = tagMapper.findHotsIds(limit);
        //判断一下是否为空
        if (tagIds == null){
            return  Result.success(Collections.emptyList());
        }

        //需求的是  tagId 和 tagName  tag对象
        //需要的是这样的一个sql语句  select * from tag where id in (1,2,3...)
        List<Tag> tagList = tagMapper.findTagdByTagIds(tagIds);

        return Result.success(tagList);
    }
}    

3.2.3 Mapper层

TagMapper
package com.jihu.blog.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.pojo.Tag;
import java.util.List;
public interface TagMapper extends BaseMapper<Tag> {

    /**
     * 查询最热的标签 前limit条
     * @param limit
     * @return
     */
    List<Long> findHotsIds(int limit);
    /*
        根据最热标签查询 最热文章名字
     */
    List<Tag> findTagdByTagIds(List<Long> tagIds);
}
TagMapper
<?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.jihu.blog.dao.mapper.TagMapper">

    <sql id="all">
        id,avatar,tag_name as tagName
    </sql>

    <select id="findHotsIds" parameterType="int" resultType="java.lang.Long">
        SELECT tag_id from ms_article_tag GROUP BY tag_id ORDER BY count(*) DESC limit #{limit}

    </select>

    <select id="findTagdByTagIds" parameterType="list" resultType="com.jihu.blog.dao.pojo.Tag">
        select id,tag_name as tagName  from ms_tag
        where id in
        <foreach collection="collection" item="tagId" separator="," open="("  close=")">
              #{tagId}
        </foreach>
    </select>
</mapper>

3.2.4 测试

image-20220404170243616

4.统一异常处理

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

AllExceptionHandler

package com.jihu.blog.handler;

import com.jihu.blog.vo.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

//对加了 @Controller 注解方法进行拦截处理   AOP的实现
@ControllerAdvice
public class AllExceptionHandler {

    //进行异常处理,  处理Exception.class的异常
    @ExceptionHandler(Exception.class)
    @ResponseBody  //返回json数据
    public Result doException(Exception ex){
        ex.printStackTrace();
        return Result.fail(-999,"系统异常");
    }

}

image-20220404171112342

5.首页-最热文章

5.1 接口说明

接口url:/articles/hot

请求方式:POST

请求参数:无

返回数据:

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

5.2 Controller层

ArticleController

@RestController
@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层

ArticleService

package com.jihu.blog.service;

import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.PageParams;

public interface ArticleService {

    Result listArticle(PageParams pageParams);

    Result hotArticle(int limit);
}

ArticleServiceImpl

@Service
public class ArticleServiceImpl implements ArticleService {

    @Autowired
    private ArticleMapper articleMapper;
/**
     * 最热文章查询
     * @param limit
     * @return
     */

    @Override
    public Result hotArticle(int limit) {
        LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.orderByDesc(Article::getViewCounts);
        lambdaQueryWrapper.select(Article::getId,Article::getTitle);
        lambdaQueryWrapper.last("limit "+ limit);
        //SELECT id, title from ms_article ORDER BY view_counts DESC limit 5
        List<Article> articles = articleMapper.selectList(lambdaQueryWrapper);

        return Result.success(copyList(articles,false,false));
    }
}    

5.4测试

image-20220404180253013

6.首页-最新文章

和最热文章非常类似,一个是根据浏览量来选择,一个是根据最新创建时间来选择

6.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相关",
            
        }
    ]
}

6.2 Controller层

在com.jihu.blog.controller.ArticleController中添加

 //首页  最新文章
    @PostMapping("new")
    public Result newArticle(){
        int limit = 5; //取前5条

        return articleService.newArticle(limit);

    }

6.3ArticleService

Result newArticle(int limit);

6.4ArticleServiceImpl

/**
 * 最新文章查询
 * @param limit
 * @return
 */
@Override
public Result newArticle(int limit) {
    LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.orderByDesc(Article::getCreateDate);
    lambdaQueryWrapper.select(Article::getId,Article::getTitle);
    lambdaQueryWrapper.last("limit "+limit);
    //SELECT id, title from ms_article ORDER BY create_data DESC limit 5
    List<Article> articles = articleMapper.selectList(lambdaQueryWrapper);

    return Result.success(copyList(articles,false,false));
}

6.5测试

image-20220404181514531

7.首页-文章归档

每一篇文章根据创建时间某年某月发表多少篇文章

7.1接口说明

接口url:/articles/listArchives

请求方式:POST

请求参数:无

返回数据:

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

7.2 Controller层

com.jihu.blog.controller.ArticleController

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

7.3 ArticleService

Result listArchives();

7.4 ArticleServiceImpl

 //文章归档
    @Override
    public Result listArchives() {

       List<Archives>  archivesList  = articleMapper.listArchives();
        return  Result.success(archivesList);

    }

7.5 ArticleMapper

package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.dos.Archives;
import com.jihu.blog.dao.pojo.Article;
import com.jihu.blog.vo.Result;
import java.util.List;

//BaseMapper mybatisplus中提供的可以让我们很方便的查询这张表
public interface ArticleMapper  extends BaseMapper<Article> {

        List<Archives> listArchives();
}

7.6 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.jihu.blog.dao.mapper.ArticleMapper">
<!--
    select YEAR(create_date) as year,YEAR(create_date) as month ,count(*) as count from ms_article GROUP BY year,MONTH  这样查询不行
-->
    
<!--create_date 为bigint 13位,直接year()不行,需要先转date型后year()。-->
<select id="listArchives"  resultType="com.jihu.blog.dao.dos.Archives" >
select year(FROM_UNIXTIME(create_date/1000)) year,month(FROM_UNIXTIME(create_date/1000)) month, count(*)
count from ms_article group by year,month;
</select>

</mapper>

7.7测试

image-20220404184134240

8.登录

8.1 接口说明

接口url:/login

请求方式:POST

请求参数:

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

返回数据:

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

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>

JWTUtils(工具类):

com.jihu.blog.utils.JWTUtils

package com.jihu.blog.utils;

import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

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);
        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;

    }
    
    //测验一下
    public static void main(String[] args) {
        String token = JWTUtils.createToken(100L);
        System.out.println(token);
        Map<String, Object> map = JWTUtils.checkToken(token);
        System.out.println(map.get("userId"));
    }
}

8.3 LoginController

package com.jihu.blog.controller;

import com.jihu.blog.service.LoginService;
import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.LoginParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

    @Autowired
    private LoginService loginService;

    ////@RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的);
    // 而最常用的使用请求体传参的无疑是POST请求了,所以使用@RequestBody接收数据时,一般都用POST方式进行提交。
    @PostMapping
    public Result Login(@RequestBody LoginParams loginParams){
        //登录 验证用户   访问用户表
        return loginService.login(loginParams);
    }
}

8.4 LoginParam 登录参数

构造LoginParam也就是我们的请求数据
com.jihu.blog.vo.params.LoginParams

package com.jihu.blog.vo.params;
import lombok.Data;
@Data
public class LoginParams {
    private String account;
    private String password;
}

8.5 LoginService

package com.jihu.blog.service;

import com.jihu.blog.dao.pojo.SysUser;
import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.LoginParams;

public interface LoginService {

    /**
     * 登录功能
     * @param loginParams
     * @return
     */

    Result login(LoginParams loginParams);
}    

导入依赖包
md5加密的依赖包:

  <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
    </dependency>

8.6 LoginServiceImpl

// @Component – 指示自动扫描组件。
//@Repository – 表示在持久层DAO组件。
//@Service – 表示在业务层服务组件。
//@Controller – 表示在表示层控制器组件。
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private RedisTemplate<String,String> redisTemplate;
    //加密盐用于加密
    private static final String salt = "mszlu!@#";

    @Override
    public Result login(LoginParams loginParams) {
        /**
         * 1.检查参数是否合法
         * 2.根据用户名和密码去user表中查询是否存在
         * 3.如果不存在   登录失败
         * 4. 如果存在  ,使用jwt生成token 返回给前端
         * 5.把token放入redis中,redis 存储 token user这个信息,设置过期时间
         * (登录认证时  先认证token字符串是否合法,再去redis认证是否存在)
         */

        String account = loginParams.getAccount();
        String password = loginParams.getPassword();
        if (StringUtils.isBlank(account) || StringUtils.isBlank(password)){
            return  Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
        }
        //把密码加密一下
        String pwd = DigestUtils.md5Hex(password + salt);
        SysUser sysUser = sysUserService.findUser(account,pwd);
        if (sysUser == null){
            return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());
        }
        //登录成功,使用JWT生成token,返回token和redis中
        String token = JWTUtils.createToken(sysUser.getId());
        //JSON.toJSONString :是把 sysUser对象转换为对应的json字符串 (参考:https://blog.csdn.net/antony9118/article/details/71023009)
        //设置过期时间为1天
        redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS);

        return Result.success(token);
    }

    //生成我们想要的密码,放于数据库用于登陆
    public static void main(String[] args) {
        System.out.println(DigestUtils.md5Hex("admin"+salt));
    }
}

8.7 SysUserService

SysUser findUser(String account, String password);

8.8 SysUserServiceImpl

  @Override
    public SysUser findUser(String account, String password) {
        LambdaQueryWrapper<SysUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(SysUser::getAccount,account);
        lambdaQueryWrapper.eq(SysUser::getPassword,password);
        lambdaQueryWrapper.select(SysUser::getAccount,SysUser::getId,SysUser::getAvatar,SysUser::getNickname);
        lambdaQueryWrapper.last("limit 1"); //保证一下查询效率   要不然可能还会一直往下查询

        return sysUserMapper.selectOne(lambdaQueryWrapper);
    }

8.9 redis配置

#整合redis
spring.redis.host=192.168.56.130
spring.redis.port=6379

8.10 统一错误码

com.jihu.blog.vo.ErrorCode

package com.jihu.blog.vo;

public enum  ErrorCode {

    PARAMS_ERROR(10001,"参数有误"),
    ACCOUNT_PWD_NOT_EXIST(10002,"用户名或密码不存在"),
    TOKEN_ERROR(10003,"token不合法"),
    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;
    }

}

8.11 测试

image-20220405103600243

9.获取用户信息

为什么实现完获取用户信息才能登陆测试呢?

token前端获取到之后,会存储在 storage中 h5 ,本地存储,存储好后,拿到storage中的token去获取用户信息,如果这个接口没实现,他就会一直请求陷入死循环

9.1 接口说明

得从http的head里面拿到这个参数,这样传参相对来说安全一些,
返回是数据是我们用户相关的数据,id,账号、昵称和头像

接口url:/users/currentUser

请求方式:GET

请求参数:

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

返回数据:

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

9.2 UsersController

package com.jihu.blog.controller;


import com.jihu.blog.service.SysUserService;
import com.jihu.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
//浅谈@RequestMapping @ResponseBody 和 @RequestBody 注解的用法与区别?
//https://blog.csdn.net/ff906317011/article/details/78552426?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2.no_search_link
@RestController
@RequestMapping("users")
public class UsersController {

    @Autowired
    private SysUserService sysUserService;

    @GetMapping("currentUser")
    public Result currentUser(@RequestHeader("Authorization") String token){
        return sysUserService.findUserByToken(token);
    }
}

9.3 SysUserService

package com.jihu.blog.service;
import com.jihu.blog.dao.pojo.SysUser;
import com.jihu.blog.vo.Result;
public interface SysUserService {

    /**
     * 根据token查询用户信息
     * @param token
     * @return
     */
    Result findUserByToken(String token);
}

9.4 SysUserServiceImpl

 @Override
    public Result findUserByToken(String token) {
        /**
         * 1.token合法性效验
         *   是否为空,解析是否成功  redis是否存在
         * 2.如果效验失败  返回错误
         * 3.如果成功,返回对应的结果  LoginUserVo
         */

  	 //去loginservice中去校验token
        SysUser sysUser = loginService.checkToken(token);
        if (sysUser == null){
            return Result.fail(ErrorCode.TOKEN_ERROR.getCode(),ErrorCode.TOKEN_ERROR.getMsg());
        }

        LoginUserVo loginUserVo = new LoginUserVo();
        loginUserVo.setAccount(sysUser.getAccount());
        loginUserVo.setAvatar(sysUser.getAvatar());
        loginUserVo.setId(sysUser.getId());
        loginUserVo.setNickname(sysUser.getNickname());

        return  Result.success(loginUserVo);

    }

9.5 LoginService

SysUser checkToken(String token);

9.6 LoginServiceImpl

 @Override
    public SysUser checkToken(String token) {
        //token为空返回null
        if (StringUtils.isBlank(token)){
            return null;
        }
        Map<String, Object> stringObjectMap = JWTUtils.checkToken(token);
        //解析失败
        if (stringObjectMap == null){
            return null;
        }
         //如果成功
        String userJson = redisTemplate.opsForValue().get("TOKEN_" + token);
        if (StringUtils.isBlank(userJson)){
            return null;
        }
        SysUser sysUser = JSON.parseObject(userJson, SysUser.class); //解析为json

        return sysUser;
    }

9.7 LoginUserVo

package com.jihu.blog.vo;
import lombok.Data;
@Data
public class LoginUserVo {
    //与页面交互

    private Long id;

    private String account;

    private String nickname;

    private String avatar;

}

9.8 测试

image-20220405104755003

10. 退出登录

登陆一个的对token进行认证,一个是在redis中进行注册,token字符串没法更改掉,只能由前端进行清除,后端能做的就是把redis进行清除

10.1 接口说明

接口url:/logout

请求方式:GET

请求参数:

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

返回数据:

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

10.2 LogoutController

package com.jihu.blog.controller;

import com.jihu.blog.service.LoginService;
import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.LoginParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("logout")
public class LogoutController {

    @Autowired
    private LoginService loginService;

    //获取头部信息这样一个参数
    @GetMapping
    public Result logout(@RequestHeader("Authorization") String token){
        return loginService.logout(token);
    }

}

10.3 LoginService

/**
     * 退出登陆
     * @param token
     * @return
     */

    Result logout(String token);

10.4 LoginServiceImpl

@Override
    public Result logout(String token) {
        //后端直接删除redis中的token
        redisTemplate.delete("TOKEN_"+token);
        return Result.success(null);
    }

10.5 测试

image-20220405105102937

11. 注册用户

11.1 接口说明

接口url:/register

请求方式:POST
post传参意味着请求参数是按照json方式传
具体可以看这篇
post和@Requestbody

请求参数:

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

返回数据:

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

11.2 RegisterController

package com.jihu.blog.controller;

import com.jihu.blog.service.LoginService;
import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.LoginParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("register")
public class RegisterController {

    @Autowired
    private LoginService loginService;

    @PostMapping
    public Result register(@RequestBody LoginParams loginParams){
       //sso  叫做 单点登录, 后期如果把登录注册功能 提出去(单独的服务,可以独立提供接口服务)
        return loginService.register(loginParams);

    }
}	

11.3 LoginService

  /**
     * 注册
     * @param loginParams
     * @return
     */
    Result register(LoginParams loginParams);

11.4 LoginServiceImpl

@Override
    public Result register(LoginParams loginParams) {
        /**
         * 1.判断参数是否合法
         * 2.判断账户是否存在,存在  返回账户已经被注册
         * 3.如果账户不存在,注册用户
         * 4.生成token
         * 5. 存入redis  并返回
         * 6. 注意   加上事务,一旦中间任何过程出现问题,注册的用户 需要回滚
         */

        String account = loginParams.getAccount();
        String password = loginParams.getPassword();
        String nickname = loginParams.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(),"账号已经被注册了");
        }
        sysUser = new SysUser();
        sysUser.setNickname(nickname);
        sysUser.setAccount(account);
        sysUser.setPassword(DigestUtils.md5Hex(password + salt));
        sysUser.setCreateDate(System.currentTimeMillis());
        sysUser.setLastLogin(System.currentTimeMillis());
        sysUser.setAvatar("/static/img/logo.b3a48c0.png");
        sysUser.setAdmin(1); //1为true
        sysUser.setDeleted(0);  //0为false
        sysUser.setEmail("");
        sysUser.setSalt("");
        sysUser.setStatus("");
        sysUserService.save(sysUser);

        String token = JWTUtils.createToken(sysUser.getId());

        redisTemplate.opsForValue().set("TOKEN_"+token,JSON.toJSONString(sysUser) ,1,TimeUnit.DAYS);

        return Result.success(token);
    }

11.5 ErrorCode

 ACCOUNT_EXIST(10004,"账号已存在"),

11.6 SysUserService

/**
     * 根据账号查找用户
     * @param account
     * @return
     */
    SysUser findUserByAccount(String account);

    /**
     * 保存用户
     * @param sysUser
     */
    void save(SysUser sysUser);

11.7 SysUserServiceImpl

 @Override
    public SysUser findUserByAccount(String account) {
        LambdaQueryWrapper<SysUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(SysUser::getAccount,account);
        lambdaQueryWrapper.last("limit 1");
        return sysUserMapper.selectOne(lambdaQueryWrapper);
    }

    @Override
    public void save(SysUser sysUser) {
        //保存用户这 id会自动生成
        //注意:  这个地方 默认生成的id 是分布式id 采用了雪花算法
        sysUserMapper.insert(sysUser);
    }

11.8 加事务

出现错误就进行回滚防止添加异常

增加@Transactional注解

com.jihu.blog.service.LoginService

@Service
@Transactional
public class LoginServiceImpl implements LoginService {}

当然 一般建议将事务注解@Transactional加在 接口上,通用一些。

package com.jihu.blog.service;

import com.jihu.blog.dao.pojo.SysUser;
import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.LoginParams;
import org.springframework.transaction.annotation.Transactional;

@Transactional
public interface LoginService {

    /**
     * 登录功能
     * @param loginParams
     * @return
     */

    Result login(LoginParams loginParams);

    SysUser checkToken(String token);
    /**
     * 退出登陆
     * @param token
     * @return
     */

    Result logout(String token);

    /**
     * 注册
     * @param loginParams
     * @return
     */
    Result register(LoginParams loginParams);
}

11.9 测试

image-20220405141622703

12. 登录拦截器

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

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

springMVC中拦截器

可以,使用拦截器,进行登录拦截,如果遇到需要登录才能访问的接口,如果未登录,拦截器直接返回,并跳转登录页面。
Javas三大器:过滤器-监听器-拦截器

12.1 拦截器实现

com.jihu.blog.handler.LoginInterceptor

package com.jihu.blog.handler;

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.jihu.blog.dao.pojo.SysUser;
import com.jihu.blog.service.LoginService;
import com.jihu.blog.vo.ErrorCode;
import com.jihu.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private LoginService loginService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //在执行controller方法(handler)之前进行执行
        /**
         * 1.需要判断 请求的接口路径 是否为 HandlerMethod (controller方法),不是的话,放行
         * 2.判断token 是否为空, 如果为空 未登录
         * 3.如果token不为空,登录验证 loginService checkToken
         * 4.如果认证成功,放行即可
         */

        if (!(handler instanceof HandlerMethod)){
            return  true;
        }
        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.isBlank(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;
        }
        SysUser sysUser = loginService.checkToken(token);
        if (sysUser == 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;
        }

        //登录验证成功,放行
        return true;
    }
}

12.2 使拦截器生效

com.jihu.blog.config.WebMVCConfig

package com.jihu.blog.config;

import com.jihu.blog.handler.LoginInterceptor;
import com.jihu.blog.service.LoginService;
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 WebMVCConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    //实现跨域请求
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET","HEAD","POST","PUT","DELETE","OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //拦截test接口,后续实际遇到需要拦截的接口时,在配置为真正的拦截接口
        registry.addInterceptor(loginInterceptor)
                    .addPathPatterns("/test");
    }
}

12.3测试

com.jihu.blog.controller.TestController

@RestController
@RequestMapping("test")
public class TestController {

    @RequestMapping
    public Result test(){
        //得到用户的信息
        SysUser sysUser = UserThreadLocal.get();
        System.out.println(sysUser);

        return Result.success(null);
    }
}

用postman进行测试,先登录找到token,然后在验证,才能成功

image-20220407104057929

13.ThreadLocal保存用户信息

redis中只放了token我们希望直接获取用户信息
好处和如何使用的
使用ThreadLocal保存用户登录信息
使用ThreadLocal替代Session完成保存用户登录信息功能

使用ThreadLocal替代Session的好处:

 可以在同一线程中很方便的获取用户信息,不需要频繁的传递session对象。

具体实现流程:

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

相关问题
Session原理
COOKIE和SESSION有什么区别?

com.jihu.blog.utils.UserThreadLocal

package com.jihu.blog.utils;

import com.jihu.blog.dao.pojo.SysUser;

public class UserThreadLocal {

    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();
    }

}

com.jihu.blog.handler.LoginInterceptor

package com.jihu.blog.handler;

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.jihu.blog.dao.pojo.SysUser;
import com.jihu.blog.service.LoginService;
import com.jihu.blog.utils.UserThreadLocal;
import com.jihu.blog.vo.ErrorCode;
import com.jihu.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private LoginService loginService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //在执行controller方法(handler)之前进行执行
        /**
         * 1.需要判断 请求的接口路径 是否为 HandlerMethod (controller方法),不是的话,放行
         * 2.判断token 是否为空, 如果为空 未登录
         * 3.如果token不为空,登录验证 loginService checkToken
         * 4.如果认证成功,放行即可
         */

        if (!(handler instanceof HandlerMethod)){
            return  true;
        }
        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.isBlank(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;
        }
        SysUser sysUser = loginService.checkToken(token);
        if (sysUser == 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;
        }

        //登录验证成功,放行
        //我希望在controller中  直接获取用户的信息 怎么获取呢?
       UserThreadLocal.put(sysUser);
        return true;
    }

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

com.jihu.blog.controller.TestController

package com.jihu.blog.controller;

import com.jihu.blog.dao.pojo.SysUser;
import com.jihu.blog.utils.UserThreadLocal;
import com.jihu.blog.vo.Result;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("test")
public class TestController {

    @RequestMapping
    public Result test(){
        //得到用户的信息
        SysUser sysUser = UserThreadLocal.get();
        System.out.println(sysUser);

        return Result.success(null);
    }
}

测试

在postman中重新发送一次http://localhost:8888/test请求,

可以看到能拿到我们的信息

image-20220407110431934

14.ThreadLocal内存泄漏

ThreadLocal原理及内存泄露预防

image-20220407110553643

实线表强引用,虚线代表弱引用

每一个Thread维护一个ThreadLocalMap, key为使用弱引用的ThreadLocal实例,value为线程变量的副本。

强引用,使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。

如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。

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

13.文章详情

13.1接口说明

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

请求方式:POST

请求参数:

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

返回数据:

{success: true, code: 200, msg: "success",…}
code: 200
data: {id: "1405916999732707330", title: "SpringBoot入门案例", summary: "springboot入门案例", commentCounts: 0,…}
msg: "success"
success: true

13.2涉及到的表

内容表

content存放makedown格式的信息
content_html存放html格式的信息

CREATE TABLE `blog`.`ms_article_body`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `content` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
  `content_html` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
  `article_id` bigint(0) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `article_id`(`article_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 38 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

类别表
avata分类图标路径
category_name图标分类的名称
description分类的描述

image-20220407142401240

CREATE TABLE `blog`.`ms_category`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  `category_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

13.3 pojo层

Category

package com.jihu.blog.dao.pojo;

import lombok.Data;

@Data
public class Category {

    private Long id;

    private String avatar;

    private String categoryName;

    private String description;

}

ArticleBody

package com.jihu.blog.dao.pojo;

import lombok.Data;
//内容表
@Data
public class ArticleBody {
    private Long id;
    private String content;
    private String contentHtml;
    private Long articleId;

}

13.4 Controller

com.jihu.blog.controller.ArticleController

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

13.5 Service层

文章表里面只有tiltle以及一些简介
ms_article 中body_id对应第二张表ms_article_body上的id
ms_category会映射到ms_article 中的category_id
需要做一些相对的关联查询

ArticleService

 /**
     * 查看文章详情
     * @param articleId
     * @return
     */
    Result findArticleById(Long articleId);

ArticleServiceImpl

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 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();
            articleVo.setTags(tagService.findTagsByArticleId(articleId));
        }
        if (isAuthor){
            Long authorId = article.getAuthorId();
            articleVo.setAuthor(sysUserService.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;
    }

    private ArticleBodyVo findArticleBodyById(Long bodyId) {
        ArticleBody articleBody = articleBodyMapper.selectById(bodyId);
        ArticleBodyVo articleBodyVo = new ArticleBodyVo();
        articleBodyVo.setContent(articleBody.getContent());
        return articleBodyVo;
    }


@Override
    public Result findArticleById(Long articleId) {
        /**
         * 1.根据id查询 文章信息
         * 2.根据bodyId和categoryid 去做关联查询
         */
        Article article = articleMapper.selectById(articleId);
        ArticleVo articleVo = copy(article,true,true,true,true);
        //查看完文章了,新增阅读数,有没有问题呢?
        //查看完文章之后,本应该直接返回数据了,这时候做了一个更新操作,更新时加写锁,阻塞其他的读操作,性能就会比较低
        // 更新 增加了此次接口的 耗时 如果一旦更新出问题,不能影响 查看文章的操作
        //线程池  可以把更新操作 扔到线程池中去执行,和主线程就不相关了
        threadService.updateArticleViewCount(articleMapper,article);
        return Result.success(articleVo);
    }

CategoryService

package com.jihu.blog.service;
import com.jihu.blog.vo.CategoryVo;
public interface CategoryService {
    CategoryVo findCategoryById(Long id);
}

CategoryServiceImpl

package com.jihu.blog.service.impl;

import com.jihu.blog.dao.mapper.CategoryMapper;
import com.jihu.blog.dao.pojo.Category;
import com.jihu.blog.service.CategoryService;
import com.jihu.blog.vo.CategoryVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CategoryServiceImpl implements CategoryService {

    @Autowired
    private CategoryMapper categoryMapper;

    @Override
    public CategoryVo findCategoryById(Long id) {
        Category category = categoryMapper.selectById(id);
        CategoryVo categoryVo = new CategoryVo();
        //因为category,categoryVo属性一样所以可以使用 BeanUtils.copyProperties
        BeanUtils.copyProperties(category,categoryVo);
        return categoryVo;
    }
}

13.6 Vo层

ArticleVo

package com.jihu.blog.vo;

import lombok.Data;

import java.util.List;

//建立与前端交互的Vo文件
@Data
public class ArticleVo {
    private Long id;

    private String title;

    private String summary;

    private int commentCounts;

    private int viewCounts;

    private int weight;
    /**
     * 创建时间
     */
    private String createDate;

    private String author;

    private ArticleBodyVo body;

    private List<TagVo> tags;

    private CategoryVo category;
}

ArticleBodyVo

package com.jihu.blog.vo;

import lombok.Data;

@Data
public class ArticleBodyVo {

    //内容
    private String content;
}

CategoryVo

package com.jihu.blog.vo;

import lombok.Data;

@Data
public class CategoryVo {
    //id,图标路径,图标名称
    private Long id;

    private String avatar;

    private String categoryName;
}

13.7 mapper层

ArticleBodyMapper

package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.pojo.ArticleBody;

public interface ArticleBodyMapper  extends BaseMapper<ArticleBody> {
}

CategoryMapper

package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.pojo.Category;

public interface CategoryMapper  extends BaseMapper<Category> {
}

13.8 测试

image-20220407164516526

14.使用线程池 更新阅读次数

/查看完文章了,新增阅读数,有没有问题呢?
//查看完文章之后,本应该直接返回数据了,这时候做了一个更新操作,更新时加写锁,阻塞其他的读操作,性能就会比较低(没办法解决,增加阅读数必然要加锁)
//更新增加了此次接口的耗时(考虑减少耗时)如果一旦更新出问题,不能影响查看操作
想到了一个技术 线程池
可以把更新操作扔到 线程池中去执行和主线程就不相关了

什么是乐观锁,什么是悲观锁
CAS原理分析

14.1线程池配置

做一个线程池的配置来开启线程池

com.jihu.blog.config.ThreadPoolConfig

package com.jihu.blog.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

//https://www.jianshu.com/p/0b8443b1adc9   关于@Configuration和@Bean的用法和理解
@Configuration
@EnableAsync
public class ThreadPoolConfig {

    @Bean("taskExecutor")
    public Executor asyncServiceExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        executor.setCorePoolSize(5);
        // 设置最大线程数
        executor.setCorePoolSize(20);
        //配置队列大小
        executor.setQueueCapacity(Integer.MAX_VALUE);
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(60);
        // 设置默认线程名称
        executor.setThreadNamePrefix("码神之路博客项目");
        // 等待所有任务结束后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        //执行初始化
        executor.initialize();
        return executor;

    }
}

14.2 使用

com.jihu.blog.service.ThreadService

package com.jihu.blog.service;

import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.jihu.blog.dao.mapper.ArticleMapper;
import com.jihu.blog.dao.pojo.Article;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class ThreadService {
    //期望此操作在线程池执行不会影响原有主线程
    //这里线程池不了解可以去看JUC并发编程
    @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更新
        updatewrapper.eq(Article::getId,article.getId());
        //设置一个为了在多线程的环境下线程安全
        //改之前再确认这个值有没有被其他线程抢先修改,类似于CAS操作 cas加自旋,加个循环就是cas
        updatewrapper.eq(Article::getViewCounts,viewCounts);
        // update article set view_count=100 where view_count=99 and id =111
        //实体类加更新条件
        articleMapper.update(articleupdate,updatewrapper);

        try {
            Thread.sleep(5000);
            System.out.println("更新完成了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

com.jihu.blog.service.impl.ArticleServiceImpl

@Autowired
private ThreadService threadService;

@Override
public ArticleVo findArticleById(Long id) {
    Article article = articleMapper.selectById(id);
    //线程池
    threadService.updateViewCount(articleMapper,article);
    return copy(article,true,true,true,true);
}

14.3测试

睡眠 ThredService中的方法 5秒,不会影响主线程的使用,即文章详情会很快的显示出来,不受影响

Bug修正
之前Article中的commentCounts,viewCounts,weight 字段为int,会造成更新阅读次数的时候,将其余两个字段设为初始值0
mybatisplus在更新文章阅读次数的时候虽然只设立了articleUpdate.setviewsCounts(viewCounts+1),
但是int默认基本数据类型为0,
mybatisplus但凡不是null就会生成到sql语句中进行更新。会出现

image-20220407165039295

理想中应该是只有views_counts改变但是因为mybatisplus规则所以会出现这个现象
所以将int改为Integer就不会出现这个问题。

15.评论列表

评论表
id评论id
content评论内容
create_date评论时间
article_id评论文章
author_id谁评论的
parent_id盖楼功能对评论的评论进行回复
to_uid给谁评论
level评论的是第几层(1级表示最上层的评论,2表示对评论的评论)

CREATE TABLE `blog`.`ms_comment`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  `create_date` bigint(0) NOT NULL,
  `article_id` int(0) NOT NULL,
  `author_id` bigint(0) NOT NULL,
  `parent_id` bigint(0) NOT NULL,
  `to_uid` bigint(0) NOT NULL,
  `level` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `article_id`(`article_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

15.1接口说明

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

请求方式:GET

请求参数:

参数名称 参数类型 说明
id long 文章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
        }
    ]
}

15.2 CommentsController

@RequestMapping("comments")
@RestController
public class CommentsController {

    @Autowired
    private CommentsService commentsService;

    /**
     * 评论列表
     * @param id
     * @return
     */
    @GetMapping("article/{id}")
    public Result comments(@PathVariable Long id){
        return  commentsService.commentsByArticleId(id);

    }
}    

15.3 Service层

15.3.1 CommentsService

public interface CommentsService {

    /**
     * 根据文章id查询所有的评论列表
     * @param id
     * @return
     */
    Result commentsByArticleId(Long id);
}    

15.3.2 CommentsServiceImpl

package com.jihu.blog.service.impl;

@Service
public class CommentsServiceImpl implements CommentsService {

    @Autowired
    private CommentMapper commentMapper;

    @Autowired
    private SysUserService sysUserService;

    @Override
    public Result commentsByArticleId(Long id) {
        /**
         * 1.根据文章id 查询 评论列表,从comment表中查询
         * 2.根据作者的id  查询作者的信息
         * 3.判断如果level=1,要去查询它有没有子评论
         * 4.如果有 根据评论id 进行查询(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);
    }

    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();
        // 相同属性copy
        BeanUtils.copyProperties(comment,commentVo);
        //作者信息
        Long authorId = comment.getAuthorId();
        UserVo userVo= sysUserService.findUserVoById(authorId);
        commentVo.setAuthor(userVo);

        //子评论
        Integer level = comment.getLevel();
        if (level == 1){
            Long id = comment.getId();
            List<CommentVo> commentVoList = findCommentByParentId(id);
            commentVo.setChildrens(commentVoList);
        }

        //to user  给谁评论
        if (level>1){
            Long toUid = comment.getToUid();
            UserVo toUserVo = sysUserService.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);
        List<Comment> comments = commentMapper.selectList(queryWrapper);
        return copyList(comments);
    }
}

15.3.3 SysUserService

  /**
     * 查询UserVo的信息
     * 查询用户信息的服务:
     * @param id
     * @return
     */
    UserVo findUserVoById(Long id);

15.3.4 SysUserServiceImpl

@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("马神之路");
        }
        UserVo userVo = new UserVo();
        BeanUtils.copyProperties(sysUser,userVo);

        return userVo;
    }

15.4 Vo层

用于返回的数据:

CommentVo

com.jihu.blog.vo.CommentVo

package com.jihu.blog.vo;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;

import java.util.List;

@Data
public class CommentVo {
    //防止前端 精度损失  把id转为string
    //把 id转化为string类型的  要不然会丢精度  导致错误
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
    private UserVo author;
    private String content;
    private List<CommentVo> childrens;
    private String createDate;
    private Integer level;
    private UserVo toUser;
}

UserVo

com.jihu.blog.vo.UserVo

package com.jihu.blog.vo;
import lombok.Data;
@Data
public class UserVo {

    private Long id;
    private String avatar;
    private String nickname;
}

15.5 测试

image-20220407211808734

16.评论

16.1 接口说明

接口url:/comments/create/change

请求方式:POST

请求参数:

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

返回数据:

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

16.2加入到登录拦截器中

com.jihu.blog.config.WebMVCConfig

@Override
    public void addInterceptors(InterceptorRegistry registry) {
        //拦截test接口,后续实际遇到需要拦截的接口时,在配置为真正的拦截接口
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/test").addPathPatterns("/comments/create/change");
    }

16.3 新建CommentParam

构建评论参数对象:

com.jihu.blog.vo.params.CommentParam

package com.jihu.blog.vo.params;

import lombok.Data;

@Data
public class CommentParam {

    private Long articleId;

    private String content;

    private Long parent;

    private Long toUserId;
}

16.4 修改CommentsController

/**
     * 评论
     * @param commentParam
     * @return
     */
    @PostMapping("create/change")
    public Result comment(@RequestBody CommentParam commentParam){
        return  commentsService.comment(commentParam);

    }

16.5 修改CommentsService

 // 评论
 Result comment(CommentParam commentParam);

16.6 修改CommentsServiceImpl

 @Override
    public Result comment(CommentParam commentParam) {
        //拿到当前用户
        SysUser sysUser = UserThreadLocal.get();
        Comment comment = new Comment();
        comment.setAuthorId(sysUser.getId());
        comment.setArticleId(commentParam.getArticleId());
        comment.setContent(commentParam.getContent());
        comment.setCreateDate(System.currentTimeMillis());
        Long parent = commentParam.getParent();
        if (parent == null || parent == 0 ){
            comment.setLevel(1);
        }else {
            comment.setLevel(2);
        }
        //如果是空,parent就是0
        comment.setParentId(parent == null ? 0 : parent);
        Long toUserId = commentParam.getToUserId();
        comment.setToUid(toUserId == null ? 0 : toUserId);
        commentMapper.insert(comment);
        return Result.success(null);
    }

修改 com.jihu.blog.vo.CommentVo

  //防止前端 精度损失 把id转为string
// 分布式id 比较长,传到前端 会有精度损失,必须转为string类型 进行传输,就不会有问题了
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;

16.7 测试

image-20220407211841583

17、写文章

写文章由 三部分组成:

  1. 获取所有文章类别
  2. 获取所有标签
  3. 发布文章

17.1 文章分类

17.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":"编程语言"}
    ]
}

17.1.2 CategoryController

package com.jihu.blog.controller;


import com.jihu.blog.service.CategoryService;
import com.jihu.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("categorys")
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

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

17.1.3 CategoryService

Result findAll();

17.1.4 CategoryServiceImpl

@Override
    public Result findAll() {
        // 没有任何参数,所有一个空的LambdaQueryWrapper即可
        List<Category> categories = categoryMapper.selectList(new LambdaQueryWrapper<>());
        //页面交互的对象
        return Result.success(copyList(categories));
    }

    private List<CategoryVo> copyList(List<Category> categories) {
        ArrayList<CategoryVo> categoryVoList = new ArrayList<>();
        for (Category category : categories) {
            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;
    }

17.1.5 测试

image-20220407224303242

17.2 获取所有标签

17.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"
        }
    ]
}

17.2.2 TagsController

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

17.2.3 TagService

    /**
     * 查询所有文章标签
     * @return
     */
    Result findAll();

17.2.4 TagServiceImpl

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

17.2.5 测试

image-20220407224549033

17.3 发布文章

17.3.1接口说明

请求内容是object({content: “ww”, contentHtml: “ww↵”})是因为本身为makedown的编辑器
id指的是文章id

image-20220407224723527

接口url:/articles/publish

请求方式:POST

请求参数:

image-20220408120619584

返回数据:

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

17.3.2修改ArticleController

  @PostMapping("publish")
    public Result publish(@RequestBody ArticleParam articleParam){
        return articleService.publish(articleParam);
    }

17.3.3 参数 param

ArticleParam
package com.jihu.blog.vo.params;

import com.jihu.blog.vo.CategoryVo;
import com.jihu.blog.vo.TagVo;
import lombok.Data;

import java.util.List;

@Data
public class ArticleParam {

    private Long id;

    private ArticleBodyParam body;

    private CategoryVo category;

    private String summary;

    private List<TagVo> tags;

    private String title;

}
ArticleBodyParam
package com.jihu.blog.vo.params;

import lombok.Data;

@Data
public class ArticleBodyParam {
    private String content;

    private String contentHtml;
}

17.3.4 Service层

ArticleService
 /**
     * 文章发布服务
     * @param articleParam
     * @return
     */
    Result publish(ArticleParam articleParam);
ArticleServiceImpl
@Override
    public Result publish(ArticleParam articleParam) {
        //注意想要拿到数据必须将接口加入到登录拦截当中
        SysUser sysUser = UserThreadLocal.get();
        /**
         * 1.发布文章  目的  构建Article对象
         * 2.作者id 当前的登录用户
         *3.标签  要将标签加入到 关联表当中
         * 4.body 内容存储 article bodyId
         */
        Article article = new Article();
        article.setAuthorId(sysUser.getId());
        article.setCategoryId(articleParam.getCategory().getId());
        article.setCreateDate(System.currentTimeMillis());
        article.setCommentCounts(0);
        article.setSummary(articleParam.getSummary());
        article.setTitle(articleParam.getTitle());
        article.setViewCounts(0);
        article.setWeight(Article.Article_Common);
        article.setBodyId(-1L);
        //插入之后 会生成一个文章id(因为新建的文章没有文章id所以要insert一下
        //官网解释:"insert后主键会自动'set到实体的ID字段。所以你只需要"getid()就好
        //利用主键自增,mp的insert操作后id值会回到参数对象中
        //https://blog.csdn.net/HSJ0170/article/details/107982866
        articleMapper.insert(article);

        //tags
        List<TagVo> tags = articleParam.getTags();
        if ( tags != null){
            for (TagVo tag : tags) {
                ArticleTag articleTag = new ArticleTag();
                articleTag.setArticleId(article.getId());
                articleTag.setTagId(tag.getId());
                articleTagMapper.insert(articleTag);
            }
        }

        //body
        ArticleBody articleBody = new ArticleBody();
        articleBody.setContent(articleParam.getBody().getContent());
        articleBody.setContentHtml(articleParam.getBody().getContentHtml());
        articleBody.setArticleId(article.getId());
        articleBodyMapper.insert(articleBody);
        //插入完之后再给一个id
        article.setBodyId(articleBody.getId());
        //MybatisPlus中的save方法什么时候执行insert,什么时候执行update
        // https://www.cxyzjd.com/article/Horse7/103868144
        //只有当更改数据库时才插入或者更新,一般查询就可以了

        articleMapper.updateById(article);
        Map<String,String > map = new HashMap<>();
        map.put("id",article.getId().toString());
        //或者这样也行
//        ArticleVo articleVo = new ArticleVo();
//        articleVo.setId(article.getId());
//        return Result.success(articleVo);

        return Result.success(map);
    }

17.3.5 修改 WebMVCConfig

当然登录拦截器中,需要加入发布文章的拦截:

 @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //拦截test接口,后续实际遇到需要拦截的接口时,在配置为真正的拦截接口
        registry.addInterceptor(loginInterceptor)
                    .addPathPatterns("/test")
                    .addPathPatterns("/comments/create/change")
                    .addPathPatterns("/articles/publish");
    }

17.3.6 mapper层

ArticleBodyMapper
package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.pojo.ArticleBody;

public interface ArticleBodyMapper  extends BaseMapper<ArticleBody> {
}

ArticleTagMapper
package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.pojo.ArticleTag;

public interface ArticleTagMapper extends BaseMapper<ArticleTag> {
}

17.3.7 ArticleVo

package com.jihu.blog.vo;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;

import java.util.List;

//建立与前端交互的Vo文件
@Data
public class ArticleVo {
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;

    private String title;

    private String summary;

    private int commentCounts;

    private int viewCounts;

    private int weight;
    /**
     * 创建时间
     */
    private String createDate;

    private String author;

    private ArticleBodyVo body;

    private List<TagVo> tags;

    private CategoryVo category;
}

17.3.8 ArticleTag

package com.jihu.blog.dao.pojo;

import lombok.Data;

@Data
public class ArticleTag {

    private Long id;

    private Long articleId;

    private Long tagId;
}

17.3.9 测试

image-20220408124010456

18.AOP日志

IOC是spring的两大核心概念之一,IOC给我们提供了一个IOCbean容器,这个容器会帮我们自动去创建对象,不需要我们手动创建,IOC实现创建的通过DI(Dependency Injection 依赖注入),我们可以通过写Java注解代码或者是XML配置方式,把我们想要注入对象所依赖的一些其他的bean,自动的注入进去,他是通过byName或byType类型的方式来帮助我们注入。正是因为有了依赖注入,使得IOC有这非常强大的好处,解耦。

可以举个例子,JdbcTemplate 或者 SqlSessionFactory 这种bean,如果我们要把他注入到容器里面,他是需要依赖一个数据源的,如果我们把JdbcTemplate 或者 Druid 的数据源强耦合在一起,会导致一个问题,当我们想要使用jdbctemplate必须要使用Druid数据源,那么依赖注入能够帮助我们在Jdbc注入的时候,只需要让他依赖一个DataSource接口,不需要去依赖具体的实现,这样的好处就是,将来我们给容器里面注入一个Druid数据源,他就会自动注入到JdbcTemplate如果我们注入一个其他的也是一样的。比如说c3p0也是一样的,这样的话,JdbcTemplate和数据源完全的解耦了,不强依赖与任何一个数据源,在spring启动的时候,就会把所有的bean全部创建好,这样的话,程序在运行的时候就不需要创建bean了,运行速度会更快,还有IOC管理bean的时候默认是单例的,可以节省时间,提高性能,

Spring IOC ,AOP,MVC 的理解

Springboot AOP日志相关讲解

在不改变原有方法基础上对原有方法进行增强

1. 新建LogAnnotation

com.jihu.blog.commom.aop.LogAnnotation

package com.jihu.blog.commom.aop;

import java.lang.annotation.*;

//type代表可以放在类上面  ,method代表可以放在方法上
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {
    String  module() default  "";
    String  operator() default "";

}

2.修改ArticleController

com.jihu.blog.controller.ArticleController

    //首页  文章列表
    @PostMapping
    //加上此注解,代表要对此接口记录日志
    @LogAnnotation(module="文章",operator="获取文章列表")
    public Result listArticle(@RequestBody PageParams pageParams){
//        int  i =10/0;
        //ArticleVo 页面接收的数据
        return articleService.listArticle(pageParams);

    }

3.新建LogAspect

com.jihu.blog.commom.aop.LogAspect

package com.jihu.blog.commom.aop;

import com.alibaba.fastjson.JSON;
import com.jihu.blog.utils.HttpContextUtils;
import com.jihu.blog.utils.IpUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

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

    @Pointcut("@annotation(com.jihu.blog.commom.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================================");

    }
}

​ 用到的方法类

4.新建HttpContextUtils

com.jihu.blog.utils.HttpContextUtils

package com.jihu.blog.utils;

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();
    }
}

5.新建IpUtils

com.jihu.blog.utils.IpUtils

package com.jihu.blog.utils;

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

6.测试

image-20220408152708831

bug修正

因为数据库中的create_date类型是bigint ,不是date类型的 所有需要进行如下操作。

防止拿到的值是null值,因为拿到的是毫秒值,需要对其进行转化,Y表示年,m表示月,对时间进行重写。

image-20220408153106485

相关函数说明

修改ArticleMapper.xml

select FROM_UNIXTIME(create_date/1000,'%Y') as year, FROM_UNIXTIME(create_date/1000,'%m') as month,count(*) as count from ms_article group by year,month

19.文章图片上传

19.1接口说明

接口url:/upload

请求方式:POST

请求参数:

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

返回数据:

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

修改pom文件引入七牛云的sdk
pom.xml

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

19.2 新建UploadController

package com.jihu.blog.controller;

import com.mszlu.blog.utils.QiniuUtils;
import com.mszlu.blog.vo.Result;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.util.UUID;

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

    //https://blog.csdn.net/justry_deng/article/details/80855235 MultipartFile介绍
    @PostMapping
    public Result upload(@RequestParam("image")MultipartFile file){
        //原始文件名称 比如说aa.png
        String originalFilename = file.getOriginalFilename();
        //唯一的文件名称
        String fileName =  UUID.randomUUID().toString()+"."+StringUtils.substringAfterLast(originalFilename, ".");
        //上传文件上传到那里呢? 七牛云 云服务器
        //降低我们自身应用服务器的带宽消耗
        boolean upload = qiniuUtils.upload(file, fileName);
        if (upload) {
            return Result.success(QiniuUtils.url+fileName);
        }
        return Result.fail(20001,"上传失败");

}

19.3 使用七牛云

注意七牛云测试域名 https://static.mszlu.com/ 一个月一回收,记得去修改。
springboot默认只上传1M的图片大小所以修改文件配置
src/main/resources/application.properties

# 上传文件总的最大值
spring.servlet.multipart.max-request-size=20MB
# 单个文件的最大值
spring.servlet.multipart.max-file-size=2MB

七牛云建立存储空间教程

19.4新建QiniuUtils

com.jihu.blog.utils.QiniuUtils.java

package com.mszlu.blog.utils;

import com.alibaba.fastjson.JSON;
import com.qiniu.http.Response;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

@Component
public class QiniuUtils {

    public static  final String url = "https://static.mszlu.com/";

//修改以下两个值放到proprietarties中,在密钥管理中获取
    @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 = "mszlu";
        //默认不指定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;
    }
}

20.导航-文章分类

20.1 查询所有的文章分类

20.1.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": "后端最牛叉"
        }
    ]
}

20.1.2 修改CategoryVo

@Data
public class CategoryVo {

    private Long id;

    private String avatar;

    private String categoryName;

    private String description;
}

20.1.3 修改CategoryController

 @GetMapping("detail")
    public Result  categorydetail(){
        return categoryService.findCategoryByDetail();
    }

20.1.4 修改CategoryService

/**
     * 查询所有文章分类
     * @return
     */
    Result findCategoryByDetail();

20.1.5 修改CategoryServiceImpl

@Override
    public Result findCategoryByDetail() {
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        List<Category> categories = categoryMapper.selectList(queryWrapper);
        return Result.success(copyList(categories));
    }

20.1.6 文章分类显示

image-20220408194105092

20.2 查询所有的标签

20.2.1 接口说明

接口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"
        }
    ]
}

20.2.2 修改TagVo

@Data
public class TagVo {

    private Long id;

    private String tagName;

    private String avatar;
}

20.2.3 修改TagsController

@GetMapping("detail")
    public Result findTagDetail(){
        return tagService.findTagDetail();
    }

20.2.4 修改TagService

 /**
     * 查询所有的标签
     * @return
     */
    Result findTagDetail();

20.2.5 修改TagServiceImpl

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

20.2.6 显示结果:

image-20220408194358661

21.分类文章列表

21.1接口说明

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

请求方式:GET

请求参数:

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

返回数据:

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

21.2修改CategoryController

@GetMapping("detail/{id}")
    public Result  categorydetailById(@PathVariable("id") Long id){
        return categoryService.categoryDetailById(id);
    }

21.3修改CategoryService

Result categoryDetailById(Long id);

21.4修改CategoryServiceImpl

  @Override
    public Result categoryDetailById(Long id) {
//        LambdaQueryWrapper<Category> queryWrapper  = new LambdaQueryWrapper<>();
        Category category = categoryMapper.selectById(id);
        return Result.success(copy(category));
    }

完成上面这些只能说是可以显示文章分类的图标了

image-20220408200924828

但是如果想显示后端所有的归属内容得在文章查询列表出进行queryWrapper查找,当文章分类标签不是null时,加入文章分类标签这个查询元素进行分类修改。

21.5修改ArticleServiceImpl

@Override
    public Result listArticle(PageParams pageParams) {

        Page<Article> page = new Page<>(pageParams.getPage(), pageParams.getPageSize());
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();

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

        //是否置顶进行排序
        queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);

        Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);
        List<Article> records = articlePage.getRecords();
        //能直接返回吗  肯定不行  所以需要进行如下转换
       List<ArticleVo> articleVoList = copyList(records,true,true);
        return Result.success(articleVoList);
    }

21.6修改PageParams

@Data
public class PageParams {

    private int page = 1;

    private int pageSize = 10;

    private Long categoryId;

    private Long tagId;
}

最后就可以显示所有文章分类的每个标签下的内容了

image-20220408201123124

22.标签文章列表

22.1接口说明

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

请求方式:GET

请求参数:

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

返回数据:

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

22.2 修改TagsController

  /**
     * 查询所有文章标签下所有的文章
     * @return
     */
    @GetMapping("detail/{id}")
    public Result findTagDetailById(@PathVariable("id") Long id){
        return tagService.findTagDetailById(id);
    }

22.3修改TagService

Result findTagDetailById(Long id);

22.4修改TagServiceImpl

 @Override
    public Result findTagDetailById(Long id) {
        Tag tag = tagMapper.selectById(id);
        return Result.success(copy(tag));
    }

完成上面这些这保证了文章标签显示出来了我们需要重写文章查询接口,保证当遇到标签查询时我们可以做到正确查询文章标签所对应的内容,要不每一个标签查出来的内容都是一样的。

image-20220408210356145

22.5修改ArticleServiceImpl

@Override
    public Result listArticle(PageParams pageParams) {

        Page<Article> page = new Page<>(pageParams.getPage(), pageParams.getPageSize());
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();

        //相当于  category_id = #{category_id}
        //根据文章分类获取文章
        if (pageParams.getCategoryId() != null){
            queryWrapper.eq(Article::getCategoryId,pageParams.getCategoryId());
        }
-----------添加的代码开始------------
        //根据标签获取文章
        ArrayList<Long> articleIdList = new ArrayList<>();
        if (pageParams.getTagId() != null){
            //加入标签条件查询
            //article表中并没有tag字段 一篇文章有多个标签
            //articie_tog article_id 1:n tag_id
            //我们需要利用一个全新的属于文章标签的queryWrapper将这篇文章的article_Tag查出来,保存到一个list当中。
            // 然后再根据queryWrapper的in方法选择我们需要的标签即可。
            LambdaQueryWrapper<ArticleTag> queryWrapper1 = new LambdaQueryWrapper<>();
             queryWrapper1.eq(ArticleTag::getTagId, pageParams.getTagId());
            List<ArticleTag> articleTags = articleTagMapper.selectList(queryWrapper1);
            for (ArticleTag articleTag : articleTags) {
                articleIdList.add(articleTag.getArticleId());
            }
            if (articleIdList.size() > 0){
                // and id in (1,2,3)
                queryWrapper.in(Article::getId,articleIdList);
            }
        }
---------添加的代码结束-----------------
        //是否置顶进行排序
        queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);

        Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);
        List<Article> records = articlePage.getRecords();
        //能直接返回吗  肯定不行  所以需要进行如下转换
       List<ArticleVo> articleVoList = copyList(records,true,true);
        return Result.success(articleVoList);
    }

22.6测试

最终的结果如下,每一个标签下都对应着该标签所对应的文章

image-20220408210807756

23. 归档文章列表

23.1接口说明

接口url:/articles

请求方式:POST

请求参数:

参数名称 参数类型 说明
year string
month string

返回数据

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

mybatisplus驼峰命名和mapper.xml使用

23.2修改PageParams

package com.jihu.blog.vo.params;

import lombok.Data;

@Data
public class PageParams {
    private  int Page =1;  //当前页数
    private  int PageSize =10;  //每页显示的数量

    private Long categoryId;
    private Long tagId;

    private String year;
    private String month;

    //为了让传递的值为6变成06
    public String getMonth(){
        if (month != null && this.month.length()==1){
            return "0"+this.month;
        }
        return  this.month;
    }

}

23.3修改ArticleServiceImpl

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

23.4修改ArticleMapper

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

23.5修改ArticleMapper.xml

<!--因为与数据库字段没法进行一一映射  所以做这么个映射关系-->
<resultMap id="articleMap" type="com.jihu.blog.dao.pojo.Article">
        <id column="id" property="id" />
        <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="author_id" property="authorId"/>
        <result column="body_id" property="bodyId"/>
        <result column="category_id" property="categoryId"/>
    </resultMap>



<select id="listArticle" resultType="com.jihu.blog.dao.pojo.Article" resultMap="articleMap" >
    select  * from ms_article
    <where>
    <!--加  1 = 1 是为了去掉 多余的and  要不然会成为 where and category_id=?  mybatis中不用加,它会自动去掉-->
    1 = 1
    <if test="categoryId != null ">
       and category_id = #{categoryId}
    </if>
    <if test="tagId != null">
        and id in (select article_id from ms_article_tag where tag_id=#{tagId})
     </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>
    </where>
    order by weight,create_date desc

</select>

resultMap和resultType区别 https://blog.csdn.net/xushiyu1996818/article/details/89075069?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-4.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-4.no_search_link
驼峰命名法 https://blog.csdn.net/A_Java_Dog/article/details/107006391?spm=1001.2101.3001.6650.6&utm_medium=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~default-6.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~default-6.no_search_link
mybatis中xml文件用法 https://blog.csdn.net/weixin_43882997/article/details/85625805
动态sql https://www.jianshu.com/p/e309ae5e4a77
驼峰命名 https://zoutao.blog.csdn.net/article/details/82685918?spm=1001.2101.3001.6650.18&utm_medium=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~default-18.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~default-18.no_search_link

23.6测试

image-20220408223431687

24、统一缓存处理(优化)

内存的访问速度 远远大于 磁盘的访问速度 (1000倍起)
Spring Cache介绍

Cache

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {
    long expire() default  1 * 60 * 1000;
    String name() default  "";
}

CacheAspect

package com.jihu.blog.commom.cache;

import com.alibaba.fastjson.JSON;
import com.jihu.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.Duration;

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

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Pointcut("@annotation(com.jihu.blog.commom.cache.Cache)")
    public void pt(){}

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp){
        try {
            Signature signature = pjp.getSignature();
            //类名
            String className = pjp.getTarget().getClass().getSimpleName();
            //调用的方法名
            String methodName = signature.getName();


            Class[] parameterTypes = new Class[pjp.getArgs().length];
            Object[] args = pjp.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)) {
                //加密 以防出现key过长以及字符转义获取不到的情况
                params = DigestUtils.md5Hex(params);
            }
            Method method = pjp.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);
            //获取Cache注解
            Cache annotation = method.getAnnotation(Cache.class);
            //缓存过期时间
            long expire = annotation.expire();
            //缓存名称
            String name = annotation.name();
            //先从redis获取
            String redisKey = name + "::" + className+"::"+methodName+"::"+params;
            String redisValue = redisTemplate.opsForValue().get(redisKey);
            if (StringUtils.isNotEmpty(redisValue)){
                log.info("走了缓存~~~,{},{}",className,methodName);
                return JSON.parseObject(redisValue, Result.class);
            }
            Object proceed = pjp.proceed();
            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,"系统错误");

    }
}

使用:

在需要的地方加上就可以了

image-20220409105124796

测试

image-20220409105231282

思考别的优化

mongodb
redis incr

1.文章可以放人es当中,便于后续中文分词搜索。

2.评论数据,可以考虑放到mongodb当中,电商系统当中 评论数据放入mongdb中

3.阅读数和评论数,考虑把阅读数和评论数 增加的时候放入 redis incr 自增,使用定时任务 定时把数据固化到数据库当中

4.为了加快访问速度,部署的时候,可以把图片,js,css等放入七牛云存储,加快网站访问速度。

25.管理后台

25.1搭建项目

25.1.1 新建maven工程 blog-admin

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>blog-parent</artifactId>
        <groupId>com.jihu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>blog-admin</artifactId>

    <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>

        <!-- 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>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>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </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>
        <!--时间的依赖-->
        <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-security</artifactId>-->
<!--        </dependency>-->
    </dependencies>
</project>

25.1.2 application.properties:

#server
server.port= 8889
spring.application.name=mszlu_admin_blog
# datasource
spring.datasource.url=jdbc:mysql://localhost:3306/blog1?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC
spring.datasource.username=root
spring.datasource.password=123456
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_

#指定mapper文件的位置
mybatis-plus.mapper-locations=classpath:mapper/*.xml

25.1.3 mybatis-plus配置:

com.jihu.blog.admin.config.MybatisPlusConfig

package com.jihu.blog.admin.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

25.1.4 启动类

package com.jihu.blog.admin;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AdminApp {

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

25.1.5导入前端工程

放入resources下的static目录中

25.1.6新建表

后台管理用户表

CREATE TABLE `blog`.`ms_admin`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

权限表

CREATE TABLE `blog`.`ms_permission`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  `path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

用户和权限的关联表

CREATE TABLE `blog`.`ms_admin_permission`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `admin_id` bigint(0) NOT NULL,
  `permission_id` bigint(0) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

权限管理页面展示

25.1.7AdminController

package com.jihu.blog.admin.controller;

import com.jihu.blog.admin.model.params.PageParam;
import com.jihu.blog.admin.pojo.Permission;
import com.jihu.blog.admin.service.PermissionService;
import com.jihu.blog.admin.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("admin")
public class AdminController {
    @Autowired
    private PermissionService permissionService;

    @PostMapping("permission/permissionList")
    public Result listpermission(@RequestBody PageParam pageParam){
        return permissionService.listpermission(pageParam);

    }

    @PostMapping("permission/add")
    public Result permissionadd(@RequestBody Permission permission){
        return permissionService.permissionadd(permission);
    }

    @PostMapping("permission/update")
    public Result permissionupdate(@RequestBody Permission permission){
        return permissionService.permissionupdate(permission);
    }

    @GetMapping("permission/delete/{id}")
    public Result permissiondeleteById(@PathVariable("id") Long id){
        return permissionService.permissiondeleteById(id);
    }
}

25.1.8 新建Permission

com.jihu.blog.admin.pojo.Permission

package com.jihu.blog.admin.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

@Data
public class Permission {
    @TableId(type =IdType.AUTO)
    private Long id;

    private String name;

    private String path;

    private String description;
}

25.1.9 新建PageParam

com.jihu.blog.admin.model.params.PageParam

package com.jihu.blog.admin.model.params;
import lombok.Data;
@Data
public class PageParam {
    //当前页
    private Integer currentPage;
    //页面个数
    private Integer pageSize;
    //查询条件
    private String queryString;
}

25.1.10 新建PageResult

package com.jihu.blog.admin.vo;
import lombok.Data;
import java.util.List;
@Data
public class PageResult<T> {
    private List<T> list;
    private Long total;
}

25.1.11 新建Result

package com.jihu.blog.admin.vo;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Result {
    private boolean success;
    private int code ;
    private String msg;
    private  Object data;

    public static Result success(Object data) {

        return new Result(true, 200, "success", data);
    }

    public static Result fail(int code,String msg) {
        return new Result(false, code, msg, null);
    }
}

25.1.12新建PermissionService

package com.jihu.blog.admin.service;

import com.jihu.blog.admin.model.params.PageParam;
import com.jihu.blog.admin.pojo.Permission;
import com.jihu.blog.admin.vo.Result;

public interface PermissionService {
    /**
     * 查询所有权限列表
     * @param pageParam
     * @return
     */
    Result listpermission(PageParam pageParam);

    /**
     * 添加权限
     * @param permission
     * @return
     */
    Result permissionadd(Permission permission);

    /**
     * 更新权限
     * @param permission
     * @return
     */
    Result permissionupdate(Permission permission);

    /**
     * 删除权限
     * @param id
     * @return
     */
    Result permissiondeleteById(Long id);
}

25.1.13新建PermissionServiceImpl

package com.jihu.blog.admin.service.Impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jihu.blog.admin.mapper.Permissionmapper;
import com.jihu.blog.admin.model.params.PageParam;
import com.jihu.blog.admin.pojo.Permission;
import com.jihu.blog.admin.service.PermissionService;
import com.jihu.blog.admin.vo.PageResult;
import com.jihu.blog.admin.vo.Result;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PermissionServiceImpl implements PermissionService {

    @Autowired
    private Permissionmapper permissionmapper;

    @Override
    public Result listpermission(PageParam pageParam) {
        /**
         * 要的数据,管理台,表的所有的字段 permission
         *  分页查询
         */
        Page<Permission> page = new Page<>(pageParam.getCurrentPage(),pageParam.getPageSize());
        LambdaQueryWrapper<Permission> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        if (StringUtils.isNoneBlank(pageParam.getQueryString())){
            lambdaQueryWrapper.eq(Permission::getName,pageParam.getQueryString());

        }

        Page<Permission> permissionPage = permissionmapper.selectPage(page, lambdaQueryWrapper);
        PageResult<Permission> pageResult = new PageResult<>();
        pageResult.setList(permissionPage.getRecords());
        pageResult.setTotal(permissionPage.getTotal());

        return Result.success(pageResult);
    }

    @Override
    public Result permissionadd(Permission permission) {
        int result = permissionmapper.insert(permission);
        return Result.success(result);
    }


    @Override
    public Result permissionupdate(Permission permission) {
        int result = permissionmapper.updateById(permission);
        return Result.success(result);
    }

    @Override
    public Result permissiondeleteById(Long id) {
        int result = permissionmapper.deleteById(id);
        return Result.success(result);
    }
}

25.1.14测试:

image-20220409142701503

26、Security集成

26.1添加依赖

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

26.2 配置

package com.jihu.blog.admin.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return  new BCryptPasswordEncoder();
    }

    public static void main(String[] args) {
        //加密策略 MD5 不安全 彩虹表  MD5 加盐
        String mszlu = new BCryptPasswordEncoder().encode("mszlu");
        System.out.println(mszlu);
    }
    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests() //开启登录认证
//                .antMatchers("/user/findAll").hasRole("admin") //访问接口需要admin的角色
                .antMatchers("/css/**").permitAll()
                .antMatchers("/img/**").permitAll()
                .antMatchers("/js/**").permitAll()
                .antMatchers("/plugins/**").permitAll()
                .antMatchers("/admin/**").access("@authService.auth(request,authentication)") //自定义service 来去实现实时的权限认证
                .antMatchers("/pages/**").authenticated()
                .and().formLogin()
                .loginPage("/login.html") //自定义的登录页面
                .loginProcessingUrl("/login") //登录处理接口
                .usernameParameter("username") //定义登录时的用户名的key 默认为username
                .passwordParameter("password") //定义登录时的密码key,默认是password
                .defaultSuccessUrl("/pages/main.html")
                .failureUrl("/login.html")
                .permitAll() //通过 不拦截,更加前面配的路径决定,这是指和登录表单相关的接口 都通过
                .and().logout() //退出登录配置
                .logoutUrl("/logout") //退出登录接口
                .logoutSuccessUrl("/login.html")
                .permitAll() //退出登录的接口放行
                .and()
                .httpBasic()
                .and()
                .csrf().disable() //csrf关闭 如果自定义登录 需要关闭
                .headers().frameOptions().sameOrigin(); //支持iframe 页面嵌套
    }
}

26.3登录认证

Admin

package com.jihu.blog.admin.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

@Data
public class Admin {
    @TableId(type = IdType.AUTO)
    private Long id;

    private String username;

    private String password;
}

SecurityUserService

package com.jihu.blog.admin.service;

import com.jihu.blog.admin.pojo.Admin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
@Component
public class SecurityUserService implements UserDetailsService {

    @Autowired
    private  AdminServilce adminServilce;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //登录的时候,把username传递到这里
        //通过username查询 admin表,如果admin存在 将密码告诉spring security
        //如果不存在  返回null  认证失败
        Admin admin = adminServilce.findAdminByUsername(username);
        if (admin == null){
            //登录失败
            return  null;
        }
        UserDetails userDetails = new User(username,admin.getPassword(),new ArrayList<>());
        //剩下的认证 就由框架帮我们完成
        return userDetails;
    }

    public static void main(String[] args) {
        System.out.println(new BCryptPasswordEncoder().encode("123456"));
    }
}

AdminServilce

package com.jihu.blog.admin.service;

import com.jihu.blog.admin.pojo.Admin;
import com.jihu.blog.admin.pojo.Permission;

import java.util.List;

public interface AdminServilce {

    Admin findAdminByUsername(String username);

    /**
     * 根据用户id查询用户的权限
     * @param id
     * @return
     */
    List<Permission> findPermissionsByAdminId(Long id);
}

AdminServilceImpl

package com.jihu.blog.admin.service.Impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jihu.blog.admin.mapper.AdminMapper;
import com.jihu.blog.admin.mapper.Permissionmapper;
import com.jihu.blog.admin.pojo.Admin;
import com.jihu.blog.admin.pojo.Permission;
import com.jihu.blog.admin.service.AdminServilce;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class AdminServilceImpl implements AdminServilce {
    @Autowired
    private AdminMapper adminMapper;
    @Autowired
    private Permissionmapper permissionmapper;

    @Override
    public Admin findAdminByUsername(String username){
        LambdaQueryWrapper<Admin> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Admin::getUsername,username);
        queryWrapper.last("limit 1");
        Admin admin = adminMapper.selectOne(queryWrapper);
        return admin;
    }

    @Override
    public List<Permission> findPermissionsByAdminId(Long id) {
        return permissionmapper.findPermissionsByAdminId(id);

    }
}

AdminMapper

package com.jihu.blog.admin.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.admin.pojo.Admin;

public interface AdminMapper  extends BaseMapper<Admin> {
}

Permissionmapper

package com.jihu.blog.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.admin.pojo.Permission;
import java.util.List;

public interface Permissionmapper extends BaseMapper<Permission> {
    List<Permission> findPermissionsByAdminId(Long adminId);
}

26.4 权限认证

AuthService

package com.jihu.blog.admin.service;

import com.jihu.blog.admin.pojo.Admin;
import com.jihu.blog.admin.pojo.Permission;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

@Service
@Slf4j
public class AuthService {
    @Autowired
    private AdminServilce adminServilce;

    public  boolean auth(HttpServletRequest request, Authentication authentication){

        //权限认证
        //请求路径
        String requestURI = request.getRequestURI();
        log.info("request url:{}", requestURI);
        Object principal = authentication.getPrincipal();
        //true代表放行 false 代表拦截
        if (principal == null ||"anonymousUser".equals(principal)){
            //未登录
            return  false;
        }
        UserDetails userDetails = (UserDetails) principal;
        String username = userDetails.getUsername();
        Admin admin = adminServilce.findAdminByUsername(username);
        if (admin == null){
            return false;
        }
        if (admin.getId() == 1){
            //认为是超级管理员
            return true;
        }
        List<Permission> permissions = adminServilce.findPermissionsByAdminId(admin.getId());
        requestURI = StringUtils.split(requestURI,'?')[0];
        for (Permission permission : permissions) {
            if (requestURI.equals(permission.getPath())){
                //代表有权限
                return true;
            }
        }
        return false;
    }

}

27.作业

添加角色,用户拥有多个角色,一个角色拥有多个权限

28.总结技术亮点

1、jwt + redis

token令牌的登录方式,访问认证速度快,session共享,安全性

redis做了令牌和用户信息的对应管理,

1,进一步增加了安全性

2、登录用户做了缓存

3、灵活控制用户的过期(续期,踢掉线等)

2、threadLocal使用了保存用户信息,请求的线程之内,可以随时获取登录的用户,做了线程隔离

3、在使用完ThreadLocal之后,做了value的删除,防止了内存泄漏(这面试说强引用。弱引用。不是明摆着让面试官间JVM嘛)

4·、线程安全-update table set value = newValue where id=1 and value=oldValue

5、线程池应用非常广,面试7个核心参数(对当前的主业务流程无影响的操作,放入线程池执行)

1.登录,记录日志

6·权限系统重点内容

7·统一日志记录,统一缓存处理

posted @ 2022-04-04 18:48  爲誰心殇  阅读(986)  评论(5编辑  收藏  举报
>