“智慧学习”平台,是一个B2C模式的在线教育系统,分为前台用户系统和后台管理系统。基于前后端分离开发,使用微服务架构,后台管理系统基于SpringBoot开发,前台用户系统基于SpringCloud开发。
C2C模式(customer to customer平台模式):用户到用户,本质上是将自己的流量或者用户转卖给视频或者直播的内容提供者,通过出售内容分成获利。例如:51cto、腾讯课堂(平台本身不负责教育)
B2C模式(business to customer会员模式):商家到用户,具有两个角色:管理员和普通用户,管理员负责后台管理系统的维护,普通用户享有前台用户系统的服务。例如:慕课网(平台自身承担教育服务)
B2B2C模式(商家到商家到用户):平台链接第三方商家,平台自身不直接提供商品服务,而是作为一个商品平台,第三方商家可以通过平台向用户售卖自己的商品。例如:京东
MyBatis 是支持普通 SQL查询,存储过程和高级映射的优秀持久层框架。MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
我们使用Mybatis-plus进行数据库操作,使用了mp的众多功能:CRUD、主键策略(@TableId( ))、自动填充(@TableField( ))、乐观锁(OptimisticLockerInterceptor插件)、分页查询(PaginationInterception插件)、逻辑删除(@TableLogic、LogicSqlInjector插件)、复杂查询(QueryWrapper)、自动代码生成器(AutoGenerator)
MyBatis-Plus提供的BaseMapper接口,内置了很多单表CRUD功能,我们只需要定义一个接口UserMapper去继承它,就能瞬间拥有这些能力
操作的核心函数测试代码都是在test目录下的测试类ApplicationTests.java编写,通过@Test注解可以进行单元测试(输入@Test后Alt+Enter自动导包import org.junit.jupiter.api.Test;,之后便可以直接运行函数进行单元测试)
3.1添加操作:
主键策略
mp自带主键策略,默认是AUTO:自动增长,如果需要其他策略可以通过在User.java类(数据库表对应的实体类)中添加注解@TableId()设置
3.2修改操作:
自动填充功能
对于表User中的两个属性create_time和update_time,我们可以手动设置时间值,也可以利用mp的自动填充功能自动设置(使用注解@TableField( ))。
乐观锁
问题描述:多个人同时修改同一条记录,会造成线程安全问题。如果不考虑事务隔离性,会产生读问题(脏读、不可重复读、幻读),也会产生写问题(丢失更新问题)。
解决方案:悲观锁(串行)、乐观锁(并行)
乐观锁原理:
取出记录时,获取当前version
更新时,带上这个version
执行更新时, set version = newVersion where version = oldVersion
如果version不对,就更新失败
乐观锁操作流程:
PS:乐观锁插件、分页插件、逻辑删除插件都在config内,系统会自动导包
import com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
3.3查询操作
3.3.2多个id批量查询
3.3.3分页查询
mp自带的分页插件有PaginationInterceptor和Pagehelper,我们使用PaginationInterceptor
也可以返回Map类型
3.4删除操作
物理删除:真实删除,底层sql语句:delete
逻辑删除:假删除,底层sql语句:update,将对应数据中代表是否被删除字段状态修改为“被删除状态”(deleted = 1),数据库中仍然可以看到这条数据
Mybatis Plus中查询操作自动添加了逻辑删除字段的判断:
/**
* 测试 逻辑删除后的查询:
* 不包括被逻辑删除的记录
*/
@Test
public void testLogicDeleteSelect() {
User user = new User();
List<User> users = userMapper.selectList(null);
users.forEach(System.out::println);
}
底层sql语句:
SELECT id,name,age,email,create_time,update_time,deleted FROM user WHERE deleted=0
3.5QueryWrapper实现复杂条件查询
样例java代码
@Test
public void testDelete() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper
.isNull("name")
.ge("age", 12)
.isNotNull("email");
int result = userMapper.delete(queryWrapper);
System.out.println("delete return count = " + result);
}
底层sql语句如下:
UPDATE user SET deleted=1 WHERE deleted=0 AND name IS NULL AND age >= ? AND email IS NOT NULL
4.1.1设计理念:前后端分离开发
4.1.2数据库设计
guli_edu是库名,edu_teacher是表名(这个库里的一个表),数据库里的一个表就对应java里的一个实体类(EduTeacher)
4.1.3搭建项目工程
在idea开发工具中,使用 Spring Initializr 快速初始化一个 Spring Boot 模块,版本使用:2.2.1.RELEASE
完善项目结构:
最终项目结构(目前还不是这个样子):
4.2.1模块配置,重点使用了mp的自动代码生成器AutoGenerator
代码生成器CodeGenerator.java可以进行全局配置(GlobalConfig)、数据库配置(DataSourceConfig)、包配置(PackageConfig)、策略配置(StrategyConfig)
package com.atguigu.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("E:\\work\\guli_parent\\service\\service_edu" + "/src/main/java");
gc.setAuthor("testjava");
gc.setOpen(false); //生成后是否打开资源管理器
gc.setFileOverride(false); //重新生成时文件是否覆盖
//UserServie
gc.setServiceName("%sService"); //去掉Service接口的首字母I
gc.setIdType(IdType.ID_WORKER_STR); //主键策略
gc.setDateType(DateType.ONLY_DATE);//定义生成的实体类中日期类型
gc.setSwagger2(true);//开启Swagger2模式
mpg.setGlobalConfig(gc);
// 3、数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("root");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);
// 4、包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName("eduservice"); //模块名
//包 com.atguigu.eduservice
pc.setParent("com.atguigu");
//包 com.atguigu.eduservice.controller
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();
}
}
4.2.2编写api接口(查找所有讲师)
初学Java我们使用new的方式来创建对象,Spring可以使用xml或注解的方式来创建对象。spring中每一个需要管理的对象称为bean,spring管理这些bean的容器,称为ioc容器。
@RestController:是@Controller与@ResponseBody的结合;
@Controller:将当前修饰的类注入SpringBoot IOC容器,使得该类所在的项目跑起来的时候,这个类就被实例化了。
@ResponseBody:修饰类时,表示该类中所有方法返回的数据,都会以JOSN字符串的形式写入response body内,返回客户端。修饰方法时,表示将方法返回的数据转换为JOSN字符串写入response body内,返回客户端。
@RequestBody:通过HttpMessageConverter转换器将WEB请求中的JOSN数据进行类型转换,绑定到指定方法的参数中。(一般结合POST请求使用)
@RequestBody(required = false)表示请求体可以为空,@RequestBody(required = true)表示请求体不可以为空。
@RequestMapping:将 HTTP 请求映射到 MVC 和 REST 控制器的处理方法上。
@GetMapping:将HTTP get请求映射到特定处理程序的方法注解,是@RequestMapping(method = RequestMethod.GET)的缩写。
@PostMapping:用于将HTTP post请求映射到特定处理程序的方法注解,是@RequestMapping(method = RequestMethod.POST)的缩写。
@DeleteMapping:用于将HTTP delete请求映射到特定处理程序的方法注解,是@RequestMapping(method = RequestMethod.DELETE)的缩写。
@Autowired:将service层的实现类注入controller层
4.2.4整合swagger进行接口测试
REST(representational state transfer):表现层状态变换,是一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。
RESTful API:就是满足REST架构风格的接口。
Swagger:是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。,具有及时性、规范性、一致性、可测性。
@swagger2:使用swagger2构建restful接口测试。可以生成文档形式的api并提供给不同的团队;便于自测,也便于领导查阅任务量;无需过多冗余的word文档;
4.2.5统一返回数据格式
接口传输数据格式最常用的有三种:JSON、XML、YAML
①JSON(JavaScript Object Notation),是一种轻量级的文本数据交换格式,使用JavaScript语法来描述数据对象,由key|value键值对构成。具有自我描述性,易于阅读编写,也易于机器解析与生成。但是JSON仍然可以独立于语言和平台,支持许多不同的编程语言,非常适用于服务器与JavaScript交互。表现形式如下:
书写规则:
数字直接表示:2.90
字符串需要加双引号:“Hello World”
对象需要大括号:对象内部的属性和值使用key:value键值对表示,属性还可以是对象、数组
数组需要中括号:数组元素之间用逗号隔开,数组元素可以是对象、数组
可以将Java对象转换成JSON数据:JSON.toJSONString
②XML(eXtensible Markup Language)可扩展标记语言,是一种用于标记电子文件使其具有结构性的标记语言。
③YAML(Yet Another Markup Language)是一种直观的能够被电脑识别的数据序列化格式。由于实现简单,解析成本低,特别适合在脚本语言中使用。
4.2.6分页查询
①Config类中配置分页插件(PaginationInterceptor)
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
②Controller类中编写分页查询方法
//3 分页查询讲师的方法
//current 当前页
//limit 每页记录数
@GetMapping("pageTeacher/{current}/{limit}")
public R pageListTeacher(@PathVariable long current,
@PathVariable long limit) {
//创建page对象
Page<EduTeacher> pageTeacher = new Page<>(current,limit);
int i = 10/0;
//调用方法实现分页
//调用方法时候,底层封装,把分页所有数据封装到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);
return R.ok().data("total",total).data("rows",records);
}
③swagger测试
4.2.7多条件分页查询
前端查询界面:
①创建查询对象:entity中创建vo文件夹→vo内创建TeacherQuery.java对象,用于存放html页面的数据(作为条件查询的条件封装对象)
②controller层编写方法
@PathVariable:接收请求路径URL中占位符的值作为方法的形参,匹配规则是同名赋值,也可以使用@PathVariable(“current”)指定接收URL中的current占位符
//4 条件查询带分页的方法
@PostMapping("pageTeacherCondition/{current}/{limit}")
public R pageTeacherCondition(@PathVariable long current,@PathVariable long limit,
@RequestBody(required = false) TeacherQuery teacherQuery) {
//创建page对象
Page<EduTeacher> pageTeacher = new Page<>(current,limit);
//构建条件
QueryWrapper<EduTeacher> wrapper = new QueryWrapper<>();
// 多条件组合查询
// mybatis学过 动态sql
String name = teacherQuery.getName();
Integer level = teacherQuery.getLevel();
String begin = teacherQuery.getBegin();
String end = teacherQuery.getEnd();
//判断条件值是否为空,如果不为空拼接条件
if(!StringUtils.isEmpty(name)) {
//构建条件
wrapper.like("name",name);
}
if(!StringUtils.isEmpty(level)) {
wrapper.eq("level",level);
}
if(!StringUtils.isEmpty(begin)) {
wrapper.ge("gmt_create",begin);
}
if(!StringUtils.isEmpty(end)) {
wrapper.le("gmt_create",end);
}
//调用方法实现条件查询分页
teacherService.page(pageTeacher,wrapper);
long total = pageTeacher.getTotal();//总记录数
List<EduTeacher> records = pageTeacher.getRecords(); //数据list集合
return R.ok().data("total",total).data("rows",records);
}
③swagger测试
4.2.8讲师添加、修改功能
自动填充封装
controller方法定义
//添加讲师接口的方法
@PostMapping("addTeacher")
public R addTeacher(@RequestBody EduTeacher eduTeacher) {
boolean save = teacherService.save(eduTeacher);
if(save) {
return R.ok();
} else {
return R.error();
}
}
//根据讲师id进行查询
@GetMapping("getTeacher/{id}")
public R getTeacher(@PathVariable String id) {
EduTeacher eduTeacher = teacherService.getById(id);
return R.ok().data("teacher",eduTeacher);
}
//讲师修改功能
@PostMapping("updateTeacher")
public R updateTeacher(@RequestBody EduTeacher eduTeacher) {
boolean flag = teacherService.updateById(eduTeacher);
if(flag) {
return R.ok();
} else {
return R.error();
}
}
4.3.1什么是“统一异常处理”
我们想要异常结果也显示为统一的返回结果对象,并且统一处理系统的异常信息,那么就需要统一异常处理。
4.3.2“需要使用到的注解”
@ControllerAdvice:是Spring3新增的注解,学名是Controller增强器,作用是给Controller控制器添加统一的操作或处理。通常结合@ExceptionHandler()用于全局异常的处理。
@ExceptionHandler()注解的使用:
基本使用方法:@ExceptionHandler()可以用来统一处理方法抛出的异常,使用时需要定义一个异常处理方法,这个方法可以处理类中其他方法(被@RequestMapping)抛出的异常。
注解的参数: @ExceptionHandler()注解内可以添加参数,参数是某个异常类.class,代表这个方法专门处理该类异常。默认是Exception
最近原则:当异常发生时,Spring会选择最接近抛出异常的处理方法。
注解方法的返回值:标识了@ExceptionHandler注解的方法,返回值类型和标识了@RequestMapping的方法是统一的。
4.4.1什么是日志
日志记录了系统行为的时间、地点、状态等信息,能够帮助我们了解并监控系统状态,在发生错误或者接近某种危险状态时能及时提醒我们处理,同时在系统产生问题时,能够快速地定位、诊断问题。在springboot项目启动、运行时,控制台就会输出日志信息:
日志格式:
日志级别:(默认是INFO,即显示日志级别INFO及以上的信息)
TRACE:普通微量的日志,级别最低的日志信息。
DEBUG:调试的时候的日志信息。TRACE和DEBUG级别的日志主要是对系统每一步的运行状态进行精确到记录。通过这种记录,可以查看某个操作每一步的执行情况,可以精确定位问题所在。
INFO:普通的日志信息,记录系统正常运行的状态,日志的默认级别
WARN:警告日志,出现的问题不影响使用,但需要注意。
ERROR:错误信息,级别较高的错误日志信息。这个级别的错误需要马上人工介入处理,但紧急程度低于fatal.ERROR和FATAL都是服务器自己的异常,像用户自己的操作不当等不应该被记为ERROR日志。
FATAL:致命错误,最高日志级别,表示需要立即被处理的系统级错误。当该错误发生时,表示服务已经出现了不可用,也可以说服务挂了,需要技术人员立即介入处理。一般情况下,一个进程的生命周期中应该只记录一次fatal级别的日志,即进程遇到无法恢复的错误推出执行时。
如何设置日志级别:logging.level.root = WARN
日志输出方式:控制台输出、日志持久化
日志持久化:不会将日志在控制台输出,而是将日志保存下来,以便出现问题时进行追溯(以.log格式文件保存)
4.4.2日志框架
市场上的日志框架有哪些:JUL、JCL、Jboss-logging、logback、log4j2、slf4j…
springboot中日志的依赖关系:(默认使用SLF4j+Logback)
4.4.3logback日志框架
我们使用Logback框架实现日志系统
①为什么选择Logback框架:
更快的执行速度、更少的内存需求;
充分的测试:logback框架的测试更加充分,因此更加稳定、可靠;
实现了slf4j接口,如果需要切换到log4j或其他框架,只需要替换一个jar包即可,不需要改变实现slf4j接口的代码,大大减少更换日志系统的工作量;
通过Groovy编写配置文件:相比于XML,Groovy风格的配置文件更加直观、连贯、简短。配置logback的传统方法是通过XML文件,现在已经有工具可以把logback.xml文件迁移至logback.groovy;
自动重新载入配置文件:logback可以在配置文件被修改后,自动重新载入;
可以优雅的从I/O错误中恢复;
自动清除旧的日志文档:通过设置TimeBasedRollingPolicy或者SizeAndTimeBasedFNATP的maxHistory属性,就可以控制日志文件的最大数量;
自动压缩归档日志文件:RollingFileAppender可以在回滚操作中,自动压缩归档日志文件。并且压缩操作是异步执行的,不会堵塞其他应用;
②配置logback日志
③使用:将错误日志输出
@Slf4j:注解在类上,为类提供一个名为log的org.slf4j.Logger日志对象,可以直接调用log对象里的方法(info、dubug、error等)输出日志信息。使用lombock插件就可以导入该注解。
slf4j:是一个接口,他只是一个日志标准,并不是日志的具体实现,它可以提供日志接口、提供获取具体日志对象的方法。常见的日志框架logback、log4j、slf4j-simple都是slf4j接口的实现类。这是门面模式的典型应用。门面模式核心就是外部与子系统的通信必须通过一个统一的外观对象(大门)进行,使得子系统更易于使用。
@Slf4j注解的功能相当于LoggerFactory.getLogger(),即创建一个日志实例对象
通过log.error()方法可以将指定类型的异常信息输出到日志文件
前端使用VScode工具编程,采用ES6标准(JavaScript语言的下一代标准),使用Vue框架(Vue.js是一套用于构建用户界面的渐进式框架,组件(component)是Vue最强大的功能之一),使用element-ui组件库。利用vue-element-admin模板,它是一个后台前端解决方案,基于vue和element-ui实现,我们主要使用了其中的vue-admin-template模板
前端整体业务流程:
4.5.1使用axios实现ajax请求操作
axios是独立的项目(工具),不是vue里面的一部分,使用axios经常和vue一起使用,用于实现ajax操作
axios是通过promise实现对ajax技术的一种封装,jQuery也可以实现ajax封装
ajax技术实现了网页的局部数据刷新,axios实现了对ajax的封装。
axios的作用:
在浏览器中可以帮助我们完成ajax请求的发送
在node.js中可以向远程接口发送请求
使用axios应用场景
vue框架的编程模板
axios的使用:发送ajax请求获得json数据并在页面显示
4.5.2前端界面模拟登录功能
4.5.3前端框架开发过程
4.5.4讲师列表前端开发
①基础架构
②讲师列表分页查询、条件查询
③讲师删除功能
④讲师添加功能
⑤讲师修改功能
先显示要修改的讲师信息
然后进行修改,最后返回讲师列表页面
阿里云对象存储OSS:海量、安全、低成本、高可靠的云存储服务。
4.6.1准备工作
4.6.2搭建OSS操作环境
4.6.3上传文件到OSS接口实现
@Component:用于标注一个类,表示该类是spring容器的一个bean,即把该类实例化到spring容器
@Controller:标注的类表示WEB层实现(Controller层),注入service层
@Service:标注的类表示Service层实现,注入dao层
@Repository:标注的类表示dao层实现(持久层),用于访问数据库
@Controller、@Service、@Repository本身都是基于@Component实现的,表示把这些类纳入到spring容器中进行管理,同时也是表明把该类标记为Spring容器中的一个Bean。
@ComponentScan是spring中的注解,定义扫描的路径,从中找出标识了(@Controller,@Service,@Repository,@Component)注解的类,自动装配到Spring的bean容器中。
Spring中的InitializingBean接口:为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候会执行该方法。afterPropertiesSet方法用于初始化。
@Value:将配置文件(application.properties)中的指定属性读取出来,使用形式为@Value(“KaTeX parse error: Expected 'EOF', got '#' at position 15: { }")或@Value("#̲{ }"),{ }内部指明属性…{aliyun.oss.file.endpoint}”)表示读取配置文件中的aliyun.oss.file.endpoint属性值。
①创建常量类ConstantPropertiesUtils.java,读取配置文件的内容
②创建Controller类OssController.java
MultipartFile是SpringMVC提供的简化上传操作的工具类,它实质上是一个接口。一个MultipartFile就表示客户端上传过来的一个文件,一个MultipartFile[]数组则表示客户端上传过来的多个文件,我么可以通过接口的方法访问文件的基本信息。不使用框架之前,都是使用原生的HttpServletRequest来封装HTTP请求信息,文件是以二进制流的形式传递到后端服务器,然后需要我们自己转化为File类。
@RestController
@RequestMapping("/eduoss/fileoss")
@CrossOrigin
public class OssController {
@Autowired
private OssService ossService;
//上传头像的方法
@PostMapping
public R uploadOssFile(MultipartFile file) {
//获取上传文件 MultipartFile
//返回上传到oss的路径
String url = ossService.uploadFileAvatar(file);
return R.ok().data("url",url);
}
}
③创建Service层接口OssService.java
package com.atguigu.oss.service;
import org.springframework.web.multipart.MultipartFile;
public interface OssService {
//上传头像到oss
String uploadFileAvatar(MultipartFile file);
}
④创建Service接口实现类OssServiceImpl.java(核心)
OSSClient是OSS的Java客户端,用于管理存储空间和文件等OSS资源。使用Java SDK发起OSS请求,您需要初始化一个OSSClient实例,并根据需要修改ClientConfiguration的默认配置项。
@Service
public class OssServiceImpl implements OssService {
//上传头像到oss
@Override
public String uploadFileAvatar(MultipartFile file) {
// 工具类获取值
String endpoint = ConstantPropertiesUtils.END_POIND;
String accessKeyId = ConstantPropertiesUtils.ACCESS_KEY_ID;
String accessKeySecret = ConstantPropertiesUtils.ACCESS_KEY_SECRET;
String bucketName = ConstantPropertiesUtils.BUCKET_NAME;
try {
// 创建OSS实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
//获取上传文件输入流
InputStream inputStream = file.getInputStream();
//获取文件名称
String fileName = file.getOriginalFilename();
//1 在文件名称里面添加随机唯一的值
String uuid = UUID.randomUUID().toString().replaceAll("-","");
// yuy76t5rew01.jpg
fileName = uuid+fileName;
//2 把文件按照日期进行分类
//获取当前日期
// 2019/11/12
String datePath = new DateTime().toString("yyyy/MM/dd");
//拼接
// 2019/11/12/ewtqr313401.jpg
fileName = datePath+"/"+fileName;
//调用oss方法实现上传
//第一个参数 Bucket名称
//第二个参数 上传到oss文件路径和文件名称 aa/bb/1.jpg
//第三个参数 上传文件输入流
ossClient.putObject(bucketName,fileName , inputStream);
// 关闭OSSClient。
ossClient.shutdown();
//把上传之后文件路径返回
//需要把上传到阿里云oss路径手动拼接出来
// https://edu-guli-1010.oss-cn-beijing.aliyuncs.com/01.jpg
String url = "https://"+bucketName+"."+endpoint+"/"+fileName;
return url;
}catch(Exception e) {
e.printStackTrace();
return null;
}
}
}
4.6.4nginx实现请求转发
①nginx是什么
Nginx(engine x)是一个高性能的HTTP和反向代理web服务器,是一款轻量级的web服务器、反向代理服务器、电子邮件代理服务器。特点是占有内存少,并发能力强。
反向代理与正向代理的区别:
反向代理(Reverse Proxy):作用在服务端,是一个虚拟的ip(VIP)。反向代理是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。
正向代理:作用在客户端,比如(是在我们用户的浏览器端设置的),正向代理是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端才能使用正向代理。
②为什么要使用nginx
高并发:是互联网分布式系统架构设计中必须考虑的因素之一,通常是指通过设计保证系统能够同时并行处理很多请求。
传统的单个tomcat服务器最大并发数有限,高并发相关的一些指标有响应时间、吞吐量、QPS(query per second)、并发用户数;
响应时间:系统对请求做出响应的时间;
吞吐量:单位时间内处理的请求数量
QPS:每秒响应请求数,与吞吐量类似
并发用户数:同时承载正常使用系统功能的用户数量
高可用:通常来描述一个系统经过专门的设计,从而减少停工时间,而保持其服务的高度可用性。
高性能:是指服务响应时间快,特别是在高并发下响应时间不会急剧增加
③nginx的使用场景(都是修改nginx.conf配置文件)
动静分离
负载均衡
④nginx启动命令(找到nginx.exe所在目录cmd打开命令提示符)
4.7.1EasyExcel
EasyExcel是阿里巴巴开源的一个excel处理框架,以使用简单、节省内存著称。EasyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析,并将一行的解析结果以观察者的模式通知处理(AnalysisEventListener)。
4.7.2建立数据库表:课程分类数据库结构
sql文件:
一开始建立的数据库表是个空表(只有表名"edu_subject"和属性),数据库表的详细内容是通过EasyExcel读取excel文件添加进去的
4.7.3基于EasyExcel读取excel文件内容到数据库
①service/service_edu/pom.xml中引入EasyExcel依赖
②利用mp的自动代码生成器生成其他代码(自动生成的EduSubject.java实体类与数据库表"edu_subject"对应)
③手动创建实体类SubjectData.java与excel对应
④controller层
⑤service层【调用EasyExcel.read方法读取excel文件到实体类SubjectData】
⑥监视器(自定义)【一行一行的读取:实现读取SubjectData实体类,封装到实体类EduSubject中,并调用subjectService.save方法将EduSubject对象信息添加至数据库表“edu_subject”中】【难点】
package com.atguigu.eduservice.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.atguigu.eduservice.entity.EduSubject;
import com.atguigu.eduservice.entity.excel.SubjectData;
import com.atguigu.eduservice.service.EduSubjectService;
import com.atguigu.servicebase.exceptionhandler.GuliException;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
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,"文件数据为空");
}
//一行一行读取,每次读取有两个值,第一个值一级分类,第二个值二级分类
//判断一级分类是否重复
EduSubject existOneSubject = this.existOneSubject(subjectService, subjectData.getOneSubjectName());
if(existOneSubject == null) { //没有相同一级分类,进行添加
existOneSubject = new EduSubject();
existOneSubject.setParentId("0");
existOneSubject.setTitle(subjectData.getOneSubjectName());//一级分类名称
subjectService.save(existOneSubject);//调用service.save方法将EduSubject对象添加至数据库中
}
//获取一级分类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);//使用wrapper条件查询(getOne只查一个)数据库,查到返回结果,查不到返回null
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) {
}
}
4.8.1创建两个实体类分别表示一级分类和二级分类
4.8.2controller层
4.8.3service层
4.8.4service实现类【核心】
package com.atguigu.eduservice.service.impl;
import com.alibaba.excel.EasyExcel;
import com.atguigu.eduservice.entity.EduSubject;
import com.atguigu.eduservice.entity.excel.SubjectData;
import com.atguigu.eduservice.entity.subject.OneSubject;
import com.atguigu.eduservice.entity.subject.TwoSubject;
import com.atguigu.eduservice.listener.SubjectExcelListener;
import com.atguigu.eduservice.mapper.EduSubjectMapper;
import com.atguigu.eduservice.service.EduSubjectService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
*
* 课程科目 服务实现类
*
*
* @author testjava
* @since 2020-02-29
*/
@Service
public class EduSubjectServiceImpl extends ServiceImpl<EduSubjectMapper, EduSubject> implements EduSubjectService {
//添加课程分类
@Override
public void saveSubject(MultipartFile file,EduSubjectService subjectService) {
try {
//文件输入流
InputStream in = file.getInputStream();
//调用方法进行读取
EasyExcel.read(in, SubjectData.class,new SubjectExcelListener(subjectService)).sheet().doRead();
}catch(Exception e){
e.printStackTrace();
}
}
//课程分类列表(树形)
@Override
public List<OneSubject> getAllOneTwoSubject() {
//1 查询所有一级分类 parentid = 0
QueryWrapper<EduSubject> wrapperOne = new QueryWrapper<>();
wrapperOne.eq("parent_id","0");
List<EduSubject> oneSubjectList = baseMapper.selectList(wrapperOne);
//2 查询所有二级分类 parentid != 0
QueryWrapper<EduSubject> wrapperTwo = new QueryWrapper<>();
wrapperTwo.ne("parent_id","0");
List<EduSubject> twoSubjectList = baseMapper.selectList(wrapperTwo);
//创建list集合,用于存储最终封装数据
List<OneSubject> finalSubjectList = new ArrayList<>();
//3 封装一级分类
//查询出来所有的一级分类list集合遍历,得到每个一级分类对象,获取每个一级分类对象值,
//封装到要求的list集合里面 List finalSubjectList
for (int i = 0; i < oneSubjectList.size(); i++) { //遍历oneSubjectList集合
//得到oneSubjectList每个eduSubject对象
EduSubject eduSubject = oneSubjectList.get(i);
//把eduSubject里面值获取出来,放到OneSubject对象里面
OneSubject oneSubject = new OneSubject();
// oneSubject.setId(eduSubject.getId());
// oneSubject.setTitle(eduSubject.getTitle());
//eduSubject值复制到对应oneSubject对象里面
BeanUtils.copyProperties(eduSubject,oneSubject);
//多个OneSubject放到finalSubjectList里面
finalSubjectList.add(oneSubject);
//在一级分类循环遍历查询所有的二级分类
//创建list集合封装每个一级分类的二级分类
List<TwoSubject> twoFinalSubjectList = new ArrayList<>();
//遍历二级分类list集合
for (int m = 0; m < twoSubjectList.size(); m++) {
//获取每个二级分类
EduSubject tSubject = twoSubjectList.get(m);
//判断二级分类parentid和一级分类id是否一样
if(tSubject.getParentId().equals(eduSubject.getId())) {
//把tSubject值复制到TwoSubject里面,放到twoFinalSubjectList里面
TwoSubject twoSubject = new TwoSubject();
BeanUtils.copyProperties(tSubject,twoSubject);
twoFinalSubjectList.add(twoSubject);
}
}
//把一级下面所有二级分类放到一级分类里面
oneSubject.setChildren(twoFinalSubjectList);
}
return finalSubjectList;
}
}
4.9.1课程添加整体架构
4.9.3使用代码生成器生成课程相关的代码
4.9.4步骤一:编辑课程基本信息
①创建vo实体类CourseInfoVo封装“编辑课程基本信息”表单信息
修改课程简介表edu_course_description(对应实体类EduCourseDescription)中id主键生成策略
②把表单提交过来的数据CourseInfoVo封装成PO对象EduCourse、EduCourseDescription,并添加至数据库(课程表edu_course和课程描述表edu_course_description)
③把讲师和分类使用下拉列表显示(前端),课程分类做成二级联动效果
④课程基本信息回显:根据课程id查询课程基本信息接口开发
根据CourseId查询数据库表edu_course、edu_course_description→封装到PO对象EduCourse、EduCourseDescription→copy到VO对象CourseInfoVo→前端调用显示
controller层(EduCourseController)
//根据课程id查询课程基本信息
@GetMapping("getCourseInfo/{courseId}")
public R getCourseInfo(@PathVariable String courseId) {
CourseInfoVo courseInfoVo = courseService.getCourseInfo(courseId);
return R.ok().data("courseInfoVo",courseInfoVo);
}
service层(EduCourseService)
//根据课程id查询课程基本信息
CourseInfoVo getCourseInfo(String courseId);
service层(EduCourseServiceImpl)
⑤修改课程基本信息功能
在数据回显页面→修改课程基本信息内容→收集页面信息封装到VO对象CourseInfoVo→copy到PO对象EduCourse、EduCourseDescription→同步到数据库表edu_course、edu_course_description
controller层(EduCourseController)
//修改课程信息
@PostMapping("updateCourseInfo")
public R updateCourseInfo(@RequestBody CourseInfoVo courseInfoVo) {
courseService.updateCourseInfo(courseInfoVo);
return R.ok();
}
service层(EduCourseService)
//修改课程信息
void updateCourseInfo(CourseInfoVo courseInfoVo);
service层(EduCourseServiceImpl)
4.9.5步骤二:编辑课程大纲
①课程大纲列表显示:从数据库读取章节、小节信息,存放在集合中传到前端树形结构显示
建立数据库表章节表edu_chapter、小节表edu_video
代码生成器自动生成代码,包含两个实体类(PO)EduChapter、EduVideo,与数据库对应
创建两个VO类ChapterVo、VideoVo供前端调用进行树形显示
编写controller层代码
编写service层代码
②章节添加、修改、删除功能
package com.atguigu.eduservice.controller;
import com.atguigu.commonutils.R;
import com.atguigu.eduservice.entity.EduChapter;
import com.atguigu.eduservice.entity.chapter.ChapterVo;
import com.atguigu.eduservice.service.EduChapterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
*
* 课程 前端控制器
*
*
* @author testjava
* @since 2020-03-02
*/
@RestController
@RequestMapping("/eduservice/chapter")
@CrossOrigin
public class EduChapterController {
@Autowired
private EduChapterService chapterService;
//课程大纲列表,根据课程id进行查询
@GetMapping("getChapterVideo/{courseId}")
public R getChapterVideo(@PathVariable String courseId) {
List<ChapterVo> list = chapterService.getChapterVideoByCourseId(courseId);
return R.ok().data("allChapterVideo",list);
}
//添加章节
@PostMapping("addChapter")
public R addChapter(@RequestBody EduChapter eduChapter) {
chapterService.save(eduChapter);
return R.ok();
}
//根据章节id查询
@GetMapping("getChapterInfo/{chapterId}")
public R getChapterInfo(@PathVariable String chapterId) {
EduChapter eduChapter = chapterService.getById(chapterId);
return R.ok().data("chapter",eduChapter);
}
//修改章节
@PostMapping("updateChapter")
public R updateChapter(@RequestBody EduChapter eduChapter) {
chapterService.updateById(eduChapter);
return R.ok();
}
//删除的方法
@DeleteMapping("{chapterId}")
public R deleteChapter(@PathVariable String chapterId) {
boolean flag = chapterService.deleteChapter(chapterId);
if(flag) {
return R.ok();
} else {
return R.error();
}
}
}
package com.atguigu.eduservice.service;
import com.atguigu.eduservice.entity.EduChapter;
import com.atguigu.eduservice.entity.chapter.ChapterVo;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
*
* 课程 服务类
*
*
* @author testjava
* @since 2020-03-02
*/
public interface EduChapterService extends IService<EduChapter> {
//课程大纲列表,根据课程id进行查询
List<ChapterVo> getChapterVideoByCourseId(String courseId);
//删除章节的方法
boolean deleteChapter(String chapterId);
}
package com.atguigu.eduservice.service.impl;
import com.atguigu.eduservice.entity.EduChapter;
import com.atguigu.eduservice.entity.EduVideo;
import com.atguigu.eduservice.entity.chapter.ChapterVo;
import com.atguigu.eduservice.entity.chapter.VideoVo;
import com.atguigu.eduservice.mapper.EduChapterMapper;
import com.atguigu.eduservice.service.EduChapterService;
import com.atguigu.eduservice.service.EduVideoService;
import com.atguigu.servicebase.exceptionhandler.GuliException;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
*
* 课程 服务实现类
*
*
* @author testjava
* @since 2020-03-02
*/
@Service
public class EduChapterServiceImpl extends ServiceImpl<EduChapterMapper, EduChapter> implements EduChapterService {
@Autowired
private EduVideoService videoService;//注入小节service
//课程大纲列表,根据课程id进行查询
@Override
public List<ChapterVo> getChapterVideoByCourseId(String courseId) {
//1 根据课程id查询课程里面所有的章节
QueryWrapper<EduChapter> wrapperChapter = new QueryWrapper<>();
wrapperChapter.eq("course_id",courseId);
List<EduChapter> eduChapterList = baseMapper.selectList(wrapperChapter);
//2 根据课程id查询课程里面所有的小节
QueryWrapper<EduVideo> wrapperVideo = new QueryWrapper<>();
wrapperVideo.eq("course_id",courseId);
List<EduVideo> eduVideoList = videoService.list(wrapperVideo);
//创建list集合,用于最终封装数据
List<ChapterVo> finalList = new ArrayList<>();
//3 遍历查询章节list集合进行封装
//遍历查询章节list集合
for (int i = 0; i < eduChapterList.size(); i++) {
//每个章节
EduChapter eduChapter = eduChapterList.get(i);
//eduChapter对象值复制到ChapterVo里面
ChapterVo chapterVo = new ChapterVo();
BeanUtils.copyProperties(eduChapter,chapterVo);
//把chapterVo放到最终list集合
finalList.add(chapterVo);
//创建集合,用于封装章节的小节
List<VideoVo> videoList = new ArrayList<>();
//4 遍历查询小节list集合,进行封装
for (int m = 0; m < eduVideoList.size(); m++) {
//得到每个小节
EduVideo eduVideo = eduVideoList.get(m);
//判断:小节里面chapterid和章节里面id是否一样
if(eduVideo.getChapterId().equals(eduChapter.getId())) {
//进行封装
VideoVo videoVo = new VideoVo();
BeanUtils.copyProperties(eduVideo,videoVo);
//放到小节封装集合
videoList.add(videoVo);
}
}
//把封装之后小节list集合,放到章节对象里面
chapterVo.setChildren(videoList);
}
return finalList;
}
删除章节的方法
@Override
public boolean deleteChapter(String chapterId) {
//根据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);
//成功 1>0 0>0
return result>0;
}
}
}
③小节添加、修改、删除功能:同上
4.9.6步骤三:课程最终发布
①根据id查询课程发布信息(收集)
方式一:业务层组装多个表多次的查询结果
方式二:数据访问层进行关联查询(我们选择这个)
测试:报告异常
问题分析:dao层编译后只有class文件,没有mapper.xml,这个问题是由maven加载机制造成的(maven加载的时候,只会把java文件夹里面.java类型文件进行编译,如果其他类型文件,不会加载)
解决方案:有三种方式,我们选择第三种
首先在pom.xml中配置如下节点:
然后在配置文件中添加如下配置:
②根据id发布课程(发布)
只需要把表edu_course里的课程状态status改为已发布Normal
①课程列表查询(分页查询&条件查询)
定义搜索对象CourseQuery用于封装查询条件
定义controller层
定义service方法
void pageQuery(Page<Course> pageParam, CourseQuery courseQuery);
service实现类
②删除课程:需要按照从内向外删除(把视频、小节、章节、描述、课程都删除)
controller层
//删除课程
@DeleteMapping("{courseId}")
public R deleteCourse(@PathVariable String courseId) {
courseService.removeCourse(courseId);
return R.ok();
}
service层
//删除课程
void removeCourse(String courseId);
serviceImpl
//删除课程
@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,"删除失败");
}
}
4.11.1阿里云视频点播简介
①定义
视频点播是集音视频采集、编辑、上传、自动化转码处理、媒体资源管理、分发加速于一体的一站式音视频点播解决方案。
②优点
视频点播的优点:窄带高清(节省流量)、短视频解决方案、智能视频AI、内容安全保障(加密防盗)
③应用场景
音视频网站、短视频、直播转点播、在线教育(为在线教育客户提供简单易用、安全可靠的视频点播服务,可以通过控制台/API等多种方式上传教学视频,强大的转码能力保证视频可以快速发布,覆盖全网的加速节点保证学生观看的流畅度。防盗链、视频加密等版权保护方案保护教学内容不被窃取)、视频生产制作、内容审核;
④功能介绍
⑤使用视频点播实现实现音视频上传、存储、处理、播放的整体流程如下:
⑥视频点播服务的基本使用
设置转码格式
分类管理
上传视频文件
配置域名
在控制台查看视频
获取web播放器代码
⑦SDK与API的关系
SDK相当于一个工具类,内部包含API(相当于方法)
4.11.2阿里云视频点播测试案例
实现功能:获取视频播放凭证、视频播放地址、上传视频到阿里云视频点播服务器三个功能
首先在service下创建模块service_vod,引入相关依赖pom.xml
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-vod</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-sdk-vod-upload</artifactId>
</dependency>
然后在service_vod/src/test/java/com.atguigu.vodtest中创建初始化类InitObject.java,实现初始化操作(静态方法initVodClient)
public class InitObject {
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;
}
}
最后在com.atguigu.vodtest包下创建TestVod测试类,实现视频播放凭证、视频播放地址、上传视频到阿里云视频点播服务器功能
package com.atguigu.vodtest;
import com.aliyun.vod.upload.impl.UploadVideoImpl;
import com.aliyun.vod.upload.req.UploadVideoRequest;
import com.aliyun.vod.upload.resp.UploadVideoResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.vod.model.v20170321.GetPlayInfoRequest;
import com.aliyuncs.vod.model.v20170321.GetPlayInfoResponse;
import com.aliyuncs.vod.model.v20170321.GetVideoPlayAuthRequest;
import com.aliyuncs.vod.model.v20170321.GetVideoPlayAuthResponse;
import java.util.List;
public class TestVod {
public static void main(String[] args) {
String accessKeyId = "LTAI4FvvVEWiTJ3GNJJqJnk7";
String accessKeySecret = "9st82dv7EvFk9mTjYO1XXbM632fRbG";
String title = "6 - What If I Want to Move Faster - upload by sdk"; //上传之后文件名称
String fileName = "F:/6 - What If I Want to Move Faster.mp4"; //本地文件路径和名称
//上传视频的方法
testUploadVideo(accessKeyId, accessKeySecret, title, fileName);
}
//3 上传视频:关键是调用UploadVideoImpl实现类的uploadVideo方法
private static void testUploadVideo(String accessKeyId, String accessKeySecret, String title, String fileName) {
//3.1创建UploadVideoRequest对象
UploadVideoRequest request = new UploadVideoRequest(accessKeyId, accessKeySecret, title, fileName);
/* 可指定分片上传时每个分片的大小,默认为2M字节 */
request.setPartSize(2 * 1024 * 1024L);
/* 可指定分片上传时的并发线程数,默认为1,(注:该配置会占用服务器CPU资源,需根据服务器情况指定)*/
request.setTaskNum(1);
//3.2创建UploadVideoImpl对象,并调用内部的uploadvideo方法获得响应对象response
UploadVideoImpl uploader = new UploadVideoImpl();
UploadVideoResponse response = uploader.uploadVideo(request);
//3.3判断上传是否成功
if (response.isSuccess()) {
System.out.print("VideoId=" + response.getVideoId() + "\n");
} else {
/* 如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因 */
System.out.print("VideoId=" + response.getVideoId() + "\n");
System.out.print("ErrorCode=" + response.getCode() + "\n");
System.out.print("ErrorMessage=" + response.getMessage() + "\n");
}
}
//1 根据视频iD获取视频播放凭证:关键是调用了响应对象的相关方法response.getPlayAuth()
public static void getPlayAuth() throws Exception{
//1.1创建初始化对象
DefaultAcsClient client = InitObject.initVodClient("LTAI4FvvVEWiTJ3GNJJqJnk7", "9st82dv7EvFk9mTjYO1XXbM632fRbG");
//1.2创建获取视频凭证request和response
GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();//获取请求对象
GetVideoPlayAuthResponse response = new GetVideoPlayAuthResponse();//获取响应对象
//1.3向request对象里面设置视频id
request.setVideoId("474be24d43ad4f76af344b9f4daaabd1");//设置请求参数
//1.4调用初始化对象里面的方法,传递request,获取数据
response = client.getAcsResponse(request);//根据请求对象获取响应对象
//1.5输出请求结果
System.out.println("playAuth:"+response.getPlayAuth());//调用响应对象的getPlayAuth方法获取视频播放凭证
}
//2 根据视频iD获取视频播放地址:关键是调用了playInfo.getPlayURL()方法获取URL访问地址
public static void getPlayUrl() throws Exception{
//2.1创建初始化对象
DefaultAcsClient client = InitObject.initVodClient("LTAI4FvvVEWiTJ3GNJJqJnk7", "9st82dv7EvFk9mTjYO1XXbM632fRbG");
//2.2创建获取视频地址request和response
GetPlayInfoRequest request = new GetPlayInfoRequest();
GetPlayInfoResponse response = new GetPlayInfoResponse();
//2.3向request对象里面设置视频id
request.setVideoId("474be24d43ad4f76af344b9f4daaabd1");
//2.4调用初始化对象里面的方法,传递request,获取数据
response = client.getAcsResponse(request);
//2.5输出请求结果
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");
}
}
4.11.3添加小节中实现上传视频功能
第一步、引入依赖pom.xml
第二步、创建配置文件application.properties
# 服务端口
server.port=8003
# 服务名
spring.application.name=service-vod
# 环境设置:dev、test、prod
spring.profiles.active=dev
#阿里云 vod
#不同的服务器,地址不同
aliyun.vod.file.keyid=LTAI4FvvVEWiTJ3GNJJqJnk7
aliyun.vod.file.keysecret=9st82dv7EvFk9mTjYO1XXbM632fRbG
# 最大上传单个文件大小:默认1M
spring.servlet.multipart.max-file-size=1024MB
# 最大置总上传的数据大小 :默认10M
spring.servlet.multipart.max-request-size=1024MB
第三步、创建常量类ConstantVodUtils
package com.atguigu.vod.Utils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ConstantVodUtils implements InitializingBean {
@Value("${aliyun.vod.file.keyid}")
private String keyid;
@Value("${aliyun.vod.file.keysecret}")
private String keysecret;
public static String ACCESS_KEY_SECRET;
public static String ACCESS_KEY_ID;
@Override
public void afterPropertiesSet() throws Exception {
ACCESS_KEY_ID = keyid;
ACCESS_KEY_SECRET = keysecret;
}
}
第四步、controller层
package com.atguigu.vod.controller;
import com.atguigu.commonutils.R;
import com.atguigu.vod.service.VodService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/eduvod/video")
@CrossOrigin
public class VodController {
@Autowired
private VodService vodService;
//上传视频到阿里云
@PostMapping("uploadAlyiVideo")
public R uploadAlyiVideo(MultipartFile file) {
//返回上传视频id
String videoId = vodService.uploadVideoAly(file);
return R.ok().data("videoId",videoId);
}
}
第五步、创建service层
package com.atguigu.vod.service;
import org.springframework.web.multipart.MultipartFile;
public interface VodService {
//上传视频到阿里云
String uploadVideoAly(MultipartFile file);
}
package com.atguigu.vod.service.impl;
import com.aliyun.vod.upload.impl.UploadVideoImpl;
import com.aliyun.vod.upload.req.UploadStreamRequest;
import com.aliyun.vod.upload.resp.UploadStreamResponse;
import com.atguigu.vod.Utils.ConstantVodUtils;
import com.atguigu.vod.service.VodService;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
@Service
public class VodServiceImpl implements VodService {
@Override
public String uploadVideoAly(MultipartFile file) {
try {
//accessKeyId, accessKeySecret
//fileName:上传文件原始名称
// 01.03.09.mp4
String fileName = file.getOriginalFilename();
//title:上传之后显示名称
String title = fileName.substring(0, fileName.lastIndexOf("."));
//inputStream:上传文件输入流
InputStream inputStream = file.getInputStream();
//1.创建UploadStreamRequest对象
UploadStreamRequest request = new UploadStreamRequest(ConstantVodUtils.ACCESS_KEY_ID,ConstantVodUtils.ACCESS_KEY_SECRET, title, fileName, inputStream);
//2.创建UploadVideoImpl对象,并调用内部的uploadStream方法实现上传,获得响应对象response
UploadVideoImpl uploader = new UploadVideoImpl();
UploadStreamResponse response = uploader.uploadStream(request);
//3.判断上传是否成功
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;
}
}
}
4.12.1统计某一天的用户注册数
在service_statistics服务中远程调用service_ucenter,对表ucenter_member进行分组查询,统计每天的会员注册数,封装到StatisticsDaily对象添加至表statistics_daily,并使用图表显示出来
①建立数据库表statistics_daily(对应实体类StatisticsDaily)
②新建service_statistics服务,利用代码生成器生成相关代码
③service_statistics服务中的controller层
@RestController
@RequestMapping("/staservice/sta")
@CrossOrigin
public class StatisticsDailyController {
@Autowired
private StatisticsDailyService staService;
//统计某一天注册人数,生成统计数据
@PostMapping("registerCount/{day}")
public R registerCount(@PathVariable String day) {
staService.registerCount(day);
return R.ok();
}
}
④service_statistics服务中serviceImpl层
@Service
public class StatisticsDailyServiceImpl extends ServiceImpl<StatisticsDailyMapper, StatisticsDaily> implements StatisticsDailyService {
@Autowired
private UcenterClient ucenterClient;
@Override
public void registerCount(String day) {
//1.添加记录之前删除表相同日期的数据
QueryWrapper<StatisticsDaily> wrapper = new QueryWrapper<>();
wrapper.eq("date_calculated",day);
baseMapper.delete(wrapper);
//2.远程调用得到某一天注册人数
R registerR = ucenterClient.countRegister(day);
Integer countRegister = (Integer)registerR.getData().get("countRegister");
//3.把获取数据添加数据库,统计分析表里面
StatisticsDaily sta = new StatisticsDaily();
sta.setRegisterNum(countRegister); //注册人数
sta.setDateCalculated(day);//统计日期
sta.setVideoViewNum(RandomUtils.nextInt(100,200));
sta.setLoginNum(RandomUtils.nextInt(100,200));
sta.setCourseNum(RandomUtils.nextInt(100,200));
baseMapper.insert(sta);
}
}
⑤service_statistics服务中创建UcenterClient接口,实现远程调用service_ucenter服务
@Component
@FeignClient("service-ucenter")
public interface UcenterClient {
//查询某一天注册人数
@GetMapping("/educenter/member/countRegister/{day}")
public R countRegister(@PathVariable("day") String day);
}
⑥service_ucenter服务中UcenterMemberController内实现countRegister()方法
//查询某一天注册人数
@GetMapping("countRegister/{day}")
public R countRegister(@PathVariable String day) {
Integer count = memberService.countRegisterDay(day);
return R.ok().data("countRegister",count);
}
⑦service_ucenter服务中UcenterMemberServiceImpl内实现countRegisterDay()方法
//查询某一天注册人数
@Override
public Integer countRegisterDay(String day) {
return baseMapper.countRegisterDay(day);
}
⑧service_ucenter服务中UcenterMemberMapper内编写SQL查询语句
<!--查询某一天注册人数-->
<select id="countRegisterDay" resultType="java.lang.Integer">
SELECT COUNT(*) FROM ucenter_member uc
WHERE DATE(uc.gmt_create)=#{day}
</select>
4.12.2设置定时任务:每天凌晨1点查询前一天的用户注册数
①在启动类添加注解@EnableScheduling
②在service_statistics服务下创建schedule模块,内部创建定时任务类ScheduledTask,内部使用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)));
}
}
4.12.3图表显示
①前端整合echarts
ECharts是百度的一个项目,后来百度把Echarts捐给apache,用于图表展示。
常规图:折线图、柱状图、散点图、饼图;
统计图:盒形图;
地理数据可视化:地图、热力图、线图;
关系数据可视化:关系图、treemap、旭日图;
多维数据可视化:平行坐标;
BI:漏斗图、仪表盘
②编写后端接口:获取【开始日期-结束日期】区间的统计数据
@RestController
@RequestMapping("/staservice/sta")
@CrossOrigin
public class StatisticsDailyController {
@Autowired
private StatisticsDailyService staService;
//图表显示,返回两部分数据,日期json数组,数量json数组
@GetMapping("showData/{type}/{begin}/{end}")
public R showData(@PathVariable String type,@PathVariable String begin,
@PathVariable String end) {
Map<String,Object> map = staService.getShowData(type,begin,end);
return R.ok().data(map);
}
}
@Service
public class StatisticsDailyServiceImpl extends ServiceImpl<StatisticsDailyMapper, StatisticsDaily> implements StatisticsDailyService {
@Autowired
private UcenterClient ucenterClient;
//图表显示,返回两部分数据,日期json数组,数量json数组
@Override
public Map<String, Object> getShowData(String type, String begin, String end) {
//根据条件查询对应数据
QueryWrapper<StatisticsDaily> wrapper = new QueryWrapper<>();
wrapper.between("date_calculated",begin,end);
wrapper.select("date_calculated",type);
List<StatisticsDaily> staList = baseMapper.selectList(wrapper);
//因为返回有两部分数据:日期 和 日期对应数量
//前端要求数组json结构,对应后端java代码是list集合
//创建两个list集合,一个日期list,一个数量list
List<String> date_calculatedList = new ArrayList<>();
List<Integer> numDataList = new ArrayList<>();
//遍历查询所有数据list集合,进行封装
for (int i = 0; i < staList.size(); i++) {
StatisticsDaily daily = staList.get(i);
//封装日期list集合
date_calculatedList.add(daily.getDateCalculated());
//封装对应数量
switch (type) {
case "login_num":
numDataList.add(daily.getLoginNum());
break;
case "register_num":
numDataList.add(daily.getRegisterNum());
break;
case "video_view_num":
numDataList.add(daily.getVideoViewNum());
break;
case "course_num":
numDataList.add(daily.getCourseNum());
break;
default:
break;
}
}
//把封装之后两个list集合放到map集合,进行返回
Map<String, Object> map = new HashMap<>();
map.put("date_calculatedList",date_calculatedList);
map.put("numDataList",numDataList);
return map;
}
}
4.12.4canal数据同步
①应用场景
在前面的统计分析中,我们采取了服务调用获取统计数据,但是这种方式耦合度高,效率低;因此我们采取另一种方式(canal同步),实时同步数据库表,就可以本地统计,效率更高,耦合度更低。
canal是阿里巴巴旗下的一款开源项目,纯Java开发,canal的原理是基于myql binlog技术。
②安装配置canal
③后端创建canal_clientedu服务模块
引入相关依赖pom.xml
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
</dependency>
创建配置文件application.properties
# 服务端口
server.port=10000
# 服务名
spring.application.name=canal-client
# 环境设置:dev、test、prod
spring.profiles.active=dev
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
编写canal客户端类
@Component
public class CanalClient {
//sql队列
private Queue<String> SQL_QUEUE = new ConcurrentLinkedQueue<>();
@Resource
private DataSource dataSource;
/**
* canal入库方法
*/
public void run() {
CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("192.168.44.132",
11111), "example", "", "");
int batchSize = 1000;
try {
connector.connect();
connector.subscribe(".*\\..*");
connector.rollback();
try {
while (true) {
//尝试从master那边拉去数据batchSize条记录,有多少取多少
Message message = connector.getWithoutAck(batchSize);
long batchId = message.getId();
int size = message.getEntries().size();
if (batchId == -1 || size == 0) {
Thread.sleep(1000);
} else {
dataHandle(message.getEntries());
}
connector.ack(batchId);
//当队列里面堆积的sql大于一定数值的时候就模拟执行
if (SQL_QUEUE.size() >= 1) {
executeQueueSql();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
} finally {
connector.disconnect();
}
}
/**
* 模拟执行队列里面的sql语句
*/
public void executeQueueSql() {
int size = SQL_QUEUE.size();
for (int i = 0; i < size; i++) {
String sql = SQL_QUEUE.poll();
System.out.println("[sql]----> " + sql);
this.execute(sql.toString());
}
}
/**
* 数据处理
*
* @param entrys
*/
private void dataHandle(List<Entry> entrys) throws InvalidProtocolBufferException {
for (Entry entry : entrys) {
if (EntryType.ROWDATA == entry.getEntryType()) {
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
EventType eventType = rowChange.getEventType();
if (eventType == EventType.DELETE) {
saveDeleteSql(entry);
} else if (eventType == EventType.UPDATE) {
saveUpdateSql(entry);
} else if (eventType == EventType.INSERT) {
saveInsertSql(entry);
}
}
}
}
/**
* 保存更新语句
*
* @param entry
*/
private void saveUpdateSql(Entry entry) {
try {
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
List<RowData> rowDatasList = rowChange.getRowDatasList();
for (RowData rowData : rowDatasList) {
List<Column> newColumnList = rowData.getAfterColumnsList();
StringBuffer sql = new StringBuffer("update " + entry.getHeader().getTableName() + " set ");
for (int i = 0; i < newColumnList.size(); i++) {
sql.append(" " + newColumnList.get(i).getName()
+ " = '" + newColumnList.get(i).getValue() + "'");
if (i != newColumnList.size() - 1) {
sql.append(",");
}
}
sql.append(" where ");
List<Column> oldColumnList = rowData.getBeforeColumnsList();
for (Column column : oldColumnList) {
if (column.getIsKey()) {
//暂时只支持单一主键
sql.append(column.getName() + "=" + column.getValue());
break;
}
}
SQL_QUEUE.add(sql.toString());
}
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
}
/**
* 保存删除语句
*
* @param entry
*/
private void saveDeleteSql(Entry entry) {
try {
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
List<RowData> rowDatasList = rowChange.getRowDatasList();
for (RowData rowData : rowDatasList) {
List<Column> columnList = rowData.getBeforeColumnsList();
StringBuffer sql = new StringBuffer("delete from " + entry.getHeader().getTableName() + " where ");
for (Column column : columnList) {
if (column.getIsKey()) {
//暂时只支持单一主键
sql.append(column.getName() + "=" + column.getValue());
break;
}
}
SQL_QUEUE.add(sql.toString());
}
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
}
/**
* 保存插入语句
*
* @param entry
*/
private void saveInsertSql(Entry entry) {
try {
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
List<RowData> rowDatasList = rowChange.getRowDatasList();
for (RowData rowData : rowDatasList) {
List<Column> columnList = rowData.getAfterColumnsList();
StringBuffer sql = new StringBuffer("insert into " + entry.getHeader().getTableName() + " (");
for (int i = 0; i < columnList.size(); i++) {
sql.append(columnList.get(i).getName());
if (i != columnList.size() - 1) {
sql.append(",");
}
}
sql.append(") VALUES (");
for (int i = 0; i < columnList.size(); i++) {
sql.append("'" + columnList.get(i).getValue() + "'");
if (i != columnList.size() - 1) {
sql.append(",");
}
}
sql.append(")");
SQL_QUEUE.add(sql.toString());
}
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
}
/**
* 入库
* @param sql
*/
public void execute(String sql) {
Connection con = null;
try {
if(null == sql) return;
con = dataSource.getConnection();
QueryRunner qr = new QueryRunner();
int row = qr.execute(con, sql);
System.out.println("update: "+ row);
} catch (SQLException e) {
e.printStackTrace();
} finally {
DbUtils.closeQuietly(con);
}
}
}
创建启动类
@SpringBootApplication
public class CanalApplication implements CommandLineRunner {
@Resource
private CanalClient canalClient;
public static void main(String[] args) {
SpringApplication.run(CanalApplication.class, args);
}
@Override
public void run(String... strings) throws Exception {
//项目启动,执行canal客户端监听
canalClient.run();
}
}
4.13.1需求分析、建数据库表
表acl_permission(实体类Permission)
表acl_role(实体类Role)
表acl_user(实体类User)
表acl_role_permission(实体类RolePermission)
表acl_user_role(实体类UserRole)
4.13.2service模块下创建“权限管理”服务service_acl
使用代码生成器生成相关代码
引入相关依赖
<dependency>
<groupId>com.atguigugroupId>
<artifactId>spring_securityartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
创建配置文件
# 服务端口
server.port=8009
# 服务名
spring.application.name=service-acl
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
spring.redis.host=192.168.44.132
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
#最小空闲
#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/atguigu/aclservice/mapper/xml/*.xml
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
①controller层:PermissionController
@RestController
@RequestMapping("/admin/acl/permission")
//@CrossOrigin
public class PermissionController {
@Autowired
private PermissionService permissionService;
//获取全部菜单
@ApiOperation(value = "查询所有菜单")
@GetMapping
public R indexAllPermission() {
List<Permission> list = permissionService.queryAllMenuGuli();
return R.ok().data("children",list);
}
@ApiOperation(value = "递归删除菜单")
@DeleteMapping("remove/{id}")
public R remove(@PathVariable String id) {
permissionService.removeChildByIdGuli(id);
return R.ok();
}
@ApiOperation(value = "给角色分配权限")
@PostMapping("/doAssign")
public R doAssign(String roleId,String[] permissionId) {
permissionService.saveRolePermissionRealtionShipGuli(roleId,permissionId);
return R.ok();
}
@ApiOperation(value = "根据角色获取菜单")
@GetMapping("toAssign/{roleId}")
public R toAssign(@PathVariable String roleId) {
List<Permission> list = permissionService.selectAllMenu(roleId);
return R.ok().data("children", list);
}
@ApiOperation(value = "新增菜单")
@PostMapping("save")
public R save(@RequestBody Permission permission) {
permissionService.save(permission);
return R.ok();
}
@ApiOperation(value = "修改菜单")
@PutMapping("update")
public R updateById(@RequestBody Permission permission) {
permissionService.updateById(permission);
return R.ok();
}
}
serviceImpl层:PermissionServiceImpl
@Service
public class PermissionServiceImpl extends ServiceImpl<PermissionMapper, Permission> implements PermissionService {
@Autowired
private RolePermissionService rolePermissionService;
@Autowired
private UserService userService;
//获取全部菜单
@Override
public List<Permission> queryAllMenu() {
QueryWrapper<Permission> wrapper = new QueryWrapper<>();
wrapper.orderByDesc("id");
List<Permission> permissionList = baseMapper.selectList(wrapper);//无序集合
List<Permission> result = bulid(permissionList);//将无序集合构造成树形结构集合
return result;
}
//根据角色获取菜单
@Override
public List<Permission> selectAllMenu(String roleId) {
List<Permission> allPermissionList = baseMapper.selectList(new QueryWrapper<Permission>().orderByAsc("CAST(id AS SIGNED)"));
//根据角色id获取角色权限
List<RolePermission> rolePermissionList = rolePermissionService.list(new QueryWrapper<RolePermission>().eq("role_id",roleId));
//转换给角色id与角色权限对应Map对象
// List permissionIdList = rolePermissionList.stream().map(e -> e.getPermissionId()).collect(Collectors.toList());
// allPermissionList.forEach(permission -> {
// if(permissionIdList.contains(permission.getId())) {
// permission.setSelect(true);
// } else {
// permission.setSelect(false);
// }
// });
for (int i = 0; i < allPermissionList.size(); i++) {
Permission permission = allPermissionList.get(i);
for (int m = 0; m < rolePermissionList.size(); m++) {
RolePermission rolePermission = rolePermissionList.get(m);
if(rolePermission.getPermissionId().equals(permission.getId())) {
permission.setSelect(true);
}
}
}
List<Permission> permissionList = bulid(allPermissionList);
return permissionList;
}
//给角色分配权限
@Override
public void saveRolePermissionRealtionShip(String roleId, String[] permissionIds) {
rolePermissionService.remove(new QueryWrapper<RolePermission>().eq("role_id", roleId));
List<RolePermission> rolePermissionList = new ArrayList<>();
for(String permissionId : permissionIds) {
if(StringUtils.isEmpty(permissionId)) continue;
RolePermission rolePermission = new RolePermission();
rolePermission.setRoleId(roleId);
rolePermission.setPermissionId(permissionId);
rolePermissionList.add(rolePermission);
}
rolePermissionService.saveBatch(rolePermissionList);
}
//递归删除菜单
@Override
public void removeChildById(String id) {
List<String> idList = new ArrayList<>();
this.selectChildListById(id, idList);
idList.add(id);
baseMapper.deleteBatchIds(idList);
}
//根据用户id获取用户菜单
@Override
public List<String> selectPermissionValueByUserId(String id) {
List<String> selectPermissionValueList = null;
if(this.isSysAdmin(id)) {
//如果是系统管理员,获取所有权限
selectPermissionValueList = baseMapper.selectAllPermissionValue();
} else {
selectPermissionValueList = baseMapper.selectPermissionValueByUserId(id);
}
return selectPermissionValueList;
}
@Override
public List<JSONObject> selectPermissionByUserId(String userId) {
List<Permission> selectPermissionList = null;
if(this.isSysAdmin(userId)) {
//如果是超级管理员,获取所有菜单
selectPermissionList = baseMapper.selectList(null);
} else {
selectPermissionList = baseMapper.selectPermissionByUserId(userId);
}
List<Permission> permissionList = PermissionHelper.bulid(selectPermissionList);
List<JSONObject> result = MemuHelper.bulid(permissionList);
return result;
}
/**
* 判断用户是否系统管理员
* @param userId
* @return
*/
private boolean isSysAdmin(String userId) {
User user = userService.getById(userId);
if(null != user && "admin".equals(user.getUsername())) {
return true;
}
return false;
}
/**
* 递归获取子节点
* @param id
* @param idList
*/
private void selectChildListById(String id, List<String> idList) {
List<Permission> childList = baseMapper.selectList(new QueryWrapper<Permission>().eq("pid", id).select("id"));
childList.stream().forEach(item -> {
idList.add(item.getId());
this.selectChildListById(item.getId(), idList);
});
}
/**
* 使用递归方法建菜单【树形结构】
* @param treeNodes
* @return
*/
private static List<Permission> bulid(List<Permission> treeNodes){
List<Permission> trees = new ArrayList<>();
for(Permission treeNode : treeNodes){
//只需要取出父节点为0的根节点Permission即可
if(treeNode.getPid().equals("0")){
treeNode.setLevel(1);
trees.add(findChildren(treeNode , treeNodes));//将根节点(pid=0)treeNode加入集合,并且继续递归查找其子节点
}
}
return trees;
}
/**
* 递归查找子节点:从集合treeNodes递归查找treeNode的子节点,并返回Permission对象treeNode
* @param treeNodes
* @return
*/
private static Permission findChildren(Permission treeNode , List<Permission> treeNodes){
treeNode.setChildren(new ArrayList<Permission>());
for(Permission it : treeNodes){
//如果该节点是子节点
if(it.getPid().equals(treeNode.getId())){
//设置该子节点it的level
it.setLevel(treeNode.getLevel() + 1);
if(treeNode.getChildren() == null){
treeNode.setChildren(new ArrayList<>());
}
//添加子节点it,同时递归查找it的子节点
treeNode.getChildren().add(findChildren(it , treeNodes));
}
}
return treeNode;
}
//========================递归查询所有菜单================================================
//获取全部菜单
@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的子节点(即pid==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);
}
}
4.13.4实现“角色管理”后端接口
controller层:RoleController
@RestController
@RequestMapping("/admin/acl/role")
//@CrossOrigin
public class RoleController {
@Autowired
private RoleService roleService;
@ApiOperation(value = "获取角色分页列表")
@GetMapping("{page}/{limit}")
public R index(
@ApiParam(name = "page", value = "当前页码", required = true)
@PathVariable Long page,
@ApiParam(name = "limit", value = "每页记录数", required = true)
@PathVariable Long limit, Role role) {
Page<Role> pageParam = new Page<>(page, limit);
QueryWrapper<Role> wrapper = new QueryWrapper<>();
if(!StringUtils.isEmpty(role.getRoleName())) {
wrapper.like("role_name",role.getRoleName());
}
roleService.page(pageParam,wrapper);
return R.ok().data("items", pageParam.getRecords()).data("total", pageParam.getTotal());
}
@ApiOperation(value = "获取角色")
@GetMapping("get/{id}")
public R get(@PathVariable String id) {
Role role = roleService.getById(id);
return R.ok().data("item", role);
}
@ApiOperation(value = "新增角色")
@PostMapping("save")
public R save(@RequestBody Role role) {
roleService.save(role);
return R.ok();
}
@ApiOperation(value = "修改角色")
@PutMapping("update")
public R updateById(@RequestBody Role role) {
roleService.updateById(role);
return R.ok();
}
@ApiOperation(value = "删除角色")
@DeleteMapping("remove/{id}")
public R remove(@PathVariable String id) {
roleService.removeById(id);
return R.ok();
}
@ApiOperation(value = "根据id列表删除角色")
@DeleteMapping("batchRemove")
public R batchRemove(@RequestBody List<String> idList) {
roleService.removeByIds(idList);
return R.ok();
}
}
service层:RoleServiceImpl
@Service
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
@Autowired
private UserRoleService userRoleService;
//根据用户获取角色数据
@Override
public Map<String, Object> findRoleByUserId(String userId) {
//查询所有的角色
List<Role> allRolesList =baseMapper.selectList(null);
//根据用户id,查询用户拥有的角色id
List<UserRole> existUserRoleList = userRoleService.list(new QueryWrapper<UserRole>().eq("user_id", userId).select("role_id"));
List<String> existRoleList = existUserRoleList.stream().map(c->c.getRoleId()).collect(Collectors.toList());
//对角色进行分类
List<Role> assignRoles = new ArrayList<Role>();
for (Role role : allRolesList) {
//已分配
if(existRoleList.contains(role.getId())) {
assignRoles.add(role);
}
}
Map<String, Object> roleMap = new HashMap<>();
roleMap.put("assignRoles", assignRoles);
roleMap.put("allRolesList", allRolesList);
return roleMap;
}
//根据用户分配角色
@Override
public void saveUserRoleRealtionShip(String userId, String[] roleIds) {
userRoleService.remove(new QueryWrapper<UserRole>().eq("user_id", userId));
List<UserRole> userRoleList = new ArrayList<>();
for(String roleId : roleIds) {
if(StringUtils.isEmpty(roleId)) continue;
UserRole userRole = new UserRole();
userRole.setUserId(userId);
userRole.setRoleId(roleId);
userRoleList.add(userRole);
}
userRoleService.saveBatch(userRoleList);
}
@Override
public List<Role> selectRoleByUserId(String id) {
//根据用户id拥有的角色id
List<UserRole> userRoleList = userRoleService.list(new QueryWrapper<UserRole>().eq("user_id", id).select("role_id"));
List<String> roleIdList = userRoleList.stream().map(item -> item.getRoleId()).collect(Collectors.toList());
List<Role> roleList = new ArrayList<>();
if(roleIdList.size() > 0) {
roleList = baseMapper.selectBatchIds(roleIdList);
}
return roleList;
}
}
4.13.5实现“用户管理”后端接口
controller层:UserController
@RestController
@RequestMapping("/admin/acl/user")
//@CrossOrigin
public class UserController {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@ApiOperation(value = "获取管理用户分页列表")
@GetMapping("{page}/{limit}")
public R index(
@ApiParam(name = "page", value = "当前页码", required = true)
@PathVariable Long page,
@ApiParam(name = "limit", value = "每页记录数", required = true)
@PathVariable Long limit,
@ApiParam(name = "courseQuery", value = "查询对象", required = false)
User userQueryVo) {
Page<User> pageParam = new Page<>(page, limit);
QueryWrapper<User> wrapper = new QueryWrapper<>();
if(!StringUtils.isEmpty(userQueryVo.getUsername())) {
wrapper.like("username",userQueryVo.getUsername());
}
IPage<User> pageModel = userService.page(pageParam, wrapper);
return R.ok().data("items", pageModel.getRecords()).data("total", pageModel.getTotal());
}
@ApiOperation(value = "新增管理用户")
@PostMapping("save")
public R save(@RequestBody User user) {
user.setPassword(MD5.encrypt(user.getPassword()));
userService.save(user);
return R.ok();
}
@ApiOperation(value = "修改管理用户")
@PutMapping("update")
public R updateById(@RequestBody User user) {
userService.updateById(user);
return R.ok();
}
@ApiOperation(value = "删除管理用户")
@DeleteMapping("remove/{id}")
public R remove(@PathVariable String id) {
userService.removeById(id);
return R.ok();
}
@ApiOperation(value = "根据id列表删除管理用户")
@DeleteMapping("batchRemove")
public R batchRemove(@RequestBody List<String> idList) {
userService.removeByIds(idList);
return R.ok();
}
@ApiOperation(value = "根据用户获取角色数据")
@GetMapping("/toAssign/{userId}")
public R toAssign(@PathVariable String userId) {
Map<String, Object> roleMap = roleService.findRoleByUserId(userId);
return R.ok().data(roleMap);
}
@ApiOperation(value = "根据用户分配角色")
@PostMapping("/doAssign")
public R doAssign(@RequestParam String userId,@RequestParam String[] roleId) {
roleService.saveUserRoleRealtionShip(userId,roleId);
return R.ok();
}
}
4.14.1SpringSecurity框架介绍
①SpringSecurity是基于Spring框架开发的,用于解决Web应用安全性。Web应用安全性包括用户认证(Authentication)、用户授权(Authorization)两个部分。
②用户认证:即登录,一般需要提供手机号和密码,系统通过校验手机号+密码来完成认证过程;
用户授权:验证某个用户是否有权限执行某个操作;
③SpringSecurity其实就是使用了filter对多请求的路径进行过滤(类似于网关gateway),有两种实现方式:基于session、基于token
基于session:类似于传统登录方式,SpringSecurity会解析来自客户端的cookie,cookie内部含有sessionId,根据sessionId找到服务器存储的session信息,根据session信息就可以查看用户信息和用户权限;
【基于token】:解析出HTTP请求头中的token值,将其加入到springsecurity的权限信息中;
4.14.2SpringSecurity基于token实现认证与授权的原理
①算法原理
②为什么使用token,而不使用session?
token可以实现分布式微服务之间的单点登录;
session会增加客户端和服务端的内存压力;
③为什么使用redis?
redis在本项目中的使用有下列三种:
首页数据通过redis进行缓存(使用注解);
注册过程使用redis缓存短信验证码(使用RedisTemplate工具类);
springsecurity权限管理中使用redis缓存登录成功的用户权限;
4.14.3后端接口实现
在spring_security引入相关依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
在service_acl中引入spring_security依赖
<dependency>
<groupId>com.atguigugroupId>
<artifactId>spring_securityartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
②创建spring_security核心配置类:TokenWebSecurityConfig
/**
*
* Security配置类:进行配置设置、密码处理、配置那些请求不拦截
*
*
* @author qy
* @since 2019-11-18
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
private UserDetailsService userDetailsService;
private TokenManager tokenManager;
private DefaultPasswordEncoder defaultPasswordEncoder;
private RedisTemplate redisTemplate;
@Autowired
public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder,
TokenManager tokenManager, RedisTemplate redisTemplate) {
this.userDetailsService = userDetailsService;
this.defaultPasswordEncoder = defaultPasswordEncoder;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
/**
* 配置设置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new UnauthorizedEntryPoint())
.and().csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and().logout().logoutUrl("/admin/acl/index/logout")
.addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)).and()
.addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))
.addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic();
}
/**
* 密码处理
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
}
/**
* 配置哪些请求不拦截
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/**",
"/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**"
);
}
}
③创建认证授权相关的工具类
DefaultPasswordEncoder:密码处理的方法
/**
*
* t密码的处理方法类型
*
*
* @author qy
* @since 2019-11-08
*/
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
public DefaultPasswordEncoder() {
this(-1);
}
/**
* @param strength
* the log rounds to use, between 4 and 31
*/
public DefaultPasswordEncoder(int strength) {
}
public String encode(CharSequence rawPassword) {
return MD5.encrypt(rawPassword.toString());
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
}
}
TokenManager:token操作的工具类
/**
*
* token管理:根据用户名生成token、根据token获取用户信息
*
*
* @author qy
* @since 2019-11-08
*/
@Component
public class TokenManager {
private long tokenExpiration = 24*60*60*1000;
private String tokenSignKey = "123456";
//根据用户名生成token
public String createToken(String username) {
String token = Jwts.builder().setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
return token;
}
//根据token获取用户信息
public String getUserFromToken(String token) {
String user = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
return user;
}
//删除token
public void removeToken(String token) {
//jwttoken无需删除,客户端扔掉即可。
}
}
TokenLogoutHandler:退出工具类
/**
*
* 退出业务逻辑类:删除redis中当前用户的信息
*
*
* @author qy
* @since 2019-11-08
*/
public class TokenLogoutHandler implements LogoutHandler {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
//退出操作:删除redis中当前用户的信息
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String token = request.getHeader("token");
if (token != null) {
tokenManager.removeToken(token);
//清空当前用户缓存中的权限数据
String userName = tokenManager.getUserFromToken(token);
redisTemplate.delete(userName);
}
ResponseUtil.out(response, R.ok());
}
}
UnauthorizedEntryPoint:未授权统一处理
/**
*
* 未授权的统一处理方式:error
*
*
* @author qy
* @since 2019-11-08
*/
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}
④创建认证授权实体类
认证实体类User :
/**
*
* 用户实体类:用于用户登录/认证
*
*
* @author qy
* @since 2019-11-08
*/
@Data
@ApiModel(description = "用户实体类")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "微信openid")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "昵称")
private String nickName;
@ApiModelProperty(value = "用户头像")
private String salt;
@ApiModelProperty(value = "用户签名")
private String token;
}
授权实体类SecurityUser:
/**
*
* 安全认证用户详情信息:用于登录成功的用户的授权处理
*
*
* @author qy
* @since 2019-11-08
*/
@Data
@Slf4j
public class SecurityUser implements UserDetails {
//当前登录用户
private transient User currentUserInfo;
//当前权限
private List<String> permissionValueList;
public SecurityUser() {
}
public SecurityUser(User user) {
if (user != null) {
this.currentUserInfo = user;
}
}
//授权操作
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
⑤创建认证和授权的filter【这两个过滤器是springsecurity的核心】
认证过滤器:TokenLoginFilter
/**
*
* 登录过滤器:继承UsernamePasswordAuthenticationFilter
* 首先执行attemptAuthentication()方法:对用户名密码进行登录校验,并获取权限列表
* 如果登录成功:执行successfulAuthentication()方法:将(用户名,权限列表)存入redis,返回含有用户名信息的token
* 如果登录失败:执行unsuccessfulAuthentication()方法,return error
*
*
* @author qy
* @since 2019-11-08
*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.setPostOnly(false);
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
}
//获取输入的用户名+密码
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
try {
User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 登录成功:把(用户名,权限列表)存入redis,返回包含用户名的token
* @param req
* @param res
* @param chain
* @param auth
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException {
SecurityUser user = (SecurityUser) auth.getPrincipal();
String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());
ResponseUtil.out(res, R.ok().data("token", token));
}
/**
* 登录失败
* @param request
* @param response
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}
授权过滤器:TokenAuthenticationFilter
/**
*
* 授权过滤器:
*
*
* @author qy
* @since 2019-11-08
*/
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager,RedisTemplate redisTemplate) {
super(authManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
logger.info("================="+req.getRequestURI());
if(req.getRequestURI().indexOf("admin") == -1) {
chain.doFilter(req, res);
return;
}
UsernamePasswordAuthenticationToken authentication = null;
try {
authentication = getAuthentication(req);
} catch (Exception e) {
ResponseUtil.out(res, R.error());
}
if (authentication != null) {
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
ResponseUtil.out(res, R.error());
}
chain.doFilter(req, res);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
// 1.从header获取token
String token = request.getHeader("token");
//token不是空,说明登录成功
if (token != null && !"".equals(token.trim())) {
//2.根据token获取用户名userName
String userName = tokenManager.getUserFromToken(token);
//3.根据userName从redis中查找相应的权限信息
List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName);
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) continue;//权限值为空,说明没有权限
//4.给用户授权
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
if (!StringUtils.isEmpty(userName)) {
return new UsernamePasswordAuthenticationToken(userName, token, authorities);
}
return null;
}
return null;
}
}
⑥在service_acl服务中创建UserDetailsServiceImpl实现类
/**
*
* 自定义userDetailsService - 认证用户详情
* 首先验证登录用户是否合法,根据username查询用户信息User;
* 如果认证成功,根据用户id查询权限列表;
* 最后将用户信息和权限列表封装到SecurityUser对象并返回;
*
*
* @author qy
* @since 2019-11-08
*/
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;
/***
* 根据账号获取用户信息+权限信息
* @param username:
* @return: org.springframework.security.core.userdetails.UserDetails
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库中取出用户信息
User user = userService.selectByUsername(username);
// 判断用户是否存在
if (null == user){
//throw new UsernameNotFoundException("用户名不存在!");
}
// 返回UserDetails实现类
com.atguigu.serurity.entity.User curUser = new com.atguigu.serurity.entity.User();
BeanUtils.copyProperties(user,curUser);
List<String> authorities = permissionService.selectPermissionValueByUserId(user.getId());
SecurityUser securityUser = new SecurityUser(curUser);
securityUser.setPermissionValueList(authorities);
return securityUser;
}
}
5.1.1微服务是什么
微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。
5.1.2SpringCloud是什么
Spring Cloud是最常用的微服务开发框架,它是一系列框架的集合。它利用Spring Boot的开发便利性简化了分布式系统基础设施的开发,如服务发现、服务注册、配置中心、消息总线、负载均衡、 熔断器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过SpringBoot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包
5.1.3SpringCloud和SpringBoot是什么关系
Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务,Spring Cloud是一个基于Spring Boot实现的开发工具;Spring Boot专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架; Spring Boot使用了默认大于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置,Spring Cloud很大的一部分是基于Spring Boot来实现,必须基于Spring Boot开发。可以单独使用Spring Boot开发项目,但是Spring Cloud离不开 Spring Boot。
5.1.4SpringCloud服务组件
服务发现(注册中心)——Netflix Eureka (Nacos)
负载均衡——Spring Cloud Ribbon
服务调用——Netflix Feign
熔断器——Netflix Hystrix
服务网关——Spring Cloud GateWay(Zuul)
分布式配置——Spring Cloud Config (Nacos)
消息总线 —— Spring Cloud Bus (Nacos)
5.2.1Nacos
①Nacos 是阿里巴巴推出来的一个新开源项目,是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。Nacos = Spring Cloud Eureka + Spring Cloud Config
②Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。
③Nacos主要提供以下四大功能:
服务发现和服务健康监测
动态配置服务
动态DNS服务
服务及其元数据管理
④Nacos结构图
⑤服务注册(Nacos配置)
5.2.2服务调用Feign
Spring Cloud Feign是基于Netflix feign实现,整合了Spring Cloud Ribbon和Spring Cloud Hystrix,除了提供这两者的强大功能外,还提供了一种声明式的Web服务客户端定义的方式。
Feign采用接口化请求调用的方式进行服务调用:在主动调用方的接口上用@FeignClient(“service-vod”)注解修饰,在框架内部,就可以讲请求转化成Feign的请求实例feign.Request,交给Feign框架处理,就可以调用service-vod服务。
删除小节时,删除视频
删除课程时,删除视频
5.2.3springcloud接口调用过程
5.2.4熔断器:Hystrix
5.3.1NUXT是什么
Nuxt.js 是一个基于 Vue.js 的轻量级应用框架,可用来创建服务端渲染 (SSR) 应用,也可充当静态站点引擎生成静态站点应用,具有优雅的代码结构分层和热加载等特性。
5.3.2服务端渲染(SSR)
服务端渲染又称SSR (Server Side Render)是在服务端完成页面的内容,而不是在客户端通过AJAX获取数据。
服务器端渲染(SSR)的优势主要在于:更好的 SEO(搜索引擎优化search engine optimistic,例如网站排名),并且可以异步获取内容。
使用服务器端渲染,我们可以获得更快的内容到达时间(time-to-content),无需等待所有的 JavaScript 都完成下载并执行,产生更好的用户体验,对于那些「内容到达时间(time-to-content)与转化率直接相关」的应用程序而言,服务器端渲染(SSR)至关重要。
5.3.3NUXT框架配置环境
5.3.4NUXT页面加载过程
5.4.1幻灯片或轮播图显示
后台管理系统接口BannerAdminController
@RestController
@RequestMapping("/educms/banneradmin")
@CrossOrigin
public class BannerAdminController {
@Autowired
private CrmBannerService bannerService;
//1 分页查询banner
@GetMapping("pageBanner/{page}/{limit}")
public R pageBanner(@PathVariable long page,@PathVariable long limit) {
Page<CrmBanner> pageBanner = new Page<>(page,limit);
bannerService.page(pageBanner,null);
return R.ok().data("items",pageBanner.getRecords()).data("total",pageBanner.getTotal());
}
//2 添加banner
@PostMapping("addBanner")
public R addBanner(@RequestBody CrmBanner crmBanner) {
bannerService.save(crmBanner);
return R.ok();
}
@ApiOperation(value = "获取Banner")
@GetMapping("get/{id}")
public R get(@PathVariable String id) {
CrmBanner banner = bannerService.getById(id);
return R.ok().data("item", banner);
}
@ApiOperation(value = "修改Banner")
@PutMapping("update")
public R updateById(@RequestBody CrmBanner banner) {
bannerService.updateById(banner);
return R.ok();
}
@ApiOperation(value = "删除Banner")
@DeleteMapping("remove/{id}")
public R remove(@PathVariable String id) {
bannerService.removeById(id);
return R.ok();
}
}
前台用户系统接口BannerFrontController
@RestController
@RequestMapping("/educms/bannerfront")
@CrossOrigin
public class BannerFrontController {
@Autowired
private CrmBannerService bannerService;
//查询所有banner
@GetMapping("getAllBanner")
public R getAllBanner() {
List<CrmBanner> list = bannerService.selectAllBanner();
return R.ok().data("list",list);
}
}
5.4.2热门讲师+热门课程显示
前台用户系统接口IndexFrontController
@RestController
@RequestMapping("/eduservice/indexfront")
@CrossOrigin
public class IndexFrontController {
@Autowired
private EduCourseService courseService;
@Autowired
private EduTeacherService teacherService;
//查询前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<>();
wrapperTeacher.orderByDesc("id");
wrapperTeacher.last("limit 4");
List<EduTeacher> teacherList = teacherService.list(wrapperTeacher);
return R.ok().data("eduList",eduList).data("teacherList",teacherList);
}
}
5.4.3使用redis缓存首页数据
由于首页数据变化不是很频繁,而且首页访问量相对较大,所以我们有必要把首页接口数据缓存到redis缓存中,减少数据库压力和提高访问速度。
在后端接口上使用注解的方式访问redis进行缓存
5.5.1常见的登录方式:我们采用分布式单点登录中的token方式
①传统单一架构的登录认证方式:cookie+session(缺陷是无法完成分布式访问)
Cookie特点
②分布式系统中单点登录
③我们采用token实现分布式系统的单点登录,优点是:
通过客户端保存数据,服务器不需要保存会话数据(即服务器无状态),节省内存开销;
减少服务端访问数据库的次数
5.5.2使用JWT生成token字符串
①token是按照一定规则生成的字符串,规则包含透明令牌(by reference token)和自包含令牌(by value token),透明令牌规则中本地服务器必须访问OAuth2授权服务器才可以校验,自包含令牌规则可以在本地服务器进行校验。因此,我们采用通用的规则JWT(JSON Web Tokens)来生成字符串。
②JWT生成的token字符串包含三部分
头部(json字符串,包含当前令牌名称以及加密算法)
+载荷(json字符串,包含主体信息(用户信息))
+签名(防伪标志,由头部信息使用base64加密之后,拼接上载荷使用base64加密之后的部分,在加上当前的密钥,进行头部中的加密算法进行加密)
③使用JWT时需要注意:
JWT本身包含认证信息,token是经过base64编码,所以可以解码,因此token加密前的对象不应该包含敏感信息,一旦信息泄露,任何人都可以获得令牌的所有权限。
为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行进行身份验证。
为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。
④JWT如何使用:直接引入依赖,复制jwt工具类
public class JwtUtils {
//常量
public static final long EXPIRE = 1000 * 60 * 60 * 24; //token过期时间
public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO"; //秘钥
//生成token字符串的方法
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) //设置token主体部分 ,存储用户信息
.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");
}
}
5.5.3登录业务流程
5.5.4整合阿里云短信服务
①开通阿里云短信服务功能
②在service模块下创建子模块service_msm,创建配置文件和启动类,引入相关依赖
在service_msm的pom.xml中引入阿里云短信依赖
我们使用redis缓存成功发送的短信验证码,并可以设置过期时间。在首页数据显示中我们使用注解的方式访问redis,现在我使用访问redis的另一种方式:使用RedisTemplate工具类。
要想使用RedisTemplate工具类必须引入依赖并创建配置类
依赖(在service-base的pom.xml中)
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置类(application.properties)
spring.redis.host=192.168.44.132
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
#最小空闲
③编写controller接口实现短信发送
@RestController
@RequestMapping("/edumsm/msm")
@CrossOrigin
public class MsmController {
@Autowired
private MsmService msmService;
@Autowired
private RedisTemplate<String,String> redisTemplate;//访问redis的工具类
//发送短信的方法
@GetMapping("send/{phone}")
public R sendMsm(@PathVariable String phone) {
//1 从redis获取验证码,如果获取到说明短信已经成功发送,无需再次发送,直接返回
//查询redis缓存
//ValueOperations ops = redisTemplate.opsForValue();//可以获取redis的ValueOperations接口(ValueOperations是k-v结构)
//String str3 = (String)ops.get("key");//获取指定key的value值
String code = redisTemplate.opsForValue().get(phone);
if(!StringUtils.isEmpty(code)) {
return R.ok();
}
//2 如果redis获取不到,说明暂未发送短信,则进行阿里云发送
//生成随机值(短信验证码),传递阿里云进行发送
code = RandomUtil.getFourBitRandom();
Map<String,Object> param = new HashMap<>();
param.put("code",code);
//调用service发送短信的方法
boolean isSend = msmService.send(param,phone);
if(isSend) {
//发送成功,把发送成功验证码放到redis里面
//设置有效时间
//ValueOperations ops = redisTemplate.opsForValue()//可以获取redis的ValueOperations接口(ValueOperations是k-v结构)
//ops.set(phone,code,5, TimeUnit.MINUTES);//添加缓存值,并设置过期时间5分钟
redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);
return R.ok();
} else {
return R.error().message("短信发送失败");
}
}
}
④编写service接口实现短信发送
@Service
public class MsmServiceImpl implements MsmService {
//发送短信的方法
@Override
public boolean send(Map<String, Object> param, String phone) {
if(StringUtils.isEmpty(phone)) return false;
DefaultProfile profile =
DefaultProfile.getProfile("default", "LTAI4FvvVEWiTJ3GNJJqJnk7", "9st82dv7EvFk9mTjYO1XXbM632fRbG");
IAcsClient client = new DefaultAcsClient(profile);
//设置相关固定的参数
CommonRequest request = new CommonRequest();
//request.setProtocol(ProtocolType.HTTPS);
request.setMethod(MethodType.POST);
request.setDomain("dysmsapi.aliyuncs.com");
request.setVersion("2017-05-25");
request.setAction("SendSms");
//设置发送相关的参数
request.putQueryParameter("PhoneNumbers",phone); //手机号
request.putQueryParameter("SignName","我的谷粒在线教育网站"); //申请阿里云 签名名称
request.putQueryParameter("TemplateCode","SMS_180051135"); //申请阿里云 模板code
request.putQueryParameter("TemplateParam", JSONObject.toJSONString(param)); //验证码数据,转换json数据传递
try {
//最终发送
CommonResponse response = client.getCommonResponse(request);
boolean success = response.getHttpResponse().isSuccess();
return success;
}catch(Exception e) {
e.printStackTrace();
return false;
}
}
}
①在service中创建service_ucenter微服务,引入依赖,创建配置类和启动类
②创建ucenter_member表(表结构如下),使用代码生成器生成代码和实体类UcenterMember(与数据库对应),在controller接口上使用@RequestBody注解将web登录请求中的JSON数据(登录手机号+密码)绑定到登录方法的参数UcenterMember中。
③创建VO对象RegisterVo用于接收前端注册信息(controller接口通过注解@RequestBody接收web请求中的注册信息并封装到方法参数RegisterVo中)
@Data
public class RegisterVo {
@ApiModelProperty(value = "昵称")
private String nickname;
@ApiModelProperty(value = "手机号")
private String mobile;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "验证码")
private String code;
}
④编写controller接口
@RestController
@RequestMapping("/educenter/member")
@CrossOrigin
public class UcenterMemberController {
@Autowired
private UcenterMemberService memberService;
//登录
@PostMapping("login")
//@RequestBody:通过HttpMessageConverter转换器将WEB请求中的JOSN数据进行类型转换,绑定到指定方法的参数中。(一般结合POST请求使用)
public R loginUser(@RequestBody UcenterMember member) {
//member对象封装手机号和密码
//调用service方法实现登录
//返回token值,使用jwt生成
String token = memberService.login(member);
return R.ok().data("token",token);
}
//注册
@PostMapping("register")
public R registerUser(@RequestBody RegisterVo registerVo) {
memberService.register(registerVo);
return R.ok();
}
//根据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);
}
}
⑤编写service接口
@Service
public class UcenterMemberServiceImpl extends ServiceImpl<UcenterMemberMapper, UcenterMember> implements UcenterMemberService {
@Autowired
private RedisTemplate<String,String> redisTemplate;
//登录的方法
@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 mobileMember = baseMapper.selectOne(wrapper);
//判断查询对象是否为空
if(mobileMember == null) {//没有这个手机号
throw new GuliException(20001,"登录失败");
}
//判断密码
//因为存储到数据库密码肯定加密的
//把输入的密码进行加密,再和数据库密码进行比较
//加密方式 MD5
if(!MD5.encrypt(password).equals(mobileMember.getPassword())) {
throw new GuliException(20001,"登录失败");
}
//判断用户是否禁用
if(mobileMember.getIsDisabled()) {
throw new GuliException(20001,"登录失败");
}
//登录成功
//生成token字符串,使用jwt工具类
String jwtToken = JwtUtils.getJwtToken(mobileMember.getId(), mobileMember.getNickname());
return jwtToken;
}
//注册的方法
@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(code) || StringUtils.isEmpty(nickname)) {
throw new GuliException(20001,"注册失败");
}
//判断验证码
//获取redis验证码
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 member = new UcenterMember();
member.setMobile(mobile);
member.setNickname(nickname);
member.setPassword(MD5.encrypt(password));//密码需要MD5加密的
member.setIsDisabled(false);//用户不禁用
member.setAvatar("http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132");
baseMapper.insert(member);
}
}
②OAuth2的功能
【第一】开放系统间授权
方式一:用户名、密码复制(适用于同一公司内部的多个系统,不能用于不授信的第三方应用)
方式二:通用开发者key万能钥匙(适用于合作商或者授信的不同业务部门之间)
方式三:特殊令牌(接近OAuth2方式,需要考虑如何管理令牌、颁发令牌、吊销令牌,需要统一的协议,因此就有了OAuth2协议)
【第二】分布式访问(单点登录)
【第三】企业内部应用认证授权
③OAuth2的优势
5.6.2微信扫描登录
微信登录是基于OAuth2.0协议标准构建的授权登录系统。
5.6.3编写接口:生成微信二维码,获得code
①application.properties添加微信配置
# 微信开放平台 appid
wx.open.app_id=wxed9954c01bb89b47
# 微信开放平台 appsecret
wx.open.app_secret=a7482517235173ddb4083788de60b90e
# 微信开放平台 重定向url
wx.open.redirect_url=http://guli.shop/api/ucenter/wx/callback
②创建常量类ConstantWxUtils
@Component
public class ConstantWxUtils implements InitializingBean {
@Value("${wx.open.app_id}")
private String appId;
@Value("${wx.open.app_secret}")
private String appSecret;
@Value("${wx.open.redirect_url}")
private String redirectUrl;
public static String WX_OPEN_APP_ID;
public static String WX_OPEN_APP_SECRET;
public static String WX_OPEN_REDIRECT_URL;
@Override
public void afterPropertiesSet() throws Exception {
WX_OPEN_APP_ID = appId;
WX_OPEN_APP_SECRET = appSecret;
WX_OPEN_REDIRECT_URL = redirectUrl;
}
}
③controller接口
@CrossOrigin
@Controller //只是请求地址,不需要返回数据
@RequestMapping("/api/ucenter/wx")
public class WxApiController {
@Autowired
private UcenterMemberService memberService;
//1 生成微信扫描二维码
@GetMapping("login")
public String getWxCode() {
//固定地址,后面拼接参数
// String url = "https://open.weixin.qq.com/" +
// "connect/qrconnect?appid="+ ConstantWxUtils.WX_OPEN_APP_ID+"&response_type=code";
// 微信开放平台授权baseUrl %s相当于?代表占位符
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进行URLEncoder编码
String redirectUrl = ConstantWxUtils.WX_OPEN_REDIRECT_URL;
try {
redirectUrl = URLEncoder.encode(redirectUrl, "utf-8");
}catch(Exception e) {
}
//设置%s里面值
String url = String.format(
baseUrl,
ConstantWxUtils.WX_OPEN_APP_ID,
redirectUrl,
"atguigu"
);
//重定向到请求微信地址里面
return "redirect:"+url;
}
}
5.6.4编写接口:通过code获取access_token,进而获取用户信息并在首页显示
①引入httpclient和gson依赖
<!--httpclient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!--commons-io-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<!--gson-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
②导入httpclient工具类HttpClientUtils(内部有发送get/post请求的方法)
public class HttpClientUtils {
public static final int connTimeout=10000;
public static final int readTimeout=10000;
public static final String charset="UTF-8";
private static HttpClient client = null;
static {
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(128);
cm.setDefaultMaxPerRoute(128);
client = HttpClients.custom().setConnectionManager(cm).build();
}
public static String postParameters(String url, String parameterStr) throws ConnectTimeoutException, SocketTimeoutException, Exception{
return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
}
public static String postParameters(String url, String parameterStr,String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception{
return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
}
public static String postParameters(String url, Map<String, String> params) throws ConnectTimeoutException,
SocketTimeoutException, Exception {
return postForm(url, params, null, connTimeout, readTimeout);
}
public static String postParameters(String url, Map<String, String> params, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
SocketTimeoutException, Exception {
return postForm(url, params, null, connTimeout, readTimeout);
}
public static String get(String url) throws Exception {
return get(url, charset, null, null);
}
public static String get(String url, String charset) throws Exception {
return get(url, charset, connTimeout, readTimeout);
}
/**
* 发送一个 Post 请求, 使用指定的字符集编码.
*
* @param url
* @param body RequestBody
* @param mimeType 例如 application/xml "application/x-www-form-urlencoded" a=1&b=2&c=3
* @param charset 编码
* @param connTimeout 建立链接超时时间,毫秒.
* @param readTimeout 响应超时时间,毫秒.
* @return ResponseBody, 使用指定的字符集编码.
* @throws ConnectTimeoutException 建立链接超时异常
* @throws SocketTimeoutException 响应超时
* @throws Exception
*/
public static String post(String url, String body, String mimeType,String charset, Integer connTimeout, Integer readTimeout)
throws ConnectTimeoutException, SocketTimeoutException, Exception {
HttpClient client = null;
HttpPost post = new HttpPost(url);
String result = "";
try {
if (StringUtils.isNotBlank(body)) {
HttpEntity entity = new StringEntity(body, ContentType.create(mimeType, charset));
post.setEntity(entity);
}
// 设置参数
Builder customReqConf = RequestConfig.custom();
if (connTimeout != null) {
customReqConf.setConnectTimeout(connTimeout);
}
if (readTimeout != null) {
customReqConf.setSocketTimeout(readTimeout);
}
post.setConfig(customReqConf.build());
HttpResponse res;
if (url.startsWith("https")) {
// 执行 Https 请求.
client = createSSLInsecureClient();
res = client.execute(post);
} else {
// 执行 Http 请求.
client = HttpClientUtils.client;
res = client.execute(post);
}
result = IOUtils.toString(res.getEntity().getContent(), charset);
} finally {
post.releaseConnection();
if (url.startsWith("https") && client != null&& client instanceof CloseableHttpClient) {
((CloseableHttpClient) client).close();
}
}
return result;
}
/**
* 提交form表单
*
* @param url
* @param params
* @param connTimeout
* @param readTimeout
* @return
* @throws ConnectTimeoutException
* @throws SocketTimeoutException
* @throws Exception
*/
public static String postForm(String url, Map<String, String> params, Map<String, String> headers, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
SocketTimeoutException, Exception {
HttpClient client = null;
HttpPost post = new HttpPost(url);
try {
if (params != null && !params.isEmpty()) {
List<NameValuePair> formParams = new ArrayList<NameValuePair>();
Set<Entry<String, String>> entrySet = params.entrySet();
for (Entry<String, String> entry : entrySet) {
formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8);
post.setEntity(entity);
}
if (headers != null && !headers.isEmpty()) {
for (Entry<String, String> entry : headers.entrySet()) {
post.addHeader(entry.getKey(), entry.getValue());
}
}
// 设置参数
Builder customReqConf = RequestConfig.custom();
if (connTimeout != null) {
customReqConf.setConnectTimeout(connTimeout);
}
if (readTimeout != null) {
customReqConf.setSocketTimeout(readTimeout);
}
post.setConfig(customReqConf.build());
HttpResponse res = null;
if (url.startsWith("https")) {
// 执行 Https 请求.
client = createSSLInsecureClient();
res = client.execute(post);
} else {
// 执行 Http 请求.
client = HttpClientUtils.client;
res = client.execute(post);
}
return IOUtils.toString(res.getEntity().getContent(), "UTF-8");
} finally {
post.releaseConnection();
if (url.startsWith("https") && client != null
&& client instanceof CloseableHttpClient) {
((CloseableHttpClient) client).close();
}
}
}
/**
* 发送一个 GET 请求
*
* @param url
* @param charset
* @param connTimeout 建立链接超时时间,毫秒.
* @param readTimeout 响应超时时间,毫秒.
* @return
* @throws ConnectTimeoutException 建立链接超时
* @throws SocketTimeoutException 响应超时
* @throws Exception
*/
public static String get(String url, String charset, Integer connTimeout,Integer readTimeout)
throws ConnectTimeoutException,SocketTimeoutException, Exception {
HttpClient client = null;
HttpGet get = new HttpGet(url);
String result = "";
try {
// 设置参数
Builder customReqConf = RequestConfig.custom();
if (connTimeout != null) {
customReqConf.setConnectTimeout(connTimeout);
}
if (readTimeout != null) {
customReqConf.setSocketTimeout(readTimeout);
}
get.setConfig(customReqConf.build());
HttpResponse res = null;
if (url.startsWith("https")) {
// 执行 Https 请求.
client = createSSLInsecureClient();
res = client.execute(get);
} else {
// 执行 Http 请求.
client = HttpClientUtils.client;
res = client.execute(get);
}
result = IOUtils.toString(res.getEntity().getContent(), charset);
} finally {
get.releaseConnection();
if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) {
((CloseableHttpClient) client).close();
}
}
return result;
}
/**
* 从 response 里获取 charset
*
* @param ressponse
* @return
*/
@SuppressWarnings("unused")
private static String getCharsetFromResponse(HttpResponse ressponse) {
// Content-Type:text/html; charset=GBK
if (ressponse.getEntity() != null && ressponse.getEntity().getContentType() != null && ressponse.getEntity().getContentType().getValue() != null) {
String contentType = ressponse.getEntity().getContentType().getValue();
if (contentType.contains("charset=")) {
return contentType.substring(contentType.indexOf("charset=") + 8);
}
}
return null;
}
/**
* 创建 SSL连接
* @return
* @throws GeneralSecurityException
*/
private static CloseableHttpClient createSSLInsecureClient() throws GeneralSecurityException {
try {
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
public boolean isTrusted(X509Certificate[] chain,String authType) throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() {
@Override
public boolean verify(String arg0, SSLSession arg1) {
return true;
}
@Override
public void verify(String host, SSLSocket ssl)
throws IOException {
}
@Override
public void verify(String host, X509Certificate cert)
throws SSLException {
}
@Override
public void verify(String host, String[] cns,
String[] subjectAlts) throws SSLException {
}
});
return HttpClients.custom().setSSLSocketFactory(sslsf).build();
} catch (GeneralSecurityException e) {
throw e;
}
}
public static void main(String[] args) {
try {
String str= post("https://localhost:443/ssl/test.shtml","name=12&page=34","application/x-www-form-urlencoded", "UTF-8", 10000, 10000);
//String str= get("https://localhost:443/ssl/test.shtml?name=12&page=34","GBK");
/*Map map = new HashMap();
map.put("name", "111");
map.put("page", "222");
String str= postForm("https://localhost:443/ssl/test.shtml",map,null, 10000, 10000);*/
System.out.println(str);
} catch (ConnectTimeoutException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SocketTimeoutException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
③编写controller层接口
@CrossOrigin
@Controller //只是请求地址,不需要返回数据
@RequestMapping("/api/ucenter/wx")
public class WxApiController {
@Autowired
private UcenterMemberService memberService;
//2 获取扫描人信息,添加数据
@GetMapping("callback")
public String callback(String code, String state) {
try {
//1 获取code值,临时票据,类似于验证码
//2 拿着code请求 微信固定的地址,得到两个值 accsess_token 和 openid
String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
"?appid=%s" +
"&secret=%s" +
"&code=%s" +
"&grant_type=authorization_code";
//拼接三个参数 :id 秘钥 和 code值
String accessTokenUrl = String.format(
baseAccessTokenUrl,
ConstantWxUtils.WX_OPEN_APP_ID,
ConstantWxUtils.WX_OPEN_APP_SECRET,
code
);
//请求这个拼接好的地址,得到返回两个值 accsess_token 和 openid
//使用httpclient发送请求,得到返回结果
String accessTokenInfo = HttpClientUtils.get(accessTokenUrl);
//从accessTokenInfo字符串获取出来两个值 accsess_token 和 openid
//把accessTokenInfo字符串转换map集合,根据map里面key获取对应值
//使用json转换工具 Gson
Gson gson = new Gson();
HashMap mapAccessToken = gson.fromJson(accessTokenInfo, HashMap.class);
String access_token = (String)mapAccessToken.get("access_token");
String openid = (String)mapAccessToken.get("openid");
//把扫描人信息添加数据库里面
//判断数据表里面是否存在相同微信信息,根据openid判断
UcenterMember member = memberService.getOpenIdMember(openid);
if(member == null) {//memeber是空,表没有相同微信数据,进行添加【用户注册】
//3 拿着得到accsess_token 和 openid,再去请求微信提供固定的地址,获取到扫描人信息,保存到数据库
//访问微信的资源服务器,获取用户信息
String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
"?access_token=%s" +
"&openid=%s";
//拼接两个参数
String userInfoUrl = String.format(
baseUserInfoUrl,
access_token,
openid
);
//发送请求
String userInfo = HttpClientUtils.get(userInfoUrl);
//获取返回userinfo字符串扫描人信息
HashMap userInfoMap = gson.fromJson(userInfo, HashMap.class);
String nickname = (String)userInfoMap.get("nickname");//昵称
String headimgurl = (String)userInfoMap.get("headimgurl");//头像
member = new UcenterMember();
member.setOpenid(openid);
member.setNickname(nickname);
member.setAvatar(headimgurl);
memberService.save(member);
}
//4.使用jwt根据member对象生成token字符串,传给前端解析显示用户信息
String jwtToken = JwtUtils.getJwtToken(member.getId(), member.getNickname());
//最后:返回首页面,通过路径传递token字符串
return "redirect:http://localhost:3000?token="+jwtToken;
}catch(Exception e) {
throw new GuliException(20001,"登录失败");
}
}
}
5.7.1名师列表
①controller接口
@RestController
@RequestMapping("/eduservice/teacherfront")
@CrossOrigin
public class TeacherFrontController {
@Autowired
private EduTeacherService teacherService;
@Autowired
private EduCourseService courseService;
//1 分页查询讲师的方法
@PostMapping("getTeacherFrontList/{page}/{limit}")
public R getTeacherFrontList(@PathVariable long page,@PathVariable long limit) {
Page<EduTeacher> pageTeacher = new Page<>(page,limit);
Map<String,Object> map = teacherService.getTeacherFrontList(pageTeacher);
//返回分页所有数据
return R.ok().data(map);
}
}
②service接口
public interface EduTeacherService extends IService<EduTeacher> {
//1 分页查询讲师的方法
Map<String, Object> getTeacherFrontList(Page<EduTeacher> pageTeacher);
}
@Service
public class EduTeacherServiceImpl extends ServiceImpl<EduTeacherMapper, EduTeacher> implements EduTeacherService {
//1 分页查询讲师的方法
@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);
//map返回
return map;
}
}
5.7.2名师详情
controller接口【根据讲师id查询讲师基本信息+讲师所讲课程】
@RestController
@RequestMapping("/eduservice/teacherfront")
@CrossOrigin
public class TeacherFrontController {
@Autowired
private EduTeacherService teacherService;
@Autowired
private EduCourseService courseService;
//2 讲师详情的功能
@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);
}
}
5.8.1课程列表
①创建VO对象CourseFrontVo封装前端页面的查询条件
@Data
public class CourseFrontVo {
@ApiModelProperty(value = "课程名称")
private String title;
@ApiModelProperty(value = "讲师id")
private String teacherId;
@ApiModelProperty(value = "一级类别id")
private String subjectParentId;
@ApiModelProperty(value = "二级类别id")
private String subjectId;
@ApiModelProperty(value = "销量排序")
private String buyCountSort;
@ApiModelProperty(value = "最新时间排序")
private String gmtCreateSort;
@ApiModelProperty(value = "价格排序")
private String priceSort;
}
②controller接口
@RestController
@RequestMapping("/eduservice/coursefront")
@CrossOrigin
public class CourseFrontController {
@Autowired
private EduCourseService courseService;
@Autowired
private EduChapterService chapterService;
//1 条件查询带分页查询课程
@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);
}
}
③service接口
public interface EduCourseService extends IService<EduCourse> {
//1 条件查询带分页查询课程前台
Map<String, Object> getCourseFrontList(Page<EduCourse> pageCourse, CourseFrontVo courseFrontVo);
}
@Service
public class EduCourseServiceImpl extends ServiceImpl<EduCourseMapper, EduCourse> implements EduCourseService {
//课程描述注入
@Autowired
private EduCourseDescriptionService courseDescriptionService;
//注入小节和章节service
@Autowired
private EduVideoService eduVideoService;
@Autowired
private EduChapterService chapterService;
//1 条件查询带分页查询课程
@Override
public Map<String, Object> getCourseFrontList(Page<EduCourse> pageParam, CourseFrontVo courseFrontVo) {
//2 根据讲师id查询所讲课程
QueryWrapper<EduCourse> wrapper = new QueryWrapper<>();
//判断条件值是否为空,不为空拼接
if(!StringUtils.isEmpty(courseFrontVo.getSubjectParentId())) { //一级分类
wrapper.eq("subject_parent_id",courseFrontVo.getSubjectParentId());
}
if(!StringUtils.isEmpty(courseFrontVo.getSubjectId())) { //二级分类
wrapper.eq("subject_id",courseFrontVo.getSubjectId());
}
if(!StringUtils.isEmpty(courseFrontVo.getBuyCountSort())) { //销售量
wrapper.orderByDesc("buy_count");
}
if (!StringUtils.isEmpty(courseFrontVo.getGmtCreateSort())) { //最新
wrapper.orderByDesc("gmt_create");
}
if (!StringUtils.isEmpty(courseFrontVo.getPriceSort())) {//价格
wrapper.orderByDesc("price");
}
baseMapper.selectPage(pageParam,wrapper);
List<EduCourse> 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);
//map返回
return map;
}
}
5.8.2课程详情【查询课程基本信息+章节小节信息】
从底层数据库mysql查询→实体类entity对象(和数据库对应)→copy到VO对象CourseWebVo→前端页面显示
①创建VO类CourseWebVo
@Data
public class CourseWebVo {
private String id;
@ApiModelProperty(value = "课程标题")
private String title;
@ApiModelProperty(value = "课程销售价格,设置为0则可免费观看")
private BigDecimal price;
@ApiModelProperty(value = "总课时")
private Integer lessonNum;
@ApiModelProperty(value = "课程封面图片路径")
private String cover;
@ApiModelProperty(value = "销售数量")
private Long buyCount;
@ApiModelProperty(value = "浏览数量")
private Long viewCount;
@ApiModelProperty(value = "课程简介")
private String description;
@ApiModelProperty(value = "讲师ID")
private String teacherId;
@ApiModelProperty(value = "讲师姓名")
private String teacherName;
@ApiModelProperty(value = "讲师资历,一句话说明讲师")
private String intro;
@ApiModelProperty(value = "讲师头像")
private String avatar;
@ApiModelProperty(value = "课程一级类别ID")
private String subjectLevelOneId;
@ApiModelProperty(value = "类别一级名称")
private String subjectLevelOne;
@ApiModelProperty(value = "课程二级类别ID")
private String subjectLevelTwoId;
@ApiModelProperty(value = "类别二级名称")
private String subjectLevelTwo;
}
②controller接口
@RestController
@RequestMapping("/eduservice/coursefront")
@CrossOrigin
public class CourseFrontController {
@Autowired
private EduCourseService courseService;
@Autowired
private EduChapterService chapterService;
//2 课程详情的方法
@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);
}
}
③service接口
public interface EduCourseService extends IService<EduCourse> {
//根据课程id,编写sql语句查询课程信息
CourseWebVo getBaseCourseInfo(String courseId);
}
public interface EduChapterService extends IService<EduChapter> {
//课程大纲列表,根据课程id进行查询
List<ChapterVo> getChapterVideoByCourseId(String courseId);
}
④serviceImpl
@Service
public class EduCourseServiceImpl extends ServiceImpl<EduCourseMapper, EduCourse> implements EduCourseService {
//课程描述注入
@Autowired
private EduCourseDescriptionService courseDescriptionService;
//注入小节和章节service
@Autowired
private EduVideoService eduVideoService;
@Autowired
private EduChapterService chapterService;
//根据课程id,编写sql语句查询课程信息
@Override
public CourseWebVo getBaseCourseInfo(String courseId) {
return baseMapper.getBaseCourseInfo(courseId);
}
}
@Service
public class EduChapterServiceImpl extends ServiceImpl<EduChapterMapper, EduChapter> implements EduChapterService {
@Autowired
private EduVideoService videoService;//注入小节service
//课程大纲列表,根据课程id进行查询
@Override
public List<ChapterVo> getChapterVideoByCourseId(String courseId) {
//1 根据课程id查询课程里面所有的章节
QueryWrapper<EduChapter> wrapperChapter = new QueryWrapper<>();
wrapperChapter.eq("course_id",courseId);
List<EduChapter> eduChapterList = baseMapper.selectList(wrapperChapter);
//2 根据课程id查询课程里面所有的小节
QueryWrapper<EduVideo> wrapperVideo = new QueryWrapper<>();
wrapperVideo.eq("course_id",courseId);
List<EduVideo> eduVideoList = videoService.list(wrapperVideo);
//创建list集合,用于最终封装数据
List<ChapterVo> finalList = new ArrayList<>();
//3 遍历查询章节list集合进行封装
//遍历查询章节list集合
for (int i = 0; i < eduChapterList.size(); i++) {
//每个章节
EduChapter eduChapter = eduChapterList.get(i);
//eduChapter对象值复制到ChapterVo里面
ChapterVo chapterVo = new ChapterVo();
BeanUtils.copyProperties(eduChapter,chapterVo);
//把chapterVo放到最终list集合
finalList.add(chapterVo);
//创建集合,用于封装章节的小节
List<VideoVo> videoList = new ArrayList<>();
//4 遍历查询小节list集合,进行封装
for (int m = 0; m < eduVideoList.size(); m++) {
//得到每个小节
EduVideo eduVideo = eduVideoList.get(m);
//判断:小节里面chapterid和章节里面id是否一样
if(eduVideo.getChapterId().equals(eduChapter.getId())) {
//进行封装
VideoVo videoVo = new VideoVo();
BeanUtils.copyProperties(eduVideo,videoVo);
//放到小节封装集合
videoList.add(videoVo);
}
}
//把封装之后小节list集合,放到章节对象里面
chapterVo.setChildren(videoList);
}
return finalList;
}
}
⑤Mapple中SQL语句(EduCourseMapper.xml中查询课程基本信息的SQL语句:多表查询)
4.8.3视频播放
阿里云视频播放支持两种播放方式:
方式一、播放地址播放(优先级最高,非加密视频)
方式二、播放凭证播放(加密视频)【我们使用这个】
controller接口代码如下:
//根据视频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,"获取凭证失败");
}
}
创建数据库表edu_comment,并利用代码生成器自动生成相关代码,实体类EduComment
5.9.1添加评论
思路:从前端接收评论内容content和课程id、token(携带用户信息),分别查询课程信息(course_id、teacher_id)和用户信息(member_id、nickname、avater),将这些信息封装到EduComment对象中,添加至数据库表edu_comment中。
content、course_id、teacher_id是在前端获取的,并绑定到后端接口中的EduComment对象中。后端接口只需要查询用户信息,需要服务调用(Feign)
①在service_edu中创建controller接口
@ApiOperation(value = "添加评论")
@PostMapping("auth/save")
public R save(@RequestBody EduComment comment, HttpServletRequest request) {
String memberId = JwtUtils.getMemberIdByJwtToken(request);//使用JWT从请求头中获取token字符串,再从token字符串获取用户id
if(StringUtils.isEmpty(memberId)) {
return R.error().code(28004).message("请登录");
}
comment.setMemberId(memberId);
//根据用户id查询用户表,获取用户信息【服务调用,单点登录】
UcenterMemberPay ucenterInfo = ucenterClient.getUcenterPay(memberId);
comment.setNickname(ucenterInfo.getNickname());
comment.setAvatar(ucenterInfo.getAvatar());
commentService.save(comment);
return R.ok();
}
②在service_edu中创建ucenterClient接口,实现对service_ucenter的远程调用
@Component
@FeignClient(name="service-ucenter",fallback = UcenterClientImpl.class)
public interface UcenterClient {
//根据用户id获取用户信息
@GetMapping("/ucenterservice/member/getUcenterPay/{memberId}")
public UcenterMemberPay getUcenterPay(@PathVariable("memberId") String memberId);
}
@Component
public class UcenterClientImpl implements UcenterClient {
@Override
public UcenterMemberPay getUcenterPay(String memberId) {
return null;
}
}
5.9.2分页查询评论
对数据库表edu_comment进行分页条件查询
在service_edu中创建controller接口
//根据课程id查询评论列表
@ApiOperation(value = "评论分页列表")
@GetMapping("{page}/{limit}")
public R index(
@ApiParam(name = "page", value = "当前页码", required = true)
@PathVariable Long page,
@ApiParam(name = "limit", value = "每页记录数", required = true)
@PathVariable Long limit,
@ApiParam(name = "courseQuery", value = "查询对象", required = false)
String courseId) {
Page<Comment> pageParam = new Page<>(page, limit);
QueryWrapper<Comment> wrapper = new QueryWrapper<>();
wrapper.eq("course_id",courseId);
commentService.page(pageParam,wrapper);
List<Comment> commentList = pageParam.getRecords();
Map<String, Object> map = new HashMap<>();
map.put("items", commentList);
map.put("current", pageParam.getCurrent());
map.put("pages", pageParam.getPages());
map.put("size", pageParam.getSize());
map.put("total", pageParam.getTotal());
map.put("hasNext", pageParam.hasNext());
map.put("hasPrevious", pageParam.hasPrevious());
return R.ok().data(map);
}
service下创建service_order模块,添加相关依赖pom.xml
<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>
创建配置类application.properties
# 服务端口
server.port=8007
# 服务名
spring.application.name=service-order
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/atguigu/eduorder/mapper/xml/*.xml
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#开启熔断机制
#feign.hystrix.enabled=true
# 设置hystrix超时时间,默认1000ms
#hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000
5.10.1订单
创建数据库表t_Order,生成相关代码,对应的实体类Order
【生成订单saveOrder()】:需要远程调用service_edu获取课程信息,且需要远程调用service_ucenter获取用户信息,并封装到Order对象,添加到数据库表t_Order;
【根据订单id查询订单信息getOrderInfo()】:直接查询数据库表t_Order;
①service_order服务的controller层:OrderController
@RestController
@RequestMapping("/eduorder/order")
@CrossOrigin
public class OrderController {
@Autowired
private OrderService orderService;
//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);
}
//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);
}
}
②service_order服务的service层:OrderService
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
@Autowired
private EduClient eduClient;
@Autowired
private UcenterClient ucenterClient;
//1 生成订单的方法
@Override
public String createOrders(String courseId, String memberId) {
//通过远程调用根据用户id获取用户信息
UcenterMemberOrder userInfoOrder = ucenterClient.getUserInfoOrder(memberId);
//通过远程调用根据课程id获取课信息
CourseWebVoOrder courseInfoOrder = eduClient.getCourseInfoOrder(courseId);
//创建Order对象,向order对象里面设置需要数据
Order order = new Order();
order.setOrderNo(OrderNoUtil.getOrderNo());//订单号
order.setCourseId(courseId); //课程id
order.setCourseTitle(courseInfoOrder.getTitle());
order.setCourseCover(courseInfoOrder.getCover());
order.setTeacherName(courseInfoOrder.getTeacherName());
order.setTotalFee(courseInfoOrder.getPrice());
order.setMemberId(memberId);
order.setMobile(userInfoOrder.getMobile());
order.setNickname(userInfoOrder.getNickname());
order.setStatus(0); //订单状态(0:未支付 1:已支付)
order.setPayType(1); //支付类型 ,微信1
baseMapper.insert(order);
//返回订单号
return order.getOrderNo();
}
}
③service_order服务中创建client模块,创建接口UcenterClient和EduClient实现远程服务调用(基于Feign注解)
@Component
@FeignClient("service-ucenter")
public interface UcenterClient {
//根据用户id获取用户信息
@PostMapping("/educenter/member/getUserInfoOrder/{id}")
public UcenterMemberOrder getUserInfoOrder(@PathVariable("id") String id);
}
@Component
@FeignClient("service-edu")
public interface EduClient {
//根据课程id查询课程信息
@PostMapping("/eduservice/coursefront/getCourseInfoOrder/{id}")
public CourseWebVoOrder getCourseInfoOrder(@PathVariable("id") String id);
}
④在common_utils模块下创建公共类UcenterMemberOrder和CourseWebVoOrder
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="UcenterMember对象", description="会员表")
public class UcenterMemberOrder implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "会员id")
@TableId(value = "id", type = IdType.ID_WORKER_STR)
private String id;
@ApiModelProperty(value = "微信openid")
private String openid;
@ApiModelProperty(value = "手机号")
private String mobile;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "昵称")
private String nickname;
@ApiModelProperty(value = "性别 1 女,2 男")
private Integer sex;
@ApiModelProperty(value = "年龄")
private Integer age;
@ApiModelProperty(value = "用户头像")
private String avatar;
@ApiModelProperty(value = "用户签名")
private String sign;
@ApiModelProperty(value = "是否禁用 1(true)已禁用, 0(false)未禁用")
private Boolean isDisabled;
@ApiModelProperty(value = "逻辑删除 1(true)已删除, 0(false)未删除")
private Boolean isDeleted;
@ApiModelProperty(value = "创建时间")
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;
@ApiModelProperty(value = "更新时间")
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date gmtModified;
}
@Data
public class CourseWebVoOrder {
private String id;
@ApiModelProperty(value = "课程标题")
private String title;
@ApiModelProperty(value = "课程销售价格,设置为0则可免费观看")
private BigDecimal price;
@ApiModelProperty(value = "总课时")
private Integer lessonNum;
@ApiModelProperty(value = "课程封面图片路径")
private String cover;
@ApiModelProperty(value = "销售数量")
private Long buyCount;
@ApiModelProperty(value = "浏览数量")
private Long viewCount;
@ApiModelProperty(value = "课程简介")
private String description;
@ApiModelProperty(value = "讲师ID")
private String teacherId;
@ApiModelProperty(value = "讲师姓名")
private String teacherName;
@ApiModelProperty(value = "讲师资历,一句话说明讲师")
private String intro;
@ApiModelProperty(value = "讲师头像")
private String avatar;
@ApiModelProperty(value = "课程一级类别ID")
private String subjectLevelOneId;
@ApiModelProperty(value = "类别一级名称")
private String subjectLevelOne;
@ApiModelProperty(value = "课程二级类别ID")
private String subjectLevelTwoId;
@ApiModelProperty(value = "类别二级名称")
private String subjectLevelTwo;
}
⑤在service_ucenter服务中UcenterMemberController实现接口:根据用户id查询用户信息
//根据用户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;
}
⑥在service_edu服务中CouseFrontController实现接口:根据课程id查询课程信息
//根据课程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;
}
5.10.2微信支付
【生成微信支付二维码createNative()】:请求微信云固定地址https://api.mch.weixin.qq.com/pay/unifiedorderhttps://api.mch.weixin.qq.com/pay/unifiedorder,解析返回结果封装到Map集合(内部含有二维码地址+其他信息),前端解析Map集合便可以生成二维码;
【根据订单id查询订单支付状态queryPayStatus()】:首先执行queryPayStatus方法,根据orderNo查询Map集合查看订单支付状态;如果已支付,就执行updateOrdersStatus方法,添加记录到支付日志表t_pay_log,并更新订单表t_order;
①service_order服务中controller层
@RestController
@RequestMapping("/eduorder/paylog")
@CrossOrigin
public class PayLogController {
@Autowired
private PayLogService payLogService;
//生成微信支付二维码接口
//参数是订单号
@GetMapping("createNative/{orderNo}")
public R createNative(@PathVariable String orderNo) {
//返回信息,包含二维码地址,还有其他需要的信息
Map map = payLogService.createNatvie(orderNo);
System.out.println("****返回二维码map集合:"+map);
return R.ok().data(map);
}
//查询订单支付状态
//参数:订单号,根据订单号查询 支付状态
@GetMapping("queryPayStatus/{orderNo}")
public R queryPayStatus(@PathVariable String orderNo) {
Map<String,String> map = payLogService.queryPayStatus(orderNo);
System.out.println("*****查询订单状态map集合:"+map);
if(map == null) {
return R.error().message("支付出错了");
}
//如果返回map里面不为空,通过map获取订单状态
if(map.get("trade_state").equals("SUCCESS")) {//支付成功
//添加记录到支付表,更新订单表订单状态
payLogService.updateOrdersStatus(map);
return R.ok().message("支付成功");
}
return R.ok().code(25000).message("支付中");
}
}
②service_order服务中的service层
public interface PayLogService extends IService<PayLog> {
//生成微信支付二维码接口
Map createNatvie(String orderNo);
//根据订单号查询订单支付状态
Map<String, String> queryPayStatus(String orderNo);
//向支付表添加记录,更新订单状态
void updateOrdersStatus(Map<String, String> map);
}
③service_order服务中的serviceImpl
@Service
public class PayLogServiceImpl extends ServiceImpl<PayLogMapper, PayLog> implements PayLogService {
@Autowired
private OrderService orderService;
//生成微信支付二维码接口
@Override
public Map createNatvie(String orderNo) {
try {
//1 根据订单号查询订单信息
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("order_no",orderNo);
Order order = orderService.getOne(wrapper);
//2 使用map设置生成二维码需要参数
Map m = new HashMap();
m.put("appid","wx74862e0dfcf69954");
m.put("mch_id", "1558950191");
m.put("nonce_str", WXPayUtil.generateNonceStr());
m.put("body", order.getCourseTitle()); //课程标题
m.put("out_trade_no", orderNo); //订单号
m.put("total_fee", order.getTotalFee().multiply(new BigDecimal("100")).longValue()+"");
m.put("spbill_create_ip", "127.0.0.1");
m.put("notify_url", "http://guli.shop/api/order/weixinPay/weixinNotify\n");
m.put("trade_type", "NATIVE");
//3 发送httpclient请求,传递参数xml格式,微信支付提供的固定的地址
HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder");
//设置xml格式的参数
client.setXmlParam(WXPayUtil.generateSignedXml(m,"T6m9iK73b0kn9g5v426MKfHQH7X8rKwb"));
client.setHttps(true);
//执行post请求发送
client.post();
//4 得到发送请求返回结果
//返回内容,是使用xml格式返回
String xml = client.getContent();
//把xml格式转换map集合,把map集合返回
Map<String,String> resultMap = WXPayUtil.xmlToMap(xml);
//最终返回数据 的封装
Map map = new HashMap();
map.put("out_trade_no", orderNo);
map.put("course_id", order.getCourseId());
map.put("total_fee", order.getTotalFee());
map.put("result_code", resultMap.get("result_code")); //返回二维码操作状态码
map.put("code_url", resultMap.get("code_url")); //二维码地址
return map;
}catch(Exception e) {
throw new GuliException(20001,"生成二维码失败");
}
}
//查询订单支付状态
@Override
public Map<String, String> queryPayStatus(String orderNo) {
try {
//1、封装参数
Map m = new HashMap<>();
m.put("appid", "wx74862e0dfcf69954");
m.put("mch_id", "1558950191");
m.put("out_trade_no", orderNo);
m.put("nonce_str", WXPayUtil.generateNonceStr());
//2 发送httpclient
HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/orderquery");
client.setXmlParam(WXPayUtil.generateSignedXml(m,"T6m9iK73b0kn9g5v426MKfHQH7X8rKwb"));
client.setHttps(true);
client.post();
//3 得到请求返回内容
String xml = client.getContent();
Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);
//6、转成Map再返回
return resultMap;
}catch(Exception e) {
return null;
}
}
//添加支付记录和更新订单状态
@Override
public void updateOrdersStatus(Map<String, String> map) {
//从map获取订单号
String orderNo = map.get("out_trade_no");
//根据订单号查询订单信息
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("order_no",orderNo);
Order order = orderService.getOne(wrapper);
//更新订单表订单状态
if(order.getStatus().intValue() == 1) { return; }
order.setStatus(1);//1代表已经支付
orderService.updateById(order);
//向支付表添加支付记录
PayLog payLog = new PayLog();
payLog.setOrderNo(orderNo); //订单号
payLog.setPayTime(new Date()); //订单完成时间
payLog.setPayType(1);//支付类型 1微信
payLog.setTotalFee(order.getTotalFee());//总金额(分)
payLog.setTradeState(map.get("trade_state"));//支付状态
payLog.setTransactionId(map.get("transaction_id")); //流水号
payLog.setAttr(JSONObject.toJSONString(map));
baseMapper.insert(payLog);
}
}
5.10.3支付成功后跳转课程详情页面
前端页面需要判断:
如果课程是免费课程,按钮显示“立即观看”;
如果课程是已经支付成功的,按钮显示“立即观看”;
如果课程没有购买,或者不是免费课程,按钮显示“立即购买”
①service_edu服务下CourseFrontController中完善getFrontCourseInfo(),增加isBuyCourse判断是否已支付;
//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);
}
②创建OrdersClient接口实现远程调用service_order服务中的isBuyCourse()方法
@Component
@FeignClient("service-order")
public interface OrdersClient {
//根据课程id和用户id查询订单表中订单状态
@GetMapping("/eduorder/order/isBuyCourse/{courseId}/{memberId}")
public boolean isBuyCourse(@PathVariable("courseId") String courseId, @PathVariable("memberId") String memberId);
}
③service_order服务下OrderController中实现isBuyCourse()方法:根据课程id和用户id查询订单状态是否支付
//根据课程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;
}
}
5.11.1网关是什么
后台管理系统是基于springboot实现的,网关使用nginx,可以进行请求转发 、负载均衡、动静分离;
前台用户系统是基于springcloud实现的,网关使用gateway,具有安全、监控、限流等功能;
5.11.2GateWay是什么
spring cloud gateway是spring官方基于spring 5.0、spring boot 2.0、project reactor等技术开发的网关,gateway是spring cloud生态系统中的网关,目标是替代netflix zuul,可以进行负载均衡、熔断降级、统一鉴权、请求过滤、路径重写、限流保护;
5.11.3GateWay原理
spring cloud gateway最基础的部分是路由,路由信息由1个ID、一个目的URL、一组断言、一组filter组成,如果断言为true,则说明请求的URL和配置匹配;
断言:java8中的断言函数,输入类型是ServerWebExchange,用于判断是否匹配来自于http request的任何信息,比如请求头和参数等;
过滤器:一个标准的spring webFilter,分为GataWay Filter和Global Filter,过滤器将会对请求和响应进行修改处理;
详细思路:首先前端http请求到达gateway(大门),通过不同的路由(大门里面有多条路)转向不同的服务器,路由包含四部分信息(id、URL、断言函数、过滤器);然后遍历每一个路由的断言函数(断言函数用于判断当前路由URL是否和http请求匹配),如果false说明不匹配(此路不通),如果true说明匹配;最后通过该路由的filter到达服务器。
5.11.4后端搭建gateway
①创建infrastructure基础服务模块,内部创建api_gateway网关服务
②引入依赖pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>infrastructureartifactId>
<groupId>com.atguigugroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>api_gatewayartifactId>
<dependencies>
<dependency>
<groupId>com.atguigugroupId>
<artifactId>common_utilsartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>com.google.code.gsongroupId>
<artifactId>gsonartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
dependencies>
project>
③配置文件application.properties
# 服务端口
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-msm服务
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/**
④启动类(@EnableDiscoveryClient注解可以使该服务在nacos中被注册)
@SpringBootApplication
@EnableDiscoveryClient
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
5.11.5网关相关配置
①解决跨域问题:api_gateway下创建config模块,内部创建配置类CorsConfig
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
②全局Filter,统一处理会员登录与外部不允许访问的服务:api_gateway下创建filter模块,内部创建AuthGlobalFilter类
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
//谷粒学院api接口,校验用户必须登录
if(antPathMatcher.match("/api/**/auth/**", path)) {
List<String> tokenList = request.getHeaders().get("token");
if(null == tokenList) {
ServerHttpResponse response = exchange.getResponse();
return out(response);
} else {
// Boolean isCheck = JwtUtils.checkToken(tokenList.get(0));
// if(!isCheck) {
ServerHttpResponse response = exchange.getResponse();
return out(response);
// }
}
}
//内部服务接口,不允许外部访问
if(antPathMatcher.match("/**/inner/**", path)) {
ServerHttpResponse response = exchange.getResponse();
return out(response);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
private Mono<Void> out(ServerHttpResponse response) {
JsonObject message = new JsonObject();
message.addProperty("success", false);
message.addProperty("code", 28004);
message.addProperty("data", "鉴权失败");
byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
//response.setStatusCode(HttpStatus.UNAUTHORIZED);
//指定编码,否则在浏览器中会中文乱码
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
}
③自定义异常处理:网关调用服务时可能会有一些异常或服务不可用,如果返回错误信息不太友好,因此需要我们覆盖处理;在api_gateway下创建handler模块,内部创建两个类(ErrorHandlerConfig+JsonExceptionHandler)
@Configuration
@EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class})
public class ErrorHandlerConfig {
private final ServerProperties serverProperties;
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public ErrorHandlerConfig(ServerProperties serverProperties,
ResourceProperties resourceProperties,
ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer,
ApplicationContext applicationContext) {
this.serverProperties = serverProperties;
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
JsonExceptionHandler exceptionHandler = new JsonExceptionHandler(
errorAttributes,
this.resourceProperties,
this.serverProperties.getError(),
this.applicationContext);
exceptionHandler.setViewResolvers(this.viewResolvers);
exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
return exceptionHandler;
}
}
public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler {
public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
ErrorProperties errorProperties, ApplicationContext applicationContext) {
super(errorAttributes, resourceProperties, errorProperties, applicationContext);
}
/**
* 获取异常属性
*/
@Override
protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
Map<String, Object> map = new HashMap<>();
map.put("success", false);
map.put("code", 20005);
map.put("message", "网关失败");
map.put("data", null);
return map;
}
/**
* 指定响应处理方法为JSON处理的方法
* @param errorAttributes
*/
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
/**
* 根据code获取对应的HttpStatus
* @param errorAttributes
*/
@Override
protected int getHttpStatus(Map<String, Object> errorAttributes) {
return 200;
}
}
5.12.1应用场景
①在系统开发过程中,开发者通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的【配置文件(application.properties)】的形式存在,这样就可以把静态的系统与实际的物理运行环境进行适配。
②一般在系统部署的过程中由【配置中心】进行配置管理。
③如果微服务架构中没有使用统一的配置中心时,会存在以下隐患:
配置文件分散在各个服务中,不方便维护;
配置内容的安全性不能够保证;
更新配置后,服务需要重启;
5.12.2实现方式
①spring cloud config
spring cloud config分为client和server两部分;
server:用于配置文件的存储(使用git或者svn)、以接口的形式将配置文件的内容提供出去;
client:通过接口获取数据、使用此数据初始化自己的应用;
②Nacos【我们使用】
Nacos可以与spring、springboot、springcloud集成,并且能够代替spring cloud Eureka、spring cloud config,通过nacos server和spring-cloud-starter-alibaba-nacos-config实现配置的动态变更。
nacos的优点:系统配置的集中管理(编辑、存储、分发)、动态更新不重启、支持回滚设置(变更管理、变更审计、历史版本管理)
5.12.3Nacos分布式配置的实现
①在nacos服务器(localhost:8848)创建统一的配置文件
②在微服务(例service-statistics)中读取nacos server的配置文件
③springboot配置文件的加载顺序
④使用命名空间可以切换环境(dev、test、prod)
⑤加载多个配置文件,并实现动态更新不重启
远程仓库有github、git等,我们选择git码云
①登录gitee官网创建远程仓库
②提交代码到本地仓库
准备工作:安装git、在idea配置git环境
创建本地仓库
将代码提交到本地仓库
③提交到远程仓库
添加远程仓库地址
将本地Git库内容,添加至远程Git库
打包(jar)、运行
①linux系统安装:Java环境(jdk环境)、maven、git、docker、jenkins
启动war包
访问jenkins管理页面:http://【linux的ip地址】:【jenkins端口号】8080;安装插件
②jenkins环境配置:配置jdk、maven、git环境
③修改自己的工程项目(guli_parent)
需要Dockerfile文件
在项目pom文件添加打包类型 和 maven插件
④在jenkins管理界面创建自动化任务
新建item
在item中指定远程仓库的地址(从远程仓库获取代码)
构建Execute shell
启动docker:service docker start
执行作业(item)
看到控制台日志输出
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。