springboot+vue hands-on project, a real online blog system

Posted by tommyboy123x on Wed, 09 Mar 2022 17:29:43 +0100

Article catalogue

Description of spring boot training project

Video link

The blog used by code God Road website has simple projects, clear needs and easy to use. It is very suitable for hand training projects.

Final product

blog.mszlu.com

Project explanation:

  1. Provide front-end engineering, and only need to implement the back-end interface
  2. The project starts with a single architecture, develops rapidly first, does not consider project optimization, and reduces the development burden
  3. After the development, start to optimize the project and improve the programming thinking ability
  4. Such as page static, cache, cloud storage, log, etc
  5. docker deployment Online
  6. ECS purchase, domain name purchase, domain name filing, etc

Technology used in the project:

springboot + mybatisplus+redis+mysql

Basic knowledge

Introduction to mybatisDao layer Mapper layer controller layer service layer model layer entity layer
Mall shopping mall learning document
mybatisplus learning documentation
mybatisplus supporting code
easycode with mybatisplus

It is recommended to install plug-ins
Use @ Data

View article code structure

Code written by automatic prompt

Quickly generate xml files

Distinguishing brackets

vscode plugin

Interview preparation

springboot personal blog project interview preparation

1. Project construction

Front end Engineering:

npm install
npm run build
npm run dev

1.1 new maven project

<?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.mszlu</groupId>
    <artifactId>blog-parent</artifactId>
    <version>1.0-SNAPSHOT</version>


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

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <!-- Exclude default 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>

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

</project>

Delete src file

1.1.2 bug s encountered

How maven child projects reference parent projects

1.2 configuration

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

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




package com.mszlu.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
//Scan the package, generate the proxy implementation class from the interface under this package, and register it in the spring container
@MapperScan("com.mszlu.blog.dao")
public class MybatisPlusConfig {
	//Paging plug-in
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }
}



package com.mszlu.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 WebConfig  implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //Cross domain configuration, which cannot be set to *, is unsafe, the front and rear end are separated, and the domain name may be inconsistent
        //Inconsistent local test ports are also considered cross domain
        registry.addMapping("/**").allowedOrigins("http://localhost:8080");
    }
}

1.3 startup

package com.mszlu.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. Home page - article list

2.1 interface description

Interface url: / articles

Request method: POST

Request parameters:

Parameter name

Parameter type

explain

page

int

Current number of pages

pageSize

int

Quantity displayed per page

Return data:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id": 1,
            "title": "springboot Introduction and introductory cases",
            "summary": "adopt Spring Boot The realized service only needs to rely on one Java Class, package it into jar,And pass`java -jar`The command can be run.

Compared with tradition Spring In terms of application, it has become very light and simple.",
            "commentCounts": 2,
            "viewCounts": 54,
            "weight": 1,
            "createDate": "2609-06-26 15:58",
            "author": "12",
            "body": null,
            "tags": [
                {
                    "id": 5,
                    "avatar": null,
                    "tagName": "444"
                },
                {
                    "id": 7,
                    "avatar": null,
                    "tagName": "22"
                },
                {
                    "id": 8,
                    "avatar": null,
                    "tagName": "11"
                }
            ],
            "categorys": null
        },
        {
            "id": 9,
            "title": "Vue.js What is it?",
            "summary": "Vue (pronunciation /vju?/,be similar to view) Is a progressive framework for building user interfaces.",
            "commentCounts": 0,
            "viewCounts": 3,
            "weight": 0,
            "createDate": "2609-06-27 11:25",
            "author": "12",
            "body": null,
            "tags": [
                {
                    "id": 7,
                    "avatar": null,
                    "tagName": "22"
                }
            ],
            "categorys": null
        },
        {
            "id": 10,
            "title": "Element relevant",
            "summary": "This section describes how to use in a project Element. ",
            "commentCounts": 0,
            "viewCounts": 3,
            "weight": 0,
            "createDate": "2609-06-27 11:25",
            "author": "12",
            "body": null,
            "tags": [
                {
                    "id": 5,
                    "avatar": null,
                    "tagName": "444"
                },
                {
                    "id": 6,
                    "avatar": null,
                    "tagName": "33"
                },
                {
                    "id": 7,
                    "avatar": null,
                    "tagName": "22"
                },
                {
                    "id": 8,
                    "avatar": null,
                    "tagName": "11"
                }
            ],
            "categorys": null
        }
    ]
}

2.2 coding

2.2.0 Spring - annotation based development

Spring annotation based development
Role of each annotation

2.2.1 table structure

Article table

CREATE TABLE `blog`.`ms_article`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `comment_counts` int(0) NULL DEFAULT NULL COMMENT 'Number of comments',
  `create_date` bigint(0) NULL DEFAULT NULL COMMENT 'Creation time',
  `summary` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'brief introduction',
  `title` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'title',
  `view_counts` int(0) NULL DEFAULT NULL COMMENT 'Number of views',
  `weight` int(0) NOT NULL COMMENT 'Top or not',
  `author_id` bigint(0) NULL DEFAULT NULL COMMENT 'author id',
  `body_id` bigint(0) NULL DEFAULT NULL COMMENT 'content id',
  `category_id` int(0) NULL DEFAULT NULL COMMENT 'category id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

Label table
id, article id, tag id. the tag id can be found indirectly through the article 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;

User table

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 'account number',
  `admin` bit(1) NULL DEFAULT NULL COMMENT 'Administrator',
  `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'head portrait',
  `create_date` bigint(0) NULL DEFAULT NULL COMMENT 'Registration time',
  `deleted` bit(1) NULL DEFAULT NULL COMMENT 'Delete',
  `email` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'mailbox',
  `last_login` bigint(0) NULL DEFAULT NULL COMMENT 'Last login time',
  `mobile_phone_number` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'cell-phone number',
  `nickname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'nickname',
  `password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'password',
  `salt` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'Encryption salt',
  `status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'state',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

entity layer

Layer: model, alias: domain
Purpose: the entity layer is used to store our entity classes, which are basically consistent with the attribute values in the database, and realize the methods of set and get.
Example: the user entity in the user table
article

package com.mszlu.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;

    /**
     * Author id
     */
    private Long authorId;
    /**
     * Content id
     */
    private Long bodyId;
    /**
     *Category id
     */
    private Long categoryId;

    /**
     * Topping
     */
    private int weight = Article_Common;


    /**
     * Creation time
     */
    private Long createDate;
}

user

package com.mszlu.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;
}

label

package com.mszlu.blog.dao.pojo;

import lombok.Data;

@Data
public class Tag {

    private Long id;

    private String avatar;

    private String tagName;

}

2.2.2 Controller

Controller layer. The controller is imported into the service layer because the methods in the service are used by us. The controller performs business operations by receiving the parameters transmitted from the front end and returns a specified path or data table

package com.mszlu.blog.api;

import com.mszlu.blog.dao.pojo.Article;
import com.mszlu.blog.service.ArticleService;
import com.mszlu.blog.vo.Archive;
import com.mszlu.blog.vo.ArticleVo;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.params.PageParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

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

    @Autowired
    private ArticleService articleService;
	//Result is the unified result return
    @PostMapping
    public Result articles(@RequestBody PageParams pageParams) {
        //ArticleVo page received data
        List<ArticleVo> articles = articleService.listArticlesPage(pageParams);

        return Result.success(articles);
    }


}

Unify the final results

package com.mszlu.blog.vo;

import com.mszlu.blog.dao.pojo.Article;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result {

    private boolean success;

    private Integer code;

    private String msg;

    private Object data;


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

Create a Vo file that interacts with the front end

package com.mszlu.blog.vo;

import com.mszlu.blog.dao.pojo.ArticleBody;
import com.mszlu.blog.dao.pojo.Category;
import com.mszlu.blog.dao.pojo.SysUser;
import com.mszlu.blog.dao.pojo.Tag;
import lombok.Data;

import java.util.List;

@Data
public class ArticleVo {

    private Long id;

    private String title;

    private String summary;

    private int commentCounts;

    private int viewCounts;

    private int weight;
    /**
     * Creation time
     */
    private String createDate;

    private String author;

    private ArticleBodyVo body;

    private List<TagVo> tags;

    private List<CategoryVo> categorys;

}

2.2.3 Service

The service layer mainly writes business logic methods. The service layer often calls the methods of dao layer (also known as mapper layer) to add, delete, modify and query data*

2.2.3.0 solving mapper red explosion

Solve mapper explosion

Establish service interface

package com.mszlu.blog.service;

import com.mszlu.blog.vo.Archive;
import com.mszlu.blog.vo.ArticleVo;
import com.mszlu.blog.vo.params.PageParams;

import java.util.List;

public interface ArticleService {

    /**
     * Paging query article list
     * @param pageParams
     * @return
     */

    List<ArticleVo> listArticlesPage(PageParams pageParams);

}

Establish the implementation class of service interface

package com.mszlu.blog.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.mszlu.blog.dao.ArticleMapper;
import com.mszlu.blog.dao.SysUserMapper;
import com.mszlu.blog.dao.pojo.Article;
import com.mszlu.blog.dao.pojo.SysUser;
import com.mszlu.blog.dao.pojo.Tag;
import com.mszlu.blog.service.ArticleService;
import com.mszlu.blog.service.SysUserService;
import com.mszlu.blog.service.TagsService;
import com.mszlu.blog.vo.ArticleBodyVo;
import com.mszlu.blog.vo.ArticleVo;
import com.mszlu.blog.vo.TagVo;
import com.mszlu.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) {
        /**
         * 1,Paging query of article database table
         */
        Page<Article> page = new Page<>(pageParams.getPage(), pageParams.getPageSize());
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
        //Whether to sort by placing it at the top or not, / / arrange it in reverse chronological order, which is equivalent to order by create_data desc
        queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);
        Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);
        //Paging query usage https://blog.csdn.net/weixin_41010294/article/details/105726879
        List<Article> records = articlePage.getRecords();
        // To return the vo data defined by us is the corresponding front-end data. We should not only return the current data, but need further processing
        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;

    }

    //"The function of eop is to correspond to the copyList. The copy between sets is decomposed into the copy between set elements
    private ArticleVo copy(Article article,boolean isTag,boolean isAuthor){
        ArticleVo articleVo = new ArticleVo();
        //BeanUtils.copyProperties usage https://blog.csdn.net/Mr_linjw/article/details/50236279
        BeanUtils.copyProperties(article, articleVo);
        articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
        //Not all interfaces require tags and author information
        if(isTag){
            Long articleId = article.getId();
            articleVo.setTags(tagService.findTagsByArticleId(articleId));
        }
        if (isAuthor) {
            //Get the author id
            Long authorId = article.getAuthorId();

            articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname());
        }
        return articleVo;

    }
}

Establish user's service interface

package com.mszlu.blog.service;

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

public interface UserService {

    SysUser findUserById(Long userId);
}

Establish user's service interface implementation class

package com.mszlu.blog.service.impl;

import com.mszlu.blog.dao.SysUserMapper;
import com.mszlu.blog.dao.pojo.SysUser;
import com.mszlu.blog.service.UserService;
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) {
        //Query by id
        //To prevent sysUser from being empty, a judgment is added
        SysUser sysUser = sysUserMapper.selectById(id);
        if (sysUser == null){
            sysUser = new SysUser();
            sysUser.setNickname("Code God's Road");
        }
        return sysUser;
    }
}

2.2.3.1mybatisplus how to deal with multi table query

This problem is encountered in the establishment of TagMapper. The method is to create an xml file for reading and writing after the establishment of TagMapper
Put the xml file in the resource folder
Folder name and xml file name must be the same as tagmapper Java folder consistency

2.2.3.2 pits encountered when creating folders

When using IntelliJ IDEA to create a multi-level folder, the folder name is com immer. monitor. Both persistence and com/immer/monitor/persistence will be displayed as shown in the figure below

But the actual structure is quite different immer. monitor. Persistence is a single folder and com/immer/monitor/persistence is a nested folder

It will cause the problem that the resource file is not found, and it is difficult to find it
The problem that the folder path cannot be found will not be displayed, because we need to ensure that mapper XML should be consistent with the folder and path of mapper

How to create multi-level folders

Establish the service interface of the tag

package com.mszlu.blog.service;

import com.mszlu.blog.dao.pojo.Tag;
import com.mszlu.blog.vo.TagVo;

import java.util.List;

public interface TagsService {


    List<TagVo> findTagsByArticleId(Long id);
}

Establish the implementation class of tag's service interface

package com.mszlu.blog.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mszlu.blog.dao.TagMapper;
import com.mszlu.blog.dao.pojo.Tag;
import com.mszlu.blog.service.TagsService;
import com.mszlu.blog.vo.TagVo;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

@Service
public class TagsServiceImpl implements TagsService {
    @Autowired
    private TagMapper tagMapper;

    /**
     * ms_article_tag Is the association table of articles and labels
     * ms_tag Table for simple labels
     * @param articleId
     * @return
     */

    @Override
    public List<TagVo> findTagsByArticleId(Long articleId) {
        //mybatisplus cannot perform multi table query
        List<Tag> tags = tagMapper.findTagsByArticleId(articleId);
        return copyList(tags);
    }

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

    }

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

2.2.4 Dao layer

Mapper layer = dao layer. Now the mapper layer generated by reverse engineering with mybatis is actually dao layer.
dao layer carries out data persistence operation on the database. Its method statement is directly aimed at the database operation, while the service layer is aimed at our controller, that is, our users. The impl of service is a file that integrates mapper and service.

Relationship between dao layer and service layer: the service layer often calls dao layer methods to add, delete, modify and query data. In real development, business operations will involve data operations, while data operations often use databases, so the service layer will often call dao layer methods.

dao layer of the article
Since we directly inherit the BaseMapper of mybatisplus, we don't need to write basic additions, deletions, modifications and queries.

