Day 1
- 项目背景
- 项目采用的商业模式
- 项目实现的功能模块
- 项目使用的技术
- 学习技术 MyBatis-Plus
商业模式
B2C
两个角色:管理员和普通用户
管理员:添加 修改 删除
普通用户:查询
本项目采用此模式。
B2B2C
举例电商平台
京东:商家:消费者
项目功能模块
b2c模式
系统后台:(管理员使用)
讲师管理模块
课程分类管理模块
课程管理模块
视频
统计分析模块
订单管理
banner管理
权限管理
系统前台:(普通用户使用)
首页数据显示
讲师列表和详情
课程列表和课程详情
视频在线播放
注册和登录功能
微信扫描登录
微信扫描支付
项目技术点
采用前后端分离开发
后端技术
- springboot
- springcloud
- mybatis-plus
- spring security
- redis
- maven
- easyExcel
- jwt
- OAuth2
前端技术
- vue
- element-ui
- axios
- nodejs
- …
其他技术
- 阿里云oss
- 阿里云视频点播服务
- 阿里云短信服务
- 微信支付和登录
- docker
- jenkins
- git
mp代码流程
mp入门:对mybatis增强,简化开发
创建数据库,创建表,添加数据
创建springboot工程

引入相关依赖
springboot和mp依赖
<?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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.4</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.atguigu</groupId> <artifactId>mpdemo1010</artifactId> <version>0.0.1-SNAPSHOT</version> <name>mpdemo1010</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.0.5</version> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--lombok用来简化实体类--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.3.5.RELEASE</version> </plugin> </plugins> </build> </project>安装lombok插件(IDEA自带)
application.properties
#mysql数据库连接,serverTimezone=GMT%2B8指北京时间东八区 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8 spring.datasource.username=root spring.datasource.password=root #mybatis日志 mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl #环境设置:dev、test、prod spring.profiles.active=dev编写代码
mp实现添加操作
@Test
void addUser(){
User user = new User();
user.setName("DDDDWWWW112");
user.setAge(123);
user.setEmail("666@123.com");
int insert = userMapper.insert(user);
System.out.println("insert:" + insert);
}
- 不需要设置id值(主键)
- mp自动生成id值(19位)
主键生成策略
自动增长 AUTO INCREMENT

UUID 每次生成唯一的随机值
排序不方便
redis实现
mp自带策略_snowflake雪花算法
@TableId(type = IdType.AUTO) private Long id;
AUTO:自动增长
ID_WORKER / ID_WORKER_STR:mp自带策略,生成19位值,分别对应数字型 / 字符型
INPUT:设置id值
NONE:输入
UUID:随机唯一值
mp实现修改操作
@Test
void updateUser(){
User user = new User();
user.setId(2L);
user.setAge(1000);
int i = userMapper.updateById(user);
System.out.println("update:" + i);
}
mp实现自动填充
数据库表中添加两个字段

