业务分析
课程计划即课程的大纲目录。课程计划分为两级:大章节和不小章节。
从课程计划查询界面上可以看出整体上是一个树型结构,课程计划表teachplan如下:
每个课程计划都有所属课程。
每个课程的课程计划有两个级别,第一级为大章节,grade为1、第二级为小章节,grade为2
第二级的parentid为第一级的id。
课程计划的显示顺序根据排序字段去显示。
课程计划列表展示时还有课程计划关联的视频信息。
课程计划关联的视频信息在teachplan_media表,结构如下
两张表是一对一关系,每个课程计划只能在teachplan_media表中存在一个视频。
课程计划查询
接口设计
接口示例如下:
GET /teachplan/22/tree-nodes
[
{
"changeDate" : null,
"courseId" : 74,
"cousePubId" : null,
"createDate" : null,
"endTime" : null,
"grade" : "2",
"isPreview" : "0",
"mediaType" : null,
"orderby" : 1,
"parentid" : 112,
"pname" : "第1章基础知识",
"startTime" : null,
"status" : null,
"id" : 113,
"teachPlanTreeNodes" : [
{
"changeDate" : null,
"courseId" : 74,
"cousePubId" : null,
"createDate" : null,
"endTime" : null,
"grade" : "3",
"isPreview" : "1",
"mediaType" : "001002",
"orderby" : 1,
"parentid" : 113,
"pname" : "第1节项目概述",
"startTime" : null,
"status" : null,
"id" : 115,
"teachPlanTreeNodes" : null,
"teachplanMedia" : {
"courseId" : 74,
"coursePubId" : null,
"mediaFilename" : "2.avi",
"mediaId" : 41,
"teachplanId" : 115,
"id" : null
}
}
],
"teachplanMedia" : null
},
{
"changeDate" : null,
"courseId" : 74,
"cousePubId" : null,
"createDate" : null,
"endTime" : null,
"grade" : "2",
"isPreview" : "0",
"mediaType" : "",
"orderby" : 1,
"parentid" : 112,
"pname" : "第2章快速入门",
"startTime" : null,
"status" : null,
"id" : 242,
"teachPlanTreeNodes" : [
{
"changeDate" : null,
"courseId" : 74,
"cousePubId" : null,
"createDate" : null,
"endTime" : null,
"grade" : "3",
"isPreview" : "1",
"mediaType" : "001002",
"orderby" : 2,
"parentid" : 242,
"pname" : "第1节搭建环境",
"startTime" : null,
"status" : null,
"id" : 244,
"teachPlanTreeNodes" : null,
"teachplanMedia" : {
"courseId" : 74,
"coursePubId" : null,
"mediaFilename" : "3.avi",
"mediaId" : 42,
"teachplanId" : 244,
"id" : null
}
},
{
"changeDate" : null,
"courseId" : 74,
"cousePubId" : null,
"createDate" : null,
"endTime" : null,
"grade" : "3",
"isPreview" : "0",
"mediaType" : "001002",
"orderby" : 3,
"parentid" : 242,
"pname" : "第2节项目概述",
"startTime" : null,
"status" : null,
"id" : 245,
"teachPlanTreeNodes" : null,
"teachplanMedia" : {
"courseId" : 74,
"coursePubId" : null,
"mediaFilename" : "1a.avi",
"mediaId" : 39,
"teachplanId" : 245,
"id" : null
}
}
],
"teachplanMedia" : null
}
]
定义DTO
响应结果需要自定义模型类
/**
* @description 课程计划树型结构dto
* @author Mr.M
* @date 2022/9/9 10:27
* @version 1.0
*/
@Data
@ToString
public class TeachplanDto extends Teachplan {
//课程计划关联的媒资信息
TeachplanMedia teachplanMedia;
//子结点
List<TeachplanDto> teachPlanTreeNodes;
}
定义接口
定义接口如下:
/**
* @description 课程计划管理接口
* @author Mr.M
* @date 2022/9/6 11:29
* @version 1.0
*/
@Api(value = "课程计划管理接口",tags = "课程计划管理接口")
@RestController
public class TeachplanController {
@ApiOperation("查询课程计划树形结构")
@ApiImplicitParam(value = "courseId",name = "课程Id",required = true,dataType = "Long",paramType = "path")
@GetMapping("/teachplan/{courseId}/tree-nodes")
public List<TeachplanDto> getTreeNodes(@PathVariable Long courseId){
return null;
}
}
DAO开发
Mapper接口使用sql查询课程计划,组成一个树型结构。
在TeachplanMapper自定义方法:
/**
* <p>
* 课程计划 Mapper 接口
* </p>
*
* @author itcast
*/
public interface TeachplanMapper extends BaseMapper<Teachplan> {
/**
* @param courseId
* @return com.xuecheng.content.model.dto.TeachplanDto
* @description 查询某课程的课程计划,组成树型结构
* @author Mr.M
* @date 2022/9/9 11:10
*/
public List<TeachplanDto> selectTreeNodes(long 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.xuecheng.content.mapper.TeachplanMapper">
<!-- 查询树形结构 -->
<select id="selectTreeNodes" resultType="com.xuecheng.content.model.dto.TeachplanDto">
</select>
</mapper>
定义mapper.xml中的sql语句,分析如下:
1、一级分类和二级分类通过teachplan表的自链接进行,如果只有一级分类其下边没有二级分类,此时也需要显示一级分类,这里使用左连接,左边是一级分类,右边是二级分类。
2、由于当还没有关联视频时teachplan_media对应的记录为空,所以需要teachplan和teachplan_media左链接。
select one.id one_id,
one.pname one_pname,
one.parentid one_parentid,
one.grade one_grade,
one.media_type one_mediaType,
one.start_time one_stratTime,
one.end_time one_endTime,
one.orderby one_orderby,
one.course_id one_courseId,
one.course_pub_id one_coursePubId,
two.id two_id,
two.pname two_pname,
two.parentid two_parentid,
two.grade two_grade,
two.media_type two_mediaType,
two.start_time two_stratTime,
two.end_time two_endTime,
two.orderby two_orderby,
two.course_id two_courseId,
two.course_pub_id two_coursePubId,
m1.media_fileName mediaFilename,
m1.id teachplanMeidaId,
m1.media_id mediaId
from teachplan one
left join teachplan two on two.parentid = one.id
left join teachplan_media m1 on two.id = m1.teachplan_id
where one.parentid = 0
and one.course_id = 117
order by one.orderby,
two.orderby
把写好的sql语句放到xml文件中
<?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.xuecheng.content.mapper.TeachplanMapper">
<!-- 课程分类树型结构查询映射结果 -->
<!-- type是指定封装结果的实体类的全类名 -->
<resultMap id="treeNodeResultMap" type="com.xuecheng.content.model.dto.TeachplanDto">
<!-- 一级数据映射 -->
<!-- 主键字段必须使用id标签进行映射, 把数据库的one_id字段映射到实体类的id字段 -->
<id column="one_id" property="id"/>
<!-- 普通字段使用result标签进行映射, 把数据库的one_pname字段映射到实体类的parentid字段 -->
<result column="one_pname" property="pname"/>
<result column="one_parentid" property="parentid"/>
<result column="one_grade" property="grade"/>
<result column="one_mediaType" property="mediaType"/>
<result column="one_stratTime" property="stratTime"/>
<result column="one_endTime" property="endTime"/>
<result column="one_orderby" property="orderby"/>
<result column="one_courseId" property="courseId"/>
<result column="one_coursePubId" property="coursePubId"/>
<!-- 一级中包含多个二级数据 -->
<!-- 映射子节点, 一对多映射, ofType用于指定list中的对象类型 -->
<collection property="teachPlanTreeNodes" ofType="com.xuecheng.content.model.dto.TeachplanDto">
<!-- 二级数据映射 -->
<id column="two_id" property="id"/>
<result column="two_pname" property="pname"/>
<result column="two_parentid" property="parentid"/>
<result column="two_grade" property="grade"/>
<result column="two_mediaType" property="mediaType"/>
<result column="two_stratTime" property="stratTime"/>
<result column="two_endTime" property="endTime"/>
<result column="two_orderby" property="orderby"/>
<result column="two_courseId" property="courseId"/>
<result column="two_coursePubId" property="coursePubId"/>
<!-- 映射子节点: 一对一映射, javaType用于指定映射的对象类型 -->
<association property="teachplanMedia" javaType="com.xuecheng.content.model.po.TeachplanMedia">
<result column="teachplanMeidaId" property="id"/>
<result column="mediaFilename" property="mediaFilename"/>
<result column="mediaId" property="mediaId"/>
<result column="two_id" property="teachplanId"/>
<result column="two_courseId" property="courseId"/>
<result column="two_coursePubId" property="coursePubId"/>
</association>
</collection>
</resultMap>
<!--课程计划树型结构查询-->
<!-- parameterType 用于指定传递过来的参数类型 -->
<!-- 结果封装 -->
<!-- 1.如果数据库的字段和实体类一致,使用 resultTyp="实体类全类名" 自动封装就行 -->
<!-- 2.如果数据库的字段和实体类不一致, 使用 resultMap="查询映射结果" 手动封装 -->
<select id="selectTreeNodes" parameterType="long" resultMap="treeNodeResultMap" >
select one.id one_id,
one.pname one_pname,
one.parentid one_parentid,
one.grade one_grade,
one.media_type one_mediaType,
one.start_time one_stratTime,
one.end_time one_endTime,
one.orderby one_orderby,
one.course_id one_courseId,
one.course_pub_id one_coursePubId,
two.id two_id,
two.pname two_pname,
two.parentid two_parentid,
two.grade two_grade,
two.media_type two_mediaType,
two.start_time two_stratTime,
two.end_time two_endTime,
two.orderby two_orderby,
two.course_id two_courseId,
two.course_pub_id two_coursePubId,
m1.media_fileName mediaFilename,
m1.id teachplanMeidaId,
m1.media_id mediaId
from teachplan one
LEFT JOIN teachplan two on one.id = two.parentid
LEFT JOIN teachplan_media m1 on m1.teachplan_id = two.id
where one.parentid = 0
and one.course_id = #{id}
order by one.orderby,
two.orderby
</select>
</mapper>
单元测试自定义的mapper
@SpringBootTest
class TeachplanMapperTests {
@Autowired
TeachplanMapper teachplanMapper;
@Test
void testSelectTreeNodes() {
List<TeachplanDto> teachplanDtos = teachplanMapper.selectTreeNodes(117L);
System.out.println(teachplanDtos);
}
}
Service开发
定义service接口
public interface TeachplanService {
/**
* 查询课程计划树型结构
* @param courseId 课程id
* @return 课程计划
*/
public List<TeachplanDto> findTeachplanTree(long courseId);
}
定义service接口实现类
public class TeachplanServiceImpl implements TeachplanService {
@Autowired
TeachplanMapper teachplanMapper;
/**
* 根据课程id查询课程计划
* @param courseId 课程id
* @return
*/
@Override
public List<TeachplanDto> findTeachplanTree(long courseId) {
return teachplanMapper.selectTreeNodes(courseId);
}
}
Controller开发
调用service完成功能
/**
* @description 课程计划管理接口
* @author Mr.M
* @date 2022/9/6 11:29
* @version 1.0
*/
@Api(value = "课程计划管理接口",tags = "课程计划管理接口")
@RestController
public class TeachplanController {
@Autowired
TeachplanService teachplanService;
@ApiOperation("查询课程计划树形结构")
@ApiImplicitParam(value = "courseId",name = "课程Id",required = true,dataType = "Long",paramType = "path")
@GetMapping("/teachplan/{courseId}/tree-nodes")
public List<TeachplanDto> getTreeNodes(@PathVariable Long courseId){
return teachplanService.findTeachplanTree(courseId);
}
}
使用httpclient测试
### 查询某个课程的课程计划
GET {{content_host}}/content/teachplan/74/tree-nodes
前后端联调
课程计划新增
业务分析
进入课程计划界面
- 点击“添加章”新增第一级课程计划。
新增成功自动刷新课程计划列表。
- 点击“添加小节”向某个第一级课程计划下添加小节。
新增成功自动刷新课程计划列表。
新增的课程计划自动排序到最后。
- 点击“章”、“节”的名称,可以修改名称、选择是否免费。
数据模型: 定义SaveTeachplanDto, 用来接收请求的参数
- 新增第一级课程计划
- 名称默认为:新章名称 [点击修改]
- grade:1
- orderby: 所属课程中同级别下排在最后
- 新增第二级课程计划
- 名称默认为:新小节名称 [点击修改]
- grade:2
- orderby: 所属课程计划中排在最后
- 修改第一级、第二级课程计划的名称,修改第二级课程计划是否免费
package com.xuecheng.content.model.dto;
/**
* 新增大章节, 小章节, 修改章节的信息
*/
@Data
@ToString
public class SaveTeachplanDto {
/***
* 教学计划id
*/
private Long id;
/**
* 课程计划名称
*/
private String pname;
/**
* 课程计划父级Id
*/
private Long parentid;
/**
* 层级,分为1、2、3级
*/
private Integer grade;
/**
* 课程类型:1视频、2文档
*/
private String mediaType;
/**
* 课程标识
*/
private Long courseId;
/**
* 课程发布标识
*/
private Long coursePubId;
/**
* 是否支持试学或预览(试看)
*/
private String isPreview;
}
接口设计
### 新增课程计划--章,当grade为1时parentid为0
POST /teachplan
Content-Type: application/json
{
"courseId" : 74,
"parentid": 0,
"grade" : 1,
"pname" : "新章名称 [点击修改]"
}
### 新增课程计划--节
POST {{content_host}}/content/teachplan
Content-Type: application/json
{
"courseId" : 74,
"parentid": 247,
"grade" : 2,
"pname" : "小节名称 [点击修改]"
}
- 同一个接口接收新增和修改两个业务请求,以是否传递课程计划id 来判断是新增还是修改。
- 如果传递了课程计划id说明当前是要修改该课程计划,否则是新增一个课程计划。
接口定义
package com.xuecheng.content.api;
/**
* @description 课程计划管理接口
* @author Mr.M
* @date 2022/9/6 11:29
* @version 1.0
*/
@Api(value = "课程计划管理接口",tags = "课程计划管理接口")
@RestController
public class TeachplanController {
@ApiOperation("课程计划创建或修改")
@PostMapping("/teachplan")
public void saveTeachplan( @RequestBody SaveTeachplanDto teachplan){
}
}
Service开发
定义保存课程计划的Service接口。
/**
* 课程计划管理接口
*/
public interface TeachplanService {
/**
* @description 添加/修改课程计划
* @param teachplanDto 课程计划信息
* @return void
* @author Mr.M
* @date 2022/9/9 13:39
*/
public void saveTeachplan(SaveTeachplanDto teachplanDto);
}
编写接口实现:
public class TeachplanServiceImpl implements TeachplanService {
@Autowired
TeachplanMapper teachplanMapper;
@Transactional
@Override
public void saveTeachplan(SaveTeachplanDto teachplanDto) {
//课程计划id
Long id = teachplanDto.getId();
//修改课程计划
if(id!=null){
Teachplan teachplan = teachplanMapper.selectById(id);
BeanUtils.copyProperties(teachplanDto,teachplan);
teachplanMapper.updateById(teachplan);
}else{
//取出同父同级别的课程计划数量
int count = getTeachplanCount(teachplanDto.getCourseId(), teachplanDto.getParentid());
Teachplan teachplanNew = new Teachplan();
//设置排序号
teachplanNew.setOrderby(count+1);
BeanUtils.copyProperties(teachplanDto,teachplanNew);
teachplanMapper.insert(teachplanNew);
}
}
/**
* @description 获取最新的排序号
* @param courseId 课程id
* @param parentId 父课程计划id
* @return int 最新排序号
* @author Mr.M
* @date 2022/9/9 13:43
*/
private int getTeachplanCount(long courseId,long parentId){
LambdaQueryWrapper<Teachplan> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Teachplan::getCourseId,courseId);
queryWrapper.eq(Teachplan::getParentid,parentId);
Integer count = teachplanMapper.selectCount(queryWrapper);
return count;
}
}
Controller完善
controller中调用service方法
/**
* @description 课程计划管理接口
* @author Mr.M
* @date 2022/9/6 11:29
* @version 1.0
*/
@Api(value = "课程计划管理接口",tags = "课程计划管理接口")
@RestController
public class TeachplanController {
@Autowired
TeachplanService teachplanService;
@ApiOperation("课程计划创建或修改")
@PostMapping("/teachplan")
public void saveTeachplan( @RequestBody SaveTeachplanDto teachplan){
teachplanService.saveTeachplan(teachplan);
}
}
前端端联调
添加大章节, 添加小章节, 修改大章节, 修改小章节