添加课程以及课程描述前后端实现、头像上传,封面上传,课程大纲实现,课程回显实现,章节的CRUD,阿里云视频点播技术,nginx的反向代理,使用springcloud服务注册与发现,feign调用(八)


添加课程以及课程描述前后端实现、头像上传,封面上传,课程大纲实现,课程回显实现,章节的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、测试熔断器效果

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杵意

谢谢金主打赏呀!!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值