项目介绍

《谷粒学院》是一个在线教育的网站

技术栈

后端

1
SpringBoot2 

数据库

1
MySQL8+

前端

1
Vue2 + ElementUi

搭建项目结构

创建父工程

新建一个父工程 guli_parent

8558467.jpg

修改在 guli_parent 结构下额 pom.xml 文件

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.szx</groupId>
<artifactId>guli_parent</artifactId>
<packaging>pom</packaging>
<version>0.0.1-SNAPSHOT</version>
<modules>
<module>service</module>
<module>common</module>
</modules>
<name>guli_parent</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<guli.version>0.0.1-SNAPSHOT</guli.version>
<mybatis-plus.version>3.0.5</mybatis-plus.version>
<velocity.version>2.0</velocity.version>
<swagger.version>2.7.0</swagger.version>
<aliyun.oss.version>2.8.3</aliyun.oss.version>
<jodatime.version>2.10.1</jodatime.version>
<poi.version>3.17</poi.version>
<commons-fileupload.version>1.3.1</commons-fileupload.version>
<commons-io.version>2.6</commons-io.version>
<httpclient.version>4.5.1</httpclient.version>
<jwt.version>0.7.0</jwt.version>
<aliyun-java-sdk-core.version>4.3.3</aliyun-java-sdk-core.version>
<aliyun-sdk-oss.version>3.1.0</aliyun-sdk-oss.version>
<aliyun-java-sdk-vod.version>2.15.2</aliyun-java-sdk-vod.version>
<aliyun-java-vod-upload.version>1.4.11</aliyun-java-vod-upload.version>
<aliyun-sdk-vod-upload.version>1.4.11</aliyun-sdk-vod-upload.version>
<fastjson.version>1.2.28</fastjson.version>
<gson.version>2.8.2</gson.version>
<json.version>20170516</json.version>
<commons-dbutils.version>1.7</commons-dbutils.version>
<canal.client.version>1.1.0</canal.client.version>
<docker.image.prefix>zx</docker.image.prefix>
<cloud-alibaba.version>0.2.2.RELEASE</cloud-alibaba.version>
</properties>


