SpringBoot搭建博客后台项目

环境搭建

项目功能

模块 功能
博客门户 博客文章、问答管理、标签管理、评论管理
博客权限管理系统 分类管理、标签管理、文章审核、广告管理、问答管理、系统权限管理

技术栈

软件 版本
JDK 1.8
Maven 3.3.9
Spring Boot 3.2.1
Spring Cloud Hoxton.SR5
Spring Security Oauth2 + jwt
Alibaba Nacos 注册中心&配置中心
Mybatis
Mybatis Plus 3.3.1
MySQL分库分表
Redis
druid连接池 1.2.20
Swagger-ui 接口文档
阿里云OSS对象存储

配置Maven

编辑Maven配置文件

<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
<localRepository>/Users/allen/workspace/maven/repository</localRepository>
<profiles>
<profile>
<id>nexus</id>
<repositories>
<repository>
<id>maven-public</id>
<name>naven-public</name>
<url>https://localhost:8081/repository/maven-public/</url>
</repository>
</repositories>
<pluginRepositories>
<url>https://localhost:8081/repository/maven-public/</url>
</pluginRepositories>
</profile>

<!-- New profile for Central Repository -->
<profile>
<id>central</id>
<repositories>
<repository>
<id>central</id>
<url>http://localhost:8081/repository/maven-central/</url>
</repository>
</repositories>
</profile>
</profiles>

<activeProfiles>
<activeProfile>alwaysActiveProfile</activeProfile>
<activeProfile>anotherAlwaysActiveProfile</activeProfile>
<activeProfile>central</activeProfile>
</activeProfiles>

</settings>

父模块springboot-blog

SpringBoot-blog
删除src目录,编辑pom.xml配置文件

<?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.acaiblog</groupId>
<artifactId>springboot-blog</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.9.RELEASE</version>
<relativePath/>
</parent>

