尚硅谷谷粒学院项目第九天内容之课程发布功能、课程列表功能、视频点播、视频上传
课程发布功能
后端
创建一个实体类,用于封装课程信息从而返回给前端
CoursePublishVo
@Data
public class CoursePublishVo {
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;//价格 ,只用于显示
}
controller
//发布课程:其实就是设置一下课程状态
@PostMapping("publishCourse/{courseId}")
public R publishCourse(@PathVariable String courseId){
EduCourse course = new EduCourse();
course.setId(courseId);
course.setStatus("Normal");
courseService.updateById(course);
return R.ok();
}
service
CoursePublishVo getCoursePublish(String courseId);
serviceImpl
//获取CoursePublish(最终发布的课程信息)
@Override
public CoursePublishVo getCoursePublish(String courseId) {
return baseMapper.getCoursePublish(courseId);
}
前端
course.js
//发布课程
publishCourse(courseId){
return request({
url:`/eduservice/course/publishCourse/${courseId}`,
method:'post'
})
},
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>
<div class="ccInfo">
<img :src="coursePublish.cover">
<div class="main">
<h2>{{ coursePublish.title }}</h2>
<p class="gray"><span>共{{ coursePublish.lessonNum }}课时</span></p>
<p><span>所属分类:{{ coursePublish.subjectLevelOne }} — {{ coursePublish.subjectLevelTwo }}</span></p>
<p>课程讲师:{{ coursePublish.teacherName }}</p>
<h3 class="red">¥{{ coursePublish.price }}</h3>
</div>
</div>
<div>
<el-button @click="previous">返回修改</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="publish">发布课程</el-button>
</div>
</div>
</template>
<script>
import course from '@/api/edu/course'
export default {
data() {
return {
saveBtnDisabled: false, // 保存按钮是否禁用
courseId:'',
coursePublish:{}
}
},
created() {
//获取路由课程id值
if(this.$route.params && this.$route.params.id) {
this.courseId = this.$route.params.id
//调用接口方法根据课程id查询
this.getCoursePublish()
}
},
methods: {
//根据课程id查询
getCoursePublish() {
course.getCoursePublihInfo(this.courseId)
.then(response => {
this.coursePublish = response.data.coursePublish
})
},
previous() {
console.log('previous')
this.$router.push({ path: '/course/chapter/'+this.courseId })
},
publish() {
course.publishCourse(this.courseId)
.then(response => {
//提示
this.$message({
type: 'success',
message: '课程发布成功!'
});
//跳转课程列表页面
this.$router.push({ path: '/course/list' })
})
}
}
}
</script>
<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>
课程列表功能
后端
controller
//分页多条件查询课程列表
@PostMapping("pageConditionQueryCourse/{current}/{size}")
public R pageConditionQueryCourse(@PathVariable long current, @PathVariable long size, @RequestBody(required=false) CourseCondition courseCondition){
Page<EduCourse> page = new Page<>(current,size);
QueryWrapper<EduCourse> queryWrapper = new QueryWrapper<>();
String title = courseCondition.getTitle();
Integer status = courseCondition.getStatus();
String subjectParentId = courseCondition.getSubjectParentId();
String teacherId = courseCondition.getTeacherId();
String minPrice = courseCondition.getMinPrice();
String maxPrice = courseCondition.getMaxPrice();
if(!StringUtils.isEmpty(title)){
queryWrapper.eq("title",title);
}
if(!StringUtils.isEmpty(status)){
queryWrapper.eq("status",status==0 ? "Draft":"Normal");//0是未发布,1是已发布
}
if(!StringUtils.isEmpty(subjectParentId)){
queryWrapper.eq("subject_parent_id",subjectParentId);
}
if(!StringUtils.isEmpty(teacherId)){
queryWrapper.eq("teacher_id",teacherId);
}
if(!StringUtils.isEmpty(minPrice)){
queryWrapper.gt("price",minPrice);
}
if(!StringUtils.isEmpty(maxPrice)){
queryWrapper.eq("price",maxPrice);
}
queryWrapper.orderByDesc("gmt_create");
courseService.page(page,queryWrapper);
long total = page.getTotal();//总记录数
List<EduCourse> list = page.getRecords();//所有数据
return R.ok().data("list",list).data("total",total);
}
//删除课程
@DeleteMapping("deleteCourse/{courseId}")
public R deleteCourse(@PathVariable String courseId){
courseService.deleteCourse(courseId);
return R.ok();
}
serviceImpl
//分页多条件查询课程列表
@PostMapping("pageConditionQueryCourse/{current}/{size}")
public R pageConditionQueryCourse(@PathVariable long current, @PathVariable long size, @RequestBody(required=false) CourseCondition courseCondition){
Page<EduCourse> page = new Page<>(current,size);
QueryWrapper<EduCourse> queryWrapper = new QueryWrapper<>();
String title = courseCondition.getTitle();
Integer status = courseCondition.getStatus();
String subjectParentId = courseCondition.getSubjectParentId();
String teacherId = courseCondition.getTeacherId();
String minPrice = courseCondition.getMinPrice();
String maxPrice = courseCondition.getMaxPrice();
if(!StringUtils.isEmpty(title)){
queryWrapper.eq("title",title);
}
if(!StringUtils.isEmpty(status)){
queryWrapper.eq("status",status==0 ? "Draft":"Normal");//0是未发布,1是已发布
}
if(!StringUtils.isEmpty(minPrice)){
queryWrapper.gt("price",minPrice);
}
if(!StringUtils.isEmpty(maxPrice)){
queryWrapper.eq("price",maxPrice);
}
queryWrapper.orderByDesc("gmt_create");
courseService.page(page,queryWrapper);
long total = page.getTotal();//总记录数
List<EduCourse> list = page.getRecords();//所有数据
return R.ok().data("list",list).data("total",total);
}
//删除课程
@DeleteMapping("deleteCourse/{courseId}")
public R deleteCourse(@PathVariable String courseId){
courseService.deleteCourse(courseId);
return R.ok();
}
前端
course.js
//分页多条件查询课程
getCourseListPage(current,limit,courseQuery){
return request({
url:`/eduservice/course/pageConditionQueryCourse/${current}/${limit}`,
method:'post',
data:courseQuery //后端requestBody要json字符串,data能把techerQuery对象转换成json字符串
})
},
//删除课程
deleteCourseById(courseId){
return request({
url:`/eduservice/course/deleteCourse/${courseId}`,
method:'post',
data:courseQuery //后端requestBody要json字符串,data能把techerQuery对象转换成json字符串
})
}
list.vue
<template>
<div class="login-container">
<!--查询表单-->
<el-form :inline="true" class="demo-form-inline">
<el-form-item>
<el-input v-model="courseQuery.title" placeholder="课程名"/>
</el-form-item>
<el-form-item>
<el-select v-model="courseQuery.status" clearable placeholder="课程状态">
<el-option :value=1 label="已发布"/>
<el-option :value=0 label="未发布"/>
</el-select>
</el-form-item>
<el-form-item>
<el-input v-model="courseQuery.minPrice" placeholder="最低价格"/>
</el-form-item>
<el-form-item>
<el-input v-model="courseQuery.maxPrice" placeholder="最高价格"/>
</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>
<!--表格-->
<el-table
:data="list"
style="width: 100%"
border
fit
highlight-current-row
element-loading-text="数据加载中"
v-loading="listLoading">
<el-table-column prop="date" label="序号" width="100" align="center">
<template slot-scope="scope">
{{ (currentPage - 1) * limit + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="title" label="名称" width="230"> </el-table-column>
<el-table-column label="状态" width="90">
<template slot-scope="scope">
{{ scope.row.status === 'Normal' ? "已发布" : "未发布" }}
</template>
</el-table-column>
<el-table-column prop="lessonNum" label="课程数" width="120" />
<el-table-column prop="price" label="价格" />
<el-table-column prop="buyCount" label="购买量" width="120" />
<el-table-column prop="viewCount" label="浏览量" width="120" />
<el-table-column label="操作" width="530" align="center">
<template slot-scope="scope">
<el-button type="primary" size="mini" icon="el-icon-edit" @click="editCourseInfo(scope.row.id)">编辑课程基本信息</el-button>
<el-button type="primary" size="mini" icon="el-icon-edit" @click="editChapterVideo(scope.row.id)">编辑课程大纲</el-button>
<el-button
type="danger"
size="mini"
icon="el-icon-delete"
@click="deleteTeacherById(scope.row.id)">
删除课程
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
:current-page="currentPage"
:page-size="limit"
:total="total"
style="padding: 30px 0; text-align: center;"
layout="total, prev, pager, next, jumper"
@current-change="getList"
/>
</div>
</template>
<script>
import course from '@/api/edu/course.js'
export default{
data() {
return {
list:null,
currentPage:1,
limit:11,
total:0,
courseQuery:{},
listLoading:''
}
},
created(){
this.getList()
},
methods:{
//编辑课程基本信息
editCourseInfo(courseId){
//路由跳转
this.$router.push({path:'/course/info/'+courseId})
},
//编辑课程大纲(编辑章节小节)
editChapterVideo(courseId){
this.$router.push({path:'/course/chapter/'+courseId})
},
//课程列表
getList(currentPage=1){
this.currentPage=currentPage
course.getCourseListPage(this.currentPage,this.limit,this.courseQuery)
.then(response=>{ //getList()方法调用成功时执行
this.list=response.data.list
})
.catch(error=>{//getList()方法调用失败时执行
console.log(error);
})
},
//清空查询条件
resetData(){
this.courseQuery={}
this.getList()
},
//删除讲师
deleteCourseById(id){
this.$confirm('此操作将永久删除讲师记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => { //点击confirmButton后执行
course.deleteCourseById(id)
.then(response=>{ //提示信息
this.$message({
type:'success',
message:'删除成功!'
})
})
this.getList(this.currentPage)
})
}
}
}
</script>
视频点播
阿里云官网——产品——企业服务与媒体服务——视频点播——管理控制台
我们上传的视频依然会存入oss,视频点播帮我们管理这些视频
api:阿里云提供的很底层的接口
sdk:sdk是api的封装,我们用sdk调用api
httpclient技术可以不使用浏览器发送请求,并得到响应
测试
创建子模块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>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
</dependencies>
InitObject.java
public class InitObject {
//填入AccessKey信息
public static DefaultAcsClient initVodClient(String accessKeyId,String accessKeySecret) throws ClientException {
String regionId = "cn-shanghai"; // 点播服务接入地域
DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
return new DefaultAcsClient(profile);
}
}
VodTest.java
public class VodTest {
public static void main(String[] args) throws ClientException {
uploadVideo("E:\\photo\\temp\\11.mp4");
}
//上传视频
public static void uploadVideo(String fileName) throws ClientException {
String accessKeyId="LTAI5t6R5cWusp7MeWpKgT1W";
String accessKeySecret="Dd6Ku0j5dQja9jh4d7cl8ULepWcNE7";
String title="111";
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);
System.out.print("RequestId=" + response.getRequestId() + "\n"); //请求视频点播服务的请求ID
if (response.isSuccess()) {
System.out.print("VideoId=" + response.getVideoId() + "\n");
} else {
/* 如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因 */
System.out.print("VideoId=" + response.getVideoId() + "\n");
System.out.print("ErrorCode=" + response.getCode() + "\n");
System.out.print("ErrorMessage=" + response.getMessage() + "\n");
}
}
//根据视频id获取视频url
public static void getVideoUrl() throws ClientException {
DefaultAcsClient client = InitObject.initVodClient("LTAI5t6R5cWusp7MeWpKgT1W","Dd6Ku0j5dQja9jh4d7cl8ULepWcNE7");
GetPlayInfoRequest request = new GetPlayInfoRequest();
request.setVideoId("4b0d8f1102f34e84a9c7b4e239deff2c");
GetPlayInfoResponse response = client.getAcsResponse(request);
List<GetPlayInfoResponse.PlayInfo> list = response.getPlayInfoList();
for(GetPlayInfoResponse.PlayInfo playInfo:list){
System.out.println("playurl:"+playInfo.getPlayURL());
}
System.out.println("VideoBase.Title:"+response.getVideoBase().getTitle());
}
//根据视频id获取视频播放凭证
public static void getVideoAuth() throws ClientException {
DefaultAcsClient client = InitObject.initVodClient("LTAI5t6R5cWusp7MeWpKgT1W", "Dd6Ku0j5dQja9jh4d7cl8ULepWcNE7");
GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();
request.setVideoId("4b0d8f1102f34e84a9c7b4e239deff2c");
GetVideoPlayAuthResponse response = client.getAcsResponse(request);
System.out.println(response.getPlayAuth());
}
}
上传视频功能
后端
在service_vod模块
创建application.properties
# 服务端口
server.port=8003
# 服务名
spring.application.name=service-vod
# 环境设置:dev、test、prod
spring.profiles.active=dev
#阿里云 vod
#不同的服务器,地址不同
aliyun.vod.file.keyid=LTAI5t6R5cWusp7MeWpKgT1W
aliyun.vod.file.keysecret=Dd6Ku0j5dQja9jh4d7cl8ULepWcNE7
# 最大上传单个文件大小:默认1M
spring.servlet.multipart.max-file-size=1024MB
# 最大置总上传的数据大小 :默认10M
spring.servlet.multipart.max-request-size=1024MB
创建启动类com.jiabei.vod.VodApplication
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@ComponentScan(basePackages = {"com.jiabei"})
public class VodApplication {
public static void main(String[] args) {
SpringApplication.run(VodApplication.class, args);
}
}
VodController
@RestController
@RequestMapping("vodservice")
@CrossOrigin
public class VodController {
@Autowired
private VodService vodService;
//上传视频
@PostMapping("uploadVideo")
public R uploadVideo(MultipartFile file){
String videoId=vodService.uploadVideo(file);
return R.ok().data("videoId",videoId);
}
}
VodService
@Service
public interface VodService {
String uploadVideo(MultipartFile file);
}
VodServiceImpl
@Component
public class VodServiceImpl implements VodService {
@Override
public String uploadVideo(MultipartFile file) {
try {
String fileName=file.getOriginalFilename(); //原名
InputStream is = file.getInputStream();
String title =null;
if(fileName!=null){
title=fileName.substring(0, fileName.lastIndexOf("."));//上传后的名称
}
UploadStreamRequest request = new UploadStreamRequest(ConstantUtils.ACCESS_KEY_ID,ConstantUtils.ACCESS_KEY_SECRET, title, fileName, is);
UploadVideoImpl uploader = new UploadVideoImpl();
UploadStreamResponse response = uploader.uploadStream(request);
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;
}
}
}
ConstanUtils
@Component
public class ConstantUtils 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;
}
}
swagger测试,能将视频上传到oss中
前端
chapter.vue中添加上传视频的组件
<el-form-item label="上传视频">
<el-upload
:on-success="handleVodUploadSuccess"
:on-remove="handleVodRemove"
:before-remove="beforeVodRemove"
:on-exceed="handleUploadExceed"
:file-list="fileList"
:action="BASE_API+'/eduvod/video/uploadAlyiVideo'"
: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>
声明变量
fileList: [],//上传文件列表
BASE_API: process.env.BASE_API+'vodservice/uploadVideo' // 接口API地址
声明方法
//上传视频成功调用的方法
handleVodUploadSuccess(response, file, fileList) {
this.video.videoSourceId = response.data.videoId
this.video.videoOriginalName = file.name
},
handleUploadExceed() {
this.$message.warning('想要重新上传视频,请先删除已上传的视频')
},
在nginx中配置端口转发规则
nginx.conf
此时测试,发现上传的视频一闪而过,传不上去,原因:视频超过了nginx允许的大小
在nginx.conf里添加
client_max_body_size 1024m;
重启nginx
此时,测试,videoSourceId就能添加到数据库里了,视频能成功上传到阿里云了
后端
VodController
//根据视频id删除视频
@DeleteMapping("deleteVideo/{id}")
public R deleteVideo(@PathVariable String id){
vodService.deleteVideo(id);
return R.ok();
}
VodServiceImpl
//删除视频
@Override
public void deleteVideo(String id) {
try{
DefaultAcsClient client = InitClient.initVodClient(ConstantUtils.ACCESS_KEY_ID, ConstantUtils.ACCESS_KEY_SECRET);
DeleteVideoRequest request = new DeleteVideoRequest();
request.setVideoIds(id);
client.getAcsResponse(request);
}catch (Exception e){
throw new GuliException(20001,"删除视频失败");
}
}
前端
video.js
//删除视频
deleteAlyVideo(videoId){
return request({
url:`vodservice/deleteVideo/${videoId}`,
method:'delete'
})
}
chapter.vue
//点击×调用这个方法:弹出确认框
beforeVodRemove(file,fileList) {
return this.$confirm(`确定移除 ${ file.name }?`);
},
//点击确认调用的方法:删除阿里云中的视频
handleVodRemove(){
//调用接口的删除视频的方法
video.deleteAlyVideo(this.video.videoSourceId)
.then(response => {
//提示信息
this.$message({
type: 'success',
message: '删除视频成功!'
});
//把文件列表清空
this.fileList = []
//把video视频id和视频名称值清空
//上传视频id赋值
this.video.videoSourceId = ''
//上传视频名称赋值
this.video.videoOriginalName = ''
})
},
测试