# guli-parent
**Repository Path**: strive_chen/guli-parent
## Basic Information
- **Project Name**: guli-parent
- **Description**: 教育项目
- **Primary Language**: Java
- **License**: MulanPSL-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 1
- **Created**: 2020-11-03
- **Last Updated**: 2024-10-22
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# guli-parent
# 介绍
## 一、什么是在线教育
### 1、起源
**萨尔曼·可汗:**孟加拉裔,1976年出生在美国,教育工作者,可汗学院(Khan Academy - https://www.khanacademy.org/)的创始人。

### 2、意义
通过网络,学员与教师即使相隔万里也可以开展教学活动;
1)突破时间和空间的限制,提升学习效率;
2)解决教育资源不平等,使教育资源共享化,降低了学习门槛。
## 二、八种商业模式
### 1、B2C模式
**Business To Customer 会员模式**
商家到用户,这种模式是自己制作大量自有版权的视频,放在自有平台上。 这种模式简单,快速,只要专心录制大量视频即可快速发展。但在中国由于版权保护意识不强,教育内容易于复制,有海量的免费资源的竞争对手众多等原因,难以取得像样的现金流。
**代表网站:**谷粒学院 http://www.gulixueyuan.com/
### 2、C2C模式
**Consumer To Consumer 平台模式**
用户到用户,这种模式本质是将自己的流量或者用户转卖给视频或者直播的内容提供者,通过出售内容分成获利。平台模式避开了非常沉重的内容和服务,扩张迅速。
**代表网站:**51cto http://edu.51cto.com/
### 3、B2B2C
**商家到商家到用户**
平台链接第三方教育机构和用户,平台一般不直接提供课程内容,而是更多承担教育的互联网载体角色,为教学过程各个环节提供全方位支持和服务。
B2B2C是C2C模式的更全面、更完整的表现形式
**代表网站:**腾讯课堂 https://ke.qq.com/
### 4、1 对 1
让一个讲师在一定时间内对一个学员进行辅导,学生按照时间支付费用。这种模式收费容易, 现金流好,产品难度不大,市场空间大,但是人力资源的获取消耗却是巨大的,如果师资上控制不好,比如优秀的讲师留不住,或者整体成本太大,都会导致 1 对 1 模式难以发展。
**代表网站:**VIPKID https://www.vipkid.com.cn/ 一对一北美外教
### 5、直播、互动
这种模式将传统课堂上的反馈,交互,答疑搬到线上。让用户容易接受,只要服务贴心,用 户就愿意买单,因此有丰富现金流。但缺陷是只能通过平台吸引用户,造成了竞争门槛过低, 模式雷同,对手众多,收益的永远是拥有流量或者用户的大平台。
**代表网站:**保利威直播云 [http://live.polyv.net](http://live.polyv.net/#/channel) 教育类企业
### 6、垂直领域
这种模式需要糅合录播,直播,1对1、帮助服务等多种手段,对学生学习某一项内容负责。这种模式收费高,有较强的壁垒。这种产品一旦形成口碑,会有稳定的用户群和收入。
代表网站:猿辅导 https://www.yuanfudao.com/ 国内 K-12 在线教育领域独角兽公司
### 7、O2O 模式
**Online To Offline 线上到线下**
就是通过免费内容或者运营,让线上平台获取用户和流量,将用户吸引到线下业务的开展,或者让学员到加盟的线下机构上课。这种模式形式简单,收益高,只要把控用户需求,吸引到用户,收费不成问题,而且符合传统的消费习惯。
**代表网站\**:\****
启德教育 https://www.eic.org.cn/
### 8、freemium
**免费增值**
Freemium最早在2006年提出, 指的是用免费服务吸引用户,然后通过增值服务,将部分免费用户转化为收费用户,实现变现。Freemium模式中有“二八定律”的因素,即一小部分对价格不敏感的高端用户,愿意为一些额外的功能付费,为服务提供者带来大部分收入。
**代表网站\**:\****学堂在线 http://www.xuetangx.com/
课程免费,如果希望得到课程的认证证书则要缴纳相应的费用
## 三、十个行业分类
### 1、母婴
尽管母婴在线教育市场已经发展多年,但行业总体仍处于赚吆喝不赚钱的状态,主要原因在于国内垂直母婴网站大多存在同质化竞争激烈和盈利模式单一问题,从在线教育的内容来看,大部分母婴网站功能类似大多是基础的母婴知识库问题,咨询交流社区的内容,特色区分并不明显。
妈妈网 https://www.mama.cn/ 类似社交网站

### 2、学前教育
学前教育的市场,整体市场还处于起步阶段,随着针对孩子的培养和教育的重视,未来市场的发展将潜力无限。
宝宝巴士 https://www.babybus.com 核心产品是丰富的幼儿早教APP

### 3、少儿外语
国内在线少儿外语教育领域持续风起云涌,比如新东方VIPKID, ABC,51talk,海绵外语,爱卡微口语,魔方英语等为代表的英语在线教育公司,以新东方,好未来,英孚为代表的传统培训机构
VIPKID https://www.vipkid.com.cn/ 北美外教一对一在线教学

4、中小学生中小学在线教育呈现多样化发展,同时竞争压力在加剧,不断有创业者进入这个领域,或者传统教育机构开始布局,尤其在国内的一线城市,目前这方面影响力广泛的APP有很多,比如学而思网校,学大教育网,作业帮,猿辅导等学而思 https://www.xueersi.com 录播、直播、一对一5、高校学生目前市场主要集中在学历教育方面,国家对于网校的毕业证正在逐步认可,但是需要教学机构相当大的影响力,除了学历教育以外,就是学校自己开发的在线课程平台,专业性课程主要对内部学生开放,其他基础性课程对公开课对外开放。中国大学慕课 http://www.icourse163.org 由高教社联手网易推出,让每一个有提升愿望的用户能够学到中国知名高校的课程,并获得认证。学堂在线 http://www.xuetangx.com/ 由清华大学研发出的中文MOOC,面向全球提供在线课程。
6、留学教育部统计显示,中国目前每年的出国留学生总数在四十万人左右,其中本科及以下层面就读的人数增长迅猛,低龄化趋势明显,在线教育市场向二三线城市蔓延,与留学相关的在线教育培训机构迅速增加。启德考培在线 http://qide.edusoho.cn/7、职业考试职业教育的投融资情况表现稳定,目前仍是在线教育领域内的热门投资板块,中国在线教育市场中职业教育的占比高达30%以上,另外官方也鼓励发展职业教育。中公教育 http://www.offcn.com/ 公务员考试,各种职业资格认证考试,线下培训和网校8、职业技能职业技能的培训,是目前在线教育市场发展迅速的领域,其中一些企业已经形成了一定的品牌,如腾讯课堂,网易云课堂,51CTO等,总体占据了市场的85%使用率,另外现有在线职业教育服务和it培训等,聚焦于垂直领域,专业性虽强。51cto http://edu.51cto.com/ 9、成人外语与少儿外语更注重基础性不同,成人外语主要是培优业务较多,更注重高水平的外语知识,同时小语种的学习人数也在增多。沪江外语:https://www.hujiang.com/10、个人兴趣调查显示,超过30%的用户表示在网上学习不仅是为了“参加考试”和“提升职业技能”,同时还是为了满足个人“兴趣爱好”。与传统教育相比,兴趣教育对用户具有更强的驱动力。社会主流人群可支配收入的增长,促使兴趣培训市场的体量进一步提升。keep:https://www.gotokeep.com/跳跳:http://www.tiaooo.com/index.html
# 软件架构
## 一、功能简介
谷粒学院,是一个B2C模式的职业技能在线教育系统,分为前台用户系统和后台运营平台。

## 二、技术架构
系统开发阶段使用了前后端分离架构,部署阶段使用了容器技术

# 工程结构
- **guli-parent:根目录(父工程),管理子模块:**
- - **common****:公共模块父节点**
- - common-util:工具类模块,所有模块都可以依赖于它
- service-base:service服务的base包,包含service服务的公共配置类,所有service模块依赖于它
- **infrastructure****:基础服务模块父节点**
- - api-gateway:api网关服务
- **service****:api接口服务父节点**
- - service-edu:教学相关api接口服务
- service-oss:阿里云oss api接口服务
- service-cms:cms api接口服务
- service-sms:短信api接口服务
- service-trade:订单和支付相关api接口服务
- service-statistics:统计报表api接口服务
- service-ucenter:会员api接口服务
- service-vod:视频点播api接口服务
# 安装教程
## 一、后端环境搭建
### 01-创建父工程
#### 创建父工程guli-parent
#### 1、创建Spring Boot项目
使用 Spring Initializr 快速初始化一个 Spring Boot 项目
Group:com.atguigu
Artifact:guli-parent
#### 2、删除src目录
#### 3、配置SpringBoot版本
```xml
2.2.1.RELEASE
```
#### 4、配置pom依赖版本号
```xml
1.8
Hoxton.RELEASE
2.2.0.RELEASE
3.3.1
2.0
2.7.0
3.1.0
2.10.1
1.3.1
2.6
3.9
4.5.1
0.7.0
4.3.3
2.15.2
1.4.11
1.2.28
2.8.2
20170516
1.7
2.1.1
3.1.0
```
#### 5、配置pom依赖
```xml
org.springframework.cloud
spring-cloud-dependencies
${cloud.version}
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
${alibaba.version}
pom
import
com.baomidou
mybatis-plus-boot-starter
${mybatis-plus.version}
com.baomidou
mybatis-plus-generator
${mybatis-plus.version}
org.apache.velocity
velocity-engine-core
${velocity.version}
io.springfox
springfox-swagger2
${swagger.version}
io.springfox
springfox-swagger-ui
${swagger.version}
com.aliyun.oss
aliyun-sdk-oss
${aliyun.oss.version}
joda-time
joda-time
${jodatime.version}
commons-fileupload
commons-fileupload
${commons-fileupload.version}
commons-io
commons-io
${commons-io.version}
org.apache.commons
commons-lang3
${commons-lang.version}
org.apache.httpcomponents
httpclient
${httpclient.version}
io.jsonwebtoken
jjwt
${jwt.version}
com.aliyun
aliyun-java-sdk-core
${aliyun-java-sdk-core.version}
com.aliyun
aliyun-java-sdk-vod
${aliyun-java-sdk-vod.version}
com.aliyun
aliyun-sdk-vod-upload
${aliyun-sdk-vod-upload.version}
com.alibaba
fastjson
${fastjson.version}
org.json
json
${json.version}
com.google.code.gson
gson
${gson.version}
commons-dbutils
commons-dbutils
${commons-dbutils.version}
com.alibaba
easyexcel
${alibaba.easyexcel.version}
org.apache.xmlbeans
xmlbeans
${apache.xmlbeans.version}
```
### 02.创建父模块common
#### 1、创建模块
在guli-parent下创建普通maven模块
Group:com.atguigu
Artifact:common
#### 2、删除src目录
#### 3、创建模块common-util
在common下创建普通maven模块
Group:com.atguigu
Artifact:common-util
**注意项目路径:**D:\project\edu\java\guli-parent\common\common-util
#### 4、创建模块service-base
##### 1、创建模块
在common下创建普通maven模块
Group:com.atguigu
Artifact:service-base
**注意项目路径:**D:\project\edu\java\guli-parent\common\service-base
##### 2、配置pom
```xml
com.atguigu
common-util
0.0.1-SNAPSHOT
org.springframework.boot
spring-boot-starter-web
com.baomidou
mybatis-plus-boot-starter
mysql
mysql-connector-java
com.baomidou
mybatis-plus-generator
org.apache.velocity
velocity-engine-core
io.springfox
springfox-swagger2
io.springfox
springfox-swagger-ui
org.projectlombok
lombok
joda-time
joda-time
org.apache.commons
commons-lang3
commons-fileupload
commons-fileupload
commons-io
commons-io
com.alibaba
fastjson
org.json
json
com.google.code.gson
gson
org.apache.httpcomponents
httpclient
```
### 03-创建service
#### 1、创建模块
在guli-parent下创建普通maven模块
Group:com.atguigu
Artifact:service
#### 2、删除src目录
#### 3、配置pom
```xml
com.atguigu
service-base
0.0.1-SNAPSHOT
org.springframework.boot
spring-boot-starter-test
test
```
### 04、创建模块service-edu
在service下创建普通maven模块
Group:com.atguigu
Artifact:service-edu
**注意项目路径:**D:\project\edu\java\guli-parent\service\service-edu
## 二、数据库设计
### 1、数据库
创建数据库:guli_edu
### **2、数据表**
执行sql脚本
### 3、数据库设计规约
**注意:**数据库设计规约并不是数据库设计的严格规范,根据不同团队的不同要求设计
本项目参考《阿里巴巴Java开发手册》:
### 4、MySQL数据库
1、表必备三字段:id, gmt_create, gmt_modified
2、单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。 说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。
3、表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint (1 表示是,0 表示否)。
说明:任何字段如果为非负数,必须是 unsigned。
注意:POJO 类中的任何布尔类型的变量,都不要加 is 前缀。数据库表示是与否的值,使用 tinyint 类型,坚持 is_xxx 的 命名方式是为了明确其取值含义与取值范围。
正例:表达逻辑删除的字段名 is_deleted,1 表示删除,0 表示未删除。
4、如果存储的字符串长度几乎相等,使用 char 定长字符串类型。
5、varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。
6、不得使用外键与级联,一切外键概念必须在应用层解决。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。
## 三、MP代码生成器
参考:https://mp.baomidou.com/guide/generator.html
### 1、创建BaseEntity
在service-base中创建BaseEntity
```java
@Data
@Accessors(chain = true)
public class BaseEntity {
@ApiModelProperty(value = "ID")
@TableId(value = "id", type = IdType.ASSIGN_ID)
private String id;
@ApiModelProperty(value = "创建时间")
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;
@ApiModelProperty(value = "更新时间")
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date gmtModified;
}
```
### 2、创建代码生成器
service-edu的test目录中创建代码生成器
```java
package com.bangdao.service.edu;
public class CodeGenerator {
@Test
public void genCode() {
String prefix = "";//根据实际修改
String moduleName = "edu";
// 1、创建代码生成器
AutoGenerator mpg = new AutoGenerator();
// 2、全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("stive");
gc.setOpen(false); //生成后是否打开资源管理器
// gc.setFileOverride(true); //重新生成时文件是否覆盖
gc.setServiceName("%sService"); //去掉Service接口的首字母I
gc.setIdType(IdType.ASSIGN_ID); //主键策略
gc.setDateType(DateType.ONLY_DATE);//定义生成的实体类中日期类型
gc.setSwagger2(true);//开启Swagger2模式
mpg.setGlobalConfig(gc);
// 3、数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/" + prefix + "guli_" + moduleName + "?serverTimezone=GMT%2B8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("root");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);
// 4、包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName(moduleName); //模块名
pc.setParent("com.bangdao.service");
pc.setController("controller");
pc.setEntity("entity");
pc.setService("service");
pc.setMapper("mapper");
mpg.setPackageInfo(pc);
// 5、策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略
strategy.setTablePrefix(moduleName + "_");//设置表前缀不生成
strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略
strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter链式操作
strategy.setLogicDeleteFieldName("is_deleted");//逻辑删除字段名
strategy.setEntityBooleanColumnRemoveIsPrefix(true);//去掉布尔值的is_前缀
//自动填充
TableFill gmtCreate = new TableFill("gmt_create", FieldFill.INSERT);
TableFill gmtModified = new TableFill("gmt_modified", FieldFill.INSERT_UPDATE);
ArrayList tableFills = new ArrayList<>();
tableFills.add(gmtCreate);
tableFills.add(gmtModified);
strategy.setTableFillList(tableFills);
strategy.setRestControllerStyle(true); //restful api风格控制器
strategy.setControllerMappingHyphenStyle(true); //url中驼峰转连字符
//设置BaseEntity
strategy.setSuperEntityClass("com.bangdao.BaseEntity");
// 填写BaseEntity中的公共字段
strategy.setSuperEntityColumns("id", "gmt_create", "gmt_modified");
mpg.setStrategy(strategy);
// 6、执行
mpg.execute();
}
}
```
### **3、执行代码生成器**
XxxServiceImpl 继承了 ServiceImpl 类,并且MP为我们在ServiceImpl中注入了 baseMapper
### 4、修改entity
Teacher.java 和 Video.java 中引入缺少的 @TableField 的包
让天下没有难学的技术
## 四、启动应用程序
### 1、创建application.yml文件
```yml
server:
port: 8110 # 服务端口
spring:
profiles:
active: dev # 环境设置
application:
name: service-edu # 服务名
datasource: # mysql数据库连接
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/guli_edu?serverTimezone=GMT%2B8
username: root
password: root
#mybatis日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
```
### 2、创建SpringBoot配置文件
在service-base中创建MybatisPlusConfig
```java
package com.bangdao.entity.com.bangdao.config;
@EnableTransactionManagement
@Configuration
@MapperScan("com.bangdao.service.*.mapper")
public class MybatisPlusConfig {
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}
```
### 3、创建SpringBoot启动类
扫描com.atguigu.guli
```java
package com.bangdao.service.edu;
@SpringBootApplication
@ComponentScan({"com.bangdao"})
public class ServiceEduApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceEduApplication.class, args);
}
}
```
### 4、运行启动类
查看控制台8110端口是否成功启动

## 五、讲师列表API
### 1、编写讲师管理接口
修改TeacherController的包名,添加 ".admin"
修改TeacherController的@RequestMapping,添加 "/admin"
```java
package com.bangdao.service.edu.controller.admin;
@RestController
@RequestMapping("/admin/edu/teacher")
public class TeacherController {
@Autowired
private TeacherService teacherService;
@GetMapping("list")
public List listAll(){
return teacherService.list();
}
}
```
### 2、统一返回的json时间格式
默认情况下json时间格式带有时区,并且是世界标准时间,和我们的时间差了八个小时
在application.yml中设置
```yml
#spring:
jackson: #返回json的全局时间格式
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
```
Teacher.java 的 joinDate字段添加数据类型转换,可以覆盖全局配置
```java
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
```
## 
### 3、重启程序
访问:http://localhost:8110/admin/edu/teacher/list 查看结果json数据
## 六、逻辑删除API
### 1、添加删除方法
TeacherController添加removeById方法
```java
@DeleteMapping("remove/{id}")
public boolean removeById(@PathVariable String id){
return teacherService.removeById(id);
}
```
### 2、使用postman测试删除

## 七、Swagger2
### 一、Swagger2介绍
https://swagger.io/
前后端分离开发模式中,api文档是最好的沟通方式。
Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。
及时性 (接口变更后,能够及时准确地通知相关前后端开发人员)规范性 (并且保证接口的规范性,如接口的地址,请求方式,参数及响应格式和错误信息)一致性 (接口信息一致,不会出现因开发人员拿到的文档版本不一致,而出现分歧)可测性 (直接在接口文档上进行测试,以方便理解业务)
### 二、配置Swagger2
#### 1、依赖
service-base
```xml
io.springfox
springfox-swagger2
io.springfox
springfox-swagger-ui
```
#### 2、创建Swagger2配置文件
在service-base中创建Swagger2Config
```java
package com.bangdao.config;
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket webApiConfig(){
return new Docket(DocumentationType.SWAGGER_2)
.groupName("网站-API")
.apiInfo(webApiInfo())
.select()
//只显示api路径下的页面
.paths(Predicates.and(PathSelectors.regex("/api/.*")))
.build();
}
@Bean
public Docket adminApiConfig(){
return new Docket(DocumentationType.SWAGGER_2)
.groupName("后台管理系统")
.apiInfo(adminApiInfo())
.select()
//只显示admin路径下的页面
.paths(Predicates.and(PathSelectors.regex("/admin/.*")))
.build();
}
private ApiInfo webApiInfo(){
return new ApiInfoBuilder()
.title("网站-API文档")
.description("本文档描述了网站微服务接口定义")
.version("1.0")
.contact(new Contact("stive", "https://www.bangdao-tech.com/", "***@126.com"))
.build();
}
private ApiInfo adminApiInfo(){
return new ApiInfoBuilder()
.title("后台管理系统-API文档")
.description("本文档描述了后台管理系统微服务接口定义")
.version("1.0")
.contact(new Contact("stive", "https://www.bangdao-tech.com/", "***@126.com"))
.build();
}
}
```
#### 3、重启服务器查看接口
[http://localhost:8110/swagger-ui.html](http://localhost:8101/swagger-ui.html)
### 三、常见注解
#### 1、API模型
entity的实体类中可以添加一些自定义设置,例如:
定义样例数据
```java
@ApiModelProperty(value = "入驻时间", example = "2010-01-01")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
private Date joinDate;
@ApiModelProperty(value = "创建时间", example = "2019-01-01 8:00:00")
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;
@ApiModelProperty(value = "更新时间", example = "2019-01-01 8:00:00")
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date gmtModified;
```
#### 2、定义接口说明和参数说明
定义在类上:@Api
定义在方法上:@ApiOperation
定义在参数上:@ApiParam
```java
package com.bangdao.service.edu.controller.admin;
@Api(tags = "讲师管理")
@RestController
@RequestMapping("/admin/edu/teacher")
public class TeacherController {
@Autowired
private TeacherService teacherService;
@ApiOperation("所有讲师列表")
@GetMapping("list")
public List list(){
return teacherService.list();
}
@ApiOperation(value = "根据ID删除讲师", notes = "根据ID删除讲师")
@DeleteMapping("remove/{id}")
public boolean removeById(@ApiParam(value = "讲师ID", required = true) @PathVariable String id){
return teacherService.removeById(id);
}
}
```
## 八、统一返回数据格式
项目中我们会将响应封装成json返回,一般我们会将所有接口的数据格式统一, 使前端对数据的操作更一致、轻松。
一般情况下,统一返回数据格式没有固定的格式,只要能描述清楚返回的数据状态以及要返回的具体数据就可以。但是一般会包含状态码、返回消息、数据这几部分内容
例如,我们的系统要求返回的基本数据格式如下:
**列表:**
```json
{
"success": true,
"code": 20000,
"message": "成功",
"data": {
"items": [
{
"id": "1",
"name": "***",
"intro": "毕业于师范大学数学系,热爱教育事业,执教数学思维6年有余"
}
]
}
}
```
**分页:**
```json
{
"success": true,
"code": 20000,
"message": "成功",
"data": {
"total": 17,
"rows": [
{
"id": "1",
"name": "***",
"intro": "毕业于师范大学数学系,热爱教育事业,执教数学思维6年有余"
}
]
}
}
```
**没有返回数据:**
```json
{
"success": true,
"code": 20000,
"message": "成功",
"data": {}
}
```
**失败:**
```json
{
"success": false,
"code": 20001,
"message": "失败",
"data": {}
}
```
因此,我们定义统一结果
```
{
"success": 布尔, //响应是否成功
"code": 数字, //响应码
"message": 字符串, //返回消息
"data": HashMap //返回数据,放在键值对中
}
```
## **九、统一返回结果**
### 1、创建返回码定义枚举类
在service-base中
创建包:com.bangdao.result
创建枚举类: ResultCodeEnum.java
```java
package com.bangdao.result;
import lombok.Getter;
import lombok.ToString;
@Getter
@ToString
public enum ResultCodeEnum {
SUCCESS(true, 20000,"成功"),
UNKNOWN_REASON(false, 20001, "未知错误"),
BAD_SQL_GRAMMAR(false, 21001, "sql语法错误"),
JSON_PARSE_ERROR(false, 21002, "json解析异常"),
PARAM_ERROR(false, 21003, "参数不正确"),
FILE_UPLOAD_ERROR(false, 21004, "文件上传错误"),
FILE_DELETE_ERROR(false, 21005, "文件刪除错误"),
EXCEL_DATA_IMPORT_ERROR(false, 21006, "Excel数据导入错误"),
VIDEO_UPLOAD_ALIYUN_ERROR(false, 22001, "视频上传至阿里云失败"),
VIDEO_UPLOAD_TOMCAT_ERROR(false, 22002, "视频上传至业务服务器失败"),
VIDEO_DELETE_ALIYUN_ERROR(false, 22003, "阿里云视频文件删除失败"),
FETCH_VIDEO_UPLOADAUTH_ERROR(false, 22004, "获取上传地址和凭证失败"),
REFRESH_VIDEO_UPLOADAUTH_ERROR(false, 22005, "刷新上传地址和凭证失败"),
FETCH_PLAYAUTH_ERROR(false, 22006, "获取播放凭证失败"),
URL_ENCODE_ERROR(false, 23001, "URL编码失败"),
ILLEGAL_CALLBACK_REQUEST_ERROR(false, 23002, "非法回调请求"),
FETCH_ACCESSTOKEN_FAILD(false, 23003, "获取accessToken失败"),
FETCH_USERINFO_ERROR(false, 23004, "获取用户信息失败"),
LOGIN_ERROR(false, 23005, "登录失败"),
COMMENT_EMPTY(false, 24006, "评论内容必须填写"),
PAY_RUN(false, 25000, "支付中"),
PAY_UNIFIEDORDER_ERROR(false, 25001, "统一下单错误"),
PAY_ORDERQUERY_ERROR(false, 25002, "查询支付结果错误"),
ORDER_EXIST_ERROR(false, 25003, "课程已购买"),
GATEWAY_ERROR(false, 26000, "服务不能访问"),
CODE_ERROR(false, 28000, "验证码错误"),
LOGIN_PHONE_ERROR(false, 28009, "手机号码不正确"),
LOGIN_MOBILE_ERROR(false, 28001, "账号不正确"),
LOGIN_PASSWORD_ERROR(false, 28008, "密码不正确"),
LOGIN_DISABLED_ERROR(false, 28002, "该用户已被禁用"),
REGISTER_MOBLE_ERROR(false, 28003, "手机号已被注册"),
LOGIN_AUTH(false, 28004, "需要登录"),
LOGIN_ACL(false, 28005, "没有权限"),
SMS_SEND_ERROR(false, 28006, "短信发送失败"),
SMS_SEND_ERROR_BUSINESS_LIMIT_CONTROL(false, 28007, "短信发送过于频繁"),
REMOTE_CALL_ERROR(false, 29001, "远程调用失败");
private Boolean success;
private Integer code;
private String message;
ResultCodeEnum(Boolean success, Integer code, String message) {
this.success = success;
this.code = code;
this.message = message;
}
}
```
### 2、创建结果类
com.bangdao.result 中创建类 R.java
```java
package com.bangdao.result;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
@ApiModel(value = "全局统一返回结果")
public class R {
@ApiModelProperty(value = "是否成功")
private Boolean success;
@ApiModelProperty(value = "返回码")
private Integer code;
@ApiModelProperty(value = "返回消息")
private String message;
@ApiModelProperty(value = "返回数据")
private Map data = new HashMap();
public R(){}
public static R ok(){
R r = new R();
r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess());
r.setCode(ResultCodeEnum.SUCCESS.getCode());
r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
return r;
}
public static R error(){
R r = new R();
r.setSuccess(ResultCodeEnum.UNKNOWN_REASON.getSuccess());
r.setCode(ResultCodeEnum.UNKNOWN_REASON.getCode());
r.setMessage(ResultCodeEnum.UNKNOWN_REASON.getMessage());
return r;
}
public static R setResult(ResultCodeEnum resultCodeEnum){
R r = new R();
r.setSuccess(resultCodeEnum.getSuccess());
r.setCode(resultCodeEnum.getCode());
r.setMessage(resultCodeEnum.getMessage());
return r;
}
public R success(Boolean success){
this.setSuccess(success);
return this;
}
public R message(String message){
this.setMessage(message);
return this;
}
public R code(Integer code){
this.setCode(code);
return this;
}
public R data(String key, Object value){
this.data.put(key, value);
return this;
}
public R data(Map map){
this.setData(map);
return this;
}
}
```
### 3、修改Controller中的返回结果
修改service-edu的TeacherController列表
```java
@ApiOperation("所有讲师列表")
@GetMapping("list")
public R listAll(){
List list = teacherService.list();
return R.ok().data("items", list).message("获取讲师列表成功");
}
```
删除
```
@ApiOperation(value = "根据ID删除讲师", notes = "根据ID删除讲师")
@DeleteMapping("remove/{id}")
public R removeById(@ApiParam(value = "讲师ID", required = true) @PathVariable String id){
boolean result = teacherService.removeById(id);
if(result){
return R.ok().message("删除成功");
}else{
return R.error().message("数据不存在");
}
}
```
### 4、重启测试
## 十、分页
#### 1、分页Controller方法
TeacherController中添加分页方法
```
@ApiOperation("分页讲师列表")
@GetMapping("list/{page}/{limit}")
public R listPage(@ApiParam(value = "当前页码", required = true) @PathVariable Long page,
@ApiParam(value = "每页记录数", required = true) @PathVariable Long limit){
Page pageParam = new Page<>(page, limit);
IPage pageModel = teacherService.page(pageParam);
return R.ok().data("pageModel", pageModel);
}
```
#### **2、Swagger中测试**
### 二、条件查询
#### 1、需求
根据讲师名称name,讲师头衔level、讲师入驻时间查询

#### 2、创建查询对象
```java
package com.bangdao.service.edu.entity.query;
@Data
@ApiModel(value="TeacherQuery对象", description="讲师查询对象")
public class TeacherQuery {
@ApiModelProperty(value = "讲师姓名")
private String name;
@ApiModelProperty(value = "讲师级别")
private Integer level;
@ApiModelProperty(value = "开始时间")
private String joinDateBegin;
@ApiModelProperty(value = "结束时间")
private String joinDateEnd;
}
```
#### 3、service
接口
```java
package com.bangdao.service.edu.service;
public interface TeacherService extends IService {
IPage selectPage(Long page, Long limit, TeacherQuery teacherQuery);
}
```
实现
```java
package com.bangdao.service.edu.service.impl;
@Service
public class TeacherServiceImpl extends ServiceImpl implements TeacherService {
@Override
public IPage selectPage(Long page, Long limit, TeacherQuery teacherQuery) {
Page pageParam = new Page<>(page, limit);
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.orderByAsc("sort");
if (teacherQuery == null){
return baseMapper.selectPage(pageParam, queryWrapper);
}
String name = teacherQuery.getName();
Integer level = teacherQuery.getLevel();
String begin = teacherQuery.getJoinDateBegin();
String end = teacherQuery.getJoinDateEnd();
if (!StringUtils.isEmpty(name)) {
//左%会使索引失效
queryWrapper.likeRight("name", name);
}
if (level != null) {
queryWrapper.eq("level", level);
}
if (!StringUtils.isEmpty(begin)) {
queryWrapper.ge("join_date", begin);
}
if (!StringUtils.isEmpty(end)) {
queryWrapper.le("join_date", end);
}
return baseMapper.selectPage(pageParam, queryWrapper);
}
}
```
#### 4、controller
TeacherController中修改listPage方法:
增加参数TeacherQuery teacherQuery,非必要
```java
teacherService.page 修改成 teacherService.selectPage,并传递teacherQuery参数
```
```java
@ApiOperation("分页讲师列表")
@GetMapping("list/{page}/{limit}")
public R listPage(@ApiParam(value = "当前页码", required = true) @PathVariable Long page,
@ApiParam(value = "每页记录数", required = true) @PathVariable Long limit,
@ApiParam("讲师列表查询对象") TeacherQuery teacherQuery){
IPage pageModel = teacherService.selectPage(page, limit, teacherQuery);
return R.ok().data("pageModel", pageModel);
}
```
#### 5、Swagger中测试
## 十一、自动填充
### 一、填充gmtCreate和gmtModified
service-base中创建自动填充处理类
```java
package com.bangdao.handler;
@Slf4j
@Component
public class CommonMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insertFill .....");
this.setFieldValByName("gmtCreate", new Date(), metaObject);
this.setFieldValByName("gmtModified", new Date(), metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("start updateFill .....");
this.setFieldValByName("gmtModified", new Date(), metaObject);
}
}
```
### 二、定义API
service-edu中新增controller方法
#### 1、新增
```java
@ApiOperation("新增讲师")
@PostMapping("save")
public R save(@ApiParam(value = "讲师对象", required = true) @RequestBody Teacher teacher){
boolean result = teacherService.save(teacher);
if (result) {
return R.ok().message("修改成功");
} else {
return R.error().message("数据不存在");
}
}
```
#### 2、根据id修改
```java
@ApiOperation("更新讲师")
@PutMapping("update")
public R updateById(@ApiParam(value = "讲师对象", required = true) @RequestBody Teacher teacher){
boolean result = teacherService.updateById(teacher);
if(result){
return R.ok().message("修改成功");
}else{
return R.error().message("数据不存在");
}
}
```
#### 3、根据id查询
```java
@ApiOperation("根据id获取讲师信息")
@GetMapping("get/{id}")
public R getById(@ApiParam(value = "讲师ID", required = true) @PathVariable String id){
Teacher teacher = teacherService.getById(id);
if(teacher != null){
return R.ok().data("item", teacher);
}else{
return R.error().message("数据不存在");
}
}
```
#### 4、根据id更新
```java
/**
* 修改讲师
* @param teacher
* @return
*/
@ApiOperation("修改讲师")
@PutMapping("update")
public R updateById(
@ApiParam(value = "讲师对象", required = true) @RequestBody Teacher teacher) {
boolean r = teacherService.updateById(teacher);
if (r) {
return R.ok().message("修改成功");
} else {
return R.error().message("修改失败");
}
}
```
## 十二、异常
### 一、什么是统一异常处理
#### 1、制造异常
Teacher.java中屏蔽下面代码
```java
// @TableField(value = "is_deleted")
private Boolean deleted;
```
#### 2、Swagger中测试
测试列表查询功能,查看结果


#### 3、什么是统一异常处理
我们想让异常结果也显示为统一的返回结果对象,并且统一处理系统的异常信息,那么需要统一异常处理
### 二、统一异常处理
#### 1、创建统一异常处理器
service-base中handler包中,
创建统一异常处理类:GlobalExceptionHandler.java:
```java
package com.bangdao.handler;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public R error(Exception e){
e.printStackTrace();
return R.error();
}
}
```
#### 2、测试
返回统一错误结果

### 三、处理特定异常
#### 1、添加异常处理方法
GlobalExceptionHandler.java中添加
```java
@ExceptionHandler(BadSqlGrammarException.class)
@ResponseBody
public R error(BadSqlGrammarException e){
e.printStackTrace();
return R.setResult(ResultCodeEnum.BAD_SQL_GRAMMAR);
}
```
#### **2、测试**

#### 3、恢复制造的异常
```java
@TableField(value = "is_deleted")
private Boolean deleted;
```
### 四、另一个例子
#### 1、制造异常
在swagger中测试新增讲师方法,输入非法的json参数,得到 HttpMessageNotReadableException
#### 2、添加异常处理方法
GlobalExceptionHandler.java中添加
```
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseBody
public R error(HttpMessageNotReadableException e){
e.printStackTrace();
return R.setResult(ResultCodeEnum.JSON_PARSE_ERROR);
}
```
#### 3、测试

# 十三、日志
#### 1、什么是日志
通过日志查看程序的运行过程,运行信息,异常信息等
#### 2、配置日志级别
日志记录器(Logger)的行为是分等级的。如下表所示:
分为:FATAL、ERROR、WARN、INFO、DEBUG
默认情况下,spring boot从控制台打印出来的日志级别只有INFO及以上级别,可以配置日志级别
```yml
# 设置日志级别
logging:
level:
root: ERROR
```
这种方式能将ERROR级别以及以上级别的日志打印在控制台上
### 二、Logback日志
spring boot内部使用Logback作为日志实现的框架。
#### 1、配置logback日志
删除application.yml中的日志配置
安装idea彩色日志插件:grep console(高版本idea默认自带)
resources 中创建 logback-spring.xml (默认日志文件的名字)
```xml
logback
DEBUG
${CONSOLE_LOG_PATTERN}
${ENCODING}
INFO
ACCEPT
DENY
${log.path}/log_info.log
${FILE_LOG_PATTERN}
${ENCODING}
${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log
100MB
15
WARN
ACCEPT
DENY
${log.path}/log_warn.log
${FILE_LOG_PATTERN}
${ENCODING}
${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log
100MB
15
ERROR
ACCEPT
DENY
${log.path}/log_error.log
${FILE_LOG_PATTERN}
${ENCODING}
${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log
100MB
15
```
#### 2、节点说明
- :定义变量
- :定义日志记录器
- - :定义日志过滤器
- :定义滚动策略
- :定义日志适配的环境
- - :根日志记录器
#### 3、控制日志级别
通过在开发环境设置以下节点的 level 属性的值,调节日志的级别
```xml
```
在controller中输出如下日志,调节日志级别查看日志开启和关闭的效果
添加@Slf4j注解:
```java
@Slf4j
public class TeacherController {
```
打印日志:
```java
public R listAll(){
log.info("所有讲师列表....................");
log.warn("所有讲师列表....................");
```
### 三、错误日志处理
#### 1、用日志记录器记录错误日志
GlobalExceptionHandler.java 中
类上添加注解
```java
@Slf4j
```
修改异常输出语句
```java
//e.printStackTrace();
log.error(e.getMessage());
```
#### 2、输出日志堆栈信息
为了保证日志的堆栈信息能够被输出,修改异常输出语句
```java
import org.apache.commons.lang3.exception.ExceptionUtils;
```
```java
//e.printStackTrace();
//log.error(e.getMessage());
log.error(ExceptionUtils.getStackTrace(e));
```
# 十四、解决跨域
## 一、网关基本概念
### 1、API网关介绍
API 网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:
(1)客户端会多次请求不同的微服务,增加了客户端的复杂性。
(2)存在跨域请求,在一定场景下处理相对复杂。
(3)认证复杂,每个服务都需要独立认证。
(4)难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。
以上这些问题可以借助 API 网关解决。API 网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 API 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 API 网关来做,这样既提高业务灵活性又不缺安全性
### **2、Spring Cloud Gateway**
**Spring cloud gateway**是spring官方基于Spring 5.0和Spring Boot2.0等技术开发的网关,Spring Cloud Gateway旨在为微服务架构提供简单、有效和统一的API路由管理方式,Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且还基于Filer链的方式提供了网关基本的功能,例如:安全、监控/埋点、限流等。
### ***3、Spring Cloud Gateway 核心概念***
下面介绍一下Spring Cloud Gateway中几个重要的概念。
(1)路由。路由是网关最基础的部分,路由信息有一个ID、一个目的URL、一组断言和一组Filter组成。如果断言路由为真,则说明请求的URL和配置匹配
(2)断言。Java8中的断言函数。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自于http request中的任何信息,比如请求头和参数等。
(3)过滤器。一个标准的Spring webFilter。Spring cloud gateway中的filter分为两种类型的Filter,分别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行修改处理。
### 4、执行流程
如下图所示,Spring cloud Gateway发出请求。然后再由Gateway Handler Mapping中找到与请求相匹配的路由,将其发送到Gateway web handler。Handler再通过指定的过滤器链将请求发送到我们实际的服务执行业务逻辑,然后返回。

#### 特点
优点:
- 性能强劲:是第一代网关Zuul的1.6倍
- 功能强大:内置了很多实用的功能,例如转发、监控、限流等
- 设计优雅,容易扩展
缺点:
- 其实现依赖Netty与WebFlux,不是传统的Servlet编程模型,学习成本高
- 不能将其部署在Tomcat、Jetty等Servlet容器里,只能打成jar包执行
- 需要Spring Boot 2.0及以上的版本,才支持
## 二、创建父模块infrastructure
### 1、创建模块
在guli-parent下创建普通maven模块
Artifact:infrastructure
### 2、删除src目录
## 三、创建模块api-gateway
### 1、创建模块
在infrastructure下创建普通maven模块
Artifact:api-gateway
### 2、配置pom
在api-gateway的pom中添加如下依赖
```xml
org.springframework.cloud
spring-cloud-starter-gateway
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
```
### 3、配置application.yml
```yml
server:
port: 8888 # 服务端口
spring:
profiles:
active: dev # 环境设置
application:
name: infrastructure-apigateway # 服务名
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos服务地址
gateway:
discovery:
locator:
enabled: true # gateway可以发现nacos中的微服务,并自动生成转发路由
#spring:
# cloud:
# gateway:
routes:
- id: service-edu
uri: http://localhost:8110
predicates:
- Path=/admin/** #讲师管理管理
```
### 4、logback.xml
修改日志输出目录名为 apigateway
### 5、创建启动类
```java
package com.bangdao;
@SpringBootApplication
@EnableDiscoveryClient
public class InfrastructureApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(InfrastructureApiGatewayApplication.class, args);
}
}
```
### 6、启动网关
启动nacos配置服务
nacos官网:https://nacos.io/zh-cn/docs/quick-start.html
nacos下载地址:https://wwx.lanzoux.com/is0Vei3en2f

启动server-edu,api-gateway服务
### 7、测试自动路由转发
http://localhost:8110/admin/edu/teacher/list

http://localhost:8888/admin/edu/teacher/list

### 8、通过域名访问
#### 1、 统一环境
我们现在访问页面使用的是:http://localhost:8888
有没有什么问题?
实际开发中,会有不同的环境:
- 开发环境:自己的电脑
- 测试环境:提供给测试人员使用的环境
- 预发布环境:数据是和生成环境的数据一致,运行最新的项目代码进去测试
- 生产环境:项目最终发布上线的环境
如果不同环境使用不同的ip去访问,可能会出现一些问题。为了保证所有环境的一致,我们会在各种环境下都使用域名来访问。
我们将使用以下域名:
- 主域名是:guli.com
- 网关域名:api.guli.com
- ...
但是最终,我们希望这些域名指向的还是我们本机的某个端口。
那么,当我们在浏览器输入一个域名时,浏览器是如何找到对应服务的ip和端口的呢?
#### 2、 域名解析
一个域名一定会被解析为一个或多个ip。这一般会包含两步:
- 本地域名解析
浏览器会首先在本机的hosts文件中查找域名映射的IP地址,如果查找到就返回IP ,没找到则进行域名服务器解析,一般本地解析都会失败,因为默认这个文件是空的。
- Windows下的hosts文件地址:C:/Windows/System32/drivers/etc/hosts
- Linux下的hosts文件所在路径: /etc/hosts
样式:
```
# My hosts
127.0.0.1 localhost
```
- 域名服务器(DNS)解析
本地解析失败,才会进行域名服务器解析,域名服务器就是网络中的一台计算机,里面记录了所有注册备案的域名和ip映射关系,一般只要域名是正确的,并且备案通过,一定能找到。
我们不可能去购买一个域名,因此我们可以伪造本地的hosts文件,实现对域名的解析。修改本地的host为:
```
127.0.0.1 api.guli.com
127.0.0.1 www.guli.com guli.com
```
这样就实现了域名的关系映射了。通过域名访问:


#### 3、 nginx反向代理
域名问题解决了,但是现在要访问后台页面,还得自己加上端口:`http://api.guli.com:8888`。
这就不够优雅了。我们希望的是直接域名访问:`http://api.guli.com`。这种情况下端口默认是80,而80端口只有一个,将来我们可能有多个工程需要通过域名访问,如何让多个工程都直接通过域名访问呢?
这里就要用到反向代理工具:Nginx

nginx官网:http://nginx.org/en/download.html
nginx下载:https://wwx.lanzoux.com/iPd8ki3fd7g
windows版

```yml
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
# 代理网关
server {
listen 80;
server_name api.guli.com;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://127.0.0.1:8888;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
```
测试结果

### 9、跨域问题
跨域:浏览器对于javascript的同源策略的限制 。
以下情况都属于跨域:
| 跨域原因说明 | 示例 |
| ------------------ | -------------------------------------- |
| 域名不同 | `www.jd.com` 与 `www.taobao.com` |
| 域名相同,端口不同 | `www.jd.com:8080` 与 `www.jd.com:8081` |
| 二级域名不同 | `item.jd.com` 与 `miaosha.jd.com` |
如果**域名和端口都相同,但是请求路径不同**,不属于跨域,如:
`www.jd.com/item`
`www.jd.com/goods`
http和https也属于跨域
而我们刚才是从`manager.gmall.com`去访问`api.gmall.com`,这属于端口不同,跨域了。
#### 1、为什么有跨域问题?
跨域不一定都会有跨域问题。
因为跨域问题是浏览器对于ajax请求的一种安全限制:**一个页面发起的ajax请求,只能是与当前页域名相同的路径**,这能有效的阻止跨站攻击。
因此:**跨域问题 是针对ajax的一种限制**。
但是这却给我们的开发带来了不便,而且在实际生产环境中,肯定会有很多台服务器之间交互,地址和端口都可能不同,怎么办?
#### 2、 解决跨域问题的方案
目前比较常用的跨域解决方案有3种:
- Jsonp
最早的解决方案,利用script标签可以跨域的原理实现。
限制:
- 需要服务的支持
- 只能发起GET请求
- nginx反向代理
思路是:利用nginx把跨域反向代理为不跨域,支持各种请求方式
缺点:需要在nginx进行额外配置,语义不清晰
- CORS
规范化的跨域请求解决方案,安全可靠。
优势:
- 在服务端进行控制是否允许跨域,可自定义规则
- 支持各种请求方式
缺点:
- 会产生额外的请求
我们这里会采用cors的跨域方案。
#### 3、 什么是cors
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
它允许浏览器向跨源服务器,发出[`XMLHttpRequest`](http://www.ruanyifeng.com/blog/2012/09/xmlhttprequest_level_2.html)请求,从而克服了AJAX只能[同源](http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html)使用的限制。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
- 浏览器端:
目前,所有浏览器都支持该功能(IE10以下不行)。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。
- 服务端:
CORS通信与AJAX没有任何差别,因此你不需要改变以前的业务逻辑。只不过,浏览器会在请求中携带一些头信息,我们需要以此判断是否允许其跨域,然后在响应头中加入一些信息即可。这一般通过过滤器完成即可。
#### 4、 原理有点复杂
> 预检请求
跨域请求会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的`XMLHttpRequest`请求,否则就报错。
一个“预检”请求的样板:
```http
OPTIONS /cors HTTP/1.1
Origin: http://localhost:1000
Access-Control-Request-Method: GET
Access-Control-Request-Headers: X-Custom-Header
User-Agent: Mozilla/5.0...
```
- Origin:会指出当前请求属于哪个域(协议+域名+端口)。服务会根据这个值决定是否允许其跨域。
- Access-Control-Request-Method:接下来会用到的请求方式,比如PUT
- Access-Control-Request-Headers:会额外用到的头信息
> 预检请求的响应
服务的收到预检请求,如果许可跨域,会发出响应:
```http
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://miaosha.jd.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 1728000
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
```
如果服务器允许跨域,需要在返回的响应头中携带下面信息:
- Access-Control-Allow-Origin:可接受的域,是一个具体域名或者*(代表任意域名)
- Access-Control-Allow-Credentials:是否允许携带cookie,默认情况下,cors不会携带cookie,除非这个值是true
- Access-Control-Allow-Methods:允许访问的方式
- Access-Control-Allow-Headers:允许携带的头
- Access-Control-Max-Age:本次许可的有效时长,单位是秒,**过期之前的ajax请求就无需再次进行预检了**
> 有关cookie:
要想操作cookie,需要满足3个条件:
- 服务的响应头中需要携带Access-Control-Allow-Credentials并且为true。
- 浏览器发起ajax需要指定withCredentials 为true
- 响应头中的Access-Control-Allow-Origin一定不能为*,必须是指定的域名
#### 5、 实现非常简单
虽然原理比较复杂,但是前面说过:
- 浏览器端都有浏览器自动完成,我们无需操心
- 服务端可以通过拦截器统一实现,不必每次都去进行跨域判定的编写。
事实上,Spring已经帮我们写好了CORS的跨域过滤器,内部已经实现了刚才所讲的判定逻辑。
```
spring-webmvc:CorsFilter
spring-webflux:CorsWebFilter
```
springcloud-gateway集成的是webflux,所以这里使用的是CorsWebFilter
在gateway中编写一个配置类,并且注册CorsWebFilter:
```java
/**
* 域名过滤器
*/
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter() {
// 初始化CORS配置对象
CorsConfiguration config = new CorsConfiguration();
// 允许的域,不要写*,否则cookie就无法使用了
config.addAllowedOrigin("http://guli.com");
config.addAllowedOrigin("http://www.guli.com");
// 允许的头信息
config.addAllowedHeader("*");
// 允许的请求方式
config.addAllowedMethod("*");
// 是否允许携带Cookie信息
config.setAllowCredentials(true);
// 添加映射路径,我们拦截一切请求
UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/**", config);
return new CorsWebFilter(corsConfigurationSource);
}
}
```
# 十五、创建OSS微服务
## 一、新建云存储微服务
### 1、创建模块
Artifact:service-oss
### 2、配置pom.xml
```xml
com.aliyun.oss
aliyun-sdk-oss
```
### 3、配置application.yml
```yml
server:
port: 8120 # 服务端口
spring:
profiles:
active: dev # 环境设置
application:
name: service-oss # 服务名
aliyun:
oss:
endpoint: 你的endponit
keyId: 你的阿里云keyid
keySecret: 你的阿里云keysecret
#bucket可以在控制台创建,也可以使用java代码创建,注意先测试bucket是否已被占用
bucketName: guli-file
```
### 4、logback-spring.xml
修改日志路径为 guli_log/oss
### 5、创建启动类
创建ServiceOssApplication.java
```java
package com.bangdao;
@SpringBootApplication
//(exclude = DataSourceAutoConfiguration.class)//取消数据源自动配置
@ComponentScan({"com.atguigu.guli"})
public class ServiceOssApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceOssApplication.class, args);
}
}
```
### 6、启动项目
测试项目初始化是否成功,报告如下错误

**原因:**jdbc依赖引入项目后,springboot的自动配置试图在配置文件中查找jdbc相关配置
**解决方案:**主类上添加注解exclude属性,禁用jdbc自动配置
```java
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源自动配置
```
## 二、实现文件上传
### 1、主选:从数据库读取配置文件
创建数据库
```mysql
CREATE TABLE `oss` (
`id` int(32) NOT NULL COMMENT '主键ID',
`endpoint` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'Endpoint(地域节点)',
`access_key_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '账号的 AccessKey ID',
`access_key` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '账号的 AccessKey',
`bucket_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'OSS存储名',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
```
##### 1.1、创建常量读取工具类OssProperties.java
```java
package com.bangdao.entity;
@Data
public class Oss {
@TableId(type = IdType.ASSIGN_ID)
private Integer id;
private String endpoint;
private String accessKeyId;
private String accessKey;
private String bucketName;
}
```
##### 1.2、创建mapper/OssProperties
```java
@Repository
public interface OssMapper extends BaseMapper {
}
```
##### 1.3、FileController
```java
@Api(tags = "阿里云文件管理")
@CrossOrigin //跨域
@RestController
@RequestMapping("/admin/oss/file")
public class FileController {
@Autowired
private FileService fileService;
/**
* 文件上传
*
* @param file
*/
@ApiOperation("文件上传")
@PostMapping("upload")
public R upload(
@ApiParam(value = "文件", required = true)
@RequestParam("file") MultipartFile file,
@ApiParam(value = "模块", required = true)
@RequestParam("module") String module) throws IOException {
InputStream inputStream = file.getInputStream();
String originalFilename = file.getOriginalFilename();
String uploadUrl = fileService.upload(inputStream, module, originalFilename);
//返回r对象
return R.ok().message("文件上传成功").data("url", uploadUrl);
}
}
```
##### 1.4、FileService
```java
public interface FileService {
/**
* 文件上传至阿里云
*/
String upload(InputStream inputStream, String module, String oFilename);
}
```
##### 1.5、FileServiceImpl
```java
@Service
public class FileServiceImpl implements FileService {
/**
* 数据库读取配置信息
*/
@Autowired
private OssPropertiesMapper ossPropertiesMapper;
String endPoint = null;
String keyId = null;
String keySecret = null;
String bucketName = null;
@Override
public String upload(InputStream inputStream, String module, String oFilename) {
List ossList = ossMapper.selectList(null);
for (Oss oss : ossList) {
endPoint = oss.getEndpoint();
keyId = oss.getAccessKeyId();
keySecret = oss.getAccessKey();
bucketName = oss.getBucketName();
}
//判断oss实例是否存在:如果不存在则创建,如果存在则获取
OSS ossClient = new OSSClientBuilder().build(endPoint, keyId, keySecret);
if (!ossClient.doesBucketExist(bucketName)) {
//创建bucket
ossClient.createBucket(bucketName);
//设置oss实例的访问权限:公共读
ossClient.setBucketAcl(bucketName, CannedAccessControlList.PublicRead);
}
//构建日期路径:avatar/2020/10/8/文件名
String folder = new DateTime().toString("yyyy/MM/dd");
//文件名:uuid.扩展名
String fileName = UUID.randomUUID().toString();
String fileExtension = oFilename.substring(oFilename.lastIndexOf("."));
String key = module + "/" + folder + "/" + fileName + fileExtension;
//文件上传至阿里云
ossClient.putObject(bucketName, key, inputStream);
// 关闭OSSClient。
ossClient.shutdown();
//返回url地址
return "https://" + bucketName + "." + endPoint + "/" + key;
}
}
```
1.6、application.yml
```yml
server:
port: 8120 # 服务端口
spring:
profiles:
active: dev # 环境设置
application:
name: service-oss # 服务名
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/guli_edu?serverTimezone=GMT%2B8
username: root
password: root
```
1.7、启动类
```java
@SpringBootApplication
//(exclude = DataSourceAutoConfiguration.class)//取消数据源自动配置
//@ComponentScan({"com.bangdao"})
@MapperScan("com.bangdao.mapper")
public class ServiceOssApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceOssApplication.class, args);
}
}
```
### 2、备选:从配置文件读取常量
创建常量读取工具类:OssProperties.java
```java
package com.bangdao.oss.util;
@Data
@Component
//注意prefix要写到最后一个 "." 符号之前
@ConfigurationProperties(prefix="aliyun.oss")
public class OssProperties {
private String endpoint;
private String keyId;
private String keySecret;
private String bucketName;
}
```
### 2、文件上传业务
创建Service接口:FileService.java
```java
package com.bangdao.service;
public interface FileService {
/**
* 文件上传至阿里云
*/
String upload(InputStream inputStream, String module, String oFilename);
}
```
实现:FileServiceImpl.java
参考SDK中的:Java->上传文件->简单上传->流式上传->上传文件流

```java
package com.bandao.service.impl;
@Service
public class FileServiceImpl implements FileService {
@Autowired
private OssProperties ossProperties;
@Override
public String upload(InputStream inputStream, String module, String oFilename) {
String endpoint = ossProperties.getEndpoint();
String keyid = ossProperties.getKeyId();
String keysecret = ossProperties.getKeySecret();
String bucketname = ossProperties.getBucketName();
//判断oss实例是否存在:如果不存在则创建,如果存在则获取
OSS ossClient = new OSSClientBuilder().build(endpoint, keyid, keysecret);
if (!ossClient.doesBucketExist(bucketname)) {
//创建bucket
ossClient.createBucket(bucketname);
//设置oss实例的访问权限:公共读
ossClient.setBucketAcl(bucketname, CannedAccessControlList.PublicRead);
}
//构建日期路径:avatar/22020/11/8/文件名
String folder = new DateTime().toString("yyyy/MM/dd");
//文件名:uuid.扩展名
String fileName = UUID.randomUUID().toString();
String fileExtension = oFilename.substring(oFilename.lastIndexOf("."));
String key = module + "/" + folder + "/" + fileName + fileExtension;
//文件上传至阿里云
ossClient.putObject(ossProperties.getBucketName(), key, inputStream);
// 关闭OSSClient。
ossClient.shutdown();
//返回url地址
return "https://" + bucketname + "." + endpoint + "/" + key;
}
}
```
### **3、控制层**
创建controller.admin:FileController.java,重启并在Swagger中测试
```java
package com.bangdao.controller.admin;
@Api(tags = "阿里云文件管理")
@CrossOrigin //跨域
@RestController
@RequestMapping("/admin/oss/file")
public class FileController {
@Autowired
private FileService fileService;
/**
* 文件上传
* @param file
*/
@ApiOperation("文件上传")
@PostMapping("upload")
public R upload(
@ApiParam(value = "文件", required = true)
@RequestParam("file") MultipartFile file,
@ApiParam(value = "模块", required = true)
@RequestParam("module") String module) throws IOException {
InputStream inputStream = file.getInputStream();
String originalFilename = file.getOriginalFilename();
String uploadUrl = fileService.upload(inputStream, module, originalFilename);
//返回r对象
return R.ok().message("文件上传成功").data("url", uploadUrl);
}
}
```
# 十六、Nacos注册中心
### 1、常见注册中心
- Eureka:Eureka是Spring Cloud Netflix中的重要组件,主要作用就是做服务注册和发现。2.0遇到性能瓶颈,停止维护,现在已经闭源。
- Consul:Consul是基于GO语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册、服务发现和配置管理的功能。
- Zookeeper:zookeeper是一个分布式服务框架,是Apache Hadoop 的一个子项目。
- Nacos(Spring Cloud Alibaba)
- - Alibaba针对Spring Cloud体系的注册中心
- 相对于 Spring Cloud Eureka 来说,Nacos 更强大
- Nacos = Spring Cloud Eureka + Spring Cloud Config + Spring Cloud Bus
### 2、为什么叫Nacos
- 前四个字母分别为 Naming 和 Configuration 的前两个字母,最后的s为Service
- - Dynamic Naming and Configuration Service
- Nacos就是:注册中心 + 配置中心的组合
- - Spring Cloud Alibaba Nacos = SpringCloudEureka + SpringCloudConfig +SpringCloudBus
### 3、Nacos下载和安装
下载地址:https://github.com/alibaba/nacos/releases
下载版本:nacos-server-1.4.0.zip 或 nacos-server-1.4.0.tar.gz,解压任意目录即可
### 4、启动Nacos
- Windows
启动:双击bin/startup.cmd运行文件
访问:http://localhost:8848/nacos
用户名密码:nacos/nacos
- Linux/Unix/Mac
- 启动命令(standalone代表着单机模式运行,非集群模式)
启动命令:sh startup.sh -m standalone

## 二、服务注册
### 1、引入依赖
service模块中配置Nacos客户端的pom依赖
```xml
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
```
### 2、添加服务配置信息
edu和oss微服务配置application.properties,在客户端微服务中添加注册Nacos服务的配置信息
```yml
#spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos服务地址
```
### 3、添加Nacos客户端注解
edu和oss启动类中添加注解
```java
@EnableDiscoveryClient
```
### 4、启动客户端微服务
启动注册中心,启动已注册的微服务,可以在Nacos服务列表中看到被注册的微服务

## 三、基于OpenFeign的服务调用
### 一、OpenFeign是什么
OpenFeign是Spring Cloud提供的一个声明式的伪Http客户端, 它使得调用远程服务就像调用本地服务一样简单, 只需要创建一个接口并添加一个注解即可。
Nacos很好的兼容了OpenFeign, OpenFeign默认集成了 Ribbon, 所以在Nacos下使用OpenFeign默认就实现了负载均衡的效果。
### 二、OpenFeign的引入
#### 1、引入依赖
service模块中配置OpenFeign的pom依赖(实际是在服务消费者端需要OpenFeign的依赖)
```xml
org.springframework.cloud
spring-cloud-starter-openfeign
```
#### 2、启动类添加注解
在edu的启动类添加如下注解
```java
@EnableFeignClients
```
### 三、OpenFeign的使用
#### 1、oss中创建测试api
服务的生产者的FileController中添加如下方法:
```java
@ApiOperation(value = "测试")
@GetMapping("test")
public R test() {
log.info("oss test被调用");
return R.ok();
}
```
#### 2、edu创建远程调用接口
服务消费者中创建feign包,创建如下接口:
```java
package com.bangdao.service.edu.feign;
@Service
@FeignClient("service-oss")
public interface OssFileService {
@GetMapping("/admin/oss/file/test")
R test();
}
```
#### 3、调用远程方法
服务消费者中的TeacherController中添加如下方法:
```java
@Autowired
private OssFileService ossFileService;
@ApiOperation("测试服务调用")
@GetMapping("test")
public R test(){
ossFileService.test();
return R.ok();
}
```
#### 4、Swagger测试
### 四、负载均衡
#### **1、配置多实例** 
#### **2、测试负载均衡**
对比两个实例输出日志的时间,可以发现默认情况下是轮询策略


#### **3、Ribbon的负载均衡策略**
| 策略名 | 策略描述 |
| ------------------------- | ------------------------------------------------------------ |
| BestAvailableRule | 选择一个最小的并发请求的server |
| AvailabilityFilteringRule | 过滤掉那些因为一直连接失败的被标记为circuit tripped的后端server,并过滤掉那些高并发的的后端server(activeconnections 超过配置的阈值) |
| WeightedResponseTimeRule | 根据响应时间分配一个weight,响应时间越长,weight越小,被选中的可能性越低。 |
| RetryRule | 对选定的负载均衡策略机上重试机制。 |
| RoundRobinRule | 轮询选择server |
| RandomRule | 随机选择一个server |
| ZoneAvoidanceRule | 综合判断server所在区域的性能和server的可用性选择server |
edu中配置负载均衡策略的方式:
```yml
service-oss: # 调用的提供者的名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
```
### 五、OpenFeign的超时控制
#### 1、模拟长流程业务
修改oss服务FileController的test方法,添加sleep 3秒:
```java
@ApiOperation(value = "测试")
@GetMapping("test")
public R test() {
log.info("oss test被调用");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return R.ok();
}
```
#### 2、远程调用测试
上面的程序在测试时会出现远程调用超时错误。如下:因为OpenFeign默认等待1秒钟,否则超时报错 
超时后,服务消费者端默认会发起一次重试 
edu中配置重试规则:每隔一秒发起重试
```yml
ribbon:
MaxAutoRetries: 0 # 同一实例最大重试次数,不包括首次调用,默认0
MaxAutoRetriesNextServer: 1 # 重试其他实例的最大重试次数,不包括首次所选的server,默认1
```
#### **3、解决方案**
edu中配置ribbon的超时时间(因为OpenFeing的底层即是对ribbon的封装)
```
ribbon:
ConnectTimeout: 10000 #连接建立的超时时长,默认1秒
ReadTimeout: 10000 #处理请求的超时时间,默认为1秒
```
### 六、OpenFeign日志
#### 1、作用
OpenFeign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解OpenFeign中Http请求的细节。即对OpenFeign远程接口调用的情况进行监控和日志输出。
#### 2、日志级别
- NONE:默认级别,不显示日志
- BASIC:仅记录请求方法、URL、响应状态及执行时间
- HEADERS:除了BASIC中定义的信息之外,还有请求和响应头信息
- FULL:除了HEADERS中定义的信息之外,还有请求和响应正文及元数据信息
#### 3、配置日志bean
在edu中创建配置文件
```java
package com.bangdao.service.edu.config;;
@Configuration
public class OpenFeignConfig {
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
```
#### 4、开启日志
edu的application.yml中指定监控的接口,以及日志级别
```yml
logging:
level:
com.atguigu.guli.service.edu.feign.OssFileService: debug #以什么级别监控哪个接口
```
# 十七、删除讲师头像
# 需求:
删除讲师的同时删除讲师头像
## 一、删除文件接口
oss微服务中实现删除文件接口
### 1、FileService接口
```yml
void removeFile(String url);
```
### 2、FileService实现
```java
@Override
public void removeFile(String url) {
List ossList = ossMapper.selectList(null);
for (Oss oss : ossList) {
endPoint = oss.getEndpoint();
keyId = oss.getAccessKeyId();
keySecret = oss.getAccessKey();
bucketName = oss.getBucketName();
}
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endPoint, keyId, keySecret);
String host = "https://" + bucketName + "." + endPoint + "/";
String objectName = url.substring(host.length());
// 删除文件。
ossClient.deleteObject(bucketName, objectName);
// 关闭OSSClient。
ossClient.shutdown();
}
```
使用的是yml配置方式阿里云的信息
```java
@Override
public void removeFile(String url) {
String endpoint = ossProperties.getEndpoint();
String keyid = ossProperties.getKeyId();
String keysecret = ossProperties.getKeySecret();
String bucketname = ossProperties.getBucketName();
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, keyid, keysecret);
String host = "https://" + bucketname + "." + endpoint + "/";
String objectName = url.substring(host.length());
// 删除文件。
ossClient.deleteObject(bucketname, objectName);
// 关闭OSSClient。
ossClient.shutdown();
}
```
### 3、FileController
```java
@ApiOperation("文件删除")
@DeleteMapping("remove")
public R removeFile(
@ApiParam(value = "要删除的文件路径", required = true)
@RequestBody String url) {
fileService.removeFile(url);
return R.ok().message("文件刪除成功");
}
```
## 二、OpenFeign远程调用
edu微服务中实现远程调用
### 1、创建远程调用接口
```java
package com.bangdao.service.edu.feign;
@Service
@FeignClient("service-oss")
public interface OssFileService {
.....
@DeleteMapping("/admin/oss/file/remove")
R removeFile(@RequestBody String url);
}
```
### 2、调用远程方法
TeacherService中增加根据讲师id删除图片的方法
接口
```java
boolean removeAvatarById(String id);
```
实现
```java
@Autowired
private OssFileService ossFileService;
@Override
public boolean removeAvatarById(String id) {
Teacher teacher = baseMapper.selectById(id);
if(teacher != null) {
String avatar = teacher.getAvatar();
if(!StringUtils.isEmpty(avatar)){
//删除图片
R r = ossFileService.removeFile(avatar);
return r.getSuccess();
}
}
return false;
}
```
### 3、controller层
修改TeacherController中的removeById方法,删除讲师的同时删除头像
```java
@ApiOperation(value = "根据ID删除讲师", notes = "根据ID删除讲师,逻辑删除")
@DeleteMapping("remove/{id}")
public R removeById(@ApiParam(value = "讲师ID", required = true) @PathVariable String id){
//删除图片
teacherService.removeAvatarById(id);
//删除讲师
boolean result = teacherService.removeById(id);
if(result){
return R.ok().message("删除成功");
}else{
return R.error().message("数据不存在");
}
}
```
### 三、前端修改
前端src/request.js修改超时时间
```
// 创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // api 的 base_url
timeout: 12000 // 请求超时时间
})
```