Sangencaotang blog system (complete notes + front-end and back-end system code implementation)

Three-update blog front-end and back-end separation system

Front-end and back-end separation blog system

1. Technology stack

SpringBoot,MybatisPlus,SpringSecurity,EasyExcel,Swagger2,Redis,Echarts,Vue

2.Create project

There are two systems, the front-end and the back-end. The code shared by the two systems is written into a common module, so that the front-end system and the back-end system respectively rely on the common module.
SGBlog project

  • sangeng-framework (public module)
  • sangeng-blog (front-end system)
  • sangeng-admin (backend system)

① Create parent module
pom.xml

<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/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.sangeng</groupId>
  <artifactId>SGBlog</artifactId>
  <packaging>pom</packaging>
  <version>1.0-SNAPSHOT</version>

<!--  父工程 自动把 三个子模块 聚合-->
  <modules>
    <module>sangeng-framework</module>
    <module>sangeng-admin</module>
    <module>sangeng-blog</module>
  </modules>

  <!--  父模块 jdk1.8 编码方式utf-8-->
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
  </properties>

<!--子模块的版本依赖控制(并不是真正导入依赖,如果子模块引入了依赖,自动变成下面指定依赖的版本)-->
  <dependencyManagement>
    <dependencies>
      <!-- SpringBoot的依赖配置-->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.5.0</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <!--fastjson依赖-->
      <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.33</version>
      </dependency>
      <!--jwt依赖-->
      <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
      </dependency>
      <!--mybatisPlus依赖-->
      <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.3</version>
      </dependency>

      <!--阿里云OSS-->
      <dependency>
        <groupId>com.aliyun.oss</groupId>
        <artifactId>aliyun-sdk-oss</artifactId>
        <version>3.10.2</version>
      </dependency>


      <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>easyexcel</artifactId>
        <version>3.0.5</version>
      </dependency>

      <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
      </dependency>
      <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
      </dependency>
    </dependencies>
  </dependencyManagement>


  <build>
    <!--配置jdk版本-->
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.1</version>
        <configuration>
<!--          读取的是,上面<properties><java.version>1.8</java.version></properties>-->
          <source>${java.version}</source>
          <target>${java.version}</target>
          <encoding>${project.build.sourceEncoding}</encoding>
        </configuration>
      </plugin>
    </plugins>

  </build>

</project>

②Create the public submodule sangeng-framework
pom.xml

<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/maven-v4_0_0.xsd">
    <parent>
        <artifactId>SGBlog</artifactId>
        <groupId>com.sangeng</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>sangeng-framework</artifactId>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--lombk-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--junit-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
<!--            <scope>test</scope>-->
        </dependency>
        <!--SpringSecurity启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--fastjson依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <!--jwt依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>
        <!--mybatisPlus依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
<!--        mybatisplus 联合主键需要的依赖-->
        <dependency>
            <groupId>com.github.jeffreyning</groupId>
            <artifactId>mybatisplus-plus</artifactId>
            <version>1.5.1-RELEASE</version>
        </dependency>

        <!--mysql数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--阿里云OSS-->
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
        </dependency>

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

<!--        easyexcel 依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
        </dependency>

<!--        swagger2 依赖-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
        </dependency>

    <!--引入七牛云 SDK(一系列jar包) 依赖-->
        <dependency>
            <groupId>com.qiniu</groupId>
            <artifactId>qiniu-java-sdk</artifactId>
            <version>[7.7.0, 7.10.99]</version>
        </dependency>

<!--        自动配置注解处理器,解决 使用到ConfigurationProperties注解时无法识别-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

<!--        swagger依赖 和 swagger前端UI依赖-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

    </dependencies>

    <build>
        <finalName>sangeng-framework</finalName>
    </build>
</project>

③Create blog front-end module sangeng-blog
pom.xml

<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/maven-v4_0_0.xsd">
    <parent>
        <artifactId>SGBlog</artifactId>
        <groupId>com.sangeng</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>sangeng-blog</artifactId>

    <dependencies>
<!--        依赖公共模块 sangeng-framework-->
        <dependency>
            <groupId>com.sangeng</groupId>
            <artifactId>sangeng-framework</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

④Create blog backend module sangeng-admin

<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/maven-v4_0_0.xsd">
    <parent>
        <artifactId>SGBlog</artifactId>
        <groupId>com.sangeng</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>sangeng-admin</artifactId>

    <dependencies>
        <!--        依赖公共模块 sangeng-framework-->
        <dependency>
            <groupId>com.sangeng</groupId>
            <artifactId>sangeng-framework</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

3. Blog front desk

3.0 Preparation

3.1 SpringBoot and MybatisPuls integration configuration test

① Create startup class
Insert image description here
② Create application.yml configuration file
Insert image description here

server:
  port: 7777
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/sg_blog?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 文件上传 设置文件大小, 即最大的尺寸
  servlet:
    multipart:
      max-file-size: 2MB
      max-request-size: 5MB

mybatis-plus:
  configuration:
    # mybatis-plus 日志信息
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      logic-delete-field: delFlag  #逻辑删除字段  (数据库中逻辑删除字段用delFlag)
      logic-delete-value: 1       # 删除值 1
      logic-not-delete-value: 0   # 未删除值 0
      id-type: auto             #主键自增(不设置用 mybatis-plus默认生成的id)
#  当用maven导入了mybatisplus的包时,必须要在yml配置中配置数据库的参数,否则运行boot启动类会报错

#自定义格式, key: value    :后面必须要有空格
oss:
  accessKey: *******************************
  secretKey: *******************************
  bucket: pk-sg-blog

③ Database
Insert image description here
The two related tables user_role and role_menu both have two joint primary keys, and no primary key auto-increment is set.
Insert image description here
Each of the other tables has a primary key id, which is auto-incrementing.
For example, in the article table:
Insert image description here
④ Create Entity, Mapper, and Service and place them under the public module framework, and
place the controller in blog and admin.

  • Use EasyCode to quickly generate
    Insert image description hereInsert image description hereInsert image description here
    the entity, mapper, and service structures of mybatisplus as follows:
    entity:
@SuppressWarnings("serial")
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sg_article")
public class Article  {
    
    
    @TableId
    private Long id;
    //标题
    private String title;
    //删除标志(0代表未删除,1代表已删除)
    private Integer delFlag;
}

mapper:

public interface ArticleMapper extends BaseMapper<Article> {
    
    
}

service:

public interface ArticleService extends IService<Article> {
    
    
}

impl

@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
    
    
}

⑤ Create Controller and write to blog,admin
Insert image description here

@RestController
@RequestMapping("/article")
public class ArticleController {
    
    

    @Autowired
    private ArticleService articleService;

   @GetMapping("/hotArticleList")
    public ResponseResult hotArticleList(){
    
    
       return  articleService.hotArticleList();
    }
}

3.1 List of popular articles

3.1.0 Article table analysis

​ Analyze what fields are needed through requirements.

3.1.1 Requirements

​ Need to query the information of the top 10 articles with the highest number of views. It is required to display the article title and page views. Allow users to click to jump to specific article details for browsing.

Note: Drafts cannot be displayed, and deleted articles cannot be queried. Sort by page views in descending order.

3.1.2 Interface design

See interface documentation

3.1.3 Basic version code implementation

①Preparation work

Unify response classes and response enums

package com.sangeng.domain;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.sangeng.enums.AppHttpCodeEnum;

import java.io.Serializable;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> implements Serializable {
    
    
    private Integer code;
    private String msg;
    private T data;

    public ResponseResult() {
    
    
        this.code = AppHttpCodeEnum.SUCCESS.getCode();
        this.msg = AppHttpCodeEnum.SUCCESS.getMsg();
    }

    public ResponseResult(Integer code, T data) {
    
    
        this.code = code;
        this.data = data;
    }

    public ResponseResult(Integer code, String msg, T data) {
    
    
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public ResponseResult(Integer code, String msg) {
    
    
        this.code = code;
        this.msg = msg;
    }

    public static ResponseResult errorResult(int code, String msg) {
    
    
        ResponseResult result = new ResponseResult();
        return result.error(code, msg);
    }
    public static ResponseResult okResult() {
    
    
        ResponseResult result = new ResponseResult();
        return result;
    }
    public static ResponseResult okResult(int code, String msg) {
    
    
        ResponseResult result = new ResponseResult();
        return result.ok(code, null, msg);
    }

    public static ResponseResult okResult(Object data) {
    
    
        ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getMsg());
        if(data!=null) {
    
    
            result.setData(data);
        }
        return result;
    }

    public static ResponseResult errorResult(AppHttpCodeEnum enums){
    
    
        return setAppHttpCodeEnum(enums,enums.getMsg());
    }

    public static ResponseResult errorResult(AppHttpCodeEnum enums, String msg){
    
    
        return setAppHttpCodeEnum(enums,msg);
    }

    public static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums){
    
    
        return okResult(enums.getCode(),enums.getMsg());
    }

    private static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums, String msg){
    
    
        return okResult(enums.getCode(),msg);
    }

    public ResponseResult<?> error(Integer code, String msg) {
    
    
        this.code = code;
        this.msg = msg;
        return this;
    }

    public ResponseResult<?> ok(Integer code, T data) {
    
    
        this.code = code;
        this.data = data;
        return this;
    }

    public ResponseResult<?> ok(Integer code, T data, String msg) {
    
    
        this.code = code;
        this.data = data;
        this.msg = msg;
        return this;
    }

    public ResponseResult<?> ok(T data) {
    
    
        this.data = data;
        return this;
    }

    public Integer getCode() {
    
    
        return code;
    }

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

    public String getMsg() {
    
    
        return msg;
    }

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

    public T getData() {
    
    
        return data;
    }

    public void setData(T data) {
    
    
        this.data = data;
    }



}
package com.sangeng.enums;

public enum AppHttpCodeEnum {
    
    
    // 成功
    SUCCESS(200,"操作成功"),
    // 登录
    NEED_LOGIN(401,"需要登录后操作"),
    NO_OPERATOR_AUTH(403,"无权限操作"),
    SYSTEM_ERROR(500,"出现错误"),
    USERNAME_EXIST(501,"用户名已存在"),
     PHONENUMBER_EXIST(502,"手机号已存在"), EMAIL_EXIST(503, "邮箱已存在"),
    REQUIRE_USERNAME(504, "必需填写用户名"),
    LOGIN_ERROR(505,"用户名或密码错误");
    int code;
    String msg;

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

    public int getCode() {
    
    
        return code;
    }

    public String getMsg() {
    
    
        return msg;
    }
}

② Code implementation

@RestController
@RequestMapping("/article")
public class ArticleController {
    
    

    @Autowired
    private ArticleService articleService;
    
    @GetMapping("/hotArticleList")
    public ResponseResult hotArticleList(){
    
    

        ResponseResult result =  articleService.hotArticleList();
        return result;
    }
}

public interface ArticleService extends IService<Article> {
    
    
    ResponseResult hotArticleList();
}

@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
    
    

    @Override
    public ResponseResult hotArticleList() {
    
    
        //查询热门文章 封装成ResponseResult返回
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
        //必须是正式文章
        queryWrapper.eq(Article::getStatus,0);
        //按照浏览量进行排序
        queryWrapper.orderByDesc(Article::getViewCount);
        //最多只查询10条
        Page<Article> page = new Page(1,10);
        page(page,queryWrapper);

        List<Article> articles = page.getRecords();
        return ResponseResult.okResult(articles);
    }
}

③ Solve cross-domain problems

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    

    @Override
    public void addCorsMappings(CorsRegistry registry) {
    
    
      // 设置允许跨域的路径
        registry.addMapping("/**")
                // 设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                // 是否允许cookie
                .allowCredentials(true)
                // 设置允许的请求方式
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);
    }

}
3.1.4 Use VO optimization

​ At present, our response format does not meet the standards of the interface document, and many extra fields are returned. This is because the results we query are encapsulated by Article, which has many fields.

In our projects, we usually use VO to receive the query results at the end. One interface corresponds to one VO, so even if the interface response field needs to be modified, you only need to change the VO.

@Data
@NoArgsConstructor
@AllArgsConstructor
public class HotArticleVo {
    
    
    private Long id;
    //标题
    private String title;

    //访问量
    private Long viewCount;
}

@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
    
    

    @Override
    public ResponseResult hotArticleList() {
    
    
        //查询热门文章 封装成ResponseResult返回
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
        //必须是正式文章
        queryWrapper.eq(Article::getStatus,0);
        //按照浏览量进行排序
        queryWrapper.orderByDesc(Article::getViewCount);
        //最多只查询10条
        Page<Article> page = new Page(1,10);
        page(page,queryWrapper);

        List<Article> articles = page.getRecords();
        //bean拷贝
        List<HotArticleVo> articleVos = new ArrayList<>();
        for (Article article : articles) {
    
    
            HotArticleVo vo = new HotArticleVo();
            BeanUtils.copyProperties(article,vo);
            articleVos.add(vo);
        }

        return ResponseResult.okResult(articleVos);
    }
}
3.1.5 Literal value processing

​ In actual projects, literal values ​​are not allowed to be used directly in code. All need to be defined as constants to use. This approach helps improve the maintainability of the code.

public class SystemConstants
{
    
    
    /**
     *  文章是草稿
     */
    public static final int ARTICLE_STATUS_DRAFT = 1;
    /**
     *  文章是正常分布状态
     */
    public static final int ARTICLE_STATUS_NORMAL = 0;
    
}
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
    
    

    @Override
    public ResponseResult hotArticleList() {
    
    
        //查询热门文章 封装成ResponseResult返回
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
        //必须是正式文章
        queryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);
        //按照浏览量进行排序
        queryWrapper.orderByDesc(Article::getViewCount);
        //最多只查询10条
        Page<Article> page = new Page(1,10);
        page(page,queryWrapper);

        List<Article> articles = page.getRecords();
        //bean拷贝
        List<HotArticleVo> articleVos = new ArrayList<>();
        for (Article article : articles) {
    
    
            HotArticleVo vo = new HotArticleVo();
            BeanUtils.copyProperties(article,vo);
            articleVos.add(vo);
        }

        return ResponseResult.okResult(articleVos);
    }
}

3.2 Bean copy tool class encapsulation

public class BeanCopyUtils {
    
    

    private BeanCopyUtils() {
    
    
    }

    public static <V> V copyBean(Object source,Class<V> clazz) {
    
    
        //创建目标对象
        V result = null;
        try {
    
    
            result = clazz.newInstance();
            //实现属性copy
            BeanUtils.copyProperties(source, result);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        //返回结果
        return result;
    }
    public static <O,V> List<V> copyBeanList(List<O> list,Class<V> clazz){
    
    
        return list.stream()
                .map(o -> copyBean(o, clazz))
                .collect(Collectors.toList());
    }
}

3.2 Query the classification list

3.2.1 Requirements

A category list needs to be displayed on the page. Users can click on a specific category to view the list of articles under that category.

​ Note: ① It is required to only display categories with official articles published ② It must be a normal category

3.2.2 Interface design
3.2.3 EasyCode code template
##导入宏定义
$!{
    
    define.vm}

##保存文件(宏定义)
#save("/entity", ".java")

##包路径(宏定义)
#setPackageSuffix("entity")

##自动导入包(全局变量)
$!{
    
    autoImport.vm}

import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
##表注释(宏定义)
#tableComment("表实体类")
@SuppressWarnings("serial")
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("$!{tableInfo.obj.name}")
public class $!{
    
    tableInfo.name}  {
    
    
#foreach($column in $tableInfo.pkColumn)
    #if(${
    
    column.comment})//${column.comment}#end
@TableId
    private $!{
    
    tool.getClsNameByFullName($column.type)} $!{
    
    column.name};
#end

#foreach($column in $tableInfo.otherColumn)
    #if(${
    
    column.comment})//${column.comment}#end

    private $!{
    
    tool.getClsNameByFullName($column.type)} $!{
    
    column.name};
#end



}

##导入宏定义
$!{
    
    define.vm}

##设置表后缀(宏定义)
#setTableSuffix("Mapper")

##保存文件(宏定义)
#save("/mapper", "Mapper.java")

##包路径(宏定义)
#setPackageSuffix("mapper")

import com.baomidou.mybatisplus.core.mapper.BaseMapper;


##表注释(宏定义)
#tableComment("表数据库访问层")
public interface $!{
    
    tableName} extends BaseMapper<$!tableInfo.name> {
    
    

}

##导入宏定义
$!{
    
    define.vm}

##设置表后缀(宏定义)
#setTableSuffix("Service")

##保存文件(宏定义)
#save("/service", "Service.java")

##包路径(宏定义)
#setPackageSuffix("service")

import com.baomidou.mybatisplus.extension.service.IService;


##表注释(宏定义)
#tableComment("表服务接口")
public interface $!{
    
    tableName} extends IService<$!tableInfo.name> {
    
    

}

##导入宏定义
$!{
    
    define.vm}

##设置表后缀(宏定义)
#setTableSuffix("ServiceImpl")

##保存文件(宏定义)
#save("/service/impl", "ServiceImpl.java")

##包路径(宏定义)
#setPackageSuffix("service.impl")

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

##表注释(宏定义)
#tableComment("表服务实现类")
@Service("$!tool.firstLowerCase($tableInfo.name)Service")
public class $!{
    
    tableName} extends ServiceImpl<$!{
    
    tableInfo.name}Mapper, $!{
    
    tableInfo.name}> implements $!{
    
    tableInfo.name}Service {
    
    

}

3.2.4 Code implementation
@RestController
@RequestMapping("/category")
public class CategoryController {
    
    

    @Autowired
    private CategoryService categoryService;

    @GetMapping("/getCategoryList")
    public ResponseResult getCategoryList(){
    
    
       return categoryService.getCategoryList();
    }
}

    
public interface CategoryService extends IService<Category> {
    
    


    ResponseResult getCategoryList();

}
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
    
    

    @Autowired
    private ArticleService articleService;

    @Override
    public ResponseResult getCategoryList() {
    
    
        //查询文章表  状态为已发布的文章
        LambdaQueryWrapper<Article> articleWrapper = new LambdaQueryWrapper<>();
        articleWrapper.eq(Article::getStatus,SystemConstants.ARTICLE_STATUS_NORMAL);
        List<Article> articleList = articleService.list(articleWrapper);
        //获取文章的分类id,并且去重
        Set<Long> categoryIds = articleList.stream()
                .map(article -> article.getCategoryId())
                .collect(Collectors.toSet());

        //查询分类表
        List<Category> categories = listByIds(categoryIds);
        categories = categories.stream().
                filter(category -> SystemConstants.STATUS_NORMAL.equals(category.getStatus()))
                .collect(Collectors.toList());
        //封装vo
        List<CategoryVo> categoryVos = BeanCopyUtils.copyBeanList(categories, CategoryVo.class);

        return ResponseResult.okResult(categoryVos);
    }
}

3.3 Query the article list by pagination

3.3.1 Requirements

​ You need to query the article list on both the homepage and category pages.

​Homepage: Query all articles

​Category page: Query articles under the corresponding category

Requirements: ① Only officially published articles can be queried ② The pinned articles must be displayed at the front

3.3.2 Interface design

See documentation

3.3.3 Code implementation