<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!--Spring Cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--mybatis-plus 持久层-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>${velocity.version}</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--swagger ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--aliyunOSS-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>${aliyun.oss.version}</version>
</dependency>
<!--日期时间工具-->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>${jodatime.version}</version>
</dependency>
<!--xls-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>${poi.version}</version>
</dependency>
<!--xlsx-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<!--文件上传-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${commons-fileupload.version}</version>
</dependency>
<!--commons-io-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<!--httpclient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpclient.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<!--aliyun-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>${aliyun-java-sdk-core.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>${aliyun-sdk-oss.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-vod</artifactId>
<version>${aliyun-java-sdk-vod.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-vod-upload</artifactId>
<version>${aliyun-java-vod-upload.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-sdk-vod-upload</artifactId>
<version>${aliyun-sdk-vod-upload.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>${json.version}</version>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>${commons-dbutils.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>${canal.client.version}</version>
</dependency>
<dependency>
<groupId>com.szx</groupId>
<artifactId>service-base</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.7.RELEASE</version>
<configuration>
<mainClass>com.szx.guli_parent.GuliParentApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>

新建service包

在 guli_parnet 目录下新建 service Modal 包

8558467.jpg

修改 service 包下面的 pom.xml 文件

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
<?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>guli_parent</artifactId>
<groupId>com.szx</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>service</artifactId>
<packaging>pom</packaging>
<modules>
<module>service_edu</module>
</modules>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<!--<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>-->
<!--hystrix依赖,主要是用 @HystrixCommand -->
<!--<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>-->
<!--服务注册-->
<!--<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>-->
<!--服务调用-->
<!--<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<!--lombok用来简化实体类:需要安装lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--xls-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
</dependency>
<!--httpclient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!--commons-io-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<!--gson-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>com.szx</groupId>
<artifactId>service-base</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.szx</groupId>
<artifactId>common_utils</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>

</project>

新建 service_edu

在上面创建的 service 包下创建子子包 service_edu

8558467.jpg

新建配置文件

在 service_edu 包下的 resource 文件夹中新建 application.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 服务端口
server.port=8001
# 服务名
spring.application.name=service_edu
# 环境设置:dev、test、prod
spring.profiles.active=dev


#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8


# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=abc123


#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

创建启动类

在 service_edu 包中创建启动类

8558467.jpg

配置代码自动生成器

代码结构比较固定,直接复制修改输入的位置和连接的数据库,然后运行即可

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
package com.szx;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import org.junit.Test;

/**
* @author
* @since 2018/12/13
*/
public class CodeGenerator {

@Test
public void run() {

// 1、创建代码生成器
AutoGenerator mpg = new AutoGenerator();

// 2、全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir("D:\\0000学习文件\\Java项目练习\\java_product_guli\\guli_parent\\service\\service_edu" + "/src/main/java");
gc.setAuthor("szx");
gc.setOpen(false); //生成后是否打开资源管理器
gc.setFileOverride(false); //重新生成时文件是否覆盖
gc.setServiceName("%sService"); //去掉Service接口的首字母I
gc.setIdType(IdType.ID_WORKER); //主键策略
gc.setDateType(DateType.ONLY_DATE);//定义生成的实体类中日期类型
gc.setSwagger2(true);//开启Swagger2模式

mpg.setGlobalConfig(gc);

// 3、数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("abc123");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);

// 4、包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.szx"); // 主包名称
pc.setModuleName("edu"); //模块名,生成的结构为:com.szx.edu

pc.setController("controller");
pc.setEntity("entity");
pc.setService("service");
pc.setMapper("mapper");
mpg.setPackageInfo(pc);

// 5、策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setInclude("edu_teacher"); // 数据库表名
strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略
strategy.setTablePrefix(pc.getModuleName() + "_"); //生成实体时去掉表前缀

strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略
strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter链式操作

strategy.setRestControllerStyle(true); //restful api风格控制器
strategy.setControllerMappingHyphenStyle(true); //url中驼峰转连字符

mpg.setStrategy(strategy);


// 6、执行
mpg.execute();
}
}

逻辑删除插件

在 service_edu 包下新建配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.szx.edu.config;

import com.baomidou.mybatisplus.core.injector.ISqlInjector;
import com.baomidou.mybatisplus.extension.injector.LogicSqlInjector;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.szx.edu.mapper")
public class EduConfig {

/**
* 逻辑删除插件
* @return
*/
@Bean
public ISqlInjector sqlInjector() {
return new LogicSqlInjector();
}
}

在实体类上添加 @TableLogic 注解表示该字段表示是否删除的字段

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
package com.szx.edu.entity;

import com.baomidou.mybatisplus.annotation.*;

import java.util.Date;
import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

/**
* <p>
* 讲师
* </p>
*
* @author szx
* @since 2022-09-21
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("edu_teacher")
@ApiModel(value="Teacher对象", description="讲师")
public class Teacher implements Serializable {

private static final long serialVersionUID = 1L;

@ApiModelProperty(value = "讲师ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;

@ApiModelProperty(value = "讲师姓名")
private String name;

@ApiModelProperty(value = "讲师简介")
private String intro;

@ApiModelProperty(value = "讲师资历,一句话说明讲师")
private String career;

@ApiModelProperty(value = "头衔 1高级讲师 2首席讲师")
private Integer level;

@ApiModelProperty(value = "讲师头像")
private String avatar;

@ApiModelProperty(value = "排序")
private Integer sort;

@ApiModelProperty(value = "逻辑删除 1(true)已删除, 0(false)未删除")
@TableLogic
private Integer isDeleted;

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

@ApiModelProperty(value = "更新时间")
private Date gmtModified;
}

讲师查询和删除

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
package com.szx.edu.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.szx.commonutils.Msg;
import com.szx.edu.entity.Teacher;
import com.szx.edu.service.TeacherService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
* <p>
* 讲师 前端控制器
* </p>
*
* @author szx
* @since 2022-09-21
*/
@Api(tags = "讲师模块")
@CrossOrigin // 设置跨域处理
@RestController
@RequestMapping("/edu/teacher")
public class TeacherController {
@Autowired
TeacherService teacherService;

/**
* 获取所有老师数据
* 地址:/edu/teacher/getAllTeacher
* @return
*/
@ApiOperation("根据条件获取所有讲师")
@GetMapping("getAllTeacher")
public Msg getAllTeacher(@ApiParam(name = "name",value = "讲师姓名") @RequestParam("name") String name){
QueryWrapper<Teacher> qw = new QueryWrapper<>();
qw.like("name",name);
List<Teacher> teacherList = teacherService.list(qw);
return Msg.Ok().data("rows",teacherList);
}

/**
* 根据ID删除老师
* 地址:/edu/teacher/{id}
* @param id
* @return
*/
@ApiOperation("根据讲师id删除讲师")
@DeleteMapping("{id}")
public Msg deleteTeacher(@ApiParam(name = "id",value = "讲师id") @PathVariable String id){
boolean flag = teacherService.removeById(id);
if(flag){
return Msg.Ok();
}else{
return Msg.Error();
}
}
}

引入swagger

新建common

在 guli_parent 父工程下新建一个 common 包,表示一些公共的配置

8558467.jpg

修改 pom.xml

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
<?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>guli_parent</artifactId>
<groupId>com.szx</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>common</artifactId>
<packaging>pom</packaging>
<modules>
<module>service-base</module>
<module>common_utils</module>
</modules>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<scope>provided</scope>
</dependency>
<!--lombok用来简化实体类:需要安装lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<scope>provided</scope>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>-->
</dependencies>

</project>

新建service-base

在 common 包下新建 service-base 包

8558467.jpg

新建SwaggerConfig

在如下位置新建

8558467.jpg

代码内容

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
package com.szx.servicebase.config;

import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
* @author songzx
* @create 2022-09-22 11:21
*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket webApiConfig(){
return new Docket(DocumentationType.SWAGGER_2)
.groupName("webApi")
.apiInfo(webApiInfo())
.select()
.paths(Predicates.not(PathSelectors.regex("/admin/.*")))
.paths(Predicates.not(PathSelectors.regex("/error.*")))
.build();
}

public ApiInfo webApiInfo(){
return new ApiInfoBuilder()
.title("网站-课程中心Api文档")
.description("文本档描述了课程中心微服务定义的接口")
.version("1.0")
.contact(new Contact("Helen", "http://atguigu.com","55317332@qq.com"))
.build();
}
}

引入swagger依赖

在 service 包下的 pom 文件中引入我们自己创建的 service-base 包

8558467.jpg

还要再启动类上添加 @ComponentScan(basePackages = {"com.szx"}) 注解,表示全局扫描以 com.szx 文件夹下面的所有组件

8558467.jpg

访问swagger页面

启动程序,在项目地址后面添加固定的地址 /swagger-ui.html

8558467.jpg

添加启动地址信息

修改启动类 main 方法

@SneakyThrows 注解会自动帮我们的代码生成 try catch并向上抛出

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
package com.szx.edu;

import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.core.env.Environment;

import java.net.InetAddress;

@Log4j2
@SpringBootApplication
@ComponentScan(basePackages = {"com.szx"})
public class EduApplication {
@SneakyThrows
public static void main(String[] args) {
ConfigurableApplicationContext application = SpringApplication.run(EduApplication.class, args);
Environment env = application.getEnvironment();
String host = InetAddress.getLocalHost().getHostAddress();
String port = env.getProperty("server.port");
log.info("\n ----------------------------------------------------------\n\t" +
"Application '{}' 正在运行中... Access URLs:\n\t" +
"Local: \t\thttp://localhost:{}\n\t" +
"External: \thttp://{}:{}\n\t" +
"Doc: \thttp://{}:{}/doc.html\n\t" +
"SwaggerDoc: \thttp://{}:{}/swagger-ui.html\n\t" +
"----------------------------------------------------------",
env.getProperty("spring.application.name"),
env.getProperty("server.port"),
host, port,
host, port,
host, port);
}
}

启动效果

8558467.jpg

统一返回对象

新建common_utils

在 common 包下新建 common_utils 包

8558467.jpg

然后定义一个状态码接口

8558467.jpg

新建Msg类

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
package com.szx.commonutils;

import lombok.Data;

import java.util.HashMap;
import java.util.Map;

@Data
public class Msg {
// 成功或者失败的状态码
Integer code;
// 接口返回的描述
String msg;
// 是否成功
boolean success;
// 返回的具体内容
Map<String,Object> data = new HashMap<>();

// 私有化构造方法
private Msg(){};

// 成功方法
public static Msg Ok(){
Msg msg = new Msg();
msg.setCode(ResultCode.SUCCESS);
msg.setMsg("成功");
msg.setSuccess(true);
return msg;
}

// 失败方法
public static Msg Error(){
Msg msg = new Msg();
msg.setCode(ResultCode.ERROR);
msg.setMsg("失败");
msg.setSuccess(false);
return msg;
}

// 设置本次接口返回的msg值
public Msg msg(String msg){
this.setMsg(msg);
return this;
}

// 设置本次返回的具体内容,直接传入一个map集合
public Msg data(HashMap<String,Object> map){
this.setData(map);
return this;
}

// 设置本次返回的data数据,通过key value 的格式
public Msg data(String key,Object value){
this.data.put(key,value);
return this;
}
}

引用common_utils

在 service 包下面的 pom 文件中引入 common_utils

8558467.jpg

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 获取所有老师数据
* 地址:/edu/teacher/getAllTeacher
* @return
*/
@ApiOperation("根据条件获取所有讲师")
@GetMapping("getAllTeacher")
public Msg getAllTeacher(@ApiParam(name = "name",value = "讲师姓名") @RequestParam("name") String name){
QueryWrapper<Teacher> qw = new QueryWrapper<>();
qw.like("name",name);
List<Teacher> teacherList = teacherService.list(qw);
return Msg.Ok().data("rows",teacherList);
}

返回的格式如下

8558467.jpg

分页插件使用

添加分页查询插件

在config配置类中添加分页插件

8558467.jpg

代码如下

1
2
3
4
5
6
7
8
/**
* 分页查询插件
* @return
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}

添加分页查询讲师的接口

1
2
3
4
5
6
7
8
9
10
@ApiOperation("分页查询讲师")
@GetMapping("pageTeacher/{pageNum}/{pageSize}")
public Msg pageTeacher(@ApiParam(value = "页码",required = true) @PathVariable("pageNum") Long pageNum,
@ApiParam(value = "每页总条数",required = true) @PathVariable("pageSize") Long pageSize){
Page<Teacher> teacherPage = new Page<>(pageNum, pageSize);
teacherService.page(teacherPage,null);
long total = teacherPage.getTotal();
List<Teacher> list = teacherPage.getRecords();
return Msg.Ok().data("total",total).data("rows",list);
}

多条件分页查询讲师接口

首先新建一个 TeacherVo

8558467.jpg

1
2
3
4
5
6
7
8
9
10
11
@Data
public class TeacherVo {
@ApiModelProperty("姓名")
String name;
@ApiModelProperty("讲师等级,1:高级讲师,2:首席讲师")
Integer level;
@ApiModelProperty("开始时间")
String startTime;
@ApiModelProperty("结束时间")
String endTime;
}

编写分页带条件查询接口

这里使用 @RequestBody 注解,表示通过 JSON 的方式获取前端传递过来的对象,自动封装到 teacherVo 实例中,并且使用了
@RequestBody 注解后接口的请求方式必须是 post 请求

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
/**
* 多条件分页查询讲师接口
* @param pageNum
* @param pageSize
* @param teacherVo
* @return
*/
@ApiOperation("多条件分页查询讲师接口")
@PostMapping("paramteacher/{pageNum}/{pageSize}")
public Msg paramTeacher(@ApiParam(value = "页码",required = true) @PathVariable("pageNum") Long pageNum,
@ApiParam(value = "每页总条数",required = true) @PathVariable("pageSize") Long pageSize,
@RequestBody TeacherVo teacherVo){
// 分页
Page<Teacher> teacherPage = new Page<>(pageNum, pageSize);
// 条件构造器
QueryWrapper<Teacher> qw = new QueryWrapper<>();

String name = teacherVo.getName();
Integer level = teacherVo.getLevel();
String startTime = teacherVo.getStartTime();
String endTime = teacherVo.getEndTime();

if(!StringUtils.isEmpty(name)){
qw.like("name",name);
}
if(!StringUtils.isEmpty(level)){
qw.like("level",level);
}
if(!StringUtils.isEmpty(startTime)){
// ge 大于等于某个时间
qw.ge("gmt_create",startTime);
}
if(!StringUtils.isEmpty(endTime)){
// le 小于等于某个时间
qw.le("gmt_create",endTime);
}

// 查询数据
teacherService.page(teacherPage,qw);
// 总数
long total = teacherPage.getTotal();
// list
List<Teacher> list = teacherPage.getRecords();
return Msg.Ok().data("total",total).data("rows",list);
}

自动填充创建时间和更新时间

首先在实体类中添加对应的注解

@TableField(fill = FieldFill.INSERT) 自动填充创建时间

@TableField(fill = FieldFill.INSERT_UPDATE) 自动填充更新时间

1
2
3
4
5
6
7
@ApiModelProperty(value = "创建时间")
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;

@ApiModelProperty(value = "更新时间")
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date gmtModified;

然后添加 MetaObjectHandler 接口实现类

在 service-base 包下新建

8558467.jpg

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
package com.szx.servicebase.config;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
* @author songzx
* @create 2022-09-23 13:20
*/
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("gmtCreate", new Date(), metaObject);
this.setFieldValByName("gmtModified", new Date(), metaObject);
}

@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("gmtModified", new Date(), metaObject);
}
}

讲师添加方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 添加讲师
* @param teacher
* @return
*/
@ApiOperation("添加讲师")
@PostMapping("addteacher")
public Msg addTeacher(@RequestBody Teacher teacher){
boolean save = teacherService.save(teacher);
if(save){
return Msg.Ok();
}else{
return Msg.Error();
}
}

新增时不需要传递id、创建时间和修改时间

8558467.jpg

修改讲师方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 根据讲师id查询单条讲师数据
* @param id
* @return
*/
@ApiOperation("根据讲师id查询单条讲师数据")
@GetMapping("getTeacherById/{id}")
public Msg getTeacherById(@PathVariable("id") Long id){
Teacher teacher = teacherService.getById(id);
return Msg.Ok().data("info",teacher);
}

/**
* 根据讲师id修改讲师
* @param teacher
* @return
*/
@ApiOperation("根据讲师id修改讲师")
@PostMapping("updateTeacher")
public Msg updateTeacher(@RequestBody Teacher teacher){
boolean b = teacherService.updateById(teacher);
return b ? Msg.Ok() : Msg.Error();
}

添加统一错误处理

首先在 service-base 包中添加处理类:GlobalExceptionHandler

8558467.jpg

1
2
3
4
5
6
7
8
9
10
11
@ControllerAdvice
public class GlobalExceptionHandler {
// 指定什么异常触发
@ExceptionHandler(Exception.class)
@ResponseBody
public Msg error(Exception e){
e.printStackTrace();
// 将异常转成string返回出去
return Msg.Error().msg(e.toString());
}
}

这里默认找不到 Msg 类,是因为我们需要在 service-base 包下的 pom 文件中引入 common_utils 包

8558467.jpg

然后再 service_edu 中就不要重复的使用 common_utils 依赖

8558467.jpg

测试错误返回

我们在根据id查询讲师的方法中手动添加一个异常代码

8558467.jpg

然后调用这个接口,可以看出返回的是经过我们统一处理后的结果

8558467.jpg

特定异常处理

针对某个特定的异常做处理,例如,针对空指针异常

还是在 GlobalExceptionHandler 类中添加异常处理方法

8558467.jpg

1
2
3
4
5
6
7
8
// 指定空指针异常处理
@ExceptionHandler(NullPointerException.class)
@ResponseBody
public Msg error(NullPointerException e){
e.printStackTrace();
// 将异常转成string返回出去
return Msg.Error().msg(e.toString());
}

自定义异常处理

首先新建一个自定义异常类并且继承 RuntimeException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.szx.servicebase.exceptionhandler;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* @author songzx
* @create 2022-09-23 14:54
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GuliException extends RuntimeException{
public Integer code;
public String msg;
}

然后还是在 GlobalExceptionHandler 中添加自定义的错误处理

1
2
3
4
5
6
7
8
// 自定义异常处理
@ExceptionHandler(GuliException.class)
@ResponseBody
public Msg error(GuliException e){
e.printStackTrace();
// 将异常转成string返回出去
return Msg.Error().msg(e.getMsg());
}

测试自定义异常

发生异常时,需要手动的抛出自定义的异常

1
2
3
4
5
6
7
try {
Map<String,Object> map = null;
map.put("name","张三");
}catch (Exception e){
// 手动抛出自定义异常
throw new GuliException(500,"自定义GuliException异常");
}

返回结果

8558467.jpg

统一日志处理

添加日志配置文件

首先要删除 application.properties 中有关日志的配置

8558467.jpg

在 resources 文件夹中添加 logback-spring.xml 配置文件,名字是固定的

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds">
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->

<contextName>logback</contextName>
<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
<property name="log.path" value="D:/guli_1010/edu"/>

<!-- 彩色日志 -->
<!-- 配置格式变量:CONSOLE_LOG_PATTERN 彩色日志格式 -->
<!-- magenta:洋红 -->
<!-- boldMagenta:粗红-->
<!-- cyan:青色 -->
<!-- white:白色 -->
<!-- magenta:洋红 -->
<property name="CONSOLE_LOG_PATTERN"
value="%yellow(%date{yyyy-MM-dd HH:mm:ss}) |%highlight(%-5level) |%blue(%thread) |%blue(%file:%line) |%green(%logger) |%cyan(%msg%n)"/>


<!--输出到控制台-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
<!-- 例如:如果此处配置了INFO级别,则后面其他位置即使配置了DEBUG级别的日志,也不会被输出 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<!-- 设置字符集 -->
<charset>UTF-8</charset>
</encoder>
</appender>


<!--输出到文件-->

<!-- 时间滚动输出 level为 INFO 日志 -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_info.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天日志归档路径以及格式 -->
<fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录info级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<!-- 时间滚动输出 level为 WARN 日志 -->
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_warn.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录warn级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>warn</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>


<!-- 时间滚动输出 level为 ERROR 日志 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_error.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录ERROR级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<!--
<logger>用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>。
<logger>仅有一个name属性,
一个可选的level和一个可选的addtivity属性。
name:用来指定受此logger约束的某一个包或者具体的某一个类。
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
如果未设置此属性,那么当前logger将会继承上级的级别。
-->
<!--
使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
第一种把<root level="INFO">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
第二种就是单独给mapper下目录配置DEBUG模式,代码如下,这样配置sql语句会打印,其他还是正常DEBUG级别:
-->
<!--开发环境:打印控制台-->
<springProfile name="dev">
<!--可以输出项目中的debug日志,包括mybatis的sql日志-->
<logger name="com.szx" level="INFO"/>

<!--
root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,默认是DEBUG
可以包含零个或多个appender元素。
-->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>


<!--生产环境:输出到文件-->
<springProfile name="pro">

<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="DEBUG_FILE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="ERROR_FILE"/>
<appender-ref ref="WARN_FILE"/>
</root>
</springProfile>

</configuration>

输出日志到文件

在 GlobalExceptionHandler 类上添加 @Slf4j 注解

8558467.jpg

将日志信息写入到文件

log.error(e.getMessage());

1
2
3
4
5
6
7
8
9
10
// 自定义异常处理
@ExceptionHandler(GuliException.class)
@ResponseBody
public Msg error(GuliException e){
// 将错误记录到文件中
log.error(e.getMessage());
e.printStackTrace();
// 将异常转成string返回出去
return Msg.Error().msg(e.getMsg());
}

测试日志打印

启动项目,可以看到控制台的打印颜色会发生变化

8558467.jpg

然后触发一个异常

8558467.jpg

打开 D:\guli_1010\edu 文件夹查看,发现已经有日志文件自动的保存在电脑中

8558467.jpg

查看 error.log 日志内容,当前错误的日志内容比较简单,下面可以将全部的错误信息保存在日志文件中

8558467.jpg

将日志堆栈信息输出到日志

新建 ExceptionUtils 类

8558467.jpg

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
package com.szx.servicebase.utils;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;

/**
* @author songzx
* @create 2022-09-23 16:25
*/
public class ExceptionUtil {
public static String getMessage(Exception e) {
StringWriter sw = null;
PrintWriter pw = null;
try {
sw = new StringWriter();
pw = new PrintWriter(sw);
// 将出错的栈信息输出到printWriter中
e.printStackTrace(pw);
pw.flush();
sw.flush();
} finally {
if (sw != null) {
try {
sw.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if (pw != null) {
pw.close();
}
}
return sw.toString();
}
}

重写 GuliException 的 toString 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GuliException extends RuntimeException{
public Integer code;
public String msg;

@Override
public String toString() {
return "GuliException{" +
"message=" + this.getMessage() +
", code=" + code +
'}';
}
}

输入日志到文件

1
log.error(ExceptionUtil.getMessage(e));

查看日志内容

8558467.jpg

添加前端后台管理系统模板

官网地址:

https://panjiachen.gitee.io/vue-element-admin-site/zh/

上传文件到阿里云OSS

获取AccessKey

首先登录 OSS 控制台查看自己的 AccessKey

8558467.jpg

然后创建bucket

8558467.jpg

添加service_oss模块

在 service 下面新建子包,并初始化如下配置文件

8558467.jpg

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#服务端口
server.port=8002
#服务名
spring.application.name=service_oss
#环境设置:dev、test、prod
spring.profiles.active=dev

#阿里云 OSS
#不同的服务器,地址不同
aliyun.oss.file.endpoint=oss-cn-hangzhou.aliyuncs.com
aliyun.oss.file.keyid=your oss key
aliyun.oss.file.keysecret=xxxxxxxx
#bucket可以在控制台创建,也可以使用java代码创建
aliyun.oss.file.bucketname=szx-bucket1
#文件前缀
aliyun.oss.file.bucketurl=https://szx-bucket1.oss-cn-hangzhou.aliyuncs.com/

导入相关依赖

1
2
3
4
5
6
7
8
9
10
11
<!-- 日期工具栏依赖 -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<!-- 阿里云oss依赖 -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.0</version>
</dependency>

创建启动类,并且在启动类上添加排除数据源的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.szx.oss;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;

/**
* 添加 exclude = DataSourceAutoConfiguration.class 是为了在编译时排除数据源的自动配置项
* @author songzx
* @create 2022-09-25 16:40
*/
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@ComponentScan(basePackages = {"com.szx"})
public class OssApplication {
public static void main(String[] args) {
SpringApplication.run(OssApplication.class,args);
}
}

添加属性工具类

实现 InitializingBean 接口并重写 afterPropertiesSet 方法,会在这个组件初始化完成后调用 afterPropertiesSet 方法

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
package com.szx.oss.utils;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

/**
* @author songzx
* @create 2022-09-25 17:10
*/

@Component
public class OssPropertyUtils implements InitializingBean {
// 地域节点
@Value("${aliyun.oss.file.endpoint}")
private String endpoint;
@Value("${aliyun.oss.file.keyid}")
private String keyid;
@Value("${aliyun.oss.file.keysecret}")
private String keysecret;
// 桶名称
@Value("${aliyun.oss.file.bucketname}")
private String bucketname;
// 文件前缀
@Value("${aliyun.oss.file.bucketurl}")
private String bucketurl;

public static String ENDPOINT;
public static String KEYID;
public static String KEYSECRET;
public static String BUCKETNAME;
public static String BUCKETURL;

// 在组件初始化完成后会触发 afterPropertiesSet 方法
@Override
public void afterPropertiesSet() throws Exception {
OssPropertyUtils.ENDPOINT=endpoint;
OssPropertyUtils.KEYID=keyid;
OssPropertyUtils.KEYSECRET=keysecret;
OssPropertyUtils.BUCKETNAME=bucketname;
OssPropertyUtils.BUCKETURL=bucketurl;
}
}

添加OssService接口和实现类

OssService接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.szx.oss.service;

import org.springframework.web.multipart.MultipartFile;

/**
* @author songzx
* @create 2022-09-25 17:16
*/
public interface OssService {

/**
* 文件上传接口,返回上传后的文件地址
* @param file
* @return
*/
String upload(MultipartFile file);
}

OssService实现类

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
package com.szx.oss.service.impl;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.PutObjectRequest;
import com.szx.oss.service.OssService;
import com.szx.oss.utils.OssPropertyUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
* @author songzx
* @create 2022-09-25 17:16
*/
@Service
public class OssServiceImpl implements OssService {

@Override
public String upload(MultipartFile file) {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = OssPropertyUtils.ENDPOINT;
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = OssPropertyUtils.KEYID;
String accessKeySecret = OssPropertyUtils.KEYSECRET;
// 填写Bucket名称,例如examplebucket。
String bucketName = OssPropertyUtils.BUCKETNAME;
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
// 也可以通过 file.getOriginalFilename() 获取文件实际名称
String objectName = "guli/" + file.getResource().getFilename();
try {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 获取文件流
InputStream inputStream = file.getInputStream();
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, inputStream);
// 关闭实例。
ossClient.shutdown();
} catch (IOException e) {
e.printStackTrace();
}

// 示例:https://szx-bucket1.oss-cn-hangzhou.aliyuncs.com/guli/亿康logo.png
return OssPropertyUtils.BUCKETURL + objectName;
}
}

添加controller完成接口调用

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
package com.szx.oss.controller;

import com.szx.commonutils.Msg;
import com.szx.oss.service.OssService;
import com.szx.oss.service.impl.OssServiceImpl;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

/**
* @author songzx
* @create 2022-09-25 17:16
*/
@Api(tags = "OSS文件上传")
@RestController
@RequestMapping("/server/oss")
public class OssController {

@Autowired
OssServiceImpl ossService;

@ApiOperation("文件上传")
@PostMapping("upload")
public Msg upload(@RequestBody MultipartFile file){

String url = ossService.upload(file);

return Msg.Ok().data("url",url);
}
}

测试

通过 swagger 来测试文件上传

8558467.jpg

查看阿里云oss控制台

8558467.jpg

文件上传完善

上传相同的文件时,后面的会覆盖前面以上传的图片,我们可以通过 uuid 加 时间的方式来防止文件名重复

1
2
3
4
5
6
7
8
// 也可以通过 file.getOriginalFilename() 获取文件实际名称
String objectName = file.getResource().getFilename();
// 文件名称前面添加uuid
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
objectName = uuid + objectName;
// 用年月日当做文件夹
String datePath = new DateTime().toString("yyyy/MM/dd");
objectName = "guli/" + datePath + "/" + objectName;

安装Window版nginx

下载

下载地址:https://nginx.org/download/nginx-1.23.1.zip

下载后解压到没有中文的目录

启动

在 nginx.exe 目录下输入如下命令启动

1
nginx.exe

8558467.jpg

快捷启动

1
2
3
d:
cd D:\Install\rundir\nginx-1.23.1
nginx.exe

停止

在 nginx.exe 目录下输入如下命令停止服务

1
2
3
d:
cd D:\Install\rundir\nginx-1.23.1
nginx.exe -s stop

重启

1
nginx -s reload

修改默认的80端口

8558467.jpg

配置服务转发

通过添加配置,匹配不同的接口路径实现接口转发的目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
http {
server {
# 对外暴露的端口
listen 9001;
# 服务名称:本地
server_name localhost;
# ~ 表示正则匹配路径,包含eduserver的转发到8001
location ~ /eduserver/ {
proxy_pass http://localhost:8001;
}
# ~ 表示正则匹配路径,包含eduoss的转发到8002
location ~ /eduoss/ {
proxy_pass http://localhost:8002;
}
}
}

使用easyExcel

写文件

添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
<!--添加easyExcel-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.1</version>
</dependency>
<!--xls-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.17</version>
</dependency>

添加一个excel对应的实体类

@ExcelProperty("学生编号") 表示要设置文件的标头名称

1
2
3
4
5
6
7
8
9
10
11
12
package com.szx;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

@Data
public class ExcelPojoDemo {
@ExcelProperty("学生编号")
Integer son;
@ExcelProperty("学生姓名")
String sname;
}

添加一个测试方法

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
package com.szx;

import com.alibaba.excel.EasyExcel;

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

public class ExcelWriteDemo {
public static void main(String[] args) {
// 设置文件名称
String fileName = "D:\\学生列表.xls";
// 添加写的操作
EasyExcel.write(fileName, ExcelPojoDemo.class).sheet("学生列表").doWrite(getSname());
}

// 添加返回list方法
public static List<ExcelPojoDemo> getSname(){
ArrayList<ExcelPojoDemo> nameList = new ArrayList<>();

for (int i = 0; i < 10; i++) {
ExcelPojoDemo pojoDemo = new ExcelPojoDemo();
pojoDemo.setSon(i);
pojoDemo.setSname("jack" + i);
nameList.add(pojoDemo);
}

return nameList;
}
}

运行查看 D 盘下生成的文件

8558467.jpg

读取文件

首先在实体类中添加 index 标记

1
2
3
4
5
6
7
@Data
public class ExcelPojoDemo {
@ExcelProperty(value = "学生编号",index = 0)
Integer son;
@ExcelProperty(value = "学生姓名",index = 1)
String sname;
}

添加文件监听器

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.szx;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;

import java.util.Map;

/**
* 实现一个读取Excel的监听器
*/
public class ExcelRadeListener extends AnalysisEventListener<ExcelPojoDemo> {
// 读取文件表头
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
System.out.println("文件表头" + headMap);
}

// 读取文件行,一行一行的读取
@Override
public void invoke(ExcelPojoDemo excelPojoDemo, AnalysisContext analysisContext) {
System.out.println(excelPojoDemo);
}

// 文件读取成功的操作
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
System.out.println("--------------文件读取完成---------------");

}
}

调用读取方法

1
2
3
4
5
6
7
public static void main(String[] args) {
// 设置文件名称
String fileName = "D:\\学生列表.xls";

// 读取操作
EasyExcel.read(fileName,ExcelPojoDemo.class,new ExcelRadeListener()).sheet().doRead();
}

实现读取Excel添加课程分类

读取Excel中的内容保存至数据库

生成代码

首先根据表名自动生成代码,对应的数据库为:edu_subject

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
package com.szx;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import org.junit.Test;

/**
* @author
* @since 2018/12/13
*/
public class CodeGenerator {

@Test
public void run() {

// 1、创建代码生成器
AutoGenerator mpg = new AutoGenerator();

// 2、全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir("D:\\0000学习文件\\Java项目练习\\java_product_guli\\guli_parent\\service\\service_edu" + "/src/main/java");
gc.setAuthor("szx");
gc.setOpen(false); //生成后是否打开资源管理器
gc.setFileOverride(false); //重新生成时文件是否覆盖
gc.setServiceName("%sService"); //去掉Service接口的首字母I
gc.setIdType(IdType.ID_WORKER); //主键策略
gc.setDateType(DateType.ONLY_DATE);//定义生成的实体类中日期类型
gc.setSwagger2(true);//开启Swagger2模式

mpg.setGlobalConfig(gc);

// 3、数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("abc123");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);

// 4、包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.szx"); // 主包名称
pc.setModuleName("edu"); //模块名,生成的结构为:com.szx.edu

pc.setController("controller");
pc.setEntity("entity");
pc.setService("service");
pc.setMapper("mapper");
mpg.setPackageInfo(pc);

// 5、策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setInclude("edu_subject"); // 数据库表名
strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略
strategy.setTablePrefix(pc.getModuleName() + "_"); //生成实体时去掉表前缀

strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略
strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter链式操作

strategy.setRestControllerStyle(true); //restful api风格控制器
strategy.setControllerMappingHyphenStyle(true); //url中驼峰转连字符

mpg.setStrategy(strategy);


// 6、执行
mpg.execute();
}
}

添加Excel对应的实体类

添加Excel文件实体类:SubjectData

8558467.jpg

1
2
3
4
5
6
7
8
9
10
/**
* Excel文件对应的实体类
*/
@Data
public class SubjectData {
@ExcelProperty(value = "一级分类",index = 0)
String oneProperties;
@ExcelProperty(value = "二级分类",index = 1)
String twoProperties;
}

接口添加方法

在 SubjectService 接口中添加方法

1
2
3
4
5
6
7
8
9
10
11
12
/**
* <p>
* 课程科目 服务类
* </p>
*
* @author szx
* @since 2022-09-28
*/
public interface SubjectService extends IService<Subject> {

void addSubjectByExcel(MultipartFile file, SubjectService subjectService);
}

在 SubjectService 接口实现类中实现这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* <p>
* 课程科目 服务实现类
* </p>
*
* @author szx
* @since 2022-09-28
*/
@Service
public class SubjectServiceImpl extends ServiceImpl<SubjectMapper, Subject> implements SubjectService {

@Autowired
SubjectService subjectService;

// 添加课程分类根据excel
@Override
@SneakyThrows
public void addSubjectByExcel(MultipartFile file, SubjectService subjectService) {
InputStream in = file.getInputStream();
EasyExcel.read(in,SubjectData.class,new SubjectListener(subjectService)).sheet().doRead();
}
}

添加监听器

添加对应的 SubjectListener

8558467.jpg

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
package com.szx.edu.listener;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.szx.edu.entity.Subject;
import com.szx.edu.entity.vo.SubjectData;
import com.szx.edu.service.SubjectService;
import lombok.SneakyThrows;

public class SubjectListener extends AnalysisEventListener<SubjectData> {
SubjectService subjectService;

public SubjectListener(){};
// 通过构造函数的方式将service传递进来,因为这个类不交SpringBoot管理,所以不能实现属性的自动注入
// 我们想使用subjectService,就要通过参数构造器将subjectService传递进来,然后赋值使用
public SubjectListener(SubjectService subjectService){
this.subjectService = subjectService;
};

@Override
public void invoke(SubjectData subjectData, AnalysisContext analysisContext) {
// 获取一级分类的名称
String oneTitle = subjectData.getOneProperties();
// 从数据库中读取这个一级分类是否存在
Subject oneSubject = getOneSubject(oneTitle,"0");
if(oneSubject == null){
// 重新new一个新的subject对象
oneSubject = new Subject();
oneSubject.setTitle(oneTitle);
// 添加一级分类到数据库,并将插入的结果赋值给 oneSubject
subjectService.save(oneSubject);
}

// 读取二级分类的名称
String twoTitle = subjectData.getTwoProperties();
// 获取一级分类的id
String parentId = oneSubject.getId();

Subject twoSubject = getOneSubject(twoTitle, parentId);
if(twoSubject == null){
twoSubject = new Subject();
twoSubject.setTitle(twoTitle);
twoSubject.setParentId(parentId);
// 保存二级分类到数据库中
subjectService.save(twoSubject);
}
}

@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {

}

// 判断一级/二级分类是否已经在数据库中
private Subject getOneSubject(String title,String parentId){
QueryWrapper<Subject> qw = new QueryWrapper<>();
qw.eq("title",title);
qw.eq("parent_id",parentId);
return subjectService.getOne(qw);
}
}

添加controller

添加controller进行调用

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
package com.szx.edu.controller;


import com.szx.commonutils.Msg;
import com.szx.edu.entity.Subject;
import com.szx.edu.service.SubjectService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;

/**
* <p>
* 课程科目 前端控制器
* </p>
*
* @author szx
* @since 2022-09-28
*/
@Api(tags = "课程分类")
@CrossOrigin
@RestController
@RequestMapping("/eduserver/subject")
public class SubjectController {

@Autowired
SubjectService subjectService;

@ApiOperation("添加课程分类")
@PostMapping("addSubject")
public Msg addSubject(MultipartFile file){
subjectService.addSubjectByExcel(file,subjectService);
return Msg.Ok();
}
}

swagger测试

首先准备如下内容的一个Excel文件

8558467.jpg

然后清空数据库表中的旧数据

8558467.jpg

打开swagger进行上传导入测试

8558467.jpg

查看数据库

8558467.jpg

导入成功

树形结构的格式返回分类信息

添加children属性

首先在表对应的实体类中添加一个属性children,用来表示二级分类的数据

8558467.jpg

@TableField(exist = false) 注解表示这个属性在数据库中没有,但是是必须的

1
2
3
@ApiModelProperty(value = "二级分类数据")
@TableField(exist = false) // 表示这个属性在数据库中没有,但是是必须的
private ArrayList<Subject> children;

接口添加查询方法

在 SubjectService 接口中添加 getSubjectTree 方法,返回的是一个 list

1
2
3
4
public interface SubjectService extends IService<Subject> {

ArrayList<Subject> getSubjectTree();
}

实现类中实现改方法

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
package com.szx.edu.service.impl;

import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.szx.edu.entity.Subject;
import com.szx.edu.entity.vo.SubjectData;
import com.szx.edu.listener.SubjectListener;
import com.szx.edu.mapper.SubjectMapper;
import com.szx.edu.service.SubjectService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
* <p>
* 课程科目 服务实现类
* </p>
*
* @author szx
* @since 2022-09-28
*/
@Service
public class SubjectServiceImpl extends ServiceImpl<SubjectMapper, Subject> implements SubjectService {

@Autowired
SubjectService subjectService;

// 添加课程分类根据excel
@Override
@SneakyThrows
public void addSubjectByExcel(MultipartFile file, SubjectService subjectService) {
InputStream in = file.getInputStream();
EasyExcel.read(in,SubjectData.class,new SubjectListener(subjectService)).sheet().doRead();
}

@Override
public ArrayList<Subject> getSubjectTree() {
// 先获取一级分类
QueryWrapper<Subject> qw = new QueryWrapper<>();
qw.eq("parent_id","0");
// 获取一级分类的数据集合
ArrayList<Subject> oneList = (ArrayList<Subject>) subjectService.list(qw);
// 遍历所有的一级分类
oneList.forEach(item->{
QueryWrapper<Subject> qw1 = new QueryWrapper<>();
// 二级的 parent_id 就是 一级分类的id
qw1.eq("parent_id",item.getId());
// 通过parent_id查询当前一级分类下的所有二级分类
ArrayList<Subject> twoList = (ArrayList<Subject>) subjectService.list(qw1);
// 将这个二级分类集赋值给一级分类的children属性中
item.setChildren(twoList);
});
// 返回数据
return oneList;
}
}

添加controller

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
package com.szx.edu.controller;


import com.szx.commonutils.Msg;
import com.szx.edu.entity.Subject;
import com.szx.edu.service.SubjectService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;

/**
* <p>
* 课程科目 前端控制器
* </p>
*
* @author szx
* @since 2022-09-28
*/
@Api(tags = "课程分类")
@CrossOrigin
@RestController
@RequestMapping("/eduserver/subject")
public class SubjectController {

@Autowired
SubjectService subjectService;

@ApiOperation("以树形结构的方式查询课程分类")
@GetMapping("getSubject")
public Msg getSubject(){
ArrayList<Subject> subjectArrayList = subjectService.getSubjectTree();
return Msg.Ok().data("rows",subjectArrayList);
}
}

swagger测试

8558467.jpg

发布课程功能

根据表名生成对应代码

  • edu_chapter 课程章节表
  • edu_comment 课程评论表
  • edu_course 课程信息表
  • edu_course_description 课程简介表
  • edu_video 课程视频地址表

Id自动生成的策略修改

修改课程简介的id策略为 IDType=INPUT,表示手动输入

8558467.jpg

新建一个CourseVo

我们在保存课程信息时会同时将课程简介信息保存过来,但是课程简介和课程信息不在一个表,所以,我们要封装一个CourseVo,里面用来映射前端页面传递过来的数据

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
package com.szx.edu.entity.vo;

import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.math.BigDecimal;
import java.util.Date;

/**
* 集成课程基本信息和课程简介信息的一个属性类
* @author songzx
* @create 2022-09-29 17:09
*/
@Data
public class CourseVo {

private static final long serialVersionUID = 1L;

@ApiModelProperty(value = "课程ID")
private String id;

@ApiModelProperty(value = "课程讲师ID")
private String teacherId;

@ApiModelProperty(value = "课程专业ID")
private String subjectId;

@ApiModelProperty(value = "课程专业父级ID")
private String subjectParentId;

@ApiModelProperty(value = "课程标题")
private String title;

@ApiModelProperty(value = "课程销售价格,设置为0则可免费观看")
private BigDecimal price;

@ApiModelProperty(value = "总课时")
private Integer lessonNum;

@ApiModelProperty(value = "课程封面图片路径")
private String cover;


@ApiModelProperty(value = "课程状态 Draft未发布 Normal已发布")
private String status;

@ApiModelProperty(value = "课程简介")
private String description;

}

接口类中添加保存课程的方法

CourseService 接口中增加下面的方法

1
2
3
4
public interface CourseService extends IService<Course> {
// 添加课程信息,并返回保存成功后的id
String saveCourse(CourseVo courseVo);
}

CourseServiceImpl 实现类中实现这个方法

下面用到的一个复制属性值方法

1
2
// 将 courseVo 中的属性值复制到 course 对象中,只会复制对应属性
BeanUtils.copyProperties(courseVo ,course);
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
package com.szx.edu.service.impl;

import com.szx.edu.entity.Course;
import com.szx.edu.entity.CourseDescription;
import com.szx.edu.entity.vo.CourseVo;
import com.szx.edu.mapper.CourseMapper;
import com.szx.edu.service.CourseService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.szx.servicebase.exceptionhandler.GuliException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
* <p>
* 课程 服务实现类
* </p>
*
* @author szx
* @since 2022-09-29
*/
@Service
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> implements CourseService {
// 自动注入课程简介的service
@Autowired
CourseDescriptionServiceImpl courseDescriptionService;


public String saveCourse(CourseVo courseVo) {
Course course = new Course();
// 将courseVo中的属性复制到course实例中
BeanUtils.copyProperties(courseVo ,course);
// 保存课程基本信息
boolean save = this.saveOrUpdate(course);
// 判断是否保存成功
if(!save){
throw new GuliException(500,"添加课程基本信息失败");
}

// 创建一个课程简介类实例
CourseDescription courseDescription = new CourseDescription();
// 设置课程简介的id是上面添加的课程基本信息的id
courseDescription.setId(course.getId());
// 从参数中获取简介内容
courseDescription.setDescription(courseVo.getDescription());
// 保存课程简介
boolean save1 = courseDescriptionService.saveOrUpdate(courseDescription);
if(!save1){
throw new GuliException(500,"添加课程简介信息失败");
}
// 保存成功后返回课程id
return course.getId();
}
}

添加Controller调用

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
package com.szx.edu.controller;


import com.szx.commonutils.Msg;
import com.szx.edu.entity.vo.CourseVo;
import com.szx.edu.service.impl.CourseServiceImpl;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* <p>
* 课程 前端控制器
* </p>
*
* @author szx
* @since 2022-09-29
*/
@Api(tags = "课程模块")
@CrossOrigin
@RestController
@RequestMapping("/eduserver/course")
public class CourseController {
@Autowired
CourseServiceImpl courseService;

@ApiOperation("添加课程基本信息")
@PostMapping("saveCourse")
public Msg saveCourse(@RequestBody CourseVo courseVo){
String courseId = courseService.saveCourse(courseVo);
return Msg.Ok().data("id",courseId);
}
}

swagger测试

发送数据

1
2
3
4
5
6
7
8
9
10
11
12
{
"cover": "string",
"description": "0001string",
"id": "",
"lessonNum": 0,
"price": 0,
"status": "string",
"subjectId": "string",
"subjectParentId": "string",
"teacherId": "string",
"title": "0001string"
}

执行结果

8558467.jpg

查看数据库

8558467.jpg

Vue3使用tinymce富文本编辑器

简介

tinymce 富文本编辑器功能强大,效果如图

8558467.jpg

下载依赖

首先安装 tinymce,这里安装指定版本,高版本会有兼容性问题

1
npm install tinymce@5.10.2

下载主题和汉化包

在 public 文件夹新建 resource 文件夹,在 resource 文件夹下再新建 langs 文件夹和 skins 文件夹

结构如图

8558467.jpg

打开这个连接 https://gitee.com/shuiche/tinymce-vue3/blob/master/langs/zh_CN.js 下载出来 zh_CN.js 文件,放在 langs 文件夹下

然后再 node_modules 中找到 tinymce 文件夹,复制 skins 文件夹下的内容到上面新建的 skins 中

8558467.jpg

新建Utils文件

在 src/utils 文件夹新建两个文件

  • src/utils/tinymce.ts
  • src/utils/onMountedOrActivated.ts

内容分别如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// tinymce.ts

// ==== isNumber 函数====
const toString = Object.prototype.toString

export function is(val?: any, type?: any) {
return toString.call(val) === `[object ${type}]`
}

export function isNumber(val?: any) {
return is(val, 'Number')
}


// ==== buildShortUUID 函数====
export function buildShortUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ==== onMountedOrActivated  hook====
import {nextTick, onMounted, onActivated} from 'vue'

export function onMountedOrActivated(hook?: any) {
let mounted: any
onMounted(() => {
hook()
nextTick(() => {
mounted = true
})
})
onActivated(() => {
if (mounted) {
hook()
}
})
}

封装组件

在 src/components 文件夹下新建一个 tinymce 文件夹,里面新建三个文件:helper.js、tinymce.js、Tinymce.vue,文件内容分别如下

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
// ==========helper.js==========
const validEvents = [
'onActivate',
'onAddUndo',
'onBeforeAddUndo',
'onBeforeExecCommand',
'onBeforeGetContent',
'onBeforeRenderUI',
'onBeforeSetContent',
'onBeforePaste',
'onBlur',
'onChange',
'onClearUndos',
'onClick',
'onContextMenu',
'onCopy',
'onCut',
'onDblclick',
'onDeactivate',
'onDirty',
'onDrag',
'onDragDrop',
'onDragEnd',
'onDragGesture',
'onDragOver',
'onDrop',
'onExecCommand',
'onFocus',
'onFocusIn',
'onFocusOut',
'onGetContent',
'onHide',
'onInit',
'onKeyDown',
'onKeyPress',
'onKeyUp',
'onLoadContent',
'onMouseDown',
'onMouseEnter',
'onMouseLeave',
'onMouseMove',
'onMouseOut',
'onMouseOver',
'onMouseUp',
'onNodeChange',
'onObjectResizeStart',
'onObjectResized',
'onObjectSelected',
'onPaste',
'onPostProcess',
'onPostRender',
'onPreProcess',
'onProgressState',
'onRedo',
'onRemove',
'onReset',
'onSaveContent',
'onSelectionChange',
'onSetAttrib',
'onSetContent',
'onShow',
'onSubmit',
'onUndo',
'onVisualAid'
]

const isValidKey = (key) => validEvents.indexOf(key) !== -1

export const bindHandlers = (initEvent, listeners, editor) => {
Object.keys(listeners)
.filter(isValidKey)
.forEach((key) => {
const handler = listeners[key]
if (typeof handler === 'function') {
if (key === 'onInit') {
handler(initEvent, editor)
} else {
editor.on(key.substring(2), (e) => handler(e, editor))
}
}
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ==========tinymce.js==========
// Any plugins you want to setting has to be imported
// Detail plugins list see https://www.tinymce.com/docs/plugins/
// Custom builds see https://www.tinymce.com/download/custom-builds/
// colorpicker/contextmenu/textcolor plugin is now built in to the core editor, please remove it from your editor configuration

export const plugins = [
'advlist anchor autolink autosave code codesample directionality fullscreen hr insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus template textpattern visualblocks visualchars wordcount'
]

export const toolbar = [
'fontsizeselect lineheight searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample',
'hr bullist numlist link preview anchor pagebreak insertdatetime media forecolor backcolor fullscreen'
]
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293

<template>
<div class="prefixCls" :style="{ width: containerWidth }">
<textarea
:id="tinymceId"
ref="elRef"
:style="{ visibility: 'hidden' }"
></textarea>
</div>
</template>

<script setup>
import tinymce from 'tinymce/tinymce'
import 'tinymce/themes/silver'
import 'tinymce/icons/default/icons'
import 'tinymce/plugins/advlist'
import 'tinymce/plugins/anchor'
import 'tinymce/plugins/autolink'
import 'tinymce/plugins/autosave'
import 'tinymce/plugins/code'
import 'tinymce/plugins/codesample'
import 'tinymce/plugins/directionality'
import 'tinymce/plugins/fullscreen'
import 'tinymce/plugins/hr'
import 'tinymce/plugins/insertdatetime'
import 'tinymce/plugins/link'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/media'
import 'tinymce/plugins/nonbreaking'
import 'tinymce/plugins/noneditable'
import 'tinymce/plugins/pagebreak'
import 'tinymce/plugins/paste'
import 'tinymce/plugins/preview'
import 'tinymce/plugins/print'
import 'tinymce/plugins/save'
import 'tinymce/plugins/searchreplace'
import 'tinymce/plugins/spellchecker'
import 'tinymce/plugins/tabfocus'
import 'tinymce/plugins/template'
import 'tinymce/plugins/textpattern'
import 'tinymce/plugins/visualblocks'
import 'tinymce/plugins/visualchars'
import 'tinymce/plugins/wordcount'
// import 'tinymce/plugins/table';

import {
computed,
nextTick,
ref,
unref,
watch,
onDeactivated,
onBeforeUnmount,
defineProps,
defineEmits,
getCurrentInstance
} from 'vue'
import {toolbar, plugins} from './tinymce'
import {buildShortUUID, isNumber} from '/@/utils/tinymce.ts'
import {bindHandlers} from './helper'
import {onMountedOrActivated} from '/@/utils/onMountedOrActivated'


const props = defineProps({
options: {
type: Object,
default: () => {
}
},
value: {
type: String
},

toolbar: {
type: Array,
default: toolbar
},
plugins: {
type: Array,
default: plugins
},
modelValue: {
type: String
},
height: {
type: [Number, String],
required: false,
default: 400
},
width: {
type: [Number, String],
required: false,
default: 'auto'
},
showImageUpload: {
type: Boolean,
default: true
}
})
const emits = defineEmits(['change', 'update:modelValue', 'inited', 'init-error'])
const {attrs} = getCurrentInstance()
const tinymceId = ref(buildShortUUID('tiny-vue'))
const containerWidth = computed(() => {
const width = props.width
if (isNumber(width)) {
return `${width}px`
}
return width
})
const editorRef = ref(null)
const fullscreen = ref(false)
const elRef = ref(null)
const tinymceContent = computed(() => props.modelValue)

const initOptions = computed(() => {
const {height, options, toolbar, plugins} = props
const publicPath = '/'
return {
selector: `#${unref(tinymceId)}`,
height,
toolbar,
menubar: 'file edit insert view format table',
plugins,
language_url: '/resource/tinymce/langs/zh_CN.js',
language: 'zh_CN',
branding: false,
default_link_target: '_blank',
link_title: false,
object_resizing: false,
auto_focus: true,
skin: 'oxide',
skin_url: '/resource/tinymce/skins/ui/oxide',
content_css: '/resource/tinymce/skins/ui/oxide/content.min.css',
...options,
setup: (editor) => {
editorRef.value = editor
editor.on('init', (e) => initSetup(e))
}
}
})

const disabled = computed(() => {
const {options} = props
const getdDisabled = options && Reflect.get(options, 'readonly')
const editor = unref(editorRef)
if (editor) {
editor.setMode(getdDisabled ? 'readonly' : 'design')
}
return getdDisabled ?? false
})

watch(
() => attrs.disabled,
() => {
const editor = unref(editorRef)
if (!editor) {
return
}
editor.setMode(attrs.disabled ? 'readonly' : 'design')
}
)

onMountedOrActivated(() => {
if (!initOptions.value.inline) {
tinymceId.value = buildShortUUID('tiny-vue')
}
nextTick(() => {
setTimeout(() => {
initEditor()
}, 30)
})
})

onBeforeUnmount(() => {
destory()
})

onDeactivated(() => {
destory()
})

function destory() {
if (tinymce !== null) {
// tinymce?.remove?.(unref(initOptions).selector!);
}
}

function initSetup(e) {
const editor = unref(editorRef)
if (!editor) {
return
}
const value = props.modelValue || ''

editor.setContent(value)
bindModelHandlers(editor)
bindHandlers(e, attrs, unref(editorRef))
}

function initEditor() {
const el = unref(elRef)
if (el) {
el.style.visibility = ''
}
tinymce
.init(unref(initOptions))
.then((editor) => {
emits('inited', editor)
})
.catch((err) => {
emits('init-error', err)
})
}

function setValue(editor, val, prevVal) {
if (
editor &&
typeof val === 'string' &&
val !== prevVal &&
val !== editor.getContent({format: attrs.outputFormat})
) {
editor.setContent(val)
}
}

function bindModelHandlers(editor) {
const modelEvents = attrs.modelEvents ? attrs.modelEvents : null
const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents

watch(
() => props.modelValue,
(val, prevVal) => {
setValue(editor, val, prevVal)
}
)

watch(
() => props.value,
(val, prevVal) => {
setValue(editor, val, prevVal)
},
{
immediate: true
}
)

editor.on(normalizedEvents || 'change keyup undo redo', () => {
const content = editor.getContent({format: attrs.outputFormat})
emits('update:modelValue', content)
emits('change', content)
})

editor.on('FullscreenStateChanged', (e) => {
fullscreen.value = e.state
})
}

function handleImageUploading(name) {
const editor = unref(editorRef)
if (!editor) {
return
}
editor.execCommand('mceInsertContent', false, getUploadingImgName(name))
const content = editor?.getContent() ?? ''
setValue(editor, content)
}

function handleDone(name, url) {
const editor = unref(editorRef)
if (!editor) {
return
}
const content = editor?.getContent() ?? ''
const val = content?.replace(getUploadingImgName(name), `<img src="${url}"/>`) ?? ''
setValue(editor, val)
}

function getUploadingImgName(name) {
return `[uploading:${name}]`
}
</script>

<style lang="scss" scoped>
.prefixCls {
position: relative;
line-height: normal;
}

textarea {
z-index: -1;
visibility: hidden;
}
</style>

这里要注意下 Tinymce.vue 组件中的 tinymce.ts 和 onMountedOrActivated 导入的路径

使用 tinymce

1
2

<tinymce v-model="description" @change="onChagneThiymce" width="100%"/>
1
2
3
4
5
6
7
8
9
import Tinymce from "/@/components/tinymce/Tinymce.vue"

const description = ref("")

// 富文本编辑器改变触发
const onChagneThiymce = (val) => {
console.log(val)
form.value.description = val
}

使用阿里云的视频点播服务

开通视频点播功能

首先打开控制台,找到相关服务进行开通

8558467.jpg

文档地址

https://help.aliyun.com/document_detail/57723.html

安装

在 service 包下新建一个子包 service_vod,然后导入下面的依赖

8558467.jpg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.6.0</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-vod</artifactId>
<version>2.16.5</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-kms</artifactId>
<version>2.10.1</version>
</dependency>

初始化client

1
2
3
4
5
6
7
8
9
10
11
public class InitClientTest {
// 初始化client对象
public static DefaultAcsClient initVodClient() throws ClientException {
String regionId = "cn-shanghai"; // 点播服务接入地域
String accessKeyId = "your oss key";
String accessKeySecret = "your oss keysecret";
DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
DefaultAcsClient client = new DefaultAcsClient(profile);
return client;
}
}

获取视频播放地址

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
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.vod.model.v20170321.GetPlayInfoRequest;
import com.aliyuncs.vod.model.v20170321.GetPlayInfoResponse;

import java.util.List;

/**
* @author songzx
* @create 2022-10-03 11:47
*/
public class GetVoidUrlTest {
/*获取播放地址函数*/
public static GetPlayInfoResponse getPlayInfo(DefaultAcsClient client) throws Exception {
GetPlayInfoRequest request = new GetPlayInfoRequest();
// 视频id
request.setVideoId("20dc391742c34a69a4a708f167df52f4");
return client.getAcsResponse(request);
}

public static void main(String[] args) throws ClientException {
DefaultAcsClient client = InitClientTest.initVodClient();

GetPlayInfoResponse response = new GetPlayInfoResponse();
try {
response = getPlayInfo(client);
List<GetPlayInfoResponse.PlayInfo> playInfoList = response.getPlayInfoList();
//播放地址
for (GetPlayInfoResponse.PlayInfo playInfo : playInfoList) {
System.out.print("播放地址 = " + playInfo.getPlayURL() + "\n");
}
//Base信息
System.out.print("视频名称 = " + response.getVideoBase().getTitle() + "\n");
} catch (Exception e) {
System.out.print("ErrorMessage = " + e.getLocalizedMessage());
}
System.out.print("RequestId = " + response.getRequestId() + "\n");
}
}

运行结果

8558467.jpg

获取视频播放凭证

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
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.vod.model.v20170321.GetVideoPlayAuthRequest;
import com.aliyuncs.vod.model.v20170321.GetVideoPlayAuthResponse;

/**
* @author songzx
* @create 2022-10-03 22:57
*/
public class GetVideoPlayAuthTest {
/*获取播放凭证函数*/
public static GetVideoPlayAuthResponse getVideoPlayAuth(DefaultAcsClient client) throws Exception {
GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();
request.setVideoId("20dc391742c34a69a4a708f167df52f4");
return client.getAcsResponse(request);
}

/*以下为调用示例*/
public static void main(String[] argv) throws ClientException {
DefaultAcsClient client = InitClientTest.initVodClient();
GetVideoPlayAuthResponse response = new GetVideoPlayAuthResponse();
try {
response = getVideoPlayAuth(client);
//播放凭证
System.out.println("播放凭证 = " + response.getPlayAuth());
//VideoMeta信息
System.out.println("视频名称 = " + response.getVideoMeta().getTitle());
} catch (Exception e) {
System.out.println("ErrorMessage = " + e.getLocalizedMessage());
}
System.out.println("RequestId = " + response.getRequestId());
}
}

运行结果

8558467.jpg

上传视频到阿里云

导入上传所需的依赖

首先下载官方提供的jar包和示例代码:下载地址

下载后将 lib 文件夹下的 aliyun-java-vod-upload-1.4.14.jar 添加到自己的项目中

8558467.jpg

然后继续导入如下的依赖

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

<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.1</version>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-vod</artifactId>
<version>2.15.11</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.28</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20170516</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>com.aliyun.vod</groupId>
<artifactId>upload</artifactId>
<version>1.4.14</version>
<scope>system</scope>
<!--这里填写jar包所在的真实地址-->
<systemPath>
D:\mygitee\00-Java全套知识学习\Java实战案例\java_product_guli\guli_parent\service\service_vod\lis\aliyun-java-vod-upload-1.4.14.jar
</systemPath>
</dependency>

上传本地视频到阿里云

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
import com.aliyun.vod.upload.impl.UploadVideoImpl;
import com.aliyun.vod.upload.req.UploadVideoRequest;
import com.aliyun.vod.upload.resp.UploadVideoResponse;

/**
* @author songzx
* @create 2022-10-04 17:25
*/
public class VoidUploadTest {
public static void main(String[] args) {
String accessKeyId = "your oss key";
String accessKeySecret = "your oss keysecret";
String title = "上传的文件名称";
String fileName = "D:\\工具\\EV录屏视频\\20220921_105611.mp4";


UploadVideoRequest request = new UploadVideoRequest(accessKeyId, accessKeySecret, title, fileName);
/* 可指定分片上传时每个分片的大小,默认为2M字节 */
request.setPartSize(2 * 1024 * 1024L);
/* 可指定分片上传时的并发线程数,默认为1,(注:该配置会占用服务器CPU资源,需根据服务器情况指定)*/
request.setTaskNum(1);

UploadVideoImpl uploader = new UploadVideoImpl();
UploadVideoResponse response = uploader.uploadVideo(request);
System.out.print("RequestId=" + response.getRequestId() + "\n"); //请求视频点播服务的请求ID
if (response.isSuccess()) {
System.out.print("VideoId=" + response.getVideoId() + "\n");
} else {
/* 如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因 */
System.out.print("VideoId=" + response.getVideoId() + "\n");
System.out.print("ErrorCode=" + response.getCode() + "\n");
System.out.print("ErrorMessage=" + response.getMessage() + "\n");
}
}
}

添加视频上传接口

添加启动类

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
package com.szx.vod;

import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.core.env.Environment;

import java.net.InetAddress;

/**
* @author songzx
* @create 2022-10-04 18:06
*/
@Log4j2
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@ComponentScan(basePackages = {"com.szx"})
public class VodApplication {
@SneakyThrows
public static void main(String[] args) {
ConfigurableApplicationContext application = SpringApplication.run(VodApplication.class, args);
Environment env = application.getEnvironment();
String host = InetAddress.getLocalHost().getHostAddress();
String port = env.getProperty("server.port");
log.info("\n ----------------------------------------------------------\n\t" +
"Application '{}' 正在运行中... Access URLs:\n\t" +
"Local: \t\thttp://localhost:{}\n\t" +
"External: \thttp://{}:{}\n\t" +
"Doc: \thttp://{}:{}/doc.html\n\t" +
"SwaggerDoc: \thttp://{}:{}/swagger-ui.html\n\t" +
"----------------------------------------------------------",
env.getProperty("spring.application.name"),
env.getProperty("server.port"),
host, port,
host, port,
host, port);
}
}

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
#服务端口
server.port=8003
#服务名
spring.application.name=service_vod
#环境设置:dev、test、prod
spring.profiles.active=dev

#阿里云 OSS
aliyun.oss.file.keyid=your oss key
aliyun.oss.file.keysecret=your oss keysecret

spring.servlet.multipart.max-file-size=1000MB
spring.servlet.multipart.max-request-size=1000MB

utils

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
package com.szx.vod.utils;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
* @author songzx
* @create 2022-10-04 18:15
*/
@Component
public class VodUtils implements InitializingBean {
@Value("${aliyun.oss.file.keyid}")
private String keyid;
@Value("${aliyun.oss.file.keysecret}")
private String keysecret;

public static String KEYID;
public static String KEYSECRET;

@Override
public void afterPropertiesSet() throws Exception {
KEYID = keyid;
KEYSECRET = keysecret;
}
}

service

接口

1
2
3
4
5
6
7
8
9
10
11
package com.szx.vod.service;

import org.springframework.web.multipart.MultipartFile;

/**
* @author songzx
* @create 2022-10-04 18:09
*/
public interface VodService {
String vodUpload(MultipartFile file);
}

实现类

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
package com.szx.vod.service.impl;

import com.aliyun.vod.upload.impl.UploadVideoImpl;
import com.aliyun.vod.upload.req.UploadStreamRequest;
import com.aliyun.vod.upload.resp.UploadStreamResponse;
import com.szx.vod.service.VodService;
import com.szx.vod.utils.VodUtils;
import lombok.SneakyThrows;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;

/**
* @author songzx
* @create 2022-10-04 18:09
*/
@Service
public class VodServiceImpl implements VodService {

@SneakyThrows
@Override
public String vodUpload(MultipartFile file) {
String fileName = file.getOriginalFilename();
String title = fileName.substring(0,fileName.lastIndexOf("."));
InputStream inputStream = file.getInputStream();

UploadStreamRequest request = new UploadStreamRequest(VodUtils.KEYID, VodUtils.KEYSECRET, title, fileName, inputStream);
UploadVideoImpl uploader = new UploadVideoImpl();
UploadStreamResponse response = uploader.uploadStream(request);
System.out.print("RequestId=" + response.getRequestId() + "\n"); //请求视频点播服务的请求ID
if (response.isSuccess()) {
System.out.print("VideoId=" + response.getVideoId() + "\n");
return response.getVideoId();
} else { //如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因
System.out.print("VideoId=" + response.getVideoId() + "\n");
System.out.print("ErrorCode=" + response.getCode() + "\n");
System.out.print("ErrorMessage=" + response.getMessage() + "\n");
return response.getVideoId();
}
}
}

controller

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
package com.szx.vod.controller;

import com.szx.commonutils.Msg;
import com.szx.vod.service.VodService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

/**
* @author songzx
* @create 2022-10-04 18:09
*/
@Api(tags = "视频上传模块")
@CrossOrigin
@RestController
@RequestMapping("/vodservice")
public class VodController {

@Autowired
VodService vodService;

@ApiOperation("视频上传")
@PostMapping("vodUpload")
public Msg vodUpload(MultipartFile file){
String vodId = vodService.vodUpload(file);
return Msg.Ok().data("vodId",vodId);
}
}

swagger测试接口

8558467.jpg

查看视频点播控制台上传成功

8558467.jpg

视频删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Boolean deleteVod(String id) {
DefaultAcsClient client = InitClient.initVodClient();
try {
deleteVideo(client,id);
} catch (Exception e) {
throw new GuliException(500,"视频删除失败");
}
return true;
}

public static DeleteVideoResponse deleteVideo(DefaultAcsClient client, String id) throws Exception {
DeleteVideoRequest request = new DeleteVideoRequest();
//支持传入多个视频ID,多个用逗号分隔
request.setVideoIds(id);
return client.getAcsResponse(request);
}

什么是微服务

微服务的由来

微服务最早由Martin Fowler与James Lewis于2014年共同提出,微服务架构风格是一种使用一套小服务来
开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,
这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实 现,以及不同数据存储技术,并保持最低限度的集中式管理。

为什么需要微服务

在传统的IT行业软件大多都是各种独立系统的堆砌,这些系统的问题总结来说就是扩展性差,可靠性不 高,维护成本高。到后面引入了SOA服务化,但是,由于
SOA 早期均使用了总线模式,这种总线模式是 与某种技术栈强绑定的,比如:J2EE。这导致很多企业的遗留系统很难对接,切换时间太长,成本太
高,新系统稳定性的收敛也需要一些时间

微服务和单体架构的区别

(1)单体架构所有的模块全都耦合在一块,代码量大,维护困难。 微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比较好解决。

(2)单体架构所有的模块都共用一个数据库,存储方式比较单一。 微服务每个模块都可以使用不同的存储方式(比如有的用redis,有的用mysql等),数据库也是单
个模块对应自己的数据库。

(3)单体架构所有的模块开发所使用的技术一样。 微服务每个模块都可以使用不同的开发技术,开发模式更灵活

微服务的本质

(1)微服务,关键其实不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构使得微服务可
以独立的部署、运行、升级,不仅如此,这个系统架构还让微服务与微服务之间在结构上“松耦合”,而在
功能上则表现为一个统一的整体。这种所谓的“统一的整体”表现出来的是统一风格的界面,统一的权限管
理,统一的安全策略,统一的上线过程,统一的日志和审计方法,统一的调度方式,统一的访问入口等 等。

(2)微服务的目的是有效的拆分应用,实现敏捷开发和部署 。

(3)微服务提倡的理念团队间应该是 inter-operate, not integrate 。inter-operate是定义好系统的边界
和接口,在一个团队内全栈,让团队自治,原因就是因为如果团队按照这样的方式组建,将沟通的成本
维持在系统内部,每个子系统就会更加内聚,彼此的依赖耦合能变弱,跨系统的沟通成本也就能降低

什么样的项目适合微服务

微服务可以按照业务功能本身的独立性来划分,如果系统提供的业务是非常底层的,如:操作系统内
核、存储系统、网络系统、数据库系统等等,这类系统都偏底层,功能和功能之间有着紧密的配合关
系,如果强制拆分为较小的服务单元,会让集成工作量急剧上升,并且这种人为的切割无法带来业务上 的真正的隔离,所以无法做到独立部署和运行,也就不适合做成微服务了

微服务开发框架

目前微服务的开发框架,最常用的有以下四个:

Spring Cloud:http://projects.spring.io/spring-cloud(现在非常流行的微服务架构)

Dubbo:http://dubbo.io

Dropwizard:http://www.dropwizard.io (关注单个微服务的开发)

Consul、etcd&etc.(微服务的模块)

什么是SpringClound

Spring Cloud是一系列框架的集合。它利用Spring Boot的开发便利性简化了分布式系统基础设施的开 发,如服务发现、服务注册、配置中心、消息总线、负载均衡、
熔断器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring并没有重复制造轮子,它只是将目前各家公司开发的比较
成熟、经得起实际考验的服务框架组合起来,通过SpringBoot风格进行再封装屏蔽掉了复杂的配置和实
现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包

Spring Cloud和Spring Boot是什么关系

Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务,Spring Cloud是一个基于Spring
Boot实现的开发工具;Spring Boot专注于快速、方便集成的单个微服务个 体,Spring Cloud关注全局的服务治理框架; Spring
Boot使用了默认大于配置的理念,很多集成方案已 经帮你选择好了,能不配置就不配置,Spring Cloud很大的一部分是基于Spring
Boot来实现,必须基 于Spring Boot开发。可以单独使用Spring Boot开发项目,但是Spring Cloud离不开 Spring Boot

Spring Cloud相关基础服务组件

服务发现——Netflix Eureka (Nacos)

服务调用——Netflix Feign

熔断器——Netflix Hystrix

服务网关——Spring Cloud GateWay

分布式配置——Spring Cloud Config (Nacos)

消息总线 —— Spring Cloud Bus (Nacos)

Nacos

基本概念

(1)Nacos 是阿里巴巴推出来的一个新开源项目,是一个更易于构建云原生应用的动态服务发现、配置 管理和服务管理平台。Nacos
致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特 性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。Nacos
帮助您更敏捷和容易 地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原 生范式)
的服务基础设施。

(2)常见的注册中心:

  1. Eureka(原生,2.0遇到性能瓶颈,停止维护)
  2. Zookeeper(支持,专业的独立产品。例如:dubbo)
  3. Consul(原生,GO语言开发)
  4. Nacos 相对于 Spring Cloud Eureka 来说,Nacos 更强大。Nacos = Spring Cloud Eureka + Spring Cloud Config Nacos 可以与
    Spring, Spring Boot, Spring Cloud 集成,并能代替 Spring Cloud Eureka, Spring Cloud Config - 通过 Nacos Server 和
    spring-cloud-starter-alibaba-nacos-discovery 实现服务的注册与发现。

(3)Nacos是以服务为主要服务对象的中间件,Nacos支持所有主流的服务发现、配置和管理。 Nacos主要提供以下四大功能:

  1. 服务发现和服务健康监测
  2. 动态配置服务
  3. 动态DNS服务
  4. 服务及其元数据管理

Nacos下载和安

(1)下载地址和版本

下载地址:https://github.com/alibaba/nacos/releases

下载版本:nacos-server-1.1.4.tar.gz或nacos-server-1.1.4.zip,解压任意目录即可

(2)启动nacos服务

  • Linux/Unix/Mac 启动命令(standalone代表着单机模式运行,非集群模式) 启动命令:sh startup.sh -m standalone
  • windows 启动命令:cmd startup.cmd 或者双击startup.cmd运行文件。 访问:http://localhost:8848/nacos 用户名密码:nacos/nacos

8558467.jpg

服务注册

添加依赖

1
2
3
4
5
<!--服务注册-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

配置文件添加配置

1
2
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

在启动类上添加一个注解

1
@EnableDiscoveryClient

启动已注册的微服务,可以在Nacos服务列表中看到被注册的微服务

8558467.jpg

Feign

概念

  • Feign是Netflix开发的声明式、模板化的HTTP客户端, Feign可以帮助我们更快捷、优雅地调 用HTTP API
  • Feign支持多种注解,例如Feign自带的注解或者JAX-RS注解等。
  • Spring Cloud对Feign进行了增强,使Feign支持了Spring MVC注解,并整合了Ribbon和Eureka,从 而让Feign的使用更加方便。
  • Spring Cloud Feign是基于Netflix feign实现,整合了Spring Cloud Ribbon和Spring Cloud Hystrix,
    除了提供这两者的强大功能外,还提供了一种声明式的Web服务客户端定义的方式。
  • Spring Cloud Feign帮助我们定义和实现依赖服务接口的定义。在Spring Cloud feign的实现下,只需
    要创建一个接口并用注解方式配置它,即可完成服务提供方的接口绑定,简化了在使用Spring Cloud Ribbon时自行封装服务调用客户端的开发量

实现服务调用

需求:我们在删除课程时同时删除视频。这就要求在 edu 服务中调用 vod 服务。

首先我们吧 edu 也在注册中心进行服务注册

8558467.jpg

添加服务调用依赖

1
2
3
4
5

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

在调用端的启动类上添加依赖

1
@EnableFeignClients // 服务调用

创建 client 包和 VodClient 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.szx.edu.client;

import com.szx.commonutils.Msg;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
* @author songzx
* @create 2022-10-07 21:21
*/
@FeignClient("service-vod") // 需要调用的服务名称
@Component
public interface VodClient {

/**
* 需要调用的接口全地址
* @param id
* @return
*/
@DeleteMapping("/vodservice/deleteVod/{id}")
Msg deleteVod(@PathVariable("id") String id);
}

然后再删除课程时调用该接口方法,Spring Clound 会自动的帮我们调用 vod 服务中的删除视频方法

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
@Autowired // 自动注入课程简介的service
CourseDescriptionServiceImpl courseDescriptionService;

@Autowired // 章节信息service
ChapterService chapterService;

@Autowired // 小节信息
VideoService videoService;

@Autowired // 视频的微服务模块
VodClient vodClient;

// 删除课程
public Boolean removeCourse(String id) {
// 删除课程基本信息
boolean b = this.removeById(id);
// 删除简介信息
boolean b1 = courseDescriptionService.removeById(id);
// 删除章节信息
QueryWrapper<Chapter> chapterQueryWrapper = new QueryWrapper<>();
chapterQueryWrapper.eq("course_id",id);
chapterService.remove(chapterQueryWrapper);
// 删除小节信息
QueryWrapper<Video> videoQueryWrapper = new QueryWrapper<>();
videoQueryWrapper.eq("course_id",id);
// 根据课程id查询这个课程下所有的小节信息
List<Video> videoList = videoService.list(videoQueryWrapper);
videoList.forEach(video -> {
// 获取这个小节对应的视频id
String videoSourceId = video.getVideoSourceId();
// 判断视频id是否为空,不为空则执行删除方法
if(StringUtils.isNotEmpty(videoSourceId)){
vodClient.deleteVod(videoSourceId);
}
});
// 视频删除成功后再删除小节信息
videoService.remove(videoQueryWrapper);
return b;
}

hystrix熔断器

添加依赖

1
2
3
4
5
6
7
8
9
10

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<!--hystrix依赖,主要是用 @HystrixCommand -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

在调用端的配置文件中添加如下配置

1
2
3
4
# 开启熔断机制
feign.hystrix.enabled=true
# 设置hystrix超时时间,默认1000ms
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=6000

然后添加熔断器的实现类,就是client接口的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
// value:需要调用的服务名称,fallback:发生熔断时调用实现类中的方法
@FeignClient(value = "service-vod",fallback = VodClientImpl.class)
@Component
public interface VodClient {

/**
* 需要调用的接口全地址
* @param id
* @return
*/
@DeleteMapping("/vodservice/deleteVod/{id}")
Msg deleteVod(@PathVariable("id") String id);
}

服务端渲染技术NUXT

什么是服务端渲染

服务端渲染又称SSR (Server Side Render)是在服务端完成页面的内容,而不是在客户端通过AJAX获取数据。

服务器端渲染(SSR)的优势主要在于:更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的 页面。

如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后 再进行页面内容的抓取。也就是说,如果
SEO 对你的站点至关重要,而你的页面又是异步获取内容,则 你可能需要服务器端渲染(SSR)解决此问题

另外,使用服务器端渲染,我们可以获得更快的内容到达时间(time-to-content),无需等待所有的 JavaScript
都完成下载并执行,产生更好的用户体验,对于那些「内容到达时间(time-to-content)与转化 率直接相关」的应用程序而言,服务器端渲染(
SSR)至关重要。

什么是Nuxt

Nuxt.js 是一个基于 Vue.js 的轻量级应用框架,可用来创建服务端渲染 (SSR) 应用,也可充当静态站点引擎
生成静态站点应用,具有优雅的代码结构分层和热加载等特性。

官网网站:

https://zh.nuxtjs.org/

添加redis缓存

添加依赖

首先添加依赖

1
2
3
4
5
6
7
8
9
10
11
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>

添加配置类

在 service-base 中添加 RedisConfig

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
package com.szx.servicebase.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
* @author songzx
* @create 2022-10-22 22:33
*/
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory
factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}

@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}

Spring Boot缓存注解

缓存@Cacheable

根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不 存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上

缓存@CachePut

使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存 中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上

缓存@CacheEvict

使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上

添加redis缓存使用

首先添加配置类

1
2
3
4
5
6
7
8
9
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0

然后再 serviceImpl 类的方法上添加注解

1
2
3
4
5
6
7
8
9
// 添加redis缓存
@Cacheable(value = "banner",key = "'selectIndexList'")
public List<CrmBanner> getBannerList() {
QueryWrapper<CrmBanner> qw = new QueryWrapper<>();
qw.orderByDesc("gmt_create");
qw.last("limit 2");
List<CrmBanner> crmBannerList = this.list(qw);
return crmBannerList;
}

查看保存到 Redis 的 key

8558467.jpg

单点登录机制

概念

在多个服务中,用户只需要登陆一次,就可以在访问多个服务

单点登录的三种方式

  • session 广播机制

    通过 session 复制的方式,将session复制到其他的服务中,不推荐使用。当服务过多时会造成性能浪费

  • cookie + redis 实现

    1. 在项目中的任何一个模块登录后,把数据放在两个地方
      1. redis:key:生成一个唯一的随机值,value:用户的数据
      2. cookie:吧 redis 里面生成的 key 放在 cookie 中
    2. 访问项目中的其他模块,发送请求带着 cookie ,获取 cookie 的值,拿着 cookie 去 redis 中获取对应的
      value,如果可以查询到数据就认为用户已经登录
  • 通过 token 实现

    1. 在项目的某个模块登录后,按照一定的规则生成字符串,吧用户登录之后的信息包含到这个字符串中,吧字符串返回
      1. 可以通过 cookie 返回
      2. 吧字符串通过地址栏返回
    2. 再去访问其他模块时,每次访问在地址栏带着生成的字符串,根据字符串获取用户信息,如果可以获取则认为用户已经登录

整合JWT

添加依赖

1
2
3
4
5
6
7
8

<dependencies>
<!-- JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
</dependencies>

添加工具类

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
package com.szx.commonutils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
* @author helen
* @since 2019/10/16
*/
public class JwtUtils {

public static final long EXPIRE = 1000 * 60 * 60 * 24;
public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";

public static String getJwtToken(String id, String nickname){

String JwtToken = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
.setSubject("guli-user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.claim("id", id)
.claim("nickname", nickname)
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();

return JwtToken;
}

/**
* 判断token是否存在与有效
* @param jwtToken
* @return
*/
public static boolean checkToken(String jwtToken) {
if(StringUtils.isEmpty(jwtToken)) return false;
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}

/**
* 判断token是否存在与有效
* @param request
* @return
*/
public static boolean checkToken(HttpServletRequest request) {
try {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) return false;
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}

/**
* 根据token获取会员id
* @param request
* @return
*/
public static String getMemberIdByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return (String)claims.get("id");
}
}

整合阿里云短信服务

安装依赖

1
2
3
4
5
6

<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.2.1</version>
</dependency>

添加controller调用

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
package com.szx.oss.controller;

import com.alibaba.nacos.client.naming.utils.RandomUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.szx.commonutils.Msg;
import com.szx.oss.service.impl.MsmServiceImpl;
import com.szx.oss.utils.RandomUtil;
import io.swagger.annotations.Api;
import org.bouncycastle.pqc.math.linearalgebra.RandUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.concurrent.TimeUnit;

/**
* @author songzx
* @create 2022-11-04 14:00
*/
@Api(tags = "短信服务")
@CrossOrigin
@RestController
@RequestMapping("/eduoss/msm")
public class MsmController {

@Autowired
MsmServiceImpl msmService;

@Autowired
RedisTemplate<String,String> redisTemplate;

@GetMapping("send/{phone}")
public Msg send(@PathVariable("phone") String phone){
try {
String code = redisTemplate.opsForValue().get(phone);
if(StringUtils.isNotEmpty(code)){
return Msg.Ok();
}else{
// 随机生成四位验证码
String codes = RandomUtil.getFourBitRandom();
HashMap<String, String> map = new HashMap<>();
map.put("code",codes);
// 5分钟有效期
redisTemplate.opsForValue().set(phone,codes,5, TimeUnit.MINUTES);
// 发送短信
msmService.send(phone,map);
}
} catch (Exception exception) {
exception.printStackTrace();
}
return Msg.Ok();
}
}
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
package com.szx.oss.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.aliyun.oss.ClientException;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.profile.DefaultProfile;
import com.google.gson.Gson;
import com.szx.oss.service.MsmService;
import com.szx.oss.utils.OssPropertyUtils;
import org.springframework.stereotype.Service;

import java.util.HashMap;

/**
* @author songzx
* @create 2022-11-04 14:01
*/
@Service
public class MsmServiceImpl implements MsmService {
public void send(String phone, HashMap<String,String> codeMap) throws Exception {
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", OssPropertyUtils.KEYID, OssPropertyUtils.KEYSECRET);
/** use STS Token
DefaultProfile profile = DefaultProfile.getProfile(
"<your-region-id>", // The region ID
"<your-access-key-id>", // The AccessKey ID of the RAM account
"<your-access-key-secret>", // The AccessKey Secret of the RAM account
"<your-sts-token>"); // STS Token
**/

IAcsClient client = new DefaultAcsClient(profile);


SendSmsRequest request = new SendSmsRequest();
request.setSignName("阿里云短信测试");
request.setTemplateCode("SMS_154950909");
request.setPhoneNumbers(phone);
request.setTemplateParam(JSONObject.toJSONString(codeMap));

try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println(new Gson().toJson(response));
} catch (ServerException e) {
e.printStackTrace();
} catch (ClientException e) {
System.out.println("RequestId:" + e.getRequestId());
}
}
}

