2022年最新《谷粒学院开发教程》:5 - 章节管理

资料
资料地址
后台管理系统目录前台展示系统目录
1 - 构建工程篇7 - 渲染前台篇
2 - 前后交互篇8 - 前台登录篇
3 - 文件上传篇9 - 前台课程篇
4 - 课程管理篇10 - 前台支付篇
5 - 章节管理篇11 - 统计分析篇
6 - 微服务治理12 - 项目完结篇


一、课程章节增删改查

1.1、后端接口

@Api(tags = "章节模块")
@CrossOrigin
@RestController
@RequestMapping("/eduservice/chapter")
public class EduChapterController {

    @Autowired
    private EduChapterService eduChapterService;

    @Autowired
    EduCourseService eduCourseService;

    // 添加章节
    @PostMapping("addChapter")
    public R addChapter(@RequestBody EduChapter eduChapter) {
        eduChapterService.save(eduChapter);
        return R.ok();
    }

    // 根据章节id查询
    @GetMapping("getChapter/{chapterId}")
    public R getChapter(@PathVariable String chapterId) {
        EduChapter eduChapter = eduChapterService.getById(chapterId);
        return R.ok().data("chapter", eduChapter);
    }

    // 修改章节
    @PostMapping("updateChapter")
    public R updateChapter(@RequestBody EduChapter eduChapter) {
        eduChapterService.updateById(eduChapter);
        return R.ok();
    }

    // 删除章节 若存在小节则不可删除
    @DeleteMapping("deleteById/{chapterId}")
    public R deleteById(@PathVariable String chapterId) {
        boolean flag = eduChapterService.deleteChapter(chapterId);
        if (flag) {
            return R.ok();
        } else {
            return R.error();
        }
    }

}
@Override
public boolean deleteChapter(String chapterId) {
    QueryWrapper<EduVideo> wrapper = new QueryWrapper<>();
    wrapper.eq("chapter_id", chapterId);
    int count = eduVideoService.count(wrapper);
    
    if (count > 0) {
        throw new LaptoyException(20001, "还有小节数据,不能删除");
    } else {
        return this.removeById(chapterId);
    }
}

1.2、前端实现

1、页面代码

<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-collapse accordion v-for="chapter in chapterVideoList" :key="chapter.id">
      <!-- 按钮组 -->
      <el-button-group>
        <el-button type="primary" size="mini" icon="el-icon-circle-plus">添加小节</el-button>
        <el-button type="primary" size="mini" icon="el-icon-edit" @click="openEditChapter(chapter.id)"></el-button>
        <el-button type="danger" size="mini" icon="el-icon-delete" @click="removeById(chapter.id)"></el-button>
      </el-button-group>
      <!-- 小节数据 -->
      <el-collapse-item :title=chapter.title>
        <div v-for="video in chapter.children" :key="video.id">
          {{ video.title }}
          <span class="acts">
            <el-button type="text">编辑</el-button>
            <el-button type="text">删除</el-button>
          </span>
        </div>
      </el-collapse-item>
    </el-collapse>

    <!-- 底部按钮 -->
    <el-form label-width="120px">
      <el-form-item>
        <el-button @click="dialogChapterFormVisible=true">添加章节</el-button>
        <el-button @click="previous">上一步</el-button>
        <el-button :disabled="saveBtnDisabled" type="primary" @click="next">下 一步</el-button>
      </el-form-item>
    </el-form>

    <!-- 添加或修改章节弹框 -->
    <el-dialog :visible.sync="dialogChapterFormVisible" title="添加章节">
      <el-form :model="chapter" label-width="120px">
        <el-form-item label="章节标题">
          <el-input v-model="chapter.title" />
        </el-form-item>
        <el-form-item label="章节排序">
          <el-input-number v-model="chapter.sort" :min="0" controls-position="right" />
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogChapterFormVisible = false">取 消</el-button>
        <el-button type="primary" @click="saveOrUpdate">确 定</el-button>
      </div>
    </el-dialog>
  </div>
</template>
data() {
  return {
    dialogChapterFormVisible: false,
    chapter: {
      title: "",
      sort: 0
    },
  };
},

稍微优化了页面
在这里插入图片描述


2、创建api - chapter.js

//添加章节
addChapter(chapter) {
    return request({
        url: `/eduservice/chapter/addChapter`,
        method: `post`,
        data: chapter
    })
},
//根据id查询章节
updateChapterById(chapterId) {
    return request({
        url: `/eduservice/chapter/getChapter/${chapterId}`,
        method: `get`,
    })
},
//修改章节
updateChapter(chapter) {
    return request({
        url: `/eduservice/chapter/updateChapter`,
        method: `post`,
        data: chapter
    })
},
//删除章节
deleteById(chapterId) {
    return request({
        url: `/eduservice/chapter/deleteById/${chapterId}`,
        method: `delete`,
    })
}

1.3、添加章节

saveChapter() {
  //设置课程id到chapter对象中,不指定课程id无法正常保存,因为数据库字段为非空
  this.chapter.courseId = this.courseId
  chapter.addChapter(this.chapter).then((resp) => {
    this.dialogChapterFormVisible = false;
    this.$message({
      message: "添加章节成功",
      type: "success",
    });
    this.chapter = {}
    this.getChapterVideoByCourseId()
  });
},
saveOrUpdate() {
  this.saveChapter()
},

1.4、修改章节

1、点击修改章节按钮根据章节id回显数据

<el-button type="primary" size="mini" icon="el-icon-edit" 
		   @click="openEditChapter(chapter.id)"></el-button>

