1.微服务异常处理

1. 异常信息枚举类的错误代码规范

ContentErrorCode编写过程:
1.创建一个枚举类并实现接口Errorcode
2.对Errorcode的方法进行实现
3.需要在改枚举类中声明出两个属性
code、desc
4.构建出该枚举类的构造方法
需要将定义全参的构造方法
5.声明枚举的信息
E_xxxxxx(6位错误代码)
前两位:服务标识
中间两位:模块标识
后两位:异常标识

PS:在getcode和getdesc的方法返回值中将code和desc传出
枚举类是一个Java类的多例形式:构建改对象的常量信息
底层会new出这个类对象,声明常量—一个枚举类相当于一个Java对象
new ContentErrorCode(12001,”保存课程基本信息失败”)

内容管理服务前两位错误服务标识为:12(在 {@link ErrorCode} 中定义)
模块错误代码定义如下:

  • 课程基本—定义模块代码为: 00

  • 课程营销—定义模块代码为: 01

  • 课程发布—定义模块代码为: 02

  • 课程下线—定义模块代码为: 03

  • 课程计划—定义模块代码为: 04

  • 课程教师—定义模块代码为: 05
  • 2.异常信息类的结构

    image.png
    image.png

    3.自定义异常类BusinessException的声明特点

    这里可以传任意实现ErrorCode接口的类或对象====多态
    设计模式:适配者模式,所体现的形式就是多态

    1. 定义异常信息类 ErrorCode 作为属性。
    2. 自定义异常继承 RuntimeException ,这样代码中不会被污染(不需要try-catch)。

    4.全局异常处理流程

    对于异常主要分两大类处理:

    • 可预知的异常由程序员在代码中主动抛出,全局异常处理类统一捕获可预知异常主要是指的业务异常信息,在代码中手动抛出定义的业务异常信息,,程序员在抛出时会指定错误代码及错误信息。
    • 不可预知的异常(运行时异常)由统一捕获Exception类型的异常。不可预知异常通常是由于系统出现bug、或一些不要抗拒的错误(比如网络中断、服务器宕机等),异常类型为RuntimeException类型(运行时异常)。

    image.png
    处理流程解释:
    1、在controller、service、dao中程序员抛出可预知异常类型,微服务框架抛出不可预知异常类型。
    2、统一由异常捕获类捕获异常,并进行处理。
    3、捕获到可预知异常则直接取出ErrorCode错误代码及错误信息,响应给用户。
    4、捕获到微服务框架中不可预知异常,错误代码则统一为99999错误代码并响应给用户。
    5、将错误代码及错误信息以Json格式响应给用户。
    //NullPointerException——RuntimeException—-Exception
    NullPointerException 向上匹配:项目中没有定义RuntimeException,再向上匹配找到项目中定义的Exception

    全局异常处理的实现方式

    从 Spring 3.0 - Spring 3.2 版本之间,对 Spring 架构和 SpringMVC 的Controller 的异常捕获提供了相应的异常处理。

    • @ExceptionHandler Spring3.0提供的标识在方法上或类上的注解,用来表明方法的处理异常类型。
      • 方法上声明捕获指定异常类型,表明方法的处理异常类型
    • @ControllerAdvice Spring3.2提供的新注解,从名字上可以看出大体意思是控制器增强, 在项目中来增强SpringMVC中的Controller。通常和@ExceptionHandler 结合使用,来处理SpringMVC的异常信息。
      • 通知类,SpringAop,面向切面编程,类上加上这个注解,可以在不影响Controller的基础上进行增强(执行前后)在
    • @ResponseStatus Spring3.0提供的标识在方法上或类上的注解,用状态代码和应返回的原因标记方法或异常类。调用处理程序方法时,状态代码将应用于HTTP响应。
      • 指定错误的响应状态码

        2.课程计划的查询

        1.内存中生成树形结构数据

        树形结构查询生成:
        1.sql语句+mybatis的封装(方式一)
        sql要进行自关联查询:树形结构有几级就需要查询几次
        mybatis的封装:resultMap来封装数据-标签 collection
        适用场景:使用一张表数据量不大,并且表数据能不易更改
        2.通过sql查询出一个数据的所有内容,然后在程序中通过代码来生成树形结构(方式二)
        sql查询:将关联数据全部查询(包含树形结构中的所有内容-list)
        程序要通过逻辑来实现树形结构的生成:遍历list来生成
        适用场景:使用于表数据量很大,而且数据经常变更
        对于数据库的自关联查询数据库表的数据量不大,且不易变化,如果自表三级关联的表数据经常变化且数据量大,那么自关联SQL查询效率会灰常的低,原因在于:
        1.自关联的连接查询可以针对小表进行查询。
        2.自连接查询会进行笛卡尔全集,(笛卡尔积:两张表行数的乘积)所以自连接查询出来的记录会是此表数据量的平方。
        课程计划(课程大纲)数据经常性的变化且数据量大,不适合使用自关联 SQL 语句查询。本次我们将通过后端代码查询出课程下的课程计划(课程大纲)数据,并通过递归方法来构建出课程计划的树形结构数据。具体步骤如下:
        1.根据课程 CourseBaseId 查询出相关的课程计划。
        2.使用 MP 的映射文件,将查询出的集合数据进行封装到 TeachplanNode 扩展类中。
        3.通过 Java 的递归方法将集合的树形结构生成。
        image.png

        2、课程计划查询接口定义

        image.png

        3、课程计划查询分析

        /
        业务分析:
        1.判断关键数据
        courseBaseId companyId
        2.判断业务数据
        课程基础信息
        判断是否存在
        判断是否是同一家机构
        判断课程是否删除
        3.根据课程id查询一门课程下的所有课程计划-LIst
        4.通过Java的递归来生成课程计划的树形结构
        .生成树形结构转为dto返回给前端
        */

        4.需求分析:
        课程计划表结构

        image.png
        2.树形表结构说明
        课程计划采用树形结构来表示一个课程下的整个课程章节信息。树形结构一共分为三级,一级的为根级,一个课程对应一条根级课程计划数据,其他两级有多条。树形结构关系仅靠 Id 和 parentId 来表示,三级数据都在一张表中,所有需要使用表的自关联来完成三级菜单数据显示。
        课程计划关联关系
        image.png
        内存中生成树形结构数据
        树形结构查询生成:
        1.sql语句+mybatis的封装(方式一)
        sql要进行自关联查询:树形结构有几级就需要查询几次
        mybatis的封装:resultMap来封装数据-标签 collection
        适用场景:使用一张表数据量不大,并且表数据能不易更改
        2.通过sql查询出一个数据的所有内容,然后在程序中通过代码来生成树形结构(方式二)
        sql查询:将关联数据全部查询(包含树形结构中的所有内容-list)
        程序要通过逻辑来实现树形结构的生成:遍历list来生成
        适用场景:使用于表数据量很大,而且数据经常变更
        对于数据库的自关联查询数据库表的数据量不大,且不易变化,如果自表三级关联的表数据经常变化且数据量大,那么自关联SQL查询效率会灰常的低,原因在于:
        1.自关联的连接查询可以针对小表进行查询。
        2.自连接查询会进行笛卡尔全集,(笛卡尔积:两张表行数的乘积)所以自连接查询出来的记录会是此表数据量的平方。
        课程计划(课程大纲)数据经常性的变化且数据量大,不适合使用自关联 SQL 语句查询。本次我们将通过后端代码查询出课程下的课程计划(课程大纲)数据,并通过递归方法来构建出课程计划的树形结构数据。具体步骤如下:
        1.根据课程 CourseBaseId 查询出相关的课程计划。
        2.使用 MP 的映射文件,将查询出的集合数据进行封装到 TeachplanNode 扩展类中。
        3.通过 Java 的递归方法将集合的树形结构生成。

        3. 课程计划信息创建和修改

        后端对于业务的分析:
        1.添加和修改操作的区别
        添加:没有teachplanid
        修改:有teachplanid
        2.添加2级和3级数据的区别
        2级:没有parentid
        3级:有parentid.
        一个课程只有一个一级课程计划
        (2)添加数据时:
        1.课程计划的一级(根级)
        ●数据来源:课程基础信息(CourseBase)
        ●创建时机:在第一次添加二级数据时自动生成
        ●注意事项:一级父级(根级)父级 ID 值为 0
        2.课程计划的二级和三级节点
        ●数据来源:教育机构人员在页面填写的数据
        ●创建时机:教育机构人员提交课程计划数据
        ●注意事项:添加时要校验父级菜单是否存在
        ○二级在添加时
        ■父级不存在(一级):自动创建并获得获得父级Id值
        ■父级存在(一级):获得父级Id值
        ○三级在添加时
        ■父级不存在(二级):抛出异常
        ■父级存在(二级):获得父级Id值
        (3)修改数据时:
        1.要判断要修改的课程计划是否有 ID 值
        课程计划在进行修改时,必须要有 ID 值,有 ID 值说明已经在数据库中存在该数据。
        2.判断关键数据是否存在
        对关键的业务数据进行判断操作,如:课程计划名称、父级 Id 值等。

    课程计划创建和修改接口定义

    image.png

    业务分析:

    主方法:
    业务分析:
    0.是否开启事务
    1.判断业务操作
    teachplandto中是否id值
    如果有:修改
    如果没有:添加
    3.业务操作后返回结果内容
    *添加方法业务分析

    添加方法业务分析:

    1. 判断关键数据
    • courseID 课程id
    • pname 课程计划名称
    • companyID 教学机构id
    • parentId(后端业务维护即可,此值必须赋值)根据父级数据赋值
    • grade等级(后端业务维护即可,此值必须赋值)根据父级数据赋值
    1. 判断业务数据
    • 课程基础信息
      • 判断是否存在
      • 判断是否是同一家教学机构
      • 判断是否删除
      • 判断审核状态:未提交、审核未通过 ,才可以编辑课程计划
    • 课程计划
      • 判断是否存在
      • 判断等级:教学机构只能操作二级和三级课程计划
    1. 获得添加课程计划的父级数据
    • 添加课程计划中要依据父级数据进行赋值
      • 添加课程计划的parentID = 父级.id
      • 添加课程计划的grade =父级.grade+1
      • 添加课程计划的orderby =父级的子级数据的个数+1 select count(*) from teachplan where parentid = ?
    1. 保存课程计划数据并判断保存后的结果
    2. 返回数据库最新的数据dto

      修改方法业务分析

      业务分析:

    3. 判断关键数据

    • 课程id
    • 课程计划名称
    • 父级id
    • 是否免费
    • 教学机构id
    1. 判断业务数据
    • 课程基础信息
      • 判断是否存在
    • 判断是否是同一家教学机构
      • 判断是否删除
      • 判断审核状态:未提交、审核未通过
    • 课程计划
      • 判断是否存在
      • 判断等级:教学机构只能操作二级和三级课程计划
    1. 将dto转为po并保存

    为了数据的安全性,只让前端修改字段:
    课程计划名称、是否免费、开始时间、结束时间

    1. 将数据库最新的数据返回给前端(dto)
    1. package com.xuecheng.content.service.impl;
    2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    3. import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
    4. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    5. import com.xuecheng.api.content.model.dto.CourseBaseDTO;
    6. import com.xuecheng.api.content.model.dto.TeachplanDTO;
    7. import com.xuecheng.common.domain.code.CommonErrorCode;
    8. import com.xuecheng.common.enums.content.CourseAuditEnum;
    9. import com.xuecheng.common.enums.content.CourseModeEnum;
    10. import com.xuecheng.common.enums.content.TeachPlanEnum;
    11. import com.xuecheng.common.exception.ExceptionCast;
    12. import com.xuecheng.common.util.StringUtil;
    13. import com.xuecheng.content.common.constant.ContentErrorCode;
    14. import com.xuecheng.content.convert.TeachplanConvert;
    15. import com.xuecheng.content.entity.Teachplan;
    16. import com.xuecheng.content.entity.TeachplanMedia;
    17. import com.xuecheng.content.entity.ex.TeachplanNode;
    18. import com.xuecheng.content.mapper.TeachplanMapper;
    19. import com.xuecheng.content.service.CourseBaseService;
    20. import com.xuecheng.content.service.TeachplanMediaService;
    21. import com.xuecheng.content.service.TeachplanService;
    22. import lombok.extern.slf4j.Slf4j;
    23. import org.springframework.beans.factory.annotation.Autowired;
    24. import org.springframework.stereotype.Service;
    25. import org.springframework.transaction.annotation.Transactional;
    26. import org.springframework.util.CollectionUtils;
    27. import org.springframework.util.ObjectUtils;
    28. import org.springframework.util.StringUtils;
    29. import java.time.LocalDateTime;
    30. import java.util.ArrayList;
    31. import java.util.List;
    32. /**
    33. * <p>
    34. * 课程计划 服务实现类
    35. * </p>
    36. *
    37. * @author itcast
    38. */
    39. @Slf4j
    40. @Service
    41. public class TeachplanServiceImpl extends ServiceImpl<TeachplanMapper, Teachplan> implements TeachplanService {
    42. @Autowired
    43. private CourseBaseService courseBaseService;
    44. @Autowired
    45. private TeachplanMediaService teachplanMediaService;
    46. /*
    47. * 业务分析:
    48. * 0.是否开启事务
    49. * 1.判断业务操作
    50. * teachplandto中是否id值
    51. * 如果有:修改
    52. * 如果没有:添加
    53. * 3.业务操作后返回结果内容
    54. * */
    55. @Transactional
    56. public TeachplanDTO createOrModifyTeachPlan(TeachplanDTO dto, Long companyId) {
    57. // 1.判断业务操作
    58. // teachplandto中是否id值
    59. Long teachPlanId = dto.getTeachPlanId();
    60. TeachplanDTO resultDTO = null;
    61. if (ObjectUtils.isEmpty(teachPlanId)) {
    62. // 如果没有:添加
    63. resultDTO = createTeachplan(dto,companyId);
    64. } else {
    65. // 如果有:修改
    66. resultDTO = modifyTeachplan(dto,companyId);
    67. }
    68. // 3.业务操作后返回结果内容
    69. return resultDTO;
    70. }
    71. /*
    72. * 业务分析:
    73. * 1.判断关键数据
    74. * 课程id
    75. * 课程计划名称
    76. * 父级id
    77. * 是否免费
    78. * 教学机构id
    79. *
    80. * 2.判断业务数据
    81. * 课程基础信息
    82. * 判断是否存在
    83. * 判断是否是同一家教学机构
    84. * 判断是否删除
    85. * 判断审核状态:未提交、审核未通过
    86. * 课程计划
    87. * 判断是否存在
    88. * 判断等级:教学机构只能操作二级和三级课程计划
    89. * 3.将dto转为po并保存
    90. * 为了数据的安全性,只让前端修改字段:
    91. * 课程计划名称、是否免费、开始时间、结束时间
    92. *
    93. * 4.将数据库最新的数据返回给前端(dto)
    94. * */
    95. private TeachplanDTO modifyTeachplan(TeachplanDTO dto, Long companyId) {
    96. //1.判断关键数据
    97. // 课程id
    98. // 课程计划名称
    99. // 教学机构id
    100. // 父级id
    101. // 是否免费
    102. // 2.判断业务数据
    103. // 课程基础信息
    104. // 判断是否存在
    105. // 判断是否是同一家教学机构
    106. // 判断是否删除
    107. // 判断审核状态:未提交、审核未通过
    108. if (ObjectUtils.isEmpty(dto.getParentid())||
    109. StringUtil.isBlank(dto.getIsPreview())
    110. ) {
    111. ExceptionCast.cast(CommonErrorCode.E_100101);
    112. }
    113. CourseBaseDTO courseBase = verifyTeachplanMsg(dto, companyId);
    114. // 课程计划
    115. // 判断是否存在
    116. // 判断等级:教学机构只能操作二级和三级课程计划
    117. Teachplan teachplan = this.getById(dto.getTeachPlanId());
    118. if (ObjectUtils.isEmpty(teachplan)) {
    119. ExceptionCast.cast(ContentErrorCode.E_120402);
    120. }
    121. Long parentid = teachplan.getParentid();
    122. if (TeachPlanEnum.FIRST_PARENTID_FLAG.equals(parentid)) {
    123. ExceptionCast.cast(ContentErrorCode.E_120417);
    124. }
    125. // 3.将课程计划信息保存
    126. // 为了数据的安全性,只让前端修改字段:
    127. // 课程计划名称、是否免费、开始时间、结束时间
    128. // update teachplan set pname = ? , is_preview = ? ,startime =? ,end=?,change_date = ? where id = ?
    129. // PS: LambdaUpdateWrapper使用了,mp的自动填充就会失效
    130. LambdaUpdateWrapper<Teachplan> updateWrapper = new LambdaUpdateWrapper<>();
    131. updateWrapper.set(Teachplan::getPname,dto.getPname());
    132. updateWrapper.set(Teachplan::getIsPreview,dto.getIsPreview());
    133. updateWrapper.set(Teachplan::getStartTime,dto.getStartTime());
    134. updateWrapper.set(Teachplan::getEndTime,dto.getEndTime());
    135. updateWrapper.set(Teachplan::getChangeDate, LocalDateTime.now());
    136. updateWrapper.eq(Teachplan::getId, dto.getTeachPlanId());
    137. boolean result = this.update(updateWrapper);
    138. if (!result) {
    139. ExceptionCast.cast(ContentErrorCode.E_120407);
    140. }
    141. // 4.将数据库最新的数据返回给前端(dto)
    142. Teachplan po = this.getById(dto.getTeachPlanId());
    143. TeachplanDTO resultDTO = TeachplanConvert.INSTANCE.entity2dto(po);
    144. return resultDTO;
    145. }
    146. private CourseBaseDTO verifyTeachplanMsg(TeachplanDTO dto, Long companyId) {
    147. //1.判断关键数据
    148. if (ObjectUtils.isEmpty(companyId)||
    149. StringUtil.isBlank(dto.getPname())||
    150. ObjectUtils.isEmpty(dto.getCourseId())
    151. ) {
    152. ExceptionCast.cast(CommonErrorCode.E_100101);
    153. }
    154. Long courseId = dto.getCourseId();
    155. // 2.判断业务数据
    156. return getCourseAndVerify(companyId, courseId);
    157. }
    158. /*
    159. * 业务分析:
    160. * 1.判断关键数据
    161. * pname grade courseid
    162. * 2.判断业务数据
    163. * 课程基础信息
    164. * 判断是否存在
    165. * 判断是否是同一家机构
    166. * 判断是否删除
    167. * 判断审核状态(未提交、审核未通过)
    168. *
    169. * 3.获得父级数据
    170. * 添加课程计划中要依据父级数据进行赋值
    171. * 添加课程计划的parentid = 父级数据id
    172. * 添加课程计划的grade = 父级数据grade+1
    173. * 添加课程计划的orderby = 父级数据所有子级数据的个数+1
    174. *
    175. * 4.保存课程计划数据并判断保存后的结果
    176. *
    177. * */
    178. private TeachplanDTO createTeachplan(TeachplanDTO dto, Long companyId) {
    179. //1.判断关键数据
    180. // pname grade courseid
    181. if (StringUtils.isEmpty(dto.getPname())||
    182. ObjectUtils.isEmpty(dto.getGrade())||
    183. ObjectUtils.isEmpty(dto.getCourseId())
    184. ) {
    185. ExceptionCast.cast(CommonErrorCode.E_100101);
    186. }
    187. // 2.判断业务数据
    188. // 课程基础信息
    189. // 判断是否存在
    190. // 判断是否是同一家机构
    191. // 判断是否删除
    192. // 判断审核状态(未提交、审核未通过)
    193. CourseBaseDTO courseBase = getCourseAndVerify(companyId, dto.getCourseId());
    194. // 3.获得父级数据
    195. // 添加课程计划中要依据父级数据进行赋值
    196. // 添加课程计划的parentid = 父级数据id
    197. // 添加课程计划的grade = 父级数据grade+1
    198. // 添加课程计划的orderby = 父级数据所有子级数据的个数+1
    199. Teachplan parentNode = getParentNode(courseBase,dto);
    200. dto.setGrade(parentNode.getGrade()+1);
    201. dto.setParentid(parentNode.getId());
    202. // select count(*) as ncount from teachplan where parentId = ?
    203. LambdaQueryWrapper<Teachplan> queryWrapper = new LambdaQueryWrapper<>();
    204. queryWrapper.eq(Teachplan::getParentid,parentNode.getId() );
    205. int count = this.count(queryWrapper);
    206. dto.setOrderby(count+1);
    207. // 4.保存课程计划数据并判断保存后的结果
    208. Teachplan teachplan = TeachplanConvert.INSTANCE.dto2entity(dto);
    209. boolean result = this.save(teachplan);
    210. if (!result) {
    211. ExceptionCast.cast(ContentErrorCode.E_120407);
    212. }
    213. Teachplan po = this.getById(teachplan.getId());
    214. TeachplanDTO resultDTO = TeachplanConvert.INSTANCE.entity2dto(po);
    215. return resultDTO;
    216. }
    217. /*
    218. * 业务分析:
    219. * 1.判断添加的课程计划是几级数据
    220. * 前端只能操作:2级和3级
    221. * 区分:三级课程计划添加时需要携带parentid
    222. * 2.获得父级数据
    223. * 获得二级
    224. * 如果获得父级数据不存在:自动在后端创建
    225. * 如果获得父级数据存在:直接返回
    226. * 获得三级
    227. * 如果获得父级数据不存在:直接抛出异常
    228. * 如果获得父级数据存在:直接返回
    229. * */
    230. private Teachplan getParentNode(CourseBaseDTO courseBase, TeachplanDTO dto) {
    231. //1.判断添加的课程计划是几级数据
    232. // 前端只能操作:2级和3级
    233. // 区分:三级课程计划添加时需要携带parentid
    234. // 2.获得父级数据
    235. if (ObjectUtils.isEmpty(dto.getParentid())) {
    236. // 获得二级
    237. // 如果获得父级数据不存在:自动在后端创建
    238. // 如果获得父级数据存在:直接返回
    239. // select * from teachplan where courseid = ? and parentid = 0 and grade = 1
    240. LambdaQueryWrapper<Teachplan> queryWrapper = new LambdaQueryWrapper<>();
    241. queryWrapper.eq(Teachplan::getParentid, TeachPlanEnum.FIRST_PARENTID_FLAG);
    242. queryWrapper.eq(Teachplan::getCourseId, courseBase.getCourseBaseId());
    243. Teachplan rootNode = this.getOne(queryWrapper);
    244. if (ObjectUtils.isEmpty(rootNode)) {
    245. rootNode = new Teachplan();
    246. rootNode.setPname(courseBase.getName());
    247. rootNode.setParentid(new Long(TeachPlanEnum.FIRST_PARENTID_FLAG));
    248. rootNode.setGrade(TeachPlanEnum.FIRST_LEVEL);
    249. // rootNode.setDescription(courseBase.getDescription());
    250. rootNode.setOrderby(TeachPlanEnum.FIRST_LEVEL);
    251. rootNode.setCourseId(courseBase.getCourseBaseId());
    252. boolean result = this.save(rootNode);
    253. if (!result) {
    254. ExceptionCast.cast(ContentErrorCode.E_120407);
    255. }
    256. }
    257. return rootNode;
    258. } else {
    259. // 获得三级
    260. // 如果获得父级数据不存在:直接抛出异常
    261. // 如果获得父级数据存在:直接返回
    262. // select * from teachplan where parentid = ?
    263. LambdaQueryWrapper<Teachplan> queryWrapper = new LambdaQueryWrapper<>();
    264. queryWrapper.eq(Teachplan::getId, dto.getParentid());
    265. Teachplan parentNode = this.getOne(queryWrapper);
    266. if (ObjectUtils.isEmpty(parentNode)) {
    267. ExceptionCast.cast(ContentErrorCode.E_120408);
    268. }
    269. return parentNode;
    270. }
    271. }
    272. private CourseBaseDTO getCourseAndVerify(Long companyId, Long courseId) {
    273. // 课程基础信息
    274. // 判断是否存在
    275. // 判断是否同一家机构
    276. // 判断是否删除
    277. // 判断课程基础信息审核状态:未提交和审核未通过
    278. CourseBaseDTO courseBase = courseBaseService.getCourseById(courseId, companyId);
    279. String auditStatus = courseBase.getAuditStatus();
    280. if (CourseAuditEnum.AUDIT_PASTED_STATUS.getCode().equals(auditStatus)||
    281. CourseAuditEnum.AUDIT_COMMIT_STATUS.getCode().equals(auditStatus)||
    282. CourseAuditEnum.AUDIT_PUBLISHED_STATUS.getCode().equals(auditStatus)
    283. ) {
    284. ExceptionCast.cast(ContentErrorCode.E_120015);
    285. }
    286. return courseBase;
    287. }
    288. }

    测试注意 parentid

    1. {
    2. "pname":"测试第一个小节",
    3. "courseId": 47,
    4. "parentid" : 99
    5. }