登录接口开发

这里对前端传过来的密码使用MD5进行加密,然后和数据库中的密码进行匹配。来判断登录密码是否正确

MD5工具类代码

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
package com.szx.educenter.utils;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;


public final class MD5 {

// 接受字符串,返回加密后的字符串
public static String encrypt(String strSrc) {
try {
char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f' };
byte[] bytes = strSrc.getBytes();
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(bytes);
bytes = md.digest();
int j = bytes.length;
char[] chars = new char[j * 2];
int k = 0;
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
chars[k++] = hexChars[b >>> 4 & 0xf];
chars[k++] = hexChars[b & 0xf];
}
return new String(chars);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException("MD5加密出错!!+" + e);
}
}

public static void main(String[] args) {
System.out.println(MD5.encrypt("111111"));
}

}

登录接口代码

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
// 登录方法
@Override
public String login(UcenterMember ucenterMember) {
if(StringUtils.isEmpty(ucenterMember.getMobile())){
throw new GuliException(500,"请输入手机号");
}
if(StringUtils.isEmpty(ucenterMember.getPassword())){
throw new GuliException(500,"请输入密码");
}

QueryWrapper<UcenterMember> qw = new QueryWrapper<>();
qw.eq("mobile",ucenterMember.getMobile());

UcenterMember member = this.getOne(qw);

if(member == null){
throw new GuliException(500,"账号不存在");
}

// 使用MD5进行密码校验
if(!MD5.encrypt(ucenterMember.getPassword()).equals(member.getPassword())){
throw new GuliException(500,"密码不正确");
}

// 根据id和用户名生成token
return JwtUtils.getJwtToken(member.getId(), member.getNickname());
}

