Project Combat - Front-end and back-end separation blog system

Project Combat - Front-end and back-end separation blog system

1. Project introduction

  • Pure back-end explanation
  • Complete foreground and background code writing
  • Mainstream technology stack (SpringBoot, MybatisPlus, SpringSecurity, EasyExcel, Swagger2, Redis, Echarts, Vue, ElementUI...)
  • Perfect and detailed demand analysis
  • Step by step from easy to difficult

2. Create a project

​ We have two systems, the front desk and the back office. The front-end engineering of both systems has been provided. So we only need to write the backend of the two systems.

​ But if you think about it, in fact, many contents of the two back-end systems may be duplicated. Here if we simply create two backend projects. Then there will be a lot of repeated code, and it needs to be modified twice when it needs to be modified. This is why code reusability is not high.

​ So we need to create a multi-module project. The code that may be used by both systems can be written into a common module, so that the front-end system and the back-end system can depend on the common modules.

① Create a parent module

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

    <groupId>com.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>

    <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>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

② Create a public submodule sangeng-framework

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>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>
        <!--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>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
        </dependency>

    </dependencies>
</project>

③ Create blog background module sangeng-admin

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

    <artifactId>sangeng-admin</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.sangeng</groupId>
            <artifactId>sangeng-framework</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

④ Create a blog front-end module sangeng-blog

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

    <artifactId>sangeng-blog</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.sangeng</groupId>
            <artifactId>sangeng-framework</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

3. Blog Frontend

3.0 Preparations

3.1 SpringBoot and MybatisPuls integration configuration test

① Create a startup class

/**
 * @Author 三更  B站: https://space.bilibili.com/663528522
 */
@SpringBootApplication
@MapperScan("com.sangeng.mapper")
public class SanGengBlogApplication {
    
    

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

②Create application.yml configuration file

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:
    # 日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      logic-delete-field: delFlag
      logic-delete-value: 1
      logic-not-delete-value: 0
      id-type: auto

③ SQL statements

​ SQL script: SGBlog\resources\SQL\sg_article.sql

④ Create entity classes, Mapper, Service

​ Pay attention to which module these files should be written in?

@SuppressWarnings("serial")
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sg_article")
public class Article  {
    
    
    @TableId
    private Long id;
    //标题
    private String title;
    //文章内容
    private String content;
    //文章类型:1 文章 2草稿
    private String type;
    //文章摘要
    private String summary;
    //所属分类id
    private Long categoryId;
    //缩略图
    private String thumbnail;
    //是否置顶(0否,1是)
    private String isTop;
    //状态(0已发布,1草稿)
    private String status;
    //评论数
    private Integer commentCount;
    //访问量
    private Long viewCount;
    //是否允许评论 1是,0否
    private String isComment;
    
    private Long createBy;
    
    private Date createTime;
    
    private Long updateBy;
    
    private Date updateTime;
    //删除标志(0代表未删除,1代表已删除)
    private Integer delFlag;

}


public interface ArticleMapper extends BaseMapper<Article> {
    
    


}

public interface ArticleService extends IService<Article> {
    
    
}

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

}

⑤ Create a Controller test interface

​ Pay attention to which module these files should be written in?

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

    @Autowired
    private ArticleService articleService;

    @GetMapping("/list")
    public List<Article> test(){
    
    
        return articleService.list();
    }
}

​ We can temporarily comment out the SpringSecurity dependency in sangeng-framework for easy testing

3.1 List of Popular Articles

3.1.0 Article table analysis

​ Analyze the required fields through requirements.

3.1.1 Requirements

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

​ Note: Drafts cannot be displayed, and deleted articles cannot be queried. To sort in descending order by pageviews.

3.1.2 Interface design

​ See interface documentation

3.1.3 Basic version code implementation

①Preparation

Unified response class and response enumeration

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 Using VO optimization

​ At present, our response format does not conform to the standard of the interface document, and many fields are returned. This is because the result of our query is encapsulated by Article, and there are many fields in Article.

​ We usually use VO to receive the query results at the end of the project. An interface corresponds to a VO, so even if the interface response field needs to be modified, only the VO needs to be changed.

@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

​ Literal values ​​are not allowed to be used directly in the code in actual projects. Both need to be defined as constants to use. This approach helps to 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 category list

3.2.0 Classification table analysis

​ Analyze the required fields through requirements.

​ For table creation SQL and initialization data, see: SGBlog\Resources\SQL\sg_category.sql

3.2.1 Requirements

[External link picture transfer failed, the source site may have an anti-theft link mechanism, it is recommended to save the picture and upload it directly (img-WdsTFuGV-1691068679627)(img/image-20220202111056036-16437714601701.png)]

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

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

3.2.2 Interface design

​ See interface documentation

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 Paging query article list

3.3.1 Requirements

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

​ Home page: query all articles

​ Category page: query articles under the corresponding category

​ Requirements: ① Only officially released articles can be queried ② Top articles should be displayed at the top

3.3.2 Interface design

​ see documentation

3.3.3 Code implementation

