文章目录
- 项目实战-前后端分离博客系统
- 1.课程介绍
- 2.创建工程
- 3.博客前台
- 3.0 准备工作
- 3.1 SpringBoot和MybatisPuls整合配置测试
- 3.1 热门文章列表
- 3.1.0 文章表分析
- 3.1.1 需求
- 3.1.2 接口设计
- 3.1.3 基础版本代码实现
- 3.1.4 使用VO优化
- 3.1.5 字面值处理
- 3.2 Bean拷贝工具类封装
- 3.2 查询分类列表
- 3.2.0 分类表分析
- 3.2.1 需求
- 3.2.2 接口设计
- 3.2.3 EasyCode代码模板
- 3.2.4 代码实现
- 3.3 分页查询文章列表
- 3.3.1 需求
- 3.3.2 接口设计
- 3.3.3 代码实现
- 3.3.4 FastJson配置
- 3.4 文章详情接口
- 3.4.1 需求
- 3.4.2 接口设计
- 3.4.3 代码实现
- 3.5 友联查询
- 3.5.0 友链表分析
- 3.5.1 需求
- 3.5.2 接口设计
- 3.5.3 代码实现
- 3.6 登录功能实现
- 3.6.0 需求
- 3.6.1 接口设计
- 3.6.2 表分析
- 3.6.3 思路分析
- 3.6.4 准备工作
- 3.6.5 登录接口代码实现
- BlogLoginController
- BlogLoginService
- SecurityConfig
- BlogLoginServiceImpl
- UserDetailServiceImpl
- LoginUser
- BlogUserLoginVo
- UserInfoVo
- 3.6.6 登录校验过滤器代码实现
- 思路
- JwtAuthenticationTokenFilter
- SecurityConfig
- 3.7 认证授权失败处理
- 3.8 统一异常处理
- 3.9 退出登录接口
- 3.9.1 接口设计
- 3.9.2 代码实现
- 3.10 查询评论列表接口
- 3.10.1 需求
- 3.10.2 评论表分析
- 3.10.3 接口设计
- 3.10.4 代码实现
- 3.10.4.1 不考虑子评论
- 3.10.4.2 查询子评论
- 3.11 发表评论接口
- 3.11.1 需求
- 3.11.2 接口设计
- 请求体:
- 响应格式:
- 3.11.3 代码实现
- 3.12 友联评论列表
- 3.12.1 需求
- 3.12.2 接口设计
- 3.12.3 代码实现
- 3.13 个人信息查询接口
- 3.13.1 需求
- 3.13.2 接口设计
- 3.13.3 代码实现
- 3.14 头像上传接口
- 3.14.1 需求
- 3.14.2 OSS
- 3.14.2.1 为什么要使用OSS
- 3.14.2.2 七牛云基本使用测试
- 3.14.2.3 七牛云测试代码编写
- 3.14.2 接口设计
- 3.14.3 代码实现
- 3.15 更新个人信息接口
- 3.15.1 需求
- 3.15.2 接口设计
- 3.15.3 代码实现
- 3.16 用户注册
- 3.16.1 需求
- 3.16.2 接口设计
- 3.16.3 代码实现
- 3.17 AOP实现日志记录
- 3.17.1 需求
- 3.17.2 思路分析
- 3.17.3 代码实现
- 3.18 更新浏览次数
- 3.18.1 需求
- 3.18.2 思路分析
- 3.18.3 铺垫知识
- 3.18.3.1 CommandLineRunner实现项目启动时预处理
- 3.18.3.2 定时任务
- 3.18.3.2.1 cron 表达式语法
- 3.18.4 接口设计
- 3.18.5 代码实现
- ①在应用启动时把博客的浏览量存储到redis中
- ②更新浏览量时去更新redsi中的数据
- ③定时任务每隔10分钟把Redis中的浏览量更新到数据库中
- ④读取文章浏览量时从redis读取
- 4. Swagger2
- 4.1 简介
- 4.2 为什么使用Swagger
- 4.3 快速入门
- 4.3.1 引入依赖
- 4.3.2 启用Swagger2
- 4.3.3 测试
- 4.4 具体配置
- 4.4.1 Controller配置
- 4.4.1 @Api 注解
- 4.4.2 接口配置
- 4.4.2.1 接口描述配置@ApiOperation
- 4.4.2.2 接口参数描述
- 4.4.3 实体类配置
- 4.4.3.1 实体的描述配置@ApiModel
- 4.4.3.2 实体的属性的描述配置@ApiModelProperty
- 4.4.4 文档信息配置
- 5. 博客后台
- 5.0 准备工作
- 5.1 后台登录
- 5.1.0 需求
- 5.1.1 接口设计
- 5.1.2 思路分析
- 5.1.3 准备工作
- 5.1.4 登录接口代码实现
- LoginController
- LoginService
- SecurityConfig
- SystemLoginServiceImpl
- UserDetailServiceImpl
- LoginUser
- 5.2 后台权限控制及动态路由
- 需求
- 功能设计
- 表分析
- 接口设计
- getInfo接口
- getRouters接口
- 代码实现
- 准备工作
- getInfo接口
- getRouters接口
- 5.3 退出登录接口
- 5.3.1 接口设计
- 5.3.2 代码实现
- 5.4 查询标签列表
- 5.4.0 需求
- 5.4.1 标签表分析
- 5.4.2 接口设计
- 5.4.3 代码实现
- 5.5 新增标签
- 5.5.0 需求
- 5.5.1 接口设计
- 5.5.2 测试
- 5.6 删除标签
- 5.6.1 接口设计
- 5.6.2 测试
- 5.7 修改标签
- 5.7.1 接口设计
- 5.7.1.1 获取标签信息
- 5.7.1.2 修改标签接口
- 5.8 写博文
- 5.8.1 需求
- 5.8.2 表分析
- 5.8.2 接口设计
- 5.8.2.1 查询所有分类接口
- 5.8.2.2 查询所有标签接口
- 5.8.2.3 上传图片
- 5.8.2.4 新增博文
- 5.8.3 代码实现
- 5.8.3.1 查询所有分类接口
- 5.8.3.2 查询所有标签接口
- 5.8.3.3 上传图片接口
- 5.8.3.4 新增博文接口
- 5.9 导出所有分类到Excel
- 5.9.1 需求
- 5.9.2 技术方案
- 5.9.3 接口设计
- 5.9.4 代码实现
- 5.10 权限控制
- 5.10.1 需求
- 5.10.2 代码实现
- 5.11 文章列表
- 5.10.1 需求
- 5.10.2 接口设计
- 5.12 修改文章
- 5.12.1 需求
- 5.12.2 分析
- 5.12.3 接口设计
- 5.12.3.1 查询文章详情接口
- 5.12.3.2 更新文章接口
- 5.13 删除文章
- 5.13.1 需求
- 5.13.2 接口设计
- 5.14 菜单列表
- 5.14.1 需求
- 5.14.2 接口设计
- 5.15 新增菜单
- 5.15.1 需求
- 5.15.2 接口设计
- 5.16 修改菜单
- 5.16.1 需求
- 5.16.2 接口设计
- 5.16.2.1 根据id查询菜单数据
- 5.16.2.2 更新菜单
- 5.17 删除菜单
- 5.17.1 需求
- 5.17.2 接口设计
- 5.18 角色列表
- 5.18.1 需求
- 5.18.2 接口设计
- 5.19 改变角色状态
- 5.19.1 需求
- 5.19.2 接口设计
- 5.20 新增角色!!
- 5.20.1 需求
- 5.20.2 接口设计
- 5.20.2.1 获取菜单树接口
- 5.20.2.2 新增角色接口
- 5.21 修改角色
- 5.21.1 需求
- 5.21.2 接口设计
- 5.21.2.1 角色信息回显接口
- 5.21.2.2 加载对应角色菜单列表树接口
- 5.21.2.3 更新角色信息接口
- 5.22 删除角色
- 5.22.1 需求
- 5.22.2 接口设计
- 5.23 用户列表
- 5.23.1 需求
- 5.23.2 接口设计
- 5.24 新增用户!!!
- 5.24.1 需求
- 5.24.2 接口设计
- 5.24.2.1 查询角色列表接口
- 5.24.2.2 新增用户
- 5.25 删除用户
- 5.25.1 需求
- 5.25.2 接口设计
- 5.26 修改用户
- 5.26.1 需求
- 5.26.2 接口设计
- 5.26.2.1 根据id查询用户信息回显接口
- 5.26.2.2 更新用户信息接口
- 5.27 分页查询分类列表
- 5.27.1 需求
- 5.27.2 接口设计
- 5.28 新增分类
- 5.28.1 需求
- 5.28.2 接口设计
- 5.29 修改分类
- 5.29.1 需求
- 5.29.2 接口设计
- 5.29.2.1 根据id查询分类
- 5.29.2.2 更新分类
- 5.30 删除分类
- 5.30.1 需求
- 5.30.2 接口设计
- 5.31 分页查询友链列表
- 5.31.1 需求
- 5.31.2 接口设计
- 5.32 新增友链
- 5.32.1 需求
- 5.32.2 接口设计
- 5.33 修改友链
- 5.33.1 需求
- 5.33.2 接口设计
- 5.33.2.1 根据id查询友联
- 5.33.2.2 修改友链
- 5.34 删除友链
- 5.34.1 需求
- 5.34.2 接口设计
项目实战-前后端分离博客系统
1.课程介绍
- 纯后端讲解
- 完整的前台后台代码编写
- 主流技术栈(SpringBoot,MybatisPlus,SpringSecurity,EasyExcel,Swagger2,Redis,Echarts,Vue,ElementUI…)
- 完善细致的需求分析
- 由易到难循序渐进
2.创建工程
我们有前台和后台两套系统。两套系统的前端工程都已经提供好了。所以我们只需要写两套系统的后端。
但是大家思考下,实际上两套后端系统的很多内容是可能重复的。这里如果我们只是单纯的创建两个后端工程。那么就会有大量的重复代码,并且需要修改的时候也需要修改两次。这就是代码复用性不高。
所以我们需要创建多模块项目,两套系统可能都会用到的代码可以写到一个公共模块中,让前台系统和后台系统分别取依赖公共模块。
① 创建父模块
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.sangeng</groupId><artifactId>SGBlog</artifactId><packaging>pom</packaging><version>1.0-SNAPSHOT</version><modules><module>sangeng-framework</module><module>sangeng-admin</module><module>sangeng-blog</module></modules><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><java.version>1.8</java.version></properties><dependencyManagement><dependencies><!-- SpringBoot的依赖配置--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.5.0</version><type>pom</type><scope>import</scope></dependency><!--fastjson依赖--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.33</version></dependency><!--jwt依赖--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency><!--mybatisPlus依赖--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3</version></dependency><!--阿里云OSS--><dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.10.2</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.0.5</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.1</version><configuration><source>${java.version}</source><target>${java.version}</target><encoding>${project.build.sourceEncoding}</encoding></configuration></plugin></plugins></build>
</project>
②创建公共子模块 sangeng-framework
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>SGBlog</artifactId><groupId>com.sangeng</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>sangeng-framework</artifactId><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--lombk--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!--junit--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--SpringSecurity启动器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--redis依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--fastjson依赖--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId></dependency><!--jwt依赖--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId></dependency><!--mybatisPlus依赖--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId></dependency><!--mysql数据库驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!--阿里云OSS--><dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId></dependency><!--AOP--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId></dependency></dependencies>
</project>
③创建博客后台模块sangeng-admin
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>SGBlog</artifactId><groupId>com.sangeng</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>sangeng-admin</artifactId><dependencies><dependency><groupId>com.sangeng</groupId><artifactId>sangeng-framework</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies>
</project>
④创建博客前台模块sangeng-blog
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>SGBlog</artifactId><groupId>com.sangeng</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>sangeng-blog</artifactId><dependencies><dependency><groupId>com.sangeng</groupId><artifactId>sangeng-framework</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies></project>
3.博客前台
3.0 准备工作
3.1 SpringBoot和MybatisPuls整合配置测试
①创建启动类
/*** @Author 三更 B站: https://space.bilibili.com/663528522*/
@SpringBootApplication
@MapperScan("com.sangeng.mapper")
public class SanGengBlogApplication {public static void main(String[] args) {SpringApplication.run(SanGengBlogApplication.class,args);}
}
②创建application.yml配置文件
server:port: 7777
spring:datasource:url: jdbc:mysql://localhost:3306/sg_blog?characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverservlet:multipart:max-file-size: 2MBmax-request-size: 5MB
mybatis-plus:configuration:# 日志log-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:logic-delete-field: delFlaglogic-delete-value: 1logic-not-delete-value: 0id-type: auto
③ SQL语句
SQL脚本:SGBlog\资源\SQL\sg_article.sql
④ 创建实体类,Mapper,Service
注意思考这些文件应该写在哪个模块下?
@SuppressWarnings("serial")
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sg_article")
public class Article {@TableIdprivate Long id;//标题private String title;//文章内容private String content;//文章类型:1 文章 2草稿private String type;//文章摘要private String summary;//所属分类idprivate Long categoryId;//缩略图private String thumbnail;//是否置顶(0否,1是)private String isTop;//状态(0已发布,1草稿)private String status;//评论数private Integer commentCount;//访问量private Long viewCount;//是否允许评论 1是,0否private String isComment;private Long createBy;private Date createTime;private Long updateBy;private Date updateTime;//删除标志(0代表未删除,1代表已删除)private Integer delFlag;}
public interface ArticleMapper extends BaseMapper<Article> {}
public interface ArticleService extends IService<Article> {
}
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {}
⑤ 创建Controller测试接口
注意思考这些文件应该写在哪个模块下?
@RestController
@RequestMapping("/article")
public class ArticleController {@Autowiredprivate ArticleService articleService;@GetMapping("/list")public List<Article> test(){return articleService.list();}
}
我们可以暂时先注释掉sangeng-framework中的SpringSecurity依赖方便测试
3.1 热门文章列表
3.1.0 文章表分析
通过需求去分析需要有哪些字段。
3.1.1 需求
需要查询浏览量最高的前10篇文章的信息。要求展示文章标题和浏览量。把能让用户自己点击跳转到具体的文章详情进行浏览。
注意:不能把草稿展示出来,不能把删除了的文章查询出来。要按照浏览量进行降序排序。
3.1.2 接口设计
见接口文档
3.1.3 基础版本代码实现
①准备工作
统一响应类和响应枚举
package com.sangeng.domain;import com.fasterxml.jackson.annotation.JsonInclude;
import com.sangeng.enums.AppHttpCodeEnum;import java.io.Serializable;@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> implements Serializable {private Integer code;private String msg;private T data;public ResponseResult() {this.code = AppHttpCodeEnum.SUCCESS.getCode();this.msg = AppHttpCodeEnum.SUCCESS.getMsg();}public ResponseResult(Integer code, T data) {this.code = code;this.data = data;}public ResponseResult(Integer code, String msg, T data) {this.code = code;this.msg = msg;this.data = data;}public ResponseResult(Integer code, String msg) {this.code = code;this.msg = msg;}public static ResponseResult errorResult(int code, String msg) {ResponseResult result = new ResponseResult();return result.error(code, msg);}public static ResponseResult okResult() {ResponseResult result = new ResponseResult();return result;}public static ResponseResult okResult(int code, String msg) {ResponseResult result = new ResponseResult();return result.ok(code, null, msg);}public static ResponseResult okResult(Object data) {ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getMsg());if(data!=null) {result.setData(data);}return result;}public static ResponseResult errorResult(AppHttpCodeEnum enums){return setAppHttpCodeEnum(enums,enums.getMsg());}public static ResponseResult errorResult(AppHttpCodeEnum enums, String msg){return setAppHttpCodeEnum(enums,msg);}public static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums){return okResult(enums.getCode(),enums.getMsg());}private static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums, String msg){return okResult(enums.getCode(),msg);}public ResponseResult<?> error(Integer code, String msg) {this.code = code;this.msg = msg;return this;}public ResponseResult<?> ok(Integer code, T data) {this.code = code;this.data = data;return this;}public ResponseResult<?> ok(Integer code, T data, String msg) {this.code = code;this.data = data;this.msg = msg;return this;}public ResponseResult<?> ok(T data) {this.data = data;return this;}public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public T getData() {return data;}public void setData(T data) {this.data = data;}nj}
package com.sangeng.enums;public enum AppHttpCodeEnum {// 成功SUCCESS(200,"操作成功"),// 登录NEED_LOGIN(401,"需要登录后操作"),NO_OPERATOR_AUTH(403,"无权限操作"),SYSTEM_ERROR(500,"出现错误"),USERNAME_EXIST(501,"用户名已存在"),PHONENUMBER_EXIST(502,"手机号已存在"), EMAIL_EXIST(503, "邮箱已存在"),REQUIRE_USERNAME(504, "必需填写用户名"),LOGIN_ERROR(505,"用户名或密码错误");int code;String msg;AppHttpCodeEnum(int code, String errorMessage){this.code = code;this.msg = errorMessage;}public int getCode() {return code;}public String getMsg() {return msg;}
}
② 代码实现
@RestController
@RequestMapping("/article")
public class ArticleController {@Autowiredprivate ArticleService articleService;@GetMapping("/hotArticleList")public ResponseResult hotArticleList(){ResponseResult result = articleService.hotArticleList();return result;}
}
public interface ArticleService extends IService<Article> {ResponseResult hotArticleList();
}
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {@Overridepublic ResponseResult hotArticleList() {//查询热门文章 封装成ResponseResult返回LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();//必须是正式文章queryWrapper.eq(Article::getStatus,0);//按照浏览量进行排序queryWrapper.orderByDesc(Article::getViewCount);//最多只查询10条Page<Article> page = new Page(1,10);page(page,queryWrapper);List<Article> articles = page.getRecords();return ResponseResult.okResult(articles);}
}
③ 解决跨域问题
@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {// 设置允许跨域的路径registry.addMapping("/**")// 设置允许跨域请求的域名.allowedOriginPatterns("*")// 是否允许cookie.allowCredentials(true)// 设置允许的请求方式.allowedMethods("GET", "POST", "DELETE", "PUT")// 设置允许的header属性.allowedHeaders("*")// 跨域允许时间.maxAge(3600);}}
3.1.4 使用VO优化
目前我们的响应格式其实是不符合接口文档的标准的,多返回了很多字段。这是因为我们查询出来的结果是Article来封装的,Article中字段比较多。
我们在项目中一般最后还要把VO来接受查询出来的结果。一个接口对应一个VO,这样即使接口响应字段要修改也只要改VO即可。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class HotArticleVo {private Long id;//标题private String title;//访问量private Long viewCount;
}
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {@Overridepublic ResponseResult hotArticleList() {//查询热门文章 封装成ResponseResult返回LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();//必须是正式文章queryWrapper.eq(Article::getStatus,0);//按照浏览量进行排序queryWrapper.orderByDesc(Article::getViewCount);//最多只查询10条Page<Article> page = new Page(1,10);page(page,queryWrapper);List<Article> articles = page.getRecords();//bean拷贝List<HotArticleVo> articleVos = new ArrayList<>();for (Article article : articles) {HotArticleVo vo = new HotArticleVo();BeanUtils.copyProperties(article,vo);articleVos.add(vo);}return ResponseResult.okResult(articleVos);}
}
3.1.5 字面值处理
实际项目中都不允许直接在代码中使用字面值。都需要定义成常量来使用。这种方式有利于提高代码的可维护性。
public class SystemConstants
{/*** 文章是草稿*/public static final int ARTICLE_STATUS_DRAFT = 1;/*** 文章是正常分布状态*/public static final int ARTICLE_STATUS_NORMAL = 0;}
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {@Overridepublic ResponseResult hotArticleList() {//查询热门文章 封装成ResponseResult返回LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();//必须是正式文章queryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);//按照浏览量进行排序queryWrapper.orderByDesc(Article::getViewCount);//最多只查询10条Page<Article> page = new Page(1,10);page(page,queryWrapper);List<Article> articles = page.getRecords();//bean拷贝List<HotArticleVo> articleVos = new ArrayList<>();for (Article article : articles) {HotArticleVo vo = new HotArticleVo();BeanUtils.copyProperties(article,vo);articleVos.add(vo);}return ResponseResult.okResult(articleVos);}
}
3.2 Bean拷贝工具类封装
public class BeanCopyUtils {private BeanCopyUtils() {}public static <V> V copyBean(Object source,Class<V> clazz) {//创建目标对象V result = null;try {result = clazz.newInstance();//实现属性copyBeanUtils.copyProperties(source, result);} catch (Exception e) {e.printStackTrace();}//返回结果return result;}public static <O,V> List<V> copyBeanList(List<O> list,Class<V> clazz){return list.stream().map(o -> copyBean(o, clazz)).collect(Collectors.toList());}
}
3.2 查询分类列表
3.2.0 分类表分析
通过需求去分析需要有哪些字段。
建表SQL及初始化数据见:SGBlog\资源\SQL\sg_category.sql
3.2.1 需求
页面上需要展示分类列表,用户可以点击具体的分类查看该分类下的文章列表。
注意: ①要求只展示有发布正式文章的分类 ②必须是正常状态的分类
3.2.2 接口设计
见接口文档
3.2.3 EasyCode代码模板
##导入宏定义
$!{define.vm}##保存文件(宏定义)
#save("/entity", ".java")##包路径(宏定义)
#setPackageSuffix("entity")##自动导入包(全局变量)
$!{autoImport.vm}import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
##表注释(宏定义)
#tableComment("表实体类")
@SuppressWarnings("serial")
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("$!{tableInfo.obj.name}")
public class $!{tableInfo.name} {
#foreach($column in $tableInfo.pkColumn)#if(${column.comment})//${column.comment}#end
@TableIdprivate $!{tool.getClsNameByFullName($column.type)} $!{column.name};
#end#foreach($column in $tableInfo.otherColumn)#if(${column.comment})//${column.comment}#endprivate $!{tool.getClsNameByFullName($column.type)} $!{column.name};
#end}
##导入宏定义
$!{define.vm}##设置表后缀(宏定义)
#setTableSuffix("Mapper")##保存文件(宏定义)
#save("/mapper", "Mapper.java")##包路径(宏定义)
#setPackageSuffix("mapper")import com.baomidou.mybatisplus.core.mapper.BaseMapper;##表注释(宏定义)
#tableComment("表数据库访问层")
public interface $!{tableName} extends BaseMapper<$!tableInfo.name> {}
##导入宏定义
$!{define.vm}##设置表后缀(宏定义)
#setTableSuffix("Service")##保存文件(宏定义)
#save("/service", "Service.java")##包路径(宏定义)
#setPackageSuffix("service")import com.baomidou.mybatisplus.extension.service.IService;##表注释(宏定义)
#tableComment("表服务接口")
public interface $!{tableName} extends IService<$!tableInfo.name> {}
##导入宏定义
$!{define.vm}##设置表后缀(宏定义)
#setTableSuffix("ServiceImpl")##保存文件(宏定义)
#save("/service/impl", "ServiceImpl.java")##包路径(宏定义)
#setPackageSuffix("service.impl")import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;##表注释(宏定义)
#tableComment("表服务实现类")
@Service("$!tool.firstLowerCase($tableInfo.name)Service")
public class $!{tableName} extends ServiceImpl<$!{tableInfo.name}Mapper, $!{tableInfo.name}> implements $!{tableInfo.name}Service {}
3.2.4 代码实现
@RestController
@RequestMapping("/category")
public class CategoryController {@Autowiredprivate CategoryService categoryService;@GetMapping("/getCategoryList")public ResponseResult getCategoryList(){return categoryService.getCategoryList();}
}
public interface CategoryService extends IService<Category> {ResponseResult getCategoryList();}
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {@Autowiredprivate ArticleService articleService;@Overridepublic ResponseResult getCategoryList() {//查询文章表 状态为已发布的文章LambdaQueryWrapper<Article> articleWrapper = new LambdaQueryWrapper<>();articleWrapper.eq(Article::getStatus,SystemConstants.ARTICLE_STATUS_NORMAL);List<Article> articleList = articleService.list(articleWrapper);//获取文章的分类id,并且去重Set<Long> categoryIds = articleList.stream().map(article -> article.getCategoryId()).collect(Collectors.toSet());//查询分类表List<Category> categories = listByIds(categoryIds);categories = categories.stream().filter(category -> SystemConstants.STATUS_NORMAL.equals(category.getStatus())).collect(Collectors.toList());//封装voList<CategoryVo> categoryVos = BeanCopyUtils.copyBeanList(categories, CategoryVo.class);return ResponseResult.okResult(categoryVos);}
}
3.3 分页查询文章列表
3.3.1 需求
在首页和分类页面都需要查询文章列表。
首页:查询所有的文章
分类页面:查询对应分类下的文章
要求:①只能查询正式发布的文章 ②置顶的文章要显示在最前面
3.3.2 接口设计
见文档
3.3.3 代码实现
MP支持分页配置
/*** @Author 三更 B站: https://space.bilibili.com/663528522*/
@Configuration
public class MbatisPlusConfig {/*** 3.4.0之后版本* @return*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor(){MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());return mybatisPlusInterceptor;}
}
在ArticleController中
@GetMapping("/articleList")public ResponseResult articleList(Integer pageNum,Integer pageSize,Long categoryId){return articleService.articleList(pageNum,pageSize,categoryId);}
在ArticleService中
ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId);
在ArticleServiceImpl中
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {@Autowiredprivate CategoryService categoryService;@Overridepublic ResponseResult hotArticleList() {//查询热门文章 封装成ResponseResult返回LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();//必须是正式文章queryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);//按照浏览量进行排序queryWrapper.orderByDesc(Article::getViewCount);//最多只查询10条Page<Article> page = new Page(1,10);page(page,queryWrapper);List<Article> articles = page.getRecords();//bean拷贝
// List<HotArticleVo> articleVos = new ArrayList<>();
// for (Article article : articles) {
// HotArticleVo vo = new HotArticleVo();
// BeanUtils.copyProperties(article,vo);
// articleVos.add(vo);
// }List<HotArticleVo> vs = BeanCopyUtils.copyBeanList(articles, HotArticleVo.class);return ResponseResult.okResult(vs);}@Overridepublic ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId) {//查询条件LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();// 如果 有categoryId 就要 查询时要和传入的相同lambdaQueryWrapper.eq(Objects.nonNull(categoryId)&&categoryId>0 ,Article::getCategoryId,categoryId);// 状态是正式发布的lambdaQueryWrapper.eq(Article::getStatus,SystemConstants.ARTICLE_STATUS_NORMAL);// 对isTop进行降序lambdaQueryWrapper.orderByDesc(Article::getIsTop);//分页查询Page<Article> page = new Page<>(pageNum,pageSize);page(page,lambdaQueryWrapper);List<Article> articles = page.getRecords();//查询categoryNamearticles.stream().map(article -> article.setCategoryName(categoryService.getById(article.getCategoryId()).getName())).collect(Collectors.toList());//articleId去查询articleName进行设置
// for (Article article : articles) {
// Category category = categoryService.getById(article.getCategoryId());
// article.setCategoryName(category.getName());
// }//封装查询结果List<ArticleListVo> articleListVos = BeanCopyUtils.copyBeanList(page.getRecords(), ArticleListVo.class);PageVo pageVo = new PageVo(articleListVos,page.getTotal());return ResponseResult.okResult(pageVo);}
}
PageVo
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageVo {private List rows;private Long total;
}
ArticleListVo
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ArticleListVo {private Long id;//标题private String title;//文章摘要private String summary;//所属分类名private String categoryName;//缩略图private String thumbnail;//访问量private Long viewCount;private Date createTime;}
在Article中增加一个字段
@TableField(exist = false)private String categoryName;
3.3.4 FastJson配置
@Bean//使用@Bean注入fastJsonHttpMessageConvertpublic HttpMessageConverter fastJsonHttpMessageConverters() {//1.需要定义一个Convert转换消息的对象FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();FastJsonConfig fastJsonConfig = new FastJsonConfig();fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");SerializeConfig.globalInstance.put(Long.class, ToStringSerializer.instance);fastJsonConfig.setSerializeConfig(SerializeConfig.globalInstance);fastConverter.setFastJsonConfig(fastJsonConfig);HttpMessageConverter<?> converter = fastConverter;return converter;}@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> converters) {converters.add(fastJsonHttpMessageConverters());}
3.4 文章详情接口
3.4.1 需求
要求在文章列表点击阅读全文时能够跳转到文章详情页面,可以让用户阅读文章正文。
要求:①要在文章详情中展示其分类名
3.4.2 接口设计
请求方式 | 请求路径 |
---|---|
Get | /article/{id} |
响应格式:
{"code": 200,"data": {"categoryId": "1","categoryName": "java","content": "内容","createTime": "2022-01-23 23:20:11","id": "1","isComment": "0","title": "SpringSecurity从入门到精通","viewCount": "114"},"msg": "操作成功"
}
3.4.3 代码实现
ArticleController中新增
@GetMapping("/{id}")public ResponseResult getArticleDetail(@PathVariable("id") Long id){return articleService.getArticleDetail(id);}
Service
ResponseResult getArticleDetail(Long id);
ServiceImpl
@Overridepublic ResponseResult getArticleDetail(Long id) {//根据id查询文章Article article = getById(id);//转换成VOArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class);//根据分类id查询分类名Long categoryId = articleDetailVo.getCategoryId();Category category = categoryService.getById(categoryId);if(category!=null){articleDetailVo.setCategoryName(category.getName());}//封装响应返回return ResponseResult.okResult(articleDetailVo);}
3.5 友联查询
3.5.0 友链表分析
通过需求去分析需要有哪些字段。
建表SQL及初始化数据见:SGBlog\资源\SQL\sg_link.sql
3.5.1 需求
在友链页面要查询出所有的审核通过的友链。
3.5.2 接口设计
请求方式 | 请求路径 |
---|---|
Get | /link/getAllLink |
响应格式:
{"code": 200,"data": [{"address": "https://www.baidu.com","description": "sda","id": "1","logo": "图片url1","name": "sda"},{"address": "https://www.qq.com","description": "dada","id": "2","logo": "图片url2","name": "sda"}],"msg": "操作成功"
}
3.5.3 代码实现
Controller
@RestController
@RequestMapping("/link")
public class LinkController {@Autowiredprivate LinkService linkService;@GetMapping("/getAllLink")public ResponseResult getAllLink(){return linkService.getAllLink();}
}
Service
public interface LinkService extends IService<Link> {ResponseResult getAllLink();
}
ServiceImpl
@Service("linkService")
public class LinkServiceImpl extends ServiceImpl<LinkMapper, Link> implements LinkService {@Overridepublic ResponseResult getAllLink() {//查询所有审核通过的LambdaQueryWrapper<Link> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(Link::getStatus, SystemConstants.LINK_STATUS_NORMAL);List<Link> links = list(queryWrapper);//转换成voList<LinkVo> linkVos = BeanCopyUtils.copyBeanList(links, LinkVo.class);//封装返回return ResponseResult.okResult(linkVos);}
}
SystemConstants
/*** 友链状态为审核通过*/public static final String LINK_STATUS_NORMAL = "0";
3.6 登录功能实现
使用我们前台和后台的认证授权统一都使用SpringSecurity安全框架来实现。
3.6.0 需求
需要实现登录功能
有些功能必须登录后才能使用,未登录状态是不能使用的。
3.6.1 接口设计
请求方式 | 请求路径 |
---|---|
POST | /login |
请求体:
{"userName":"sg","password":"1234"
}
响应格式:
{"code": 200,"data": {"token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0ODBmOThmYmJkNmI0NjM0OWUyZjY2NTM0NGNjZWY2NSIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTY0Mzg3NDMxNiwiZXhwIjoxNjQzOTYwNzE2fQ.ldLBUvNIxQCGemkCoMgT_0YsjsWndTg5tqfJb77pabk","userInfo": {"avatar": "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fi0.hdslb.com%2Fbfs%2Farticle%2F3bf9c263bc0f2ac5c3a7feb9e218d07475573ec8.gi","email": "23412332@qq.com","id": 1,"nickName": "sg333","sex": "1"}},"msg": "操作成功"
}
3.6.2 表分析
建表SQL及初始化数据见:SGBlog\资源\SQL\sys_user.sql
顺便生成下User和UserMapper后面会用到
3.6.3 思路分析
登录
①自定义登录接口
调用ProviderManager的方法进行认证 如果认证通过生成jwt
把用户信息存入redis中
②自定义UserDetailsService
在这个实现类中去查询数据库
注意配置passwordEncoder为BCryptPasswordEncoder
校验:
①定义Jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContextHolder
3.6.4 准备工作
①添加依赖
注意放开Security依赖的注释
<!--redis依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--fastjson依赖--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.33</version></dependency><!--jwt依赖--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency>
②工具类和相关配置类
见 :SGBlog\资源\登录功能所需资源
3.6.5 登录接口代码实现
BlogLoginController
@RestController
public class BlogLoginController {@Autowiredprivate BlogLoginService blogLoginService;@PostMapping("/login")public ResponseResult login(@RequestBody User user){return blogLoginService.login(user);}
}
BlogLoginService
public interface BlogLoginService {ResponseResult login(User user);
}
SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/login").anonymous()// 除上面外的所有请求全部不需要认证即可访问.anyRequest().permitAll();http.logout().disable();//允许跨域http.cors();}@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}
BlogLoginServiceImpl
@Service
public class BlogLoginServiceImpl implements BlogLoginService {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCache redisCache;@Overridepublic ResponseResult login(User user) {UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);//判断是否认证通过if(Objects.isNull(authenticate)){throw new RuntimeException("用户名或密码错误");}//获取userid 生成tokenLoginUser loginUser = (LoginUser) authenticate.getPrincipal();String userId = loginUser.getUser().getId().toString();String jwt = JwtUtil.createJWT(userId);//把用户信息存入redisredisCache.setCacheObject("bloglogin:"+userId,loginUser);//把token和userinfo封装 返回//把User转换成UserInfoVoUserInfoVo userInfoVo = BeanCopyUtils.copyBean(loginUser.getUser(), UserInfoVo.class);BlogUserLoginVo vo = new BlogUserLoginVo(jwt,userInfoVo);return ResponseResult.okResult(vo);}
}
UserDetailServiceImpl
@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据用户名查询用户信息LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getUserName,username);User user = userMapper.selectOne(queryWrapper);//判断是否查到用户 如果没查到抛出异常if(Objects.isNull(user)){throw new RuntimeException("用户不存在");}//返回用户信息// TODO 查询权限信息封装return new LoginUser(user);}
}
LoginUser
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {private User user;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUserName();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
BlogUserLoginVo
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BlogUserLoginVo {private String token;private UserInfoVo userInfo;
}
UserInfoVo
@Data
@Accessors(chain = true)
public class UserInfoVo {/*** 主键*/private Long id;/*** 昵称*/private String nickName;/*** 头像*/private String avatar;private String sex;private String email;}
3.6.6 登录校验过滤器代码实现
思路
①定义Jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContextHolder
JwtAuthenticationTokenFilter
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取请求头中的tokenString token = request.getHeader("token");if(!StringUtils.hasText(token)){//说明该接口不需要登录 直接放行filterChain.doFilter(request, response);return;}//解析获取useridClaims claims = null;try {claims = JwtUtil.parseJWT(token);} catch (Exception e) {e.printStackTrace();//token超时 token非法//响应告诉前端需要重新登录ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);WebUtils.renderString(response, JSON.toJSONString(result));return;}String userId = claims.getSubject();//从redis中获取用户信息LoginUser loginUser = redisCache.getCacheObject("bloglogin:" + userId);//如果获取不到if(Objects.isNull(loginUser)){//说明登录过期 提示重新登录ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);WebUtils.renderString(response, JSON.toJSONString(result));return;}//存入SecurityContextHolderUsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);filterChain.doFilter(request, response);}}
SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/login").anonymous()//jwt过滤器测试用,如果测试没有问题吧这里删除了.antMatchers("/link/getAllLink").authenticated()// 除上面外的所有请求全部不需要认证即可访问.anyRequest().permitAll();http.logout().disable();//把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//允许跨域http.cors();}@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}
}
3.7 认证授权失败处理
目前我们的项目在认证出错或者权限不足的时候响应回来的Json是Security的异常处理结果。但是这个响应的格式肯定是不符合我们项目的接口规范的。所以需要自定义异常处理。
AuthenticationEntryPoint 认证失败处理器
AccessDeniedHandler 授权失败处理器
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {authException.printStackTrace();//InsufficientAuthenticationException//BadCredentialsExceptionResponseResult result = null;if(authException instanceof BadCredentialsException){result = ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_ERROR.getCode(),authException.getMessage());}else if(authException instanceof InsufficientAuthenticationException){result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);}else{result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR.getCode(),"认证或授权失败");}//响应给前端WebUtils.renderString(response, JSON.toJSONString(result));}
}
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {accessDeniedException.printStackTrace();ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NO_OPERATOR_AUTH);//响应给前端WebUtils.renderString(response, JSON.toJSONString(result));}
}
配置Security异常处理器
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@AutowiredAuthenticationEntryPoint authenticationEntryPoint;@AutowiredAccessDeniedHandler accessDeniedHandler;@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/login").anonymous()//jwt过滤器测试用,如果测试没有问题吧这里删除了.antMatchers("/link/getAllLink").authenticated()// 除上面外的所有请求全部不需要认证即可访问.anyRequest().permitAll();//配置异常处理器http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);http.logout().disable();//把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//允许跨域http.cors();}@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}
}
3.8 统一异常处理
实际我们在开发过程中可能需要做很多的判断校验,如果出现了非法情况我们是期望响应对应的提示的。但是如果我们每次都自己手动去处理就会非常麻烦。我们可以选择直接抛出异常的方式,然后对异常进行统一处理。把异常中的信息封装成ResponseResult响应给前端。
SystemException
/*** @Author 三更 B站: https://space.bilibili.com/663528522*/
public class SystemException extends RuntimeException{private int code;private String msg;public int getCode() {return code;}public String getMsg() {return msg;}public SystemException(AppHttpCodeEnum httpCodeEnum) {super(httpCodeEnum.getMsg());this.code = httpCodeEnum.getCode();this.msg = httpCodeEnum.getMsg();}}
GlobalExceptionHandler
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {@ExceptionHandler(SystemException.class)public ResponseResult systemExceptionHandler(SystemException e){//打印异常信息log.error("出现了异常! {}",e);//从异常对象中获取提示信息封装返回return ResponseResult.errorResult(e.getCode(),e.getMsg());}@ExceptionHandler(Exception.class)public ResponseResult exceptionHandler(Exception e){//打印异常信息log.error("出现了异常! {}",e);//从异常对象中获取提示信息封装返回return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR.getCode(),e.getMessage());}
}
3.9 退出登录接口
3.9.1 接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
POST | /logout | 需要token请求头 |
响应格式:
{"code": 200,"msg": "操作成功"
}
3.9.2 代码实现
要实现的操作:
删除redis中的用户信息
BlogLoginController
@PostMapping("/logout")public ResponseResult logout(){return blogLoginService.logout();}
BlogLoginService
ResponseResult logout();
BlogLoginServiceImpl
@Overridepublic ResponseResult logout() {//获取token 解析获取useridAuthentication authentication = SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser = (LoginUser) authentication.getPrincipal();//获取useridLong userId = loginUser.getUser().getId();//删除redis中的用户信息redisCache.deleteObject("bloglogin:"+userId);return ResponseResult.okResult();}
SecurityConfig
要关闭默认的退出登录功能。并且要配置我们的退出登录接口需要认证才能访问
@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/login").anonymous()//注销接口需要认证才能访问.antMatchers("/logout").authenticated()//jwt过滤器测试用,如果测试没有问题吧这里删除了.antMatchers("/link/getAllLink").authenticated()// 除上面外的所有请求全部不需要认证即可访问.anyRequest().permitAll();//配置异常处理器http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);//关闭默认的注销功能http.logout().disable();//把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//允许跨域http.cors();}
3.10 查询评论列表接口
3.10.1 需求
文章详情页面要展示这篇文章下的评论列表。
效果如下:
3.10.2 评论表分析
通过需求去分析需要有哪些字段。
建表SQL及初始化数据见:SGBlog\资源\SQL\sg_comment.sql
顺便生成下对应的代码
3.10.3 接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /comment/commentList | 不需要token请求头 |
Query格式请求参数:
articleId:文章id
pageNum: 页码
pageSize: 每页条数
响应格式:
{"code": 200,"data": {"rows": [{"articleId": "1","children": [{"articleId": "1","content": "你说啥?","createBy": "1","createTime": "2022-01-30 10:06:21","id": "20","rootId": "1","toCommentId": "1","toCommentUserId": "1","toCommentUserName": "sg333","username": "sg333"}],"content": "asS","createBy": "1","createTime": "2022-01-29 07:59:22","id": "1","rootId": "-1","toCommentId": "-1","toCommentUserId": "-1","username": "sg333"}],"total": "15"},"msg": "操作成功"
}
3.10.4 代码实现
3.10.4.1 不考虑子评论
CommentController
@RestController
@RequestMapping("/comment")
public class CommentController {@Autowiredprivate CommentService commentService;@GetMapping("/commentList")public ResponseResult commentList(Long articleId,Integer pageNum,Integer pageSize){return commentService.commentList(articleId,pageNum,pageSize);}
}
CommentService
public interface CommentService extends IService<Comment> {ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize);
}
CommentServiceImpl
@Service("commentService")
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {@Autowiredprivate UserService userService;@Overridepublic ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize) {//查询对应文章的根评论LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();//对articleId进行判断queryWrapper.eq(Comment::getArticleId,articleId);//根评论 rootId为-1queryWrapper.eq(Comment::getRootId,-1);//分页查询Page<Comment> page = new Page(pageNum,pageSize);page(page,queryWrapper);List<CommentVo> commentVoList = toCommentVoList(page.getRecords());return ResponseResult.okResult(new PageVo(commentVoList,page.getTotal()));}private List<CommentVo> toCommentVoList(List<Comment> list){List<CommentVo> commentVos = BeanCopyUtils.copyBeanList(list, CommentVo.class);//遍历vo集合for (CommentVo commentVo : commentVos) {//通过creatyBy查询用户的昵称并赋值String nickName = userService.getById(commentVo.getCreateBy()).getNickName();commentVo.setUsername(nickName);//通过toCommentUserId查询用户的昵称并赋值//如果toCommentUserId不为-1才进行查询if(commentVo.getToCommentUserId()!=-1){String toCommentUserName = userService.getById(commentVo.getToCommentUserId()).getNickName();commentVo.setToCommentUserName(toCommentUserName);}}return commentVos;}
}
CommentVo
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommentVo {private Long id;//文章idprivate Long articleId;//根评论idprivate Long rootId;//评论内容private String content;//所回复的目标评论的useridprivate Long toCommentUserId;private String toCommentUserName;//回复目标评论idprivate Long toCommentId;private Long createBy;private Date createTime;private String username;
}
3.10.4.2 查询子评论
CommentVo在之前的基础上增加了 private List children;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommentVo {private Long id;//文章idprivate Long articleId;//根评论idprivate Long rootId;//评论内容private String content;//所回复的目标评论的useridprivate Long toCommentUserId;private String toCommentUserName;//回复目标评论idprivate Long toCommentId;private Long createBy;private Date createTime;private String username;private List<CommentVo> children;
}
CommentServiceImpl
@Service("commentService")
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {@Autowiredprivate UserService userService;@Overridepublic ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize) {//查询对应文章的根评论LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();//对articleId进行判断queryWrapper.eq(Comment::getArticleId,articleId);//根评论 rootId为-1queryWrapper.eq(Comment::getRootId,-1);//分页查询Page<Comment> page = new Page(pageNum,pageSize);page(page,queryWrapper);List<CommentVo> commentVoList = toCommentVoList(page.getRecords());//查询所有根评论对应的子评论集合,并且赋值给对应的属性for (CommentVo commentVo : commentVoList) {//查询对应的子评论List<CommentVo> children = getChildren(commentVo.getId());//赋值commentVo.setChildren(children);}return ResponseResult.okResult(new PageVo(commentVoList,page.getTotal()));}/*** 根据根评论的id查询所对应的子评论的集合* @param id 根评论的id* @return*/private List<CommentVo> getChildren(Long id) {LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(Comment::getRootId,id);queryWrapper.orderByAsc(Comment::getCreateTime);List<Comment> comments = list(queryWrapper);List<CommentVo> commentVos = toCommentVoList(comments);return commentVos;}private List<CommentVo> toCommentVoList(List<Comment> list){List<CommentVo> commentVos = BeanCopyUtils.copyBeanList(list, CommentVo.class);//遍历vo集合for (CommentVo commentVo : commentVos) {//通过creatyBy查询用户的昵称并赋值String nickName = userService.getById(commentVo.getCreateBy()).getNickName();commentVo.setUsername(nickName);//通过toCommentUserId查询用户的昵称并赋值//如果toCommentUserId不为-1才进行查询if(commentVo.getToCommentUserId()!=-1){String toCommentUserName = userService.getById(commentVo.getToCommentUserId()).getNickName();commentVo.setToCommentUserName(toCommentUserName);}}return commentVos;}
}
3.11 发表评论接口
3.11.1 需求
用户登录后可以对文章发表评论,也可以对评论进行回复。
用户登录后也可以在友链页面进行评论。
3.11.2 接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
POST | /comment | 需要token头 |
请求体:
回复了文章:
{"articleId":1,"type":0,"rootId":-1,"toCommentId":-1,"toCommentUserId":-1,"content":"评论了文章"}
回复了某条评论:
{"articleId":1,"type":0,"rootId":"3","toCommentId":"3","toCommentUserId":"1","content":"回复了某条评论"}
如果是友链评论,type应该为1
响应格式:
{"code":200,"msg":"操作成功"
}
3.11.3 代码实现
CommentController
@PostMappingpublic ResponseResult addComment(@RequestBody Comment comment){return commentService.addComment(comment);}
CommentService
ResponseResult addComment(Comment comment);
CommentServiceImpl
@Overridepublic ResponseResult addComment(Comment comment) {//评论内容不能为空if(!StringUtils.hasText(comment.getContent())){throw new SystemException(AppHttpCodeEnum.CONTENT_NOT_NULL);}save(comment);return ResponseResult.okResult();}
SecurityUtils
/*** @Author 三更 B站: https://space.bilibili.com/663528522*/
public class SecurityUtils
{/*** 获取用户**/public static LoginUser getLoginUser(){return (LoginUser) getAuthentication().getPrincipal();}/*** 获取Authentication*/public static Authentication getAuthentication() {return SecurityContextHolder.getContext().getAuthentication();}public static Boolean isAdmin(){Long id = getLoginUser().getUser().getId();return id != null && 1L == id;}public static Long getUserId() {return getLoginUser().getUser().getId();}
}
配置MP字段自动填充
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {Long userId = null;try {userId = SecurityUtils.getUserId();} catch (Exception e) {e.printStackTrace();userId = -1L;//表示是自己创建}this.setFieldValByName("createTime", new Date(), metaObject);this.setFieldValByName("createBy",userId , metaObject);this.setFieldValByName("updateTime", new Date(), metaObject);this.setFieldValByName("updateBy", userId, metaObject);}@Overridepublic void updateFill(MetaObject metaObject) {this.setFieldValByName("updateTime", new Date(), metaObject);this.setFieldValByName(" ", SecurityUtils.getUserId(), metaObject);}
}
用注解标识哪些字段在什么情况下需要自动填充
/*** 创建人的用户id*/@TableField(fill = FieldFill.INSERT)private Long createBy;/*** 创建时间*/@TableField(fill = FieldFill.INSERT)private Date createTime;/*** 更新人*/@TableField(fill = FieldFill.INSERT_UPDATE)private Long updateBy;/*** 更新时间*/@TableField(fill = FieldFill.INSERT_UPDATE)private Date updateTime;
3.12 友联评论列表
3.12.1 需求
友链页面也需要查询对应的评论列表。
3.12.2 接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /comment/linkCommentList | 不需要token请求头 |
Query格式请求参数:
pageNum: 页码
pageSize: 每页条数
响应格式:
{"code": 200,"data": {"rows": [{"articleId": "1","children": [{"articleId": "1","content": "回复友链评论3","createBy": "1","createTime": "2022-01-30 10:08:50","id": "23","rootId": "22","toCommentId": "22","toCommentUserId": "1","toCommentUserName": "sg333","username": "sg333"}],"content": "友链评论2","createBy": "1","createTime": "2022-01-30 10:08:28","id": "22","rootId": "-1","toCommentId": "-1","toCommentUserId": "-1","username": "sg333"}],"total": "1"},"msg": "操作成功"
}
3.12.3 代码实现
CommentController 修改了之前的文章评论列表接口,并且增加了新的友联评论接口
@GetMapping("/commentList")public ResponseResult commentList(Long articleId,Integer pageNum,Integer pageSize){return commentService.commentList(SystemConstants.ARTICLE_COMMENT,articleId,pageNum,pageSize);} @GetMapping("/linkCommentList")public ResponseResult linkCommentList(Integer pageNum,Integer pageSize){return commentService.commentList(SystemConstants.LINK_COMMENT,null,pageNum,pageSize);}
SystemConstants增加了两个常量
/*** 评论类型为:文章评论*/public static final String ARTICLE_COMMENT = "0";/*** 评论类型为:友联评论*/public static final String LINK_COMMENT = "1";
CommentService修改了commentList方法,增加了一个参数commentType
ResponseResult commentList(String commentType, Long articleId, Integer pageNum, Integer pageSize);
CommentServiceImpl修改commentList方法的代码,必须commentType为0的时候才增加articleId的判断,并且增加了一个评论类型的添加。
@Overridepublic ResponseResult commentList(String commentType, Long articleId, Integer pageNum, Integer pageSize) {//查询对应文章的根评论LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();//对articleId进行判断queryWrapper.eq(SystemConstants.ARTICLE_COMMENT.equals(commentType),Comment::getArticleId,articleId);//根评论 rootId为-1queryWrapper.eq(Comment::getRootId,-1);//评论类型queryWrapper.eq(Comment::getType,commentType);//分页查询Page<Comment> page = new Page(pageNum,pageSize);page(page,queryWrapper);List<CommentVo> commentVoList = toCommentVoList(page.getRecords());//查询所有根评论对应的子评论集合,并且赋值给对应的属性for (CommentVo commentVo : commentVoList) {//查询对应的子评论List<CommentVo> children = getChildren(commentVo.getId());//赋值commentVo.setChildren(children);}return ResponseResult.okResult(new PageVo(commentVoList,page.getTotal()));}
3.13 个人信息查询接口
3.13.1 需求
进入个人中心的时候需要能够查看当前用户信息
3.13.2 接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /user/userInfo | 需要token请求头 |
不需要参数
响应格式:
{"code":200,"data":{"avatar":"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fi0.hdslb.com%2Fbfs%2Farticle%2F3bf9c263bc0f2ac5c3a7feb9e218d07475573ec8.gi","email":"23412332@qq.com","id":"1","nickName":"sg333","sex":"1"},"msg":"操作成功"
}
3.13.3 代码实现
UserController
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@GetMapping("/userInfo")public ResponseResult userInfo(){return userService.userInfo();}
}
UserService增加方法定义
public interface UserService extends IService<User> {ResponseResult userInfo();}
UserServiceImpl实现userInfo方法
@Overridepublic ResponseResult userInfo() {//获取当前用户idLong userId = SecurityUtils.getUserId();//根据用户id查询用户信息User user = getById(userId);//封装成UserInfoVoUserInfoVo vo = BeanCopyUtils.copyBean(user,UserInfoVo.class);return ResponseResult.okResult(vo);}
SecurityConfig配置该接口必须认证后才能访问
@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/login").anonymous()//注销接口需要认证才能访问.antMatchers("/logout").authenticated()//个人信息接口必须登录后才能访问.antMatchers("/user/userInfo").authenticated()// 除上面外的所有请求全部不需要认证即可访问.anyRequest().permitAll();//配置异常处理器http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);//关闭默认的注销功能http.logout().disable();//把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//允许跨域http.cors();}
3.14 头像上传接口
3.14.1 需求
在个人中心点击编辑的时候可以上传头像图片。上传完头像后,可以用于更新个人信息接口。
3.14.2 OSS
3.14.2.1 为什么要使用OSS
因为如果把图片视频等文件上传到自己的应用的Web服务器,在读取图片的时候会占用比较多的资源。影响应用服务器的性能。
所以我们一般使用OSS(Object Storage Service对象存储服务)存储图片或视频。
3.14.2.2 七牛云基本使用测试
秘钥
3.14.2.3 七牛云测试代码编写
①添加依赖
<dependency><groupId>com.qiniu</groupId><artifactId>qiniu-java-sdk</artifactId><version>[7.7.0, 7.7.99]</version></dependency>
②复制修改案例代码
application.yml
oss:accessKey: xxxxsecretKey: xxxxbucket: sg-blog
OSSTest.java
@SpringBootTest
@ConfigurationProperties(prefix = "oss")
public class OSSTest {private String accessKey;private String secretKey;private String bucket;public void setAccessKey(String accessKey) {this.accessKey = accessKey;}public void setSecretKey(String secretKey) {this.secretKey = secretKey;}public void setBucket(String bucket) {this.bucket = bucket;}@Testpublic void testOss(){//构造一个带指定 Region 对象的配置类Configuration cfg = new Configuration(Region.autoRegion());//...其他参数参考类注释UploadManager uploadManager = new UploadManager(cfg);//...生成上传凭证,然后准备上传
// String accessKey = "your access key";
// String secretKey = "your secret key";
// String bucket = "sg-blog";//默认不指定key的情况下,以文件内容的hash值作为文件名String key = "2022/sg.png";try {
// byte[] uploadBytes = "hello qiniu cloud".getBytes("utf-8");
// ByteArrayInputStream byteInputStream=new ByteArrayInputStream(uploadBytes);InputStream inputStream = new FileInputStream("C:\\Users\\root\\Desktop\\Snipaste_2022-02-28_22-48-37.png");Auth auth = Auth.create(accessKey, secretKey);String upToken = auth.uploadToken(bucket);try {Response response = uploadManager.put(inputStream,key,upToken,null, null);//解析上传成功的结果DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);System.out.println(putRet.key);System.out.println(putRet.hash);} catch (QiniuException ex) {Response r = ex.response;System.err.println(r.toString());try {System.err.println(r.bodyString());} catch (QiniuException ex2) {//ignore}}} catch (Exception ex) {//ignore}}
}
3.14.2 接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
POST | /upload | 需要token |
参数:
img,值为要上传的文件
请求头:
Content-Type :multipart/form-data;
响应格式:
{"code": 200,"data": "文件访问链接","msg": "操作成功"
}
3.14.3 代码实现
@RestController
public class UploadController {@Autowiredprivate UploadService uploadService;@PostMapping("/upload")public ResponseResult uploadImg(MultipartFile img){return uploadService.uploadImg(img);}
}
public interface UploadService {ResponseResult uploadImg(MultipartFile img);
}
@Service
@Data
@ConfigurationProperties(prefix = "oss")
public class OssUploadService implements UploadService {@Overridepublic ResponseResult uploadImg(MultipartFile img) {//判断文件类型//获取原始文件名String originalFilename = img.getOriginalFilename();//对原始文件名进行判断if(!originalFilename.endsWith(".png")){throw new SystemException(AppHttpCodeEnum.FILE_TYPE_ERROR);}//如果判断通过上传文件到OSSString filePath = PathUtils.generateFilePath(originalFilename);String url = uploadOss(img,filePath);// 2099/2/3/wqeqeqe.pngreturn ResponseResult.okResult(url);}private String accessKey;private String secretKey;private String bucket;private String uploadOss(MultipartFile imgFile, String filePath){//构造一个带指定 Region 对象的配置类Configuration cfg = new Configuration(Region.autoRegion());//...其他参数参考类注释UploadManager uploadManager = new UploadManager(cfg);//默认不指定key的情况下,以文件内容的hash值作为文件名String key = filePath;try {InputStream inputStream = imgFile.getInputStream();Auth auth = Auth.create(accessKey, secretKey);String upToken = auth.uploadToken(bucket);try {Response response = uploadManager.put(inputStream,key,upToken,null, null);//解析上传成功的结果DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);System.out.println(putRet.key);System.out.println(putRet.hash);return "http://r7yxkqloa.bkt.clouddn.com/"+key;} catch (QiniuException ex) {Response r = ex.response;System.err.println(r.toString());try {System.err.println(r.bodyString());} catch (QiniuException ex2) {//ignore}}} catch (Exception ex) {//ignore}return "www";}
}
PathUtils
/*** @Author 三更 B站: https://space.bilibili.com/663528522*/
public class PathUtils {public static String generateFilePath(String fileName){//根据日期生成路径 2022/1/15/SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd/");String datePath = sdf.format(new Date());//uuid作为文件名String uuid = UUID.randomUUID().toString().replaceAll("-", "");//后缀和文件后缀一致int index = fileName.lastIndexOf(".");// test.jpg -> .jpgString fileType = fileName.substring(index);return new StringBuilder().append(datePath).append(uuid).append(fileType).toString();}
}
3.15 更新个人信息接口
3.15.1 需求
在编辑完个人资料后点击保存会对个人资料进行更新。
3.15.2 接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
PUT | /user/userInfo | 需要token请求头 |
参数
请求体中json格式数据:
{"avatar":"https://sg-blog-oss.oss-cn-beijing.aliyuncs.com/2022/01/31/948597e164614902ab1662ba8452e106.png","email":"23412332@qq.com","id":"1","nickName":"sg333","sex":"1"
}
响应格式:
{"code":200,"msg":"操作成功"
}
3.15.3 代码实现
UserController
@PutMapping("/userInfo")public ResponseResult updateUserInfo(@RequestBody User user){return userService.updateUserInfo(user);}
UserService
ResponseResult updateUserInfo(User user);
UserServiceImpl
@Overridepublic ResponseResult updateUserInfo(User user) {updateById(user);return ResponseResult.okResult();}
3.16 用户注册
3.16.1 需求
要求用户能够在注册界面完成用户的注册。要求用户名,昵称,邮箱不能和数据库中原有的数据重复。如果某项重复了注册失败并且要有对应的提示。并且要求用户名,密码,昵称,邮箱都不能为空。
注意:密码必须密文存储到数据库中。
3.16.2 接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
POST | /user/register | 不需要token请求头 |
参数
请求体中json格式数据:
{"email": "string","nickName": "string","password": "string","userName": "string"
}
响应格式:
{"code":200,"msg":"操作成功"
}
3.16.3 代码实现
UserController
@PostMapping("/register")public ResponseResult register(@RequestBody User user){return userService.register(user);}
UserService
ResponseResult register(User user);
UserServiceImpl
@Autowiredprivate PasswordEncoder passwordEncoder;@Overridepublic ResponseResult register(User user) {//对数据进行非空判断if(!StringUtils.hasText(user.getUserName())){throw new SystemException(AppHttpCodeEnum.USERNAME_NOT_NULL);}if(!StringUtils.hasText(user.getPassword())){throw new SystemException(AppHttpCodeEnum.PASSWORD_NOT_NULL);}if(!StringUtils.hasText(user.getEmail())){throw new SystemException(AppHttpCodeEnum.EMAIL_NOT_NULL);}if(!StringUtils.hasText(user.getNickName())){throw new SystemException(AppHttpCodeEnum.NICKNAME_NOT_NULL);}//对数据进行是否存在的判断if(userNameExist(user.getUserName())){throw new SystemException(AppHttpCodeEnum.USERNAME_EXIST);}if(nickNameExist(user.getNickName())){throw new SystemException(AppHttpCodeEnum.NICKNAME_EXIST);}//...//对密码进行加密String encodePassword = passwordEncoder.encode(user.getPassword());user.setPassword(encodePassword);//存入数据库save(user);return ResponseResult.okResult();}
public enum AppHttpCodeEnum {// 成功SUCCESS(200,"操作成功"),// 登录NEED_LOGIN(401,"需要登录后操作"),NO_OPERATOR_AUTH(403,"无权限操作"),SYSTEM_ERROR(500,"出现错误"),USERNAME_EXIST(501,"用户名已存在"),PHONENUMBER_EXIST(502,"手机号已存在"), EMAIL_EXIST(503, "邮箱已存在"),REQUIRE_USERNAME(504, "必需填写用户名"),CONTENT_NOT_NULL(506, "评论内容不能为空"),FILE_TYPE_ERROR(507, "文件类型错误,请上传png文件"),USERNAME_NOT_NULL(508, "用户名不能为空"),NICKNAME_NOT_NULL(509, "昵称不能为空"),PASSWORD_NOT_NULL(510, "密码不能为空"),EMAIL_NOT_NULL(511, "邮箱不能为空"),NICKNAME_EXIST(512, "昵称已存在"),LOGIN_ERROR(505,"用户名或密码错误");int code;String msg;AppHttpCodeEnum(int code, String errorMessage){this.code = code;this.msg = errorMessage;}public int getCode() {return code;}public String getMsg() {return msg;}
}
3.17 AOP实现日志记录
3.17.1 需求
需要通过日志记录接口调用信息。便于后期调试排查。并且可能有很多接口都需要进行日志的记录。
接口被调用时日志打印格式如下:
3.17.2 思路分析
相当于是对原有的功能进行增强。并且是批量的增强,这个时候就非常适合用AOP来进行实现。
3.17.3 代码实现
日志打印格式
log.info("=======Start=======");// 打印请求 URLlog.info("URL : {}",);// 打印描述信息log.info("BusinessName : {}", );// 打印 Http methodlog.info("HTTP Method : {}", );// 打印调用 controller 的全路径以及执行方法log.info("Class Method : {}.{}", );// 打印请求的 IPlog.info("IP : {}",);// 打印请求入参log.info("Request Args : {}",);// 打印出参log.info("Response : {}", );// 结束后换行log.info("=======End=======" + System.lineSeparator());
3.18 更新浏览次数
3.18.1 需求
在用户浏览博文时要实现对应博客浏览量的增加。
3.18.2 思路分析
我们只需要在每次用户浏览博客时更新对应的浏览数即可。
但是如果直接操作博客表的浏览量的话,在并发量大的情况下会出现什么问题呢?
如何去优化呢?
①在应用启动时把博客的浏览量存储到redis中
②更新浏览量时去更新redis中的数据
③每隔10分钟把Redis中的浏览量更新到数据库中
④读取文章浏览量时从redis读取
3.18.3 铺垫知识
3.18.3.1 CommandLineRunner实现项目启动时预处理
如果希望在SpringBoot应用启动时进行一些初始化操作可以选择使用CommandLineRunner来进行处理。
我们只需要实现CommandLineRunner接口,并且把对应的bean注入容器。把相关初始化的代码重新到需要重新的方法中。
这样就会在应用启动的时候执行对应的代码。
@Component
public class TestRunner implements CommandLineRunner {@Overridepublic void run(String... args) throws Exception {System.out.println("程序初始化");}
}
3.18.3.2 定时任务
定时任务的实现方式有很多,比如XXL-Job等。但是其实核心功能和概念都是类似的,很多情况下只是调用的API不同而已。
这里就先用SpringBoot为我们提供的定时任务的API来实现一个简单的定时任务,让大家先对定时任务里面的一些核心概念有个大致的了解。
实现步骤
① 使用@EnableScheduling注解开启定时任务功能
我们可以在配置类上加上@EnableScheduling
@SpringBootApplication
@MapperScan("com.sangeng.mapper")
@EnableScheduling
public class SanGengBlogApplication {public static void main(String[] args) {SpringApplication.run(SanGengBlogApplication.class,args);}
}
② 确定定时任务执行代码,并配置任务执行时间
使用@Scheduled注解标识需要定时执行的代码。注解的cron属性相当于是任务的执行时间。目前可以使用 0/5 * * * * ? 进行测试,代表从0秒开始,每隔5秒执行一次。
注意:对应的bean要注入容器,否则不会生效。
@Component
public class TestJob {@Scheduled(cron = "0/5 * * * * ?")public void testJob(){//要执行的代码System.out.println("定时任务执行了");}
}
3.18.3.2.1 cron 表达式语法
cron表达式是用来设置定时任务执行时间的表达式。
很多情况下我们可以用 : 在线Cron表达式生成器 来帮助我们理解cron表达式和书写cron表达式。
但是我们还是有需要学习对应的Cron语法的,这样可以更有利于我们书写Cron表达式。
如上我们用到的 0/5 * * * * ? *,cron表达式由七部分组成,中间由空格分隔,这七部分从左往右依次是:
秒(059),分钟(059),小时(0~23),日期(1-月最后一天),月份(1-12),星期几(1-7,1表示星期日),年份(一般该项不设置,直接忽略掉,即可为空值)
通用特殊字符:, - * / (可以在任意部分使用)
星号表示任意值,例如:
* * * * * ?
表示 “ 每年每月每天每时每分每秒 ” 。
,
可以用来定义列表,例如 :
1,2,3 * * * * ?
表示 “ 每年每月每天每时每分的每个第1秒,第2秒,第3秒 ” 。
定义范围,例如:
1-3 * * * * ?
表示 “ 每年每月每天每时每分的第1秒至第3秒 ”。
/
每隔多少,例如
5/10 * * * * ?
表示 “ 每年每月每天每时每分,从第5秒开始,每10秒一次 ” 。即 “ / ” 的左侧是开始值,右侧是间隔。如果是从 “ 0 ” 开始的话,也可以简写成 “ /10 ”
日期部分还可允许特殊字符: ? L W
星期部分还可允许的特殊字符: ? L #
?
只可用在日期和星期部分。表示没有具体的值,使用?要注意冲突。日期和星期两个部分如果其中一个部分设置了值,则另一个必须设置为 “ ? ”。
例如:
0\* * * 2 * ?和
0\* * * ? * 2
同时使用?和同时不使用?都是不对的
例如下面写法就是错的
* * * 2 * 2和
* * * ? * ?
W
只能用在日期中,表示当月中最接近某天的工作日
0 0 0 31W * ?
表示最接近31号的工作日,如果31号是星期六,则表示30号,即星期五,如果31号是星期天,则表示29号,即星期五。如果31号是星期三,则表示31号本身,即星期三。
L
表示最后(Last),只能用在日期和星期中
在日期中表示每月最后一天,在一月份中表示31号,在六月份中表示30号
也可以表示每月倒是第N天。例如: L-2表示每个月的倒数第2天
0 0 0 LW * ?
LW可以连起来用,表示每月最后一个工作日,即每月最后一个星期五
在星期中表示7即星期六
0 0 0 ? * L
表示每个星期六
0 0 0 ? * 6L
若前面有其他值的话,则表示最后一个星期几,即每月的最后一个星期五
只能用在星期中,表示第几个星期几
0 0 0 ? * 6#3
表示每个月的第三个星期五。
3.18.4 接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
PUT | /article/updateViewCount/{id} | 不需要token请求头 |
参数
请求路径中携带文章id
响应格式:
{"code":200,"msg":"操作成功"
}
3.18.5 代码实现
①在应用启动时把博客的浏览量存储到redis中
实现CommandLineRunner接口,在应用启动时初始化缓存。
@Component
public class ViewCountRunner implements CommandLineRunner {@Autowiredprivate ArticleMapper articleMapper;@Autowiredprivate RedisCache redisCache;@Overridepublic void run(String... args) throws Exception {//查询博客信息 id viewCountList<Article> articles = articleMapper.selectList(null);Map<String, Integer> viewCountMap = articles.stream().collect(Collectors.toMap(article -> article.getId().toString(), article -> {return article.getViewCount().intValue();//}));//存储到redis中redisCache.setCacheMap("article:viewCount",viewCountMap);}
}
②更新浏览量时去更新redsi中的数据
RedisCache增加方法
public void incrementCacheMapValue(String key,String hKey,long v){redisTemplate.boundHashOps(key).increment(hKey, v);}
ArticleController中增加方法更新阅读数
@PutMapping("/updateViewCount/{id}")public ResponseResult updateViewCount(@PathVariable("id") Long id){return articleService.updateViewCount(id);}
ArticleService中增加方法
ResponseResult updateViewCount(Long id);
ArticleServiceImpl中实现方法
@Overridepublic ResponseResult updateViewCount(Long id) {//更新redis中对应 id的浏览量redisCache.incrementCacheMapValue("article:viewCount",id.toString(),1);return ResponseResult.okResult();}
③定时任务每隔10分钟把Redis中的浏览量更新到数据库中
Article中增加构造方法
public Article(Long id, long viewCount) {this.id = id;this.viewCount = viewCount;}
@Component
public class UpdateViewCountJob {@Autowiredprivate RedisCache redisCache;@Autowiredprivate ArticleService articleService;@Scheduled(cron = "0/5 * * * * ?")public void updateViewCount(){//获取redis中的浏览量Map<String, Integer> viewCountMap = redisCache.getCacheMap("article:viewCount");List<Article> articles = viewCountMap.entrySet().stream().map(entry -> new Article(Long.valueOf(entry.getKey()), entry.getValue().longValue())).collect(Collectors.toList());//更新到数据库中articleService.updateBatchById(articles);}
}
④读取文章浏览量时从redis读取
@Overridepublic ResponseResult getArticleDetail(Long id) {//根据id查询文章Article article = getById(id);//从redis中获取viewCountInteger viewCount = redisCache.getCacheMapValue("article:viewCount", id.toString());article.setViewCount(viewCount.longValue());//转换成VOArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class);//根据分类id查询分类名Long categoryId = articleDetailVo.getCategoryId();Category category = categoryService.getById(categoryId);if(category!=null){articleDetailVo.setCategoryName(category.getName());}//封装响应返回return ResponseResult.okResult(articleDetailVo);}
4. Swagger2
4.1 简介
Swagger 是一套基于 OpenAPI 规范构建的开源工具,可以帮助我们设计、构建、记录以及使用 Rest API。
4.2 为什么使用Swagger
当下很多公司都采取前后端分离的开发模式,前端和后端的工作由不同的工程师完成。在这种开发模式下,维持一份及时更新且完整的 Rest API 文档将会极大的提高我们的工作效率。传统意义上的文档都是后端开发人员手动编写的,相信大家也都知道这种方式很难保证文档的及时性,这种文档久而久之也就会失去其参考意义,反而还会加大我们的沟通成本。而 Swagger 给我们提供了一个全新的维护 API 文档的方式,下面我们就来了解一下它的优点:
1.代码变,文档变。只需要少量的注解,Swagger 就可以根据代码自动生成 API 文档,很好的保证了文档的时效性。
2.跨语言性,支持 40 多种语言。
3.Swagger UI 呈现出来的是一份可交互式的 API 文档,我们可以直接在文档页面尝试 API 的调用,省去了准备复杂的调用参数的过程。
4.3 快速入门
4.3.1 引入依赖
<dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId></dependency>
4.3.2 启用Swagger2
在启动类上或者配置类加 @EnableSwagger2 注解
@SpringBootApplication
@MapperScan("com.sangeng.mapper")
@EnableScheduling
@EnableSwagger2
public class SanGengBlogApplication {public static void main(String[] args) {SpringApplication.run(SanGengBlogApplication.class,args);}
}
4.3.3 测试
访问:http://localhost:7777/swagger-ui.html 注意其中localhost和7777要调整成实际项目的域名和端口号。
4.4 具体配置
4.4.1 Controller配置
4.4.1 @Api 注解
属性介绍:
tags 设置标签
description 设置描述信息
@RestController
@RequestMapping("/comment")
@Api(tags = "评论",description = "评论相关接口")
public class CommentController {
}
4.4.2 接口配置
4.4.2.1 接口描述配置@ApiOperation
@GetMapping("/linkCommentList")@ApiOperation(value = "友链评论列表",notes = "获取一页友链评论")public ResponseResult linkCommentList(Integer pageNum,Integer pageSize){return commentService.commentList(SystemConstants.LINK_COMMENT,null,pageNum,pageSize);}
4.4.2.2 接口参数描述
@ApiImplicitParam 用于描述接口的参数,但是一个接口可能有多个参数,所以一般与 @ApiImplicitParams 组合使用。
@GetMapping("/linkCommentList")@ApiOperation(value = "友链评论列表",notes = "获取一页友链评论")@ApiImplicitParams({@ApiImplicitParam(name = "pageNum",value = "页号"),@ApiImplicitParam(name = "pageSize",value = "每页大小")})public ResponseResult linkCommentList(Integer pageNum,Integer pageSize){return commentService.commentList(SystemConstants.LINK_COMMENT,null,pageNum,pageSize);}
4.4.3 实体类配置
4.4.3.1 实体的描述配置@ApiModel
@ApiModel用于描述实体类。
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(description = "添加评论dto")
public class AddCommentDto{//..
}
4.4.3.2 实体的属性的描述配置@ApiModelProperty
@ApiModelProperty用于描述实体的属性
@ApiModelProperty(notes = "评论类型(0代表文章评论,1代表友链评论)")private String type;
4.4.4 文档信息配置
@Configuration
public class SwaggerConfig {@Beanpublic Docket customDocket() {return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.sangeng.controller")).build();}private ApiInfo apiInfo() {Contact contact = new Contact("团队名", "http://www.my.com", "my@my.com");return new ApiInfoBuilder().title("文档标题").description("文档描述").contact(contact) // 联系方式.version("1.1.0") // 版本.build();}
}
5. 博客后台
5.0 准备工作
前端工程启动
npm install
npm run dev
①创建启动类
/*** @Author 三更 B站: https://space.bilibili.com/663528522*/
@SpringBootApplication
@MapperScan("com.sangeng.mapper")
public class BlogAdminApplication {public static void main(String[] args) {SpringApplication.run(BlogAdminApplication.class, args);}
}
②创建application.yml配置文件
server:port: 8989
spring:datasource:url: jdbc:mysql://localhost:3306/sg_blog?characterEncoding=utf-8&serverTimezone=UTCusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverservlet:multipart:max-file-size: 2MBmax-request-size: 5MBmybatis-plus:configuration:# 日志log-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:logic-delete-field: delFlaglogic-delete-value: 1logic-not-delete-value: 0id-type: auto
③ SQL语句
SQL脚本:SGBlog\资源\SQL\sg_tag.sql
④ 创建实体类,Mapper,Service
注意思考这些文件应该写在哪个模块下?
Tag
@SuppressWarnings("serial")
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sg_tag")
public class Tag {@TableIdprivate Long id;private Long createBy;private Date createTime;private Long updateBy;private Date updateTime;//删除标志(0代表未删除,1代表已删除)private Integer delFlag;//备注private String remark;//标签名private String name;}
TagMapper
/*** 标签(Tag)表数据库访问层** @author makejava* @since 2022-07-19 22:33:35*/
public interface TagMapper extends BaseMapper<Tag> {}
TagService
/*** 标签(Tag)表服务接口** @author makejava* @since 2022-07-19 22:33:38*/
public interface TagService extends IService<Tag> {}
TagServiceImpl
/*** 标签(Tag)表服务实现类** @author makejava* @since 2022-07-19 22:33:38*/
@Service("tagService")
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {}
⑤ 创建Controller测试接口
注意思考这些文件应该写在哪个模块下?
TagController /content/tag
@RestController
@RequestMapping("/content/tag")
public class TagController {@Autowiredprivate TagService tagService;@GetMapping("/list")public ResponseResult list(){return ResponseResult.okResult(tagService.list());}
}
⑥添加security相关类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@AutowiredAuthenticationEntryPoint authenticationEntryPoint;@AutowiredAccessDeniedHandler accessDeniedHandler;@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问
// .antMatchers("/login").anonymous()
// //注销接口需要认证才能访问
// .antMatchers("/logout").authenticated()
// .antMatchers("/user/userInfo").authenticated()
// .antMatchers("/upload").authenticated()// 除上面外的所有请求全部不需要认证即可访问.anyRequest().permitAll();//配置异常处理器http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);//关闭默认的注销功能http.logout().disable();//把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//允许跨域http.cors();}@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}
}
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取请求头中的tokenString token = request.getHeader("token");if(!StringUtils.hasText(token)){//说明该接口不需要登录 直接放行filterChain.doFilter(request, response);return;}//解析获取useridClaims claims = null;try {claims = JwtUtil.parseJWT(token);} catch (Exception e) {e.printStackTrace();//token超时 token非法//响应告诉前端需要重新登录ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);WebUtils.renderString(response, JSON.toJSONString(result));return;}String userId = claims.getSubject();//从redis中获取用户信息LoginUser loginUser = redisCache.getCacheObject("login:" + userId);//如果获取不到if(Objects.isNull(loginUser)){//说明登录过期 提示重新登录ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);WebUtils.renderString(response, JSON.toJSONString(result));return;}//存入SecurityContextHolderUsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);filterChain.doFilter(request, response);}}
5.1 后台登录
后台的认证授权也使用SpringSecurity安全框架来实现。
5.1.0 需求
需要实现登录功能
后台所有功能都必须登录才能使用。
5.1.1 接口设计
请求方式 | 请求路径 |
---|---|
POST | /user/login |
请求体:
{"userName":"sg","password":"1234"
}
响应格式:
{"code": 200,"data": {"token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0ODBmOThmYmJkNmI0NjM0OWUyZjY2NTM0NGNjZWY2NSIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTY0Mzg3NDMxNiwiZXhwIjoxNjQzOTYwNzE2fQ.ldLBUvNIxQCGemkCoMgT_0YsjsWndTg5tqfJb77pabk"},"msg": "操作成功"
}
5.1.2 思路分析
登录
①自定义登录接口
调用ProviderManager的方法进行认证 如果认证通过生成jwt
把用户信息存入redis中
②自定义UserDetailsService
在这个实现类中去查询数据库
注意配置passwordEncoder为BCryptPasswordEncoder
校验:
①定义Jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContextHolder
5.1.3 准备工作
①添加依赖
前面已经添加过相关依赖,不需要做什么处理
<!--redis依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--fastjson依赖--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.33</version></dependency><!--jwt依赖--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency>
5.1.4 登录接口代码实现
LoginController
复制一份BlogLoginController ,命名为LoginController,其中注入 LoginService
请求地址修改为/user/login即可
@RestController
public class LoginController {@Autowiredprivate LoginService loginService;@PostMapping("/user/login")public ResponseResult login(@RequestBody User user){if(!StringUtils.hasText(user.getUserName())){//提示 必须要传用户名throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);}return loginService.login(user);}}
LoginService
复制一份BlogLoginService命名为LoginService即可
public interface LoginService {ResponseResult login(User user);}
SecurityConfig
之前已经复制过了
SystemLoginServiceImpl
复制一份,LoginServiceImpl,命名为SystemLoginServiceImpl 实现 LoginService
login方法中存redis的key的前缀修改为login
返回的数据中只要返回token
@Service
public class SystemLoginServiceImpl implements LoginService {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCache redisCache;@Overridepublic ResponseResult login(User user) {UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);//判断是否认证通过if(Objects.isNull(authenticate)){throw new RuntimeException("用户名或密码错误");}//获取userid 生成tokenLoginUser loginUser = (LoginUser) authenticate.getPrincipal();String userId = loginUser.getUser().getId().toString();String jwt = JwtUtil.createJWT(userId);//把用户信息存入redisredisCache.setCacheObject("login:"+userId,loginUser);//把token封装 返回Map<String,String> map = new HashMap<>();map.put("token",jwt);return ResponseResult.okResult(map);}
}
UserDetailServiceImpl
复用原来的即可
LoginUser
复用原来的即可
5.2 后台权限控制及动态路由
需求
后台系统需要能实现不同的用户权限可以看到不同的功能。
用户只能使用他的权限所允许使用的功能。
功能设计
之前在我的SpringSecurity的课程中就介绍过RBAC权限模型。没有学习过的可以去看下 RBAC权限模型 。这里我们就是在RBAC权限模型的基础上去实现这个功能。
表分析
通过需求去分析需要有哪些字段。
建表SQL及初始化数据见:SGBlog\资源\SQL\sg_menu.sql
接口设计
getInfo接口
是
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /getInfo | 需要token请求头 |
请求参数:
无
响应格式:
如果用户id为1代表管理员,roles 中只需要有admin,permissions中需要有所有菜单类型为C或者F的,状态为正常的,未被删除的权限
{"code":200,"data":{"permissions":["system:user:list","system:role:list","system:menu:list","system:user:query","system:user:add"//此次省略1000字],"roles":["admin"],"user":{"avatar":"http://r7yxkqloa.bkt.clouddn.com/2022/03/05/75fd15587811443a9a9a771f24da458d.png","email":"23412332@qq.com","id":1,"nickName":"sg3334","sex":"1"}},"msg":"操作成功"
}
getRouters接口
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /getRouters | 需要token请求头 |
请求参数:
无
响应格式:
前端为了实现动态路由的效果,需要后端有接口能返回用户所能访问的菜单数据。
注意:返回的菜单数据需要体现父子菜单的层级关系
如果用户id为1代表管理员,menus中需要有所有菜单类型为C或者M的,状态为正常的,未被删除的权限
数据格式如下:
{"code":200,"data":{"menus":[{"children":[],"component":"content/article/write/index","createTime":"2022-01-08 11:39:58","icon":"build","id":2023,"menuName":"写博文","menuType":"C","orderNum":"0","parentId":0,"path":"write","perms":"content:article:writer","status":"0","visible":"0"},{"children":[{"children":[],"component":"system/user/index","createTime":"2021-11-12 18:46:19","icon":"user","id":100,"menuName":"用户管理","menuType":"C","orderNum":"1","parentId":1,"path":"user","perms":"system:user:list","status":"0","visible":"0"},{"children":[],"component":"system/role/index","createTime":"2021-11-12 18:46:19","icon":"peoples","id":101,"menuName":"角色管理","menuType":"C","orderNum":"2","parentId":1,"path":"role","perms":"system:role:list","status":"0","visible":"0"},{"children":[],"component":"system/menu/index","createTime":"2021-11-12 18:46:19","icon":"tree-table","id":102,"menuName":"菜单管理","menuType":"C","orderNum":"3","parentId":1,"path":"menu","perms":"system:menu:list","status":"0","visible":"0"}],"createTime":"2021-11-12 18:46:19","icon":"system","id":1,"menuName":"系统管理","menuType":"M","orderNum":"1","parentId":0,"path":"system","perms":"","status":"0","visible":"0"}]},"msg":"操作成功"
}
代码实现
准备工作
生成menu和role表对于的类
getInfo接口
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class AdminUserInfoVo {private List<String> permissions;private List<String> roles;private UserInfoVo user;
}
@RestController
public class LoginController {@Autowiredprivate LoginService loginService;@Autowiredprivate MenuService menuService;@Autowiredprivate RoleService roleService;@PostMapping("/user/login")public ResponseResult login(@RequestBody User user){if(!StringUtils.hasText(user.getUserName())){//提示 必须要传用户名throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);}return loginService.login(user);}@GetMapping("getInfo")public ResponseResult<AdminUserInfoVo> getInfo(){//获取当前登录的用户LoginUser loginUser = SecurityUtils.getLoginUser();//根据用户id查询权限信息List<String> perms = menuService.selectPermsByUserId(loginUser.getUser().getId());//根据用户id查询角色信息List<String> roleKeyList = roleService.selectRoleKeyByUserId(loginUser.getUser().getId());//获取用户信息User user = loginUser.getUser();UserInfoVo userInfoVo = BeanCopyUtils.copyBean(user, UserInfoVo.class);//封装数据返回AdminUserInfoVo adminUserInfoVo = new AdminUserInfoVo(perms,roleKeyList,userInfoVo);return ResponseResult.okResult(adminUserInfoVo);}}
RoleServiceImpl selectRoleKeyByUserId方法
@Service("menuService")
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {@Overridepublic List<String> selectPermsByUserId(Long id) {//如果是管理员,返回所有的权限if(id == 1L){LambdaQueryWrapper<Menu> wrapper = new LambdaQueryWrapper<>();wrapper.in(Menu::getMenuType,SystemConstants.MENU,SystemConstants.BUTTON);wrapper.eq(Menu::getStatus,SystemConstants.STATUS_NORMAL);List<Menu> menus = list(wrapper);List<String> perms = menus.stream().map(Menu::getPerms).collect(Collectors.toList());return perms;}//否则返回所具有的权限return getBaseMapper().selectPermsByUserId(id);}
}
MenuMapper
/*** 菜单权限表(Menu)表数据库访问层** @author makejava* @since 2022-08-09 22:32:07*/
public interface MenuMapper extends BaseMapper<Menu> {List<String> selectPermsByUserId(Long userId);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sangeng.mapper.MenuMapper"><select id="selectPermsByUserId" resultType="java.lang.String">SELECTDISTINCT m.permsFROM`sys_user_role` urLEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`WHEREur.`user_id` = #{userId} ANDm.`menu_type` IN ('C','F') ANDm.`status` = 0 ANDm.`del_flag` = 0</select>
</mapper>
MenuServiceImpl selectPermsByUserId方法
@Service("roleService")
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {@Overridepublic List<String> selectRoleKeyByUserId(Long id) {//判断是否是管理员 如果是返回集合中只需要有adminif(id == 1L){List<String> roleKeys = new ArrayList<>();roleKeys.add("admin");return roleKeys;}//否则查询用户所具有的角色信息return getBaseMapper().selectRoleKeyByUserId(id);}
}
public interface RoleMapper extends BaseMapper<Role> {List<String> selectRoleKeyByUserId(Long userId);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sangeng.mapper.RoleMapper"><select id="selectRoleKeyByUserId" resultType="java.lang.String">SELECTr.`role_key`FROM`sys_user_role` urLEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`WHEREur.`user_id` = #{userId} ANDr.`status` = 0 ANDr.`del_flag` = 0</select>
</mapper>
getRouters接口
RoutersVo
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RoutersVo {private List<Menu> menus;
}
LoginController
@GetMapping("getRouters")public ResponseResult<RoutersVo> getRouters(){Long userId = SecurityUtils.getUserId();//查询menu 结果是tree的形式List<Menu> menus = menuService.selectRouterMenuTreeByUserId(userId);//封装数据返回return ResponseResult.okResult(new RoutersVo(menus));}
MenuService
public interface MenuService extends IService<Menu> {List<String> selectPermsByUserId(Long id);List<Menu> selectRouterMenuTreeByUserId(Long userId);
}
MenuServiceImpl
@Overridepublic List<Menu> selectRouterMenuTreeByUserId(Long userId) {MenuMapper menuMapper = getBaseMapper();List<Menu> menus = null;//判断是否是管理员if(SecurityUtils.isAdmin()){//如果是 获取所有符合要求的Menumenus = menuMapper.selectAllRouterMenu();}else{//否则 获取当前用户所具有的Menumenus = menuMapper.selectRouterMenuTreeByUserId(userId);}//构建tree//先找出第一层的菜单 然后去找他们的子菜单设置到children属性中List<Menu> menuTree = builderMenuTree(menus,0L);return menuTree;}private List<Menu> builderMenuTree(List<Menu> menus, Long parentId) {List<Menu> menuTree = menus.stream().filter(menu -> menu.getParentId().equals(parentId)).map(menu -> menu.setChildren(getChildren(menu, menus))).collect(Collectors.toList());return menuTree;}/*** 获取存入参数的 子Menu集合* @param menu* @param menus* @return*/private List<Menu> getChildren(Menu menu, List<Menu> menus) {List<Menu> childrenList = menus.stream().filter(m -> m.getParentId().equals(menu.getId())).map(m->m.setChildren(getChildren(m,menus))).collect(Collectors.toList());return childrenList;}
MenuMapper.java
List<Menu> selectAllRouterMenu();List<Menu> selectRouterMenuTreeByUserId(Long userId);
MenuMapper.xml
<select id="selectAllRouterMenu" resultType="com.sangeng.domain.entity.Menu">SELECTDISTINCT m.id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, IFNULL(m.perms,'') AS perms, m.is_frame, m.menu_type, m.icon, m.order_num, m.create_timeFROM`sys_menu` mWHEREm.`menu_type` IN ('C','M') ANDm.`status` = 0 ANDm.`del_flag` = 0ORDER BYm.parent_id,m.order_num</select><select id="selectRouterMenuTreeByUserId" resultType="com.sangeng.domain.entity.Menu">SELECTDISTINCT m.id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, IFNULL(m.perms,'') AS perms, m.is_frame, m.menu_type, m.icon, m.order_num, m.create_timeFROM`sys_user_role` urLEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`WHEREur.`user_id` = #{userId} ANDm.`menu_type` IN ('C','M') ANDm.`status` = 0 ANDm.`del_flag` = 0ORDER BYm.parent_id,m.order_num</select>
查询的列:
SELECT DISTINCT m.id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, IFNULL(m.perms,‘’) AS perms, m.is_frame, m.menu_type, m.icon, m.order_num, m.create_time
注意需要按照parent_id和order_num排序
5.3 退出登录接口
5.3.1 接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
POST | /user/logout | 需要token请求头 |
响应格式:
{"code": 200,"msg": "操作成功"
}
5.3.2 代码实现
要实现的操作:
删除redis中的用户信息
LoginController
@PostMapping("/user/logout")public ResponseResult logout(){return loginServcie.logout();}
LoginService
ResponseResult logout();
SystemLoginServiceImpl
@Overridepublic ResponseResult logout() {//获取当前登录的用户idLong userId = SecurityUtils.getUserId();//删除redis中对应的值redisCache.deleteObject("login:"+userId);return ResponseResult.okResult();}
SecurityConfig
要关闭默认的退出登录功能。并且要配置我们的退出登录接口需要认证才能访问
@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/user/login").anonymous()
// //注销接口需要认证才能访问
// .antMatchers("/logout").authenticated()
// .antMatchers("/user/userInfo").authenticated()
// .antMatchers("/upload").authenticated()// 除上面外的所有请求全部不需要认证即可访问.anyRequest().authenticated();//配置异常处理器http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);//关闭默认的注销功能http.logout().disable();//把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//允许跨域http.cors();}
5.4 查询标签列表
5.4.0 需求
为了方便后期对文章进行管理,需要提供标签的功能,一个文章可以有多个标签。
在后台需要分页查询标签功能,要求能根据标签名进行分页查询。 后期可能会增加备注查询等需求。
注意:不能把删除了的标签查询出来。
5.4.1 标签表分析
通过需求去分析需要有哪些字段。
5.4.2 接口设计
请求方式 | 请求路径 |
---|---|
Get | content/tag/list |
Query格式请求参数:
pageNum: 页码
pageSize: 每页条数
name:标签名
remark:备注
响应格式:
{"code":200,"data":{"rows":[{"id":4,"name":"Java","remark":"sdad"}],"total":1},"msg":"操作成功"
}
5.4.3 代码实现
Controller
@RestController
@RequestMapping("/content/tag")
public class TagController {@Autowiredprivate TagService tagService;@GetMapping("/list")public ResponseResult<PageVo> list(Integer pageNum, Integer pageSize, TagListDto tagListDto){return tagService.pageTagList(pageNum,pageSize,tagListDto);}
}
Service
public interface TagService extends IService<Tag> {ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto);
}
@Service("tagService")
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {@Overridepublic ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto) {//分页查询LambdaQueryWrapper<Tag> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(StringUtils.hasText(tagListDto.getName()),Tag::getName,tagListDto.getName());queryWrapper.eq(StringUtils.hasText(tagListDto.getRemark()),Tag::getRemark,tagListDto.getRemark());Page<Tag> page = new Page<>();page.setCurrent(pageNum);page.setSize(pageSize);page(page, queryWrapper);//封装数据返回PageVo pageVo = new PageVo(page.getRecords(),page.getTotal());return ResponseResult.okResult(pageVo);}
}
5.5 新增标签
5.5.0 需求
点击标签管理的新增按钮可以实现新增标签的功能。
5.5.1 接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
POST | /content/tag | 需要token请求头 |
请求体格式:
{"name":"c#","remark":"c++++"}
响应格式:
{"code":200,"msg":"操作成功"
}
5.5.2 测试
测试时注意,添加到数据库中的记录有没有 创建时间,更新时间,创建人,更新人字段。
5.6 删除标签
5.6.1 接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
DELETE | /content/tag/{id} | 需要token请求头 |
请求参数在path中
例如:content/tag/6 代表删除id为6的标签数据
响应格式:
{"code":200,"msg":"操作成功"
}
5.6.2 测试
注意测试删除后在列表中是否查看不到该条数据
数据库中该条数据还是存在的,只是修改了逻辑删除字段的值
5.7 修改标签
5.7.1 接口设计
5.7.1.1 获取标签信息
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /content/tag/{id} | 需要token请求头 |
请求参数在path中
例如:content/tag/6 代表获取id为6的标签数据
响应格式:
{"code":200,"data":{"id":4,"name":"Java","remark":"sdad"},"msg":"操作成功"
}
5.7.1.2 修改标签接口
请求方式 | 请求地址 | 请求头 |
---|---|---|
PUT | /content/tag | 需要token请求头 |
请求体格式:
{"id":7,"name":"c#","remark":"c++++"}
响应格式:
{"code":200,"msg":"操作成功"
}
5.8 写博文
5.8.1 需求
需要提供写博文的功能,写博文时需要关联分类和标签。
可以上传缩略图,也可以在正文中添加图片。
文章可以直接发布,也可以保存到草稿箱。
5.8.2 表分析
标签和文章需要关联所以需要一张关联表。
SQL脚本:SGBlog\资源\SQL\sg_article_tag.sql
5.8.2 接口设计
思考下需要哪些接口才能实现这个功能?
5.8.2.1 查询所有分类接口
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /content/category/listAllCategory | 需要token请求头 |
请求参数:
无
响应格式:
{"code":200,"data":[{"description":"wsd","id":1,"name":"java"},{"description":"wsd","id":2,"name":"PHP"}],"msg":"操作成功"
}
5.8.2.2 查询所有标签接口
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /content/tag/listAllTag | 需要token请求头 |
请求参数:
无
响应格式:
{"code":200,"data":[{"id":1,"name":"Mybatis"},{"id":4,"name":"Java"}],"msg":"操作成功"
}
5.8.2.3 上传图片
请求方式 | 请求地址 | 请求头 |
---|---|---|
POST | /upload | 需要token请求头 |
参数:
img,值为要上传的文件
请求头:
Content-Type :multipart/form-data;
响应格式:
{"code": 200,"data": "文件访问链接","msg": "操作成功"
}
5.8.2.4 新增博文
请求方式 | 请求地址 | 请求头 |
---|---|---|
POST | /content/article | 需要token请求头 |
请求体格式:
{"title":"测试新增博文","thumbnail":"https://sg-blog-oss.oss-cn-beijing.aliyuncs.com/2022/08/21/4ceebc07e7484beba732f12b0d2c43a9.png","isTop":"0","isComment":"0","content":"# 一级标题\n## 二级标题\n![Snipaste_20220228_224837.png](https://img-blog.csdnimg.cn/img_convert/1086c00fadf44a96d1540cf7ce4718c2.png)\n正文","tags":[1,4],"categoryId":1,"summary":"哈哈","status":"1"
}
响应格式:
{"code":200,"msg":"操作成功"
}
5.8.3 代码实现
5.8.3.1 查询所有分类接口
CategoryController
/*** @Author 三更 B站: https://space.bilibili.com/663528522*/
@RestController
@RequestMapping("/content/category")
public class CategoryController {@Autowiredprivate CategoryService categoryService;@GetMapping("/listAllCategory")public ResponseResult listAllCategory(){List<CategoryVo> list = categoryService.listAllCategory();return ResponseResult.okResult(list);}}
CategoryVo修改,增加description属性
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CategoryVo {private Long id;private String name;//描述private String description;
}
CategoryService增加listAllCategory方法
public interface CategoryService extends IService<Category> {ResponseResult getCategoryList();List<CategoryVo> listAllCategory();
}
SystemConstants中增加常量
/** 正常状态 */public static final String NORMAL = "0";
CategoryServiceImpl增加方法
@Overridepublic List<CategoryVo> listAllCategory() {LambdaQueryWrapper<Category> wrapper = new LambdaQueryWrapper<>();wrapper.eq(Category::getStatus, SystemConstants.NORMAL);List<Category> list = list(wrapper);List<CategoryVo> categoryVos = BeanCopyUtils.copyBeanList(list, CategoryVo.class);return categoryVos;}
5.8.3.2 查询所有标签接口
TagVo
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TagVo {private Long id;//标签名private String name;}
TagController
@GetMapping("/listAllTag")public ResponseResult listAllTag(){List<TagVo> list = tagService.listAllTag();return ResponseResult.okResult(list);}
TagService 增加listAllTag方法
List<TagVo> listAllTag();
TagServiceImpl
@Overridepublic List<TagVo> listAllTag() {LambdaQueryWrapper<Tag> wrapper = new LambdaQueryWrapper<>();wrapper.select(Tag::getId,Tag::getName);List<Tag> list = list(wrapper);List<TagVo> tagVos = BeanCopyUtils.copyBeanList(list, TagVo.class);return tagVos;}
5.8.3.3 上传图片接口
在sangeng-admin中增加UploadController
/*** @Author 三更 B站: https://space.bilibili.com/663528522*/
@RestController
public class UploadController {@Autowiredprivate UploadService uploadService;@PostMapping("/upload")public ResponseResult uploadImg(@RequestParam("img") MultipartFile multipartFile) {try {return uploadService.uploadImg(multipartFile);} catch (IOException e) {e.printStackTrace();throw new RuntimeException("文件上传上传失败");}}
}
5.8.3.4 新增博文接口
ArticleController
/*** @Author 三更 B站: https://space.bilibili.com/663528522*/
@RestController
@RequestMapping("/content/article")
public class ArticleController {@Autowiredprivate ArticleService articleService;@PostMappingpublic ResponseResult add(@RequestBody AddArticleDto article){return articleService.add(article);}}
AddArticleDto
注意增加tags属性用于接收文章关联标签的id
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AddArticleDto {private Long id;//标题private String title;//文章内容private String content;//文章摘要private String summary;//所属分类idprivate Long categoryId;//缩略图private String thumbnail;//是否置顶(0否,1是)private String isTop;//状态(0已发布,1草稿)private String status;//访问量private Long viewCount;//是否允许评论 1是,0否private String isComment;private List<Long> tags;}
Article 修改这样创建时间创建人修改时间修改人可以自动填充
@TableField(fill = FieldFill.INSERT)private Long createBy;@TableField(fill = FieldFill.INSERT)private Date createTime;@TableField(fill = FieldFill.INSERT_UPDATE)private Long updateBy;@TableField(fill = FieldFill.INSERT_UPDATE)private Date updateTime;
ArticleService增加方法
ResponseResult add(AddArticleDto article);
创建ArticleTag表相关的实体类,mapper,service,serviceimpl等
@TableName(value="sg_article_tag")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ArticleTag implements Serializable {private static final long serialVersionUID = 625337492348897098L;/*** 文章id*/private Long articleId;/*** 标签id*/private Long tagId;}
ArticleServiceImpl增加如下代码
@Autowiredprivate ArticleTagService articleTagService;@Override@Transactionalpublic ResponseResult add(AddArticleDto articleDto) {//添加 博客Article article = BeanCopyUtils.copyBean(articleDto, Article.class);save(article);List<ArticleTag> articleTags = articleDto.getTags().stream().map(tagId -> new ArticleTag(article.getId(), tagId)).collect(Collectors.toList());//添加 博客和标签的关联articleTagService.saveBatch(articleTags);return ResponseResult.okResult();}
5.9 导出所有分类到Excel
5.9.1 需求
在分类管理中点击导出按钮可以把所有的分类导出到Excel文件中。
5.9.2 技术方案
使用EasyExcel实现Excel的导出操作。
https://github.com/alibaba/easyexcel
https://easyexcel.opensource.alibaba.com/docs/current/quickstart/write#%E7%A4%BA%E4%BE%8B%E4%BB%A3%E7%A0%81-1
5.9.3 接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /content/category/export | 需要token请求头 |
请求参数:
无
响应格式:
成功的话可以直接导出一个Excel文件
失败的话响应格式如下:
{"code":500,"msg":"出现错误"
}
5.9.4 代码实现
工具类方法修改
WebUtils
public static void setDownLoadHeader(String filename, HttpServletResponse response) throws UnsupportedEncodingException {response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setCharacterEncoding("utf-8");String fname= URLEncoder.encode(filename,"UTF-8").replaceAll("\\+", "%20");response.setHeader("Content-disposition","attachment; filename="+fname);}
CategoryController
@GetMapping("/export")public void export(HttpServletResponse response){try {//设置下载文件的请求头WebUtils.setDownLoadHeader("分类.xlsx",response);//获取需要导出的数据List<Category> categoryVos = categoryService.list();List<ExcelCategoryVo> excelCategoryVos = BeanCopyUtils.copyBeanList(categoryVos, ExcelCategoryVo.class);//把数据写入到Excel中EasyExcel.write(response.getOutputStream(), ExcelCategoryVo.class).autoCloseStream(Boolean.FALSE).sheet("分类导出").doWrite(excelCategoryVos);} catch (Exception e) {//如果出现异常也要响应jsonResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR);WebUtils.renderString(response, JSON.toJSONString(result));}}
ExcelCategoryVo
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ExcelCategoryVo {@ExcelProperty("分类名")private String name;//描述@ExcelProperty("描述")private String description;//状态0:正常,1禁用@ExcelProperty("状态0:正常,1禁用")private String status;
}
5.10 权限控制
5.10.1 需求
需要对导出分类的接口做权限控制。
sg eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJkZGJkNjM5MWJiZTA0NmMzOTc4NDg1ZTcxNWQ3YjQ0MSIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTY2MjI0NDE4NywiZXhwIjoxNjYyMzMwNTg3fQ.z4JGwFN3lWyVbOCbhikCe-O4D6SvCQFEE5eQY3jDJkw
sangeng
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0Y2I1ZjhmMTc0Mjk0NzM0YjI4Y2M1NTQzYjQ2Yjc1YyIsInN1YiI6IjYiLCJpc3MiOiJzZyIsImlhdCI6MTY2MjI0NDQzMywiZXhwIjoxNjYyMzMwODMzfQ.yEkbyGYXBp5ndnyq-3acdgpvqx2mnI8B9fK9f3Y6Jco
5.10.2 代码实现
SecurityConfig
@EnableGlobalMethodSecurity(prePostEnabled = true)
UserDetailsServiceImpl
@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate MenuMapper menuMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据用户名查询用户信息LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getUserName,username);User user = userMapper.selectOne(queryWrapper);//判断是否查到用户 如果没查到抛出异常if(Objects.isNull(user)){throw new RuntimeException("用户不存在");}//返回用户信息if(user.getType().equals(SystemConstants.ADMAIN)){List<String> list = menuMapper.selectPermsByUserId(user.getId());return new LoginUser(user,list);}return new LoginUser(user,null);}
}
LoginUser
增加属性
private List<String> permissions;
PermissionService
hasPermisson
@Service("ps")
public class PermissionService {/*** 判断当前用户是否具有permission* @param permission 要判断的权限* @return*/public boolean hasPermission(String permission){//如果是超级管理员 直接返回trueif(SecurityUtils.isAdmin()){return true;}//否则 获取当前登录用户所具有的权限列表 如何判断是否存在permissionList<String> permissions = SecurityUtils.getLoginUser().getPermissions();return permissions.contains(permission);}
}
CategoryController
@PreAuthorize("@ps.hasPermission('content:category:export')")@GetMapping("/export")public void export(HttpServletResponse response){try {//设置下载文件的请求头WebUtils.setDownLoadHeader("分类.xlsx",response);//获取需要导出的数据List<Category> categoryVos = categoryService.list();List<ExcelCategoryVo> excelCategoryVos = BeanCopyUtils.copyBeanList(categoryVos, ExcelCategoryVo.class);//把数据写入到Excel中EasyExcel.write(response.getOutputStream(), ExcelCategoryVo.class).autoCloseStream(Boolean.FALSE).sheet("分类导出").doWrite(excelCategoryVos);} catch (Exception e) {//如果出现异常也要响应jsonResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR);WebUtils.renderString(response, JSON.toJSONString(result));}}
5.11 文章列表
5.10.1 需求
为了对文章进行管理,需要提供文章列表,
在后台需要分页查询文章功能,要求能根据标题和摘要模糊查询。
注意:不能把删除了的文章查询出来
5.10.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | /content/article/list | 是 |
Query格式请求参数:
pageNum: 页码
pageSize: 每页条数
title:文章标题
summary:文章摘要
响应格式:
{"code":200,"data":{"rows":[{"categoryId":"1","content":"嘻嘻嘻嘻嘻嘻","createTime":"2022-01-24 07:20:11","id":"1","isComment":"0","isTop":"1","status":"0","summary":"SpringSecurity框架教程-Spring Security+JWT实现项目级前端分离认证授权","thumbnail":"https://sg-blog-oss.oss-cn-beijing.aliyuncs.com/2022/01/31/948597e164614902ab1662ba8452e106.png","title":"SpringSecurity从入门到精通","viewCount":"161"}],"total":"1"},"msg":"操作成功"
}
5.12 修改文章
5.12.1 需求
点击文章列表中的修改按钮可以跳转到写博文页面。回显示该文章的具体信息。
用户可以在该页面修改文章信息。点击更新按钮后修改文章。
5.12.2 分析
这个功能的实现首先需要能够根据文章id查询文章的详细信息这样才能实现文章的回显。
如何需要提供更新文章的接口。
5.12.3 接口设计
5.12.3.1 查询文章详情接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | content/article/{id} | 是 |
Path格式请求参数:
id: 文章id
响应格式:
{"code":200,"data":{"categoryId":"1","content":"xxxxxxx","createBy":"1","createTime":"2022-08-28 15:15:46","delFlag":0,"id":"10","isComment":"0","isTop":"1","status":"0","summary":"啊实打实","tags":["1","4","5"],"thumbnail":"https://sg-blog-oss.oss-cn-beijing.aliyuncs.com/2022/08/28/7659aac2b74247fe8ebd9e054b916dbf.png","title":"委屈饿驱蚊器","updateBy":"1","updateTime":"2022-08-28 15:15:46","viewCount":"0"},"msg":"操作成功"
}
5.12.3.2 更新文章接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
PUT | content/article | 是 |
请求体参数格式:
{"categoryId":"1","content":"![Snipaste_20220228_224837.png](https://img-blog.csdnimg.cn/img_convert/52a9de421193879754807013bfd7f98b.png)\n\n# 十大\n## 时代的","createBy":"1","createTime":"2022-08-28 15:15:46","delFlag":0,"id":"10","isComment":"0","isTop":"1","status":"0","summary":"啊实打实2","tags":["1","4","5"],"thumbnail":"https://sg-blog-oss.oss-cn-beijing.aliyuncs.com/2022/08/28/7659aac2b74247fe8ebd9e054b916dbf.png","title":"委屈饿驱蚊器","updateBy":"1","updateTime":"2022-08-28 15:15:46","viewCount":"0"
}
响应格式:
{"code":200,"msg":"操作成功"
}
5.13 删除文章
5.13.1 需求
点击文章后面的删除按钮可以删除该文章
注意:是逻辑删除不是物理删除
5.13.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
DELETE | content/article/{id} | 是 |
Path请求参数:
id:要删除的文章id
响应格式:
{"code":200,"msg":"操作成功"
}
5.14 菜单列表
5.14.1 需求
需要展示菜单列表,不需要分页。
可以针对菜单名进行模糊查询
也可以针对菜单的状态进行查询。
菜单要按照父菜单id和orderNum进行排序
5.14.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
GET | system/menu/list | 是 |
Query请求参数:
status : 状态
menuName: 菜单名
响应格式:
{"code":200,"data":[{"component":"content/article/write/index","icon":"build","id":"2023","isFrame":1,"menuName":"写博文","menuType":"C","orderNum":0,"parentId":"0","path":"write","perms":"content:article:writer","remark":"","status":"0","visible":"0"},{"icon":"system","id":"1","isFrame":1,"menuName":"系统管理","menuType":"M","orderNum":1,"parentId":"0","path":"system","perms":"","remark":"系统管理目录","status":"0","visible":"0"},{"icon":"table","id":"2017","isFrame":1,"menuName":"内容管理","menuType":"M","orderNum":4,"parentId":"0","path":"content","remark":"","status":"0","visible":"0"},{"component":"system/user/index","icon":"user","id":"100","isFrame":1,"menuName":"用户管理","menuType":"C","orderNum":1,"parentId":"1","path":"user","perms":"system:user:list","remark":"用户管理菜单","status":"0","visible":"0"},{"component":"system/role/index","icon":"peoples","id":"101","isFrame":1,"menuName":"角色管理","menuType":"C","orderNum":2,"parentId":"1","path":"role","perms":"system:role:list","remark":"角色管理菜单","status":"0","visible":"0"},{"component":"system/menu/index","icon":"tree-table","id":"102","isFrame":1,"menuName":"菜单管理","menuType":"C","orderNum":3,"parentId":"1","path":"menu","perms":"system:menu:list","remark":"菜单管理菜单","status":"0","visible":"0"},{"component":"","icon":"#","id":"1001","isFrame":1,"menuName":"用户查询","menuType":"F","orderNum":1,"parentId":"100","path":"","perms":"system:user:query","remark":"","status":"0","visible":"0"},{"component":"","icon":"#","id":"1002","isFrame":1,"menuName":"用户新增","menuType":"F","orderNum":2,"parentId":"100","path":"","perms":"system:user:add","remark":"","status":"0","visible":"0"},{"component":"","icon":"#","id":"1003","isFrame":1,"menuName":"用户修改","menuType":"F","orderNum":3,"parentId":"100","path":"","perms":"system:user:edit","remark":"","status":"0","visible":"0"},{"component":"","icon":"#","id":"1004","isFrame":1,"menuName":"用户删除","menuType":"F","orderNum":4,"parentId":"100","path":"","perms":"system:user:remove","remark":"","status":"0","visible":"0"},{"component":"","icon":"#","id":"1005","isFrame":1,"menuName":"用户导出","menuType":"F","orderNum":5,"parentId":"100","path":"","perms":"system:user:export","remark":"","status":"0","visible":"0"},{"component":"","icon":"#","id":"1006","isFrame":1,"menuName":"用户导入","menuType":"F","orderNum":6,"parentId":"100","path":"","perms":"system:user:import","remark":"","status":"0","visible":"0"},{"component":"","icon":"#","id":"1007","isFrame":1,"menuName":"重置密码","menuType":"F","orderNum":7,"parentId":"100","path":"","perms":"system:user:resetPwd","remark":"","status":"0","visible":"0"},{"component":"","icon":"#","id":"1008","isFrame":1,"menuName":"角色查询","menuType":"F","orderNum":1,"parentId":"101","path":"","perms":"system:role:query","remark":"","status":"0","visible":"0"},{"component":"","icon":"#","id":"1009","isFrame":1,"menuName":"角色新增","menuType":"F","orderNum":2,"parentId":"101","path":"","perms":"system:role:add","remark":"","status":"0","visible":"0"},{"component":"","icon":"#","id":"1010","isFrame":1,"menuName":"角色修改","menuType":"F","orderNum":3,"parentId":"101","path":"","perms":"system:role:edit","remark":"","status":"0","visible":"0"},{"component":"","icon":"#","id":"1011","isFrame":1,"menuName":"角色删除","menuType":"F","orderNum":4,"parentId":"101","path":"","perms":"system:role:remove","remark":"","status":"0","visible":"0"},{"component":"","icon":"#","id":"1012","isFrame":1,"menuName":"角色导出","menuType":"F","orderNum":5,"parentId":"101","path":"","perms":"system:role:export","remark":"","status":"0","visible":"0"},{"component":"","icon":"#","id":"1013","isFrame":1,"menuName":"菜单查询","menuType":"F","orderNum":1,"parentId":"102","path":"","perms":"system:menu:query","remark":"","status":"0","visible":"0"},{"component":"","icon":"#","id":"1014","isFrame":1,"menuName":"菜单新增","menuType":"F","orderNum":2,"parentId":"102","path":"","perms":"system:menu:add","remark":"","status":"0","visible":"0"},{"component":"","icon":"#","id":"1015","isFrame":1,"menuName":"菜单修改","menuType":"F","orderNum":3,"parentId":"102","path":"","perms":"system:menu:edit","remark":"","status":"0","visible":"0"},{"component":"","icon":"#","id":"1016","isFrame":1,"menuName":"菜单删除","menuType":"F","orderNum":4,"parentId":"102","path":"","perms":"system:menu:remove","remark":"","status":"0","visible":"0"},{"component":"content/article/index","icon":"build","id":"2019","isFrame":1,"menuName":"文章管理","menuType":"C","orderNum":0,"parentId":"2017","path":"article","perms":"content:article:list","remark":"","status":"0","visible":"0"},{"component":"content/category/index","icon":"example","id":"2018","isFrame":1,"menuName":"分类管理","menuType":"C","orderNum":1,"parentId":"2017","path":"category","perms":"content:category:list","remark":"","status":"0","visible":"0"},{"component":"content/link/index","icon":"404","id":"2022","isFrame":1,"menuName":"友链管理","menuType":"C","orderNum":4,"parentId":"2017","path":"link","perms":"content:link:list","remark":"","status":"0","visible":"0"},{"component":"content/tag/index","icon":"button","id":"2021","isFrame":1,"menuName":"标签管理","menuType":"C","orderNum":6,"parentId":"2017","path":"tag","perms":"content:tag:index","remark":"","status":"0","visible":"0"},{"icon":"#","id":"2028","isFrame":1,"menuName":"导出分类","menuType":"F","orderNum":1,"parentId":"2018","path":"","perms":"content:category:export","remark":"","status":"0","visible":"0"},{"icon":"#","id":"2024","isFrame":1,"menuName":"友链新增","menuType":"F","orderNum":0,"parentId":"2022","path":"","perms":"content:link:add","remark":"","status":"0","visible":"0"},{"icon":"#","id":"2025","isFrame":1,"menuName":"友链修改","menuType":"F","orderNum":1,"parentId":"2022","path":"","perms":"content:link:edit","remark":"","status":"0","visible":"0"},{"icon":"#","id":"2026","isFrame":1,"menuName":"友链删除","menuType":"F","orderNum":1,"parentId":"2022","path":"","perms":"content:link:remove","remark":"","status":"0","visible":"0"},{"icon":"#","id":"2027","isFrame":1,"menuName":"友链查询","menuType":"F","orderNum":2,"parentId":"2022","path":"","perms":"content:link:query","remark":"","status":"0","visible":"0"}],"msg":"操作成功"
}
5.15 新增菜单
5.15.1 需求
可以新增菜单
5.15.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
POST | content/article | 是 |
请求体参数:
Menu类对应的json格式
响应格式:
{"code":200,"msg":"操作成功"
}
5.16 修改菜单
5.16.1 需求
能够修改菜单,但是修改的时候不能把父菜单设置为当前菜单,如果设置了需要给出相应的提示。并且修改失败。
5.16.2 接口设计
5.16.2.1 根据id查询菜单数据
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | system/menu/{id} | 是 |
Path格式请求参数:
id: 菜单id
响应格式:
{"code":200,"data":{"icon":"table","id":"2017","menuName":"内容管理","menuType":"M","orderNum":"4","parentId":"0","path":"content","remark":"","status":"0","visible":"0"},"msg":"操作成功"
}
5.16.2.2 更新菜单
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
PUT | system/menu | 是 |
请求体参数:
Menu类对应的json格式
响应格式:
{"code":200,"msg":"操作成功"
}
如果把父菜单设置为当前菜单:
{"code":500,"msg":"修改菜单'写博文'失败,上级菜单不能选择自己"
}
5.17 删除菜单
5.17.1 需求
能够删除菜单,但是如果要删除的菜单有子菜单则提示:存在子菜单不允许删除 并且删除失败。
5.17.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
DELETE | content/article/{menuId} | 是 |
Path参数:
menuId:要删除菜单的id
响应格式:
{"code":200,"msg":"操作成功"
}
如果要删除的菜单有子菜单则
{"code":500,"msg":"存在子菜单不允许删除"
}
5.18 角色列表
5.18.1 需求
需要有角色列表分页查询的功能。
要求能够针对角色名称进行模糊查询。
要求能够针对状态进行查询。
要求按照role_sort进行升序排列。
5.18.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
GET | system/role/list | 是 |
Query格式请求参数:
pageNum: 页码
pageSize: 每页条数
roleName:角色名称
status:状态
响应格式:
{"code":200,"data":{"rows":[{"id":"12","roleKey":"link","roleName":"友链审核员","roleSort":"1","status":"0"}],"total":"1"},"msg":"操作成功"
}
5.19 改变角色状态
5.19.1 需求
要求能够修改角色的停启用状态
5.19.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
PUT | system/role/changeStatus | 是 |
请求体:
{"roleId":"11","status":"1"}
响应格式:
{"code":200,"msg":"操作成功"
}
5.20 新增角色!!
5.20.1 需求
需要提供新增角色的功能。新增角色时能够直接设置角色所关联的菜单权限。
5.20.2 接口设计
5.20.2.1 获取菜单树接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
GET | /system/menu/treeselect | 是 |
无需请求参数
响应格式:
{"code":200,"data":[{"children":[],"id":"2023","label":"写博文","parentId":"0"},{"children":[{"children":[{"children":[],"id":"1001","label":"用户查询","parentId":"100"},{"children":[],"id":"1002","label":"用户新增","parentId":"100"},{"children":[],"id":"1003","label":"用户修改","parentId":"100"},{"children":[],"id":"1004","label":"用户删除","parentId":"100"},{"children":[],"id":"1005","label":"用户导出","parentId":"100"},{"children":[],"id":"1006","label":"用户导入","parentId":"100"},{"children":[],"id":"1007","label":"重置密码","parentId":"100"}],"id":"100","label":"用户管理","parentId":"1"},{"children":[{"children":[],"id":"1008","label":"角色查询","parentId":"101"},{"children":[],"id":"1009","label":"角色新增","parentId":"101"},{"children":[],"id":"1010","label":"角色修改","parentId":"101"},{"children":[],"id":"1011","label":"角色删除","parentId":"101"},{"children":[],"id":"1012","label":"角色导出","parentId":"101"}],"id":"101","label":"角色管理","parentId":"1"},{"children":[{"children":[],"id":"1013","label":"菜单查询","parentId":"102"},{"children":[],"id":"1014","label":"菜单新增","parentId":"102"},{"children":[],"id":"1015","label":"菜单修改","parentId":"102"},{"children":[],"id":"1016","label":"菜单删除","parentId":"102"}],"id":"102","label":"菜单管理","parentId":"1"}],"id":"1","label":"系统管理","parentId":"0"},{"children":[{"children":[],"id":"2019","label":"文章管理","parentId":"2017"},{"children":[{"children":[],"id":"2028","label":"导出分类","parentId":"2018"}],"id":"2018","label":"分类管理","parentId":"2017"},{"children":[{"children":[],"id":"2024","label":"友链新增","parentId":"2022"},{"children":[],"id":"2025","label":"友链修改","parentId":"2022"},{"children":[],"id":"2026","label":"友链删除","parentId":"2022"},{"children":[],"id":"2027","label":"友链查询","parentId":"2022"}],"id":"2022","label":"友链管理","parentId":"2017"},{"children":[],"id":"2021","label":"标签管理","parentId":"2017"}],"id":"2017","label":"内容管理","parentId":"0"}],"msg":"操作成功"
}
5.20.2.2 新增角色接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
POST | system/role | 是 |
请求体:
{"roleName":"测试新增角色","roleKey":"wds","roleSort":0,"status":"0","menuIds":["1","100"],"remark":"我是角色备注"
}
响应格式:
{"code":200,"msg":"操作成功"
}
5.21 修改角色
5.21.1 需求
需要提供修改角色的功能。修改角色时可以修改角色所关联的菜单权限
5.21.2 接口设计
5.21.2.1 角色信息回显接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | system/role/{id} | 是 |
Path格式请求参数:
id: 角色id
响应格式:
{"code":200,"data":{"id":"11","remark":"嘎嘎嘎","roleKey":"aggag","roleName":"嘎嘎嘎","roleSort":"5","status":"0"},"msg":"操作成功"
}
5.21.2.2 加载对应角色菜单列表树接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | /system/menu/roleMenuTreeselect/{id} | 是 |
Path格式请求参数:
id: 角色id
响应格式:
字段介绍
menus:菜单树。
checkedKeys:角色所关联的菜单权限id列表。
{"code":200,"data":{"menus":[{"children":[],"id":"2023","label":"写博文","parentId":"0"},{"children":[{"children":[{"children":[],"id":"1001","label":"用户查询","parentId":"100"},{"children":[],"id":"1002","label":"用户新增","parentId":"100"},{"children":[],"id":"1003","label":"用户修改","parentId":"100"},{"children":[],"id":"1004","label":"用户删除","parentId":"100"},{"children":[],"id":"1005","label":"用户导出","parentId":"100"},{"children":[],"id":"1006","label":"用户导入","parentId":"100"},{"children":[],"id":"1007","label":"重置密码","parentId":"100"}],"id":"100","label":"用户管理","parentId":"1"},{"children":[{"children":[],"id":"1008","label":"角色查询","parentId":"101"},{"children":[],"id":"1009","label":"角色新增","parentId":"101"},{"children":[],"id":"1010","label":"角色修改","parentId":"101"},{"children":[],"id":"1011","label":"角色删除","parentId":"101"},{"children":[],"id":"1012","label":"角色导出","parentId":"101"}],"id":"101","label":"角色管理","parentId":"1"},{"children":[{"children":[],"id":"1013","label":"菜单查询","parentId":"102"},{"children":[],"id":"1014","label":"菜单新增","parentId":"102"},{"children":[],"id":"1015","label":"菜单修改","parentId":"102"},{"children":[],"id":"1016","label":"菜单删除","parentId":"102"}],"id":"102","label":"菜单管理","parentId":"1"}],"id":"1","label":"系统管理","parentId":"0"},{"children":[{"children":[],"id":"2019","label":"文章管理","parentId":"2017"},{"children":[{"children":[],"id":"2028","label":"导出分类","parentId":"2018"}],"id":"2018","label":"分类管理","parentId":"2017"},{"children":[{"children":[],"id":"2024","label":"友链新增","parentId":"2022"},{"children":[],"id":"2025","label":"友链修改","parentId":"2022"},{"children":[],"id":"2026","label":"友链删除","parentId":"2022"},{"children":[],"id":"2027","label":"友链查询","parentId":"2022"}],"id":"2022","label":"友链管理","parentId":"2017"},{"children":[],"id":"2021","label":"标签管理","parentId":"2017"}],"id":"2017","label":"内容管理","parentId":"0"}],"checkedKeys":["1001" ]},"msg":"操作成功"
}
5.21.2.3 更新角色信息接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
PUT | system/role | 是 |
请求体:
{"id":"13","remark":"我是角色备注","roleKey":"wds","roleName":"测试新增角色","roleSort":0,"status":"0","menuIds":["1","100","1001"]
}
响应格式:
{"code":200,"msg":"操作成功"
}
5.22 删除角色
5.22.1 需求
删除固定的某个角色(逻辑删除)
5.22.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
DELETE | system/role/{id} | 是 |
Path请求参数:
id:要删除的角色id
响应格式:
{"code":200,"msg":"操作成功"
}
5.23 用户列表
5.23.1 需求
需要用户分页列表接口。
可以根据用户名模糊搜索。
可以进行手机号的搜索。
可以进行状态的查询。
5.23.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
GET | system/user/list | 是 |
Query格式请求参数:
pageNum: 页码
pageSize: 每页条数
userName:用户名
phonenumber:手机号
status:状态
响应格式:
{"code":200,"data":{"rows":[{"avatar":"http://r7yxkqloa.bkt.clouddn.com/2022/03/05/75fd15587811443a9a9a771f24da458d.png","createTime":"2022-01-05 17:01:56","email":"23412332@qq.com","id":"1","nickName":"sg3334","phonenumber":"18888888888","sex":"1","status":"0","updateBy":"1","updateTime":"2022-03-13 21:36:22","userName":"sg"}],"total":"1"},"msg":"操作成功"
}
5.24 新增用户!!!
5.24.1 需求
需要新增用户功能。新增用户时可以直接关联角色。
注意:新增用户时注意密码加密存储。
用户名不能为空,否则提示:必需填写用户名
用户名必须之前未存在,否则提示:用户名已存在
手机号必须之前未存在,否则提示:手机号已存在
邮箱必须之前未存在,否则提示:邮箱已存在
5.24.2 接口设计
5.24.2.1 查询角色列表接口
注意:查询的是所有状态正常的角色
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
GET | /system/role/listAllRole | 是 |
响应格式:
{"code":200,"data":[{"createBy":"0","createTime":"2021-11-12 18:46:19","delFlag":"0","id":"1","remark":"超级管理员","roleKey":"admin","roleName":"超级管理员","roleSort":"1","status":"0","updateBy":"0"},{"createBy":"0","createTime":"2021-11-12 18:46:19","delFlag":"0","id":"2","remark":"普通角色","roleKey":"common","roleName":"普通角色","roleSort":"2","status":"0","updateBy":"0","updateTime":"2022-01-02 06:32:58"},{"createTime":"2022-01-06 22:07:40","delFlag":"0","id":"11","remark":"嘎嘎嘎","roleKey":"aggag","roleName":"嘎嘎嘎","roleSort":"5","status":"0","updateBy":"1","updateTime":"2022-09-12 10:00:25"},{"createTime":"2022-01-16 14:49:30","delFlag":"0","id":"12","roleKey":"link","roleName":"友链审核员","roleSort":"1","status":"0","updateTime":"2022-01-16 16:05:09"}],"msg":"操作成功"
}
5.24.2.2 新增用户
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
POST | system/user | 是 |
请求体:
{"userName":"wqeree","nickName":"测试新增用户","password":"1234343","phonenumber":"18889778907","email":"233@sq.com","sex":"0","status":"0","roleIds":["2"]
}
响应格式:
{"code":200,"msg":"操作成功"
}
5.25 删除用户
5.25.1 需求
删除固定的某个用户(逻辑删除)
5.25.2 接口设计
不能删除当前操作的用户
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
DELETE | /system/user/{id} | 是 |
Path请求参数:
id:要删除的用户id
响应格式:
{"code":200,"msg":"操作成功"
}
5.26 修改用户
5.26.1 需求
需要提供修改用户的功能。修改用户时可以修改用户所关联的角色。
5.26.2 接口设计
5.26.2.1 根据id查询用户信息回显接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | /system/user/{id} | 是 |
Path格式请求参数:
id: 用户id
响应格式:
roleIds:用户所关联的角色id列表
roles:所有角色的列表
user:用户信息
{"code":200,"data":{"roleIds":["11"],"roles":[{"createBy":"0","createTime":"2021-11-12 18:46:19","delFlag":"0","id":"1","remark":"超级管理员","roleKey":"admin","roleName":"超级管理员","roleSort":"1","status":"0","updateBy":"0"},{"createBy":"0","createTime":"2021-11-12 18:46:19","delFlag":"0","id":"2","remark":"普通角色","roleKey":"common","roleName":"普通角色","roleSort":"2","status":"0","updateBy":"0","updateTime":"2022-01-02 06:32:58"},{"createTime":"2022-01-06 22:07:40","delFlag":"0","id":"11","remark":"嘎嘎嘎","roleKey":"aggag","roleName":"嘎嘎嘎","roleSort":"5","status":"0","updateBy":"1","updateTime":"2022-09-11 20:34:49"},{"createTime":"2022-01-16 14:49:30","delFlag":"0","id":"12","roleKey":"link","roleName":"友链审核员","roleSort":"1","status":"0","updateTime":"2022-01-16 16:05:09"}],"user":{"email":"weq@2132.com","id":"14787164048663","nickName":"sg777","sex":"0","status":"0","userName":"sg777"}},"msg":"操作成功"
}
5.26.2.2 更新用户信息接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
PUT | /system/user | 是 |
请求体:
{"email":"weq@2132.com","id":"14787164048663","nickName":"sg777","sex":"1","status":"0","userName":"sg777","roleIds":["11"]
}
响应格式:
{"code":200,"msg":"操作成功"
}
5.27 分页查询分类列表
5.27.1 需求
需要分页查询分类列表。
能根据分类名称进行模糊查询。
能根据状态进行查询。
5.27.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
GET | content/category/list | 是 |
Query格式请求参数:
pageNum: 页码
pageSize: 每页条数
name:分类名
status: 状态
响应格式:
{"code":200,"data":{"rows":[{"description":"wsd","id":"1","name":"java","status":"0"},{"description":"wsd","id":"2","name":"PHP","status":"0"}],"total":"2"},"msg":"操作成功"
}
5.28 新增分类
5.28.1 需求
需要新增分类功能
5.28.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
POST | /content/category | 是 |
请求体:
{"name":"威威","description":"是的","status":"0"
}
响应格式:
{"code":200,"msg":"操作成功"
}
5.29 修改分类
5.29.1 需求
需要提供修改分类的功能
5.29.2 接口设计
5.29.2.1 根据id查询分类
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | content/category/{id} | 是 |
Path格式请求参数:
id: 分类id
响应格式:
{"code":200,"data":{"description":"qwew","id":"4","name":"ww","status":"0"},"msg":"操作成功"
}
5.29.2.2 更新分类
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
PUT | /content/category | 是 |
请求体:
{"description":"是的","id":"3","name":"威威2","status":"0"
}
响应格式:
{"code":200,"msg":"操作成功"
}
5.30 删除分类
5.30.1 需求
删除某个分类(逻辑删除)
5.30.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
DELETE | /content/category/{id} | 是 |
Path请求参数:
id:要删除的分类id
响应格式:
{"code":200,"msg":"操作成功"
}
5.31 分页查询友链列表
5.31.1 需求
需要分页查询友链列表。
能根据友链名称进行模糊查询。
能根据状态进行查询。
5.31.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
GET | /content/link/list | 是 |
Query格式请求参数:
pageNum: 页码
pageSize: 每页条数
name:友链名
status:状态
响应格式:
{"code":200,"data":{"rows":[{"address":"https://www.baidu.com","description":"sda","id":"1", "logo":"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fn1.itc.cn%2Fimg8%2Fwb%2Frecom%2F2016%2F05%2F10%2F146286696706220328.PNG&refer=http%3A%2F%2Fn1.itc.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1646205529&t=f942665181eb9b0685db7a6f59d59975","name":"sda","status":"0"}],"total":"1"},"msg":"操作成功"
}
5.32 新增友链
5.32.1 需求
需要新增友链功能
5.32.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
POST | /content/link | 是 |
请求体:
{"name":"sda","description":"weqw","address":"wewe","logo":"weqe","status":"2"
}
响应格式:
{"code":200,"msg":"操作成功"
}
5.33 修改友链
5.33.1 需求
需要提供修改友链的功能
5.33.2 接口设计
5.33.2.1 根据id查询友联
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | content/link/{id} | 是 |
Path格式请求参数:
id: 友链id
响应格式:
{"code":200,"data":{"address":"wewe","description":"weqw","id":"4","logo":"weqe","name":"sda","status":"2"},"msg":"操作成功"
}
5.33.2.2 修改友链
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
PUT | /content/link | 是 |
请求体:
{"address":"https://www.qq.com","description":"dada2","id":"2","logo":"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fn1.itc.cn%2Fimg8%2Fwb%2Frecom%2F2016%2F05%2F10%2F146286696706220328.PNG&refer=http%3A%2F%2Fn1.itc.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1646205529&t=f942665181eb9b0685db7a6f59d59975","name":"sda","status":"0"
}
响应格式:
{"code":200,"msg":"操作成功"
}
5.34 删除友链
5.34.1 需求
删除某个友链(逻辑删除)
5.34.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
DELETE | /content/link/{id} | 是 |
Path请求参数:
id:要删除的友链id
响应格式:
{"code":200,"msg":"操作成功"
}