package com.mszlu.blog.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mszlu.blog.dao.pojo.Article;
import com.mszlu.blog.vo.ArticleVo;

import java.util.List;

public interface ArticleMapper extends BaseMapper<Article> {
	
}

dao layer of label

package com.mszlu.blog.dao;

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

import java.util.List;

public interface TagMapper extends BaseMapper<Tag> {

    List<Tag> findTagsByArticleId(Long articleId);
}

Author's dao layer

package com.mszlu.blog.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mszlu.blog.dao.pojo.SysUser;

public interface SysUserMapper extends BaseMapper<SysUser> {
}

This is what to do when mybatisplus encounters multi table query mentioned above. We need to establish our own xml file for joint query operation

In the mapper file
Using mapperX plug-in, we can create
Command with method name select

    <select id="findTagsByArticleId" resultType="com.mszlu.blog.dao.pojo.Tag"></select>

The total xml file is as follows

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

<mapper namespace="com.mszlu.blog.dao.mapper.TagMapper">

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

<!--        List<Tag> findTagsByArticleId(Long articleId);
In this file, id Represents the method name, parameterType Indicates the name of the input variable, resultType Represents the type of the generic-->

    <select id="findTagsByArticleId" parameterType="long" resultType="com.mszlu.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.5 testing

3. Home page - Hottest label

3.1 interface description

Interface url: / tags/hot

Request method: GET

Request parameters: None

id
Label name
We expect all lists of articles to be displayed by clicking on the tab

Return data:

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

3.2 coding

3.2.1 Controller

package com.mszlu.blog.api;

import com.mszlu.blog.service.ArticleService;
import com.mszlu.blog.service.TagsService;
import com.mszlu.blog.vo.Archive;
import com.mszlu.blog.vo.ArticleVo;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.TagVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

//@RestController returns json data on behalf of us, and @ RequestMapping("tags") represents path mapping
@RestController
@RequestMapping("tags")
public class TagsController {

    @Autowired
    private TagService tagService;

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

vo represents the data that the back end interacts with the front end

package com.mszlu.blog.vo;

import lombok.Data;

@Data
public class TagVo {

    private Long id;

    private String tagName;
}

3.2.2 Service

Establish service interface

package com.mszlu.blog.service;

import com.mszlu.blog.vo.TagVo;

import java.util.List;

public interface TagsService {

    Result hots(int limit);

}

Create serviceimpl implementation class

package com.mszlu.blog.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mszlu.blog.dao.TagMapper;
import com.mszlu.blog.dao.pojo.Tag;
import com.mszlu.blog.service.TagsService;
import com.mszlu.blog.vo.TagVo;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

@Service
public class TagsServiceImpl implements TagsService {
    @Autowired
    private TagMapper tagMapper;

     @Override
    public Result hots(int limit) {
        /**
         * The hottest label is the label ms_article_tag in tag_id is our hottest tag with the largest number of sorting
         * 1,Tags have the largest number of articles, which is the hottest tag
         * 2,Query by tag_id group count, from large to small, take the first limit
         */
        List<Long> tagIds = tagMapper.findHotsTagIds(limit);
        //We can't judge if ID (1,3) is empty
        //  CollectionUtils.isEmpty effect https://blog.csdn.net/qq_42848910/article/details/105717235
        if(CollectionUtils.isEmpty(tagIds)){
            return Result.success(Collections.emptyList());
        }
        //tagId and tagName Tag objects are required
        //Our sql statement is similar to select * from tag where id in (1,2,3)
        List<Tag> tagList = tagMapper.findTagsByTagIds(tagIds);
        return Result.success(tagList);
    }


}

3.2.3 Dao

TagMapper.java

package com.mszlu.blog.dao;

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

import java.util.List;

public interface TagMapper extends BaseMapper<Tag> {

    /**
     * Query the top n hottest labels
     * @param limit
     * @return
     */
    List<Long> findHotsTagIds(int limit);

    List<Tag> findTagsByTagIds(List<Long> tagIds);
}

TagMapper.xml file
Be sure to understand the business logic of all tables and know what value you want to return before operating
We use the method of findHotsTagIds in ms_article_tag found in tag table_ id

Then multi table query, tag_id is Ms_ id in the tag table. We found the first two IDS we want in the findHotsTagIds method, and then used the dynamic mysql method to select the id and tagName options. Writing and reading more can basically become a qualified crud engineer

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

<mapper namespace="com.mszlu.blog.dao.TagMapper">

<!--    List<Long> findHotsTagIds(int limit);-->
<!--    parameterType="int"It was added by ourselves, because the label we entered will not be automatically generated,#{limit} parameters passed for ourselves -- >
<!--  GROUP by usage  https://www.runoob.com/sql/sql-groupby.html-->
<!--    sql The meaning of the sentence is in ms_article_tag Find in table tag_id,according to tag_id Aggregate them together, and then according to count(*)The number of is sorted in descending order, and finally two pieces of data are limited to output-->
    <select id="findHotsTagIds" parameterType="int" resultType="java.lang.Long">
        select tag_id from ms_article_tag GROUP BY tag_id ORDER BY count(*) DESC LIMIT #{limit}
    </select>
<!--    List<Tag> findTagsByTagIds(List<Long> tagIds);Because the type of input is list therefore parameterType The value of is list-->
<!--    foreach usage https://www.cnblogs.com/fnlingnzb-learner/p/10566452.html
            amount to for Loop through the incoming one id Set, each id adopt sql Statement to find the corresponding tag object-->
    <select id="findTagsByTagIds" parameterType="list" resultType="com.mszlu.blog.dao.pojo.Tag">
        select id,tag_name as tagName from ms_tag
        where  id in
        <foreach collection="tagIds" item="tagId" separator="," open="(" close=")">
            #{tagId}
        </foreach>
    </select>

</mapper>

3.2.4 testing

The hottest label is displayed

4.1. Unified exception handling

Whether it is the controller layer or the service or dao layer, it is possible to report exceptions. If it is an expected exception, it can be directly captured and handled. If it is an unexpected exception, it needs to be handled and recorded uniformly, and give users relatively friendly information.

package com.mszlu.blog.handler;

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

//Implementation of Aop for intercepting the method annotated with @ controller
@ControllerAdvice
public class AllExceptionHandler {

    //Handle exceptions and exceptions Class exception
    @ExceptionHandler(Exception.class)
    @ResponseBody //Return json data. If it is not added, it will return to the page
    public Result doException(Exception ex) {
        //e.printStackTrace(); Is the stack information of the print exception, indicating the cause of the error,
        // In fact, when an exception occurs, you usually have to deal with the exception, which is a good habit of programming, so e.printStackTrace() can facilitate you to debug the program!
        ex.printStackTrace();
        return Result.fail(-999,"System abnormality");

    }
}

4.2. Home page - Hottest article

In MS_ View in article table_ Counts indicates the number of views, and the more, the more popular

4.2.1 interface description

Interface url: / articles/hot

Request method: POST

Request parameters:

Parameter name

Parameter type

explain

Return data:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id": 1,
            "title": "springboot Introduction and introductory cases",
        },
        {
            "id": 9,
            "title": "Vue.js What is it?",
        },
        {
            "id": 10,
            "title": "Element relevant",
            
        }
    ]
}

4.2.2 Controller

ArticleController.java

    /**
     * The hottest article on the home page
     * @return
     */
@PostMapping("hot")
    public Result hotArticle(){
        int limit = 5;
        return articleService.hotArticle(limit);
    }

4.2.3 Service

src/main/java/com/mszlu/blog/service/ArticleService.java

package com.mszlu.blog.service;

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

/**
 * @Author ljm
 * @Date 2021/10/11 10:30
 * @Version 1.0
 */
public interface ArticleService {

    /**
     * Paging query article list
     * @param pageParams
     * @return
     */
    Result listArticle(PageParams pageParams);

    /**
     * Hottest article
     * @param limit
     * @return
     */
    Result hotArticle(int limit);
}

src/main/java/com/mszlu/blog/service/impl/ArticleServiceImpl.java

    @Override
    public Result hotArticle(int limit) {
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.orderByDesc(Article::getViewCounts);
        queryWrapper.select(Article::getId,Article::getTitle);
        //The word "limit" should be followed by a space. Don't forget to add a space, otherwise the data will be spelled together
        queryWrapper.last("limit "+limit);
        //select id,title from article order by view_counts desc limt 5
        List<Article> articles = articleMapper.selectList(queryWrapper);

        //Return vo object
        return Result.success(copyList(articles,false,false));
    }

4.2.4 testing

4.3. Home page - latest articles

It is very similar to the hottest articles. One is selected according to the number of views and the other is selected according to the latest creation time

4.3.1 interface description

Interface url: / articles/new

Request method: POST

Request parameters:

Parameter name

Parameter type

explain

Return data:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id": 1,
            "title": "springboot Introduction and introductory cases",
        },
        {
            "id": 9,
            "title": "Vue.js What is it?",
        },
        {
            "id": 10,
            "title": "Element relevant",
            
        }
    ]
}

4.3.1 Controller

 /**
     * Latest articles on home page
     * @return
     */
    @PostMapping("new")
    public Result newArticles(){
        int limit = 5;
        return articleService.newArticles(limit);
    }

4.3.2 Service

src/main/java/com/mszlu/blog/service/ArticleService.java

    /**
     * Latest article
     * @param limit
     * @return
     */
    Result newArticles(int limit);

src/main/java/com/mszlu/blog/service/impl/ArticleServiceImpl.java

@Override
    public Result newArticles(int limit) {
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.orderByDesc(Article::getCreateDate);
        queryWrapper.select(Article::getId,Article::getTitle);
        queryWrapper.last("limit "+limit);
        //select id,title from article order by create_date desc limit 5
        List<Article> articles = articleMapper.selectList(queryWrapper);

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

4.4. Home page - article archiving

How many articles are published in each article according to the creation time, a year and a month

4.4.1 interface description

Interface url: / articles/listArchives

Request method: POST

Request parameters:

Parameter name

Parameter type

explain

Return data:

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


?        select year(create_date) as year,month(create_date) as month,count(*) as count from ms_article group by year,month

however
p9 up create in the sql given by the master_ Date is bigint 13 bits, but direct year() cannot. You need to change the date type first and then year().

select year(FROM_UNIXTIME(create_date/1000)) year,month(FROM_UNIXTIME(create_date/1000)) month, count(*) count from ms_article group by year,month;

So we can find out the results

4.4.1 Controller

src/main/java/com/mszlu/blog/controller/ArticleController.java

  /**
     * Home page article archiving
     * @return
     */
    @PostMapping("listArchives")
    public Result listArchives(){
        return articleService.listArchives();
    }

The following is in Src / main / Java / COM / mszlu / blog / service / impl / articleserviceimpl Return value used in Java

package com.mszlu.blog.dao.dos;

import lombok.Data;

/**
 * @Author ljm
 * @Date 2021/10/12 17:19
 * @Version 1.0
 * do The object found in the object database does not need to be persisted. Because do is the keyword, an s is added to become dos
 */
@Data
public class Archives {

    private Integer year;

    private Integer month;

    private Long count;
}

4.4.2 Service

src/main/java/com/mszlu/blog/service/ArticleService.java

    /**
     * Article archiving
     * @return
     */
    Result listArchives();

src/main/java/com/mszlu/blog/service/impl/ArticleServiceImpl.java

@Override
public Result listArchives() {
        /*
        Article archiving
         */
    List<Archives> archivesList = articleMapper.listArchives();
    return Result.success(archivesList);
}

4.4.3 Dao

src/main/java/com/mszlu/blog/dao/mapper/ArticleMapper.java

package com.mszlu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mszlu.blog.dao.pojo.Article;

import java.util.List;
import java.util.Map;

public interface ArticleMapper extends BaseMapper<Article> {

  List<Archives> listArchives();

}

src/main/resources/com/mszlu/blog/dao/mapper/ArticleMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!--MyBatis configuration file-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--found ArticleMapper.xml Reuse after file mybatisX one touch  select sentence-->
<mapper namespace="com.mszlu.blog.dao.mapper.ArticleMapper">


    <select id="listArchives" resultType="com.mszlu.blog.dao.dos.Archives">
        select year(FROM_UNIXTIME(create_date/1000)) as year,month(FROM_UNIXTIME(create_date/1000)) as month, count(*) as count from ms_article
        group by year,month
    </select>

</mapper>

4.4.4 testing

Note: the front-end project needs to use the app under the data of the day

5.1. Sign in

5.1.1 interface description

Interface url: / login

Request method: POST

Request parameters:

Parameter name

Parameter type

explain

account

string

account number

password

string

password

Return data:

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

5.1.2 JWT

Login using JWT technology.

jwt can generate an encrypted token as a user login token, which will be issued to the client after the user logs in successfully.

When requesting a resource or interface that needs to be logged in, the token is carried, and the backend verifies whether the token is legal.

jwt consists of three parts: A.B.C

A: Header, {"type": "JWT", "alg": "HS256"} fixed

B: playload stores information, such as user id, expiration time, etc., which can be decrypted and cannot store sensitive information

C: Visa, A and B are encrypted with the secret key. As long as the secret key is not lost, it can be considered safe.

jwt verification is mainly to verify whether Part C is legal.

Import dependent packages

Dependent packages:

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

src/main/java/com/mszlu/blog/utils/JWTUtils.java
Tools:

package com.mszlu.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) // Issuing algorithm, the secret key is jwtToken
                .setClaims(claims) // body data should be unique and set by yourself
                .setIssuedAt(new Date()) // Set issuing time
                .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));// Effective time of day
        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;

    }

}

5.1.3 Controller

src/main/java/com/mszlu/blog/controller/LoginController.java

package com.mszlu.blog.controller;

import com.mszlu.blog.service.LoginService;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.params.LoginParam;
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;
    //@Usage and understanding of requestbody and @ ResponseBody https://blog.csdn.net/zhanglf02/article/details/78470219
    //On the usage and difference between @ RequestMapping @ResponseBody and @ RequestBody annotation