In ArticleController

    @GetMapping("/articleList")
    public ResponseResult articleList(Integer pageNum,Integer pageSize,Long categoryId){
    
    
        return articleService.articleList(pageNum,pageSize,categoryId);
    }

In ArticleService

ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId);

In ArticleServiceImpl


@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
    
    

    @Autowired
    private CategoryService categoryService;

    @Override
    public ResponseResult hotArticleList() {
    
    
        //查询热门文章 封装成ResponseResult返回
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
        //必须是正式文章
        queryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);
        //按照浏览量进行排序
        queryWrapper.orderByDesc(Article::getViewCount);
        //最多只查询10条
        Page<Article> page = new Page(1,10);
        page(page,queryWrapper);

        List<Article> articles = page.getRecords();
        //bean拷贝
//        List<HotArticleVo> articleVos = new ArrayList<>();
//        for (Article article : articles) {
    
    
//            HotArticleVo vo = new HotArticleVo();
//            BeanUtils.copyProperties(article,vo);
//            articleVos.add(vo);
//        }
        List<HotArticleVo> vs = BeanCopyUtils.copyBeanList(articles, HotArticleVo.class);
        return ResponseResult.okResult(vs);
    }

    @Override
    public ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId) {
    
    
        //查询条件
        LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        // 如果 有categoryId 就要 查询时要和传入的相同
        lambdaQueryWrapper.eq(Objects.nonNull(categoryId)&&categoryId>0 ,Article::getCategoryId,categoryId);
        // 状态是正式发布的
        lambdaQueryWrapper.eq(Article::getStatus,SystemConstants.ARTICLE_STATUS_NORMAL);
        // 对isTop进行降序
        lambdaQueryWrapper.orderByDesc(Article::getIsTop);

        //分页查询
        Page<Article> page = new Page<>(pageNum,pageSize);
        page(page,lambdaQueryWrapper);

        List<Article> articles = page.getRecords();
        //查询categoryName
        articles.stream()
                .map(article -> article.setCategoryName(categoryService.getById(article.getCategoryId()).getName()))
                .collect(Collectors.toList());
        //articleId去查询articleName进行设置
//        for (Article article : articles) {
    
    
//            Category category = categoryService.getById(article.getCategoryId());
//            article.setCategoryName(category.getName());
//        }

        //封装查询结果
        List<ArticleListVo> articleListVos = BeanCopyUtils.copyBeanList(page.getRecords(), ArticleListVo.class);

        PageVo pageVo = new PageVo(articleListVos,page.getTotal());
        return ResponseResult.okResult(pageVo);
    }
}

PageVo

@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageVo {
    
    
    private List rows;
    private Long total;
}

ArticleListVo

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ArticleListVo {
    
    

    private Long id;
    //标题
    private String title;
    //文章摘要
    private String summary;
    //所属分类名
    private String categoryName;
    //缩略图
    private String thumbnail;


    //访问量
    private Long viewCount;

    private Date createTime;


}

Add a field to Article

    @TableField(exist = false)
    private String categoryName;
3.3.4 FastJson configuration
    @Bean//使用@Bean注入fastJsonHttpMessageConvert
    public HttpMessageConverter fastJsonHttpMessageConverters() {
    
    
        //1.需要定义一个Convert转换消息的对象
        FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
        fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
		
        SerializeConfig.globalInstance.put(Long.class, ToStringSerializer.instance);

        fastJsonConfig.setSerializeConfig(SerializeConfig.globalInstance);
        fastConverter.setFastJsonConfig(fastJsonConfig);
        HttpMessageConverter<?> converter = fastConverter;
        return converter;
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    
    
        converters.add(fastJsonHttpMessageConverters());
    }

3.4 Article details interface

3.4.1 Requirements

​ It is required to jump to the article details page when clicking to read the full text in the article list, allowing users to read the text of the article.

Requirements: ① Display the category name in the article details

3.4.2 Interface design
Request method Request path
Get /article/{id}

Response format:

{
    
    
  "code": 200,
  "data": {
    
    
    "categoryId": "1",
    "categoryName": "java",
    "content": "内容",
    "createTime": "2022-01-23 23:20:11",
    "id": "1",
    "isComment": "0",
    "title": "SpringSecurity从入门到精通",
    "viewCount": "114"
  },
  "msg": "操作成功"
}
3.4.3 Code implementation

New in ArticleController

    @GetMapping("/{id}")
    public ResponseResult getArticleDetail(@PathVariable("id") Long id){
    
    
        return articleService.getArticleDetail(id);
    }

Service

ResponseResult getArticleDetail(Long id);

ServiceImpl

    @Override
    public ResponseResult getArticleDetail(Long id) {
    
    
        //根据id查询文章
        Article article = getById(id);
        //转换成VO
        ArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class);
        //根据分类id查询分类名
        Long categoryId = articleDetailVo.getCategoryId();
        Category category = categoryService.getById(categoryId);
        if(category!=null){
    
    
            articleDetailVo.setCategoryName(category.getName());
        }
        //封装响应返回
        return ResponseResult.okResult(articleDetailVo);
    }

3.5 Youlian query

3.5.1 Requirements

​ On the Friend Links page, you need to query all the friend links that have passed the review.

3.5.2 Interface design
Request method Request path
Get /link/getAllLink

Response format:

{
    
    
  "code": 200,
  "data": [
    {
    
    
      "address": "https://www.baidu.com",
      "description": "sda",
      "id": "1",
      "logo": "图片url1",
      "name": "sda"
    },
    {
    
    
      "address": "https://www.qq.com",
      "description": "dada",
      "id": "2",
      "logo": "图片url2",
      "name": "sda"
    }
  ],
  "msg": "操作成功"
}
3.5.3 Code implementation

Controller

@RestController
@RequestMapping("/link")
public class LinkController {
    
    

    @Autowired
    private LinkService linkService;

    @GetMapping("/getAllLink")
    public ResponseResult getAllLink(){
    
    
        return linkService.getAllLink();
    }
}

Service

public interface LinkService extends IService<Link> {
    
    

    ResponseResult getAllLink();
}


ServiceImpl

@Service("linkService")
public class LinkServiceImpl extends ServiceImpl<LinkMapper, Link> implements LinkService {
    
    

    @Override
    public ResponseResult getAllLink() {
    
    
        //查询所有审核通过的
        LambdaQueryWrapper<Link> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Link::getStatus, SystemConstants.LINK_STATUS_NORMAL);
        List<Link> links = list(queryWrapper);
        //转换成vo
        List<LinkVo> linkVos = BeanCopyUtils.copyBeanList(links, LinkVo.class);
        //封装返回
        return ResponseResult.okResult(linkVos);
    }
}

SystemConstants

    /**
     * 友链状态为审核通过
     */
    public static final String  LINK_STATUS_NORMAL = "0";

3.6 Login function implementation

​ Our front-end and back-end authentication and authorization are unified using the SpringSecurity security framework.

3.6.0 Requirements

​Need to implement login function

​ Some functions must be logged in before they can be used, and cannot be used if you are not logged in.

3.6.1 Interface design
Request method Request path
POST /login

Request body:

{
    
    
    "userName":"sg",
    "password":"1234"
}

Response format:

{
    
    
    "code": 200,
    "data": {
    
    
        "token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0ODBmOThmYmJkNmI0NjM0OWUyZjY2NTM0NGNjZWY2NSIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTY0Mzg3NDMxNiwiZXhwIjoxNjQzOTYwNzE2fQ.ldLBUvNIxQCGemkCoMgT_0YsjsWndTg5tqfJb77pabk",
        "userInfo": {
    
    
            "avatar": "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fi0.hdslb.com%2Fbfs%2Farticle%2F3bf9c263bc0f2ac5c3a7feb9e218d07475573ec8.gi",
            "email": "[email protected]",
            "id": 1,
            "nickName": "sg333",
            "sex": "1"
        }
    },
    "msg": "操作成功"
}
3.6.2 Idea analysis

Log in

①Customized login interface

​ Call the ProviderManager method for authentication. If the authentication passes, jwt is generated.

​ Store user information in redis

②Customize UserDetailsService

​ Query the database in this implementation class

​ Pay attention to configuring passwordEncoder as BCryptPasswordEncoder

check:

①Define Jwt authentication filter

​ Get token

​ Parse the token to get the userid in it

​ Get user information from redis

​Save to SecurityContextHolder

3.6.4 Preparation

①Add dependencies

Pay attention to the comment about releasing the Security dependency

        <!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--fastjson依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.33</version>
        </dependency>
        <!--jwt依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
3.6.5 Login interface code implementation
BlogLoginController
@RestController
public class BlogLoginController {
    
    
    @Autowired
    private BlogLoginService blogLoginService;

    @PostMapping("/login")
    public ResponseResult login(@RequestBody User user){
    
    
        return blogLoginService.login(user);
    }
}

BlogLoginService
public interface BlogLoginService {
    
    
    ResponseResult login(User user);
}

SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Bean
    public PasswordEncoder passwordEncoder(){
    
    
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
                // 除上面外的所有请求全部不需要认证即可访问
                .anyRequest().permitAll();


        http.logout().disable();
        //允许跨域
        http.cors();
    }
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
    
    
        return super.authenticationManagerBean();
    }
}
BlogLoginServiceImpl

@Service
public class BlogLoginServiceImpl implements BlogLoginService {
    
    

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    @Override
    public ResponseResult login(User user) {
    
    
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //判断是否认证通过
        if(Objects.isNull(authenticate)){
    
    
            throw new RuntimeException("用户名或密码错误");
        }
        //获取userid 生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);
        //把用户信息存入redis
        redisCache.setCacheObject("bloglogin:"+userId,loginUser);

        //把token和userinfo封装 返回
        //把User转换成UserInfoVo
        UserInfoVo userInfoVo = BeanCopyUtils.copyBean(loginUser.getUser(), UserInfoVo.class);
        BlogUserLoginVo vo = new BlogUserLoginVo(jwt,userInfoVo);
        return ResponseResult.okResult(vo);
    }
}
UserDetailServiceImpl
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
        //根据用户名查询用户信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(queryWrapper);
        //判断是否查到用户  如果没查到抛出异常
        if(Objects.isNull(user)){
    
    
            throw new RuntimeException("用户不存在");
        }
        //返回用户信息
        // TODO 查询权限信息封装
        return new LoginUser(user);
    }
}
LoginUser
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
    
    

    private User user;


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
        return null;
    }

    @Override
    public String getPassword() {
    
    
        return user.getPassword();
    }

    @Override
    public String getUsername() {
    
    
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
    
    
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
    
    
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
    
    
        return true;
    }

    @Override
    public boolean isEnabled() {
    
    
        return true;
    }
}

BlogUserLoginVo
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BlogUserLoginVo {
    
    

    private String token;
    private UserInfoVo userInfo;
}
UserInfoVo
@Data
@Accessors(chain = true)
public class UserInfoVo {
    
    
    /**
     * 主键
     */
    private Long id;

    /**
     * 昵称
     */
    private String nickName;

    /**
     * 头像
     */
    private String avatar;

    private String sex;

    private String email;


}

3.6.6 Login verification filter code implementation
Ideas

①Define Jwt authentication filter

​ Get token

​ Parse the token to get the userid in it

​ Get user information from redis

​Save to SecurityContextHolder

JwtAuthenticationTokenFilter
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
    

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    
    
        //获取请求头中的token
        String token = request.getHeader("token");
        if(!StringUtils.hasText(token)){
    
    
            //说明该接口不需要登录  直接放行
            filterChain.doFilter(request, response);
            return;
        }
        //解析获取userid
        Claims claims = null;
        try {
    
    
            claims = JwtUtil.parseJWT(token);
        } catch (Exception e) {
    
    
            e.printStackTrace();
            //token超时  token非法
            //响应告诉前端需要重新登录
            ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
            WebUtils.renderString(response, JSON.toJSONString(result));
            return;
        }
        String userId = claims.getSubject();
        //从redis中获取用户信息
        LoginUser loginUser = redisCache.getCacheObject("bloglogin:" + userId);
        //如果获取不到
        if(Objects.isNull(loginUser)){
    
    
            //说明登录过期  提示重新登录
            ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
            WebUtils.renderString(response, JSON.toJSONString(result));
            return;
        }
        //存入SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        filterChain.doFilter(request, response);
    }


}
SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
    
    
        return super.authenticationManagerBean();
    }

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
                //jwt过滤器测试用,如果测试没有问题吧这里删除了
                .antMatchers("/link/getAllLink").authenticated()
                // 除上面外的所有请求全部不需要认证即可访问
                .anyRequest().permitAll();


        http.logout().disable();
        //把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //允许跨域
        http.cors();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
    
    
        return new BCryptPasswordEncoder();
    }
}

3.7 Authentication and authorization failure handling

​ At present, the Json that our project responds to when there is an authentication error or insufficient permissions is the result of Security's exception handling. But the format of this response definitely does not comply with the interface specification of our project. So you need to customize exception handling.

​ AuthenticationEntryPoint authentication failure handler

AccessDeniedHandler authorization failure handler

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    
    

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    
    
        authException.printStackTrace();
        //InsufficientAuthenticationException
        //BadCredentialsException
        ResponseResult result = null;
        if(authException instanceof BadCredentialsException){
    
    
            result = ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_ERROR.getCode(),authException.getMessage());
        }else if(authException instanceof InsufficientAuthenticationException){
    
    
            result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
        }else{
    
    
            result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR.getCode(),"认证或授权失败");
        }
        //响应给前端
        WebUtils.renderString(response, JSON.toJSONString(result));
    }
}

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    
    
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    
    
        accessDeniedException.printStackTrace();
        ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NO_OPERATOR_AUTH);
        //响应给前端
        WebUtils.renderString(response, JSON.toJSONString(result));
    }
}

Configure Security exception handler

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
    
    
        return super.authenticationManagerBean();
    }

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Autowired
    AuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    AccessDeniedHandler accessDeniedHandler;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
                //jwt过滤器测试用,如果测试没有问题吧这里删除了
                .antMatchers("/link/getAllLink").authenticated()
                // 除上面外的所有请求全部不需要认证即可访问
                .anyRequest().permitAll();

        //配置异常处理器
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);

        http.logout().disable();
        //把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //允许跨域
        http.cors();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
    
    
        return new BCryptPasswordEncoder();
    }
}

3.8 Unified exception handling

​In fact, we may need to do a lot of judgment and verification during the development process. If an illegal situation occurs, we expect to respond to the corresponding prompts. But it would be very troublesome if we handle it manually every time. We can choose to throw exceptions directly and then handle them uniformly. Encapsulate the information in the exception into ResponseResult and send it to the front end.

SystemException

/**
 * @Author 三更  B站: https://space.bilibili.com/663528522
 */
public class SystemException extends RuntimeException{
    
    

    private int code;

    private String msg;

    public int getCode() {
    
    
        return code;
    }

    public String getMsg() {
    
    
        return msg;
    }

    public SystemException(AppHttpCodeEnum httpCodeEnum) {
    
    
        super(httpCodeEnum.getMsg());
        this.code = httpCodeEnum.getCode();
        this.msg = httpCodeEnum.getMsg();
    }
    
}

GlobalExceptionHandler

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    

    @ExceptionHandler(SystemException.class)
    public ResponseResult systemExceptionHandler(SystemException e){
    
    
        //打印异常信息
        log.error("出现了异常! {}",e);
        //从异常对象中获取提示信息封装返回
        return ResponseResult.errorResult(e.getCode(),e.getMsg());
    }


    @ExceptionHandler(Exception.class)
    public ResponseResult exceptionHandler(Exception e){
    
    
        //打印异常信息
        log.error("出现了异常! {}",e);
        //从异常对象中获取提示信息封装返回
        return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR.getCode(),e.getMessage());
    }
}

3.9 Log out of the login interface

3.9.1 Interface design
Request method Request address Request header
POST /logout Requires token request header

Response format:

{
    
    
    "code": 200,
    "msg": "操作成功"
}
3.9.2 Code implementation

What to do:

​Delete user information in redis

BlogLoginController

    @PostMapping("/logout")
    public ResponseResult logout(){
    
    
        return blogLoginService.logout();
    }

BlogLoginService

ResponseResult logout();

BlogLoginServiceImpl

    @Override
    public ResponseResult logout() {
    
    
        //获取token 解析获取userid
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        //获取userid
        Long userId = loginUser.getUser().getId();
        //删除redis中的用户信息
        redisCache.deleteObject("bloglogin:"+userId);
        return ResponseResult.okResult();
    }

SecurityConfig

To turn off the default logout feature. And to configure our logout interface, authentication is required to access it.

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
            	//注销接口需要认证才能访问
                .antMatchers("/logout").authenticated()
                //jwt过滤器测试用,如果测试没有问题吧这里删除了
                .antMatchers("/link/getAllLink").authenticated()
                // 除上面外的所有请求全部不需要认证即可访问
                .anyRequest().permitAll();

        //配置异常处理器
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);
		//关闭默认的注销功能
        http.logout().disable();
        //把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //允许跨域
        http.cors();
    }

3.10 Query comment list interface

3.10.1 Requirements

The article details page should display the list of comments under this article.

3.10.2 Interface design
Request method Request address Request header
GET /comment/commentList No token request header is required

Query format request parameters:

articleId: article id

pageNum: page number

pageSize: number of items per page

Response format:

{
    
    
    "code": 200,
    "data": {
    
    
        "rows": [
            {
    
    
                "articleId": "1",
                "children": [
                    {
    
    
                        "articleId": "1",
                        "content": "你说啥?",
                        "createBy": "1",
                        "createTime": "2022-01-30 10:06:21",
                        "id": "20",
                        "rootId": "1",
                        "toCommentId": "1",
                        "toCommentUserId": "1",
                        "toCommentUserName": "sg333",
                        "username": "sg333"
                    }
                ],
                "content": "asS",
                "createBy": "1",
                "createTime": "2022-01-29 07:59:22",
                "id": "1",
                "rootId": "-1",
                "toCommentId": "-1",
                "toCommentUserId": "-1",
                "username": "sg333"
            }
        ],
        "total": "15"
    },
    "msg": "操作成功"
}
3.10.4 Code implementation
3.10.4.1 Sub-comments are not considered

CommentController

@RestController
@RequestMapping("/comment")
public class CommentController {
    
    

    @Autowired
    private CommentService commentService;

    @GetMapping("/commentList")
    public ResponseResult commentList(Long articleId,Integer pageNum,Integer pageSize){
    
    
        return commentService.commentList(articleId,pageNum,pageSize);
    }
}

CommentService

public interface CommentService extends IService<Comment> {
    
    

    ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize);
}

CommentServiceImpl

@Service("commentService")
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {
    
    

    @Autowired
    private UserService userService;

    @Override
    public ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize) {
    
    
        //查询对应文章的根评论
        LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
        //对articleId进行判断
        queryWrapper.eq(Comment::getArticleId,articleId);
        //根评论 rootId为-1
        queryWrapper.eq(Comment::getRootId,-1);

        //分页查询
        Page<Comment> page = new Page(pageNum,pageSize);
        page(page,queryWrapper);

        List<CommentVo> commentVoList = toCommentVoList(page.getRecords());

        return ResponseResult.okResult(new PageVo(commentVoList,page.getTotal()));
    }

    private List<CommentVo> toCommentVoList(List<Comment> list){
    
    
        List<CommentVo> commentVos = BeanCopyUtils.copyBeanList(list, CommentVo.class);
        //遍历vo集合
        for (CommentVo commentVo : commentVos) {
    
    
            //通过creatyBy查询用户的昵称并赋值
            String nickName = userService.getById(commentVo.getCreateBy()).getNickName();
            commentVo.setUsername(nickName);
            //通过toCommentUserId查询用户的昵称并赋值
            //如果toCommentUserId不为-1才进行查询
            if(commentVo.getToCommentUserId()!=-1){
    
    
                String toCommentUserName = userService.getById(commentVo.getToCommentUserId()).getNickName();
                commentVo.setToCommentUserName(toCommentUserName);
            }
        }
        return commentVos;
    }
}


