本文最后更新于:1 年前
项目实战-前后端分离博客系统
1.课程介绍
- 纯后端讲解
- 完整的前台后台代码编写
- 主流技术栈(SpringBoot,MybatisPlus,SpringSecurity,EasyExcel,Swagger2,Redis,Echarts,Vue,ElementUI….)
- 完善细致的需求分析
- 由易到难循序渐进
2.创建工程
我们有前台和后台两套系统。两套系统的前端工程都已经提供好了。所以我们只需要写两套系统的后端。
但是大家思考下,实际上两套后端系统的很多内容是可能重复的。这里如果我们只是单纯的创建两个后端工程。那么就会有大量的重复代码,并且需要修改的时候也需要修改两次。这就是代码复用性不高。
所以我们需要创建多模块项目,两套系统可能都会用到的代码可以写到一个公共模块中,让前台系统和后台系统分别取依赖公共模块。
① 创建父模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| <?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> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.5.0</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.33</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3</version> </dependency>
<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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| <?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> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
<dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> </dependency>
<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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?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整合配置测试
①创建启动类
1 2 3 4 5 6 7 8 9 10 11
|
@SpringBootApplication @MapperScan("com.sangeng.mapper") public class SanGengBlogApplication {
public static void main(String[] args) { SpringApplication.run(SanGengBlogApplication.class,args); } }
|
②创建application.yml配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| server: port: 7777 spring: datasource: url: jdbc:mysql://localhost:3306/sg_blog?characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 2MB max-request-size: 5MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: delFlag logic-delete-value: 1 logic-not-delete-value: 0 id-type: auto
|
③ SQL语句
SQL脚本:SGBlog\资源\SQL\sg_article.sql
④ 创建实体类,Mapper,Service
注意思考这些文件应该写在哪个模块下?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| @SuppressWarnings("serial") @Data @AllArgsConstructor @NoArgsConstructor @TableName("sg_article") public class Article { @TableId private Long id; private String title; private String content; private String type; private String summary; private Long categoryId; private String thumbnail; private String isTop; private String status; private Integer commentCount; private Long viewCount; private String isComment; private Long createBy; private Date createTime; private Long updateBy; private Date updateTime; private Integer delFlag;
}
|
1 2 3 4 5
| public interface ArticleMapper extends BaseMapper<Article> {
}
|
1 2 3
| public interface ArticleService extends IService<Article> { }
|
1 2 3 4
| @Service public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
}
|
⑤ 创建Controller测试接口
注意思考这些文件应该写在哪个模块下?
1 2 3 4 5 6 7 8 9 10 11 12 13
| @RestController @RequestMapping("/article") public class ArticleController {
@Autowired private 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 基础版本代码实现
①准备工作
统一响应类和响应枚举
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
| 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; }
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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; } }
|
② 代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @RestController @RequestMapping("/article") public class ArticleController {
@Autowired private ArticleService articleService; @GetMapping("/hotArticleList") public ResponseResult hotArticleList(){
ResponseResult result = articleService.hotArticleList(); return result; } }
|
1 2 3 4
| public interface ArticleService extends IService<Article> { ResponseResult hotArticleList(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Service public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
@Override public ResponseResult hotArticleList() { LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Article::getStatus,0); queryWrapper.orderByDesc(Article::getViewCount); Page<Article> page = new Page(1,10); page(page,queryWrapper);
List<Article> articles = page.getRecords(); return ResponseResult.okResult(articles); } }
|
③ 解决跨域问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Configuration public class WebConfig implements WebMvcConfigurer {
@Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowCredentials(true) .allowedMethods("GET", "POST", "DELETE", "PUT") .allowedHeaders("*") .maxAge(3600); }
}
|
3.1.4 使用VO优化
目前我们的响应格式其实是不符合接口文档的标准的,多返回了很多字段。这是因为我们查询出来的结果是Article来封装的,Article中字段比较多。
我们在项目中一般最后还要把VO来接受查询出来的结果。一个接口对应一个VO,这样即使接口响应字段要修改也只要改VO即可。
1 2 3 4 5 6 7 8 9 10 11 12
| @Data @NoArgsConstructor @AllArgsConstructor public class HotArticleVo { private Long id; private String title;
private Long viewCount; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @Service public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
@Override public ResponseResult hotArticleList() { LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Article::getStatus,0); queryWrapper.orderByDesc(Article::getViewCount); Page<Article> page = new Page(1,10); page(page,queryWrapper);
List<Article> articles = page.getRecords(); 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 字面值处理
实际项目中都不允许直接在代码中使用字面值。都需要定义成常量来使用。这种方式有利于提高代码的可维护性。
1 2 3 4 5 6 7 8 9 10 11 12
| public class SystemConstants {
public static final int ARTICLE_STATUS_DRAFT = 1;
public static final int ARTICLE_STATUS_NORMAL = 0; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @Service public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
@Override public ResponseResult hotArticleList() { LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL); queryWrapper.orderByDesc(Article::getViewCount); Page<Article> page = new Page(1,10); page(page,queryWrapper);
List<Article> articles = page.getRecords(); 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拷贝工具类封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class BeanCopyUtils {
private BeanCopyUtils() { }
public static <V> V copyBean(Object source,Class<V> clazz) { V result = null; try { result = clazz.newInstance(); BeanUtils.copyProperties(source, result); } catch (Exception e) { e.printStackTrace(); } return result; } public static <O,V> List<V> copyBeanList(List<O> list,Class<V> clazz){ return list.stream() .map(o -> copyBean(o, clazz)) .collect(Collectors.toList()); } }
|
3.2 查询分类列表
3.2.0 分类表分析
通过需求去分析需要有哪些字段。
建表SQL及初始化数据见:SGBlog\资源\SQL\sg_category.sql
3.2.1 需求

页面上需要展示分类列表,用户可以点击具体的分类查看该分类下的文章列表。
注意: ①要求只展示有发布正式文章的分类 ②必须是正常状态的分类
3.2.2 接口设计
见接口文档
3.2.3 EasyCode代码模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| ##导入宏定义 $!{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}) @TableId private $!{tool.getClsNameByFullName($column.type)} $!{column.name}; #end
#foreach($column in $tableInfo.otherColumn) #if(${column.comment})
private $!{tool.getClsNameByFullName($column.type)} $!{column.name}; #end
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ##导入宏定义 $!{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> {
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ##导入宏定义 $!{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> {
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| ##导入宏定义 $!{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 代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @RestController @RequestMapping("/category") public class CategoryController {
@Autowired private CategoryService categoryService;
@GetMapping("/getCategoryList") public ResponseResult getCategoryList(){ return categoryService.getCategoryList(); } }
|
1 2 3 4 5 6
| public interface CategoryService extends IService<Category> {
ResponseResult getCategoryList();
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @Service("categoryService") public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
@Autowired private ArticleService articleService;
@Override public ResponseResult getCategoryList() { LambdaQueryWrapper<Article> articleWrapper = new LambdaQueryWrapper<>(); articleWrapper.eq(Article::getStatus,SystemConstants.ARTICLE_STATUS_NORMAL); List<Article> articleList = articleService.list(articleWrapper); 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()); List<CategoryVo> categoryVos = BeanCopyUtils.copyBeanList(categories, CategoryVo.class);
return ResponseResult.okResult(categoryVos); } }
|
3.3 分页查询文章列表
3.3.1 需求
在首页和分类页面都需要查询文章列表。
首页:查询所有的文章
分类页面:查询对应分类下的文章
要求:①只能查询正式发布的文章 ②置顶的文章要显示在最前面
3.3.2 接口设计
见文档
3.3.3 代码实现
MP支持分页配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
@Configuration public class MbatisPlusConfig {
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor(){ MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return mybatisPlusInterceptor; } }
|
在ArticleController中
1 2 3 4
| @GetMapping("/articleList") public ResponseResult articleList(Integer pageNum,Integer pageSize,Long categoryId){ return articleService.articleList(pageNum,pageSize,categoryId); }
|
在ArticleService中
1
| ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId);
|
在ArticleServiceImpl中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| @Service public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
@Autowired private CategoryService categoryService;
@Override public ResponseResult hotArticleList() { LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL); queryWrapper.orderByDesc(Article::getViewCount); Page<Article> page = new Page(1,10); page(page,queryWrapper);
List<Article> articles = page.getRecords();
List<HotArticleVo> vs = BeanCopyUtils.copyBeanList(articles, HotArticleVo.class); return ResponseResult.okResult(vs); }
@Override public ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId) { LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.eq(Objects.nonNull(categoryId)&&categoryId>0 ,Article::getCategoryId,categoryId); lambdaQueryWrapper.eq(Article::getStatus,SystemConstants.ARTICLE_STATUS_NORMAL); lambdaQueryWrapper.orderByDesc(Article::getIsTop);
Page<Article> page = new Page<>(pageNum,pageSize); page(page,lambdaQueryWrapper);
List<Article> articles = page.getRecords(); articles.stream() .map(article -> article.setCategoryName(categoryService.getById(article.getCategoryId()).getName())) .collect(Collectors.toList());
List<ArticleListVo> articleListVos = BeanCopyUtils.copyBeanList(page.getRecords(), ArticleListVo.class);
PageVo pageVo = new PageVo(articleListVos,page.getTotal()); return ResponseResult.okResult(pageVo); } }
|
PageVo
1 2 3 4 5 6 7 8
| @Data @NoArgsConstructor @AllArgsConstructor public class PageVo { private List rows; private Long total; }
|
ArticleListVo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @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中增加一个字段
1 2
| @TableField(exist = false) private String categoryName;
|
3.3.4 FastJson配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Bean public HttpMessageConverter fastJsonHttpMessageConverters() { FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); FastJsonConfig fastJsonConfig = new FastJsonConfig(); fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat); fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss"); SerializeConfig.globalInstance.put(Long.class, ToStringSerializer.instance);
fastJsonConfig.setSerializeConfig(SerializeConfig.globalInstance); fastConverter.setFastJsonConfig(fastJsonConfig); HttpMessageConverter<?> converter = fastConverter; return converter; }
@Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(fastJsonHttpMessageConverters()); }
|
3.4 文章详情接口
3.4.1 需求
要求在文章列表点击阅读全文时能够跳转到文章详情页面,可以让用户阅读文章正文。
要求:①要在文章详情中展示其分类名
3.4.2 接口设计
请求方式 |
请求路径 |
Get |
/article/{id} |
响应格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| { "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中新增
1 2 3 4
| @GetMapping("/{id}") public ResponseResult getArticleDetail(@PathVariable("id") Long id){ return articleService.getArticleDetail(id); }
|
Service
1
| ResponseResult getArticleDetail(Long id);
|
ServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Override public ResponseResult getArticleDetail(Long id) { Article article = getById(id); ArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class); 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 |
响应格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| { "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
1 2 3 4 5 6 7 8 9 10 11 12 13
| @RestController @RequestMapping("/link") public class LinkController {
@Autowired private LinkService linkService;
@GetMapping("/getAllLink") public ResponseResult getAllLink(){ return linkService.getAllLink(); } }
|
Service
1 2 3 4 5 6
| public interface LinkService extends IService<Link> {
ResponseResult getAllLink(); }
|
ServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Service("linkService") public class LinkServiceImpl extends ServiceImpl<LinkMapper, Link> implements LinkService {
@Override public ResponseResult getAllLink() { LambdaQueryWrapper<Link> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Link::getStatus, SystemConstants.LINK_STATUS_NORMAL); List<Link> links = list(queryWrapper); List<LinkVo> linkVos = BeanCopyUtils.copyBeanList(links, LinkVo.class); return ResponseResult.okResult(linkVos); } }
|
SystemConstants
1 2 3 4
|
public static final String LINK_STATUS_NORMAL = "0";
|
3.6 登录功能实现
使用我们前台和后台的认证授权统一都使用SpringSecurity安全框架来实现。
3.6.0 需求
需要实现登录功能
有些功能必须登录后才能使用,未登录状态是不能使用的。
3.6.1 接口设计
请求体:
1 2 3 4
| { "userName":"sg", "password":"1234" }
|
响应格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| { "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依赖的注释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.33</version> </dependency>
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
|
②工具类和相关配置类
见 :SGBlog\资源\登录功能所需资源
3.6.5 登录接口代码实现
BlogLoginController
1 2 3 4 5 6 7 8 9 10 11
| @RestController public class BlogLoginController { @Autowired private BlogLoginService blogLoginService;
@PostMapping("/login") public ResponseResult login(@RequestBody User user){ return blogLoginService.login(user); } }
|
BlogLoginService
1 2 3 4
| public interface BlogLoginService { ResponseResult login(User user); }
|
SecurityConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/login").anonymous() .anyRequest().permitAll();
http.logout().disable(); http.cors(); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
|
BlogLoginServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Service public class BlogLoginServiceImpl implements BlogLoginService {
@Autowired private AuthenticationManager authenticationManager;
@Autowired private RedisCache redisCache;
@Override public ResponseResult login(User user) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); if(Objects.isNull(authenticate)){ throw new RuntimeException("用户名或密码错误"); } LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String userId = loginUser.getUser().getId().toString(); String jwt = JwtUtil.createJWT(userId); redisCache.setCacheObject("bloglogin:"+userId,loginUser);
UserInfoVo userInfoVo = BeanCopyUtils.copyBean(loginUser.getUser(), UserInfoVo.class); BlogUserLoginVo vo = new BlogUserLoginVo(jwt,userInfoVo); return ResponseResult.okResult(vo); } }
|
UserDetailServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Service public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired private UserMapper userMapper;
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUserName,username); User user = userMapper.selectOne(queryWrapper); if(Objects.isNull(user)){ throw new RuntimeException("用户不存在"); } return new LoginUser(user); } }
|
LoginUser
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| @Data @AllArgsConstructor @NoArgsConstructor public class LoginUser implements UserDetails {
private User user;
@Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; }
@Override public String getPassword() { return user.getPassword(); }
@Override public String getUsername() { return user.getUserName(); }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return true; } }
|
BlogUserLoginVo
1 2 3 4 5 6 7 8
| @Data @NoArgsConstructor @AllArgsConstructor public class BlogUserLoginVo {
private String token; private UserInfoVo userInfo; }
|
UserInfoVo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired private RedisCache redisCache;
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader("token"); if(!StringUtils.hasText(token)){ filterChain.doFilter(request, response); return; } Claims claims = null; try { claims = JwtUtil.parseJWT(token); } catch (Exception e) { e.printStackTrace(); ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN); WebUtils.renderString(response, JSON.toJSONString(result)); return; } String userId = claims.getSubject(); LoginUser loginUser = redisCache.getCacheObject("bloglogin:" + userId); if(Objects.isNull(loginUser)){ ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN); WebUtils.renderString(response, JSON.toJSONString(result)); return; } UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null); SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response); }
}
|
SecurityConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
@Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/login").anonymous() .antMatchers("/link/getAllLink").authenticated() .anyRequest().permitAll();
http.logout().disable(); http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); http.cors(); }
@Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
|
3.7 认证授权失败处理
目前我们的项目在认证出错或者权限不足的时候响应回来的Json是Security的异常处理结果。但是这个响应的格式肯定是不符合我们项目的接口规范的。所以需要自定义异常处理。
AuthenticationEntryPoint 认证失败处理器
AccessDeniedHandler 授权失败处理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { authException.printStackTrace(); ResponseResult result = null; if(authException instanceof BadCredentialsException){ result = ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_ERROR.getCode(),authException.getMessage()); }else if(authException instanceof InsufficientAuthenticationException){ result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN); }else{ result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR.getCode(),"认证或授权失败"); } WebUtils.renderString(response, JSON.toJSONString(result)); } }
|
1 2 3 4 5 6 7 8 9 10 11
| @Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { accessDeniedException.printStackTrace(); ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NO_OPERATOR_AUTH); WebUtils.renderString(response, JSON.toJSONString(result)); } }
|
配置Security异常处理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
@Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Autowired AuthenticationEntryPoint authenticationEntryPoint; @Autowired AccessDeniedHandler accessDeniedHandler;
@Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/login").anonymous() .antMatchers("/link/getAllLink").authenticated() .anyRequest().permitAll();
http.exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler);
http.logout().disable(); http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); http.cors(); }
@Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
|
3.8 统一异常处理
实际我们在开发过程中可能需要做很多的判断校验,如果出现了非法情况我们是期望响应对应的提示的。但是如果我们每次都自己手动去处理就会非常麻烦。我们可以选择直接抛出异常的方式,然后对异常进行统一处理。把异常中的信息封装成ResponseResult响应给前端。
SystemException
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @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请求头 |
响应格式:
1 2 3 4
| { "code": 200, "msg": "操作成功" }
|
3.9.2 代码实现
要实现的操作:
删除redis中的用户信息
BlogLoginController
1 2 3 4
| @PostMapping("/logout") public ResponseResult logout(){ return blogLoginService.logout(); }
|
BlogLoginService
1
| ResponseResult logout();
|
BlogLoginServiceImpl
1 2 3 4 5 6 7 8 9 10 11
| @Override public ResponseResult logout() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); Long userId = loginUser.getUser().getId(); redisCache.deleteObject("bloglogin:"+userId); return ResponseResult.okResult(); }
|
SecurityConfig
要关闭默认的退出登录功能。并且要配置我们的退出登录接口需要认证才能访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/login").anonymous() .antMatchers("/logout").authenticated() .antMatchers("/link/getAllLink").authenticated() .anyRequest().permitAll();
http.exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler);
http.logout().disable(); 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: 每页条数
响应格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| { "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
1 2 3 4 5 6 7 8 9 10 11 12 13
| @RestController @RequestMapping("/comment") public class CommentController {
@Autowired private CommentService commentService;
@GetMapping("/commentList") public ResponseResult commentList(Long articleId,Integer pageNum,Integer pageSize){ return commentService.commentList(articleId,pageNum,pageSize); } }
|
CommentService
1 2 3 4 5
| public interface CommentService extends IService<Comment> {
ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize); }
|
CommentServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| @Service("commentService") public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {
@Autowired private UserService userService;
@Override public ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize) { LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Comment::getArticleId,articleId); queryWrapper.eq(Comment::getRootId,-1);
Page<Comment> page = new Page(pageNum,pageSize); page(page,queryWrapper);
List<CommentVo> commentVoList = toCommentVoList(page.getRecords());
return ResponseResult.okResult(new PageVo(commentVoList,page.getTotal())); }
private List<CommentVo> toCommentVoList(List<Comment> list){ List<CommentVo> commentVos = BeanCopyUtils.copyBeanList(list, CommentVo.class); for (CommentVo commentVo : commentVos) { String nickName = userService.getById(commentVo.getCreateBy()).getNickName(); commentVo.setUsername(nickName); if(commentVo.getToCommentUserId()!=-1){ String toCommentUserName = userService.getById(commentVo.getToCommentUserId()).getNickName(); commentVo.setToCommentUserName(toCommentUserName); } } return commentVos; } }
|
CommentVo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Data @NoArgsConstructor @AllArgsConstructor public class CommentVo { private Long id; private Long articleId; private Long rootId; private String content; private Long toCommentUserId; private String toCommentUserName; private Long toCommentId;
private Long createBy;
private Date createTime;
private String username; }
|
3.10.4.2 查询子评论
CommentVo在之前的基础上增加了 private List children;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Data @NoArgsConstructor @AllArgsConstructor public class CommentVo { private Long id; private Long articleId; private Long rootId; private String content; private Long toCommentUserId; private String toCommentUserName; private Long toCommentId;
private Long createBy;
private Date createTime;
private String username;
private List<CommentVo> children; }
|
CommentServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| @Service("commentService") public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {
@Autowired private UserService userService;
@Override public ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize) { LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Comment::getArticleId,articleId); queryWrapper.eq(Comment::getRootId,-1);
Page<Comment> page = new Page(pageNum,pageSize); page(page,queryWrapper);
List<CommentVo> commentVoList = toCommentVoList(page.getRecords());
for (CommentVo commentVo : commentVoList) { List<CommentVo> children = getChildren(commentVo.getId()); commentVo.setChildren(children); }
return ResponseResult.okResult(new PageVo(commentVoList,page.getTotal())); }
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); for (CommentVo commentVo : commentVos) { String nickName = userService.getById(commentVo.getCreateBy()).getNickName(); commentVo.setUsername(nickName); 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头 |
请求体:
回复了文章:
1
| {"articleId":1,"type":0,"rootId":-1,"toCommentId":-1,"toCommentUserId":-1,"content":"评论了文章"}
|
回复了某条评论:
1
| {"articleId":1,"type":0,"rootId":"3","toCommentId":"3","toCommentUserId":"1","content":"回复了某条评论"}
|
如果是友链评论,type应该为1
响应格式:
1 2 3 4
| { "code":200, "msg":"操作成功" }
|
3.11.3 代码实现
CommentController
1 2 3 4
| @PostMapping public ResponseResult addComment(@RequestBody Comment comment){ return commentService.addComment(comment); }
|
CommentService
1
| ResponseResult addComment(Comment comment);
|
CommentServiceImpl
1 2 3 4 5 6 7 8 9
| @Override public ResponseResult addComment(Comment comment) { if(!StringUtils.hasText(comment.getContent())){ throw new SystemException(AppHttpCodeEnum.CONTENT_NOT_NULL); } save(comment); return ResponseResult.okResult(); }
|
SecurityUtils
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
public class SecurityUtils {
public static LoginUser getLoginUser() { return (LoginUser) getAuthentication().getPrincipal(); }
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字段自动填充
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { Long userId = null; try { userId = SecurityUtils.getUserId(); } catch (Exception e) { e.printStackTrace(); userId = -1L; } this.setFieldValByName("createTime", new Date(), metaObject); this.setFieldValByName("createBy",userId , metaObject); this.setFieldValByName("updateTime", new Date(), metaObject); this.setFieldValByName("updateBy", userId, metaObject); }
@Override public void updateFill(MetaObject metaObject) { this.setFieldValByName("updateTime", new Date(), metaObject); this.setFieldValByName(" ", SecurityUtils.getUserId(), metaObject); } }
|
用注解标识哪些字段在什么情况下需要自动填充
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
@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: 每页条数
响应格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| { "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 修改了之前的文章评论列表接口,并且增加了新的友联评论接口
1 2 3 4 5 6 7 8
| @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增加了两个常量
1 2 3 4 5 6 7 8
|
public static final String ARTICLE_COMMENT = "0";
public static final String LINK_COMMENT = "1";
|
CommentService修改了commentList方法,增加了一个参数commentType
1
| ResponseResult commentList(String commentType, Long articleId, Integer pageNum, Integer pageSize);
|
CommentServiceImpl修改commentList方法的代码,必须commentType为0的时候才增加articleId的判断,并且增加了一个评论类型的添加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @Override public ResponseResult commentList(String commentType, Long articleId, Integer pageNum, Integer pageSize) { LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SystemConstants.ARTICLE_COMMENT.equals(commentType),Comment::getArticleId,articleId); queryWrapper.eq(Comment::getRootId,-1);
queryWrapper.eq(Comment::getType,commentType);
Page<Comment> page = new Page(pageNum,pageSize); page(page,queryWrapper);
List<CommentVo> commentVoList = toCommentVoList(page.getRecords());
for (CommentVo commentVo : commentVoList) { List<CommentVo> children = getChildren(commentVo.getId()); commentVo.setChildren(children); }
return ResponseResult.okResult(new PageVo(commentVoList,page.getTotal())); }
|
3.13 个人信息查询接口
3.13.1 需求
进入个人中心的时候需要能够查看当前用户信息
3.13.2 接口设计
请求方式 |
请求地址 |
请求头 |
GET |
/user/userInfo |
需要token请求头 |
不需要参数
响应格式:
1 2 3 4 5 6 7 8 9 10 11
| { "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
1 2 3 4 5 6 7 8 9 10 11 12 13
| @RestController @RequestMapping("/user") public class UserController {
@Autowired private UserService userService;
@GetMapping("/userInfo") public ResponseResult userInfo(){ return userService.userInfo(); } }
|
UserService增加方法定义
1 2 3 4 5 6
| public interface UserService extends IService<User> {
ResponseResult userInfo();
}
|
UserServiceImpl实现userInfo方法
1 2 3 4 5 6 7 8 9 10
| @Override public ResponseResult userInfo() { Long userId = SecurityUtils.getUserId(); User user = getById(userId); UserInfoVo vo = BeanCopyUtils.copyBean(user,UserInfoVo.class); return ResponseResult.okResult(vo); }
|
SecurityConfig配置该接口必须认证后才能访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .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(); 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 七牛云测试代码编写
①添加依赖
1 2 3 4 5
| <dependency> <groupId>com.qiniu</groupId> <artifactId>qiniu-java-sdk</artifactId> <version>[7.7.0, 7.7.99]</version> </dependency>
|
②复制修改案例代码
application.yml
1 2 3 4
| oss: accessKey: 1ltxb75nnwzFNIok8MHVI2RcZzgUJDn3qxCSSzgM secretKey: XN4T97vXwUAEUz7LXITWHf3wy6FBHF8pMMtYhwXj bucket: sg-blog
|
OSSTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| @SpringBootTest @ConfigurationProperties(prefix = "oss") public class OSSTest {
private String accessKey; private String secretKey; private String bucket;
public void setAccessKey(String accessKey) { this.accessKey = accessKey; }
public void setSecretKey(String secretKey) { this.secretKey = secretKey; }
public void setBucket(String bucket) { this.bucket = bucket; }
@Test public void testOss(){ Configuration cfg = new Configuration(Region.autoRegion());
UploadManager uploadManager = new UploadManager(cfg);
String key = "2022/sg.png";
try {
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) { } } } catch (Exception ex) { }
} }
|
3.14.2 接口设计
请求方式 |
请求地址 |
请求头 |
POST |
/upload |
需要token |
参数:
img,值为要上传的文件
请求头:
Content-Type :multipart/form-data;
响应格式:
1 2 3 4 5
| { "code": 200, "data": "文件访问链接", "msg": "操作成功" }
|
3.14.3 代码实现
1 2 3 4 5 6 7 8 9 10 11
| @RestController public class UploadController { @Autowired private UploadService uploadService;
@PostMapping("/upload") public ResponseResult uploadImg(MultipartFile img){ return uploadService.uploadImg(img); } }
|
1 2 3 4
| public interface UploadService { ResponseResult uploadImg(MultipartFile img); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| @Service @Data @ConfigurationProperties(prefix = "oss") public class OssUploadService implements UploadService { @Override public ResponseResult uploadImg(MultipartFile img) { String originalFilename = img.getOriginalFilename(); if(!originalFilename.endsWith(".png")){ throw new SystemException(AppHttpCodeEnum.FILE_TYPE_ERROR); }
String filePath = PathUtils.generateFilePath(originalFilename); String url = uploadOss(img,filePath); return ResponseResult.okResult(url); }
private String accessKey; private String secretKey; private String bucket;
private String uploadOss(MultipartFile imgFile, String filePath){ Configuration cfg = new Configuration(Region.autoRegion()); UploadManager uploadManager = new UploadManager(cfg); 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) { } } } catch (Exception ex) { } return "www"; } }
|
PathUtils
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
public class PathUtils {
public static String generateFilePath(String fileName){ SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd/"); String datePath = sdf.format(new Date()); String uuid = UUID.randomUUID().toString().replaceAll("-", ""); int index = fileName.lastIndexOf("."); String 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格式数据:
1 2 3 4 5 6 7
| { "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" }
|
响应格式:
1 2 3 4
| { "code":200, "msg":"操作成功" }
|
3.15.3 代码实现
UserController
1 2 3 4
| @PutMapping("/userInfo") public ResponseResult updateUserInfo(@RequestBody User user){ return userService.updateUserInfo(user); }
|
UserService
1
| ResponseResult updateUserInfo(User user);
|
UserServiceImpl
1 2 3 4 5
| @Override public ResponseResult updateUserInfo(User user) { updateById(user); return ResponseResult.okResult(); }
|
3.16 用户注册
3.16.1 需求
要求用户能够在注册界面完成用户的注册。要求用户名,昵称,邮箱不能和数据库中原有的数据重复。如果某项重复了注册失败并且要有对应的提示。并且要求用户名,密码,昵称,邮箱都不能为空。
注意:密码必须密文存储到数据库中。
3.16.2 接口设计
请求方式 |
请求地址 |
请求头 |
POST |
/user/register |
不需要token请求头 |
参数
请求体中json格式数据:
1 2 3 4 5 6
| { "email": "string", "nickName": "string", "password": "string", "userName": "string" }
|
响应格式:
1 2 3 4
| { "code":200, "msg":"操作成功" }
|
3.16.3 代码实现
UserController
1 2 3 4
| @PostMapping("/register") public ResponseResult register(@RequestBody User user){ return userService.register(user); }
|
UserService
1
| ResponseResult register(User user);
|
UserServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @Autowired private PasswordEncoder passwordEncoder; @Override public ResponseResult register(User user) { if(!StringUtils.hasText(user.getUserName())){ throw new SystemException(AppHttpCodeEnum.USERNAME_NOT_NULL); } if(!StringUtils.hasText(user.getPassword())){ throw new SystemException(AppHttpCodeEnum.PASSWORD_NOT_NULL); } if(!StringUtils.hasText(user.getEmail())){ throw new SystemException(AppHttpCodeEnum.EMAIL_NOT_NULL); } if(!StringUtils.hasText(user.getNickName())){ throw new SystemException(AppHttpCodeEnum.NICKNAME_NOT_NULL); } if(userNameExist(user.getUserName())){ throw new SystemException(AppHttpCodeEnum.USERNAME_EXIST); } if(nickNameExist(user.getNickName())){ throw new SystemException(AppHttpCodeEnum.NICKNAME_EXIST); } String encodePassword = passwordEncoder.encode(user.getPassword()); user.setPassword(encodePassword); save(user); return ResponseResult.okResult(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| 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 代码实现
日志打印格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| log.info("=======Start=======");
log.info("URL : {}",);
log.info("BusinessName : {}", );
log.info("HTTP Method : {}", );
log.info("Class Method : {}.{}", );
log.info("IP : {}",);
log.info("Request Args : {}",);
log.info("Response : {}", );
log.info("=======End=======" + System.lineSeparator());
|
3.18 更新浏览次数
4. Swagger2使用