SpringBoot项目实战

本文最后更新于: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>
<!-- 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

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

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
/**
* @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配置文件

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;
//文章类型:1 文章 2草稿
private String type;
//文章摘要
private String summary;
//所属分类id
private Long categoryId;
//缩略图
private String thumbnail;
//是否置顶(0否,1是)
private String isTop;
//状态(0已发布,1草稿)
private String status;
//评论数
private Integer commentCount;
//访问量
private Long viewCount;
//是否允许评论 1是,0否
private String isComment;

private Long createBy;

private Date createTime;

private Long updateBy;

private Date updateTime;
//删除标志(0代表未删除,1代表已删除)
private Integer delFlag;

}


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() {
//查询热门文章 封装成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);
}
}

③ 解决跨域问题

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("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.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() {
//查询热门文章 封装成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 字面值处理

​ 实际项目中都不允许直接在代码中使用字面值。都需要定义成常量来使用。这种方式有利于提高代码的可维护性。

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() {
//查询热门文章 封装成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拷贝工具类封装

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();
//实现属性copy
BeanUtils.copyProperties(source, result);
} catch (Exception e) {
e.printStackTrace();
}
//返回结果
return result;
}
public static <O,V> List<V> copyBeanList(List<O> list,Class<V> clazz){
return list.stream()
.map(o -> copyBean(o, clazz))
.collect(Collectors.toList());
}
}

3.2 查询分类列表

3.2.0 分类表分析

​ 通过需求去分析需要有哪些字段。

​ 建表SQL及初始化数据见:SGBlog\资源\SQL\sg_category.sql

3.2.1 需求

image-20220202111056036

​ 页面上需要展示分类列表,用户可以点击具体的分类查看该分类下的文章列表。

​ 注意: ①要求只展示有发布正式文章的分类 ②必须是正常状态的分类

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})//${column.comment}#end
@TableId
private $!{tool.getClsNameByFullName($column.type)} $!{column.name};
#end

#foreach($column in $tableInfo.otherColumn)
#if(${column.comment})//${column.comment}#end

private $!{tool.getClsNameByFullName($column.type)} $!{column.name};
#end



}

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);
//获取文章的分类id,并且去重
Set<Long> categoryIds = articleList.stream()
.map(article -> article.getCategoryId())
.collect(Collectors.toSet());

//查询分类表
List<Category> categories = listByIds(categoryIds);
categories = categories.stream().
filter(category -> SystemConstants.STATUS_NORMAL.equals(category.getStatus()))
.collect(Collectors.toList());
//封装vo
List<CategoryVo> categoryVos = BeanCopyUtils.copyBeanList(categories, CategoryVo.class);

return ResponseResult.okResult(categoryVos);
}
}

3.3 分页查询文章列表

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
/**
* @Author 三更 B站: https://space.bilibili.com/663528522
*/
@Configuration
public class MbatisPlusConfig {

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

在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() {
//查询热门文章 封装成ResponseResult返回
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
//必须是正式文章
queryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);
//按照浏览量进行排序
queryWrapper.orderByDesc(Article::getViewCount);
//最多只查询10条
Page<Article> page = new Page(1,10);
page(page,queryWrapper);

List<Article> articles = page.getRecords();
//bean拷贝
// List<HotArticleVo> articleVos = new ArrayList<>();
// for (Article article : articles) {
// HotArticleVo vo = new HotArticleVo();
// BeanUtils.copyProperties(article,vo);
// articleVos.add(vo);
// }
List<HotArticleVo> vs = BeanCopyUtils.copyBeanList(articles, HotArticleVo.class);
return ResponseResult.okResult(vs);
}

@Override
public ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId) {
//查询条件
LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
// 如果 有categoryId 就要 查询时要和传入的相同
lambdaQueryWrapper.eq(Objects.nonNull(categoryId)&&categoryId>0 ,Article::getCategoryId,categoryId);
// 状态是正式发布的
lambdaQueryWrapper.eq(Article::getStatus,SystemConstants.ARTICLE_STATUS_NORMAL);
// 对isTop进行降序
lambdaQueryWrapper.orderByDesc(Article::getIsTop);

//分页查询
Page<Article> page = new Page<>(pageNum,pageSize);
page(page,lambdaQueryWrapper);

List<Article> articles = page.getRecords();
//查询categoryName
articles.stream()
.map(article -> article.setCategoryName(categoryService.getById(article.getCategoryId()).getName()))
.collect(Collectors.toList());
//articleId去查询articleName进行设置
// for (Article article : articles) {
// Category category = categoryService.getById(article.getCategoryId());
// article.setCategoryName(category.getName());
// }

//封装查询结果
List<ArticleListVo> articleListVos = BeanCopyUtils.copyBeanList(page.getRecords(), ArticleListVo.class);

PageVo pageVo = new PageVo(articleListVos,page.getTotal());
return ResponseResult.okResult(pageVo);
}
}

