目录
一、课程相关表的关系
二、实现添加课程基本信息功能
1、使用代码生成器生成相关代码
还是只需要改这里
2、创建vo类封装表单提交的数据
@Data
public class CourseInfoVo {
@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则可免费观看")
// 0.01
private BigDecimal price;
@ApiModelProperty(value = "总课时")
private Integer lessonNum;
@ApiModelProperty(value = "课程封面图片路径")
private String cover;
@ApiModelProperty(value = "课程简介")
private String description;
}
3、controller
@RestController
@RequestMapping("/eduservice/course")
@CrossOrigin
public class EduCourseController {
@Autowired
private EduCourseService eduCourseService;
/**
* 添加课程的基本信息
* @param courseInfoVo 封装好的course信息
* @return
*/
@PostMapping("/addCourseInfo")
public R addCourseInfo(@RequestBody CourseInfoVo courseInfoVo){
eduCourseService.saveCourseInfo(courseInfoVo);
return R.ok();
}
}
4、service
public interface EduCourseService extends IService<EduCourse> {
/**
* 添加课程的基本信息
* @param courseInfoVo
*/
void saveCourseInfo(CourseInfoVo courseInfoVo);
}
5、serviceImpl
@Service
public class EduCourseServiceImpl extends ServiceImpl<EduCourseMapper, EduCourse> implements EduCourseService {
@Autowired
private EduCourseDescriptionService eduCourseDescriptionService;
/**
* 添加课程的基本信息
* @param courseInfoVo
*/
@Override
public void saveCourseInfo(CourseInfoVo courseInfoVo) {
//向课程表中添加课程基本信息
//因为传入的courseInfoVo 和 eduCourse 不一样,所以要转换一下
EduCourse eduCourse = new EduCourse();
BeanUtils.copyProperties(courseInfoVo, eduCourse);
int insert = baseMapper.insert(eduCourse);
if (insert <= 0){
throw new GuliException(20001, "添加课程信息失败");
}
//获取课程id,用来和课程描述进行关联
String cid = eduCourse.getId();
//向课程描述表中加课程描述信息
EduCourseDescription eduCourseDescription = new EduCourseDescription();
eduCourseDescription.setDescription(courseInfoVo.getDescription());
eduCourseDescription.setId(cid);
eduCourseDescriptionService.save(eduCourseDescription);
}
}
这里需要注意的点是:由于课程和课程描述是一对一关系,所以我们直接把课程描述的id值设置为课程的id,所以需要把描述的主键生成策略改为INPUT(其实不改也行)
6、启动测试
启动后,打开swagger-ui
再看数据库中,edu_course 和 edu_course_description中都插入了数据。
三、实现修改课程基本信息功能
实现修改功能,首先要实现修改的时候信息回显(查询)功能;然后实现修改功能。
1、controller
/**
* 根据课程id查询课程信息
* @param courseId
* @return
*/
@GetMapping("/getCourseInfo/{courseId}")
public R getCourseInfo(@PathVariable String courseId){
CourseInfoVo courseInfoVo = eduCourseService.getCourseInfo(courseId);
return R.ok().data("courseInfoVo", courseInfoVo);
}
/**
* 修改课程信息
* @param courseInfoVo
* @return
*/
@PostMapping("/updateCourseInfo")
public R updateCourseInfo(@RequestBody CourseInfoVo courseInfoVo){
eduCourseService.updateCourseInfo(courseInfoVo);
return R.ok();
}
2、service
/**
* 根据课程id查询课程信息
* @param courseId
* @return
*/
CourseInfoVo getCourseInfo(String courseId);
/**
* 修改课程信息
* @param courseInfoVo
*/
void updateCourseInfo(CourseInfoVo courseInfoVo);
3、serviceImpl
/**
* 根据课程id查询课程信息
* @param courseId
* @return
*/
@Override
public CourseInfoVo getCourseInfo(String courseId) {
//根据id查课程表
EduCourse course = baseMapper.selectById(courseId);
CourseInfoVo courseInfoVo = new CourseInfoVo();
BeanUtils.copyProperties(course, courseInfoVo);
//根据id查描述表
EduCourseDescription description = eduCourseDescriptionService.getById(courseId);
courseInfoVo.setDescription(description.getDescription());
return courseInfoVo;
}
/**
* 修改课程信息
* @param courseInfoVo
*/
@Override
public void updateCourseInfo(CourseInfoVo courseInfoVo) {
//根据id修改课程表
EduCourse eduCourse = new EduCourse();
BeanUtils.copyProperties(courseInfoVo, eduCourse);
int update = baseMapper.updateById(eduCourse);
if (update <= 0){
throw new GuliException(20001, "修改课程信息失败");
}
//根据id修改描述表
EduCourseDescription eduCourseDescription = new EduCourseDescription();
eduCourseDescription.setId(courseInfoVo.getId());
eduCourseDescription.setDescription(courseInfoVo.getDescription());
eduCourseDescriptionService.updateById(eduCourseDescription);
}
四、实现课程章节的大纲显示功能
逻辑上与课程分类的显示基本相同(都是树形显示)
1、controller
@RestController
@RequestMapping("/eduservice/chapter")
@CrossOrigin
public class EduChapterController {
@Autowired
private EduChapterService chapterService;
/**
* 课程大纲列表,根据课程id进行查询
* @param courseId
* @return
*/
@GetMapping("getChapterVideo/{courseId}")
public R getChapterVideo(@PathVariable String courseId){
List<ChapterVo> list = chapterService.getChapterVideoByCourseId(courseId);
return R.ok().data("allChapterVideo", list);
}
}
2、service
public interface EduChapterService extends IService<EduChapter> {
/**
* 课程大纲列表,根据课程id查询课程
* @param courseId
* @return
*/
List<ChapterVo> getChapterVideoByCourseId(String courseId);
}
3、serviceImpl
@Service
public class EduChapterServiceImpl extends ServiceImpl<EduChapterMapper, EduChapter> implements EduChapterService {
//注入小节的service,才能查询小节信息
@Autowired
private EduVideoService videoService;
/**
* 课程大纲列表,根据课程id查询课程
* @param courseId
* @return
*/
@Override
public List<ChapterVo> getChapterVideoByCourseId(String courseId) {
//1 根据课程id查询课程里面所有的章节
QueryWrapper<EduChapter> wrapperChapter = new QueryWrapper<>();
wrapperChapter.eq("course_id",courseId);
List<EduChapter> eduChapterList = baseMapper.selectList(wrapperChapter);
//2 根据课程id查询课程里面所有的小节
QueryWrapper<EduVideo> wrapperVideo = new QueryWrapper<>();
wrapperVideo.eq("course_id",courseId);
List<EduVideo> eduVideoList = videoService.list(wrapperVideo);
//创建list集合,用于最终封装数据
List<ChapterVo> finalList = new ArrayList<>();
//3 遍历查询章节list集合进行封装
//遍历查询章节list集合
for (int i = 0; i < eduChapterList.size(); i++) {
//每个章节
EduChapter eduChapter = eduChapterList.get(i);
//eduChapter对象值复制到ChapterVo里面
ChapterVo chapterVo = new ChapterVo();
BeanUtils.copyProperties(eduChapter,chapterVo);
//把chapterVo放到最终list集合
finalList.add(chapterVo);
//创建集合,用于封装章节的小节
List<VideoVo> videoList = new ArrayList<>();
//4 遍历查询小节list集合,进行封装
for (int m = 0; m < eduVideoList.size(); m++) {
//得到每个小节
EduVideo eduVideo = eduVideoList.get(m);
//判断:小节里面chapterid和章节里面id是否一样
if(eduVideo.getChapterId().equals(eduChapter.getId())) {
//进行封装
VideoVo videoVo = new VideoVo();
BeanUtils.copyProperties(eduVideo,videoVo);
//放到小节封装集合
videoList.add(videoVo);
}
}
//把封装之后小节list集合,放到章节对象里面
chapterVo.setChildren(videoList);
}
return finalList;
}
}
五、实现课程章节的添加、修改功能
1、controller
/**
* 添加章节
* @param eduChapter
* @return
*/
@PostMapping("/addChapter")
public R addChapter(@RequestBody EduChapter eduChapter){
chapterService.save(eduChapter);
return R.ok();
}
/**
* 根据id查询章节 用于修改时的回显
* @param chapterId
* @return
*/
@GetMapping("getChapterInfo/{chapterId}")
public R getChapterInfo(@PathVariable String chapterId){
EduChapter chapter = chapterService.getById(chapterId);
return R.ok().data("chapter", chapter);
}
/**
* 修改章节信息
* @param eduChapter
* @return
*/
@PostMapping("updateChapter")
public R updateChapter(@RequestBody EduChapter eduChapter){
chapterService.updateById(eduChapter);
return R.ok();
}
因为没有什么特殊的业务要求,所以直接用MP里提供的方法就可以。
六、 实现课程章节的删除功能
删除章节的时候需要注意:章节下还有小节,如果直接删除章节,小节还是会存在,并且成为无用的数据,所以不能直接用MP提供的删除方法。
我们有两种解决思路:
1、在删除章节的同时将下面的所有小节删掉;
2、如果章节下面有小节,就不能删除(必须等小节删完了才能删章节)
这次我们采用第二种方法处理,在下面删除课程(第十步)中也会采用第一种方法。实际工作中具体用哪一种,取决于业务需求。
1、controller
/**
* 删除章节
* @param chapterId
* @return
*/
@DeleteMapping("/deleteChapter/{chapterId}")
public R deleteChapter(@PathVariable String chapterId){
//这里要注意的问题是,章节下有小节,如果直接删除章节,小节还是会存在,所以不能直接用MP提供的删除方法
boolean delete = chapterService.deleteChapter(chapterId);
if (delete){
return R.ok();
} else{
return R.error();
}
}
2、service
/**
* 删除章节
* @param chapterId
*/
boolean deleteChapter(String chapterId);
3、serviceimpl
/**
* 删除章节
* @param chapterId
*/
@Override
public boolean deleteChapter(String chapterId) {
//如果章节下面有小节,就不能删除(必须等小节删完了才能删章节)
QueryWrapper<EduVideo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("chapter_id", chapterId);
int count = videoService.count(queryWrapper);
if (count > 0){ //章节下有小节
throw new GuliException(20001, "不能删除");
} else {
int result = baseMapper.deleteById(chapterId);
//result=1说明成功删除,result=0说明失败
return result>0;
}
}
七、实现小节的增加、删除和修改功能
每个课程对应的章节下有若干个小节。
代码比较简单,就直接贴controller了。
@RestController
@RequestMapping("/eduservice/video")
@CrossOrigin
public class EduVideoController {
@Autowired
private EduVideoService eduVideoService;
/**
* 增加小节
* @param eduVideo
* @return
*/
@PostMapping("/addVideo")
public R addVideo(@RequestBody EduVideo eduVideo){
eduVideoService.save(eduVideo);
return R.ok();
}
/**
* 删除小节(后续需要将小节中的视频也删掉)
* @param id
* @return
*/
@DeleteMapping("{id}")
public R deleteVideo(@PathVariable String id){
eduVideoService.removeById(id);
return R.ok();
}
/**
* 根据id查找小节 用于修改小节时的回显
* @param id
* @return
*/
@GetMapping("{id}")
public R getVideoById(@PathVariable String id){
EduVideo video = eduVideoService.getById(id);
return R.ok().data("video", video);
}
/**
* 修改小节
* @param eduVideo
* @return
*/
@PostMapping("/updateVideo")
public R updateVideo(@RequestBody EduVideo eduVideo){
eduVideoService.updateById(eduVideo);
return R.ok();
}
}
八、实现课程信息确认功能
在增加课程信息时,在发布课程之前,需要确认课程信息是否正确 。
因为课程的所有信息需要查询多张表才能得到,所以需要我们自己编写SQL语句来实现。
1、controller
/**
* 根据课程id查询课程确认信息
* @param id
* @return
*/
@GetMapping("/getPublishCourseInfo/{id}")
public R getPublishCourseInfo(@PathVariable String id){
CoursePublishVo coursePublishVo = eduCourseService.publishCourseInfo(id);
return R.ok().data("publishCourse", coursePublishVo);
}
2、service
/**
* 根据课程id查询课程确认信息
* @param id
* @return
*/
CoursePublishVo publishCourseInfo(String id);
3、mapper
public interface EduCourseMapper extends BaseMapper<EduCourse> {
public CoursePublishVo getPublishCourseInfo(String courseId);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.shang.eduservice.mapper.EduCourseMapper">
<!-- 根据课程id查询课程确认信息 -->
<select id="getPublishCourseInfo" resultType="com.shang.eduservice.entity.vo.CoursePublishVo">
SELECT ec.id, ec.title, ec.price, ec.lesson_num as lessonNum,ec.cover,
et.name as teacherName,
es1.title AS subjectLevelOne,
es2.title AS subjectLevelTwo
FROM edu_course ec LEFT OUTER JOIN edu_course_description ecd ON ec.`id`=ecd.`id`
LEFT OUTER JOIN edu_teacher et ON ec.`teacher_id`=et.id
LEFT OUTER JOIN edu_subject es1 ON ec.`subject_parent_id`=es1.`id`
LEFT OUTER JOIN edu_subject es2 ON ec.`subject_id`=es2.`id`
WHERE ec.`id`=#{couseId};
</select>
</mapper>
4、servceImpl
@Override
public CoursePublishVo publishCourseInfo(String id) {
//调用自己写的mapper
return baseMapper.getPublishCourseInfo(id);
}
5、swagger测试
发现报错:Invalidbound statement (not found)
具体原因和解决方法我已经写在另一篇博客里了,传送门:
org.apache.ibatis.binding.BindingException: Invalidbound statement (not found)的解决方案和造成原因分析(超详细)
6、修改后重新测试
查到数据。
九、课程最终确认
在course表中,有一个字段用来标记是否发布。
只要修改一下这个字段的属性值就可以完成发布了。
/**
* 课程最终发布
* @param id
* @return
*/
@PostMapping("/publishCourse/{id}")
public R publishCourse(@PathVariable String id){
EduCourse eduCourse = new EduCourse();
eduCourse.setId(id);
//这里如果把状态定义为枚举类型 会更好一些
eduCourse.setStatus("Normal");
eduCourseService.updateById(eduCourse);
return R.ok();
}
十、实现课程大纲列表显示
逻辑上和之前的讲师列表一样,就不多赘述了。
十一、删除课程
首先我们要知道,在处理表与表一对多的关系时,通常是将多的那一张表创建字段作为外键,指向为一的那张表的主键。其实,在实际工作中,一般是不将外键设置出来的,程序员只要心里清楚这是外键就可以了,因为设置外键之后必须保持数据一致性,会带来一些问题(比如想要删除课程,就必须先删除他下面的所有章节;想要删除章节,就必须先删除他下面的所有小节)
这次我们删除采用的方法是,删除课程时,先把他下面的所有东西删掉。
1、controller
/**
* 删除课程
* @param courseId
* @return
*/
@DeleteMapping("{courseId}")
public R deleteCourse(@PathVariable String courseId){
eduCourseService.removeCourse(courseId);
return R.ok();
}
2、service
/**
* 删除课程
* @param courseId
*/
void removeCourse(String courseId);
3、serviceImpl
@Override
public void removeCourse(String courseId) {
//根据课程id删除小节
eduVideoService.removeVideoByCourseId(courseId);
//根据课程id删除章节
eduChapterService.removeChapterByCourseId(courseId);
//根据课程id删除描述(课程和描述是一对一关系)
eduCourseDescriptionService.removeById(courseId);
//根据课程id删除课程
int result = baseMapper.deleteById(courseId);
if (result <= 0){
throw new GuliException(20001, "删除课程失败");
}
}
4、小节中
public interface EduVideoService extends IService<EduVideo> {
/**
* 根据课程id删除小节
* @param courseId
*/
void removeVideoByCourseId(String courseId);
}
@Service
public class EduVideoServiceImpl extends ServiceImpl<EduVideoMapper, EduVideo> implements EduVideoService {
/**
* 根据课程id删除小节
* @param courseId
*/
@Override
public void removeVideoByCourseId(String courseId) {
// TODO 删除小节的时候,还要删除对应的视频
QueryWrapper<EduVideo> queryWrapper = new QueryWrapper();
queryWrapper.eq("course_id", courseId);
baseMapper.delete(queryWrapper);
}
}
5、章节中
/**
* 根据课程id删除章节
* @param courseId
*/
void removeChapterByCourseId(String courseId);
/**
* 根据课程id删除其下面的所有章节
* @param courseId
*/
@Override
public void removeChapterByCourseId(String courseId) {
QueryWrapper<EduChapter> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("course_id", courseId);
baseMapper.delete(queryWrapper);
}
十二、实现添加小节时上传视频的功能
上传视频使用阿里云视频点播功能,具体操作参考我的博客:
谷粒学苑项目实战(十四):实现阿里云视频点播功能(java编码实现)
1、引入依赖
2、创建application配置文件
# 服务端口
server.port=8003
# 服务名
spring.application.name=service-vod
# 环境设置:dev、test、prod
spring.profiles.active=dev
#阿里云 vod
#不同的服务器,地址不同
aliyun.vod.file.keyid=xxxxxx
aliyun.vod.file.keysecret=xxxxx
# 最大上传单个文件大小:默认1M
spring.servlet.multipart.max-file-size=1024MB
# 最大置总上传的数据大小 :默认10M
spring.servlet.multipart.max-request-size=1024MB
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#开启熔断机制
feign.hystrix.enabled=true
# 设置hystrix超时时间,默认1000ms
#hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=6000
3、启动类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@ComponentScan(basePackages = {"com.shang"})
public class VodApplication {
public static void main(String[] args) {
SpringApplication.run(VodApplication.class);
}
}
4、controller
/**
* 上传视频到阿里云
* @param file
* @return
*/
@PostMapping("uploadAlyivideo")
public R uploadAlyivideo(MultipartFile file){
//获取要上传的视频id
String videoId = vodService.uploadVideoAly(file);
return R.ok();
}
5、service
/**
* 上传视频到阿里云
* @param file
* @return
*/
String uploadVideoAly(MultipartFile file);
6、serviceImpl
/**
* 上传文件到阿里云
* @param file
* @return
*/
@Override
public String uploadVideoAly(MultipartFile file) {
try {
String accessKeyId = ConstantVodUtils.ACCESS_KEY_ID;
String accessKeySecret = ConstantVodUtils.ACCESS_KEY_SECRET;
String title = "哈哈哈"; //上传后的名称
String fileName = file.getOriginalFilename(); //文件原始名称
InputStream inputStream = file.getInputStream();
UploadStreamRequest request = new UploadStreamRequest(accessKeyId, accessKeySecret, title, fileName, inputStream);
/* 是否使用默认水印(可选),指定模板组ID时,根据模板组配置确定是否使用默认水印*/
//request.setShowWaterMark(true);
/* 自定义消息回调设置,参数说明参考文档 https://help.aliyun.com/document_detail/86952.html#UserData */
//request.setUserData(""{\"Extend\":{\"test\":\"www\",\"localId\":\"xxxx\"},\"MessageCallback\":{\"CallbackURL\":\"http://test.test.com\"}}"");
/* 视频分类ID(可选) */
//request.setCateId(0);
/* 视频标签,多个用逗号分隔(可选) */
//request.setTags("标签1,标签2");
/* 视频描述(可选) */
//request.setDescription("视频描述");
/* 封面图片(可选) */
//request.setCoverURL("http://cover.sample.com/sample.jpg");
/* 模板组ID(可选) */
//request.setTemplateGroupId("8c4792cbc8694e7084fd5330e56a33d");
/* 工作流ID(可选) */
//request.setWorkflowId("d4430d07361f0*be1339577859b0177b");
/* 存储区域(可选) */
//request.setStorageLocation("in-201703232118266-5sejdln9o.oss-cn-shanghai.aliyuncs.com");
/* 开启默认上传进度回调 */
// request.setPrintProgress(true);
/* 设置自定义上传进度回调 (必须继承 VoDProgressListener) */
// request.setProgressListener(new PutObjectProgressListener());
/* 设置应用ID*/
//request.setAppId("app-1000000");
/* 点播服务接入点 */
//request.setApiRegionId("cn-shanghai");
/* ECS部署区域*/
// request.setEcsRegionId("cn-shanghai");
UploadVideoImpl uploader = new UploadVideoImpl();
UploadStreamResponse response = uploader.uploadStream(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");
}
return response.getVideoId();
} catch (Exception e){
e.printStackTrace();
return null;
}
}