CommentVo

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommentVo {
    
    
    private Long id;
    //文章id
    private Long articleId;
    //根评论id
    private Long rootId;
    //评论内容
    private String content;
    //所回复的目标评论的userid
    private Long toCommentUserId;
    private String toCommentUserName;
    //回复目标评论id
    private Long toCommentId;

    private Long createBy;

    private Date createTime;

    private String username;
}

3.10.4.2 Query sub-comments

CommentVo added private List children based on the previous one;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommentVo {
    
    
    private Long id;
    //文章id
    private Long articleId;
    //根评论id
    private Long rootId;
    //评论内容
    private String content;
    //所回复的目标评论的userid
    private Long toCommentUserId;
    private String toCommentUserName;
    //回复目标评论id
    private Long toCommentId;

    private Long createBy;

    private Date createTime;

    private String username;

    private List<CommentVo> children;
}

CommentServiceImpl

@Service("commentService")
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {
    
    

    @Autowired
    private UserService userService;

    @Override
    public ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize) {
    
    
        //查询对应文章的根评论
        LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
        //对articleId进行判断
        queryWrapper.eq(Comment::getArticleId,articleId);
        //根评论 rootId为-1
        queryWrapper.eq(Comment::getRootId,-1);

        //分页查询
        Page<Comment> page = new Page(pageNum,pageSize);
        page(page,queryWrapper);

        List<CommentVo> commentVoList = toCommentVoList(page.getRecords());

        //查询所有根评论对应的子评论集合,并且赋值给对应的属性
        for (CommentVo commentVo : commentVoList) {
    
    
            //查询对应的子评论
            List<CommentVo> children = getChildren(commentVo.getId());
            //赋值
            commentVo.setChildren(children);
        }

        return ResponseResult.okResult(new PageVo(commentVoList,page.getTotal()));
    }

    /**
     * 根据根评论的id查询所对应的子评论的集合
     * @param id 根评论的id
     * @return
     */
    private List<CommentVo> getChildren(Long id) {
    
    

        LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Comment::getRootId,id);
        queryWrapper.orderByAsc(Comment::getCreateTime);
        List<Comment> comments = list(queryWrapper);

        List<CommentVo> commentVos = toCommentVoList(comments);
        return commentVos;
    }

    private List<CommentVo> toCommentVoList(List<Comment> list){
    
    
        List<CommentVo> commentVos = BeanCopyUtils.copyBeanList(list, CommentVo.class);
        //遍历vo集合
        for (CommentVo commentVo : commentVos) {
    
    
            //通过creatyBy查询用户的昵称并赋值
            String nickName = userService.getById(commentVo.getCreateBy()).getNickName();
            commentVo.setUsername(nickName);
            //通过toCommentUserId查询用户的昵称并赋值
            //如果toCommentUserId不为-1才进行查询
            if(commentVo.getToCommentUserId()!=-1){
    
    
                String toCommentUserName = userService.getById(commentVo.getToCommentUserId()).getNickName();
                commentVo.setToCommentUserName(toCommentUserName);
            }
        }
        return commentVos;
    }
}

3.11 Comment interface

3.11.1 Requirements

After logging in, users can comment on articles and reply to comments.

After logging in, users can also comment on the friend link page.

3.11.2 Interface design
Request method Request address Request header
POST /comment Requires token header
Request body:

Replied to the article:

{
    
    "articleId":1,"type":0,"rootId":-1,"toCommentId":-1,"toCommentUserId":-1,"content":"评论了文章"}

Reply to a comment:

{
    
    "articleId":1,"type":0,"rootId":"3","toCommentId":"3","toCommentUserId":"1","content":"回复了某条评论"}

If it is a friend link comment, type should be 1

Response format:
{
    
    
	"code":200,
	"msg":"操作成功"
}
3.11.3 Code implementation

CommentController

    @PostMapping
    public ResponseResult addComment(@RequestBody Comment comment){
    
    
        return commentService.addComment(comment);
    }

CommentService

ResponseResult addComment(Comment comment);

CommentServiceImpl

    @Override
    public ResponseResult addComment(Comment comment) {
    
    
        //评论内容不能为空
        if(!StringUtils.hasText(comment.getContent())){
    
    
            throw new SystemException(AppHttpCodeEnum.CONTENT_NOT_NULL);
        }
        save(comment);
        return ResponseResult.okResult();
    }

SecurityUtils

/**
 * @Author 三更  B站: https://space.bilibili.com/663528522
 */
public class SecurityUtils
{
    
    

    /**
     * 获取用户
     **/
    public static LoginUser getLoginUser()
    {
    
    
        return (LoginUser) getAuthentication().getPrincipal();
    }

    /**
     * 获取Authentication
     */
    public static Authentication getAuthentication() {
    
    
        return SecurityContextHolder.getContext().getAuthentication();
    }

    public static Boolean isAdmin(){
    
    
        Long id = getLoginUser().getUser().getId();
        return id != null && 1L == id;
    }

    public static Long getUserId() {
    
    
        return getLoginUser().getUser().getId();
    }
}

3.12 Youlian comment list

3.12.1 Requirements

​ The friend link page also needs to query the corresponding comment list.

3.12.2 Interface design
Request method Request address Request header
GET /comment/linkCommentList No token request header is required

Query format request parameters:

pageNum: page number

pageSize: number of items per page

Response format:

{
    
    
    "code": 200,
    "data": {
    
    
        "rows": [
            {
    
    
                "articleId": "1",
                "children": [
                    {
    
    
                        "articleId": "1",
                        "content": "回复友链评论3",
                        "createBy": "1",
                        "createTime": "2022-01-30 10:08:50",
                        "id": "23",
                        "rootId": "22",
                        "toCommentId": "22",
                        "toCommentUserId": "1",
                        "toCommentUserName": "sg333",
                        "username": "sg333"
                    }
                ],
                "content": "友链评论2",
                "createBy": "1",
                "createTime": "2022-01-30 10:08:28",
                "id": "22",
                "rootId": "-1",
                "toCommentId": "-1",
                "toCommentUserId": "-1",
                "username": "sg333"
            }
        ],
        "total": "1"
    },
    "msg": "操作成功"
}
3.12.3 Code implementation

CommentController modified the previous article comment list interface and added a new Youlian comment interface

    @GetMapping("/commentList")
    public ResponseResult commentList(Long articleId,Integer pageNum,Integer pageSize){
    
    
        return commentService.commentList(SystemConstants.ARTICLE_COMMENT,articleId,pageNum,pageSize);
    }   
    @GetMapping("/linkCommentList")
    public ResponseResult linkCommentList(Integer pageNum,Integer pageSize){
    
    
        return commentService.commentList(SystemConstants.LINK_COMMENT,null,pageNum,pageSize);
    }

SystemConstants adds two constants

    /**
     * 评论类型为:文章评论
     */
    public static final String ARTICLE_COMMENT = "0";
    /**
     * 评论类型为:友联评论
     */
    public static final String LINK_COMMENT = "1";

CommentService modified the commentList method and added a parameter commentType

ResponseResult commentList(String commentType, Long articleId, Integer pageNum, Integer pageSize);

CommentServiceImpl modifies the code of the commentList method. It only adds articleId judgment when commentType is 0, and adds a comment type.

    @Override
    public ResponseResult commentList(String commentType, Long articleId, Integer pageNum, Integer pageSize) {
    
    
        //查询对应文章的根评论
        LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
        //对articleId进行判断
        queryWrapper.eq(SystemConstants.ARTICLE_COMMENT.equals(commentType),Comment::getArticleId,articleId);
        //根评论 rootId为-1
        queryWrapper.eq(Comment::getRootId,-1);

        //评论类型
        queryWrapper.eq(Comment::getType,commentType);

        //分页查询
        Page<Comment> page = new Page(pageNum,pageSize);
        page(page,queryWrapper);

        List<CommentVo> commentVoList = toCommentVoList(page.getRecords());

        //查询所有根评论对应的子评论集合,并且赋值给对应的属性
        for (CommentVo commentVo : commentVoList) {
    
    
            //查询对应的子评论
            List<CommentVo> children = getChildren(commentVo.getId());
            //赋值
            commentVo.setChildren(children);
        }

        return ResponseResult.okResult(new PageVo(commentVoList,page.getTotal()));
    }

3.13 Personal information query interface

3.13.1 Requirements

When entering the personal center, you need to be able to view the current user information

3.13.2 Interface design
Request method Request address Request header
GET /user/userInfo Requires token request header

No parameters required

Response format:

{
    
    
	"code":200,
	"data":{
    
    
		"avatar":"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fi0.hdslb.com%2Fbfs%2Farticle%2F3bf9c263bc0f2ac5c3a7feb9e218d07475573ec8.gi",
		"email":"[email protected]",
		"id":"1",
		"nickName":"sg333",
		"sex":"1"
	},
	"msg":"操作成功"
}
3.13.3 Code implementation

UserController

@RestController
@RequestMapping("/user")
public class UserController {
    
    

    @Autowired
    private UserService userService;

    @GetMapping("/userInfo")
    public ResponseResult userInfo(){
    
    
        return userService.userInfo();
    }
}

UserService adds method definition

public interface UserService extends IService<User> {
    
    

    ResponseResult userInfo();

}

UserServiceImpl implements userInfo method

    @Override
    public ResponseResult userInfo() {
    
    
        //获取当前用户id
        Long userId = SecurityUtils.getUserId();
        //根据用户id查询用户信息
        User user = getById(userId);
        //封装成UserInfoVo
        UserInfoVo vo = BeanCopyUtils.copyBean(user,UserInfoVo.class);
        return ResponseResult.okResult(vo);
    }

SecurityConfig configures that the interface must be authenticated before it can be accessed

   @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
                //注销接口需要认证才能访问
                .antMatchers("/logout").authenticated()
            	//个人信息接口必须登录后才能访问
                .antMatchers("/user/userInfo").authenticated()
                // 除上面外的所有请求全部不需要认证即可访问
                .anyRequest().permitAll();

        //配置异常处理器
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);
        //关闭默认的注销功能
        http.logout().disable();
        //把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //允许跨域
        http.cors();
    }

3.14 Avatar upload interface

3.14.1 Requirements

​ You can upload an avatar picture when you click Edit in the personal center. After uploading the avatar, it can be used to update the personal information interface.

3.14.2 OSS
3.14.2.1 Why use OSS

​ Because if you upload files such as pictures and videos to the web server of your own application, it will take up more resources when reading the pictures. Affect application server performance.

​So we generally use OSS (Object Storage Service) to store pictures or videos.

3.14.2.2 Qiniu Cloud basic usage test
3.14.2.3 Qiniu Cloud test code writing

①Add dependencies

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

②Copy and modify the case code

application.yml

oss:
  accessKey: xxxx
  secretKey: xxxx
  bucket: sg-blog

OSSTest.java

@SpringBootTest
@ConfigurationProperties(prefix = "oss")
public class OSSTest {
    
    

    private String accessKey;
    private String secretKey;
    private String bucket;

    public void setAccessKey(String accessKey) {
    
    
        this.accessKey = accessKey;
    }

    public void setSecretKey(String secretKey) {
    
    
        this.secretKey = secretKey;
    }

    public void setBucket(String bucket) {
    
    
        this.bucket = bucket;
    }

    @Test
    public void testOss(){
    
    
        //构造一个带指定 Region 对象的配置类
        Configuration cfg = new Configuration(Region.autoRegion());
        //...其他参数参考类注释

        UploadManager uploadManager = new UploadManager(cfg);
        //...生成上传凭证,然后准备上传
//        String accessKey = "your access key";
//        String secretKey = "your secret key";
//        String bucket = "sg-blog";

        //默认不指定key的情况下,以文件内容的hash值作为文件名
        String key = "2022/sg.png";

        try {
    
    
//            byte[] uploadBytes = "hello qiniu cloud".getBytes("utf-8");
//            ByteArrayInputStream byteInputStream=new ByteArrayInputStream(uploadBytes);


            InputStream inputStream = new FileInputStream("C:\\Users\\root\\Desktop\\Snipaste_2022-02-28_22-48-37.png");
            Auth auth = Auth.create(accessKey, secretKey);
            String upToken = auth.uploadToken(bucket);

            try {
    
    
                Response response = uploadManager.put(inputStream,key,upToken,null, null);
                //解析上传成功的结果
                DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
                System.out.println(putRet.key);
                System.out.println(putRet.hash);
            } catch (QiniuException ex) {
    
    
                Response r = ex.response;
                System.err.println(r.toString());
                try {
    
    
                    System.err.println(r.bodyString());
                } catch (QiniuException ex2) {
    
    
                    //ignore
                }
            }
        } catch (Exception ex) {
    
    
            //ignore
        }

    }
}
3.14.2 Interface design
Request method Request address Request header
POST /upload need token

parameter:

img, the value is the file to be uploaded

Request header:

​ Content-Type :multipart/form-data;

Content-Type, content type, generally refers to the Content-Type that exists in a web page. It is used to define the type of network files and the encoding of the web page, and determines what form and encoding the browser will read the file in.

Common media format types are as follows:

text/html: HTML format
text/plain: plain text
format text/xml: XML format
image/gif: gif image format
image/jpeg: jpg image format
image/png: png image format
Media format types starting with application:

application/xhtml+xml: XHTML format
application/xml: XML data format
application/atom+xml: Atom XML aggregation format
application/json: JSON data format
application/pdf: pdf format
application/msword: Word document format
application/octet-stream : Binary stream data (such as common file downloads)
application/x-www-form-urlencoded : The default encType in the form data is encoded into key/value format and sent to the server (the default submission data format of the form)
common The media format is used when uploading files:

multipart/form-data: This format needs to be used when uploading files in a form.

Response format:

{
    
    
    "code": 200,
    "data": "文件访问链接",
    "msg": "操作成功"
}
3.14.3 Code implementation
@RestController
public class UploadController {
    
    
    @Autowired
    private UploadService uploadService;

    @PostMapping("/upload")
    public ResponseResult uploadImg(MultipartFile img){
    
    
        return uploadService.uploadImg(img);
    }
}

public interface UploadService {
    
    
    ResponseResult uploadImg(MultipartFile img);
}

@Service
@Data
@ConfigurationProperties(prefix = "oss")
public class OssUploadService implements UploadService {
    
    
    @Override
    public ResponseResult uploadImg(MultipartFile img) {
    
    
        //判断文件类型
        //获取原始文件名
        String originalFilename = img.getOriginalFilename();
        //对原始文件名进行判断
        if(!originalFilename.endsWith(".png")){
    
    
            throw new SystemException(AppHttpCodeEnum.FILE_TYPE_ERROR);
        }

        //如果判断通过上传文件到OSS
        String filePath = PathUtils.generateFilePath(originalFilename);
        String url = uploadOss(img,filePath);//  2099/2/3/wqeqeqe.png
        return ResponseResult.okResult(url);
    }

    private String accessKey;
    private String secretKey;
    private String bucket;


    private String uploadOss(MultipartFile imgFile, String filePath){
    
    
        //构造一个带指定 Region 对象的配置类
        Configuration cfg = new Configuration(Region.autoRegion());
        //...其他参数参考类注释
        UploadManager uploadManager = new UploadManager(cfg);
        //默认不指定key的情况下,以文件内容的hash值作为文件名
        String key = filePath;
        try {
    
    
            InputStream inputStream = imgFile.getInputStream();
            Auth auth = Auth.create(accessKey, secretKey);
            String upToken = auth.uploadToken(bucket);
            try {
    
    
                Response response = uploadManager.put(inputStream,key,upToken,null, null);
                //解析上传成功的结果
                DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
                System.out.println(putRet.key);
                System.out.println(putRet.hash);
                return "http://r7yxkqloa.bkt.clouddn.com/"+key;
            } catch (QiniuException ex) {
    
    
                Response r = ex.response;
                System.err.println(r.toString());
                try {
    
    
                    System.err.println(r.bodyString());
                } catch (QiniuException ex2) {
    
    
                    //ignore
                }
            }
        } catch (Exception ex) {
    
    
            //ignore
        }
        return "www";
    }
}

PathUtils

/**
 * @Author 三更  B站: https://space.bilibili.com/663528522
 */
public class PathUtils {
    
    

    public static String generateFilePath(String fileName){
    
    
        //根据日期生成路径   2022/1/15/
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd/");
        String datePath = sdf.format(new Date());
        //uuid作为文件名
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        //后缀和文件后缀一致
        int index = fileName.lastIndexOf(".");
        // test.jpg -> .jpg
        String fileType = fileName.substring(index);
        return new StringBuilder().append(datePath).append(uuid).append(fileType).toString();
    }
}

3.15 Update personal information interface

3.15.1 Requirements

After editing the personal information, click Save to update the personal information.

3.15.2 Interface design

Request method Request address Request header
PUT /user/userInfo Requires token request header

parameter

json format data in the request body:

{
    
    
    "avatar":"https://sg-blog-oss.oss-cn-beijing.aliyuncs.com/2022/01/31/948597e164614902ab1662ba8452e106.png",
    "email":"[email protected]",
    "id":"1",
    "nickName":"sg333",
    "sex":"1"
}

Response format:

{
    
    
	"code":200,
	"msg":"操作成功"
}
3.15.3 Code implementation

UserController

    @PutMapping("/userInfo")
    public ResponseResult updateUserInfo(@RequestBody User user){
    
    
        return userService.updateUserInfo(user);
    }

UserService

ResponseResult updateUserInfo(User user);

UserServiceImpl

    @Override
    public ResponseResult updateUserInfo(User user) {
    
    
        updateById(user);
        return ResponseResult.okResult();
    }

3.16 User registration

3.16.1 Requirements

​ Requires users to complete user registration on the registration interface. It is required that the user name, nickname, and email address cannot be duplicated with the original data in the database. If a certain item fails to register repeatedly, there should be a corresponding prompt. And it is required that the username, password, nickname, and email address cannot be empty.

Note: Passwords must be stored in ciphertext in the database.

3.16.2 Interface design

Request method Request address Request header
POST /user/register No token request header is required

parameter

json format data in the request body:

{
    
    
  "email": "string",
  "nickName": "string",
  "password": "string",
  "userName": "string"
}

Response format:

{
    
    
	"code":200,
	"msg":"操作成功"
}
3.16.3 Code implementation

UserController

    @PostMapping("/register")
    public ResponseResult register(@RequestBody User user){
    
    
        return userService.register(user);
    }

UserService

ResponseResult register(User user);