//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
//@The RequestBody is mainly used to receive the data in the json string passed from the front end to the back end (the data in the request body); The most common way to use the request body to transfer parameters is undoubtedly the POST request, so when using @ RequestBody to receive data, it is generally submitted in the form of POST.

    @PostMapping
    public Result login(@RequestBody LoginParam loginParam){
        //Login authentication user access user table
        return loginService.login(loginParam);

    }
}

Construct LoginParam, that is, our request data
src/main/java/com/mszlu/blog/vo/params/LoginParam.java

package com.mszlu.blog.vo.params;

import lombok.Data;

/**
 * @Author ljm
 * @Date 2021/10/12 20:06
 * @Version 1.0
 */
@Data
public class LoginParam {
    private String account;

    private String password;
}

5.1.4 Service

src/main/java/com/mszlu/blog/service/LoginService.java

package com.mszlu.blog.service;

import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.params.LoginParam;

public interface LoginService {
    /**
     * Sign in
     * @param loginParam
     * @return
     */
    Result login(LoginParam loginParam);
}

Import dependent packages
md5 encrypted dependent packages:

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

src/main/java/com/mszlu/blog/service/impl/LoginServiceImpl.java

package com.mszlu.blog.service.impl;

import com.alibaba.fastjson.JSON;
import com.mszlu.blog.dao.pojo.SysUser;
import com.mszlu.blog.service.LoginService;
import com.mszlu.blog.service.SysUserService;
import com.mszlu.blog.utils.JWTUtils;
import com.mszlu.blog.vo.ErrorCode;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.params.LoginParam;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;
//Spring auto scan component// https://blog.csdn.net/u010002184/article/details/72870065
// @Component – indicates that components are automatically scanned.
//@Repository – represents DAO components in the persistence layer.
//@Service – represents a service component in the business layer.
//@Controller – represents the controller component at the presentation layer.
@Service
public class LoginServiceImpl implements LoginService {

	//Encryption salt is used for encryption
    private static final String slat = "mszlu!@#";
    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public Result login(LoginParam loginParam) {
            /**
         * 1. Check whether the parameters are legal
         * 2. According to the user name and password, go to the user table to query whether it exists
         * 3. If there is no login failure
         * 4. If it exists, use jwt to generate a token and return it to the front end
         * 5. token Put it into redis. Redis token: user information sets the expiration time
         *  (When logging in for authentication, first verify whether the token string is legal and whether redis authentication exists)
         */
        String account = loginParam.getAccount();
        String password = loginParam.getPassword();
        if (StringUtils.isBlank(account) || StringUtils.isBlank(password)){
            return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
        }
        String pwd = DigestUtils.md5Hex(password + slat);
        SysUser sysUser = sysUserService.findUser(account,pwd);
        if (sysUser == null){
            return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());
        }
        //Log in successfully, use JWT to generate a token and return it to token and redis
        String token = JWTUtils.createToken(sysUser.getId());
        // JSON.toJSONString usage https://blog.csdn.net/antony9118/article/details/71023009
        //The expiration time is 100 days
        //redisTemplate usage https://blog.csdn.net/lydms/article/details/105224210 
        redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),100, TimeUnit.DAYS);
        return Result.success(token);
    }

//Generate the password we want and put it in the database for login
    public static void main(String[] args) {
        System.out.println(DigestUtils.md5Hex("admin"+slat));
    }
}

src/main/java/com/mszlu/blog/service/impl/SysUserServiceImpl.java

   @Override
    public SysUser findUser(String account, String password) {
        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SysUser::getAccount,account);
        queryWrapper.eq(SysUser::getPassword,password);
        //account id avatar name
        queryWrapper.select(SysUser::getAccount,SysUser::getId,SysUser::getAdmin,SysUser::getNickname);
        //Increase query efficiency and query only one item
        queryWrapper.last("limit 1");

        return sysUserMapper.selectOne(queryWrapper);
    }

src/main/java/com/mszlu/blog/service/SysUserService.java

    SysUser findUser(String account, String pwd);

5.1.5 login parameters, redis configuration, unified error code

src/main/resources/application.properties

spring.redis.host=localhost
spring.redis.port=6379

src/main/java/com/mszlu/blog/vo/ErrorCode.java

package com.mszlu.blog.vo;

public enum  ErrorCode {

    PARAMS_ERROR(10001,"Parameter error"),
    ACCOUNT_PWD_NOT_EXIST(10002,"User name or password does not exist"),
    NO_PERMISSION(70001,"No access"),
    SESSION_TIME_OUT(90001,"session time out"),
    NO_LOGIN(90002,"Not logged in"),;

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

5.1.6 testing

Use the postman test, because after logging in, you need to jump to the page for token authentication. If the interface is not written, there will be problems in the front end.

After the token is obtained by the front end, it will be stored in the storage h5, which is stored locally

postman
redis view

5.2. Get user information

Why can I log in after obtaining user information?

After the token is obtained by the front-end, it will be stored in the storage h5 and stored locally. After storage, it will get the token in the storage to obtain the user information. If this interface is not implemented, it will always request to fall into an endless loop

5.2.1 interface description

You have to get this parameter from the head of http. In this way, it is relatively safe to transfer parameters,
The returned data is the data related to our users, including id, account number, nickname and avatar

Interface url: / users/currentUser

Request method: GET

Request parameters:

Parameter name

Parameter type

explain

Authorization

string

Header information (TOKEN)

Return data:

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

5.2.2 Controller

src/main/java/com/mszlu/blog/controller/UsersController.java

package com.mszlu.blog.controller;

import com.mszlu.blog.service.SysUserService;
import com.mszlu.blog.vo.Result;
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;

//On the usage and difference between @ RequestMapping @ResponseBody and @ RequestBody annotation
//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
//@The RequestBody is mainly used to receive the data in the json string passed from the front end to the back end (the data in the request body); The most common way to use the request body to transfer parameters is undoubtedly the POST request, so when using @ RequestBody to receive data, it is generally submitted in the form of POST.
@RestController
@RequestMapping("users")
public class UserController {

    @Autowired
    private SysUserService sysUserService;

    @GetMapping("currentUser")
    public Result currentUser(@RequestHeader("Authorization") String token){

        return sysUserService.findUserByToken(token);
    }
}

5.2.3 Service

src/main/java/com/mszlu/blog/service/SysUserService.java

    /**
     * Query user information according to token
     * @param token
     * @return
     */
 Result findUserByToken(String token);

src/main/java/com/mszlu/blog/service/impl/SysUserServiceImpl.java

 	    //This pop-up problem can be solved by adding @ Repository to the corresponding mapper so that spring can recognize it
    @Autowired
    private SysUserMapper sysUserMapper;
    @Autowired
    private LoginService loginService;
    @Override
    public Result findUserByToken(String token) {
        /**
         * 1,token Validity verification
         * Whether it is empty, whether the parsing is successful, and whether redis exists
         * 2,If the verification fails, an error is returned
         *3,If successful, the corresponding result LoginUserVo is returned
         */

        //Check the token in loginservice
        SysUser sysUser = loginService.checkToken(token);
        if(sysUser == null){
            return Result.fail(ErrorCode.TOKEN_ERROR.getCode(),ErrorCode.TOKEN_ERROR.getMsg());
        }
        LoginUserVo loginUserVo = new LoginUserVo();
        loginUserVo.setId(sysUser.getId());
        loginUserVo.setNickname(sysUser.getNickname());
        loginUserVo.setAvatar(sysUser.getAvatar());
        loginUserVo.setAccount(sysUser.getAccount());
        return Result.success(loginUserVo);

    }

src/main/java/com/mszlu/blog/service/LoginService.java

package com.mszlu.blog.service;

import com.mszlu.blog.dao.pojo.SysUser;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.params.LoginParam;

/**
 * @Author ljm
 * @Date 2021/10/12 20:04
 * @Version 1.0
 */
public interface LoginService {
    /**
     * Login function
     * @param loginParam
     * @return
     */
    Result login(LoginParam loginParam);

    SysUser checkToken(String token);
}

src/main/java/com/mszlu/blog/service/impl/LoginServiceImpl.java

    @Override
    public SysUser checkToken(String token) {
        //Null returned if token is null
        if(StringUtils.isBlank(token)){
            return null;
        }
        Map<String, Object> stringObjectMap = JWTUtils.checkToken(token);
        //Parsing failed
        if(stringObjectMap ==null){
            return null;
        }
        //If successful
        String userJson =  redisTemplate.opsForValue().get("TOKEN_"+token);
        if (StringUtils.isBlank(userJson)) {
            return null;
        }
        //Resolve back to sysUser object
        SysUser sysUser = JSON.parseObject(userJson, SysUser.class);
        return sysUser;
    }

5.2.4 LoginUserVo

src/main/java/com/mszlu/blog/vo/LoginUserVo.java

package com.mszlu.blog.vo;

import lombok.Data;

@Data
public class LoginUserVo {
	//Interact with pages

    private Long id;

    private String account;

    private String nickname;

    private String avatar;
}

5.2.5 testing

5.3. Log out

One is to log in and authenticate the token. The other is to register in redis. The token string cannot be changed. It can only be cleared by the front end. What the back end can do is to clear redis

5.3.1 interface description

Interface url: / logout

Request method: GET

Request parameters:

Parameter name

Parameter type

explain

Authorization

string

Header information (TOKEN)

Return data:

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

5.3.2 Controller

src/main/java/com/mszlu/blog/controller/LogoutController.java

package com.mszlu.blog.controller;

import com.mszlu.blog.service.LoginService;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.params.LoginParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

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

    @Autowired
    private LoginService loginService;

//Get a parameter such as header information
    @GetMapping
    public Result logout(@RequestHeader("Authorization") String token){
        return loginService.logout(token);
    }
}

5.3.3 Service

src/main/java/com/mszlu/blog/service/LoginService.java

    /**
     * Exit login
     * @param token
     * @return
     */
    Result logout(String token);

src/main/java/com/mszlu/blog/service/impl/LoginServiceImpl.java

  @Override
    public Result logout(String token) {
    //The backend directly deletes the token in redis
        redisTemplate.delete("TOKEN_"+token);
        return Result.success(null);
    }

5.3.4 testing

6.1. register

6.1.1 interface description

Interface url: / register

Request method: POST
post parameter passing means that the request parameters are passed in json mode
See this article for details
post and @ Requestbody

Request parameters:

Parameter name

Parameter type

explain

account

string

account number

password

string

password

nickname

string

nickname

Return data:

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

6.1.2 Controller

src/main/java/com/mszlu/blog/controller/RegisterController.java

package com.mszlu.blog.controller;

import com.mszlu.blog.service.LoginService;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.params.LoginParam;
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;
    
//The back end passes multiple parameters, and the front end only selects the parameters it needs
    @PostMapping
    public Result register(@RequestBody LoginParam loginParam){
    //sso single sign on, if the login registration function is proposed later (separate service, interface service can be provided independently)
        return loginService.register(loginParam);
    }
}

Add a new parameter nickname to the parameter LoginParam.
src/main/java/com/mszlu/blog/vo/params/LoginParam.java

package com.mszlu.blog.vo.params;

import lombok.Data;

@Data
public class LoginParam {

    private String account;

    private String password;

    private String nickname;
}

6.1.3 Service

src/main/java/com/mszlu/blog/service/impl/LoginServiceImpl.java

 @Override
    public Result register(LoginParam loginParam) {
            /**
         * 1. Judge whether the parameter is legal
         * 2. Judge whether the account exists and return that the account has been registered
         * 3. Does not exist, registered user
         * 4. Generate token
         * 5. Save to redis and return
         * 6. Note that with the addition of transactions, once any process in the middle has problems, the registered user needs to be rolled back
         */
         String account = loginParam.getAccount();
        String password = loginParam.getPassword();
        String nickname = loginParam.getNickname();
        if (StringUtils.isBlank(account)
                || StringUtils.isBlank(password)
                || StringUtils.isBlank(nickname)
        ){
            return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
        }
        SysUser sysUser = this.sysUserService.findUserByAccount(account);
        if (sysUser != null){
            return Result.fail(ErrorCode.ACCOUNT_EXIST.getCode(),ErrorCode.ACCOUNT_EXIST.getMsg());
        }
        sysUser = new SysUser();
        sysUser.setNickname(nickname);
        sysUser.setAccount(account);
        sysUser.setPassword(DigestUtils.md5Hex(password+slat));
        sysUser.setCreateDate(System.currentTimeMillis());
        sysUser.setLastLogin(System.currentTimeMillis());
        sysUser.setAvatar("/static/img/logo.b3a48c0.png");
        sysUser.setAdmin(1); //1 is true
        sysUser.setDeleted(0); // 0 is false
        sysUser.setSalt("");
        sysUser.setStatus("");
        sysUser.setEmail("");
        this.sysUserService.save(sysUser);

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

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

In errorcode Add one in Java
src/main/java/com/mszlu/blog/vo/ErrorCode.java

ACCOUNT_EXIST(10004,"Account already exists"),

The save and findUserByAccount methods in sysUserService do not need to construct interfaces and implementation classes
src/main/java/com/mszlu/blog/service/SysUserService.java

    /**
     * Find users by account
     * @param account
     * @return
     */
 SysUser findUserByAccount(String account);
    /**
     * Save user
     * @param sysUser
     */
    void save(SysUser sysUser);

src/main/java/com/mszlu/blog/service/impl/SysUserServiceImpl.java

  	@Override
    public SysUser findUserByAccount(String account) {
        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SysUser::getAccount,account);
        //Ensure that only one item can be queried
        queryWrapper.last("limit 1");
        return sysUserMapper.selectOne(queryWrapper);
    }

    @Override
    public void save(SysUser sysUser) {
         //Save the user id, which will be generated automatically
        //The default id generated in this place is the distributed id snowflake algorithm
        //mybatis-plus
        this.sysUserMapper.insert(sysUser);
    }