<!-- 依赖版本管理-->
<properties>
<spring-cloud.version>Hoxton.SR5</spring-cloud.version>
<cloud-alibaba.version>2.2.0.RELEASE</cloud-alibaba.version>
<mybatis-plus.version>3.3.1</mybatis-plus.version>
<druid.version>1.1.21</druid.version>
<kaptcha.version>2.3.2</kaptcha.version>
<fastjson.version>1.2.8</fastjson.version>
<commons-lang.version>2.6</commons-lang.version>
<commons-collections.version>3.2.2</commons-collections.version>
<common-io.version>2.6</common-io.version>
<httpclientutil.version>1.0.4</httpclientutil.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<!-- 依赖声明-->
<dependencyManagement>
<dependencies>
<!-- springcloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<!-- -maven不支持多继承,使用 import 来依赖管理配置-->
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- mybatis-plus启动器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<version>1.9.1.RELEASE</version>
</dependency>
<!-- kaptcha 用于图形验证码-->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>${kaptcha.version}</version>
</dependency>
<!-- oss-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.8.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastJson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- http请求工具-->
<dependency>
<groupId>com.arronlong</groupId>
<artifactId>httpclientutil</artifactId>
<version>${httpclientutil.version}</version>
</dependency>
<!-- 工具依赖-->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>${commons-lang.version}</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>${commons-collections.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${common-io.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<!-- springboot打包插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<resources>
<resource>
<!-- 编译时,默认情况下不会将 mapper.xml文件编译进去,src/main/java 资源文件的路径, **/*.xml 需要编译打包的文件类型是xml文件-->
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
</project>

公共工具模块

创建模块

创建blog-util模块
blog-util
编辑blog-util/pom.xml配置文件

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

<artifactId>springboot-blog-util</artifactId>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<!-- mybatis-plus启动器-->
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 配置处理器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- lombok setter,getter-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- swagger-->
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
</dependency>
<!-- oss-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.23</version>
</dependency>
<!-- http请求工具-->
<dependency>
<groupId>com.arronlong</groupId>
<artifactId>httpclientutil</artifactId>
</dependency>
<!-- 工具依赖-->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
</dependencies>

</project>

添加自定义日志文件

创建blog-util/src/main/resources/logback.xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!--梦学谷 www.mengxuegu.com -->

<configuration>
<!-- 彩色日志 -->
<!-- 彩色日志依赖的渲染类 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
<conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
<conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<!-- ch.qos.logback.core.ConsoleAppender 表示控制台输出 -->
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</layout>
</appender>
<!--nacos相关日志级别-->
<logger name="com.alibaba.nacos.client" level="ERROR" additivity="false"/>
<root level="info">
<appender-ref ref="stdout" />
</root>
</configuration>

创建请求工具类

创建工具类com.acaiblog.util.tools.RequestUtil

package com.acaiblog.util.tools;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class RequestUtil {
public static String[] extractAndDecodeHeader(String header) throws IOException {
// `Basic ` 后面开始截取 clientId:clientSecret
byte[] base64Token = header.trim().substring(6).getBytes(StandardCharsets.UTF_8);
byte[] decoded;
try {
decoded = Base64.getDecoder().decode(base64Token);
} catch (IllegalArgumentException var8) {
throw new RuntimeException("请求头解析失败: " + header);
}
String token = new String(decoded,"UTF-8");
int delim = token.indexOf(":");
if (delim == -1) {
throw new RuntimeException("请求头无效: " + token);
} else {
return new String[]{token.substring(0,delim), token.substring(delim + 1)};
}
}
}

整合 Lombok

Lombok介绍

官方网址: https://www.projectlombok.org/features/all
Lombok工具提供一系列的注解,使用这些注解可以不用定义getter、setter、equals、构造方法等,可以消除java代码的臃肿,它会在编译时在字节码文件自动生成这些通用的方法,简化开发 人员的工作。

Lombok常用方法

注解 描述
@Getter 生成 getter 方法
@Setter 生成 setter 方法
@ToString 生成 toString 方法
@NoArgsConstructor 生成无参构造方法
@AllArgsConstructor 生成包含所有属性的构造方法
@RequiredArgsConstructor 生成包含常量和标识了NotNull的变量的构造方法(私有的private)
@Data 生成 setter、getter、toString、hashCode、equals 和 @RequiredArgsConstructor 方法
@Accessors(chain = true) 生成的 setter 方法返回当前对象

Lombok使用

检查IDE安装lombok插件

请求参数基础类BaseRequst

很多查询接口都在分页功能,而分页查询都需要至少接收请求两个参数:当前页码current和每页显示多少条记录size,通过这两个请求参数可以获取到每页查询的记录。
因为我们采用MyBatis-Plus对数据库操作,而它提供了对应的分页功能,要将currentsize封装到Page对象中,它就能够自动实现分页。
为了减少代码的重复,所以当前创建一个请求参数基础类BaseRequest<T>,其中T接收实体类,对应的对哪个表进行分页。
创建com.acaiblog.util.base.BaseRequest

package com.acaiblog.util.base;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;

import java.io.Serializable;

/**
* 请求参数基础类,带分页参数
* @param <T> 实体类
*/
@Data
@Accessors(chain = true)
public class BaseRequest implements Serializable {
@ApiModelProperty(value = "页码", required = true) // swagger用于生成接口文档
private long current;

@ApiModelProperty(value = "每页显示多少条", required = true)
private long size;
/**
* @reutrn 分页对象
*/
@ApiModelProperty(hidden = true) //不在swagger显示
public IPage<T> getPage() {
return new Page<T>().setCurrent(this.current).setSize(this.size);
}
}

统一规范响应枚举ResultEnum

ResultEnum枚举类是为了搭配Result规范响应的结果,创建com.acaiblog.util.enums.ResultEnum

package com.acaiblog.util.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ResultEnum {
SUCCESS(20000, "成功"),
ERROR(999, "错误"),
UNAUTHENTICATED(401, "请先通过身份认证"),
AUTH_FAIL(1400, "认证失败"),
TOKEN_PAST(1401, "身份过期,请重新登录"),
TOKEN_ERROR(1402, "身份令牌错误"),
HEADEA_ERROR(1403, "请求头错误"),
AUTH_USERNAME_NONE(1405, "用户名不能为空"),
AUTH_PASSWORD_NONE(1406, "用户密码不能为空"),
MENU_NO(306, "没有菜单权限,请联系管理员");
private Integer code;
public String desc;
}

规范统一响应结果Result

为了规范响应的结果,创建一个Result类来统一响应JSON格式。创建com.acaiblog.util.base.Result

package com.acaiblog.util.base;

import com.acaiblog.util.enums.ResultEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;

@Data
@NoArgsConstructor // 无参构造


// 方法
@AllArgsConstructor
public final class Result implements Serializable {
private static final Logger logger = LoggerFactory.getLogger(Result.class);

private static final long serialVersionUID = 1L;

/**
* 响应业务状态码
*/
private Integer code;

/**
* 响应信息
*/
private String message;

/**
* 响应数据
*/
private Object data;

public static Result ok() {
return new Result(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.desc, null);
}

public static Result ok(Object data) {
return new Result(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.desc, data);
}

public static Result ok(String message, Object data) {
return new Result(ResultEnum.SUCCESS.getCode(), message, data);
}

public static Result error(String message) {
logger.debug("返回错误:code={}, message={}", ResultEnum.ERROR.getCode(), message);
return new Result(ResultEnum.SUCCESS.getCode(), message, null);
}

public static Result build(int code, String message) {
logger.debug("返回结果:code={}, message={}", code, message);
return new Result(code,message,null);
}

public static Result build(ResultEnum resultEnum) {
logger.debug("返回结果:code={}, message={}", resultEnum.getCode(), resultEnum.getDesc());
return new Result(resultEnum.getCode(), resultEnum.getDesc(), null);
}

}

API接口模块

作用

在API接口模块中统一管理项目模型类(实体类),和统一定义Feign远程调用的接口,原因如下:

  1. 接口的定义离不开数据模型,所以统一在此处定义模型类。
  2. API模块中定义的接口将作为各微服务间远程调用使用,Spring Cloud Feign中使用。
  3. 接口统一管理,方便维护。

创建API模块

blog-api

pom.xml依赖

编辑blog-api/pom.xml

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

<artifactId>blog-api</artifactId>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>com.acaiblog</groupId>
<artifactId>blog-util</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- feign 调用服务接口-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
</project>

博客文章模块

创建blog-article模块

blog-article

pom.xml依赖

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

<artifactId>blog-article</artifactId>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>com.acaiblog</groupId>
<artifactId>blog-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
</dependencies>
<!-- 打包插件-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

创建启动类

创建启动类com.acaiblog.ArticleApplication

package com.acaiblog;

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

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

创建数据库

CREATE TABLE `advert` (
`id` varchar(40) NOT NULL COMMENT '主键',
`title` varchar(255) DEFAULT NULL COMMENT '广告标题',
`image_url` varchar(255) DEFAULT NULL COMMENT '广告图片',
`advert_url` varchar(255) DEFAULT NULL COMMENT '广告链接',
`advert_target` varchar(255) DEFAULT NULL COMMENT '广告跳转方式(_blank:新窗口打开,_self:当前窗口打开)',
`position` tinyint(3) DEFAULT '1' COMMENT '广告位置(1:首页轮播)',
`status` tinyint(3) DEFAULT '1' COMMENT '状态(1:正常,0:禁用)',
`sort` int(11) DEFAULT NULL COMMENT '排序',
`create_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='广告信息表';

CREATE TABLE `article` (
`id` varchar(40) NOT NULL COMMENT '主键',
`user_id` varchar(40) DEFAULT NULL COMMENT '发布者用户id',
`nick_name` varchar(40) DEFAULT NULL COMMENT '发布者用户昵称',
`user_image` varchar(255) DEFAULT NULL COMMENT '发布者头像url',
`title` varchar(255) DEFAULT NULL COMMENT '文章标题',
`summary` text COMMENT '文章简介',
`image_url` varchar(255) DEFAULT NULL COMMENT '文章主图地址',
`md_content` longtext COMMENT 'md主体内容',
`html_content` longtext COMMENT 'html主体内容',
`view_count` int(11) DEFAULT '0' COMMENT '浏览次数',
`thumhup` int(11) DEFAULT '0' COMMENT '点赞数',
`status` tinyint(3) DEFAULT '1' COMMENT '0: 已删除, 1:未审核,2:审核通过,3:审核未通过',
`ispublic` tinyint(3) DEFAULT '1' COMMENT '0:不公开,1:公开',
`create_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章信息表';

CREATE TABLE `article_label` (
`id` varchar(40) NOT NULL COMMENT '主键',
`article_id` varchar(40) DEFAULT NULL COMMENT '文章 id',
`label_id` varchar(40) DEFAULT NULL COMMENT '标签id',
`create_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章标签中间表';

CREATE TABLE `category` (
`id` varchar(40) NOT NULL COMMENT '主键',
`name` varchar(50) DEFAULT NULL COMMENT '分类名称',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`status` tinyint(3) DEFAULT '1' COMMENT '状态(1:正常,0:禁用)',
`sort` int(11) DEFAULT '1' COMMENT '排序',
`create_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章分类表';

CREATE TABLE `comment` (
`id` varchar(40) NOT NULL COMMENT '主键',
`parent_id` varchar(40) DEFAULT '-1' COMMENT '-1表示正常回复,其他值表示是评论的回复',
`user_id` varchar(40) DEFAULT NULL COMMENT '评论者用户id',
`nick_name` varchar(40) DEFAULT NULL COMMENT '评论者用户昵称',
`user_image` varchar(255) DEFAULT NULL COMMENT '评论者头像url',
`article_id` varchar(40) DEFAULT NULL COMMENT '文章id',
`content` text COMMENT '评论内容',
`create_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论信息表';

CREATE TABLE `label` (
`id` varchar(40) NOT NULL COMMENT '主键',
`category_id` varchar(40) DEFAULT NULL COMMENT '分类id',
`name` varchar(40) DEFAULT NULL COMMENT '标签名称',
`create_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签表';

CREATE TABLE `oauth_access_token` (
`token_id` varchar(128) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(128) NOT NULL,
`user_name` varchar(128) DEFAULT NULL,
`client_id` varchar(128) DEFAULT NULL,
`authentication` blob,
`refresh_token` varchar(128) DEFAULT NULL,
PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

CREATE TABLE `oauth_client_details` (
`client_id` varchar(128) NOT NULL,
`resource_ids` varchar(128) DEFAULT NULL,
`client_secret` varchar(128) DEFAULT NULL,
`scope` varchar(128) DEFAULT NULL,
`authorized_grant_types` varchar(128) DEFAULT NULL,
`web_server_redirect_uri` varchar(128) DEFAULT NULL,
`authorities` varchar(128) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(128) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

CREATE TABLE `oauth_client_token` (
`token_id` varchar(128) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(128) NOT NULL,
`user_name` varchar(128) DEFAULT NULL,
`client_id` varchar(128) DEFAULT NULL,
PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

CREATE TABLE `oauth_code` (
`code` varchar(128) DEFAULT NULL,
`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

CREATE TABLE `oauth_refresh_token` (
`token_id` varchar(128) DEFAULT NULL,
`token` blob,
`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

CREATE TABLE `question` (
`id` varchar(40) NOT NULL COMMENT '主键',
`user_id` varchar(40) DEFAULT NULL COMMENT '发布者用户id',
`nick_name` varchar(40) DEFAULT NULL COMMENT '发布者用户昵称',
`user_image` varchar(255) DEFAULT NULL COMMENT '发布者头像url',
`title` varchar(255) DEFAULT NULL COMMENT '问题标题',
`md_content` text COMMENT 'md问题内容',
`html_content` text COMMENT 'html问题内容',
`view_count` int(11) DEFAULT '0' COMMENT '浏览次数',
`thumhup` int(11) DEFAULT '0' COMMENT '点赞数',
`reply` int(11) DEFAULT '0' COMMENT '回复数',
`status` tinyint(3) DEFAULT '1' COMMENT '状态,0:已删除, 1:未解决,2:已解决',
`create_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问题信息表';

CREATE TABLE `question_label` (
`id` varchar(40) NOT NULL COMMENT '主键',
`question_id` varchar(40) DEFAULT NULL COMMENT '文章 id',
`label_id` varchar(40) DEFAULT NULL COMMENT '标签id',
`create_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问题标签中间表';

CREATE TABLE `replay` (
`id` varchar(40) NOT NULL COMMENT '主键',
`parent_id` varchar(40) DEFAULT NULL COMMENT '-1 表示正常回答,其他值表示是回答的回复',
`user_id` varchar(40) DEFAULT NULL COMMENT '回答者id',
`nick_name` varchar(40) DEFAULT NULL COMMENT '回答者用户昵称',
`user_image` varchar(255) DEFAULT NULL COMMENT '回答者头像url',
`question_id` varchar(40) DEFAULT NULL COMMENT '问题id',
`md_content` text COMMENT 'md问题内容',
`html_content` text COMMENT 'html问题内容',
`create_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='回答信息表';

CREATE TABLE `sys_menu` (
`id` varchar(40) NOT NULL COMMENT '菜单 ID',
`parent_id` varchar(40) DEFAULT NULL COMMENT '父菜单 ID (0为顶级菜单)',
`name` varchar(60) DEFAULT NULL COMMENT '菜单名称',
`url` varchar(255) DEFAULT NULL COMMENT '请求地址',
`type` int(3) DEFAULT '1' COMMENT '类型(1目录,2菜单,3按钮)',
`code` varchar(60) DEFAULT NULL COMMENT '授权标识符',
`icon` varchar(200) DEFAULT NULL COMMENT '图标',
`sort` int(11) DEFAULT '1' COMMENT '排序',
`remark` varchar(200) DEFAULT NULL COMMENT '备注',
`create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='菜单信息表';

CREATE TABLE `sys_role` (
`id` varchar(40) NOT NULL COMMENT '角色 ID',
`name` varchar(60) DEFAULT NULL COMMENT '角色名称',
`remark` varchar(200) DEFAULT NULL COMMENT '角色说明',
`create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`update_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色信息表';

CREATE TABLE `sys_role_menu` (
`id` varchar(40) NOT NULL COMMENT '主键 ID',
`role_id` varchar(40) DEFAULT NULL COMMENT '角色 ID',
`menu_id` varchar(40) DEFAULT NULL COMMENT '菜单 ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色权限表';

CREATE TABLE `sys_user` (
`id` varchar(40) NOT NULL COMMENT '用户 ID',
`username` varchar(60) DEFAULT NULL COMMENT '用户名',
`password` varchar(60) DEFAULT NULL COMMENT '密码,加密存储, admin/1234',
`is_account_non_expired` int(2) DEFAULT '1' COMMENT '帐户是否过期(1 未过期,0已过期)',
`is_account_non_locked` int(2) DEFAULT '1' COMMENT '帐户是否被锁定(1 未过期,0已过期)',
`is_credentials_non_expired` int(2) DEFAULT '1' COMMENT '密码是否过期(1 未过期,0已过期)',
`is_enabled` int(2) DEFAULT '1' COMMENT '帐户是否可用(1 可用,0 删除用户)',
`nick_name` varchar(60) DEFAULT NULL COMMENT '昵称',
`image_url` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '头像url',
`mobile` varchar(20) DEFAULT NULL COMMENT '注册手机号',
`email` varchar(50) DEFAULT NULL COMMENT '注册邮箱',
`create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`pwd_update_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '密码更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `username` (`username`) USING BTREE,
UNIQUE KEY `mobile` (`mobile`) USING BTREE,
UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户信息表';

CREATE TABLE `sys_user_role` (
`id` varchar(40) NOT NULL COMMENT '主键 ID',
`user_id` varchar(40) DEFAULT NULL COMMENT '用户 ID',
`role_id` varchar(40) DEFAULT NULL COMMENT '角色 ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户角色关系表';

整合Swagger

介绍

Swagger(https://swagger.io/)是最流行的API开发工具,它遵循OpenAPI Specification(OpenAPI规范,也简称OAS)。
OpenAPI Specification (https://github.com/OAI/OpenAPI-Specification) 是Linux基金会的一个项目,规定了一种用来描述API格式或API定义的语言,来规范RESTful服务开发过程,目前版本是V3.0,并且已经发布并开源在github上。 OpenAPI是规范,Swagger是具体实现。
Swagger可以贯穿于整个API生态,如API的设计、编写API文档、测试和部署。
Swagger是一种通用的,和编程语言无关的API描述规范。
Spring Boot可以集成Swagger来生成接口文档。

常用注解

@Api

/**
* 描述:用于描述API
* @param value:API的标题
* @param tags:API的标签
* @param description:API的详细描述
*/
@Api(value = "Pet Store API", tags = "Pet Operations")

@ApiOperation

/**
* 描述:用于描述操作或端点
* @param value:操作的简要描述
* @param notes:操作的详细描述
* @param nickname:操作的别名
*/
@ApiOperation(value = "Get pet by ID", notes = "Returns a pet based on ID", nickname = "getPetById")

@ApiParam

/**
* 描述:用于描述操作参数
* @param name:参数名称
* @param value:参数描述
* @param required:是否必须
*/
@ApiParam(name = "petId", value = "ID of the pet", required = true)

@ApiImplicitParams

/**
* 描述:用于描述多个操作参数
*/
@ApiImplicitParams({
@ApiImplicitParam(name = "username", value = "Username", required = true),
@ApiImplicitParam(name = "password", value = "Password", required = true)
})

@ApiImplicitParam

/**
* 描述:用于描述操作参数
* @param name:参数名称
* @param value:参数描述
* @param paramType:参数类型
* @param dataType:参数的数据类型
* @param example:参数的示例
*/
@ApiImplicitParam(name = "pet", value = "Pet object", required = true, dataType = "Pet", paramType = "body")

@ApiResponses

/**
* 描述:用于描述操作的多个响应
*/
@ApiResponses({
@ApiResponse(code = 200, message = "Successful operation"),
@ApiResponse(code = 404, message = "Pet not found")
})

@ApiResponse

/**
* 描述:用于描述操作的单个响应
* @param code:HTTP状态码
* @param message:响应消息
*/
@ApiResponse(code = 200, message = "Successful operation")

@ApiModel

/**
* 描述:用于描述数据模型
* @param value: 数据模型的名称
* @param description:数据模型的描述
*/
@ApiModel(value = "Pet", description = "Pet information")

@ApiModelProperty

/**
* 描述:用于描述数据模型的属性
* @param value:属性
* @param example:属性示例
*/
@ApiModelProperty(value = "Pet name", example = "Buddy")

Swagger使用

在blog-util工程的pom.xml文件中添加第三方swagger依赖

<!--        swagger-->
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<!-- 版本号 在父工程的pom.xml已经声明了-->
</dependency>

在启动类上添加@EnableSwagger2Doc,启动Swagger。编辑blog-article/src/main/java/com/acaiblog/ArticleApplication.java

package com.acaiblog;

import com.spring4all.swagger.EnableSwagger2Doc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

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

com.acaiblog.article.controller.CategoryController接口中添加Swagger注解

package com.acaiblog.article.controller;

import com.acaiblog.article.req.CategoryREQ;
import com.acaiblog.article.service.ICategoryService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(value = "分类接口", description = "分类接口,提供增删改查功能")
@RestController // 返回json属性
@RequestMapping("/category")
public class CategoryController {
@Autowired
ICategoryService categoryService;

@ApiOperation("根据分类名称与状态查询分类列表接口")
@PostMapping("/search")
public Result search(@RequestBody CategoryREQ req) {
return categoryService.queryPage(req);
}
}

com.acaiblog.article.req.CategoryREQ类中添加Swagger注解

package com.acaiblog.article.req;

import com.acaiblog.entities.Category;
import com.acaiblog.util.base.BaseRequest;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;

/**
* 分类查询条件属性
*/
@Data
@Accessors
@ApiModel(value = "CategoryREQ对象", description = "类别查询条件")
public class CategoryREQ extends BaseRequest<Category> {
/**
* 分类名称
*/
@ApiModelProperty(value = "分类名称")
private String name;

/**
* 状态,1正常,0禁用
*/
@ApiModelProperty(value = "分类状态")
private Integer status;
}

在 com.mengxuegu.blog.entities.Category 实体类中添加 Swagger 注解

package com.acaiblog.entities;

import io.swagger.annotations.ApiModel;

@Data
@TableName
@ApiModel(value = "Category对象", description = "类别信息表")
public class Category implements Serializable {
}

在启动类上添加@EnableSwagger2Doc注解开启生成接口文档

package com.acaiblog;

import com.spring4all.swagger.EnableSwagger2Doc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

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

访问Swagger:http://127.0.0.1:8001/article/swagger-ui.html#/

分类管理

列表接口

需求分析

通过分类名称、状态查询列表数据,并实现分页功能

类别请求类CategoryQEQ

REQ:作为request简写,主要作用是把将查询条件请求参数封装为一个对象。
比如:分类模块,通过分类名称、状态码、页码和每页显示条数 作为条件,查询出对应分类数据。则可以将这些请求参数封装成一个REQ对象,其中页码和每页显示条数在blog-util中的BaseReuest已经定义了,新建的CategoryQEQ类直接继承它即可。
创建分类实体类Category,com.acaiblog.entities.Category

package com.acaiblog.entities;

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

import java.io.Serializable;
import java.util.Date;

@Data
@TableName
public class Category implements Serializable {
private static final long serialVersionUID = 1L;

/**
* 主键,分布式id
*/
@TableId
private String id;

/**
* 分类名称
*/
private String name;

/**
* 备注
*/
private String remark;

/**
* 状态,1正常,0禁用
*/
private Integer status;

/**
* 排序
*/
private Integer sort;

/**
* 创建时间
*/
private Date createDate;

/**
* 更新时间
*/
private Date updateDate;
}

创建com.acaiblog.article.req.CategoryREQ

package com.acaiblog.article.req;

import com.acaiblog.entities.Category;
import com.acaiblog.util.base.BaseRequest;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;

/**
* 分类查询条件属性
*/
@Data
@Accessors
public class CategoryREQ extends BaseRequest<Category> {
/**
* 分类名称
*/
@ApiModelProperty(value = "分类名称")
private String name;

/**
* 状态,1正常,0禁用
*/
@ApiModelProperty(value = "分类状态")
private Integer status;
}

编写CategoryMapper

创建接口com.acaiblog.article.mapper.CategoryMapper继承BaseMapper<Category>接口。MyBatis-PlusBaseMapper<T>接口提供了很多对T表的数据操作方法

package com.acaiblog.article.mapper;

import com.acaiblog.entities.Category;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

public interface CategoryMapper extends BaseMapper<Category> {
}

java目录下创建映射文件com.acaiblog.article.mapper.xml.CategoryMapper.xml

<?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.acaiblog.article.mapper.CategoryMapper">
</mapper>

SpringBoot配置文件

resources下创建application.yaml文件

swagger:
description: '博客分类、标签、文章、广告接口'
server:
port: 8001
servlet:
context-path: /article # 上下文件路径,请求前缀 ip:port/article
spring:
application:
name: article-server # 应用名称
# 数据源
datasource:
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 123456
url: jdbc:mariadb://localhost:3306/blog?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true
driver-class-name: org.mariadb.jdbc.Driver
# 数据源其他配置, 在 DruidConfig配置类中手动绑定
initialSize: 8
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
mybatis-plus:
type-aliases-package: com.acaiblog.entities
# xxxMapper.xml 路径
mapper-locations: classpath*:com/acaiblog/article/mapper/**/*.xml
# 日志级别,会打印sql语句
logging:
level:
com.acaiblog.article.mapper: debug
debug: false

业务层

创建接口com.acaiblog.article.service.ICategoryService继承IService<Category>接口实现IService<T>接口,提供了常用更复杂的对T数据表的操作,比如:支持Lambda表达式,批量删除、自动新增或更新操作等方法。定义一个通过条件分页查询分类信息的方法queryPage

package com.acaiblog.article.service;

import com.acaiblog.article.req.CategoryREQ;
import com.acaiblog.entities.Category;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* 文章分类表 服务类
*/
public interface ICategoryService extends IService<Category> {
/**
* 分页查询分类信息
* @param req 查询条件
*/
Result queryPage(CategoryREQ req);
}

创建实现类com.acaiblog.article.service.impl.CategoryServiceImpl继承ServiceImpl<CategoryMapper, Category>类 , 并且实现ICategoryServic接口

package com.acaiblog.article.service.impl;

import com.acaiblog.article.mapper.CategoryMapper;
import com.acaiblog.article.req.CategoryREQ;
import com.acaiblog.article.service.ICategoryService;
import com.acaiblog.entities.Category;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements ICategoryService {
@Override
public Result queryPage(CategoryREQ req) {
QueryWrapper<Category> wrapper = new QueryWrapper();
if (req.getStatus() != null) {
wrapper.eq("status", req.getStatus());
}
if (StringUtils.isNotEmpty(req.getName())) {
wrapper.like("name", req.getName());
}
wrapper.orderByDesc("status").orderByAsc("sort");
return Result.ok(baseMapper.selectPage(req.getPage(), wrapper));
}
}

控制层

创建控制层类com.acaiblog.article.controller.CategoryController

package com.acaiblog.article.controller;
import com.acaiblog.article.req.CategoryREQ;
import com.acaiblog.article.service.ICategoryService;
import com.acaiblog.util.base.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* 文章分类表,前端控制器
*/
@RestController // 所有方法的返回值转换为Json字符串进行响应
@RequestMapping("/category")
public class CategoryController {
@Autowired
ICategoryService categoryService;

/**
* 分页查询,@RequestBody 请求体中的json数据
* @param req 分类名与状态查询和分页参数
* @return
*/
@PostMapping("/search") // post请求/category/search
public Result search(@RequestBody CategoryREQ req) {
return categoryService.queryPage(req);
}
}

Mybatis-plus配置类

添加Mybatis-Plus配置类开启事务管理、Mapper接口扫描、 分页功能。创建com.acaiblog.article.config.MybatisPlusConfig

package com.acaiblog.article.config;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@EnableTransactionManagement // 开启事务管理
@MapperScan("com.acaiblog.article.mapper") // 扫描Mapper接口
@Configuration
public class MybatisPlusConfig {
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}

测试

使用postman发送post请求:http://127.0.0.1:8001/article/category/search
返回数据:

{
"code": 20000,
"message": "成功",
"data": {
"records": [],
"total": 0,
"size": 0,
"current": 0,
"orders": [],
"hitCount": false,
"searchCount": true,
"pages": 0
}
}

分类增删改查接口

控制层

编辑com.acaiblog.article.controller.CategoryController

package com.acaiblog.article.controller;

import com.acaiblog.article.req.CategoryREQ;
import com.acaiblog.article.service.ICategoryService;
import com.acaiblog.entities.Category;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* 文章分类表,前端控制器
*/
@RestController // 所有方法的返回值转换为Json字符串进行响应
@RequestMapping("/category")
public class CategoryController {
@Autowired
ICategoryService categoryService;

/**
* 通过类别id查询类别详情
* @param id
* @return
*/
@ApiOperation("查询类别详情接口")
@ApiImplicitParam(name = "id", value = "类别ID", required = true)
@GetMapping("/{id}")
public Result view(@PathVariable("id") String id) {
return Result.ok(categoryService.getById(id));
}

/**
* 更新类别
* @param category
* @return
*/
@ApiOperation("更新类别信息接口")
@PutMapping // 对应请求地址是 /category put方法
public Result update(@RequestBody Category category) {
categoryService.updateById(category);
return Result.ok();
}

/**
* 新增类别接口
* @param category
* @return
*/
@ApiOperation("新增类别接口")
@PostMapping // 对应请求地址是/category post方法
public Result save(@RequestBody Category category) {
categoryService.save(category);
return Result.ok();
}

@ApiOperation("删除类别接口")
@ApiImplicitParam(name = "id", value = "类别ID", required = true)
@DeleteMapping("/{id}")
public Result delete(@PathVariable("id") String id) {
categoryService.removeById(id);
return Result.ok();
}
}

业务层

新增或修改分类时,应该将更新时间updateDate设置为当前时间。编辑com.acaiblog.article.service.impl.CategoryServiceImpl实现类中 ,重写MyBatis-plusServiceImpl类中提供的updateById方法

package com.acaiblog.article.service.impl;

import org.springframework.stereotype.Service;

@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements ICategoryService {

/**
* 更新,重写ServiceImpl父类方法,设置更新时间
* @param category
* @return
*/
public boolean updateById(Category category) {
category.setUpdateDate(new Date());
return super.updateById(category);
}
}

分类状态接口

查询所有分类状态正常的数据
编辑com.acaiblog.article.service.ICategoryService接口创建一个方法findAllNormal

package com.acaiblog.article.service;

/**
* 文章分类表 服务类
*/
public interface ICategoryService extends IService<Category> {
/**
* 分页查询分类信息
* @param req 查询条件
*/
Result queryPage(CategoryREQ req);

/**
* 查询所有正常状态的分类
* @return
*/
Result findAllNormal();
}

编辑com.acaiblog.article.service.impl.CategoryServiceImpl实现类实现查询逻辑

package com.acaiblog.article.service.impl;

@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements ICategoryService {

@Override
public Result findAllNormal() {
QueryWrapper<Category> wrapper = new QueryWrapper<>();
wrapper.eq("status", 1); // 1 正常 0 禁用
return Result.ok(baseMapper.selectList(wrapper));
}
}

编辑com.acaiblog.article.controller.CategoryController类中实现list方法

package com.acaiblog.article.controller;

/**
* 文章分类表,前端控制器
*/
@RestController // 所有方法的返回值转换为Json字符串进行响应
@RequestMapping("/category")
public class CategoryController {
@Autowired
ICategoryService categoryService;

/**
* 查询状态正常的类别接口
* @return
*/
@ApiOperation("查询状态正常的类别接口")
@GetMapping("/list")
public Result list() {
return categoryService.findAllNormal();
}
}

MyBatis-plus代码生成器

MyBatis-Plus代码生成器是MyBatis-Plus框架的一个重要组件,主要用于生成与数据库表结构对应的实体类、Mapper接口以及基本的XML映射文件。参考官网:https://mybatis.plus/guide/generator.html

创建生成器模块

blog-genertor

pom依赖

编辑blog-generator/pom.xml

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

<artifactId>blog-generator</artifactId>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.acaiblog</groupId>
<artifactId>blog-util</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- 代码生成器核心依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
</dependencies>
</project>

生成器常用配置

相关代码生成器配置项,可参考文档:https://mybatis.plus/config/

创建生成器代码

创建类com.acaiblog.generator.CodeGenerator

package com.acaiblog.generator;

import ch.qos.logback.classic.Logger;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import org.apache.commons.lang.StringUtils;
import org.slf4j.LoggerFactory;
import org.slf4j.impl.StaticLoggerBinder;

import java.util.Scanner;

public class CodeGenerator {
// 生成的代码放到哪个模块
private static final String PROJECT_NAME = "blog-article";
// 数据库名称
private static final String DATABASE_NAME = "blog";
// 子包名
private static final String MODULE_NAME = "article";
// 执行main方法控制台输入模块表名回车自动生成对应项目目录中

public static void main(String[] args) {
// 代码生成器
AutoGenerator autoGenerator = new AutoGenerator();
// 数据源配置
DataSourceConfig dataSourceConfig = new DataSourceConfig();
dataSourceConfig.setUrl("jdbc:mariadb://localhost:3306/" + DATABASE_NAME + "?useUnicode=true&useSSL=false&characterEncoding=utf8");
dataSourceConfig.setDriverName("org.mariadb.jdbc.Driver");
dataSourceConfig.setUsername("root");
dataSourceConfig.setPassword("123456");
autoGenerator.setDataSource(dataSourceConfig);
// 全局配置
GlobalConfig globalConfig = new GlobalConfig();
String projectPath = System.getProperty("user.dir") + "/";
globalConfig.setOutputDir(projectPath + PROJECT_NAME + "/src/main/java");
globalConfig.setIdType(IdType.ASSIGN_ID); // 分布式id
globalConfig.setAuthor("阿才的博客");
globalConfig.setFileOverride(true); // 覆盖现有的代码
globalConfig.setOpen(false); // 是否生成后打开
globalConfig.setDateType(DateType.ONLY_DATE);
globalConfig.setSwagger2(true); // 实体属性swagger注解
autoGenerator.setGlobalConfig(globalConfig);
// 包配置
PackageConfig packageConfig = new PackageConfig();
packageConfig.setParent("com.acaiblog"); // 父包名
packageConfig.setController(MODULE_NAME + ".controller");
packageConfig.setService(MODULE_NAME + ".service");
packageConfig.setServiceImpl(MODULE_NAME + ".service.impl");
packageConfig.setMapper(MODULE_NAME + ".mapper");
packageConfig.setXml(MODULE_NAME + ".mapper.xml");
packageConfig.setEntity("entities"); // 实体类存储包名
autoGenerator.setPackageInfo(packageConfig);
// 策略配置
StrategyConfig strategyConfig = new StrategyConfig();
strategyConfig.setNaming(NamingStrategy.underline_to_camel);
strategyConfig.setColumnNaming(NamingStrategy.underline_to_camel);
strategyConfig.setEntityLombokModel(true); // 使用lombok
strategyConfig.setEntitySerialVersionUID(true); // 实体类实现接口Serializable
strategyConfig.setRestControllerStyle(true); // @RestController
strategyConfig.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategyConfig.setControllerMappingHyphenStyle(true);
strategyConfig.setTablePrefix("mxg_"); // 去掉表前缀
autoGenerator.setStrategy(strategyConfig);
autoGenerator.setTemplateEngine(new FreemarkerTemplateEngine());
autoGenerator.execute();
}
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入" + tip + ":");
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip);
}
}

生成label标签代码

执行blog-generator/src/main/java/com/acaiblog/generator/CodeGenerator.javamain方法:
执行报错:

log4j:ERROR setFile(null,true) call failed.
java.io.FileNotFoundException: /logs/httpclient/httputil.log (No such file or directory)

报错原因:pom.xml中引用了httpclientutil,在/Users/allen/workspace/maven/repository/com/arronlong/httpclientutil/1.0.4/httpclientutil-1.0.4.jar!/log4j.properties日志文件中定义了该日志文件导致。

标签管理

列表接口

需求分析

通过标签名称、分类id查询列表数据,并实现分页功能。

修改标签实体类

因为标签列表展示需要显示分类名称,所以在Label实体类中添加categoryName属性。编辑blog-article/src/main/java/com/acaiblog/entities/Label.java

package com.acaiblog.entities;

public class Label implements Serializable {

@ApiModelProperty("分类名称")
@TableField(exist = false) // 不是label表中的字段
private String categoryName;
}

标签请求类LabelREQ

将标签名称和分类ID属性封装成一个BO对象作为条件,并继承BaseRequest。 创建com.acaiblog.article.req.LabelREQ

package com.acaiblog.article.req;

import com.acaiblog.util.base.BaseRequest;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;

/**
* 标签条件查询属性
*/
@Data
@Accessors
@ApiModel(value = "LabelREQ对象", description = "标签条件查询条件")
public class LabelREQ extends BaseRequest {
@ApiModelProperty(value = "标签名称")
private String name;

@ApiModelProperty(value = "分类ID")
private String categoryId;

}

LabelMapper

编辑blog-article/src/main/java/com/acaiblog/article/mapper/LabelMapper.java中定义条件分页查询标签数据的Mapper接口方法

package com.acaiblog.article.mapper;

import com.acaiblog.article.req.LabelREQ;
import com.acaiblog.entities.Label;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Param;

/**
* <p>
* 标签表 Mapper 接口
* </p>
*
* @author 阿才的博客
* @since 2024-01-04
*/
public interface LabelMapper extends BaseMapper<Label> {
/**
* 只要将非 limit 的sql 语句写在 对应的 id="queryPage"里面(LabelMapper.xml)
* 不需要手动去分页,而mybaits-plus会自动实现分页
* 但是你必须第1个参数传入Page,第2个参数通过 @Param 取别名
* 最终查询到的数据会被封装到IPage实现里面
* @param page
* @param req
* @return
*/
IPage<Label> queryPage(IPage<Label> page, @Param("req")LabelREQ req);
}

编辑blog-article/src/main/java/com/acaiblog/article/mapper/xml/LabelMapper.xml中定义条件分页查询的SQL语句

<?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.acaiblog.article.mapper.LabelMapper">
<!-- 条件查询所有标签-->
<select id="queryPage" resultMap="Label">
SELECT
t1.`id`, t1.`category_id`, t1.`name`, t1.`create_date`, t1.`update_date`, t2.NAME category_name
FROM label t1 JOIN category t2
ON t1.category_id = t2.id
WHERE 1=1
<if test="req.name != null and req.name !=''">
and t1.name LIKE CONCAT('%', #{req.name}, '%')
</if>
<if test="req.categoryId != null and req.categoryId !=''">
and t1.category_id = #{req.categoryId}
</if>
ORDER BY t1.`update_date` desc
</select>
</mapper>

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/ILabelService.java接口定义queryPage抽象方法

package com.acaiblog.article.service;

import com.acaiblog.article.req.LabelREQ;
import com.acaiblog.entities.Label;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 条件与分页查询标签列表
* </p>
*
* @author 阿才的博客
* @since 2024-01-04
*/
public interface ILabelService extends IService<Label> {
Result queryPage(LabelREQ req);
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/LabelServiceImpl.java类实现ILabelService接口方法

package com.acaiblog.article.service.impl;

import com.acaiblog.article.req.LabelREQ;
import com.acaiblog.entities.Label;
import com.acaiblog.article.mapper.LabelMapper;
import com.acaiblog.article.service.ILabelService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

/**
* <p>
* 标签表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-04
*/
@Service
public class LabelServiceImpl extends ServiceImpl<LabelMapper, Label> implements ILabelService {
@Override
public Result queryPage(LabelREQ req) {
IPage<Label> page = baseMapper.queryPage(req.getPage(), req);
return Result.ok(page);
}
}

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/LabelController.java实现search方法接收查询请求,响应数据。

package com.acaiblog.article.controller;


import com.acaiblog.article.req.LabelREQ;
import com.acaiblog.article.service.ILabelService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

/**
* <p>
* 标签表 前端控制器
* </p>
*
* @author 阿才的博客
* @since 2024-01-04
*/
@RestController
@Api(value = "标签管理接口", description = "标签管理接口,提供增删改查")
@RequestMapping("//label")
public class LabelController {
@Autowired
ILabelService labelService;

/**
* 分页查询,@RequestBody 请求体中的json数据
* @param req
* @return
*/
@ApiOperation("根据标签名称与状态查询标签分页列表接口")
@PostMapping("/search")
public Result search(@RequestBody LabelREQ req) {
return labelService.queryPage(req);
}

}

接口测试

发送POST请求:http://127.0.0.1:8001/article/label/search

{
"categoryId": "2",
"current": 1,
"name": "spring",
"size": 20
}

增删改接口

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/LabelController.java类实现标签增删改接口

package com.acaiblog.article.controller;


import com.acaiblog.article.req.LabelREQ;
import com.acaiblog.article.service.ILabelService;
import com.acaiblog.entities.Label;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 标签表 前端控制器
* </p>
*
* @author 阿才的博客
* @since 2024-01-04
*/
@RestController
@Api(value = "标签管理接口", description = "标签管理接口,提供增删改查")
@RequestMapping("/label")
public class LabelController {
@Autowired
ILabelService labelService;

@ApiOperation("根据ID查询标签详情接口")
@ApiImplicitParam(name = "id", value = "标签ID", required = true)
@GetMapping("/id")
public Result view(@PathVariable("id") String id) {
return Result.ok(labelService.getById(id));
}

@ApiOperation("修改标签接口")
@PutMapping
public Result update(@RequestBody Label label) {
labelService.updateById(label);
return Result.ok();
}

@ApiOperation("新增标签接口")
@PostMapping
public Result save(@RequestBody Label label) {
labelService.save(label);
return Result.ok();
}

@ApiOperation("标签删除接口")
@ApiImplicitParam(name = "id", value = "标签ID", required = true)
@DeleteMapping("/id")
public Result delete(@PathVariable("id") String id) {
labelService.removeById(id);
return Result.ok();
}
}

业务层

修改标签时,应该将更新时间updateDate设置为当前时间,在blog-article/src/main/java/com/acaiblog/article/service/impl/LabelServiceImpl.java重写updateById方法

package com.acaiblog.article.service.impl;

import com.acaiblog.article.req.LabelREQ;
import com.acaiblog.entities.Label;
import com.acaiblog.article.mapper.LabelMapper;
import com.acaiblog.article.service.ILabelService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

import java.util.Date;

/**
* <p>
* 标签表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-04
*/
@Service
public class LabelServiceImpl extends ServiceImpl<LabelMapper, Label> implements ILabelService {

/**
* 重写ServiceImpl父类方法,设置更新时间
* @param label
* @return
*/
@Override
public boolean updateById(Label label) {
label.setUpdateDate(new Date());
return super.updateById(label);
}
}

接口测试

  1. 查询,GET:localhost:8001/article/label/1
  2. 新增,POST 方式,请求地址:localhost:8001/article/label
  3. 修改,PUT 方式,请求地址:localhost:8001/article/label
  4. 删除,DELETE 方式,请求地址:localhost:8001/article/label/f563383db2fabf56a0e6b3ee986c6b15

标签和分类查询接口

需求分析

查询分类表category中状态为正常的数据,与对应分类下所有的标签label

Category实体类

编辑blog-article/src/main/java/com/acaiblog/entities/Category.java封装当前类下所有标签

package com.acaiblog.entities;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;
import java.util.List;

@Data
@TableName
@ApiModel(value = "Category对象", description = "类别信息表")
public class Category implements Serializable {

@ApiModelProperty(value = "分类中的标签集合")
@TableField(exist = false)
private List<Label> labelList;
}

CategoryMapper

编辑blog-article/src/main/java/com/acaiblog/article/mapper/CategoryMapper.java添加抽象方法findCategoryAndLabel

package com.acaiblog.article.mapper;

import com.acaiblog.entities.Category;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import java.util.List;

public interface CategoryMapper extends BaseMapper<Category> {
/**
* 查询正常状态下分类及所有分类标签
* @return
*/
List<Category> findCategoryAndLabel();
}

编辑blog-article/src/main/java/com/acaiblog/article/mapper/xml/CategoryMapper.xml添加sql查询实现


<?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.acaiblog.article.mapper.CategoryMapper">
<!-- 封装分类及其所有标签-->
<resultMap id="categoryLabelMap" type="Category">
<!-- 映射主键-->
<id column="id" property="id"/>
<!-- 映射一对一-->
<result column="name" property="name"/>
<!-- 映射一对多关系 property: 对应 Category 的属性名 javaType: 属性的数据类型 ofType: 配置集合内部的数据类型-->
<collection property="labelList" javaType="list" ofType="Label">
<!-- 映射主键-->
<id column="label_id" property="id"/>
<!-- 映射普通属性-->
<result column="label_name" property="name"/>
</collection>
</resultMap>
<select id="findCategoryAndLabel" resultMap="categoryLabelMap">
SELECT
t1.id, t1.name, t2.id label_id, t2.name label_name
FROM
category t1 LEFT JOIN label t2
ON
t1.id = t2.category_id
WHERE t1.`status` =1
ORDER BY t1.sort
</select>
</mapper>

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/ICategoryService.java添加抽象方法findCategoryAndLabel

package com.acaiblog.article.service;

/**
* 文章分类表 服务类
*/
public interface ICategoryService extends IService<Category> {

/**
* 查询正常状态的分类及分类下的所有标签
* @return
*/
Result findCategoryAndLabel();
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/CategoryServiceImpl.java添加抽象方法findCategoryAndLabel的实现

package com.acaiblog.article.service.impl;

@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements ICategoryService {

@Override
public Result findCategoryAndLabel() {
return Result.ok(baseMapper.findCategoryAndLabel());
}
}

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/CategoryController.java类中实现findCategoryAndLabel方法

package com.acaiblog.article.controller;

/**
* 文章分类表,前端控制器
*/
@RestController // 所有方法的返回值转换为Json字符串进行响应
@RequestMapping("/category")
public class CategoryController {

@ApiOperation("查询正常状态的分类及分类下的所有标签接口")
@GetMapping("/label/list")
public Result findCategoryAndLabel() {
return categoryService.findCategoryAndLabel();
}
}

测试接口

发送GET请求:http://127.0.0.1:8001/article/category/label/list

API接口

为第三方系统提供单独的API接口,比如:http://127.0.0.1:8001/api/category
在blog-article模块下创建com.acaiblog.article.api.ApiCategoryController

package com.acaiblog.article.api;

import com.acaiblog.article.service.ICategoryService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(value = "分类管理API接口", description = "分类管理API接口,不需要身份认证就可以访问")
@RestController
@RequestMapping("/api/category")
public class ApiCategoryController {
@Autowired
ICategoryService categoryService;

/**
* 获取所有正常状态的分类
* @return
*/
@GetMapping("/list")
public Result list() {
return categoryService.findAllNormal();
}

@ApiOperation("查询正常状态的分类及分类下的所有标签接口")
@GetMapping("/label/list")
public Result findCategoryAndLabel() {
return categoryService.findCategoryAndLabel();
}
}

文章管理

生成文章代码模版

运行blog-generator/src/main/java/com/acaiblog/generator/CodeGenerator.java类的main方法输入表名article生成文章模版代码。生成的代码路径在blog-article/src/main/java/com/acaiblog/article

列表接口

需求分析

通过文章标题 title、状态查询列表数据,并实现分页功能。

文章状态枚举类

在blog-util模块创建com.acaiblog.util.enums.ArticleStatusEnum

package com.acaiblog.util.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ArticleStatusEnum {
DELETE(0, "已删除"), WAIT(1, "待审核"), SUCCESS(2, "审核通过"), FAIL(3, "审核失败");
private Integer code;
private String desc;
}

文章请求类

将文章标题title、状态属性封装成一个REQ对象作为条件,继承BaseRequest<Article>。在blog-article模块创建com.acaiblog.article.req.ArticleREQ请求类

package com.acaiblog.article.req;

import com.acaiblog.entities.Article;
import com.acaiblog.util.base.BaseRequest;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;

@Data // 自动生成类的常用方法,如Getter、Setter、toString
@Accessors // Lombok注解,支持链式调用(fluent API)
@ApiModel(value = "ArticleREQ对象", description = "文章查询条件") // Swagger注解,用于描述类的信息,生成API文档时会被引用
public class ArticleREQ extends BaseRequest<Article> {
@ApiModelProperty(value = "文章标题")
private String title;

@ApiModelProperty(value = "0:已删除, 1:未审核, 2:已审核")
private Integer status;
}

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/IArticleService.java接口定义条件分页查询文章数据的queryPage抽象方法

package com.acaiblog.article.service;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 文章信息表 服务类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface IArticleService extends IService<Article> {
Result queryPage(ArticleREQ req);
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/ArticleServiceImpl.java类实现IArticleService接口的queryPage方法

package com.acaiblog.article.service.impl;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.article.mapper.ArticleMapper;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

/**
* <p>
* 文章信息表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements IArticleService {
@Override
public Result queryPage(ArticleREQ req) {
QueryWrapper<Article> wrapper = new QueryWrapper<>();

if (req.getStatus() != null) {
wrapper.eq("status", req.getStatus());
}
if (StringUtils.isNotEmpty(req.getTitle())) {
wrapper.like("title", req.getTitle());
}
// 排序
wrapper.orderByDesc("update_date");
return Result.ok(baseMapper.selectPage(req.getPage(), wrapper));
}

}

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/ArticleController.java实现search方法接收查询请求,响应数据

package com.acaiblog.article.controller;


import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

/**
* <p>
* 文章信息表 前端控制器
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Api(value = "文章管理接口", description = "文章管理接口,提供增删改查")
@RestController
@RequestMapping("/article")
public class ArticleController {
@Autowired
private IArticleService articleService;

@ApiOperation("根据文章标题与状态查询文章分页列表接口")
@PostMapping("/search")
public Result search(@RequestBody ArticleREQ req) { // @RequestBody将前端post请求中的data传递给接口
return articleService.queryPage(req);
}
}

接口测试

发送POST请求:http://127.0.0.1:8001/article/article/search

文章详情接口

实体Entity

编辑blog-article/src/main/java/com/acaiblog/entities/Article.java添加labelIds(用于修改文章时,回显标签)和labelList(用于查看文章详情时,显示标签名)

package com.acaiblog.entities;

import com.baomidou.mybatisplus.annotation.IdType;
import java.util.Date;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import java.util.List;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
* <p>
* 文章信息表
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value="Article对象", description="文章信息表")
public class Article implements Serializable {

@ApiModelProperty(value = "所属标签ID集合")
@TableField(exist = false)
private List<String> labelIds;

@ApiModelProperty(value = "所属标签对象集合")
@TableField(exist = false)
private List<String> labelList;
}

数据访问层

通过文章id查询文章详情与标签,编辑blog-article/src/main/java/com/acaiblog/article/mapper/ArticleMapper.java类添加抽象方法findArticleAndLabelById

package com.acaiblog.article.mapper;

import com.acaiblog.entities.Article;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

/**
* <p>
* 文章信息表 Mapper 接口
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface ArticleMapper extends BaseMapper<Article> {
Article findArticleAndLabelById(String id);
}

编辑blog-article/src/main/java/com/acaiblog/article/mapper/xml/ArticleMapper.xml添加sql查询实现

<?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.acaiblog.article.mapper.ArticleMapper">
<!-- 通过文章id查询文章详情与标签-->
<select id="findArticleAndLabelById" resultMap="ArticleAndLabelMap">
SELECT
t1.*, t3.id label_id, t3.`name` label_name
FROM
article t1
LEFT JOIN article_label t2 ON t1.id = t2.article_id
LEFT JOIN label t3 ON t2.label_id =t3.id
WHERE
t1.id = #{id}
</select>
<!-- 封装文章详情与标签-->
<resultMap id="ArticleAndLabelMap" type="Article">
<!-- 映射主键-->
<id column="id" property="id"/>
<!-- 映射一对一-->
<result column="title" property="title"/>
<result column="user_id" property="userId"/>
<result column="nick_name" property="nickName"/>
<result column="user_image" property="userImage"/>
<result column="summary" property="summary"/>
<result column="image_url" property="imageUrl"/>
<result column="md_content" property="mdContent"/>
<result column="html_content" property="htmlContent"/>
<result column="view_count" property="viewCount"/>
<result column="thumhup" property="thumhup"/>
<result column="status" property="status"/>
<result column="ispublic" property="ispublic"/>
<result column="create_date" property="createDate"/>
<result column="update_date" property="updateDate"/>
<!-- 映射一对多关系 property: 对应 Category 的属性名 javaType: 属性的数据类型 ofType: 配置集合内部的数据类型-->
<!-- 封装标签ID集合-->
<collection property="labelIds" javaType="list" ofType="string">
<!-- 映射主键-->
<id column="id" property="id"/>
</collection>
<!-- 封装标签对象集合-->
<collection property="labelList" javaType="list" ofType="Label">
<!-- 映射主键-->
<id column="id" property="id"/>
<!-- 映射普通属性-->
<result column="name" property="name"/>
</collection>
</resultMap>
</mapper>

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/IArticleService.java添加抽象方法findArticleAndLabelById

package com.acaiblog.article.service;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 文章信息表 服务类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface IArticleService extends IService<Article> {

/**
* 通过文章id查询文章详情与标签
* @param id
* @return
*/
Result findArticleAndLabelById(String id);
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/ArticleServiceImpl.java重写findArticleAndLabelById实现查询

package com.acaiblog.article.service.impl;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.article.mapper.ArticleMapper;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

/**
* <p>
* 文章信息表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements IArticleService {

@Override
public Result findArticleAndLabelById(String id) {
return Result.ok(baseMapper.findArticleAndLabelById(id));
}
}

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/ArticleController.java类实现view方法

package com.acaiblog.article.controller;


import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 文章信息表 前端控制器
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Api(value = "文章管理接口", description = "文章管理接口,提供增删改查")
@RestController
@RequestMapping("/article")
public class ArticleController {
@Autowired
private IArticleService articleService;

@ApiOperation("根据文章ID查询文件详情与标签")
@ApiImplicitParam(name = "id", value = "文章ID", required = true)
@GetMapping("/{id}")
public Result view(@PathVariable("id") String id) {
return articleService.findArticleAndLabelById(id);
}
}

接口测试

发送GET请求:http://127.0.0.1:8001/article/article/1

API接口

创建com.acaiblog.article.api.ApiArticleController

package com.acaiblog.article.api;

import com.acaiblog.article.service.IArticleService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(value = "文章管理API接口", description = "提供增删改查接口")
@RequestMapping("/api/article")
@RestController
public class ApiArticleController {
@Autowired
private IArticleService articleService;

@ApiOperation("查询文章详情接口")
@ApiImplicitParam(name = "id", value = "文章ID", required = true)
@GetMapping("/{id}")
public Result view(@PathVariable("id") String id) {
return articleService.findArticleAndLabelById(id);
}
}

文章修改接口

需求分析

文章提交修改数据,要操作文章信息表article和文章标签中间表artile_label;通过文章 id 删除文章标签中间表;新增到文章标签中间表;更新或保存到文章信息表

数据层

编辑blog-article/src/main/java/com/acaiblog/article/mapper/ArticleMapper.java类定义删除与新增文章标签中间表数据操作方法

package com.acaiblog.article.mapper;

import com.acaiblog.entities.Article;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
* <p>
* 文章信息表 Mapper 接口
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface ArticleMapper extends BaseMapper<Article> {

/**
* 通过文章ID删除文章中间表标签数据
* @param articleId
* @return
*/
boolean deleteArticleLabel(@Param("articleId") String articleId);

/**
* 新增文章标签中间表数据
* @param articleId
* @param labelIds
* @return
*/
boolean saveArticleLabel(@Param("articleId") String articleId, @Param("labelIds") List<String> labelIds);
}

编辑blog-article/src/main/java/com/acaiblog/article/mapper/xml/ArticleMapper.xml编写删除与新增sql;新增语句中的主键id不是mysql自增长的,所以需要我们传入id值 ,则调用mybatisplus的方法生成id

<?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.acaiblog.article.mapper.ArticleMapper">
<!-- 通过文章id删除文章标签中间表-->
<select id="deleteArticleLabel">
DELETE FROM article_label where article_id = #{articleId}
</select>
<!-- 新增文章标签中间表数据-->
<insert id="saveArticleLabel">
INSERT INTO article_label(id, article_id, label_id) VALUES
<foreach collection="labelIds" item="item" separator=",">
('${@com.baomidou.mybatisplus.core.toolkit.IdWorker@getId()}', #{articleId}, #{item})
</foreach>
</insert>
</mapper>

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/IArticleService.java类添加updateOrSave抽象方法

package com.acaiblog.article.service;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 文章信息表 服务类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface IArticleService extends IService<Article> {

/**
* 文章新增或保存
* @param article
* @return
*/
Result updateOrSave(Article article);
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/ArticleServiceImpl.java重写IService中的updateOrSave方法

package com.acaiblog.article.service.impl;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.article.mapper.ArticleMapper;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;

/**
* <p>
* 文章信息表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements IArticleService {

@Transactional // 事务管理
@Override
public Result updateOrSave(Article article) {
// 如果id不为空,则为更新操作
if (StringUtils.isNotEmpty(article.getId())) {
// 先删除文章标签中间表数据,再新增到中间表
baseMapper.deleteArticleLabel(article.getId());
// 设置更新时间
article.setUpdateDate(new Date());
}
// 如果文章是不公开的,直接审核通过。否则待审核
if (article.getIspublic() == 0) {
article.setStatus(ArticleStatusEnum.SUCCESS.getCode());
} else {
article.setStatus(ArticleStatusEnum.WAIT.getCode());
}
// 更新或保存到文章信息表(不能放到最后,因为新增后,要返回新增id到article.id里)
super.saveOrUpdate(article);
// 判断article.getLabelIds()是否为空
if (CollectionUtils.isNotEmpty(article.getLabelIds())) {
baseMapper.saveArticleLabel(article.getId(), article.getLabelIds());
}
return Result.ok(article.getId());
}
}

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/ArticleController.java类实现update方法

package com.acaiblog.article.controller;


import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 文章信息表 前端控制器
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Api(value = "文章管理接口", description = "文章管理接口,提供增删改查")
@RestController
@RequestMapping("/article")
public class ArticleController {

@ApiOperation("修改文章接口")
@PutMapping
public Result update(@RequestBody Article article) {
return articleService.updateOrSave(article);
}
}

接口测试

发送PUT请求:http://127.0.0.1:8001/article/article

{
"id": "1",
"title": "修改标题xxxx",
"labelIds": ["1", "3"]
}

文章新增接口

新增文章业务逻辑在上面ArticleServiceImpl#UpdateOrsave修改文章时已经实现了,所以新增接口只要写控制层即可。
编辑blog-article/src/main/java/com/acaiblog/article/controller/ArticleController.java实现save方法

package com.acaiblog.article.controller;


import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 文章信息表 前端控制器
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Api(value = "文章管理接口", description = "文章管理接口,提供增删改查")
@RestController
@RequestMapping("/article")
public class ArticleController {

@ApiOperation("新增文章接口")
@PostMapping
public Result save(@RequestBody Article article) {
return articleService.updateOrSave(article);
}
}

文章删除接口

需求分析

针对删除文章,采用假删除,将文章状态修改为0 ,表示已删除

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/IArticleService.java添加修改文章状态抽象方法updateStatus

package com.acaiblog.article.service;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 文章信息表 服务类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface IArticleService extends IService<Article> {

/**
* 修改状态:0: 已删除, 1:未审核,2:审核通过,3:审核未通过,
* @param id
* @param statusEnum
* @return
*/
Result updateStatus(String id, ArticleStatusEnum statusEnum);
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/ArticleServiceImpl.java实现updateStatus

package com.acaiblog.article.service.impl;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.article.mapper.ArticleMapper;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;

/**
* <p>
* 文章信息表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements IArticleService {

@Override
public Result updateStatus(String id, ArticleStatusEnum statusEnum) {
Article article = baseMapper.selectById(id);
article.setStatus(statusEnum.getCode());
article.setUpdateDate(new Date());
baseMapper.updateById(article); // MyBatis-Plus 框架提供的方法之一,用于按照实体对象的主键(ID)更新数据库中对应记录的数据
return Result.ok();
}
}

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/ArticleController.java类实现delete方法

package com.acaiblog.article.controller;


import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 文章信息表 前端控制器
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Api(value = "文章管理接口", description = "文章管理接口,提供增删改查")
@RestController
@RequestMapping("/article")
public class ArticleController {

@ApiOperation("删除文章接口")
@ApiImplicitParam(name = "id", value = "文章ID", required = true)
@DeleteMapping("/{id}")
public Result delete(@PathVariable("id") String id) {
return articleService.updateStatus(id, ArticleStatusEnum.DELETE);
}

}

接口测试

发送DELETE请求:http://127.0.0.1:8001/article/article/1

文章审核接口

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/ArticleController.java添加审核通过和审核不通过接口

package com.acaiblog.article.controller;


import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 文章信息表 前端控制器
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Api(value = "文章管理接口", description = "文章管理接口,提供增删改查")
@RestController
@RequestMapping("/article")
public class ArticleController {

@ApiOperation("审核通过接口")
@ApiImplicitParam(name = "id", value = "文章ID", required = true)
@GetMapping("/audit/success/{id}")
public Result success(@PathVariable("id") String id) {
return articleService.updateStatus(id, ArticleStatusEnum.SUCCESS);
}

@ApiOperation("审核不通过接口")
@ApiImplicitParam(name = "id", value = "文章ID", required = true)
@GetMapping("/audit/fail/{id}")
public Result fail(@PathVariable("id") String id) {
return articleService.updateStatus(id, ArticleStatusEnum.FAIL);
}
}

接口测试

发送GET请求:http://127.0.0.1:8001/article/article/audit/success/1
发送GET请求:http://127.0.0.1:8001/article/article/audit/fail/1

查询发布文章

需求分析

根据用户ID查询个人所发布的公开或未公开的文章列表

封装请求类

将用户ID、文章是否公开isPublic(0:不公开,1:公开) 属性封装成一个ArticleUserREQ请求类,用来查询个人用户发布的文章。创建com.acaiblog.article.req.ArticleUserREQ

package com.acaiblog.article.req;

import com.acaiblog.util.base.BaseRequest;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors
@ApiModel(value = "ArticleUserREQ请求对象", description = "获取指定用户文章的请求条件")
public class ArticleUserREQ extends BaseRequest {
@ApiModelProperty(value = "用户ID")
private String userId;

@ApiModelProperty(value = "是否公开,1:公开 0:不公开")
private Integer isPublic;
}

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/IArticleService.java接口定义findListByUserId抽象方法

package com.acaiblog.article.service;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 文章信息表 服务类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface IArticleService extends IService<Article> {

/**
* 根据用户ID查询公开或未公开文章列表
* @param req
* @return
*/
Result findListByUserId(ArticleUserREQ req);
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/ArticleServiceImpl.java实现findListByUserId接口的方法

package com.acaiblog.article.service.impl;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.article.mapper.ArticleMapper;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;

/**
* <p>
* 文章信息表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements IArticleService {

@Override
public Result findListByUserId(ArticleUserREQ req) {
if (StringUtils.isBlank(req.getUserId())) {
return Result.ok("无效的用户ID:" + req.getUserId());
}
QueryWrapper<Article> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", req.getUserId());
if (req.getIsPublic() != null) {
queryWrapper.eq("ispublic", req.getIsPublic());
}
// 排序
queryWrapper.orderByDesc("update_date");
return Result.ok(baseMapper.selectPage(req.getPage(), queryWrapper));
}
}

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/ArticleController.java添加findListByUserId方法接受查询请求,响应数据

package com.acaiblog.article.controller;


import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 文章信息表 前端控制器
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Api(value = "文章管理接口", description = "文章管理接口,提供增删改查")
@RestController
@RequestMapping("/article")
public class ArticleController {
@Autowired
private IArticleService articleService;

@ApiOperation("根据用户ID查询用户文章列表数据")
@PostMapping("/user")
public Result findListByUserId(@RequestBody ArticleUserREQ req) {
return articleService.findListByUserId(req);
}
}

接口测试

发送POST请求:http://127.0.0.1:8001/article/article/user

{ "current": 1, "size": 10, "userId": "9", "isPublic": 1 }

点赞接口

需求分析

根据文章的点赞显示文章点赞数量

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/IArticleService.java接口定义更新点赞数抽象方法

package com.acaiblog.article.service;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 文章信息表 服务类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface IArticleService extends IService<Article> {

/**
* 更新点赞数
* @param id
* @param count
* @return
*/
Result updateThumhub(String id, int count);
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/ArticleServiceImpl.java实现点赞数方法

package com.acaiblog.article.service.impl;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.article.mapper.ArticleMapper;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;

/**
* <p>
* 文章信息表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements IArticleService {

@Override
public Result updateThumhub(String id, int count) {
if (count != -1 && count !=1) {
return Result.error("文章点赞操作无效");
}
if (StringUtils.isBlank(id)) {
return Result.error("无效操作");
}
Article article = baseMapper.selectById(id);
if (article == null) {
return Result.error("文章不存在");
}
if (article.getThumhup() <= 0 && count == -1) {
return Result.error("无效操作");
}
article.setThumhup(article.getThumhup() + count);
baseMapper.updateById(article);
return Result.ok();
}
}

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/ArticleController.java调用service层方法

package com.acaiblog.article.controller;


import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 文章信息表 前端控制器
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Api(value = "文章管理接口", description = "文章管理接口,提供增删改查")
@RestController
@RequestMapping("/article")
public class ArticleController {
@Autowired
private IArticleService articleService;

@ApiOperation("更新点赞数")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "文章ID", required = true),
@ApiImplicitParam(name = "count", value = "点赞数", required = true)
})
@PutMapping("/thumb/{id}/{count}")
public Result updateThumhub(@PathVariable("id") String id, @PathVariable("count") int count) {
return articleService.updateThumhub(id,count);
}
}

统计总文章接口

编辑blog-article/src/main/java/com/acaiblog/article/service/IArticleService.java接口定义getArticleTotal抽象方法

package com.acaiblog.article.service;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 文章信息表 服务类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface IArticleService extends IService<Article> {

/**
* 查询文章总数
* @return
*/
Result getArticleTotal();
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/ArticleServiceImpl.java类实现getArticleTotal方法

package com.acaiblog.article.service.impl;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.article.mapper.ArticleMapper;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;

/**
* <p>
* 文章信息表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements IArticleService {

@Override
public Result getArticleTotal() {
// 查询总文章数
QueryWrapper<Article> queryWrapper = new QueryWrapper<>();
// 查询文章状态是审核通过
queryWrapper.eq("status", ArticleStatusEnum.SUCCESS.getCode());
// 公开
queryWrapper.eq("ispublic", 1);
int total = baseMapper.selectCount(queryWrapper);
return Result.ok(total);
}
}

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/ArticleController.java接受统计总文章请求,响应数据

package com.acaiblog.article.controller;


import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 文章信息表 前端控制器
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Api(value = "文章管理接口", description = "文章管理接口,提供增删改查")
@RestController
@RequestMapping("/article")
public class ArticleController {
@Autowired
private IArticleService articleService;

@ApiOperation("统计审核通过且公开的文章总数")
@GetMapping("/total")
public Result getArticleTotal() {
return articleService.getArticleTotal();
}

}

统计各分类文章接口

需求分析

统计各分类下的文章数的SQL语句有点复杂,为了方便维护,我们创建一个视图,在java代码中调用视图查询。

创建MySQL数据库统计视图

CREATE VIEW v_category_total
AS
SELECT
t1.`name`,
IFNULL(SUM(t3.total), 0) AS `value`
FROM
category t1
LEFT JOIN label t2 ON t1.id = t2.category_id
LEFT JOIN (
SELECT
m2.label_id,
COUNT(m1.id) total
FROM
article m1
JOIN article_label m2 ON m1.id = m2.article_id WHERE
m1. STATUS = 2 AND m1.ispublic = 1
GROUP BY
m2.label_id
) t3 ON t2.id = t3.label_id
GROUP BY
t1.`name`

测试:通过视图查询数据

SELECT `name`, `value` FROM v_category_total

数据访问层

编辑blog-article/src/main/java/com/acaiblog/article/mapper/ArticleMapper.java接口中添加统计的抽象方法

package com.acaiblog.article.mapper;

import com.acaiblog.entities.Article;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

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

/**
* <p>
* 文章信息表 Mapper 接口
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface ArticleMapper extends BaseMapper<Article> {

/**
* 统计每个分类下的文章数
* @return
*/
List<Map<String, Objects>> selectCategoryTotal();
}

编辑blog-article/src/main/java/com/acaiblog/article/mapper/xml/ArticleMapper.xml实现查询视图获取数据

<?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.acaiblog.article.mapper.ArticleMapper">
<!-- 通过视图查询各技术频道总文章数-->
<select id="selectCategoryTotal" resultType="map">
SELECT `name`, `value` FROM v_category_total
</select>
</mapper>

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/IArticleService.java接口定义selectCategoryTotal抽象方法

package com.acaiblog.article.service;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 文章信息表 服务类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface IArticleService extends IService<Article> {

/**
* 统计每个分类下的文章数
* @return
*/
Result selectCategoryTotal();
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/ArticleServiceImpl.java实现selectCategoryTotal方法

package com.acaiblog.article.service.impl;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.article.mapper.ArticleMapper;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;

/**
* <p>
* 文章信息表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements IArticleService {

@Override
public Result selectCategoryTotal() {
List<Map<String, Objects>> maps = baseMapper.selectCategoryTotal();

// 将分类名称提取到集合中
List<Object> nameList = new ArrayList<>();
for (Map<String, Objects> map : maps) {
nameList.add(map.get("name"));
}

// 封装响应数据
Map<String, Object> data = new HashMap<>();
data.put("nameAndLabelList", maps);
data.put("nameList", nameList);

// 检查 data 是否为 null,避免构造 Result 时抛出异常
if (data != null) {
return Result.ok(data);
} else {
// 如果 data 为 null,可以根据实际情况处理错误,例如返回一个错误状态的 Result
return Result.error("处理数据时发生错误");
}
}

}

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/ArticleController.java接受统计各分类文章数请求,响应数据

package com.acaiblog.article.controller;


import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 文章信息表 前端控制器
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Api(value = "文章管理接口", description = "文章管理接口,提供增删改查")
@RestController
@RequestMapping("/article")
public class ArticleController {
@Autowired
private IArticleService articleService;

@ApiOperation("统计各分类下的文章数")
@GetMapping("/category/total")
public Result categoryTotal() {
return articleService.selectCategoryTotal();
}
}

统计6个月发布文章接口

需求分析

近6个月发布的文章数的SQL语句有点复杂,为了方便维护,我们创建一个视图,在java代码中调用视图查询

创建 统计视图

CREATE VIEW v_month_aritcle_total
AS
SELECT t1.`year_month`, count(t2.id) AS total FROM
(
-- 先查询近6个月的月份
SELECT DATE_FORMAT(CURDATE(), '%Y-%m') AS `year_month`
UNION SELECT DATE_FORMAT((CURDATE() - INTERVAL 1 MONTH), '%Y-%m') AS `year_month` UNION SELECT DATE_FORMAT((CURDATE() - INTERVAL 2 MONTH), '%Y-%m') AS `year_month` UNION SELECT DATE_FORMAT((CURDATE() - INTERVAL 3 MONTH), '%Y-%m') AS `year_month` UNION SELECT DATE_FORMAT((CURDATE() - INTERVAL 4 MONTH), '%Y-%m') AS `year_month` UNION SELECT DATE_FORMAT((CURDATE() - INTERVAL 5 MONTH), '%Y-%m') AS `year_month` ) t1
LEFT JOIN article t2
ON t1.`year_month` = DATE_FORMAT(t2.create_date, '%Y-%m')
-- 注意:使用使用 AND, 不要使用 WHERE 不然月份没有发布文章就不会显示对应月份出来AND t2.`status` = 2 AND t2.ispublic = 1
GROUP BY t1.`year_month`

通过视图查询数据

SELECT `year_month`, `total` FROM v_month_aritcle_total

数据访问层

编辑blog-article/src/main/java/com/acaiblog/article/mapper/ArticleMapper.java接口添加统计抽象方法

package com.acaiblog.article.mapper;

import com.acaiblog.entities.Article;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

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

/**
* <p>
* 文章信息表 Mapper 接口
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface ArticleMapper extends BaseMapper<Article> {

/**
* 统计近6个月发布的文章数
* @return
*/
List<Map<String, Objects>> selectMonthArticleTotal();
}

编辑blog-article/src/main/java/com/acaiblog/article/mapper/xml/ArticleMapper.xml实现查询视图统计数据

<?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.acaiblog.article.mapper.ArticleMapper">
<!-- 通过视图查询近6个月文章总记录-->
<select id="selectMonthArticleTotal" resultType="map">
SELECT `year_month` , `total` FROM v_month_aritcle_total
</select>
</mapper>

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/IArticleService.java定义接口selectMonthArticleTotal抽象方法

package com.acaiblog.article.service;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 文章信息表 服务类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface IArticleService extends IService<Article> {

/**
* 统计近6个月分别发布的文章数
* @return
*/
Result selectMonthArticleTotal();
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/ArticleServiceImpl.java实现selectMonthArticleTotal方法

package com.acaiblog.article.service.impl;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.article.mapper.ArticleMapper;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;

/**
* <p>
* 文章信息表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements IArticleService {

@Override
public Result selectMonthArticleTotal() {
List<Map<String, Objects>> maps = baseMapper.selectMonthArticleTotal();
// 将年月提取到集合中
List<Object> yearMonthList = new ArrayList<>();
// 将每个月的文章数提取集合中
List<Object> articleTotalList = new ArrayList<>();
for(Map<String, Objects> map: maps) {
yearMonthList.add(map.get("year_month"));
articleTotalList.add(map.get("total"));
}
// 封装响应数据
Map<String, List<Object>> data = new HashMap<>();
data.put("yearMonthList", yearMonthList);
data.put("articleTotalList", articleTotalList);
return Result.ok(data);
}
}

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/ArticleController.java接收统计查询近6个月发布的文章数请求,响应数据。

package com.acaiblog.article.controller;


import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 文章信息表 前端控制器
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Api(value = "文章管理接口", description = "文章管理接口,提供增删改查")
@RestController
@RequestMapping("/article")
public class ArticleController {
@Autowired
private IArticleService articleService;

@ApiOperation("接收统计查询近6个月发布的文章数")
@GetMapping("/month/total")
public Result monthArticleTotal() {
return articleService.selectMonthArticleTotal();
}
}

接口测试

发送GET请求:http://127.0.0.1:8001/article/article/month/total

API接口

浏览数

更新浏览次数是没有登录状态下也可以调用此接口,请求URL为:/api/article/viewCount/{id}

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/IArticleService.java定义更新浏览数的抽象方法

package com.acaiblog.article.service;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 文章信息表 服务类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface IArticleService extends IService<Article> {

/**
* 更新浏览次数
* @param id
* @return
*/
Result updateViewCount(String id);
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/ArticleServiceImpl.java实现updateViewCount方法

package com.acaiblog.article.service.impl;

import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.article.mapper.ArticleMapper;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;

/**
* <p>
* 文章信息表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements IArticleService {

@Override
public Result updateViewCount(String id) {
if(StringUtils.isBlank(id)) {
return Result.error("无效操作");
}
Article article = baseMapper.selectById(id);
if(article == null) {
return Result.error("文章不存在");
}
article.setViewCount(article.getViewCount() + 1);
baseMapper.updateById(article);
return Result.ok();
}
}

控制层

编辑blog-article/src/main/java/com/acaiblog/article/api/ApiArticleController.java

package com.acaiblog.article.api;

import com.acaiblog.article.service.IArticleService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@Api(value = "文章管理API接口", description = "文章管理API接口,不需要身份认证就可以访问")
@RequestMapping("/api/article")
@RestController
public class ApiArticleController {
@Autowired
private IArticleService articleService;

@ApiOperation("更新文章浏览次数")
@ApiImplicitParam(name = "id", value = "文章ID", required = true)
@PutMapping("/viewCount/{id}")
public Result updateViewCount(@PathVariable("id") String id) {
return articleService.updateViewCount(id);
}
}

接口测试

发送PUT请求:http://127.0.0.1:8001/article/api/article/viewCount/1

公开文章

需求分析

在博客门户网首页根据类别ID展示不同类别的文章列表,在标签页 根据标签ID展示不同标签的文章列表。
展示的是公开且已审核的文章,并且带分页功能。所以要将 类别ID 、 标签ID 、是否公开、状态 等作为主要条件来查询文章数据。

文章列表请求类

创建com.acaiblog.article.req.ArticleListREQ

package com.acaiblog.article.req;

import com.acaiblog.entities.Article;
import com.acaiblog.util.base.BaseRequest;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors
@ApiModel(value = "ArticleListREQ对象", description = "文章列表查询条件")
public class ArticleListREQ extends BaseRequest<Article> {
@ApiModelProperty(value = "标签ID")
private String labelId;

@ApiModelProperty(value = "分类ID")
private String categoryId;
}

数据访问层

编辑blog-article/src/main/java/com/acaiblog/article/mapper/ArticleMapper.java添加通过分类id或标签id查询文章列表方法

package com.acaiblog.article.mapper;

import com.acaiblog.article.req.ArticleListREQ;
import com.acaiblog.entities.Article;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Param;

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

/**
* <p>
* 文章信息表 Mapper 接口
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface ArticleMapper extends BaseMapper<Article> {

/**
* 通过分类id或标签id查询文章列表
* @param page
* @param req
* @return
*/
IPage<Article> findListByLableIdOrCategoryId(IPage<Article> page, @Param("req")ArticleListREQ req);
}

编辑blog-article/src/main/java/com/acaiblog/article/mapper/xml/ArticleMapper.xml实现SQL查询语句公开且审核通过的文章,条件标签id和类别id

<?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.acaiblog.article.mapper.ArticleMapper">
<!-- 通过分类id或标签id查询公开且已审核通过的文章列表-->
<select id="findListByLableIdOrCategoryId" resultType="Article">
SELECT
DISTINCT t3.*
FROM
label t1
JOIN article_label t2 ON t1.id = t2.label_id
JOIN article t3 ON t2.article_id = t3.id
WHERE t3.ispublic = 1 AND t3.`status` = 2
<if test="req.labelId != null and req.labelId != ''">
AND t1.id = #{req.labelId}
</if>
<if test="req.categoryId !=null and req.categoryId != ''"> AND t1.category_id = #{req.categoryId}
</if>
ORDER BY t3.update_date DESC
</select>
</mapper>

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/IArticleService.java接口定义findListByLabelIdOrCategoryId抽象方法

package com.acaiblog.article.service;

import com.acaiblog.article.req.ArticleListREQ;
import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 文章信息表 服务类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface IArticleService extends IService<Article> {

/**
* 公开且审核通过的文章列表
* @param req
* @return
*/
Result findListByLabelIdOrCategoryId(ArticleListREQ req);
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/ArticleServiceImpl.java实现findListByLabelIdOrCategoryId方法

package com.acaiblog.article.service.impl;

import com.acaiblog.article.req.ArticleListREQ;
import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.article.mapper.ArticleMapper;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;

/**
* <p>
* 文章信息表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements IArticleService {

@Override
public Result findListByLabelIdOrCategoryId(ArticleListREQ req) {
return Result.ok(baseMapper.findListByLableIdOrCategoryId(req.getPage(), req));
}
}

控制层

编辑blog-article/src/main/java/com/acaiblog/article/api/ApiArticleController.java实现 list 方法接收查询请求,响应数据

package com.acaiblog.article.api;

import com.acaiblog.article.req.ArticleListREQ;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@Api(value = "文章管理API接口", description = "文章管理API接口,不需要身份认证就可以访问")
@RequestMapping("/api/article")
@RestController
public class ApiArticleController {
@Autowired
private IArticleService articleService;

@ApiOperation("公开且已审核的文章列表接口")
@PostMapping("/list")
public Result list(@RequestBody ArticleListREQ req) {
return articleService.findListByLabelIdOrCategoryId(req);
}
}

接口测试

发送POST请求:http://127.0.0.1:8001/article/api/article/list

文章评论

生成代码

运行blog-generator/src/main/java/com/acaiblog/generator/CodeGenerator.java脚本生成评论代码模版

请输入表名,多个英文逗号分割:
comment

新增评论接口

控制层

新增评论接口直接引用Mybatis-plus已经提供好的。编辑blog-article/src/main/java/com/acaiblog/article/controller/CommentController.java添加save新增评论方法

package com.acaiblog.article.controller;


import com.acaiblog.article.service.ICommentService;
import com.acaiblog.entities.Comment;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

/**
* <p>
* 评论信息表 前端控制器
* </p>
*
* @author 阿才的博客
* @since 2024-01-08
*/
@RestController
@RequestMapping("/comment")
public class CommentController {
@Autowired // Spring容器自动地在你的类中注入依赖的 bean
private ICommentService commentService;

@ApiOperation("新增评论信息接口")
@PostMapping
public Result save(@RequestBody Comment comment) {
commentService.save(comment);
return Result.ok();
}
}

接口测试

发送POST请求:http://127.0.0.1:8001/article/comment

批量删除评论接口

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/ICommentService.java添加删除评论deleteById方法

package com.acaiblog.article.service;

import com.acaiblog.entities.Comment;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 评论信息表 服务类
* </p>
*
* @author 阿才的博客
* @since 2024-01-08
*/
public interface ICommentService extends IService<Comment> {
Result deleteById(String id);

}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/CommentServiceImpl.java实现deleteById方法

package com.acaiblog.article.service.impl;

import com.acaiblog.entities.Comment;
import com.acaiblog.article.mapper.CommentMapper;
import com.acaiblog.article.service.ICommentService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.ArrayList;

/**
* <p>
* 评论信息表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-08
*/
@Service
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements ICommentService {
@Override
public Result deleteById(String id) {
if (StringUtils.isBlank(id)) {
return Result.error("评论ID不能为空");
}
ArrayList<String> ids = new ArrayList<>();
// 先把要删除的一级评论id放入到ids集合中
ids.add(id);
// 批量删除集合中的id
baseMapper.deleteBatchIds(ids);
return Result.ok();
}

}

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/CommentController.java实现delete方法接收查询请求,响应数据

package com.acaiblog.article.controller;

import com.acaiblog.article.service.ICommentService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(value = "文章评论API接口", description = "文章评论API接口,不需要身份认证就可以访问")
@RequestMapping("/api/comment")
@RestController
public class CommentController {
@Autowired
private ICommentService commentService;

@ApiOperation("删除评论接口")
@ApiImplicitParam(name = "id", value = "评论ID", required = true)
@DeleteMapping("/{id}")
public Result deleteById(@PathVariable("id") String id) {
return commentService.deleteById(id);
}
}

接口测试

发送DELETE请求:http://127.0.0.1:8001/article/api/comment/1

API接口

实体类

编辑blog-article/src/main/java/com/acaiblog/entities/Comment.java子评论集合children,递归查询出子评论封装到此集合中

package com.acaiblog.entities;

import com.baomidou.mybatisplus.annotation.IdType;
import java.util.Date;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import java.util.List;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
* <p>
* 评论信息表
* </p>
*
* @author 阿才的博客
* @since 2024-01-08
*/
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value="Comment对象", description="评论信息表")
public class Comment implements Serializable {

@ApiModelProperty(value = "自评论集合")
@TableField(exist = false)
List<Comment> children;
}

数据访问层

编辑blog-article/src/main/java/com/acaiblog/article/mapper/CommentMapper.java添加抽象方法findByArticleId

package com.acaiblog.article.mapper;

import com.acaiblog.entities.Comment;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
* <p>
* 评论信息表 Mapper 接口
* </p>
*
* @author 阿才的博客
* @since 2024-01-08
*/
public interface CommentMapper extends BaseMapper<Comment> {
List<Comment> findByArticleId(@Param("articleId") String articleId);

}

编辑blog-article/src/main/java/com/acaiblog/article/mapper/xml/CommentMapper.xml添加sql查询实现

<?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.acaiblog.article.mapper.CommentMapper">
<!-- 通过文章id级联查询所有评论-->
<select id="findByArticleId" resultMap="commentResultMap">
SELECT
* FROM comment WHERE parent_id = -1
AND
article_id = #{articleId}
ORDER BY
create_date DESC
</select>
<!-- 递归查询子评论-->
<select id="findByParentId" resultMap="commentResultMap">
SELECT
* from comment
WHERE
parent_id = #{id}
</select>
<resultMap id="commentResultMap" type="Comment">
<id column="id" property="id"/>
<result column="parent_id" property="parentId" />
<result column="user_id" property="userId" />
<result column="nick_name" property="nickName" />
<result column="user_image" property="userImage" />
<result column="article_id" property="articleId" />
<result column="content" property="content" />
<result column="create_date" property="createDate" />
<!-- 映射一对多关系(映射集合) property: 对应 Category 的属性名 javaType: 属性的数据类型 ofType: 配置集合内部的数据类型-->
<collection property="children" javaType="list" ofType="Comment" column="id" select="findByParentId"/>
</resultMap>
</mapper>

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/ICommentService.java添加抽象方法findByArticleId

package com.acaiblog.article.service;

import com.acaiblog.entities.Comment;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 评论信息表 服务类
* </p>
*
* @author 阿才的博客
* @since 2024-01-08
*/
public interface ICommentService extends IService<Comment> {

/**
* 通过文章id级联查询所有评论
* @param id
* @return
*/
Result findByArticleId(String id);
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/CommentServiceImpl.java添加findByArticleId方法实现

package com.acaiblog.article.service.impl;

import com.acaiblog.entities.Comment;
import com.acaiblog.article.mapper.CommentMapper;
import com.acaiblog.article.service.ICommentService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

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

/**
* <p>
* 评论信息表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-08
*/
@Service
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements ICommentService {

/**
* 通过文章id级联查询所有评论
* @param articleId
* @return
*/
@Override
public Result findByArticleId(String articleId) {
if(StringUtils.isBlank(articleId)) {
return Result.error("文章ID不能为空");
}
List<Comment> list = baseMapper.findByArticleId(articleId);
return Result.ok(list);
}

}

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/CommentController.java实现通过文章id级联查询所有评论请求,响应数据

package com.acaiblog.article.controller;

import com.acaiblog.article.service.ICommentService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@Api(value = "文章评论API接口", description = "文章评论API接口,不需要身份认证就可以访问")
@RequestMapping("/api/comment")
@RestController
public class CommentController {
@Autowired
private ICommentService commentService;

@ApiOperation("通过文章id级联查询所有评论")
@ApiImplicitParam(name = "articleId", value = "文章ID", required = true)
@GetMapping("/list/{articleId}")
public Result findByArticleId(@PathVariable("articleId") String articleId) {
return commentService.findByArticleId(articleId);
}
}

接口测试

发送GET请求:http://127.0.0.1:8001/article/api/comment/list/1

OSS对象存储

上传与删除文件

参考官方提供的 SDK: https://help.aliyun.com/document_detail/32008.html?spm=a2c4g.11186623.6.764.753758a8YW2zNr

需求分析

文件上传与删除功能使用阿里云OSS对象存储实现,使用之前需要先配置OSS以下功能:

  1. 开通对象存储OSS服务
  2. 创建Bucket
  3. 设置Bucket公共读写权限

添加OSS SDK依赖

编辑pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0">
<dependencies>
<!-- oss-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.8.0</version>
</dependency>
</dependencies>
</project>

创建OSS配置类

blog-util下创建一个com.acaiblog.util.properties.BlogProperties类,统一管理整个项目在application.yml中自定义配置信息,比如:有阿里云相关的配置信息。

package com.acaiblog.util.properties;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.io.Serializable;

@Setter
@Getter
@Component
@ConfigurationProperties(prefix = "blog") // 指定了配置属性的前缀,即这个类将用于读取以"blog"为前缀的配置属性
public class BlogProperties implements Serializable {
private AliyunProperties aliyunProperties;
}

blog-util下创建一个阿里云配置信息类com.acaiblog.util.properties.AliyunProperties

package com.acaiblog.util.properties;

import lombok.Data;

import java.io.Serializable;

@Data
public class AliyunProperties implements Serializable { // implements Serializable主要用于在Java中实现对象的序列化和反序列化
private String endpoint;
private String accessKey;
private String accessKeySecret;

private String bucketName;
/**
* Bucket域名,访问文件时作为URL前缀
*/
private String bucketDomain;
}

配置OSS

blog-article模块中的application.yml中添加如下配置

blog:
aliyun:
endpoint: http://oss-cn-hangzhou.aliyuncs.com
accessKey: LTAI5tEbgBbt8ZGGf5QvkKHN
accessKeySecret: XgR3TndiffZOyZOoBNTptA1eeeATM1
bucketName: acaiops
backetDomain: https://acaiops.oss-cn-hangzhou.aliyuncs.com

工具类AliyunUtil

blog-util模块创建com.acaiblog.util.enums.PlatformEnum上传文件分类枚举类,如文章,头像类型

package com.acaiblog.util.enums;

/**
* 上传的文件,属性哪种类型的文件
*/
public enum PlatformEnum {
/* 文章,用户(头像) */
ARTICLE, USER;
}

blog-util创建阿里云工具类com.acaiblog.util.aliyun.AliyunUtil涉及功能:上传、删除图片文件

package com.acaiblog.util.aliyun;

import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.PlatformEnum;
import com.acaiblog.util.properties.AliyunProperties;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.comm.ResponseMessage;
import com.aliyun.oss.model.PutObjectResult;
import org.apache.commons.lang.time.DateFormatUtils;
import org.springframework.web.multipart.MultipartFile;

import java.util.Date;
import java.util.UUID;

/**
* 阿里云工具类
*/
public final class AliyunUtil {
/**
* 上传图片文件
* @param platformEnum
* @param file
* @param aliyunProperties
* @return
*/
public static Result uploadFileToOss(PlatformEnum platformEnum, MultipartFile file, AliyunProperties aliyunProperties) {
// 上传文件所在目录名,当天上传的文件放到当天日期的目录下
String folderName = platformEnum.name().toLowerCase() + "/" + DateFormatUtils.format(new Date(),"yyyMMdd");
// 保存到OSS的文件名采用uuid的方式
String fileName = UUID.randomUUID().toString().replace("-","");
// 从原始文件名中,获取文件扩展名
String fileExtensionName = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
// 文件在OSS中的完整路径
String filePath = folderName + "/" + fileName + fileExtensionName;
OSS ossClient = null;
try {
ossClient = new OSSClientBuilder().build(aliyunProperties.getEndpoint(),
aliyunProperties.getAccessKey(),
aliyunProperties.getAccessKeySecret());
// 上传文件到OSS并响应结果
PutObjectResult putObjectResult = ossClient.putObject(aliyunProperties.getBucketName(),
filePath,
file.getInputStream());
ResponseMessage responseMessage = putObjectResult.getResponse();
if (responseMessage == null) {
// 上传成功返回上传文件的完整路径
return Result.ok(aliyunProperties.getBucketDomain() + filePath);
} else {
// 上传失败,OOS服务端会响应状态码和错误信息
String errorMsg = String.format(String.format("OSS响应状态码: %s 错误信息: %s", responseMessage.getStatusCode(),
responseMessage.getErrorResponseAsString()));
return Result.error(errorMsg);
}
} catch (Exception e) {
return Result.error(e.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}

}

/**
* 根据URL删除OSS文件
* @param fileUrl
* @param aliyunProperties
* @return
*/
public static Result delete(String fileUrl, AliyunProperties aliyunProperties) {
String filePath = fileUrl.replace(aliyunProperties.getBucketDomain(), "");
OSS ossClient = null;
try {
ossClient = new OSSClientBuilder().build(aliyunProperties.getEndpoint(),
aliyunProperties.getAccessKey(),
aliyunProperties.getAccessKeySecret());
// 删除
ossClient.deleteObject(aliyunProperties.getBucketName(), filePath);
return Result.ok();
} catch (Exception e) {
return Result.error(String.format("删除失败: %s", e.getMessage()));
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
}

控制层

blog-article模块创建com.acaiblog.article.controller.FileController

package com.acaiblog.article.controller;

import com.acaiblog.util.aliyun.AliyunUtil;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.PlatformEnum;
import com.acaiblog.util.properties.BlogProperties;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

/**
* 图片文件控制器
*/
@Api(value = "文件管理接口", description = "从OSS上传或删除文件")
@RequestMapping("/file")
@RestController
public class FileController {
@Autowired
private BlogProperties blogProperties;

@ApiOperation("文件上传到OSS")
@PostMapping("/upload")
public Result upload(@RequestParam("file")MultipartFile file) {
return AliyunUtil.uploadFileToOss(PlatformEnum.ARTICLE, file, blogProperties.getAliyunProperties());
}

@ApiOperation("删除OSS文件")
@DeleteMapping("/delete")
public Result delete(@RequestParam(value = "fileUrl",required = false) String fileUrl) {
return AliyunUtil.delete(fileUrl, blogProperties.getAliyunProperties());
}
}

广告管理

生成模版代码

运行blog-generator/src/main/java/com/acaiblog/generator/CodeGenerator.javamain方法生成代码模版

请输入表名,多个英文逗号分割:
advert

列表接口

分页请求类

将查询条件广告标题和状态属性封装成一个 AdvertBO 对象,并继承BaseRequest<Advert>。创建com.acaiblog.article.req.AdvertREQ

package com.acaiblog.article.req;

import com.acaiblog.entities.Advert;
import com.acaiblog.entities.Article;
import com.acaiblog.util.base.BaseRequest;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors
@ApiModel(value = "AdvertREQ对象", description = "广告查询条件")
public class AdvertREQ extends BaseRequest<Advert> {
@ApiModelProperty(value = "广告标题")
private String title;

@ApiModelProperty(value = "广告状态 1:正常 0: 禁用")
private Integer status;
}

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/IAdvertService.java添加抽象方法queryPage

package com.acaiblog.article.service;

import com.acaiblog.article.req.AdvertREQ;
import com.acaiblog.entities.Advert;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 广告信息表 服务类
* </p>
*
* @author 阿才的博客
* @since 2024-01-09
*/
public interface IAdvertService extends IService<Advert> {
/**
* 分页查询广告列表
* @param req
* @return
*/
Result queryPage(AdvertREQ req);
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/AdvertServiceImpl.java分页查询queryPage实现逻辑

package com.acaiblog.article.service.impl;

import com.acaiblog.article.req.AdvertREQ;
import com.acaiblog.entities.Advert;
import com.acaiblog.article.mapper.AdvertMapper;
import com.acaiblog.article.service.IAdvertService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

/**
* <p>
* 广告信息表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-09
*/
@Service
public class AdvertServiceImpl extends ServiceImpl<AdvertMapper, Advert> implements IAdvertService {

@Override
public Result queryPage(AdvertREQ req) {
QueryWrapper<Advert> queryWrapper = new QueryWrapper<>();
if (req.getStatus() != null) {
queryWrapper.eq("status", req.getStatus());
}
if (StringUtils.isNotEmpty(req.getTitle())) {
queryWrapper.like("title", req.getTitle());
}
queryWrapper.orderByDesc("status");
// 分页对象
return Result.ok(baseMapper.selectPage(req.getPage(), queryWrapper));
}
}

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/AdvertController.java调用业务层代码

package com.acaiblog.article.controller;


import com.acaiblog.article.req.AdvertREQ;
import com.acaiblog.article.service.IAdvertService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 广告信息表 前端控制器
* </p>
*
* @author 阿才的博客
* @since 2024-01-09
*/
@Api(value = "广告管理接口", description = "广告管理接口")
@RestController
@RequestMapping("/advert")
public class AdvertController {
@Autowired
private IAdvertService advertService;

@ApiOperation("根据广告标题与状态查询广告分页列表接口")
@PostMapping("/search")
public Result search(@RequestBody AdvertREQ req) {
return advertService.queryPage(req);
}

}

接口测试

发送POST请求:http://127.0.0.1:8001/article/advert/search

删除广告与图片接口

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/IAdvertService.java添加删除数据与图片的抽象方法deleteById

package com.acaiblog.article.service;

import com.acaiblog.article.req.AdvertREQ;
import com.acaiblog.entities.Advert;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 广告信息表 服务类
* </p>
*
* @author 阿才的博客
* @since 2024-01-09
*/
public interface IAdvertService extends IService<Advert> {

/**
* 添加删除数据与图片的抽象方法
* @param req
* @return
*/
Result deleteById(String id);
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/AdvertServiceImpl.java实现deleteById方法,删除数据与OSS图片

package com.acaiblog.article.service.impl;

import com.acaiblog.article.req.AdvertREQ;
import com.acaiblog.entities.Advert;
import com.acaiblog.article.mapper.AdvertMapper;
import com.acaiblog.article.service.IAdvertService;
import com.acaiblog.util.aliyun.AliyunUtil;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.properties.BlogProperties;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
* <p>
* 广告信息表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-09
*/
@Service
public class AdvertServiceImpl extends ServiceImpl<AdvertMapper, Advert> implements IAdvertService {
@Autowired
private BlogProperties blogProperties;

@Override
@Transactional
public Result deleteById(String id) {
// 通过广告id获取图片URL
String imageUrl = baseMapper.selectById(id).getImageUrl();
// 删除表中广告
baseMapper.deleteById(id);
// 删除OSS图片
if (StringUtils.isNotEmpty(imageUrl)) {
AliyunUtil.delete(imageUrl, blogProperties.getAliyunProperties());
}
return Result.ok();
}
}

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/AdvertController.java实现删除广告API接口

package com.acaiblog.article.controller;


import com.acaiblog.article.req.AdvertREQ;
import com.acaiblog.article.service.IAdvertService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 广告信息表 前端控制器
* </p>
*
* @author 阿才的博客
* @since 2024-01-09
*/
@Api(value = "广告管理接口", description = "广告管理接口")
@RestController
@RequestMapping("/advert")
public class AdvertController {
@Autowired
private IAdvertService advertService;

@ApiOperation("删除广告及广告图片")
@DeleteMapping("/{id}")
@ApiImplicitParam(name = "id", value = "广告ID", required = true)
public Result delete(@PathVariable("id") String id) {
return advertService.deleteById(id);
}

}

接口测试

发送DELETE请求:http://127.0.0.1:8001/article/advert/1

查询修改删除接口

控制层

编辑blog-article/src/main/java/com/acaiblog/article/controller/AdvertController.java实现控制层API抽象方法

package com.acaiblog.article.controller;


import com.acaiblog.article.req.AdvertREQ;
import com.acaiblog.article.service.IAdvertService;
import com.acaiblog.entities.Advert;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

/**
* <p>
* 广告信息表 前端控制器
* </p>
*
* @author 阿才的博客
* @since 2024-01-09
*/
@Api(value = "广告管理接口", description = "广告管理接口")
@RestController
@RequestMapping("/advert")
public class AdvertController {
@Autowired
private IAdvertService advertService;

@ApiOperation("查询广告详情")
@ApiImplicitParam(name = "id", value = "广告ID", required = true)
@GetMapping("/{id}")
public Result view(@PathVariable("id") String id) {
return Result.ok(advertService.getById(id));
}

@ApiOperation("修改广告信息")
@ApiImplicitParam(name = "id", value = "广告ID", required = true)
@PutMapping
public Result update(@RequestBody Advert advert) {
advert.setUpdateDate(new Date());
advertService.updateById(advert);
return Result.ok();
}

@ApiOperation("新增广告")
@PostMapping
public Result add(@RequestBody Advert advert) {
advertService.save(advert);
return Result.ok();
}
}

API

查询指定位置的广告信息

需求分析

在门户网站有多个位置要显示广告,下面就根据广告位置编号查询出这个位置所对应的广告信息

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/IAdvertService.java添加抽象方法findByPosition

package com.acaiblog.article.service;

import com.acaiblog.article.req.AdvertREQ;
import com.acaiblog.entities.Advert;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 广告信息表 服务类
* </p>
*
* @author 阿才的博客
* @since 2024-01-09
*/
public interface IAdvertService extends IService<Advert> {

/**
* 查询指定广告位置下的所有广告信息接口,状态是正常的
* @param position
* @return
*/
Result findByPosition(int position);
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/AdvertServiceImpl.java实现findByPosition方法编写查询逻辑

package com.acaiblog.article.service.impl;

import com.acaiblog.article.req.AdvertREQ;
import com.acaiblog.entities.Advert;
import com.acaiblog.article.mapper.AdvertMapper;
import com.acaiblog.article.service.IAdvertService;
import com.acaiblog.util.aliyun.AliyunUtil;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.properties.BlogProperties;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
* <p>
* 广告信息表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-09
*/
@Service
public class AdvertServiceImpl extends ServiceImpl<AdvertMapper, Advert> implements IAdvertService {

@Override
public Result findByPosition(int position) {
QueryWrapper<Advert> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("position", position);
queryWrapper.eq("status", 1); // 正常
queryWrapper.orderByDesc("sort"); // 升序
return Result.ok(baseMapper.selectList(queryWrapper));
}
}

控制层

创建com.acaiblog.article.api.ApiAdvertController类实现根据位置查询广告信息接口

package com.acaiblog.article.api;

import com.acaiblog.article.service.IAdvertService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@Api(tags = "api-advert", value = "广告管理API接口")
@RequestMapping("/api/advert")
@RestController
public class ApiAdvertController {
@Autowired
private IAdvertService advertService;

@ApiOperation("根据广告位置查询广告信息")
@ApiImplicitParam(name = "position", value = "广告位置", required = true)
@GetMapping("/show/{position}")
public Result show(@PathVariable("position") int position) {
return advertService.findByPosition(position);
}
}

接口测试

发送GET请求:http://127.0.0.1:8001/article/api/advert/show/1

问答微服务

搭建微服务

创建blog-question模块

配置pom.xml依赖,编辑blog-question/pom.xml

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

<artifactId>blog-question</artifactId>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.acaiblog</groupId>
<artifactId>blog-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

配置application.yml

swagger:
title: 阿才的博客接口文档
description: 阿才的博客接口文档
exclude-path: /error # 剔除请求

server:
port: 8002
servlet:
context-path: /question

spring:
application:
name: quesition-server # 应用名称
datasource:
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/blog_question?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true
driver-class-name: org.mariadb.jdbc.Driver
# 数据源其他配置, 在 DruidConfig配置类中手动绑定initialSize: 8
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL

mybatis-plus:
type-aliases-package: com.mengxuegu.blog.entities
# xxxMapper.xml 路径
mapper-locations: classpath*:com/acaiblog/blog/question/mapper/**/*.xml

blog:
# 阿里云配置
aliyun:
endpoint: http://oss-cn-shenzhen.aliyuncs.com # OSS 端点,根据自己地域替换accessKeyId: xxx # 根据自己的配置
accessKeySecret: xxx # 根据自己的配置
bucketName: mengxuegu # 存储空间名称
# Bucket域名,访问文件时作为URL前缀,注意前面加上 https 和结尾加上 /
bucketDomain: https://mengxuegu.oss-cn-shenzhen.aliyuncs.com/

# 日志级别,会打印sql语句
logging:
level:
com.acaiblog.blog.question.mapper: debug

创建启动类com.acaiblog.blog.QuestionApplication

package com.acaiblog.blog;

import com.spring4all.swagger.EnableSwagger2Doc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

编辑blog-generator/src/main/java/com/acaiblog/generator/CodeGenerator.java修改配置

public class CodeGenerator {
// 生成的代码放到哪个模块
private static final String PROJECT_NAME = "blog-quesition";
// 数据库名称
private static final String DATABASE_NAME = "blog";
// 子包名
private static final String MODULE_NAME = "blog";
}

运行blog-generator/src/main/java/com/acaiblog/generator/CodeGenerator.java生成模版代码

请输入表名,多个英文逗号分割:
question

创建com.acaiblog.blog.config.MybatisPlusConfig添加MybatisPlusConfig配置类

package com.acaiblog.blog.config;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@EnableTransactionManagement // 开启事务管理
@MapperScan("com.acaiblog.blog.mapper") // 扫描Mapper接口
@Configuration
public class MybatisPlusConfig {
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}

问答新增与更新接口

需求分析

如果是更新,删除question_lable问题标签中间表数据,且设置更新时间
问题数据新增或更新到question
问题所属标签保存到question_lable

数据访问层

编辑blog-question/src/main/java/com/acaiblog/blog/mapper/QuestionMapper.java添加删除和新增问题标签中间表方法

package com.acaiblog.blog.mapper;

import com.acaiblog.entities.Question;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
* <p>
* 问题信息表 Mapper 接口
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
public interface QuestionMapper extends BaseMapper<Question> {
/**
* 通过问题 id 删除问题标签中间表
* @param questionId
* @return
*/
boolean deleteQuestionLabel(@Param("questionId") String questionId);

/**
* 新增问题标签中间表数据
* @param qustionId
* @param labelIds
* @return
*/
boolean saveQuestionLabel(@Param("questionId") String qustionId, @Param("labelIds")List labelIds);
}

编辑blog-question/src/main/java/com/acaiblog/blog/mapper/xml/QuestionMapper.xml添加方法的sql实现

<?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.acaiblog.blog.mapper.QuestionMapper">
<delete id="deleteQuestionLabel">
DELETE FROM question_label where question_id = #{questionId}
</delete>
<insert id="saveQuestionLabel">
INSERT INTO question_label(id, question_id, label_id) VALUES
<foreach collection="labelIds" item="item" separator=",">
('${@com.baomidou.mybatisplus.core.toolkit.IdWorker@getId()}', #{questionId}, #{item})
</foreach>
</insert>
</mapper>

业务层

编辑blog-question/src/main/java/com/acaiblog/blog/service/IQuestionService.java添加接口方法

package com.acaiblog.blog.service;

import com.acaiblog.entities.Question;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 问题信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
public interface IQuestionService extends IService<Question> {
/**
* 更新或保存数据
* @param question
* @return
*/
Result updateOrSave(Question question);

}

编辑blog-question/src/main/java/com/acaiblog/blog/service/impl/QuestionServiceImpl.java添加接口实现

package com.acaiblog.blog.service.impl;

import com.acaiblog.entities.Question;
import com.acaiblog.blog.mapper.QuestionMapper;
import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.Date;

/**
* <p>
* 问题信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {
@Override
public Result updateOrSave(Question question) {
// id不为空是更新操作
if (StringUtils.isNotEmpty(question.getId())) {
// 先删除问题标签中间表数据,再新增到中间表
baseMapper.deleteQuestionLabel(question.getId());
// 设置更新时间
question.setUpdateDate(new Date());
// 更新或保存到文章信息表
super.saveOrUpdate(question);
}
// 新增到文章标签中间表
if(CollectionUtils.isNotEmpty(question.getLabelIds())){
baseMapper.saveQuestionLabel(question.getId(), question.getLabelIds());
}
return Result.ok(question.getId());
}

}

控制层

编辑blog-question/src/main/java/com/acaiblog/blog/controller/QuestionController.java实现API接口

package com.acaiblog.blog.controller;

import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 问题信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Api(value = "问答管理接口", tags = "question")
@RestController
@RequestMapping("/question")
public class QuestionController {
@Autowired
private IQuestionService questionService;

@ApiOperation("修改问答信息")
@PutMapping
public Result update(@RequestBody Question question) {
return questionService.updateOrSave(question);
}

@ApiOperation("新增问答信息")
@PostMapping
public Result save(@RequestBody Question question) {
return questionService.updateOrSave(question);
}
}

问答删除接口

需求分析

假删除,通过 问题id 修改状态为 0 ,表示已删除

业务层

编辑com/acaiblog/blog/service/IQuestionService.java定义删除、更新状态接口

package com.acaiblog.blog.service;

import com.acaiblog.entities.Question;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 问题信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
public interface IQuestionService extends IService<Question> {

/**
* 根据ID删除问答
* @param id
* @return
*/
Result deleteById(String id);

/**
* 根据ID更新状态
* @param id
* @param status
* @return
*/
Result updateStatus(String id, Integer status);

}

编辑blog-question/src/main/java/com/acaiblog/blog/service/impl/QuestionServiceImpl.java定义删除、更新状态具体实现

package com.acaiblog.blog.service.impl;

import com.acaiblog.entities.Question;
import com.acaiblog.blog.mapper.QuestionMapper;
import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.Date;

/**
* <p>
* 问题信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {

/**
* 假删除,通过 问题id 修改状态为 0 ,表示已删除
* @param id
* @return
*/
@Override
public Result deleteById(String id) {
return this.updateStatus(id,0);
}

@Override
public Result updateStatus(String id, Integer status) {
// 查询数据
Question question = baseMapper.selectById(id);
// 设置状态
question.setStatus(status);
// 设置更新时间
question.setUpdateDate(new Date());
// 更新数据
baseMapper.updateById(question);
return Result.ok();
}
}

控制层

编辑blog-question/src/main/java/com/acaiblog/blog/controller/QuestionController.java实现删除和更新状态API接口

package com.acaiblog.blog.controller;


import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 问题信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Api(tags = "question", description = "问答管理接口")
@RestController
@RequestMapping("/question")
public class QuestionController {
@Autowired
private IQuestionService questionService;

@ApiOperation("根据ID删除问答")
@DeleteMapping("/{id}")
@ApiImplicitParam(name = "id", value = "问答ID", required = true)
public Result deleteById(@PathVariable("id") String id) {
return questionService.deleteById(id);
}

@ApiOperation("根据ID更新问答状态")
@PutMapping("/status/{id}")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "问答ID", required = true),
@ApiImplicitParam(name = "status", value = "问答状态", required = true)
})
public Result updateStatus(@PathVariable("id") String id, @PathVariable("status") Integer status) {
return questionService.updateStatus(id,status);
}
}

更新点赞数接口

业务层

编辑blog-question/src/main/java/com/acaiblog/blog/service/IQuestionService.java

package com.acaiblog.blog.service;

import com.acaiblog.entities.Question;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 问题信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
public interface IQuestionService extends IService<Question> {

/**
* 更新点赞接口
* @param id
* @param count
* @return
*/
Result updateThumhup(String id, int count);

}

编辑blog-question/src/main/java/com/acaiblog/blog/service/impl/QuestionServiceImpl.java实现更新点赞接口具体实现

package com.acaiblog.blog.service.impl;

import com.acaiblog.entities.Question;
import com.acaiblog.blog.mapper.QuestionMapper;
import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.Date;

/**
* <p>
* 问题信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {

@Override
public Result updateThumhup(String id, int count) {
if(count != -1 && count != 1) {
return Result.error("无效操作");
}
if(StringUtils.isBlank(id)) {
return Result.error("无效操作");
}
Question question = baseMapper.selectById(id); if(question == null) {
return Result.error("问题不存在");
}
if(question.getThumhup() <= 0 && count == -1) { return Result.error("无效操作");
}
question.setThumhup( question.getThumhup() + count ); // 不用设置更新时间,更新时间是编辑后才设置
baseMapper.updateById(question);
return Result.ok();
}
}

控制层

编辑blog-question/src/main/java/com/acaiblog/blog/controller/QuestionController.java实现点赞API接口

package com.acaiblog.blog.controller;


import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 问题信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Api(tags = "question", description = "问答管理接口")
@RestController
@RequestMapping("/question")
public class QuestionController {
@Autowired
private IQuestionService questionService;

@ApiOperation("更新点赞数")
@PutMapping("/thumb/{id}/{count}")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "问答ID", required = true),
@ApiImplicitParam(name = "count", value = "点赞数", required = true)
})
public Result updateThumhup(@PathVariable("id") String id, @PathVariable("count") int count) {
return questionService.updateThumhup(id,count);
}

}

查询用户发布问答接口

需求分析

根据用户ID查询个人所发布的问题列表,带分页功能。

问答请求类

将用户ID属性封装成一个QuestionUserREQ对象作为条件,创建com.acaiblog.blog.req.QuestionUserREQ

package com.acaiblog.blog.req;

import com.acaiblog.entities.Question;
import com.acaiblog.util.base.BaseRequest;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
@ApiModel(value = "QuestionUserREQ对象", description = "获取指定用户问答的查询条件")
public class QuestionUserREQ extends BaseRequest<Question> {
@ApiModelProperty(value = "用户ID")
private String userId;
}

业务层

编辑blog-question/src/main/java/com/acaiblog/blog/service/IQuestionService.java定义findListByUserId接口

package com.acaiblog.blog.service;

import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 问题信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
public interface IQuestionService extends IService<Question> {

/**
* 根据用户ID查询问答列表
* @param req
* @return
*/
Result findListByUserId(QuestionUserREQ req);

}

编辑blog-question/src/main/java/com/acaiblog/blog/service/impl/QuestionServiceImpl.java定义findListByUserId具体实现方法

package com.acaiblog.blog.service.impl;

import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.blog.mapper.QuestionMapper;
import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;

/**
* <p>
* 问题信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {

@Override
public Result findListByUserId(QuestionUserREQ req) {
if (StringUtils.isEmpty(req.getUserId())) {
return Result.error("无效用户信息");
}
QueryWrapper<Question> queryWrapper = new QueryWrapper<>();
queryWrapper.in("status", Arrays.asList(1,2));
// 根据用户id查询
queryWrapper.eq("user_id", req.getUserId());
// 排序
queryWrapper.orderByDesc("update_date");
return Result.ok(baseMapper.selectPage(req.getPage(), queryWrapper));
}
}

控制层

编辑blog-question/src/main/java/com/acaiblog/blog/controller/QuestionController.java实现查询指定用户发布的问答

package com.acaiblog.blog.controller;


import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 问题信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Api(tags = "question", description = "问答管理接口")
@RestController
@RequestMapping("/question")
public class QuestionController {
@Autowired
private IQuestionService questionService;

@ApiOperation("根据用户ID查询问题列表")
@PostMapping("/user")
public Result findListByUserId(@RequestBody QuestionUserREQ req) {
return questionService.findListByUserId(req);
}

}

统计问答总记录接口

业务层

编辑blog-question/src/main/java/com/acaiblog/blog/service/IQuestionService.java定义getQuestionTotal接口

package com.acaiblog.blog.service;

import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 问题信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
public interface IQuestionService extends IService<Question> {

/**
* 查询问答总记录数
* @return
*/
Result getQuestionTotal();

}

编辑blog-question/src/main/java/com/acaiblog/blog/service/impl/QuestionServiceImpl.java实现getQuestionTotal具体实现方法

package com.acaiblog.blog.service.impl;

import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.blog.mapper.QuestionMapper;
import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;

/**
* <p>
* 问题信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {

@Override
public Result getQuestionTotal() {
QueryWrapper<Question> queryWrapper = new QueryWrapper<>();
queryWrapper.in("status", Arrays.asList(1,2));
int total = baseMapper.selectCount(queryWrapper);
return Result.ok(total);
}
}

控制层

编辑blog-question/src/main/java/com/acaiblog/blog/controller/QuestionController.java实现统计问答总记录API接口

package com.acaiblog.blog.controller;


import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 问题信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Api(tags = "question", description = "问答管理接口")
@RestController
@RequestMapping("/question")
public class QuestionController {
@Autowired
private IQuestionService questionService;

@ApiOperation("统计问答总记录接口")
@GetMapping("/total")
public Result getQuestionTotal() {
return questionService.getQuestionTotal();
}

}

API

公开API,第三方系统可以通过API/xxx访问或调用数据

热门问答分页列表

业务层

编辑blog-question/src/main/java/com/acaiblog/blog/service/IQuestionService.java添加接口方法

package com.acaiblog.blog.service;

import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 问题信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
public interface IQuestionService extends IService<Question> {

/**
* 查询热门回答列表
* @param req
* @return
*/
Result findHotList(BaseRequest<Question> req);

}

编辑blog-question/src/main/java/com/acaiblog/blog/service/impl/QuestionServiceImpl.java实现接口具体实现方法

package com.acaiblog.blog.service.impl;

import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.blog.mapper.QuestionMapper;
import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;

/**
* <p>
* 问题信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {

@Override
public Result findHotList(BaseRequest<Question> req) {
QueryWrapper<Question> queryWrapper = new QueryWrapper<>();
// 查询状态为1和2的数据
queryWrapper.in("status", Arrays.asList(1,2));
// 按照回复降序排列
queryWrapper.orderByDesc("reply");
return Result.ok(baseMapper.selectPage(req.getPage(), queryWrapper));
}
}

控制层

创建com.acaiblog.blog.api.ApiQuestionController实现热门回答API接口

package com.acaiblog.blog.api;

import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(value = "API接口", tags = "API")
@RestController
@RequestMapping("/api/question")
public class ApiQuestionController {
@Autowired
private IQuestionService questionService;

@ApiOperation("热门回答列表")
@PostMapping("/hot")
public Result findHostList(@RequestBody BaseRequest<Question> req) {
return Result.ok(questionService.findHotList(req));
}
}

最新问答分页列表

业务层

编辑blog-question/src/main/java/com/acaiblog/blog/service/IQuestionService.java实现最新问答接口

package com.acaiblog.blog.service;

import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 问题信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
public interface IQuestionService extends IService<Question> {
/**
* 最新问答列表
* @param req
* @return
*/
Result findNewList(BaseRequest<Question> req);

}

编辑blog-question/src/main/java/com/acaiblog/blog/service/impl/QuestionServiceImpl.java实现最新问答接口具体实现

package com.acaiblog.blog.service.impl;

import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.blog.mapper.QuestionMapper;
import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;

/**
* <p>
* 问题信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {

@Override
public Result findNewList(BaseRequest<Question> req) {
QueryWrapper<Question> queryWrapper = new QueryWrapper<>();
queryWrapper.in("status", Arrays.asList(1,2));
queryWrapper.orderByDesc("update_date");
return Result.ok(baseMapper.selectPage(req.getPage(), queryWrapper));
}
}

控制层

编辑blog-question/src/main/java/com/acaiblog/blog/api/ApiQuestionController.java实现最新问答列表API接口

package com.acaiblog.blog.api;

import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(value = "API接口", tags = "API")
@RestController
@RequestMapping("/api/question")
public class ApiQuestionController {
@Autowired
private IQuestionService questionService;

@ApiOperation("最新问答列表")
@PostMapping("/new")
public Result findNewList(@RequestBody BaseRequest<Question> req) {
return Result.ok(questionService.findNewList(req));
}
}

等待问答分页列表

业务层

编辑blog-question/src/main/java/com/acaiblog/blog/service/IQuestionService.java实现等待问答接口

package com.acaiblog.blog.service;

import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 问题信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
public interface IQuestionService extends IService<Question> {

/**
* 等待问答列表
* @param req
* @return
*/
Result findWaitList(BaseRequest<Question> req);

}

编辑blog-question/src/main/java/com/acaiblog/blog/service/impl/QuestionServiceImpl.java

package com.acaiblog.blog.service.impl;

import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.blog.mapper.QuestionMapper;
import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;

/**
* <p>
* 问题信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {

@Override
public Result findWaitList(BaseRequest<Question> req) {
QueryWrapper<Question> queryWrapper = new QueryWrapper<>();
queryWrapper.in("status", Arrays.asList(1,2));
// 查询回复为0,按问题创建时间降序排列
queryWrapper.eq("reply", 0);
queryWrapper.orderByDesc("create_date");
return Result.ok(baseMapper.selectPage(req.getPage(), queryWrapper));
}
}

控制层

编辑blog-question/src/main/java/com/acaiblog/blog/api/ApiQuestionController.java实现等待问答列表API接口

package com.acaiblog.blog.api;

import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(value = "API接口", tags = "API")
@RestController
@RequestMapping("/api/question")
public class ApiQuestionController {
@Autowired
private IQuestionService questionService;

@ApiOperation("等待问答列表")
@PostMapping("/wait")
public Result findWaitList(@RequestBody BaseRequest<Question> req) {
return Result.ok(questionService.findWaitList(req));
}
}

通过标签ID查询问题列表

数据访问层

编辑blog-question/src/main/java/com/acaiblog/blog/mapper/QuestionMapper.java添加findListByLabelId接口方法

package com.acaiblog.blog.mapper;

import com.acaiblog.entities.Question;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
* <p>
* 问题信息表 Mapper 接口
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
public interface QuestionMapper extends BaseMapper<Question> {

/**
* 根据标签ID分页查询问题列表
* @param page
* @param labelId
* @return
*/
IPage<Question> findListByLabelId(IPage<Question> page, @Param("labelId") String labelId);
}

编辑blog-question/src/main/java/com/acaiblog/blog/mapper/xml/QuestionMapper.xml添加findListByLabelId方法sql查询实现

<?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.acaiblog.blog.mapper.QuestionMapper">
<select id="findListByLabelId" resultMap="Question">
SELECT DISTINCT t1.*
FROM question t1
LEFT JOIN question_label t2 ON t1.id = t2.question_id
WHERE t1.`status` != 0
AND t2.label_id = #{labelId}
ORDER BY t1.update_date DESC
</select>
</mapper>

业务层

编辑blog-question/src/main/java/com/acaiblog/blog/service/IQuestionService.java实现接口

package com.acaiblog.blog.service;

import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 问题信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
public interface IQuestionService extends IService<Question> {
/**
* 根据标签ID分页查询问题列表
* @param req
* @param labelId
* @return
*/
Result findListByLabelId(BaseRequest<Question> req, String labelId);

}

编辑blog-question/src/main/java/com/acaiblog/blog/service/impl/QuestionServiceImpl.java实现findListByLabelId接口具体实现

package com.acaiblog.blog.service.impl;

import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.blog.mapper.QuestionMapper;
import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;

/**
* <p>
* 问题信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {

@Override
public Result findListByLabelId(BaseRequest<Question> req, String labelId) {
if (StringUtils.isEmpty(labelId)) {
return Result.error("标签ID不能为空");
}
return Result.ok(baseMapper.findListByLabelId(req.getPage(), labelId));
}
}

控制层

编辑blog-question/src/main/java/com/acaiblog/blog/api/ApiQuestionController.java实现通过标签ID查询问答列表API接口

package com.acaiblog.blog.api;

import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@Api(value = "API接口", tags = "API")
@RestController
@RequestMapping("/api/question")
public class ApiQuestionController {
@Autowired
private IQuestionService questionService;

@ApiOperation("根据标签ID获取问答列表")
@PostMapping("/list/{labelId}")
@ApiImplicitParam(name = "labelId", value = "标签ID", required = true)
public Result findListByLabelId(@RequestBody BaseRequest<Question> req, @PathVariable("labelId") String labelId) {
return questionService.findListByLabelId(req, labelId);
}
}

问答详情页

需求分析

  1. 查询问答详情与标签ids
  2. feign远程调用Article微服务查询所属标签
  3. 查询问题对应问答列表

重构question实体类

编辑blog-question/src/main/java/com/acaiblog/entities/Question.java添加以下属性

package com.acaiblog.entities;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.w3c.dom.stylesheets.LinkStyle;

import java.awt.*;
import java.io.Serializable;
import java.util.Date;
import java.util.List;

/**
* <p>
* 问题信息表
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value="Question对象", description="问题信息表")
public class Question implements Serializable {

private static final long serialVersionUID = 1L;

@ApiModelProperty(value = "所属标签ID集合")
@TableField(exist = false)
private List<String> labelIds;

@ApiModelProperty(value = "所属标签对象集合")
@TableField(exist = false)
private List<Label> labelList;
}

数据访问层

编辑blog-question/src/main/java/com/acaiblog/blog/mapper/QuestionMapper.java添加查询问题和标签ids方法

package com.acaiblog.blog.mapper;

import com.acaiblog.entities.Question;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
* <p>
* 问题信息表 Mapper 接口
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
public interface QuestionMapper extends BaseMapper<Question> {

/**
* 根据问题ID查询问题详情与标签ids
* @param id
* @return
*/
Question findQuestionAndLabelIdsByid(String id);
}

编辑blog-question/src/main/java/com/acaiblog/blog/mapper/xml/QuestionMapper.xml添加findQuestionAndLabelIdsByid方法sql实现

<?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.acaiblog.blog.mapper.QuestionMapper">
<select id="findQuestionAndLabelIdsByid" resultMap="QuestionAndLabelIdsMap">
SELECT q.*, ql.label_id
FROM question q
LEFT JOIN question_label ql ON q.id = ql.question_id
WHERE q.id = #{id}
</select>
<resultMap id="QuestionAndLabelIdsMap" type="Question">
<!--映射主键-->
<id column="id" property="id"/>
<!--映射一对一-->
<result column="user_id" property="userId"/>
<result column="nick_name" property="nickName"/>
<result column="user_image" property="userImage"/> <result column="title" property="title"/>
<result column="md_content" property="mdContent"/> <result column="html_content" property="htmlContent"/>
<result column="view_count" property="viewCount"/>
<result column="thumhup" property="thumhup"/>
<result column="reply" property="reply"/>
<result column="status" property="status"/>
<result column="create_date" property="createDate"/> <result column="update_date" property="updateDate"/>
<!-- 映射一对多关系(映射集合)
property: 对应 Category 的属性名
javaType: 属性的数据类型
ofType: 配置集合内部的数据类型
-->
<!--封装标签ID集合-->
<collection property="labelIds" javaType="list" ofType="string" > <!-- 映射主键 -->
<id column="label_id" property="id"/>
</collection>
</resultMap>
</mapper>

业务层

编辑blog-question/src/main/java/com/acaiblog/blog/service/IQuestionService.java添加通过问题id查询详情接口方法

package com.acaiblog.blog.service;

import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 问题信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
public interface IQuestionService extends IService<Question> {

/**
* 通过问题id查询详情
* @param id
* @return
*/
Result findById(String id);
}

编辑blog-question/src/main/java/com/acaiblog/blog/service/impl/QuestionServiceImpl.java添加接口实现通过问题id查询详情,还有Feign远程调用Article微服务查询标签信息没有实现

package com.acaiblog.blog.service.impl;

import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.blog.mapper.QuestionMapper;
import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;

/**
* <p>
* 问题信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {

@Override
public Result findById(String id) {
Question question = baseMapper.findQuestionAndLabelIdsByid(id);
if (question == null) {
return Result.error("为查询到相关问题信息");
}
// Feign 程调用 Article 微服务查询标签信息
if (CollectionUtils.isNotEmpty(question.getLabelIds())) {

}
return Result.ok(question);
}
}

控制层

编辑blog-question/src/main/java/com/acaiblog/blog/api/ApiQuestionController.java实现查看问题详情接口方法

package com.acaiblog.blog.api;

import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@Api(value = "API接口", tags = "API")
@RestController
@RequestMapping("/api/question")
public class ApiQuestionController {
@Autowired
private IQuestionService questionService;

@ApiOperation("查询问题详情接口")
@GetMapping("/{id}")
@ApiImplicitParam(name = "id", value = "问题ID", required = true)
public Result view(@PathVariable("id") String id) {
return questionService.findById(id);
}
}

更新浏览数

业务层

编辑blog-question/src/main/java/com/acaiblog/blog/service/IQuestionService.java接口定义更新浏览次数抽象方法

package com.acaiblog.blog.service;

import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 问题信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
public interface IQuestionService extends IService<Question> {

/**
* 更新浏览次数
* @param id
* @return
*/
Result updateViewCount(String id);

}

编辑blog-question/src/main/java/com/acaiblog/blog/service/impl/QuestionServiceImpl.java添加updateViewCount接口具体实现

package com.acaiblog.blog.service.impl;

import com.acaiblog.blog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.blog.mapper.QuestionMapper;
import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;

/**
* <p>
* 问题信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {

@Override
public Result updateViewCount(String id) {
if (StringUtils.isBlank(id)) {
return Result.error("无效操作");
}
Question question = baseMapper.selectById(id);
if (question == null) {
return Result.error("问题不存在");
}
question.setViewCount(question.getViewCount() + 1);
baseMapper.updateById(question);
return Result.ok();
}
}

控制层

编辑blog-question/src/main/java/com/acaiblog/blog/api/ApiQuestionController.java添加更新浏览数API接口

package com.acaiblog.blog.api;

import com.acaiblog.blog.service.IQuestionService;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@Api(value = "API接口", tags = "API")
@RestController
@RequestMapping("/api/question")
public class ApiQuestionController {
@Autowired
private IQuestionService questionService;

@ApiOperation("更新浏览数")
@PutMapping("/viewCount/{id}")
@ApiImplicitParam(name = "id", value = "问题ID", required = true)
public Result updateViewCount(@PathVariable("id") String id) {
return questionService.updateViewCount(id);
}
}

问答评论管理

批量删除问题回答评论

实体类

创建实体类com.acaiblog.entities.Replay

package com.acaiblog.entities;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;
import java.util.Date;

@Data
@EqualsAndHashCode
@ApiModel(value = "Replay对象", description = "回复评论表")
public class Replay implements Serializable {
// java中用于实现Serializable接口的类中的一个属性。这个属性的作用是为了版本控制,确保在对象的序列化和反序列化过程中,类的版本保持一致。
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "主键")
@TableId(value = "id", type = IdType.ASSIGN_ID)
private String id;

@ApiModelProperty(value = "上一层评论ID")
private String parentId;

@ApiModelProperty(value = "用户ID")
private String userId;

@ApiModelProperty(value = "发布者用户昵称")
private String nickName;

@ApiModelProperty(value = "发布者头像url")
private String userImage;

@ApiModelProperty(value = "问题ID")
private String questionId;

@ApiModelProperty(value = "md问题内容")
private String mdContent;

@ApiModelProperty(value = "html问题内容")
private String htmlContent;

@ApiModelProperty(value = "创建时间")
private Date createDate;
}

数据访问层

创建com.acaiblog.blog.mapper.ReplayMapper

package com.acaiblog.blog.mapper;

import com.acaiblog.entities.Replay;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

public interface ReplayMapper extends BaseMapper<Replay> {
}

业务层

创建com.acaiblog.blog.service.IReplayService

package com.acaiblog.blog.service;

import com.acaiblog.entities.Replay;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

public interface IReplayService extends IService<Replay> {
/**
* 通过回答评论id递归删除
* @param id
* @return
*/
Result deleteById(String id);
}

创建com.acaiblog.blog.service.impl.ReplayServicelmpl添加实现删除回答评论后要更新问题表中的回答数量,删除多少条减多少条。

package com.acaiblog.blog.service.impl;

import com.acaiblog.blog.mapper.QuestionMapper;
import com.acaiblog.blog.mapper.ReplayMapper;
import com.acaiblog.blog.service.IReplayService;
import com.acaiblog.entities.Question;
import com.acaiblog.entities.Replay;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
public class ReplayServicelmpl extends ServiceImpl<ReplayMapper, Replay> implements IReplayService {
@Autowired
private QuestionMapper questionMapper;

private void getIds(List<String> ids, String parentId) {
// 查询子评论对象
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("parent_id", parentId);
List<Replay> replayList = baseMapper.selectList(queryWrapper);
if (CollectionUtils.isNotEmpty(replayList)) {
for (Replay replay: replayList) {
String id = replay.getId();
ids.add(id);
// 递归继续查询子评论id
this.getIds(ids, id);
}
}
}

@Transactional // 进行事物管理
@Override
public Result deleteById(String id) {
if (StringUtils.isBlank(id)) {
return Result.error("回答评论ID不能为空");
}
// 要删除的回答评论ids
ArrayList<String> ids = new ArrayList<>();
// 先把要删除的一级回答id放入到集合中
ids.add(id);
// 递归的子评论 id 加入到集合中
this.getIds(ids, id);
// 删除回答评论后,还要更新问题表中的回答数量,则先查问题id
Replay replay = baseMapper.selectById(id);
// 批量删除集合中的id
int size = baseMapper.deleteBatchIds(ids);
if (size > 0) {
// 更新问题表中的回答数量
Question question = questionMapper.selectById(replay.getQuestionId());
question.setReply(question.getReply() - size);
questionMapper.updateById(question);
}
return Result.ok();
}
}

控制层

创建com.acaiblog.blog.controller.ReplayController

package com.acaiblog.blog.controller;

import com.acaiblog.blog.service.IReplayService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(value = "问答评论管理", tags = "Replay")
@RestController
@RequestMapping("/replay")
public class ReplayController {
@Autowired
IReplayService replayService;

@ApiOperation("删除回答评论接口")
@DeleteMapping("/{id}")
@ApiImplicitParam(name = "id", value = "回答评论ID", required = true)
public Result delete(@PathVariable("id") String id) {
return replayService.deleteById(id);
}
}

问答新增

需求分析

插入回答数据到回答表replay,并且更新question问题表中的回答数量。

业务层

编辑blog-question/src/main/java/com/acaiblog/blog/service/IReplayService.java添加add接口

package com.acaiblog.blog.service;

import com.acaiblog.entities.Replay;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

public interface IReplayService extends IService<Replay> {

/**
* 新增回答并更新问题表中的回答数量
* @param replay
* @return
*/
Result add(Replay replay);
}

编辑blog-question/src/main/java/com/acaiblog/blog/service/impl/ReplayServicelmpl.java添加add接口具体实现

package com.acaiblog.blog.service.impl;

import com.acaiblog.blog.mapper.QuestionMapper;
import com.acaiblog.blog.mapper.ReplayMapper;
import com.acaiblog.blog.service.IReplayService;
import com.acaiblog.entities.Question;
import com.acaiblog.entities.Replay;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
public class ReplayServicelmpl extends ServiceImpl<ReplayMapper, Replay> implements IReplayService {
@Autowired
private QuestionMapper questionMapper;

@Transactional
@Override
public Result add(Replay replay) {
boolean ok = this.save(replay);
if (ok) {
// 更新问题表中的回答数量
Question question = questionMapper.selectById(replay.getQuestionId());
question.setReply(question.getReply() + 1);
questionMapper.updateById(question);
}
return Result.ok();
}
}

控制层

编辑blog-question/src/main/java/com/acaiblog/blog/controller/ReplayController.java实现问答新增API接口

package com.acaiblog.blog.controller;

import com.acaiblog.blog.service.IReplayService;
import com.acaiblog.entities.Replay;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@Api(value = "问答评论管理", tags = "Replay")
@RestController
@RequestMapping("/replay")
public class ReplayController {
@Autowired
IReplayService replayService;

@ApiOperation("新增问答接口")
@PostMapping
public Result add(@RequestBody Replay replay) {
return replayService.add(replay);
}
}

升级Springboot和SpringCloud

编辑pom.xml点击Maven》Reload All Maven Projects

<?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>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/>
</parent>

<!-- 依赖版本管理-->
<properties>
<spring-cloud.version>Hoxton.SR7</spring-cloud.version>
</properties>
</project>

启动报错:

org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is com.google.common.util.concurrent.ExecutionError: com.google.common.util.concurrent.ExecutionError: java.lang.NoClassDefFoundError: javax/validation/constraints/Min

解决方式:springboot2.3.x没有自动引入validation对应的包,所以要想使用校验功能要手动引入包。编辑blog-util/pom.xml然后reimport

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

Nacos服务注册与发现

xxxxxxxxxx     <context:component-scan base-package=”com.acaiblog.spring”/>    <context:property-placeholder location=”classpath:db.properties”/>                                                                            tx:annotation-driven    <tx:advice id=”tx” transaction-manager=”transactionManager”>        tx:attributes            <tx:method name=”find*” read-only=”true”/>            xml

Nacos是阿里巴巴推出来的开源项目,这是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。Nacos致力于帮助您发现、配置和管理微服务。Nacos提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
官网:https://nacos.io/zh-cn/
源码:https://github.com/alibaba/nacos
SpingCloud 与 Nacos 版本说明参考:https://github.com/alibaba/spring-cloud-alibaba/wiki/版本说明

Nacos与Spring Cloud对比

特点 Nacos Spring Cloud
服务注册与发现 支持服务注册、发现和健康检查 Eureka服务注册与发现,也支持Consul和Zookeeper
配置管理 提供动态配置管理和分布式配置 使用Spring Cloud Config进行分布式配置管理
负载均衡 内置负载均衡功能,支持多种负载均衡策略 集成Ribbon负载均衡
网关 内置API网关,支持动态路由和流量管理 使用Spring Cloud Gateway或Zuul作为API网关
分布式事务 提供分布式事务支持 使用Spring Cloud Sleuth和Zipkin进行分布式追踪
分布式配置中心 支持分布式配置中心 使用Spring Cloud Config Server进行配置中心

工作原理

Nacos提供Nacos Server服务端与Nacos Client客户端 ,服务端即是Nacos服务注册中心,Nacos客户端微服务向Nacos服务端注册与发现。

  1. Nacos Server是服务端,负责管理各个微服务注册和发现。
  2. 在微服务上添加Nacos Client代码,就会访问到Nacos Server将此微服务注册在Nacos Server中,从而使其他微服务消费方能够找到。
  3. 微服务(服务消费者)需要调用另一个微服务(服务提供者)时,从Nacos Server中获取服务调用地址,进行远程调用。

搭建Nacos服务端

docker部署

docker run --name nacos-server -e MODE=standalone -p 8848:8848 -d --restart always nacos/nacos-server:v2.3.0-slim

访问Nacos

启动成功后,Nacos控制台可以访问了,Nacos控制台主要旨在于增强对于服务列表,健康状态管理,服务治理,分布式配置管理等方面的管控能力。
浏览器访问:http://127.0.0.1:8848/nacos/index.html ,默认的账号密码为:nacos/nacos

微服务注册到Nacos

文章微服务注册

添加依赖

编辑blog-article/pom.xml添加

<dependency>
<!-- 不指定版本maven会自动匹配适合的版本-->
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-startr-alibaba-nacos-discovery</artifactId>
</dependency>

编辑父工程pom.xml添加

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<!-- -maven不支持多继承,使用 import 来依赖管理配置-->
<scope>import</scope>
</dependency>

配置application.yml

编辑blog-article/src/main/resources/application.yaml添加nacos地址

spring:
application:
name: article-server # 应用名称
cloud:
nacos:
discovery:
server-addr: localhost:8849

启动类添加注释

编辑blog-article/src/main/java/com/acaiblog/ArticleApplication.java添加注解EnableDiscoveryClient

package com.acaiblog;

import com.spring4all.swagger.EnableSwagger2Doc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@EnableSwagger2Doc
@SpringBootApplication
public class ArticleApplication {
public static void main(String[] args) {
SpringApplication.run(ArticleApplication.class, args);
System.out.println("http://127.0.0.1:8001/article/swagger-ui.html#");
}
}

测试

  1. 启动nacos server

  2. 检查springboot启动日志

    18:41:25.312  INFO 12346 --- [           main] c.a.c.n.registry.NacosServiceRegistry    : nacos registry, DEFAULT_GROUP article-server 192.168.207.211:8001 register  
  3. 登录Nocos控制台,查看服务列表中,已经显示了文章微服务注册上来的服务信息。其中服务名article-server就是对应application.yml中的spring.application.name的值

    问答微服务注册

    添加依赖

    编辑blog-question/pom.xml,maven》reload

    <dependency>
    <!-- 不指定版本maven会匹配合适的版本-->
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

    配置application.yml

    spring:
    application:
    name: question-server # 应用名称
    cloud:
    nacos:
    discovery:
    server-addr: localhost:8849

    启动类添加注解

    编辑blog-question/src/main/java/com/acaiblog/blog/QuestionApplication.java启动类添加nacos客户端注解,并启动question项目

    package com.acaiblog.blog;

    import com.spring4all.swagger.EnableSwagger2Doc;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

    @EnableDiscoveryClient
    @EnableSwagger2Doc
    @SpringBootApplication
    public class QuestionApplication {
    public static void main(String[] args) {
    SpringApplication.run(QuestionApplication.class);
    System.out.println(String.format("API UI: http://127.0.0.1:8002/question/swagger-ui.html#/"));
    }
    }

    测试

    登录nacos管理台应该可以看到两个服务:article-server、question-server

    Nacos配置中心

    分布式架构配置

    在分布式微服务架构中,由于服务数量很多 ,每个服务都有自己的配置文件,然后每个系统往往还会准备开发环境、测试环境、正式环境。
    假设某系统有10个微服务,那么至少有10个配置文件吧,三个环境(dev\test\prod),那就有30个配置文件需要进行管理。
    这么多配置文件的管理起来就很麻烦,稍有不慎可能就会出现改错了、不生效….等等问题,所以一套集中式的、动态的配置管理功能是必不可少的,在Nacos中集成了分布式配置中心组件来解决这个问题。

    Nacos配置中心的作用

  4. 集中管理配置文件

  5. 不同环境不同配置,动态化的配置更新,根据不同环境部署,如dev/test/prod

  6. 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置,服务会向配置中心统一拉取自已的配置信息

  7. 当配置发生变动时,服务不需要重启即可感知到配置的变化并使用修改后的配置信息

  8. 将配置信息以REST接口的形式暴露

新建配置

Data ID格式

完整格式如下:

${prefix}-${spring.profiles.active}.${file-extension}

article-server-dev.yml配置文件为例:

配置项 描述
spring.application.name prefix 的默认值,用于拼接 dataId
spring.cloud.nacos.config.prefix 可配置的 prefix,用于拼接 dataId
spring.profiles.active 当前环境对应的 profile,用于拼接 dataId
spring.profile.active 当为空时,dataId 拼接格式变成 ${prefix}.${file-extension}
spring.cloud.nacos.config.file-extension 配置内容的数据格式,支持 propertiesyaml/yml 类型。

操作步骤

登录Nacos控制台》配置管理》配置列表》新建配置

Data ID: article-server.yml
Group: DEFAULT_GROUP
配置格式: YAML
配置内容:
user:
name: acai
age: 31

文章微服务配置管理

添加依赖

编辑blog-article/pom.xml添加以下依赖,并执行maven reload

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
<dependency>

创建bootstrap.yml

创建blog-article/src/main/resources/bootstrap.yml配置文件

spring:
application:
name: article-server
cloud:
nacos:
discovery:
server-addr: localhost:8849 # 注册中心地址
config:
server-addr: localhost:8849 # 配置中心地址
file-extension: yml #配置中心配置文件格式

获取配置中心配置

新建ConfigController类,通过Rest方式验证是否能够读取到nacos上的配置信息

package com.acaiblog.article.controller;

import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "Nacos", description = "Nacos配置管理接口")
@RefreshScope
@RestController
public class ConfigController {
@Value("${user.name}")
private String name;

@Value("${user.age}")
private String age;

@RequestMapping("/config")
public String getConfig() {
String content = String.format("name: %s, age: %s",name,age);
System.out.println(content);
return content;
}
}

请求API接口:http://127.0.0.1:8001/article/config ,返回以下数据

name: acai, age: 31

修改Nacos数据库为MySQL

在mysql数据库创建nacos数据库

create database nacos;

创建数据库表,参考:https://raw.githubusercontent.com/alibaba/nacos/2.3.0/distribution/conf/mysql-schema.sql
使用docker容器创建nacos并指定使用mysql数据库

docker run --name nacos-server -e MODE=standalone -p 8848:8848 -e SPRING_DATASOURCE_PLATFORM=mysql -e MYSQL_SERVICE_HOST=192.168.207.211 -e MYSQL_SERVICE_DB_NAME=nacos -e MYSQL_SERVICE_USER=root -e MYSQL_SERVICE_PASSWORD=123456 -d --restart always nacos/nacos-server:v2.3.0-slim

重新运行项目测试

多环境下切换配置文件

  1. 在nacos控制台创建Data ID为article-server-dev.yml的配置信息,配置内容复制blog-article服务的application.yml内容过去
  2. 在nacos控制台创建Data ID为article-server-prod.yml的配置信息,配置内容复制blog-article服务的application.yml内容过去
  3. 方式一:引用dev开发环境配置,在bootstrap.yml配置文件添加spring.profiles.active=dev,这种写死在配置文件中不推荐
  4. 方式二:通过IDE编辑启动文件,设置Actives profiles为dev
  5. 注释application.yml文件中的内容,启动测试
  6. 打jar包运行时,使用命令行参数指定:java -jar xxxx.jar --spring.profiles.active=dev

问答微服务配置管理

添加依赖

编辑blog-question/pom.xml,并执行maven》reload

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

创建bootstrap.yml

创建blog-question/src/main/resources/bootstrap.yml配置文件

spring:
application:
name: question-server
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 注册中心地址
config:
server-addr: localhost:8848 # 配置中心地址
file-extension: yml #配置中心配置文件格式
profiles:
active: dev # 指定环境

Nacos创建开发和生产配置文件

  1. 在Nacos创建question-server-dev.ymlquestion-server-prod.yml配置文件,文件内容复制application.yml文件
  2. 注释application.yml文件内容,启动测试

Nacos与Feign服务间接口调用

Feign简介

在分页式微服务架构的项目中,服务间可能需要存在接口间的调用 ,比如:问答微服务 需要调用 文章微服务 查询标签信息。目前调用大都用的是Feign去调用其他服务接口, 为服务调用提供了更优雅的方式。
FeignNetflix公司开源的轻量级Rest客户端( https://github.com/OpenFeign/feign ),使用Feign可以非常方便、简单的实现Http客户端,使用Feign只需要定义一个接口,然后在接口上添加注解即可。
Spring Cloud已对Feign进行了封装。 Feign接口统一在blog-api模块中进行定义,方便统一管理。

需求分析

问答微服务需要调用文章微服务查询标签信息。在问答详情页接口预留了一个通过标签ids获取标签名,标签名是位于文章微服务中,这样我们需要在 问答微服务远程调用文章微服务接口来进行获取。

添加依赖

编辑blog-api/pom.xml

<!--        feign 调用服务接口-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

定义文章Feign接口与实现

介绍

使用@FeignClient注解标识,指定是调用哪个微服务,就是那个spring.application.name值。标识后,注册中心会根据这个服务名称去找对应的微服务IP:Port,从而调用到这个服务接口。
blog-api定义Feign控制层接口com.acaiblog.blog.feign.IFeignArticleController,将作为各微服务间远程调用使用。

@FeignClient注解参数

参数 描述
value(可选) 指定微服务接口的名称。
path(可选) 在Feign调用时添加的前缀。如果微服务中配置了context-path值,则它与接口实现类的微服务的context-path值一致。如果未配置,不需要指定path。

需求分析

问答微服务 需要调用 文章微服务 查询标签信息。在 问答详情页接口 预留了一个通过标签ids获取标签名,标签名是位于 文章微服务 中,这样我们需要在 问答微服务 远程调用 文章微服务接口来进行获取。

添加依赖

编辑blog-api/pom.xml

<!--        feign 调用服务接口-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

定义文章Feign接口与实现

复制blog-article/src/main/java/com/acaiblog/entities/Label.javablog-api/src/main/java/com/acaiblog/blog/entities/Label.java
blog-api模块下创建com.acaiblog.blog.feign.IFeignArticleController

package com.acaiblog.feign;

import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.List;

// value 指定是哪个微服务接口,
// path 是在 Feign 调用时会加上此前缀,它与接口实现类的微服务中配置的 context-path 值一致,如果微服务中没有配置 context-path 下面就不需要写 path
@FeignClient(value = "article-server", path = "/article")
public interface IFeignArticleController {
@ApiOperation("Feign接口-根据标签ids查询对应的标签信息")
@GetMapping("/api/feign/label/list/{ids}")
@ApiImplicitParam(allowMultiple = true, dataType = "String", name = "ids", value = "标签集合", required = true)
List<com.acaiblog.entities.Label> getLabelListByIds(@PathVariable("ids") List<String> labelIds);
}

blog-article模块创建com.acaiblog.article.feign.FeignArticleController

package com.acaiblog.article.feign;

import com.acaiblog.article.service.ILabelService;
import com.acaiblog.feign.IFeignArticleController;
import com.acaiblog.entities.Label;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class FeignArticleController implements IFeignArticleController {
@Autowired
private ILabelService labelService;

@Override
public List<Label> getLabelListByIds(List<String> labelIds) {
return labelService.listByIds(labelIds);
}

}

测试控制层接口

访问GET请求:http://127.0.0.1:8001/article/api/feign/label/list/1%2C2

问答服务调用文章服务接口

需求分析

在问答详情页接口预留了一个通过标签ids获取标签名,标签名是位于文章微服务中,这样我们需要在问答微服务远程调用文章微服务接口来进行获取。

重构问答业务层代码

blog-question模块中,编辑blog-question/src/main/java/com/acaiblog/service/impl/QuestionServiceImpl.java

package com.acaiblog.service.impl;

import com.acaiblog.entities.Label;
import com.acaiblog.feign.IFeignArticleController;
import com.acaiblog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.mapper.QuestionMapper;
import com.acaiblog.service.IQuestionService;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.Date;
import java.util.List;

/**
* <p>
* 问题信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {
@Autowired
private IFeignArticleController feignArticleController;

@Override
public Result findById(String id) {
Question question = baseMapper.findQuestionAndLabelIdsByid(id);
if (question == null) {
return Result.error("为查询到相关问题信息");
}
// Feign 程调用 Article 微服务查询标签信息
if (CollectionUtils.isNotEmpty(question.getLabelIds())) {
List<Label> labelList = feignArticleController.getLabelListByIds(question.getLabelIds());
System.out.println("labelList: " + labelList);
question.setLabelList(labelList);
}
return Result.ok(question);
}
}

启动类添加注解

编辑blog-question模块的QuestionApplication启动类加上@EnableFeignClients扫描@Feign标识的接口。编辑blog-question/src/main/java/com/acaiblog/QuestionApplication.java

package com.acaiblog;

import com.spring4all.swagger.EnableSwagger2Doc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients
@EnableDiscoveryClient
@EnableSwagger2Doc
@SpringBootApplication
public class QuestionApplication {
public static void main(String[] args) {
SpringApplication.run(QuestionApplication.class);
System.out.println(String.format("API UI: http://127.0.0.1:8002/question/swagger-ui.html#/"));
}
}

测试Feign调用

发送GET请求:http://127.0.0.1:8002/question/api/question/1

系统权限微服务

需求分析

包含用户管理,角色管理,菜单管理等模块

创建数据库表

执行sql:https://gitee.com/acaiblog/springboot-blog/raw/master/files/system.sql

搭建系统权限微服务

创建blog-system模块

添加依赖

编辑blog-system/pom.xml

<dependencies>
<dependency>
<groupId>com.acaiblog</groupId>
<artifactId>blog-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- Nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<!-- Nacos配置中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-nacos-config</artifactId>
</dependency>
<!-- Spring Seucrity加密模块-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<!-- 热部署ctrl+F9-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

创建bootstrap.yml配置文件

创建blog-system/src/main/resources/bootstrap.yml

spring:
application:
name: system-server
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 注册中心地址
config:
server-addr: localhost:8848 # 配置中心地址
file-extension: yml #配置中心配置文件格式
profiles:
active: dev # 指定环境

Nacos创建配置文件

在Nacos控制台创建system-server-dev.yml配置文件

swagger:
description: '系统权限'
server:
port: 8003
servlet:
context-path: /system # 上下文件路径,请求前缀 ip:port/article
spring:
application:
name: system-server # 应用名称
cloud:
nacos:
discovery:
server-addr: localhost:8848
# 数据源
datasource:
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 123456
url: jdbc:mariadb://localhost:3306/blog?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true
driver-class-name: org.mariadb.jdbc.Driver
# 数据源其他配置, 在 DruidConfig配置类中手动绑定
initialSize: 8
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
druid:
testWhileIdle: true
validationQuery: SELECT 1 FROM DUAL
mybatis-plus:
type-aliases-package: com.acaiblog.entities
# xxxMapper.xml 路径
mapper-locations: classpath*:com/acaiblog/article/mapper/**/*.xml
# 日志级别,会打印sql语句
logging:
level:
com.acaiblog.article.mapper: debug
debug: false

创建启动类

创建com.acaiblog.SystemApplication启动类

package com.acaiblog;

import com.spring4all.swagger.EnableSwagger2Doc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@EnableSwagger2Doc
@SpringBootApplication
public class SystemApplication {
public static void main(String[] args) {
SpringApplication.run(SystemApplication.class, args);
System.out.println(String.format("API UI: http://127.0.0.1:8003/question/swagger-ui.html#/"));
}
}

生成代码模版

编辑blog-generator/src/main/java/com/acaiblog/generator/CodeGenerator.java

public class CodeGenerator {
// 生成的代码放到哪个模块
private static final String PROJECT_NAME = "blog-system";
// 数据库名称
private static final String DATABASE_NAME = "blog";
// 子包名
private static final String MODULE_NAME = "system";
}

运行脚本

请输入表名,多个英文逗号分割:
sys_user,sys_role,sys_menu

MybatisPlusConfig配置类

配置类的作用是配置MyBatis Plus的分页插件,并开启了Spring的事务管理功能,同时指定了MyBatis的Mapper接口所在的包。创建com.acaiblog.system.config.MybatisPlusConfig配置类

package com.acaiblog.system.config;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@EnableTransactionManagement // 开启事务管理
@MapperScan("com.acaiblog.system.mapper") // 扫描Mapper接口
@Configuration
public class MybatisPlusConfig {
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}

测试

运行项目测试

菜单管理

菜单列表

需求分析

菜单是有限的,所以可以不需要分页的,可通过菜单名称查询菜单列表,要将子菜单放到 children 属性中。

封装请求类

将查询条件菜单名称封装成一个SysMenuREQ对象,编辑blog-system/src/main/java/com/acaiblog/entities/SysMenu.java添加一个封装子菜单集合的属性children

package com.acaiblog.entities;

import com.baomidou.mybatisplus.annotation.IdType;
import java.util.Date;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import java.util.List;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
* <p>
* 菜单信息表
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value="SysMenu对象", description="菜单信息表")
public class SysMenu implements Serializable {

private static final long serialVersionUID = 1L;

@ApiModelProperty(value = "子菜单集合")
@TableField(exist = false) // 不是sys_menu表字段
private List<SysMenu> children;
}

创建com.acaiblog.system.req.SysMenuREQ请求类,因为不需要分页,所以不需要继承BaseRequst

package com.acaiblog.system.req;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors
@ApiModel(value = "SysMenuREQ对象", description = "菜单列表查询条件")
public class SysMenuREQ {
@ApiModelProperty(value = "菜单名称")
private String name;
}

服务层

编辑blog-system/src/main/java/com/acaiblog/system/service/ISysMenuService.java添加queryList接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysMenu;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 菜单信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysMenuService extends IService<SysMenu> {
Result queryList(SysMenuREQ req);

}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysMenuServiceImpl.java添加queryList接口具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysMenu;
import com.acaiblog.system.mapper.SysMenuMapper;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.system.service.ISysMenuService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

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

/**
* <p>
* 菜单信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements ISysMenuService {
private List<SysMenu> childrenMenu(List<SysMenu> menuList, SysMenu menu) {
// 封装菜单的parentid = id子菜单集合
List<SysMenu> children = new ArrayList<>();
// 每次都迭代所有菜单,判断是否为 menu 的子菜单
for (SysMenu m: menuList) {
// 如果 m.parentId 等于 id 则就是它的子菜单
if (m.getParentId().equals(menu.getId())) {
// 是子菜单,则递归去找这个菜单的子菜单
children.add((SysMenu) childrenMenu(menuList,m));
}
}
// 封装menu子菜单
menu.setChildren(children);
return (List<SysMenu>) menu;
}
private List<SysMenu> buildTree(List<SysMenu> menuList) {
// 获取根菜单
List<SysMenu> rootMenuList = new ArrayList<>();
for (SysMenu menu: menuList) {
// 如果 m.parentId 等于 0 就是根菜单
if (menu.getParentId().equals("0")) {
rootMenuList.add(menu);
}
}
// 根菜单下的子菜单
for (SysMenu menu: rootMenuList) {
childrenMenu(menuList, menu);
}
// 返回根菜单,因为根菜单中封装了子菜单
return rootMenuList;
}
@Override
public Result queryList(SysMenuREQ req) {
QueryWrapper<SysMenu> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotEmpty(req.getName())) {
queryWrapper.like("name", req.getName());
}
// sort升序 update_date降序
queryWrapper.orderByDesc("sort").orderByDesc("update_date");
// 获取所有菜单列表
List<SysMenu> menuList = baseMapper.selectList(queryWrapper);
// 封装树状菜单并响应
return Result.ok(this.buildTree(menuList));
}

}

控制层

编辑blog-system/src/main/java/com/acaiblog/system/controller/SysMenuController.java实现查询菜单API接口

package com.acaiblog.system.controller;


import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.system.service.ISysMenuService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

/**
* <p>
* 菜单信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Api(tags = "system", description = "系统权限接口")
@RestController
@RequestMapping("/menu")
public class SysMenuController {
@Autowired
private ISysMenuService sysMenuService;

@ApiOperation("菜单列表接口")
@GetMapping("/list")
public Result list(@RequestBody SysMenuREQ req) {
return sysMenuService.queryList(req);
}

}

测试

发送POST请求:http://127.0.0.1:8003/system/menu/search

删除菜单

需求分析

删除操作将当前节点菜单与子节点都删除

服务层

编辑blog-system/src/main/java/com/acaiblog/system/service/ISysMenuService.java添加deleteById接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysMenu;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 菜单信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysMenuService extends IService<SysMenu> {

/**
* 根据ID删除菜单
* @param id
* @return
*/
Result deleteById(String id);

}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysMenuServiceImpl.java添加deleteById接口具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysMenu;
import com.acaiblog.system.mapper.SysMenuMapper;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.system.service.ISysMenuService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

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

/**
* <p>
* 菜单信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements ISysMenuService {

@Override
public Result deleteById(String id) {
// 查询菜单
SysMenu menuToDelete = baseMapper.selectById(id);

if (menuToDelete == null) {
// 如果菜单不存在,可以根据实际需求处理
return Result.error("菜单不存在");
}

// 删除 parent_id = id 的子资源
LambdaQueryWrapper<SysMenu> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(SysMenu::getParentId, id);
baseMapper.delete(lambdaQueryWrapper);

// 删除指定 id 的菜单
baseMapper.deleteById(id);

return Result.ok();
}

}

控制层

编辑blog-system/src/main/java/com/acaiblog/system/controller/SysMenuController.java实现删除菜单API接口

package com.acaiblog.system.controller;


import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.system.service.ISysMenuService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 菜单信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Api(tags = "system", description = "系统权限接口")
@RestController
@RequestMapping("/menu")
public class SysMenuController {
@Autowired
private ISysMenuService sysMenuService;

@ApiOperation("根据ID删除菜单接口")
@DeleteMapping("/{id}")
@ApiImplicitParam(name = "id", value = "菜单ID", required = true)
public Result deleteById(@PathVariable("id") String id) {
return sysMenuService.deleteById(id);
}
}

测试

发送DELETE请求:http://127.0.0.1:8003/system/menu/1

菜单增、查、改接口

需求分析

通过新增、Id查询、修改菜单逻辑都可以通过MyBatis-plus默认提供的方法直接使用。

控制层

编辑blog-system/src/main/java/com/acaiblog/system/controller/SysMenuController.java添加增、查、改API接口

package com.acaiblog.system.controller;


import com.acaiblog.entities.SysMenu;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.system.service.ISysMenuService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.bouncycastle.pqc.crypto.newhope.NHOtherInfoGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

/**
* <p>
* 菜单信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Api(tags = "system", description = "系统权限接口")
@RestController
@RequestMapping("/menu")
public class SysMenuController {
@Autowired
private ISysMenuService sysMenuService;

@ApiOperation("新增菜单接口")
@PostMapping
public Result save(@RequestBody SysMenu sysMenu) {
sysMenuService.save(sysMenu);
return Result.ok();
}

@ApiOperation("查询菜单详情接口")
@GetMapping("/{id}")
@ApiImplicitParam(name = "id", value = "菜单ID", required = true)
public Result view(@PathVariable("id") String id) {
return Result.ok(sysMenuService.getById(id));
}

@ApiOperation("修改菜单接口")
@PutMapping
public Result update(@RequestBody SysMenu sysMenu) {
sysMenu.setUpdateDate(new Date());
return Result.ok(sysMenuService.updateById(sysMenu));
}
}

角色管理

列表接口

需求分析

角色列表有查询和分页功能

请求类

创建com.acaiblog.system.req.SysRoleREQ将查询条件角色名称封装成一个SysRoleREQ对象

package com.acaiblog.system.req;

import com.acaiblog.entities.SysRole;
import com.acaiblog.util.base.BaseRequest;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors
@ApiModel(value = "SysRoleREQ对象", description = "角色查询条件")
public class SysRoleREQ extends BaseRequest<SysRole> {
@ApiModelProperty(value = "角色名称")
private String name;
}

服务层

编辑blog-system/src/main/java/com/acaiblog/system/service/ISysRoleService.java添加分页查询接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysRole;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 角色信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysRoleService extends IService<SysRole> {
/**
* 角色列表分页查询
* @param req
* @return
*/
Result queryPage(SysRoleREQ req);

}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysRoleServiceImpl.java添加queryPage接口具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysRole;
import com.acaiblog.system.mapper.SysRoleMapper;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.system.req.SysRoleREQ;
import com.acaiblog.system.service.ISysRoleService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

/**
* <p>
* 角色信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements ISysRoleService {

@Override
public Result queryPage(SysRoleREQ req) {
QueryWrapper<SysRole> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotEmpty(req.getName())) {
queryWrapper.like("name", req.getName());
}
queryWrapper.orderByDesc("update_date");
return Result.ok(baseMapper.selectPage(req.getPage(), queryWrapper));
}
}

控制层

编辑blog-system/src/main/java/com/acaiblog/system/controller/SysRoleController.java实现条件查询角色列表API接口

package com.acaiblog.system.controller;


import com.acaiblog.system.req.SysRoleREQ;
import com.acaiblog.system.service.ISysRoleService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

/**
* <p>
* 角色信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Api(tags = "Role", description = "角色管理接口")
@RestController
@RequestMapping("/role")
public class SysRoleController {
@Autowired
ISysRoleService sysRoleService;

@ApiOperation("根据角色名称查询列表接口")
@PostMapping("/search")
public Result search(@RequestBody SysRoleREQ req) {
return sysRoleService.queryPage(req);
}
}

测试

发送POST请求:http://127.0.0.1:8003/system/role/search

增、查、改接口

需求分析

通过新增、ID查询、修改角色逻辑都可以通过MyBatis-plus默认提供的方法直接使用。

控制层

编辑blog-system/src/main/java/com/acaiblog/system/controller/SysRoleController.java实现角色增、查、改API接口

package com.acaiblog.system.controller;


import com.acaiblog.entities.SysRole;
import com.acaiblog.system.req.SysRoleREQ;
import com.acaiblog.system.service.ISysRoleService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

/**
* <p>
* 角色信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Api(tags = "Role", description = "角色管理接口")
@RestController
@RequestMapping("/role")
public class SysRoleController {
@Autowired
ISysRoleService sysRoleService;

@ApiOperation("新增角色API接口")
@PostMapping
public Result save(@RequestBody SysRole sysRole) {
sysRoleService.save(sysRole);
return Result.ok("新增角色成功");
}

@ApiOperation("查询角色详情接口")
@PostMapping("/{id}")
@ApiImplicitParam(name = "id", value = "角色ID", required = true)
public Result view(@PathVariable("id") String id) {
return Result.ok(sysRoleService.getById(id));
}

@ApiOperation("修改角色接口")
@PutMapping
public Result update(@RequestBody SysRole sysRole) {
sysRole.setUpdateDate(new Date());
sysRoleService.updateById(sysRole);
return Result.ok();
}
}

删除接口

需求分析

  1. 通过角色id删除角色信息表数据sys_role
  2. 通过角色id删除角色菜单关系表数据sys_role_menu

数据访问层

编辑blog-system/src/main/java/com/acaiblog/system/mapper/SysRoleMapper.java添加deleteRoleMenuByRoleId接口方法

package com.acaiblog.system.mapper;

import com.acaiblog.entities.SysRole;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

/**
* <p>
* 角色信息表 Mapper 接口
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface SysRoleMapper extends BaseMapper<SysRole> {
boolean deleteRoleMenuByRoleId(@Param("roleId") String roleId);
}

编辑blog-system/src/main/java/com/acaiblog/system/mapper/xml/SysRoleMapper.xml添加sql实现

<?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.acaiblog.system.mapper.SysRoleMapper">
<delete id="deleteRoelMenuByRoleId">
DELETE FROM sys_role_menu WHERE role_id = #{roleId}
</delete>
</mapper>

业务层

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysRoleServiceImpl.java添加deleteById接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysRole;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.system.req.SysRoleREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 角色信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysRoleService extends IService<SysRole> {

/**
* 根据ID删除角色
* @param id
* @return
*/
Result deleteById(String id);
}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysRoleServiceImpl.java添加deleteById接口的具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysRole;
import com.acaiblog.system.mapper.SysRoleMapper;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.system.req.SysRoleREQ;
import com.acaiblog.system.service.ISysRoleService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

/**
* <p>
* 角色信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements ISysRoleService {

@Override
public Result deleteById(String id) {
// 通过角色 id 删除角色信息表数据
baseMapper.deleteById(id);
// 通过角色 id 删除角色菜单关系表数据
baseMapper.deleteRoleMenuByRoleId(id);
return Result.ok();
}
}

控制层

编辑blog-system/src/main/java/com/acaiblog/system/controller/SysRoleController.java添加删除角色API接口

package com.acaiblog.system.controller;


import com.acaiblog.entities.SysRole;
import com.acaiblog.system.req.SysRoleREQ;
import com.acaiblog.system.service.ISysRoleService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

/**
* <p>
* 角色信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Api(tags = "Role", description = "角色管理接口")
@RestController
@RequestMapping("/role")
public class SysRoleController {
@Autowired
ISysRoleService sysRoleService;

@ApiOperation("删除角色接口")
@DeleteMapping("/{id}")
@ApiImplicitParam(name = "id", value = "角色ID", required = true)
public Result deleteById(@PathVariable("id") String id) {
return sysRoleService.deleteById(id);
}
}

测试

发送DELTE请求:http://127.0.0.1:8003/system/role/1

查询角色拥有的菜单

需求分析

根据角色id查询此角色拥有的权限菜单ids,涉及表sys_rolesys_role_menu
新增角色菜单权限数据到sys_role_menu,但是要先删除此表数据,然后再新增。

数据访问层

编辑blog-system/src/main/java/com/acaiblog/system/mapper/SysRoleMapper.java添加findMenuIdsById方法

package com.acaiblog.system.mapper;

import com.acaiblog.entities.SysRole;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
* <p>
* 角色信息表 Mapper 接口
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface SysRoleMapper extends BaseMapper<SysRole> {

/**
* 根据角色id查询此角色拥有的权限菜单 ids
* @param id
* @return
*/
List<String> findMenuIdsById(@Param("id") String id);
}

编辑blog-system/src/main/java/com/acaiblog/system/mapper/xml/SysRoleMapper.xml添加sql实现

<?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.acaiblog.system.mapper.SysRoleMapper">
<select id="findMenuIdsById" resultType="String">
SELECT menu_id FROM sys_role_menu WHERE role_id = #{id}
</select>
</mapper>

业务层

编辑blog-system/src/main/java/com/acaiblog/system/service/ISysRoleService.java添加findMenuIdsById接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysRole;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.system.req.SysRoleREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 角色信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysRoleService extends IService<SysRole> {

/**
* 根据角色id查询此角色拥有的权限菜单 ids
* @param id
* @return
*/
Result findMenuIdsById(String id);
}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysRoleServiceImpl.java添加findMenuIdsById接口具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysRole;
import com.acaiblog.system.mapper.SysRoleMapper;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.system.req.SysRoleREQ;
import com.acaiblog.system.service.ISysRoleService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

/**
* <p>
* 角色信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements ISysRoleService {

@Override
public Result findMenuIdsById(String id) {
return Result.ok(baseMapper.findMenuIdsById(id));
}
}

控制层

编辑blog-system/src/main/java/com/acaiblog/system/controller/SysRoleController.java实现查询角色拥有的菜单API接口

package com.acaiblog.system.controller;


import com.acaiblog.entities.SysRole;
import com.acaiblog.system.req.SysRoleREQ;
import com.acaiblog.system.service.ISysRoleService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

/**
* <p>
* 角色信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Api(tags = "Role", description = "角色管理接口")
@RestController
@RequestMapping("/role")
public class SysRoleController {
@Autowired
ISysRoleService sysRoleService;

@ApiOperation("查询角色拥有的菜单API接口")
@GetMapping("/{id}")
@ApiImplicitParam(name = "id", value = "角色ID", required = true)
public Result findMenuIdsById(@PathVariable("id") String id) {
return sysRoleService.findMenuIdsById(id);
}
}

测试

发送GET请求:http://127.0.0.1:8003/system/role/1

新增角色关系数据

需求分析

  1. 根据角色id查询此角色拥有的权限菜单ids,涉及表sys_rolesys_role_menu
  2. 新增角色菜单权限数据到sys_role_menu,但是要先删除此表数据,然后再新增。

数据访问层

编辑blog-system/src/main/java/com/acaiblog/system/mapper/SysRoleMapper.java添加保存角色菜单接口

package com.acaiblog.system.mapper;

import com.acaiblog.entities.SysRole;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
* <p>
* 角色信息表 Mapper 接口
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface SysRoleMapper extends BaseMapper<SysRole> {

/**
* 新增角色菜单权限数据到 sys_role_menu
* @param roleId 角色id
* @param menuIds 菜单ids集合
* @return
*/
boolean saveRoleMenu(@Param("roleId") String roleId, @Param("menuIds") List<String> menuIds);

}

编辑blog-system/src/main/java/com/acaiblog/system/mapper/xml/SysRoleMapper.xml添加sql实现

<?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.acaiblog.system.mapper.SysRoleMapper">
<insert id="saveRoleMenu">
INSERT INTO sys_role_menu(id, role_id, menu_id) VALUES
<foreach collection="menuIds" item="item" index="index" separator=",">
(
'${@com.baomidou.mybatisplus.core.toolkit.IdWorker@getId()}', #{roleId}, #{item}
)
</foreach>
</insert>
</mapper>

业务层

编辑blog-system/src/main/java/com/acaiblog/system/service/ISysRoleService.java添加saveRoleMenu接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysRole;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.system.req.SysRoleREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.List;

/**
* <p>
* 角色信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysRoleService extends IService<SysRole> {

/**
* 新增角色菜单权限数据到 sys_role_menu
* @param roleId 角色id
* @param menuIds 菜单ids集合
* @return
*/
Result saveRoleMenu(String roleId, List<String> menuIds);
}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysRoleServiceImpl.java添加saveRoleMenu接口具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysRole;
import com.acaiblog.system.mapper.SysRoleMapper;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.system.req.SysRoleREQ;
import com.acaiblog.system.service.ISysRoleService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.List;

/**
* <p>
* 角色信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements ISysRoleService {

@Override
public Result saveRoleMenu(String roleId, List<String> menuIds) {
// 先删除角色菜单关系表数据
baseMapper.deleteRoleMenuByRoleId(roleId);
// 再保存新的角色菜单关系表数据
if (StringUtils.isNotEmpty(menuIds.toString())) {
baseMapper.saveRoleMenu(roleId, menuIds);
}
return Result.ok("新增角色菜单权限数据成功");
}

}

控制层

编辑blog-system/src/main/java/com/acaiblog/system/controller/SysRoleController.java添加新增角色菜单权限数据API接口

package com.acaiblog.system.controller;


import com.acaiblog.entities.SysRole;
import com.acaiblog.system.req.SysRoleREQ;
import com.acaiblog.system.service.ISysRoleService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Date;
import java.util.List;

/**
* <p>
* 角色信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Api(tags = "Role", description = "角色管理接口")
@RestController
@RequestMapping("/role")
public class SysRoleController {
@Autowired
ISysRoleService sysRoleService;

// allowMultiple=true 表示数组格式的参数,dataType="String" 表示数组中参数的类型
@ApiOperation("新增角色菜单权限数据接口")
@PostMapping("/{id}/menu/save")
@ApiImplicitParams({
@ApiImplicitParam(name = "roleId", value = "角色ID", required = true),
@ApiImplicitParam(name = "menuIds", value = "菜单列表", allowMultiple = true, dataType = "String", required = true)
})
public Result saveRoleMenu(@PathVariable("roleId") String roleId, @RequestBody List<String> menuIds) {
return sysRoleService.saveRoleMenu(roleId,menuIds);
}
}

测试

发送POST请求:http://127.0.0.1:8003/system/role/1/menu/save

用户管理

列表接口

请求类

将查询条件角色名称封装成一个SysUserREQ对象,创建com.acaiblog.system.req.SysUserREQ

package com.acaiblog.system.req;

import com.acaiblog.entities.SysUser;
import com.acaiblog.util.base.BaseRequest;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
@ApiModel(value = "SysUserREQ对象", description = "用户查询条件")
public class SysUserREQ extends BaseRequest<SysUser> {
@ApiModelProperty(value = "用户名")
private String userName;

@ApiModelProperty(value = "手机号码")
private String mobile;
}

服务层

编辑blog-system/src/main/java/com/acaiblog/system/service/ISysUserService.java添加queryPage接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysUser;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 用户信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysUserService extends IService<SysUser> {
/**
* 分页查询用户列表
* @param req
* @return
*/
Result queryPage(SysUserREQ req);

}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysUserServiceImpl.java添加分页查询用户列表具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysUser;
import com.acaiblog.system.mapper.SysUserMapper;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

/**
* <p>
* 用户信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {

@Override
public Result queryPage(SysUserREQ req) {
QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotEmpty(req.getUserName())) {
queryWrapper.like("username", req.getUserName());
}
if (StringUtils.isNotEmpty(req.getMobile())) {
queryWrapper.like("mobile", req.getMobile());
}
queryWrapper.orderByDesc("update_date");
return Result.ok(baseMapper.selectPage(req.getPage(), queryWrapper));
}
}

控制层

编辑blog-system/src/main/java/com/acaiblog/system/controller/SysUserController.java添加分页查询用户列表API接口

package com.acaiblog.system.controller;


import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

/**
* <p>
* 用户信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Api(tags = "User", description = "用户管理接口")
@RestController
@RequestMapping("/user")
public class SysUserController {
@Autowired
private ISysUserService sysUserService;

@ApiOperation("分页查询用户列表接口")
@PostMapping("/search")
public Result search(@RequestBody SysUserREQ req) {
return sysUserService.queryPage(req);
}

}

测试

发送POST请求:http://127.0.0.1:8003/system/user/search

查询用户角色接口

需求分析

根据用户id查询此用户拥有的角色ids,涉及表sys_usersys_user_role
新增用户角色关系数据到sys_user_role,但是要先删除此表数据,然后再新增。

数据访问层

编辑blog-system/src/main/java/com/acaiblog/system/mapper/SysUserMapper.java添加findRoleIdsById接口

package com.acaiblog.system.mapper;

import com.acaiblog.entities.SysUser;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
* <p>
* 用户信息表 Mapper 接口
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface SysUserMapper extends BaseMapper<SysUser> {

/**
* 根据用户id查询此用户拥有的角色ids
* @param id
* @return
*/
List<String> findRoleIdsById(@Param("id") String id);

}

编辑blog-system/src/main/java/com/acaiblog/system/mapper/xml/SysUserMapper.xml添加findRoleIdsById接口sql实现

<?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.acaiblog.system.mapper.SysUserMapper">
<select id="findRoleIdsById" resultType="String">
SELECT role_id FROM sys_user_role WHERE user_id = #{id}
</select>

</mapper>

业务层

编辑blog-system/src/main/java/com/acaiblog/system/service/ISysUserService.java添加findRoleIdsById接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysUser;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 用户信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysUserService extends IService<SysUser> {

/**
* 根据用户id查询此用户拥有的角色ids
* @param id 用户ID
* @return
*/
Result findRoleIdsById(String id);

}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysUserServiceImpl.java添加findRoleIdsById接口具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysUser;
import com.acaiblog.system.mapper.SysUserMapper;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

/**
* <p>
* 用户信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {

@Override
public Result findRoleIdsById(String id) {
return Result.ok(baseMapper.findRoleIdsById(id));
}
}

控制层

编辑blog-system/src/main/java/com/acaiblog/system/controller/SysUserController.java添加根据用户ID查询用户拥有角色列表

package com.acaiblog.system.controller;


import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 用户信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Api(tags = "User", description = "用户管理接口")
@RestController
@RequestMapping("/user")
public class SysUserController {
@Autowired
private ISysUserService sysUserService;

@ApiOperation("根据用户ID查询用户角色列表")
@PostMapping("/{id}/role/ids")
@ApiImplicitParam(name = "id", value = "用户ID", required = true)
public Result findRoleIdsById(@PathVariable("id") String id) {
return sysUserService.findRoleIdsById(id);
}

}

测试

发送POST请求:http://127.0.0.1:8003/system/user/1/role/ids

新增用户角色接口

需求分析

  1. 根据用户id查询此用户拥有的角色ids,涉及表sys_usersys_user_role
  2. 新增用户角色关系数据到sys_user_role,但是要先删除此表数据,然后再新增。

数据访问层

编辑blog-system/src/main/java/com/acaiblog/system/mapper/SysUserMapper.java添加

package com.acaiblog.system.mapper;

import com.acaiblog.entities.SysUser;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
* <p>
* 用户信息表 Mapper 接口
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface SysUserMapper extends BaseMapper<SysUser> {

/**
* 通过用户 id 删除用户角色关系表数据
* @param id
* @return
*/
boolean deleteUserRoleByUserId(@Param("userId") String id);

/**
* 新增用户角色关系数据到 sys_user_role
* @param userId
* @param roleIds
* @return
*/
boolean saveUserRole(@Param("userId") String userId, @Param("roleIds") List<String> roleIds);

}

编辑blog-system/src/main/java/com/acaiblog/system/mapper/xml/SysUserMapper.xml添加sql实现

<?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.acaiblog.system.mapper.SysUserMapper">
<!-- 通过用户 id 删除用户角色关系表数据-->
<delete id="deleteUserRoleByUserId">
DELETE FROM sys_user_role WHERE user_id = #{userId}
</delete>
<!-- 新增用户角色关系数据-->
<insert id="saveUserRole">
INSERT INTO sys_user_role(id, user_id, role_id) VALUES
<foreach collection="roleIds" item="item" index="index" separator=",">
(
'${@com.baomidou.mybatisplus.core.toolkit.IdWorker@getId()}', #{userId}, #{item}
)
</foreach>
</insert>

</mapper>

业务层

编辑blog-system/src/main/java/com/acaiblog/system/service/ISysUserService.java添加保存用户角色接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysUser;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.List;

/**
* <p>
* 用户信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysUserService extends IService<SysUser> {

/**
* 新增用户角色关系数据到 sys_user_role
* @param userId 用户ID
* @param roleIds 角色ID集合
* @return
*/
Result saveUserRole(String userId, List<String> roleIds);

}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysUserServiceImpl.java添加保存用户角色具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysUser;
import com.acaiblog.system.mapper.SysUserMapper;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.List;

/**
* <p>
* 用户信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {

@Override
public Result saveUserRole(String userId, List<String> roleIds) {
// 先删除用户角色关系表数据
baseMapper.deleteUserRoleByUserId(userId);
// 再保存新的用户角色关系数据
if (StringUtils.isNotEmpty(roleIds.toString())) {
baseMapper.saveUserRole(userId,roleIds);
}else {
return Result.error("用户角色列表不能为空");
}
return Result.ok();
}
}

控制层

编辑blog-system/src/main/java/com/acaiblog/system/controller/SysUserController.java添加保存用户角色API接口

package com.acaiblog.system.controller;


import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
* <p>
* 用户信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Api(tags = "User", description = "用户管理接口")
@RestController
@RequestMapping("/user")
public class SysUserController {
@Autowired
private ISysUserService sysUserService;

@ApiOperation("新增用户角色关系数据接口")
@PostMapping("/{userId}/role/save")
@ApiImplicitParams({
@ApiImplicitParam(name = "userId", value = "用户ID", required = true),
@ApiImplicitParam(name = "roleIds", value = "用户角色集合", required = true,
allowMultiple = true, dataType = "String") // allowMultiple=true 表示数组格式的参数,dataType="String" 表示数组中参数的类型
})
public Result saveUserRole(@PathVariable("userId") String userId, @RequestBody List<String> roleIds) {
return sysUserService.saveUserRole(userId, roleIds);
}

}

测试

发送POST请求:http://127.0.0.1:8003/system/user/1/role/save

删除用户接口

需求分析

假删除,通过用户id将is_enabled 状态更新为0表示删除

业务层

编辑blog-system/src/main/java/com/acaiblog/system/service/ISysUserService.java添加deleteById接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysUser;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.List;

/**
* <p>
* 用户信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysUserService extends IService<SysUser> {

/**
* 通过用户ID删除用户
* @param id
* @return
*/
Result deleteById(String id);

}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysUserServiceImpl.java添加删除用户具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysUser;
import com.acaiblog.system.mapper.SysUserMapper;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;

/**
* <p>
* 用户信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {

@Override
public Result deleteById(String id) {
SysUser sysUser = baseMapper.selectById(id);
if (sysUser == null) {
return Result.error(String.format("用户ID: %s不存在",id));
}
sysUser.setIsEnabled(0); // 0 删除 1可用
sysUser.setUpdateDate(new Date());
baseMapper.updateById(sysUser);
return Result.ok();
}
}

控制层

编辑blog-system/src/main/java/com/acaiblog/system/controller/SysUserController.java添加删除用户API接口

package com.acaiblog.system.controller;


import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
* <p>
* 用户信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Api(tags = "User", description = "用户管理接口")
@RestController
@RequestMapping("/user")
public class SysUserController {
@Autowired
private ISysUserService sysUserService;

@ApiOperation("根据用户ID删除用户")
@DeleteMapping("/{id}")
@ApiImplicitParam(name = "id", value = "用户ID", required = true)
public Result deleteById(@PathVariable("id") String id) {
return sysUserService.deleteById(id);
}

}

测试

发送DELETE请求:http://127.0.0.1:8003/system/user/1

新增、查询用户接口

需求分析

通过新增、ID查询逻辑都可以通过MyBatis-plus默认提供的方法直接使用。针对新增用户的密码进行SpringSecurity提供的crypto模块加密处理。

整合spring-security-crypto

编辑blog-system/pom.xml

<!--        Spring Seucrity加密模块-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>

定义PasswordEncoderConfig配置类,创建com.acaiblog.system.config.PasswordEncoderConfig

package com.acaiblog.system.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

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

控制层

编辑blog-system/src/main/java/com/acaiblog/system/controller/SysUserController.java添加新增、查询用户信息API接口

package com.acaiblog.system.controller;


import com.acaiblog.entities.SysUser;
import com.acaiblog.system.config.PasswordEncoderConfig;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
* <p>
* 用户信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Api(tags = "User", description = "用户管理接口")
@RestController
@RequestMapping("/user")
public class SysUserController {
@Autowired
private ISysUserService sysUserService;

@Autowired
private PasswordEncoder passwordEncoder; // 需要加密的地方注入 PasswordEncoder ,调用 .encode() 方法用于加密,.matches() 方法用于校验密码是否正

@ApiOperation("新增用户信息接口")
@PostMapping
public Result save(@RequestBody SysUser sysUser) {
// 密码加密处理
String password = passwordEncoder.encode(sysUser.getPassword());
sysUser.setPassword(password);
// 新增
sysUserService.save(sysUser);
return Result.ok("添加用户成功");
}

@ApiOperation("根据用户ID查询用户信息")
@GetMapping("/{userId}")
@ApiImplicitParam(name = "userId", value = "用户ID", required = true)
public Result view(@PathVariable("userId") String userid) {
return Result.ok(sysUserService.getById(userid));
}

}

测试

发送POST请求:http://127.0.0.1:8003/system/user

修改用户密码接口

需求分析

  1. 检查原密码是否正确
  2. 提交修改后的新密码

修改密码请求类

创建com.acaiblog.system.req.SysUserCheckPasswordREQ请求类

package com.acaiblog.system.req;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;

import java.io.Serializable;

@Data
@Accessors(chain = true)
@ApiModel("校验密码参数")
public class SysUserCheckPasswordREQ implements Serializable {
@ApiModelProperty(value = "用户ID", required = true)
private String userId;

@ApiModelProperty(value = "旧密码", required = true)
private String oldPassword;
}

创建修改密码请求类com.acaiblog.system.req.SysUserUpdatePasswordREQ,继承SysUserCheckPasswordREQ

package com.acaiblog.system.req;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
@ApiModel("修改密码请求参数")
public class SysUserUpdatePasswordREQ extends SysUserCheckPasswordREQ{
@ApiModelProperty("新密码")
private String newPassword;

@ApiModelProperty("确认密码")
private String repPassword;
}

业务层

编辑blog-system/src/main/java/com/acaiblog/system/service/ISysUserService.java添加检查、修改密码接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysUser;
import com.acaiblog.system.req.SysUserCheckPasswordREQ;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.req.SysUserUpdatePasswordREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.List;

/**
* <p>
* 用户信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysUserService extends IService<SysUser> {

/**
* 检查用户密码
* @param req
* @return
*/
Result checkPassword(SysUserCheckPasswordREQ req);

/**
* 更新用户密码
* @param req
* @return
*/
Result updatePassword(SysUserUpdatePasswordREQ req);

}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysUserServiceImpl.java添加检查、修改用户密码具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysUser;
import com.acaiblog.system.mapper.SysUserMapper;
import com.acaiblog.system.req.SysUserCheckPasswordREQ;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.req.SysUserUpdatePasswordREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;