MP supports paging configuration

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

    /**
     * 3.4.0之后版本
     * @return
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
    
    
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

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

3.5.0 Friend List Analysis

​ Analyze the required fields through requirements.

​ For table creation SQL and initialization data, see: SGBlog\Resources\SQL\sg_link.sql

3.5.1 Requirements

​ On the friend chain page, you need to query all the approved friend chains.

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 Realization of login function

​ Both the authentication and authorization of our foreground and background are implemented using the Spring Security security framework.

3.6.0 Requirements

​ Need to implement the login function

​ Some functions must be logged in to use, and cannot be used without logging 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 Table analysis

​ To create table SQL and initialize data, see: SGBlog\Resources\SQL\sys_user.sql

By the way, generate User and UserMapper which will be used later

3.6.3 Idea analysis

Log in

①Custom login interface

​ Call the method of ProviderManager for authentication. If the authentication passes, generate jwt

​ Store user information in redis

② Customize UserDetailsService

​ In this implementation class to query the database

​ Pay attention to configure passwordEncoder as BCryptPasswordEncoder

check:

① Define the Jwt authentication filter

​ Get token

​ Parse the token to get the userid in it

​ Get user information from redis

​ into the SecurityContextHolder

3.6.4 Preparations

① Add dependencies

Pay attention to let go of the annotations that Security depends on

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

②Tools and related configuration classes

​ See: SGBlog\Resources\Resources required for login function

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

train of thought

① Define the Jwt authentication filter

​ Get token

​ Parse the token to get the userid in it

​ Get user information from redis

​ into the 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 processing

​ At present, when our project responds with an authentication error or insufficient permissions, the Json returned is the exception processing result of Security. But the format of this response definitely does not conform to the interface specification of our project. So custom exception handling is required.

​ 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 there is an illegal situation, we expect to respond to the corresponding prompt. But it will be very troublesome if we manually handle it every time. We can choose to throw an exception directly, and then handle the exception uniformly. Encapsulate the information in the exception into a ResponseResult response 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 Logout interface

3.9.1 Interface design

request method request address request header
POST /logout A token request header is required

Response format:

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

3.9.2 Code implementation

Operations to be achieved:

​ 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 requires authentication to access

    @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 a list of comments under this article.

​ The effect is as follows:

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-OPmslOPJ-1691068679629)(img/image-20220208214106296.png)]

3.10.2 Comment Form Analysis

​ Analyze the required fields through requirements.

​ For table creation SQL and initialization data, see: SGBlog\resources\SQL\sg_comment.sql

By the way, generate the corresponding code

3.10.3 Interface design

request method request address request header
GET /comment/commentList No token request header required

Query format request parameters:

articleId: article id

pageNum: page number

pageSize: number of entries 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 Subcomments 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 subcomments

CommentVo added private List children on the previous basis;

@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 Interface for posting comments

3.11.1 Requirements

​ After logging in, the user can comment on the article and reply to the comment.

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

3.11.2 Interface design

request method request address request header
POST /comment Token header is required
Request body:

replied to the article:

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

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

Configure MP field autofill

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    
    
    @Override
    public void insertFill(MetaObject metaObject) {
    
    
        Long userId = null;
        try {
    
    
            userId = SecurityUtils.getUserId();
        } catch (Exception e) {
    
    
            e.printStackTrace();
            userId = -1L;//表示是自己创建
        }
        this.setFieldValByName("createTime", new Date(), metaObject);
        this.setFieldValByName("createBy",userId , metaObject);
        this.setFieldValByName("updateTime", new Date(), metaObject);
        this.setFieldValByName("updateBy", userId, metaObject);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
    
    
        this.setFieldValByName("updateTime", new Date(), metaObject);
        this.setFieldValByName(" ", SecurityUtils.getUserId(), metaObject);
    }
}

Use annotations to identify which fields need to be automatically filled under what circumstances

    /**
     * 创建人的用户id
     */
    @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;

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 required

Query format request parameters:

pageNum: page number

pageSize: number of entries 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 Union 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, only when the commentType is 0, the articleId judgment is added, and a comment type is added.

    @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 A token request header is required

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 the 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 your 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 pictures, videos and other files to your own application's web server, it will take up more resources when reading pictures. Affect application server performance.

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

3.14.2.2 Basic use test of Qiniu Cloud

[External link picture transfer failed, the source site may have an anti-theft link mechanism, it is recommended to save the picture and upload it directly (img-O2hNTpZ4-1691068679630)(img/image-20220227224537701.png)]

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-q2qbiRzK-1691068679631)(img/image-20220227224443813.png)]

Secret key

[External link picture transfer failed, the source site may have an anti-theft link mechanism, it is recommended to save the picture and upload it directly (img-Fj0Vnmh5-1691068679631)(img/image-20220228230512598.png)]

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-mdt8mmrq-1691068679631)(img/image-20220228230933808.png)]

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;

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 profile, click Save to update the profile.

3.15.2 Interface design

request method request address request header
PUT /user/userInfo A token request header is required

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

​ Require users to be able to complete user registration on the registration interface. It is required that the user name, nickname, and mailbox cannot be duplicated with the original data in the database. If a certain item repeats the registration failure and there must be a corresponding prompt. And the user name, password, nickname, and email address cannot be empty.

​ Note: Passwords must be stored in cipher text in the database.

3.16.2 Interface design

request method request address request header
POST /user/register No token request header 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 implements logging

3.17.1 Requirements

​ It is necessary to record interface call information through logging. It is convenient for later debugging and investigation. And there may be many interfaces that need to be logged.

​ When the interface is called, the log printing format is as follows:

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-Gq0HdyeP-1691068679632)(img/image-20220313133714102.png)]

3.17.2 Idea analysis

​ It is equivalent to enhancing the original function. And it is a batch enhancement, which is very suitable for implementation with AOP at this time.

3.17.3 Code implementation

log print format

        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 Update View Count

3.18.1 Requirements

​ When users browse blog posts, it is necessary to increase the number of corresponding blog views.

3.18.2 Idea Analysis

​ We only need to update the corresponding number of views every time a user browses the blog.

​ But if you directly manipulate the pageviews of the blog table, what problems will arise in the case of a large amount of concurrency?

​ How to optimize it?

① Store the page views of the blog in redis when the application starts

②Update the data in redis when updating the pageview

③ Update the pageviews in Redis to the database every 10 minutes

④ Read from redis when reading article views

3.18.3 Laying knowledge

3.18.3.1 CommandLineRunner implements preprocessing at project startup

​ If you want to perform some initialization operations when the SpringBoot application starts, you can choose to use CommandLineRunner for processing.

We only need to implement the CommandLineRunner interface and inject the corresponding bean into the container. Put the relevant initialization code back into the method that needs to be restarted.

This will execute the corresponding code when the application starts.

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

3.18.3.2 Scheduled tasks

There are many ways to implement timing tasks, such as XXL-Job and so on. But in fact, the core functions and concepts are similar. In many cases, the APIs called are different.

​ Here, we first use the timing task API provided by SpringBoot to implement a simple timing task, so that everyone can have a general understanding of some core concepts in the timing task.

Implementation steps

① Use the @EnableScheduling annotation to enable the scheduled task function

​ We can add @EnableScheduling to the configuration class

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

② Determine the execution code of the scheduled task and configure the task execution time