验证登录方法,登录成功后会返回一个token

8558467.jpg

注册接口开发

首先编写一个用于注册的VO类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public class RegisterUcenter {

@ApiModelProperty(value = "手机号")
private String mobile;

@ApiModelProperty(value = "密码")
private String password;

@ApiModelProperty(value = "昵称")
private String nickname;

@ApiModelProperty(value = "验证码")
private String code;
}

然后编写对应的controller

1
2
3
4
5
6
@PostMapping("register")
@ApiModelProperty("登录")
public Msg register(@RequestBody RegisterUcenter registerUcenter){
ucenterMemberService.register(registerUcenter);
return Msg.Ok();
}

编写对应 server 方法

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
// 注册
public void register(RegisterUcenter registerUcenter) {
String nickname = registerUcenter.getNickname();
String password = registerUcenter.getPassword();
String mobile = registerUcenter.getMobile();
String code = registerUcenter.getCode();

// 判断信息是否完整
if(StringUtils.isEmpty(nickname)
|| StringUtils.isEmpty(password)
|| StringUtils.isEmpty(mobile)
|| StringUtils.isEmpty(code))
{
throw new GuliException("请将信息填写完整");
}

// 判断该用户是否已经注册
QueryWrapper<UcenterMember> qw = new QueryWrapper<>();
qw.eq("mobile",mobile);
int count = this.count(qw);
if(count > 0){
throw new GuliException("该用户已存在,请勿重复注册");
}

// 判断验证码是否正确
String redisCode = redisTemplate.opsForValue().get(mobile);
if(!code.equals(redisCode)){
throw new GuliException("请输入正确的验证码");
}

// 执行添加数据库的操作
UcenterMember ucenterMember = new UcenterMember();
ucenterMember.setMobile(mobile);
ucenterMember.setNickname(nickname);
ucenterMember.setPassword(MD5.encrypt(password));
ucenterMember.setAvatar("https://songzx0106.github.io/images/favicon.png");
ucenterMember.setIsDisabled(false);

this.save(ucenterMember);

}

