一、添加项目基本信息完善
课程添加的简介整合文本编辑器
1、准备工作
复制富文本编辑器组件到项目中
复制Tinymce到components,复制tinymce4.7.5到static
编辑build/webpack.dev.conf.js,加上
new HtmlWebpackPlugin({
...,
templateParameters: {
BASE_URL: config.dev.assetsPublicPath + config.dev.assetsSubDirectory
}
})
index.html引入脚本文件
<script src=<%= BASE_URL %>/ tinymce4. 7.5 / tinymce. min. js> </ script>
<script src=<%= BASE_URL %>/ tinymce4. 7.5 / langs/ zh_CN. js> </ script>
修改该文件后需要重启,js></位置会有一个小红波浪线,是框架本身解析的问题,不影响使用
2、使用富文本编辑器组件
info页面引入组件并声明
import Tinymce from '@/components/Tinymce' //引入组件
export default {
components: {Tinymce}, //声明组件
info页面使用标签使用组件实现文本编辑器
< el-form-item label = " 课程简介" >
< tinymce :height = " 300" v-model = " courseInfo.description" />
</ el-form-item>
info页面加上文本编辑器的样式,调整上传图片按钮的高度
< style scoped >
.tinymce-container {
line-height : 29px;
}
</ style>
样式一般写在页面的最后
3、测试
给简介文字加粗,加上图片,存进数据库的是标签形式,如< strong> 、< img> 标签,数据库字段是text类型。
其中的图片先通过bese64编码为更利于存储的格式,然后存进数据库
注意
text类型是有大小限制的,如果 添加的图片太大,可能会导致数据库存不下,会导致只有edu_course插入成功,而edu_course_description插入失败,抛出异常,提示data too long
如果使用默认封面不会调用上传图片的接口,阿里云oss中不会上传新的图片
问题:添加课程保存之后,数据库的edu_course表中添加的数据中的subject_parent_id为空
原因:添加课程的接口接收的参数CourseInfoVo对象,其中不包含subjectParentId属性
解决:CourseInfoVo加上subjectParentId属性
@ApiModelProperty(value = "课程一级分类ID")
private String subjectParentId;
注意前端页面中的courseInfo对象中的属性和后端CourseInfoVo类中的属性要对上
二、课程大纲管理
1、课程大纲列表显示
后端接口
章节和小节是一对多的关系,参考一级分类和二级分类
创建两个实体类,章节和小节,章节类中创建list集合存储对应的小节
@Data
@ApiModel ( value = "课程章节实体类" )
public class ChapterVo {
@ApiModelProperty ( value = "课程章节ID" )
private String id;
@ApiModelProperty ( value = "课程章节名称" )
private String title;
@ApiModelProperty ( value = "课程章节对应小节列表" )
private List< VideoVo> children = new ArrayList < > ( ) ;
}
@Data
@ApiModel ( value = "课程小节实体类" )
public class VideoVo {
@ApiModelProperty ( value = "课程小节ID" )
private String id;
@ApiModelProperty ( value = "课程小节名称" )
private String title;
}
EduChapterController中创建接口,通过路径传入课程id,调用service,查询课程大纲
注入service
@Autowired
EduChapterService eduChapterService;
@GetMapping ( "findChapterVideo/{courseId}" )
public R findChapterVideo ( @PathVariable String courseId) {
List< ChapterVo> chapterVideo = eduChapterService. findChapterVideo ( courseId) ;
return R. ok ( ) . data ( "chapterVideo" , chapterVideo) ;
}
EduChapterService创建方法findChapterVideo并在实现类中重写
查询课程id对应章节
QueryWrapper< EduChapter> wrapperChapter = new QueryWrapper < > ( ) ;
wrapperChapter. eq ( "course_id" , courseId) ;
List< EduChapter> chapters = baseMapper. selectList ( wrapperChapter) ;
因为在chapter的service中使用baseMapper只能查询chapter,要查询video需要注入video的service
@Autowired
private EduVideoService eduVideoService;
查询课程id对应小节
QueryWrapper< EduVideo> wrapperVideo = new QueryWrapper < > ( ) ;
wrapperVideo. eq ( "course_id" , courseId) ;
List< EduVideo> videos = eduVideoService. list ( wrapperVideo) ;
封装章节和小节
List< ChapterVo> chapterVos = new ArrayList < > ( ) ;
for ( EduChapter chapter : chapters) {
ChapterVo chapterVo = new ChapterVo ( ) ;
BeanUtils. copyProperties ( chapter, chapterVo) ;
List< VideoVo> videoVos = new ArrayList < > ( ) ;
for ( EduVideo video : videos) {
if ( video. getChapterId ( ) . equals ( chapter. getId ( ) ) ) {
VideoVo videoVo = new VideoVo ( ) ;
BeanUtils. copyProperties ( video, videoVo) ;
videoVos. add ( videoVo) ;
}
}
chapterVo. setChildren ( videoVos) ;
chapterVos. add ( chapterVo) ;
}
return chapterVos;
使用swagger进行测试,输入课程id,点击try
前端整合
之前的分类树形结构使用了框架封装的标签来实现,只需要传入数据,遍历过程不需要写
这里使用ul和li标签来遍历显示课程大纲
在api/ edu中创建接口文件chapter. js
import request from '@/utils/request'
export default {
getChapterVideoList ( courseId) {
return request ( {
url: `/ eduservice/ chapter/ findChapterVideo/ ${ cour
method: 'get'
} )
}
}
chapter页面
引入chapter文件
import chapter from '@/api/edu/chapter'
data中定义大纲列表变量
chapterVideoList: { }
创建根据课程id查询大纲列表的方法getChapterVideoList,调用chapter中的方法
getChapterVideoList ( ) {
chapter. getChapterVideoList ( this . courseId)
. then ( response => {
this . chapterVideoList = response. data. chapterVid
} )
}
这里的courseId可以通过取得路径中的id来获取,因为之前第一步在跳转到第二步时将课程id加到了路径之后
在data中创建变量courseId
courseId: ''
created方法中获取路径中的id并调用方法
if ( this . $route. params && this . $route. params. i
this . courseId = this . $route. params. id
this . getChapterVideoList ( )
}
使用ul和li标签以及v- for 指令来显示课程大纲
< ul>
< li v- for = "chapter in chapterVideoList" : key= "chapte
{ { chapter. title} }
< ul>
< li v- for = "video in chapter.children" : key= "vide
{ { video. title} }
< / li>
< / ul>
< / li>
< / ul>
在页面的最后加上样式
< style scoped>
. chapterList {
position: relative;
list- style: none;
margin: 0 ;
padding: 0 ;
}
. chapterList li {
position: relative;
}
. chapterList p {
float: left;
font- size: 20 px;
margin: 10 px 0 ;
padding: 10 px;
height: 70 px;
line- height: 50 px;
width: 100 % ;
border: 1 px solid #DDD ;
}
. chapterList . acts {
float: right;
font- size: 14 px;
}
. videoList {
padding- left: 50 px;
}
. videoList p {
float: left;
font- size: 14 px;
margin: 10 px 0 ;
padding: 10 px;
height: 50 px;
line- height: 30 px;
width: 100 % ;
border: 1 px dotted #DDD ;
}
< / style>
注意:有时候浏览器因为缓存刷新不了没有效果,可以重启浏览器或者ctrl+ f5强制刷新
三、修改课程基本信息
创建课程大纲页面点击上一步,回到课程基本信息页面,信息回显,进行修改
后端接口
回显数据需要创建根据课程id查询课程基本信息接口
controller中创建接口
@GetMapping ( "findCourseById/{id}" )
public R findCourseById ( @PathVariable String id) {
CourseInfoVo courseInfo = eduCourseService. findCourseInfoById ( id) ;
return R. ok ( ) . data ( "courseInfo" , courseInfo) ;
}
service中创建方法,返回值是CourseInfoVo类型,因为其包含了课程修改页面的所有信息,包括课程简介
public CourseInfoVo findCourseInfoById ( String id) {
CourseInfoVo courseInfo = new CourseInfoVo ( ) ;
EduCourse course = baseMapper. selectById ( id) ;
BeanUtils. copyProperties ( course, courseInfo) ;
EduCourseDescription courseDescription = eduCourseDescriptionService. getById ( id) ;
BeanUtils. copyProperties ( courseDescription, courseInfo) ;
return courseInfo;
}
修改课程基本信息接口
controller中创建接口
@PostMapping ( "updateCourseInfo" )
public R updateCourseInfo ( @RequestBody CourseInfoVo courseInfo) {
eduCourseService. updateCourseInfo ( courseInfo) ;
return R. ok ( ) ;
}
service中创建方法
public void updateCourseInfo ( CourseInfoVo courseInfo) {
EduCourse course = new EduCourse ( ) ;
BeanUtils. copyProperties ( courseInfo, course) ;
int updateCourse = baseMapper. updateById ( course) ;
if ( updateCourse < 0 ) {
throw new OrangeException ( 20001 , "修改课程表失败" ) ;
}
EduCourseDescription courseDescription = new EduCourseDescription ( ) ;
BeanUtils. copyProperties ( courseInfo, courseDescription) ;
eduCourseDescriptionService. updateById ( courseDescription) ;
}
前端
course接口文件中创建方法
findCourseInfoById ( id) {
return request ( {
url: `/eduservice/course/findCourseById/ ${ id} ` ,
method: 'get'
} )
} ,
updateCourseInfo ( courseInfo) {
return request ( {
url: `/eduservice/course/updateCourseInfo` ,
method: 'post' ,
data: courseInfo
} )
}
chapter页面
previous和next方法中的跳转路径拼接上课程id
previous ( ) {
this . $router. push ( { path: '/course/info/' + this . courseId} )
} ,
next ( ) {
this . $router. push ( { path: '/course/publish/' + this . courseId} )
}
info页面
数据回显
data
创建属性courseId
courseId: ''
methods
创建方法findCourseInfoById,调用course文件中的方法,传入参数课程id,使用返回的数据进行回显
findCourseInfoById ( ) {
course. findCourseInfoById ( this . courseId)
. then ( response => {
this . courseInfo = response. data. courseInfo
} )
}
created方法
获取路径中的课程id
调用course文件中的findCourseInfoById方法
if ( this . $route. params && this . $route. params. id) {
this . courseId = this . $route. params. id
this . findCourseInfoById ( )
}
问题:回显时二级分类显示的是对应的id值,而不是对应的title名
原因:每次进入页面时会查询所有的一级分类存入一级分类数组,二级分类只有当选中一级分类时才会将数据存入二级分类的数组
一级分类下拉列表数据的回显,底层通过存储的一级分类id和所有一级分类id比较,如果相同就对对应的option加上属性selected
进入页面时,courseInfo通过findCourseInfoById方法进行了初始化,subjectOneList通过findAllOneSubject进行了初始化,所以可以进行比较来找到对应的一级分类的ti
而courseInfo中虽然初始化了subjectId,但subjectTwoList没有进行初始化,所以只能显示二级分类的id,无法比较获取二级分类的title
解决
created方法根据路径中是否存在id将操作分为添加和修改
created ( ) {
if ( this . $route. params && this . $route. params. id) {
this . courseId = this . $route. params. id
this . findCourseInfoById ( )
} else {
this . findAllTeacher ( )
this . findAllOneSubject ( )
}
}
findCourseInfoById方法填充完courseInfo对象后,调用subject的方法getSubjectList填充一级分类数组,
然后比较当前courseInfo对象中的一级分类id和一级分类数组的哪一个一级分类对应,将其对应的二级分类取出来填充二级分类数组,
有了二级分类数组和当前courseInfo中的二级分类id,框架就会帮我们找出要回显的二级分类并进行回显
findCourseInfoById ( ) {
course. findCourseInfoById ( this . courseId)
. then ( response => {
this . courseInfo = response. data. courseInfo
subject. getSubjectList ( )
. then ( response => {
this . subjectOneList = response. data. list
for ( var i = 0 ; i < this . subjectOneList. length; i++ ) {
var oneSubject = this . subjectOneList[ i]
if ( oneSubject. id == this . courseInfo. subjectParentId) {
this . subjectTwoList = oneSubject. children
}
}
} )
this . findAllTeacher ( )
} )
}
判断点击下一步按钮时是添加还是修改
创建两个方法addCourseInfo和updateCourseInfo
根据courseInfo对象中是否包含id来判断
注意添加数据会返回课程id,但是修改数据不会返回课程id,所以需要使用修改页面路径中的课程id,而跳转到修改页面时已经将路径中的id赋值给了data中的courseId
遇到的问题
bug
回显课程基本信息时,点击添加课程,不会清空当前回显的信息
解决
created中如果路径不含id,清空teacher对象
created ( ) {
if ( this . $route. params && this . $route. params. id) {
this . courseId = this . $route. params. id
this . findCourseInfoById ( )
} else {
this . findAllTeacher ( )
this . findAllOneSubject ( )
courseInfo = { }
this . courseInfo. cover = 'https://xm-edu-orange.oss-cn-hangzhou.aliyuncs.com/2021/03/09/44d9f352a9fc46f0bda74f50508026fdfile.png'
this . courseInfo. lessonNum = 0
this . courseInfo. price = 0
}
}
上面的代码失效的原因是添加和编辑属于两次路由跳转跳到了同一个页面,页面中的created方法只会执行一次
使用vue监听来解决
将上面created中的代码封装为一个init方法,加入到methods中
init ( ) {
if ( this . $route. params && this . $route. params. id) {
this . courseId = this . $route. params. id
this . findCourseInfoById ( )
} else {
courseInfo = { }
this . findAllTeacher ( )
this . findAllOneSubject ( )
}
}
在info页面中添加watch属性,每次路由变化时,调用init方法
watch: {
$route ( to, from ) {
this . init ( )
}
}
created方法也调用init方法
created ( ) {
this . init ( )
}
添加课程清空回显的数据之后,点击更改课程封面,调用了接口将图片上传到了oss,但图片不刷新,还是显示默认图片
解决
在图片上传成功后调用的方法AvatarUploadSuccess中加上强制刷新
AvatarUploadSuccess ( response) {
this . courseInfo. cover = response. data. url
this . $forceUpdate ( )
}
出现403错误状态码可能的两种原因
跨域
路径写错了
四、章节增删改
1、添加章节
后端
EduChapterController加上添加章节接口、根据id查询章节接口、修改章节接口
@PostMapping ( "addChapter" )
public R addChapter ( @RequestBody EduChapter chapter) {
eduChapterService. save ( chapter) ;
return R. ok ( ) ;
}
@GetMapping ( "findChapterById/{chapterId}" )
public R findChapterById ( @PathVariable String chapterId) {
EduChapter chapter = eduChapterService. getById ( chapterId) ;
return R. ok ( ) . data ( "chapter" , chapter) ;
}
@PostMapping ( "updateChapter" )
public R updateChapter ( @RequestBody EduChapter chapter) {
eduChapterService. updateById ( chapter) ;
return R. ok ( ) ;
}
删除章节接口
章节中没有小节,直接删除
章节中中有小节
把小节一起删除
不让章节被删除
这里使用第二种
@DeleteMapping ( "deleteChapterById/{chapterId}" )
public R deleteChapterById ( @PathVariable String chapterId) {
boolean flag = eduChapterService. deleteChapter ( chapterId) ;
return flag ? R. ok ( ) : R. error ( ) ;
}
EduChapterServiceImpl
@Override
public boolean deleteChapter ( String chapterId) {
QueryWrapper< EduVideo> wrapper = new QueryWrapper < > ( ) ;
wrapper. eq ( "chapter_id" , chapterId) ;
int videoCount = eduVideoService. count ( wrapper) ;
if ( videoCount == 0 ) {
int affectRows = baseMapper. deleteById ( chapterId) ;
return affectRows > 0 ;
} else {
throw new OrangeException ( 20001 , "章节中包含小节,不允许删除,请先删除小节!" ) ;
}
}
前端
添加添加章节按钮
< el- button type= "text" > 添加章节< / el- button>
点击添加章节按钮,弹出输入框,点击保存完成添加
使用element- ui的dialog中嵌套表单的对话框
< el- button type= "text" @click= "dialogChapterFormVisible = true" > 添加章节< / el- button>
< el- dialog title= "添加章节" : visible. sync= "dialogChapterFormVisible" >
< el- form : model= "chapter" label- width= "120px" >
< el- form- item label= "章节标题" >
< el- input v- model= "chapter.title" > < / el- input>
< / el- form- item>
< el- form- item label= "章节排序" : label- width= "formLabelWidth" >
< el- input- number v- model= "chapter.sort" : min= "1" controls- position= "right" / >
< / el- form- item>
< / el- form>
< div slot= "footer" class = "dialog-footer" >
< el- button @click= "dialogChapterFormVisible = false" > 取 消< / el- button>
< el- button type= "primary" @click= "saveOrUpdate" > 确 定< / el- button>
< / div>
< / el- dialog>
data加上对应变量
dialogChapterFormVisible: false
chapter: {
title: '' ,
sort: 1
}
创建方法openAddChapterDialog,绑定到添加章节按钮的onclick事件上
< el- button type= "text" @click= "openAddChapterDialog()" > 添加章节< / el- button>
openAddChapterDialog ( ) {
this . dialogChapterFormVisible = true
this . chapter = { }
}
在chapter文件中加上上面的接口
addChapter ( chapter) {
return request ( {
url: `/eduservice/chapter/addChapter` ,
method: 'post' ,
data: chapter
} )
} ,
getChapterById ( chapterId) {
return request ( {
url: `/eduservice/chapter/findChapterById/ ${ chapterId} ` ,
method: 'get'
} )
} ,
updateChapter ( chapter) {
return request ( {
url: `/eduservice/chapter/updateChapter` ,
method: 'post' ,
data: chapter
} )
} ,
deleteChapter ( chapterId) {
return request ( {
url: `/eduservice/chapter/deleteChapterById/ ${ chapterId} ` ,
method: 'delete'
} )
}
chapter页面创建添加章节的方法,调用chapter文件中的方法
addChapter ( ) {
this . chapter. courseId = this . courseId
chapter. addChapter ( this . chapter)
. then ( response => {
this . dialogChapterFormVisible = false
this . $message ( {
type: 'success' ,
message: '添加章节成功!'
} ) ;
this . getChapterVideoList ( )
} )
}
saveOrUpdate方法中调用
saveOrUpdate ( ) {
this . addChapter ( )
}
测试
提示执行了全局异常处理,表示后端代码有错误
错误:course_id没有默认值
原因:填写弹框只填写了章节的标题和排序,没有填写课程id,而章节表的course_id是非空的
解决:chapter对象加上属性courseId,将课程id设置到chapter对象中
chapter: {
title: '' ,
sort: 1 ,
courseId: ''
}
add ( ) {
this . chapter. courseId = this . courseId
2、修改章节
前端
在遍历章节的li中加上编辑按钮和删除按钮
< p>
{ { chapter. title} }
< span class = "acts" >
< el- button type= "text" @click= "openUpdateChapterDialog(chapter.id)" > 编辑< / el- button>
< el- button type= "text" > 删除< / el- button>
< / span>
< / p>
错误:加上按钮编辑和删除按钮后,无法点击,鼠标悬浮不会变成小手
原因:可能是p标签的float: left和. acts的float: right冲突了
解决:删除p标签的float: left 两个p标签都删除
创建方法openUpdateChapterDialog,调用chapter文件的方法,将返回的数据赋值给chapter对象
openUpdateChapterDialog ( chapterId) {
this . dialogChapterFormVisible = true
chapter. getChapterById ( chapterId)
. then ( response => {
this . chapter = response. data. chapter
} )
}
创建方法updateChapter,调用chapter文件的方法
updateChapter ( ) {
chapter. updateChapter ( this . chapter)
. then ( response => {
this . dialogChapterFormVisible = false
this . $message ( {
type: 'success' ,
message: '修改章节成功!'
} ) ;
this . getChapterVideoList ( )
} )
}
saveOrUpdate方法中判断是添加章节操作还是修改章节操作
saveOrUpdate ( ) {
if ( this . chapter. id) {
this . updateChapter ( )
} else {
this . addChapter ( )
}
}
3、删除章节
前端
给删除按钮绑定上onclick调用的方法
< el- button type= "text" @click= "deleteChapter(chapter.id)" > 删除< / el- button>
创建方法deleteChapter
deleteChapter ( chapterId) {
this . $confirm ( '此操作将永久删除该章节记录, 是否继续?' , '提示' , {
confirmButtonText: '确定' ,
cancelButtonText: '取消' ,
type: 'warning'
} ) . then ( ( ) => {
chapter. deleteChapter ( chapterId)
. then ( response => {
this . $message ( {
type: 'success' ,
message: '删除章节成功!'
} ) ;
this . getChapterVideoList ( )
} )
} ) . catch ( ( ) => {
this . $message ( {
type: 'info' ,
message: '已取消删除'
} ) ;
} ) ;
}
五、小节增删改
添加小节时需要课程id 章节id
后端
创建小节的增删改接查口
@PostMapping ( "addVideo" )
public R addVideo ( @RequestBody EduVideo video) {
boolean flag = eduVideoService. save ( video) ;
return flag ? R. ok ( ) : R. error ( ) ;
}
@DeleteMapping ( "deleteVideoById/{id}" )
public R deleteVideoById ( @PathVariable String id) {
boolean flag = eduVideoService. removeById ( id) ;
return flag ? R. ok ( ) : R. error ( ) ;
}
@PostMapping ( "updateVideo" )
public R updateVideo ( @RequestBody EduVideo video) {
boolean flag = eduVideoService. updateById ( video) ;
return flag ? R. ok ( ) : R. error ( ) ;
}
@GetMapping ( "findVideoById/{id}" )
public R findVideoById ( @PathVariable String id) {
EduVideo video = eduVideoService. getById ( id) ;
return R. ok ( ) . data ( "video" , video) ;
}
前端
在章节的编辑按钮前增加一个添加课时按钮
< el- button type= "text" @click= "openAddVideoDialog(chapter.id)" > 添加课时< / el- button>
创建添加课时的弹框
< el- dialog title= "添加课时" : visible. sync= "dialogVideoFormVisible" >
< el- form : model= "video" label- width= "120px" >
< el- form- item label= "课时标题" >
< el- input v- model= "video.title" > < / el- input>
< / el- form- item>
< el- form- item label= "课时排序" >
< el- input- number v- model= "video.sort" : min= "1" controls- position= "right" / >
< / el- form- item>
< el- form- item label= "是否免费" >
< el- radio- group v- model= "video.isFree" >
< el- radio : label= "true" > 免费< / el- radio>
< el- radio : label= "false" > 默认< / el- radio>
< / el- radio- group>
< / el- form- item>
< el- form- item label= "上传视频" >
< ! -- TODO -- >
< / el- form- item>
< / el- form>
< div slot= "footer" class = "dialog-footer" >
< el- button @click= "dialogVideoFormVisible = false" > 取 消< / el- button>
< el- button : disabled= "saveVideoDisabled" type= "primary" @click= "saveOrUpdateVideo" > 确 定< / el- button>
< / div>
< / el- dialog>
data中创建变量
dialogVideoFormVisible: false ,
saveVideoDisabled: false ,
video: {
title: '' ,
sort: 1 ,
isFree: 0 ,
videoSourceId: '' ,
chapterId: '' ,
courseId: '' ,
id: ''
}
创建接口文件api/ video,加上增删改查接口
import request from '@/utils/request'
export default {
addVideo ( video) {
return request ( {
url: `/eduservice/video/addVideo` ,
method: 'post' ,
data: video
} )
} ,
getVideoById ( videoId) {
return request ( {
url: `/eduservice/video/findVideoById/ ${ videoId} ` ,
method: 'get'
} )
} ,
updateVideo ( video) {
return request ( {
url: `/eduservice/video/updateVideo` ,
method: 'post' ,
data: video
} )
} ,
deleteVideoById ( videoId) {
return request ( {
url: `/eduservice/video/deleteVideoById/ ${ videoId} ` ,
method: 'delete'
} )
}
}
引入video文件
import video from '@/api/edu/video'
创建增删改查方法,调用video中的方法
增
openAddVideoDialog ( chapterId) {
this . dialogVideoFormVisible = true
this . video = { }
this . video. chapterId = chapterId
} ,
addVideo ( ) {
this . video. courseId = this . courseId
video. addVideo ( this . video)
. then ( response => {
this . dialogVideoFormVisible = false
this . $message ( {
type: 'success' ,
message: '添加小节成功!'
} ) ;
this . getChapterVideoList ( )
} )
} ,
saveOrUpdateVideo ( ) {
this . addVideo ( )
}
六、课程信息确认
1、编写sql语句进行多表操作
根据课程id查询课程的基本信息,包括封面 简介 讲师 分类
需要查询edu_course edu_course_description edu_teacher edu_subject
多表查询的三种方式
1 前端 11 Java 2
2 后端 12 vue 1
3 运维 13 MySQL null
内连接
查询两张表有关联的数据
上面两张表只有前两条数据有关联,所以只能查出前两条数据
左外连接
查询左表的所有数据以及右表的和左表有关联的数据
上面两张表可以查出左表的三条数据和右表的前两条数据
右外连接
查询右表的所有数据以及左表的和右表有关联的数据
上面两张表可以查出右表的三条数据和左表的前两条数据
这里不适合使用内连接,因为有些课程可能没有简介、分类或者讲师,使用内连接的话这些课程都查询不出来
所以使用左外连接,将课程表作为左表,查询课程表中的所有课程以及简介表、讲师表、分类表中和课程表有关联的数据
查询课程表左外连接简介表、讲师表和分类表
select ec.id, ec.title, ec.price, ec.lesson_num as lessonNum, ec.cover,
ecd.description,
et.name as teacherName,
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 = ?
2、后端
在mapper接口中定义方法,在mapper的配置文件中定义sql语句
创建CoursePublishVo类用于封装多表查询返回的数据
@Data
public class CoursePublishVo {
@ApiModelProperty ( value = "课程ID" )
private String id;
@ApiModelProperty ( value = "课程标题" )
private String title;
@ApiModelProperty ( value = "课程销售价格,设置为0则可免费观看" )
private BigDecimal price;
@ApiModelProperty ( value = "总课时" )
private Integer lessonNum;
@ApiModelProperty ( value = "课程封面图片路径" )
private String cover;
@ApiModelProperty ( value = "课程简介" )
private String description;
@ApiModelProperty ( value = "讲师姓名" )
private String teacherName;
@ApiModelProperty ( value = "一级分类标题" )
private String oneSubject;
@ApiModelProperty ( value = "二级分类标题" )
private String twoSubject;
}
EduCourseMapper
创建查询课程发布时要显示的数据的方法,返回CoursePublishVo对象
public interface EduCourseMapper extends BaseMapper < EduCourse> {
public CoursePublishVo getCoursePublishInfo ( String courseId) ;
}
EduCourseMapper. xml
使用select标签定义sql语句,标签属性id填写mapper接口中的方法名,resultType填写方法返回值类型的全路径,不要写成resultMap,这是在xml中自定义的类型
< select id= "getCoursePublishInfo" resultType= "com.xm.eduservice.entity.vo.CoursePublishVo" >
select ec. id, ec. title, ec. price, ec. lesson_num as lessonNum, ec. cover,
ecd. description,
et. name as teacherName,
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 = #{ courseId} ;
< / select>
注意CoursePublishVo的属性和sql语句中的字段要对应
如果mapper接口的方法只有一个参数,那么最后的参数#{ } 内可以随便写个参数名
EduCourseController中创建方法
@GetMapping ( "findCoursePublishInfo/{id}" )
public R findCoursePublishInfo ( @PathVariable String id) {
CoursePublishVo coursePublishInfo = eduCourseService. findCoursePublishInfo ( id) ;
return R. ok ( ) . data ( "coursePublishInfo" , coursePublishInfo) ;
}
EduCourseServiceImpl
@Override
public CoursePublishVo findCoursePublishInfo ( String id) {
CoursePublishVo coursePublishInfo = baseMapper. getCoursePublishInfo ( id) ;
return coursePublishInfo;
}
调用mapper中自定义的方法,mapper调用对应的xml文件中的sql语句,将结果返回
错误:org. apache. ibatis. binding. BindingException: Invalid bound statement ( not found) : com. xm. eduservice. mapper. EduCourseMapper. getCoursePublishInfo
原因:xml中方法名写错了
或者由于maven默认的加载机制,maven项目中会将编译后的class 文件放在target目录中,项目中有xml文件而target目录中没有就会报这个错误,因为maven默认只加载Java文件
解决:
法一 将xml文件夹复制到target中的对应位置 但每次都需要复制
法二 将xml文件夹放在resources文件夹中,maven会加载resources文件夹中的文件 但会破坏项目结构
法三 通过配置实现 推荐
service的pom. xml
< build>
< resources>
< resource>
< directory> src/ main/ java< / directory>
< includes>
< include> * *
3、前端
course文件定义获取发布确认信息的接口
getCoursePublishInfo ( id) {
return request ( {
url: `/eduservice/course/findCoursePublishInfo/ ${ id} ` ,
method: 'get'
} )
}
publish页面
引入course文件
import course from '@/api/edu/course'
data中创建变量
courseId: '' ,
coursePublishInfo: {
id: '' ,
title: '' ,
lessonNum: 0 ,
price: 0 ,
cover: '' ,
description: '' ,
teacherName: '' ,
oneSubject: '' ,
twoSubject: ''
}
创建根据课程id查询课程确认信息的方法getCoursePublishInfo
getCoursePublishInfo ( ) {
course. getCoursePublishInfo ( this . courseId)
. then ( response => {
this . coursePublishInfo = response. data. coursePublishInfo
} )
}
created方法
获取路径中的chapter页面push过来的课程id,并调用方法
if ( this . $route. params && this . $route. params. id) {
this . courseId = this . $route. params. id
this . getCoursePublishInfo ( )
}
显示课程确认信息
< ! -- 显示课程确认信息 -- >
< div class = "ccInfo" >
< img : src= "coursePublishInfo.cover" >
< div class = "main" >
< h2> { { coursePublishInfo. title} } < / h2>
< p class = "gray" > < span> 共{ { coursePublishInfo. lessonNum} } 课时< / span> < / p>
< p> < span> 所属分类:{ { coursePublishInfo. oneSubject} } - { { coursePublishInfo. twoSubject} } < / span> < / p>
< p> 课程讲师:{ { coursePublishInfo. teacherName} } < / p>
< h3 class = "red" > ¥{ { coursePublishInfo. price} } < / h3>
< / div>
< / div>
< div>
< el- button : disabled= "saveBtnDisabled" type= "primary" @click= "previous" > 返回修改< / el- button>
< el- button : disabled= "saveBtnDisabled" type= "primary" @click= "publish" > 发布课程< / el- button>
< / div>
注意这里的按钮放在div中,而不是放在el- form- item中,否则会和样式产生冲突,chapter页面也一样
样式
< style scoped>
. ccInfo {
width: 60 % ;
background: #f5f5f5;
padding: 20 px;
overflow: hidden;
border: 1 px dashed #DDD ;
margin: 0 auto;
margin- bottom: 40 px;
position: relative;
}
. ccInfo img {
background: #d6d6d6;
width: 500 px;
height: 278 px;
display: block;
float: left;
border: none;
}
. ccInfo . main {
margin- left: 520 px;
}
. ccInfo . main h2 {
font- size: 28 px;
margin- bottom: 30 px;
line- height: 1 ;
font- weight: normal;
}
. ccInfo . main p {
margin- bottom: 10 px;
word- wrap: break - word;
line- height: 24 px;
max- height: 48 px;
overflow: hidden;
}
. ccInfo . main p {
margin- bottom: 10 px;
word- wrap: break - word;
line- height: 24 px;
max- height: 48 px;
overflow: hidden;
}
. ccInfo . main h3 {
left: 540 px;
bottom: 20 px;
line- height: 1 ;
font- size: 20 px;
color: #d32f24;
font- weight: normal;
position: absolute;
}
< / style>
4、课程发布
将课程表数据的status改为Normal已发布,默认是未发布是Draft
后端
EduCourseController创建课程发布接口
@PostMapping ( "publishCourse/{id}" )
public R publishCourse ( @PathVariable String id) {
EduCourse course = new EduCourse ( ) ;
course. setId ( id) ;
course. setStatus ( "Normal" ) ;
boolean flag = eduCourseService. updateById ( course) ;
return flag ? R. ok ( ) : R. error ( ) ;
}
前端
course文件中定义发布课程的接口
publishCourse ( id) {
return request ( {
url: `/eduservice/course/publishCourse/ ${ id} ` ,
method: 'post'
} )
}
publish页面的publish方法
publish ( ) {
course. publishCourse ( this . courseId)
. then ( response => {
this . $confirm ( '确定发布课程?' , '提示' , {
confirmButtonText: '确定' ,
cancelButtonText: '取消' ,
type: 'warning'
} ) . then ( ( ) => {
course. publishCourse ( this . courseId)
. then ( response => {
this . $message ( {
type: 'success' ,
message: '发布课程成功!'
} ) ;
this . $router. push ( { path: '/course/list' } )
} )
} ) . catch ( ( ) => {
this . $message ( {
type: 'info' ,
message: '已取消发布'
} ) ;
} ) ;
} )
}