{{ coursePublish.title }}
共{{ coursePublish.lessonNum }}课时
所属分类:{{ coursePublish.subjectLevelOne }} — {{ coursePublish.subjectLevelTwo }}
课程讲师:{{ coursePublish.teacherName }}
项目源码与所需资料
链接:https://pan.baidu.com/s/1azwRyyFwXz5elhQL0BhkCA?pwd=8z59
提取码:8z59
1.课程信息确认页面会显示课程名称、课程价格、课程简介、课程所属分类、课程所属讲师等等…我们要从数据表中查询这些信息,其中从edu_course表查询课程名称、课程价格;从edu_course_description表查询课程简介;从edu_subject表查询课程所属分类;从edu_teacher表查询课程所属讲师。这些数据并没有在一张数据表中,我们应该怎么解决呢?在"demo09-课程管理"的"3.1.3业务层实现类"中我们的做法是:分别查询这两张表,然后将查询到的数据封装到一个VO实体类中。此时我们也可以通过分别查询这几张数据表并封装数据来实现需求,但是在"demo09-课程管理"的"3.1.3业务层实现类"中只涉及到两张数据表,而我们此时涉及到的数据表太多了,所以不建议这样做
2.建议通过手写sql语句来实现
假如左边的数据表是课程一级分类表,右边的数据表是课程二级分类表,且右边的表的第三列存放的是课程一级分类的id
1.实际开发中我们经常用内连接和左外连接,右外连接用的不多。我们此时用内连接可以吗?当然可以,但是我们某一门课可能会没有简介,所以用内连接不太合适,所以我们这里使用左外连接查询
2.在数据库编写如下sql命令
SELECT ec.id,ec.title,ec.price,ec.lesson_num,
ecd.description,
et.name,
es1.title AS oneSubject,
es2.title AS twoSubject
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='1562267576652808193'
在entity–>vo包下创建vo类CoursePublishVo来封装数据
@Data
public class CoursePublishVo {
private String id;
private String title;
private String cover;
private Integer lessonNum;
private String subjectLevelOne;
private String subjectLevelTwo;
private String teacherName;
private String price;//只用于显示
}
我们在前面的代码编写中有时只需处理控制层,有时只需处理控制层和业务层,还从来没有处理过持久层。但我们此时手写sql语句,所以需要处理持久层
在EduCourseMapper中定义"根据课程id查询课程具体信息"的抽象方法
public interface EduCourseMapper extends BaseMapper<EduCourse> {
//根据课程id查询课程具体信息
public CoursePublishVo getPublishCourseInfo(String courseId);
}
1.点击"DataBase"
2.点击加号(“+”),然后点击Data Source下的MySQL
3.输入我们的mysql密码和数据库名字,然后点击"Ok"
4.填写mysql的账号密码,然后点击"Ok"
5.此时我们就可以看到数据库guli中的数据表了,说明此时连接成功
在EduCourseMapper.xml中编写刚刚定义的抽象方法的映射
<!--sql语句:根据课程id查询课程具体信息-->
<select id="getPublishCourseInfo" resultType="com.atguigu.eduservice.entity.vo.CoursePublishVo">
SELECT ec.id,ec.title,ec.cover,ec.lesson_num AS lessonNum,ec.price,
es1.title AS subjectLevelOne,
es2.title AS subjectLevelTwo,
et.name AS teacherName
FROM edu_course ec 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
LEFT OUTER JOIN edu_teacher et ON ec.teacher_id=et.id
WHERE ec.id=#{courseId}
</select>
为什么截图中的第6行、第7行、第8行、第9行要分别用AS lessonNum
、AS subjectLevelOne
、subjectLevelTwo
、teacherName
来起别名?
我们在"1.4.1在mapper中定义方法"定义的抽象方法只有一个参数,所以截图中第13行的WHERE ec.id=#{courseId}
中的courseId可以随便写(不过别写成中文啊!!!)
在控制器EduCourseController中定义方法
//根据课程id查询课程具体信息
@GetMapping("getPublishCourseInfo/{id}")
public R getPublishCourseInfo(@PathVariable String id) {
CoursePublishVo coursePublishVo= courseService.publishCourseInfo(id);
return R.ok().data("publishCourse", coursePublishVo);
}
在业务层接口EduCourseService定义抽象方法
//根据课程id查询课程具体信息
CoursePublishVo publishCourseInfo(String id);
在业务层实现类EduCourseServiceImpl中实现上一步定义的抽象方法
//根据课程id查询课程具体信息
@Override
public CoursePublishVo publishCourseInfo(String id) {
//调用mapper中的方法
CoursePublishVo publishCourseInfo = baseMapper.getPublishCourseInfo(id);
return publishCourseInfo;
}
我们在"demo07-课程分类管理"的"7.4.2业务层实现类"的第1步说过,在业务层调用mapper中的方法我们有两种方式:使用baseMapper.xxx或使用this.xxx。但是因为我们此时是调用mapper中我们自己定义的方法,所以我们只能用BaseMapper.xxx
1.重启EduApplication服务,使用swagger进行测试
2.在输入框输入课程id后点击"Try it out!"
3.执行了异常,说明我们接口是有问题的,解决方法在后面的"1.9加载问题"
1.接口报错如下
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.atguigu.eduservice.mapper.EduCourseMapper.getPublishCourseInfo
2.出现这种报错我们首先查看EduCourseMapper.xml的
3.其实这个错误是maven默认加载机制造成的
①我们先看下面这张图,此时是有xml文件的
②然后再看下图,发现编译后没有xml文件了,也就是说编译时并没有编译xml文件
③这是maven默认加载机制造成的:加载时只会加载java文件夹下的.java类型文件,而不会加载java文件夹下的xml类型的文件
解决方法有很多种:
1.方法一:
将这些xml文件复制粘贴到编译后的mapper文件夹下(我没有用这种方法,所以我把具体操作截图后又将编译后的mapper文件夹下的这些xml文件删掉了)
这种方法不建议用,因为每次修改xml文件后都需要手动再将修改后的xml文件复制到编译后的mapper文件夹下,很麻烦
2.方法二:
将这些xml文件剪切粘贴到resources目录下(我没有用这种方法,所以我把具体操作截图后又将resources目录下的这些xml文件剪切粘贴到了mapper文件夹下,即归位)
这种方法不建议用,因为我们的代码都是用代码生成器生成的,人家给的结构就是xml在mapper文件夹下。这种方法可行,但以后每次再新生成xml文件就又需要我们将该xml文件剪切粘贴到resources文件夹下,挺麻烦的
3.方法三(推荐使用):
①在pom.xml中进行配置(我们可以在service_edu的pom.xml中进行配置,但我们后期别的项目中可能也需要这样的配置,所以我们选择在service的pom.xml中进行配置)
<build>
<resources>
<resource>
<directory>src/main/javadirectory>
<includes>
<include>**/*.xmlinclude>
includes>
<filtering>falsefiltering>
resource>
resources>
build>
第123行的
中的**(两个星号)
表示多层目录,如果是*(一个星号)
则表示java目录下的一层目录,一样加载不到我们的xml文件(因为我们的xml文件是在java目录下的很多层目录下的)
②点击"Load Maven Changes"刷新pom文件
③在application.properties中进行配置
#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/atguigu/eduservice/mapper/xml/*.xml
1.重启EduApplication服务,可以看到编译后mapper文件夹下就有xml文件了
2.在输入框输入课程id后点击"Try it out!"
3.可以看到成功返回了数据,测试成功
在src–>api–>edu–>course.js页面中编写方法调用后端接口
//5.根据课程id查询课程具体信息
getPublishCourseInfo(courseId) {
return request({
url: `/eduservice/course/getPublishCourseInfo/${courseId}`,
method: 'get'
})
}
在publish.vue页面得到路由中的课程id
//获取路由中的课程id
if(this.$route.params && this.$route.params.id) {
this.courseId = this.$route.params.id
}
截图中第32行用到了数据模型courseId,现在我们去定义这个数据模型
1.我们接下来需要调用在"2.1在api中定义方法"定义的方法getPublishCourseInfo,所以需要在publish.vue页面中引入course.js文件
import course from '@/api/edu/course'
2.在publish.vue页面中定义方法来调用api中的方法
//根据课程id查询课程具体信息
getCoursePublishId() {
//调用api中的方法
course.getPublishCourseInfo(this.courseId)
.then(response => {
this.publishCourse = response.data.publishCourse
})
},
截图的第43行用到了数据模型coursePublish。现在我们去定义这个数据模型
我们在created方法中调用在"2.3调用api中的方法"的第2步定义的getCoursePublishId方法以初始化页面得到该门课程的信息
//调用方法:根据课程id查询课程具体信息
this.getCoursePublishId()
1.将方框圈起来的部分删掉
2.将老师给的代码复制过来
{{ coursePublish.title }}
共{{ coursePublish.lessonNum }}课时
所属分类:{{ coursePublish.subjectLevelOne }} — {{ coursePublish.subjectLevelTwo }}
课程讲师:{{ coursePublish.teacherName }}
¥{{ coursePublish.price }}
返回修改
发布课程
3.将老师给的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>
在地址栏输入http://localhost:9528/#/course/publish/1562267576652808193进行测试,可以看到成功显示数据
1.虽然此时数据库中有课程信息,但是我们还没有最终发布课程,只有当我们最终发布课程了,前台用户才可以看到这个课程
2.先看一下我们创建的edu_course表中status字段的含义
可以知道我们是通过status字段的值来判断该门课程是否发布:值为Draft时课程未发布;值为Normal时课程已发布
3.由下图可知当我们新的课程数据插入数据库中时,status字段默认是Draft,也就是说默认是未发布状态
在控制器EduCourseController中编写方法用于将课程的发布状态改为"已发布"
//课程最终发布(修改edu_course表中status字段的值)
@PostMapping("publishCourse/{id}")
public R publishCourse(@PathVariable String id) {
EduCourse eduCourse = new EduCourse();
eduCourse.setId(id);
eduCourse.setStatus("Normal"); //设置课程发布状态为"已发布"
courseService.updateById(eduCourse);
return R.ok();
}
在course.js中定义方法来调用上一步编写的后端接口
//6.课程最终发布(将课程的发布状态改为"已发布")
publishCourse(courseId) {
return request({
url: `/eduservice/course/publishCourse/${courseId}`,
method: 'post'
})
}
1.可以看到,"发布课程"按钮我们给它绑定的方法是publish方法
2.publish方法我们曾经编写过
3.完整的publish方法如下
//课程最终发布
publish() {
course.publishCourse(this.courseId)
.then(response => {
//提示发布成功
this.$message({
type: 'success',
message: '发布成功!'
});
//跳转到list.vue页面
this.$router.push({path: '/course/list'})
})
}
1.在地址栏输入http://localhost:9528/#/course/publish/1562267576652808193,然后点击"发布课程"
2.提示发布成功
3.去数据库查看,可以看到这条数据的status字段的值确实被修改为了Normal
其实和讲师列表是一样的,老师这里只做了最基本的实现,后期的条件查询带分页由我们自己完善,那我暂时也只做最基本的实现吧
在控制器EduCourseController中编写方法
//课程列表最基本实现(条件查询带分页后期再完善)
@GetMapping
public R getCourseList() {
List<EduCourse> list = courseService.list(null);
return R.ok().data("list", list);
}
在course.js中编写方法用于调用上一步编写的后端接口
//7.课程列表
getListCourse() {
return request({
url: `/eduservice/course`,
method: 'get'
})
}
此时list.vue(是course目录下的list.vue)中没有任何代码,我们说过了课程列表页面和讲师列表页面很相似,所以我们直接将讲师列表页面的代码全部复制粘贴到课程列表页面,并根据需求进行修改,修改后的课程列表页面如下
{{ scope.$index + 1 }}
{{ scope.row.status==='Normal'?'已发布':'未发布' }}
编辑课程基本信息
编辑课程大纲
删除课程信息
:to="'/course/chapter/'+scope.row.id"
是为了实现:点击"编辑课程大纲 "按钮时走/course/chapter/+课程id
路由从而在浏览器展现chapter.vue页面。截图中第37行的:to="'/course/info/'+scope.row.id"
同理自行测试
1.课程里面有课程描述、章节,章节里面又有小节,小节里面又有视频,所以我们删除课程时需要将视频、小节、章节、课程描述还有课程本身都给删除掉
2.外键约束
两张表一对多关联时,在多的那一方创建字段,指向一的那一方的主键,这个字段就叫做外键
拿课程表和章节表举例:
3.去章节表看一下:
这里的course_id字段就是外键。但是我们发现我们并没有给这个字段添加foreign key将这个外键声明出来,我们只是自己心里知道它是一个外键,这种方式我们称之为物理外键。
为什么不给外键字段添加foreign key呢?有一个原因是:如果添加了foreign key那么删除数据时就必须先删除视频,然后删除小节,然后删除章节,再删除课程描述,最后才能删除课程本身,否则就会报错(其中删除课程描述不一定非要在删除章节之后,因为课程描述只和课程本身有关联,所以只要是在删除课程本身之前删除课程描述就可以了)
如果不添加foreign key那么我们删除数据时就没有先后之别了,不过还是建议使用刚刚说的顺序进行删除
在控制器EduCourseController中编写代码
//删除课程
@DeleteMapping("{courseId}")
public R deleteCourse(@PathVariable String courseId) {
courseService.removeCourse(courseId);
return R.ok();
}
在业务层接口EduCourseService中定义抽象方法
//删除课程
void removeCourse(String courseId);
在业务层实现类EduCourseServiceImpl中实现上一步定义的抽象方法:
1.我们会在抽象方法中删除小节表、章节表、课程描述表中的数据,其中课程描述表我们曾经已经注入过了,现在我们来注入小节表和章节表
//注入小节和章节service
@Autowired
private EduVideoServiceImpl eduVideoService;
@Autowired
private EduChapterService chapterService;
2.在EduCourseServiceImpl中编写代码
//删除课程
@Override
public void removeCourse(String courseId) {
//1.根据课程id删除小节
eduVideoService.removeVideoByCourseId(courseId);
//2.根据课程id删除章节
chapterService.removeChapterByCourseId(courseId);
//3.根据课程id删除课程描述
courseDescriptionService.removeDescriptionByCourseId(courseId);
//4.根据课程id删除课程本身
int result = baseMapper.deleteById(courseId);
if (result == 0) { //删除失败
throw new GuliException(20001, "删除失败");
}
}
1.在业务层接口EduVideoService中定义抽象方法
//根据课程id删除小节
void removeVideoByCourseId(String courseId);
2.在业务层实现类EduVideoServiceImpl中实现上一步定义的抽象方法
//根据课程id删除小节
@Override
public void removeVideoByCourseId(String courseId) {
//TODO 删除小节前需要先删除小节下的视频文件
QueryWrapper<EduVideo> wrapper = new QueryWrapper<>();
wrapper.eq("course_id", courseId);
baseMapper.delete(wrapper);
}
删除小节时需要先删除小节中的视频,这个业务我们后面实现
1.在业务层接口EduChapterService中定义抽象方法
//根据课程id删除章节
void removeChapterByCourseId(String courseId);
2.在业务层实现类EduChapterServiceImpl中实现上一步定义的抽象方法
有没有朋友疑惑为什么不将removeVideoByCourseId方法和removeChapterByCourseId方法的逻辑编写在业务层实现类EduCourseServiceImpl的removeCourse方法中呢?我们这样做是为了解耦
重启后端项目后使用swagger自行测试吧
//8.根据课程id删除课程
deleteCourseById(courseId) {
return request({
url: `/eduservice/course/${courseId}`,
method: 'delete'
})
}
给list.vue页面的"删除课程信息"按钮绑定事件
//根据课程id删除课程
deleteCourse(courseId) {
this.$confirm('此操作将永久删除课程记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
course.deleteCourseById(courseId)
.then(response => {
//1.提示删除成功
this.$message({
type: 'success',
message: '删除成功!'
});
//2.回到列表页面
this.getList()
})
.catch(error => {}) //删除失败
})
},
自行测试