/**
* <p>
* 用户信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {
@Autowired
private PasswordEncoder passwordEncoder;

@Override
public Result checkPassword(SysUserCheckPasswordREQ req) {
if (StringUtils.isEmpty(req.getUserId())) {
return Result.error("用户ID不能为空");
} else if (StringUtils.isEmpty(req.getOldPassword())) {
return Result.error("原密码不能为空");
}
SysUser sysUser = baseMapper.selectById(req.getUserId());
if (sysUser == null) {
return Result.error("用户不存在");
}
// 校验密码
if (!passwordEncoder.matches(req.getOldPassword(), sysUser.getPassword())) {
return Result.error("原密码校验失败");
}
return Result.ok();
}

@Override
public Result updatePassword(SysUserUpdatePasswordREQ req) {
if (StringUtils.isEmpty(req.getUserId())) {
return Result.error("用户ID不能为空");
} else if (StringUtils.isEmpty(req.getNewPassword())) {
return Result.error("新密码不能为空");
} else if (StringUtils.isEmpty(req.getRepPassword())) {
return Result.error("确认密码不能为空");
} else if (!req.getNewPassword().equals(req.getRepPassword())) {
return Result.error("新密码与确认密码不一致");
}

SysUser sysUser = baseMapper.selectById(req.getUserId());
if (sysUser == null) {
return Result.error("用户不存在");
}
if (StringUtils.isNotEmpty(req.getOldPassword())) {
if (!passwordEncoder.matches(req.getOldPassword(), sysUser.getPassword())) {
return Result.error("原密码校验错误");
}
}
sysUser.setPassword(passwordEncoder.encode(req.getNewPassword()));
baseMapper.updateById(sysUser);
return Result.ok();
}
}

控制层

编辑blog-system/src/main/java/com/acaiblog/system/controller/SysUserController.java添加检查、更新用户密码API接口

package com.acaiblog.system.controller;


import com.acaiblog.entities.SysUser;
import com.acaiblog.system.config.PasswordEncoderConfig;
import com.acaiblog.system.req.SysUserCheckPasswordREQ;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.req.SysUserUpdatePasswordREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
* <p>
* 用户信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Api(tags = "User", description = "用户管理接口")
@RestController
@RequestMapping("/user")
public class SysUserController {
@Autowired
private ISysUserService sysUserService;

@Autowired
private PasswordEncoder passwordEncoder; // 需要加密的地方注入 PasswordEncoder ,调用 .encode() 方法用于加密,.matches() 方法用于校验密码是否正确

@ApiOperation("检查用户密码接口")
@PostMapping("/check/password")
public Result checkPassword(@RequestBody SysUserCheckPasswordREQ req) {
return sysUserService.checkPassword(req);
}

@ApiOperation("修改用户密码接口")
@PutMapping("/password")
public Result updatePassword(@RequestBody SysUserUpdatePasswordREQ req) {
return sysUserService.updatePassword(req);
}

}

测试

发送POST请求:http://127.0.0.1:8003/system/user/check/password
发送PUT请求:http://127.0.0.1:8003/system/user/password

修改用户信息接口

需求分析

  1. 判断用户的昵称或头像是否修改,如果被修改则更新文章微服务blog-article和问答微服务blog-question中对应表的用户nick_nameuser_image值。调用文章和问答微服务的远程接口来实现更新。
  2. 更新blog_systemsys_user用户表数据。

文章微服务更新用户信息Feign接口

更新用户请求类

blog-api模块中创建com.acaiblog.feign.req.UserInfoREQ接收系统微服务传递的用户信息

package com.acaiblog.feign.req;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.Serializable;

@Data
@AllArgsConstructor // 有参构造器
@ApiModel("更新用户信息请求类")
public class UserInfoREQ implements Serializable {
@ApiModelProperty(value = "用户ID", required = true)
private String userId;

@ApiModelProperty(value = "用户昵称")
private String nickName;

@ApiModelProperty(value = "用户头像")
private String userImage;
}

数据访问层

在blog-article模块中,编辑blog-article/src/main/java/com/acaiblog/article/mapper/ArticleMapper.java添加updateUserInfo接口

package com.acaiblog.article.mapper;

import com.acaiblog.article.req.ArticleListREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.feign.req.UserInfoREQ;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Param;

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

/**
* <p>
* 文章信息表 Mapper 接口
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface ArticleMapper extends BaseMapper<Article> {

/**
* 更新用户信息
* @param req
* @return
*/
boolean updateUserInfo(UserInfoREQ req);
}