在注册时有用到短信验证码,需要先发送短信,然后短信会保存在 Redis 中,在注册的时候去判断用户输入的验证码和 Redis
中保存的验证码是否一致,如果不一致则表示验证码输入错误或者验证码过期。

在经过一系列的判断后都没有问题,则将信息保存在数据库中即可。

整合微信扫码登录

获取微信key

首先在资源文件中添加微信相关key,这里要用实际公司中的

1
2
3
wx.open.appid=wxed9954c01bb89b47
wx.open.appsecret=a7482517235173ddb4083788de60b90e
wx.open.redirecturl=http://localhost:8160/api/ucenter/wx/callback

添加工具类

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
package com.szx.educenter.utils;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
* @author songzx
* @date 2023/2/26
* @apiNote
*/
@Component
public class WxAppidUtils implements InitializingBean {

@Value("${wx.open.appid}")
private String appId;
@Value("${wx.open.appsecret}")
private String appSecret;
@Value("${wx.open.redirecturl}")
private String redirectUrl;

public static String WX_OPEN_APP_ID;
public static String WX_OPEN_APP_SECRET;
public static String WX_OPEN_REDIRECT_URL;

@Override
public void afterPropertiesSet() throws Exception {
WX_OPEN_APP_ID = appId;
WX_OPEN_APP_SECRET = appSecret;
WX_OPEN_REDIRECT_URL = redirectUrl;
}
}