2、修改方法

// 修改章节弹窗回显数据
openEditChapter(id) {
  this.dialogChapterFormVisible = true;
  chapter.updateChapterById(id).then((resp) => {
    this.chapter = resp.data.data
  })
},
// 修改章节
updateChapter(id) {
  //设置课程id到chapter对象中,不指定课程id无法正常保存,因为数据库字段为非空
  this.chapter.courseId = this.courseId
  chapter.updateChapter(this.chapter).then((resp) => {
    this.dialogChapterFormVisible = false;
    this.$message({
      message: "修改章节成功",
      type: "success",
    });
    this.getChapterVideoByCourseId();
  });
},

1.5、删除章节

<el-button type="danger" size="mini" icon="el-icon-delete" 
		   @click="removeById(chapter.id)"></el-button>
// 删除章节
removeById(chapterId) {
  this.$confirm("此操作将永久删除章节信息, 是否继续?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    chapter.deleteById(chapterId).then((resp) => {
      this.$message({
        type: "success",
        message: "删除成功!",
      });
      this.getChapterVideoByCourseId();
    });
  });
},

二、课程章节小节功能

2.1、后端接口

@Api(tags = "小节模块")
@RestController
@RequestMapping("/eduservice/video")
@CrossOrigin //解决跨域问题
public class EduVideoController {

    @Autowired
    private EduVideoService eduVideoService;

    //添加小节
    @PostMapping("/addVideo")
    public R addVideo(@RequestBody EduVideo eduVideo) {
        eduVideoService.save(eduVideo);
        return R.ok();
    }

    //删除小节
    // TODO 后面这个方法需要完善,删除小节的时候,同时也要把视频删除
    @DeleteMapping("/deleteVideo/{id}")
    public R deleteVideo(@PathVariable String id) {
        eduVideoService.removeById(id);
        return R.ok();
    }

    //修改小节
    @PostMapping("/updateVideo")
    public R updateVideo(@RequestBody EduVideo eduVideo) {
        eduVideoService.updateById(eduVideo);
        return R.ok();
    }

    //根据小节id查询
    @GetMapping("/getVideoById/{videoId}")
    public R getVideoById(@PathVariable String videoId) {
        EduVideo eduVideo = eduVideoService.getById(videoId);
        return R.ok().data("data", eduVideo);
    }
}

2.2、前端实现

1、页面

<!-- 章节数据折叠面板 -->
<el-collapse accordion v-for="chapter in chapterVideoList" :key="chapter.id">
  <!-- 按钮组 -->
  <el-button-group>
    <el-button type="primary" size="mini" icon="el-icon-circle-plus" 
      		   @click="openSaveVideoForm(chapter.id)">添加小节</el-button>
  </el-button-group>
</el-collapse>

<!--添加小节表单-->
<el-dialog :visible.sync="dialogVideoFormVisible" title="添加小节">
  <el-form :model="video" label-width="120px">
    <el-form-item label="小节标题">
      <el-input v-model="video.title" />
    </el-form-item>
    <el-form-item label="小节排序">
      <el-input-number v-model="video.sort" :min="0" controls-position="right" />
    </el-form-item>
    <el-form-item label="是否免费">
      <el-radio-group v-model="video.free">
        <el-radio :label="true">免费</el-radio>
        <el-radio :label="false">默认</el-radio>
      </el-radio-group>
    </el-form-item>
    <el-form-item label="上传视频">
      <!-- TODO -->
    </el-form-item>
  </el-form>
  <div slot="footer" class="dialog-footer">
    <el-button @click="dialogVideoFormVisible = false">取 消</el-button>
    <el-button :disabled="saveVideoBtnDisabled" type="primary" @click="saveOrUpdateVideo">确 定</el-button>
  </div>
</el-dialog>
data() {
  return {
    saveVideoBtnDisabled: false,
    dialogVideoFormVisible: false,
    video: {
      sort: 0,
      title: "",
      free: ""
    }
  };
},

2、API - src\api\teacher\video.js

import request from '@/utils/request' //引入已经封装好的axios 和 拦截器

export default {
    //添加小节
    addVideo(video) {
        return request({
            url: `/eduservice/video/addVideo`,
            method: `post`,
            data: video
        })
    },
    //根据id查询小节
    getVideoById(videoId) {
        return request({
            url: `/eduservice/video/getVideoById/${videoId}`,
            method: `get`,
        })
    },
    //修改小节
    updateVideo(video) {
        return request({
            url: `/eduservice/video/updateVideo`,
            method: `post`,
            data: video
        })
    },
    //删除小节
    deleteById(videoId) {
        return request({
            url: `/eduservice/video/deleteVideo/${videoId}`,
            method: `delete`,
        })
    },
}

2.3、添加小节

openSaveVideoForm(id) {
  this.dialogVideoFormVisible = true
  this.video.courseId = this.courseId
  this.video.chapterId = id
},
saveOrUpdateVideo(){
  if (this.video.id) {
    //修改小节
  } else {
    //新增小节
    this.saveVideo();
  }
},
// 添加小节
saveVideo() {
  video.addVideo(this.video).then((resp) => {
    this.dialogVideoFormVisible = false;
    this.$message({
      message: "添加小节成功",
      type: "success",
    });
    this.video = {}
    this.getChapterVideoByCourseId();
  });
},

2.4、删除小节