6.1.4 additional services

Roll back when an error occurs to prevent adding exceptions
Add @ Transactional annotation
src/main/java/com/mszlu/blog/service/impl/LoginServiceImpl.java

@Service
@Transactional
public class LoginServiceImpl implements LoginService {}

Of course, it is generally recommended to add the transaction annotation @ Transactional to the interface, which is more general.
src/main/java/com/mszlu/blog/service/LoginService.java

package com.mszlu.blog.service;

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

/**
 * @Author ljm
 * @Date 2021/10/12 20:04
 * @Version 1.0
 */

@Transactional
public interface LoginService {
    /**
     * Login function
     * @param loginParam
     * @return
     */
    Result login(LoginParam loginParam);

    SysUser checkToken(String token);

    /**
     * Exit login
     * @param token
     * @return
     */
    Result logout(String token);


    /**
     * register
     * @param loginParam
     * @return
     */
    Result register(LoginParam loginParam);

}

During the test, you can stop redis. After the redis connection is abnormal, the newly added user should perform the rollback operation.

6.1.5 testing

6.2. Login interceptor

Every time you access a resource that needs to be logged in, you need to judge in the code. Once the login logic changes, the code has to be changed, which is very inappropriate.

So can we make a unified login judgment?

Interceptor in spring MVC

Yes, use the interceptor to intercept the login. If you encounter an interface that needs to be accessed by login, if you don't log in, the interceptor will directly return and jump to the login page.
Three Javas devices: filter listener interceptor

6.2.1 interceptor implementation

src/main/java/com/mszlu/blog/handler/LoginInterceptor.java

package com.mszlu.blog.handler;

import com.alibaba.fastjson.JSON;
import com.mszlu.blog.dao.pojo.SysUser;
import com.mszlu.blog.service.LoginService;
import com.mszlu.blog.vo.ErrorCode;
import com.mszlu.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
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 {
            //Execute before executing the controller method (Handler)
        /**
         * 1. You need to judge whether the requested interface path is a HandlerMethod (controller method)
         * 2. Judge whether the token is empty. If it is empty, you are not logged in
         * 3. If the token is not empty, log in and verify loginService checkToken
         * 4. If the certification is successful, it can be released
         */
        //If it's not our way to release
        if (!(handler instanceof HandlerMethod)){
        //The handler may be requestresourcehandler. The springboot program accesses the static resources and queries the static directory under the classpath by default
            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(), "Not logged in");
            //Set browser recognition to return json
            response.setContentType("application/json;charset=utf-8");
            //https://www.cnblogs.com/qlqwjy/p/7455706.html response.getWriter().print()
            //SON.toJSONString converts an object into a Json string
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        SysUser sysUser = loginService.checkToken(token);
        if (sysUser == null){
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "Not logged in");
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        //Yes, login status, release
                //Login verification successful, release
        //I want to get the user's information directly in the controller. How can I get it?

        return true;
    }
}

6.2.2 make interceptor effective

src/main/java/com/mszlu/blog/config/WebMVCConfig.java

package com.mszlu.blog.config;

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

@Configuration
public class WebMVCConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //Cross domain configuration
        registry.addMapping("/**").allowedOrigins("http://localhost:8080");
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    //Assuming that the test interface is intercepted, the real intercepting interface will be configured when the intercepting interface is actually encountered later
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/test");
    }
}

6.2.3 testing

src/main/java/com/mszlu/blog/controller/TestController.java

package com.mszlu.blog.controller;

import com.mszlu.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(){
        return Result.success(null);
    }
}

src/main/java/com/mszlu/blog/handler/LoginInterceptor.java returns true for release, and the test interface can be accessed normally

6.3. ThreadLocal saves user information

Only token is put in redis. We want to get user information directly
Benefits and how to use them
Use ThreadLocal to save user login information
Use ThreadLocal instead of Session to complete the function of saving user login information

Benefits of using ThreadLocal instead of Session:

 User information can be easily obtained in the same thread without frequent transmission session Object.

Specific implementation process:

   In the login business code, when the user logs in successfully, a login credential is generated and stored in the redis In,
   Save the string in the voucher in cookie Return to the client in.
   Use an interceptor to intercept requests from cookie Get voucher string and redis Match the vouchers in to obtain user information,
   Store user information in ThreadLocal If you hold the user information in this request, you can use the user information in subsequent operations.

Related issues
Session principle
What's the difference between COOKIE and SESSION?

src/main/java/com/mszlu/blog/utils/UserThreadLocal.java

package com.mszlu.blog.utils;

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

public class UserThreadLocal {

    private UserThreadLocal(){}
    //Thread variable isolation
    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();
    }
}

src/main/java/com/mszlu/blog/handler/LoginInterceptor.java

package com.mszlu.blog.handler;

import com.alibaba.fastjson.JSON;
import com.mszlu.blog.dao.pojo.SysUser;
import com.mszlu.blog.service.LoginService;
import com.mszlu.blog.utils.UserThreadLocal;
import com.mszlu.blog.vo.ErrorCode;
import com.mszlu.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 {
        //Execute before executing the controller method (Handler)
        /**
         * 1. You need to judge whether the requested interface path is a HandlerMethod (controller method)
         * 2. Judge whether the token is empty. If it is empty, you are not logged in
         * 3. If the token is not empty, log in and verify loginService checkToken
         * 4. If the certification is successful, it can be released
         */
        if (!(handler instanceof HandlerMethod)){
            //The handler may be requestresourcehandler. The springboot program accesses the static resources and queries the static directory under the classpath by default
            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(), "Not logged in");
            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(), "Not logged in");
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        //Login verification successful, release
        //I want to get the user's information directly in the controller. How can I get it?
        UserThreadLocal.put(sysUser);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
       //If you do not delete the information used up in ThreadLocal, there is a risk of memory leakage
        UserThreadLocal.remove();
    }
}

src/main/java/com/mszlu/blog/controller/TestController.java

package com.mszlu.blog.controller;

import com.mszlu.blog.dao.pojo.SysUser;
import com.mszlu.blog.utils.UserThreadLocal;
import com.mszlu.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 sysUser = UserThreadLocal.get();
        System.out.println(sysUser);
        return Result.success(null);
    }
}

7.1. ThreadLocal memory leak

ThreadLocal principle and memory leakage prevention

Solid lines represent strong references and dotted lines represent weak references

Each Thread maintains a threadlocalmap. Key is the ThreadLocal instance using weak reference, and value is the copy of Thread variable.

Strong reference, the most commonly used reference. An object has strong reference and will not be recycled by the garbage collector. When the memory space is insufficient, the Java virtual machine would rather throw OutOfMemoryError error to make the program terminate abnormally than recycle this object.

If you want to disassociate a strong reference from an object, you can explicitly assign the reference to null, so that the JVM can reclaim the object at the right time.

Weak reference: when the JVM performs garbage collection, the objects associated with weak references will be recycled regardless of whether the memory is sufficient or not. In java, use java Lang.ref.weakreference class.

7.2. Article details

7.2.1 interface description

Interface url: / articles/view/{id}

Request method: POST

Request parameters:

Parameter name

Parameter type

explain

id

long

Article id (path parameter)

Return data:

{success: true, code: 200, msg: "success",...}
code: 200
data: {id: "1405916999732707330", title: "SpringBoot Introductory case", summary: "springboot Introductory case", commentCounts: 0,...}
msg: "success"
success: true

7.2.2 tables involved

table of content

content stores information in makedown format
content_html stores information in html format

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;

src/main/java/com/mszlu/blog/dao/pojo/ArticleBody.java

package com.mszlu.blog.dao.pojo;

import lombok.Data;
//table of content
@Data
public class ArticleBody {

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

Category table
avata path classification Icon
category_name of the icon for the category
description Description of classification

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;

src/main/java/com/mszlu/blog/dao/pojo/Category.java

package com.mszlu.blog.dao.pojo;

import lombok.Data;
//Category table
@Data
public class Category {

    private Long id;

    private String avatar;

    private String categoryName;

    private String description;
}

7.2.3 Controller

src/main/java/com/mszlu/blog/controller/ArticleController.java

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

7.2.4 Service

src/main/java/com/mszlu/blog/service/ArticleService.java

    /**
     * View Article Details
     * @param articleId
     * @return
     */
    Result findArticleById(Long articleId);

There are only tilt and some introductions in the article table
ms_ Body in Article_ id corresponds to the second table ms_article_ id on body
ms_category will be mapped to ms_category in Article_ id
You need to do some relative association queries

src/main/java/com/mszlu/blog/service/impl/ArticleServiceImpl.java

    @Override
    public Result findArticleById(Long articleId) {
        /**
         * 1. Query article information according to id
         * 2. Do association query according to bodyId and categoryid
         */
        Article article = this.articleMapper.selectById(articleId);
        ArticleVo articleVo = copy(article, true, true,true,true);
        //After viewing the article, the number of new readings. Is there a problem?
        //After viewing the article, the data should be returned directly. At this time, an update operation is performed. When updating, write lock is added to block other read operations, and the performance will be relatively low
        // The update increases the time-consuming of the interface. If there is a problem with the update, the operation of viewing the article cannot be affected
        //The thread pool can throw the update operation into the thread pool for execution, which is not related to the main thread
       //threadService.updateArticleViewCount(articleMapper,article);
        return Result.success(articleVo);
    }

src/main/java/com/mszlu/blog/vo/ArticleVo.java

package com.mszlu.blog.vo;

import lombok.Data;

import java.util.List;

@Data
public class ArticleVo {

    private Long id;

    private String title;

    private String summary;

    private int commentCounts;

    private int viewCounts;

    private int weight;
    /**
     * Creation time
     */
    private String createDate;

    private String author;

    private ArticleBodyVo body;

    private List<TagVo> tags;

    private CategoryVo category;

}

src/main/java/com/mszlu/blog/vo/ArticleBodyVo.java

package com.mszlu.blog.vo;

import lombok.Data;

@Data
public class ArticleBodyVo {

//content
    private String content;
}

src/main/java/com/mszlu/blog/vo/CategoryVo.java

package com.mszlu.blog.vo;

import lombok.Data;

@Data
public class CategoryVo {

//id, icon path, icon name
    private Long id;

    private String avatar;

    private String categoryName;
}

Attribute filling in ArticleVo:
src/main/java/com/mszlu/blog/service/impl/ArticleServiceImpl.java

//Method overload. The method name is the same, and the number of parameters is different
    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) {
        List<ArticleVo> articleVoList = new ArrayList<>();
        for (Article record : records) {
            articleVoList.add(copy(record,isTag,isAuthor,isBody,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;
    }

    @Autowired
    private CategoryService categoryService;
//With body information and category information
    private ArticleVo copy(Article article, boolean isTag, boolean isAuthor, boolean isBody,boolean isCategory){
        ArticleVo articleVo = new ArticleVo();
        articleVo.setId(String.valueOf(article.getId()));
        BeanUtils.copyProperties(article,articleVo);
        //Time can't copy because it's a long type
        articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
        //Not all interfaces need labels and author information
        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;
    }

    @Autowired
    private CategoryService categoryService;

    private CategoryVo findCategory(Long categoryId) {
        return categoryService.findCategoryById(categoryId);
    }
    //Building ArticleBodyMapper
    @Autowired
    private ArticleBodyMapper articleBodyMapper;

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

src/main/java/com/mszlu/blog/dao/mapper/ArticleBodyMapper.java

package com.mszlu.blog.dao.mapper;

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

public interface ArticleBodyMapper extends BaseMapper<ArticleBody> {
}

src/main/java/com/mszlu/blog/dao/mapper/CategoryMapper.java

package com.mszlu.blog.dao.mapper;

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

public interface CategoryMapper extends BaseMapper<Category> {
}

src/main/java/com/mszlu/blog/service/CategoryService.java

package com.mszlu.blog.service;

import com.mszlu.blog.vo.CategoryVo;


public interface CategoryService {

    CategoryVo findCategoryById(Long id);
}

src/main/java/com/mszlu/blog/service/impl/CategoryServiceImpl.java

package com.mszlu.blog.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mszlu.blog.dao.mapper.CategoryMapper;
import com.mszlu.blog.dao.pojo.Category;
import com.mszlu.blog.service.CategoryService;
import com.mszlu.blog.vo.CategoryVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


//Inject spring 
@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();
        //Because the attributes of category and categoryvo are the same, you can use BeanUtils copyProperties
        BeanUtils.copyProperties(category,categoryVo);
        return categoryVo;
    }
}

7.2.5 testing

7.3. Number of reads updated using thread pool

//After viewing the article, the number of new readings. Is there a problem?
//After viewing the article, the data should be returned directly. At this time, an update operation is performed. When updating, the write lock is added to block other read operations, and the performance will be relatively low (there is no way to solve it, and the lock must be added to increase the number of reads)
//The update increases the time-consuming of the interface (consider reducing the time-consuming). If there is a problem with the update, the viewing operation cannot be affected
Thought of a technology thread pool
You can throw the update operation into the thread pool for execution, which is not related to the main thread
What is optimistic lock and what is pessimistic lock
CAS principle analysis

7.3.1 configuration of route pool

Make a thread pool configuration to open the thread pool
src/main/java/com/mszlu/blog/config/ThreadPoolConfig.java

package com.mszlu.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 Usage and understanding of @ Configuration and @ Bean
@Configuration
@EnableAsync //Turn on Multithreading
public class ThreadPoolConfig {

    @Bean("taskExecutor")
    public Executor asyncServiceExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // Set the number of core threads
        executor.setCorePoolSize(5);
        // Set the maximum number of threads
        executor.setMaxPoolSize(20);
        //Configure queue size
        executor.setQueueCapacity(Integer.MAX_VALUE);
        // Set thread active time (seconds)
        executor.setKeepAliveSeconds(60);
        // Set default thread name
        executor.setThreadNamePrefix("Code God Road blog project");
        // Wait for all tasks to finish before closing the thread pool
        executor.setWaitForTasksToCompleteOnShutdown(true);
        //Perform initialization
        executor.initialize();
        return executor;
    }
}