​ Use the @Scheduled annotation to identify code that needs to be executed regularly. The cron attribute of the annotation is equivalent to the execution time of the task. Currently, 0/5 * * * * ? can be used for testing, which means that it will be executed every 5 seconds starting from 0 seconds.

​ Note: The corresponding bean must be injected into the container, otherwise it will not take effect.

@Component
public class TestJob {
    
    

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

3.18.3.2.1 cron expression syntax

A cron expression is an expression used to set the execution time of a scheduled task.

​ In many cases we can use: Online Cron Expression Generator to help us understand cron expressions and write cron expressions.

​ But we still need to learn the corresponding Cron syntax, which will be more conducive to our writing Cron expressions.

As we used above 0/5 * * * * ? *, the cron expression consists of seven parts separated by spaces. These seven parts are from left to right:

Second (0 59), minute (0 59), hour (0~23), date (1-the last day of the month), month (1-12), day of the week (1-7, 1 means Sunday), year (general This item is not set, just ignore it, and it can be a null value)

Universal special characters: , - * / (can be used in any part)

An asterisk means any value, for example:

* * * * * ?

It means "every hour, every minute, every day, every month, every year".

,

Can be used to define lists, for example:

1,2,3 * * * * ?

Indicates "every 1st, 2nd, and 3rd second of every day, hour, and minute of every year, month, and day".

Define the scope, for example:

1-3 * * * * ?

Indicates "the 1st to 3rd second of every hour of every day of every year and every month".

/

How often, for example

5/10 * * * * ?

It means "every hour, every day, every day, every month, every year, starting from the 5th second, every 10 seconds". That is, the left side of "/" is the start value, and the right side is the interval. If it starts from "0", it can also be abbreviated as "/10"

Date parts also allow special characters: ? LW

Special characters that are also allowed in the week part: ? L #

?

Available only for the date and day of the week parts. Indicates that there is no specific value, use ? to pay attention to conflicts. If one of the two parts of date and week is set to a value, the other must be set to " ? ".

For example:

0\* * * 2 * ?
0\* * * ? * 2

It is wrong to use both? and not to use at the same time.

For example, the following writing is wrong

* * * 2 * 2
* * * ? * ?

W

Can only be used in dates, indicating the working day closest to a certain day in the current month

0 0 0 31W * ?

Indicates the working day closest to the 31st. If the 31st is a Saturday, it means the 30th, which is Friday. If the 31st is a Sunday, it means the 29th, which is Friday. If the 31st is a Wednesday, it means the 31st itself, which is Wednesday.

L

Indicates the last (Last), can only be used in dates and weeks

The last day of each month in the date, the 31st in January, and the 30th in June

It can also represent the Nth day of each month. For example: L-2 means the penultimate 2nd day of each month

0 0 0 LW * ?
LW can be used together to indicate the last working day of each month, that is, the last Friday of each month

In the week means 7 which is Saturday

0 0 0 ? * L
表示每个星期六
0 0 0 ? * 6L
若前面有其他值的话,则表示最后一个星期几,即每月的最后一个星期五

It can only be used in the week, indicating the first few days of the week

0 0 0 ? * 6#3
表示每个月的第三个星期五。

3.18.4 Interface design

request method request address request header
PUT /article/updateViewCount/{id} No token request header required

parameter

​ Carry the article id in the request path

Response format:

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

3.18.5 Code implementation

① Store the page views of the blog in redis when the application starts

​ Implement the CommandLineRunner interface to initialize the cache when the application starts.

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

②Update the data in redsi when updating the pageview

RedisCache increase method

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

Increase method update reading number in ArticleController

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

Add method in ArticleService

ResponseResult updateViewCount(Long id);

Implementation method in ArticleServiceImpl

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

Add construction method in 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, which can help us design, build, record and use Rest API.

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 done by different engineers. In this development mode, maintaining a timely updated and complete Rest API document will greatly improve our work efficiency. Documents in the traditional sense 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, and will increase our Communication costs. And Swagger provides us with a brand-new way of maintaining API documents. Let's take a look at its advantages:

1. The code changes and the document changes. With only a few annotations, Swagger can automatically generate API documentation based on the code, which ensures the timeliness of the documentation.
2. Cross-lingual, supporting more than 40 languages.
3. What Swagger UI presents is 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 @EnableSwagger2 annotation on 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.3.3 Testing

​ Visit: http://localhost:7777/swagger-ui.html Note that localhost and 7777 should be adjusted to the domain name and port number of the actual project.

4.4 Specific configuration

4.4.1 Controller configuration

4.4.1 @Api annotation

Attribute 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 is used to describe the parameters of the interface, but an interface may have multiple parameters, so it is generally used in combination with @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 Entity class configuration

4.4.3.1 Entity description configuration @ApiModel

@ApiModel is used to describe entity classes.

@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(description = "添加评论dto")
public class AddCommentDto{
    
    
    //..
}
4.4.3.2 Description configuration of entity properties @ApiModelProperty

@ApiModelProperty is used to describe the properties of entities

    @ApiModelProperty(notes = "评论类型(0代表文章评论,1代表友链评论)")
    private String type;

4.4.4 Document information configuration

@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. Blog background

5.0 Preparations

Front-end engineering start

npm install

npm run dev

① Create a startup class

/**
 * @Author 三更  B站: https://space.bilibili.com/663528522
 */
@SpringBootApplication
@MapperScan("com.sangeng.mapper")
public class BlogAdminApplication {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(BlogAdminApplication.class, args);
    }
}

②Create application.yml configuration file

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:
    # 日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      logic-delete-field: delFlag
      logic-delete-value: 1
      logic-not-delete-value: 0
      id-type: auto


③ SQL statements

​ SQL script: SGBlog\resource\SQL\sg_tag.sql

④ Create entity classes, Mapper, Service

​ Pay attention to which module these files should be written in?

Tag

@SuppressWarnings("serial")
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sg_tag")
public class Tag  {
    
    
    @TableId
    private Long id;

    
    private Long createBy;
    
    private Date createTime;
    
    private Long updateBy;
    
    private Date updateTime;
    //删除标志(0代表未删除,1代表已删除)
    private Integer delFlag;
    //备注
    private String remark;
    //标签名
    private String name;



}

TagMapper

/**
 * 标签(Tag)表数据库访问层
 *
 * @author makejava
 * @since 2022-07-19 22:33:35
 */
