项目源码与所需资料
链接:https://pan.baidu.com/s/1azwRyyFwXz5elhQL0BhkCA?pwd=8z59
提取码:8z59
demo10-课程管理
1.课程信息确认后端
1.1分析
1.课程信息确认页面会显示课程名称、课程价格、课程简介、课程所属分类、课程所属讲师等等…我们要从数据表中查询这些信息,其中从edu_course表查询课程名称、课程价格;从edu_course_description表查询课程简介;从edu_subject表查询课程所属分类;从edu_teacher表查询课程所属讲师。这些数据并没有在一张数据表中,我们应该怎么解决呢?在"demo09-课程管理"的"3.1.3业务层实现类"中我们的做法是:分别查询这两张表,然后将查询到的数据封装到一个VO实体类中。此时我们也可以通过分别查询这几张数据表并封装数据来实现需求,但是在"demo09-课程管理"的"3.1.3业务层实现类"中只涉及到两张数据表,而我们此时涉及到的数据表太多了,所以不建议这样做
2.建议通过手写sql语句来实现
1.2多表连接查询
1.2.1多表查询的三种方式
假如左边的数据表是课程一级分类表,右边的数据表是课程二级分类表,且右边的表的第三列存放的是课程一级分类的id
- 内连接
- 查询的两张表有关联的数据就叫做内连接
- 那么查询上图中的两张表,就会查询出来四条数据:前端、后端、Java、vue
- 左外连接
- 查询时将查询左边表的所有数据,查询右边表时只查询和左边表有关联的数据
- 那么查询上图中的两张表,就会查询出来五条数据:前端、后端、运维、Java、vue
- 右外连接
- 查询时将查询右边表的所有数据,查询左边表时只查询和右边表有关联的数据,实际上就是左外连接反过来呗
- 那么查询上图中的两张表,就会查询出来五条数据:前端、后端、Java、vue、mysql
1.2.2我们应该用哪种方式
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'
- 我们将课程一级分类和课程二级分类都存到了edu_subject这张表中,此时应该怎么做:如果一张表中有多个字段同时关联一张表,我们就需要查询多次
- 1562267576652808193是我的edu_course数据表中的id,你们写自己数据表中的
1.3创建vo类
在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;//只用于显示
}
1.4持久层
我们在前面的代码编写中有时只需处理控制层,有时只需处理控制层和业务层,还从来没有处理过持久层。但我们此时手写sql语句,所以需要处理持久层
1.4.1在mapper中定义方法
在EduCourseMapper中定义"根据课程id查询课程具体信息"的抽象方法
public interface EduCourseMapper extends BaseMapper<EduCourse> {
//根据课程id查询课程具体信息
public CoursePublishVo getPublishCourseInfo(String courseId);
}
1.4.2在idea中连接数据库
1.点击"DataBase"
2.点击加号(“+”),然后点击Data Source下的MySQL
3.输入我们的mysql密码和数据库名字,然后点击"Ok"
4.填写mysql的账号密码,然后点击"Ok"
5.此时我们就可以看到数据库guli中的数据表了,说明此时连接成功
1.4.3编写映射
在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
来起别名?- 因为我们vo类CoursePublishVo中定义的属性名是lessonNum、subjectLevelOne、subjectLevelTwo、teacher Name。所以我们需要起别名,并且起的别名一定要和CoursePublishVo中的属性名对应上
-
我们在"1.4.1在mapper中定义方法"定义的抽象方法只有一个参数,所以截图中第13行的
WHERE ec.id=#{courseId}
中的courseId可以随便写(不过别写成中文啊!!!) -
<select>标签中的id属性值就填写我们在"1.4.1在mapper中定义方法"定义的抽象方法的名字
-
<select>标签中的resultType属性是方法返回值的类型,这里我们需要填写CoursePublishVo类的全路径:
com.atguigu.eduservice.entity.vo
,末尾再加上.CoursePublishVo
1.5控制层
在控制器EduCourseController中定义方法
//根据课程id查询课程具体信息
@GetMapping("getPublishCourseInfo/{id}")
public R getPublishCourseInfo(@PathVariable String id) {
CoursePublishVo coursePublishVo= courseService.publishCourseInfo(id);
return R.ok().data("publishCourse", coursePublishVo);
}
1.6业务层接口
在业务层接口EduCourseService定义抽象方法
//根据课程id查询课程具体信息
CoursePublishVo publishCourseInfo(String id);
1.7业务层实现类
在业务层实现类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.8测试
1.重启EduApplication服务,使用swagger进行测试
2.在输入框输入课程id后点击"Try it out!"
3.执行了异常,说明我们接口是有问题的,解决方法在后面的"1.9加载问题"
1.9加载问题
1.9.1分析问题
1.接口报错如下
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.atguigu.eduservice.mapper.EduCourseMapper.getPublishCourseInfo
2.出现这种报错我们首先查看EduCourseMapper.xml的<select>标签中id属性值和我们在EduCourseMapper中定义的方法名字是否对应上,检查过后发现我们这里是对应上的
3.其实这个错误是maven默认加载机制造成的
①我们先看下面这张图,此时是有xml文件的
②然后再看下图,发现编译后没有xml文件了,也就是说编译时并没有编译xml文件
③这是maven默认加载机制造成的:加载时只会加载java文件夹下的.java类型文件,而不会加载java文件夹下的xml类型的文件
1.9.2解决问题
解决方法有很多种:
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中进行配置)
<!-- 项目打包时会将java目录中的*.xml文件也进行打包 -->
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
第123行的<include>**/*.xml</include>
中的**(两个星号)
表示多层目录,如果是*(一个星号)
则表示java目录下的一层目录,一样加载不到我们的xml文件(因为我们的xml文件是在java目录下的很多层目录下的)
②点击"Load Maven Changes"刷新pom文件
③在application.properties中进行配置
#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/atguigu/eduservice/mapper/xml/*.xml
1.9.3再次测试
1.重启EduApplication服务,可以看到编译后mapper文件夹下就有xml文件了
2.在输入框输入课程id后点击"Try it out!"
3.可以看到成功返回了数据,测试成功
2.课程信息确认前端
2.1在api中定义方法
在src–>api–>edu–>course.js页面中编写方法调用后端接口
//5.根据课程id查询课程具体信息
getPublishCourseInfo(courseId) {
return request({
url: `/eduservice/course/getPublishCourseInfo/${
courseId}`,
method: 'get'
})
}
2.2获取路径中的课程id
在publish.vue页面得到路由中的课程id
//获取路由中的课程id
if(this.$route.params && this.$route.params.id) {
this.courseId = this.$route.params.id
}
截图中第32行用到了数据模型courseId,现在我们去定义这个数据模型
2.3调用api中的方法
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。现在我们去定义这个数据模型
2.4初始化页面
我们在created方法中调用在"2.3调用api中的方法"的第2步定义的getCoursePublishId方法以初始化页面得到该门课程的信息
//调用方法:根据课程id查询课程具体信息
this.getCoursePublishId()
2.5将内容显示到页面上
1.将方框圈起来的部分删掉
2.将老师给的代码复制过来
<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>
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>
2.6测试
在地址栏输入http://localhost:9528/#/course/publish/1562267576652808193进行测试,可以看到成功显示数据
3.课程最终发布后端
3.1分析
1.虽然此时数据库中有课程信息,但是我们还没有最终发布课程,只有当我们最终发布课程了,前台用户才可以看到这个课程
2.先看一下我们创建的edu_course表中status字段的含义
可以知道我们是通过status字段的值来判断该门课程是否发布:值为Draft时课程未发布;值为Normal时课程已发布
3.由下图可知当我们新的课程数据插入数据库中时,status字段默认是Draft,也就是说默认是未发布状态
3.2控制层
在控制器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();
}
4.课程最终发布前端
4.1在api中定义方法
在course.js中定义方法来调用上一步编写的后端接口
//6.课程最终发布(将课程的发布状态改为"已发布")
publishCourse(courseId) {
return request({
url: `/eduservice/course/publishCourse/${
courseId}`,
method: 'post'
})
}
4.2调用api中的方法
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'})
})
}
4.3测试
1.在地址栏输入http://localhost:9528/#/course/publish/1562267576652808193,然后点击"发布课程"
2.提示发布成功
3.去数据库查看,可以看到这条数据的status字段的值确实被修改为了Normal
5.课程列表后端
5.1分析
其实和讲师列表是一样的,老师这里只做了最基本的实现,后期的条件查询带分页由我们自己完善,那我暂时也只做最基本的实现吧
5.2控制层
在控制器EduCourseController中编写方法
//课程列表最基本实现(条件查询带分页后期再完善)
@GetMapping
public R getCourseList() {
List<EduCourse> list = courseService.list(null);
return R.ok().data("list", list);
}
6.课程列表前端
6.1在api中定义方法
在course.js中编写方法用于调用上一步编写的后端接口
//7.课程列表
getListCourse() {
return request({
url: `/eduservice/course`,
method: 'get'
})
}
6.2编写list.vue页面
此时list.vue(是course目录下的list.vue)中没有任何代码,我们说过了课程列表页面和讲师列表页面很相似,所以我们直接将讲师列表页面的代码全部复制粘贴到课程列表页面,并根据需求进行修改,修改后的课程列表页面如下
<template>
<div class="app-container">
<!-- 表格 -->
<el-table
:data="list"
border
fit
highlight-current-row>
<el-table-column
label="序号"
width="70"
align="center">
<template slot-scope="scope">
{
{ scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="title" label="课程名称" width="80" />
<el-table-column label="课程状态" width="80">
<template slot-scope="scope">
{
{ scope.row.status==='Normal'?'已发布':'未发布' }}
</template>
</el-table-column>
<el-table-column prop="lessonNum" label="课时数"/>
<el-table-column prop="gmtCreate" label="添加时间" width="160"/>
<el-table-column prop="viewCount" label="浏览数量" width="80" />
<el-table-column label="操作" width="450" align="center">
<template slot-scope="scope">
<router-link :to="'/course/info/'+scope.row.id">
<el-button type="primary" size="mini" icon="el-icon-edit">编辑课程基本信息</el-button>
</router-link>
<router-link :to="'/course/chapter/'+scope.row.id">
<el-button type="primary" size="mini" icon="el-icon-edit">编辑课程大纲</el-button>
</router-link>
<el-button type="danger" size="mini" icon="el-icon-delete">删除课程信息</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
//引入course.js文件
import course from '@/api/edu/course'
export default {
data() { //定义变量和初始值
return {
list: null //查询之后接口返回的数据赋值给list
}
},
created() { //页面渲染之前执行,一般用来调用methods中定义的方法
//调用
this.getList()
},
methods: { //创建具体的方法,调用course.js中定义的方法
//课程列表的方法
getList() {
//调用方法,使用axios发送ajax请求
course.getListCourse()
.then(response => {
this.list = response.data.list
})
}
}
}
</script>
- 截图中第41行的
:to="'/course/chapter/'+scope.row.id"
是为了实现:点击"编辑课程大纲 "按钮时走/course/chapter/+课程id
路由从而在浏览器展现chapter.vue页面。截图中第37行的:to="'/course/info/'+scope.row.id"
同理 - 删除按钮的后端和前端代码我们接下来就会说
6.3测试
自行测试
7.课程删除后端
7.1分析
1.课程里面有课程描述、章节,章节里面又有小节,小节里面又有视频,所以我们删除课程时需要将视频、小节、章节、课程描述还有课程本身都给删除掉
2.外键约束
两张表一对多关联时,在多的那一方创建字段,指向一的那一方的主键,这个字段就叫做外键
拿课程表和章节表举例:
3.去章节表看一下:
这里的course_id字段就是外键。但是我们发现我们并没有给这个字段添加foreign key将这个外键声明出来,我们只是自己心里知道它是一个外键,这种方式我们称之为物理外键。
为什么不给外键字段添加foreign key呢?有一个原因是:如果添加了foreign key那么删除数据时就必须先删除视频,然后删除小节,然后删除章节,再删除课程描述,最后才能删除课程本身,否则就会报错(其中删除课程描述不一定非要在删除章节之后,因为课程描述只和课程本身有关联,所以只要是在删除课程本身之前删除课程描述就可以了)
如果不添加foreign key那么我们删除数据时就没有先后之别了,不过还是建议使用刚刚说的顺序进行删除
7.2控制层
在控制器EduCourseController中编写代码
//删除课程
@DeleteMapping("{courseId}")
public R deleteCourse(@PathVariable String courseId) {
courseService.removeCourse(courseId);
return R.ok();
}
7.3业务层接口
在业务层接口EduCourseService中定义抽象方法
//删除课程
void removeCourse(String courseId);
7.4业务层实现类
在业务层实现类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, "删除失败");
}
}
7.5编写removeVideoByCourseId方法
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);
}
删除小节时需要先删除小节中的视频,这个业务我们后面实现
7.6编写removeChapterByCourseId方法
1.在业务层接口EduChapterService中定义抽象方法
//根据课程id删除章节
void removeChapterByCourseId(String courseId);
2.在业务层实现类EduChapterServiceImpl中实现上一步定义的抽象方法
7.7疑问
有没有朋友疑惑为什么不将removeVideoByCourseId方法和removeChapterByCourseId方法的逻辑编写在业务层实现类EduCourseServiceImpl的removeCourse方法中呢?我们这样做是为了解耦
7.8测试
重启后端项目后使用swagger自行测试吧
8.课程删除前端
8.1在api中定义方法
//8.根据课程id删除课程
deleteCourseById(courseId) {
return request({
url: `/eduservice/course/${
courseId}`,
method: 'delete'
})
}
8.2绑定事件
给list.vue页面的"删除课程信息"按钮绑定事件
8.3调用api中的方法
//根据课程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 => {
}) //删除失败
})
},
8.4测试
自行测试