文章目录
添加课程以及课程描述前后端实现、头像上传,封面上传,课程大纲实现,课程回显实现,章节的CRUD,阿里云视频点播技术,nginx的反向代理,使用springcloud服务注册与发现,feign调用
添加课程以及课程描述
一、添加课程后端接口实现
1、课程数据表之间的关系
2、使用mp生成工具生成表对应的层代码
3、课程Controller层
package com.xii.eduservice.controller;
import com.xii.commonutils.R;
import com.xii.eduservice.entity.vo.CourseInfoForm;
import com.xii.eduservice.service.EduCourseService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* <p>
* 课程管理 前端控制器
* </p>
*
* @author xhjava
* @since 2022-02-13
*/
@RestController
@Api(description="课程管理")
@RequestMapping("/eduservice/educourse")
@CrossOrigin
public class EduCourseController {
//注入接口
@Autowired
private EduCourseService eduCourseService;
@PostMapping("saveCourse")
@ApiOperation(value = "新增课程")
public R saveCourse(
@ApiParam(name = "CourseInfoForm", value = "课程基本信息", required = true)
@RequestBody CourseInfoForm courseInfoForm){ //使用封装好的类 CourseInfoForm 可以同时添加描述
String id = eduCourseService.saveCourseInfo(courseInfoForm);
//返回添加课程的id
return R.ok().data("id",id);
}
}
4、定义添加实体CourseInfoForm
前端与之实体类对应
CourseInfoForm.java
package com.xii.eduservice.entity.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
/**
* @description: 定义form表单对象 由于前段添加课程与数据库信息不一致 所以自定义一个类
*
* @author wangxihao
* @email wangxh0108@163.com
**/
@ApiModel(value = "课程基本信息", description = "编辑课程基本信息的表单对象")
@Data
public class CourseInfoForm {
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 = "课程标题")
private String title;
@ApiModelProperty(value = "课程销售价格,设置为0则可免费观看")
private BigDecimal price;
@ApiModelProperty(value = "总课时")
private Integer lessonNum;
@ApiModelProperty(value = "课程封面图片路径")
private String cover;
@ApiModelProperty(value = "课程简介")
private String description;
}
5、Service层
public interface EduCourseService extends IService<EduCourse> {
//添加课程
String saveCourseInfo(CourseInfoForm courseInfoForm);
}
package com.xii.eduservice.service.impl;
import com.xii.eduservice.entity.EduCourse;
import com.xii.eduservice.entity.EduCourseDescription;
import com.xii.eduservice.entity.vo.CourseInfoForm;
import com.xii.eduservice.mapper.EduCourseMapper;
import com.xii.eduservice.service.EduCourseDescriptionService;
import com.xii.eduservice.service.EduCourseService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xii.servicebase.exceptionhandler.XiiException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* <p>
* 课程 服务实现类
* </p>
*
* @author xhjava
* @since 2022-02-13
*/
@Service
public class EduCourseServiceImpl extends ServiceImpl<EduCourseMapper, EduCourse> implements EduCourseService {
//用于添加描述
@Autowired
private EduCourseDescriptionService eduCourseDescriptionService;
/**
* @description: 添加课程
*
* @author wangxihao
* @email wangxh0108@163.com
**/
@Override
public String saveCourseInfo(CourseInfoForm courseInfoForm) {
EduCourse eduCourse = new EduCourse();
BeanUtils.copyProperties(courseInfoForm,eduCourse); //将封装的对象courseInfoForm 里面的数据赋值eduCourse eduCourse用于添加
int insert = baseMapper.insert(eduCourse);
// this.save(eduCourse); 添加也可以使用这个
if(insert == 0){
throw new XiiException(20001,"添加课程失败");
}
//向课程简介表中添加课程描述 -使id值与课程id相同
String id = eduCourse.getId();
EduCourseDescription eduCourseDescription = new EduCourseDescription();
eduCourseDescription.setId(id);
eduCourseDescription.setDescription(courseInfoForm.getDescription());
String id = eduCourseDescriptionService.save(eduCourseDescription);
return id;
}
}
在课程描述的实体类中:
课程实体类记得加上自动时间
6、测试
由于数据库版本和数据不符合数据库规范因素,会有异常,但是数据添加成功,不用影响后续操作。
详细原因请看
https://blog.csdn.net/wang121213145/article/details/122924891
成功
二、添加课程前端请求
1、添加路由
index.js
// 课程管理
{
path: '/course',
component: Layout,
redirect: '/edu/course/list',
name: 'Course',
meta: { title: '课程管理', icon: 'form' },
children: [{
path: 'list',
name: 'EduCourseList',
component: () =>
import ('@/views/edu/course/list'),
meta: { title: '课程列表' }
},
{
path: 'info',
name: 'EduCourseInfo',
component: () =>
import ('@/views/edu/course/info'),
meta: { title: '发布课程' }
},
{
path: 'info/:id',
name: 'EduCourseInfoEdit',
component: () =>
import ('@/views/edu/course/info'),
meta: { title: '编辑课程基本信息', noCache: true },
hidden: true
},
{
path: 'chapter/:id',
name: 'EduCourseChapterEdit',
component: () =>
import ('@/views/edu/course/chapter'),
meta: { title: '编辑课程大纲', noCache: true },
hidden: true
},
{
path: 'publish/:id',
name: 'EduCoursePublishEdit',
component: () =>
import ('@/views/edu/course/publish'),
meta: { title: '发布课程', noCache: true },
hidden: true
}
]
},
2、添加路由组件
编辑课程信息页面
info.vue
<template>
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="1" process-status="wait" align-center style="margin-bottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="提交发布"/>
</el-steps>
<el-form label-width="120px">
<el-form-item label="课程标题">
<el-input v-model="courseInfo.title" placeholder=" 示例:机器学习项目课:从基础到搭建项目视频课程。专业名称注意大小写"/>
</el-form-item>
<!-- 所属分类 TODO -->
<!-- 课程讲师 TODO -->
<el-form-item label="总课时">
<el-input-number :min="0" v-model="courseInfo.lessonNum" controls-position="right" placeholder="请填写课程的总课时数"/>
</el-form-item>
<!-- 课程简介 TODO -->
<el-form-item label="课程描述">
<el-input v-model="courseInfo.description" placeholder=" 介绍一下你的课程吧"/>
</el-form-item>
<!-- 课程封面 TODO -->
<el-form-item label="课程价格">
<el-input-number :min="0" v-model="courseInfo.price" controls-position="right" placeholder="免费课程请设置为0元"/> 元
</el-form-item>
<el-form-item>
<el-button :disabled="saveBtnDisabled" type="primary" @click="next">保存并下一步</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import course from '@/api/edu/course'
export default {
data() {
return {
saveBtnDisabled: false, // 保存按钮是否禁用
courseInfo:{
title: '',
subjectId: '',
teacherId: '',
lessonNum: 0,
description: '',
cover: '',
price: 0
}
}
},
created() {
console.log('info created')
},
methods: {
next() {
course.addCourseList(this.courseInfo) //添加成功后弹出层
.then(response => {
this.$message({
type:'success',
message:'添加课程信息成功!'
})
this.$router.push({ path: '/course/chapter/'+response.data.id })
})
// console.log('next')
//跳转到第二步
}
}
}
</script>
课程大纲页面
chapter.vue
<template>
<!-- 课程大纲 -->
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="2" process-status="wait" align-center style="margin-bottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="提交发布"/>
</el-steps>
<el-form label-width="120px">
<el-form-item>
<el-button @click="previous">上一步</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="next">下一步</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
saveBtnDisabled: false // 保存按钮是否禁用
}
},
created() {
console.log('chapter created')
},
methods: {
previous() {
console.log('previous')
this.$router.push({ path: '/course/info/1' })
},
next() {
console.log('next')
this.$router.push({ path: '/course/publish/1' })
}
}
}
</script>
课程发布页面
publish.vue
<template>
<!-- 最终发布 -->
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="3" process-status="wait" align-center style="margin-bottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="提交发布"/>
</el-steps>
<el-form label-width="120px">
<el-form-item>
<el-button @click="previous">返回修改</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="publish">发布课程</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
saveBtnDisabled: false // 保存按钮是否禁用
}
},
created() {
console.log('publish created')
},
methods: {
previous() {
console.log('previous')
this.$router.push({ path: '/course/chapter/1' })
},
publish() {
console.log('publish')
this.$router.push({ path: '/course/list' })
}
}
}
</script>
3、编辑课程基本信息前端实现
1、定义api
import request from '@/utils/request'
//课程列表 请求后端
export default {
addCourseList(courseInfo) {
return request({
url: `/eduservice/educourse/saveCourse`,
method: 'post',
data: courseInfo
})
}
}
测试:
4、完善添加课程信息页面(教师列表、一级分类)
加入
<!-- 一级分类 -->
<el-form-item label="课程类别">
<el-select
v-model="courseInfo.subjectParentId"
placeholder="请选择">
<el-option
v-for="subject in subjectOneList"
:key="subject.id"
:label="subject.title"
:value="subject.id"/>
</el-select>
</el-form-item>
<!-- 课程讲师 TODO -->
<el-form-item label="课程讲师">
<el-select
v-model="courseInfo.teacherId"
placeholder="请选择">
<el-option
v-for="teacher in teacherList"
:key="teacher.id"
:label="teacher.name"
:value="teacher.id"/>
<!-- 提交的是value -->
</el-select>
</el-form-item>
导入api
import subject from ‘@/api/edu/subject’
subject.js
import request from '@/utils/request'
//课程列表 请求后端
export default {
getSubjectListPage() {
return request({
url: `/eduservice/subject/getAllSubject`,
method: 'get'
})
},
}
course.js
//发送教师列表请求
getTeachers() {
return request({
url: `/eduservice/teacher/findAll`,
method: 'get'
})
}
data中的数据
teacherList:[], //讲师列表
subjectOneList: [],//一级分类列表
subSubjectList: []//二级分类列表
方法
created() {
// console.log('info created')
//初始化所有 讲师
this.getTeacherLists()
//初始化一级分类
this.initSubjectList()
},
methods: {
//查看所有讲师
getTeacherLists(){
course.getTeachers()
.then(response => {
//赋值
this.teacherList = response.data.items})
},
//得到一级分类列表
initSubjectList() {
subject.getSubjectListPage()
.then(response => {
//赋值给下拉框对象
this.subjectOneList = response.data.list
})
}
···
}
成功显示
5、完善添加课程信息页面(二级分类、头像上传)
级联显示二级分类
<!-- 二级分类 -->
<el-select v-model="courseInfo.subjectId" placeholder="请选择">
<el-option
v-for="subject in subSubjectList"
:key="subject.value"
:label="subject.title"
:value="subject.id"/>
</el-select>
注册change事件
<el-select @change="subjectLevelOneChanged" ......
定义change事件方法
subjectLevelOneChanged(value){
for(let i = 0;i<this.subjectOneList.length;i++){
if(value === this.subjectOneList[i].id){
this.subSubjectList = this.subjectOneList[i].children
//二级分类已经选择的subjectIdid值清空
// console.log(this.courseInfo.subjectId)
this.courseInfo.subjectId = ''
}
}
},
头像上传
组件模板
在info.vue中添加上传组件模板
<!-- 课程封面-->
<el-form-item label="课程封面">
<el-upload
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
:action="BASE_API+'/eduoss/fileoss/upload'"
class="avatar-uploader">
<img :src="courseInfo.cover" width="300px">
</el-upload>
</el-form-item>
结果回调
···
//上传头像之后的方法
handleAvatarSuccess(res, file) {
console.log(res)// 上传响应 res为返回值
console.log(URL.createObjectURL(file.raw))// base64编码
this.courseInfo.cover = res.data.url
},
//上传头像之前的方法
beforeAvatarUpload(file) {
const isJPG = file.type === 'image/jpeg'
const isLt2M = file.size / 1024 / 1024 < 2
if (!isJPG) {
this.$message.error('上传头像图片只能是 JPG 格式!')
}
if (!isLt2M) {
this.$message.error('上传头像图片大小不能超过 2MB!')
}
return isJPG && isLt2M
},
···
data数据
测试
启动后台OSS
上传成功!
5、完善添加课程信息页面(富文本编辑器)
一、Tinymce可视化编辑器
参考
https://panjiachen.gitee.io/vue-element-admin/#/components/tinymce
https://panjiachen.gitee.io/vue-element-admin/#/example/create
组件初始化
Tinymce是一个传统javascript插件,默认不能用于Vue.js因此需要做一些特殊的整合步骤
复制脚本库
将脚本库复制到项目的static目录下(在vue-element-admin-master的static路径下)
配置html变量
在 guli-admin/build/webpack.dev.conf.js 中添加配置
使在html页面中可是使用这里定义的BASE_URL变量
new HtmlWebpackPlugin({
......,
templateParameters: {
BASE_URL: config.dev.assetsPublicPath + config.dev.assetsSubDirectory
}
})
引入js脚本
在guli-admin/index.html 中引入js脚本
<script src=<%= BASE_URL %>/tinymce4.7.5/tinymce.min.js></script>
<script src=<%= BASE_URL %>/tinymce4.7.5/langs/zh_CN.js></script>
在这里保存的时候,会报错,因为js格式化插件的问题,所以禁用下插件
组件引入
为了让Tinymce能用于Vue.js项目,vue-element-admin-master对Tinymce进行了封装,下面我们将它引入到我们的课程信息页面
1、复制组件
src/components/Tinymce
2、引入组件
课程信息组件中引入 Tinymce
import Tinymce from '@/components/Tinymce'
export default {
components: { Tinymce },
......
}
3、组件模板
<!-- 课程简介-->
<el-form-item label="课程简介">
<tinymce :height="300" v-model="courseInfo.description"/>
</el-form-item>
4、组件样式
在info.vue文件的最后添加如下代码,调整上传图片按钮的高度
<style scoped>
.tinymce-container {
line-height: 29px;
}
</style>
5、图片的base64编码
Tinymce中的图片上传功能直接存储的是图片的base64编码,因此无需图片服务器
完成!
三、课程大纲实现
1、课程大纲后端实现
EduChapterController.java
/**
* <p>
* 课程 前端控制器
* </p>
*
* @author xhjava
* @since 2022-02-13
*/
@RestController
@Api(value = "根据课程id获得章节列表")
@RequestMapping("/eduservice/educhapter")
public class EduChapterController {
@Autowired
private EduChapterService eduChapterService;
@ApiOperation(value = "嵌套章节数据列表")
@GetMapping("nestedlist/{courseId}")
public R nestedListByCourseId(
@ApiParam(name = "courseId", value = "课程ID", required = true)
@PathVariable String courseId){ //请求过来的参数
List<ChapterVo> chapterVoList = eduChapterService.nestedList(courseId);
return R.ok().data("items", chapterVoList);
}
}
接口
/**
* <p>
* 课程 服务类
* </p>
*
* @author xhjava
* @since 2022-02-13
*/
public interface EduChapterService extends IService<EduChapter> {
//获得所有章节
List<ChapterVo> nestedList(String courseId);
}
和课程一级分类二级分类的封装方法一样的
实现类EduChapterServiceImpl.java
package com.xii.eduservice.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xii.eduservice.entity.EduChapter;
import com.xii.eduservice.entity.EduVideo;
import com.xii.eduservice.entity.chapter.ChapterVo;
import com.xii.eduservice.entity.chapter.VideoVo;
import com.xii.eduservice.mapper.EduChapterMapper;
import com.xii.eduservice.service.EduChapterService;
import com.xii.eduservice.service.EduVideoService;
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;
/**
* <p>
* 课程 服务实现类
* </p>
*
* @author xhjava
* @since 2022-02-13
*/
@Service
public class EduChapterServiceImpl extends ServiceImpl<EduChapterMapper, EduChapter> implements EduChapterService {
@Autowired
private EduVideoService eduVideoService;
/**
* @description: 获得章节
*
* @author wangxihao
* @email wangxh0108@163.com
**/
@Override
public List<ChapterVo> nestedList(String courseId) {
//最终要的到的数据列表
ArrayList<ChapterVo> chapterVoArrayList = new ArrayList<>();
//获取章节信息
QueryWrapper<EduChapter> queryWrapper1 = new QueryWrapper<>();
queryWrapper1.eq("course_id", courseId);
queryWrapper1.orderByAsc("sort", "id");
List<EduChapter> chapters = baseMapper.selectList(queryWrapper1);
//获取课时信息
QueryWrapper<EduVideo> queryWrapper2 = new QueryWrapper<>();
queryWrapper2.eq("course_id", courseId);
queryWrapper2.orderByAsc("sort", "id");
List<EduVideo> videos = eduVideoService.list(queryWrapper2);
//填充章节vo数据
int count1 = chapters.size();
for (int i = 0; i < count1; i++) {
EduChapter chapter = chapters.get(i);
//创建章节vo对象
ChapterVo chapterVo = new ChapterVo();
BeanUtils.copyProperties(chapter, chapterVo);
chapterVoArrayList.add(chapterVo);
//填充课时vo数据
ArrayList<VideoVo> videoVoArrayList = new ArrayList<>();
int count2 = videos.size();
for (int j = 0; j < count2; j++) {
EduVideo video = videos.get(j);
if(chapter.getId().equals(video.getChapterId())){
//创建课时vo对象
VideoVo videoVo = new VideoVo();
BeanUtils.copyProperties(video, videoVo);
videoVoArrayList.add(videoVo);
}
}
chapterVo.setChildren(videoVoArrayList);
}
return chapterVoArrayList;
}
}
测试
2、课程大纲前端实现
页面部分
<!-- 章节 -->
<ul class="chanpterList">
<li
v-for="chapter in chapterNestedList"
:key="chapter.id">
<p>
{{ chapter.title }}
<span class="acts">
<el-button type="text">添加课时</el-button>
<el-button style="" type="text">编辑</el-button>
<el-button type="text">删除</el-button>
</span>
</p>
<!-- 视频 -->
<ul class="chanpterList videoList">
<li
v-for="video in chapter.children"
:key="video.id">
<p>{{ video.title }}
<span class="acts">
<el-button type="text">编辑</el-button>
<el-button type="text">删除</el-button>
</span>
</p>
</li>
</ul>
</li>
</ul>
样式
<style scoped>
.chanpterList{
position: relative;
list-style: none;
margin: 0;
padding: 0;
}
.chanpterList li{
position: relative;
}
.chanpterList p{
float: left;
font-size: 20px;
margin: 10px 0;
padding: 10px;
height: 70px;
line-height: 50px;
width: 100%;
border: 1px solid #DDD;
}
.chanpterList .acts {
float: right;
font-size: 14px;
}
.videoList{
padding-left: 50px;
}
.videoList p{
float: left;
font-size: 14px;
margin: 10px 0;
padding: 10px;
height: 50px;
line-height: 30px;
width: 100%;
border: 1px dotted #DDD;
}
</style>
方法
data() {
return {
saveBtnDisabled: false, // 保存按钮是否禁用
chapterNestedList:[], //小节列表
courseId:'' //课程id
}
},
created() {
//获得路由的id值 有参数并且参数有id
// console.log(this.$router.params)
if(this.$route.params && this.$route.params.id){ //route 路由 注意不要写成 router
console.log(this.$route.params.id)
this.courseId = this.$route.params.id
//根据课程id查询章节和小节
this.getChapterVideo()
}
// console.log('chapter created')
},
methods: {
//根据课程id查询章节和小节
getChapterVideo(){
chapter.getAllChapterVideo(this.courseId)
.then((result) => { //成功返回
this.chapterNestedList = result.data.items
}).catch((err) => { //失败返回
});
},
previous() {
this.$router.push({ path: '/course/info/'+this.courseId })
},
next() {
console.log('next')
this.$router.push({ path: '/course/publish/'+this.courseId })
}
}
}
章节显示成功!
四、点击上一步课程信息回显以及更新课程信息
1、点击上一步课程信息回显以及更新课程信息后端实现
Controller层
@ApiOperation(value = "根据ID查询课程")
@GetMapping("courseinfo/{courseid}")
public R getById(
@ApiParam(name = "id", value = "课程ID", required = true)
@PathVariable String courseid){
CourseInfoForm courseInfoForm = eduCourseService.getCourseInfoFormById(courseid);
return R.ok().data("item", courseInfoForm);
}
@ApiOperation(value = "更新课程")
@PostMapping("updatecourseinfo")
public R updateCourseInfoById(
@ApiParam(name = "CourseInfoForm", value = "课程基本信息", required = true)
@RequestBody CourseInfoForm courseInfoForm){
eduCourseService.updateCourseInfoById(courseInfoForm);
return R.ok();
}
接口实现类
EduCourseServiceImpl.java
/**
* @description: 根据ID查询课程
*
* @author wangxihao
* @email wangxh0108@163.com
**/
@Override
public CourseInfoForm getCourseInfoFormById(String id) {
// 根据课程id课程信息
EduCourse course = this.getById(id);
if(course == null){
throw new XiiException(20001, "数据不存在");
}
//创建返回的对象
CourseInfoForm courseInfoForm = new CourseInfoForm();
//课程信息存入返回对象
BeanUtils.copyProperties(course, courseInfoForm);
// 根据课程id课程描述信息
EduCourseDescription courseDescription = eduCourseDescriptionService.getById(id);
if(course != null){
//描述信息存入返回对象
courseInfoForm.setDescription(courseDescription.getDescription());
}
return courseInfoForm;
}
/**
* @description: 更新课程
*
* @author wangxihao
* @email wangxh0108@163.com
**/
@Override
public void updateCourseInfoById(CourseInfoForm courseInfoForm) {
//保存课程基本信息
EduCourse course = new EduCourse();
BeanUtils.copyProperties(courseInfoForm, course);
//修改课程表
boolean resultCourseInfo = this.updateById(course);
if(!resultCourseInfo){
throw new XiiException(20001, "课程信息保存失败");
}
//保存课程描述信息
EduCourseDescription courseDescription = new EduCourseDescription();
courseDescription.setDescription(courseInfoForm.getDescription());
courseDescription.setId(course.getId());
//修改描述表
boolean resultDescription = eduCourseDescriptionService.updateById(courseDescription);
if(!resultDescription){
throw new XiiException(20001, "课程详情信息保存失败");
}
2、点击上一步课程信息回显以及更新课程信息前端实现
info.vue
created() {
//得到url中的参数 用于下一步到这一步的数据回显
if (this.$route.params && this.$route.params.id) {
this.courseId= this.$route.params.id
//根据id获取课程基本信息用于回显
this.getInfo()
}else{
//初始化所有 讲师
this.getTeacherLists()
//初始化一级分类
this.initSubjectList()
}
},
methods: {
//用于下一步返回到这一步的数据回显
getInfo() {
console.log(this.courseId)
//通过课程id获得课程信息 将课程信息赋值给课程对象courseInfo
course.getCourseInfoById(this.courseId).then(response => {
this.courseInfo = response.data.item
//下拉列表的数据回显 下拉列表的问题 因为二级列表对象subSubjectList只有onchange的时候才会有值 现在要循环给他赋值
// 初始化分类列表 通过getSubjectListPage得到课程列表 赋值给一级列表集合 因为在courseInfo中有一级列表id 复制后就会显示
subject.getSubjectListPage().then(response => {
this.subjectOneList = response.data.list
for (let i = 0; i < this.subjectOneList.length; i++) {
//循环一级标题 得到所有二级标题 判断所有二级标题的父id 相等的话 就赋值给二级标题集合
if (this.subjectOneList[i].id === this.courseInfo.subjectParentId) {
this.subSubjectList = this.subjectOneList[i].children
}
}
})
// 初始化所有 讲师
this.getTeacherLists()
}).catch((response) => {
this.$message({
type: 'error',
message: response.message
})
})
},
······
点击上一步的时候,课程回显成功!!
md 。。。累死了写到这
补充 403 错误一般就是跨域问题或者路径写错了
五、章节的CRUD
1、后端接口
EduChapterController.java
package com.xii.eduservice.controller;
import com.xii.commonutils.R;
import com.xii.eduservice.entity.EduChapter;
import com.xii.eduservice.entity.chapter.ChapterVo;
import com.xii.eduservice.service.EduChapterService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* <p>
* 课程 前端控制器
* </p>
*
* @author xhjava
* @since 2022-02-13
*/
@RestController
@Api(description = "根据课程id获得章节列表")
@RequestMapping("/eduservice/educhapter")
@CrossOrigin
public class EduChapterController {
@Autowired
private EduChapterService eduChapterService;
/**
* @description: 根据id获得嵌套的数据列表
*
* @author wangxihao
* @email wangxh0108@163.com
**/
@ApiOperation(value = "嵌套章节数据列表")
@GetMapping("nestedlist/{courseId}")
public R nestedListByCourseId(
@ApiParam(name = "courseId", value = "课程ID", required = true)
@PathVariable String courseId){ //请求过来的参数
List<ChapterVo> chapterVoList = eduChapterService.nestedList(courseId);
return R.ok().data("items", chapterVoList);
}
@ApiOperation(value = "添加章节")
@PostMapping("addChapter")
public R addChapter(@RequestBody EduChapter chapter){
eduChapterService.save(chapter);
return R.ok();
}
/**
* @description: 根据id查询章节
*
* @author wangxihao
* @email wangxh0108@163.com
**/
@ApiOperation(value = "根据id查询")
@GetMapping("getChapterInfoById/{chapterId}")
public R getChapter(@PathVariable String chapterId){
EduChapter byId = eduChapterService.getById(chapterId);
return R.ok().data("chapter",byId);
}
/**
* @description: 修改章节
*
* @author wangxihao
* @email wangxh0108@163.com
**/
@ApiOperation(value = "修改章节")
@PostMapping("updateChapter")
public R updateChapter(@RequestBody EduChapter eduChapter){
eduChapterService.updateById(eduChapter);
return R.ok();
}
/**
* @description: 根据id进行删除
*
* @author wangxihao
* @email wangxh0108@163.com
**/
@ApiOperation(value = "根据id进行删除")
@DeleteMapping("removeChapterById/{ChapterId}")
public R removeChapter(@PathVariable String ChapterId){
//删除章节的时候 如果里面有小节 不让他删除
boolean b = eduChapterService.deleteChapter(ChapterId);
if(b){
return R.ok();
}else {
return R.error();
}
}
}
service层
public interface EduChapterService extends IService<EduChapter> {
//获得所有章节
List<ChapterVo> nestedList(String courseId);
//删除章节 有小节不允许删除
boolean deleteChapter(String chapterId);
}
package com.xii.eduservice.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xii.eduservice.entity.EduChapter;
import com.xii.eduservice.entity.EduVideo;
import com.xii.eduservice.entity.chapter.ChapterVo;
import com.xii.eduservice.entity.chapter.VideoVo;
import com.xii.eduservice.mapper.EduChapterMapper;
import com.xii.eduservice.service.EduChapterService;
import com.xii.eduservice.service.EduVideoService;
import com.xii.servicebase.exceptionhandler.XiiException;
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;
/**
* <p>
* 课程 服务实现类
* </p>
*
* @author xhjava
* @since 2022-02-13
*/
@Service
public class EduChapterServiceImpl extends ServiceImpl<EduChapterMapper, EduChapter> implements EduChapterService {
//小节接口注入
@Autowired
private EduVideoService eduVideoService;
/**
* @description: 获得章节 章节和小节的嵌套
*
* @author wangxihao
* @email wangxh0108@163.com
**/
@Override
public List<ChapterVo> nestedList(String courseId) {
//最终要的到的数据列表
ArrayList<ChapterVo> chapterVoArrayList = new ArrayList<>();
//获取章节信息
QueryWrapper<EduChapter> queryWrapper1 = new QueryWrapper<>();
queryWrapper1.eq("course_id", courseId);
queryWrapper1.orderByAsc("sort", "id");
List<EduChapter> chapters = baseMapper.selectList(queryWrapper1);
//获取课时信息
QueryWrapper<EduVideo> queryWrapper2 = new QueryWrapper<>();
queryWrapper2.eq("course_id", courseId);
queryWrapper2.orderByAsc("sort", "id");
List<EduVideo> videos = eduVideoService.list(queryWrapper2);
//填充章节vo数据
int count1 = chapters.size();
for (int i = 0; i < count1; i++) {
EduChapter chapter = chapters.get(i);
//创建章节vo对象
ChapterVo chapterVo = new ChapterVo();
BeanUtils.copyProperties(chapter, chapterVo);
chapterVoArrayList.add(chapterVo);
//填充课时vo数据
ArrayList<VideoVo> videoVoArrayList = new ArrayList<>();
int count2 = videos.size();
for (int j = 0; j < count2; j++) {
EduVideo video = videos.get(j);
if(chapter.getId().equals(video.getChapterId())){
//创建课时vo对象
VideoVo videoVo = new VideoVo();
BeanUtils.copyProperties(video, videoVo);
videoVoArrayList.add(videoVo);
}
}
chapterVo.setChildren(videoVoArrayList);
}
return chapterVoArrayList;
}
/**
* @description: 删除章节 如果里面有小节的时候 不让删除
*
* @author wangxihao
* @email wangxh0108@163.com
**/
@Override
public boolean deleteChapter(String chapterId) {
QueryWrapper<EduVideo> queryWrapper = new QueryWrapper<>();
//根据章节id查询里面是否有小节
queryWrapper.eq("chapter_id",chapterId);
//得到数量
int count = eduVideoService.count(queryWrapper);
boolean b = false;
if(count > 0){
//有数据不允许删除
throw new XiiException(20001,"不能删除");
}else{
//删除
b = this.removeById(chapterId);
}
return b;
}
}
2、前端实现
页面
<!-- 章节 -->
<ul class="chanpterList">
<li
v-for="chapter in chapterNestedList"
:key="chapter.id">
<p>
{{ chapter.title }}
<span class="acts">
<el-button type="text">添加课时</el-button>
<el-button style="" type="text" @click="saveOrUpdate">编辑</el-button>
<el-button type="text">删除</el-button>
</span>
</p>
<!-- 视频 -->
<ul class="chanpterList videoList">
<li
v-for="video in chapter.children"
:key="video.id">
<p>{{ video.title }}
<span class="acts">
<el-button type="text">编辑</el-button>
<el-button type="text">删除</el-button>
</span>
</p>
</li>
</ul>
</li>
</ul>
样式
<style scoped>
.chanpterList{
position: relative;
list-style: none;
margin: 0;
padding: 0;
}
.chanpterList li{
position: relative;
}
.chanpterList p{
/* 浮动会导致编辑按钮无法点击 */
/* float: left; */
font-size: 20px;
margin: 10px 0;
padding: 10px;
height: 70px;
line-height: 50px;
width: 100%;
border: 1px solid #DDD;
}
.chanpterList .acts {
float: right;
font-size: 14px;
}
.videoList{
padding-left: 50px;
}
.videoList p{
float: left;
font-size: 14px;
margin: 10px 0;
padding: 10px;
height: 50px;
line-height: 30px;
width: 100%;
border: 1px dotted #DDD;
}
</style>
添加请求Api
chapter.js
import request from '@/utils/request'
export default {
//根据课程id获取章节和小节数据列表
getAllChapterVideo(courseId){
return request({
url:'/eduservice/educhapter/nestedlist/'+courseId,
method:'get'
})
},
//添加章节和小节数据列表
addChapterList(chapter){
return request({
url:'/eduservice/educhapter/addChapter',
method:'post',
data:chapter
})
},
//根据id获取章节和小节数据列表
getChapterVideoByChapterId(chapterId){
return request({
url:'/eduservice/educhapter/getChapterInfoById/'+chapterId,
method:'get'
})
},
//修改章节和小节数据列表
updateChapterVideo(chapter){
return request({
url:'/eduservice/educhapter/updateChapter',
method:'post',
data:chapter
})
},
//删除章节通过id
deleteChapterById(chapterId){
return request({
url:'/eduservice/educhapter/removeChapterById/'+chapterId,
method:'delete'
})
}
}
在chapter.vue中的方法,包含添加删除和修改
export default {
data() {
return {
saveBtnDisabled: false, // 保存按钮是否禁用
chapterNestedList:[], //小节列表
courseId:'', //课程id
dialogChapterFormVisible:false, //弹框方法
chapter:{ //章节对象
id:'',
title: '',
sort: 0,
courseId:''
}
}
},
created() {
//获得路由的id值 有参数并且参数有id
// console.log(this.$router.params)
if(this.$route.params && this.$route.params.id){ //route 路由 注意不要写成 router
console.log(this.$route.params.id)
this.courseId = this.$route.params.id
//根据课程id查询章节和小节
this.getChapterVideo()
}
// console.log('chapter created')
},
methods: {
//点击添加章节
addChapter(){
//先清空下文本框的值
this.chapter.title = ''
this.chapter.sort = 0
this.dialogChapterFormVisible=true
},
//删除章节
deleteChapter(chapterid){
this.$confirm('此操作将永久删除该标题, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
//调用teacher中的id
chapter.deleteChapterById(chapterid)
.then(response => {
this.$message({
type: 'success',
message: '删除成功!'
})
this.getChapterVideo()// 刷新列表
})
})
},
//点击编辑修改章节数据回显
editChapter(chapterId){
//弹框
console.log("弹框")
this.dialogChapterFormVisible = true
//调用接口
chapter.getChapterVideoByChapterId(chapterId)
.then(response => {
this.chapter = response.data.chapter
})
},
//点击提交添加章节或者更新
saveOrUpdate() {
this.saveBtnDisabled = true
//判断添加修改
if (!this.chapter.id) {
this.saveData()
console.log("添加")
} else {
console.log("修改")
this.updateData()
}
},
//提交添加章节
saveData() {
this.chapter.courseId = this.courseId
//调用添加章节方法
chapter.addChapterList(this.chapter)
.then(response => {
this.$message({
type: 'success',
message: '保存成功!'
})
this.helpSave()
}).catch((response) => {
this.$message({
type: 'error',
message: response.message
})
})
},
//提交修改章节
updateData() {
//调用修改的方法
chapter.updateChapterVideo(this.chapter)
.then(response => {
this.$message({
type: 'success',
message: '修改成功!'
})
this.helpSave()
}).catch((response) => {
this.$message({
type: 'error',
message: response.message
})
})
},
//点击添加提交后的操作
helpSave(){
this.dialogChapterFormVisible = false// 如果保存成功则关闭对话框
this.getChapterVideo()// 刷新列表
this.chapter.title = ''// 重置章节标题
this.chapter.sort = 0// 重置章节标题
this.saveBtnDisabled = false
},
//根据课程id查询章节和小节
getChapterVideo(){
chapter.getAllChapterVideo(this.courseId)
.then((result) => { //成功返回
this.chapterNestedList = result.data.items
}).catch((err) => { //失败返回
});
},
previous() {
this.$router.push({ path: '/course/info/'+this.courseId })
},
next() {
console.log('next')
this.$router.push({ path: '/course/publish/'+this.courseId })
}
}
}
测试添加,添加和修改和删除成功!
3、视频上传阿里云
关于视频学习的文章见:
https://blog.csdn.net/wang121213145/article/details/123075602
下面视频点播微服务进行实现
六、视频点播微服务
1、创建微服务模块
Artifact:service-vod
2、pom
(1)service-vod中引入依赖
<dependencies>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-vod</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-sdk-vod-upload</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
</dependencies>
3、application.properties
# 服务端口
server.port=8003
# 服务名
spring.application.name=service-vod
# 环境设置:dev、test、prod
spring.profiles.active=dev
#阿里云 vod
#不同的服务器,地址不同
aliyun.vod.file.keyid=your accessKeyId
aliyun.vod.file.keysecret=your accessKeySecret
# 最大上传单个文件大小:默认1M
spring.servlet.multipart.max-file-size=1024MB
# 最大置总上传的数据大小 :默认10M
spring.servlet.multipart.max-request-size=1024MB
4、启动类
VodApplication.java
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //因为oss默认配置数据源 exclude是默认不加载数据库 不然会报错
@ComponentScan(basePackages = {"com.xii"}) //加载的配置类 主要是对位于swagger的加载 扫描com.xii下的包 全局 //配置宝扫描规则 自动整合到swagger
public class VodApplication {
public static void main(String[] args){
SpringApplication.run(VodApplication.class,args);
}
}
2、整合阿里云vod实现视频上传
1、创建常量类
ConstantPropertiesUtil.java
package com.xii.eduVod.utils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @description: 常量类 获取阿里云的key 和 secret
*
* @author wangxihao
* @email wangxh0108@163.com
**/
/* InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候都会执行该方法。 */
@Component
public class ConstantPropertiesUtil 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;
}
}
2、复制工具类到项目中
AliyunVodSDKUtils.java
package com.guli.vod.util;
public class AliyunVodSDKUtils {
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;
}
}
3、配置swagger
web和admin
4、创建service
接口:VideoService.java
String uploadVideo(MultipartFile file);
实现:VideoServiceImpl.java
package com.xii.eduVod.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.xii.eduVod.service.VideoService;
import com.xii.eduVod.utils.ConstantPropertiesUtil;
import com.xii.servicebase.exceptionhandler.XiiException;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
/**
* @description: 上传视频实现类
*
* @author wangxihao
* @email wangxh0108@163.com
**/
@Service
public class VideoServiceImpl implements VideoService {
@Override
public String uploadVideo(MultipartFile file) {
try {
//文件上传的输入流
InputStream inputStream = file.getInputStream();
//上传文件原始的名字
String originalFilename = file.getOriginalFilename();
//title 文件上传后的标题
String title = originalFilename.substring(0, originalFilename.lastIndexOf("."));
//文件上传
UploadStreamRequest request = new UploadStreamRequest(
ConstantPropertiesUtil.ACCESS_KEY_ID,
ConstantPropertiesUtil.ACCESS_KEY_SECRET,
title, originalFilename, inputStream);
UploadVideoImpl uploader = new UploadVideoImpl();
UploadStreamResponse response = uploader.uploadStream(request);
//如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。
// 其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因
String videoId = response.getVideoId();
if (!response.isSuccess()) {
String errorMessage = "阿里云上传错误:" + "code:" + response.getCode() + ", message:" + response.getMessage();
// log.warn(errorMessage);
System.out.println(errorMessage);
if(StringUtils.isEmpty(videoId)){
throw new XiiException(20001, errorMessage);
}
}
return videoId;
} catch (IOException e) {
throw new XiiException(20001, "guli vod 服务上传失败");
}
}
}
5、创建controller
VideoAdminController.java
package com.xii.eduVod.controller;
import com.xii.commonutils.R;
import com.xii.eduVod.service.VideoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@Api(description = "阿里云视频点播服务")
@CrossOrigin
@RestController
@RequestMapping("/vod/video")
public class VideoAdminController {
@Autowired
private VideoService videoService;
@PostMapping("upload")
@ApiOperation(value = "文件上传")
public R uploadVideo(
@ApiParam(name = "file", value = "文件", required = true)
@RequestParam("file") MultipartFile file) throws Exception {
String videoId = videoService.uploadVideo(file);
return R.ok().message("视频上传成功").data("videoId", videoId);
}
}
6、启动后端vod微服务
7、swagger测试
成功!
七、前端整合视频上传
1、配置nginx反向代理
将接口地址加入nginx配置
location ~ /vod/ {
proxy_pass http://localhost:8003;
}
配置nginx上传文件大小,否则上传时会有 413 (Request Entity Too Large) 异常
打开nginx主配置文件nginx.conf,找到http{},添加
client_max_body_size 1024m;
重启nginx
nginx -s reload
2、视频上传前端实现
1、数据定义
fileList: [],//上传文件列表
BASE_API: process.env.BASE_API // 接口API地址
2、整合上传组件
<el-form-item label="上传视频">
<el-upload
:on-success="handleVodUploadSuccess"
:on-remove="handleVodRemove"
:before-remove="beforeVodRemove"
:on-exceed="handleUploadExceed"
:file-list="fileList"
:action="BASE_API+'/admin/vod/video/upload'"
:limit="1"
class="upload-demo">
<el-button size="small" type="primary">上传视频</el-button>
<el-tooltip placement="right-end">
<div slot="content">最大支持1G,<br>
支持3GP、ASF、AVI、DAT、DV、FLV、F4V、<br>
GIF、M2T、M4V、MJ2、MJPEG、MKV、MOV、MP4、<br>
MPE、MPG、MPEG、MTS、OGG、QT、RM、RMVB、<br>
SWF、TS、VOB、WMV、WEBM 等视频格式上传</div>
<i class="el-icon-question"/>
</el-tooltip>
</el-upload>
</el-form-item>
3、方法定义
//成功回调
handleVodUploadSuccess(response, file, fileList) {
this.video.videoSourceId = response.data.videoId
},
//视图上传多于一个视频
handleUploadExceed(files, fileList) {
this.$message.warning('想要重新上传视频,请先删除已上传的视频')
},
这里后端调用的是后端第六节整合阿里云上传的接口,直接调用。
4、测试 文件上传成功!
3、视频删接口
这里的视频删除不是删除小节,而是上传视频之后的删除 。点击x号进行删除。
文档:服务端SDK->Java SDK->媒资管理
https://help.aliyun.com/document_detail/61065.html?spm=a2c4g.11186623.6.831.654b3815cIxvma#h2–div-id-deletevideo-div-7
一、后端
1、service
接口
void removeVideo(String videoId);
实现
@Override
public void removeVideo(String videoId) {
try{
DefaultAcsClient client = AliyunVodSDKUtils.initVodClient(
ConstantPropertiesUtil.ACCESS_KEY_ID,
ConstantPropertiesUtil.ACCESS_KEY_SECRET);
DeleteVideoRequest request = new DeleteVideoRequest();
request.setVideoIds(videoId);
DeleteVideoResponse response = client.getAcsResponse(request);
System.out.print("RequestId = " + response.getRequestId() + "\n");
}catch (ClientException e){
throw new GuliException(20001, "视频删除失败");
}
}
2、controller
@DeleteMapping("{videoId}")
public R removeVideo(@ApiParam(name = "videoId", value = "云端视频id", required = true)
@PathVariable String videoId){
videoService.removeVideo(videoId);
return R.ok().message("视频删除成功");
}
二、前端
1、定义api
api/edu/vod.js
import request from '@/utils/request'
const api_name = '/admin/vod/video'
export default {
removeById(id) {
return request({
url: `${api_name}/${id}`,
method: 'delete'
})
}
}
2、组件方法
views/edu/course/chapter.vue
import vod from '@/api/edu/vod'
beforeVodRemove(file, fileList) {
return this.$confirm(`确定移除 ${file.name}?`)
},
handleVodRemove(file, fileList) {
console.log(file)
vod.removeById(this.video.videoSourceId).then(response=>{
this.$message({
type: 'success',
message: response.message
})
})
},
4、小节删除
小节删除涉及到springcloud服务注册与发现,服务调用等。下一节详解。
八、服务调用(小节管理)
1、Feign服务调用,删除视频
1、基本概念
关于feign的文章写在另一篇中,可以去我主页查看:
https://blog.csdn.net/wang121213145/category_11655698.html?spm=1001.2014.3001.5482
二、实现服务调用
1、需求
删除课时的同时删除云端视频
2、服务注册
服务注册在:https://blog.csdn.net/wang121213145/category_11655698.html?spm=1001.2014.3001.5482
使用nacos注册中心
在service模块添加pom依赖
<!--服务调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
3、在调用端的启动类添加注解
@EnableFeignClients
4、创建包和接口
@Component
@FeignClient("service_vod") //要调用服务的服务名
public interface VodClient {
/**
* @description: 定义调用方法的路径
* 调用service_vod中的删除视频接口
*
* 注意要写成全路径 @PathVariable 要加("id") 这样就可以调用这个接口中的服务
* @author wangxihao
* @email wangxh0108@163.com
**/
@DeleteMapping("/vod/video/{videoId}")
public R removeVideo(@PathVariable("videoId") String videoId);
}
5、调用微服务
在EduVideoController中
// TODO 待完善 课时部分 ---已完善
@ApiOperation(value = "删除小节")
@DeleteMapping("deleteVideo/{videoid}")
public R deleteById(@PathVariable String videoid){
//删除小节中的视频 根据视频id
//先得到视频id
EduVideo byId = eduVideoService.getById(videoid);
String videoSourceId = byId.getVideoSourceId();
if(!StringUtils.isEmpty(videoSourceId)){
//远程调用 删除视频
vodClient.removeVideo(videoSourceId);
}
//删除小节
eduVideoService.removeById(videoid);
return R.ok();
}
6、测试
启动相关微服务
Swagger测试删除课时的功能
删除成功!
2、小节CRUD
和章节大差不差,这里就不一一写了。
九、课程删除
list.vue
细节自己搞定,这里不过多描述
1、删除课程
后端主要代码:
//删除课程
@DeleteMapping("{courseId}")
public R deleteCourse(@PathVariable String courseId) {
eduCourseService.removeCourse(courseId);
return R.ok();
}
这里依然使用微服务调用批量删除视频
service
//删除课程
@Override
public void removeCourse(String courseId) {
//1 根据课程id删除小节
eduVideoService.removeVideoByCourseId(courseId);
//2 根据课程id删除章节
eduChapterService.removeChapterByCourseId(courseId);
//3 根据课程id删除描述
eduCourseDescriptionService.removeById(courseId);
//4 根据课程id删除课程本身
int result = baseMapper.deleteById(courseId);
if(result == 0) { //失败返回
throw new XiiException(20001,"删除失败");
}
}
微服务中视频删除接口:
@Override
public void removeMoreAlyVideo(List videoIdList) {
try {
//初始化对象
DefaultAcsClient client = AliyunVodSDKUtils.initVodClient(ConstantPropertiesUtil.ACCESS_KEY_ID, ConstantPropertiesUtil.ACCESS_KEY_SECRET);
//创建删除视频request对象
DeleteVideoRequest request = new DeleteVideoRequest();
//videoIdList值转换成 1,2,3
String videoIds = org.apache.commons.lang.StringUtils.join(videoIdList.toArray(), ",");
//向request设置视频id
request.setVideoIds(videoIds);
//调用初始化对象的方法实现删除
client.getAcsResponse(request);
}catch(Exception e) {
e.printStackTrace();
throw new XiiException(20001,"删除视频失败");
}
}
小节实现类
eduvideoserviceimpl
package com.xii.eduservice.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xii.eduservice.client.VodClient;
import com.xii.eduservice.entity.EduVideo;
import com.xii.eduservice.mapper.EduVideoMapper;
import com.xii.eduservice.service.EduVideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
/**
* <p>
* 课程视频 服务实现类
* </p>
*
* @author xhjava
* @since 2022-02-13
*/
@Service
public class EduVideoServiceImpl extends ServiceImpl<EduVideoMapper, EduVideo> implements EduVideoService {
//注入vodClient
@Autowired
private VodClient vodClient;
//1 根据课程id删除小节
@Override
public void removeVideoByCourseId(String courseId) {
//1 根据课程id查询课程所有的视频id
QueryWrapper<EduVideo> wrapperVideo = new QueryWrapper<>();
wrapperVideo.eq("course_id",courseId);
wrapperVideo.select("video_source_id");
List<EduVideo> eduVideoList = baseMapper.selectList(wrapperVideo);
// List<EduVideo>变成List<String>
List<String> videoIds = new ArrayList<>();
for (int i = 0; i < eduVideoList.size(); i++) {
EduVideo eduVideo = eduVideoList.get(i);
String videoSourceId = eduVideo.getVideoSourceId();
if(!StringUtils.isEmpty(videoSourceId)) {
//放到videoIds集合里面
videoIds.add(videoSourceId);
}
}
//根据多个视频id删除多个视频
if(videoIds.size()>0) {
vodClient.deleteBatch(videoIds);
}
QueryWrapper<EduVideo> wrapper = new QueryWrapper<>();
wrapper.eq("course_id",courseId);
baseMapper.delete(wrapper);
}
}
前端参照教师列表的增删改,这里省略…
测试
阿里云视频成功删除!
十、Hystrix基本概念
1、Spring Cloud调用接口过程
Spring Cloud 在接口调用上,大致会经过如下几个组件配合:
Feign ----->Hystrix —>Ribbon —>Http Client(apache http components 或者 Okhttp) 具体交互流程上,如下图所示:
(1)接口化请求调用当调用被@FeignClient注解修饰的接口时,在框架内部,将请求转换成Feign的请求实例feign.Request,交由Feign框架处理。
(2)Feign :转化请求Feign是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求,封装了Http调用流程。
(3)Hystrix:熔断处理机制 Feign的调用关系,会被Hystrix代理拦截,对每一个Feign调用请求,Hystrix都会将其包装成HystrixCommand,参与Hystrix的流控和熔断规则。如果请求判断需要熔断,则Hystrix直接熔断,抛出异常或者使用FallbackFactory返回熔断Fallback结果;如果通过,则将调用请求传递给Ribbon组件。
(4)Ribbon:服务地址选择 当请求传递到Ribbon之后,Ribbon会根据自身维护的服务列表,根据服务的服务质量,如平均响应时间,Load等,结合特定的规则,从列表中挑选合适的服务实例,选择好机器之后,然后将机器实例的信息请求传递给Http Client客户端,HttpClient客户端来执行真正的Http接口调用;
(5)HttpClient :Http客户端,真正执行Http调用根据上层Ribbon传递过来的请求,已经指定了服务地址,则HttpClient开始执行真正的Http请求
2、Hystrix概念
Hystrix 是一个供分布式系统使用,提供延迟和容错功能,保证复杂的分布系统在面临不可避免的失败时,仍能有其弹性。
比如系统中有很多服务,当某些服务不稳定的时候,使用这些服务的用户线程将会阻塞,如果没有隔离机制,系统随时就有可能会挂掉,从而带来很大的风险。SpringCloud使用Hystrix组件提供断路器、资源隔离与自我修复功能。下图表示服务B触发了断路器,阻止了级联失败
3、feign结合Hystrix使用
改造service-edu模块
1、在service的pom中添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<!--hystrix依赖,主要是用 @HystrixCommand -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2、在配置文件中添加hystrix配置
#开启熔断机制
feign.hystrix.enabled=true
# 设置hystrix超时时间,默认1000ms
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=6000
3、在service-edu的client包里面创建熔断器的实现类
@Component
public class VodFileDegradeFeignClient implements VodClient {
@Override
public R removeVideo(String videoId) {
return R.error().message("time out");
}
@Override
public R removeVideoList(List videoIdList) {
return R.error().message("time out");
}
}
4、修改VodClient接口的注解
@FeignClient(name = "service-vod", fallback = VodFileDegradeFeignClient.class)
@Component
public interface VodClient {
@DeleteMapping(value = "/eduvod/vod/{videoId}")
public R removeVideo(@PathVariable("videoId") String videoId);
@DeleteMapping(value = "/eduvod/vod/delete-batch")
public R removeVideoList(@RequestParam("videoIdList") List videoIdList);
}
5、测试熔断器效果