public interface TagMapper extends BaseMapper<Tag> {
    
    

}



TagService

/**
 * 标签(Tag)表服务接口
 *
 * @author makejava
 * @since 2022-07-19 22:33:38
 */
public interface TagService extends IService<Tag> {
    
    

}

TagServiceImpl

/**
 * 标签(Tag)表服务实现类
 *
 * @author makejava
 * @since 2022-07-19 22:33:38
 */
@Service("tagService")
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {
    
    

}


⑤ Create a Controller test interface

​ Pay attention to which module these files should be written in?

TagController /content/tag

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

    @GetMapping("/list")
    public ResponseResult list(){
    
    
        return ResponseResult.okResult(tagService.list());
    }
}


⑥ Add security related classes

@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()
//                //注销接口需要认证才能访问
//                .antMatchers("/logout").authenticated()
//                .antMatchers("/user/userInfo").authenticated()
//                .antMatchers("/upload").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();
    }
}

@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("login:" + 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);
    }


}

5.1 Background login

​ Background authentication and authorization are also implemented using the Spring Security security framework.

5.1.0 Requirements

​ Need to implement the login function

All background functions must be logged in to use.

5.1.1 Interface design

request method request path
POST /user/login

Request body:

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

Response format:

{
    
    
    "code": 200,
    "data": {
    
    
        "token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0ODBmOThmYmJkNmI0NjM0OWUyZjY2NTM0NGNjZWY2NSIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTY0Mzg3NDMxNiwiZXhwIjoxNjQzOTYwNzE2fQ.ldLBUvNIxQCGemkCoMgT_0YsjsWndTg5tqfJb77pabk"
    },
    "msg": "操作成功"
}

5.1.2 Idea Analysis

Log in

①Custom login interface

​ Call the method of ProviderManager for authentication. If the authentication passes, generate jwt

​ Store user information in redis

② Customize UserDetailsService

​ In this implementation class to query the database

​ Pay attention to configure passwordEncoder as BCryptPasswordEncoder

check:

① Define the Jwt authentication filter

​ Get token

​ Parse the token to get the userid in it

​ Get user information from redis

​ into the SecurityContextHolder

5.1.3 Preparations

① Add dependencies

Relevant dependencies have been added before, no need to do anything

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

5.1.4 Login interface code implementation

LoginController

Make a copy of BlogLoginController, name it LoginController, and inject LoginService into it

Change the request address to /user/login

@RestController
public class LoginController {
    
    
    @Autowired
    private LoginService loginService;

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

}
LoginService

Copy a BlogLoginService and name it LoginService

public interface LoginService {
    
    
    ResponseResult login(User user);

}

SecurityConfig

has been copied before

SystemLoginServiceImpl

Make a copy, LoginServiceImpl, named SystemLoginServiceImpl to implement LoginService

The prefix of the key stored in redis in the login method is changed to login

In the returned data, only the token is returned

@Service
public class SystemLoginServiceImpl implements LoginService {
    
    

    @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("login:"+userId,loginUser);

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

Just reuse the original

LoginUser

Just reuse the original

5.2 Background authority control and dynamic routing

need

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

​ The user can only use the functions allowed by his authorization.

feature design

​ I have introduced the RBAC permission model in my Spring Security course before. Those who have not studied it can look at the RBAC permission model . Here we are implementing this function based on the RBAC permission model.

table analysis

​ Analyze the required fields through requirements.

​ For table creation SQL and initialization data, see: SGBlog\Resources\SQL\sg_menu.sql

interface design

getInfo interface

yes

request method request address request header
GET /getInfo A token request header is required

Request parameters:

none

Response format:

If the user id is 1 to represent the administrator, only admin is required in roles, and all permissions whose menu type is C or F, status is normal, and have not been deleted must 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":"操作成功"
}
getRouters interface
request method request address request header
GET /getRouters A token request header is required

Request parameters:

none

Response format:

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

​ Note: The returned menu data needs to reflect the hierarchical relationship of the parent-child menu

​ If the user id is 1 to represent the administrator, there must be all menu types of C or M in the menus, the status is normal, and the permission has 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 the class for the 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>
getRouters interface

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 it needs to be sorted by parent_id and order_num

5.3 Logout interface

5.3.1 Interface design

request method request address request header
POST /user/logout A token request header is required

Response format:

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

5.3.2 Code implementation

Operations to be achieved:

​ 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 requires authentication to access

    @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 management of articles later, it is necessary to provide the function of tags. An article can have multiple tags.

​ In the background, the page-by-page query label function is required, and it is required to perform page-by-page query according to the label name. Requirements such as remark inquiries may be added later .

​ Note: Deleted tags cannot be queried.

5.4.1 Label table analysis

​ Analyze the required fields 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 entries per page

name: label name

remark: remark

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 tags

5.5.0 Requirements

​ Click the Add button of Tag Management to realize the function of adding tags.

5.5.1 Interface design

request method request address request header
POST /content/tag A token request header is required

Request body format:

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

Response format:

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

5.5.2 Testing

When testing, pay attention to whether the records added to the database have creation time, update time, creator, and updater fields.

5.6 Delete tags

5.6.1 Interface design

request method request address request header
DELETE /content/tag/{id} A token request header is required

The request parameter is in the path

For example: content/tag/6 means to delete the tag data with id 6

Response format:

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

5.6.2 Testing

Pay attention to whether the data cannot be seen in the list after the test is deleted

The piece of data still exists in the database, but the value of the tombstone field is modified

5.7 Modify label

5.7.1 Interface design

5.7.1.1 Get label information
request method request address request header
GET /content/tag/{id} A token request header is required

The request parameter is in the path

For example: content/tag/6 means to get the tag data with id 6

Response format:

{
    
    
	"code":200,
	"data":{
    
    
        "id":4,
        "name":"Java",
        "remark":"sdad"
	},
	"msg":"操作成功"
}
5.7.1.2 Modify label interface
request method request address request header
PUT /content/tag A token request header is required

请求体格式:

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

响应格式:

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

5.8 写博文

5.8.1 需求

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

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

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

5.8.2 表分析

​ 标签和文章需要关联所以需要一张关联表。