Java类中添加实体类属性
不需要set值到对象,使用mp的方式实现添加
在实体类里对自动填充属性添加注解
//注意数据表create_time,update_time与下面命名特点 @TableField(fill = FieldFill.INSERT) private Date createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private Date updateTime;创建类,实现接口 MetaObjectHandler,并实现接口里的方法
package com.atguigu.mpdemo1010.handler; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.util.Date; @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.setFieldValByName("createTime",new Date(),metaObject); this.setFieldValByName("updateTime",new Date(),metaObject); this.setFieldValByName("version",1,metaObject); this.setFieldValByName("deleted",0,metaObject); } @Override public void updateFill(MetaObject metaObject) { this.setFieldValByName("updateTime",new Date(),metaObject); } }
乐观锁
数据库表中添加字段versioin,作为乐观锁版本号
Java对应实体类添加版本号属性version,并添加注解
@Version@Version //版本号注解 @TableField(fill = FieldFill.INSERT) private Integer version;在Config配置类中,配置乐观锁插件
//乐观锁插件 @Bean public OptimisticLockerInterceptor optimisticLockerInterceptor() { return new OptimisticLockerInterceptor(); }测试
/** * 测试 乐观锁插件 */ @Test public void testOptimisticLocker() { //查询 User user = userMapper.selectById(1373611971844370433L); //修改数据 user.setName("Helen Yao"); user.setEmail("helen@qq.com"); //执行更新 userMapper.updateById(user); }
mp实现简单查询
根据id查询
User user = userMapper.selectById(1373611971844370433L);多个id批量查询
//多id批量查询 @Test void testSelectDemo(){ List<User> users = userMapper.selectBatchIds(Arrays.asList(1L,2L,3L)); System.out.println(users); }
mp实现分页查询
在Config配置类中,配置分页插件
//分页插件 @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); }编写分页代码
//BaseMapper接口的selectPage方法 IPage<T> selectPage(IPage<T> var1, @Param("ew") Wrapper<T> var2);//创建Page对象,传入参数 //参数1:当前页,参数2:每页显示的记录数 Page<User> page = new Page<>(1,3); //调用mp的分页查询方法selectPage,把所有数据封装到page对象中 userMapper.selectPage(page,null); //通过page对象获取分页数据 System.out.println(page.getCurrent());
mp实现逻辑删除
物理删除
//BaseMapper接口的deleteById方法 int deleteById(Serializable var1);批量删除
//BaseMapper接口的deleteBatchIds方法 int deleteBatchIds(@Param("coll") Collection<? extends Serializable> var1);逻辑删除
数据库表中添加逻辑删除字段deleted

Java对应实体类中添加属性和注解
@TableLogic //逻辑删除注解 @TableField(fill = FieldFill.INSERT) private Integer deleted;配置逻辑删除插件
//逻辑删除插件 @Bean public ISqlInjector sqlInjector(){ return new LogicSqlInjector(); }执行代码,deleted值最终变为1
mp实现复杂条件查询
使用QueryWrapper构建条件
创建QueryWrapper对象,调用其方法实现各种条件查询
- ge >=
- gt >
- le <=
- lt <
- eq =
- ne !=
- like 模糊查询
- orderByDesc / orderByAsc 排序
- last 语句最后拼接sql语句
- between 范围
- select 指定列
Day 2
前后端分离开发概念
讲师管理模块(后端)
讲师CRUD操作
前后端分离开发

项目准备工作
创建数据库,创建讲师数据表 edu_teacher
CREATE TABLE `edu_teacher` ( `id` char(19) NOT NULL COMMENT '讲师ID', `name` varchar(20) NOT NULL COMMENT '讲师姓名', `intro` varchar(500) NOT NULL DEFAULT '' COMMENT '讲师简介', `career` varchar(500) DEFAULT NULL COMMENT '讲师资历,一句话说明讲师', `level` int(10) unsigned NOT NULL COMMENT '头衔 1高级讲师 2首席讲师', `avatar` varchar(255) DEFAULT NULL COMMENT '讲师头像', `sort` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '排序', `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除', `gmt_create` datetime NOT NULL COMMENT '创建时间', `gmt_modified` datetime NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='讲师';创建项目结构

代码生成器
创建application.properties配置文件
编写controller service mapper代码内容
mp提供代码生成器,生成相关代码
//CodeGenerator.java
//根据实际需要自行更改配置
package com.example.demo;
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(projectPath + "/src/main/java");
gc.setAuthor("testjava");
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");
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("root");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);
// 4、包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName("edu"); //模块名
pc.setParent("com.example.demo");
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();
}
}
讲师列表接口
创建controller
@Api(description="讲师管理") @RestController @RequestMapping("/eduservice/teacher") @CrossOrigin public class EduTeacherController { //访问地址:http://localhost:8001/eduservice/teacher/findAll //把service注入 @Autowired private EduTeacherService teacherService; //1 查询讲师表中所有数据 //rest风格 @ApiOperation(value = "所有讲师列表") @GetMapping("findAll") public R findAllTeacher(){ //调用service方法实现查询操作 List<EduTeacher> list = teacherService.list(null); return R.ok().data("items",list); } }创建启动类
@SpringBootApplication @ComponentScan(basePackages = {"com.atguigu"}) public class EduApplication { public static void main(String[] args) { SpringApplication.run(EduApplication.class,args); } }创建配置类,配置mapper扫描等等
@Configuration @MapperScan("com.atguigu.eduservice.mapper") public class EduConfig { ... }测试,使用端口8001:
讲师逻辑删除接口
在配置类EduConfig中,配置逻辑删除插件
//逻辑删除插件 @Bean public ISqlInjector sqlInjector() { return new LogicSqlInjector(); }在实体类EduTeacher中,对应逻辑删除属性上添加注解
@TableLogic@ApiModelProperty(value = "逻辑删除 1(true)已删除, 0(false)未删除") @TableLogic private Boolean isDeleted;编写controller内的方法
//2 逻辑删除讲师 //@PathVariable:得到浏览器路径中的值id(必要) @ApiOperation(value = "根据ID删除讲师") @DeleteMapping("{id}") public R removeTeacher( @ApiParam(name = "id", value = "讲师ID", required = true) @PathVariable String id){ boolean flag = teacherService.removeById(id); if (flag){ return R.ok(); }else { return R.error(); } }@DeleteMapping("{id}"):id值需要通过路径传递@PathVariable String id:获取路径中的id值例:
localhost:8001/edu/delete/114514如何测试?
delete提交不同于get\post提交
借助一些工具:
swagger测试(重点)
postman(了解)
swagger整合
优点:
- 生成在线接口文档
- 方便接口测试
创建公共模块,整合swagger,以便所有模块都能使用
父工程guli_parent -> 子模块common -> 子模块service_base,创建配置类SwaggerConfig
@Configuration //配置类 @EnableSwagger2 //swagger注解 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(); } private ApiInfo webApiInfo(){ return new ApiInfoBuilder() .title("网站-课程中心API文档") .description("本文档描述了课程中心微服务接口定义") .version("1.0") .contact(new Contact("Helen", "http://atguigu.com", "55317332@qq.com")) .build(); } }在service_edu中,引入service_base依赖
<dependency> <groupId>com.atguigu</groupId> <artifactId>service_base</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>在service_edu启动类添加注解,设置包扫描规则
@SpringBootApplication @ComponentScan(basePackages = {"com.atguigu"}) public class EduApplication { public static void main(String[] args) { SpringApplication.run(EduApplication.class,args); } }访问swagger
统一结果返回
json数据格式有2种:
对象 数组
这两种格式一般混合使用
{
"success": boolean, //响应是否成功
"code": num, //响应码
"message": string, //返回消息
"data": HashMap //返回数据,存放在键值对中
}
在common模块,创建子模块 common_utils
创建interface,定义数据返回状态码
public interface ResultCode { public static Integer SUCCESS = 20000;//成功 public static Integer ERROR = 20001;//失败 }定义数据返回格式
//统一返回结果类 @Data public class R { @ApiModelProperty(value = "是否成功") private Boolean success; @ApiModelProperty(value = "返回码") private Integer code; @ApiModelProperty(value = "返回消息") private String message; @ApiModelProperty(value = "返回数据") private Map<String, Object> data = new HashMap<String, Object>(); }使用统一结果
在service中,引入 common_utils
<dependency> <groupId>com.atguigu</groupId> <artifactId>common_utils</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>设置接口返回结果都是
Rpublic R findAllTeacher(){ //调用service方法实现查询操作 List<EduTeacher> list = teacherService.list(null); return R.ok().data("items",list); }
讲师分页查询和条件查询
讲师分页功能
在配置类EduConfig中,配置mp分页插件
//分页插件 @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); }编写讲师分页查询接口方法
//3 分页查询讲师方法 //current 当前页 //limit 每页记录数 @GetMapping("pageTeacher/{current}/{limit}") public R pageListTeacher(@PathVariable long current, @PathVariable long limit){ //创建page对象 Page<EduTeacher> pageTeacher = new Page<>(current,limit); //调用方法实现分页 //调用方法时,底层封装,把分页所有数据封装到pageTeacher对象里面 teacherService.page(pageTeacher,null); long total = pageTeacher.getTotal();//总记录数 List<EduTeacher> records = pageTeacher.getRecords();//每页数据List集合 // Map map = new HashMap<>(); // map.put("total",total); // map.put("rows",records); // return R.ok().data(map); //利用map存放返回数据,二选一均可 return R.ok().data("total",total).data("rows",records); }
多条件组合查询带分页
实现效果:

把条件值传递到接口里面
条件值封装到对象,对象传递到接口
根据条件值进行判断,拼接条件
//4 条件查询带分页的方法 @PostMapping("pageTeacherCondition/{current}/{limit}") public R pageTeacherCondition(@PathVariable long current, @PathVariable long limit, @RequestBody(required = false) TeacherQuery teacherQuery){ ... }RequestBody:
需要使用post提交方式
使用json传递数据,把json数据封装到对应对象里
@RequestBody(required = false),即参数值可以为空ResponseBody:
返回json数据
讲师添加功能
自动填充配置
@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); } }@ApiModelProperty(value = "创建时间") @TableField(fill = FieldFill.INSERT) private Date gmtCreate; @ApiModelProperty(value = "更新时间") @TableField(fill = FieldFill.INSERT_UPDATE) private Date gmtModified;编写controller
//5 添加讲师接口方法 @PostMapping("addTeacher") public R addTeacher(@RequestBody EduTeacher eduTeacher){ boolean save = teacherService.save(eduTeacher); if (save){ return R.ok(); }else { return R.error(); } }
讲师修改功能
根据讲师id进行查询
//6 根据讲师id进行查询 @GetMapping("getTeacher/{id}") public R getTeacher(@PathVariable String id){ EduTeacher eduTeacher = teacherService.getById(id); return R.ok().data("teacher",eduTeacher); }讲师修改
//7 讲师修改 @PostMapping("updateTeacher") public R updateTeacher(@RequestBody EduTeacher eduTeacher){ boolean flag = teacherService.updateById(eduTeacher); if (flag){ return R.ok(); }else { return R.error(); } }
Day 3
- 统一异常处理
- 全局异常处理
- 特定异常处理
- 自定义异常处理
- 统一日志处理
- Logback
- 前端知识
- es6语法
- vue指令
统一异常处理
全局异常处理
@ControllerAdvice //全局异常处理注解 @Slf4j public class GlobalExceptionHandler { @ResponseBody @ExceptionHandler(Exception.class) public R error(Exception e){ e.printStackTrace(); return R.error().message("执行了全局异常处理"); } }特定异常处理
//特定异常 @ResponseBody @ExceptionHandler(ArithmeticException.class) public R error(ArithmeticException e){ e.printStackTrace(); return R.error().message("执行了ArithmeticException异常处理"); }自定义异常处理
创建自定义异常类,继承RuntimeException
编写异常属性
@Data @AllArgsConstructor //生成有参构造器 @NoArgsConstructor //生成无参构造器 public class GuliException extends RuntimeException{ private Integer code;//状态码 private String msg;//异常信息 }在统一异常类添加规则
//自定义异常 @ResponseBody @ExceptionHandler(GuliException.class) public R error(GuliException e){ log.error(e.getMessage()); e.printStackTrace(); return R.error().code(e.getCode()).message(e.getMsg()); }执行自定义异常
try{ int a = 10 / 0; }catch(Exception e){ throw new GuliException(20001,"执行了自定义异常"); }
统一日志处理
日志级别
ERROR WARN INFO DEBUG
#设置日志级别 logging.level.root=WARN把日志输出到控制台和文件中,使用日志工具
log4j
logback日志工具
删除application.properties中的日志配置
#mybatis日志 #mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl #设置日志级别 #logging.level.root=DEBUG在resources目录,创建logback-spring.xml
如果程序运行出现异常,则把异常信息输出到文件中
log.error(e.getMessage());
es6介绍
es6,即ECMAScript 6.0
- es6与es5
- es6代码简洁,es5代码复杂
- es6浏览器兼容性差,es5浏览器兼容性好
- es6是一套标准规范,JavaScript遵守了这套规范
vue入门
vue是一个构建页面前端的框架
使用vscode快捷生成html页面
引入vue的js文件,类似于JQuery
<script src="vue.min.js"></script>在html页面创建div标签,并添加id属性
<div id="app"></div>编写vue代码,是一套固定的结构
<script> //创建一个vue对象 new Vue({ el: '#app', //绑定vue的作用范围 data: { //定义页面中显示的模型数据 message: 'Hello Vue!' } }) </script>使用插值表达式
{{xxxx}},获取data里面定义的值<div id="app"> {{message}} </div>
Day 4
- axios,在vue中发送ajax请求(重点)
- element-ui
- nodejs
- npm(重点)
- babel
- 模块化(重点)
- webpack
- 搭建项目前端环境(重点)
axios
axios是独立的项目,不是vue里的一部分,只是axios经常与vue一起使用,实现ajax操作
使用axios应用场景

axios使用
创建html页面,引入js文件,包含两个js文件:vue和axios
<script src="vue.min.js"></script> <script src="axios.min.js"></script>编写axios代码
创建json文件,创建数据
{ "success":true, "code":20000, "message":成功, "data":{ "items":[ {"name":"lucy","age":20}, {"name":"mary","age":30}, {"name":"jack","age":40} ] } }使用axios发送ajax请求,请求文件得到数据,在页面显示

nodejs
nodejs是什么
类比学过的 java,运行 java 需要 jdk 环境
正在学的 nodejs,是 JavaScript 的运行环境,用于执行 JavaScript 代码
不需要浏览器,直接使用 nodejs 运行 JavaScript 代码
模拟服务器效果,比如tomcat
安装nodejs
使用nodejs执行 JavaScript 代码
node xxx.js在vscode中打开终端窗口(cmd窗口),执行js代码

npm
npm是什么
包管理工具
- 在后端开发中使用过maven,maven构建项目,管理jar依赖,并联网下载依赖
- npm类似于maven,用于前端,管理前端js依赖,并联网下载依赖
安装npm
- 在安装nodejs时,npm也会一并安装
- 使用
npm -v,查看是否安装成功
演示npm具体操作
npm项目初始化操作
使用命令
npm init -y项目初始化之后,生成文件 package.json ,类似于后端pom.xml文件
npm下载指定js依赖,使用命令
npm install xxxx(依赖名称)根据package.json下载依赖,使用命令
npm install
babel
babel是什么
babel是转码器,把es6代码转换成es5代码
因为写的代码是es6代码,es6代码的浏览器兼容性很差,改为使用es5浏览器兼容性好
babel的使用
安装babel工具
使用命令
npm install --global babel-cli创建js文件,编写es6代码
创建babel配置文件
.babelrc{ "presets":["es2015"], "plugins":[] }安装es2015转码器
npm install --save-dev babel-preset-es2015使用命令进行转码(以下es6 es5均是文件夹名称)
根据文件转码:
babel es6/xxx.js -o es5/xxx.js根据文件夹转码:
babel es6 -d es5
模块化
是什么
- 开发后端接口的时候,开发controller service mapper,controller注入service,service注入mapper
- 在后端中,类与类之间的调用成为后端的模块化操作
- 在前端中,js与js之间的调用成为前端的模块化操作
es5实现模块化操作
在01.js定义js方法
//1 创建js方法 const sum = function(a,b){ return parseInt(a) + parseInt(b) } const subtract = function(a,b){ return parseInt(a) - parseInt(b) } //2 设置哪些方法可以被其他js调用 module.exports = { sum, subtract }在02.js调用01.js里的方法
//调用01.js里面的方法 //1 引入01.js文件 const m = require("./01.js") //2 调用 console.log(m,sum(1,2)) consule.log(m,subtract(10,3))测试
node 02.jses6模块化代码写法一
注意:如果使用es6写法实现模块化操作,在nodejs环境中不能直接运行,需要使用babel把es6代码转换为es5代码,才可以在nodejs运行
01.js定义方法
//定义方法,设置哪些方法可以被其他js调用 export function getList(){ console.log("getList...") } export function save(){ console.log("save...") }02.js调用01.js的方法
//调用01.js的方法,引入01.js文件,进行调用 import {getList,save} from './01.js' //调用方法 getList() save()es6模块化代码写法二
01.js
//定义方法,设置哪些方法可以被其他js调用 export default { getList(){ console.log('getList...') }, update(){ console.log('update...') } }02.js
//调用01.js的方法,引入01.js文件,进行调用 import m from './01.js' //调用方法 m.getList() m.update()
webpack
是什么
打包工具,可以把多个静态资源文件打包成一个文件

webpack使用
安装webpack工具
npm install -g webpack webpack-cl查看是否安装成功
webpack -v创建js文件用于打包操作
创建三个js文件:
在common.js和utils.js定义方法
exports.info = function(str){ document.write(str); }exports.add = function(a,b){ return a + b; }在main.js引入common和utils
const common = require('./common'); const utils = require('./utils'); common.info('Hello world!' + utils.add(100, 200));创建webpack配置文件,配置打包信息
webpack.config.js
const path = require("path"); //Node.js内置模块 module.exports = { entry: './src/main.js', //配置入口文件 output: { path: path.resolve(__dirname, './dist'), //输出路径,__dirname:当前文件所在路径 filename: 'bundle.js' //输出文件 } }使用命令执行打包操作
webpack #有警告webpack --mode=development #没有警告最终测试
创建html文件,引入打包之后的js文件,使用浏览器查看效果
<script src="dist/bundle.js"></script>
打包css
创建css文件,写样式内容
body { background-color: red; }在main.js中,引入css文件
//css文件引入 require('./style.css')安装css加载工具
npm install --save-dev style-loader css-loader修改webpack配置文件
const path = require("path"); //Node.js内置模块 module.exports = { //... output:{}, module: { rules: [ { test: /\.css$/, //打包规则应用到以css结尾的文件上 use: ['style-loader', 'css-loader'] } ] } }
搭建前端页面环境

选取一个模板(框架)进行环境搭建
vue-admin-template
找到模板源文件,并解压至工作区
通过vscode终端,下载依赖
//通过配置文件下载依赖 npm install启动模板项目
npm run dev
前端页面环境说明
前端框架入口:
index.html
main.js
前端页面环境使用的框架,主要基于两种技术:
vue-admin-template = vue + element-ui
build目录
存放项目构建的脚本文件
config目录
index.js
修改
useEslint: false为truedev.env.js
修改
BASE_API: '"http://localhost:8001"',即后端接口地址src目录

Day 5
讲师管理前端开发
- 讲师列表(分页条件查询)
- 讲师添加
- 讲师删除功能
- 讲师修改功能
先把后端管理系统登录改造到本地
后面登录功能添加权限框架 spring security
改造登录到本地接口
把登录请求地址改造到本地,修改配置文件
config目录的dev.env.js
BASE_API: '"http://localhost:9001"'登录操作调用两个方法:login登录操作,info登录后获取用户信息
创建接口两个方法实现登录
- login 返回token值
- info 返回roles name avatar
开发接口
@RestController @RequestMapping("/eduservice/user") @CrossOrigin //解决跨域问题 public class EduLoginController { //login @PostMapping("login") public R login(){ return R.ok().data("token","admin"); } //info @GetMapping("info") public R info(){ return R.ok().data("roles","[admin]").data("name","admin").data("avatar","https://cdn.jsdelivr.net/gh/Rayucan/imageCloud/data/20210202224329.png"); } }修改api目录login.js,修改本地接口路径
import request from '@/utils/request' export function login(username, password) { return request({ url: '/eduservice/user/login', method: 'post', data: { username, password } }) } export function getInfo(token) { return request({ url: '/eduservice/user/info', method: 'get', params: { token } }) }最终测试出现问题
Access-Control-Allow-Origin,即跨域问题跨域问题
通过一个地址去访问另外一个地址,此过程中如果三个地方任何一个不同,便会出现
- 访问协议 http https
- ip地址 192.168.1.1 172.11.11.11
- 端口号 9528 8001
解决跨域问题
在后端controller接口添加注解(常用)
@CrossOrigin使用网关解决(后面用到)
前端框架开发过程介绍
添加路由
src/router/index.js 进行配置
{ path: '/', component: Layout, redirect: '/dashboard', name: 'Dashboard', hidden: true, children: [{ path: 'dashboard', //路由对应页面 component: () => import('@/views/dashboard/index') }] },实现效果:点击某个路由,显示路由对应页面
在views目录创建vue页面
api目录创建js文件,定义接口地址和参数
import request from '@/utils/request' export function getList(params) { return request({ url: '/table/list', method: 'get', params }) }在vue页面引入js文件,调用方法实现功能
引入
import user from '...'使用element-ui显示数据内容
data:{ }, created() { }, methods:{ }
讲师列表前端开发
添加路由
{ path: '/teacher', component: Layout, redirect: '/teacher/table', name: '讲师管理', meta: { title: '讲师管理', icon: 'example' }, children: [ { path: 'table', name: '讲师列表', component: () => import('@/views/edu/teacher/list'), meta: { title: '讲师列表', icon: 'table' } }, { path: 'save', name: '添加讲师', component: () => import('@/views/edu/teacher/save'), meta: { title: '添加讲师', icon: 'tree' } }, { path: 'edit/:id', name: 'EduTeacherEdit', component: () => import('@/views/edu/teacher/save'), meta: { title: '编辑讲师', noCache: true }, hidden: true } ] },创建对应页面

在api目录创建teacher.js定义访问的接口地址
import request from '@/utils/request' export default { //1 讲师列表(条件查询分页) //current当前页 limit每页记录数 teacherQuery条件对象 getTeacherListPage(current,limit,teacherQuery) { return request({ //url: '/eduservice/teacher/pageTeacherCondition/'+current+"/"+limit, url: `/eduservice/teacher/pageTeacherCondition/${current}/${limit}`, method: 'post', //teacherQuery条件对象,后端使用RequestBody获取数据 //data表示把对象转换json进行传递到接口里面 data: teacherQuery }) }, //删除讲师 deleteTeacherId(id) { return request({ url: `/eduservice/teacher/${id}`, method: 'delete' }) }, //添加讲师 addTeacher(teacher) { return request({ url: `/eduservice/teacher/addTeacher`, method: 'post', data: teacher }) }, //根据id查询讲师 getTeacherInfo(id) { return request({ url: `/eduservice/teacher/getTeacher/${id}`, method: 'get' }) }, //修改讲师 updateTeacherInfo(teacher) { return request({ url: `/eduservice/teacher/updateTeacher`, method: 'post', data: teacher }) } }在讲师列表页面list.vue,调用定义的接口方法,得到接口返回数据
data(){ return{ } }, created(){ }, methods:{ }把请求接口获取的数据,在页面进行显示
查阅element-ui官方文档,copy表格代码
<template> <el-table :data="tableData" style="width: 100%"> <el-table-column prop="date" label="日期" width="180"> </el-table-column> <el-table-column prop="name" label="姓名" width="180"> </el-table-column> <el-table-column prop="address" label="地址"> </el-table-column> </el-table> </template> <script> export default { data() { return { tableData: [{ date: '2016-05-02', name: '王小虎', address: '上海市普陀区金沙江路 1518 弄' }, { date: '2016-05-04', name: '王小虎', address: '上海市普陀区金沙江路 1517 弄' }, { date: '2016-05-01', name: '王小虎', address: '上海市普陀区金沙江路 1519 弄' }, { date: '2016-05-03', name: '王小虎', address: '上海市普陀区金沙江路 1516 弄' }] } } } </script>讲师列表添加分页实现
<!-- 分页 --> <el-pagination :current-page="page" :page-size="limit" :total="total" style="padding: 30px 0; text-align: center;" layout="total, prev, pager, next, jumper" @current-change="getList" />分页方法修改,添加页码参数
methods:{ //讲师列表的方法 getList(page=1) { this.page = page } }添加条件查询
使用element-ui组件实现
在列表上面添加条件输入表单,使用v-model数据绑定
<el-form-item> <el-input v-model="teacherQuery.name" placeholder="讲师名"/> </el-form-item><el-button type="primary" icon="el-icon-search" @click="getList()">查询</el-button>清空功能
- 先清空表单输入的条件
- 再查询所有数据
resetData() {//清空的方法 //表单输入项数据清空 this.teacherQuery = {} //查询所有讲师数据 this.getList() }
讲师删除功能前端实现
在每条记录后面添加删除按钮,按钮绑定事件,并在绑定事件的方法传递删除讲师的id值
<el-button type="danger" size="mini" icon="el-icon-delete" @click="removeDataById(scope.row.id)">删除</el-button>在api目录teacher.js定义删除接口的地址
//删除讲师 deleteTeacherId(id) { return request({ url: `/eduservice/teacher/${id}`, method: 'delete' }) },vue页面调用,实现删除
//删除讲师的方法 removeDataById(id) { this.$confirm('此操作将永久删除讲师记录, 是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { //点击确定,执行then方法 //调用删除的方法 teacher.deleteTeacherId(id) .then(response =>{//删除成功 //提示信息 this.$message({ type: 'success', message: '删除成功!' }); //回到列表页面 this.getList() }) }) //点击取消,执行catch方法 }
讲师添加功能前端实现
点击”添加讲师”按钮,进入表单页面,输入讲师信息
在表单页面,点击”保存”,提交接口,添加至数据库
api定义接口地址
//添加讲师 addTeacher(teacher) { return request({ url: `/eduservice/teacher/addTeacher`, method: 'post', data: teacher }) }在vue页面实现调用
//添加讲师的方法 saveTeacher() { teacherApi.addTeacher(this.teacher) .then(response => {//添加成功 //提示信息 this.$message({ type: 'success', message: '添加成功!' }); //回到列表页面 路由跳转 this.$router.push({path:'/teacher/table'}) }) }
讲师修改功能
每条记录后面添加”修改”按钮
点击”修改”按钮,进入表单页面,进行数据回显
根据讲师id查询所有讲师,实现数据回显
通过路由跳转,进入数据回显页面,在路由index页面添加路由
{ path: 'edit/:id', name: 'EduTeacherEdit', component: () => import('@/views/edu/teacher/save'), meta: { title: '编辑讲师', noCache: true }, hidden: true }<router-link :to="'/teacher/edit/'+scope.row.id"> <el-button type="primary" size="mini" icon="el-icon-edit">修改</el-button> </router-link>在表单页面实现数据回显
teacher.js中定义根据id查询接口
//根据id查询讲师 getTeacherInfo(id) { return request({ url: `/eduservice/teacher/getTeacher/${id}`, method: 'get' }) }vue页面中调用接口,实现数据回显
//根据讲师id查询的方法 getInfo(id) { teacherApi.getTeacherInfo(id) .then(response => { this.teacher = response.data.teacher }) }调用根据id查询方法
由于”添加”和”修改”均使用save.vue页面,如何区别添加还是修改?
只有修改的时候才进行数据回显。
即:判断路径里是否有讲师id,如果有id为修改,否则为添加
created() { //页面渲染之前执行 this.init() }methods:{ init() { //判断路径有id值,做修改 if(this.$route.params && this.$route.params.id) { //从路径获取id值 const id = this.$route.params.id //调用根据id查询的方法 this.getInfo(id) } else { //路径没有id值,做添加 //清空表单 this.teacher = {} } } }
最终修改实现
在api的teacher.js定义修改接口
//修改讲师 updateTeacherInfo(teacher) { return request({ url: `/eduservice/teacher/updateTeacher`, method: 'post', data: teacher }) }在vue页面调用修改方法
saveOrUpdate() { //判断修改还是添加 //根据teacher是否有id if(!this.teacher.id) { //添加 this.saveTeacher() } else { //修改 this.updateTeacher() } }//修改讲师的方法 updateTeacher() { teacherApi.updateTeacherInfo(this.teacher) .then(response => { //提示信息 this.$message({ type: 'success', message: '修改成功!' }); //回到列表页面 路由跳转 this.$router.push({path:'/teacher/table'}) })
路由切换问题
第一次:点击”修改”,数据回显
第二次:再次点击”添加讲师”,进入表单页面,出现问题
问题:回显数据仍然存在,没有进行表单数据清空
原因:
多次路由跳转到同一页面,页面中的created()方法只执行一次,后续跳转不会执行
解决方式:
添加讲师时,清空表单数据
init() {
//判断路径有id值,做修改
if(this.$route.params && this.$route.params.id) {
//从路径获取id值
const id = this.$route.params.id
//调用根据id查询的方法
this.getInfo(id)
} else { //路径没有id值,做添加
//清空表单
this.teacher = {}
}
}
最终解决:
使用vue监听
watch: { //监听
$route(to, from) { //路由变化方式,路由发生变化,方法就会执行
this.init()
}
}
Day 6
- 添加讲师实现头像上传功能
- 阿里云oss存储服务
- Nginx使用
- 添加课程分类功能
- 使用EasyExcel,读取excel内容,添加数据
- 课程分类列表
- 树形结构显示
阿里云oss
开发准备
- 在阿里云官网,找到对象存储OSS服务,开通
- 进入阿里云oss管理控制台,创建阿里云oss许可证(AccessKey)
阿里云官方参考文档:https://help.aliyun.com/document_detail/32008.html
搭建项目环境
在service创建子模块service_oss
service_oss中引入oss相关依赖
<dependencies> <!-- 阿里云oss依赖 --> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> </dependency> <!-- 日期工具栏依赖 --> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> </dependency> </dependencies>创建配置文件application.properties
#服务端口 server.port=8002 #服务名 spring.application.name=service-oss #环境设置:dev、test、prod spring.profiles.active=dev #阿里云 OSS #不同的服务器,地址不同 aliyun.oss.file.endpoint= aliyun.oss.file.keyid= aliyun.oss.file.keysecret= #bucket可以在控制台创建,也可以使用java代码创建 aliyun.oss.file.bucketname=创建启动类,报错

原因:
启动时,SpringBoot会去寻找数据库配置,然而现在模块并不需要操作数据库,也没有配置数据库
解决方式:
添加数据库配置
在启动类添加注解属性,不加载数据库配置(使用这种方式)
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
上传文件接口实现
创建常量类,读取配置文件内容
@Component public class ConstantPropertiesUtils 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; //定义公开静态常量 public static String END_POINT; public static String ACCESS_KEY_ID; public static String ACCESS_KEY_SECRET; public static String BUCKET_NAME; @Override public void afterPropertiesSet() throws Exception { END_POINT = endpoint; ACCESS_KEY_ID = keyId; ACCESS_KEY_SECRET = keySecret; BUCKET_NAME = bucketName; } }创建controller service
@RestController @RequestMapping("/eduoss/fileoss") @CrossOrigin public class OssController { @Autowired private OssService ossService; //上传头像方法 @PostMapping public R uploadOssFile(MultipartFile file){ //获取上传文件 String url = ossService.uploadFileAvatar(file); return R.ok().data("url",url); } }@Service @CrossOrigin public class OssServiceImpl implements OssService { //上传头像到oss @Override public String uploadFileAvatar(MultipartFile file) { //通过工具类获取值 String endpoint = ConstantPropertiesUtils.END_POINT ; String accessKeyId = ConstantPropertiesUtils.ACCESS_KEY_ID; String accessKeySecret = ConstantPropertiesUtils.ACCESS_KEY_SECRET; String bucketName = ConstantPropertiesUtils.BUCKET_NAME; try { // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); // 获取上传文件输入流 InputStream inputStream = file.getInputStream(); // 获取文件名称 String fileName = file.getOriginalFilename(); // 1 在文件名称中加入随机值(去掉-),防止重名 String uuid = UUID.randomUUID().toString().replaceAll("-",""); fileName = uuid + fileName; // 2 文件按照日期进行分类 // 获取当前日期 String datePath = new DateTime().toString("yyyy/MM/dd"); // 拼接 fileName = datePath + "/" + fileName; // 调用oss方法实现上传 // 参数1 Bucket名称 // 参数2 上传到oss文件路径和文件名称 // 参数3 上传文件输入流 ossClient.putObject(bucketName, fileName, inputStream); // 关闭OSSClient。 ossClient.shutdown(); // 返回上传文件路径 String url = "https://" + bucketName + "." + endpoint + "/" + fileName; return url; }catch (Exception e){ e.printStackTrace(); return null; } } }
上传文件接口完善
问题:上传相同名称文件,造成文件覆盖
解决方式:文件名称添加随机唯一值UUID
// 1 在文件名称中加入随机值(去掉-),防止重名 String uuid = UUID.randomUUID().toString().replaceAll("-",""); fileName = uuid + fileName;文件分类管理
根据日期进行分类
// 2 文件按照日期进行分类 // 获取当前日期 String datePath = new DateTime().toString("yyyy/MM/dd"); // 拼接 fileName = datePath + "/" + fileName;
Nginx
反向代理服务器
- 请求转发
- 负载均衡
- 动静分离
注意:如果使用cmd命令行启动nginx,关闭命令行窗口,nginx并不会停止
需要手动停止:
nginx.exe -s stop
什么是请求转发?

什么是负载均衡?

配置nginx,实现请求转发
找到nginx配置文件nginx.conf
nginx.conf中进行配置
修改nginx默认端口,把80改为81
server { listen 81; server_name localhost; }配置nginx转发规则
在http {}里,新建配置
server { listen 9001; server_name localhost; location ~ /eduservice/ { proxy_pass http://localhost:8001; } location ~ /eduoss/ { proxy_pass http://localhost:8002; } }
重启nginx,先停止后启动
修改前端请求地址,改为nginx地址
dev.env.js
module.exports = merge(prodEnv, { NODE_ENV: '"development"', BASE_API: '"http://localhost:9001"', })
上传头像前端整合
在添加讲师页面,创建上传组件,实现上传
使用element-ui组件实现
在视频提供的源码里找到组件,复制到src\components

添加讲师页面使用此复制上传组件
使用组件
data()定义变量和初始值
data() { //上传弹框组件是否显示 imagecropperShow:false, imagecropperKey:0,//上传组件key值 BASE_API:process.env.BASE_API, //获取dev.env.js里面地址 saveBtnDisabled:false // 保存按钮是否禁用 }引入组件和声明组件
import ImageCropper from '@/components/ImageCropper' import PanThumb from '@/components/PanThumb'export default { components: { ImageCropper, PanThumb }, }修改上传接口地址
编写close方法和上传成功的方法
<el-form-item label="讲师头像"> <!-- 头衔缩略图 --> <pan-thumb :image="teacher.avatar"/> <!-- 文件上传按钮 --> <el-button type="primary" icon="el-icon-upload" @click="imagecropperShow=true">更换头像 </el-button> <!-- v-show:是否显示上传组件 :key:类似于id,如果一个页面多个图片上传控件,可以做区分 :url:后台上传的url地址 @close:关闭上传组件 @crop-upload-success:上传成功后的回调 <input type="file" name="file"/> --> <image-cropper v-show="imagecropperShow" :width="300" :height="300" :key="imagecropperKey" :url="BASE_API+'/eduoss/fileoss'" field="file" @close="close" @crop-upload-success="cropSuccess"/> </el-form-item>methods:{ close() { //关闭上传弹框的方法 this.imagecropperShow=false //上传组件初始化 this.imagecropperKey = this.imagecropperKey+1 }, //上传成功方法 cropSuccess(data) { this.imagecropperShow=false //上传之后接口返回图片地址 this.teacher.avatar = data.url this.imagecropperKey = this.imagecropperKey+1 }, }
课程分类存储结构
假定有一级分类:前端开发、后端开发
二级分类:vue、js、java、c++
数据库表如何存储二级分类?
使用parent_id字段。

EasyExcel读写操作
写操作
引入easyexcel依赖
<dependencies> <!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel --> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.1.1</version> </dependency> </dependencies>创建实体类,与excel数据对应
@Data public class DemoData { //设置excel表头名称 @ExcelProperty(value = "学生编号",index = 0) private Integer sno; @ExcelProperty(value = "学生姓名",index = 1) private String sname; }实现写操作
@Test public void test() { //实现excel写操作 String filename = "D:\\write.xlsx"; //调用easyexcel实现写操作 //参数1 文件路径 参数2 实体类class EasyExcel.write(filename,DemoData.class).sheet("学生列表").doWrite(getData());
读操作
创建excel对应实体类,标记对应列关系,同上
@Data public class DemoData { //设置excel表头名称 @ExcelProperty(value = "学生编号",index = 0) private Integer sno; @ExcelProperty(value = "学生姓名",index = 1) private String sname; }创建监听,读取excel文件
public class ExcelListener extends AnalysisEventListener<DemoData> { //逐行读取excel内容 @Override public void invoke(DemoData demoData, AnalysisContext analysisContext) { System.out.println("****"+demoData); } //读取表头内容 @Override public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) { System.out.println("表头:"+headMap); } //读取完成之后 @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { } }最终方法调用
@Test public void test(){ //实现excel读操作 String filename = "D:\\write.xlsx"; EasyExcel.read(filename,DemoData.class,new ExcelListener()).sheet().doRead(); }
读操作分类
引入easyexcel依赖
使用代码生成器,生成课程分类代码
创建实体类,对应excel关系
@Data public class SubjectData { @ExcelProperty(index = 0) private String oneSubjectName; @ExcelProperty(index = 1) private String twoSubjectName; }实现监听器
public class SubjectExcelListener extends AnalysisEventListener<SubjectData> { //SubjectExcelListener不会交给Spring进行管理,需要自己new,也不能注入其他对象 public EduSubjectService subjectService; public SubjectExcelListener() { } public SubjectExcelListener(EduSubjectService subjectService) { this.subjectService = subjectService; } //逐行读取excel内容 @Override public void invoke(SubjectData subjectData, AnalysisContext analysisContext) { if (subjectData == null){ throw new GuliException(20001,"文件数据为空"); } //添加一级分类 //逐行读取时每次都有两个值,值1 一级分类,值2 二级分类 //先判断一级分类是否重复 EduSubject existOneSubject = this.existOneSubject(subjectService, subjectData.getOneSubjectName()); if (existOneSubject == null){ existOneSubject = new EduSubject(); existOneSubject.setParentId("0"); existOneSubject.setTitle(subjectData.getOneSubjectName()); subjectService.save(existOneSubject); } //获取一级分类id String pid = existOneSubject.getId(); //添加二级分类 EduSubject existTwoSubject = this.existTwoSubject(subjectService, subjectData.getTwoSubjectName(), pid); if (existTwoSubject == null){ existTwoSubject = new EduSubject(); existTwoSubject.setParentId(pid); existTwoSubject.setTitle(subjectData.getTwoSubjectName()); subjectService.save(existTwoSubject); } } //判断一级分类不能重复添加 private EduSubject existOneSubject(EduSubjectService subjectService,String name){ QueryWrapper<EduSubject> wrapper = new QueryWrapper<>(); wrapper.eq("title",name); wrapper.eq("parent_id",0); EduSubject oneSubject = subjectService.getOne(wrapper); return oneSubject; } //判断二级分类不能重复添加 private EduSubject existTwoSubject(EduSubjectService subjectService,String name,String pid){ QueryWrapper<EduSubject> wrapper = new QueryWrapper<>(); wrapper.eq("title",name); wrapper.eq("parent_id",pid); EduSubject twoSubject = subjectService.getOne(wrapper); return twoSubject; } @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { } }
Day 7
- 添加课程分类前端实现
- 课程分类列表显示功能(树形)
- 课程管理模块需求
- 添加课程基本信息功能
添加课程分类前端实现
添加课程分类路由
router/index.js

创建课程分类页面,修改路由对应的页面路径
views/edu/subject/list.vue
views/edu/subject/save.vue
在添加课程分类页面,实现效果
save.vue添加上传组件

课程分类列表-树形显示

参考tree模块,整合前端
TODO: 创建接口,把分类按照要求的格式返回数据
data() { return { filterText: '', data2: [{ id: 1, label: 'Level one 1', children: [{ id: 4, label: 'Level two 1-1', children: [{ id: 9, label: 'Level three 1-1-1' }, { id: 10, label: 'Level three 1-1-2' }] }] }, { id: 2, label: 'Level one 2', children: [{ id: 5, label: 'Level two 2-1' }, { id: 6, label: 'Level two 2-2' }] }, { id: 3, label: 'Level one 3', children: [{ id: 7, label: 'Level two 3-1' }, { id: 8, label: 'Level two 3-2' }] }], defaultProps: { children: 'children', label: 'label' } } }针对返回数据创建对应实体类
两个实体类:一级分类 二级分类
在两个实体类之间表示关系(一级分类包含多个二级分类)
//一级分类 实体类 @Data public class OneSubject { private String id; private String title; //一级分类包含多个二级分类 private List<TwoSubject> children = new ArrayList<>(); }编写具体封装代码
List<EduSubject> oneSubjectList===>List<OneSubject> finalSubjectList
课程发布流程

细节:
创建vo实体类,用于封装表单数据
把表单提交的数据添加至数据库
向两张表添加:课程表和课程描述表
把”讲师”和”分类”用列表显示
“分类”用二级联动效果实现(类似于 xx省 xx市 的关系效果)
课程相关表的关系

添加课程基本信息接口
- 使用代码生成器生成课程相关代码
- 创建vo类,封装表单提交的数据
- 编写 controller service
注意:课程描述表对应id需要修改生成策略
public class EduCourseDescription implements Serializable {
@ApiModelProperty(value = "课程ID")
@TableId(value = "id", type = IdType.INPUT) //手动设置id
private String id;
}
添加课程基本信息前端
添加课程管理路由
该路由为隐藏路由,实现页面跳转
编写表单页面,实现接口
添加完成后返回课程id,便于之后添加大纲
参照后端接口部分:
//添加课程基本信息 @PostMapping("addCourseInfo") public R addCourseInfo(@RequestBody CourseInfoVo courseInfoVo){ //返回添加之后的课程id String id = courseService.saveCourseInfo(courseInfoVo); return R.ok().data("courseId",id); }
二级联动显示
//点击某个一级分类,触发change,显示对应二级分类
subjectLevelOneChanged(value) {
//value就是一级分类id值
//遍历所有的分类,包含一级和二级
for(var i=0;i<this.subjectOneList.length;i++) {
//每个一级分类
var oneSubject = this.subjectOneList[i]
//判断:所有一级分类id 和 点击一级分类id是否一样
if(value === oneSubject.id) {
//从一级分类获取里面所有的二级分类
this.subjectTwoList = oneSubject.children
//把二级分类id值清空
this.courseInfo.subjectId = ''
}
}
},
Day 8
添加课程基本信息 完善
整合文本编辑器
修改课程基本信息功能 实现
课程大纲管理
课程大纲列表显示
章节添加 修改 删除
小节添加 修改 删除
课程信息确认
编写sql语句实现
课程最终发布
整合文本编辑器
复制文本编辑器组件至项目路径
build/webpack.dev.conf.js添加配置
plugins: [ new webpack.DefinePlugin({ 'process.env': require('../config/dev.env') }), new webpack.HotModuleReplacementPlugin(), // https://github.com/ampedandwired/html-webpack-plugin new HtmlWebpackPlugin({ filename: 'index.html', template: 'index.html', inject: true, favicon: resolve('favicon.ico'), title: 'vue-admin-template', templateParameters: { BASE_URL: config.dev.assetsPublicPath + config.dev.assetsSubDirectory } }) ]index.html引入脚本文件
<script src=<%= BASE_URL %>/tinymce4.7.5/tinymce.min.js></script> <script src=<%= BASE_URL %>/tinymce4.7.5/langs/zh_CN.js></script>使用文本编辑器组件
import Tinymce from '@/components/Tinymce' //引入组件 export default { //声明组件 components: { Tinymce }, ... }利用标签实现文本编辑器组件
<!-- 课程简介--> <el-form-item label="课程简介"> <tinymce :height="300" v-model="courseInfo.description"/> </el-form-item>
课程大纲列表功能
参考 Day 7 课程分类列表
创建两个实体类 章节 小节
章节实体类中,使用list表示小节
编写封装代码
前端整合
课程信息修改功能
效果
- 点击 上一步,回显课程基本信息数据
- 在数据回显页面,修改数据,保存,数据库内容也进行修改
后端接口
根据课程id查询课程基本信息接口
//根据课程id查询课程信息 @GetMapping("getCourseInfo/{courseId}") public R getCourseInfo(@PathVariable String courseId){ CourseInfoVo courseInfoVo = courseService.getCourseInfo(courseId); return R.ok().data("courseInfoVo",courseInfoVo); }修改课程信息接口
//修改课程信息 @PostMapping("updateCourseInfo") public R updateCourseInfo(@RequestBody CourseInfoVo courseInfoVo){ courseService.updateCourseInfo(courseInfoVo); return R.ok(); }
前端
api中course.js定义两个方法
修改chapter页面,跳转路径
info页面实现数据回显
获取路由课程id,调用根据id查询接口,显示数据
章节添加 修改 删除
添加
点击 添加章节 按钮,弹出添加框,输入章节信息,点击 保存
删除
如果章节无小节,直接删除
如果章节有小节,如何删除?
删除章节,小节全部删除
若存在小节,不允许删除
@Override public boolean deleteChapter(String chapterId) { //根据章节id查询小节,如果有数据,则不删除 QueryWrapper<EduVideo> wrapper = new QueryWrapper<>(); wrapper.eq("chapter_id",chapterId); int count = videoService.count(wrapper); //判断 if (count > 0){ throw new GuliException(20001,"不能删除"); }else { //删除章节 int result = baseMapper.deleteById(chapterId); return result > 0; //简写 } }
课程信息确认

这些数据需要查询多张表,一般编写sql语句来实现
即:多表连接查询
<!--sql语句:根据课程id查询课程确认信息-->
<select id="getPublishCourseInfo" resultType="com.atguigu.eduservice.entity.vo.CoursePublishVo">
SELECT ec.id,ec.title,ec.price,ec.lesson_num AS lessonNum,ec.cover,
et.name AS teacherName,
es1.title AS subjectLevelOne,
es2.title AS subjectLevelTwo
FROM edu_course ec LEFT OUTER JOIN edu_course_description ecd ON ec.id=ecd.id
LEFT OUTER JOIN edu_teacher et ON ec.teacher_id=et.id
LEFT OUTER JOIN edu_subject es1 ON ec.subject_parent_id=es1.id
LEFT OUTER JOIN edu_subject es2 ON ec.subject_id=es2.id
WHERE ec.id=#{courseId}
</select>
报错
在mapper包创建xml文件编写sql语句,执行后出现错误

原因:
maven默认加载机制问题。
maven加载时,只对src/main/java文件夹里的*.java文件进行编译,其他类型文件不加载
解决方式:
xml文件复制到target目录
xml文件放到resources目录
修改配置
pom.xml
<!-- 项目打包时会将java目录中的*.xml文件也进行打包 --> <build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> </resources> </build>application.properties
#配置mapper xml文件的路径 mybatis-plus.mapper-locations=classpath:com/atguigu/eduservice/mapper/xml/*.xml
Day 9
- 课程最终发布 实现
- 课程信息确认
- 课程发布
- 课程列表
- 课程列表显示
- 课程删除
- 阿里云视频点播服务
- 添加小节实现视频上传
课程列表功能

课程删除
课程包含课程描述、章节、小节、视频
删除课程,把上述部分全部删除

外键一般不建议声明出来
@Override
public void removeCourse(String courseId) {
//1 根据课程id删除小节
eduVideoService.removeVideoByCourseId(courseId);
//2 根据课程id删除章节
chapterService.removeChapterByCourseId(courseId);
//3 根据课程id删除描述
courseDescriptionService.removeById(courseId);
//4 根据课程id删除课程本身
int result = baseMapper.deleteById(courseId);
if (result == 0){
throw new GuliException(20001,"删除失败");
}
}
阿里云视频点播
- 阿里云开通视频点播服务,选择按流量计费
- 视频点播官方文档:https://help.aliyun.com/document_detail/51512.html
- 服务端:后端接口
- 客户端:浏览器、Android、ios
- API:阿里云提供的固定地址,只需调用这个固定地址,并向地址传递参数,就可以实现某些功能
- SDK:对API方式进行封装,更方便使用。例如EasyExcel调用阿里云提供的类或接口中方法实现某些功能
- HttpClient 技术使得可以调用API地址
http://vod.cn-shanghai.aliyuncs.com?Action=GetPlayInfo&VideoId=114514
SDK 获取视频地址和凭证
- 获取视频播放地址
- 获取视频播放凭证
- 上传到阿里云视频点播服务
阿里云对上传视频可以进行加密,此后使用地址不能播放视频,所以数据库不是存储地址,而是存储视频id
在 service 创建子模块 service_vod,引入相关依赖
<dependencies> <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> </dependency> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-vod</artifactId> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-sdk-vod-upload</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </dependency> <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> </dependency> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> </dependency> </dependencies>初始化操作,创建DefaultAcsClient对象(test目录)
public static DefaultAcsClient initVodClient(String accessKeyId, String accessKeySecret) throws ClientException { String regionId = "cn-shanghai"; // 点播服务接入区域 DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret); DefaultAcsClient client = new DefaultAcsClient(profile); return client; }根据视频id,获取视频播放地址
public void getPlayUrl() throws ClientException { //1 根据视频id获取视频地址 //创建初始化对象 DefaultAcsClient client = InitObject.initVodClient(" "," "); //创建获取视频地址request response GetPlayInfoRequest request = new GetPlayInfoRequest(); GetPlayInfoResponse response = new GetPlayInfoResponse(); //向request对象设置视频id request.setVideoId("a60bc97edc9045618980a758a4f895c8"); //调用初始化对象里面的方法,传递request,获取数据 response = client.getAcsResponse(request); List<GetPlayInfoResponse.PlayInfo> playInfoList = response.getPlayInfoList(); //播放地址 for (GetPlayInfoResponse.PlayInfo playInfo : playInfoList) { System.out.print("PlayInfo.PlayURL = " + playInfo.getPlayURL() + "\n"); } //Base信息 System.out.print("VideoBase.Title = " + response.getVideoBase().getTitle() + "\n"); }根据视频id,获取视频播放凭证
public void getPlayAuth() throws ClientException { //根据视频id获取视频播放凭证 //创建初始化对象 DefaultAcsClient client = InitObject.initVodClient(" "," "); //创建获取视频凭证request和response GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest(); GetVideoPlayAuthResponse response = new GetVideoPlayAuthResponse(); //向request对象设置视频id request.setVideoId("a60bc97edc9045618980a758a4f895c8"); response = client.getAcsResponse(request); System.out.println("playauth:" + response.getPlayAuth()); }
SDK 上传视频
注意:aliyun-java-vod-upload的jar依赖并未开源,需要到官网SDK下载jar包,并安装到本地maven仓库
通过官方jar包中自带的UploadVideoDemo.java文件,改造想要的功能
引入依赖
创建 application.properties 配置文件
创建启动类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @ComponentScan(basePackages = {"com.atguigu"}) public class VodApplication { public static void main(String[] args) { SpringApplication.run(VodApplication.class,args); } }创建controller service
@RestController @RequestMapping("/eduvod/video") @CrossOrigin public class VodController { @Autowired private VodService vodService; //上传视频到阿里云 @PostMapping("uploadAliVideo") public R uploadAliVideo(MultipartFile file){ //返回上传视频的id String videoId = vodService.uploadVideoAli(file); return R.ok().data("videoId",videoId); } }@Service public class VodServiceImpl implements VodService { @Override public String uploadVideoAli(MultipartFile file) { try { //accessKeyId, accessKeySecret //fileName 上传文件原始名称 String fileName = file.getOriginalFilename(); //title 上传之后的名称 String title = fileName.substring(0,fileName.lastIndexOf(".")); //inputStream 上传文件输入流 InputStream inputStream = file.getInputStream(); UploadStreamRequest request = new UploadStreamRequest(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET, title, fileName, inputStream); UploadVideoImpl uploader = new UploadVideoImpl(); UploadStreamResponse response = uploader.uploadStream(request); String videoId = null; if (response.isSuccess()) { videoId = response.getVideoId(); } else { //如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因 videoId = response.getVideoId(); } return videoId; }catch (Exception e){ e.printStackTrace(); return null; } } }测试,出现tomcat上传大小问题
解决方式:
在application.properties中进行配置
# 最大上传单个文件大小:默认1M spring.servlet.multipart.max-file-size=1024MB # 最大置总上传的数据大小 :默认10M spring.servlet.multipart.max-request-size=1024MB
前端同样出现问题
在nginx.conf配置8003端口规则
location ~ /eduvod/ { proxy_pass http://localhost:8003; }nginx支持的上传大小有限制
POST http://localhost:9001/eduvod/video/uploadAliVideo 413(Request Entity Too Large)413:即请求体过大
在nginx.conf中添加大小设置
http { include mime.types; default_type application/octet-stream; client_max_body_size 1024m; ... }
Day 10
- 添加小节删除视频(包括阿里云视频)
- springcloud
- 删除小节删除视频
- 删除课程删除视频
添加小节删除视频
删除阿里云视频接口
//根据视频id删除阿里云视频 @DeleteMapping("removeAliVideo/{id}") public R removeAliVideo(@PathVariable String id){ try { //初始化对象 DefaultAcsClient client = InitVodClient.initVodClient(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET); //创建删除视频request对象 DeleteVideoRequest request = new DeleteVideoRequest(); //向request设置视频id request.setVideoIds(id); //调用初始化对象方法实现删除 client.getAcsResponse(request); return R.ok(); }catch (Exception e){ e.printStackTrace(); throw new GuliException(20001,"删除视频失败"); } }前端页面调用接口
在api定义接口路径
//删除视频 deleteAliyunvod(id) { return request({ url: '/eduvod/video/removeAliVideo/'+id, method: 'delete' }) }chapter.vue页面调用
handleVodRemove() { //调用接口的删除视频的方法 video.deleteAliyunvod(this.video.videoSourceId) .then(response => { //提示信息 this.$message({ type: 'success', message: '删除视频成功!' }); //把文件列表清空 this.fileList = [] //把video视频id和视频名称值清空 //上传视频id赋值 this.video.videoSourceId = '' //上传视频名称赋值 this.video.videoOriginalName = '' }) },
微服务与SpringCloud
微服务

微服务是架构风格
把一个项目拆分成独立的多个服务
多个服务独立运行,每个服务占用独立进程
SpringCloud
- SpringCloud并不是一种技术,而是多种技术总称,多种框架集合
- 使用SpringCloud这些框架,实现微服务操作
- 使用SpringCloud,需要依赖技术SpringBoot
SpringCloud相关基础服务组件

Nacos
Nacos是注册中心,相当于房产中介
若要实现不同微服务模块之间的调用,需把这些模块注册到注册中心,此后实现相互调用

Nacos流程

Nacos注册过程
访问nacos:
用户名:nacos
密码:nacos
service的pom引入依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2.2.5.RELEASE</version> <exclusions> <exclusion> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </exclusion> </exclusions> </dependency>在注册服务的配置文件application.properties中,配置Nacos地址
# nacos服务地址 spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848在主启动类添加注解
@EnableDiscoveryClient访问Nacos控制台的服务列表,查看是否注册成功
OpenFeign
服务调用实现过程
前提:需要互相调用的服务在Nacos进行注册
service模块引入依赖
<!--服务调用--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>调用端service-edu,主启动类添加注解
@EnableFeignClients调用端service-edu,创建interface,使用注解指定调用的服务名称,指定调用的方法路径(绝对路径)
@Component @FeignClient(name = "service-vod") public interface VodClient { //定义调用的方法路径 //@PathVariable注解一定要指定参数名称,否则出错 @DeleteMapping("/eduvod/video/removeAlyVideo/{id}") public R removeAlyVideo(@PathVariable("id") String id); }实现删除小节,同时删除阿里云视频
//删除小节 @DeleteMapping("{id}") public R deleteVideo(@PathVariable String id){ //参数为小节id,需要根据小节id,得到视频id EduVideo eduVideo = videoService.getById(id); String videoSourceId = eduVideo.getVideoSourceId(); //判断小节里是否有视频id if (!StringUtils.isEmpty(videoSourceId)){ //根据视频id,"远程调用"实现视频删除 vodClient.removeAlyVideo(videoSourceId); } //删除小节 videoService.removeById(id); return R.ok(); }
删除课程删除视频
一个课程有很多章节,一个章节有很多小节,每个小节有视频
删除课程时,有对应多个视频需要删除
在service-vod创建接口
public interface VodService { ... void removeMoreAlyVideo(List videoIdList); }@Override public void removeMoreAlyVideo(List videoIdList) { try { //初始化对象 DefaultAcsClient client = InitVodClient.initVodClient(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET); //创建删除视频request对象 DeleteVideoRequest request = new DeleteVideoRequest(); //videoIdList中的id转换成 值1,值2,值3 的形式 String videoIds = StringUtils.join(videoIdList.toArray(), ","); //向request设置视频id request.setVideoIds(videoIds); //调用初始化对象的方法实现删除 client.getAcsResponse(request); }catch(Exception e) { e.printStackTrace(); throw new GuliException(20001,"删除视频失败"); } }在service-edu,调用service-vod接口,实现删除多个视频的功能
@Override public void removeVideoByCourseId(String courseId) { //根据课程id,查询所有视频id QueryWrapper<EduVideo> wrapperVideo = new QueryWrapper<>(); wrapperVideo.eq("course_id",courseId); wrapperVideo.select("video_source_id"); List<EduVideo> eduVideoList = baseMapper.selectList(wrapperVideo); //List<EduVideo>转换为List<String> List<String> videoIds = new ArrayList<>(); for (int i = 0; i < eduVideoList.size(); i++) { EduVideo eduVideo = eduVideoList.get(i); String videoSourceId = eduVideo.getVideoSourceId(); //判断是否为空,非空则加入 if (!StringUtils.isEmpty(videoSourceId)){ videoIds.add(videoSourceId); } } if (videoIds.size() > 0){ //多个视频id,删除对应多个视频 vodClient.deleteBatch(videoIds); } QueryWrapper<EduVideo> wrapper = new QueryWrapper<>(); wrapper.eq("course_id",courseId); baseMapper.delete(wrapper); }运行代码出现错误:
java.util.List<?> Not declared?原因:List泛型没有定义
解决:
//删除多个阿里云视频 @DeleteMapping("/eduvod/video/delete-batch") public R deleteBatch(@RequestParam("videoIdList") List<String> videoIdList);
SpringCloud调用接口过程
SpringCloud对于接口调用,大致会经过如下几个组件配合:
Feign -> Hystrix -> Ribbon -> HttpClient (Apache http components 或 Okhttp)
具体交互流程

SpringCloud熔断器

添加熔断器依赖
在调用端配置文件中,开启熔断器
#开启熔断机制 feign.hystrix.enabled=true创建interface后,还需要创建其实现类,出错时实现错误内容输出
@Component public class VodFileDegradeFeignClient implements VodClient { //出错之后会执行 @Override public R removeAlyVideo(String id) { return R.error().message("删除视频出错了"); } @Override public R deleteBatch(List<String> videoIdList) { return R.error().message("删除多个视频出错了"); } }在interface添加注解和属性
@FeignClient(name = "service-vod",fallback = VodFileDegradeFeignClient.class) //调用的服务名称 @Component public interface VodClient { ... }
Day 11
搭建项目前台环境
NUXT框架
整合前台系统页面
首页显示banner数据
轮播图或幻灯片
首页显示热门课程和名师
首页数据使用redis缓存

搭建项目前台环境

NUXT目录结构

NUXT页面加载过程
layouts文件夹下的default.vue只布局头信息、尾信息
而中间信息则是从pages文件夹index.vue引入

后端首页数据banner
service创建子模块service_cms
创建application.properties配置文件
创建数据库crm_banner,根据表使用代码生成器
前台banner显示,后台banner管理

查询所有banner
public List<CrmBanner> selectAllBanner() { //根据id降序排列,显示排列之后的前两条记录 QueryWrapper<CrmBanner> wrapper = new QueryWrapper<>(); wrapper.orderByDesc("id"); //last可以拼接sql语句 wrapper.last("limit 2"); List<CrmBanner> list = baseMapper.selectList(wrapper); return list; }
首页热门课程和名师接口
//查询前8条热门课程,查询前4条名师
@GetMapping("index")
public R index(){
//查询前8条热门课程
QueryWrapper<EduCourse> wrapper = new QueryWrapper<>();
wrapper.orderByDesc("id");
wrapper.last("limit 8");
List<EduCourse> eduList = courseService.list(wrapper);
//查询前4条名师
QueryWrapper<EduTeacher> wrapperTeacher = new QueryWrapper<>();
wrapper.orderByDesc("id");
wrapper.last("limit 4");
List<EduTeacher> teacherList = teacherService.list(wrapperTeacher);
return R.ok().data("eduList",eduList).data("teacherList",teacherList);
}
首页数据显示(前端)
前端环境准备
下载axios依赖
npm install axios封装axios
import axios from 'axios' import cookie from 'js-cookie' import { MessageBox, Message } from 'element-ui' // 创建axios实例 const service = axios.create({ baseURL: `http://localhost:9001`, // api的base_url timeout: 20000 // 请求超时时间 }) export default service
首页banner数据显示
创建api文件夹,在api文件夹创建js文件,定义调用接口路径
import request from '@/utils/request' export default { getListBanner() { return request({ url: '/educms/bannerfront/getAllBanner', method: 'get' }) } }在页面index.vue,调用接口得到数据进行显示
created() { //调用查询banner的方法 this.getBannerList() } methods:{ //查询banner数据 getBannerList() { banner.getListBanner() .then(response => { this.bannerList = response.data.data.list }) } }nginx进行访问规则配置
location ~ /educms/ { proxy_pass http://localhost:8004; }
Redis
- 基于 key-value 进行存储
- 支持多种数据结构:string字符串 list列表 hash哈希 set集合 zset有序集合
- 支持持久化,通过内存进行存储,也可以存到硬盘里
- 支持过期时间,支持事务
- 一般情况下,把经常查询,不经常修改,且不重要的数据放到redis作为缓存
创建redis配置类,放到common模块下
引入SpringBoot整合Redis的相关依赖
<!-- 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>创建redis缓存配置类,配置插件
@EnableCaching @Configuration public class RedisConfig extends CachingConfigurerSupport { ... }
在查询所有banner的方法上,添加缓存注解@Cacheable

@Cacheable(key = "'selectIndexList'",value = "banner") @Override public List<CrmBanner> selectAllBanner() { ... }application.properties
# redis spring.redis.host=192.168.161.129 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虚拟机Linux环境下,启动redis
Day 12
- 登录实现流程
- 注册接口
- 整合jwt
整合阿里云短信服务(已替换为邮件发送)
- 登录接口
- 注册和登录前端实现
单点登录三种方式
单一服务器模式
使用session对象实现
- 登录成功之后,把用户数据放到session里面
- 从session获取数据,判断是否登录
session.setAttribute("user",user);
session.getAttribute("user");
集群部署模式

单点登录三种常见方式
session 广播机制实现
即session复制
使用 cookie+redis 实现
- 在项目中任何一个模块进行登录,登陆之后,把数据放到两个地方
- redis:key生成随机唯一值(ip或用户id等),value存放用户数据
- cookie:把redis生成的key放到cookie里
- 访问项目其他模块,携带cookie进行发送请求
- 通过cookie取值,到redis进行查询,根据key查询数据,若存在数据则判断登录
- 在项目中任何一个模块进行登录,登陆之后,把数据放到两个地方
使用 token 实现
什么是token?
按照一定规则生成的字符串。可以包含用户信息
- 在项目某个模块登录,登录之后,按照特定规则生成字符串,用户信息包含到字符串中,返回字符串
- 字符串可以通过cookie返回
- 也可以通过地址栏返回
- 访问其他模块,每次访问在地址栏携带生成的字符串,访问模块中获取该字符串,根据字符串获取用户信息
- 在项目某个模块登录,登录之后,按照特定规则生成字符串,用户信息包含到字符串中,返回字符串
JWT
token是按照一定规则生成的字符串,且包含用户信息
一般采用通用规则 JWT
JWT生成的字符串包含三部分:
- JWT头信息
- 有效载荷,包含主体信息(用户信息)
- 哈希签名,防伪标志
如何使用
引入 JWT 依赖
<!-- JWT--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> </dependency>创建 JWT 工具类
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"); } }
阿里云短信服务
邮件发送服务
由于阿里云审核方面的问题,已替换为邮件发送,具体参考另一篇文章
Redis验证码有效时间问题
//这里的phone只起到账号作用
@GetMapping("/send/{phone}")
public R sendMail(@PathVariable String phone){
//1 从redis获取验证码,如果获取到直接返回
String code = redisTemplate.opsForValue().get(phone);
if (!StringUtils.isEmpty(code)){
return R.ok();
}
//2 获取不到,生成随机验证码,发送邮件
code = RandomUtil.getFourBitRandom();
Boolean isSend = msmService.sendMail(code,phone);
//发送成功,账号对应的邮件验证码存入redis,并设置有效时间为5分钟
if (isSend){
redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);
return R.ok();
}else {
return R.error().message("发送邮件失败");
}
}
登录接口
- 在service下面创建子模块service_ucenter
- 创建用户表,使用代码生成器生成controller entity mapper service
controller
//登录
@PostMapping("login")
public R loginUser(@RequestBody UcenterMember member){
//member封装手机号和密码
//调用service
//返回token,使用jwt生成
String token = memberService.login(member);
return R.ok().data("token",token);
}
service
@Override
public String login(UcenterMember member) {
//获取登录手机号和密码
String mobile = member.getMobile();
String password = member.getPassword();
if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)){
throw new GuliException(20001,"登录失败");
}
//判断手机号是否存在
QueryWrapper<UcenterMember > wrapper = new QueryWrapper<>();
wrapper.eq("mobile",mobile);
UcenterMember ucenterMember = baseMapper.selectOne(wrapper);
if (ucenterMember == null){
throw new GuliException(20001,"手机号不存在");
}
//判断密码是否正确
//注意:数据库密码是经过加密的,md5加密
//要先把输入密码加密后进行比较
if (!MD5.encrypt(password).equals(ucenterMember.getPassword())){
throw new GuliException(20001,"密码错误");
}
//判断账号是否禁用
if (ucenterMember.getIsDisabled()){
throw new GuliException(20001,"账号被禁用");
}
//登录成功后生成token字符串
String jwtToken = JwtUtils.getJwtToken(ucenterMember.getId(), ucenterMember.getNickname());
return jwtToken;
}
注册接口
创建实体类,封装注册数据
@Data @ApiModel(value="注册对象", description="注册对象") public class RegisterVo { @ApiModelProperty(value = "昵称") private String nickname; @ApiModelProperty(value = "手机号") private String mobile; @ApiModelProperty(value = "密码") private String password; @ApiModelProperty(value = "验证码") private String code; }controller
//注册 @PostMapping("register") public R registerUser(@RequestBody RegisterVo registerVo){ memberService.register(registerVo); return R.ok(); }service
@Override public void register(RegisterVo registerVo) { //获取注册数据 String code = registerVo.getCode(); String mobile = registerVo.getMobile(); String nickname = registerVo.getNickname(); String password = registerVo.getPassword(); //非空判断 if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password) || StringUtils.isEmpty(nickname) || StringUtils.isEmpty(code)){ throw new GuliException(20001,"注册失败"); } //判断验证码 String redisCode = redisTemplate.opsForValue().get(mobile); if (!code.equals(redisCode)){ throw new GuliException(20001,"注册失败"); } //手机号不能重复 QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>(); wrapper.eq("mobile",mobile); Integer count = baseMapper.selectCount(wrapper); if (count > 0){ throw new GuliException(20001,"注册失败"); } //数据添加至数据库 UcenterMember ucenterMember = new UcenterMember(); ucenterMember.setMobile(mobile); ucenterMember.setNickname(nickname); ucenterMember.setPassword(MD5.encrypt(password)); //密码需要MD5加密 ucenterMember.setIsDisabled(false); ucenterMember.setAvatar("http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132"); baseMapper.insert(ucenterMember); }接口:根据token字符串获取用户信息
//根据token获取用户信息 @GetMapping("getMemberInfo") public R getMemberInfo(HttpServletRequest request){ //调用jwt工具类,根据request对象获取头信息,返回用户id String memberId = JwtUtils.getMemberIdByJwtToken(request); //查询数据库,根据id获取用户信息 UcenterMember member = memberService.getById(memberId); return R.ok().data("userInfo",member); }
注册与登录页面前端整合
安装插件
npm install element-ui npm install vue-qriously在nuxt中使用插件element-ui
plugins/nuxt-swiper-plugin.js
import Vue from 'vue' import VueAwesomeSwiper from 'vue-awesome-swiper' import VueQriously from 'vue-qriously' import ElementUI from 'element-ui' //element-ui的全部组件 import 'element-ui/lib/theme-chalk/index.css'//element-ui的css Vue.use(ElementUI) //使用elementUI Vue.use(VueQriously) Vue.use(VueAwesomeSwiper)整合注册页面
layouts文件夹创建登录注册布局页面sign.vue
修改登录和注册地址
layouts/default.vue
<li v-if="!loginInfo.id" id="no-login"> <a href="/login" title="登录"> <em class="icon18 login-icon"> </em> <span class="vam ml5">登录</span> </a> | <a href="/register" title="注册"> <span class="vam ml5">注册</span> </a> </li>创建注册页面register.vue
整合注册功能
api创建register.js,定义注册接口的方法
实现倒计时效果,使用js定时器方法实现
nginx配置路径匹配规则
location ~ /edumsm/ { proxy_pass http://localhost:8005; } location ~ /educenter/ { proxy_pass http://localhost:8006; }
Day 13
- 登录前端整合
- 微信扫描登录
- OAuth2
- 微信登录
登录前端整合
在api创建login.js,定义接口地址
安装插件
npm install js-cookie登录及登录之后,首页显示数据实现过程分析

OAuth2
OAuth2是针对特定问题的一种解决方案
- 开放系统间的授权
- 分布式访问
开放系统间的授权

分布式访问(单点登录)

微信扫描登录
准备工作:
- 注册开发者资质
- 申请网站应用名称
- 需要域名地址
service-ucenter模块配置文件,微信id、秘钥和域名地址
创建类读取配置文件内容
@GetMapping("login") public String getWxCode(){ String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" + "?appid=%s" + "&redirect_uri=%s" + "&response_type=code" + "&scope=snsapi_login" + "&state=%s" + "#wechat_redirect"; //对redirect_url进行编码 String redirect_url = ConstantWxUtils.WX_OPEN_REDIRECT_URL; try { redirect_url = URLEncoder.encode(redirect_url, "utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } String url = String.format( baseUrl, ConstantWxUtils.WX_OPEN_APP_ID, redirect_url, "atguigu" ); //重定向到请求微信地址 return "redirect:" + url; }生成微信扫描二维码
- 直接请求微信提供的固定地址,向地址后面拼接参数
扫描之后获取扫描人信息
技术点:
httpclient
json转换工具
fast json, gson, jackson
扫描之后,执行本地的callback方法,获取两个值,跳转时进行传递
- state:原样传递
- code:类似于手机验证码,唯一随机值(临时票据)
通过code值,请求微信提供的固定地址,获取另外两个值
- access_token:访问凭证
- openid:每个微信唯一标识
通过access_token和openid,再去请求微信提供的固定地址,获取扫描人信息(昵称、头像等)
在首页显示微信信息,如昵称、头像
把扫描后的信息放到cookie,跳转到首页显示
但是存在一个问题:cookie无法实现跨域访问
解决方式:使用jwt,生成token字符串,通过路径传递到首页
//使用jwt,根据member对象,生成token字符串 String jwtToken = JwtUtils.getJwtToken(member.getId(), member.getNickname()); return "redirect:http://localhost:3000?token=" + jwtToken;
首页显示数据
- 首页路径中有token字符串,获取token
- token值放入cookie
- 前端拦截器,判断cookie是否有token,若有,获取token,放到header里
- 调用后端接口,根据token值获取用户信息,用户信息存入cookie
Day 14
- 名师列表功能
- 名师详情功能
- 课程列表功能
- 课程详情功能
- 整合阿里云视频播放器
- 课程评论功能
名师列表功能
分页查询名师接口
@Override public Map<String, Object> getTeacherFrontList(Page<EduTeacher> pageParam) { QueryWrapper<EduTeacher> wrapper = new QueryWrapper<>(); wrapper.orderByDesc("id"); //把分页数据封装到pageTeacher对象 baseMapper.selectPage(pageParam,wrapper); List<EduTeacher> records = pageParam.getRecords(); long current = pageParam.getCurrent(); long pages = pageParam.getPages(); long size = pageParam.getSize(); long total = pageParam.getTotal(); boolean hasNext = pageParam.hasNext(); boolean hasPrevious = pageParam.hasPrevious(); //分页数据放到map集合 Map<String,Object> map = new HashMap<>(); map.put("items", records); map.put("current", current); map.put("pages", pages); map.put("size", size); map.put("total", total); map.put("hasNext", hasNext); map.put("hasPrevious", hasPrevious); return map; }整合前端页面
- 在api创建js文件,定义接口地址
- 在页面引入js文件,调用方法进行显示
讲师详情功能
修改前端讲师列表页面超链接,改成讲师id
后端讲师详情接口
根据讲师id查询讲师基本信息
根据讲师id查询讲师所讲课程
@GetMapping("getTeacherFrontInfo/{teacherId}") public R getTeacherFrontInfo(@PathVariable String teacherId){ //1 根据讲师id,查询讲师基本信息 EduTeacher eduTeacher = teacherService.getById(teacherId); //2 根据讲师id,查询所讲课程 QueryWrapper<EduCourse> wrapper = new QueryWrapper<>(); wrapper.eq("teacher_id",teacherId); List<EduCourse> courseList = courseService.list(wrapper); return R.ok().data("teacher",eduTeacher).data("courseList",courseList); }
课程列表功能
条件查询带分页功能
创建 vo 对象,封装条件数据
@Data public class CourseFrontVo{ ... }编写 controller service
@PostMapping("getFrontCourseList/{page}/{limit}") public R getFrontCourseList(@PathVariable long page, @PathVariable long limit, @RequestBody(required = false) CourseFrontVo courseFrontVo) { Page<EduCourse> pageCourse = new Page<>(page,limit); Map<String,Object> map = courseService.getCourseFrontList(pageCourse,courseFrontVo); //返回分页所有数据 return R.ok().data(map); }课程列表前端整合
课程详情功能
编写 sql 语句,根据课程 id 查询课程信息
- 课程基本信息
- 课程分类
- 课程描述
- 所属讲师
根据课程 id 查询章节和小节
@GetMapping("getFrontCourseInfo/{courseId}") public R getFrontCourseInfo(@PathVariable String courseId) { //根据课程id,编写sql语句查询课程信息 CourseWebVo courseWebVo = courseService.getBaseCourseInfo(courseId); //根据课程id查询章节和小节 List<ChapterVo> chapterVideoList = chapterService.getChapterVideoByCourseId(courseId); return R.ok().data("courseWebVo",courseWebVo).data("chapterVideoList",chapterVideoList); }
整合阿里云播放器
创建接口,根据视频 id 获取视频播放凭证
//根据视频id获取视频凭证 @GetMapping("getPlayAuth/{id}") public R getPlayAuth(@PathVariable String id) { try { //创建初始化对象 DefaultAcsClient client = InitVodCilent.initVodClient(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET); //创建获取凭证request和response对象 GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest(); //向request设置视频id request.setVideoId(id); //调用方法得到凭证 GetVideoPlayAuthResponse response = client.getAcsResponse(request); String playAuth = response.getPlayAuth(); return R.ok().data("playAuth",playAuth); }catch(Exception e) { throw new GuliException(20001,"获取凭证失败"); } }修改前端,实现点击某个小节,打开新的页面进行视频播放
Day15
- 课程评论功能
- 课程支付功能
课程评论功能
实现以下效果:

具体实现过程
- 创建课程评论表 edu_comment
- 创建接口和两个方法
- 分页查询课程评论方法
- 添加评论方法

注意:评论前必须先登录
- 从 header 获取 token(从 request 获取)
- 根据 token 获取用户 id(使用 jwt)
- 根据用户 id 查询用户表,获取数据
课程支付功能
需求分析


支付相关接口汇总
- 生成订单接口
- 根据订单 id 查询订单信息接口
- 微信支付二维码生成接口
- 订单支付状态查询接口
生成订单接口
生成订单需要:课程信息、用户信息
远程调用-Nacos

service_order
//1 生成订单的方法
@PostMapping("createOrder/{courseId}")
public R saveOrder(@PathVariable String courseId, HttpServletRequest request) {
//创建订单,返回订单号
String orderNo =
orderService.createOrders(courseId,JwtUtils.getMemberIdByJwtToken(request));
return R.ok().data("orderId",orderNo);
}
service_edu
//根据课程id查询课程信息
@PostMapping("getCourseInfoOrder/{id}")
public CourseWebVoOrder getCourseInfoOrder(@PathVariable String id) {
CourseWebVo courseInfo = courseService.getBaseCourseInfo(id);
CourseWebVoOrder courseWebVoOrder = new CourseWebVoOrder();
BeanUtils.copyProperties(courseInfo,courseWebVoOrder);
return courseWebVoOrder;
}
service_ucenter
//根据用户id获取用户信息
@PostMapping("getUserInfoOrder/{id}")
public UcenterMemberOrder getUserInfoOrder(@PathVariable String id) {
UcenterMember member = memberService.getById(id);
//把member对象里面值复制给UcenterMemberOrder对象
UcenterMemberOrder ucenterMemberOrder = new UcenterMemberOrder();
BeanUtils.copyProperties(member,ucenterMemberOrder);
return ucenterMemberOrder;
}
根据订单 id 查询订单信息
//2 根据订单id查询订单信息
@GetMapping("getOrderInfo/{orderId}")
public R getOrderInfo(@PathVariable String orderId) {
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("order_no",orderId);
Order order = orderService.getOne(wrapper);
return R.ok().data("item",order);
}
微信支付二维码生成接口
准备:微信支付 id,商户号,商户 key
引入微信支付 maven 依赖
<dependencies>
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>
编写接口
//生成微信支付二维码接口
//参数是订单号
@GetMapping("createNative/{orderNo}")
public R createNative(@PathVariable String orderNo) {
//返回信息,包含二维码地址,还有其他需要的信息
Map map = payLogService.createNatvie(orderNo);
System.out.println("****返回二维码map集合:"+map);
return R.ok().data(map);
}
Day 16
- 支付后执行过程总结
- 课程详情页面观看和购买功能完善
- 系统后台统计分析模块
- 统计分析模块需求
- 生成统计数据
- 使用图表显示统计数据
支付后流程

在课程详情页面,有立即购买/立即观看按钮
- 如果课程是免费的,按钮为
立即观看 - 如果课程是付费的,且已支付,按钮为
立即观看 - 如果课程是付费的,但未支付,按钮为
立即购买
如何判断课程是否已支付
根据课程 id 和用户 id,查询订单表中的订单状态
若状态值为 1,表示已支付,否则未支付
//根据课程id和用户id查询订单表中订单状态
@GetMapping("isBuyCourse/{courseId}/{memberId}")
public boolean isBuyCourse(@PathVariable String courseId,@PathVariable String memberId) {
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("course_id",courseId);
wrapper.eq("member_id",memberId);
wrapper.eq("status",1);//支付状态 1代表已经支付
int count = orderService.count(wrapper);
if(count>0) { //已经支付
return true;
} else {
return false;
}
}
在课程详情页面显示立即购买/立即观看,需要修改课程详情查询接口
添加返回值,返回当前显示详情的课程是否已被购买
//2 课程详情的方法
@GetMapping("getFrontCourseInfo/{courseId}")
public R getFrontCourseInfo(@PathVariable String courseId, HttpServletRequest request) {
//根据课程id,编写sql语句查询课程信息
CourseWebVo courseWebVo = courseService.getBaseCourseInfo(courseId);
//根据课程id查询章节和小节
List<ChapterVo> chapterVideoList = chapterService.getChapterVideoByCourseId(courseId);
//根据课程id和用户id查询当前课程是否已经支付过了
boolean buyCourse = ordersClient.isBuyCourse(courseId, JwtUtils.getMemberIdByJwtToken(request));
return R.ok().data("courseWebVo",courseWebVo).data("chapterVideoList",chapterVideoList).data("isBuy",buyCourse);
}
统计分析模块
- 统计项目中每天有多少注册人数
- 将其用图表显示出来
准备工作
创建表,用于存储统计数据
统计某一天注册人数
查询用户表ucenter_member,得到需要的数据
SELECT COUNT(*) FROM ucenter_member uc WHERE DATE(uc.gmt_create)='2020-03-09'
最终实现过程
生成统计数据

图表显示数据
把统计分析表中存储的数据,使用图表显示

定时任务
定时任务,就是特定时候自动执行程序
类似闹钟
在启动类添加注解
@EnableScheduling public class StaApplication { public static void main(String[] args) { SpringApplication.run(StaApplication.class, args); } }创建定时任务类,在类中利用 cron 表达式设置执行规则
@Component public class ScheduledTask { @Autowired private StatisticsDailyService staService; // 0/5 * * * * ?表示每隔5秒执行一次这个方法 @Scheduled(cron = "0/5 * * * * ?") public void task1() { System.out.println("**************task1执行了.."); } //在每天凌晨1点,把前一天数据进行数据查询添加 @Scheduled(cron = "0 0 1 * * ?") public void task2() { staService.registerCount(DateUtil.formatDate(DateUtil.addDays(new Date(), -1))); } }
Day 17
- canal 数据同步工具
- SpringCloud 组件—— Gateway 网关
- 权限管理模块
- 权限管理需求
- 权限管理接口
canal 数据同步工具
canal 是阿里巴巴的一个开源项目,基于 Java 开发,支持 MySQL 数据库
应用场景
把远程库内容同步到本地库

canal 同步过程
linux 系统
安装 MySQL 数据库
创建数据库和数据表
安装 canal 数据同步工具
本地 windows 系统
- 创建数据库和数据表
修改 linux 系统 MySQL 数据库配置
检查 binlog 功能是否开启
show variables like 'log_bin'
若 Value 为 OFF,表示未开启
若 Value 为 ON,表示已开启
修改 MySQL 配置文件 my.cnf
vi /etc/my.cnf
追加内容
log-bin=mysql-bin
binlog_format=ROW
server_id=1
重启 MySQL 数据库
重启后再去查看 binlog,若变为 ON,则已经开启
安装 canal 工具
把 canal 压缩文件上传到 linux 系统
解压缩
修改 canal 配置文件
vi conf/example/instance.properties# 需要改成自己的数据库信息 canal.instance.master.address= canal.instance.dbUsername= canal.instance.dbPassword= canal.instance.filter.regex=启动 canal
canal 代码编写
- 创建模块
canal_clientedu - 在该模块中引入依赖
- 创建 application.properties 配置文件
- 创建 canal 客户端类
CanalClient,执行启动类CanalApplication
Gateway 网关
什么是网关
在客户端和服务端中间隔一面墙,其作用:请求转发、负载均衡、权限控制等

Gateway

Gateway 具体使用
创建微服务模块
api_gateway在模块中引入相关依赖
创建启动类和配置文件,在配置文件中配置网关规则
@SpringBootApplication @EnableDiscoveryClient public class ApiGatewayApplication { public static void main(String[] args) { SpringApplication.run(ApiGatewayApplication.class, args); } }# 服务端口 server.port=8222 # 服务名 spring.application.name=service-gateway # nacos服务地址 spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 #使用服务发现路由 spring.cloud.gateway.discovery.locator.enabled=true #设置路由id spring.cloud.gateway.routes[0].id=service-acl #设置路由的uri lb://nacos注册服务名称 spring.cloud.gateway.routes[0].uri=lb://service-acl #设置路由断言,代理servicerId为auth-service的/auth/路径 spring.cloud.gateway.routes[0].predicates= Path=/*/acl/** #配置service-edu服务 spring.cloud.gateway.routes[1].id=service-edu spring.cloud.gateway.routes[1].uri=lb://service-edu spring.cloud.gateway.routes[1].predicates= Path=/eduservice/** #配置service-edu服务 spring.cloud.gateway.routes[2].id=service-msm spring.cloud.gateway.routes[2].uri=lb://service-msm spring.cloud.gateway.routes[2].predicates= Path=/edumsm/**
Gateway 负载均衡

权限管理模块
权限管理需求

后端接口整合
创建子模块,在
service里创建service_acl该模块引入依赖
<dependencies> <dependency> <groupId>com.atguigu</groupId> <artifactId>spring_security</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </dependency> </dependencies>引入
service_acl模块的核心代码,包含application.properties在
common模块中引入spring_security代码,包含pom.xml
递归查询菜单

//========================递归查询所有菜单================================================
//获取全部菜单
@Override
public List<Permission> queryAllMenuGuli() {
//1 查询菜单表所有数据
QueryWrapper<Permission> wrapper = new QueryWrapper<>();
wrapper.orderByDesc("id");
List<Permission> permissionList = baseMapper.selectList(wrapper);
//2 把查询所有菜单list集合按照要求进行封装
List<Permission> resultList = bulidPermission(permissionList);
return resultList;
}
//把返回所有菜单list集合进行封装的方法
public static List<Permission> bulidPermission(List<Permission> permissionList) {
//创建list集合,用于数据最终封装
List<Permission> finalNode = new ArrayList<>();
//把所有菜单list集合遍历,得到顶层菜单 pid=0菜单,设置level是1
for(Permission permissionNode : permissionList) {
//得到顶层菜单 pid=0菜单
if("0".equals(permissionNode.getPid())) {
//设置顶层菜单的level是1
permissionNode.setLevel(1);
//根据顶层菜单,向里面进行查询子菜单,封装到finalNode里面
finalNode.add(selectChildren(permissionNode,permissionList));
}
}
return finalNode;
}
private static Permission selectChildren(Permission permissionNode, List<Permission> permissionList) {
//1 因为向一层菜单里面放二层菜单,二层里面还要放三层,把对象初始化
permissionNode.setChildren(new ArrayList<Permission>());
//2 遍历所有菜单list集合,进行判断比较,比较id和pid值是否相同
for(Permission it : permissionList) {
//判断 id和pid值是否相同
if(permissionNode.getId().equals(it.getPid())) {
//把父菜单的level值+1
int level = permissionNode.getLevel()+1;
it.setLevel(level);
//如果children为空,进行初始化操作
if(permissionNode.getChildren() == null) {
permissionNode.setChildren(new ArrayList<Permission>());
}
//把查询出来的子菜单放到父菜单里面
permissionNode.getChildren().add(selectChildren(it,permissionList));
}
}
return permissionNode;
}
递归删除菜单
//============递归删除菜单==================================
@Override
public void removeChildByIdGuli(String id) {
//1 创建list集合,用于封装所有删除菜单id值
List<String> idList = new ArrayList<>();
//2 向idList集合设置删除菜单id
this.selectPermissionChildById(id,idList);
//把当前id封装到list里面
idList.add(id);
baseMapper.deleteBatchIds(idList);
}
//2 根据当前菜单id,查询菜单里面子菜单id,封装到list集合
private void selectPermissionChildById(String id, List<String> idList) {
//查询菜单里面子菜单id
QueryWrapper<Permission> wrapper = new QueryWrapper<>();
wrapper.eq("pid",id);
wrapper.select("id");
List<Permission> childIdList = baseMapper.selectList(wrapper);
//把childIdList里面菜单id值获取出来,封装idList里面,做递归查询
childIdList.stream().forEach(item -> {
//封装idList里面
idList.add(item.getId());
//递归查询
this.selectPermissionChildById(item.getId(),idList);
});
}
角色分配菜单
//=========================给角色分配菜单=======================
@Override
public void saveRolePermissionRealtionShipGuli(String roleId, String[] permissionIds) {
//roleId角色id
//permissionId菜单id 数组形式
//1 创建list集合,用于封装添加数据
List<RolePermission> rolePermissionList = new ArrayList<>();
//遍历所有菜单数组
for(String perId : permissionIds) {
//RolePermission对象
RolePermission rolePermission = new RolePermission();
rolePermission.setRoleId(roleId);
rolePermission.setPermissionId(perId);
//封装到list集合
rolePermissionList.add(rolePermission);
}
//添加到角色菜单关系表
rolePermissionService.saveBatch(rolePermissionList);
}
Day 18
- 整合 Spring Security 权限框架
- Nacos 配置中心
- 提交项目到远程 Git 仓库( Gitee / GitHub )
Spring Security
框架介绍
Spring Security 主要包含两部分:用户认证和用户授权
用户认证
用户进行登录时,输入用户名及密码,后端查询数据库,验证用户名和密码是否正确,若正确则认证成功
用户授权
用户登录系统后,该登录用户可能是不同的角色,若为管理员,则具有所有操作功能,若为普通用户,则只拥有部分操作功能
Spring Security 本质上就是过滤器(Filter),对请求进行过滤
授权过程

代码执行过程

Nacos 配置中心
配置中心的作用

Spring Boot 配置文件
配置文件的加载顺序
bootstrap.propertiesapplication.propertiesspring.profiles.active=dev
application-dev.properties
Day 19
持续化部署工具 jenkins
- 手动打包运行
- 自动打包运行
Day 20 总结
- 总结项目功能
- 总结项目技术点
- 总结项目问题
项目描述
对项目总体介绍
在线教育项目采用 B2C 商业模块,使用微服务架构,项目采用前后端分离开发
项目功能模块
在线教育项目分为前台系统和后台系统
- 前台系统
- 首页数据显示
- 课程列表和详情
- 课程支付
- 课程视频播放
- 微信登录
- 微信支付
- …
- 后台系统
- 权限管理
- 课程管理
- 统计分析
- 课程分类管理
- …
项目涉及技术
项目采用前后端分离开发
- 前端技术
- vue
- element-ui
- nuxt
- babel
- 后端技术
- Spring Boot
- Spring Cloud
- EasyExcel
- …
- 第三方技术
- 阿里云 OSS
- 视频点播
- 短信服务
总结项目功能
准备
- 启动后端接口
- 启动前端项目(前台系统和后台系统)
项目后台管理系统功能
登录功能
Spring Security 框架
权限管理模块
菜单管理
- 列表总览、添加、修改、删除
角色管理
列表总览、添加、修改、删除(批量删除)
为角色分配菜单
用户管理
列表总览、添加、修改、删除(批量删除)
为用户分配角色
权限管理表和关系
使用五张表
acl_permissionacl_roleacl_role_permissionacl_useracl_user_role
讲师管理模块
条件查询分页列表、添加、修改、删除
课程分类模块
- 添加课程分类
- 读取 Excel 课程分类数据,并添加到数据库中
- 课程分类列表
- 使用树形结构显示课程分类列表
课程管理模块
课程列表功能
添加课程
课程发布流程:
第一步填写课程基本信息
第二步添加课程大纲(章节和小节)
第三步确认课程信息,最终课程发布
如何判断课程是否已被发布
使用 status 字段
添加课程时,中途停止添加,重新添加另一课程,如何找到之前未发布的课程,继续发布?
根据课程状态,到课程列表中查询未发布的课程,点击课程右边超链接,继续发布课程
添加小节,上传课程视频
统计分析模块
- 生成统计数据
- 统计数据图表显示
项目前台用户系统功能
首页数据显示
- 显示幻灯片
- 显示热门课程
- 显示名师
注册功能
获取手机验证码
登录功能
输入用户名和密码登录(一般登录)
SSO (单点登录)
单点登录常见方式:
- Session 广播机制
- 使用 Cookie + Redis
- 使用 token
JWT
使用 JWT 生成 token 字符串
JWT 由三部分组成
登录实现流程
调用登录接口,返回 token,添加到 Cookie
创建前端拦截器进行判断,若 Cookie 中包含 token,把 token 添加到 Header,调用接口,根据 token 获取用户信息,添加到 Cookie,显示用户信息
微信扫描登录
OAuth 2
用于解决两个问题:开放系统间授权和分布式访问
如何获取扫描人信息?
扫描后调用微信接口,返回 code (临时票据),请求微信 URL,得到 access_token (访问凭证)和 openid (微信唯一标识),再次请求微信 URL,得到扫描人信息(昵称、头像等)
名师列表功能
名师详情功能
课程列表功能
条件分页查询
课程详情页
- 课程信息显示(包括课程概要、分类、讲师、课程大纲)
- 判断课程是否需要购买
课程视频在线播放
课程支付功能(微信支付)
- 生成课程订单
- 生成微信支付二维码
- 微信支付
微信支付实现流程
若课程是收费课程,点击立即购买,生成订单,点击订单进行支付,生成微信二维码,扫描二维码实现支付
支付后,每隔 3 秒查询支付状态(是否支付成功),若未支付则继续等待,若支付成功,则更新订单状态,向支付记录表添加支付成功记录
总结项目技术点(前端)
vue
基本语法
常见指令:
v-bindv-modelv-ifv-forv-html绑定事件:
v-on-click@click生命周期:
created()页面渲染前mounted()页面渲染后ES6 规范
Element-ui
nodejs
nodejs 是 JavaScript 运行环境,无需浏览器就可直接运行 js 代码,模拟服务器效果
NPM
- 包管理工具,类似 Maven
- npm命令:
npm initnpm install xxxx
Babel
转码器,可以把 ES6 代码转换成 ES5 代码
前端模块化
通过一个页面或一个 js 文件,调用另一个 js 文件里的方法
出现的问题:ES6 的模块化无法在 Node.js 中执行,需要用 Babel 编辑成 ES5 后再执行
后台系统
使用 vue-admin-template,基于 vue + Element-ui
前台系统
使用 Nuxt,基于 vue
服务器渲染技术
Echarts
图表工具
总结项目技术点(后端)
项目采用微服务架构
SpringBoot
- SpringBoot 本质就是 Spring,只是快速构建 Spring 工程的脚手架
- 细节
- 启动类包扫描机制
- 设置扫描规则
@ComponentScan("包路径") - 配置类
- SpringBoot 配置文件
- 配置文件类型:properties 和 yml
- 配置文件加载顺序:
bootstrapapplicationapplicaiton-dev
SpringCloud
是很多框架的总称,使用这些框架可以实现微服务架构
框架组成
- 服务发现
Netflix EurekaNacos - 服务调用
Netflix Feign - 熔断器
Netflix Hystrix - 服务网关
Spring Cloud Gateway - 分布式配置
Spring Cloud ConfigNacos - 消息总线
Spring Cloud BusNacos
- 服务发现
项目中使用
Alibaba Nacos替代 SpringCloud 一些组件- Nacos 作为注册中心
- Nacos 作为配置中心
Feign
服务调用,一个微服务调用另一个微服务,即远程调用
熔断器
Gateway 网关
SpringCloud 之前使用 zuul 网关,目前使用 Gateway 网关
MyBatisPlus
- MyBatisPlus 就是 MyBatis 增强版
- 自动填充
- 乐观锁
- 逻辑删除
- 代码生成器
EasyExcel
- 阿里巴巴提供的操作 Excel 工具,代码简洁且效率高
- 对 POI 进行封装,采用 SAX 方式解析
- 项目中应用场景:添加课程分类,读取 Excel 数据
SpringSecurity
- 在项目整合框架实现权限管理功能
- SpringSecurity 框架组成:认证和授权
- SpringSecurity 认证过程
- SpringSecurity 代码执行过程
Redis
- 首页数据通过 Redis 进行缓存
- Redis 数据类型
- Redis 缓存适用于不太重要或不经常更改的数据
Nginx
- 反向代理服务器
- 请求转发,负载均衡,动静分离
OAuth2 + JWT
- OAuth2 是一套特定问题的解决方案
- JWT 三个组成部分
HttpClient
- 发送请求返回响应的工具,无需浏览器完成请求和响应的过程
- 应用场景:微信登录获取扫描人信息,微信支付查询支付状态
Cookie
- 客户端技术
- 每次发送请求携带 Cookie
- Cookie 有默认会话级别,一般关闭浏览器 Cookie 则消失
- Cookie 可以设置有效时长
setMaxAge()
参考另一篇文章 Cookie 和 Session
微信支付
阿里云 OSS
- 文件存储服务器
- 应用场景:添加讲师时上传讲师头像
阿里云视频点播
- 视频上传、播放、删除
- 整合阿里云视频播放器
阿里云短信服务
注册时发送手机验证码
Git
代码提交到远程 GIt 仓库
Docker + Jenkins
- 手动打包运行
- IDEA 打包
- Jenkins 自动化部署
总结项目问题
前端:路由切换问题
多次路由跳转到同一 vue 界面,页面 created() 方法只会执行一次
解决:使用 vue 监听
前端:ES6 模块化运行问题
Nodejs 不能直接运行 ES6 模块化代码,需要使用 Babel 把 ES6 代码转换为 ES5 代码执行
MyBatisPlus 生成19位 id 值问题
MyBatisPlus 生成 id 值为 19 位,JavaScript 处理数字类型时,只会处理到 16 位
跨域问题
访问协议、ip地址、端口号,三者有任一不一样,就会产生跨域问题
解决:
- 在 Controller 添加注解
@CrossOrigin - 通过网关解决
413 问题
上传视频时,由于 Nginx 有上传文件大小限制,若超过其大小,则会出现 413 问题
413:请求体过大
解决:
在 Nginx 配置文件中设置客户端大小
Maven 加载问题
Maven 加载项目时,默认不会加载 src/java 中的 .xml 类型文件
解决:
- 直接复制 xml 文件到 target 目录(不推荐)
- 通过配置实现
- Post link: http://example.com/2021/07/01/Cloud-Learning/
- Copyright Notice: All articles in this blog are licensed under unless otherwise stated.