7.3.1 use

src/main/java/com/mszlu/blog/service/ThreadService.java

package com.mszlu.blog.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mszlu.blog.dao.mapper.ArticleMapper;
import com.mszlu.blog.dao.pojo.Article;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class ThreadService {
    //It is expected that the execution of this operation in the thread pool will not affect the original main thread
    //If you don't know about thread pool here, you can see JUC concurrent programming
    @Async("taskExcutor")
    public void updateArticleViewCount(ArticleMapper articleMapper, Article article) {

        Integer viewCounts = article.getViewCounts();
        Article articleupdate = new Article();
        articleupdate.setViewCounts(viewCounts+1);
        LambdaQueryWrapper<Article> updatewrapper = new LambdaQueryWrapper<>();
        //Update by id
        updatewrapper.eq(Article::getId ,article.getId());
        //Set one for thread safety in a multithreaded environment
        //Before changing, confirm whether this value has been modified by other threads first. It is similar to cas operation, cas plus spin, and adding a loop is cas
        updatewrapper.eq(Article ::getViewCounts,viewCounts );
        // update article set view_count=100 where view_count=99 and id =111
        //Entity class plus update condition
        articleMapper.update(articleupdate,updatewrapper);
        try {
            Thread.sleep(5000);
            System.out.println("The update is complete");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

src/main/java/com/mszlu/blog/service/impl/ArticleServiceImpl.java

    @Autowired
    private ThreadService threadService;

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

7.3.3 testing

Sleep the method in ThredService for 5 seconds, which will not affect the use of the main thread, that is, the article details will be displayed quickly and will not be affected

Bug correction

When the commentCounts, viewCounts and weight fields in the previous Article are int, which will cause the reading times to be updated, set the other two fields to the initial value of 0
mybatisplus only set up articleupdate.com when updating the reading times of articles setviewsCounts(viewCounts+1),
But the default basic data type of int is 0,
If mybatisplus is not null, it will be generated into sql statements for updating. Will appear
Ideally, there should be only views_counts changes, but this happens because of the mybatisplus rule
Therefore, changing int to Integer will not cause this problem.
src/main/java/com/mszlu/blog/dao/pojo/Article.java

package com.mszlu.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 Integer commentCounts;

    private Integer viewCounts;

    /**
     * Author id
     */
    private Long authorId;
    /**
     * Content id
     */
    private Long bodyId;
    /**
     *Category id
     */
    private Long categoryId;

    /**
     * Topping
     */
    private Integer weight;


    /**
     * Creation time
     */
    private Long createDate;
}

8.1. Comment list

Comment form
id comment id
Content comment content
create_date comment time
article_id comment article
author_ Who commented
parent_id building function to reply to comments
to_uid to whom
What level is the comment level (Level 1 indicates the top level comment, and level 2 indicates the comment on the comment)

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;

src/main/java/com/mszlu/blog/dao/pojo/Comment.java

package com.mszlu.blog.dao.pojo;

import lombok.Data;

@Data
public class Comment {

    private Long id;

    private String content;

    private Long createDate;

    private Long articleId;

    private Long authorId;

    private Long parentId;

    private Long toUid;

    private Integer level;
}

8.1.1 interface description

Interface url: / comments/article/{id}

Request method: GET

Request parameters:

Parameter name

Parameter type

explain

id

long

Article id (path parameter)

Return data:

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

Code structure

8.1.2 Controller

src/main/java/com/mszlu/blog/controller/CommentsController.java

package com.mszlu.blog.controller;

import com.mszlu.blog.service.CommentsService;
import com.mszlu.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

    @Autowired
    private CommentsService commentsService;

    @GetMapping("article/{id}")
    public Result comments(@PathVariable("id") Long articleId){

        return commentsService.commentsByArticleId(articleId);

    }
}

8.1.3 Service

src/main/java/com/mszlu/blog/service/CommentsService.java

package com.mszlu.blog.service;

import com.mszlu.blog.vo.Result;

public interface CommentsService {

    /**
     * Query all comment lists according to the article id
     * @param id
     * @return
     */
    Result commentsByArticleId(Long id);
}

src/main/java/com/mszlu/blog/service/impl/CommentsServiceImpl.java

package com.mszlu.blog.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mszlu.blog.dao.mapper.CommentMapper;
import com.mszlu.blog.dao.pojo.Comment;
import com.mszlu.blog.service.CommentsService;
import com.mszlu.blog.service.SysUserService;
import com.mszlu.blog.vo.CommentVo;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.UserVo;
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 CommentsServiceImpl implements CommentsService {

    @Autowired
    private CommentMapper commentMapper;
    @Autowired
    private SysUserService sysUserService;
    @Override
    public Result commentsByArticleId(Long articleId) {
            /**
         * 1. Query the comment list according to the article id and query from the comment table
         * 2. Query the author's information according to the author's id
         * 3. Judge whether there are sub comments if level = 1
         * 4. If yes, query according to the comment ID (parent_id)
         */
        LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
        //Query according to Article id
        queryWrapper.eq(Comment::getArticleId,id );
        //Query according to hierarchical relationship
        queryWrapper.eq(Comment::getLevel,1 );
        List<Comment> comments = commentMapper.selectList(queryWrapper);
        
        List<CommentVo> commentVoList = copyList(comments);
        return Result.success(commentVoList);
    }
    //Judge the comment in the list table
    public List<CommentVo> copyList(List<Comment> commentList){
        List<CommentVo> commentVoList = new ArrayList<>();
        for (Comment comment : commentList) {
            commentVoList.add(copy(comment));
        }
        return commentVoList;
    }

    private CommentVo copy(Comment comment) {
        CommentVo commentVo = new CommentVo();
        // Same attribute copy
        BeanUtils.copyProperties(comment,commentVo);
        commentVo.setId(String.valueOf(comment.getId()));
        //Author information
        Long authorId = comment.getAuthorId();
        UserVo userVo = this.sysUserService.findUserVoById(authorId);
        commentVo.setAuthor(userVo);
        //Sub comment
        Integer level = comment.getLevel();
        if (1 == level){
            Long id = comment.getId();
            List<CommentVo> commentVoList = findCommentsByParentId(id);
            commentVo.setChildrens(commentVoList);
        }
        //to User to whom
        if (level > 1){
            Long toUid = comment.getToUid();
            UserVo toUserVo = this.sysUserService.findUserVoById(toUid);
            commentVo.setToUser(toUserVo);
        }
        return commentVo;
    }

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




}

Returned data:
src/main/java/com/mszlu/blog/vo/CommentVo.java

package com.mszlu.blog.vo;

import com.mszlu.blog.dao.pojo.SysUser;
import lombok.Data;

import java.util.List;

@Data
public class CommentVo  {

    private Long id;

    private UserVo author;

    private String content;

    private List<CommentVo> childrens;

    private String createDate;

    private Integer level;

    private UserVo toUser;
}

src/main/java/com/mszlu/blog/vo/UserVo.java

package com.mszlu.blog.vo;

import lombok.Data;

@Data
public class UserVo {

    private String nickname;

    private String avatar;

    private Long id;
}

The SysUserService provides a service for querying user information:
src/main/java/com/mszlu/blog/service/SysUserService.java

  UserVo findUserVoById(Long id);

src/main/java/com/mszlu/blog/service/impl/SysUserServiceImpl.java

    @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("Code God's Road");
        }
        UserVo userVo = new UserVo();
        userVo.setAvatar(sysUser.getAvatar());
        userVo.setNickname(sysUser.getNickname());
        userVo.setId(sysUser.getId());
        return userVo;
    }

8.2. comment

8.2.1 interface description

Interface url: / comments/create/change

Request method: POST

Request parameters:

Parameter name

Parameter type

explain

articleId

long

Article id

content

string

Comment content

parent

long

Parent comment id

toUserId

long

Commented user id

Return data:

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

8.2.2 add to login interceptor

src/main/java/com/mszlu/blog/config/WebMVCConfig.java

@Override
    public void addInterceptors(InterceptorRegistry registry) {
        //Intercept the test interface. When the interface to be intercepted is actually encountered later, it will be configured as a real intercepting interface
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/test").addPathPatterns("/comments/create/change");
    }

8.2.3 Controller

Code structure

Build comment parameter object:
src/main/java/com/mszlu/blog/vo/params/CommentParam.java

package com.mszlu.blog.vo.params;

import lombok.Data;

@Data
public class CommentParam {

    private Long articleId;

    private String content;

    private Long parent;

    private Long toUserId;
}

src/main/java/com/mszlu/blog/controller/CommentsController.java

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

8.2.4 Service

src/main/java/com/mszlu/blog/service/CommentsService.java

    Result comment(CommentParam commentParam);

src/main/java/com/mszlu/blog/service/impl/CommentsServiceImpl.java

 @Override
    public Result comment(CommentParam commentParam) {
    //Get the current user
        SysUser sysUser = UserThreadLocal.get();
        Comment comment = new Comment();
        comment.setArticleId(commentParam.getArticleId());
        comment.setAuthorId(sysUser.getId());
        comment.setContent(commentParam.getContent());
        comment.setCreateDate(System.currentTimeMillis());
        Long parent = commentParam.getParent();
        if (parent == null || parent == 0) {
            comment.setLevel(1);
        }else{
            comment.setLevel(2);
        }
        //If it is empty, the parent is 0
        comment.setParentId(parent == null ? 0 : parent);
        Long toUserId = commentParam.getToUserId();
        comment.setToUid(toUserId == null ? 0 : toUserId);
        this.commentMapper.insert(comment);
        return Result.success(null);
    }


  //Prevent the loss of front-end precision and convert id to string
// The distributed id is relatively long, and there will be precision loss when it is transmitted to the front end. It must be converted to string type for transmission, so there will be no problem
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;

write an article

Writing articles requires three interfaces:

  1. Get all article categories

  2. Get all tags

  3. Publish articles

9.1. All article categories

9.1.1 interface description

Interface url: / categories

Request method: GET

Request parameters:

Parameter name

Parameter type

explain

Return data:

{
    "success":true,
 	"code":200,
    "msg":"success",
    "data":
    [
        {"id":1,"avatar":"/category/front.png","categoryName":"front end"},	
        {"id":2,"avatar":"/category/back.png","categoryName":"back-end"},
        {"id":3,"avatar":"/category/lift.jpg","categoryName":"life"},
        {"id":4,"avatar":"/category/database.png","categoryName":"database"},
        {"id":5,"avatar":"/category/language.png","categoryName":"programing language"}
    ]
}

9.1.2 Controller

src/main/java/com/mszlu/blog/controller/CategoryController.java

package com.mszlu.blog.controller;

import com.mszlu.blog.service.CategoryService;
import com.mszlu.blog.vo.CategoryVo;
import com.mszlu.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 listCategory() {
        return categoryService.findAll();
    }
}

9.1.3 Service

src/main/java/com/mszlu/blog/service/impl/CategoryServiceImpl.java

//id is inconsistent. You need to reset it
    public CategoryVo copy(Category category){
        CategoryVo categoryVo = new CategoryVo();
        BeanUtils.copyProperties(category,categoryVo);
        //id is inconsistent. You need to reset it
        categoryVo.setId(String.valueOf(category.getId()));
        return categoryVo;
    }
    public List<CategoryVo> copyList(List<Category> categoryList){
        List<CategoryVo> categoryVoList = new ArrayList<>();
        for (Category category : categoryList) {
            categoryVoList.add(copy(category));
        }
        return categoryVoList;
    }

    @Override
    public Result findAll() {
    // Without any parameters, all you need is an empty LambdaQueryWrapper
        List<Category> categories = this.categoryMapper.selectList(new LambdaQueryWrapper<>());
         //Object of page interaction
        return Result.success(copyList(categories));
    }

9.2. All article Tags

9.2.1 interface description

Interface url: / tags

Request method: GET

Request parameters:

Parameter name

Parameter type

explain

Return data:

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

9.2.2 Controller

src/main/java/com/mszlu/blog/controller/TagsController.java

    @Autowired
    private TagService tagService;
 	@GetMapping
    public Result findAll(){
        /**
     * Query all article Tags
     * @return
     */
        return tagService.findAll();
    }

9.2.3 Service

src/main/java/com/mszlu/blog/service/TagService.java

    /**
     * Query all article Tags
     * @return
     */
    Result findAll();

src/main/java/com/mszlu/blog/service/impl/TagServiceImpl.java

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

9.3. Publish articles

9.3.1 interface description

The content of the request is object({content: "ww", contentHtml: "ww?"})Because they are makedown Editor for
id Refers to the article id

Interface url: / articles/publish

Request method: POST

Request parameters:

Parameter name

Parameter type

explain

title

string

Article title

id

long

Article id (edit value)

body