​ SQL脚本:SGBlog\资源\SQL\sg_article_tag.sql

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":"操作成功"
}
5.8.2.2 查询所有标签接口
请求方式 请求地址 请求头
GET /content/tag/listAllTag 需要token请求头

请求参数:

​ 无

响应格式:

{
    
    
	"code":200,
	"data":[
		{
    
    
			"id":1,
			"name":"Mybatis"
		},
		{
    
    
			"id":4,
			"name":"Java"
		}
	],
	"msg":"操作成功"
}
5.8.2.3 上传图片
请求方式 请求地址 请求头
POST /upload 需要token请求头

参数:

​ img,值为要上传的文件

请求头:

​ Content-Type :multipart/form-data;

响应格式:

{
    
    
    "code": 200,
    "data": "文件访问链接",
    "msg": "操作成功"
}
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/8fa5c66c917fe16261a80a8b0970d8f0.png)\n正文",
    "tags":[
        1,
        4
    ],
    "categoryId":1,
    "summary":"哈哈",
    "status":"1"
}

响应格式:

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

5.8.3 代码实现

5.8.3.1 查询所有分类接口

CategoryController

/**
 * @Author 三更  B站: https://space.bilibili.com/663528522
 */
@RestController
@RequestMapping("/content/category")
public class CategoryController {
    
    
    @Autowired
    private CategoryService categoryService;
    
    @GetMapping("/listAllCategory")
    public ResponseResult listAllCategory(){
    
    
        List<CategoryVo> list = categoryService.listAllCategory();
        return ResponseResult.okResult(list);
    }

    
}

CategoryVo修改,增加description属性

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CategoryVo {
    
    

    private Long id;
    private String name;
    //描述
    private String description;
}

CategoryService增加listAllCategory方法

public interface CategoryService extends IService<Category> {
    
    


    ResponseResult getCategoryList();

    List<CategoryVo> listAllCategory();
}

SystemConstants中增加常量

    /** 正常状态 */
    public static final String NORMAL = "0";

CategoryServiceImpl增加方法

    @Override
    public List<CategoryVo> listAllCategory() {
    
    
        LambdaQueryWrapper<Category> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Category::getStatus, SystemConstants.NORMAL);
        List<Category> list = list(wrapper);
        List<CategoryVo> categoryVos = BeanCopyUtils.copyBeanList(list, CategoryVo.class);
        return categoryVos;
    }
5.8.3.2 查询所有标签接口

TagVo

@Data
@AllArgsConstructor
@NoArgsConstructor
public class TagVo {
    
    
    private Long id;

    //标签名
    private String name;



}

TagController

    @GetMapping("/listAllTag")
    public ResponseResult listAllTag(){
    
    
        List<TagVo> list = tagService.listAllTag();
        return ResponseResult.okResult(list);
    }

TagService 增加listAllTag方法

List<TagVo> listAllTag();

TagServiceImpl

    @Override
    public List<TagVo> listAllTag() {
    
    
        LambdaQueryWrapper<Tag> wrapper = new LambdaQueryWrapper<>();
        wrapper.select(Tag::getId,Tag::getName);
        List<Tag> list = list(wrapper);
        List<TagVo> tagVos = BeanCopyUtils.copyBeanList(list, TagVo.class);
        return tagVos;
    }
5.8.3.3 上传图片接口

在sangeng-admin中增加UploadController

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

    @Autowired
    private UploadService uploadService;

    @PostMapping("/upload")
    public ResponseResult uploadImg(@RequestParam("img") MultipartFile multipartFile) {
    
    
        try {
    
    
            return uploadService.uploadImg(multipartFile);
        } catch (IOException e) {
    
    
            e.printStackTrace();
            throw new RuntimeException("文件上传上传失败");
        }
    }
}
5.8.3.4 新增博文接口

ArticleController

/**
 * @Author 三更  B站: https://space.bilibili.com/663528522
 */
@RestController
@RequestMapping("/content/article")
public class ArticleController {
    
    

    @Autowired
    private ArticleService articleService;

    @PostMapping
    public ResponseResult add(@RequestBody AddArticleDto article){
    
    
        return articleService.add(article);
    }


}

AddArticleDto

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

@Data
@AllArgsConstructor
@NoArgsConstructor
public class AddArticleDto {
    
    

    private Long id;
    //标题
    private String title;
    //文章内容
    private String content;
    //文章摘要
    private String summary;
    //所属分类id
    private Long categoryId;

    //缩略图
    private String thumbnail;
    //是否置顶(0否,1是)
    private String isTop;
    //状态(0已发布,1草稿)
    private String status;
    //访问量
    private Long viewCount;
    //是否允许评论 1是,0否
    private String isComment;
    private List<Long> tags;

}

Article 修改这样创建时间创建人修改时间修改人可以自动填充

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

ArticleService增加方法

ResponseResult add(AddArticleDto article);

创建ArticleTag表相关的实体类,mapper,service,serviceimpl等

@TableName(value="sg_article_tag")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ArticleTag implements Serializable {
    
    
    private static final long serialVersionUID = 625337492348897098L;
    
    /**
    * 文章id
    */
    private Long articleId;
    /**
    * 标签id
    */
    private Long tagId;



}

ArticleServiceImpl增加如下代码

    @Autowired
    private ArticleTagService articleTagService;

    @Override
    @Transactional
    public ResponseResult add(AddArticleDto articleDto) {
    
    
        //添加 博客
        Article article = BeanCopyUtils.copyBean(articleDto, Article.class);
        save(article);


        List<ArticleTag> articleTags = articleDto.getTags().stream()
                .map(tagId -> new ArticleTag(article.getId(), tagId))
                .collect(Collectors.toList());

        //添加 博客和标签的关联
        articleTagService.saveBatch(articleTags);
        return ResponseResult.okResult();
    }

5.9 导出所有分类到Excel

5.9.1 需求

​ 在分类管理中点击导出按钮可以把所有的分类导出到Excel文件中。

5.9.2 技术方案

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

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

​ https://easyexcel.opensource.alibaba.com/docs/current/quickstart/write#%E7%A4%BA%E4%BE%8B%E4%BB%A3%E7%A0%81-1

5.9.3 接口设计

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

请求参数:

​ 无

响应格式:

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

失败的话响应格式如下:

{
    
    
	"code":500,
	"msg":"出现错误"
}

5.9.4 代码实现

工具类方法修改

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);
            //获取需要导出的数据
            List<Category> categoryVos = categoryService.list();

            List<ExcelCategoryVo> excelCategoryVos = BeanCopyUtils.copyBeanList(categoryVos, ExcelCategoryVo.class);
            //把数据写入到Excel中
            EasyExcel.write(response.getOutputStream(), ExcelCategoryVo.class).autoCloseStream(Boolean.FALSE).sheet("分类导出")
                    .doWrite(excelCategoryVos);

        } catch (Exception e) {
    
    
            //如果出现异常也要响应json
            ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR);
            WebUtils.renderString(response, JSON.toJSONString(result));
        }
    }

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

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

sg eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJkZGJkNjM5MWJiZTA0NmMzOTc4NDg1ZTcxNWQ3YjQ0MSIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTY2MjI0NDE4NywiZXhwIjoxNjYyMzMwNTg3fQ.z4JGwFN3lWyVbOCbhikCe-O4D6SvCQFEE5eQY3jDJkw

sangeng

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0Y2I1ZjhmMTc0Mjk0NzM0YjI4Y2M1NTQzYjQ2Yjc1YyIsInN1YiI6IjYiLCJpc3MiOiJzZyIsImlhdCI6MTY2MjI0NDQzMywiZXhwIjoxNjYyMzMwODMzfQ.yEkbyGYXBp5ndnyq-3acdgpvqx2mnI8B9fK9f3Y6Jco

5.10.2 代码实现

SecurityConfig

@EnableGlobalMethodSecurity(prePostEnabled = true)

UserDetailsServiceImpl

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MenuMapper menuMapper;

    @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("用户不存在");
        }
        //返回用户信息
        if(user.getType().equals(SystemConstants.ADMAIN)){
    
    
            List<String> list = menuMapper.selectPermsByUserId(user.getId());
            return new LoginUser(user,list);
        }
        return new LoginUser(user,null);
    }
}

LoginUser

增加属性

private List<String> permissions;

PermissionService

hasPermisson

@Service("ps")
public class PermissionService {
    
    

    /**
     * 判断当前用户是否具有permission
     * @param permission 要判断的权限
     * @return
     */
    public boolean hasPermission(String permission){
    
    
        //如果是超级管理员  直接返回true
        if(SecurityUtils.isAdmin()){
    
    
            return true;
        }
        //否则  获取当前登录用户所具有的权限列表 如何判断是否存在permission
        List<String> permissions = SecurityUtils.getLoginUser().getPermissions();
        return permissions.contains(permission);
    }
}

CategoryController

    @PreAuthorize("@ps.hasPermission('content:category:export')")
    @GetMapping("/export")
    public void export(HttpServletResponse response){
    
    
        try {
    
    
            //设置下载文件的请求头
            WebUtils.setDownLoadHeader("分类.xlsx",response);
            //获取需要导出的数据
            List<Category> categoryVos = categoryService.list();

            List<ExcelCategoryVo> excelCategoryVos = BeanCopyUtils.copyBeanList(categoryVos, ExcelCategoryVo.class);
            //把数据写入到Excel中
            EasyExcel.write(response.getOutputStream(), ExcelCategoryVo.class).autoCloseStream(Boolean.FALSE).sheet("分类导出")
                    .doWrite(excelCategoryVos);

        } catch (Exception e) {
    
    
            //如果出现异常也要响应json
            ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR);
            WebUtils.renderString(response, JSON.toJSONString(result));
        }
    }

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":"操作成功"
}

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":"操作成功"
}
5.12.3.2 更新文章接口
请求方式 请求路径 是否需求token头
PUT content/article

请求体参数格式:

{
    
    
    "categoryId":"1",
    "content":"![Snipaste_20220228_224837.png](https://img-blog.csdnimg.cn/img_convert/d6c190c07ea2152b5eb6e6f9c01d5d78.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":"操作成功"
}

5.13 删除文章

5.13.1 需求

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

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

5.13.2 接口设计

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

Path请求参数:

id:要删除的文章id