PageVo

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//使用@Bean注入fastJsonHttpMessageConvert
public HttpMessageConverter fastJsonHttpMessageConverters() {
//1.需要定义一个Convert转换消息的对象
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");

SerializeConfig.globalInstance.put(Long.class, ToStringSerializer.instance);

fastJsonConfig.setSerializeConfig(SerializeConfig.globalInstance);
fastConverter.setFastJsonConfig(fastJsonConfig);
HttpMessageConverter<?> converter = fastConverter;
return converter;
}

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(fastJsonHttpMessageConverters());
}

3.4 文章详情接口

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) {
//根据id查询文章
Article article = getById(id);
//转换成VO
ArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class);
//根据分类id查询分类名
Long categoryId = articleDetailVo.getCategoryId();
Category category = categoryService.getById(categoryId);
if(category!=null){
articleDetailVo.setCategoryName(category.getName());
}
//封装响应返回
return ResponseResult.okResult(articleDetailVo);
}

3.5 友联查询

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);
//转换成vo
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 接口设计

请求方式 请求路径
POST /login

请求体:

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
<!--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
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
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()
// 除上面外的所有请求全部不需要认证即可访问
.anyRequest().permitAll();


http.logout().disable();
//允许跨域
http.cors();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
BlogLoginServiceImpl
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("用户名或密码错误");
}
//获取userid 生成token
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
//把用户信息存入redis
redisCache.setCacheObject("bloglogin:"+userId,loginUser);

//把token和userinfo封装 返回
//把User转换成UserInfoVo
UserInfoVo userInfoVo = BeanCopyUtils.copyBean(loginUser.getUser(), UserInfoVo.class);
BlogUserLoginVo vo = new BlogUserLoginVo(jwt,userInfoVo);
return ResponseResult.okResult(vo);
}
}
UserDetailServiceImpl
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("用户不存在");
}
//返回用户信息
// TODO 查询权限信息封装
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 {
//获取请求头中的token
String token = request.getHeader("token");
if(!StringUtils.hasText(token)){
//说明该接口不需要登录 直接放行
filterChain.doFilter(request, response);
return;
}
//解析获取userid
Claims claims = null;
try {
claims = JwtUtil.parseJWT(token);
} catch (Exception e) {
e.printStackTrace();
//token超时 token非法
//响应告诉前端需要重新登录
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
WebUtils.renderString(response, JSON.toJSONString(result));
return;
}
String userId = claims.getSubject();
//从redis中获取用户信息
LoginUser loginUser = redisCache.getCacheObject("bloglogin:" + userId);
//如果获取不到
if(Objects.isNull(loginUser)){
//说明登录过期 提示重新登录
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
WebUtils.renderString(response, JSON.toJSONString(result));
return;
}
//存入SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

filterChain.doFilter(request, response);
}


}
SecurityConfig
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
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()
//jwt过滤器测试用,如果测试没有问题吧这里删除了
.antMatchers("/link/getAllLink").authenticated()
// 除上面外的所有请求全部不需要认证即可访问
.anyRequest().permitAll();


http.logout().disable();
//把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//允许跨域
http.cors();
}

@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}

3.7 认证授权失败处理

​ 目前我们的项目在认证出错或者权限不足的时候响应回来的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();
//InsufficientAuthenticationException
//BadCredentialsException
ResponseResult result = null;
if(authException instanceof BadCredentialsException){
result = ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_ERROR.getCode(),authException.getMessage());
}else if(authException instanceof InsufficientAuthenticationException){
result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
}else{
result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR.getCode(),"认证或授权失败");
}
//响应给前端
WebUtils.renderString(response, JSON.toJSONString(result));
}
}

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
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()
//jwt过滤器测试用,如果测试没有问题吧这里删除了
.antMatchers("/link/getAllLink").authenticated()
// 除上面外的所有请求全部不需要认证即可访问
.anyRequest().permitAll();

//配置异常处理器
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);