添加controller

用户请求这个接口后使浏览器重定向到微信提供的地址,直接生成一个登录二维码。

注意这个 controller 不能使用 @RestController 注解,要使用 @Controller 注解

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
package com.szx.educenter.controller;

import com.szx.educenter.utils.WxAppidUtils;
import com.szx.servicebase.exceptionhandler.GuliException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

/**
* @author songzx
* @date 2023/2/26
* @apiNote
*/
@CrossOrigin
@Controller
@RequestMapping("/wx/open")
public class WxLoginController {

@GetMapping("/login")
public String wxLogin(){
// 微信开放平台授权baseUrl
String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
"?appid=%s" +
"&redirect_uri=%s" +
"&response_type=code" +
"&scope=snsapi_login" +
"&state=%s" +
"#wechat_redirect";
// 回调地址
String redirectUrl = WxAppidUtils.WX_OPEN_REDIRECT_URL; //获取业务服务器重定向地址
try {
redirectUrl = URLEncoder.encode(redirectUrl, "UTF-8"); //url编码
} catch (UnsupportedEncodingException e) {
throw new GuliException(20001, e.getMessage());
}

// 使用 String.format 方法将字符串中的 %s 依次替换为实际参数
String qrcodeUrl = String.format(baseUrl, WxAppidUtils.WX_OPEN_APP_ID, redirectUrl, "imhelen");

// 重定向到新地址,直接展示用于登录的二维码
return "redirect:" + qrcodeUrl;
}
}