响应格式:

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

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":"system",
			"id":"1",
			"isFrame":1,
			"menuName":"系统管理",
			"menuType":"M",
			"orderNum":1,
			"parentId":"0",
			"path":"system",
			"perms":"",
			"remark":"系统管理目录",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"icon":"table",
			"id":"2017",
			"isFrame":1,
			"menuName":"内容管理",
			"menuType":"M",
			"orderNum":4,
			"parentId":"0",
			"path":"content",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"system/user/index",
			"icon":"user",
			"id":"100",
			"isFrame":1,
			"menuName":"用户管理",
			"menuType":"C",
			"orderNum":1,
			"parentId":"1",
			"path":"user",
			"perms":"system:user:list",
			"remark":"用户管理菜单",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"system/role/index",
			"icon":"peoples",
			"id":"101",
			"isFrame":1,
			"menuName":"角色管理",
			"menuType":"C",
			"orderNum":2,
			"parentId":"1",
			"path":"role",
			"perms":"system:role:list",
			"remark":"角色管理菜单",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"system/menu/index",
			"icon":"tree-table",
			"id":"102",
			"isFrame":1,
			"menuName":"菜单管理",
			"menuType":"C",
			"orderNum":3,
			"parentId":"1",
			"path":"menu",
			"perms":"system:menu:list",
			"remark":"菜单管理菜单",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"",
			"icon":"#",
			"id":"1001",
			"isFrame":1,
			"menuName":"用户查询",
			"menuType":"F",
			"orderNum":1,
			"parentId":"100",
			"path":"",
			"perms":"system:user:query",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"",
			"icon":"#",
			"id":"1002",
			"isFrame":1,
			"menuName":"用户新增",
			"menuType":"F",
			"orderNum":2,
			"parentId":"100",
			"path":"",
			"perms":"system:user:add",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"",
			"icon":"#",
			"id":"1003",
			"isFrame":1,
			"menuName":"用户修改",
			"menuType":"F",
			"orderNum":3,
			"parentId":"100",
			"path":"",
			"perms":"system:user:edit",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"",
			"icon":"#",
			"id":"1004",
			"isFrame":1,
			"menuName":"用户删除",
			"menuType":"F",
			"orderNum":4,
			"parentId":"100",
			"path":"",
			"perms":"system:user:remove",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"",
			"icon":"#",
			"id":"1005",
			"isFrame":1,
			"menuName":"用户导出",
			"menuType":"F",
			"orderNum":5,
			"parentId":"100",
			"path":"",
			"perms":"system:user:export",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"",
			"icon":"#",
			"id":"1006",
			"isFrame":1,
			"menuName":"用户导入",
			"menuType":"F",
			"orderNum":6,
			"parentId":"100",
			"path":"",
			"perms":"system:user:import",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"",
			"icon":"#",
			"id":"1007",
			"isFrame":1,
			"menuName":"重置密码",
			"menuType":"F",
			"orderNum":7,
			"parentId":"100",
			"path":"",
			"perms":"system:user:resetPwd",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"",
			"icon":"#",
			"id":"1008",
			"isFrame":1,
			"menuName":"角色查询",
			"menuType":"F",
			"orderNum":1,
			"parentId":"101",
			"path":"",
			"perms":"system:role:query",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"",
			"icon":"#",
			"id":"1009",
			"isFrame":1,
			"menuName":"角色新增",
			"menuType":"F",
			"orderNum":2,
			"parentId":"101",
			"path":"",
			"perms":"system:role:add",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"",
			"icon":"#",
			"id":"1010",
			"isFrame":1,
			"menuName":"角色修改",
			"menuType":"F",
			"orderNum":3,
			"parentId":"101",
			"path":"",
			"perms":"system:role:edit",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"",
			"icon":"#",
			"id":"1011",
			"isFrame":1,
			"menuName":"角色删除",
			"menuType":"F",
			"orderNum":4,
			"parentId":"101",
			"path":"",
			"perms":"system:role:remove",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"",
			"icon":"#",
			"id":"1012",
			"isFrame":1,
			"menuName":"角色导出",
			"menuType":"F",
			"orderNum":5,
			"parentId":"101",
			"path":"",
			"perms":"system:role:export",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"",
			"icon":"#",
			"id":"1013",
			"isFrame":1,
			"menuName":"菜单查询",
			"menuType":"F",
			"orderNum":1,
			"parentId":"102",
			"path":"",
			"perms":"system:menu:query",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"",
			"icon":"#",
			"id":"1014",
			"isFrame":1,
			"menuName":"菜单新增",
			"menuType":"F",
			"orderNum":2,
			"parentId":"102",
			"path":"",
			"perms":"system:menu:add",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"",
			"icon":"#",
			"id":"1015",
			"isFrame":1,
			"menuName":"菜单修改",
			"menuType":"F",
			"orderNum":3,
			"parentId":"102",
			"path":"",
			"perms":"system:menu:edit",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"",
			"icon":"#",
			"id":"1016",
			"isFrame":1,
			"menuName":"菜单删除",
			"menuType":"F",
			"orderNum":4,
			"parentId":"102",
			"path":"",
			"perms":"system:menu:remove",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"content/article/index",
			"icon":"build",
			"id":"2019",
			"isFrame":1,
			"menuName":"文章管理",
			"menuType":"C",
			"orderNum":0,
			"parentId":"2017",
			"path":"article",
			"perms":"content:article:list",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"content/category/index",
			"icon":"example",
			"id":"2018",
			"isFrame":1,
			"menuName":"分类管理",
			"menuType":"C",
			"orderNum":1,
			"parentId":"2017",
			"path":"category",
			"perms":"content:category:list",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"content/link/index",
			"icon":"404",
			"id":"2022",
			"isFrame":1,
			"menuName":"友链管理",
			"menuType":"C",
			"orderNum":4,
			"parentId":"2017",
			"path":"link",
			"perms":"content:link:list",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"component":"content/tag/index",
			"icon":"button",
			"id":"2021",
			"isFrame":1,
			"menuName":"标签管理",
			"menuType":"C",
			"orderNum":6,
			"parentId":"2017",
			"path":"tag",
			"perms":"content:tag:index",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"icon":"#",
			"id":"2028",
			"isFrame":1,
			"menuName":"导出分类",
			"menuType":"F",
			"orderNum":1,
			"parentId":"2018",
			"path":"",
			"perms":"content:category:export",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"icon":"#",
			"id":"2024",
			"isFrame":1,
			"menuName":"友链新增",
			"menuType":"F",
			"orderNum":0,
			"parentId":"2022",
			"path":"",
			"perms":"content:link:add",
			"remark":"",
			"status":"0",
			"visible":"0"
		},
		{
    
    
			"icon":"#",
			"id":"2025",
			"isFrame":1,
			"menuName":"友链修改",
			"menuType":"F",
			"orderNum":1,
			"parentId":"2022",
			"path":"",
			"perms":"content:link:edit",
			"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"
		},
		{
    
    
			"icon":"#",
			"id":"2027",
			"isFrame":1,
			"menuName":"友链查询",
			"menuType":"F",
			"orderNum":2,
			"parentId":"2022",
			"path":"",
			"perms":"content:link:query",
			"remark":"",
			"status":"0",
			"visible":"0"
		}
	],
	"msg":"操作成功"
}

5.15 新增菜单

5.15.1 需求

​ 可以新增菜单

5.15.2 接口设计

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

请求体参数:

​ Menu类对应的json格式

响应格式:

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

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":"操作成功"
}
5.16.2.2 更新菜单
请求方式 请求路径 是否需求token头
PUT system/menu

请求体参数:

​ Menu类对应的json格式

响应格式:

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

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

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

5.17 删除菜单

5.17.1 需求

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

5.17.2 接口设计

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

Path参数:

menuId:要删除菜单的id

响应格式:

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

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

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

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":"操作成功"
}

5.19 改变角色状态

5.19.1 需求

​ 要求能够修改角色的停启用状态

5.19.2 接口设计

请求方式 请求路径 是否需求token头
PUT system/role/changeStatus

请求体:

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