object({content: "ww", contentHtml: "

ww

"})

Article content

category

{id: 2, avatar: "/ category/back.png", catalyname: "back end"}

Article category

summary

string

Article overview

tags

[{id: 5}, {id: 6}]

Article label

Return data:

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

Code structure

9.3.2 Controller

src/main/java/com/mszlu/blog/controller/ArticleController.java

    //  @The RequestBody is mainly used to receive the data in the json string passed from the front end to the back end (the data in the request body);
    //  The most common way to use the request body to transfer parameters is undoubtedly the POST request, so when using @ RequestBody to receive data, it is generally submitted in the form of POST.
	@PostMapping("publish")
    public Result publish(@RequestBody ArticleParam articleParam){
        return articleService.publish(articleParam);
    }

We need to establish a parameter object to receive the data from the front end
src/main/java/com/mszlu/blog/vo/params/ArticleParam.java

package com.mszlu.blog.vo.params;

import com.mszlu.blog.vo.CategoryVo;
import com.mszlu.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;
}

src/main/java/com/mszlu/blog/vo/params/ArticleBodyParam.java

package com.mszlu.blog.vo.params;

import lombok.Data;

@Data
public class ArticleBodyParam {

    private String content;

    private String contentHtml;

}

9.3.3 Service

src/main/java/com/mszlu/blog/service/ArticleService.java

    /**
     * Article publishing service
     * @param articleParam
     * @return
     */
    Result publish(ArticleParam articleParam);

src/main/java/com/mszlu/blog/service/impl/ArticleServiceImpl.java

@Override
    @Transactional
    public Result publish(ArticleParam articleParam) {
    //Note that if you want to get the data, you must add the interface to the interceptor
        SysUser sysUser = UserThreadLocal.get();

        /**
         * 1. The purpose of publishing articles is to build Article objects
         * 2. Author id current login user
         * 3. Tag to add the tag to the association list
         * 4. body Content store 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);
        //After insertion, an article id will be generated (because the new article does not have an article id, so you need to insert it
        //The official website explained: "after insart, the primary key will be automatically 'set to the ID field of the entity. So you only need" getid(). "
//        Using the self increment of the primary key, the id value of mp will return to the parameter object after the insert operation
        //https://blog.csdn.net/HSJ0170/article/details/107982866
        this.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());
                this.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);
//Give an id after inserting
        article.setBodyId(articleBody.getId());
         //When does the save method in MybatisPlus execute insert and update
        // https://www.cxyzjd.com/article/Horse7/103868144
       //Insert or update only when changing the database. General query is OK
        articleMapper.updateById(article);
        
        ArticleVo articleVo = new ArticleVo();
        articleVo.setId(article.getId());
        return Result.success(articleVo);
    }

src/main/java/com/mszlu/blog/config/WebMVCConfig.java

Of course, to log in to the interceptor, you need to add the configuration of publishing articles:

 @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //Intercept the test interface. When the interface to be intercepted is actually encountered later, it will be configured as a real intercepting interface
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/test")
                .addPathPatterns("/comments/create/change")
                .addPathPatterns("/articles/publish");
    }

src/main/java/com/mszlu/blog/dao/mapper/ArticleTagMapper.java

package com.mszlu.blog.dao.mapper;

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

public interface ArticleTagMapper  extends BaseMapper<ArticleTag> {
}

src/main/java/com/mszlu/blog/dao/mapper/ArticleBodyMapper.java

package com.mszlu.blog.dao.mapper;

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

public interface ArticleBodyMapper extends BaseMapper<ArticleBody> {
}

src/main/java/com/mszlu/blog/vo/ArticleVo.java

package com.mszlu.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 ArticleVo {
	//Be sure to add, or there will be a loss of accuracy
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;

    private String title;

    private String summary;

    private Integer commentCounts;

    private Integer viewCounts;

    private Integer weight;
    /**
     * Creation time
     */
    private String createDate;

    private String author;

    private ArticleBodyVo body;

    private List<TagVo> tags;

    private CategoryVo category;

}

src/main/java/com/mszlu/blog/dao/pojo/ArticleTag.java

package com.mszlu.blog.dao.pojo;

import lombok.Data;

@Data
public class ArticleTag {

    private Long id;

    private Long articleId;

    private Long tagId;
}

9.3.4 testing

9.4. AOP log

IOC is one of the two core concepts of spring. IOC provides us with an IOCbean container, which will help us create objects automatically without manual creation. IOC is created through DI (Dependency Injection). We can write Java annotation code or XML configuration, Automatically inject some other beans that we want to inject into the object. It helps us inject through byName or byType. It is precisely because of Dependency Injection that IOC has the very powerful advantage of decoupling.

For example, if we want to inject beans like JdbcTemplate or SqlSessionFactory into the container, they need to rely on a data source. If we strongly couple the data sources of JdbcTemplate or Druid, it will lead to a problem. When we want to use Jdbc template, we must use Druid data source, Dependency injection can help us to rely on a DataSource interface instead of a specific implementation when injecting into the Jdbc. The advantage is that when we inject a druid data source into the container in the future, it will be automatically injected into the JdbcTemplate. If we inject another one, it is the same. For example, c3p0 is the same. In this way, the JdbcTemplate is completely decoupled from the data source and does not rely strongly on any data source. When spring starts, all beans will be created. In this way, the program does not need to create beans when running, and the running speed will be faster. In addition, when IOC manages beans, it is a singleton by default, It can save time and improve performance,

Understanding of spring IOC, AOP and MVC

Explanation of Springboot AOP log

The original method is not enhanced on the basis of the original method
src/main/java/com/mszlu/blog/common/aop/LogAnnotation.java

package com.mszlu.blog.common.aop;

import java.lang.annotation.*;

/**
 * Log annotation
 */
 //ElementType. The type representative can be placed on the class, and the method representative can be placed on the method
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {

    String module() default "";

    String operation() default "";
}

Adding this annotation means that we need log output for this interface
src/main/java/com/mszlu/blog/controller/ArticleController.java

    @PostMapping
    //Adding this annotation means that the interface should be logged
    @LogAnnotation(module = "article",operation = "Get article list")
    public Result listArticle(@RequestBody PageParams pageParams){

        return articleService.listArticle(pageParams);

    }

src/main/java/com/mszlu/blog/common/aop/LogAspect.java

package com.mszlu.blog.common.aop;

import com.alibaba.fastjson.JSON;
import com.mszlu.blog.utils.HttpContextUtils;
import com.mszlu.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;

/**
 * @Author ljm
 * @Date 2021/10/18 21:01
 * @Version 1.0
 */
@Component
@Aspect //Aspect defines the relationship between notification and pointcut
@Slf4j
public class LogAspect {

    @Pointcut("@annotation(com.mszlu.blog.common.aop.LogAnnotation)")
    public void pt(){
    }

    //Around Advice 
    @Around("pt()")
    public Object log(ProceedingJoinPoint point) throws Throwable {
        long beginTime = System.currentTimeMillis();
        //Execution method
        Object result = point.proceed();
        //Execution time (MS)
        long time = System.currentTimeMillis() - beginTime;
        //Save log
        recordLog(point, 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.operation());

        //Requested method name
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = signature.getName();
        log.info("request method:{}",className + "." + methodName + "()");

//        //Requested parameters
        Object[] args = joinPoint.getArgs();
        String params = JSON.toJSONString(args[0]);
        log.info("params:{}",params);

        //Get request set IP address
        HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
        log.info("ip:{}", IpUtils.getIpAddr(request));


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




}

Method class used
src/main/java/com/mszlu/blog/utils/HttpContextUtils.java

package com.mszlu.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();
    }

}

src/main/java/com/mszlu/blog/utils/IpUtils.java

package com.mszlu.blog.utils;

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

import javax.servlet.http.HttpServletRequest;

/**
 * Get Ip
 *
 */
@Slf4j
public class IpUtils {

    /**
     * Get IP address
     * <p>
     * If you use reverse proxy software such as Nginx, you cannot pass request Getremoteaddr() get IP address
     * If multi-level reverse proxy is used, the value of x-forward-for is not only one, but a string of IP addresses. The first valid IP string in x-forward-for that is not unknown is the real IP address
     */
    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);
        }

        // If you use a proxy, get the first IP address
        if (StringUtils.isEmpty(ip) && ip.length() > maxLength) {
            int idx = ip.indexOf(seperator);
            if (idx > 0) {
                ip = ip.substring(0, idx);
            }
        }

        return ip;
    }

    /**
     * Get ip address
     *
     * @return
     */
    public static String getIpAddr() {
        HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
        return getIpAddr(request);
    }
}

The result is

ip address query is

bug fix

Prevent the obtained value from being null, because the obtained value is a millisecond value, which needs to be converted. Y represents the year and m represents the month. Rewrite the time.

Article archiving:

Description of correlation function

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

10.1. Article image upload

10.1.1 interface description

Interface url: / upload

Request method: POST

Request parameters:

Parameter name

Parameter type

explain

image

file

Name of uploaded file

Return data:

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

Modify the pom file and introduce the sdk of qiniu cloud
pom.xml

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

10.1.2 Controller

src/main/java/com/mszlu/blog/controller/UploadController.java

package com.mszlu.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 introduction
    @PostMapping
    public Result upload(@RequestParam("image")MultipartFile file){
        //Original file name, such as AA png
        String originalFilename = file.getOriginalFilename();
        //Unique file name
        String fileName =  UUID.randomUUID().toString()+"."+StringUtils.substringAfterLast(originalFilename, ".");
        //Where to upload the file? Qiniu cloud server
        //Reduce the bandwidth consumption of our own application servers
        boolean upload = qiniuUtils.upload(file, fileName);
        if (upload) {
            return Result.success(QiniuUtils.url+fileName);
        }
        return Result.fail(20001,"Upload failed");

}

10.1.3 use qiniu cloud

Note the domain name of qiniu cloud test https://static.mszlu.com/ It's recycled once a month. Remember to modify it.
springboot only uploads 1M images by default, so modify the file configuration
src/main/resources/application.properties

# Maximum total uploaded files
spring.servlet.multipart.max-request-size=20MB
# Maximum value of a single file
spring.servlet.multipart.max-file-size=2MB

Seven cattle cloud storage space tutorial

src/main/java/com/mszlu/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/";

//Modify the following two values, put them into Proprietaries, and obtain them in key management
    @Value("${qiniu.accessKey}")
    private  String accessKey;
    @Value("${qiniu.accessSecretKey}")
    private  String accessSecretKey;

    public  boolean upload(MultipartFile file,String fileName){

        //Construct a configuration class with a specified Region object
        Configuration cfg = new Configuration(Region.huabei());
        //... Other parameters refer to class notes
        UploadManager uploadManager = new UploadManager(cfg);
        //... Generate the upload voucher, then prepare to upload, modify the upload name and create the space name for yourself (it's your own)
        String bucket = "mszlu";
        //If the key is not specified by default, the hash value of the file content is used as the file name
        try {
            byte[] uploadBytes = file.getBytes();
            Auth auth = Auth.create(accessKey, accessSecretKey);
            String upToken = auth.uploadToken(bucket);
                Response response = uploadManager.put(uploadBytes, fileName, upToken);
                //Analyze the result of successful upload
                DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);
                return true;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        return false;
    }
}

10.1.4 testing

10.2. Navigation - article classification

10.2.1 query all article classifications

10.2.1.1 interface description

Interface url: / categories / detail

Request method: GET

Request parameters:

Parameter name

Parameter type

explain

Return data:

{
    "success": true, 
    "code": 200, 
    "msg": "success", 
    "data": [
        {
            "id": 1, 
            "avatar": "/static/category/front.png", 
            "categoryName": "front end", 
            "description": "What's the front end, big front end"
        }, 
        {
            "id": 2, 
            "avatar": "/static/category/back.png", 
            "categoryName": "back-end", 
            "description": "The best fork at the back"
        }, 
        {
            "id": 3, 
            "avatar": "/static/category/lift.jpg", 
            "categoryName": "life", 
            "description": "Interesting things in life"
        }, 
        {
            "id": 4, 
            "avatar": "/static/category/database.png", 
            "categoryName": "database", 
            "description": "The database doesn't work"
        }, 
        {
            "id": 5, 
            "avatar": "/static/category/language.png", 
            "categoryName": "programing language", 
            "description": "Many languages, which should I learn?"
        }
    ]
}

src/main/java/com/mszlu/blog/vo/CategoryVo.java

package com.mszlu.blog.vo;

import lombok.Data;

@Data
public class CategoryVo {

    private Long id;

    private String avatar;

    private String categoryName;

    private String description;
}

10.2.1.2 Controller

src/main/java/com/mszlu/blog/controller/CategoryController.java

@GetMapping("detail")
    public Result categoriesDetail(){
        return categoryService.findAllDetail();
    }

10.2.1.3 Service

src/main/java/com/mszlu/blog/service/CategoryService.java

    Result findAllDetail();

src/main/java/com/mszlu/blog/service/impl/CategoryServiceImpl.java

 @Override
    public Result findAllDetail() {
        List<Category> categories = categoryMapper.selectList(new LambdaQueryWrapper<>());
        //Object of page interaction
        return Result.success(copyList(categories));
    }

Article classification display

10.2.2 query all labels

10.2.2.1 interface description

Interface url: / tags/detail

Request method: GET

Request parameters:

Parameter name

Parameter type

explain

Return data:

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

10.2.2.3 Controller

src/main/java/com/mszlu/blog/vo/TagVo.java

package com.mszlu.blog.vo;

import lombok.Data;

@Data
public class TagVo {

    private Long id;

    private String tagName;

    private String avatar;
}

src/main/java/com/mszlu/blog/controller/TagsController.java

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

10.2.2.4 Service

src/main/java/com/mszlu/blog/service/TagService.java

    Result findAllDetail();

src/main/java/com/mszlu/blog/service/impl/TagServiceImpl.java

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

Label display

10.3. Classified article list

10.3.1 interface description

Interface url: / category/detail/{id}

Request method: GET

Request parameters:

Parameter name

Parameter type

explain

id

Classification id

Path parameters

Return data:

{
    "success": true, 
    "code": 200, 
    "msg": "success", 
    "data": 
        {
            "id": 1, 
            "avatar": "/static/category/front.png", 
            "categoryName": "front end", 
            "description": "What's the front end, big front end"
        }
}

10.3.2 Controller

src/main/java/com/mszlu/blog/controller/CategoryController.java

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

10.3.3 Service

src/main/java/com/mszlu/blog/service/CategoryService.java

Result categoryDetailById(Long id);

src/main/java/com/mszlu/blog/service/impl/CategoryServiceImpl.java

