项目总结
项目层次划分
controller
@Autowired注解
spring可以自动帮你把Bean里面引用的对象的setter/getter方法省略,它会自动帮你set/get。
@Autowired注释进行自动注入时,spring容器中匹配的候选Bean数目必须有且仅有一个。
当找不到一个匹配的Bean时,spring容器将抛出BeanCreationException异常,并指出必须至少拥有一个匹配的Bean。
如果spring容器中拥有多个候选Bean,spring容器在启动时也会抛出BeanCreationException
这个时候就可以借助@Qualifier注释指定注入Bean的名称,这样@Autowired遇到多个候选Bean的问题也就解决了。
Java Bean
- 所有属性为private
- 提供默认构造方法
- 提供getter和setter
- 实现serializable接口
是一种符合一定规范编写的Java类,不是一种技术,而是一种规范
@RestController
里面的@Controller表示spring管理的对象,@ResponseBody还表示里面的内容需要返回数据
@RequestMapping("")
返回路径
@ApiOperation(value = “”)
在swagger中标注方法的作用
@GetMapping和@PostMapping和@DeleteMapping
访问方法的路径
entity
作用是跟数据库中的表做映射
implements Serializable
必要性:Java中,一切都是对象,在分布式环境中经常需要将Object从这一端或设备传递到另一端。这就需要有一种可以在两端传输数据的协议。Java序列化机制就是为了解决这个问题而产生的。
Java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据
@Data
生成getter,setter,toString,hashCode,equals方法
@TableId()
主键注解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dq6joa6N-1625484227988)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210629151349111.png)]
@ApiModelProperty()
用于方法,字段; 表示对model属性的说明或者数据操作更改
@TableLogic
在字段上加上这个注解再执行BaseMapper的删除方法时,删除方法会变成修改,表示逻辑删除(要先引入逻辑删除插件)
@TableField(value)
当数据库中的表字段名和实体设计的属性名不一致,我们可以通过@TableField的value来映射
以及有些属性字段不需要映射到数据库,仅仅系统里临时用,或者记录等功能的时候,我们有可以通过@TableField的exist属性来配置;
自动填充:
fill = FieleFill.INSERT //创建时间
fill = FiledFill.INSERT_UPDATE //更新时间
config
config是指基于Java配置的spring
@Configuration
在类上打上这一标签,表示这个类是配置类
@Bean
这个配置等同于之前在Xml里的配置
<bean id="objectMapper" class="org.codehaus.jackson.map.ObjectMapper" />
mapper
传统的方式是pojo dao(连接Mybatis,配置mapper.xml文件)servcie controller
Mybatis框架的映射,继承了BaseMapper(通用的Mapper)
service
@Service(“userService”)
告诉Spring,当Spring要创建UserServiceImpl的实例时,bean的名字必须叫做"userService",这样当Action需要使用UserServiceImpl的实例时,就可以由Spring创建好的"userService",然后注入给Action
基本配置
编写配置文件
#服务端口
spring.port=
#服务名
spring.application.name=
#环境设置:dev、test、prod
spring.profiles.active=
#mysql数据库连接
spring.datasource.drive-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.activemq.password=root123
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
使用代码生成器生成相关的各个类
CodeGenerate.java生成
统一结果返回
统一返回数据格式:
项目中我们会将响应封装成json返回,一般我们会将所有的接口的数据格式统一,使得前端对数据的操作更一致轻松
一般情况下,统一返回数据格式没有固定的格式,只要能描述清楚返回的数据状态以及要返回的具体数据就可以。但是一般会包含如下状态码、返回消息、数据这几部分内容
{
"success":布尔 //响应是否成功
"code":数字 //响应码
"message": 字符串 //返回消息
"data": HashMap //返回数据,放在键值对中
}
1、创建interface,定义数据返回状态码
public interface ResultCode {
public static Integer SUCCESS = 20000;//成功
public static Integer ERROR = 20001;//失败
}
2、创建结果类
使用链式编程
this关键字的应用:
1.this调用本类中的属性,也就是类中的成员变量
2.this调用本类中的其他方法
3.this调用本类中其他构造方法,调用时要放在构造方法的首行
public class Student { //定义一个类,类的名字为student。
public Student() { //定义一个方法,名字与类相同故为构造方法
this(“Hello!”);
}
public Student(String name) { //定义一个带形式参数的构造方法
}
}
this关键字除了可以调用成员变量之外,还可以调用构造方法。
在一个Java类中,其方法可以分为成员方法和构造方法两种。构造方法是一个与类同名的方法,在Java类中必须存在一个构造方法。如果在代码中没有显示的体现构造方法的话,那么编译器在编译的时候会自动添加一个没有形式参数的构造方法。这个构造方法跟普通的成员方法还是有很多不同的地方。如构造方法一律是没有返回值的,而且也不用void关键字来说明这个构造方法没有返回值。而普通的方法可以有返回值、也可以没有返回值,程序员可以根据自己的需要来定义。不过如果普通的方法没有返回值的话,那么一定要在方法定义的时候采用void关键字来进行说明。其次构造方法的名字有严格的要求,即必须与类的名字相同。也就是说,Java编译器发现有个方法与类的名字相同才把其当作构造方法来对待。而对于普通方法的话,则要求不能够与类的名字相同,而且多个成员方法不能够采用相同的名字。在一个类中可以存在多个构造方法,这些构造方法都采用相同的名字,只是形式参数不同。Java语言就凭形式参数不同来判断调用那个构造方法。
在上面这段代码中,定义了两个构造方法,一个带参数,另一个没有带参数。构造方法都不会有返回值,不过由于构造方法的特殊性,为此不必要在构造方法定义时带上void关键字来说明这个问题。在第一个没有带参数的构造方法中,使用了this(“Hello!”)这句代码,这句代码表示什么含义呢?在构造方法中使this关键字表示调用类中的构造方法。如果一个类中有多个构造方法,因为其名字都相同,跟类名一致,那么这个this到底是调用哪个构造方法呢?其实,这跟采用其他方法引用构造方法一样,都是通过形式参数来调用构造方法的。如上例中,**this关键字后面加上了一个参数,那么就表示其引用的是带参数的构造方法。如果现在有三个构造方法,分别为不带参数、带一个参数、带两个参数。那么Java编译器会根据所传递的参数数量的不同,来判断该调用哪个构造方法。**从上面示例中可以看出,this关键字不仅可以用来引用成员变量,而且还可以用来引用构造方法。
不过如果要使用这种方式来调用构造方法的话,有一个语法上的限制。一般来说,利用this关键字来调用构造方法,只有在无参数构造方法中第一句使用this调用有参数的构造方法。否则的话,翻译的时候,就会有错误信息。这跟引用成员变量不同。如果引用成员变量的话,this关键字是没有位置上的限制的。如果不熟悉这个限制的话,那么还是老老实实的采用传统的构造方法调用方式为好。虽然比较麻烦,但是至少不会出错。
@Data
public class R {
@ApiModelProperty(value = "是否成功")
private Boolean success;
@ApiModelProperty(value = "返回码")
private Integer code;
@ApiModelProperty(value = "返回消息")
private String message;
@ApiModelProperty(value = "返回数据")
private Map<String, Object> data = new HashMap<String, Object>();
//私有化构造方法
private R(){}
//成功静态方法
public static R ok(){
R r = new R();
r.setSuccess(true);
r.setCode(ResultCode.SUCCESS);
r.setMessage("成功");
return r;
}
//失败静态方法
public static R error(){
R r = new R();
r.setSuccess(false);
r.setCode(ResultCode.ERROR);
r.setMessage("失败");
return r;
}
public R success(Boolean success){
this.setSuccess(success);
return this;
}
public R message(String message){
this.setMessage(message);
return this;
}
public R code(Integer code){
this.setCode(code);
return this;
}
public R data(String key, Object value){
this.data.put(key, value);
return this;
}
public R data(Map<String, Object> map) {
this.setData(map);
return this;
}
}
编写讲师方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-stPdF4ix-1625484227991)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210630192809153.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VOplx5MM-1625484227993)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210630193845267.png)]
查询所有数据并以List的形式返回
@ApiOperation(value = "查询所有的讲师列表")
@GetMapping("findAll")
public R findAll(){
//list(wrapper)以集合的形式返回数据
List<EduTeacher> list = teacherService.list(null);
return R.ok().data("items",list);
}
逻辑删除讲师
@ApiOperation("根据id逻辑删除讲师")
@GetMapping("deleteTeacher")
public R deleteTeacher(@ApiParam(name = "id",value = "讲师id", required = true)
@PathVariable String id){
boolean flag = teacherService.removeById(id);
if (flag){
return R.ok();
}else {
return R.error();
}
}
@ApiParams
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yr20pMGi-1625484227995)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210629193646130.png)]
分页查询讲师方法
@ApiOperation("实现分页功能")
@GetMapping("PageTeacher/{current}/{limit}")
public R pageListTeacher(@PathVariable long current,
@PathVariable long limit){
Page<EduTeacher> teacherPage = new Page<>(current, limit);
//把分页所有数据封装到pageTeacher对象里面
teacherService.page(teacherPage, null);
//总记录数
long total = teacherPage.getTotal();
//数据list集合
List<EduTeacher> records = teacherPage.getRecords();
return R.ok().data("total",total).data("rows",records);
}
其中R.ok().data(“total”,total).data(“rows”,records)等价于
Map map = new HashMap();
map.put("total",total);
map.put("row",records);
return R.ok().data(map);
分页条件查询讲师方法
controller类:
@PostMapping("pageTeacherCondition/{page}/{limit}")
public R pageTeacherCondition(@PathVariable long page,
@PathVariable long limit,
@ApiParam(name = "teacherQuery", value = "查询对象")
@RequestBody(required = false) TeacherQuery teacherquery){
//创建page对象
Page<EduTeacher> eduTeacherPage = new Page<>(page,limit);
//调用pageQuery方法
teacherService.pageQuery(eduTeacherPage, teacherquery);
//总记录数
long total = eduTeacherPage.getTotal();
//list集合存储
List<EduTeacher> records = eduTeacherPage.getRecords();
return R.ok().data("total",total).data("rows",records);
}
其中pageQuery方法的代码为:
@Override
public void pageQuery(Page<EduTeacher> pageParam, TeacherQuery teacherQuery) {
//初始化条件
QueryWrapper<EduTeacher> teacherQueryWrapper = new QueryWrapper<>();
//根据Sort进行升序排列
teacherQueryWrapper.orderByAsc("sort");
//判断是否有条件
if (teacherQuery == null){
//得到查询分页的数据
baseMapper.selectPage(pageParam, teacherQueryWrapper);
//不返回的话,teacherQuery可能会出现空指针异常
return;
}
//得到teacherQuery里的四个所需属性值
String name = teacherQuery.getName();
Integer level = teacherQuery.getLevel();
String begin = teacherQuery.getBegin();
String end = teacherQuery.getEnd();
//根据四个属性来查询
//StringUtil方法操作对象是String类型的对象,如果值为空是安全的
//判断为空的标准是str == null或 str.length()==0
//判断名字
if (!StringUtils.isEmpty(name)){
teacherQueryWrapper.like("name",name);
}
//是否等于level
if (!StringUtils.isEmpty(level)){
teacherQueryWrapper.eq("level",level);
}
//大于设定的开始时间
if (!StringUtils.isEmpty(begin)){
teacherQueryWrapper.ge("gmt_create", begin);
}
//小于设定的结束时间
if (!StringUtils.isEmpty(end)){
teacherQueryWrapper.le("gmt_create", end);
}
//结果按时间降序排列
teacherQueryWrapper.orderByDesc("gmt_create");
//得到查询分页数据
baseMapper.selectPage(pageParam, teacherQueryWrapper);
}
添加讲师方法
@ApiOperation("新增讲师")
@PostMapping("addTeacher")
public R addTeacher(@RequestBody EduTeacher eduTeacher){ //RequestBody 接收的是请求体里面的数据
//保存传入的eduTeacher
boolean save = teacherService.save(eduTeacher);
if (save){
return R.ok();
}else {
return R.error();
}
}
根据id查询讲师
@ApiOperation("根据Id查询讲师")
@GetMapping("getTeacherById/{id}")
public R getTeacherById(@PathVariable String id){
EduTeacher eduTeacher = teacherService.getById(id);
return R.ok().data("teacher", eduTeacher);
}
修改讲师
@ApiOperation("修改讲师")
@PostMapping("updateTeacher")
public R updateTeacher(@RequestBody EduTeacher eduTeacher){
boolean flag = teacherService.updateById(eduTeacher);
if (flag){
return R.ok();
}else {
return R.error();
}
}
统一处理
自动填充封装
在Service-base模块中
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* @Despriction 插入数据时自动填充
* @Author qt
* @Date 2021/7/1 20:49
*/
@Override
public void insertFill(MetaObject metaObject) {
//属性名称,不是字段名称
this.setFieldValByName("gmtCreate", new Date(), metaObject);
this.setFieldValByName("gmtModified", new Date(), metaObject);
}
/**
* @Despriction 更新时自动插入
* @Author qt
* @Date 2021/7/1 20:52
*/
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("gmtModified", new Date(), metaObject);
}
}
在实体类添加自动填充注解
@ApiModelProperty(value = "创建时间")
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;
@ApiModelProperty(value = "更新时间")
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date gmtModified;
统一异常处理
让异常结果显示为统一的返回结果对象,并且统一处理系统的异常信息
@ControllerAdvice
public class GlobalExceptionHandler {
//指定出现什么异常都会执行这个方法
@ExceptionHandler(Exception.class)
@ResponseBody //为了能够返回数据
public R error(Exception e){
e.printStackTrace();
return R.error().message("执行了全局异常处理");
}
//指定异常处理
@ExceptionHandler(ArithmeticException.class)
@ResponseBody //为了能够返回数据
public R error(ArithmeticException e){
e.printStackTrace();
return R.error().message("执行了ArithmeticException异常处理");
}
//自定义异常
@ExceptionHandler(GuliException.class)
@ResponseBody //为了能够返回数据
public R error(GuliException e){
e.printStackTrace();
return R.error().code(e.getCode()).message(e.getMsg());
}
}
@ControllerAdvice
可以实现三个方面的功能
1、全局异常处理
2、全局数据绑定
3、全局数据预处理
@Data
@AllArgsConstructor //生成有参数的构造方法
@NoArgsConstructor //生成无参构造方法
public class GuliException extends RuntimeException{
private Integer code;// 状态码
private String msg; //异常信息
}
上传文件
上传功能
基本配置
新建模块service-oss,引入相关依赖
<dependencies>
<!-- 阿里云oss依赖 -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<!-- 日期工具栏依赖 -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
</dependencies>
配置application.properties
#服务端口
server.port=8002
#服务名
spring.application.name=service-oss
#环境设置:dev、test、prod
spring.profiles.active=dev
#阿里云 OSS
#不同的服务器,地址不同
aliyun.oss.file.endpoint=your endpoint
aliyun.oss.file.keyid=your accessKeyId
aliyun.oss.file.keysecret=your accessKeySecret
#bucket可以在控制台创建,也可以使用java代码创建
aliyun.oss.file.bucketname=guli-file
创建启动类
@SpringBootApplication
@ComponentScan({"com.atguigu"})
public class OssApplication {
public static void main(String[] args) {
SpringApplication.run(OssApplication.class, args);
}
}
启动时会报错,springboot会默认加载org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration这个类,而DataSourceAutoConfiguration类使用了@Configuration注解向spring注入了dataSource bean,又因为项目(oss模块)中并没有关于dataSource相关的配置信息,所以当spring创建dataSource bean时因缺少相关的信息就会报错。
解决方法:
在@SpringBootApplication注解上加上exclude,解除自动加载DataSourceAutoConfiguration
实现文件上传
1、从配置文件读取常量
创建常量读取工具类
//常量类,读取配置文件application.properties中的配置
@Component
public class ConstantPropertiedUtils implements InitializingBean {
//读取配置文件内容
@Value("${aliyun.oss.file.endpoint}")
private String endpoint;
@Value("${aliyun.oss.file.keyid}")
private String keyId;
@Value("${aliyun.oss.file.keysecret}")
private String keySecret;
@Value("${aliyun.oss.file.bucketname}")
private String bucketName;
//定义公开静态常量
public static String END_POINT;
public static String ACCESS_KEY_ID;
public static String ACCESS_KEY_SECRET;
public static String BUCKET_NAME;
@Override
public void afterPropertiesSet() throws Exception {
END_POINT = endpoint;
ACCESS_KEY_ID = keyId;
ACCESS_KEY_SECRET = keySecret;
BUCKET_NAME = bucketName;
}
}
implements initialzingBean
用spring的initialzingBean的afterPropertiesSet来初始化配置信息,这个方法将在所有的属性被初始化后调用
@Value
使用@Value读取application.properties里的配置内容
编写文件上传方法,参考官方文档
控制层:
@Api(tags = "阿里云文件管理")
@RestController
@RequestMapping("/eduoss/fileoss")
@CrossOrigin
public class OssController {
//注入service方法
@Autowired
private OssService ossService;
//上传头像的方法
@PostMapping
public R uploadOssFile(MultipartFile file){
//获取上传文件 MultipartFile
//返回上传到oss的路径
String url = ossService.uploadFileAvatar(file);
return R.ok().data("url", url);
}
}
在service中创建uploadFileAvatar方法,并在impl中实现
@Service
public class OssServiceImpl implements OssService {
//上传文件到oss
@Override
public String uploadFileAvatar(MultipartFile file) {
//工具类获取值
String endpoint = ConstantPropertiedUtils.END_POINT;
String accessKeyId = ConstantPropertiedUtils.ACCESS_KEY_ID;
String accessKeySecret = ConstantPropertiedUtils.ACCESS_KEY_SECRET;
String bucketName = ConstantPropertiedUtils.BUCKET_NAME;
try{
// 创建OSSClient实例
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
//获取上传文件的输入流
InputStream inputStream = file.getInputStream();
//获取文件名称
String fileName = file.getOriginalFilename();
// 在文件名称里面添加随机唯一的值
String uuid = UUID.randomUUID().toString().replaceAll("-","");
//
fileName = uuid + fileName;
//把文件按照日期进行分类
//获取当前日期
String datePath = new DateTime().toString("yyyy/MM/dd");
//拼接
fileName = datePath+ "/"+ fileName;
// 调用oss方法实现上传
//第一个参数 Bucket名称 第二个参数 上传到oss文件路径和文件名称
//第三个参数 上传文件输入流
ossClient.putObject(bucketName,fileName,inputStream);
// 关闭OSSClient。
ossClient.shutdown();
//把上传之后的文件路径返回
//需要把上传到阿里oss路径手动拼接出来
String url = "http://"+ bucketName + "." +endpoint+"/"+ fileName;
return url;
}catch (Exception e){
e.printStackTrace();
return null;
}
}
}
MultipartFile
MultipartFile为org.springframework.web.mutipart包下的一个类,也就是说如果想使用MultipartFile这个类必须引入spring框架,一般来讲,使用MultipartFile这个类主要是实现以表单的形式进行文件上传功能
课程
课程导入
使用easyexcel批量导入课程
1、引入esayexcel依赖
2、创建和Excel对应的实体类
@Data
public class ExcelSubjectData {
@ExcelProperty(index = 0)
//对应excel的第一列
private int oneSubjectName;
@ExcelProperty(index = 1)
//对应excel的第二列
private String twoSubjectName; }
3、创建对应的控制类
//获取上传过来的文件,把文件内容读取出来
@ApiOperation(value = "添加课程分类")
@PostMapping("addSubject")
public R addSubject(MultipartFile file){
//1 获取上传的excel文件 MultipartFile
//返回错误提示消息
subjectService.saveSubject(file, subjectService);
return R.ok();
}
4、接口及实现类
//添加课程分类
@Override
public void saveSubject(MultipartFile file, EduSubjectService subjectService) {
try{
//文件输入流
InputStream in = file.getInputStream();
//调用方法进行读取
//这里需要指定用哪个class去读,然后读取第一个sheet 文件流会自动关闭
EasyExcel.read(in, SubjectData.class,new SubjectExcelListener(subjectService)).sheet().doRead();
}catch (Exception e){
e.printStackTrace();
}
}
文件流
按照流向划分分为输入流和输出流
输入流:就是以程序为中心点,硬盘上的文件内容流入到程序中可以存储数据的东西中比如说数组,用read方法
输出流:就说以程序为中心点,程序中的数组或者其他的可以存储数据的东西中的内容输出到硬盘文件中,用write方法,需要用flush方法刷新一下,确保程序中的数组内容写出到硬盘文件中。
InputStream
InputStream类是字节输入流的抽象类,不能通过new关键字来创建该实例对象,需要其子类创建该实例对象。
5、创建读取excel监听器
public class SubjectExcelListener extends AnalysisEventListener<SubjectData> {
//因为SubjectExcelListener不能交给spring进行管理,需要自己new,不能注入其他对象
public EduSubjectService subjectService;
public SubjectExcelListener(){}
//创建有参数构造,传递subjectService用于操作数据库
public SubjectExcelListener(EduSubjectService subjectService){
this.subjectService = subjectService;
}
//判断一级分类不能重复添加
private EduSubject existOneSubject(EduSubjectService subjectService,String name){
QueryWrapper<EduSubject> wrapper = new QueryWrapper<>();
wrapper.eq("title",name);
wrapper.eq("parent_id","0");
EduSubject oneSubject = subjectService.getOne(wrapper);
return oneSubject;
}
//判断二级分类不能重复添加
private EduSubject existTwoSubject(EduSubjectService subjectService,String name,String pid){
QueryWrapper<EduSubject> wrapper = new QueryWrapper<>();
wrapper.eq("title",name);
wrapper.eq("parent_id",pid);
EduSubject twoSubject = subjectService.getOne(wrapper);
return twoSubject;
}
//读取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);
}
//获取一级分类的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);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}
读取之前导入的课程
创建一级分类和二级分类的实体类:
//一级分类
@Data
public class OneSubject {
private String id;
private String title;
//一个一级分类有多个二级分类
private List<TwoSubject> children = new ArrayList<>();
}
//二级分类
@Data
public class TwoSubject {
private String id;
private String title;
}
创建控制类:
//课程分类列表(树形)
@GetMapping("getAllSubject")
public R getAllSubject(){
//list集合泛型是一级分类
List<OneSubject> list = subjectService.getAllOneTwoSubject();
return R.ok().data("list",list);
}
创建其接口和实现类
//课程分类列表(树形)
@Override
public List<OneSubject> getAllOneTwoSubject() {
//1查询出所有一级分类 parent_id=0
QueryWrapper<EduSubject> wrapper = new QueryWrapper<>();
wrapper.eq("parent_id", "0");
List<EduSubject> oneSubjectList = baseMapper.selectList(wrapper);
//2查询出所有二级分类 parent_id!=0
QueryWrapper<EduSubject> wrapper2 = new QueryWrapper<>();
wrapper2.ne("parent_id", "0");
List<EduSubject> twoSubjectList = baseMapper.selectList(wrapper2);
//3封装一级分类
List<OneSubject> finnalList = new ArrayList<>();
for (EduSubject eduSubject : oneSubjectList) {
//new OneSubject设置值,add加入list
OneSubject oneSubject = new OneSubject();
// oneSubject.setId(eduSubject.getId());
// oneSubject.setTitle(eduSubject.getTitle());
//复制操作
BeanUtils.copyProperties(eduSubject, oneSubject);
finnalList.add(oneSubject);
//4封装二级分类
//创建list集合封装每一个一级分类的二级分类
ArrayList<TwoSubject> twoFinnalList = new ArrayList<>();
for (EduSubject eduSubject2 : twoSubjectList) {
if (eduSubject.getId().equals(eduSubject2.getParentId())) {
TwoSubject twoSubject = new TwoSubject();
//如过一级分类的id==二级分类的parent_id,进行封装
BeanUtils.copyProperties(eduSubject2, twoSubject);
twoFinnalList.add(twoSubject);
}
}
oneSubject.setChildren(twoFinnalList);
}
return finnalList;
}
BeanUtils.copyProperties
如果有两个具有很多相同属性的JavaBean,传统方法是使用user.setUsername(uForm.getUsername)来将相同属性逐一赋值,如果使用BeanUtils.Properties()方法,可以将相同名称的属性进行赋值,如果存在名称不相同的属性,则BeanUtils不对这些属性进行处理,需要手动处理。
添加课程基本信息
创建课程基本信息的实体类:
@Data
public class CourseInfoVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "课程ID")
private String id;
@ApiModelProperty(value = "课程讲师ID")
private String teacherId;
@ApiModelProperty(value = "课程专业ID")
private String subjectId;
@ApiModelProperty(value = "一级分类ID")
private String subjectParentId;
@ApiModelProperty(value = "课程标题")
private String title;
@ApiModelProperty(value = "课程销售价格,设置为0则可免费观看")
//精度0.01
private BigDecimal price;
@ApiModelProperty(value = "总课时")
private Integer lessonNum;
@ApiModelProperty(value = "课程封面图片路径")
private String cover;
@ApiModelProperty(value = "课程简介")
private String description;
}
implements Serializable
实现Serialzable接口的目的是为类可持久化,比如在网络传输或本地存储,为系统的分布和异构部署提供先决条件,若没有序列化,现在我们熟悉的远程调用、对象数据库都不可能存在
serialVersionUID
serialVersionUID适用于java序列化机制,简单来说,Java序列化机制是通过判断类的serialVersionUID来验证的版本一致的。在进行反序列化时,JVM会把传来的字节流中的serialVerisonUID于本地相应实体类的serialVersionUID进行比较,如果相同说明是一致的,可以进行反序列化,否则会出现反序列化版本一致的异常,即是InvalidCastException
serialVersionUID有两种显示的生成方式:
1、默认的1L
2、根据包名、类名、继承关系,非私有的方法和属性、以及参数,返回值等诸多因子计算得出的,极度复杂生成的一个64位哈希字段,基本上计算出来的这个值是唯一的。
controller类:
//添加课程基本信息的方法
@PostMapping("addCourseInfo")
public R addCourseInfo(@RequestBody CourseInfoVo courseInfoVo){
String id = eduCourseService.saveCourseInfo(courseInfoVo);
//返回添加之后的课程id,为了后面添加大纲使用
return R.ok().data("courseId",id);
}
创建其接口和实现类:
@Override
public String saveCourseInfo(CourseInfoVo courseInfoVo) {
// 向课程表添加课程基本信息
//CourseInfoVo对象转换eduCourse对象
EduCourse eduCourse = new EduCourse();
//将courseInfoVo的相同属性给educourse赋值
BeanUtils.copyProperties(courseInfoVo, eduCourse);
int insert = baseMapper.insert(eduCourse);
if (insert <= 0){
//添加失败
throw new GuliException(20001,"添加课程信息失败");
}
//获取添加之后课程id
String cid = eduCourse.getId();
// 向eduCourseDescription中添加课程简介
EduCourseDescription courseDescription = new EduCourseDescription();
BeanUtils.copyProperties(courseInfoVo, courseDescription);
//设置描述的id就是课程id
courseDescription.setId(cid);
//保存描述记录
courseDescriptionService.save(courseDescription);
return cid;
}
根据id获取信息
controller类:
@ApiOperation("根据课程id查询课程基本信息")
@GetMapping("getCourseInfo/{courseId}")
public R getCourseInfo(@PathVariable String courseId){
CourseInfoVo courseInfoVo = eduCourseService.getCourseInfo(courseId);
return R.ok().data("courseInfoVo", courseInfoVo);
}
接口及其实现类:
@Override
public CourseInfoVo getCourseInfo(String courseId) {
// 涉及到两张表 eduCourse 和 eduCourseDescription
// 先根据Id查询eduCourse里的信息
EduCourse eduCourse = baseMapper.selectById(courseId);
CourseInfoVo courseInfoVo = new CourseInfoVo();
// 将其值复制到courseInfoVo中
BeanUtils.copyProperties(eduCourse,courseInfoVo);
// 查询描述表
EduCourseDescription courseDescription = courseDescriptionService.getById(courseId);
// 将描述复制到courseInfoVo中
courseInfoVo.setDescription(courseDescription.getDescription());
return courseInfoVo;
}
修改课程信息
控制类:
@ApiOperation("修改课程信息")
@PostMapping("updateCourseInfo")
public R updateCourseId(@RequestBody CourseInfoVo courseInfoVo){
eduCourseService.updateCourseInfo(courseInfoVo);
return R.ok();
}
接口及其实现类:
@Override
public void updateCourseInfo(CourseInfoVo courseInfoVo) {
//修改课程表
EduCourse eduCourse = new EduCourse();
BeanUtils.copyProperties(courseInfoVo,eduCourse);
int update = baseMapper.updateById(eduCourse);
if (update == 0){
throw new GuliException(20001,"修改课程信息失败");
}
//修改描述表
EduCourseDescription description = new EduCourseDescription();
description.setId(courseInfoVo.getId());
description.setDescription(courseInfoVo.getDescription());
boolean update1 = courseDescriptionService.updateById(description);
if (!update1){
throw new GuliException(20001,"修改课程信息失败");
}
}
根据id查询课程发布信息
方式1:业务层组装多个表多次的查询结果
方式2:数据访问层进行关联查询
采用方式2实现:
定义Vo
@Data
@ApiModel(value = "课程发布信息")
public class CoursePublishVo implements Serializable {
@ApiModelProperty(value = "课程Id")
private String id;
@ApiModelProperty(value = "标题")
private String title;
@ApiModelProperty(value = "头像")
private String cover;
@ApiModelProperty(value = "课程数量")
private Integer lessonNum;
@ApiModelProperty(value = "一级目录")
private String subjectLevelOne;
@ApiModelProperty(value = "二级目录")
private String subjectLevelTwo;
@ApiModelProperty(value = "讲师姓名")
private String teacherName;
@ApiModelProperty(value = "价格")
private String price;
}
controller类:
@ApiOperation("根据课程id查询课程")
@GetMapping("getPublishCourseInfo/{id}")
public R getPublishCourseInfo(@PathVariable String id){
CoursePublishVo coursePublishVo = eduCourseService.publishCourseInfo(id);
return R.ok().data("publishCourse",coursePublishVo);
}
在EduCourseMapper里创建接口
CoursePublishVo getPublishCourseInfo(String id);
根据下图来编写sql语句:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yFzFD5AW-1625484227997)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210703163156711.png)]
实现Mapper为
<mapper namespace="com.yutou.eduservice.mapper.EduCourseMapper">
<!--sql语句:根据课程id查询课程确认信息-->
<select id="getPublishCourseInfo" resultType="com.yutou.eduservice.entity.vo.CoursePublishVo">
SELECT
c.title,
c.cover,
c.lesson_num AS lessonNum,
CONVERT(c.price, DECIMAL(8,2)) AS price,
s1.title AS subjectLevelOne,
s2.title AS subjectLevelTwo,
t.name AS teacherName
FROM
edu_course c
LEFT JOIN edu_teacher t ON c.teacher_id = t.id
LEFT JOIN edu_subject s1 ON c.subject_parent_id = s1.id
LEFT JOIN edu_subject s2 ON c.subject_id = s2.id
WHERE
c.id = #{id}
</select>
实现接口及类内容为:
CoursePublishVo publishCourseInfo(String id);
@Override
public CoursePublishVo publishCourseInfo(String id) {
//调用mapper
CoursePublishVo publishCourseInfo = baseMapper.getPublishCourseInfo(id);
return publishCourseInfo;
}
报错:
问题是dao层编译后只有class文件,没有mapper.xml,因为maven工程在默认情况下src/main/java目录下的所有资源文件是不发布到target目录下的
解决方案:
在项目的pom中配置如下节点
<!-- 项目打包时会将java目录中的*.xml文件也进行打包 -->
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml
</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
在springboot配置文件中添加配置
#配置mapper xml文件的路径
mybatis-plus.mapper-location=classpath:com/guli/edu/mapper/xml/*.xml
根据id发布课程
controller:
@ApiOperation(value = "根据id发布课程")
@PostMapping("publishCourse/{id}")
public R publishCourse(@PathVariable String id){
boolean b = eduCourseService.publishCourseById(id);
if (b){
return R.ok();
}else {
return R.error();
}
}
接口及其实现类:
@Override
public boolean publishCourseById(String id) {
EduCourse course = new EduCourse();
//设置课程id和状态
course.setId(id);
course.setStatus("Normal");
//返回值,判断是否更新成功
int count = baseMapper.updateById(course);
return count > 0;
}
分页条件查询课程列表
controller类:
@ApiOperation(value = "条件查询分页讲师列表")
@PostMapping("getCourseList/{current}/{limit}")
public R getCourseList(@PathVariable long current,
@PathVariable long limit,
@RequestBody(required = false) CourseQuery courseQuery){
Page<EduCourse> eduCoursePage = new Page<>(current, limit);
//把分页所有数据封装到coursePage里
eduCourseService.pageQuery(eduCoursePage, courseQuery);
//总记录数
long total = eduCoursePage.getTotal();
//数据list集合
List<EduCourse> records = eduCoursePage.getRecords();
return R.ok().data("total",total).data("records",records);
}
接口及其实现类:
/**
* @Param [eduCoursePage, courseQuery]
* @return void
* @Despriction 根据条件查询分页
* @Author qt
* @Date 2021/7/2 14:27
* TODO 完成价格浏览量销量的范围查询
*/
@Override
public void pageQuery(Page<EduCourse> eduCoursePage, CourseQuery courseQuery) {
//初始化条件
QueryWrapper<EduCourse> courseQueryWrapper = new QueryWrapper<>();
if (courseQuery == null){
//得到查询分页的数据
baseMapper.selectPage(eduCoursePage, courseQueryWrapper);
//不返回的话,courseQuery可能会出现空指针异常
return;
}
//得到courseQuery里面的四个所需属性值
String name = courseQuery.getName();
Integer price = courseQuery.getPrice();
Integer views = courseQuery.getViews();
Integer sales = courseQuery.getSales();
//根据四个属性来查询
if (!StringUtils.isEmpty(name)){
courseQueryWrapper.like("title",name);
}
if (!StringUtils.isEmpty(price)){
courseQueryWrapper.ge("price",price);
}
if (!StringUtils.isEmpty(price)){
courseQueryWrapper.le("price",price);
}
if (!StringUtils.isEmpty(views)){
courseQueryWrapper.ge("view_count",views);
}
if (!StringUtils.isEmpty(sales)){
courseQueryWrapper.ge("buy_count",sales);
}
//结果按浏览量降序排列
courseQueryWrapper.orderByDesc("view_count");
//得到查询分页结果
baseMapper.selectPage(eduCoursePage, courseQueryWrapper);
}
条件查询的实体类为:
@ApiModel(value = "Course条件查询对象", description = "课程查询对象封装")
@Data
public class CourseQuery implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "课程名称,模糊查询")
private String name;
@ApiModelProperty(value = "价格")
private Integer price;
@ApiModelProperty(value = "浏览量")
private Integer views;
@ApiModelProperty(value = "销量")
private Integer sales;
}
删除课程
controller类:
@ApiOperation(value = "删除课程")
@DeleteMapping("{courseId}")
public R deleteCourse(@PathVariable String courseId){
boolean result = eduCourseService.removeCourse(courseId);
if (result){
return R.ok();
}else {
return R.error().message("删除失败");
}
}
接口及其实现类:
如果确定删除,则首先删除video记录,然后删除chapter记录,最后删除Course记录
一个课程可以有多个video,多个chapter所以需要逐一删除
删除课程的同时删除云端资源
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R3ObzkW9-1625484228000)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210703194656444.png)]
@Override
public boolean removeCourse(String courseId) {
// 根据课程id删除小节
eduVideoService.removeVideoByCourseId(courseId);
//根据课程id删除章节
eduChapterService.removeChapterByCourseId(courseId);
//根据课程id删除描述 一对一 所以可以直接删除
courseDescriptionService.removeById(courseId);
// 根据课程id删除课程本身
int result = baseMapper.deleteById(courseId);
return result > 0;
}
其中removeVideoByCourseId(courseId)实现类为:
@Override
public void removeVideoByCourseId(String courseId) {
// 根据课程Id查询课程所有的视频id
QueryWrapper<EduVideo> wrapperVideo = new QueryWrapper<>();
wrapperVideo.eq("course_id",courseId);
wrapperVideo.select("video_source_id");
List<EduVideo> eduVideoList = baseMapper.selectList(wrapperVideo);
//List<EduVideo>变成List<String>
List<String> videoIds = new ArrayList<>();
for (EduVideo eduVideo : eduVideoList) {
String videoSourceId = eduVideo.getVideoSourceId();
//如果videoSourceId不为空
if (!StringUtils.isEmpty(videoSourceId)) {
//放到videoIds集合里面
videoIds.add(videoSourceId);
}
// 放到VideoIds集合里面
if (videoIds.size() > 0) {
videoIds.removeVideoList(videoSourceIdList);
}
}
//根据多个视频id删除多个视频,批量删除
vodClient.deleteBatch(videoIds);
QueryWrapper<EduVideo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("course_Id",courseId);
baseMapper.delete(queryWrapper);
}
removeChapterByCourseId(courseId)的实现类:
直接删除chapter即可,chapter下没有子课程
//根据id删除章节
@Override
public void removeChapterByCourseId(String courseId) {
QueryWrapper<EduChapter> wrapper = new QueryWrapper<>();
wrapper.eq("course_id",courseId);
baseMapper.delete(wrapper);
}
上传文件
创建控制类:
@Api(tags = "文件管理")
@RestController
@RequestMapping("/eduoss/fileoss")
public class OssController {
@Autowired
private OssService ossService;
@ApiOperation("文件上传功能")
@PostMapping("upload")
public R uploadOssFile(MultipartFile file){
//获取上传文件 MultipartFile
//返回上传到oss的路径
String url = ossService.uploadFileAvatar(file);
return R.ok().message("文件上传成功").data("url", url);
}
}
接口及实现类:
@Service
public class OssServiceImpl implements OssService {
//上传文件到oss
@Override
public String uploadFileAvatar(MultipartFile file) {
//工具类获取值
String endpoint = ConstantPropertiedUtils.END_POINT;
String accessKeyId = ConstantPropertiedUtils.ACCESS_KEY_ID;
String accessKeySecret = ConstantPropertiedUtils.ACCESS_KEY_SECRET;
String bucketName = ConstantPropertiedUtils.BUCKET_NAME;
try{
// 创建OSSClient实例
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
//获取上传文件的输入流
InputStream inputStream = file.getInputStream();
//获取文件名称
String fileName = file.getOriginalFilename();
// 在文件名称里面添加随机唯一的值
String uuid = UUID.randomUUID().toString().replaceAll("-","");
//
fileName = uuid + fileName;
//把文件按照日期进行分类
//获取当前日期
String datePath = new DateTime().toString("yyyy/MM/dd");
//拼接
fileName = datePath+ "/"+ fileName;
// 调用oss方法实现上传
//第一个参数 Bucket名称 第二个参数 上传到oss文件路径和文件名称
//第三个参数 上传文件输入流
ossClient.putObject(bucketName,fileName,inputStream);
// 关闭OSSClient。
ossClient.shutdown();
//把上传之后的文件路径返回
//需要把上传到阿里oss路径手动拼接出来
String url = "http://"+ bucketName + "." +endpoint+"/"+ fileName;
return url;
}catch (Exception e){
e.printStackTrace();
return null;
}
}
}
章节功能实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nEnKtUUA-1625484228001)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210703153459937.png)]
根据上图创建对应的实体类:
@Data
@ApiModel(value = "章节信息")
public class ChapterVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("章节id")
private String id;
@ApiModelProperty("章节题目")
private String title;
@ApiModelProperty(value = "小节")
private List<VideoVo> children = new ArrayList<>();
}
Data
@ApiModel("课时信息")
public class VideoVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "课时id")
private String id;
@ApiModelProperty(value = "题目")
private String title;
@ApiModelProperty(value = "是否免费")
private Boolean free;
}
查询章节数据
controller类
@ApiOperation(value = "嵌套章节数据列表")
@GetMapping("getChapterVideo/{courseId}")
public R getChapterVideo(@PathVariable String courseId){
List<ChapterVo> list = chapterService.getChapterVideoByCourseId(courseId);
return R.ok().data("allChapterVideo",list);
}
接口及其实现类为:
@Override
public List<ChapterVo> getChapterVideoByCourseId(String courseId) {
// 根据课程id,查询课程里面所有的章节
QueryWrapper<EduChapter> wrapperChapter = new QueryWrapper<>();
wrapperChapter.eq("course_id",courseId);
List<EduChapter> eduChapterList = baseMapper.selectList(wrapperChapter);
// 根据课程id,查询课程里面所有的小节
QueryWrapper<EduVideo> wrapperVideo = new QueryWrapper<>();
wrapperVideo.eq("course_id",courseId);
List<EduVideo> eduVideoList = videoService.list(wrapperVideo);
//创建List集合,用于最终封装数据
List<ChapterVo> finalList = new ArrayList<>();
// 遍历查询章节List集合进行封装
// 遍历查询章节list集合
for (EduChapter eduChapter : eduChapterList) {
//每个章节
//eduChapter对象值复制到ChapterVo里面
ChapterVo chapterVo = new ChapterVo();
BeanUtils.copyProperties(eduChapter, chapterVo);
//把chapterVo放到最终list集合
finalList.add(chapterVo);
//创建结合,用于封装章节的小节
List<VideoVo> videoVoList = new ArrayList<>();
// 遍历查询小节list集合,进行封装
for (EduVideo eduVideo : eduVideoList) {
//得到整个小节
if (eduVideo.getChapterId().equals(eduChapter.getId())) {
//进行封装
VideoVo videoVo = new VideoVo();
BeanUtils.copyProperties(eduVideo, videoVo);
//放到小节封装集合
videoVoList.add(videoVo);
}
}
//把封装之后小节List集合,放到章节对象里面
chapterVo.setChildren(videoVoList);
}
return finalList;
}
删除章节
controller类
@ApiOperation(value = "删除章节")
@DeleteMapping("{chapterId}")
public R deleteChapter(@PathVariable String chapterId){
boolean flag = chapterService.deleteChapterById(chapterId);
if (flag){
return R.ok();
}else {
return R.error();
}
}
接口及其实现类:
@Override
public boolean deleteChapterById(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);
return result>0 ;
}
}
整合阿里云vod实现视频上传
配置
@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_ID;
public static String ACCESS_KEY_SECRET;
@Override
public void afterPropertiesSet() throws Exception {
ACCESS_KEY_ID = keyid;
ACCESS_KEY_SECRET = keysecret;
}
}
复制工具类:
public class InitVodClient {
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;
}
}
上传视频
controller类:
@ApiOperation("上传视频到阿里云")
@PostMapping("uploadAlyVideo")
public R uploadAlyVideo(MultipartFile multipartFile){
//返回上传视频id
String videoId = vodService.uploadVideoAly(multipartFile);
return R.ok().data("videoId",videoId);
}
接口及其实现类:
@Override
public String uploadVideoAly(MultipartFile multipartFile) {
try{
//filename: 上传文件原始名称
String fileName = multipartFile.getOriginalFilename();
//title: 上传之后显示名称
//得到最后一个.的位置
String title = fileName.substring(0, fileName.lastIndexOf("."));
//inputStream: 上传文件输入流
InputStream inputStream = multipartFile.getInputStream();
UploadStreamRequest request = new UploadStreamRequest(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET, title, fileName, inputStream);
UploadVideoImpl uploader = new UploadVideoImpl();
UploadStreamResponse response = uploader.uploadStream(request);
//请求视频点播服务的请求ID
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;
}
}
根据id删除视频
controller类:
@ApiOperation(value = "根据视频id删除阿里云视频")
@DeleteMapping("removeAlyVideo/{id}")
public R removeAlyVideo(@PathVariable String id){
vodService.removeVideo(id);
return R.ok().message("视频删除成功");
}
接口及其实现类:
@Override
public void removeVideo(String id) {
try {
DefaultAcsClient client = InitVodClient.initVodClient(
ConstantVodUtils.ACCESS_KEY_ID,
ConstantVodUtils.ACCESS_KEY_SECRET
);
DeleteVideoRequest request = new DeleteVideoRequest();
request.setVideoIds(id);
DeleteVideoResponse response = client.getAcsResponse(request);
System.out.println("RequestId = " + response.getRequestId() + "\n");
}catch (Exception e){
throw new GuliException(20001, "视频删除失败");
}
}
批量删除视频
controller类:
@ApiOperation(value = "删除多个阿里云视频")
@DeleteMapping("delete-batch")
public R deleteBatch(@RequestParam("videoList") List<String> videoList){
vodService.removeMoreAlyVideo(videoList);
return R.ok();
}
接口及其实现类:
@Override
public void removeMoreAlyVideo(List<String> videoList) {
try{
//初始化对象
DefaultAcsClient client = InitVodClient.initVodClient(
ConstantVodUtils.ACCESS_KEY_ID,
ConstantVodUtils.ACCESS_KEY_SECRET);
//创建删除视频的request对象
DeleteVideoRequest request = new DeleteVideoRequest();
//videoList值转换为 1,2,3的形式
String videoIds = StringUtils.join(videoList.toArray(), ",");
//向request设置视频id
request.setVideoIds(videoIds);
//调用初始化对象的方法实现删除
client.getAcsResponse(request);
}catch (Exception e){
e.printStackTrace();
throw new GuliException(20001,"删除视频失败");
}
}
服务调用
删除课时的同时删除云端视频
在调用端的启动类添加注解
@EnableFeignClients
创建包和接口
//防止在其他位置注入VodClient时报错
@Component
@FeignClient(name = "service-vod",fallback = VodFileDegradeFeignClient.class) //调用的服务名称
public interface VodClient {
@ApiOperation(value = "根据视频id删除阿里云视频")
@DeleteMapping("/eduvod/video/removeAlyVideo/{id}")
public R removeAlyVideo(@PathVariable("id") String id);
@ApiOperation(value = "删除多个阿里云视频")
@DeleteMapping("delete-batch")
public R deleteBatch(@RequestParam("/eduvod/video/videoList") List<String> videoList);
}
controller:
@ApiOperation(value = "删除小节同时删除阿里云视频")
@DeleteMapping("{id}")
public R deleteVideo(@PathVariable String id){
boolean flag = videoService.removeVideoById(id);
if (flag){
return R.ok();
}else {
return R.error().message("删除失败");
}
}
接口及其实现类:
@Override
public boolean removeVideoById(String id) {
//查询云端视频id
EduVideo video = baseMapper.selectById(id);
String videoSourceId = video.getVideoSourceId();
//删除视频资源
if (!StringUtils.isEmpty(videoSourceId)){
vodClient.removeAlyVideo(videoSourceId);
}
int result = baseMapper.deleteById(id);
return result > 0;
}
feign结合Hystrix使用
引入Hystrix相关依赖
在配置文件中添加hystrix配置
在service-edu的client包里面创建熔断器的实现类
@Component
public class VodFileDegradeFeignClient implements VodClient {
//出错之后会执行
@Override
public R removeAlyVideo(String id) {
return R.error().message("删除视频出错了");
}
@Override
public R deleteBatch(List<String> videoList) {
return R.error().message("删除多个视频出错了");
}
}
修改VodClient接口的注解
@FeignClient(name = "service-vod",fallback = VodFileDegradeFeignClient.class)
banner
新建service-cms模块
配置文件
# 服务端口
server.port=8004
# 服务名
spring.application.name=service-cms
# 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=root123
#返回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/yutou/educms/mapper/xml/*.xml
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
配置类:
@SpringBootApplication
@ComponentScan({"com.yutou"})
@MapperScan("com.yutou.educms.mapper")
public class CmsApplication {
public static void main(String[] args) {
SpringApplication.run(CmsApplication.class,args);
}
}
controller:
@RestController
@RequestMapping("/educms/banneradmin")
@CrossOrigin
public class BannerAdminController {
@Autowired
private CrmBannerService bannerService;
//分页查询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());
}
//添加banner
@PostMapping("addBanner")
public R addBanner(@PathVariable CrmBanner crmBanner){
bannerService.save(crmBanner);
return R.ok();
}
//根据id查询
@GetMapping("get/{id}")
public R get(@PathVariable String id){
CrmBanner banner = bannerService.getById(id);
return R.ok().data("item",banner);
}
//修改banner
@PostMapping("update")
public R updateById(@PathVariable CrmBanner crmBanner){
bannerService.updateById(crmBanner);
return R.ok();
}
//删除Banner
@DeleteMapping("remove/{id}")
public R deleteById(@PathVariable String id){
bannerService.removeById(id);
return R.ok();
}
}
创建Banner前台查询接口
@RestController
@RequestMapping("/educms/bannerfront")
@CrossOrigin
public class BannerFrontController {
@Autowired
private CrmBannerService bannerService;
@ApiOperation(value = "查询所有banner")
@GetMapping("getAllBanner")
public R getAllBanner(){
List<CrmBanner> list = bannerService.selectAllBanner();
return R.ok().data("list",list);
}
}
接口及其实现类:
@Service
public class CrmBannerServiceImpl extends ServiceImpl<CrmBannerMapper, CrmBanner> implements CrmBannerService {
//查询所有的banner
@Override
public List<CrmBanner> selectAllBanner() {
//根据id进行降序排列,只显示前2条记录
QueryWrapper<CrmBanner> wrapper = new QueryWrapper<>();
wrapper.orderByDesc("id");
//last方法,拼接sql语句
wrapper.last("limit 2");
return baseMapper.selectList(null);
}
}
集成Redis
添加redis配置类
@Configuration //配置类
@EnableCaching //开启缓存
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory
factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config =
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
在接口中添加redis缓存
Spring Boot缓存注解
@Cacheable
根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回,若不存在,则执行方法,并把返回的结果存入缓存中,一般用在查询方法上
@CachePut
使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上
@CacheEvict
使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上
banner实现缓存
#redis配置
spring.redis.host=192.168.179.128
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
修改CrmBannerServiceImpl,添加redis缓存注解
@Cacheable(key = "'selectIndexList'",value = "banner")
JWT进行跨域身份验证
传统用户身份认证
Internet服务无法与用户身份验证分开,一般过程如下:
1、用户向服务器发送用户名和密码
2、验证服务器后,相关数据(如用户角色,登录时间等)将保存在当前会话中。
3、服务器向用户返回session_id,session信息都会写入到用户的Cookie
4、用户的每个后续请求都将通过在Cookie中取出session_id传给服务器
5、服务器收到session_id并对比之前保存的数据,确认用户的身份
这种模式最大的问题是,没有分布式架构,无法支持横向扩展
解决方案
1、session广播
2、将透明令牌存入cookie,将用户信息存入redis
另一种灵活的解决方案:
使用自包含令牌,通过客户端保存数据,而服务器不保存会话数据,JWT是这种解决方案的代表
JWT的组成
该对象为一个很长的字符串,字符串之间通过.分隔符分为三个子串
每一个子串表示了一个功能块,总共有以下三个部分:JWT头、有效载荷和签名
JWT头
JWT头部分是一个描述JWT元数据的JSON对象,包含需要传递的数据,JWT指定默认7个默认字段供选择
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于表示该JWT
JWT的原则
JWT的原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户
之后,当用户与服务器通信时,客户在请求中发回JSON对象。服务器仅依赖于这个JSON对象来标识用户。为了防止用户篡改数据,服务器将在生成对象时添加签名。
服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。
JWT用法
客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。
此后,客户端将在与服务器交互中都会带JWT。如果将它存储在Cookie中,就可以自动发送,但是不会跨域,因此一般是将它放入HTTP请求的Healer Authorization字段中。当跨域时,也可以将JWT被放置于POST请求的数据主体中。
JWT问题和趋势
- JWT不仅可用于认证,还可用于信息交换。善于JWT有助于减少服务器请求数据库的次数。
- 生产的token可以包含基本信息
- 存储在客户端,不占用服务端的内存资源
- JWT默认不加密,但可以加密
- 当JWT未加密时,一些私密数据无法通过JWT传输
- JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦JWT签发,在有效期内将会一直有效。
- JWT本身包含认证消息
- 为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。
权限管理
新建模块service-acl
引入依赖
<dependencies>
<dependency>
<groupId>com.yutou</groupId>
<artifactId>spring_security</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>
创建权限管理相关的表(至少五个表)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jdjxSInN-1625484228002)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210705184150969.png)]
配置文件:
# 服务端口
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=root123
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
spring.redis.host=192.168.179.128
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/yutou/aclservice/mapper/xml/*.xml
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=192.168.179.128:8848
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
获取所有菜单
controller
@ApiOperation(value = "查询所有菜单")
@GetMapping
public R indexAllPermission() {
List<Permission> list = permissionService.queryAllMenuGuli();
return R.ok().data("children",list);
}
接口及其实现类:
@Override
public List<Permission> queryAllMenuGuli() {
//1 查询菜单表所有数据
QueryWrapper<Permission> wrapper = new QueryWrapper<>();
wrapper.orderByDesc("id");
List<Permission> permissionList = baseMapper.selectList(wrapper);
//2 把查询所有菜单list集合按照要求进行封装
return bulidPermission(permissionList);
}
//把返回所有菜单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;
}
删除菜单
controller:
@ApiOperation(value = "递归删除菜单")
@DeleteMapping("remove/{id}")
public R remove(@PathVariable String id) {
permissionService.removeChildByIdGuli(id);
return R.ok();
}
接口及其实现类:
@Override
public void removeChildByIdGuli(String id) {
//1 创建list集合,用于封装所有删除菜单id值
List<String> idList = new ArrayList<>();
//2 向idList集合设置删除菜单id
this.selectPermissionChildById(id,idList);
//把当前id封装到list里面
idList.add(id);
//删除集合
baseMapper.deleteBatchIds(idList);
}
//2 根据当前菜单id,查询菜单里面子菜单id,封装到list集合
private void selectPermissionChildById(String id, List<String> idList) {
//查询菜单里面子菜单id
QueryWrapper<Permission> wrapper = new QueryWrapper<>();
wrapper.eq("pid",id);
wrapper.select("id");
List<Permission> childIdList = baseMapper.selectList(wrapper);
//把childIdList里面菜单id值获取出来,封装idList里面,做递归查询
childIdList.stream().forEach(item -> {
//封装idList里面
idList.add(item.getId());
//递归查询
this.selectPermissionChildById(item.getId(),idList);
});
}
给角色分配权限
controller:
@ApiOperation(value = "给角色分配权限")
@PostMapping("/doAssign")
public R doAssign(String roleId,String[] permissionId) {
permissionService.saveRolePermissionRealtionShipGuli(roleId,permissionId);
return R.ok();
}
接口及其实现类:
@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);
}
}
rmissionNode.getChildren() == null) {
permissionNode.setChildren(new ArrayList<Permission>());
}
//把查询出来的子菜单放到父菜单里面
permissionNode.getChildren().add(selectChildren(it,permissionList));
}
}
return permissionNode;
}
删除菜单
controller:
@ApiOperation(value = "递归删除菜单")
@DeleteMapping("remove/{id}")
public R remove(@PathVariable String id) {
permissionService.removeChildByIdGuli(id);
return R.ok();
}
接口及其实现类:
@Override
public void removeChildByIdGuli(String id) {
//1 创建list集合,用于封装所有删除菜单id值
List<String> idList = new ArrayList<>();
//2 向idList集合设置删除菜单id
this.selectPermissionChildById(id,idList);
//把当前id封装到list里面
idList.add(id);
//删除集合
baseMapper.deleteBatchIds(idList);
}
//2 根据当前菜单id,查询菜单里面子菜单id,封装到list集合
private void selectPermissionChildById(String id, List<String> idList) {
//查询菜单里面子菜单id
QueryWrapper<Permission> wrapper = new QueryWrapper<>();
wrapper.eq("pid",id);
wrapper.select("id");
List<Permission> childIdList = baseMapper.selectList(wrapper);
//把childIdList里面菜单id值获取出来,封装idList里面,做递归查询
childIdList.stream().forEach(item -> {
//封装idList里面
idList.add(item.getId());
//递归查询
this.selectPermissionChildById(item.getId(),idList);
});
}
给角色分配权限
controller:
@ApiOperation(value = "给角色分配权限")
@PostMapping("/doAssign")
public R doAssign(String roleId,String[] permissionId) {
permissionService.saveRolePermissionRealtionShipGuli(roleId,permissionId);
return R.ok();
}
接口及其实现类:
@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);
}
}