http.logout().disable();
//把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//允许跨域
http.cors();
}

@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}

3.8 统一异常处理

​ 实际我们在开发过程中可能需要做很多的判断校验,如果出现了非法情况我们是期望响应对应的提示的。但是如果我们每次都自己手动去处理就会非常麻烦。我们可以选择直接抛出异常的方式,然后对异常进行统一处理。把异常中的信息封装成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
/**
* @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

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() {
//获取token 解析获取userid
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
//获取userid
Long userId = loginUser.getUser().getId();
//删除redis中的用户信息
redisCache.deleteObject("bloglogin:"+userId);
return ResponseResult.okResult();
}

SecurityConfig

要关闭默认的退出登录功能。并且要配置我们的退出登录接口需要认证才能访问

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

​ 文章详情页面要展示这篇文章下的评论列表。

​ 效果如下:

image-20220208214106296

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<>();
//对articleId进行判断
queryWrapper.eq(Comment::getArticleId,articleId);
//根评论 rootId为-1
queryWrapper.eq(Comment::getRootId,-1);

//分页查询
Page<Comment> page = new Page(pageNum,pageSize);
page(page,queryWrapper);

List<CommentVo> commentVoList = toCommentVoList(page.getRecords());

return ResponseResult.okResult(new PageVo(commentVoList,page.getTotal()));
}

private List<CommentVo> toCommentVoList(List<Comment> list){
List<CommentVo> commentVos = BeanCopyUtils.copyBeanList(list, CommentVo.class);
//遍历vo集合
for (CommentVo commentVo : commentVos) {
//通过creatyBy查询用户的昵称并赋值
String nickName = userService.getById(commentVo.getCreateBy()).getNickName();
commentVo.setUsername(nickName);
//通过toCommentUserId查询用户的昵称并赋值
//如果toCommentUserId不为-1才进行查询
if(commentVo.getToCommentUserId()!=-1){
String toCommentUserName = userService.getById(commentVo.getToCommentUserId()).getNickName();
commentVo.setToCommentUserName(toCommentUserName);
}
}
return commentVos;
}
}


CommentVo

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;
//文章id
private Long articleId;
//根评论id
private Long rootId;
//评论内容
private String content;
//所回复的目标评论的userid
private Long toCommentUserId;
private String toCommentUserName;
//回复目标评论id
private Long toCommentId;

private Long createBy;

private Date createTime;

private String username;
}

3.10.4.2 查询子评论

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;
//文章id
private Long articleId;
//根评论id
private Long rootId;
//评论内容
private String content;
//所回复的目标评论的userid
private Long toCommentUserId;
private String toCommentUserName;
//回复目标评论id
private Long toCommentId;

private Long createBy;

private Date createTime;

private String username;

private List<CommentVo> children;
}

CommentServiceImpl

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<>();
//对articleId进行判断
queryWrapper.eq(Comment::getArticleId,articleId);
//根评论 rootId为-1
queryWrapper.eq(Comment::getRootId,-1);

//分页查询
Page<Comment> page = new Page(pageNum,pageSize);
page(page,queryWrapper);

List<CommentVo> commentVoList = toCommentVoList(page.getRecords());

//查询所有根评论对应的子评论集合,并且赋值给对应的属性
for (CommentVo commentVo : commentVoList) {
//查询对应的子评论
List<CommentVo> children = getChildren(commentVo.getId());
//赋值
commentVo.setChildren(children);
}

return ResponseResult.okResult(new PageVo(commentVoList,page.getTotal()));
}

/**
* 根据根评论的id查询所对应的子评论的集合
* @param id 根评论的id
* @return
*/
private List<CommentVo> getChildren(Long id) {

LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Comment::getRootId,id);
queryWrapper.orderByAsc(Comment::getCreateTime);
List<Comment> comments = list(queryWrapper);

List<CommentVo> commentVos = toCommentVoList(comments);
return commentVos;
}

private List<CommentVo> toCommentVoList(List<Comment> list){
List<CommentVo> commentVos = BeanCopyUtils.copyBeanList(list, CommentVo.class);
//遍历vo集合
for (CommentVo commentVo : commentVos) {
//通过creatyBy查询用户的昵称并赋值
String nickName = userService.getById(commentVo.getCreateBy()).getNickName();
commentVo.setUsername(nickName);
//通过toCommentUserId查询用户的昵称并赋值
//如果toCommentUserId不为-1才进行查询
if(commentVo.getToCommentUserId()!=-1){
String toCommentUserName = userService.getById(commentVo.getToCommentUserId()).getNickName();
commentVo.setToCommentUserName(toCommentUserName);
}
}
return commentVos;
}
}