响应格式:

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

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"
						},
						{
    
    
							"children":[],
							"id":"1002",
							"label":"用户新增",
							"parentId":"100"
						},
						{
    
    
							"children":[],
							"id":"1003",
							"label":"用户修改",
							"parentId":"100"
						},
						{
    
    
							"children":[],
							"id":"1004",
							"label":"用户删除",
							"parentId":"100"
						},
						{
    
    
							"children":[],
							"id":"1005",
							"label":"用户导出",
							"parentId":"100"
						},
						{
    
    
							"children":[],
							"id":"1006",
							"label":"用户导入",
							"parentId":"100"
						},
						{
    
    
							"children":[],
							"id":"1007",
							"label":"重置密码",
							"parentId":"100"
						}
					],
					"id":"100",
					"label":"用户管理",
					"parentId":"1"
				},
				{
    
    
					"children":[
						{
    
    
							"children":[],
							"id":"1008",
							"label":"角色查询",
							"parentId":"101"
						},
						{
    
    
							"children":[],
							"id":"1009",
							"label":"角色新增",
							"parentId":"101"
						},
						{
    
    
							"children":[],
							"id":"1010",
							"label":"角色修改",
							"parentId":"101"
						},
						{
    
    
							"children":[],
							"id":"1011",
							"label":"角色删除",
							"parentId":"101"
						},
						{
    
    
							"children":[],
							"id":"1012",
							"label":"角色导出",
							"parentId":"101"
						}
					],
					"id":"101",
					"label":"角色管理",
					"parentId":"1"
				},
				{
    
    
					"children":[
						{
    
    
							"children":[],
							"id":"1013",
							"label":"菜单查询",
							"parentId":"102"
						},
						{
    
    
							"children":[],
							"id":"1014",
							"label":"菜单新增",
							"parentId":"102"
						},
						{
    
    
							"children":[],
							"id":"1015",
							"label":"菜单修改",
							"parentId":"102"
						},
						{
    
    
							"children":[],
							"id":"1016",
							"label":"菜单删除",
							"parentId":"102"
						}
					],
					"id":"102",
					"label":"菜单管理",
					"parentId":"1"
				}
			],
			"id":"1",
			"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"
		}
	],
	"msg":"操作成功"
}
5.20.2.2 新增角色接口
请求方式 请求路径 是否需求token头
POST system/role

请求体:

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

响应格式:

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

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":"操作成功"
}
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":[
							{
    
    
								"children":[],
								"id":"1001",
								"label":"用户查询",
								"parentId":"100"
							},
							{
    
    
								"children":[],
								"id":"1002",
								"label":"用户新增",
								"parentId":"100"
							},
							{
    
    
								"children":[],
								"id":"1003",
								"label":"用户修改",
								"parentId":"100"
							},
							{
    
    
								"children":[],
								"id":"1004",
								"label":"用户删除",
								"parentId":"100"
							},
							{
    
    
								"children":[],
								"id":"1005",
								"label":"用户导出",
								"parentId":"100"
							},
							{
    
    
								"children":[],
								"id":"1006",
								"label":"用户导入",
								"parentId":"100"
							},
							{
    
    
								"children":[],
								"id":"1007",
								"label":"重置密码",
								"parentId":"100"
							}
						],
						"id":"100",
						"label":"用户管理",
						"parentId":"1"
					},
					{
    
    
						"children":[
							{
    
    
								"children":[],
								"id":"1008",
								"label":"角色查询",
								"parentId":"101"
							},
							{
    
    
								"children":[],
								"id":"1009",
								"label":"角色新增",
								"parentId":"101"
							},
							{
    
    
								"children":[],
								"id":"1010",
								"label":"角色修改",
								"parentId":"101"
							},
							{
    
    
								"children":[],
								"id":"1011",
								"label":"角色删除",
								"parentId":"101"
							},
							{
    
    
								"children":[],
								"id":"1012",
								"label":"角色导出",
								"parentId":"101"
							}
						],
						"id":"101",
						"label":"角色管理",
						"parentId":"1"
					},
					{
    
    
						"children":[
							{
    
    
								"children":[],
								"id":"1013",
								"label":"菜单查询",
								"parentId":"102"
							},
							{
    
    
								"children":[],
								"id":"1014",
								"label":"菜单新增",
								"parentId":"102"
							},
							{
    
    
								"children":[],
								"id":"1015",
								"label":"菜单修改",
								"parentId":"102"
							},
							{
    
    
								"children":[],
								"id":"1016",
								"label":"菜单删除",
								"parentId":"102"
							}
						],
						"id":"102",
						"label":"菜单管理",
						"parentId":"1"
					}
				],
				"id":"1",
				"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":"操作成功"
}
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":"操作成功"
}

5.22 删除角色

5.22.1 需求

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

5.22.2 接口设计

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

Path请求参数:

id:要删除的角色id

响应格式:

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

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":"操作成功"
}

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":"1",
			"remark":"超级管理员",
			"roleKey":"admin",
			"roleName":"超级管理员",
			"roleSort":"1",
			"status":"0",
			"updateBy":"0"
		},
		{
    
    
			"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":"操作成功"
}
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":"操作成功"
}

5.25 删除用户

5.25.1 需求

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

5.25.2 接口设计

不能删除当前操作的用户

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

Path请求参数:

id:要删除的用户id

响应格式:

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

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"
			},
			{
    
    
				"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-11 20:34:49"
			},
			{
    
    
				"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"
			}
		],
		"user":{
    
    
			"email":"[email protected]",
			"id":"14787164048663",
			"nickName":"sg777",
			"sex":"0",
			"status":"0",
			"userName":"sg777"
		}
	},
	"msg":"操作成功"
}
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":"操作成功"
}

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":"操作成功"
}

5.28 新增分类

5.28.1 需求

​ 需要新增分类功能

5.28.2 接口设计

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

请求体:

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

响应格式:

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

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":"操作成功"
}
5.29.2.2 更新分类
请求方式 请求路径 是否需求token头
PUT /content/category

请求体:

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

响应格式:

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

5.30 删除分类

5.30.1 需求

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

5.30.2 接口设计

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

Path请求参数:

id:要删除的分类id

响应格式:

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

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":"操作成功"
}

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":"操作成功"
}

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":"操作成功"
}
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":"操作成功"
}

5.34 删除友链

5.34.1 需求

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

5.34.2 接口设计

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

Path请求参数:

id:要删除的友链id

响应格式:

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

Guess you like

Origin blog.csdn.net/2201_75506216/article/details/132092637