<!-- 章节数据折叠面板 -->
<el-collapse accordion v-for="chapter in chapterVideoList" :key="chapter.id">
  <!-- 按钮组 -->
  
  <!-- 小节数据 -->
  <el-collapse-item :title=chapter.title>
    <div v-for="video in chapter.children" :key="video.id">
      {{ video.title }}
      <span class="acts">
        <el-button type="text">编辑</el-button>
        <el-button type="text" @click="removeVideo(video.id)">删除</el-button>
      </span>
    </div>
  </el-collapse-item>
</el-collapse>
// 删除小节
removeVideo(id) {
  this.$confirm("此操作将永久删除小节信息, 是否继续?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    video.deleteById(id).then((resp) => {
      this.$message({
        type: "success",
        message: "删除成功!",
      });
      this.getChapterVideoByCourseId();
    });
  });
},

2.5、修改小节

<!-- 章节数据折叠面板 -->
<el-collapse accordion v-for="chapter in chapterVideoList" :key="chapter.id">
  <!-- 按钮组 -->

  <!-- 小节数据 -->
  <el-collapse-item :title=chapter.title>
    <div v-for="video in chapter.children" :key="video.id">
      {{ video.title }}
      <span class="acts">
        <el-button type="text" @click="openEditVideoForm(video.id)">编辑</el-button>
        <el-button type="text" @click="removeVideo(video.id)">删除</el-button>
      </span>
    </div>
  </el-collapse-item>
</el-collapse>
// 展开编辑表单
openEditVideoForm(id) {
  this.dialogVideoFormVisible = true;
  video.getVideoById(id).then((resp) => {
    this.video = resp.data.data;
  });
},

// 修改小节
updateVideo() {
  video.updateVideo(this.video).then((resp) => {
    this.dialogVideoFormVisible = false;
    this.$message({
      message: "修改小节成功",
      type: "success",
    });
    this.getChapterVideoByCourseId();
  });
},

// 添加或修改小节
saveOrUpdateVideo() {
  if (this.video.id) {
    //修改小节
    this.updateVideo();
  } else {
    //新增小节
    this.saveVideo();
  }
},

三、课程发布信息预览

3.1、后端接口

1、创建 VO

@Data
public class CoursePublishVo implements Serializable {

    private static final long serialVersionUID = 1L;

    private String id;//课程id

    private String title; //课程名称

    private String cover; //封面

    private Integer lessonNum;//课时数

    private String subjectLevelOne;//一级分类

    private String subjectLevelTwo;//二级分类

    private String teacherName;//讲师名称

    private String price;//价格 ,只用于显示

}

2、控制层

// 根据课程id查询课程确认信息
@GetMapping("/getPublishCourseInfo/{id}")
public R getPublishCourseInfo(@PathVariable String id) {
    CoursePublishVo publishCourseInfo = eduCourseService.getPublishCourseInfo(id);
    return R.ok().data("data", publishCourseInfo);
}

3、业务层

@Override
public CoursePublishVo getPublishCourseInfo(String id) {
    return eduCourseMapper.getPublishCourseInfo(id);
}

4、数据层

CoursePublishVo getPublishCourseInfo(String id);
<select id="getPublishCourseInfo" resultType="com.laptoy.eduservice.entity.vo.CoursePublishVo">
    SELECT ec.id,
           ec.title,
           ec.cover,
           ec.lesson_num AS lessonNum,
           ec.price,
           s1.title      AS subjectLevelOne,
           s2.title      AS subjectLevelTwo,
           t.name        AS teacherName
    FROM edu_course ec
             LEFT JOIN edu_teacher t ON ec.teacher_id = t.id
             LEFT JOIN edu_subject s1 ON ec.subject_parent_id = s1.id
             LEFT JOIN edu_subject s2 ON ec.subject_id = s2.id
    WHERE ec.id = #{id}
</select>

5、将xml文件放到 resources/xml 目录下,并再YML文件指定