编辑blog-article/src/main/java/com/acaiblog/article/mapper/xml/ArticleMapper.xml添加修改用户信息sql实现

<update id="updateUserInfo">
UPDATE article SET nick_name = #{nickName}, user_image = #{userImage}
WHERE user_id = #{userId};
UPDATE comment SET nick_name = #{nickName}, user_image = #{userImage}
WHERE user_id = #{userId};
</update>

mybatis是默认不支持执行多条update,需要在propertes或者yml配置文件中配置的数据库URL后面追加&allowMultiQueries=true

spring:
application:
name: article-server # 应用名称
# 数据源
datasource:
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 123456
url: jdbc:mariadb://localhost:3306/blog?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true
driver-class-name: org.mariadb.jdbc.Driver

业务层

编辑blog-article/src/main/java/com/acaiblog/article/service/IArticleService.java添加更新用户信息接口

package com.acaiblog.article.service;

import com.acaiblog.article.req.ArticleListREQ;
import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.feign.req.UserInfoREQ;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 文章信息表 服务类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
public interface IArticleService extends IService<Article> {

/**
* 更新文章与评论表中的用户信息
* @param req
* @return
*/
Result updateUserInfo(UserInfoREQ req);
}

编辑blog-article/src/main/java/com/acaiblog/article/service/impl/ArticleServiceImpl.java添加更新文章与评论表中用户信息的具体实现