然后启动服务,访问 /wx/open/login,然后浏览器自动跳转页面,出现如下二维码

8558467.jpg

实现扫码后的回调方法

扫码后微信会回调我们配置好的回调方法,同时携带code和state信息

  1. 我们通过code获取access_token和openid
  2. 得到openid后去数据库查询对应的用户信息
    1. 查不到,根据access_token和openid请求固定的接口获取用户信息并保存到数据库中
    2. 可以查到,将用户信息查询出来
  3. 将查询到的用户id和nickname封装成token,通过路径传参重定向前端页面
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
// 扫码后回调
@GetMapping("callback")
public String callback(String code,String state){

//向认证服务器发送请求换取access_token
String baseAccessTokenUrl =
"https://api.weixin.qq.com/sns/oauth2/access_token" +
"?appid=%s" +
"&secret=%s" +
"&code=%s" +
"&grant_type=authorization_code";

String accessTokenUrl = String.format(baseAccessTokenUrl, WxAppidUtils.WX_OPEN_APP_ID, WxAppidUtils.WX_OPEN_APP_SECRET, code);

String openid = "";
try {
String accessResInfo = HttpClientUtils.get(accessTokenUrl);
Gson gson = new Gson();
HashMap<String,Object> hashMap = gson.fromJson(accessResInfo,HashMap.class);
String accessToken = (String)hashMap.get("access_token");
openid = (String)hashMap.get("openid");

// 根据openid获取用户是否登陆过
UcenterMember byOpenid = memberService.getUserInfoByOpenid(openid);

if(byOpenid == null){
// 根据accessToken和openid获取用户信息
//访问微信的资源服务器,获取用户信息
String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
"?access_token=%s" +
"&openid=%s";

String userInfoUrl = String.format(baseUserInfoUrl, accessToken, openid);

String resultUserInfo = null;
try {
resultUserInfo = HttpClientUtils.get(userInfoUrl);
} catch (Exception e) {
throw new GuliException(20001, "获取用户信息失败");
}
//解析json
HashMap<String, Object> mapUserInfo = gson.fromJson(resultUserInfo,HashMap.class);
String nickname = (String)mapUserInfo.get("nickname");
String headimgurl = (String)mapUserInfo.get("headimgurl");

byOpenid = new UcenterMember();
byOpenid.setOpenid(openid);
byOpenid.setNickname(nickname);
byOpenid.setAvatar(headimgurl);

memberService.save(byOpenid);
}

String jwtToken = JwtUtils.getJwtToken(byOpenid.getId(), byOpenid.getNickname());

return "redirect:http://192.168.3.120:8011/?jwtToken=" + jwtToken;

} catch (Exception e) {
e.printStackTrace();
}
return null;
}