mybatis-plus:
  mapper-locations: /mapper/*.xml

在这里插入图片描述


3.3、前端实现

1、定义API - course.js

//课程确认信息显示
getPublishCourseInfo(courseId){
    return request({
        url:"/eduservice/course/getpublishCourseInfo/"+courseId,
        method: 'get',
    })
}

2、页面

<div class="ccInfo">
  <img :src="publishCourseInfo.cover" />
  <div class="main">
    <h2>{{ publishCourseInfo.title }}</h2>
    <p class="gray">
      <span>共{{ publishCourseInfo.lessonNum }}课时</span>
    </p>
    <p>
      <span>所属分类:{{ publishCourseInfo.subjectLevelOne }} : {{ publishCourseInfo.subjectLevelTwo }}</span>
    </p>
    <p>课程讲师:{{ publishCourseInfo.teacherName }}</p>
    <h3 class="red">¥{{ publishCourseInfo.price }}</h3>
  </div>
</div>

3、方法

export default {
  data() {
    return {
      saveBtnDisabled: false,
      courseId: '',
      publishCourseInfo: {},
    };
  },
  methods: {
    //根据课程id查询
    getPublishCourseInfo() {
      course.getPublishCourseInfo(this.courseId)
        .then(resp => {
          this.publishCourseInfo = resp.data.data
          console.log(this.publishCourseInfo)
        })
    },
    // 跳转到上一步
    previous() {
      this.$router.push("/course/chapter/" + this.courseId);
    },
    publish() {
      this.$router.push("/course/list");
    }
  },
  created() {
    //获取路由中的id值
    if (this.$route.params && this.$route.params.id) {
      this.courseId = this.$route.params.id
      //调用接口方法根据课程id查询课程信息
      this.getPublishCourseInfo()
    }
  },
};

4、css

<style scoped>
.ccInfo {
  background: #f5f5f5;
  padding: 20px;
  overflow: hidden;
  border: 1px dashed #ddd;
  margin-bottom: 40px;
  position: relative;
}
.ccInfo img {
  background: #d6d6d6;
  width: 500px;
  height: 278px;
  display: block;
  float: left;
  border: none;
}
.ccInfo .main {
  margin-left: 520px;
}
.ccInfo .main h2 {
  font-size: 28px;
  margin-bottom: 30px;
  line-height: 1;
  font-weight: normal;
}
.ccInfo .main p {
  margin-bottom: 10px;
  word-wrap: break-word;
  line-height: 24px;
  max-height: 48px;
  overflow: hidden;
}
.ccInfo .main p {
  margin-bottom: 10px;
  word-wrap: break-word;
  line-height: 24px;
  max-height: 48px;
  overflow: hidden;
}
.ccInfo .main h3 {
  left: 540px;
  bottom: 20px;
  line-height: 1;
  font-size: 28px;
  color: #d32f24;
  font-weight: normal;
  position: absolute;
}
</style>

5、测试

在这里插入图片描述
在这里插入图片描述


四、课程发布

4.1、后端接口

1、控制层

//课程最终发布
//修改课程状态
@PostMapping("publishCourse/{id}")
public R publishCourse(@PathVariable String id){
    EduCourse eduCourse = new EduCourse();
    eduCourse.setStatus("Normal"); //设置课程发布状态
    eduCourse.setId(id);
    boolean flag = eduCourseService.updateById(eduCourse);
    if (flag){
        return R.ok();
    }else {
        return R.error();
    }
}

4.2、前端页面

1、API

//课程最终发布
publishCourse(courseId) {
    return request({
        url: "/eduservice/course/publishCourse/" + courseId,
        method: 'post',
    })
}

2、方法

//发布课程
publish() {
  this.$confirm("你确定要发布此课程, 是否继续?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    course.publishCourse(this.courseId).then((resp) => {
      this.$message({
        message: "课程发布成功",
        type: "success",
      });
      //跳转课程列表页面
      this.$router.push({ path: "/course/list" });
    });
  });
}

五、课程列表

5.1、后端接口

1、实体类作为模糊查询条件

@ApiModel(value = "Course查询对象", description = "课程查询对象封装")
@Data
public class CourseQuery implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "课程名称,模糊查询")
    private String title;

    @ApiModelProperty(value = "发布状态 Normal已发布 Draft未发布")
    private String status;
}

2、控制层

//多条件查询课程带分页
@ApiOperation(value = "多条件查询课程带分页")
@PostMapping("/pageCourseCondition/{page}/{limit}")
public R pageCourseCondition(@ApiParam(name = "page", value = "当前页码", required = true) @PathVariable Long page,
                             @ApiParam(name = "limit", value = "每页记录数", required = true) @PathVariable Long limit,
                             @RequestBody(required = false) CourseQuery courseQuery) {//通过封装courseQuery对象来直接传递查询条件
    //创建分页page对象
    Page<EduCourse> pageParam = new Page<>(page, limit);

    //调用方法实现多条件分页查询
    eduCourseService.pageQuery(pageParam, courseQuery);

    //获取查询到的数据
    List<EduCourse> records = pageParam.getRecords();

    //获取总记录数
    long total = pageParam.getTotal();
    return R.ok().data("total", total).data("data", records);
}

3、业务层

@Override
public void pageQuery(Page<EduCourse> pageParam, CourseQuery courseQuery) {
    QueryWrapper<EduCourse> wrapper = new QueryWrapper<>();
    
    String title = courseQuery.getTitle();
    String status = courseQuery.getStatus();
    
    if (!StringUtils.isEmpty(title)) {
        wrapper.like("title", title);//参数1:数据库字段名; 参数2:模糊查询的值
    }
    if (!StringUtils.isEmpty(status)) {
        wrapper.eq("status", status);
    }
    wrapper.orderByDesc("gmt_create");
    baseMapper.selectPage(pageParam, wrapper);
}

5.2、前端实现

1、页面

<template>
  <div>
	<!--多条件查询表单-->
	<el-form :inline="true" class="demo-form-inline" style="margin-left: 20px; margin-top: 12px">
	  <el-form-item label="课程名称">
	    <el-input v-model="courseQuery.title" placeholder="请输入名称"></el-input>
	  </el-form-item>
	  <el-form-item label="发布状态">
	    <el-select v-model="courseQuery.status" placeholder="课程状态">
	      <el-option label="已发布" :value="'Normal'"></el-option>
	      <el-option label="未发布" :value="'Draft'"></el-option>
	    </el-select>
	  </el-form-item>
	  <el-form-item>
	    <el-button type="primary" icon="el-icon-search" @click="getList()">查询</el-button>
	    <el-button type="default" @click="resetData()">清空</el-button>
	  </el-form-item>
	</el-form>
	
	<!-- 展示表格 -->
    <el-table :data="list" style="width: 100%" height="620" border fit highlight-current-row element-loading-text="数据加载中">
      <el-table-column prop="date" label="序号" width="70" align="center">
        <template slot-scope="scope">
          {{ (page - 1) * limit + scope.$index + 1 }}
        </template>
      </el-table-column>
      <el-table-column prop="title" label="课程名称" width="400">
      </el-table-column>
      <el-table-column label="发布状态" width="80">
        <template slot-scope="scope">
          {{ scope.row.status === "Normal" ? "已发布" : "未发布" }}
        </template>
      </el-table-column>
      <el-table-column prop="lessonNum" label="课时数" width="100" />
      <el-table-column prop="gmtCreate" label="添加时间" width="300" />
      <el-table-column prop="viewCount" label="浏览数量" width="200" />
      <el-table-column label="操作" align="center">
        <template slot-scope="scope">
          <router-link :to="'/teacher/edit/' + scope.row.id">
            <el-button type="primary" size="mini" icon="el-icon-edit" plain>编辑基本信息</el-button>
          </router-link>
          <router-link :to="'/teacher/edit/' + scope.row.id">
            <el-button type="info" size="mini" icon="el-icon-edit" plain>编辑课程大纲</el-button>
          </router-link>
          <el-button type="danger" size="mini" icon="el-icon-delete" @click="removeById(scope.row.id)" plain>点击删除课程</el-button>
        </template>
      </el-table-column>
    </el-table>
    
    <!--分页组件-->
    <el-pagination background layout="prev, pager, next,total,jumper" :total="total" :page-size="limit" style="padding: 30px 0; text-align: center" :current-page="page" @current-change="getList">
    </el-pagination>
  </div>
</template>

2、API - course.js

//课程列表多条件分页查询
//page:当前页,limit:每页记录数,teacherQuery:条件对象
getCourseListPage(page, limit, courseQuery) {
    return request({
        url: `/eduservice/course/pageCourseCondition/${page}/${limit}`,
        method: 'post',
        data: courseQuery
    })
},

3、js

import course from "@/api/teacher/course.js";

export default {
  data() {
    return {
      list: null, //查询之后给接口返回的数据装的集合
      page: 1, //当前页
      limit: 10, //每页显示记录数
      courseQuery: {}, //条件封装对象
      total: 0, //总记录数
    };
  },
  created() {
    this.getList();
  },
  methods: {
    getList(page = 1) {
      this.page = page;
      course
        .getCourseListPage(this.page, this.limit, this.courseQuery)
        .then((resp) => {
          this.list = resp.data.data;
          this.total = resp.data.total;
        }) //请求成功
        .catch((err) => {
          console.log(err);
        }); //请求失败
    },
    //清空方法
    resetData() {
      //表单输入项数据清空
      this.courseQuery = {};
      //查询所有课程数据
      this.getList();
    },
  },
};

在这里插入图片描述


六、课程删除

6.1、后端接口

1、控制层

//课程列表中删除课程方法
@DeleteMapping("/removeCourseById/{id}")
public R removeCourseById(@PathVariable String id) {
    boolean flag = eduCourseService.removeCourse(id);
    if (flag) {
        return R.ok();
    } else {
        return R.error();
    }
}

2、业务层

//删除课程
@Transactional
@Override
public boolean removeCourse(String id) {
    //1、根据课程id删除小节
    eduVideoService.removeVideoByCourseId(id);
    
    //2、根据课程id删除章节部分
    eduChapterService.removeChapterByCourseId(id);
    
    //3、根据课程id删除课程描述
    descriptionService.removeById(id);
    
    //4、根据课程id删除课程本身
	boolean flag = this.removeById(id);
    
    if (flag) {
        return true;
    } else {
        throw new LaptoyException(20001, "删除失败");
    }
}

3、删除小节

//根据课程id删除小节
// TODO 删除小节,要删除对应的视频文件
@Override
public void removeVideoByCourseId(String id) {
    QueryWrapper<EduVideo> wrapper = new QueryWrapper<>();
    wrapper.eq("course_id", id);
    baseMapper.delete(wrapper);
}

4、删除章节

@Override
public void removeChapterByCourseId(String id) {
    QueryWrapper<EduChapter> wrapper = new QueryWrapper<>();
    wrapper.eq("course_id",id);
    baseMapper.delete(wrapper);
}

6.2、前端实现

1、API - course.js

//删除课程
removeCourseById(courseId) {
    return request({
        url: "/eduservice/course/removeCourseById/" + courseId,
        method: 'delete',
    })
}

2、方法

<el-button type="danger" size="mini" icon="el-icon-delete" 
		   @click="removeById(scope.row.id)" plain>点击删除课程</el-button>

<script>
// 删除课程
removeById(id) {
  this.$confirm("此操作将永久删除该课程记录, 是否继续?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    course.removeCourseById(id).then((resp) => {
      this.$message({
        type: "success",
        message: "删除成功!",
      });
      this.getList();
    });
  });
},
</script>

七、编辑基本信息及大纲

1、页面

<el-table-column label="操作" align="center">
  <template slot-scope="scope">
    <router-link :to="'/course/info/' + scope.row.id">
      <el-button type="primary" size="mini" icon="el-icon-edit" plain>编辑基本信息</el-button>
    </router-link>
    <router-link :to="'/course/chapter/' + scope.row.id">
      <el-button type="info" size="mini" icon="el-icon-edit" plain>编辑课程大纲</el-button>
    </router-link>
    <el-button type="danger" size="mini" icon="el-icon-delete" @click="removeById(scope.row.id)" plain>点击删除课程</el-button>
  </template>
</el-table-column>

2、添加路由

{
  path: 'info/:id',
  name: 'EduCourseInfoEdit',
  component: () => import('@/views/edu/course/info.vue'),
  meta: { title: '编辑课程基本信息', noCache: true },
  hidden: true
},
{
  path: 'chapter/:id',
  name: 'EduCourseChapterEdit',
  component: () => import('@/views/edu/course/chapter.vue'),
  meta: { title: '编辑课程大纲', noCache: true },
  hidden: true
},

八、阿里云视频点播

8.1、简介

视频点播( ApsaraVideo for VoD )是集音视频采集、编辑、上传、自动化转码处理、媒体资源管理、分发加速于一体的一站式音视频点播解决方案。

开通服务-按流量计费

在这里插入图片描述


8.2、使用

1、开启 存储管理

在这里插入图片描述

2、添加 转码模板组
在这里插入图片描述
在这里插入图片描述

3、上传视频

在这里插入图片描述


九、使用服务端SDK

9.1、简介

1、简介

  • sdk的方式将api进行了进一步的封装,不用自己创建工具类。
  • 我们可以基于服务端SDK编写代码来调用点播API,实现对点播产品和服务的快速操作

2、功能介绍

  • SDK封装了对API的调用请求和响应,避免自行计算较为繁琐的 API签名。
  • 支持所有点播服务的API,并提供了相应的示例代码。
  • 支持7种开发语言,包括:Java、Python、PHP、.NET、Node.js、Go、C/C++。
  • 通常在发布新的API后,我们会及时同步更新SDK,所以即便您没有找到对应API的示例代码,也可以参考旧的示例自行实现调用。

9.2、获取视频

1、在 service模块 新建微服务模块 service_vod

2、POM

<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.alibaba</groupId>
    <artifactId>fastjson</artifactId>
</dependency>
<dependency>
    <groupId>org.json</groupId>
    <artifactId>json</artifactId>
</dependency>
<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
</dependency>

3、测试获取视频播放地

//初始化类
public class InitObject {

    public static DefaultAcsClient initVodClient(String accessKeyId, String accessKeySecret) throws ClientException {
        String regionId = "cn-shanghai";  // 点播服务接入区域
        DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
        DefaultAcsClient client = new DefaultAcsClient(profile);
        return client;
    }

    public static void main(String[] args) throws ClientException {
        //1、根据视频id获取视频播放地址
        //创建初始化对象
        DefaultAcsClient cl = InitObject.initVodClient("LTAI5tL5FrVJBuQadij4KRvJ", "Xs7dHUvxCdHLd0K5iFK7NWEbdUN7GG");
        //创建获取视频地址request对象和response对象
        GetPlayInfoResponse response = new GetPlayInfoResponse();
        GetPlayInfoRequest request = new GetPlayInfoRequest();
        //向request对象设置视频id值
        request.setVideoId("1a383ac7aa7f4b8197714ca6f886e5be");


        //调用初始化对象里面的方法传递request,获取数据
        response = cl.getAcsResponse(request);
        List<GetPlayInfoResponse.PlayInfo> playInfoList = response.getPlayInfoList();
        //播放地址
        for (GetPlayInfoResponse.PlayInfo playInfo : playInfoList) {
            System.out.print("PlayInfo.PlayURL = " + playInfo.getPlayURL() + "\n");
        }
        //Base信息
        System.out.print("VideoBase.Title = " + response.getVideoBase().getTitle() + "\n");//VideoBase.Title = 6 - What If I Want to Move Faster.mp4
    }
}
PlayInfo.PlayURL = https://outin-327a9fa3dcae11ecbc2400163e1c8dba.oss-cn-shanghai.aliyuncs.com/sv/60fc3bbf-180ff24e597/60fc3bbf-180ff24e597.mp4?Expires=1653551691&OSSAccessKeyId=LTAIrkwb21KyGjJl&Signature=h0s4mbCbwFd8797gs1KXWxJAUpU%3D
VideoBase.Title = 视频点播控制台 - Google Chrome 2022-05-25 21-26-51.mp4

3、测试获取视频播放凭证(加密视频)

public static void main(String[] args) throws ClientException {
    //创建初始化对象
    DefaultAcsClient cl = InitObject.initVodClient("LTAI5tL5FrVJBuQadij4KRvJ", "Xs7dHUvxCdHLd0K5iFK7NWEbdUN7GG");
    //创建获取视频地址request对象和response对象
    GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();
    //向request对象设置视频id值
    request.setVideoId("1a383ac7aa7f4b8197714ca6f886e5be");

    GetVideoPlayAuthResponse response = cl.getAcsResponse(request);

    //播放凭证
    System.out.print("PlayAuth = " + response.getPlayAuth() + "\n");
    //VideoMeta信息
    System.out.print("VideoMeta.Title = " + response.getVideoMeta().getTitle() + "\n");
}
PlayAuth = eyJTZWN1cml0eVRva2VuIjoiQ0FJU2h3TjFxNkZ0NUIyeWZTaklyNWJESGZqVG40eHA0cUM1ZWw3WTFuay9kZUZHdWJmOW1qejJJSDlJZEhWb0FPOGZ2dlUwbTJ0WTdQc1psck1xR3NFZUhoZWJONUlwdDg0T29GMzlKcExGc3QySjZyOEpqc1VZcDVBQTdFYXBzdlhKYXNEVkVmbDJFNVhFTWlJUi8wMGU2TC8rY2lyWXBUWEhWYlNDbFo5Z2FQa09Rd0M4ZGtBb0xkeEtKd3hrMnQxNFVtWFdPYVNDUHdMU2htUEJMVXhtdldnR2wyUnp1NHV5M3ZPZDVoZlpwMXI4eE80YXhlTDBQb1AyVjgxbExacGxlc3FwM0k0U2M3YmFnaFpVNGdscjhxbHg3c3BCNVN5Vmt0eVdHVWhKL3phTElvaXQ3TnBqZmlCMGVvUUFQb3BGcC9YNmp2QWF3UExVbTliWXhncGhCOFIrWGo3RFpZYXV4N0d6ZW9XVE84MCthS3p3TmxuVXo5bUxMZU9WaVE0L1ptOEJQdzQ0RUxoSWFGMElVRTF5R21DQ2QvWDRvZ3VSUDF6N0VwTG9pdjltamNCSHFIeno1c2VQS2xTMVJMR1U3RDBWSUpkVWJUbHphRUpHZ1RTNExmWldJbGNUS0FNOVd1MlBNYXgzYlFGRHI1M3ZzVGJiWHpaYjBtcHR1UG56ZDE0Sk9CS2cxMUtVR29BQlUrY0Q3SXpmeDIvSHJmNXA2cWdSa2UvdDNaMjFPUXF1NkRsSkV3VnpPL2poWngva3JNNitGNWlEK25kT1JHQitZbDJtcTN6SlBnclQxNVdveE5QdXJndy9xbWNib1BWR3dtV01qa3RKOXo0YS90TnA0SWNLRGxKQStvOWFvOWdLbUpPZWJFMnZXT1FicE1LbGFmOER6Q1JCMTl5NmZDUi9qWDFObndub3RTQT0iLCJBdXRoSW5mbyI6IntcIkNJXCI6XCJLenVKNUlCVmRIc0E2eTNlTXFDbS9vYXVOMFZwVFBmYkU0L1VaUUt5c0dhVzc3MjRTWEZQejVyUHlyOGRSN0VaZXVFenpEdndDcDRIY2NoZFlkMjdMVy8yK0R2S1lySXRsVGxlTCtUa0lsZz1cIixcIkNhbGxlclwiOlwiNldQZVY4MkttMDlUbGNGUzVWeDB0SjUxY2pCWEVYb2FuZEk4dEU4OENjWT1cIixcIkV4cGlyZVRpbWVcIjpcIjIwMjItMDUtMjZUMDc6MDQ6NTZaXCIsXCJNZWRpYUlkXCI6XCIxYTM4M2FjN2FhN2Y0YjgxOTc3MTRjYTZmODg2ZTViZVwiLFwiU2lnbmF0dXJlXCI6XCI3Vjhza3RPTnRIK1NBZGV0bVFWRk9CU29vRmc9XCJ9IiwiVmlkZW9NZXRhIjp7IlN0YXR1cyI6Ik5vcm1hbCIsIlZpZGVvSWQiOiIxYTM4M2FjN2FhN2Y0YjgxOTc3MTRjYTZmODg2ZTViZSIsIlRpdGxlIjoi6KeG6aKR54K55pKt5o6n5Yi25Y+wIC0gR29vZ2xlIENocm9tZSAyMDIyLTA1LTI1IDIxLTI2LTUxLm1wNCIsIkNvdmVyVVJMIjoiaHR0cDovL291dGluLTMyN2E5ZmEzZGNhZTExZWNiYzI0MDAxNjNlMWM4ZGJhLm9zcy1jbi1zaGFuZ2hhaS5hbGl5dW5jcy5jb20vMWEzODNhYzdhYTdmNGI4MTk3NzE0Y2E2Zjg4NmU1YmUvc25hcHNob3RzLzNmOTg0ZTE0YjU5OTQxNTM5ODhmNDdiMGI1MDQxMDZhLTAwMDAxLmpwZz9FeHBpcmVzPTE2NTM1NTIxOTYmT1NTQWNjZXNzS2V5SWQ9TFRBSXJrd2IyMUt5R2pKbCZTaWduYXR1cmU9c040UEljem9UbUFPSFJzaHhRTXhMd3pxc09vJTNEIiwiRHVyYXRpb24iOjUuMzE3N30sIkFjY2Vzc0tleUlkIjoiU1RTLk5VdlZCZ3JTSFViUnh4aTJ5anlualZyVngiLCJBY2Nlc3NLZXlTZWNyZXQiOiI3dzRnaWtuSEFlbndCTVYxN2hqbXNqekw4cUZIdlRmZ0tHNjFtWDJ1cVdKcCIsIlJlZ2lvbiI6ImNuLXNoYW5naGFpIiwiQ3VzdG9tZXJJZCI6MTk2MzMzNzIxMTEyMTk2NH0=
VideoMeta.Title = 视频点播控制台 - Google Chrome 2022-05-25 21-26-51.mp4

9.3、上传视频

1、POM - 默认找不到该依赖

<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-sdk-vod-upload</artifactId>
</dependency>

2、将资料的 5-lib/aliyun-java-vod-upload-1.4.11.jar 放到maven仓库bin目录下并执行如下命令安装上述依赖

mvn install:install-file  -DgroupId=com.aliyun  -DartifactId=aliyun-sdk-vod-upload -Dversion=1.4.11 -Dpackaging=jar  -Dfile=aliyun-java-vod-upload-1.4.11.jar

3、测试

@Test
public void testFileUpload() {
    String accessKeyId = "LTAI5tL5FrVJBuQadij4KRvJ";
    String accessKeySecret = "Xs7dHUvxCdHLd0K5iFK7NWEbdUN7GG";
    String title = "My Upload Video"; //上传之后文件的名称
    String fileName = "C:\\Users\\lapto\\Desktop\\myVideo.mp4"; //本地文件的路径和名称
    UploadVideoRequest request = new UploadVideoRequest(accessKeyId, accessKeySecret, title, fileName);
    
    /* 可指定分片上传时每个分片的大小,默认为2M字节 */
    request.setPartSize(2 * 1024 * 1024L);
    /* 可指定分片上传时的并发线程数,默认为1,(注:该配置会占用服务器CPU资源,需根据服务器情况指定)*/
    request.setTaskNum(1);
    UploadVideoImpl uploader = new UploadVideoImpl();
    UploadVideoResponse response = uploader.uploadVideo(request);
    if (response.isSuccess()) {
        System.out.print("VideoId=" + response.getVideoId() + "\n"); //获取到上传视频的id
    } else {
        /* 如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因 */
        System.out.print("VideoId=" + response.getVideoId() + "\n");
        System.out.print("ErrorCode=" + response.getCode() + "\n");
        System.out.print("ErrorMessage=" + response.getMessage() + "\n");
    }
}