@Override
    public Result categoriesDetailById(Long id) {
        Category category = categoryMapper.selectById(id);
        //Convert to CategoryVo
        CategoryVo categoryVo = copy(category);
        return Result.success(categoryVo);
    }

After completing the above, it can only be said that the icon of article classification can be displayed

However, if you want to display all the attribution content of the back end, you have to search the queryWrapper in the article query list. When the article classification label is not null, add the query element of article classification label to modify the classification.
src/main/java/com/mszlu/blog/service/impl/ArticleServiceImpl.java

    @Override
    public Result listArticle(PageParams pageParams) {
        /**
         * 1,Paging query of article database table
         */
        Page<Article> page = new Page<>(pageParams.getPage(), pageParams.getPageSize());
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
        //Add the classification id to the parameter of the query article. If it is not empty, add the classification condition  
        if (pageParams.getCategoryId()!=null) {
            //and category_id=#{categoryId}
            queryWrapper.eq(Article::getCategoryId,pageParams.getCategoryId());
        }
        //Whether to sort by placing it at the top or not, / / arrange it in reverse chronological order, which is equivalent to order by create_data desc
        queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);
        Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);
        //Paging query usage https://blog.csdn.net/weixin_41010294/article/details/105726879
        List<Article> records = articlePage.getRecords();
        // To return the vo data defined by us is the corresponding front-end data. We should not only return the current data, but need further processing
        List<ArticleVo> articleVoList =copyList(records,true,true);
        return Result.success(articleVoList);
    }

src/main/java/com/mszlu/blog/vo/params/PageParams.java

package com.mszlu.blog.vo.params;

import lombok.Data;

@Data
public class PageParams {

    private int page = 1;

    private int pageSize = 10;

    private Long categoryId;

    private Long tagId;
}

Finally, you can display the content under each label of all article categories

10.4. Tag article list

10.4.1 interface description

Interface url: / tags/detail/{id}

Request method: GET

Request parameters:

Parameter name

Parameter type

explain

id

Tag id

Path parameters

Return data:

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

10.4.2 Controller

src/main/java/com/mszlu/blog/controller/TagsController.java

    @GetMapping("detail/{id}")
    public Result findADetailById(@PathVariable("id") Long id){
        /**
         * Query all articles under all articles tab
         * @return
         */
        return tagService.findADetailById(id);
    }

10.4.3 Service

src/main/java/com/mszlu/blog/service/TagService.java

Result findADetailById(Long id);

src/main/java/com/mszlu/blog/service/impl/TagServiceImpl.java

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

Completing the above ensures that the article tag is displayed. We need to rewrite the article query interface to ensure that when we encounter label query, we can correctly query the content corresponding to the article tag, otherwise the content found by each tag is the same.

10.4.4 modify the original query article interface

src/main/java/com/mszlu/blog/service/impl/ArticleServiceImpl.java

        //Add tag condition query
        //There is no tag field in article table. An article has multiple tags
        //articie_tog article_id 1: n tag_id
        //We need to use a new queryWrapper that belongs to the article tag to convert the article of this article_ Find tag and save it in a list.
        // Then select the tag we need according to the in method of queryWrapper.



    @Override
    public Result listArticle(PageParams pageParams) {
        /**
         * 1,Paging query of article database table
         */
        Page<Article> page = new Page<>(pageParams.getPage(), pageParams.getPageSize());
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
        if (pageParams.getCategoryId()!=null) {
            //and category_id=#{categoryId}
            queryWrapper.eq(Article::getCategoryId,pageParams.getCategoryId());
        }
        List<Long> articleIdList = new ArrayList<>();
        if(pageParams.getTagId()!=null){
            //Add tag condition query
            //There is no tag field in article table. An article has multiple tags
            //articie_tog article_id 1: n tag_id
            //We need to use a new queryWrapper that belongs to the article tag to convert the article of this article_ Find tag and save it in a list.
            // Then select the tag we need according to the in method of queryWrapper.

            LambdaQueryWrapper<ArticleTag> articleTagLambdaQueryWrapper = new LambdaQueryWrapper<>();
            articleTagLambdaQueryWrapper.eq(ArticleTag::getTagId,pageParams.getTagId());
            List<ArticleTag> articleTags = articleTagMapper.selectList(articleTagLambdaQueryWrapper);
            for (ArticleTag articleTag : articleTags) {
                articleIdList.add(articleTag.getArticleId());
            }
            if (articleTags.size() > 0) {
                // and id in(1,2,3)
                queryWrapper.in(Article::getId,articleIdList);
            }

        }
        //Whether to sort by placing it at the top or not, / / arrange it in reverse chronological order, which is equivalent to order by create_data desc
        queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);
        Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);
        //Paging query usage https://blog.csdn.net/weixin_41010294/article/details/105726879
        List<Article> records = articlePage.getRecords();
        // To return the vo data defined by us is the corresponding front-end data. We should not only return the current data, but need further processing
        List<ArticleVo> articleVoList =copyList(records,true,true);
        return Result.success(articleVoList);
    }

10.4.5 testing

The final result is as follows. Each label corresponds to the article corresponding to the label

11.1. List of archived articles

11.1.1 interface description

Interface url: / articles

Request method: POST

Request parameters:

Parameter name

Parameter type

explain

year

string

year

month

string

month

Return data:

{
    "success": true, 
    "code": 200, 
    "msg": "success", 
    "data": [Article list. The data is the same as the previous article list interface]
        
}

mybatisplus hump naming and mapper XML usage

11.1.2 article list parameters

src/main/java/com/mszlu/blog/vo/params/PageParams.java

package com.mszlu.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;

    //Pass 6 and it becomes 06
    public String getMonth(){
        if (this.month != null && this.month.length() == 1){
            return "0"+this.month;
        }
        return this.month;
    }
}

11.1.3 using custom sql to implement Article List

src/main/java/com/mszlu/blog/service/impl/ArticleServiceImpl.java

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



    <resultMap id="articleMap" type="com.mszlu.blog.dao.pojo.Article">
        <id column="id" property="id" />
        <result column="author_id" property="authorId"/>
        <result column="comment_counts" property="commentCounts"/>
        <result column="create_date" property="createDate"/>
        <result column="summary" property="summary"/>
        <result column="title" property="title"/>
        <result column="view_counts" property="viewCounts"/>
        <result column="weight" property="weight"/>
        <result column="body_id" property="bodyId"/>
        <result column="category_id" property="categoryId"/>
    </resultMap>

 
<!-- resultMap and resultType difference   https://blog.csdn.net/xushiyu1996818/article/details/89075069?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-4.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-4.no_search_link-->
<!--Hump nomenclature   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%7Edefault%7EBlogCommendFromBaidu%7Edefault-6.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-6.no_search_link-->
<!--    Long categoryId,-->
<!--    Long tagId,-->
<!--    String year,-->
<!--    String month-->
<!--mybatis in xml File usage    https://blog.csdn.net/weixin_43882997/article/details/85625805-->
<!--dynamic sql    https://www.jianshu.com/p/e309ae5e4a77-->
<!--Hump naming    https://zoutao.blog.csdn.net/article/details/82685918?spm=1001.2101.3001.6650.18&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-18.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-18.no_search_link-->
    <select id="listArticle" resultMap="articleMap">
        select * from ms_article
        <where>
            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>

11.1.4 testing

give the result as follows

11.2. Unified cache processing (optimized)

The access speed of memory is much higher than that of disk (from 1000 times)
Introduction to Spring Cache
src/main/java/com/mszlu/blog/common/cache/Cache.java

package com.mszlu.blog.common.cache;


import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {

    long expire() default 1 * 60 * 1000;

    String name() default "";

}

src/main/java/com/mszlu/blog/common/cache/CacheAspect.java

package com.mszlu.blog.common.cache;

import com.alibaba.fastjson.JSON;
import com.mszlu.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.core.annotation.AliasFor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

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

@Aspect
@Component
@Slf4j
public class CacheAspect {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

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

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp){
        try {
            Signature signature = pjp.getSignature();
            //Class name
            String className = pjp.getTarget().getClass().getSimpleName();
            //Method name called
            String methodName = signature.getName();


            Class[] parameterTypes = new Class[pjp.getArgs().length];
            Object[] args = pjp.getArgs();
            //parameter
            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)) {
                //Encrypt to prevent the key from being too long and the character escape cannot be obtained
                params = DigestUtils.md5Hex(params);
            }
            Method method = pjp.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);
            //Get Cache annotation
            Cache annotation = method.getAnnotation(Cache.class);
            //Cache expiration time
            long expire = annotation.expire();
            //Cache name
            String name = annotation.name();
            //Get it from redis first
            String redisKey = name + "::" + className+"::"+methodName+"::"+params;
            String redisValue = redisTemplate.opsForValue().get(redisKey);
            if (StringUtils.isNotEmpty(redisValue)){
                log.info("Go cache~~~,{},{}",className,methodName);
                return JSON.parseObject(redisValue, Result.class);
            }
            Object proceed = pjp.proceed();
            redisTemplate.opsForValue().set(redisKey,JSON.toJSONString(proceed), Duration.ofMillis(expire));
            log.info("Store in cache~~~ {},{}",className,methodName);
            return proceed;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return Result.fail(-999,"System error");
    }

}

use:

   @PostMapping("hot")
    @Cache(expire = 5 * 60 * 1000,name = "hot_article")
    public Result hotArticle(){
        int limit = 5;
        return articleService.hotArticle(limit);
    }

11.3. Think about other optimizations

mongodb
redis incr

  1. Articles can be put into es, which is convenient for subsequent Chinese word segmentation search. Spring boot tutorial has integration with ES
  2. The comment data can be put into mongodb and the e-commerce system. The comment data can be put into mongo
  3. For the number of readings and comments, consider adding the number of readings and comments to the redis incr self increment when the number of readings and comments increases, and use the scheduled task to regularly fix the data to the database
  4. In order to speed up the access speed, when deploying, you can put pictures, js, css, etc. into qiniu cloud storage to speed up the website access speed

Making a background and using spring security to make a permission system is of great help to the work

Register, record and deploy domain names

Management background

12.1. Construction project

12.1.1 new maven project 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-parent2</artifactId>
        <groupId>com.mszlu</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>
            <!-- Exclude default 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>

12.1.2 configuration

application.properties:

server.port=8889
spring.application.name=mszlu_admin_blog

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

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

# Maximum total uploaded files
spring.servlet.multipart.max-request-size=20MB
# Maximum value of a single file
spring.servlet.multipart.max-file-size=2MB

Mybatis plus configuration:

package com.mszlu.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
@MapperScan("com.mszlu.blog.admin.mapper")
public class MybatisPlusConfig {

    //Paging plug-in
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }
}

12.1.3 startup

package com.mszlu.blog.admin;

import com.alibaba.fastjson.annotation.JSONField;
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);
    }
}

12.1.4 import front-end engineering

Put it into the static directory under resources, and the front-end project is listed in the data

12.1.5 new table

Background management user table

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;

Permission table

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;

Association table of users and permissions

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;

12.2. Authority management

12.2.1 Controller

package com.mszlu.blog.admin.controller;

import com.mszlu.blog.admin.model.params.PageParam;
import com.mszlu.blog.admin.pojo.Permission;
import com.mszlu.blog.admin.service.PermissionService;
import com.mszlu.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 permissionList(@RequestBody PageParam pageParam){
        return permissionService.listPermission(pageParam);
    }

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

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

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



package com.mszlu.blog.admin.model.params;

import lombok.Data;

@Data
public class PageParam {

    private Integer currentPage;

    private Integer pageSize;

    private String queryString;
}



package com.mszlu.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;
}

12.2.2 Service

package com.mszlu.blog.admin.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.mszlu.blog.admin.mapper.PermissionMapper;
import com.mszlu.blog.admin.model.params.PageParam;
import com.mszlu.blog.admin.pojo.Permission;
import com.mszlu.blog.admin.vo.PageResult;
import com.mszlu.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 PermissionService {

    @Autowired
    private PermissionMapper permissionMapper;

    public Result listPermission(PageParam pageParam){
        Page<Permission> page = new Page<>(pageParam.getCurrentPage(),pageParam.getPageSize());
        LambdaQueryWrapper<Permission> queryWrapper = new LambdaQueryWrapper<>();
        if (StringUtils.isNotBlank(pageParam.getQueryString())) {
            queryWrapper.eq(Permission::getName,pageParam.getQueryString());
        }
        Page<Permission> permissionPage = this.permissionMapper.selectPage(page, queryWrapper);
        PageResult<Permission> pageResult = new PageResult<>();
        pageResult.setList(permissionPage.getRecords());
        pageResult.setTotal(permissionPage.getTotal());
        return Result.success(pageResult);
    }

    public Result add(Permission permission) {
        this.permissionMapper.insert(permission);
        return Result.success(null);
    }

    public Result update(Permission permission) {
        this.permissionMapper.updateById(permission);
        return Result.success(null);
    }

    public Result delete(Long id) {
        this.permissionMapper.deleteById(id);
        return Result.success(null);
    }
}



package com.mszlu.blog.admin.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mszlu.blog.admin.pojo.Permission;

import java.util.List;

public interface PermissionMapper extends BaseMapper<Permission> {

    
}



package com.mszlu.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);
    }
}



package com.mszlu.blog.admin.vo;

import lombok.Data;

import java.util.List;

@Data
public class PageResult<T> {

    private List<T> list;

    private Long total;
}

12.2.3 testing

12.3. Security integration

12.3.1 add dependency

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

12.3.2 configuration