UserServiceImpl

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Override
    public ResponseResult register(User user) {
    
    
        //对数据进行非空判断
        if(!StringUtils.hasText(user.getUserName())){
    
    
            throw new SystemException(AppHttpCodeEnum.USERNAME_NOT_NULL);
        }
        if(!StringUtils.hasText(user.getPassword())){
    
    
            throw new SystemException(AppHttpCodeEnum.PASSWORD_NOT_NULL);
        }
        if(!StringUtils.hasText(user.getEmail())){
    
    
            throw new SystemException(AppHttpCodeEnum.EMAIL_NOT_NULL);
        }
        if(!StringUtils.hasText(user.getNickName())){
    
    
            throw new SystemException(AppHttpCodeEnum.NICKNAME_NOT_NULL);
        }
        //对数据进行是否存在的判断
        if(userNameExist(user.getUserName())){
    
    
            throw new SystemException(AppHttpCodeEnum.USERNAME_EXIST);
        }
        if(nickNameExist(user.getNickName())){
    
    
            throw new SystemException(AppHttpCodeEnum.NICKNAME_EXIST);
        }
        //...
        //对密码进行加密
        String encodePassword = passwordEncoder.encode(user.getPassword());
        user.setPassword(encodePassword);
        //存入数据库
        save(user);
        return ResponseResult.okResult();
    }

public enum AppHttpCodeEnum {
    
    
    // 成功
    SUCCESS(200,"操作成功"),
    // 登录
    NEED_LOGIN(401,"需要登录后操作"),
    NO_OPERATOR_AUTH(403,"无权限操作"),
    SYSTEM_ERROR(500,"出现错误"),
    USERNAME_EXIST(501,"用户名已存在"),
     PHONENUMBER_EXIST(502,"手机号已存在"), EMAIL_EXIST(503, "邮箱已存在"),
    REQUIRE_USERNAME(504, "必需填写用户名"),
    CONTENT_NOT_NULL(506, "评论内容不能为空"),
    FILE_TYPE_ERROR(507, "文件类型错误,请上传png文件"),
    USERNAME_NOT_NULL(508, "用户名不能为空"),
    NICKNAME_NOT_NULL(509, "昵称不能为空"),
    PASSWORD_NOT_NULL(510, "密码不能为空"),
    EMAIL_NOT_NULL(511, "邮箱不能为空"),
    NICKNAME_EXIST(512, "昵称已存在"),
    LOGIN_ERROR(505,"用户名或密码错误");
    int code;
    String msg;

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

    public int getCode() {
    
    
        return code;
    }

    public String getMsg() {
    
    
        return msg;
    }
}

3.17 AOP实现日志记录

3.17.1 需求

​ 需要通过日志记录接口调用信息。便于后期调试排查。并且可能有很多接口都需要进行日志的记录。

3.17.2 思路分析

​ 相当于是对原有的功能进行增强。并且是批量的增强,这个时候就非常适合用AOP来进行实现。

3.17.3 代码实现

日志打印格式

        log.info("=======Start=======");
        // 打印请求 URL
        log.info("URL            : {}",);
        // 打印描述信息
        log.info("BusinessName   : {}", );
        // 打印 Http method
        log.info("HTTP Method    : {}", );
        // 打印调用 controller 的全路径以及执行方法
        log.info("Class Method   : {}.{}", );
        // 打印请求的 IP
        log.info("IP             : {}",);
        // 打印请求入参
        log.info("Request Args   : {}",);
        // 打印出参
        log.info("Response       : {}", );
        // 结束后换行
        log.info("=======End=======" + System.lineSeparator());

3.18 更新浏览次数

3.18.1 需求

​ 在用户浏览博文时要实现对应博客浏览量的增加。

3.18.2 思路分析

​ 我们只需要在每次用户浏览博客时更新对应的浏览数即可。

​ 但是如果直接操作博客表的浏览量的话,在并发量大的情况下会出现什么问题呢?

​ 如何去优化呢?

①在应用启动时把博客的浏览量存储到redis中

②更新浏览量时去更新redis中的数据

③每隔10分钟把Redis中的浏览量更新到数据库中

④读取文章浏览量时从redis读取

3.18.3 铺垫知识
3.18.3.1 CommandLineRunner实现项目启动时预处理

​ 如果希望在SpringBoot应用启动时进行一些初始化操作可以选择使用CommandLineRunner来进行处理。

​ 我们只需要实现CommandLineRunner接口,并且把对应的bean注入容器。把相关初始化的代码重新到需要重新的方法中。

​ 这样就会在应用启动的时候执行对应的代码。

@Component
public class TestRunner implements CommandLineRunner {
    
    
    @Override
    public void run(String... args) throws Exception {
    
    
        System.out.println("程序初始化");
    }
}

3.18.3.2 定时任务

​ 定时任务的实现方式有很多,比如XXL-Job等。但是其实核心功能和概念都是类似的,很多情况下只是调用的API不同而已。

​ 这里就先用SpringBoot为我们提供的定时任务的API来实现一个简单的定时任务,让大家先对定时任务里面的一些核心概念有个大致的了解。

实现步骤

① 使用@EnableScheduling注解开启定时任务功能

​ 我们可以在配置类上加上@EnableScheduling

@SpringBootApplication
@MapperScan("com.sangeng.mapper")
@EnableScheduling
public class SanGengBlogApplication {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(SanGengBlogApplication.class,args);
    }
}

② 确定定时任务执行代码,并配置任务执行时间

​ 使用@Scheduled注解标识需要定时执行的代码。注解的cron属性相当于是任务的执行时间。目前可以使用 0/5 * * * * ? 进行测试,代表从0秒开始,每隔5秒执行一次。

​ 注意:对应的bean要注入容器,否则不会生效。

@Component
public class TestJob {
    
    

    @Scheduled(cron = "0/5 * * * * ?")
    public void testJob(){
    
    
        //要执行的代码
        System.out.println("定时任务执行了");
    }
}

3.18.3.2.1 cron 表达式语法

​ cron表达式是用来设置定时任务执行时间的表达式。

​ 很多情况下我们可以用 : 在线Cron表达式生成器 来帮助我们理解cron表达式和书写cron表达式。

​ 但是我们还是有需要学习对应的Cron语法的,这样可以更有利于我们书写Cron表达式。

如上我们用到的 0/5 * * * * ? *,cron表达式由七部分组成,中间由空格分隔,这七部分从左往右依次是:

秒(0-59),分钟(0-59),小时(0~23),日期(1-月最后一天),月份(1-12),星期几(1-7,1表示星期日),年份(一般该项不设置,直接忽略掉,即可为空值)

通用特殊字符:, - * / (可以在任意部分使用)

星号表示任意值,例如:

* * * * * ?

表示 “ 每年每月每天每时每分每秒 ” 。

,

可以用来定义列表,例如 :

1,2,3 * * * * ?

表示 “ 每年每月每天每时每分的每个第1秒,第2秒,第3秒 ” 。

定义范围,例如:

1-3 * * * * ?

表示 “ 每年每月每天每时每分的第1秒至第3秒 ”。

/

每隔多少,例如

5/10 * * * * ?

表示 “ 每年每月每天每时每分,从第5秒开始,每10秒一次 ” 。即 “ / ” 的左侧是开始值,右侧是间隔。如果是从 “ 0 ” 开始的话,也可以简写成 “ /10 ”

3.18.4 接口设计

请求方式 请求地址 请求头
PUT /article/updateViewCount/{id} 不需要token请求头

参数

​ 请求路径中携带文章id

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}
3.18.5 代码实现
①在应用启动时把博客的浏览量存储到redis中

​ 实现CommandLineRunner接口,在应用启动时初始化缓存。

@Component
public class ViewCountRunner implements CommandLineRunner {
    
    

    @Autowired
    private ArticleMapper articleMapper;

    @Autowired
    private RedisCache redisCache;

    @Override
    public void run(String... args) throws Exception {
    
    
        //查询博客信息  id  viewCount
        List<Article> articles = articleMapper.selectList(null);
        Map<String, Integer> viewCountMap = articles.stream()
                .collect(Collectors.toMap(article -> article.getId().toString(), article -> {
    
    
                    return article.getViewCount().intValue();//
                }));
        //存储到redis中
        redisCache.setCacheMap("article:viewCount",viewCountMap);
    }
}

②更新浏览量时去更新redsi中的数据

RedisCache增加方法

    public void incrementCacheMapValue(String key,String hKey,long v){
    
    
        redisTemplate.boundHashOps(key).increment(hKey, v);
    }

Add method to Update Reading Count in ArticleController

    @PutMapping("/updateViewCount/{id}")
    public ResponseResult updateViewCount(@PathVariable("id") Long id){
    
    
        return articleService.updateViewCount(id);
    }

Add method to ArticleService

ResponseResult updateViewCount(Long id);

Implementation methods in ArticleServiceImpl

    @Override
    public ResponseResult updateViewCount(Long id) {
    
    
        //更新redis中对应 id的浏览量
        redisCache.incrementCacheMapValue("article:viewCount",id.toString(),1);
        return ResponseResult.okResult();
    }
③ Scheduled tasks update the views in Redis to the database every 10 minutes

Add construction method to Article

    public Article(Long id, long viewCount) {
    
    
        this.id = id;
        this.viewCount = viewCount;
    }
@Component
public class UpdateViewCountJob {
    
    

    @Autowired
    private RedisCache redisCache;

    @Autowired
    private ArticleService articleService;

    @Scheduled(cron = "0/5 * * * * ?")
    public void updateViewCount(){
    
    
        //获取redis中的浏览量
        Map<String, Integer> viewCountMap = redisCache.getCacheMap("article:viewCount");

        List<Article> articles = viewCountMap.entrySet()
                .stream()
                .map(entry -> new Article(Long.valueOf(entry.getKey()), entry.getValue().longValue()))
                .collect(Collectors.toList());
        //更新到数据库中
        articleService.updateBatchById(articles);

    }
}

④Read from redis when reading article views
    @Override
    public ResponseResult getArticleDetail(Long id) {
    
    
        //根据id查询文章
        Article article = getById(id);
        //从redis中获取viewCount
        Integer viewCount = redisCache.getCacheMapValue("article:viewCount", id.toString());
        article.setViewCount(viewCount.longValue());
        //转换成VO
        ArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class);
        //根据分类id查询分类名
        Long categoryId = articleDetailVo.getCategoryId();
        Category category = categoryService.getById(categoryId);
        if(category!=null){
    
    
            articleDetailVo.setCategoryName(category.getName());
        }
        //封装响应返回
        return ResponseResult.okResult(articleDetailVo);
    }

4. Swagger2

4.1 Introduction

Swagger is a set of open source tools based on the OpenAPI specification that can help us design, build, record, and use Rest APIs.

4.2 Why use Swagger

At present, many companies adopt a development model in which front-end and back-end are separated, and the work of front-end and back-end is completed by different engineers. In this development model, maintaining a timely and complete Rest API document will greatly improve our work efficiency. In the traditional sense, documents are manually written by back-end developers. I believe everyone knows that it is difficult to ensure the timeliness of documents in this way. Such documents will lose their reference significance over time, but will also increase our communication costs. Swagger provides us with a brand new way to maintain API documents. Let's take a look at its advantages:

1. The code changes and the documentation changes. With only a small amount of annotations, Swagger can automatically generate API documents based on the code, which ensures the timeliness of the documents.
2. Cross-language, supporting more than 40 languages.
3. Swagger UI presents an interactive API document. We can try API calls directly on the document page, eliminating the need to prepare complex call parameters.

4.3 Quick Start

4.3.1 Introducing dependencies
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
        </dependency>

4.3.2 Enable Swagger2

Add the @EnableSwagger2 annotation to the startup class or configuration class

@SpringBootApplication
@MapperScan("com.sangeng.mapper")
@EnableScheduling
@EnableSwagger2
public class SanGengBlogApplication {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(SanGengBlogApplication.class,args);
    }
}

4.4 Specific configuration

4.4.1 Controller configuration
4.4.1 @Api annotation

Property introduction:

tags set tags

description Set description information

@RestController
@RequestMapping("/comment")
@Api(tags = "评论",description = "评论相关接口")
public class CommentController {
    
    
}
4.4.2 Interface configuration
4.4.2.1 Interface description configuration @ApiOperation
    @GetMapping("/linkCommentList")
    @ApiOperation(value = "友链评论列表",notes = "获取一页友链评论")
    public ResponseResult linkCommentList(Integer pageNum,Integer pageSize){
    
    
        return commentService.commentList(SystemConstants.LINK_COMMENT,null,pageNum,pageSize);
    }
4.4.2.2 Interface parameter description

@ApiImplicitParam 用于描述接口的参数,但是一个接口可能有多个参数,所以一般与 @ApiImplicitParams 组合使用。

    @GetMapping("/linkCommentList")
    @ApiOperation(value = "友链评论列表",notes = "获取一页友链评论")
    @ApiImplicitParams({
    
    
           @ApiImplicitParam(name = "pageNum",value = "页号"),
           @ApiImplicitParam(name = "pageSize",value = "每页大小")
    }
    )
    public ResponseResult linkCommentList(Integer pageNum,Integer pageSize){
    
    
        return commentService.commentList(SystemConstants.LINK_COMMENT,null,pageNum,pageSize);
    }
4.4.3 实体类配置
4.4.3.1 实体的描述配置@ApiModel

@ApiModel用于描述实体类。

@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(description = "添加评论dto")
public class AddCommentDto{
    
    
    //..
}
4.4.3.2 实体的属性的描述配置@ApiModelProperty

@ApiModelProperty用于描述实体的属性

    @ApiModelProperty(notes = "评论类型(0代表文章评论,1代表友链评论)")
    private String type;
4.4.4 文档信息配置
@Configuration
public class SwaggerConfig {
    
    
    @Bean
    public Docket customDocket() {
    
    
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sangeng.controller"))
                .build();
    }

    private ApiInfo apiInfo() {
    
    
        Contact contact = new Contact("团队名", "http://www.my.com", "[email protected]");
        return new ApiInfoBuilder()
                .title("文档标题")
                .description("文档描述")
                .contact(contact)   // 联系方式
                .version("1.1.0")  // 版本
                .build();
    }
}

5. 博客后台

5.0 准备工作

前端工程启动

npm run dev

①创建启动类
Insert image description here
②创建application.yml配置文件
Insert image description here

server:
  port: 8989
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/sg_blog?characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  servlet:
    multipart:
      max-file-size: 2MB
      max-request-size: 5MB

mybatis-plus:
  configuration:
    # mybatis-plus 日志信息
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      logic-delete-field: delFlag  #逻辑删除字段  (数据库中逻辑删除字段用delFlag)
      logic-delete-value: 1       # 删除值 1
      logic-not-delete-value: 0   # 未删除值 0
      id-type: auto             #主键自增(不设置用 mybatis-plus默认生成的id)
#  当用maven导入了mybatisplus的包时,必须要在yml配置中配置数据库的参数,否则运行boot启动类会报错

③ 整体目录结构

Insert image description here
Insert image description here

④ 创建Entity,Mapper,Service
EasyCode自动生成,然后放入指定包中
下面以Tag为例细说,后面类似的看这里。
Entity
Insert image description here
MapperInsert image description here
Service
Insert image description here

⑤ Controller
Insert image description here

⑥添加security相关类——SecurityConfigJwtAuthenticationTokenFilter
使admin模块能够使用SpringSecurity(拦截,密码加密,权限等)
Insert image description here
SecurityConfig:

import ...

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // @PreAuthorize和@PostAuthorize注解生效
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
//    为啥注入的不是实现类呢? 不传实现类是为了符合开闭原则
    @Autowired
    AuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    AccessDeniedHandler accessDeniedHandler;

    @Bean
    public PasswordEncoder passwordEncoder(){
    
     //对比密码时 加密方式
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()   //对登录放行,登录不需要认证。  接口必须携带token
//                .antMatchers("/logout").authenticated()//退出接口 必须认证,即必须携带token才能 发出退出请求
//                .antMatchers("/user/userInfo").authenticated() //这个接口需要认证之后才能访问
//                .antMatchers("/upload").authenticated() //前端vue上传图片时,没有要求token,所以后端不需要认证,不需要传token即可
//                .antMatchers("/link/getAllLink").authenticated() //这个接口需要认证之后才能访问
                // 除上面外的所有请求全部不需要认证即可访问
//                .anyRequest().permitAll(); //所有需求都允许访问
                .anyRequest().authenticated(); //所有需求 都需要认证

        //配置异常处理器
        http.exceptionHandling()
                        .authenticationEntryPoint(authenticationEntryPoint) //认证失败处理器
                         .accessDeniedHandler(accessDeniedHandler); //授权失败处理器

        http.logout().disable(); //关闭默认 logout功能  禁用security默认的注销功能

        //将过滤器 配置到 UsernamePasswordAuthenticationFilter之前
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //允许跨域
        http.cors();
    }

    @Override
    @Bean     //暴露ProvideManager方法注入到spring bean容器中
    public AuthenticationManager authenticationManagerBean() throws Exception {
    
    
//        这一段配置用于登录时认证,只有使用了这个配置才能自动注入AuthenticationManager,并使用它来进行用户认证
        return super.authenticationManagerBean();
    }
}

JwtAuthenticationTokenFilter:

import ...

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
    

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    
    
//      登录校验过滤器        定义Jwt认证过滤器  浏览器登录后,再次发送请求时,会携带token,让服务器验证

        //获取请求头中的token
        String token = request.getHeader("token");
        if(!StringUtils.hasText(token)){
    
     //没有token就说明是第一次登录,直接放行
            //说明该接口 不需要登录,直接放行
            filterChain.doFilter(request,response);
            return;   //放行,程序到此结束
        }
        //解析token 获取userid
        Claims claims = null;
        try {
    
    
            claims = JwtUtil.parseJWT(token);
        } catch (Exception e) {
    
    
            e.printStackTrace();
            //token超时,token非法
            //响应告诉前端重新登录 (响应一个json格式)
            ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);// 401 请重新登录
            WebUtils.renderString(response, JSON.toJSONString(result)); //转成json,并把json串写到响应体当中
            return;
        }
        String userId = claims.getSubject(); // 拿到userid
        //从redis获取用户信息(如果获取失败,则验证失败)
        LoginUser loginUser = redisCache.getCacheObject("login:" + userId);
        //如果获取不到
        if (Objects.isNull(loginUser)){
    
    
            //说明登录过期,重新登录
            ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);// 401 请重新登录
            WebUtils.renderString(response, JSON.toJSONString(result)); //转成json,并把json串写到响应体当中
            return;
        }

        //存入SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);//未认证,用两个参数,认证过,用三个参数
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

//        都执行完了要 放行,让下面的filter处理
        filterChain.doFilter(request,response);
    }
}

5.1 后台登录

​ 后台的认证授权使用SpringSecurity安全框架来实现。

5.1.0 需求

​ 登录功能

​ 后台所有功能都必须登录才能使用。

5.1.1 接口设计
请求方式 请求路径
POST /user/login

请求体:

{
    
    
    "userName":"sg",
    "password":"1234"
}

响应格式:

{
    
    
    "code": 200,
    "data": {
    
    
        "token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0ODBmOThmYmJkNmI0NjM0OWUyZjY2NTM0NGNjZWY2NSIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTY0Mzg3NDMxNiwiZXhwIjoxNjQzOTYwNzE2fQ.ldLBUvNIxQCGemkCoMgT_0YsjsWndTg5tqfJb77pabk"
    },
    "msg": "操作成功"
}
5.1.2 思路分析