在这里插入图片描述


十、视频点播微服务

10.1、后端接口

1、service_vod微服务 主启动类

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@ComponentScan(basePackages = {"com.laptoy"}) //用于扫描swagger
public class Service_vod_Main8003 {
    public static void main(String[] args) {
        SpringApplication.run(Service_vod_Main8003.class, args);
    }
}

2、YML

# 服务端口号
server.port=8003
# 服务名
spring.application.name=service-vod
# nacos注册中心
spring.cloud.nacos.discovery.server-addr=120.76.55.55:8848
# 环境设置 dev test prod
spring.profiles.active=dev
# 最大上传单个文件大小:默认1M
spring.servlet.multipart.max-file-size=1024MB
# 最大置总上传的数据大小 :默认10M
spring.servlet.multipart.max-request-size=1024MB
# 阿里云 vod
aliyun.vod.file.keyid=LTAI5tL5FrVJBuQadij4KRvJ
aliyun.vod.file.keysecret=Xs7dHUvxCdHLd0K5iFK7NWEbdUN7GG

3、控制层

@RestController
@CrossOrigin
@RequestMapping("/eduvod/video")
public class VodController {

    @Autowired
    private VodService vodService;

    //上传视频到阿里云
    @PostMapping("/uploadAliyunVideo")
    public R uploadAliyunVideo(MultipartFile file) {
        //返回上传视频的id
        String videoId = vodService.uploadVideoAliyun(file);
        return R.ok().data("data", videoId);
    }

}