package com.acaiblog.article.service.impl;

import com.acaiblog.article.req.ArticleListREQ;
import com.acaiblog.article.req.ArticleREQ;
import com.acaiblog.article.req.ArticleUserREQ;
import com.acaiblog.entities.Article;
import com.acaiblog.article.mapper.ArticleMapper;
import com.acaiblog.article.service.IArticleService;
import com.acaiblog.feign.req.UserInfoREQ;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ArticleStatusEnum;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;

/**
* <p>
* 文章信息表 服务实现类
* </p>
*
* @author 阿才的博客
* @since 2024-01-05
*/
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements IArticleService {

@Override
public boolean updateUserInfo(UserInfoREQ req) {
return baseMapper.updateUserInfo(req);
}
}

控制层

编辑blog-api/src/main/java/com/acaiblog/feign/IFeignArticleController.java添加API接口

package com.acaiblog.feign;

import com.acaiblog.feign.req.UserInfoREQ;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.List;

// value 指定是哪个微服务接口,
// path 是在 Feign 调用时会加上此前缀,它与接口实现类的微服务中配置的 context-path 值一致,如果微服务中没有配置 context-path 下面就不需要写 path
@FeignClient(value = "article-server", path = "/article")
public interface IFeignArticleController {

@ApiOperation("更新文章表和评论表中的用户信息")
@PutMapping("/feign/article/user")
boolean updateUserInfo(@RequestBody UserInfoREQ req);
}