前端根据路径token显示登录信息

1
2
3
4
5
6
7
8
9
// 判断是否是微信登录
let jwtToken = this.$route.query.jwtToken;
if (jwtToken) {
cookie.set("guli_token", jwtToken);
getLoginInfo().then((info) => {
cookie.set("userinfo", JSON.stringify(info.data.info));
this.userInfo = info.data.info;
});
}

效果展示

8558467.jpg

安装Jenkins

基础环境准备

在 Linux 系统中安装好如下内容

Java JDK1.8安装

通过宝塔面板安装 java 项目管理器即可

8558467.jpg

输入 java -version 查看版本

8558467.jpg

Git安装

1
yum -y install git

然后通过 git --version 来检查是否安装成功

8558467.jpg

Maven安装

首先打开官网下载 gz 包

Index of /maven/maven-3/3.6.3/binaries (apache.org)

选择 apache-maven-3.6.1-bin.tar.gz 包下载

8558467.jpg

然后上传到 /usr/local 目录中

在该目录下执行解压操作

1
tar -zxvf apache-maven-3.6.3-bin.tar.gz

修改环境变量

1
vim /etc/profile 

在文件结尾输入下面的代码

1
2
export MAVEN_HOME=/usr/local/apache-maven-3.6.3
export PATH=$PATH:$MAVEN_HOME/bin

然后按下 Esc,输入 :wq 保存并退出,如果输入有误,可以输入 :q! 不保存并退出

通过命令source /etc/profile让profile文件立即生效

1
source /etc/profile

然后输入 mvn -v 查看版本

8558467.jpg

安装Docker

通过宝塔安装

8558467.jpg

输入命令查看 docker 版本

8558467.jpg

Jenkins安装

上传并启动

打开这个地址,下载 war 包,打开下面的地址下载最新的即可

Index of /war (jenkins.io)

8558467.jpg

将下载的 war 包上传到 usr/local/jenkins

然后 cd 到 usr/local/jenkins 输入命令启动 Jenkins

1
nohup java -jar jenkins.war --httpPort=8088

这里注意要提前吧 8088 端口开放

查看进程

1
ps -ef | grep jenkins

8558467.jpg

查看所有进程

1
ps -aux

8558467.jpg

杀死指定 pid 的进程

1
kill -g 8613

查看当前运行的端口

1
netstat -tunlp

8558467.jpg

通过ip加端口号访问

输入 ip:8088 进行访问,出现下面的界面表示启动成功

8558467.jpg

查看并输入密码

我们通过 cat 命令查看上面图片给出的文件内容,即是密码

8558467.jpg

配置国内镜像

输入密码后,点击继续,稍等片刻会出现下面的图

8558467.jpg

这里不要不要急着点击安装,需要先去配置一下国内的镜像,然后再来点击。否则会安装失败

这里我们先关掉浏览器,打开linux系统进行操作

首先关闭的 Jenkins 进程

8558467.jpg

然后进入 Jenkins 目录,并查看文件夹下的文件

1
2
3
cd /root/.jenkins/updates/

ls

8558467.jpg

在该目录下执行如下命令

1
sed -i 's/http:\/\/updates.jenkins-ci.org\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g' default.json && sed -i 's/http:\/\/www.google.com/https:\/\/www.baidu.com/g' default.json

这是直接修改的配置文件,如果前边 Jenkins 用 sudo 启动的话,那么这里的两个 sed 前均需要加上 sudo

8558467.jpg

运行后没有报错就表示执行成功

然后重启 Jenkins,需要再次输入密码

8558467.jpg

上图表示正在安装中,但是都是错误,没关系,我们等待进度条完成后点击继续即可

创建账户

我这里账户名和密码都是 songzx

8558467.jpg

实例配置默认,点击保存并完成

8558467.jpg

进入Jenkins首页

8558467.jpg

环境配置

配置java jdk

点击首页的 Manage Jenkins

8558467.jpg

选择全局工具配置

8558467.jpg

查询 jdk 命令

1
which java

8558467.jpg

配置git

git 配置的是路径,通过下面的命令查询

1
which git

8558467.jpg

8558467.jpg

配置Maven

8558467.jpg

8558467.jpg