4、业务层

@Service
public class VodServiceImpl implements VodService {

    @Value("${aliyun.vod.file.keyid}")
    private String accessKeyId;

    @Value("${aliyun.vod.file.keysecret}")
    private String accessKeySecret;

    @Override
    public String uploadVideoAliyun(MultipartFile file) {

        try {
            //fileName:上传文件原始名称
            String fileName = file.getOriginalFilename();
            //title:上传之后显示名称
            String title = fileName.substring(0, fileName.lastIndexOf("."));
            //inputStream:上传文件的输入流
            InputStream inputStream = file.getInputStream();

            UploadStreamRequest request = new UploadStreamRequest(accessKeyId, accessKeySecret, title, fileName, inputStream);

            UploadVideoImpl uploader = new UploadVideoImpl();
            UploadStreamResponse response = uploader.uploadStream(request);
            System.out.print("RequestId=" + response.getRequestId() + "\n");  //请求视频点播服务的请求ID
            
            String videoId = null;
            
            if (response.isSuccess()) {
                videoId = response.getVideoId();
            } else { //如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因
                videoId = response.getVideoId();
            }
            return videoId;

        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

}

5、测试
在这里插入图片描述


10.2、前端实现

1、nginx.conf 添加配置并重启 nginx

http {	
	<--文件上传最大大小 --> 
	client_max_body_size 1024m;
	
	server {
		listen       9001;
		server_name  localhost;
	
		...
		
		location ~ /eduvod/ {
			proxy_pass http://localhost:8003;
		}
	}
}

2、页面

<el-form-item label="上传视频">
  <el-upload class="upload-demo" :on-success="handleVodUploadSuccess" :before-remove="beforeVodRemove" 
  			 :file-list="fileList" :action="BASE_API + '/eduvod/video/uploadAliyunVideo'" :limit="1" >
    <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、js

data() {
  return {
    video: {
      sort: 0,
      title: "",
      free: "",
      videoSourceId: ""
    },
    fileList: [], //上传文件列表
    BASE_API: process.env.BASE_API, // 接口API地址
  };
},
methods: {
  // 上传成功执行方法
  handleVodUploadSuccess(response, file, fileList) {
    this.video.videoSourceId = response.data.data
  },
  beforeVodRemove(file, fileList) {
    return this.$confirm(`确定移除 ${file.name}?`);
  },
}

10.3、视频删除

1、控制层

// 根据视频id删除阿里云视频
@DeleteMapping("/removeAliyunVideoById/{id}")
public R removeAliyunVideoById(@PathVariable String id) {
    vodService.removeAliyunVideoById(id);
    return R.ok();
}

2、业务层

// 根据id删除阿里云视频
@Override
public void removeAliyunVideoById(String id) {
    try {
        DefaultAcsClient client = initVodClient(accessKeyId, accessKeySecret);
        DeleteVideoRequest request = new DeleteVideoRequest();
        request.setVideoIds(id);
        DeleteVideoResponse response = client.getAcsResponse(request);
        System.out.println("RequestId = " + response.getRequestId() + "\n");
    } catch (ClientException e) {
        throw new LaptoyException(20001, "视频删除失败");
    }
}

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
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Laptoy

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值