编辑blog-article/src/main/java/com/acaiblog/article/feign/FeignArticleController.java添加更新用户信息具体实现

package com.acaiblog.article.feign;

import com.acaiblog.article.service.IArticleService;
import com.acaiblog.article.service.ILabelService;
import com.acaiblog.feign.IFeignArticleController;
import com.acaiblog.entities.Label;
import com.acaiblog.feign.req.UserInfoREQ;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Api(tags = "feign-article", description = "文章Feign接口")
@RestController
public class FeignArticleController implements IFeignArticleController {

@Autowired
private IArticleService articleService;

@Override
public boolean updateUserInfo(UserInfoREQ req) {
return articleService.updateUserInfo(req);
}
}

测试

发送PUT请求:http://127.0.0.1:8001/article/feign/article/user

问答微服务更新用户信息接口

数据访问层

编辑blog-question/src/main/java/com/acaiblog/mapper/QuestionMapper.java添加updateUserInfo接口

package com.acaiblog.mapper;

import com.acaiblog.entities.Question;
import com.acaiblog.feign.req.UserInfoREQ;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
* <p>
* 问题信息表 Mapper 接口
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
public interface QuestionMapper extends BaseMapper<Question> {

/**
* 更新问题与回答表中的用户信息
* @param req
* @return
*/
boolean updateUserInfo(UserInfoREQ req);
}

编辑blog-question/src/main/java/com/acaiblog/mapper/xml/QuestionMapper.xml添加更新用户信息sql实现

<update id="updateUserInfo">
UPDATE question SET nick_name = #{nickName}, user_image = #{userImage} WHERE user_id = #{userId}; -- 最后要有分号
UPDATE replay SET nick_name = #{nickName}, user_image = #{userImage} WHERE user_id = #{userId};
</update>

mybatis是默认不支持执行多条update,需要在propertes或者yml配置文件中配置的数据库URL后面追加&allowMultiQueries=true

spring:
application:
name: article-server # 应用名称
# 数据源
datasource:
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 123456
url: jdbc:mariadb://localhost:3306/blog?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true
driver-class-name: org.mariadb.jdbc.Driver

业务层

编辑blog-question/src/main/java/com/acaiblog/service/IQuestionService.java添加updateUserInfo接口

package com.acaiblog.service;

import com.acaiblog.feign.req.UserInfoREQ;
import com.acaiblog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 问题信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
public interface IQuestionService extends IService<Question> {

/**
* 更新问题与回答表中的用户信息
* @param req
* @return
*/
boolean updateUserInfo(UserInfoREQ req);
}

编辑blog-question/src/main/java/com/acaiblog/service/impl/QuestionServiceImpl.java添加更新问题与回答表中的用户信息具体实现

package com.acaiblog.service.impl;

import com.acaiblog.entities.Label;
import com.acaiblog.feign.IFeignArticleController;
import com.acaiblog.feign.req.UserInfoREQ;
import com.acaiblog.req.QuestionUserREQ;
import com.acaiblog.entities.Question;
import com.acaiblog.mapper.QuestionMapper;
import com.acaiblog.service.IQuestionService;
import com.acaiblog.util.base.BaseRequest;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.Date;
import java.util.List;

/**
* <p>
* 问题信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-09
*/
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {
@Autowired
private IFeignArticleController feignArticleController;

@Override
public boolean updateUserInfo(UserInfoREQ req) {
return baseMapper.updateUserInfo(req);
}
}

控制层

在blog-api模块中创建com.acaiblog.feign.IFeignQuestionController

package com.acaiblog.feign;

import com.acaiblog.entities.Label;
import com.acaiblog.feign.req.UserInfoREQ;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.List;

@FeignClient(value = "question-server", path = "/question")
public interface IFeignQuestionController {
@ApiOperation("更新问题表和回答表中的用户信息")
@PutMapping("/feign/question/user")
boolean updateUserInfo(@RequestBody UserInfoREQ req);
}

在blog-question模块下创建com.acaiblog.feign.FeignQuestionController

package com.acaiblog.feign;

import com.acaiblog.feign.req.UserInfoREQ;
import com.acaiblog.service.IQuestionService;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "feign-question", description = "被远程调用的问题微服务接口")
@RestController
public class FeignQuestionController implements IFeignQuestionController{
@Autowired
private IQuestionService questionService;

@Override
public boolean updateUserInfo(UserInfoREQ req) {
return questionService.updateUserInfo(req);
}
}

业务层

编辑blog-system/src/main/java/com/acaiblog/system/service/ISysUserService.java添加update接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysUser;
import com.acaiblog.system.req.SysUserCheckPasswordREQ;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.req.SysUserUpdatePasswordREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.List;

/**
* <p>
* 用户信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysUserService extends IService<SysUser> {

/**
* 更新用户信息
* @param sysUser
* @return
*/
Result update(SysUser sysUser);

}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysUserServiceImpl.java添加更新用户具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysUser;
import com.acaiblog.feign.IFeignArticleController;
import com.acaiblog.feign.IFeignQuestionController;
import com.acaiblog.feign.req.UserInfoREQ;
import com.acaiblog.system.mapper.SysUserMapper;
import com.acaiblog.system.req.SysUserCheckPasswordREQ;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.req.SysUserUpdatePasswordREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;

/**
* <p>
* 用户信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {
@Autowired
private PasswordEncoder passwordEncoder;

@Autowired
private IFeignArticleController feignArticleController;

@Autowired
private IFeignQuestionController feignQuestionController;

@Override
public Result update(SysUser sysUser) {
// 查询用户信息
SysUser user = baseMapper.selectById(sysUser.getId());
if (user == null) {
return Result.error("用户不存在");
}
// 判断更新的信息中昵称和头像是否被改变
if (!StringUtils.equals(sysUser.getNickName(), user.getNickName())) {
UserInfoREQ req = new UserInfoREQ(sysUser.getId(),sysUser.getNickName(), sysUser.getImageUrl());
feignArticleController.updateUserInfo(req);
feignQuestionController.updateUserInfo(req);
}
sysUser.setUpdateDate(new Date());
baseMapper.updateById(sysUser);
return Result.ok();
}
}

控制层

编辑blog-system/src/main/java/com/acaiblog/system/controller/SysUserController.java添加更新用户信息API接口

package com.acaiblog.system.controller;


import com.acaiblog.entities.SysUser;
import com.acaiblog.system.config.PasswordEncoderConfig;
import com.acaiblog.system.req.SysUserCheckPasswordREQ;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.req.SysUserUpdatePasswordREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
* <p>
* 用户信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Api(tags = "User", description = "用户管理接口")
@RestController
@RequestMapping("/user")
public class SysUserController {
@Autowired
private ISysUserService sysUserService;

@Autowired
private PasswordEncoder passwordEncoder; // 需要加密的地方注入 PasswordEncoder ,调用 .encode() 方法用于加密,.matches() 方法用于校验密码是否正确

@ApiOperation("更新用户信息接口")
@PutMapping
public Result update(@RequestBody SysUser sysUser) {
return sysUserService.update(sysUser);
}
}

重写启动类

编辑blog-system/src/main/java/com/acaiblog/SystemApplication.java启动类添加@EnableFeignClients注解

package com.acaiblog;

import com.spring4all.swagger.EnableSwagger2Doc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients
@EnableDiscoveryClient
@EnableSwagger2Doc
@SpringBootApplication
public class SystemApplication {
public static void main(String[] args) {
SpringApplication.run(SystemApplication.class, args);
System.out.println(String.format("API UI: http://127.0.0.1:8003/system/swagger-ui.html#/"));
}
}

统计用户数接口

业务层

编辑blog-system/src/main/java/com/acaiblog/system/service/ISysUserService.java添加getUserTotal接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysUser;
import com.acaiblog.system.req.SysUserCheckPasswordREQ;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.req.SysUserUpdatePasswordREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.List;

/**
* <p>
* 用户信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysUserService extends IService<SysUser> {

/**
* 统计用户数
* @return
*/
Result getUserTotal();

}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysUserServiceImpl.java添加查询统计用户数具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysUser;
import com.acaiblog.feign.IFeignArticleController;
import com.acaiblog.feign.IFeignQuestionController;
import com.acaiblog.feign.req.UserInfoREQ;
import com.acaiblog.system.mapper.SysUserMapper;
import com.acaiblog.system.req.SysUserCheckPasswordREQ;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.req.SysUserUpdatePasswordREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;

/**
* <p>
* 用户信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {
@Autowired
private PasswordEncoder passwordEncoder;

@Autowired
private IFeignArticleController feignArticleController;

@Autowired
private IFeignQuestionController feignQuestionController;

@Override
public Result getUserTotal() {
QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
// 帐户是否可用(1 可用,0 删除用户)
queryWrapper.eq("is_enabled", 1);
Integer total = baseMapper.selectCount(queryWrapper);
return Result.ok(total);
}
}

控制层

编辑blog-system/src/main/java/com/acaiblog/system/controller/SysUserController.java添加统计用户数API接口

package com.acaiblog.system.controller;


import com.acaiblog.entities.SysUser;
import com.acaiblog.system.config.PasswordEncoderConfig;
import com.acaiblog.system.req.SysUserCheckPasswordREQ;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.req.SysUserUpdatePasswordREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
* <p>
* 用户信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Api(tags = "User", description = "用户管理接口")
@RestController
@RequestMapping("/user")
public class SysUserController {
@Autowired
private ISysUserService sysUserService;

@Autowired
private PasswordEncoder passwordEncoder; // 需要加密的地方注入 PasswordEncoder ,调用 .encode() 方法用于加密,.matches() 方法用于校验密码是否正确

@ApiOperation("统计用户数接口")
@GetMapping("/total")
public Result getUserTotal() {
return sysUserService.getUserTotal();
}

}

测试

发送GET请求:http://127.0.0.1:8003/system/user/total

获取用户菜单权限接口

需求分析

通过用户ID查询出这个用户拥有的权限菜单,查询后过滤出目录和菜单类型数据,不要按钮。因为用于当用户登录后获取用户菜单,来渲染后台管理系统的左侧导航菜单。涉及表sys_usersys_user_rolesys_rolesys_role_menusys_menu

数据访问层

编辑blog-system/src/main/java/com/acaiblog/system/service/ISysMenuService.java添加findByUserId接口

package com.acaiblog.system.mapper;

import com.acaiblog.entities.SysMenu;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
* <p>
* 菜单信息表 Mapper 接口
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface SysMenuMapper extends BaseMapper<SysMenu> {

/**
* 查询指定用户ID的所拥有的权限(目录、菜单、按钮)
* @param userId
* @return
*/
List<SysMenu> findByUserId(@Param("userId") String userId);

}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysMenuServiceImpl.java添加findByUserId接口sql实现

<?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.acaiblog.system.mapper.SysMenuMapper">
<select id="findByUserId" resultType="SysMenu">
SELECT DISTINCT
m.id,
m.parent_id,
m.name,
m.url,
m.type,
m.code,
m.icon,
m.sort,
m.remark,
m.create_date,
m.update_date
FROM
sys_user AS u
LEFT JOIN sys_user_role AS ur ON u.id = ur.user_id LEFT JOIN sys_role AS r ON ur.role_id = r.id
LEFT JOIN sys_role_menu AS rm ON rm.role_id = r.id LEFT JOIN sys_menu AS m ON rm.menu_id = m.id
WHERE
u.id = #{userId}
</select>

</mapper>

业务层

编辑blog-system/src/main/java/com/acaiblog/system/service/ISysMenuService.java添加findUserMenuTree接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysMenu;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
* <p>
* 菜单信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysMenuService extends IService<SysMenu> {

/**
* 通过用户id查询拥有的权限菜单树
* @param id
* @return
*/
Result findUserMenuTree(String id);

}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysMenuServiceImpl.java添加findUserMenuTree接口具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysMenu;
import com.acaiblog.system.mapper.SysMenuMapper;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.system.service.ISysMenuService;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ResultEnum;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.Lists;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.*;

/**
* <p>
* 菜单信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements ISysMenuService {

@Override
public Result findUserMenuTree(String id) {
// 通过用户id查询出拥有的权限(目录、菜单、按钮)
List<SysMenu> menuList = baseMapper.findByUserId(id);
// 当userId不存在时,menuList 是空的;当存在此用户,但是没有分配权限时会有一条空记录
if (CollectionUtils.isNotEmpty(menuList) || menuList.get(0) == null) {
// 没有权限
return Result.build(ResultEnum.MENU_NO);
}
// 获取权限集合中所有的目录、菜单
List<SysMenu> dirMenuList = Lists.newArrayList();
// 封装权限按钮标识符
List<String> buttonList = Lists.newArrayList();
for (SysMenu menu: menuList) {
if (menu.getType().equals(1) || menu.getType().equals(2)) {
dirMenuList.add(menu);
} else {
// button权限
buttonList.add(menu.getCode());
}
}
// 封装树状菜单
List<SysMenu> menuTreeList = buildTree(dirMenuList);
// 封装响应数据
Map<String, Object> data = new HashMap<>();
data.put("menuTreeList", menuTreeList);
data.put("buttonList", buttonList);
return Result.ok(menuTreeList);
}
}

控制层

编辑blog-system/src/main/java/com/acaiblog/system/controller/SysMenuController.java添加查询用户菜单权限数API接口

package com.acaiblog.system.controller;


import com.acaiblog.entities.SysMenu;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.system.service.ISysMenuService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.bouncycastle.pqc.crypto.newhope.NHOtherInfoGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

/**
* <p>
* 菜单信息表 前端控制器
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Api(tags = "Menu", description = "菜单管理接口")
@RestController
@RequestMapping("/menu")
public class SysMenuController {
@Autowired
private ISysMenuService sysMenuService;

@ApiOperation("查询用户菜单权限接口")
@GetMapping("/user/{userId}")
@ApiImplicitParam(name = "userId", value = "用户ID", required = true)
public Result findUserMenuTree(@PathVariable("userId") String id) {
return sysMenuService.findUserMenuTree(id);
}
}

测试

发送GET请求:http://127.0.0.1:8003/system/menu/user/14

API-注册用户接口

校验用户

编辑blog-system/src/main/java/com/acaiblog/system/service/ISysUserService.java添加checkUsername接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysUser;
import com.acaiblog.system.req.SysUserCheckPasswordREQ;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.req.SysUserUpdatePasswordREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.List;

/**
* <p>
* 用户信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysUserService extends IService<SysUser> {

/**
* 校验用户名
* @param username
* @return
*/
Result checkUsername(String username);

}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysUserServiceImpl.java添加校验用户名具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysUser;
import com.acaiblog.feign.IFeignArticleController;
import com.acaiblog.feign.IFeignQuestionController;
import com.acaiblog.feign.req.UserInfoREQ;
import com.acaiblog.system.mapper.SysUserMapper;
import com.acaiblog.system.req.SysUserCheckPasswordREQ;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.req.SysUserUpdatePasswordREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;

/**
* <p>
* 用户信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {
@Autowired
private PasswordEncoder passwordEncoder;

@Autowired
private IFeignArticleController feignArticleController;

@Autowired
private IFeignQuestionController feignQuestionController;

@Override
public Result checkUsername(String username) {
QueryWrapper<SysUser> sysUserQueryWrapper = new QueryWrapper<>();
sysUserQueryWrapper.eq("username", username);
SysUser sysUser = baseMapper.selectOne(sysUserQueryWrapper);
// 查询到则存在,存在 data=true 已被注册,不存在 data=false 未被注册
return Result.ok(sysUser == null ? false:true);
}
}

创建公开API接口类com.acaiblog.system.api.ApiSysUserController接收校验用户名请求

package com.acaiblog.system.api;

import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "api-system", description = "用户管理接口")
@RestController
@RequestMapping("/api/user")
public class ApiSysUserController {
@Autowired
private ISysUserService sysUserService;

@ApiOperation("校验用户名是否存在")
@GetMapping("/username/{username}")
@ApiImplicitParam(name = "username", value = "用户名称", required = true)
public Result checkUsername(@PathVariable("username") String username) {
return sysUserService.checkUsername(username);
}
}

测试:发送GET请求:http://127.0.0.1:8003/system/api/user/username/acai

提交注册信息

创建注册信息请求类com.acaiblog.system.req.RegisterREQ

package com.acaiblog.system.req;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;

@Data
@ApiModel("注册用户信息请求类")
public class RegisterREQ implements Serializable {
@ApiModelProperty("用户名称")
private String username;

@ApiModelProperty("用户密码")
private String password;

@ApiModelProperty("确认密码")
private String repPassword;
}

编辑blog-system/src/main/java/com/acaiblog/system/service/ISysUserService.java添加register接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysUser;
import com.acaiblog.system.req.RegisterREQ;
import com.acaiblog.system.req.SysUserCheckPasswordREQ;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.req.SysUserUpdatePasswordREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.List;

/**
* <p>
* 用户信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysUserService extends IService<SysUser> {

/**
* 提交用户注册信息
* @param req
* @return
*/
Result register(RegisterREQ req);

}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysUserServiceImpl.java添加register接口具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysUser;
import com.acaiblog.feign.IFeignArticleController;
import com.acaiblog.feign.IFeignQuestionController;
import com.acaiblog.feign.req.UserInfoREQ;
import com.acaiblog.system.mapper.SysUserMapper;
import com.acaiblog.system.req.RegisterREQ;
import com.acaiblog.system.req.SysUserCheckPasswordREQ;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.req.SysUserUpdatePasswordREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;

/**
* <p>
* 用户信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {
@Autowired
private PasswordEncoder passwordEncoder;

@Autowired
private IFeignArticleController feignArticleController;

@Autowired
private IFeignQuestionController feignQuestionController;

@Override
public Result register(RegisterREQ req) {
if(StringUtils.isEmpty(req.getUsername())) {
return Result.error("用户名不能为空,请重试");
} else if(StringUtils.isEmpty(req.getPassword())) {
return Result.error("密码不能为空,请重试");
} else if(StringUtils.isEmpty(req.getRepPassword())) {
return Result.error("确认密码不能为空,请重试");
} else if( !StringUtils.equals(req.getPassword(), req.getRepPassword())) {
return Result.error("两次输入的密码不一致");
}
Result result = this.checkUsername(req.getUsername());
if( (Boolean) result.getData() ) {
return Result.error("用户已被注册,请更换个用户名");
}
// 校验都通过,新增用户信息
SysUser sysUser = new SysUser();
sysUser.setUsername( req.getUsername() );
// 默认昵称和用户名一样
sysUser.setNickName( req.getUsername() );
sysUser.setPassword( passwordEncoder.encode(req.getPassword()) ); // 提交用户信息
this.save(sysUser);
return Result.ok();
}
}

编辑blog-system/src/main/java/com/acaiblog/system/api/ApiSysUserController.java添加注册用户信息API接口

package com.acaiblog.system.api;

import com.acaiblog.system.req.RegisterREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@Api(tags = "api-system", description = "用户管理接口")
@RestController
@RequestMapping("/api/user")
public class ApiSysUserController {
@Autowired
private ISysUserService sysUserService;

@ApiOperation("注册用户接口")
@PostMapping("/register")
public Result register(@RequestBody RegisterREQ req) {
return sysUserService.register(req);
}
}

前端对接后端API接口

单点登录注册功能

修改请求协议内容api方法: src\api\auth.js

export function getProtocol(){
return request({
// url: `${window.location.href}/protocol.html`, // 访问的是public/protocol.html
url: `${window.location.protocol}//${window.location.host}/xieyi.html`,
method: 'get'
})
}

编辑vue.config.js,以/dev-api/system开头的请求代理到 http://localhost:8003

module.exports = {
devServer: {
port: 8080, // 端口号,如果端口号被占用,会自动提升1
host: "localhost", //主机名
https: false, //协议
open: true, //启动服务时自动打开浏览器访问
proxy: { // 开发环境代理配置
[process.env.VUE_APP_BASE_API + '/system']: {
target: 'http://localhost:8003',
changeOrigin: true,
pathRewrite: {
[ '^' + process.env.VUE_APP_BASE_API ]: ''
}
},
// '/dev-api': {
[process.env.VUE_APP_BASE_API] :{
// 目标服务器地址
target: process.env.VUE_APP_SERVICE_URL,
changeOrigin: true, // 开启代理服务器,
pathRewrite: {
// 将 请求地址前缀 /dev-api 替换为 空的,
// '^/dev-api': '',
[ '^' + process.env.VUE_APP_BASE_API]: ''
},
}
}
},
lintOnSave: false, // 关闭格式检查
productionSourceMap: false, // 打包时不会生成 .map 文件,加快打包速度

}

测试注册用户,查看数据库是否新增用户信息

权限管理系统

修改前端部分blog-admin\vue.config.js中的代理配置如下:以/dev-api/system开头的请求代理到 http://localhost:8003

devServer: {
port: port,
open: false,
overlay: {
warnings: false,
errors: true
},
// before: require('./mock/mock-server.js'),
proxy: {
[process.env.VUE_APP_BASE_API + '/system']: {
target: 'http://localhost:8003',
changeOrigin: true,
pathRewrite: {
[ '^' + process.env.VUE_APP_BASE_API ]: ''
}
},
[process.env.VUE_APP_BASE_API]: {
target: 'http://localhost:7308/mock/6573d745983c2f002b1f684d/blog-admin',
// target: 'http://127.0.0.1:8001',
changeOrigin: true,
pathRewrite: {
['^' + process.env.VUE_APP_BASE_API]: ''
}
}
}
}

Spring Security Oauth2 认证微服务

搭建认证微服务

创建blog-Oauth2子模块

添加pom依赖

<dependencies>
<dependency>
<groupId>com.acaiblog</groupId>
<artifactId>blog-api</artifactId> <version>1.0-SNAPSHOT</version>
</dependency>
<!-- Spring Security、OAuth2 和JWT等 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!--redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>-->
<!-- nacos 客户端 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
<!-- nacos 配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
<!--热部署 ctrl+f9-->
<dependency>
<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <scope>test</scope>
</dependency>
</dependencies>

配置bootstrap.yml

创建blog-Oauth2/src/main/resources/bootstrap.yml

spring:
application:
name: auth-server
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 注册中心地址
config:
server-addr: localhost:8848 # 配置中心地址
file-extension: yml #配置中心配置文件格式
profiles:
active: dev # 指定环境

Nacos创建配置文件

在Nacos控制台创建auth-server-dev.yml

swagger:
description: '系统认证'
server:
port: 8004
servlet:
context-path: /auth # 上下文件路径,请求前缀 ip:port/article
spring:
application:
name: auth-server # 应用名称
cloud:
nacos:
discovery:
server-addr: localhost:8848
# 数据源
datasource:
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 123456
url: jdbc:mariadb://localhost:3306/blog?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true
driver-class-name: org.mariadb.jdbc.Driver
# 数据源其他配置, 在 DruidConfig配置类中手动绑定
initialSize: 8
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
druid:
testWhileIdle: true
validationQuery: SELECT 1 FROM DUAL
mybatis-plus:
type-aliases-package: com.acaiblog.entities
# xxxMapper.xml 路径
mapper-locations: classpath*:com/acaiblog/auth/mapper/**/*.xml
# 日志级别,会打印sql语句
logging:
level:
com.acaiblog.auth.mapper: debug
debug: false

