一、课程相关表的关系
二、实现添加课程基本信息功能
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
1. /** 2. * 删除章节 3. * @param chapterId 4. */ 5. 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)的解决方案和造成原因分析(超详细)
查到数据。
九、课程最终确认
在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; } }