登录

​ ①自定义登录接口

​ 调用ProviderManager的方法进行认证 如果认证通过生成jwt

​ 把用户信息存入redis中

​ ②自定义UserDetailsService

​ 在这个实现类中去查询数据库

​ Pay attention to configuring passwordEncoder as BCryptPasswordEncoder

check:

①Define Jwt authentication filter

​ Get token

​ Parse the token to get the userid in it

​ Get user information from redis

​Save to SecurityContextHolder

5.1.3 Preparation

①Add dependencies, add the following dependencies to the pom of the sangeng-framework module

  • redis dependency
  • fastjsondependency
  • jwt dependency
5.1.4 Login interface code implementation
LoginController

Insert image description here

LoginService

Insert image description here

SecurityConfig

Add security related classes - SecurityConfigand JwtAuthenticationTokenFiltercode in the front-end system.
Enable the admin module to use SpringSecurity (interception, password encryption, permissions, etc.)
Insert image description here

LoginServiceImpl
@Service
public class SystemLoginServiceImpl implements LoginService {
    
    
    @Autowired
    private AuthenticationManager authenticationManager;//SecurityConfig配置AuthenticationManager已经注入到容器当中,可以使用
    //AuthenticationManager.ProviderManager的方法进行认证
    @Autowired
    private RedisCache redisCache; //存入redis,需要使用rediscache
    @Override
    public ResponseResult login(User user) {
    
    
//        调用ProviderManager的方法进行认证 如果认证通过生成jwt
//      UsernamePasswordAuthenticationToken -> AbstractAuthenticationToken -> Authentication
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());//传用户名 密码
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);//调用认证方法 参数是Authentication,使用它的实现类UsernamePasswordAuthenticationToken
//        authenticationManager 实际上 会默认调用UserDetailsService接口去认证,所以要重写UserDetailsService接口

        //security会使用UserDetailsService实现类中的loadUserByUsername方法进行校验,所以要重写该方法
//        return new LoginUser(user) 返回给 Authentication authenticate,包含了 UserDetails对象(权限信息 + 用户信息)
//        authenticationManager.authenticate() 认证的时候,自动进行密码比对
        //判断是否通过
        if (Objects.isNull(authenticate)){
    
    
            throw new RuntimeException("用户名或密码错误");
        }
        //获取userid,生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();//获取认证主体 强转成LoginUser
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId); //jwt:把userid加密后的密文 即token

        //把用户信息存入redis   格式(bloglogin:id,loginUser)  (包含权限信息)
        redisCache.setCacheObject("login:"+userId,loginUser);

        //把token封装 返回
        Map<String,String> map = new HashMap<>();
        map.put("token",jwt);
        return ResponseResult.okResult(map);
    }

    @Override
    public ResponseResult logout() {
    
    
        //获取当前登录的用户id
        Long userId = SecurityUtils.getUserId();
        //删除redis中对应的值
        redisCache.deleteObject("login:"+userId);

        return ResponseResult.okResult();
    }

}

5.2 Background permission control and dynamic routing

​ The backend system needs to be able to implement different user permissions to see different functions.

​A user can only use the functions allowed by his permissions.

feature design

RBAC permission model

table analysis

Insert image description here
Insert image description here

Insert image description here
Insert image description here

Insert image description hereInsert image description here

Interface design
getInfo interface
Request method Request address Request header
GET /getInfo Requires token request header

Request parameters: None

Response format:

If the user ID is 1, it represents the administrator. Only admin needs to be included in roles, and all permissions with menu type C or F, normal status, and undeleted permissions need to be included in permissions.

{
    
    
	"code":200,
	"data":{
    
    
		"permissions":[
			"system:user:list",
            "system:role:list",
			"system:menu:list",
			"system:user:query",
			"system:user:add"
            //此次省略1000字
		],
		"roles":[
			"admin"
		],
		"user":{
    
    
			"avatar":"http://r7yxkqloa.bkt.clouddn.com/2022/03/05/75fd15587811443a9a9a771f24da458d.png",
			"email":"[email protected]",
			"id":1,
			"nickName":"sg3334",
			"sex":"1"
		}
	},
	"msg":"操作成功"
}
getRoutersInterface
Request method Request address Request header
GET /getRouters Requires token request header

Request parameters: None

Response format:

In order to achieve the effect of dynamic routing on the front end, the back end needs to have an interface that can return menu data that users can access.

Note: The returned menu data needs to reflect the hierarchical relationship between parent and child menus

​ If the user ID is 1, it represents the administrator. Menus needs to have all menu types of C or M, the status is normal, and the permissions have not been deleted.

The data format is as follows:

{
    
    
	"code":200,
	"data":{
    
    
		"menus":[
			{
    
    
				"children":[],
				"component":"content/article/write/index",
				"createTime":"2022-01-08 11:39:58",
				"icon":"build",
				"id":2023,
				"menuName":"写博文",
				"menuType":"C",
				"orderNum":"0",
				"parentId":0,
				"path":"write",
				"perms":"content:article:writer",
				"status":"0",
				"visible":"0"
			},
			{
    
    
				"children":[
					{
    
    
						"children":[],
						"component":"system/user/index",
						"createTime":"2021-11-12 18:46:19",
						"icon":"user",
						"id":100,
						"menuName":"用户管理",
						"menuType":"C",
						"orderNum":"1",
						"parentId":1,
						"path":"user",
						"perms":"system:user:list",
						"status":"0",
						"visible":"0"
					},
					{
    
    
						"children":[],
						"component":"system/role/index",
						"createTime":"2021-11-12 18:46:19",
						"icon":"peoples",
						"id":101,
						"menuName":"角色管理",
						"menuType":"C",
						"orderNum":"2",
						"parentId":1,
						"path":"role",
						"perms":"system:role:list",
						"status":"0",
						"visible":"0"
					},
					{
    
    
						"children":[],
						"component":"system/menu/index",
						"createTime":"2021-11-12 18:46:19",
						"icon":"tree-table",
						"id":102,
						"menuName":"菜单管理",
						"menuType":"C",
						"orderNum":"3",
						"parentId":1,
						"path":"menu",
						"perms":"system:menu:list",
						"status":"0",
						"visible":"0"
					}
				],
				"createTime":"2021-11-12 18:46:19",
				"icon":"system",
				"id":1,
				"menuName":"系统管理",
				"menuType":"M",
				"orderNum":"1",
				"parentId":0,
				"path":"system",
				"perms":"",
				"status":"0",
				"visible":"0"
			}
		]
	},
	"msg":"操作成功"
}
Code
Preparation

​ Generate classes for menu and role tables

getInfo interface
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class AdminUserInfoVo {
    
    

    private List<String> permissions;

    private List<String> roles;

    private UserInfoVo user;
}
@RestController
public class LoginController {
    
    
    @Autowired
    private LoginService loginService;

    @Autowired
    private MenuService menuService;

    @Autowired
    private RoleService roleService;

    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody User user){
    
    
        if(!StringUtils.hasText(user.getUserName())){
    
    
            //提示 必须要传用户名
            throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);
        }
        return loginService.login(user);
    }

    @GetMapping("getInfo")
    public ResponseResult<AdminUserInfoVo> getInfo(){
    
    
        //获取当前登录的用户
        LoginUser loginUser = SecurityUtils.getLoginUser();
        //根据用户id查询权限信息
        List<String> perms = menuService.selectPermsByUserId(loginUser.getUser().getId());
        //根据用户id查询角色信息
        List<String> roleKeyList = roleService.selectRoleKeyByUserId(loginUser.getUser().getId());

        //获取用户信息
        User user = loginUser.getUser();
        UserInfoVo userInfoVo = BeanCopyUtils.copyBean(user, UserInfoVo.class);
        //封装数据返回

        AdminUserInfoVo adminUserInfoVo = new AdminUserInfoVo(perms,roleKeyList,userInfoVo);
        return ResponseResult.okResult(adminUserInfoVo);
    }

}

RoleServiceImpl selectRoleKeyByUserId方法

@Service("menuService")
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {
    
    

    @Override
    public List<String> selectPermsByUserId(Long id) {
    
    
        //如果是管理员,返回所有的权限
        if(id == 1L){
    
    
            LambdaQueryWrapper<Menu> wrapper = new LambdaQueryWrapper<>();
            wrapper.in(Menu::getMenuType,SystemConstants.MENU,SystemConstants.BUTTON);
            wrapper.eq(Menu::getStatus,SystemConstants.STATUS_NORMAL);
            List<Menu> menus = list(wrapper);
            List<String> perms = menus.stream()
                    .map(Menu::getPerms)
                    .collect(Collectors.toList());
            return perms;
        }
        //否则返回所具有的权限
        return getBaseMapper().selectPermsByUserId(id);
    }
}

MenuMapper

/**
 * 菜单权限表(Menu)表数据库访问层
 *
 * @author makejava
 * @since 2022-08-09 22:32:07
 */
public interface MenuMapper extends BaseMapper<Menu> {
    
    

    List<String> selectPermsByUserId(Long userId);
}

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

    <select id="selectPermsByUserId" resultType="java.lang.String">
        SELECT
            DISTINCT m.perms
        FROM
            `sys_user_role` ur
            LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
            LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
        WHERE
            ur.`user_id` = #{userId} AND
            m.`menu_type` IN ('C','F') AND
            m.`status` = 0 AND
            m.`del_flag` = 0
    </select>
</mapper>

MenuServiceImpl selectPermsByUserId method

@Service("roleService")
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
    
    

    @Override
    public List<String> selectRoleKeyByUserId(Long id) {
    
    
        //判断是否是管理员 如果是返回集合中只需要有admin
        if(id == 1L){
    
    
            List<String> roleKeys = new ArrayList<>();
            roleKeys.add("admin");
            return roleKeys;
        }
        //否则查询用户所具有的角色信息
        return getBaseMapper().selectRoleKeyByUserId(id);
    }
}
public interface RoleMapper extends BaseMapper<Role> {
    
    

    List<String> selectRoleKeyByUserId(Long userId);
}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sangeng.mapper.RoleMapper">
    <select id="selectRoleKeyByUserId" resultType="java.lang.String">
        SELECT
            r.`role_key`
        FROM
            `sys_user_role` ur
            LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
        WHERE
            ur.`user_id` = #{userId} AND
            r.`status` = 0 AND
            r.`del_flag` = 0
    </select>
</mapper>
getRoutersInterface

RoutersVo

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RoutersVo {
    
    

    private List<Menu> menus;
}

LoginController

    @GetMapping("getRouters")
    public ResponseResult<RoutersVo> getRouters(){
    
    
        Long userId = SecurityUtils.getUserId();
        //查询menu 结果是tree的形式
        List<Menu> menus = menuService.selectRouterMenuTreeByUserId(userId);
        //封装数据返回
        return ResponseResult.okResult(new RoutersVo(menus));
    }

MenuService

public interface MenuService extends IService<Menu> {
    
    

    List<String> selectPermsByUserId(Long id);

    List<Menu> selectRouterMenuTreeByUserId(Long userId);
}

MenuServiceImpl

@Override
    public List<Menu> selectRouterMenuTreeByUserId(Long userId) {
    
    
        MenuMapper menuMapper = getBaseMapper();
        List<Menu> menus = null;
        //判断是否是管理员
        if(SecurityUtils.isAdmin()){
    
    
            //如果是 获取所有符合要求的Menu
            menus = menuMapper.selectAllRouterMenu();
        }else{
    
    
            //否则  获取当前用户所具有的Menu
            menus = menuMapper.selectRouterMenuTreeByUserId(userId);
        }

        //构建tree
        //先找出第一层的菜单  然后去找他们的子菜单设置到children属性中
        List<Menu> menuTree = builderMenuTree(menus,0L);
        return menuTree;
    }

    private List<Menu> builderMenuTree(List<Menu> menus, Long parentId) {
    
    
        List<Menu> menuTree = menus.stream()
                .filter(menu -> menu.getParentId().equals(parentId))
                .map(menu -> menu.setChildren(getChildren(menu, menus)))
                .collect(Collectors.toList());
        return menuTree;
    }

    /**
     * 获取存入参数的 子Menu集合
     * @param menu
     * @param menus
     * @return
     */
    private List<Menu> getChildren(Menu menu, List<Menu> menus) {
    
    
        List<Menu> childrenList = menus.stream()
                .filter(m -> m.getParentId().equals(menu.getId()))
                .map(m->m.setChildren(getChildren(m,menus)))
                .collect(Collectors.toList());
        return childrenList;
    }

MenuMapper.java

    List<Menu> selectAllRouterMenu();

    List<Menu> selectRouterMenuTreeByUserId(Long userId);

MenuMapper.xml

 <select id="selectAllRouterMenu" resultType="com.sangeng.domain.entity.Menu">
        SELECT
          DISTINCT m.id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, IFNULL(m.perms,'') AS perms, m.is_frame,  m.menu_type, m.icon, m.order_num, m.create_time
        FROM
            `sys_menu` m
        WHERE
            m.`menu_type` IN ('C','M') AND
            m.`status` = 0 AND
            m.`del_flag` = 0
        ORDER BY
            m.parent_id,m.order_num
    </select>
    <select id="selectRouterMenuTreeByUserId" resultType="com.sangeng.domain.entity.Menu">
        SELECT
          DISTINCT m.id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, IFNULL(m.perms,'') AS perms, m.is_frame,  m.menu_type, m.icon, m.order_num, m.create_time
        FROM
            `sys_user_role` ur
            LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
            LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
        WHERE
            ur.`user_id` = #{userId} AND
            m.`menu_type` IN ('C','M') AND
            m.`status` = 0 AND
            m.`del_flag` = 0
        ORDER BY
            m.parent_id,m.order_num
    </select>

Query columns:

SELECT DISTINCT m.id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, IFNULL(m.perms,‘’) AS perms, m.is_frame, m.menu_type, m.icon, m.order_num, m.create_time

Note that you need to sort by parent_id and order_num

5.3 Log out of the login interface

5.3.1 Interface design
Request method Request address Request header
POST /user/logout Requires token request header

Response format:

{
    
    
    "code": 200,
    "msg": "操作成功"
}
5.3.2 Code implementation

What to do:

​Delete user information in redis

LoginController

    @PostMapping("/user/logout")
    public ResponseResult logout(){
    
    
        return loginServcie.logout();
    }

LoginService

ResponseResult logout();

SystemLoginServiceImpl

    @Override
    public ResponseResult logout() {
    
    
        //获取当前登录的用户id
        Long userId = SecurityUtils.getUserId();
        //删除redis中对应的值
        redisCache.deleteObject("login:"+userId);
        return ResponseResult.okResult();
    }

SecurityConfig

To turn off the default logout feature. And to configure our logout interface, authentication is required to access it.

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
//                //注销接口需要认证才能访问
//                .antMatchers("/logout").authenticated()
//                .antMatchers("/user/userInfo").authenticated()
//                .antMatchers("/upload").authenticated()
                // 除上面外的所有请求全部不需要认证即可访问
                .anyRequest().authenticated();

        //配置异常处理器
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);
        //关闭默认的注销功能
        http.logout().disable();
        //把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //允许跨域
        http.cors();
    }

5.4 Query tag list

5.4.0 Requirements

In order to facilitate the later management of articles, the tag function needs to be provided. An article can have multiple tags.

​ In the background, we need a paging tag query function, which requires paging query based on the tag name. In the future, requirements such as remark inquiries may be added .

Note: Deleted tags cannot be queried.

5.4.1 Tag table analysis

​ Analyze what fields are needed through requirements.

5.4.2 Interface design
Request method Request path
Get content/tag/list

Query format request parameters:

pageNum: page number

pageSize: number of items per page

name: tag name

remark: remarks

Response format:

{
    
    
	"code":200,
	"data":{
    
    
		"rows":[
			{
    
    
				"id":4,
				"name":"Java",
				"remark":"sdad"
			}
		],
		"total":1
	},
	"msg":"操作成功"
}
5.4.3 Code implementation

Controller

@RestController
@RequestMapping("/content/tag")
public class TagController {
    
    
    @Autowired
    private TagService tagService;

    @GetMapping("/list")
    public ResponseResult<PageVo> list(Integer pageNum, Integer pageSize, TagListDto tagListDto){
    
    
        return tagService.pageTagList(pageNum,pageSize,tagListDto);
    }
}


Service

public interface TagService extends IService<Tag> {
    
    

    ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto);
}

@Service("tagService")
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {
    
    

    @Override
    public ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto) {
    
    
        //分页查询
        LambdaQueryWrapper<Tag> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(StringUtils.hasText(tagListDto.getName()),Tag::getName,tagListDto.getName());
        queryWrapper.eq(StringUtils.hasText(tagListDto.getRemark()),Tag::getRemark,tagListDto.getRemark());

        Page<Tag> page = new Page<>();
        page.setCurrent(pageNum);
        page.setSize(pageSize);
        page(page, queryWrapper);
        //封装数据返回
        PageVo pageVo = new PageVo(page.getRecords(),page.getTotal());
        return ResponseResult.okResult(pageVo);
    }
}
  • 以下代码都是自己手敲的,配有 相关知识点。

5.5 Add new tags

5.5.0 Requirements

​Click the new button of tag management to realize the function of adding new tags.

5.5.1 接口设计
请求方式 请求地址 请求头
POST /content/tag 需要token请求头

请求体格式:

{
    
    "name":"c#","remark":"c++++"}

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}
5.5.2 实现

Insert image description here

Insert image description here
创建时间,修改时间 的日期格式 这里是在 service中实现,也可以在 对应的实体类Tag中添加注解实现。

  • @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")

Insert image description here

5.6 删除标签

5.6.1 接口设计
请求方式 请求地址 请求头
DELETE /content/tag/{id} 需要token请求头

请求参数在path中

例如:content/tag/6 代表删除id为6的标签数据

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}
5.6.2 实现

删除后在列表中是查看不到该条数据

数据库中该条数据还是存在的,只是修改了逻辑删除字段的值:
需要再配置文件中设置
Insert image description here

Insert image description hereInsert image description here

5.7 修改标签

5.7.1 接口设计
5.7.1.1 获取标签信息
请求方式 请求地址 请求头
GET /content/tag/{id} 需要token请求头

请求参数在path中

例如:content/tag/6 代表获取id为6的标签数据

响应格式:

{
    
    
	"code":200,
	"data":{
    
    
        "id":4,
        "name":"Java",
        "remark":"sdad"
	},
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here

5.7.1.2 修改标签接口
请求方式 请求地址 请求头
PUT /content/tag 需要token请求头

请求体格式:

{
    
    "id":7,"name":"c#","remark":"c++++"}

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现

Insert image description here

Insert image description here

5.8 写博文

5.8.1 需求

​ 需要提供写博文的功能,写博文时需要关联分类和标签。

​ 可以上传缩略图,也可以在正文中添加图片。

​ 文章可以直接发布,也可以保存到草稿箱。

5.8.2 表分析

写文章,文章对应有标签,文章还有分类
Insert image description here

Insert image description here

5.8.2 接口设计
5.8.2.1 查询所有分类接口
请求方式 请求地址 请求头
GET /content/category/listAllCategory 需要token请求头

请求参数:

​ 无

响应格式:

{
    
    
	"code":200,
	"data":[
		{
    
    
			"description":"wsd",
			"id":1,
			"name":"java"
		},
		{
    
    
			"description":"wsd",
			"id":2,
			"name":"PHP"
		}
	],
	"msg":"操作成功"
}

实现:

Insert image description here
Insert image description here
Insert image description here

5.8.2.2 查询所有标签接口
请求方式 请求地址 请求头
GET /content/tag/listAllTag 需要token请求头

请求参数:

​ 无

响应格式:

{
    
    
	"code":200,
	"data":[
		{
    
    
			"id":1,
			"name":"Mybatis"
		},
		{
    
    
			"id":4,
			"name":"Java"
		}
	],
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here

5.8.2.3 上传图片
请求方式 请求地址 请求头
POST /upload 需要token请求头

参数:

​ img,值为要上传的文件

请求头:

​ Content-Type :multipart/form-data;

响应格式:

{
    
    
    "code": 200,
    "data": "文件访问链接",
    "msg": "操作成功"
}

实现

Insert image description here
serviceImp

package com.sangeng.service.impl;
import ...

@Service
@Data //生成getter和sertter方法,这里三个成员变量主要使用getter方法进行赋值
@ConfigurationProperties(prefix = "oss") //@ConfigurationProperties 的作用: 让JavaBean中属性值要和配置文件application.xml进行映射
public class OssUploadService implements UploadService {
    
    
    @Override
    public ResponseResult uploadImg(MultipartFile img) {
    
     //img是网络中传过来的流对象
        //判断文件类型
            //获取原始文件名
        String originalFilename = img.getOriginalFilename(); //得到上传时的文件名(本机需要上传的文件名,比如111.jpg上传到oss)
        //对原始文件名进行判断(只接受.png .jpg格式文件)
        if (!originalFilename.endsWith(".png") && !originalFilename.endsWith(".jpg")){
    
    
            //如果文件名 不是以 .png 结尾,抛出异常
            throw new SystemException(AppHttpCodeEnum.FILE_TYPE_ERROR);
        }
        //如果判断通过,则上传文件到OSS
//        根据当前文件名,生成一个存放路径 2023/6.14/uuid.png  或.jpg
        String filePath = PathUtils.generateFilePath(originalFilename);// 2022/1/15/+uuid+.jpg 得到文件路径
        String url = uploadOss(img,filePath); //上传文件至OSS后返回一个外链(可以通过此外链url 访问OSS中上传的图片)

        return ResponseResult.okResult(url); //返回结果data是外链 值
    }

    private String accessKey;
    private String secretKey;
    private String bucket;

    private String uploadOss(MultipartFile imgFile, String filePath){
    
    
//        注:用七牛云的oss,所以导包的时候 要导入七牛云的包。     用人家的代码,导人家的包 com.qiniu.storage
        //构造一个带指定 Region 对象的配置类
//修改1.Region指定数据存储区域,autoRegion()自动根据七牛云账号找到选的区域(我选的是 华北)
        Configuration cfg = new Configuration(Region.autoRegion());
        cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2;// 指定分片上传版本
        UploadManager uploadManager = new UploadManager(cfg);
//...生成上传凭证,然后准备上传

   注:为了安全起见,AK,SK,bucket存储空间名,都是从application.xml 配置文件中读取到的
修改2.复制七牛云官网-个人中心-密钥管理-  AK和SK
//        String accessKey = "";
//        String secretKey = "";
修改3.创建存储空间的名字 pk-sg-blog
//        String bucket = "";

//默认不指定key的情况下,以文件内容的hash值作为文件名,  比如上传一张图片,名字为hash值生成的名字
//修改4.指定上传文件到oss时,文件的存储名
        String key = filePath; // 指定生成路径
        try {
    
    
//修改5 注释掉默认上传,改成 前端传过来的文件流
//            byte[] uploadBytes = "hello qiniu cloud".getBytes("utf-8");
//            ByteArrayInputStream byteInputStream=new ByteArrayInputStream(uploadBytes);
//上传文件——前端传过来的图片
//            获取到网络中传过来的流对象的inputstream
            InputStream inputStream = imgFile.getInputStream();//前端传过来的文件转成inputstream流

            Auth auth = Auth.create(accessKey, secretKey);//创建凭证
            String upToken = auth.uploadToken(bucket); //上传凭证
            try {
    
    
//修改6 put方法 第一个参数 要放上面 自己定义的 inputStream对象
                Response response = uploadManager.put(inputStream,key,upToken,null, null);
                //解析上传成功的结果
                DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
                System.out.println(putRet.key); //111.png  key值就是上传后的 文件名
                System.out.println(putRet.hash); //Fo2AVLRHugoNbek6XZ8Uy-DCnuSL
//              外链 http://rw7y62wqd.hb-bkt.clouddn.com/111.png
//                外链= http://+ 测试域名+文件名               七牛云测试域名,免费用30天,过期回收域名
                return "http://rw7y62wqd.hb-bkt.clouddn.com/"+key;   //上传成功,返回一个对应的外链
            } catch (QiniuException ex) {
    
    
                Response r = ex.response;
                System.err.println(r.toString());
                try {
    
    
                    System.err.println(r.bodyString());
                } catch (QiniuException ex2) {
    
    
                    //ignore
                }
            }
        } catch (Exception ex) {
    
    
            //ignore  异常类型 改大一点
        }
        return "www";
    }

}

5.8.2.4 新增博文
请求方式 请求地址 请求头
POST /content/article 需要token请求头

请求体格式:

{
    
    
    "title":"测试新增博文",
    "thumbnail":"https://sg-blog-oss.oss-cn-beijing.aliyuncs.com/2022/08/21/4ceebc07e7484beba732f12b0d2c43a9.png",
    "isTop":"0",
    "isComment":"0",
    "content":"# 一级标题\n## 二级标题\n![Snipaste_20220228_224837.png](https://img-blog.csdnimg.cn/img_convert/9a98336c43a0605a50ef539e6558f380.png)\n正文",
    "tags":[
        1,
        4
    ],
    "categoryId":1,
    "summary":"哈哈",
    "status":"1"
}

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here
Insert image description here

5.8.3 Dto+自动填充

AddArticleDto

注意增加tags属性用于接收文章关联标签的id

Insert image description here

Article 修改这样创建时间创建人修改时间修改人可以自动填充(需要mybatisplus配置文件,配置自动填充拦截器)

    @TableField(fill = FieldFill.INSERT)
    private Long createBy;
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateBy;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;

Insert image description here
代码

package com.sangeng.handler.mybatisplus;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.sangeng.utils.SecurityUtils;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.util.Date;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    
    
    //配置自动填充 拦截器
    //insert操作时填充方法
    @Override
    public void insertFill(MetaObject metaObject) {
    
    
        Long userId = null;
//        try {
    
    
            userId = SecurityUtils.getUserId(); //从token中拿到userid,自动填充
//        } catch (Exception e) {
    
    
//            e.printStackTrace();
//        注册的时候,不能获取当前userid,所以userid设置为-1
//            userId = -1L;//表示是自己创建
//        }
        this.setFieldValByName("createTime", new Date(), metaObject);
        this.setFieldValByName("createBy",userId , metaObject);
        this.setFieldValByName("updateTime", new Date(), metaObject);
        this.setFieldValByName("updateBy", userId, metaObject);
    }
    //update操作时填充方法
    @Override
    public void updateFill(MetaObject metaObject) {
    
    
        this.setFieldValByName("updateTime", new Date(), metaObject);
        this.setFieldValByName(" ", SecurityUtils.getUserId(), metaObject);
    }
}

5.9 导出所有分类到Excel

5.9.1 需求

​ 分类管理 导出按钮 把所有分类 导出到 Excel文件中。

5.9.2 技术方案

​ 使用EasyExcel实现Excel的导出操作。

​ https://github.com/alibaba/easyexcel

EasyExcel官网

5.9.3 接口设计

请求方式 请求地址 请求头
GET /content/category/export 需要token请求头

请求参数:

​ 无

响应格式:

成功的话可以直接导出一个Excel文件

失败的话响应格式如下:

{
    
    
	"code":500,
	"msg":"出现错误"
}
5.9.4 代码实现

工具类方法修改
Insert image description here
Insert image description here
Insert image description here

WebUtils

    public static void setDownLoadHeader(String filename, HttpServletResponse response) throws UnsupportedEncodingException {
    
    
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String fname= URLEncoder.encode(filename,"UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition","attachment; filename="+fname);
    }

CategoryController

@GetMapping("/export")
    public void export(HttpServletResponse response){
    
    
        //设置下载文件的请求头
        try {
    
    
            WebUtils.setDownLoadHeader("分类.xlsx",response); //设置 响应头,excel的名字
            //获取需要导出的数据   查询到所有的分类数据
            List<Category> categoryVos = categoryService.list();

            List<ExcelCategoryVo> excelCategoryVos = BeanCopyUtils.copyBeanList(categoryVos, ExcelCategoryVo.class);

            //把数据写入都excel中  write(输出流,封装实体类对象)
            EasyExcel.write(response.getOutputStream(), ExcelCategoryVo.class).autoCloseStream(Boolean.FALSE).sheet("sheet_分类导出")
                    .doWrite(excelCategoryVos);  // 要导出的数据excelCategoryVos, 写到excel中
        } catch (Exception e) {
    
    
            //如果出现异常 也要相应json
            ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR);
            WebUtils.renderString(response, JSON.toJSONString(result)); // 把json写入到响应当中
        }

    }

ExcelCategoryVo

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ExcelCategoryVo {
    
    
    @ExcelProperty("分类名")
    private String name;
    //描述
    @ExcelProperty("描述")
    private String description;

    //状态0:正常,1禁用
    @ExcelProperty("状态0:正常,1禁用")
    private String status;
}

5.10 权限控制

5.10.1 需求

​ 对导出分类的接口 做权限控制。

5.10.2 代码实现

Insert image description here
Insert image description here

SecurityConfig

@EnableGlobalMethodSecurity(prePostEnabled = true)

UserDetailsServiceImpl

package com.sangeng.service.impl;
import ...

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private MenuMapper menuMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
        //security会使用UserDetailsService实现类中的loadUserByUsername方法进行校验
        //根据用户名查询用户信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(queryWrapper);
        //判断是否查到用户  如果没查到抛出异常
        if (Objects.isNull(user)){
    
     throw new RuntimeException("用户不存在"); }
        //返回用户信息    返回的是UserDatails对象,后面才做密码校验   密码SpringSecurity自动校验
        //TODO 查询权限信息封装 如果是后台用户才需要查询权限封装 (前台用户不需要查询权限)
        if (user.getType().equals(SystemConstants.ADMIN)){
    
    
            List<String> perms = menuMapper.selectPermsByUserId(user.getId()); //权限列表
            return new LoginUser(user,perms);
        }
        //  定义一个loginUser 实现 UserDetails,即可返回
        return new LoginUser(user,null); //UserDetails对象(权限信息 + 用户信息)
    }
}

LoginUser

增加属性 private List permissions;

import ...
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
    
    
//    该类 存储用户信息 + 权限信息
    private User user; //user封装成 LoginUser的成员变量,存储用户信息

    //(后台管理员)用户权限
    private List<String> permission;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
        //返回一个用户的 权限集合   springsecurity是通过这个方法 获取权限 (可以将permission转换成 该方法类型并返回)
        return null;
    }

    @Override
    public String getPassword() {
    
    
        return user.getPassword();
    }

    @Override
    public String getUsername() {
    
    
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
    
    
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
    
    
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
    
    
        return true;
    }

    @Override
    public boolean isEnabled() {
    
    
        return true;
    }
}

Insert image description here

PermissionService

hasPermisson


@Service("ps")  //对应自定义权限中定义的方法
// @PreAuthorize("@ps.hasPermission('content:category:export')") @GetMapping("/export")
public class PermissionService {
    
    
//    定义注解hasPermission
    /**
     * 判断当前用户是否具有permission  (权限)
     * @param permission 要判断的权限
     * @return 是 否  具有权限
     */
    public boolean hasPermission(String permission){
    
    
        //如果是超级管理员,直接返回true
        if (SecurityUtils.isAdmin()){
    
    
            return true;
        }
        //否则 获取当前登录用户所具有的权限列表,然后 判断是否具有permission
        List<String> perms = SecurityUtils.getLoginUser().getPermission();
        return perms.contains(permission);
    }
}

Insert image description here

CategoryController

    @PreAuthorize("@ps.hasPermission('content:category:export')") //自定义权限,看登录的用户是否有 这个权限,有才能访问该请求。 要实现@Service("ps") public class PermissionService {}
    @GetMapping("/export")
    public void export(HttpServletResponse response){
    
    
        //设置下载文件的请求头
        try {
    
    
            WebUtils.setDownLoadHeader("分类.xlsx",response); //设置 响应头,excel的名字
            //获取需要导出的数据   查询到所有的分类数据
            List<Category> categoryVos = categoryService.list();

            List<ExcelCategoryVo> excelCategoryVos = BeanCopyUtils.copyBeanList(categoryVos, ExcelCategoryVo.class);

            //把数据写入都excel中  write(输出流,封装实体类对象)
            EasyExcel.write(response.getOutputStream(), ExcelCategoryVo.class).autoCloseStream(Boolean.FALSE).sheet("sheet_分类导出")
                    .doWrite(excelCategoryVos);  // 要导出的数据excelCategoryVos, 写到excel中
        } catch (Exception e) {
    
    
            //如果出现异常 也要相应json
            ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR);
            WebUtils.renderString(response, JSON.toJSONString(result)); // 把json写入到响应当中
        }

    }

Insert image description here

5.11 文章列表

5.10.1 需求

​ 为了对文章进行管理,需要提供文章列表,

​ 在后台需要分页查询文章功能,要求能根据标题和摘要模糊查询

​ 注意:不能把删除了的文章查询出来

5.10.2 接口设计
请求方式 请求路径 是否需求token头
Get /content/article/list

Query格式请求参数:

pageNum: 页码

pageSize: 每页条数

title:文章标题

summary:文章摘要

响应格式:

{
    
    
	"code":200,
	"data":{
    
    
		"rows":[
			{
    
    
				"categoryId":"1",
				"content":"嘻嘻嘻嘻嘻嘻",
				"createTime":"2022-01-24 07:20:11",
				"id":"1",
				"isComment":"0",
				"isTop":"1",
				"status":"0",
				"summary":"SpringSecurity框架教程-Spring Security+JWT实现项目级前端分离认证授权",
				"thumbnail":"https://sg-blog-oss.oss-cn-beijing.aliyuncs.com/2022/01/31/948597e164614902ab1662ba8452e106.png",
				"title":"SpringSecurity从入门到精通",
				"viewCount":"161"
			}
		],
		"total":"1"
	},
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here
Insert image description here

5.12 修改文章

5.12.1 需求

​ 点击文章列表中的修改按钮可以跳转到写博文页面。回显示该文章的具体信息。

​ 用户可以在该页面修改文章信息。点击更新按钮后修改文章。

5.12.2 分析

​ 首先根据文章id查询文章的详细信息 实现文章的回显。

5.12.3 接口设计
5.12.3.1 查询文章详情接口
请求方式 请求路径 是否需求token头
Get content/article/{id}

Path格式请求参数:

id: 文章id

响应格式:

{
    
    
	"code":200,
	"data":{
    
    
		"categoryId":"1",
		"content":"xxxxxxx",
		"createBy":"1",
		"createTime":"2022-08-28 15:15:46",
		"delFlag":0,
		"id":"10",
		"isComment":"0",
		"isTop":"1",
		"status":"0",
		"summary":"啊实打实",
		"tags":[
			"1",
			"4",
			"5"
		],
		"thumbnail":"https://sg-blog-oss.oss-cn-beijing.aliyuncs.com/2022/08/28/7659aac2b74247fe8ebd9e054b916dbf.png",
		"title":"委屈饿驱蚊器",
		"updateBy":"1",
		"updateTime":"2022-08-28 15:15:46",
		"viewCount":"0"
	},
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here
Insert image description here

5.12.3.2 更新文章接口
请求方式 请求路径 是否需求token头
PUT content/article

请求体参数格式:

{
    
    
    "categoryId":"1",
    "content":"![Snipaste_20220228_224837.png](https://img-blog.csdnimg.cn/img_convert/8ce7b01011cc38b1b3d9f6a3597ccb7d.png)\n\n# 十大\n## 时代的",
    "createBy":"1",
    "createTime":"2022-08-28 15:15:46",
    "delFlag":0,
    "id":"10",
    "isComment":"0",
    "isTop":"1",
    "status":"0",
    "summary":"啊实打实2",
    "tags":[
        "1",
        "4",
        "5"
    ],
    "thumbnail":"https://sg-blog-oss.oss-cn-beijing.aliyuncs.com/2022/08/28/7659aac2b74247fe8ebd9e054b916dbf.png",
    "title":"委屈饿驱蚊器",
    "updateBy":"1",
    "updateTime":"2022-08-28 15:15:46",
    "viewCount":"0"
}

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here
Insert image description here

5.13 删除文章

5.13.1 需求

​ 点击文章后面的删除按钮可以删除该文章

​ 注意:是逻辑删除不是物理删除

5.13.2 接口设计

请求方式 请求路径 是否需求token头
DELETE content/article/{id}

Path请求参数:

id:要删除的文章id

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here
Insert image description here

5.14 菜单列表

5.14.1 需求

​ 需要展示菜单列表,不需要分页。

​ 可以针对菜单名进行模糊查询

​ 也可以针对菜单的状态进行查询。

​ 菜单要按照父菜单id和orderNum进行排序

5.14.2 接口设计
请求方式 请求路径 是否需求token头
GET system/menu/list

Query请求参数:

status : 状态

menuName: 菜单名

响应格式:

{
    
    
	"code":200,
	"data":[
		{
    
    
			"component":"content/article/write/index",
			"icon":"build",
			"id":"2023",
			"isFrame":1,
			"menuName":"写博文",
			"menuType":"C",
			"orderNum":0,
			"parentId":"0",
			"path":"write",
			"perms":"content:article:writer",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		...
		{
    
    
			"icon":"#",
			"id":"2026",
			"isFrame":1,
			"menuName":"友链删除",
			"menuType":"F",
			"orderNum":1,
			"parentId":"2022",
			"path":"",
			"perms":"content:link:remove",
			"remark":"",
			"status":"0",
			"visible":"0"
		}
	],
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here
Insert image description here

5.15 新增菜单

5.15.1 需求

​ 新增菜单

5.15.2 接口设计

请求方式 请求路径 是否需求token头
POST content/menu

请求体参数:

​ Menu类对应的json格式

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here

5.16 修改菜单

5.16.1 需求

​ 能够修改菜单,但是修改的时候不能把父菜单设置为当前菜单,如果设置了需要给出相应的提示。并且修改失败。

5.16.2 接口设计
5.16.2.1 根据id查询菜单数据
请求方式 请求路径 是否需求token头
Get system/menu/{id}

Path格式请求参数:

id: 菜单id

响应格式:

{
    
    
	"code":200,
	"data":{
    
    
		"icon":"table",
		"id":"2017",
		"menuName":"内容管理",
		"menuType":"M",
		"orderNum":"4",
		"parentId":"0",
		"path":"content",
		"remark":"",
		"status":"0",
		"visible":"0"
	},
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here
Insert image description here

5.16.2.2 更新菜单
请求方式 请求路径 是否需求token头
PUT system/menu

请求体参数:

​ Menu类对应的json格式

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

如果把父菜单设置为当前菜单:

{
    
    
	"code":500,
	"msg":"修改菜单'写博文'失败,上级菜单不能选择自己"
}

实现
Insert image description here
Insert image description here
Insert image description here

5.17 删除菜单

5.17.1 需求

​ 能够删除菜单,但是如果要删除的菜单有子菜单则提示:存在子菜单不允许删除 并且删除失败。

5.17.2 接口设计
请求方式 请求路径 是否需求token头
DELETE content/article/{menuId}

Path参数:

menuId:要删除菜单的id

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

如果要删除的菜单有子菜单则

{
    
    
	"code":500,
	"msg":"存在子菜单不允许删除"
}

实现
Insert image description here

5.18 角色列表

5.18.1 需求

​ 需要有角色列表分页查询的功能。

​ 要求能够针对角色名称进行模糊查询。

​ 要求能够针对状态进行查询。

​ 要求按照role_sort进行升序排列。

5.18.2 接口设计
请求方式 请求路径 是否需求token头
GET system/role/list

Query格式请求参数:

pageNum: 页码

pageSize: 每页条数

roleName:角色名称

status:状态

响应格式:

{
    
    
	"code":200,
	"data":{
    
    
		"rows":[
			{
    
    
				"id":"12",
				"roleKey":"link",
				"roleName":"友链审核员",
				"roleSort":"1",
				"status":"0"
			}
		],
		"total":"1"
	},
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here

Insert image description here

5.19 改变角色状态

5.19.1 需求

修改角色的停启用状态

5.19.2 接口设计
请求方式 请求路径 是否需求token头
PUT system/role/changeStatus

请求体:

{
    
    "roleId":"11","status":"1"}

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现Insert image description here
Insert image description here

Insert image description here

5.20 新增角色!!

5.20.1 需求

​ 新增角色时能够直接设置角色所关联的菜单权限。

5.20.2 接口设计
5.20.2.1 获取菜单树接口
请求方式 请求路径 是否需求token头
GET /system/menu/treeselect

无需请求参数

响应格式:

{
    
    
	"code":200,
	"data":[
		{
    
    
			"children":[],
			"id":"2023",
			"label":"写博文",
			"parentId":"0"
		},
		{
    
    
			"children":[
							{
    
    
							"children":[
											{
    
    
												"children":[],
												"id":"1001",
												"label":"用户查询",
												"parentId":"100"
											},
												...
									  ]
						],
			"id":"2017",
			"label":"内容管理",
			"parentId":"0"
		}
	],
	"msg":"操作成功"
}

实现: 三级子菜单功能 + 二级子菜单功能
Insert image description here
ServiceImpl 三级子菜单功能

//    增加用户角色时,展示菜单的三级目录
    @Override
    public ResponseResult treeSelect() {
    
    
        //1. Menu表中的所有数据 转成MenuTreeVo集合        并设置(id,label,parentId)
        // Menu -> MenuTreeVo (id,label,parentId,children=null)
        List<MenuTreeVo> menuTreeVo = list().stream()
                .map(menu -> new MenuTreeVo(menu.getId(), menu.getMenuName(), menu.getParentId(), null))
                .collect(Collectors.toList());
        //2. 根据parent_id(参数2) 将列表menuTreeVo(参数1)建立成 3级菜单树      函数名builderMenuTree2
        List<MenuTreeVo> menuTreeVo1 = builderMenuTree2(menuTreeVo,0L);
        return ResponseResult.okResult(menuTreeVo1);
    }

    private List<MenuTreeVo> builderMenuTree2(List<MenuTreeVo> menuTreeVo, long parentId) {
    
    
        //第一层
        List<MenuTreeVo> collect = menuTreeVo.stream()
                .filter(m -> m.getParentId().equals(parentId)) //过滤出 父菜单
                //getChildren2(menuTreeVo, m1) 根据m1 在 menuTreeVo列表中 循环找子菜单(m1表示3个父菜单)
                .map(m1 -> m1.setChildren(getChildren2(menuTreeVo, m1)))  //第2,3层——循环设置children,
                .collect(Collectors.toList());
        return collect;
    }

    private List<MenuTreeVo> getChildren2(List<MenuTreeVo> menuTreeVo, MenuTreeVo m1) {
    
    
        //getChildren2(menuTreeVo, m1) 根据m1(m1表示3个父菜单) 在 menuTreeVo列表中 循环找子菜单
        List<MenuTreeVo> collect2 = menuTreeVo.stream()
                .filter(m2 -> m2.getParentId().equals(m1.getId())) //找1个父菜单的子菜单
                .map(m3 -> m3.setChildren(getChildren2(menuTreeVo, m3))) //第2,3层递归设置children,
                .collect(Collectors.toList());
        return collect2;
    }

ServiceImpl 二级子菜单功能

//    增加用户角色时,展示菜单的 二级目录
    @Override
    public ResponseResult treeSelect() {
    
    
        //1.找出parentId=0的根菜单 存到列表中
        LambdaQueryWrapper<Menu> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Menu::getParentId, 0);
        Page<Menu> page = new Page<>();
        page(page, queryWrapper);  //根据queryWrapper查询
        //2 转成MenuTreeVo集合   Menu -> MenuTreeVo (children,id,label,parentId)
        //2.1 Menu -> MenuTreeVo (id,label,parentId)
        List<MenuTreeVo> menuTreeVos = toMenuTreeVo(page.getRecords());

        //2.2 查询所有根菜单的子菜单 for循环 找出各个children ,设置到children属性
        // 解决(children)
        //查询所有根菜单对应的子菜单集合,并且赋值给对应属性
        //从parentId != 0 (表示子菜单) 开始查,因为 parentId==0是根菜单,根菜单不可能是 某个菜单的子菜单
        for (MenuTreeVo menuTreeVo : menuTreeVos) {
    
     //从根菜单中找 子菜单
                List<MenuTreeVo> children = getTreeChildren(menuTreeVo.getId());
//                if (treeVos.size() != 0){  //当有子菜单才设置子菜单,否则不设置(不然的话,子菜单为空,将子菜单设置成了一个空地址)
                    menuTreeVo.setChildren(children);
//                }
        }
        return ResponseResult.okResult(menuTreeVos);
    }

    private List<MenuTreeVo> getTreeChildren(Long id) {
    
    
        //菜单的id 等于 根菜单id,则该条菜单 为子菜单
        LambdaQueryWrapper<Menu> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Menu::getParentId,id); //Menu表中parentId = id的数据
        queryWrapper.orderByAsc(Menu::getId); //根据id升序

        List<Menu> menus = list(queryWrapper);

//        转成MenuTreeVo集合
        List<MenuTreeVo> menuTreeVos = toMenuTreeVo(menus);
        return menuTreeVos;
    }

    private List<MenuTreeVo> toMenuTreeVo(List<Menu> lists) {
    
    
//        Menu -> MenuTreeVo (id,label,parentId)
//        解决(id,parentId)
        List<MenuTreeVo> menuTreeVos = BeanCopyUtils.copyBeanList(lists, MenuTreeVo.class);
//        解决(children,label)
        for (MenuTreeVo menuTreeVo : menuTreeVos) {
    
     //遍历上面的 copyBeanList的列表
            //        解决(label)
            if (menuTreeVo.getId()!=0){
    
     //如果Id !=0 才查询 (Id = 0 表示根菜单,根据0查用户,会查到null)
                Menu menu = menuService.getById(menuTreeVo.getId()); //查到对应的menu
                menuTreeVo.setLabel(menu.getMenuName());
            }
        }
        return menuTreeVos;
    }
5.20.2.2 新增角色接口
请求方式 请求路径 是否需求token头
POST system/role

请求体:

{
    
    
    "roleName":"测试新增角色",
    "roleKey":"wds",
    "roleSort":0,
    "status":"0",
    "menuIds":[
        "1",
        "100"
    ],
    "remark":"我是角色备注"
}

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here

Insert image description here

5.21 修改角色

5.21.1 需求

​ 修改角色时可以修改角色所关联的菜单权限

5.21.2 接口设计
5.21.2.1 角色信息回显接口
请求方式 请求路径 是否需求token头
Get system/role/{id}

Path格式请求参数:

id: 角色id

响应格式:

{
    
    
	"code":200,
	"data":{
    
    
		"id":"11",
		"remark":"嘎嘎嘎",
		"roleKey":"aggag",
		"roleName":"嘎嘎嘎",
		"roleSort":"5",
		"status":"0"
	},
	"msg":"操作成功"
}

实现

Insert image description here
Insert image description here
Insert image description here

5.21.2.2 加载对应角色菜单列表树接口
请求方式 请求路径 是否需求token头
Get /system/menu/roleMenuTreeselect/{id}

Path格式请求参数:

id: 角色id

响应格式:

字段介绍

​ menus:菜单树。

​ checkedKeys:角色所关联的菜单权限id列表。

{
    
    
	"code":200,
	"data":{
    
    
		"menus":[
			{
    
    
				"children":[],
				"id":"2023",
				"label":"写博文",
				"parentId":"0"
			},
			
			{
    
    
				"children":[
					{
    
    
						"children":[],
						"id":"2019",
						"label":"文章管理",
						"parentId":"2017"
					},
					{
    
    
						"children":[
							{
    
    
								"children":[],
								"id":"2028",
								"label":"导出分类",
								"parentId":"2018"
							}
						],
						"id":"2018",
						"label":"分类管理",
						"parentId":"2017"
					},
					{
    
    
						"children":[
							{
    
    
								"children":[],
								"id":"2024",
								"label":"友链新增",
								"parentId":"2022"
							},
							{
    
    
								"children":[],
								"id":"2025",
								"label":"友链修改",
								"parentId":"2022"
							},
							{
    
    
								"children":[],
								"id":"2026",
								"label":"友链删除",
								"parentId":"2022"
							},
							{
    
    
								"children":[],
								"id":"2027",
								"label":"友链查询",
								"parentId":"2022"
							}
						],
						"id":"2022",
						"label":"友链管理",
						"parentId":"2017"
					},
					{
    
    
						"children":[],
						"id":"2021",
						"label":"标签管理",
						"parentId":"2017"
					}
				],
				"id":"2017",
				"label":"内容管理",
				"parentId":"0"
			}
		],
		"checkedKeys":[
			"1001"  
		]
	},
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here
RoleMenuServiceImpl

    @Autowired
    private RoleMenuService roleMenuService;
    @Override
    public ResponseResult roleMenuTreeselectById(Long id) {
    
     //参数id是 角色id
        RoleMenuTreeVo roleMenuTreeVo = new RoleMenuTreeVo();
        //1.设置menus 把3级菜单展示出来
//        Menu -> MenuTreeVo集合
        List<MenuTreeVo> menuTreeVo = list().stream()
                .map(menu -> new MenuTreeVo(menu.getId(), menu.getMenuName(), menu.getParentId(), null))
                .collect(Collectors.toList());
        //根据parent_id(参数2) 将列表menuTreeVo(参数1)建立成 3级菜单树      函数名builderMenuTree2
        List<MenuTreeVo> menus = builderMenuTree2(menuTreeVo,0);

        roleMenuTreeVo.setMenus(menus);

        //2.设置checkedKeys(3级菜单中 用户id所具有的权限啊展示出来) 用户角色所关联的 菜单id
        LambdaQueryWrapper<RoleMenu> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(RoleMenu::getRoleId,id);
        List<RoleMenu> roleMenus = roleMenuService.list(queryWrapper);
        List<String> menuIds = roleMenus.stream()
                .map(roleMenu -> roleMenu.getMenuId().toString())
                .collect(Collectors.toList());
        roleMenuTreeVo.setCheckedKeys(menuIds);
        return ResponseResult.okResult(roleMenuTreeVo);
    }
    
    private List<MenuTreeVo> builderMenuTree2(List<MenuTreeVo> menuTreeVo, long parentId) {
    
    
        //第一层
        List<MenuTreeVo> collect = menuTreeVo.stream()
                .filter(m -> m.getParentId().equals(parentId)) //过滤出 父菜单
                //getChildren2(menuTreeVo, m1) 根据m1 在 menuTreeVo列表中 循环找子菜单(m1表示3个父菜单)
                .map(m1 -> m1.setChildren(getChildren2(menuTreeVo, m1)))  //第2,3层——循环设置children,
                .collect(Collectors.toList());
        return collect;
    }
    
    private List<MenuTreeVo> getChildren2(List<MenuTreeVo> menuTreeVo, MenuTreeVo m1) {
    
    
        //getChildren2(menuTreeVo, m1) 根据m1(m1表示3个父菜单) 在 menuTreeVo列表中 循环找子菜单
        List<MenuTreeVo> collect2 = menuTreeVo.stream()
                .filter(m2 -> m2.getParentId().equals(m1.getId())) //找1个父菜单的子菜单
                .map(m3 -> m3.setChildren(getChildren2(menuTreeVo, m3))) //第2,3层递归设置children,
                .collect(Collectors.toList());
        return collect2;
    }
5.21.2.3 更新角色信息接口
请求方式 请求路径 是否需求token头
PUT system/role

请求体:

{
    
    
    "id":"13",
    "remark":"我是角色备注",
    "roleKey":"wds",
    "roleName":"测试新增角色",
    "roleSort":0,
    "status":"0",
    "menuIds":[
        "1",
        "100",
        "1001"
    ]
}

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here
Insert image description here

5.22 删除角色

5.22.1 需求

​ 删除固定的某个角色(逻辑删除)

5.22.2 接口设计

请求方式 请求路径 是否需求token头
DELETE system/role/{id}

Path请求参数:

id:要删除的角色id

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现:直接调用IService中的removeById()方法
Insert image description here

5.23 用户列表

5.23.1 需求

​ 需要用户分页列表接口。

​ 可以根据用户名模糊搜索。

​ 可以进行手机号的搜索。

​ 可以进行状态的查询。

5.23.2 接口设计
请求方式 请求路径 是否需求token头
GET system/user/list

Query格式请求参数:

pageNum: 页码

pageSize: 每页条数

userName:用户名

phonenumber:手机号

status:状态

响应格式:

{
    
    
	"code":200,
	"data":{
    
    
		"rows":[
			{
    
    
				"avatar":"http://r7yxkqloa.bkt.clouddn.com/2022/03/05/75fd15587811443a9a9a771f24da458d.png",
				"createTime":"2022-01-05 17:01:56",
				"email":"[email protected]",
				"id":"1",
				"nickName":"sg3334",
				"phonenumber":"18888888888",
				"sex":"1",
				"status":"0",
				"updateBy":"1",
				"updateTime":"2022-03-13 21:36:22",
				"userName":"sg"
			}
		],
		"total":"1"
	},
	"msg":"操作成功"
}

实现
Insert image description here

Insert image description here
Insert image description here

5.24 新增用户!!!

5.24.1 需求

​ 需要新增用户功能。新增用户时可以直接关联角色。

​ 注意:新增用户时注意密码加密存储。

​ 用户名不能为空,否则提示:必需填写用户名

​ 用户名必须之前未存在,否则提示:用户名已存在

​ 手机号必须之前未存在,否则提示:手机号已存在

​ 邮箱必须之前未存在,否则提示:邮箱已存在

5.24.2 接口设计
5.24.2.1 查询角色列表接口

注意:查询的是所有状态正常的角色

请求方式 请求路径 是否需求token头
GET /system/role/listAllRole

响应格式:

{
    
    
	"code":200,
	"data":[
		{
    
    
			"createBy":"0",
			"createTime":"2021-11-12 18:46:19",
			"delFlag":"0",
			"id":"2",
			"remark":"普通角色",
			"roleKey":"common",
			"roleName":"普通角色",
			"roleSort":"2",
			"status":"0",
			"updateBy":"0",
			"updateTime":"2022-01-02 06:32:58"
		},
		{
    
    
			"createTime":"2022-01-06 22:07:40",
			"delFlag":"0",
			"id":"11",
			"remark":"嘎嘎嘎",
			"roleKey":"aggag",
			"roleName":"嘎嘎嘎",
			"roleSort":"5",
			"status":"0",
			"updateBy":"1",
			"updateTime":"2022-09-12 10:00:25"
		},
		{
    
    
			"createTime":"2022-01-16 14:49:30",
			"delFlag":"0",
			"id":"12",
			"roleKey":"link",
			"roleName":"友链审核员",
			"roleSort":"1",
			"status":"0",
			"updateTime":"2022-01-16 16:05:09"
		}
	],
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here

Insert image description here

5.24.2.2 新增用户
请求方式 请求路径 是否需求token头
POST system/user

请求体:

{
    
    
    "userName":"wqeree",
    "nickName":"测试新增用户",
    "password":"1234343",
    "phonenumber":"18889778907",
    "email":"[email protected]",
    "sex":"0",
    "status":"0",
    "roleIds":[
        "2"
    ]
}

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here

@Override
    @Transactional
    public ResponseResult addUser(UserVo2 userVo2) {
    
    
        //校验
//        1.用户名不能为空,否则提示:必需填写用户名
        if (Objects.isNull(userVo2.getUserName())){
    
    
           throw new SystemException(AppHttpCodeEnum.USERNAME_NOT_NULL);
        }
//    	  2.用户名必须之前未存在,否则提示:用户名已存在
//        if (!userMapper.selectUserNameBoolean(userVo2.getUserName())){
    
    
//            throw new SystemException(AppHttpCodeEnum.USERNAME_EXIST);
//        }
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName,userVo2.getUserName());
        if (getOne(queryWrapper) != null){
    
    
            throw new SystemException(AppHttpCodeEnum.USERNAME_EXIST);
        }

//        3.手机号必须之前未存在,否则提示:手机号已存在
//        if (!userMapper.selectPhoneBoolean(userVo2.getPhonenumber())){
    
    
//            throw new SystemException(AppHttpCodeEnum.PHONENUMBER_EXIST)
//        }
        LambdaQueryWrapper<User> queryWrapper1 = new LambdaQueryWrapper<>();
        queryWrapper1.eq(User::getPhonenumber,userVo2.getPhonenumber());
        if (getOne(queryWrapper1) != null){
    
    
            throw new SystemException(AppHttpCodeEnum.PHONENUMBER_EXIST);
        }

//    	   4.邮箱必须之前未存在,否则提示:邮箱已存在
//        if (!userMapper.selectEmailBoolean(userVo2.getEmail())){
    
    
//            throw new SystemException(AppHttpCodeEnum.EMAIL_EXIST);
//        }
        LambdaQueryWrapper<User> queryWrapper2 = new LambdaQueryWrapper<>();
        queryWrapper2.eq(User::getEmail,userVo2.getEmail());
        if (getOne(queryWrapper2) != null){
    
    
            throw new SystemException(AppHttpCodeEnum.EMAIL_EXIST);
        }
        //1.保存到 user表
        User user = BeanCopyUtils.copyBean(userVo2, User.class);
//        5.新增用户时注意密码加密存储。
        //对密码进行加密
        String encodePassword = passwordEncoder.encode(user.getPassword());//对明文密码 进行加密,得到密文
        user.setPassword(encodePassword); //将password加密后的密文,存到user中
        save(user);  //新增一条用户记录

        //2.保存到user_role表
        Long userId = user.getId();
        List<UserRole> userRoleList = userVo2.getRoleIds().stream()
                .map(roleId -> new UserRole(userId, Long.parseLong(roleId)))
                .collect(Collectors.toList());
        userRoleService.saveBatch(userRoleList);
        return ResponseResult.okResult();
    }

5.25 删除用户

5.25.1 需求

删除固定的某个用户(逻辑删除)

5.25.2 接口设计

不能删除当前操作的用户

请求方式 请求路径 是否需求token头
DELETE /system/user/{id}

Path请求参数:

id:要删除的用户id

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现
controller层 事务
Insert image description here

5.26 修改用户

5.26.1 需求

需要提供修改用户的功能。修改用户时可以修改用户所关联的角色。

5.26.2 接口设计
5.26.2.1 根据id查询用户信息回显接口
请求方式 请求路径 是否需求token头
Get /system/user/{id}

Path格式请求参数:

id: 用户id

响应格式:

roleIds:用户所关联的角色id列表

roles:所有角色的列表

user:用户信息

{
    
    
	"code":200,
	"data":{
    
    
		"roleIds":[
			"11"
		],
		"roles":[
			{
    
    
				"createBy":"0",
				"createTime":"2021-11-12 18:46:19",
				"delFlag":"0",
				"id":"1",
				"remark":"超级管理员",
				"roleKey":"admin",
				"roleName":"超级管理员",
				"roleSort":"1",
				"status":"0",
				"updateBy":"0"
			}
				...
		],
		"user":{
    
    
			"email":"[email protected]",
			"id":"14787164048663",
			"nickName":"sg777",
			"sex":"0",
			"status":"0",
			"userName":"sg777"
		}
	},
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here
Insert image description here

5.26.2.2 更新用户信息接口
请求方式 请求路径 是否需求token头
PUT /system/user

请求体:

{
    
    
    "email":"[email protected]",
    "id":"14787164048663",
    "nickName":"sg777",
    "sex":"1",
    "status":"0",
    "userName":"sg777",
    "roleIds":[
        "11"
    ]
}

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here

Insert image description here

5.27 分页查询分类列表

5.27.1 需求

​ 需要分页查询分类列表。

​ 能根据分类名称进行模糊查询。

​ 能根据状态进行查询。

5.27.2 接口设计
请求方式 请求路径 是否需求token头
GET content/category/list

Query格式请求参数:

pageNum: 页码

pageSize: 每页条数

name:分类名

status: 状态

响应格式:

{
    
    
	"code":200,
	"data":{
    
    
		"rows":[
			{
    
    
				"description":"wsd",
				"id":"1",
				"name":"java",
				"status":"0"
			},
			{
    
    
				"description":"wsd",
				"id":"2",
				"name":"PHP",
				"status":"0"
			}
		],
		"total":"2"
	},
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here
Insert image description here

5.28 新增分类

5.28.1 需求

​ 需要新增分类功能

5.28.2 接口设计
请求方式 请求路径 是否需求token头
POST /content/category

请求体:

{
    
    
    "name":"威威",
    "description":"是的",
    "status":"0"
}

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现Insert image description here
Insert image description here

Insert image description here

5.29 修改分类

5.29.1 需求

​ 需要提供修改分类的功能

5.29.2 接口设计
5.29.2.1 根据id查询分类
请求方式 请求路径 是否需求token头
Get content/category/{id}

Path格式请求参数:

id: 分类id

响应格式:

{
    
    
	"code":200,
	"data":{
    
    
		"description":"qwew",
		"id":"4",
		"name":"ww",
		"status":"0"
	},
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here
Insert image description here

5.29.2.2 更新分类
请求方式 请求路径 是否需求token头
PUT /content/category

请求体:

{
    
    
    "description":"是的",
    "id":"3",
    "name":"威威2",
    "status":"0"
}

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here
Insert image description here

5.30 删除分类

5.30.1 需求

​ 删除某个分类(逻辑删除)

5.30.2 接口设计
请求方式 请求路径 是否需求token头
DELETE /content/category/{id}

Path请求参数:

id:要删除的分类id

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现
Insert image description here

5.31 分页查询友链列表

5.31.1 需求

​ 需要分页查询友链列表。

​ 能根据友链名称进行模糊查询。

​ 能根据状态进行查询。

5.31.2 接口设计
请求方式 请求路径 是否需求token头
GET /content/link/list

Query格式请求参数:

pageNum: 页码

pageSize: 每页条数

name:友链名

status:状态

响应格式:

{
    
    
	"code":200,
	"data":{
    
    
		"rows":[
			{
    
    
				"address":"https://www.baidu.com",
				"description":"sda",
				"id":"1",						       "logo":"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fn1.itc.cn%2Fimg8%2Fwb%2Frecom%2F2016%2F05%2F10%2F146286696706220328.PNG&refer=http%3A%2F%2Fn1.itc.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1646205529&t=f942665181eb9b0685db7a6f59d59975",
				"name":"sda",
				"status":"0"
			}
		],
		"total":"1"
	},
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here

Insert image description here

5.32 新增友链

5.32.1 需求

​ 需要新增友链功能

5.32.2 接口设计
请求方式 请求路径 是否需求token头
POST /content/link

请求体:

{
    
    
    "name":"sda",
    "description":"weqw",
    "address":"wewe",
    "logo":"weqe",
    "status":"2"
}

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here
Insert image description here

5.33 修改友链

5.33.1 需求

​ 需要提供修改友链的功能

5.33.2 接口设计
5.33.2.1 根据id查询友联
请求方式 请求路径 是否需求token头
Get content/link/{id}

Path格式请求参数:

id: 友链id

响应格式:

{
    
    
	"code":200,
	"data":{
    
    
		"address":"wewe",
		"description":"weqw",
		"id":"4",
		"logo":"weqe",
		"name":"sda",
		"status":"2"
	},
	"msg":"操作成功"
}

实现
Insert image description here
Insert image description here

Insert image description here

5.33.2.2 修改友链
请求方式 请求路径 是否需求token头
PUT /content/link

请求体:

{
    
    
    "address":"https://www.qq.com",
    "description":"dada2",
    "id":"2",
    "logo":"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fn1.itc.cn%2Fimg8%2Fwb%2Frecom%2F2016%2F05%2F10%2F146286696706220328.PNG&refer=http%3A%2F%2Fn1.itc.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1646205529&t=f942665181eb9b0685db7a6f59d59975",
    "name":"sda",
    "status":"0"
}

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现

Insert image description here
Insert image description here
Insert image description here

5.34 删除友链

5.34.1 需求

​ 删除某个友链(逻辑删除)

5.34.2 接口设计
请求方式 请求路径 是否需求token头
DELETE /content/link/{id}

Path请求参数:

id:要删除的友链id

响应格式:

{
    
    
	"code":200,
	"msg":"操作成功"
}

实现
Insert image description here

6.重点知识

6.1 工具类 BeanCopyUtils

Insert image description here

package com.sangeng.utils;

import com.sangeng.domain.entity.Article;
import com.sangeng.domain.vo.HotArticleVo;
import org.springframework.beans.BeanUtils;

import java.util.List;
import java.util.stream.Collectors;

public class BeanCopyUtils {
    
    

//    构造方法设置为私有的方法
    private BeanCopyUtils() {
    
    
    }
//    单个实体类拷贝(将一个源对象 拷贝 至  字节码class)
//    通过反射创建目标对象,然后再拷贝
    public static <V> V copyBean(Object source,Class<V> clazz) {
    
    
        //创建目标对象     传过来什么类型,就返回什么类型,使用泛型(  <V> V泛型方法,返回值类型 )
        V result = null;  //提升作用域
        try {
    
    

            result = clazz.newInstance();
            //实现属性copy
            BeanUtils.copyProperties(source, result);

        } catch (Exception e) {
    
    
            throw new RuntimeException(e);
        }

        //返回结果
        return result;
    }

//    集合拷贝
//  (后面要用)声明泛型O,V  返回类型     public <T> void say(){} 表明是泛型方法
    public static <O,V>  List<V> copyBeanList(List<O> list, Class<V> clazz){
    
    
//        先将list集合  转成流->流当中元素的转换(转换方式copyBean方法) 返回一个泛型V -> 收集操作,泛型转成list
        return list.stream()
                .map(o -> copyBean(o, clazz))
                .collect(Collectors.toList());
    }
}

单个实体类拷贝

 //封装成User
User user = BeanCopyUtils.copyBean(userVo2, User.class);//拷贝成 类型User.class

Insert image description here

集合拷贝
拷贝整个list集合,集合中的每一个元素 按照单个实体类拷贝。
将page.getRecords()查到的集合 拷贝 成 List集合

 List<UserVo> userVos = BeanCopyUtils.copyBeanList(page.getRecords(), UserVo.class);

6.2 mybatisplus 解决 主键id雪花算法生成方式,改成主键id自增方式

使用mybatisplus时,当实体类entity Article主键id
如果*不用@TableId标注 主键id *,则插入数据时,默认使用雪花算法生成主键id即使数据库设置了 主键自增

Insert image description here
解决雪花算法问题,使用 自增主键id
6.2.1 数据库表
Insert image description here

6.2.2 局部设置 主键id自增方式
Insert image description here

6.2.2 全局设置 主键id自增方式
Insert image description here
Insert image description here

6.3 事务 @Transactional

在方法前面 加个注解@Transactional 即可实现aop事务
Insert image description here

6.4 mybatisplus 分页查询

mybatis-plus分页查询(springboot中实现单表和多表查询)

6.5 mybatisplus 自动 插入时间

插入数据时,自动插入 创建时间,更新时间,创建人,更新人。

6.6 ResponseResult (响应类) 和 AppHttpCodeEnum (响应枚举类)

ResponseResult Insert image description here
AppHttpCodeEnum
Insert image description here

ResponseResult

package com.sangeng.domain;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.sangeng.enums.AppHttpCodeEnum;

import java.io.Serializable;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> implements Serializable {
    
    
//    ResponseResult<T> 作用: <T>指定ResponseResult类中 字段data的数据类型 为泛型
    private Integer code;  //编码
    private String msg;    //message
    private T data;       // 泛型, 传入data什么类型,data就是什么类型

    // 返回 { 200; "操作成功"; }    AppHttpCodeEnum枚举类中 SUCCESS(200,"操作成功"),
    public ResponseResult() {
    
     //如果什么参数都不传,则返回 { 200; "操作成功"; }
        this.code = AppHttpCodeEnum.SUCCESS.getCode();
        this.msg = AppHttpCodeEnum.SUCCESS.getMsg();
    }
    // 返回 { code; data; }
    public ResponseResult(Integer code, T data) {
    
    
        this.code = code;
        this.data = data;
    }
    // 返回 { code; msg; data;}
    public ResponseResult(Integer code, String msg, T data) {
    
    
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
    // 返回 { code; msg;}
    public ResponseResult(Integer code, String msg) {
    
    
        this.code = code;
        this.msg = msg;
    }

    public static ResponseResult errorResult(int code, String msg) {
    
    //errorResult 出错方法
        ResponseResult result = new ResponseResult();
        return result.error(code, msg);
    }
    public static ResponseResult okResult() {
    
     //errorResult 正常方法
        ResponseResult result = new ResponseResult();
        return result;
    }
    public static ResponseResult okResult(int code, String msg) {
    
    //errorResult 正常方法
        ResponseResult result = new ResponseResult();
        return result.ok(code, null, msg);
    }
    //errorResult 正常方法
    public static ResponseResult okResult(Object data) {
    
    //传入什么类型,则泛型T就为什么类型
        ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getMsg());
        if(data!=null) {
    
    
            result.setData(data);
        }
        return result;
    }
    public static ResponseResult errorResult(AppHttpCodeEnum enums){
    
    
        return setAppHttpCodeEnum(enums,enums.getMsg());
    }
    public static ResponseResult errorResult(AppHttpCodeEnum enums, String msg){
    
    
        return setAppHttpCodeEnum(enums,msg);
    }
    public static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums){
    
    
        return okResult(enums.getCode(),enums.getMsg());
    }
    private static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums, String msg){
    
    
        return okResult(enums.getCode(),msg);
    }
    public ResponseResult<?> error(Integer code, String msg) {
    
    
        this.code = code;
        this.msg = msg;
        return this;
    }
    public ResponseResult<?> ok(Integer code, T data) {
    
    
        this.code = code;
        this.data = data;
        return this;
    }
    public ResponseResult<?> ok(Integer code, T data, String msg) {
    
    
        this.code = code;
        this.data = data;
        this.msg = msg;
        return this;
    }

    public ResponseResult<?> ok(T data) {
    
    
        this.data = data;
        return this;
    }
    //  getter和  setter
    public Integer getCode() {
    
    
        return code;
    }

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

    public String getMsg() {
    
    
        return msg;
    }

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

    public T getData() {
    
    
        return data;
    }

    public void setData(T data) {
    
    
        this.data = data;
    }
}

AppHttpCodeEnum

  • 枚举是一个被命名的整型常数的集合,用于声明一组带标识符的常数。
  • 枚举是一个特殊的类,一般表示一组常量
package com.sangeng.enums;
//使用menu定义枚举类
public enum AppHttpCodeEnum {
    
    
    //必须在枚举类的第一行声明枚举类对象,并且枚举类对象之间以“,”分隔,以“;”结尾。 必需声明枚举对象参数 的字段,并有构造方法
    // 成功
    SUCCESS(200,"操作成功"),
    // 登录
    NEED_LOGIN(401,"需要登录后操作"),
    NO_OPERATOR_AUTH(403,"无权限操作"),
    SYSTEM_ERROR(500,"出现错误"),
    USERNAME_EXIST(501,"用户名已存在"),
    PHONENUMBER_EXIST(502,"手机号已存在"),
    EMAIL_EXIST(503, "邮箱已存在"),
    REQUIRE_USERNAME(504, "必需填写用户名"),
    LOGIN_ERROR(505,"用户名或密码错误"),
    CONTENT_NOT_NULL(506,"评论内容不能为空" ),
    FILE_TYPE_ERROR(507,"文件类型错误,请上传png或jpg文件" ),
    USERNAME_NOT_NULL(508,"用户名不能为空" ),
    NICKNAME_NOT_NULL(509,"昵称不能为空" ),
    PASSWORD_NOT_NULL(510,"密码不能为空" ),
    EMAIL_NOT_NULL(511,"邮箱不能为空" );

    int code;
    String msg;
    AppHttpCodeEnum(int code, String errorMessage){
    
     //构造方法(两个参数,上面声明枚举才能传两个参数)
        this.code = code;
        this.msg = errorMessage;
    }
    //由此处 以上 是枚举类必需的
    public int getCode() {
    
    //getter
        return code;
    }
    public String getMsg() {
    
    //getter
        return msg;
    }
}
  • ResponseResult使用

controller
Insert image description here
service接口
Insert image description here
serviceImpl
Insert image description here

  • AppHttpCodeEnum使用
    Insert image description here

6.7 SystemConstants (常量类)

Insert image description here

package com.sangeng.constants;

public class SystemConstants
{
    
    
    /**
     *  文章是草稿
     */
    public static final int ARTICLE_STATUS_DRAFT = 1;
    /**
     *  文章是正常分布状态
     */
    public static final int ARTICLE_STATUS_NORMAL = 0;
    /**
     * 分类是正常状态
     */
    public static final String  STATUS_NORMAL = "0";
    /**
     * 友链状态为审核通过     0
     * ctrl shift U 大小写切换
     */
    public static final String  LINK_STATUS_NORMAL = "0";


    /**
     * 评论类型:文章评论 0
     */
    public static final String ARTICLE_COMMENT = "0";
    /**
     * 评论类型:友链评论 1
     */
    public static final String LINK_COMMENT = "1";

    /**
     * 菜单和按钮
     */
    public static final String MENU = "C";
    public static final String BUTTON = "F";

    /**
     * 1:表示后台管理员,0 表示前台用户
     */
    public static final String ADMIN = "1";
}
  • SystemConstants 常量类 使用
    Insert image description here

6.8 Exception

  1. 异常类 SystemException
  2. 异常处理类 GlobalExceptionHandler

Insert image description here
异常类 SystemException
Insert image description here

package com.sangeng.exception;
import com.sangeng.enums.AppHttpCodeEnum;
public class SystemException extends RuntimeException{
    
    
    private int code;
    private String msg;
    public int getCode() {
    
    
        return code;
    }
    public String getMsg() {
    
    
        return msg;
    }
    public SystemException(AppHttpCodeEnum httpCodeEnum) {
    
    
        //通过枚举类 得到 其中的的常量  填充code,msg
        super(httpCodeEnum.getMsg()); //调用父类的super方法,必须写
        this.code = httpCodeEnum.getCode();
        this.msg = httpCodeEnum.getMsg();
    }
    // throw new SystemException(507,"必须填写用户名")  抛出异常是传入参数
    public SystemException(String message, int code, String msg) {
    
    
        super(message); //调用父类的super方法,必须写
        this.code = code;
        this.msg = msg;
    }
}

Exception handling class GlobalExceptionHandler
(handles the above exception class SystemException, such as: exception information, exception coding)
Insert image description here

package com.sangeng.handler.exception;

import com.sangeng.domain.ResponseResult;
import com.sangeng.enums.AppHttpCodeEnum;
import com.sangeng.exception.SystemException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;

//@ControllerAdvice  //controller中出现了异常,会在这里统一处理
//@ResponseBody    //将处理方法的返回值 放到 响应体中

@Slf4j  //日志
@RestControllerAdvice //@RestControllerAdvice = @ControllerAdvice + @ResponseBody
//处理方法的返回值 都会转换成json放到响应体中
public class GlobalExceptionHandler {
    
    

    @ExceptionHandler(SystemException.class) //处理SystemException异常
    public ResponseResult systemExceptionHandler(SystemException e){
    
    
        //打印异常信息
        log.error("出现了异常!{}",e);
        //从异常对象中获取提示信息封装返回,(封装到响应体中,因为controller请求方法中 返回类型是ResponseResult )
        return ResponseResult.errorResult(e.getCode(), e.getMsg()); //前端返回json对象 { "code":504, "msg":"必需填写用户名" }
    }

    @ExceptionHandler(Exception.class) //处理Exception异常
    public ResponseResult exceptionHandler(Exception e){
    
    
        //打印异常信息
        log.error("出现了异常!{}",e);
        //从异常对象中获取提示信息封装返回
        return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR.getCode(),e.getMessage());
    }
}

Usage example
1. Use enumeration class to obtain exception information

USERNAME_NOT_NULL(508,"用户名不能为空" )

Insert image description here

2. Throw an exception and manually pass in the exception information

if(!StringUtils.hasText(user.getUserName())){
    
    
 throw new SystemException(508,"用户名不能为空" );
 }

6.9 AOP implements logging

AOP - Log
aspect class LogAspect

6.10 Testing Objects.nonNull()

  • Objects.nonNull(name) determines if name is not empty
  • Objects.isNull(name) determines if name is empty

Insert image description here

6.11 mybatisplus implements addition, deletion, modification and query

6.12 dto

Guess you like

Origin blog.csdn.net/qq_45432276/article/details/131996136