package com.mszlu.blog.admin.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

    public static void main(String[] args) {
        //Encryption policy MD5 unsafe rainbow table MD5 salt
        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() //Turn on login authentication
//                .antMatchers("/user/findAll").hasRole("admin") / / the provider needs the role of admin
                .antMatchers("/css/**").permitAll()
                .antMatchers("/img/**").permitAll()
                .antMatchers("/js/**").permitAll()
                .antMatchers("/plugins/**").permitAll()
                .antMatchers("/admin/**").access("@authService.auth(request,authentication)") //Customize the service to realize real-time authority authentication
                .antMatchers("/pages/**").authenticated()
                .and().formLogin()
                .loginPage("/login.html") //Custom login page
                .loginProcessingUrl("/login") //Login processing interface
                .usernameParameter("username") //The key that defines the user name during login is username by default
                .passwordParameter("password") //Define the password key when logging in. The default is password
                .defaultSuccessUrl("/pages/main.html")
                .failureUrl("/login.html")
                .permitAll() //By not intercepting, it is determined by the previously configured path, which means that the interfaces related to the login form pass through
                .and().logout() //Exit login configuration
                .logoutUrl("/logout") //Exit login interface
                .logoutSuccessUrl("/login.html")
                .permitAll() //Exit login interface release
                .and()
                .httpBasic()
                .and()
                .csrf().disable() //csrf off if custom login needs to be closed
                .headers().frameOptions().sameOrigin();
    }
}

12.3.3 login authentication

package com.mszlu.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;
}



package com.mszlu.blog.admin.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mszlu.blog.admin.mapper.AdminMapper;
import com.mszlu.blog.admin.pojo.Admin;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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
@Slf4j
public class SecurityUserService implements UserDetailsService {
    @Autowired
    private AdminService adminService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("username:{}",username);
        //When the user logs in, spring security forwards the request to this
        //Find the User according to the User name. There is no exception thrown. Assemble the User name, password and authorization list into the User object of springSecurity and return it
        Admin adminUser = adminService.findAdminByUserName(username);
        if (adminUser == null){
            throw new UsernameNotFoundException("user name does not exist");
        }
        ArrayList<GrantedAuthority> authorities = new ArrayList<>();
        UserDetails userDetails = new User(username,adminUser.getPassword(), authorities);
        //The rest of the authentication is done by the framework
        return userDetails;
    }

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



package com.mszlu.blog.admin.service;

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

import java.util.List;

@Service
public class AdminService {

    @Autowired
    private AdminMapper adminMapper;
    @Autowired
    private PermissionMapper permissionMapper;

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

    public List<Permission> findPermissionsByAdminId(Long adminId){
        return permissionMapper.findPermissionsByAdminId(adminId);
    }

}



package com.mszlu.blog.admin.mapper;

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

public interface AdminMapper extends BaseMapper<Admin> {
}



package com.mszlu.blog.admin.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mszlu.blog.admin.pojo.Permission;

import java.util.List;

public interface PermissionMapper extends BaseMapper<Permission> {

    List<Permission> findPermissionsByAdminId(Long adminId);
}

12.3.4 authority authentication

package com.mszlu.blog.admin.service;

import com.mszlu.blog.admin.mapper.AdminMapper;
import com.mszlu.blog.admin.pojo.Admin;
import com.mszlu.blog.admin.pojo.Permission;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

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

@Service
@Slf4j
public class AuthService {

    @Autowired
    private AdminService adminService;

    public boolean auth(HttpServletRequest request, Authentication authentication){
  //Authority authentication, request path
        String requestURI = request.getRequestURI();
        log.info("request url:{}", requestURI);
        //true for release and false for interception
        Object principal = authentication.getPrincipal();
        if (principal == null || "anonymousUser".equals(principal)){
            //Not logged in
            return false;
        }
        UserDetails userDetails = (UserDetails) principal;
        String username = userDetails.getUsername();
        Admin admin = adminService.findAdminByUserName(username);
        if (admin == null){
            return false;
        }
        if (admin.getId() == 1){
            //Think it's a super administrator
            return true;
        }
        List<Permission> permissions = adminService.findPermissionsByAdminId(admin.getId());
        requestURI = StringUtils.split(requestURI,'?')[0];
        for (Permission permission : permissions) {
            if (requestURI.equals(permission.getPath())){
                log.info("Permission passed");
                return true;
            }
        }
        return false;
    }
}



package com.mszlu.blog.admin.service;

import org.springframework.security.core.GrantedAuthority;

public class MySimpleGrantedAuthority implements GrantedAuthority {
    private String authority;
    private String path;

    public MySimpleGrantedAuthority(){}

    public MySimpleGrantedAuthority(String authority){
        this.authority = authority;
    }

    public MySimpleGrantedAuthority(String authority,String path){
        this.authority = authority;
        this.path = path;
    }

    @Override
    public String getAuthority() {
        return authority;
    }

    public String getPath() {
        return path;
    }
}



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

<mapper namespace="com.mszlu.blog.admin.mapper.PermissionMapper">

    <select id="findPermissionsByAdminId" parameterType="long" resultType="com.mszlu.blog.admin.pojo.Permission">
        select * from ms_permission where id in (select permission_id from ms_admin_permission where admin_id=#{adminId})
    </select>
</mapper>

12.4. task

Add roles, users have multiple roles, and one role has multiple permissions

13. Summarize technical highlights

1,jwt + redis

Token token login mode, fast access authentication, session sharing and security

redis manages tokens and user information accordingly,
1. Further increase the security
2. The logged in user has made a cache
3. Flexibly control the expiration of users (renewal, kicking off the line, etc.)

2. threadLocal is used to save user information. Within the requested thread, the logged in user can be obtained at any time, and thread isolation is done

3. After using ThreadLocal, the value is deleted to prevent memory leakage (strong reference and weak reference are said in the interview. It's not obvious to let the interviewers share the JVM)

4. Thread safety - update table set value = newValue where id=1 and value=oldValue

5. Thread pool is widely used, with 7 core parameters (operations that have no impact on the current main business process are put into the thread pool for execution)

1. Log in and record the log

6. Key contents of authority system

7. Unified logging and cache processing

14. Front end

Find home first Vue, usually put the home page here
General folder of views
The components folder generally stores vue customized components
Generally, views uses various components

The router folder stores routes and jumps to different pages through different paths
store is generally used for storage
The utils folder is generally a tool class
Request is usually a request
api is the definition of some interfaces with the backend
Static pages generated after packaging dist folder

First look
Dev.env.config in config directory JS configuration backend access path
Deploy production environment

Look at the static directory. category is the image path

This is configured in the database

Look at the src directory again

api represents the definition of back-end interface access, including all back-end access interfaces

In the api folder JS as an example

import request from '@/request'


export function getArticles(query, page) {
  return request({
    url: '/articles',//Access path
    method: 'post',//Access method post
    //Transfer parameters
    data: {
      page: page.pageNumber,
      pageSize: page.pageSize,
      name: page.name,
      sort: page.sort,
      year: query.year,
      month: query.month,
      tagId: query.tagId,
      categoryId: query.categoryId
    }
  })
}

export function getHotArtices() {
  return request({
    url: '/articles/hot',//The name of the interface path can also be changed at will
    method: 'post'//If you want to change the access method to get, you can modify it directly
  })
}

export function getNewArtices() {
  return request({
    url: '/articles/new',
    method: 'post'
  })
}

export function viewArticle(id) {
  return request({
    url: `/articles/view/${id}`,
    method: 'post'
  })
}

export function getArticlesByCategory(id) {
  return request({
    url: `/articles/category/${id}`,
    method: 'post'
  })
}

export function getArticlesByTag(id) {
  return request({
    url: `/articles/tag/${id}`,
    method: 'post'
  })
}


export function publishArticle(article,token) {
  return request({
    headers: {'Authorization': token},
    url: '/articles/publish',
    method: 'post',
    data: article
  })
}

export function listArchives() {
  return request({
    url: '/articles/listArchives',
    method: 'post'
  })
}

export function getArticleById(id) {
  return request({
    url: `/articles/${id}`,
    method: 'post'
  })
}

In login JS file

import request from '@/request'

export function login(account, password) {
  const data = {
    account,
    password
  }
  return request({
    url: '/login',
    method: 'post',
    data
  })
}

export function logout(token) {
  return request({
    headers: {'Authorization': token},//Get the token through headers on the back end
    url: '/logout',
    method: 'get'
  })
}

export function getUserInfo(token) {
  return request({
    headers: {'Authorization': token},
    url: '/users/currentUser',
    method: 'get'
  })
}

export function register(account, nickname, password) {
  const data = {
    account,
    nickname,
    password
  }
  return request({
    url: '/register',
    method: 'post',
    data
  })
}

At home Vue folder

<template>
  <div id="home">
    <el-container>
    	
    	<base-header :activeIndex="activeIndex"></base-header>//head
		  
		  <router-view class="me-container"/>//container
		  
			<base-footer v-show="footerShow"></base-footer>//tail
		  
		</el-container>
		
  </div>
  
</template>

<script>
//Components corresponds to the components directory, and views corresponds to the views directory
import BaseFooter from '@/components/BaseFooter'
import BaseHeader from '@/views/BaseHeader'

export default {
  name: 'Home',
  data (){
  	return {
  			activeIndex: '/',
  			footerShow:true
  	}
  },
  components:{
  	'base-header':BaseHeader,
  	'base-footer':BaseFooter
  },
  beforeRouteEnter (to, from, next){
  	 next(vm => {
    	vm.activeIndex = to.path
  	})
  },
  beforeRouteUpdate (to, from, next) {
	  if(to.path == '/'){
	  	this.footerShow = true
	  }else{
	  	this.footerShow = false
	  }
	  this.activeIndex = to.path
	  next()
	}
}
</script>

<style>

.me-container{
  margin: 100px auto 140px;
}
</style>

Srccomponentsbasefooter. In the components folder Vue folder

<template>
  <el-footer class="me-area">
    <div class="me-footer">
      <p>Designed by
        <strong>
          <router-link to="/" class="me-login-design-color">Code God's Road</router-link>
        </strong>
      </p>
    </div>
  </el-footer>

</template>

<script>

  export default {
    name: 'BaseFooter',
    data() {
      return {}
    },
    methods: {},
    mounted() {
    }
  }
</script>

<style>

  .el-footer {
    min-width: 100%;
    box-shadow: 0 -2px 3px hsla(0, 0%, 7%, .1), 0 0 0 1px hsla(0, 0%, 7%, .1);
    position: absolute;
    bottom: 0;
    left: 0;
    z-index: 1024;
  }

  .me-footer {
    text-align: center;
    line-height: 60px;
    font-family: 'Open Sans', sans-serif;
    font-size: 18px;

  }

  .me-login-design-color {
    color: #5FB878 !important;
  }

</style>

Corresponding to the bottom of the picture
srciewsBaseHeader.vue file header

<template>
  <el-header class="me-area">
    <el-row class="me-header">

      <el-col :span="4" class="me-header-left">
        <router-link to="/" class="me-title">
          <img src="../assets/img/logo.png" />
        </router-link>
      </el-col>

      <el-col v-if="!simple" :span="16">
        <el-menu :router=true menu-trigger="click" active-text-color="#5FB878" :default-active="activeIndex"
                 mode="horizontal">
          <el-menu-item index="/">home page</el-menu-item>
          <el-menu-item index="/category/all">Article classification</el-menu-item>
          <el-menu-item index="/tag/all">label</el-menu-item>
          <el-menu-item index="/archives">Article archiving</el-menu-item>

          <el-col :span="4" :offset="4">
            <el-menu-item index="/write"><i class="el-icon-edit"></i>write an article</el-menu-item>
          </el-col>

        </el-menu>
      </el-col>

      <template v-else>
        <slot></slot>
      </template>

      <el-col :span="4">
        <el-menu :router=true menu-trigger="click" mode="horizontal" active-text-color="#5FB878">

          <template v-if="!user.login">
            <el-menu-item index="/login">
              <el-button type="text">Sign in</el-button>
            </el-menu-item>
            <el-menu-item index="/register">
              <el-button type="text">register</el-button>
            </el-menu-item>
          </template>

          <template v-else>
            <el-submenu index>
              <template slot="title">
                <img class="me-header-picture" :src="user.avatar"/>//Avatar acquisition
              </template>
              <el-menu-item index @click="logout"><i class="el-icon-back"></i>sign out</el-menu-item>
            </el-submenu>
          </template>
        </el-menu>
      </el-col>

    </el-row>
  </el-header>
</template>

<script>
  export default {
    name: 'BaseHeader',
    props: {
      activeIndex: String,
      simple: {
        type: Boolean,
        default: false
      }
    },
    data() {
      return {}
    },
    computed: {
      user() {
        let login = this.$store.state.account.length != 0
        let avatar = this.$store.state.avatar
        return {
          login, avatar
        }
      }
    },
    methods: {
      logout() {
        let that = this
        this.$store.dispatch('logout').then(() => {
          this.$router.push({path: '/'})
        }).catch((error) => {
          if (error !== 'error') {
            that.$message({message: error, type: 'error', showClose: true});
          }
        })
      }
    }
  }
</script>

<style>

  .el-header {
    position: fixed;
    z-index: 1024;
    min-width: 100%;
    box-shadow: 0 2px 3px hsla(0, 0%, 7%, .1), 0 0 0 1px hsla(0, 0%, 7%, .1);
  }

  .me-title {
    margin-top: 10px;
    font-size: 24px;
  }

  .me-header-left {
    margin-top: 10px;
  }

  .me-title img {
    max-height: 2.4rem;
    max-width: 100%;
  }

  .me-header-picture {
    width: 36px;
    height: 36px;
    border: 1px solid #ddd;
    border-radius: 50%;
    vertical-align: middle;
    background-color: #5fb878;
  }
</style>

Corresponding to the top of the picture

BaseHeader. logout in Vue essentially calls index. In the store folder JS file

There are a lot of materials at the front end of online learning, but if the knowledge learned is not systematic, only a superficial taste when encountering problems and no further research, it is difficult to achieve real technological improvement. hope This systematic technical system We have a direction reference.

Topics: Javascript Front-end Interview