创建启动类

创建com.acaiblog.AuthServerApplication

package com.acaiblog;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class AuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServerApplication.class, args);
System.out.println(String.format("API UI: http://127.0.0.1:8004/system/swagger-ui.html#/"));
}
}

创建UserDetailsService

配置UserDetailsService实现类访问数据查询用户信息:通过请求的用户名去数据库中查询用户信息;通过用户id去获取权限信息。以上两个功能数据都来源于blog-system微服务中,所以我们要先在blog-system定义远程调用接口。

用户信息业务层

blog-system服务中通过用户名去数据库中查询用户信息,编辑blog-system/src/main/java/com/acaiblog/system/service/ISysUserService.java添加查询用户信息接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysUser;
import com.acaiblog.system.req.RegisterREQ;
import com.acaiblog.system.req.SysUserCheckPasswordREQ;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.req.SysUserUpdatePasswordREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.List;

/**
* <p>
* 用户信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysUserService extends IService<SysUser> {

/**
* 通过用户名查询用户信息
* @param username
* @return
*/
SysUser findByUsername(String username);

}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysUserServiceImpl.java添加通过用户名查询用户信息具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysUser;
import com.acaiblog.feign.IFeignArticleController;
import com.acaiblog.feign.IFeignQuestionController;
import com.acaiblog.feign.req.UserInfoREQ;
import com.acaiblog.system.mapper.SysUserMapper;
import com.acaiblog.system.req.RegisterREQ;
import com.acaiblog.system.req.SysUserCheckPasswordREQ;
import com.acaiblog.system.req.SysUserREQ;
import com.acaiblog.system.req.SysUserUpdatePasswordREQ;
import com.acaiblog.system.service.ISysUserService;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;

/**
* <p>
* 用户信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {

@Override
public SysUser findByUsername(String username) {
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
return baseMapper.selectOne(queryWrapper);
}
}

用户权限业务层

blog-system服务中通过用户id去获取权限信息,编辑blog-system/src/main/java/com/acaiblog/system/service/ISysMenuService.java添加查询用户id接口

package com.acaiblog.system.service;

import com.acaiblog.entities.SysMenu;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.util.base.Result;
import com.baomidou.mybatisplus.extension.service.IService;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
* <p>
* 菜单信息表 服务类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
public interface ISysMenuService extends IService<SysMenu> {

/**
* 通过用户id查询用户权限列表
* @param id
* @return
*/
List<SysMenu> findByUserId(String id);

}

编辑blog-system/src/main/java/com/acaiblog/system/service/impl/SysMenuServiceImpl.java添加通过用户id查询用户权限列表具体实现

package com.acaiblog.system.service.impl;

import com.acaiblog.entities.SysMenu;
import com.acaiblog.system.mapper.SysMenuMapper;
import com.acaiblog.system.req.SysMenuREQ;
import com.acaiblog.system.service.ISysMenuService;
import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ResultEnum;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.Lists;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.logging.Logger;

/**
* <p>
* 菜单信息表 服务实现类
* </p>
*
* @author baocai.guo@qq.com
* @since 2024-01-19
*/
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements ISysMenuService {

@Override
public List<SysMenu> findByUserId(String id) {
// 通过用户id查询拥有权限
List<SysMenu> menuList = baseMapper.findByUserId(id);
// 当userId不存在时,menuList 是空的;当存在此用户,但是没有分配权限时会有一条空记录
if (CollectionUtils.isEmpty(menuList) || menuList.get(0) == null) {
return null;
}
return menuList;
}

}

系统管理服务Feign接口

在blog-api模块创建com.acaiblog.feign.IFeignSystemController

package com.acaiblog.feign;

import com.acaiblog.entities.SysMenu;
import com.acaiblog.entities.SysUser;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.List;

// value 指定是哪个微服务接口, path 是在 Feign 调用时会加上此前缀,对应实现类服务的 context-path
@FeignClient(value = "system-server", path = "/system")
public interface IFeignSystemController {
@ApiOperation("通过用户名查询用户信息Feign接口")
@GetMapping("/api/feign/user/{username}")
@ApiImplicitParam(name = "username", value = "用户名称", required = true)
SysUser findUserByUsername(@PathVariable("username") String username);

@ApiOperation("通过用户ID查询用户拥有权限Feign接口")
@GetMapping("/api/feign/menu/{userId}")
@ApiImplicitParam(name = "userId", value = "用户ID", required = true)
List<SysMenu> findMenuByUserId(@PathVariable("userId") String userId);
}

系统管理Feign接口实现类

在blog-system模块创建com.acaiblog.system.feign.FeignSystemController

package com.acaiblog.system.feign;

import com.acaiblog.entities.SysMenu;
import com.acaiblog.entities.SysUser;
import com.acaiblog.feign.IFeignSystemController;
import com.acaiblog.system.service.ISysMenuService;
import com.acaiblog.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class FeignSystemController implements IFeignSystemController {
@Autowired
private ISysUserService sysUserService;

/**
* 通过用户名查询用户信息
* @param username
* @return
*/
@Override
public SysUser findUserByUsername(String username) {
return sysUserService.findByUsername(username);
}

@Autowired
private ISysMenuService sysMenuService;

/**
* 根据用户ID查询用户拥有的权限列表
* @param userId
* @return
*/
@Override
public List<SysMenu> findMenuByUserId(String userId) {
return sysMenuService.findByUserId(userId);
}
}

实现UserDetailsService逻辑

因为UserDetailsService接口中有一个UserDetails loadUserByUsername(String username)抽象方法,它的返回值UserDetails接口,我们要创建一个JwtUser类实现这个接口。
注意:isAccountNonExpired声明了boolean类型,但是在构造器是Integer类型接收,原因是数据库sys_user表中存储的是整型,所以我们然后转boolean,即:this.isAccountNonExpired = isAccountNonExpired == 1 ? true: false; @JSONField(serialize = false) // 忽略转json,因为后面我们要将这个类对象转成json。
在blog-Oauth2模块创建com.acaiblog.auth2.service.JwtUser

package com.acaiblog.auth2.service;

import com.alibaba.fastjson.annotation.JSONField;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.List;

@Data
public class JwtUser implements UserDetails {
@ApiModelProperty(value = "用户ID")
private String uid;

@ApiModelProperty(value = "用户名")
private String username;

@JSONField(serialize = false) // 忽略转json
@ApiModelProperty(value = "密码,加密存储, admin/1234")
private String password;

@ApiModelProperty(value = "昵称")
private String nickName;

@ApiModelProperty(value = "头像url")
private String imageUrl;

@ApiModelProperty(value = "注册手机号")
private String mobile;

@ApiModelProperty(value = "注册邮箱")
private String email;

// 1 true 0 false
@JSONField(serialize = false) // 忽略转json
@ApiModelProperty(value = "帐户是否过期(1 未过期,0已过期)")
private boolean isAccountNonExpired; // 不要写小写 boolean

@JSONField(serialize = false) // 忽略转json
@ApiModelProperty(value = "帐户是否被锁定(1 未过期,0已过期)")
private boolean isAccountNonLocked;

@JSONField(serialize = false) // 忽略转json
@ApiModelProperty(value = "密码是否过期(1 未过期,0已过期)")
private boolean isCredentialsNonExpired;

@JSONField(serialize = false) // 忽略转json
@ApiModelProperty(value = "帐户是否可用(1 可用,0 删除用户)")
private boolean isEnabled;

/**
* 封装用户拥有的菜单权限标识
*/
@JSONField(serialize = false) // 忽略转json
private List<GrantedAuthority> authorities;

// isAccountNonExpired 是 Integer 类型接收,然后转 boolean
public JwtUser(String uid, String username, String password,
String nickName, String imageUrl, String mobile, String email, Integer isAccountNonExpired, Integer isAccountNonLocked,
Integer isCredentialsNonExpired, Integer isEnabled,
List<GrantedAuthority> authorities) {
this.uid = uid;
this.username = username;
this.password = password;
this.nickName = nickName;
this.imageUrl = imageUrl;
this.mobile = mobile;
this.email = email;
this.isAccountNonExpired = isAccountNonExpired == 1 ? true : false;
this.isAccountNonLocked = isAccountNonLocked == 1 ? true : false;
this.isCredentialsNonExpired = isCredentialsNonExpired == 1 ? true : false;
this.isEnabled = isEnabled == 1 ? true : false;
this.authorities = authorities;
}
}

在blog-oauth2创建com.acaiblog.auth2.service.UserDetailsServiceImpl实现UserDetailsService接口

package com.acaiblog.auth2.service;

import com.acaiblog.entities.SysMenu;
import com.acaiblog.entities.SysUser;
import com.acaiblog.feign.IFeignSystemController;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

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

@Service // 不一定不要少了
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired // 检查启动类注解 @EnableFeignClients
private IFeignSystemController feignSystemController;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 判断用户名是否为空
if (StringUtils.isEmpty(username)) {
throw new BadCredentialsException("用户名不能为空");
}
// 2. 通过用户名查询数据库中的用户信息
SysUser sysUser = feignSystemController.findUserByUsername(username);
if (sysUser == null) {
throw new BadCredentialsException("用户名或密码错误");
}
// 3. 通过用户id去查询数据库的拥有的权限信息
List<SysMenu> menuList =
feignSystemController.findMenuByUserId(sysUser.getId());
// 4. 封装权限信息(权限标识符code)
List<GrantedAuthority> authorities = null;
if (CollectionUtils.isNotEmpty(menuList)) {
authorities = new ArrayList<>();
for (SysMenu menu : menuList) {
// 权限标识
String code = menu.getCode();
authorities.add(new SimpleGrantedAuthority(code));
}
}
// 5. 构建UserDetails接口的实现类JwtUser对象
JwtUser jwtUser = new JwtUser(
sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(), sysUser.getNickName(), sysUser.getImageUrl(), sysUser.getMobile(), sysUser.getEmail(),
sysUser.getIsAccountNonExpired(), sysUser.getIsAccountNonLocked(), sysUser.getIsCredentialsNonExpired(), sysUser.getIsEnabled(),
authorities);
return jwtUser;
}
}

加密器配置类

在blog-oauth2微服务添加com.acaiblog.auth2.config.PasswordEncoderConfig配置BCryptPasswordEncoder加密器到容器中。

package com.acaiblog.auth2.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

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

安全配置类

在blog-auth2模块创建com.acaiblog.auth2.config.SpringSecurityConfig

package com.acaiblog.auth2.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetailsService;

@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
// 读取用户信息进行认证
authenticationManagerBuilder.userDetailsService(userDetailsService);
}

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

配置JWT管理令牌