3.11 发表评论接口

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
/**
* @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字段自动填充

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
/**
* 创建人的用户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: 每页条数

响应格式:

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<>();
//对articleId进行判断
queryWrapper.eq(SystemConstants.ARTICLE_COMMENT.equals(commentType),Comment::getArticleId,articleId);
//根评论 rootId为-1
queryWrapper.eq(Comment::getRootId,-1);

//评论类型
queryWrapper.eq(Comment::getType,commentType);

//分页查询
Page<Comment> page = new Page(pageNum,pageSize);
page(page,queryWrapper);

List<CommentVo> commentVoList = toCommentVoList(page.getRecords());

//查询所有根评论对应的子评论集合,并且赋值给对应的属性
for (CommentVo commentVo : commentVoList) {
//查询对应的子评论
List<CommentVo> children = getChildren(commentVo.getId());
//赋值
commentVo.setChildren(children);
}

return ResponseResult.okResult(new PageVo(commentVoList,page.getTotal()));
}

3.13 个人信息查询接口

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() {
//获取当前用户id
Long userId = SecurityUtils.getUserId();
//根据用户id查询用户信息
User user = getById(userId);
//封装成UserInfoVo
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
.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 七牛云基本使用测试

image-20220227224537701

image-20220227224443813

秘钥

image-20220228230512598

image-20220228230933808

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(){
//构造一个带指定 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;

响应格式:

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

//如果判断通过上传文件到OSS
String filePath = PathUtils.generateFilePath(originalFilename);
String url = uploadOss(img,filePath);// 2099/2/3/wqeqeqe.png
return ResponseResult.okResult(url);
}

private String accessKey;
private String secretKey;
private String bucket;


private String uploadOss(MultipartFile imgFile, String filePath){
//构造一个带指定 Region 对象的配置类
Configuration cfg = new Configuration(Region.autoRegion());
//...其他参数参考类注释
UploadManager uploadManager = new UploadManager(cfg);
//默认不指定key的情况下,以文件内容的hash值作为文件名
String key = filePath;
try {
InputStream inputStream = imgFile.getInputStream();
Auth auth = Auth.create(accessKey, secretKey);
String upToken = auth.uploadToken(bucket);
try {
Response response = uploadManager.put(inputStream,key,upToken,null, null);
//解析上传成功的结果
DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
System.out.println(putRet.key);
System.out.println(putRet.hash);
return "http://r7yxkqloa.bkt.clouddn.com/"+key;
} catch (QiniuException ex) {
Response r = ex.response;
System.err.println(r.toString());
try {
System.err.println(r.bodyString());
} catch (QiniuException ex2) {
//ignore
}
}
} catch (Exception ex) {
//ignore
}
return "www";
}
}

PathUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @Author 三更 B站: https://space.bilibili.com/663528522
*/
public class PathUtils {

public static String generateFilePath(String fileName){
//根据日期生成路径 2022/1/15/
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd/");
String datePath = sdf.format(new Date());
//uuid作为文件名
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
//后缀和文件后缀一致
int index = fileName.lastIndexOf(".");
// test.jpg -> .jpg
String fileType = fileName.substring(index);
return new StringBuilder().append(datePath).append(uuid).append(fileType).toString();
}
}

3.15 更新个人信息接口

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

​ 需要通过日志记录接口调用信息。便于后期调试排查。并且可能有很多接口都需要进行日志的记录。

​ 接口被调用时日志打印格式如下:

image-20220313133714102

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=======");
// 打印请求 URL
log.info("URL : {}",);
// 打印描述信息
log.info("BusinessName : {}", );
// 打印 Http method
log.info("HTTP Method : {}", );
// 打印调用 controller 的全路径以及执行方法
log.info("Class Method : {}.{}", );
// 打印请求的 IP
log.info("IP : {}",);
// 打印请求入参
log.info("Request Args : {}",);
// 打印出参
log.info("Response : {}", );
// 结束后换行
log.info("=======End=======" + System.lineSeparator());

3.18 更新浏览次数

4. Swagger2使用


SpringBoot项目实战
http://example.com/2021/12/12/SpringBoot项目实战/
作者
Crush
发布于
2021年12月12日
更新于
2023年7月9日
许可协议