环境搭建 项目功能
模块
功能
博客门户
博客文章、问答管理、标签管理、评论管理
博客权限管理系统
分类管理、标签管理、文章审核、广告管理、问答管理、系统权限管理
技术栈
软件
版本
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 删除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 > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-dependencies</artifactId > <version > ${spring-cloud.version}</version > <type > pom</type > <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 > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > ${mybatis-plus.version}</version > </dependency > <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 > <dependency > <groupId > com.github.penggle</groupId > <artifactId > kaptcha</artifactId > <version > ${kaptcha.version}</version > </dependency > <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 > <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 > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > <resources > <resource > <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/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 > <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 > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency > <dependency > <groupId > com.spring4all</groupId > <artifactId > swagger-spring-boot-starter</artifactId > </dependency > <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 > <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" ?> <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}}" /> <appender name ="stdout" class ="ch.qos.logback.core.ConsoleAppender" > <layout class ="ch.qos.logback.classic.PatternLayout" > <pattern > ${CONSOLE_LOG_PATTERN}</pattern > </layout > </appender > <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 { 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
对数据库操作,而它提供了对应的分页功能,要将current
和size
封装到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;@Data @Accessors(chain = true) public class BaseRequest implements Serializable { @ApiModelProperty(value = "页码", required = true) private long current; @ApiModelProperty(value = "每页显示多少条", required = true) private long size; @ApiModelProperty(hidden = true) 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
远程调用的接口,原因如下:
接口的定义离不开数据模型,所以统一在此处定义模型类。
API模块中定义的接口将作为各微服务间远程调用使用,Spring Cloud Feign
中使用。
接口统一管理,方便维护。
创建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 > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency > </dependencies > </project >
博客文章模块 创建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(value = "Pet Store API", tags = "Pet Operations")
@ApiOperation @ApiOperation(value = "Get pet by ID", notes = "Returns a pet based on ID", nickname = "getPetById")
@ApiParam @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 @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 @ApiResponse(code = 200, message = "Successful operation")
@ApiModel @ApiModel(value = "Pet", description = "Pet information")
@ApiModelProperty @ApiModelProperty(value = "Pet name", example = "Buddy")
Swagger使用 在blog-util工程的pom.xml文件中添加第三方swagger依赖
<dependency > <groupId > com.spring4all</groupId > <artifactId > swagger-spring-boot-starter</artifactId > </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 @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; @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 ; @TableId private String id; private String name; private String remark; 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; @ApiModelProperty(value = "分类状态") private Integer status; }
编写CategoryMapper 创建接口com.acaiblog.article.mapper.CategoryMapper
继承BaseMapper<Category>
接口。MyBatis-Plus
的BaseMapper<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 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 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 mapper-locations: classpath*:com/acaiblog/article/mapper/**/*.xml 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> { 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 @RequestMapping("/category") public class CategoryController { @Autowired ICategoryService categoryService; @PostMapping("/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") @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 @RequestMapping("/category") public class CategoryController { @Autowired ICategoryService categoryService; @ApiOperation("查询类别详情接口") @ApiImplicitParam(name = "id", value = "类别ID", required = true) @GetMapping("/{id}") public Result view (@PathVariable("id") String id) { return Result.ok(categoryService.getById(id)); } @ApiOperation("更新类别信息接口") @PutMapping public Result update (@RequestBody Category category) { categoryService.updateById(category); return Result.ok(); } @ApiOperation("新增类别接口") @PostMapping 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-plus
的ServiceImpl
类中提供的updateById
方法
package com.acaiblog.article.service.impl;import org.springframework.stereotype.Service;@Service public class CategoryServiceImpl extends ServiceImpl <CategoryMapper, Category> implements ICategoryService { 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> { Result queryPage (CategoryREQ req) ; 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 ); return Result.ok(baseMapper.selectList(wrapper)); } }
编辑com.acaiblog.article.controller.CategoryController
类中实现list方法
package com.acaiblog.article.controller;@RestController @RequestMapping("/category") public class CategoryController { @Autowired ICategoryService categoryService; @ApiOperation("查询状态正常的类别接口") @GetMapping("/list") public Result list () { return categoryService.findAllNormal(); } }
MyBatis-plus代码生成器 MyBatis-Plus代码生成器是MyBatis-Plus框架的一个重要组件,主要用于生成与数据库表结构对应的实体类、Mapper接口以及基本的XML映射文件。参考官网:https://mybatis.plus/guide/generator.html
创建生成器模块
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" ; 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); globalConfig.setAuthor("阿才的博客" ); globalConfig.setFileOverride(true ); globalConfig.setOpen(false ); globalConfig.setDateType(DateType.ONLY_DATE); globalConfig.setSwagger2(true ); 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 ); strategyConfig.setEntitySerialVersionUID(true ); strategyConfig.setRestControllerStyle(true ); 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.java
中main
方法: 执行报错:
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) 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;public interface LabelMapper extends BaseMapper <Label> { 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;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;@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;@RestController @Api(value = "标签管理接口", description = "标签管理接口,提供增删改查") @RequestMapping("//label") public class LabelController { @Autowired ILabelService labelService; @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.*;@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;@Service public class LabelServiceImpl extends ServiceImpl <LabelMapper, Label> implements ILabelService { @Override public boolean updateById (Label label) { label.setUpdateDate(new Date ()); return super .updateById(label); } }
接口测试
查询,GET:localhost:8001/article/label/1
新增,POST 方式,请求地址:localhost:8001/article/label
修改,PUT 方式,请求地址:localhost:8001/article/label
删除,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> { 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" /> <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> { 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 @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; @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 @Accessors @ApiModel(value = "ArticleREQ对象", description = "文章查询条件") 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;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;@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;@Api(value = "文章管理接口", description = "文章管理接口,提供增删改查") @RestController @RequestMapping("/article") public class ArticleController { @Autowired private IArticleService articleService; @ApiOperation("根据文章标题与状态查询文章分页列表接口") @PostMapping("/search") public Result search (@RequestBody ArticleREQ req) { 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;@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;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" > <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" /> <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;public interface IArticleService extends IService <Article> { 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;@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.*;@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;public interface ArticleMapper extends BaseMapper <Article> { boolean deleteArticleLabel (@Param("articleId") String articleId) ; 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" > <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;public interface IArticleService extends IService <Article> { 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;@Service public class ArticleServiceImpl extends ServiceImpl <ArticleMapper, Article> implements IArticleService { @Transactional @Override public Result updateOrSave (Article article) { 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()); } super .saveOrUpdate(article); 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.*;@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.*;@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;public interface IArticleService extends IService <Article> { 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;@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); 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.*;@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.*;@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;public interface IArticleService extends IService <Article> { 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;@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.*;@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;public interface IArticleService extends IService <Article> { 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;@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.*;@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;public interface IArticleService extends IService <Article> { 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;@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.*;@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;public interface ArticleMapper extends BaseMapper <Article> { 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;public interface IArticleService extends IService <Article> { 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.*;@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); if (data != null ) { return Result.ok(data); } else { 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.*;@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;public interface ArticleMapper extends BaseMapper <Article> { 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" > <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;public interface IArticleService extends IService <Article> { 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.*;@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.*;@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;public interface IArticleService extends IService <Article> { 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.*;@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;public interface ArticleMapper extends BaseMapper <Article> { 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" > <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;public interface IArticleService extends IService <Article> { 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.*;@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
脚本生成评论代码模版
新增评论接口 控制层 新增评论接口直接引用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;@RestController @RequestMapping("/comment") public class CommentController { @Autowired 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;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;@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 <>(); ids.add(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;@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;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" > <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" /> <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;public interface ICommentService extends IService <Comment> { 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;@Service public class CommentServiceImpl extends ServiceImpl <CommentMapper, Comment> implements ICommentService { @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以下功能:
开通对象存储OSS服务
创建Bucket
设置Bucket公共读写权限
添加OSS SDK依赖 编辑pom.xml
<project xmlns ="http://maven.apache.org/POM/4.0.0" > <dependencies > <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") 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 { private String endpoint; private String accessKey; private String accessKeySecret; private String bucketName; 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 { public static Result uploadFileToOss (PlatformEnum platformEnum, MultipartFile file, AliyunProperties aliyunProperties) { String folderName = platformEnum.name().toLowerCase() + "/" + DateFormatUtils.format(new Date (),"yyyMMdd" ); String fileName = UUID.randomUUID().toString().replace("-" ,"" ); String fileExtensionName = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("." )); String filePath = folderName + "/" + fileName + fileExtensionName; OSS ossClient = null ; try { ossClient = new OSSClientBuilder ().build(aliyunProperties.getEndpoint(), aliyunProperties.getAccessKey(), aliyunProperties.getAccessKeySecret()); PutObjectResult putObjectResult = ossClient.putObject(aliyunProperties.getBucketName(), filePath, file.getInputStream()); ResponseMessage responseMessage = putObjectResult.getResponse(); if (responseMessage == null ) { return Result.ok(aliyunProperties.getBucketDomain() + filePath); } else { 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(); } } } 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.java
main方法生成代码模版
列表接口 分页请求类 将查询条件广告标题和状态属性封装成一个 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;public interface IAdvertService extends IService <Advert> { 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;@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.*;@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;public interface IAdvertService extends IService <Advert> { 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;@Service public class AdvertServiceImpl extends ServiceImpl <AdvertMapper, Advert> implements IAdvertService { @Autowired private BlogProperties blogProperties; @Override @Transactional public Result deleteById (String id) { String imageUrl = baseMapper.selectById(id).getImageUrl(); baseMapper.deleteById(id); 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.*;@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;@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;public interface IAdvertService extends IService <Advert> { 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;@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 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL mybatis-plus: type-aliases-package: com.mengxuegu.blog.entities mapper-locations: classpath*:com/acaiblog/blog/question/mapper/**/*.xml blog: aliyun: endpoint: http://oss-cn-shenzhen.aliyuncs.com accessKeySecret: xxx bucketName: mengxuegu bucketDomain: https://mengxuegu.oss-cn-shenzhen.aliyuncs.com/ 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
生成模版代码
创建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") @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;public interface QuestionMapper extends BaseMapper <Question> { boolean deleteQuestionLabel (@Param("questionId") String questionId) ; 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;public interface IQuestionService extends IService <Question> { 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;@Service public class QuestionServiceImpl extends ServiceImpl <QuestionMapper, Question> implements IQuestionService { @Override public Result updateOrSave (Question question) { 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.*;@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;public interface IQuestionService extends IService <Question> { Result deleteById (String id) ; 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;@Service public class QuestionServiceImpl extends ServiceImpl <QuestionMapper, Question> implements IQuestionService { @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.*;@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;public interface IQuestionService extends IService <Question> { 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;@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.*;@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;public interface IQuestionService extends IService <Question> { 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;@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 )); 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.*;@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;public interface IQuestionService extends IService <Question> { 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;@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.*;@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;public interface IQuestionService extends IService <Question> { 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;@Service public class QuestionServiceImpl extends ServiceImpl <QuestionMapper, Question> implements IQuestionService { @Override public Result findHotList (BaseRequest<Question> req) { QueryWrapper<Question> queryWrapper = new QueryWrapper <>(); 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;public interface IQuestionService extends IService <Question> { 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;@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;public interface IQuestionService extends IService <Question> { 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;@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 )); 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;public interface QuestionMapper extends BaseMapper <Question> { 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;public interface IQuestionService extends IService <Question> { 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;@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); } }
问答详情页 需求分析
查询问答详情与标签ids
feign远程调用Article微服务查询所属标签
查询问题对应问答列表
重构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;@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;public interface QuestionMapper extends BaseMapper <Question> { 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" /> <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;public interface IQuestionService extends IService <Question> { 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;@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("为查询到相关问题信息" ); } 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;public interface IQuestionService extends IService <Question> { 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;@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 { 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> { 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); this .getIds(ids, id); } } } @Transactional @Override public Result deleteById (String id) { if (StringUtils.isBlank(id)) { return Result.error("回答评论ID不能为空" ); } ArrayList<String> ids = new ArrayList <>(); ids.add(id); this .getIds(ids, id); Replay replay = baseMapper.selectById(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> { 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服务端注册与发现。
Nacos Server是服务端,负责管理各个微服务注册和发现。
在微服务上添加Nacos Client代码,就会访问到Nacos Server将此微服务注册在Nacos Server中,从而使其他微服务消费方能够找到。
微服务(服务消费者)需要调用另一个微服务(服务提供者)时,从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 > <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 > <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#" ); } }
测试
启动nacos server
检查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
登录Nocos控制台,查看服务列表中,已经显示了文章微服务注册上来的服务信息。其中服务名article-server
就是对应application.yml
中的spring.application.name
的值
问答微服务注册 添加依赖 编辑blog-question/pom.xml
,maven》reload
<dependency > <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配置中心的作用
集中管理配置文件
不同环境不同配置,动态化的配置更新,根据不同环境部署,如dev/test/prod
运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置,服务会向配置中心统一拉取自已的配置信息
当配置发生变动时,服务不需要重启即可感知到配置的变化并使用修改后的配置信息
将配置信息以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
配置内容的数据格式,支持 properties
和 yaml/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 ,返回以下数据
修改Nacos数据库为MySQL 在mysql数据库创建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
重新运行项目测试
多环境下切换配置文件
在nacos控制台创建Data ID为article-server-dev.yml的配置信息,配置内容复制blog-article服务的application.yml内容过去
在nacos控制台创建Data ID为article-server-prod.yml的配置信息,配置内容复制blog-article服务的application.yml内容过去
方式一:引用dev开发环境配置,在bootstrap.yml配置文件添加spring.profiles.active=dev
,这种写死在配置文件中不推荐
方式二:通过IDE编辑启动文件,设置Actives profiles为dev
注释application.yml文件中的内容,启动测试
打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创建开发和生产配置文件
在Nacos创建question-server-dev.yml
和question-server-prod.yml
配置文件,文件内容复制application.yml文件
注释application.yml文件内容,启动测试
Nacos与Feign服务间接口调用 Feign简介 在分页式微服务架构的项目中,服务间可能需要存在接口间的调用 ,比如:问答微服务 需要调用 文章微服务 查询标签信息。目前调用大都用的是Feign
去调用其他服务接口, 为服务调用提供了更优雅的方式。Feign
是Netflix
公司开源的轻量级Rest客户端( https://github.com/OpenFeign/feign ),使用Feign可以非常方便、简单的实现Http客户端,使用Feign
只需要定义一个接口,然后在接口上添加注解即可。Spring Cloud
已对Feign
进行了封装。 Feign
接口统一在blog-api
模块中进行定义,方便统一管理。
需求分析 问答微服务需要调用文章微服务查询标签信息。在问答详情页接口预留了一个通过标签ids获取标签名,标签名是位于文章微服务中,这样我们需要在 问答微服务远程调用文章微服务接口来进行获取。
添加依赖 编辑blog-api/pom.xml
<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
<dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency >
定义文章Feign接口与实现 复制blog-article/src/main/java/com/acaiblog/entities/Label.java
到blog-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;@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;@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("为查询到相关问题信息" ); } 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 > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-alibaba-nacos-discovery</artifactId > <version > 2.2.0.RELEASE</version > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-alibaba-nacos-config</artifactId > </dependency > <dependency > <groupId > org.springframework.security</groupId > <artifactId > spring-security-crypto</artifactId > </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 >
创建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 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 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 mapper-locations: classpath*:com/acaiblog/article/mapper/**/*.xml 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") @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;@Data @EqualsAndHashCode(callSuper = false) @ApiModel(value="SysMenu对象", description="菜单信息表") public class SysMenu implements Serializable { private static final long serialVersionUID = 1L ; @ApiModelProperty(value = "子菜单集合") @TableField(exist = false) 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;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;@Service public class SysMenuServiceImpl extends ServiceImpl <SysMenuMapper, SysMenu> implements ISysMenuService { private List<SysMenu> childrenMenu (List<SysMenu> menuList, SysMenu menu) { List<SysMenu> children = new ArrayList <>(); for (SysMenu m: menuList) { if (m.getParentId().equals(menu.getId())) { children.add((SysMenu) childrenMenu(menuList,m)); } } menu.setChildren(children); return (List<SysMenu>) menu; } private List<SysMenu> buildTree (List<SysMenu> menuList) { List<SysMenu> rootMenuList = new ArrayList <>(); for (SysMenu menu: menuList) { 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()); } 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;@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;public interface ISysMenuService extends IService <SysMenu> { 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;@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("菜单不存在" ); } LambdaQueryWrapper<SysMenu> lambdaQueryWrapper = new LambdaQueryWrapper <>(); lambdaQueryWrapper.eq(SysMenu::getParentId, id); baseMapper.delete(lambdaQueryWrapper); 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.*;@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;@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;public interface ISysRoleService extends IService <SysRole> { 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;@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;@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;@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(); } }
删除接口 需求分析
通过角色id删除角色信息表数据sys_role
通过角色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;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;public interface ISysRoleService extends IService <SysRole> { 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;@Service public class SysRoleServiceImpl extends ServiceImpl <SysRoleMapper, SysRole> implements ISysRoleService { @Override public Result deleteById (String id) { baseMapper.deleteById(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;@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_role
,sys_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;public interface SysRoleMapper extends BaseMapper <SysRole> { 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;public interface ISysRoleService extends IService <SysRole> { 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;@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;@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
新增角色关系数据 需求分析
根据角色id查询此角色拥有的权限菜单ids,涉及表sys_role
,sys_role_menu
。
新增角色菜单权限数据到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;public interface SysRoleMapper extends BaseMapper <SysRole> { 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;public interface ISysRoleService extends IService <SysRole> { 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;@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;@Api(tags = "Role", description = "角色管理接口") @RestController @RequestMapping("/role") public class SysRoleController { @Autowired ISysRoleService sysRoleService; @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;public interface ISysUserService extends IService <SysUser> { 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;@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;@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_user
,sys_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;public interface SysUserMapper extends BaseMapper <SysUser> { 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;public interface ISysUserService extends IService <SysUser> { 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;@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.*;@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
新增用户角色接口 需求分析
根据用户id查询此用户拥有的角色ids,涉及表sys_user
,sys_user_role
新增用户角色关系数据到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;public interface SysUserMapper extends BaseMapper <SysUser> { boolean deleteUserRoleByUserId (@Param("userId") String id) ; 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" > <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;public interface ISysUserService extends IService <SysUser> { 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;@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;@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;public interface ISysUserService extends IService <SysUser> { 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;@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 ); 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;@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
<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;@Api(tags = "User", description = "用户管理接口") @RestController @RequestMapping("/user") public class SysUserController { @Autowired private ISysUserService sysUserService; @Autowired private PasswordEncoder passwordEncoder; @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
修改用户密码接口 需求分析
检查原密码是否正确
提交修改后的新密码
修改密码请求类 创建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;public interface ISysUserService extends IService <SysUser> { Result checkPassword (SysUserCheckPasswordREQ req) ; 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;@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;@Api(tags = "User", description = "用户管理接口") @RestController @RequestMapping("/user") public class SysUserController { @Autowired private ISysUserService sysUserService; @Autowired private PasswordEncoder passwordEncoder; @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
修改用户信息接口 需求分析
判断用户的昵称或头像是否修改,如果被修改则更新文章微服务blog-article和问答微服务blog-question中对应表的用户nick_name
、user_image
值。调用文章和问答微服务的远程接口来实现更新。
更新blog_system
中sys_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;public interface ArticleMapper extends BaseMapper <Article> { 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;public interface IArticleService extends IService <Article> { 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.*;@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;@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;public interface QuestionMapper extends BaseMapper <Question> { 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;public interface IQuestionService extends IService <Question> { 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;@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;public interface ISysUserService extends IService <SysUser> { 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;@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;@Api(tags = "User", description = "用户管理接口") @RestController @RequestMapping("/user") public class SysUserController { @Autowired private ISysUserService sysUserService; @Autowired private PasswordEncoder passwordEncoder; @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;public interface ISysUserService extends IService <SysUser> { 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;@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 <>(); 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;@Api(tags = "User", description = "用户管理接口") @RestController @RequestMapping("/user") public class SysUserController { @Autowired private ISysUserService sysUserService; @Autowired private PasswordEncoder passwordEncoder; @ApiOperation("统计用户数接口") @GetMapping("/total") public Result getUserTotal () { return sysUserService.getUserTotal(); } }
测试 发送GET请求:http://127.0.0.1:8003/system/user/total
获取用户菜单权限接口 需求分析 通过用户ID查询出这个用户拥有的权限菜单,查询后过滤出目录和菜单类型数据,不要按钮。因为用于当用户登录后获取用户菜单,来渲染后台管理系统的左侧导航菜单。涉及表sys_user
、sys_user_role
、sys_role
、sys_role_menu
、sys_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;public interface SysMenuMapper extends BaseMapper <SysMenu> { 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;public interface ISysMenuService extends IService <SysMenu> { 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.*;@Service public class SysMenuServiceImpl extends ServiceImpl <SysMenuMapper, SysMenu> implements ISysMenuService { @Override public Result findUserMenuTree (String id) { List<SysMenu> menuList = baseMapper.findByUserId(id); 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 { 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;@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;public interface ISysUserService extends IService <SysUser> { 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;@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); 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;public interface ISysUserService extends IService <SysUser> { 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;@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.protocol} //${window .location.host} /xieyi.html` , method : 'get' }) }
编辑vue.config.js
,以/dev-api/system
开头的请求代理到 http://localhost:8003
module .exports = { devServer : { port : 8080 , 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 ]: '' } }, [process.env .VUE_APP_BASE_API ] :{ target : process.env .VUE_APP_SERVICE_URL , changeOrigin : true , pathRewrite : { [ '^' + process.env .VUE_APP_BASE_API ]: '' }, } } }, lintOnSave : false , productionSourceMap : false , }
测试注册用户,查看数据库是否新增用户信息
权限管理系统 修改前端部分blog-admin\vue.config.js
中的代理配置如下:以/dev-api/system
开头的请求代理到 http://localhost:8003
devServer : { port : port, open : false , overlay : { warnings : false , errors : true }, 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' , 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 > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-oauth2</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > </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 >
配置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 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 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 mapper-locations: classpath*:com/acaiblog/auth/mapper/**/*.xml 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;public interface ISysUserService extends IService <SysUser> { 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;@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;public interface ISysMenuService extends IService <SysMenu> { 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;@Service public class SysMenuServiceImpl extends ServiceImpl <SysMenuMapper, SysMenu> implements ISysMenuService { @Override public List<SysMenu> findByUserId (String id) { List<SysMenu> menuList = baseMapper.findByUserId(id); 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;@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; @Override public SysUser findUserByUsername (String username) { return sysUserService.findByUsername(username); } @Autowired private ISysMenuService sysMenuService; @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) @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; @JSONField(serialize = false) @ApiModelProperty(value = "帐户是否过期(1 未过期,0已过期)") private boolean isAccountNonExpired; @JSONField(serialize = false) @ApiModelProperty(value = "帐户是否被锁定(1 未过期,0已过期)") private boolean isAccountNonLocked; @JSONField(serialize = false) @ApiModelProperty(value = "密码是否过期(1 未过期,0已过期)") private boolean isCredentialsNonExpired; @JSONField(serialize = false) @ApiModelProperty(value = "帐户是否可用(1 可用,0 删除用户)") private boolean isEnabled; @JSONField(serialize = false) private List<GrantedAuthority> authorities; 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 private IFeignSystemController feignSystemController; @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { if (StringUtils.isEmpty(username)) { throw new BadCredentialsException ("用户名不能为空" ); } SysUser sysUser = feignSystemController.findUserByUsername(username); if (sysUser == null ) { throw new BadCredentialsException ("用户名或密码错误" ); } List<SysMenu> menuList = feignSystemController.findMenuByUserId(sysUser.getId()); List<GrantedAuthority> authorities = null ; if (CollectionUtils.isNotEmpty(menuList)) { authorities = new ArrayList <>(); for (SysMenu menu : menuList) { String code = menu.getCode(); authorities.add(new SimpleGrantedAuthority (code)); } } 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-oauth2
的resources
文件夹下。
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 (); KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory ( new ClassPathResource ("oauth2.jks" ), "oauth2" .toCharArray() ); jwtAccessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("oauth2" )); return jwtAccessTokenConverter; } @Bean public TokenStore tokenStore () { 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 public ClientDetailsService jdbcClientDetailsService () { return new JdbcClientDetailsService (dataSource); } @Override public void configure (ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception { clientDetailsServiceConfigurer.withClientDetails(jdbcClientDetailsService()); } @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Autowired private TokenStore tokenStore; @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; @Override public void configure (AuthorizationServerEndpointsConfigurer endpointsConfigurer) throws Exception { endpointsConfigurer.authenticationManager(authenticationManager); endpointsConfigurer.userDetailsService(userDetailsService); endpointsConfigurer.tokenStore(tokenStore).accessTokenConverter(jwtAccessTokenConverter); } @Override public void configure (AuthorizationServerSecurityConfigurer serverSecurityConfigurer) throws Exception { 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 =用户名或密码错误 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 { 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请求工具依赖
<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 LoadBalancerClient loadBalancerClient; public Result refreshToken (String header, String refreshToken) throws HttpProcessException { ServiceInstance serviceInstance = loadBalancerClient.choose("auth-server" ); if (serviceInstance == null ) { return Result.error("未找到有效的认证微服务,请检查认证微服务是否注册到Nacos" ); } 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); 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
<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: host: localhost port: 6379 password:
使用redis @Resource private RedisTemplate redisTemplate;redisTemplate.opsForValue().set() redisTemplate.delete(key) redisTemplate.opsForValue().get(key)
Redis管理Token
Redis存储有效令牌
生成Jwt访问令牌的时候,将Jwt Token存入redis中
扩展Jwt的验证功能,验证redis中是否存在数据,如果存在则token有效,否则无效
退出系统时将Redis中的数据删除。
项目使用TokenStore为JwtTokenStore管理JWT访问令牌
存储令牌是调用JwtTokenStore
的storeAccessToken
方法,但这个方法是个空的,我们要覆盖该方法将token存入redis中。
客户端退出时,删除客户端存储的token, 并调用服务器的接口删除服务器上存储的令牌,删除令牌最终调用的是JwtTokenStore
的removeAccessToken
方法,所以只要实现该方法,就能达到删除令牌的效果。
编辑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 () { return new JwtTokenStore (jwtAccessTokenConverter()) { @Override public void storeAccessToken (OAuth2AccessToken token, OAuth2Authentication auth2Authentication) { if (token.getAdditionalInformation().containsKey("jti" )) { String jti = token.getAdditionalInformation().get("jti" ).toString(); redisTemplate.opsForValue().set( jti,token.getValue(),token.getExpiresIn(), TimeUnit.MINUTES ); super .storeAccessToken(token,auth2Authentication); } } @Override public void removeAccessToken (OAuth2AccessToken token) { if (token.getAdditionalInformation().containsKey("jti" )) { 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.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 ; 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 tokenRequest = new TokenRequest (MapUtils.EMPTY_MAP, clientId, clientDetails.getScope(), "custom" ); OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails); OAuth2Authentication oAuth2Authentication = new OAuth2Authentication (oAuth2Request, authentication); 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 { 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 { String accessToken = request.getParameter("accessToken" ); if (StringUtils.isNotEmpty(accessToken)) { OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(accessToken); if (oAuth2AccessToken != null ) { 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.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
<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 (); 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 () { 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 @EnableGlobalMethodSecurity(prePostEnabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Autowired private TokenStore tokenStore; @Override public void configure (ResourceServerSecurityConfigurer resourceServerSecurityConfigurer) { resourceServerSecurityConfigurer.tokenStore(tokenStore); } @Override public void configure (HttpSecurity httpSecurity) throws Exception { httpSecurity.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/v2/api-docs" , "/v2/feign-docs" , "/swagger-resources/configuration/ui" , "/swagger-resources" , "/swagger-resources/configuration/security" , "/swagger-ui.html" , "/webjars/**" ).permitAll() .antMatchers("/api/**" ).permitAll() .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" }
请求文章微服务接口
编辑Nacos配置article-server-dev.yml
文件,添加swagger: description: '博客分类、标签、文章、广告接口' authorization: key-name: Authorization
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" } }
访问 http://127.0.0.1:8001/article/swagger-ui.html# 》Authorize 》Value输入
发送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"
检查能够正常访问接口数据{ "code": 20000, "message": "成功", "data": [] }
问答微服务安全配置 添加依赖 编辑blog-question/pom.xml
<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.txt
到blog-question/src/main/resources/public.txt
复制blog-article/src/main/java/com/acaiblog/oauth2/config/JwtTokenStoreConfig.java
到blog-question/src/main/java/com/acaiblog/oauth2/config/JwtTokenStoreConfig.java
复制blog-article/src/main/java/com/acaiblog/oauth2/config/ResourceServerConfig.java
到blog-question/src/main/java/com/acaiblog/oauth2/config/ResourceServerConfig.java
测试接口:http://127.0.0.1:8002/question/swagger-ui.html#/
系统微服务安全配置 添加依赖 编辑blog-system/pom.xml
<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.txt
到blog-question/src/main/resources/public.txt
复制blog-article/src/main/java/com/acaiblog/oauth2/config/JwtTokenStoreConfig.java
到blog-question/src/main/java/com/acaiblog/oauth2/config/JwtTokenStoreConfig.java
复制blog-article/src/main/java/com/acaiblog/oauth2/config/ResourceServerConfig.java
到blog-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;@Component public class FeignRequestInterceptor implements RequestInterceptor { @Override public void apply (RequestTemplate requestTemplate) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes == null ) { HttpServletRequest request = attributes.getRequest(); String token = request.getHeader(HttpHeaders.AUTHORIZATION); if (StringUtils.isNotBlank(token)) { requestTemplate.header(HttpHeaders.AUTHORIZATION, token); } } } }
测试 查询问题详情接口,此接口远程调用 文章微服务 的标签数据。
重启blog-question微服务
访问:http://127.0.0.1:8002/question/swagger-ui.html#/
发送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) { 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 (); 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); 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 { public static SysUser getUserInfo () { 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
目录中的JwtTokenStoreConfig
和AuthUtil
类拷贝到重构文章和系统微服务的对应目录下即可。
方法级别权限 在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) { return articleService.queryPage(req); }
测试:发送POST请求:http://127.0.0.1:8001/article/article/search
Gateway统一网关和限流微服务 概述 网关的作用相当于一个过虑器、拦截器,它可以拦截多个服务的请求。使用网关校验用户的身份是否合法。
用户请求某个资源服务前,需要先通过网关访问Oauth2
认证授权服务请求一个AccessToken
用户通过认证授权服务得到AccessToken
后,通过api
网关调用其他资源服务A、B、C
资源服务根据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 > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-gateway</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis-reactive</artifactId > </dependency > <dependency > <groupId > com.nimbusds</groupId > <artifactId > nimbus-jose-jwt</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > </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 > </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错误。
添加依赖
<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: - name: RequestRateLimiter args: key-resolver: "#{@uriKeyResolver}" redis-rate-limiter.replenishRate: 2 redis-rate-limiter.burstCapacity: 4 - id: blog-question uri: lb://question-server predicates: - Path=/question/** filters: - name: RequestRateLimiter args: key-resolver: "#{@uriKeyResolver}" redis-rate-limiter.replenishRate: 2 redis-rate-limiter.burstCapacity: 4 - id: blog-system uri: lb://system-server predicates: - Path=/system/** filters: - name: RequestRateLimiter args: key-resolver: "#{@uriKeyResolver}" redis-rate-limiter.replenishRate: 2 redis-rate-limiter.burstCapacity: 4 - id: blog-auth uri: lb://auth-server predicates: - Path=/auth/** filters: - name: RequestRateLimiter args: 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()); } }
测试限流
启动redis , redis-server版本要用3以上的版本.
数据在redis中存储的时间只有几秒,所以得使用monitor指令来动态的观察.
浏览顺频繁发送:http://localhost:6001/article/api/article/1 当每秒达到4次请求后,每秒只能请求2次了。
自定义认证过滤器转发请求 概述 Gateway的核心就是过虑器,通过过虑器实现请求过虑,身份校验等。自定义过虑器需要实现全局过滤器GlobalFilter
和Ordered
接口,分别实现接口中的如下方法: 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;@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)); if (StringUtils.indexOfAny(path, white) != -1 ) { return chain.filter(exchange); } 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); 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
):
Header
头部 :用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。base64enc({ "alg":"HS256","TYPE":"JWT"})
// eyAiYWxnIjoiSFMyNTYiLCJUWVBFIjoiSldUIn0=
Payload
载荷:可以把用户名、角色等无关紧要的信息保存到Payload部分。base64enc({"user":"vichin","pwd":"weichen123"})
// 用户的关键信息eyJ1c2VyIjoidmljaGluIiwicH
Signature
(签名): Signature
部分是根据header+payload+secretKey
进行加密算出来的,如果Payload
被篡改,就可以在解密Signature
的时候校验是否被篡改。HMACSHA256(base64enc(header)+","+base64enc(payload), secretKey)
Header
和Payload
部分使用的是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; @Override public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); 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(); 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); 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 () { return 10 ; } }