·JSON Web Token(JWT)是一个开放的行业标准(RFC 7519)`,它定义了一种紧凑且独立的方式,用于在各方之间作为JSON对象安全地传输信息。此信息可以通过数字签名进行验证和信任。JWT可以使用密码(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名 ,防止被篡改。
JWT官网:https://jwt.io

非对称加密算法

非对称加密算法需要两个密钥:公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。公钥与私钥是一对,如果用私钥对数据进行加密,只有用对应的公钥才能解密。

生成密钥证书

公私钥对可以使用jdk的命令keytool来生成,首先来看一下这个命令下有哪些参数

参数 描述
-certreq 生成证书请求
-changealias 更改条目的别名
-delete 删除条目
-exportcert 导出证书
-genkeypair 生成密钥对
-genseckey 生成密钥
-gencert 根据证书请求生成证书
-importcert 导入证书或证书链
-importpass 导入口令
-importkeystore 从其他密钥库导入一个或所有条目
-keypasswd 更改条目的密钥口令
-list 列出密钥库中的条目
-printcert 打印证书内容
-printcertreq 打印证书请求的内容
-printcrl 打印 CRL 文件的内容
-storepasswd 更改密钥库的存储口令
-showinfo 显示安全相关信息

生成密钥证书文件,每个证书包含公钥和私钥, 执行以下命令

$ keytool -genkeypair -alias oauth2 -keyalg RSA -keypass oauth2 -keystore oauth2.jks -storepass oauth2
您的名字与姓氏是什么?
[Unknown]: acai
您的组织单位名称是什么?
[Unknown]: acai
您的组织名称是什么?
[Unknown]: acai
您所在的城市或区域名称是什么?
[Unknown]: XiAn
您所在的省/市/自治区名称是什么?
[Unknown]: ShanXi
该单位的双字母国家/地区代码是什么?
[Unknown]: CN
CN=acai, OU=acai, O=acai, L=XiAn, ST=ShanXi, C=CN是否正确?
[否]: y

正在为以下对象生成 2,048 位RSA密钥对和自签名证书 (SHA256withRSA) (有效期为 90 天):
CN=acai, OU=acai, O=acai, L=XiAn, ST=ShanXi, C=CN

执行命令后会生成oauth2.jks文件将oauth2.jks文件拷贝到认证服务器blog-oauth2resources文件夹下。

JWT管理信息配置类

在blog-auth模块下创建com.acaiblog.auth2.config.JwtTokenStoreConfig

package com.acaiblog.auth2.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

@Configuration
public class JwtTokenStoreConfig {
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
// 采用非对称加密jwt 第1个参数就是密钥证书文件,第2个参数 密钥库口令, 私钥进行签名
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
new ClassPathResource("oauth2.jks"), "oauth2".toCharArray()
);
jwtAccessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("oauth2"));
return jwtAccessTokenConverter;
}

@Bean
public TokenStore tokenStore() {
// Jwt管理令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
}

认证服务器配置类

创建认证服务器配置类com.acaiblog.auth2.config.AuthorizationServerConfig

package com.acaiblog.auth2.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.config.annotation.configuration.ClientDetailsServiceConfiguration;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import javax.sql.DataSource;

@Configuration
@EnableAuthorizationServer // 开启认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;

@Bean // 客户端使用jdbc管理
public ClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}

/**
* 配置被允许访问此认证服务器的客户端信息: 数据库方式 如:门户客户端,后台客户端
* @param clientDetailsServiceConfigurer
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception {
// jdbc管理客户端
clientDetailsServiceConfigurer.withClientDetails(jdbcClientDetailsService());
}

@Autowired
// 在 SpringSecurityConfig 中添加到容器了, 密码模式需要
private AuthenticationManager authenticationManager;

@Autowired
private UserDetailsService userDetailsService;

@Autowired
// token管理方式,引用 JwtTokenStoreConfig 配置的
private TokenStore tokenStore;

@Autowired
// jwt 转换器
private JwtAccessTokenConverter jwtAccessTokenConverter;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpointsConfigurer) throws Exception {
// password 要这个 AuthenticationManager 实例
endpointsConfigurer.authenticationManager(authenticationManager);
// 刷新令牌时需要使用
endpointsConfigurer.userDetailsService(userDetailsService);
// 令牌的管理方式
endpointsConfigurer.tokenStore(tokenStore).accessTokenConverter(jwtAccessTokenConverter);
}

@Override
public void configure(AuthorizationServerSecurityConfigurer serverSecurityConfigurer) throws Exception {
// /oauth/check_token 解析令牌,默认情况 下拒绝访问
serverSecurityConfigurer.checkTokenAccess("permitAll()");
}

}

关闭CSRF攻击

编辑blog-Oauth2/src/main/java/com/acaiblog/auth2/config/SpringSecurityConfig.java覆盖configure(HttpSecurity)进行关闭csrf攻击,不然调用不到接口

package com.acaiblog.auth2.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetailsService;

import javax.servlet.http.HttpSession;

@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable();
}
}

测试

创建数据库表

DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(128) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
`resource_ids` varchar(128) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
`client_secret` varchar(128) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
`scope` varchar(128) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
`authorized_grant_types` varchar(128) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
`web_server_redirect_uri` varchar(128) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
`authorities` varchar(128) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
`access_token_validity` int(11) NULL DEFAULT NULL,
`refresh_token_validity` int(11) NULL DEFAULT NULL,
`additional_information` varchar(4096) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
`autoapprove` varchar(128) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic;

在数据库表中插入数据

INSERT INTO `oauth_client_details` VALUES ('blog-admin', '', '$2a$10$uA51hWL5yusFBoEvZOAZbeaYYqUaFV7xjdDB8GA.4iViNiCSK9xKO', 'all', 'password,refresh_token', '', 'all', NULL, NULL, NULL, 'false');

测试令牌

使用postman发送POST请求:http://127.0.0.1:8004/auth/oauth/token
Auth》Type Basic Auth 》username: oauth_client_details表中client_id字段; password:oauth_client_details表中client_secret密码
Body》x-www-form-urlencoded》grant_type:password username:sys_user表中的用户名;password:sys_user表中的用户密码

汉化认证响应信息

创建com.acaiblog.auth2.config.ReloadMessageConfig认证提示信息配置类

package com.acaiblog.auth2.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;

@Configuration
public class ReloadMessageConfig {
@Bean
// // 加载中文的认证提示信息
public ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages_zh_CN");
return messageSource;
}
}

创建blog-oauth2/src/main/resources/messages_zh_CN.properties

AbstractAccessDecisionManager.accessDenied=不允许访问
AbstractLdapAuthenticationProvider.emptyPassword=用户名或密码错误
AbstractSecurityInterceptor.authenticationNotFound=未在SecurityContext中查找到认证对象
AbstractUserDetailsAuthenticationProvider.badCredentials=用户名或密码错误
AbstractUserDetailsAuthenticationProvider.credentialsExpired=用户凭证已过期
AbstractUserDetailsAuthenticationProvider.disabled=用户已失效
AbstractUserDetailsAuthenticationProvider.expired=用户帐号已过期
AbstractUserDetailsAuthenticationProvider.locked=用户帐号已被锁定
AbstractUserDetailsAuthenticationProvider.onlySupports=仅仅支持UsernamePasswordAuthenticationToken
AccountStatusUserDetailsChecker.credentialsExpired=用户凭证已过期
AccountStatusUserDetailsChecker.disabled=用户已失效
AccountStatusUserDetailsChecker.expired=用户帐号已过期
AccountStatusUserDetailsChecker.locked=用户帐号已被锁定
AclEntryAfterInvocationProvider.noPermission=给定的Authentication对象({0})根本无权操控领域对象({1})
AnonymousAuthenticationProvider.incorrectKey=展示的AnonymousAuthenticationToken不含有预期的key
BindAuthenticator.badCredentials=用户名或密码错误
BindAuthenticator.emptyPassword=用户名或密码错误
CasAuthenticationProvider.incorrectKey=展示的CasAuthenticationToken不含有预期的key
CasAuthenticationProvider.noServiceTicket=未能够正确提供待验证的CAS服务票根
ConcurrentSessionControlAuthenticationStrategy.exceededAllowed=已经超过了当前主体({0})被允许的最大会话数量
DigestAuthenticationFilter.incorrectRealm=响应结果中的Realm名字({0})同系统指定的Realm名字({1})不吻合
DigestAuthenticationFilter.incorrectResponse=错误的响应结果
DigestAuthenticationFilter.missingAuth=遗漏了针对'auth' QOP的、必须给定的摘要取值; 接收到的头信息为{0}
DigestAuthenticationFilter.missingMandatory=遗漏了必须给定的摘要取值; 接收到的头信息为{0}
DigestAuthenticationFilter.nonceCompromised=Nonce令牌已经存在问题了,{0}
DigestAuthenticationFilter.nonceEncoding=Nonce未经过Base64编码; 相应的nonce取值为 {0}
DigestAuthenticationFilter.nonceExpired=Nonce已经过期/超时
DigestAuthenticationFilter.nonceNotNumeric=Nonce令牌的第1部分应该是数字,但结果却是{0}
DigestAuthenticationFilter.nonceNotTwoTokens=Nonce应该由两部分取值构成,但结果却是{0}
DigestAuthenticationFilter.usernameNotFound=用户名{0}未找到
JdbcDaoImpl.noAuthority=没有为用户{0}指定角色
JdbcDaoImpl.notFound=未找到用户{0}
LdapAuthenticationProvider.badCredentials=用户名或密码错误
LdapAuthenticationProvider.credentialsExpired=用户凭证已过期
LdapAuthenticationProvider.disabled=用户已失效
LdapAuthenticationProvider.expired=用户帐号已过期
LdapAuthenticationProvider.locked=用户帐号已被锁定
LdapAuthenticationProvider.emptyUsername=用户名不允许为空
LdapAuthenticationProvider.onlySupports=仅仅支持UsernamePasswordAuthenticationToken
PasswordComparisonAuthenticator.badCredentials=用户名或密码错误
#PersistentTokenBasedRememberMeServices.cookieStolen=Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.
ProviderManager.providerNotFound=未查找到针对{0}的AuthenticationProvider
RememberMeAuthenticationProvider.incorrectKey=展示RememberMeAuthenticationToken不含有预期的key
RunAsImplAuthenticationProvider.incorrectKey=展示的RunAsUserToken不含有预期的key
SubjectDnX509PrincipalExtractor.noMatching=未在subjectDN\: {0}中找到匹配的模式
SwitchUserFilter.noCurrentUser=不存在当前用户
SwitchUserFilter.noOriginalAuthentication=不能够查找到原先的已认证对象

扩展认证的响应数据

当认证成功后,如果客户端需要其他用户信息,则可以进行扩展

创建扩展器TokenEnhancer

创建com.acaiblog.auth2.config.JwtTokenEnhancer

package com.acaiblog.auth2.config;

import com.acaiblog.auth2.service.JwtUser;
import com.alibaba.fastjson.JSON;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.stereotype.Component;
import springfox.documentation.spring.web.json.Json;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

@Component
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
// 扩展令牌内容
JwtUser jwtUser = (JwtUser) oAuth2Authentication.getPrincipal();
Map<String, Object> map = new LinkedHashMap<>();
map.put("userInfo", JSON.toJSON(jwtUser));

// 设置附加信息
((DefaultOAuth2AccessToken)oAuth2AccessToken).setAdditionalInformation(map);
return oAuth2AccessToken;
}
}

认证服务器注入扩展器

编辑blog-oauth2/src/main/java/com/acaiblog/auth2/config/AuthorizationServerConfig.java

package com.acaiblog.auth2.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.config.annotation.configuration.ClientDetailsServiceConfiguration;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.List;

@Configuration
@EnableAuthorizationServer // 开启认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
// 注入扩展器
private TokenEnhancer jwtTokenEnhancer;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpointsConfigurer) throws Exception {
// password 要这个 AuthenticationManager 实例
endpointsConfigurer.authenticationManager(authenticationManager);
// 刷新令牌时需要使用
endpointsConfigurer.userDetailsService(userDetailsService);
// 令牌的管理方式
endpointsConfigurer.tokenStore(tokenStore).accessTokenConverter(jwtAccessTokenConverter);

// 认证扩展器
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> enhancerList = new ArrayList<>();
enhancerList.add(jwtTokenEnhancer);
enhancerList.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(enhancerList);
endpointsConfigurer.tokenEnhancer(enhancerChain).accessTokenConverter(jwtAccessTokenConverter);
}

}

测试

postman发送POST请求:http://127.0.0.1:8004/auth/oauth/token 响应数据多了userInfo

自定义令牌刷新逻辑

通过/oauth/token端点指定grant_type=refresh_token刷新令牌模式,即可获取新令牌。但是上面如果传递的Basic请求头错误,只会响应401未认证,而不会提示更详细错误信息。
编辑blog-util/pom.xml添加http请求工具依赖

<!--http请求工具-->
<dependency>
<groupId>com.arronlong</groupId>
<artifactId>httpclientutil</artifactId>
<version>1.0.4</version>
</dependency>

在blog-oauth模块创建com.acaiblog.web.service.AuthService刷新令牌业务层

package com.acaiblog.web.service;

import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ResultEnum;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.arronlong.httpclientutil.HttpClientUtil;
import com.arronlong.httpclientutil.common.HttpConfig;
import com.arronlong.httpclientutil.exception.HttpProcessException;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Header;
import com.arronlong.httpclientutil.common.HttpHeader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.stereotype.Service;

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

@Service
public class AuthService {
@Autowired
// 负载均衡client
LoadBalancerClient loadBalancerClient;

public Result refreshToken(String header, String refreshToken) throws HttpProcessException {
// 采用客户端负载均衡,从 Nacos 获取认证服务器的 ip 和端口
ServiceInstance serviceInstance = loadBalancerClient.choose("auth-server");
if (serviceInstance == null) {
return Result.error("未找到有效的认证微服务,请检查认证微服务是否注册到Nacos");
}
// 请求刷新令牌URL
String refreshTokenUrl = serviceInstance.getUri().toString() + "/auth/oauth/token";
// 封装刷新令牌请求参数
Map<String, Object> map = new HashMap<>();
map.put("grant_type", "refresh_token");
map.put("refresh_token", refreshToken);

// 构建配置请求参数(网址、请求参数、编码、client)
Header[] headers = HttpHeader.custom() // 自定义认证
.contentType(HttpHeader.Headers.APP_FORM_URLENCODED) // 数据类型
.authorization(header) // 认证请求头
.build();
HttpConfig config = HttpConfig.custom().headers(headers)
.url(refreshTokenUrl)
.map(map);

// 发送请求响应令牌
String token = HttpClientUtil.post(config);
JSONObject jsonToken = JSON.parseObject(token);
if (StringUtils.isNotEmpty(jsonToken.getString("error"))) {
return Result.build(ResultEnum.TOKEN_PAST);
}
return Result.ok(jsonToken);
}
}

在blog-oauth模块创建com.acaiblog.web.controller.AuthController刷新令牌控制层

package com.acaiblog.web.controller;

import com.acaiblog.util.base.Result;
import com.acaiblog.util.tools.RequestUtil;
import com.acaiblog.web.service.AuthService;
import com.arronlong.httpclientutil.exception.HttpProcessException;
import com.google.common.base.Preconditions;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpHeaders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@RestController
public class AuthController {
Logger logger = (Logger) LoggerFactory.getLogger(getClass());

@Autowired
AuthService authService;

@Autowired
PasswordEncoder passwordEncoder;

@Autowired
ClientDetailsService clientDetailsService;

private static final String HEADER_TYPE = "Basic";

@ApiOperation("刷新令牌接口")
@GetMapping("/user/refreshToken")
public Result refreshToken(HttpServletRequest request) {
try {
// 获取请求中的刷新令牌
String refreshToken = request.getParameter("refreshToken");
Preconditions.checkArgument(StringUtils.isNotEmpty(refreshToken)); // 令牌不能为空
// 请求头
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null || !header.startsWith(HEADER_TYPE)) {
throw new UnapprovedClientAuthenticationException("请求头中无client信息");
}

String[] tokens = RequestUtil.extractAndDecodeHeader(header);
assert tokens.length == 2;
String clientId = tokens[0];
String clientSecret = tokens[1];
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
if (clientDetails == null) {
throw new UnapprovedClientAuthenticationException("clentId对应的配置信息不存在:" + clientId);
}

// 客户端密码是否正确
if (!passwordEncoder.matches(clientSecret, clientDetails.getClientSecret())) {
throw new UnapprovedClientAuthenticationException("无效clientSecret");
}
// 获取刷新令牌
return authService.refreshToken(header,refreshToken);
} catch (IOException e) {
logger.error("refreshToken={}", e.getMessage(), e);
throw new RuntimeException(e);
} catch (HttpProcessException e) {
throw new RuntimeException(e);
}
}

}

Redis管理JWT令牌-登录/注销

docker安装redis

docker run -idt --name redis -p 6379:6379 --restart always redis:7.2.1

整合Redis

编辑blog-auth模块blog-oauth2/pom.xml

<!--redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置Redis连接信息

编辑Nacos认证微服务配置文件,添加以下内容

spring:
application:
name: auth-server # 应用名称
cloud:
nacos:
discovery:
server-addr: localhost:8848
# redis
redis:
host: localhost
port: 6379
#如果没有密码设置为空
password:

使用redis

@Resource // 不要少了
private RedisTemplate redisTemplate;
// 常用方法
redisTemplate.opsForValue().set() //保存数据
redisTemplate.delete(key) //删除数据
redisTemplate.opsForValue().get(key) // 查询key对应value

Redis管理Token

Redis存储有效令牌

  1. 生成Jwt访问令牌的时候,将Jwt Token存入redis中
  2. 扩展Jwt的验证功能,验证redis中是否存在数据,如果存在则token有效,否则无效
  3. 退出系统时将Redis中的数据删除。

项目使用TokenStore为JwtTokenStore管理JWT访问令牌

  1. 存储令牌是调用JwtTokenStorestoreAccessToken方法,但这个方法是个空的,我们要覆盖该方法将token存入redis中。
  2. 客户端退出时,删除客户端存储的token, 并调用服务器的接口删除服务器上存储的令牌,删除令牌最终调用的是JwtTokenStoreremoveAccessToken方法,所以只要实现该方法,就能达到删除令牌的效果。

编辑blog-oauth2/src/main/java/com/acaiblog/auth2/config/JwtTokenStoreConfig.java类TokenStore方法注入RedisTemplate

package com.acaiblog.auth2.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Configuration
public class JwtTokenStoreConfig {
@Resource
private RedisTemplate redisTemplate;

@Bean
public TokenStore tokenStore() {
// Jwt管理令牌
return new JwtTokenStore(jwtAccessTokenConverter()) { // JwtTokenStore匿名类中重写方法
// 存储到redis
@Override
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication auth2Authentication) {
// 将Jwt的唯一标识jti为Key存入redis中,并保持与原Jwt有一致的时效性
if (token.getAdditionalInformation().containsKey("jti")) {
String jti = token.getAdditionalInformation().get("jti").toString();
// (key,value,有效时间,时间单位秒)
redisTemplate.opsForValue().set(
jti,token.getValue(),token.getExpiresIn(), TimeUnit.MINUTES
);
super.storeAccessToken(token,auth2Authentication);
}
}
// 删除redis token
@Override
public void removeAccessToken(OAuth2AccessToken token) {
if (token.getAdditionalInformation().containsKey("jti")) {
// 通过Jwt的唯一标识jti为Key删除redis中数据
String jti = token.getAdditionalInformation().get("jti").toString();
redisTemplate.delete(jti);
}
super.removeAccessToken(token);
}
};

}
}

测试:发送POST请求:http://127.0.0.1:8004/auth/oauth/token 登录到Redis查看是否有数据

登录功能

编辑blog-oauth2/src/main/java/com/acaiblog/auth2/config/SpringSecurityConfig.java配置表单登录方式http.formLogin()

package com.acaiblog.auth2.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetailsService;

import javax.servlet.http.HttpSession;

@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// httpSecurity.csrf().disable();
// 关闭csrf攻击
httpSecurity.formLogin()
.and()
.csrf().disable();
}
}

登录成功处理器

成功处理器获取token值响应,创建com.acaiblog.auth2.CustomAuthenticationSuccessHandler不要放到与SpringSecurityConfig同级包,不然功能都会失效,要放到父包上

package com.acaiblog.auth2;

import com.acaiblog.util.base.Result;
import com.acaiblog.util.enums.ResultEnum;
import com.acaiblog.util.tools.RequestUtil;
import com.acaiblog.web.service.AuthService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.collections.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.annotation.Resources;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@Component("customAuthenticationSuccessHandler")
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
Logger logger = LoggerFactory.getLogger(getClass());

@Autowired
private PasswordEncoder passwordEncoder;

@Resource
private ClientDetailsService clientDetailsService;

@Resource
private AuthorizationServerTokenServices authorizationServerTokenServices;

private static final String HEADER_TYPE = "Basic";

@Autowired
private AuthService authService;
@Resource
private ObjectMapper objectMapper;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
logger.info("登录成功 {}",authentication.getPrincipal());
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
logger.info("header: {}", header);
// 封装响应效果
Result result = null;
try {
if (header == null || !header.startsWith(HEADER_TYPE)) {
throw new UnapprovedClientAuthenticationException("请求头无客户端信息");
}
// 解析请求头
String[] tokens = RequestUtil.extractAndDecodeHeader(header);
assert tokens.length == 2; // 断言式,如果tokens的长度为2则继续执行否则抛出AssertionError异常
String clientId = tokens[0];
String clientSecret = tokens[1];
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
if (clientDetails == null) {
throw new UnapprovedClientAuthenticationException("无效clientId");
}

// 客户端密码是否正确
if (!passwordEncoder.matches(clientSecret, clientDetails.getClientSecret())) {
throw new UnapprovedClientAuthenticationException("无效clientSecret");
}

// 构建 tokenRequest 和 oAuth2Request 组合成 oAuth2Authentication 去获取 accessToken
TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP, clientId, clientDetails.getScope(), "custom");
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);

// 获取accessToken
OAuth2AccessToken oAuth2AccessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
result = Result.ok(oAuth2AccessToken);
} catch (IOException e) {
logger.error("refreshToken={}", e.getMessage(), e);
// 认证失败
result = Result.build(ResultEnum.AUTH_FAIL.getCode(), e.getMessage());
}
// 响应结果
response.setContentType("application/json;charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString( result ));
}
}

登录失败处理器

在blog-oauth模块创建com.acaiblog.auth2.CustomAuthenticationFailureHandler

package com.acaiblog.auth2;

import com.acaiblog.util.base.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

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

@Component("customAuthenticationFailureHandler")
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, org.springframework.security.core.AuthenticationException e) throws IOException, ServletException {
// 响应错误信息:json格式
response.setContentType("application/json;charset=UTF-8");
String result = objectMapper.writeValueAsString(Result.error(e.getMessage()));
response.getWriter().write(result);
}
}

退出登录功能

get请求:http://localhost:8001/auth/logout?accessToken=0decd3ef67804618bfb87d7b99f1d3ad
springsecurity提供了/logout 来实现退出功能,但是针对删除缓存中的token数据还需要我们自己实现。可以在退出成功处理器LogoutSuccessHandler,将redis中的token删除,调用上面实现的JwtTokenStore.removeAccessToken方法即可删除 。

定义退出成功处理器

创建com.acaiblog.auth2.CustomLogoutSuccessHandler

package com.acaiblog.auth2;

import com.acaiblog.util.base.Result;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

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

/**
* 退出成功处理器
*/
@Component("customLogoutSuccessHandler")
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
private TokenStore tokenStore;


@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 获取accessToken
String accessToken = request.getParameter("accessToken");
if (StringUtils.isNotEmpty(accessToken)) {
// 转换token对象
OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(accessToken);
if (oAuth2AccessToken != null) {
// 删除redis访问令牌
tokenStore.removeAccessToken(oAuth2AccessToken);
}
}
// 退出成功
response.setContentType("application/json;charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.write( Result.ok().toJsonString() );
}
}

注入成功处理器

编辑blog-oauth2/src/main/java/com/acaiblog/auth2/config/SpringSecurityConfig.java

package com.acaiblog.auth2.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.http.HttpSession;

@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;

@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;

@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;

@Autowired
private LogoutSuccessHandler logoutSuccessHandler;

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// httpSecurity.csrf().disable();
// 关闭csrf攻击
httpSecurity.formLogin()
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.and()

.logout()
.logoutSuccessHandler(logoutSuccessHandler)
.and()

.csrf().disable();
}
}

测试

发送POST请求:http://127.0.0.1:8004/auth/login 检查redis是否有数据

资源服务器安全配置

根据密钥证书获取公钥

在请求资源服务器(文章、问答、系统微服务)接口时,都必须要求在请求头带上jwt令牌来访问服务接口,而认证服务器生成的jwt令牌是通过非对称私钥进行加密了,资源服务收到请求后,要解析出jwt令牌就需要公钥进行解析出来。所以下面要通过 密钥证书获取公钥,放到资源服务器中,这样资源服务器可以直接解析出有效信息。

获取公钥

如果没有keytool命令安装openssl即可

$ cd blog-oauth2/src/main/resources
$ keytool -list -rfc --keystore oauth2.jks | openssl x509 -inform pem -pubkey
输入密钥库口令: oauth2

创建blog-article/src/main/resources/public.txt将上一步生成的公钥复制到public.txt文件中。


-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0TNthoX21HIGNieX3JWD
wT177QPekJleGirtMBIcKoqgKWS5/BNDAY3ipp7JHpnGAz1s/anLwpPjCz2g7bD7
86EfbtD2idl57fJV5X6fXpIyQlK+lZJD8YyjBy22or51GHTNfAW3yeldfzC5FAVO
kQ9dRmJMGcUmG+ydEn4DolpxP3+1rYz0JblVQDa3mwS33VyIOd1Ph61S/MSpFfI0
zzgxvL8/qAB4Zq4vpEFCQyPrQlqDdWfNfXVE4qB+VdTjkRgQSwmxP1FV7FmxmnV2
gFdeMmKOEVNDCmbghUpGxsGYOVhfcObZN8kV2m4B88QOL6E5q/CWpSyJrUWTU3+6
EwIDAQAB
-----END PUBLIC KEY-----

文章微服务安全配置

当前微服务的所有接口,不管是有身份还是没有身份的用户都可以访问到, 我们应该让请求在它的请求头中带着有效 token 过来,才允许访问到对应有权限的接口。

添加依赖

编辑blog-article/pom.xml

<!-- Spring Security、OAuth2 和JWT等 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

接口测试

浏览器请求:http://127.0.0.1:8001/article/swagger-ui.html# 要求提供用户名和密码

Jwt管理令牌配置类

在blog-article模块创建com.acaiblog.oauth2.config.JwtTokenStoreConfig

package com.acaiblog.oauth2.config;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import java.io.IOException;

@Configuration
public class JwtTokenStoreConfig {
Logger logger = LoggerFactory.getLogger(getClass());
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 非对称加密,资源服务器使用公钥解密 public.txt
ClassPathResource resource = new ClassPathResource("public.txt");
String publicKey = null;
try {
publicKey = IOUtils.toString(resource.getInputStream(), "UTF-8");
} catch (IOException e) {
logger.error("Error reading public key from file", e);
e.printStackTrace();
}
converter.setVerifierKey(publicKey);
return converter;

}

@Bean
public TokenStore tokenStore() {
// Jwt管理令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
}

配置资源服务器

在blog-article模块中创建com.acaiblog.oauth2.config.ResourceServerConfig

package com.acaiblog.oauth2.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfiguration;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;

@Configuration
@EnableResourceServer // 标识为资源服务器,请求服务中的资源,就要带着token过来,找不到token或token是无效访问不了资源
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级别权限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;

@Override
public void configure(ResourceServerSecurityConfigurer resourceServerSecurityConfigurer) {
resourceServerSecurityConfigurer.tokenStore(tokenStore); // JWT管理令牌
}

@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.sessionManagement()
// 不使用也不会创建HttpSession实例,因为我们使用 token 方式
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 授权规则配置
.authorizeRequests()
// 放行 swagger-ui 相关请求
.antMatchers("/v2/api-docs", "/v2/feign-docs", "/swagger-resources/configuration/ui", "/swagger-resources",
"/swagger-resources/configuration/security", "/swagger-ui.html", "/webjars/**").permitAll()
// 放行 /api 开头的请求
.antMatchers("/api/**").permitAll()
// 所有请求,都需要有all范围(scope)
.antMatchers("/**").access("#oauth2.hasScope('all')")
// 其他所有请求都要先通过认证
.anyRequest().authenticated();

}
}

接口测试

访问:http://127.0.0.1:8001/article/swagger-ui.html# 不会提示认证
访问接口:http://127.0.0.1:8001/article/advert/1 ,返回401错误:

{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}

请求文章微服务接口

  1. 编辑Nacos配置article-server-dev.yml文件,添加
    swagger:
    description: '博客分类、标签、文章、广告接口'
    authorization:
    key-name: Authorization
  2. postman发送POST请求:http://localhost:8004/auth/login?username=admin&password=123456
    {
    "code": 20000,
    "message": "成功",
    "data": {
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySW5mbyI6eyJ1aWQiOiI5Iiwibmlja05hbWUiOiLmoqblrabosLciLCJpbWFnZVVybCI6Imh0dHBzOi8vbWVuZ3h1ZWd1Lm9zcy1jbi1zaGVuemhlbi5hbGl5dW5jcy5jb20vYXJ0aWNsZS8yMDIwMDUyMi84NjY1ZDczYWUyNDg0YmQyOGJjMmZmNDEwMzUzODM4NS5wbmciLCJtb2JpbGUiOiIxODg4ODg4ODg4OCIsImVtYWlsIjoibWVuZ3h1ZWd1ODg4QDE2My5jb20iLCJ1c2VybmFtZSI6ImFkbWluIn0sInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTcwNjU1MzkyNSwiYXV0aG9yaXRpZXMiOlsiYXJ0aWNsZTpzZWFyY2giLCJ1c2VyOnBhc3N3b3JkIiwicm9sZSIsImFydGljbGU6YXVkaXQiLCJjYXRlZ29yeTpkZXRlbGUiLCJibG9nIiwiYWR2ZXJ0OmFkZCIsInJvbGU6YWRkIiwibGFiZWw6ZWRpdCIsInB1YmxpYyIsInVzZXI6c2VhcmNoIiwicm9sZTpkZWxldGUiLCJtZW51OmRlbGV0ZSIsImFkdmVydDpzZWFyY2giLCJhcnRpY2xlOnZpZXciLCJsYWJlbDphZGQiLCJyb2xlOnBlcm1pc3Npb24iLCJhZHZlcnQ6ZWRpdCIsInVzZXI6YWRkIiwidXNlcjpkZWxldGUiLCJjYXRlZ29yeTpzZWFyY2giLCJjYXRlZ29yeTplZGl0Iiwicm9sZTpzZWFyY2giLCJhZHZlciIsImluZGV4IiwibWVudTplZGl0IiwibGFiZWwiLCJtZW51IiwibWVudTpzZWFyY2giLCJhcnRpY2xlIiwiYWR2ZXJ0OmRlbGV0ZSIsIm1lbnU6YWRkIiwic3lzdGVtIiwibGFiZWw6ZGVsZXRlIiwiYXJ0aWNsZTpkZWxldGUiLCJ1c2VyOmVkaXQiLCJsYWJlbDpzZWFyY2giLCJ1c2VyOnJvbGUiLCJyb2xlOmVkaXQiLCJjYXRlZ29yeSIsInVzZXIiLCJjYXRlZ29yeTphZGQiXSwianRpIjoiMzBhNzg3NjMtYmFiZi00ZjNhLTljYzQtZmZhMTk4NDUyZjFiIiwiY2xpZW50X2lkIjoiYmxvZy1hZG1pbiJ9.bdJ7MZlmp7k0mnn8nsIl-g7c2sOtU3nyxZ5dFehX9wtoZScjI4-pkRPgxA9iTpOGqZyIKxeIhPXY4876b-l3u6_v77GoLyCjkln_nc-EPWDX0zIHCfniL6AxB178a-FfYMlxtbDItcDTLgB60Osr2Yx2tWs7Wee5k7ggnoiI4jpxbyozKfhTihHjTpyTjYxX0fPg9WW979N6Dtws5qtYt5eQ9sRXFd8aulK7hhDEugjV_M8E1ECPmzD5Y0rasJ3znpOtMRhmmzom3862xu4ERwqGFmYUkcfrZr6yAuBM7U0cAsmDfcpAoskX-EIrNxYMEUeUk9oZHBeo-tPCiKjBlw",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySW5mbyI6eyJ1aWQiOiI5Iiwibmlja05hbWUiOiLmoqblrabosLciLCJpbWFnZVVybCI6Imh0dHBzOi8vbWVuZ3h1ZWd1Lm9zcy1jbi1zaGVuemhlbi5hbGl5dW5jcy5jb20vYXJ0aWNsZS8yMDIwMDUyMi84NjY1ZDczYWUyNDg0YmQyOGJjMmZmNDEwMzUzODM4NS5wbmciLCJtb2JpbGUiOiIxODg4ODg4ODg4OCIsImVtYWlsIjoibWVuZ3h1ZWd1ODg4QDE2My5jb20iLCJ1c2VybmFtZSI6ImFkbWluIn0sInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6IjMwYTc4NzYzLWJhYmYtNGYzYS05Y2M0LWZmYTE5ODQ1MmYxYiIsImV4cCI6MTcwOTEwMjcyNSwiYXV0aG9yaXRpZXMiOlsiYXJ0aWNsZTpzZWFyY2giLCJ1c2VyOnBhc3N3b3JkIiwicm9sZSIsImFydGljbGU6YXVkaXQiLCJjYXRlZ29yeTpkZXRlbGUiLCJibG9nIiwiYWR2ZXJ0OmFkZCIsInJvbGU6YWRkIiwibGFiZWw6ZWRpdCIsInB1YmxpYyIsInVzZXI6c2VhcmNoIiwicm9sZTpkZWxldGUiLCJtZW51OmRlbGV0ZSIsImFkdmVydDpzZWFyY2giLCJhcnRpY2xlOnZpZXciLCJsYWJlbDphZGQiLCJyb2xlOnBlcm1pc3Npb24iLCJhZHZlcnQ6ZWRpdCIsInVzZXI6YWRkIiwidXNlcjpkZWxldGUiLCJjYXRlZ29yeTpzZWFyY2giLCJjYXRlZ29yeTplZGl0Iiwicm9sZTpzZWFyY2giLCJhZHZlciIsImluZGV4IiwibWVudTplZGl0IiwibGFiZWwiLCJtZW51IiwibWVudTpzZWFyY2giLCJhcnRpY2xlIiwiYWR2ZXJ0OmRlbGV0ZSIsIm1lbnU6YWRkIiwic3lzdGVtIiwibGFiZWw6ZGVsZXRlIiwiYXJ0aWNsZTpkZWxldGUiLCJ1c2VyOmVkaXQiLCJsYWJlbDpzZWFyY2giLCJ1c2VyOnJvbGUiLCJyb2xlOmVkaXQiLCJjYXRlZ29yeSIsInVzZXIiLCJjYXRlZ29yeTphZGQiXSwianRpIjoiZWMwYWZmM2QtZTgxYi00ZWNlLTliMjItOTFkYzE0MTRiNjM2IiwiY2xpZW50X2lkIjoiYmxvZy1hZG1pbiJ9.QeHU60GM0mvgODT_LUQmhGXpo-r9xtqrVl5VYBm7C2hJp3tqPAonydtq_UazpKpICAGmkLgm-jjtLrabGtUHhhSQITqlOybBGeSKx8Zup-9cLTG9Ut8zeulhVbPrhuJkD6RQXCgm8dyZz90_EqysN0pO6QlJECs8_3aV6_hsXefCEdbTAYyUcin0UIergW_zk2lojFjTX3kCjmWuVrbZsYhnva5EBNdpT0iw6ZgTDr-r4Rt3Ki_sTT6eCco9x3cTB2-l4L9kr_yjBHkD4Y2Yc-bc6XbYWGYxEn3k-nvu6jx829pFhF6P6Bgaajin37D2vpbPPHrYxcfxibkqe91APg",
    "expires_in": 43199,
    "scope": "all",
    "userInfo": {
    "uid": "9",
    "nickName": "梦学谷",
    "imageUrl": "https://mengxuegu.oss-cn-shenzhen.aliyuncs.com/article/20200522/8665d73ae2484bd28bc2ff4103538385.png",
    "mobile": "18888888888",
    "email": "mengxuegu888@163.com",
    "username": "admin"
    },
    "jti": "30a78763-babf-4f3a-9cc4-ffa198452f1b"
    }
    }
  3. 访问 http://127.0.0.1:8001/article/swagger-ui.html# 》Authorize 》Value输入
    Bearer <access_token>
  4. 发送GET请求:http://127.0.0.1:8001/article/api/advert/show/1 在CURL中可以看到发送请求时携带了Authorization: Bearer
    curl -X GET "http://127.0.0.1:8001/article/article/1" -H "accept: */*" -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySW5mbyI6eyJ1aWQiOiI5Iiwibmlja05hbWUiOiLmoqblrabosLciLCJpbWFnZVVybCI6Imh0dHBzOi8vbWVuZ3h1ZWd1Lm9zcy1jbi1zaGVuemhlbi5hbGl5dW5jcy5jb20vYXJ0aWNsZS8yMDIwMDUyMi84NjY1ZDczYWUyNDg0YmQyOGJjMmZmNDEwMzUzODM4NS5wbmciLCJtb2JpbGUiOiIxODg4ODg4ODg4OCIsImVtYWlsIjoibWVuZ3h1ZWd1ODg4QDE2My5jb20iLCJ1c2VybmFtZSI6ImFkbWluIn0sInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTcwNjU1NTU0NywiYXV0aG9yaXRpZXMiOlsiYXJ0aWNsZTpzZWFyY2giLCJ1c2VyOnBhc3N3b3JkIiwicm9sZSIsImFydGljbGU6YXVkaXQiLCJjYXRlZ29yeTpkZXRlbGUiLCJibG9nIiwiYWR2ZXJ0OmFkZCIsInJvbGU6YWRkIiwibGFiZWw6ZWRpdCIsInB1YmxpYyIsInVzZXI6c2VhcmNoIiwicm9sZTpkZWxldGUiLCJtZW51OmRlbGV0ZSIsImFkdmVydDpzZWFyY2giLCJhcnRpY2xlOnZpZXciLCJsYWJlbDphZGQiLCJyb2xlOnBlcm1pc3Npb24iLCJhZHZlcnQ6ZWRpdCIsInVzZXI6YWRkIiwidXNlcjpkZWxldGUiLCJjYXRlZ29yeTpzZWFyY2giLCJjYXRlZ29yeTplZGl0Iiwicm9sZTpzZWFyY2giLCJhZHZlciIsImluZGV4IiwibWVudTplZGl0IiwibGFiZWwiLCJtZW51IiwibWVudTpzZWFyY2giLCJhcnRpY2xlIiwiYWR2ZXJ0OmRlbGV0ZSIsIm1lbnU6YWRkIiwic3lzdGVtIiwibGFiZWw6ZGVsZXRlIiwiYXJ0aWNsZTpkZWxldGUiLCJ1c2VyOmVkaXQiLCJsYWJlbDpzZWFyY2giLCJ1c2VyOnJvbGUiLCJyb2xlOmVkaXQiLCJjYXRlZ29yeSIsInVzZXIiLCJjYXRlZ29yeTphZGQiXSwianRpIjoiNmFmMmFjYTctMjg1MS00NjIyLWFmMmYtNjg1YzAxYWMzMTFkIiwiY2xpZW50X2lkIjoiYmxvZy1hZG1pbiJ9.z8miYw7MAb3tzbvNDEwUuYs0xP9zDb5hzTZDTIVLTFGVex4Ml60Dol-VkMKPk6ZWjcONjROaXY59_ks7qOd7IInJ3z2_i_M_EYnFNaEDynOyanUVa6NnI23aJzdp90qwuwWz9fl-frdnwsznnBZVG3f2rpfZeh6o68xq4X-qKaJm3YVVjfpQ5MpAArNt5KcpGYHupJhHj_gNQRGuVtdTDaH0B6OX844e43b3-rpuq9uH7nuEE2YV4xf_NpyNIe5Q6aMqD-bAVp1sa0MepASOqQbwnNX2jUuGkE8mOj1mbQAax0Z2g3k3Y68JV0Go7Uz92D4gET5_21VVk-fW_j0wng"
  5. 检查能够正常访问接口数据
    {
    "code": 20000,
    "message": "成功",
    "data": []
    }

    问答微服务安全配置

    添加依赖

    编辑blog-question/pom.xml
    <!-- Spring Security、OAuth2 和JWT等-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>

    Jwt管理令牌配置类

    编辑Nacos配置question-server-dev.yml文件
    swagger:
    description: '博客分类、标签、文章、广告接口'
    authorization:
    key-name: Authorization
    复制blog-article/src/main/resources/public.txtblog-question/src/main/resources/public.txt
    复制blog-article/src/main/java/com/acaiblog/oauth2/config/JwtTokenStoreConfig.javablog-question/src/main/java/com/acaiblog/oauth2/config/JwtTokenStoreConfig.java
    复制blog-article/src/main/java/com/acaiblog/oauth2/config/ResourceServerConfig.javablog-question/src/main/java/com/acaiblog/oauth2/config/ResourceServerConfig.java
    测试接口:http://127.0.0.1:8002/question/swagger-ui.html#/

系统微服务安全配置

添加依赖

编辑blog-system/pom.xml

<!-- Spring Security、OAuth2 和JWT等-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

Jwt管理令牌配置类

编辑Nacos配置system-server-dev.yml文件

swagger:
description: '博客分类、标签、文章、广告接口'
authorization:
key-name: Authorization

复制blog-article/src/main/resources/public.txtblog-question/src/main/resources/public.txt
复制blog-article/src/main/java/com/acaiblog/oauth2/config/JwtTokenStoreConfig.javablog-question/src/main/java/com/acaiblog/oauth2/config/JwtTokenStoreConfig.java
复制blog-article/src/main/java/com/acaiblog/oauth2/config/ResourceServerConfig.javablog-question/src/main/java/com/acaiblog/oauth2/config/ResourceServerConfig.java
测试接口:http://127.0.0.1:8003/system/swagger-ui.html#/

Swagger隐藏security相关接口

Swagger-ui页面会多有security相关监控请求接口,可以把它们隐藏。编辑Nacos配置文件剔除以/actuator开头的请求,就不会显示

swagger:
description: '博客分类、标签、文章、广告接口'
exclude-path: /actuator/**,/error

Feign请求拦截器

需求

PUT方式请求更新用户接口:http://localhost:8003/system/user 报以下的错

feign.FeignException$Unauthorized: [401] during [PUT] to [http://article-server/article/feign/article/user] [IFeignArticleController#updateUserInfo(UserInfoREQ)]

解决方式

微服务加上安全配置后,微服务之间使用Feign进行远程调用也需要携带JWT令牌,通过Feign拦截器实现携带JWT远程调用。

定义拦截器

在blog-api中创建Feign拦截器com.acaiblog.interceptor.FeignRequestInterceptor

package com.acaiblog.interceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
* Feign 拦截器让请求头携带 token
*/
@Component
public class FeignRequestInterceptor implements RequestInterceptor {

@Override
public void apply(RequestTemplate requestTemplate) {
// 使用 RequestContextHolder 工具获取 request 相关变量
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
// 获取 request
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StringUtils.isNotBlank(token)) {
requestTemplate.header(HttpHeaders.AUTHORIZATION, token);
}
}
}
}

测试

查询问题详情接口,此接口远程调用 文章微服务 的标签数据。

  1. 重启blog-question微服务
  2. 访问:http://127.0.0.1:8002/question/swagger-ui.html#/
  3. 发送GET请求:http://127.0.0.1:8002/question/feign/question/user 能够正常返回数据

资源服务器获取认证用户信息

测试获取用户信息

编辑blog-question/src/main/java/com/acaiblog/api/ApiQuestionController.java view中添加以下代码

public Result view(@PathVariable("id") String id) {
// 获取从Security上下文中获取认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 获取用户详情
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();

return questionService.findById(id);
}

添加加载扩展信息的转换器

需要添加加载扩展信息的转换器才可以获取到用户信息。编辑blog-system/src/main/java/com/acaiblog/system/oauth2/config/JwtTokenStoreConfig.java类中创建一个内部类的DefaultAccessTokenConverter子类,实现转换逻辑。并添加到JwtAccessTokenConverter

package com.acaiblog.system.oauth2.config;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import java.io.IOException;
import java.util.Map;

@Configuration(value = "jwtTokenStoreConfigSystem")
public class JwtTokenStoreConfig {
Logger logger = LoggerFactory.getLogger(getClass());

private class CustomAccessTokenConverter extends DefaultAccessTokenConverter {
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
OAuth2Authentication authentication = super.extractAuthentication(claims);
return authentication;
}

}

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 非对称加密,资源服务器使用公钥解密 public.txt
ClassPathResource resource = new ClassPathResource("public.txt");
// ClassPathResource resource = new ClassPathResource("private.key");
String publicKey = null;
try {
publicKey = IOUtils.toString(resource.getInputStream(), "UTF-8");
} catch (IOException e) {
logger.error("Error reading public key from file", e);
e.printStackTrace();
}
converter.setVerifierKey(publicKey);
// 定制 AccessToken 转换器添加扩展内容到JWT的转换器中 ++++++++++++++++
converter.setAccessTokenConverter(new CustomAccessTokenConverter());
return converter;

}
}

封装工具类获取用户信息AuthUtil

blog-question创建com.acaiblog.question.oauth2.config.AuthUtil封装获取用户信息工具类

package com.acaiblog.question.oauth2.config;

import com.acaiblog.entities.SysUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;

import java.util.Map;
import java.util.Objects;

public class AuthUtil {
/**
* 获取用户信息
* @return
*/
public static SysUser getUserInfo() {
// 获取从Security上下文中获取认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
Map<String, Object> map = (Map<String, Object>) details.getDecodedDetails();
Map<String, Object> userInfo = (Map<String, Object>) map.get("userInfo");
SysUser user = new SysUser();
String mobile = (String) userInfo.get("mobile");
user.setId((String) userInfo.get("uid"));
user.setUsername((String) userInfo.get("username"));
user.setEmail((String) userInfo.get("email"));
user.setNickName((String) userInfo.get("nickName"));
user.setImageUrl((String) userInfo.get("imageUrl"));
return user;
}
}

重构文章和系统微服务

重构文章和系统微服务获取用户信息。将blog-question/src/main/java/com/acaiblog/question/oauth2/config目录中的JwtTokenStoreConfigAuthUtil类拷贝到重构文章和系统微服务的对应目录下即可。

方法级别权限

在blog-article微服务中检查资源配置类com.acaiblog.article.oauth2.config.ResourceServerConfig是否开启了方法级别权限控制

package com.acaiblog.article.oauth2.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfiguration;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;

@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级别权限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
}

在对应请求方法上添加PreAuthorize权限注解,当调用请求前进行校验是否有权限方法。 其中的article:search对应的是sys_menu表中权限编码code字段值, 拥有此编码用户即可方法此方法。

@PreAuthorize("hasAuthority('article:search')")
@ApiOperation("根据文章标题与状态查询文章分页列表接口")
@PostMapping("/search")
public Result search(@RequestBody ArticleREQ req) { // @RequestBody将前端post请求中的data传递给接口
return articleService.queryPage(req);
}

测试:发送POST请求:http://127.0.0.1:8001/article/article/search

Gateway统一网关和限流微服务

概述

网关的作用相当于一个过虑器、拦截器,它可以拦截多个服务的请求。使用网关校验用户的身份是否合法。

  1. 用户请求某个资源服务前,需要先通过网关访问Oauth2认证授权服务请求一个AccessToken
  2. 用户通过认证授权服务得到AccessToken后,通过api网关调用其他资源服务A、B、C
  3. 资源服务根据AccessToken验证该token的用户请求是否有效。

搭建网关微服务

创建blog-gateway模块

配置pom.xml

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

<artifactId>blog-gateway</artifactId>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<!-- gateway 路由网关依赖-->
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- gateway 结合 Redis 实现限流-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!-- 解析jwt-->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
</dependency>
<!-- nacos 客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- nacos 配置中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- 热部署 ctrl+f9-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

application.yml配置文件

创建blog-gateway/src/main/resources/application.yml配置文件

server:
port: 6001
spring:
application:
name: gateway-server
redis:
host: acaiblog.top
port: 6379
password: 123456
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848

启动类

创建启动类com.acaiblog.GatewayServerApplication

package com.acaiblog;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

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

登录Nacos控制台查看微服务是否被注册

开启服务名区分转发请求

application.yml中添加开启使用服务名称调用目标服务的配置

server:
port: 6001
spring:
application:
name: gateway-server
redis:
host: acaiblog.top
port: 6379
password: 123456
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
# 开启使用服务名称调用目标服务
enabled: true

配置路由前缀转发请求

编辑blog-gateway/src/main/resources/application.yml

server:
port: 6001
spring:
application:
name: gateway-server
redis:
host: acaiblog.top
port: 6379
password: 123456
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
routes:
- id: blog-article
uri: http://localhost:8081
predicates:
- Path=/article/**

- id: blog-question
uri: lb://question-server
predicates:
- Path=/question/**

- id: blog-system
uri: lb://system-server
predicates:
- Path=/system/**

- id: blog-auth
uri: lb://auth-server
predicates:
- Path=/auth/**

网关限流

需求

限制每个ip地址1秒可发送的多少个请求。如果超过限制的请求返回429错误。

添加依赖

<!-- gateway 结合 Redis 实现限流 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>
<dependency>

配置application.yml

server:
port: 6001
spring:
application:
name: gateway-server
redis:
host: acaiblog.top
port: 6379
password: 123456
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
routes:
- id: blog-article
uri: http://localhost:8081
predicates:
- Path=/article/**
filters:
# 开启限流 RequestRateLimiter
- name: RequestRateLimiter
args:
# 限流过滤器的 Bean 名称
key-resolver: "#{@uriKeyResolver}"
# 希望允许用户每秒执行多少个请求。令牌桶填充的速率。
redis-rate-limiter.replenishRate: 2
# 允许用户在一秒钟内完成的最大请求数。 这是令牌桶可以容纳的令牌数量,将此值设置为零将阻
#止所有请求
redis-rate-limiter.burstCapacity: 4

- id: blog-question
uri: lb://question-server
predicates:
- Path=/question/**
filters:
# 开启限流 RequestRateLimiter
- name: RequestRateLimiter
args:
# 限流过滤器的 Bean 名称
key-resolver: "#{@uriKeyResolver}"
# 希望允许用户每秒执行多少个请求。令牌桶填充的速率。
redis-rate-limiter.replenishRate: 2
# 允许用户在一秒钟内完成的最大请求数。 这是令牌桶可以容纳的令牌数量,将此值设置为零将阻
#止所有请求
redis-rate-limiter.burstCapacity: 4

- id: blog-system
uri: lb://system-server
predicates:
- Path=/system/**
filters:
# 开启限流 RequestRateLimiter
- name: RequestRateLimiter
args:
# 限流过滤器的 Bean 名称
key-resolver: "#{@uriKeyResolver}"
# 希望允许用户每秒执行多少个请求。令牌桶填充的速率。
redis-rate-limiter.replenishRate: 2
# 允许用户在一秒钟内完成的最大请求数。 这是令牌桶可以容纳的令牌数量,将此值设置为零将阻
#止所有请求
redis-rate-limiter.burstCapacity: 4

- id: blog-auth
uri: lb://auth-server
predicates:
- Path=/auth/**
filters:
# 开启限流 RequestRateLimiter
- name: RequestRateLimiter
args:
# 限流过滤器的 Bean 名称
key-resolver: "#{@uriKeyResolver}"
# 希望允许用户每秒执行多少个请求。令牌桶填充的速率。
redis-rate-limiter.replenishRate: 2
# 允许用户在一秒钟内完成的最大请求数。 这是令牌桶可以容纳的令牌数量,将此值设置为零将阻
#止所有请求
redis-rate-limiter.burstCapacity: 4

网关限流过滤器

在blog-gateway模块创建com.acaiblog.filter.UriKeyResolver

package com.acaiblog.filter;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
* 网关限流过滤器
*/
@Component("uriKeyResolver")
public class UriKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getURI().getPath());
}
}

测试限流

  1. 启动redis , redis-server版本要用3以上的版本.
  2. 数据在redis中存储的时间只有几秒,所以得使用monitor指令来动态的观察.
  3. 浏览顺频繁发送:http://localhost:6001/article/api/article/1 当每秒达到4次请求后,每秒只能请求2次了。

自定义认证过滤器转发请求

概述

Gateway的核心就是过虑器,通过过虑器实现请求过虑,身份校验等。自定义过虑器需要实现全局过滤器GlobalFilterOrdered接口,分别实现接口中的如下方法:
filter:过滤器的业务逻辑。
getOrder:此方法返回整型数值,通过此数值来定义过滤器的执行顺序,数字越小优先级越高。

认证过滤器

创建自定义过滤器com.acaiblog.filter.AuthenticationFilter,实现GlobalFilter,Ordered,实现抽象方法。
所有请求(包括获取token的认证请求)都经过此过虑器,判断请求头部信息是否有Authorization,如果有转发到微服务,否则拒绝访问。

package com.acaiblog.filter;

import com.alibaba.fastjson.JSONObject;
import com.google.common.net.HttpHeaders;
import org.apache.commons.lang.StringUtils;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;

/**
* API网关统一拦截器,用于验证请求的请求头是否有 `Authorization`
*/
@Component
public class AuthenticationFilter implements GlobalFilter, Ordered {
/**
* 白名单:直接放行请求前缀
*/
private static final String[] white = {"/api/", ""};

private Logger logger = (Logger) LoggerFactory.getLogger(getClass());
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = (ServerHttpRequest) exchange.getRequest();
ServerHttpResponse response = (ServerHttpResponse) exchange.getResponse();
// 请求路径
String path = request.getPath().pathWithinApplication().value();
logger.info(String.format("发送{} 请求到{}", request.getMethod(), path));
// 公开API接口,无需认证
if (StringUtils.indexOfAny(path, white) != -1) {
// 直接放行
return chain.filter(exchange);
}
// 获取请求头中 key 为 "Authorization" 的值,
// 获取token时,要带上 Authorization : Basic client_id:client_secret
// 请求应用接口,要带上 Authorization : Bearer token
String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
// 如果请求路径中不存在,不转发请求,响应提示
if (StringUtils.isEmpty(authorization)) {
// 响应消息内容对象
JSONObject message = new JSONObject();
// 响应状态
message.put("code", 1401);
message.put("message", "缺少身份凭证");
// 转换响应消息内容对象为字节
byte[] bits = message.toJSONString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
// 设置响应对象状态码 401
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 设置响应对象内容并且指定编码,否则在浏览器中会中文乱码
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
// 返回响应对象
return response.writeWith(Mono.just(buffer));

}
logger.info("请求头有 Authorization 放行请求");
// 如果不为空,就通过,并接收调用目标服务后响应的结果
return chain.filter(exchange);
}

@Override
public int getOrder() {
// // 过滤器执行优先级,越小越优先
return 0;
}
}

自定义令牌校验过滤器

需求

如果请求头带有Bearer类型访问令牌,则校验访问jwt令牌是否已过期,Redis中不存在则已过期。
JWT由三部分组成(Header,Payload,Signature):

  1. Header头部 :用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。base64enc({ "alg":"HS256","TYPE":"JWT"}) // eyAiYWxnIjoiSFMyNTYiLCJUWVBFIjoiSldUIn0=
  2. Payload载荷:可以把用户名、角色等无关紧要的信息保存到Payload部分。base64enc({"user":"vichin","pwd":"weichen123"}) // 用户的关键信息eyJ1c2VyIjoidmljaGluIiwicH
  3. Signature(签名): Signature部分是根据header+payload+secretKey进行加密算出来的,如果Payload被篡改,就可以在解密Signature的时候校验是否被篡改。HMACSHA256(base64enc(header)+","+base64enc(payload), secretKey)

HeaderPayload部分使用的是Base64编码,几乎等于明文,可直接解析出来 。校验是否被篡改就是通过解密第3部分签名 , 解密成功就没有被篡改,解密失败就被篡改。

令牌校验过滤器

在blog-gateway模块中创建com.acaiblog.filter.JwtTokenStoreConfig

package com.acaiblog.filter;

import com.nimbusds.jose.JWSObject;
import net.minidev.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;

@Component // 不要少了
public class AccessTokenFilter implements GlobalFilter, Ordered {
Logger logger = LoggerFactory.getLogger(getClass());


@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 校验请求头中的令牌是否有效,查询redis中是否存在 ,不存在则无效jwt
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 请求对象
ServerHttpRequest request = exchange.getRequest();
// 响应对象
ServerHttpResponse response = exchange.getResponse();

// 获取token
String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
String token = StringUtils.substringAfter(authorization, "Bearer ");

if(StringUtils.isEmpty(token)) {
// 如果为空,可能是白名单的请求,则直接放行
return chain.filter(exchange);
}
// 响应错误信息
String message = null;

try {
JWSObject jwsObject = JWSObject.parse(token);
JSONObject jsonObject = jwsObject.getPayload().toJSONObject();

// 校验redis中是否存在对应jti的token
String jti = jsonObject.get("jti").toString();
// 查询是否存在
Object value = redisTemplate.opsForValue().get(jti);
if(value == null) {
logger.info("令牌已过期 {}", token);
message = "您的身份已过期, 请重新认证!";
}

} catch (ParseException e) {
logger.error("解析令牌失败 {}", token);
message = "无效令牌";
}

if(message == null) {
// 如果令牌存在,则通过
return chain.filter(exchange);
}

// 响应错误提示信息
JSONObject result = new JSONObject();
result.put("code", 1401);
result.put("message", message);

// 转换响应消息内容对象为字节
byte[] bits = result.toJSONString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
// 设置响应对象状态码 401
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 设置响应对象内容并且指定编码,否则在浏览器中会中文乱码
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
// 返回响应对象
return response.writeWith( Mono.just(buffer) );
}

@Override
public int getOrder() {
// 这个AccessTokenFilter过滤器在 AuthenticationFilter 之后执行
return 10;
}
}
文章作者: 慕容峻才
文章链接: https://www.acaiblog.top/SpringBoot搭建博客后台项目/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿才的博客
微信打赏
支付宝打赏