今日重点
=============================
数据字典:
系统中不易更改且维护在后端的数据 系统常量数据
全局异常处理器 :
不设置异常只要后端系统抛出异常,前端永远接收为500 不知道具体是什么原因导致的错误!
框架级别的错误:提示服务器正在繁忙,后端日志打印信息!
微服务层的错误:尽量提示细的错误信息,让前端客户能知道是什么异常!
全局异常信息处理:
公共异常信息:
公用的异常信息提示,比如传入参数异常,压根都到不了微服务中就报错了,这种就可以使用公共异常信息!
业务模块异常信息:
提供自己为服务模块中抛出的异常信息,具体到每个细小的原因介绍!
GlobalEx全局异常处理器:
处理 Exception(系统框架异常) BusinessException(微服务业务异常)
微服务主启动类主键中添加( scanBasePackages={“异常信息的包路径”,“自己启动类的包路径”} )
前提需要引入基本服务的jar包依赖!
自定义异常信息处理:
- 处理业务异常数据
- 1.接受业务异常信息
- 定义异常信息属性
- 2.异常信息不能影响核心代码内容
- 自定义异常继承 {@link RuntimeException} ,这样代码中不会被污染(不需要try-catch)
自定义异常以ErrorCode作为属性,自定义异常类继承异常类,向上匹配,无侵性,ExceptionCast.cast()
微服务异常信息类:
服务名+ErrorCode(类名) :
枚举类是一个常量类,构造方法为私有的,定义了大量对象信息并对属性进行赋值,不让外面调用方进行修改!枚举类是java中的多例模式
实现ErrorCode接口
实现方法
定义属性
code错误代码 desc错误信息
定义枚举信息
定义规范: E错误信息(错误代码错误信息)
本项目错误代码6位:
服务标识(2) 模块标识(2) 异常标识(2)
内容管理:12 课程基本信息:00 自己定义
E_120000(120000,课程名称不能为空)
E_120001(120001,课程大分类不能为空)
重写get方法需要改造: 否则获取枚举信息将为null值!
return this.code;
retuen this.desc;
全局异常处理实现:
@Exceptionhandler 用来表明方法的处理异常信息 @ContorllerAdvice 控制器的增强 AOP(思想:代码无侵入性)在项目中增强springmvc中的异常体现! @ResponseStatus 方法和类上的注解, 用状态代码和返回的原因标记方法或者异常类! BUSEX EXCEP 俩个异常类型都匹配不到的话,Spring底层就会向上匹配找寻父类异常,直到找到匹配的EXCEP处理! 由于系统框架异常是不能修改底层源码的,所以对于系统框架异常只要有捕获到抛出异常,一律响应 99999 服务器正在繁忙
业务异常信息类的声明
全局异常处理流程
课程计划表结构设计:
id 创建时间 修改时间 排序字段 是否支持试学
一张表内,用等级进行区分一级的主键ID 与二级的parentId相同,实现一种自关联模式!
使用java中继承解决 在不影响po类属性对于数据库表字段的情况下,还要给前端显示树型状的回显信息! 业务分析: 查询不需要开启事务 判断关键数据 CoursebaseID companyid
判断业务数据 是否存在 是否属于同一家机构 审核状态不需要判断只是查询 是否删除
获得课程下的所有课程计划 生成树形结构返回
课程计划添加和修改:
===添加与修改俩个业务后端是共用一个接口实现业务===
1.区别课程计划的业务操作: 添加 是不带teachplanid 添加是在根目录下创建的所有没有必要带着id 修改 是 带teachplanid 而修改是需要知道属于那一个根目录所以需要带着id
2.区别课程计划的等级:
添加章节 没有taechplanid和parentid
章节是添加在根目录下的所以不需要由前端指定parentid和teachplanid
添加小节 没有teachplanid但是有parentid
添加小节需要指定添加到哪一个二级目录下 所以需要带着父级也就是章节的id,用来做区分! 需要指定parentid 由于一个课程计划只会有一个一级根目录,所有一级根目录是由后端维护的,所有添加的课程章节或者是小节都是添加在此根目录上的,前端不会显示此根目录的id!
课程计划的添加业务分析:
1. 需要开启事务支持
2. 判断业务操作
teachplandto中是否id值
如果有是: 修改 没有: 就是添加
3. 业务操作后返回结果内容
主方法内不写具体实现,只判断是添加还要修改 创建单独俩个的方法进行具体实现!
创建方法: 业务分析
1.VO关键数据判断 pname grade(等级) courseid
2.业务数据判断 是否存在 是否同一家机构 判断是否已经删除 判断审核状态(未提交,审核未通过)
3.由于是添加 所以要获得父级数据 如果是添加二级数据 创建或者带一个跟目录id 如果是添加三级数据 需要有二级的id
课程计划结构示意图
==============================
学习目标
1熟悉微服务异常处理结构
2熟悉异常信息类的结构并能够在业务服务中定义
3熟悉自定义异常类的结构
4熟悉项目的全局异常处理流程
5能够使用SpringMVC的注解实现全局异常处理
6熟悉课程计划查询的业务需求
7能够根据文档定义程计划查询接口
8能够使用Java的递归查询课程计划树形机构
9熟悉课程计划新增的业务需求
10能够根据文档定义程计划新增接口
11能够完成新增课程计划的功能实现
12能够根据文档定义课程计划删除接口
13能够完成新删除课程计划的功能实现
1.微服务异常处理(背)
学成在线微服务的业务操作中,我们需要对操作的数据进行业务判断。如果有问题,中断业务操作并且返回错数据,错误的详细信息需要告诉前端,前端就可以对错误数据进行相应的处理。微服务端对错误的数据需要进行异常的抛出,还需要对错误数据进行统一处理,并不是将异常信息交给 JVM 处理。
下面我们需要对之前的代码加以分析,并使用现有的框架进行统一异常的处理,构建出错误的标准信息并响应给调用者,如下图:
微服务异常流程
根据上面的示例图,我们要在此章节中对一下运行流程给出解决具体方案:
异常处理结构图
1.定义异常信息类。
2.定义自定义异常。
3.创建统一处理异常类。
1.1 业错误数据处理
在之前对课程基本信息进行 ‘增、删、改’ 操作时,我们都需要对传入的数据进行关键业务数据判断。如果数据有问题,需要将具体错误信息返回给调用者信息。
在代码中,可以使用 Java 中的异常来实现其功能,下面将详细讲解异常处理的方式内容。
1.1.1 业务异常信息类的声明
通过上面业务异常类的声明,我们已经确定标准的异常信息接口 ErrorCode 类。现要对各个业务模块和各个模块中的公共的异常信息进行定义,这样就解决异常信息在业务代码中写死,也为各个业务模块异常信息进行统一管理便于代码的维护。
各个模块的异常信息都需要按照标准的异常信息接口 ErrorCode 类作为基准来去定义业务模块异常信息类。
异常信息类的结构
这里我们需要使用枚举来实现对业务模块异常信息类的实现。下面是对公共的异常信息和内容管理模块异常信息类的定义:<br />●异常信息接口(项目中已经存在,无需创建) 在 xc-common 中定义业务异常信息数据为:
package com.xuecheng.common.domain.code;
/**
* <p>
* 微服务业务异常编码规范接口
* </p>
*/
public interface ErrorCode {
//错误代码
int getCode();
//错误信息
String getDesc();
}
●内容管理异常信息类 内容管理异常信息类属于内容管理模块下的,所有需要定义在 xc-content-service
package com.xuecheng.content.common.constant;
import com.xuecheng.common.domain.code.ErrorCode;
/**
* 内容管理服务业务异常枚举类
* <hr>
* 后四位错误代码由开发者开发中进行定义<br>
* 内容管理服务前两位错误服务标识为:12(在 {@link ErrorCode} 中定义)
* 模块错误代码定义如下:
* <ul>
* <li>课程基本--定义模块代码为: 00</li>
* <li>课程营销--定义模块代码为: 01</li>
* <li>课程发布--定义模块代码为: 02</li>
* <li>课程下线--定义模块代码为: 03</li>
* <li>课程计划--定义模块代码为: 04</li>
* <li>课程教师--定义模块代码为: 05</li>
* </ul>
* @Description: 定义内容管理中的业务异常代码
*/
public enum ContentErrorCode implements ErrorCode {
E_120001(120001, "保存课程基本信息失败"),
E_120002(120002, "保存课程基本信息时,课程分类的分类二级分类信息不能空"),
E_120003(120003, "保存课程基本信息时,课程分类的分类三级分类信息不能空"),
E_120004(120004, "保存课程基本信息时,课程名称不能空"),
E_120005(120005, "保存课程基本信息时,学习模式不能为空"),
E_120006(120006, "保存课程基本信息时,教学模式不能为空"),
E_120007(120007, "保存课程基本信息时,课程等级不能为空"),
E_120008(120008, "保存的课程内容已存在,无法保存"),
E_120009(120009, "修改数据的ID值不能为空"),
E_120010(120010, "修改数据不存在"),
E_120011(120011, "修改课程数据已发布或审核通过,无法修改"),
E_120012(120012, "删除课程数据中存在已发布或审核通过,无法删除"),
E_120013(120013, "课程信息不存在"),
E_120014(120014, "课程数据已发布或审核通过,无法操作"),
E_120015(120015, "课程审核状态异常"),
E_120016(120016, "审核的状态是能为:审核通过或审核未通过"),
E_120017(120017, "操作课程数据失败"),
E_120018(120018, "公司id不能为空"),
E_120019(120019, "课程使用人群不能为空"),
E_120020(120020, "课程的收费模式不能为空"),
E_120021(120021, "课程信息已经删除"),
E_120022(120022, "发送远端请求获得文件上传凭证失败"),
E_120023(120023, "审核课程数据异常"),
E_120101(120101, "查询的课程营销信息不存在"),
E_120102(120102, "要操作的课程营销信息不能为空"),
E_120103(120103, "要操作的课程营销信息找不到对应的课程信息"),
E_120104(120104, "课程营销信息中的收费标准不能为空"),
E_120105(120105, "课程营销信息中的有效性不能为空"),
E_120106(120106, "课程营销信息中的过期时间不能为空"),
E_120107(120107, "课程营销操作失败"),
E_120108(120108, "课程营销关联的课程已被关联"),
E_120109(120109, "课程营销关联的课程信息不能为空"),
E_120201(120201, "课程没有审核通过,无法发布"),
E_120202(120202, "课程发布时发送事务消息失败"),
E_120203(120203, "课程发布消息不存在"),
E_120204(120204, "课程详情页发布失败"),
E_120205(120205, "课程发布数据保存失败"),
E_120206(120206, "发布消息数据操作失败"),
E_120401(120401, "操作的课程计划信息不能为空"),
E_120402(120402, "课程计划信息不存在,无法操作"),
E_120403(120403, "操作的课程计划类型信息不能为空"),
E_120404(120404, "课程计划没有关联课程信息"),
E_120405(120405, "课程计划已经发布无法操作"),
E_120406(120406, "课程计划关联的课程信息不一致,无法操作"),
E_120407(120407, "操作课程计划信息失败"),
E_120408(120408, "课程计划无父级信息,无法操作"),
E_120409(120409, "课程计划信息还有子级信息,无法操作"),
E_120410(120410, "课程计划不是第三级,无法关联媒资信息"),
E_120411(120411, "课程计划和媒资关联失败"),
E_120412(120412, "课程计划已经删除,无法操作"),
E_120413(120413, "课程计划名称不能为空"),
E_120414(120414, "课程计划已经关联媒资信息,无法删除"),
E_120415(120415, "创建父级课程计划数据失败"),
E_120416(120416, "课程计划关联媒资信息必须审核通过"),
E_1200501(1200501, "课程教师信息操作失败"),
E_1200502(1200502, "课程不能填写相同名称的教师信息"),
E_1200503(1200503, "课程教师信息不存在"),
E_1200601(1200601, "获得上传凭证失败,请稍后再试"),
;
private int code;
private String desc;
ContentErrorCode(int code, String desc) {
this.code = code;
this.desc = desc;
}
public int getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
●公共异常信息类(项目中已经存在,无需创建) 公共异常信息类被各个业务模块公用,所有需要定义在 xc-common 下
package com.xuecheng.common.domain.code;
/**
* 前两位:服务标识
* 中间两位:模块标识
* 后两位:异常标识
*/
public enum CommonErrorCode implements ErrorCode {
////////////////////////////////////公用异常编码 //////////////////////////
SUCCESS(0, "成功"),
FUSE(-1, "网关调用熔断"),
/**
* 传入参数与接口不匹配
*/
E_100101(100101,"传入参数与接口不匹配"),
/**
* 验证码错误
*/
E_100102(100102,"验证码错误"),
/**
* 验证码为空
*/
E_100103(100103,"验证码为空"),
/**
* 查询结果为空
*/
E_100104(100104,"查询结果为空"),
/**
* ID格式不正确或超出Long存储范围
*/
E_100105(100105,"ID格式不正确或超出Long存储范围"),
E_100106(100106,"请求失败"),
E_100107(100107,"请求的数据状态有误"),
E_100108(100108,"无权访问数据"),
E_100109(100109,"手机号格式不正确"),
E_100110(100110,"用户名为空"),
E_100111(100111,"密码为空"),
E_100112(100112,"手机号为空"),
E_100113(100113,"手机号已存在"),
E_100114(100114,"用户名已存在"),
E_100115(100115,"密码不正确"),
E_100116(100116,"传入对象为空"),
////////////////////////////////////SAAS服务异常编码110 //////////////////////////
E_110001(110001,"账号不存在"),
E_110002(110002,"角色编码在同一租户中已存在,不可重复"),
E_110003(110003,"角色为空"),
E_110004(110004,"角色已绑定账号,被使用中不可删除"),
E_110005(110005,"权限集合为空"),
E_110006(110006,"参数为空"),
E_110007(110007,"未查询到租户关联的角色"),
E_110008(110008,"账号被其他租户使用,不可删除"),
E_403000(403000,"你无权限访问"),
E_999980(999980,"调用微服务-检索服务 被熔断"),
E_999981(999981,"调用微服务-内容服务 被熔断"),
E_999990(999990,"调用微服务-交易中心 被熔断"),
E_999991(999991,"调用微服务-授权服务 被熔断"),
E_999992(999992,"调用微服务-用户服务 被熔断"),
E_999993(999993,"调用微服务-资源服务 被熔断"),
E_999994(999994,"调用微服务-同步服务 被熔断"),
E_999995(999995,"调用微服务-统一账户服务 被熔断"),
E_999996(999996,"调用微服务-存管代理服务 被熔断"),
/**
* 调用微服务-还款服务 被熔断
*/
E_999997(999997,"调用微服务-还款服务 被熔断"),
CUSTOM(999998,"自定义异常"),
/**
* 未知错误
*/
UNKOWN(999999,"未知错误");
private int code;
private String desc;
public int getCode() {
return code;
}
public String getDesc() {
return desc;
}
private CommonErrorCode(int code, String desc) {
this.code = code;
this.desc = desc;
}
public static CommonErrorCode setErrorCode(int code) {
for (CommonErrorCode errorCode : CommonErrorCode.values()) {
if (errorCode.getCode()==code) {
return errorCode;
}
}
return null;
}
}
上面的异常信息类声明主要对微服务应用运行流程中,业务异常信息的定义,需要对标准的异常信息接口 ErrorCode 类下创建实现类,其实现类使用枚举方式来对业务异常信息进行统一管理。
1.1.2 业务异常类的声明
通过来说,Java 终止方式继续运行,可以使用异常来进行终止。使用异常有一下方面的好处:
1. 通过异常可以有效的终止方法
2. 并且可以在声明异常时传入错误信息。
在项目中,对于业务数据判断,很多情况下都是使用异常来实现错误信息传递。
既然业务判断要抛出异常,对此异常在项目中的归类为业务异常。业务异常一般为后端开发者在进行业务数据判断时,对于不合法的数据信息主动抛出异常。项目中为标明为业务异常,所有我们需要首选创建自定义异常类。自定义异常信息要满足下面的要求:
1.接受业务异常信息 。
定义异常信息类 ErrorCode 作为属性。
2.异常信息不能影响核心代码内容。
自定义异常继承 {@link RuntimeException} ,这样代码中不会被污染(不需要try-catch)。
●定义业务异常(已经定义,无需声明)针对上面的要求,我们在 xc-common 中定义业务异常类为:
package com.xuecheng.common.exception;
import com.xuecheng.common.domain.code.ErrorCode;
/**
* 业务异常类信息
* 处理业务异常数据
* 1.接受业务异常信息
* 定义异常信息属性
* 2.异常信息不能影响核心代码内容
* 自定义异常继承 {@link RuntimeException} ,这样代码中不会被污染(不需要try-catch)
*/
public class BusinessException extends RuntimeException {
//定义异常信息属性
private ErrorCode errorCode;
/**
* 无参构造
*/
public BusinessException() {
super();
}
/**
* 异常信息属性构造方法
* @param errorCode {@link ErrorCode} 异常信息属性
*/
public BusinessException(ErrorCode errorCode) {
super();
this.errorCode = errorCode;
}
/**
* 异常信息属性和异常错误信息构造方法
* @param errorCode {@link ErrorCode} 异常信息属性
* @param exceptionMsg {@link String} 异常错误信息
*/
public BusinessException(ErrorCode errorCode, String exceptionMsg) {
super(exceptionMsg);
this.errorCode = errorCode;
}
/**
* 异常信息属性和异常对象构造方法
* @param errorCode {@link ErrorCode} 异常信息属性
* @param exception {@link Throwable} 异常对象信息
*/
public BusinessException(ErrorCode errorCode, Throwable exception) {
super(exception);
this.errorCode = errorCode;
}
/**
* 适配Rest服务业务异常信息的抛出
* @param code {@link String} 操作代码
* @param desc {@link String} 错误信息
*/
public BusinessException(int code,String desc) {
super();
//1.构建ErrorCode匿名内部类,并返回
ErrorCode errorCode = new ErrorCode() {
@Override
public int getCode() {
return code;
}
@Override
public String getDesc() {
return desc;
}
};
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() {
return errorCode;
}
public void setErrorCode(ErrorCode errorCode) {
this.errorCode = errorCode;
}
}
}
●异常抛出工具类(已经再项目中定义,无需再次定义) 为简化在service业务中对异常的抛出,在 xc-common 中定义异常抛出工具类:
package com.xuecheng.common.exception;
import com.xuecheng.common.domain.code.ErrorCode;
/**
* <p>异常抛出的工具类</p>
*
* @Description: 异常抛出的工具类,简化业务异常抛出代码
*/
public class ExceptionCast {
private ExceptionCast() {
}
/**
* 抛出业务异常类,并传入业务错误信息(枚举)
* @param errorCode {@link Enum} 业务错误信息
*/
public static void cast(ErrorCode errorCode){
throw new BusinessException(errorCode);
}
public static void cast(boolean expression, ErrorCode errorCode){
if (expression) {
throw new BusinessException(errorCode);
}
}
/**
* 抛出业务异常类,并传入业务错误信息(枚举)和异常信息
* @param errorCode {@link Enum} 业务错误信息
* @param throwable {@link Throwable} 异常信息
*/
public static void castWithException(ErrorCode errorCode,Throwable throwable){
throw new BusinessException(errorCode,throwable);
}
/**
* 抛出业务异常类,并传入业务错误信息(枚举)和异常信息(字符串信息)
* @param errorCode {@link Enum} 业务错误信息
* @param msg {@link String} 异常信息
*/
public static void castWithExceptionMsg(ErrorCode errorCode,String msg){
throw new BusinessException(errorCode,msg);
}
/**
* 适配Rest服务调用的业务异常信息抛出
* @param code
* @param desc
*/
public static void castWithCodeAndDesc(int code,String desc){
throw new BusinessException(code,desc);
}
}
上面的异常类声明主要对微服务应用运行流程中:
1.业务层对错误业务数据进行异常抛出类 BusinessException 声明。
2.标准的异常信息接口 ErrorCode 类的声明。
通过上面的异常类和工具类的声明,我们解决业务流程中对业务判断非法数据业务操作。这样 service 层的业务代码中对业务数据的判断就可以改为以异常来传递错误信息给予调用者,具体代码如下:
业务异常处理非法业务数据信息代码示例
/**
* 对修改的课程数据进行必要数据的判断
*/
private void verifyCoursebaseMsg(CourseBaseDTO courseBaseDTO){
//2. 对关键数据进行判断
//2.1 判断课程名称
if (StringUtil.isBlank(courseBaseDTO.getName())) {
ExceptionCast.cast(ContentErrorCode.E_120004);
}
//2.2 判断课程大分类
if (StringUtil.isBlank(courseBaseDTO.getMt())) {
ExceptionCast.cast(ContentErrorCode.E_120002);
}
//2.3 判断课程小分类
if (StringUtil.isBlank(courseBaseDTO.getSt())) {
ExceptionCast.cast(ContentErrorCode.E_120003);
}
//2.4 判断课程等级
if (StringUtil.isBlank(courseBaseDTO.getGrade())) {
ExceptionCast.cast(ContentErrorCode.E_120007);
}
//2.5 判断课程教学模式
if (StringUtil.isBlank(courseBaseDTO.getTeachmode())) {
ExceptionCast.cast(ContentErrorCode.E_120006);
}
}
现在的业务层对业务异常的抛出和异常信息的定义已经做了相应的声明和实现,但现在工程中并没有对其异常信息进行统一处理,除了业务异常信息还有微服务框架中抛出的错误异常也要进行统一处理。所有下面的小结中对统一处理进行讲解。
1.1.3 全局异常处器
1.1.3.1 全局异常处理流程
编写学成在线微服务端的全局异常处理类,对微服务的项目做全局异常处理。对于异常主要分两大类处理:
●可预知的异常由程序员在代码中主动抛出,全局异常处理类统一捕获可预知异常主要是指的业务异常信息,在代码中手动抛出定义的业务异常信息,,程序员在抛出时会指定错误代码及错误信息。
●不可预知的异常(运行时异常)由统一捕获Exception类型的异常。不可预知异常通常是由于系统出现bug、或一些不要抗拒的错误(比如网络中断、服务器宕机等),异常类型为RuntimeException类型(运行时异常)。
全局异常处理流程图
上图流程解释:
1、在controller、service、dao中程序员抛出可预知异常类型,微服务框架抛出不可预知异常类型。
2、统一由异常捕获类捕获异常,并进行处理。
3、捕获到可预知异常则直接取出错误代码及错误信息,响应给用户。
4、捕获到微服务框架中不可预知异常,错误代码则统一为99999错误代码并响应给用户。
5、将错误代码及错误信息以Json格式响应给用户。
1.1.3.2 全局异常处理实现
从 Spring 3.0 - Spring 3.2 版本之间,对 Spring 架构和 SpringMVC 的Controller 的异常捕获提供了相应的异常处理。
●@ExceptionHandlerSpring3.0提供的标识在方法上或类上的注解,用来表明方法的处理异常类型。
●@ControllerAdviceSpring3.2提供的新注解,从名字上可以看出大体意思是控制器增强, 在项目中来增强SpringMVC中的Controller。通常和@ExceptionHandler 结合使用,来处理SpringMVC的异常信息。
●@ResponseStatusSpring3.0提供的标识在方法上或类上的注解,用状态代码和应返回的原因标记方法或异常类。 调用处理程序方法时,状态代码将应用于HTTP响应。
通过上面的两个注解便可实现微服务端全局异常处理,具体代码如下:
●全局异常处理异常类(已经再项目中定义,无需再次定义) 由于各个微服务业务模块都需要对异常进行全局处理,所有需要定义在 xc-common下。
package com.xuecheng.common.exception;
import com.xuecheng.common.domain.code.CommonErrorCode;
import com.xuecheng.common.domain.code.ErrorCode;
import com.xuecheng.common.domain.response.RestErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
/**
* 日志:
* 1.日志框架:{@link Slf4j}
* 2.Slf4j 只是一个规范的Api没有实现类:使用 log4j2
* 3.每一个类中如果要使用日志
* private static final Logger log = xxx :创建一个日志对象
* 4.Lombok使用 @Slf4j 来简化日志对象的创建
*
* 全局异常处理:
* 1.全局异常处理的结构
* 2.全局异常处理的实现技术
* 3.异常处理的封装类
* 4.对于异常的日志记录
* 5.异常处理的过程
* 向上寻找父类型
*/
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理可预知异常(业务模块中的异常信息)
* @param businessException {@link BusinessException} 业务异常类
* @return RestErrorResponse - 相应错误信息封装类,封装错误代码(code) 和 错误信息(desc)
*/
@ResponseBody
@ExceptionHandler(value = BusinessException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse customException(BusinessException businessException) {
/*
* error方法参数:
* 1.错误信息
* 在错误信息中可以使用占位符 :{}
* 2.具体的错误内容
* e.getMessage() 会把占位符进行替换
* */
log.error("【系统异常】{}", businessException.getMessage(), businessException);
//1.获得异常信息属性
ErrorCode errorCode = businessException.getErrorCode();
//2.将异常信息属性的 错误代码(code) 和 错误信息(desc)封装到 RestErrorResponse 中
return new RestErrorResponse(errorCode);
}
/**
* 处理不可预知异常(微服务框架抛出不可预知异常类型)
* @param exception {@link Exception} 异常类
* @return RestErrorResponse - 相应错误信息封装类,封装错误代码(code) 和 错误信息(desc)
*/
@ResponseBody
@ExceptionHandler(value = Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse exception(Exception exception) {
//0.将错误信息记录到日志中
log.error("【系统异常】{}", exception.getMessage(), exception);
//1.将异常信息属性的 错误代码(code) 和 错误信息(desc)封装到 RestErrorResponse 中
return new RestErrorResponse(String.valueOf(CommonErrorCode.UNKOWN.getCode()),
CommonErrorCode.UNKOWN.getDesc() + ":" + exception.getMessage());
}
}
上面的全局异常处理器声明主要对微服务应用运行流程中,Spring 框架提供的相关注解实现了对微服务端统一处理异常功能。
1.1.4 全局异常处器测试
在对全局异常处理器测试,我们要对启动类的包扫描进行配置,因为 xc-common 中有公共的异常信息相关内容。我们需要在启动内容管理微服务时去加载基础工程 xc-common 下的 Spring 组件。
●修改启动类的包扫描 通过注解 @SpringBootApplication 对包扫描进行重新定义。
/**
* <p>
* 内容管理启动类
* </p>
*/
@EnableSwagger2Doc
@SpringBootApplication(scanBasePackages = {"com.xuecheng.content","com.xuecheng.common.exception"})
public class ContentApplication {
public static void main(String[] args) {
SpringApplication.run(ContentApplication.class,args);
}
}
上面包扫描代码解释:
1.扫描 com.xuecheng.content 包,此包为内容管理微服务基础包。
2.扫描 com.xuecheng.common 包,此包为基础工程 xc-common 基础包。
2.课程计划查询
在内容管理需求中,有对课程计划信息管理的需求,课程所关联的课程计划信息,教学机构需管理课程计划与媒资信息的关联关系,课程计划为课程内容的大纲,主要方便学员学习和视频播放列表的展示。
教学机构在添加课程数据时,一共是分为三个步骤来进行填写数据,第一个步骤为添加课程基本信息,第二个步骤则是添加课程计划,课程计划也成为课程大纲。
课程信息的三个步骤
●内容管理对课程计划管理 课程计划需要在内容管理中完成相关数据的关联操作,课程计划定义了课程的章节内容,教学机构在教学管理中心中需要在一个课程下添加对应的课程计划树形结构数据,并关联相应的数据,例如课程计划与媒资管理的关联、课程计划下添加课程作业等。 课程计划的树形结构分为直播和录播两个模式,其结构一致,但添加的关联内容不一致。如下图
内容管理的课程计划—录播
录播中的课程计划中,一个课程计划可以关联 视频、作业、文档,三者选择其一。
内容管理的课程计划—直播
直播中的课程计划,只能添加课程的直播时间段,对直播后的内容可以通过回看的方式来观看视频。
●学习中心对课程计划查询 通过内容管理对课程发布后,学员就可以通过学习中心对发布后的课程进行学习,课程学习主要是以课程章节来学习课程。而课程章节就为课程下的课程计划信息。在课程学习页面中,会将课程计划以课程章节的形式进行展示。
学习中心的课程计划
教学机构对课程计划管理功能包括:添加课程计划、删除课程计划、修改课程计划等。本章节对课程计划查询来进行实现。
2.1 课程计划查询接口业务需求
在内容管理中,需要在课程下来创建课程的课程计划,课程计划不管是直播还是录播,主要以树形结构来展示,课程计划查询主要以课程 ID 来进行查询。
2.1.1 数据模型(表结构)
内容管理服务的数据库 xc_content 中定义了课程计划数据结构表。
1.课程计划表说明
课程计划表结构
在上图中的表结构中,主要字段为:
1.自身信息描述:id,pname,grade,media_type,description等
2.父信息描述:parentId
3.树形结构排序:orderby
下面我们要创建出课程基本数据(teachplan)表的 PO 类,创建出的位置在工程项目包结构位置:com.xuecheng.content.entity 。具体代码如下:
●课程计划 PO 类(代码生成器生成)
package com.xuecheng.content.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
* 课程计划
* </p>
*/
@Data
@TableName("teachplan")
public class Teachplan implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 课程计划名称
*/
private String pname;
/**
* 课程计划父级Id
*/
private Long parentid;
/**
* 层级,分为1、2、3级
*/
private Integer grade;
/**
* 课程类型:1视频、2文档
*/
private String mediaType;
/**
* 开始直播时间
*/
private LocalDateTime startTime;
/**
* 直播结束时间
*/
private LocalDateTime endTime;
/**
* 章节及课程时介绍
*/
private String description;
/**
* 时长,单位时:分:秒
*/
private String timelength;
/**
* 排序字段
*/
private Integer orderby;
/**
* 课程标识
*/
private Long courseId;
/**
* 课程发布标识
*/
private Long coursePubId;
/**
* 状态(1正常 0删除)
*/
private Integer status;
/**
* 是否支持试学或预览(试看)
*/
private String isPreview;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createDate;
/**
* 修改时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime changeDate;
}
2.1.2 业务功能分析
1.课程计划的树形 json 结构
课程计划可以使用自关联将三级菜单数据一起查询出来,并且需要给前端返回如下的数据格式:
{
"courseId": 28,
"createDate": "2019-09-16T10:45:36",
"grade": 1,
"orderby": 1,
"parentid": 0,
"pname": "Redis从入门到项目实战",
"startTime": "2019-09-16T10:45:36",
"status": 1,
"teachPlanId": 28,
"teachPlanTreeNodes": [
{
"courseId": 28,
"createDate": "2019-09-16T10:45:36",
"grade": 2,
"orderby": 1,
"parentid": 28,
"pname": "第一章:redis简介",
"startTime": "2019-09-16T10:45:36",
"status": 1,
"teachPlanId": 29,
"teachPlanTreeNodes": [
{
"courseId": 28,
"createDate": "2019-09-16T10:45:36",
"grade": 3,
"mediaType": "1",
"orderby": 1,
"parentid": 29,
"pname": "第一节 NoSQL简介",
"startTime": "2019-09-16T10:45:36",
"status": 1,
"teachPlanId": 37
},
........
]
},
........
]
}
从上面的可以看出是 Json 数组格式,一级下包含多个二级数据,二级菜单又包含多个三级数据。
2.树形表结构说明
课程计划采用树形结构来表示一个课程下的整个课程章节信息。树形结构一共分为三级,一级的为根级,一个课程对应一条根级课程计划数据,其他两级有多条。树形结构关系仅靠 Id 和 parentId 来表示,三级数据都在一张表中,所有需要使用表的自关联来完成三级菜单数据显示。
课程计划关联关系
3.实体类的封装
对于课程计划(课程大纲)的树形结构格式的数据需要实体类来封装起来,但对于现在课程分类的 PO 类里缺少集合属性。
所以需要在现有的课程计划(课程大纲) PO 实体类进行扩展,增加集合属性,这样就可以避免修改 PO类造成数据库表的字段和PO类型属性对应不上的新现象。
课程计划扩展类
package com.xuecheng.content.entity.ext;
import com.xuecheng.content.entity.Teachplan;
import lombok.Data;
import java.util.List;
/**
* <p>
* 课程计划树形结构扩展PO类
* </p>
*
* @Description: 查询三级课程计划树形结构扩展类
*/
@Data
public class TeachplanNode extends Teachplan {
/**
* 子节点数据集合,课程计划为3级
* 为方便树形结构数据的返回,定义子节点的集合属性
*/
List<TeachplanNode> childrenNodes;
}
2.1.3 内存中生成树形结构数据
树形结构查询生成:
1.sql语句+mybatis的封装(方式一)
sql要进行自关联查询:树形结构有几级就需要查询几次
mybatis的封装:resultMap来封装数据-标签 collection
适用场景:使用一张表数据量不大,并且表数据能不易更改
2.通过sql查询出一个数据的所有内容,然后在程序中通过代码来生成树形结构(方式二)
sql查询:将关联数据全部查询(包含树形结构中的所有内容-list)
程序要通过逻辑来实现树形结构的生成:遍历list来生成
适用场景:使用于表数据量很大,而且数据经常变更
对于数据库的自关联查询数据库表的数据量不大,且不易变化,如果自表三级关联的表数据经常变化且数据量大,那么自关联SQL查询效率会灰常的低,原因在于:
1.自关联的连接查询可以针对小表进行查询。
2.自连接查询会进行笛卡尔全集,(笛卡尔积:两张表行数的乘积)所以自连接查询出来的记录会是此表数据量的平方。
课程计划(课程大纲)数据经常性的变化且数据量大,不适合使用自关联 SQL 语句查询。本次我们将通过后端代码查询出课程下的课程计划(课程大纲)数据,并通过递归方法来构建出课程计划的树形结构数据。具体步骤如下:
1.根据课程 CourseBaseId 查询出相关的课程计划。
2.使用 MP 的映射文件,将查询出的集合数据进行封装到 TeachplanNode 扩展类中。
3.通过 Java 的递归方法将集合的树形结构生成。
1.根据Id查询出相关的课程计划
本次将采用 SQL 语句将课程计划的三级菜单一并查出,方便业务端的数据封装。
查询课程 ID 为 28 的课程计划信息
select
`id`,
`pname`,
`parentid`,
`grade`,
`media_type`,
`start_time`,
`end_time`,
`description`,
`timelength`,
`orderby`,
`course_id`,
`course_pub_id`,
`status`,
`is_preview`,
`create_date`,
`change_date`
from
`teachplan`
where
course_id = 28
order by
grade,
orderby
结果示例图
2.使用 MP 的映射文件,将查询出的数据进行封装到 TeachplanNode 扩展类中
●TeachplanMapper.xml 对 TeachplanNode 数据的映射。
<?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">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.xuecheng.content.entity.ex.TeachplanNode">
<id column="id" property="id" />
<result column="pname" property="pname" />
<result column="parentid" property="parentid" />
<result column="grade" property="grade" />
<result column="media_type" property="mediaType" />
<result column="start_time" property="startTime" />
<result column="end_time" property="endTime" />
<result column="description" property="description" />
<result column="timelength" property="timelength" />
<result column="orderby" property="orderby" />
<result column="course_id" property="courseId" />
<result column="course_pub_id" property="coursePubId" />
<result column="status" property="status" />
<result column="is_preview" property="isPreview" />
<result column="create_date" property="createDate" />
<result column="change_date" property="changeDate" />
</resultMap>
<select id="selectByCourseId" parameterType="java.lang.Long" resultMap="BaseResultMap">
select
`id`,
`pname`,
`parentid`,
`grade`,
`media_type`,
`start_time`,
`end_time`,
`description`,
`timelength`,
`orderby`,
`course_id`,
`course_pub_id`,
`status`,
`is_preview`,
`create_date`,
`change_date`
from
`teachplan`
where
course_id = #{courseId}
order by
grade,
orderby
</select>
</mapper>
TeachplanMapper 接口
package com.xuecheng.content.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xuecheng.content.entity.Teachplan;
import com.xuecheng.content.entity.ex.TeachplanNode;
import java.util.List;
public interface TeachplanMapper extends BaseMapper<Teachplan> {
List<TeachplanNode> selectByCourseId(Long courseId);
}
3.通过 Java 的递归方法将集合的树形结构生成
●测试递归方法
package com.xuecheng.content.mapper;
import com.alibaba.fastjson.JSONObject;
import com.xuecheng.common.enums.content.TeachPlanEnum;
import com.xuecheng.content.entity.ex.TeachplanNode;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.ObjectUtils;
import java.util.ArrayList;
import java.util.List;
/**
* <p></p>
*
* @Description:
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class TeachplanMapperTest {
@Autowired
private TeachplanMapper teachplanMapper;
@Test
public void test01() {
//1.获得课程下的课程计划数据
List<TeachplanNode> teachplanNodes = teachplanMapper.selectByCourseId(28L);
//2.获得一级课程计划
TeachplanNode root = teachplanNodes.get(0);
//3.调用递归方法生成树形结构数据
generateTreeNodes(root,teachplanNodes);
//4.转为 Json 数据
System.out.println(JSONObject.toJSONString(root));
}
/**
* 递归生成树形结构数据
* @param currentNode 当前的父级
* @param nodes 集合数据
*/
public void generateTreeNodes(TeachplanNode currentNode, List<TeachplanNode> nodes) {
// 判断父级的集合是否为空,为空则需要创建出集合对象
if (ObjectUtils.isEmpty(currentNode.getChildrenNodes())) {
currentNode.setChildrenNodes(new ArrayList<TeachplanNode>());
}
//循环 nodes 集合数据
for (TeachplanNode node : nodes) {
//1.判断循环的当前数据的 parentId 是否等于 当前的 node 的 id 值。
if (node.getParentid().equals(currentNode.getId())) {
//1.1 将循环中的 node 添加到当前父级 node 集合数据中
currentNode.getChildrenNodes().add(node);
//1.2 判循环中的 node 的级别是的为 3级,如果不为 3级需要进行下一轮的递归操作
if (!(node.getGrade().equals(TeachPlanEnum.THIRD_LEVEL))) {
generateTreeNodes(node,nodes);
}
}
}
}
}
2.2 课程计划查询功能业务实现
2.2.1 课程计划查询接口定义
1.接口参数列表
根据前后端传入参数列表来定义接口
Http接口地址
接口传入传出列表
2. 数据传输DTO类创建
在 xc-api 工程的 com.xuecheng.api.content.model 包下创建类,如下:
●课程计划数据传输对象DTO
package com.xuecheng.api.content.model;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
/**
* <p>
* 课程计划
* </p>
*/
@Data
@ApiModel(value="TeachplanDTO", description="课程计划")
public class TeachplanDTO implements Serializable {
@ApiModelProperty(value = "课程计划Id")
private Long teachPlanId;
@ApiModelProperty(value = "课程计划名称")
private String pname;
@ApiModelProperty(value = "课程计划父级Id")
private Long parentid;
@ApiModelProperty(value = "层级,分为1、2、3级")
private String grade;
@ApiModelProperty(value = "开始直播时间(仅限直播类型,直播时不能为空)")
private LocalDateTime startTime;
@ApiModelProperty(value = "开始直播时间(仅限直播类型,直播时不能为空)")
private LocalDateTime endTime;
@ApiModelProperty(value = "排序字段")
private Integer orderby;
@ApiModelProperty(value = "课程标识")
private Long courseId;
@ApiModelProperty(value = "课程发布标识")
private Long cousePubId;
@ApiModelProperty(value = "发布状态")
private String status;
@ApiModelProperty(value = "是否支持试学或预览")
private String isPreview;
@ApiModelProperty(value = "创建时间")
private LocalDateTime createDate;
@ApiModelProperty(value = "修改时间")
private LocalDateTime changeDate;
@ApiModelProperty(value = "课程计划子级树形结构集合")
private List<TeachplanDTO> teachPlanTreeNodes;
}
上述代码解释:
1.其中 Id 的属性名称改为 teachPlanId
2.定义子级树形结构集合 teachPlanTreeNodes
3. 接口编写
在 com.xuecheng.api.content 包下创建接口类,页面查询接口定义如下:
●课程计划查询接口
package com.xuecheng.api.content;
import com.xuecheng.api.content.model.TeachplanDTO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import java.util.List;
/**
* <p>
* 课程计划服务API
* </p>
*/
@Api(value = "课程计划信息管理")
public interface TeachplanApi {
@ApiOperation(value = "根据课程Id查询课程计划树形结构(树形结构为三级目录)")
@ApiImplicitParam(name = "courseId" , value = "课程Id值",required = true,dataType = "Long", paramType = "path", example = "1")
TeachplanDTO queryTreeNodesByCourseId(Long courseId);
}
2.2.2 课程计划查询接口开发
在之前的代码基础上开发课程计划功能,已经对数据源、持久层框架的配置信息都已配置,下面我们直接开始应用三层开发。
1.DAO编写
之前的测试中已经编写,无需要此编写。
2. service编写
对于service层,除了进行业务层逻辑判断和操作外,还需要将 PO 数据转为 DTO 数据,所以需要编写课程计划的对象属性映射。
●对象属性转换类
@Mapper
public interface TeachplanConvert {
TeachplanConvert INSTANCE = Mappers.getMapper(TeachplanConvert.class);
@Mappings({
@Mapping(source = "id",target = "teachPlanId"),
@Mapping(source = "childrenNodes",target = "teachPlanTreeNodes")
})
TeachplanDTO node2dto(TeachplanNode teachplanNode);
/**
* 集合数据映射是依赖于单个的数据映射方法
* @return
*/
List<TeachplanDTO> nodes2dtos(List<TeachplanNode> nodes);
}
●接口
package com.xuecheng.content.service;
import com.xuecheng.api.content.model.TeachplanDTO;
import com.xuecheng.common.domain.response.RestResponse;
import com.xuecheng.content.entity.Teachplan;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 课程计划 服务类
* </p>
*/
public interface TeachplanService extends IService<Teachplan> {
/**
* 根据课程Id查询课程计划(树形结构)
* @param courseId
* @param companyId
* @return
*/
TeachplanDTO queryTreeNodesByCourseId(Long courseId,Long companyId);
}
实现类
/**
* <p>
* 课程计划 服务实现类
* </p>
*/
@Service
public class TeachplanServiceImpl extends ServiceImpl<TeachplanMapper, Teachplan> implements TeachplanService {
@Autowired
private CourseBaseService courseBaseService;
/*
* 业务分析:
* 1.判断关键数据
* courseId companyId
* 2.判断业务数据
* 课程基础信息
* 判断是否存在
* 判断是否是同一家教学机构
* 判断是否删除
* 3.根据Courseid查询课程计划数据
* 4.通过java的递归生成课程计划树形结构
* 5.将数据转为dto并返回
* */
public TeachplanDTO queryTreeNodesByCourseId(Long courseId, Long companyId) {
// 1.判断关键数据
// courseId companyId
if (ObjectUtils.isEmpty(courseId)||
ObjectUtils.isEmpty(companyId)
) {
ExceptionCast.cast(CommonErrorCode.E_100101);
}
// 2.判断业务数据
// 课程基础信息
// 判断是否存在
// 判断是否是同一家教学机构
// 判断是否删除
LambdaQueryWrapper<CourseBase> baseQueryWrapper = new LambdaQueryWrapper<>();
baseQueryWrapper.eq(CourseBase::getId, courseId);
baseQueryWrapper.eq(CourseBase::getCompanyId, companyId);
CourseBase courseBase = courseBaseService.getOne(baseQueryWrapper);
if (ObjectUtils.isEmpty(courseBase)) {
ExceptionCast.cast(ContentErrorCode.E_120013);
}
Integer status = courseBase.getStatus();
if (!(CommonEnum.USING_FLAG.getCodeInt().equals(status))) {
ExceptionCast.cast(ContentErrorCode.E_120021);
}
// 3.根据Courseid查询课程计划数据
TeachplanMapper baseMapper = this.getBaseMapper();
List<TeachplanNode> nodes = baseMapper.selectTreeNodesByCourseId(courseId);
// 4.通过java的递归生成课程计划树形结构
TeachplanDTO teachplanDTO = null;
if (CollectionUtils.isEmpty(nodes)) {
teachplanDTO = new TeachplanDTO();
} else {
TeachplanNode rootNode = nodes.remove(0);
generateTreeNodes(rootNode,nodes);
teachplanDTO = TeachplanConvert.INSTANCE.node2dto(rootNode);
}
// 5.将数据转为dto并返回
return teachplanDTO;
}
private void generateTreeNodes(TeachplanNode parentNode, List<TeachplanNode> nodes) {
// 1.判断父级集合是否为空,如果为空需要创建
if (CollectionUtils.isEmpty(parentNode.getChildrenNodes())) {
parentNode.setChildrenNodes(new ArrayList<>(8));
}
// 2.循环遍历集合生成树形结构
for (TeachplanNode node : nodes) {
// 2.1 判断node是否和parentnode有关联,如果有关联将node存放到parentNode集合中
if (ObjectUtils.nullSafeEquals(parentNode.getId(),node.getParentid())) {
parentNode.getChildrenNodes().add(node);
// 2.2 生成node集合数据
// 如果是第三级课程计划,无需递归
if (!(TeachPlanEnum.THIRD_LEVEL.equals(node.getGrade()))) {
generateTreeNodes(node,nodes);
}
}
}
}
}
3. Controller 编写
package com.xuecheng.content.controller;
import com.xuecheng.api.content.TeachPlanAPI;
import com.xuecheng.api.content.model.TeachplanDTO;
import com.xuecheng.content.service.TeachplanService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* <p>
* 课程计划控制层
* </p>
*/
@RestController
public class TeachplanController implements TeachplanApi {
@Autowired
private TeachplanService teachplanService;
@GetMapping("teachplan/{courseId}/tree-nodes")
public TeachplanDTO queryTreeNodesByCourseId(@PathVariable Long courseId) {
//1. 获得公司Id
Long companyId = SecurityUtil.getCompanyId();
//2. 调用service获得 dto数据
TeachplanDTO teachplanDTO =
teachplanService.queryTreeNodesByCourseId(courseId, companyId);
return teachplanDTO;
}
}
2.2.3 信息接口测试
测试环境需要启动的微服务有:
1.注册中心 xc-discover-service (端口:63000)
2.服务网关 xc-gateway-service (端口:63010)
3.内容管理 xc-content-service (端口:63040)
1. 使用 postman 在请求信息中添加请求参数
GET http://localhost:63010/content/teachplan/28/tree-nodes
●请求头(访问令牌)
请求头的 key 值: authorization
请求头的 value 值:
Bearer ewogICAgImF1ZCI6IFsKICAgICAgICAieHVlY2hlbmctcmVzb3VyY2UiCiAgICBdLAogICAgInBheWxvYWQiOiB7CiAgICAgICAgIjExNzcxNDQyMDk0NjMxMjgxMjUiOiB7CiAgICAgICAgICAgICJyZXNvdXJjZXMiOiBbCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJ1c2VyX2F1dGhvcml0aWVzIjogewogICAgICAgICAgICAgICAgInJfMDAxIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb21wYW55X21vZGlmeSIsCgkJCQkJInhjX2NvbXBhbnlfdmlldyIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2RlbCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2VkaXQiLAoJCQkJCSJ4Y19jb3Vyc2VfYmFzZV9saXN0IiwKCQkJCQkieGNfY291cnNlX2Jhc2Vfc2F2ZSIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX3ZpZXciLAoJCQkJCSJ4Y19jb3Vyc2VfcHVibGlzaCIsCgkJCQkJInhjX21hcmtldF9zYXZlX21vZGlmeSIsCgkJCQkJInhjX21hcmtldF92aWV3IiwKCQkJCQkieGNfbWVkaWFfZGVsIiwKCQkJCQkieGNfbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX3ByZXZpZXciLAoJCQkJCSJ4Y19tZWRpYV9zYXZlIiwKCQkJCQkieGNfdGVhY2hlcl9saXN0IiwKCQkJCQkieGNfdGVhY2hlcl9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaGVyX3NhdmUiLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2NvcnJlY3Rpb24iLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2xpc3QiLAoJCQkJCSJ4Y190ZWFjaHBsYW53b3JrX2RlbCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfbGlzdCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfc2F2ZV9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaHBsYW5fZGVsIiwKCQkJCQkieGNfdGVhY2hwbGFuX3NhdmVfbW9kaWZ5IiwKCQkJCQkieGNfdGVhY2hwbGFuX3ZpZXciCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgInJfMDAyIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb3Vyc2VfYWRtaW5fbGlzdCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2NvbW1pdCIsCgkJCQkJInhjX3N5c3RlbV9jYXRlZ29yeSIsCgkJCQkJInhjX21fbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX2F1ZGl0IgogICAgICAgICAgICAgICAgXQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfSwKICAgICJ1c2VyX25hbWUiOiAieGMtdXNlci1maXJzdCIsCiAgICAic2NvcGUiOiBbCiAgICAgICAgInJlYWQiCiAgICBdLAogICAgIm1vYmlsZSI6ICIxNTAxMjM0NTY3OCIsCiAgICAiZXhwIjogMTYwNjUyNTEyMiwKICAgICJjbGllbnRfYXV0aG9yaXRpZXMiOiBbCiAgICAgICAgIlJPTEVfVVNFUiIKICAgIF0sCiAgICAianRpIjogIjFlYjdlOTg3LWQ3YzItNDBmNS1iMGQ2LWNkNjEzOWNiMThlMCIsCiAgICAiY2xpZW50X2lkIjogInhjLWNvbS1wbGF0Zm9ybSIsCiAgICAiY29tcGFueUlkIjogMTIzMjE0MTQyNQp9
3. 课程计划信息创建和修改
为便于前端调用后端的接口,本次对课程计划的创建和修改操作统一到一个接口下。课程计划为树形结构,树形结构为三级,教育结构在对课程计划信息创建和修改时,分别要做不同的操作。
3.1 课程计划创建和修改接口业务需求
对于课程计划的创建和修改操作时,其相对需求不一样,下面我们分别进行说明。
3.1.1 课程计划创建业务需求
课程计划的添加是根据课程类别的不同而不同,直播课程和录播课程的课程计划树形结构数据展示所填的的内容也不同,下面我们分情况说明。
3.1.1.1 课程计划创建业务分析
(1)课程章节显示结构
在录播课程中,通过添加树形结构数据后,在对应的节点中添加详细信息。小章节可以关联课程资源信息。如下图:
课程计划树形结构示意图
上图解释:
注释①:小章节关联课程资源后的展示。
注释②:小章节关联课程资源前的展示。
学成在线的录播课程是采用小章节的学习模式来进行学习,对于小章节应该是先有视频,再有作业。视频和作业都会独立占用一个小章节,小章节中关联的资源只能为作业、视频、文档其中的一种。
(2)课程章节添加
教育机构在课程下添加教学计划时,只能对二级或三级课程计划进行添加,对于页面中提交数据的不同,相应的会增加二级或三级课程计划数据,具体如下图例:
●课程计划数据二级添加表单
添加二级课程计划图例
上图解释:
注释①:添加的课程计划大章节,大章节为二级课程计划。
●课程计划数据三级添加表单
添加三级课程计划图例
上图解释:
注释①:选择上级节点是,添加的课程计划为二级课程计划。
3.1.2 课程计划修改业务需求
修改课程计划只能在原有的课程下的课程计划信息进行修改操作,直播和录播的修改大致相同,修改的内容主要是对大章节和小章节。
●操作界面展示如下:
修改大章节内容
上图解释:
注释①:在大章节的章节名称文本框中修改章节名称。
修改录播小章节内容
上图解释:
注释①:1.在小章节的章节名称文本框中修改章节名称。
2.修改小章节管理资源信息。
●具体需求如下:
1.在课程计划界面中,可以对每个课程计划的内容进行修改。
2.对于大章节的内容修改,只能修改名称。
2.1 在大章节的章节名称文本框中修改内容,鼠标离开文本框,并向后端保存大章节数据。
3.在大章节后点击 “添加小节” 按钮,添加课程计划小章节内容。
3.1 在填写完毕后,鼠标离开文本框,并向后端保存小章节数据。
4.小章节中可以填写录播或直播的相关内容数据。
4.1 录播课程的小章节可以关联资源(视频、文档、作业)。
4.2 直播课程的小章节填写直播的时间。
后端对于业务的分析:
1.添加和修改操作的区别
添加:没有teachplanid
修改:有teachplanid
2.添加2级和3级数据的区别
2级:没有parentid
3级:有parentid
3.1.3 课程计划数据操作要求
添加时,后端的业务端要对其数据要加以判断,来区分所添加的课程计划所属级别。对于不同的级别的课程计划,要做不同的数据处理。修改时,要对关键数据进行判断。下面是具体的数据结构和处理方式:
(1)数据结构操作:
对于整个的课程计划树形结构的操作,需要根据课程基本来分别进行不同的处理,下面是课程计划的结构示意图。
课程计划结构示意图
页面对应数据示意图
上图解释:
注释①:课程计划大章节,大章节代表二级课程计划。
注释②:课程计划小章节,小章节代表三级课程计划。
(2)添加数据时:
1.课程计划的一级(根级)
●数据来源:课程基础信息(CourseBase)
●创建时机:在第一次添加二级数据时自动生成
●注意事项:一级父级(根级)父级 ID 值为 0
2.课程计划的二级和三级节点
●数据来源:教育机构人员在页面填写的数据
●创建时机:教育机构人员提交课程计划数据
●注意事项:添加时要校验父级菜单是否存在
○二级在添加时
■父级不存在(一级):自动创建并获得获得父级Id值
■父级存在(一级):获得父级Id值
○三级在添加时
■父级不存在(二级):抛出异常
■父级存在(二级):获得父级Id值
(3)修改数据时:
1.要判断要修改的课程计划是否有 ID 值
课程计划在进行修改时,必须要有 ID 值,有 ID 值说明已经在数据库中存在该数据。
2.判断关键数据是否存在
对关键的业务数据进行判断操作,如:课程计划名称、父级 Id 值等。
3.2 课程计划创建和修改功能业务实现
下面将会实现课程计划创建和修改功能,课程基本信息的保存是在内容管理系统服务,所以此接口的实现将在内容管理微服务中开发,接口信息依然定义在 Api 工程中。
3.2.1 课程计划创建和修改接口定义
1.接口参数列表
根据前后端传入参数列表来定义接口
Http接口地址
接口传入传出列表
1. 传入传出的实体类封装
●传入VO数据
package com.xuecheng.api.content.model.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 课程计划
*/
@Data
@ApiModel(value="TeachplanVO", description="课程计划")
public class TeachplanVO implements Serializable {
@ApiModelProperty(value = "课程计划Id")
private Long teachPlanId;
@ApiModelProperty(value = "课程标识", required = true)
private Long courseId;
@ApiModelProperty(value = "课程计划名称", required = true)
private String pname;
@ApiModelProperty(value = "课程计划父级Id")
private Long parentid;
@ApiModelProperty(value = "层级,分为1、2、3级")
private Integer grade;
@ApiModelProperty(value = "课程计划资源类型", required = true)
private String mediaType;
@ApiModelProperty(value = "开始直播时间(仅限直播类型,直播时不能为空)")
private LocalDateTime startTime;
@ApiModelProperty(value = "开始直播时间(仅限直播类型,直播时不能为空)")
private LocalDateTime endTime;
@ApiModelProperty(value = "排序字段")
private Integer orderby;
@ApiModelProperty(value = "是否支持试学或预览, 1是免费,0或空是收费")
private String isPreview;
}
●传出 DTO 数据
课程计划 DTO 已经声明,无需再次创建。
2. 接口编写
在 xc-api 工程中的 TeachPlanApi 中添加接口方法。
●课程计划创建和修改接口
@Api(value = "课程计划信息管理")
public interface TeachplanApi {
//其他代码省略
@ApiOperation(value= "新增或修改课程计划")
@ApiImplicitParam(name = "teachplanVO",value = "课程计划VO" ,
required = true, dataType = "TeachplanVO",paramType = "body")
TeachplanDTO createOrModifyTeachPlan(TeachplanVO teachplanVO);
}
3.2.2 课程计划创建和修改接口开发
由于本次功能实现是将新增和修改两个功能同时实现,后端在进行实现时,主要是通过对 TeachplanDTO 中的 Id 进行判断。具体判断如下:
1.TeachplanDTO 中的 Id 不为空并且在数据库中存在此 Id 数据,进行修改操作。
2.TeachplanDTO 中的 为空,进行添加操作。
1.DAO编写
Mybatis Plus 已经简化了单表操作,它提供的 Api 就可以完成添加数据操作,所有不需要进行编写。
2. service编写
●属性转换器
package com.xuecheng.content.convert;
import com.xuecheng.api.content.model.TeachplanDTO;
import com.xuecheng.content.entity.Teachplan;
import com.xuecheng.content.entity.ex.TeachplanNode;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper
public interface TeachplanConvert {
TeachplanConvert INSTANCE = Mappers.getMapper(TeachplanConvert.class);
// 其他代码省略
@Mapping(source = "teachPlanId",target = "id")
Teachplan dto2entity(TeachplanDTO dto);
@Mapping(source = "id",target = "teachPlanId")
TeachplanDTO entity2dto(Teachplan teachplan);
}
●接口
public interface TeachplanService extends IService<Teachplan> {
/**
* 创建或修改课程计划信息
* @param teachplanDTO {@link Teachplan} 课程计划信息
* @return TeachplanDTO 课程计划信息
*/
TeachplanDTO createOrModifyTeachPlan(TeachplanDTO teachplanDTO,Long companyId);
}
实现类
package com.xuecheng.content.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xuecheng.api.content.model.dto.TeachplanDTO;
import com.xuecheng.common.domain.code.CommonErrorCode;
import com.xuecheng.common.enums.common.CommonEnum;
import com.xuecheng.common.enums.content.CourseAuditEnum;
import com.xuecheng.common.enums.content.TeachPlanEnum;
import com.xuecheng.common.exception.ExceptionCast;
import com.xuecheng.common.util.StringUtil;
import com.xuecheng.content.common.constant.ContentErrorCode;
import com.xuecheng.content.convert.TeachplanConvert;
import com.xuecheng.content.entity.CourseBase;
import com.xuecheng.content.entity.Teachplan;
import com.xuecheng.content.entity.ex.TeachplanNode;
import com.xuecheng.content.mapper.TeachplanMapper;
import com.xuecheng.content.service.CourseBaseService;
import com.xuecheng.content.service.TeachplanService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.util.ArrayList;
import java.util.List;
/**
* <p>
* 课程计划 服务实现类
* </p>
*
* @author itcast
*/
@Slf4j
@Service
public class TeachplanServiceImpl extends ServiceImpl<TeachplanMapper, Teachplan> implements TeachplanService {
@Autowired
private CourseBaseService courseBaseService;
/*
* 业务分析:--主体方法
* 1.判断业务操作
* 有teachplanid-->修改
* 没有teachplanid-->新增
* 2.操作完业务需要返回结果:dto
* */
@Transactional
public TeachplanDTO createOrModifyTeachplan(TeachplanDTO dto, Long companyId) {
TeachplanDTO resultDTO = null;
// 1.判断业务操作
// 有teachplanid-->修改
// 没有teachplanid-->新增
if (ObjectUtils.isEmpty(dto.getTeachPlanId())) {
// 没有teachplanid-->新增
resultDTO = createTeachplan(dto,companyId);
} else {
// 有teachplanid-->修改
resultDTO = modifyTeachplan(dto,companyId);
}
// 2.操作完业务需要返回结果:dto
return resultDTO;
}
private TeachplanDTO modifyTeachplan(TeachplanDTO dto, Long companyId) {
return null;
}
/*
* 业务分析:
* 1.判断关键数据
2.判断业务数据
课程基础信息
判断是否存在
判断是否同一家机构
判断是否删除
判断课程基础信息审核状态:未提交和审核未通过
课程计划
判断是否存在
3.修改课程计划数据
pname starttime endtime is_preview
4.判断修改后的结果
5.将数据库最新的数据转为dto并返回
* */
private TeachplanDTO modifyTeachplan(TeachplanDTO dto, Long companyId) {
// 1.判断关键数据
// 2.判断业务数据
// 课程基础信息
// 判断是否存在
// 判断是否同一家机构
// 判断是否删除
// 判断课程基础信息审核状态:未提交和审核未通过
CourseBaseDTO baseDTO = verifyTeachplanMsg(dto, companyId);
// 课程计划
// 判断是否存在
LambdaQueryWrapper<Teachplan> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Teachplan::getId, dto.getTeachPlanId());
int count = this.count(queryWrapper);
if (count < 1) {
ExceptionCast.cast(ContentErrorCode.E_120402);
}
// 3.修改课程计划数据
// pname starttime endtime is_preview
LambdaUpdateWrapper<Teachplan> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(Teachplan::getPname, dto.getPname());
updateWrapper.set(Teachplan::getStartTime, dto.getStartTime());
updateWrapper.set(Teachplan::getEndTime, dto.getEndTime());
updateWrapper.set(Teachplan::getIsPreview, dto.getIsPreview());
updateWrapper.eq(Teachplan::getId, dto.getTeachPlanId());
// 4.判断修改后的结果
boolean result = this.update(updateWrapper);
if (!result) {
ExceptionCast.cast(ContentErrorCode.E_120407);
}
// 5.将数据库最新的数据转为dto并返回
Teachplan po = this.getById(dto.getTeachPlanId());
TeachplanDTO resultDTO = TeachplanConvert.INSTANCE.entity2dto(po);
return resultDTO;
}
/*
* 新增课程计划业务分析:
* 1.判断关键数据
* 课程计划的名称不能为空
* 课程id不能为空
* 教学机构id不能为空
*
* 2.判断业务数据
* 课程基础信息
* 判断是否存在
* 判断是否是同一家教学机构
* 判断是否删除
* 判断审核状态:未提交、审核未通过
*
* 3.获得新增课程计划的父级数据
* 给新增课程计划赋值
* grade:父级的grade+1
* parentid:父级的id
* orderby:select max(order) from teachplan where parentid = 父级id
*
*
* 4.将dto转为po并保存到数据库中
* 判断保存后的结果
* 5.将数据库最新的数据转为dto并返回
* */
private TeachplanDTO createTeachplan(TeachplanDTO dto, Long companyId) {
//1.判断关键数据
// 课程计划的名称不能为空
// 课程id不能为空
// 教学机构id不能为空
if (ObjectUtils.isEmpty(dto.getPname())||
ObjectUtils.isEmpty(dto.getCourseId())||
ObjectUtils.isEmpty(companyId)
) {
ExceptionCast.cast(CommonErrorCode.E_100101);
}
// 2.判断业务数据
// 课程基础信息
// 判断是否存在
// 判断是否是同一家教学机构
// 判断是否删除
// 判断审核状态:未提交、审核未通过
CourseBaseDTO courseBase = courseBaseService.getCourseBaseById(dto.getCourseId(), companyId);
String auditStatus = courseBase.getAuditStatus();
if (CourseAuditEnum.AUDIT_PASTED_STATUS.getCode().equals(auditStatus)||
CourseAuditEnum.AUDIT_COMMIT_STATUS.getCode().equals(auditStatus)||
CourseAuditEnum.AUDIT_PUBLISHED_STATUS.getCode().equals(auditStatus)
) {
ExceptionCast.cast(ContentErrorCode.E_120015);
}
// 3.获得新增课程计划的父级数据
// 给新增课程计划赋值
// grade:父级的grade+1
// parentid:父级的id
// orderby:select max(order) from teachplan where parentid = 父级id
Teachplan parentNode = generateParentNode(dto,courseBase);
int childrenGrade = parentNode.getGrade() + 1;
dto.setGrade(childrenGrade+"");
dto.setParentid(parentNode.getId());
TeachplanMapper baseMapper = this.getBaseMapper();
Integer orderBy = baseMapper.selectOrderByParentId(parentNode.getId());
// 如果查询出orderBy为null --> 默认赋值为1
// 如果查询出orderBy为不null --> orderBy+1
if (ObjectUtils.isEmpty(orderBy)) {
dto.setOrderby(TeachPlanEnum.FIRST_LEVEL);
} else {
dto.setOrderby(orderBy+1);
}
// 4.将dto转为po并保存到数据库中
Teachplan teachplan = TeachplanConvert.INSTANCE.dto2entity(dto);
boolean result = this.save(teachplan);
// 判断保存后的结果
if (!result) {
ExceptionCast.cast(ContentErrorCode.E_120407);
}
// 5.将数据库最新的数据转为dto并返回
Teachplan po = this.getById(teachplan.getId());
TeachplanDTO resultDTO = TeachplanConvert.INSTANCE.entity2dto(po);
return resultDTO;
}
/*
* 业务分析:
* 1.判断新增数据是二级还是三级课程计划
* 判断新增数据是否有parentId值
* 如果有:三级课程计划
* 如果没有:二级课程计划
* 2.获得二级课程计划的父级数据
* 查询数据库获得一级课程计划:courseId、parentid=0
* 如果没有:自动创建并保存数据--数据来源 coursebase
* 如果有:直接返回
* 3.获得三级课程计划的父级数据
* 查询数据库获得二级课程计划:parentId
* 如果没有:直接抛出异常
* 如果有:直接返回
* */
private Teachplan generateParentNode(TeachplanDTO dto, CourseBaseDTO courseBase) {
//1.判断新增数据是二级还是三级课程计划
// 判断新增数据是否有parentId值
// 如果有:三级课程计划
// 如果没有:二级课程计划
Long parentid = dto.getParentid();
if (ObjectUtils.isEmpty(parentid)) {
// 2.获得二级课程计划的父级数据
// 查询数据库获得一级课程计划:courseId、parentid=0
// 如果没有:自动创建并保存数据--数据来源 coursebase
// 如果有:直接返回
LambdaQueryWrapper<Teachplan> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Teachplan::getCourseId, courseBase.getCourseBaseId());
queryWrapper.eq(Teachplan::getParentid, TeachPlanEnum.FIRST_PARENTID_FLAG);
Teachplan rootParent = this.getOne(queryWrapper);
if (ObjectUtils.isEmpty(rootParent)) {
rootParent = new Teachplan();
rootParent.setPname(courseBase.getName());
rootParent.setParentid(new Long(TeachPlanEnum.FIRST_PARENTID_FLAG));
rootParent.setGrade(TeachPlanEnum.FIRST_LEVEL);
rootParent.setDescription(courseBase.getDescription());
rootParent.setOrderby(TeachPlanEnum.FIRST_LEVEL);
rootParent.setCourseId(courseBase.getCourseBaseId());
boolean save = this.save(rootParent);
if (!save) {
ExceptionCast.cast(ContentErrorCode.E_120415);
}
}
return rootParent;
} else {
// 3.获得三级课程计划的父级数据
// 查询数据库获得二级课程计划:parentId
// 如果没有:直接抛出异常
// 如果有:直接返回
LambdaQueryWrapper<Teachplan> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Teachplan::getId, parentid);
Teachplan teachplan = this.getOne(queryWrapper);
if (ObjectUtils.isEmpty(teachplan)) {
ExceptionCast.cast(ContentErrorCode.E_120408);
}
return teachplan;
}
}
}
3. Controller 编写
●属性转换器
@Mapper
public interface TeachplanConvert {
TeachplanConvert INSTANCE = Mappers.getMapper(TeachplanConvert.class);
// 其他代码省略
TeachplanDTO vo2dto(TeachplanVO teachplanVO);
}
@RestController
public class TeachPlanController implements TeachPlanAPI {
@Autowired
private TeachplanService teachplanService;
//其他代码省略
@PostMapping("teachplan")
public TeachplanDTO createOrModifyTeachPlan(@RequestBody TeachplanVO teachplanVO) {
//1. 获得公司Id
Long companyId = SecurityUtil.getCompanyId();
//2. 将vo数据转为 dto 数据
TeachplanDTO dto = TeachplanConvert.INSTANCE.vo2dto(teachplanVO);
//2.调用service获得 dto数据
TeachplanDTO result = teachplanService.createOrModifyTeachPlan(dto, companyId);
return result;
}
}
3.2.3 信息创建接口测试
测试环境需要启动的微服务有:
1.注册中心 xc-discover-service (端口:63000)
2.服务网关 xc-gateway-service (端口:63010)
3.内容管理 xc-content-service (端口:63040)
1. 使用 postman 在请求信息中添加请求参数
POST http://localhost:63010/content/teachplan
●请求头(访问令牌)
请求头的 key 值: authorization
请求头的 value 值:
Bearer ewogICAgImF1ZCI6IFsKICAgICAgICAieHVlY2hlbmctcmVzb3VyY2UiCiAgICBdLAogICAgInBheWxvYWQiOiB7CiAgICAgICAgIjExNzcxNDQyMDk0NjMxMjgxMjUiOiB7CiAgICAgICAgICAgICJyZXNvdXJjZXMiOiBbCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJ1c2VyX2F1dGhvcml0aWVzIjogewogICAgICAgICAgICAgICAgInJfMDAxIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb21wYW55X21vZGlmeSIsCgkJCQkJInhjX2NvbXBhbnlfdmlldyIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2RlbCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2VkaXQiLAoJCQkJCSJ4Y19jb3Vyc2VfYmFzZV9saXN0IiwKCQkJCQkieGNfY291cnNlX2Jhc2Vfc2F2ZSIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX3ZpZXciLAoJCQkJCSJ4Y19jb3Vyc2VfcHVibGlzaCIsCgkJCQkJInhjX21hcmtldF9zYXZlX21vZGlmeSIsCgkJCQkJInhjX21hcmtldF92aWV3IiwKCQkJCQkieGNfbWVkaWFfZGVsIiwKCQkJCQkieGNfbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX3ByZXZpZXciLAoJCQkJCSJ4Y19tZWRpYV9zYXZlIiwKCQkJCQkieGNfdGVhY2hlcl9saXN0IiwKCQkJCQkieGNfdGVhY2hlcl9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaGVyX3NhdmUiLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2NvcnJlY3Rpb24iLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2xpc3QiLAoJCQkJCSJ4Y190ZWFjaHBsYW53b3JrX2RlbCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfbGlzdCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfc2F2ZV9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaHBsYW5fZGVsIiwKCQkJCQkieGNfdGVhY2hwbGFuX3NhdmVfbW9kaWZ5IiwKCQkJCQkieGNfdGVhY2hwbGFuX3ZpZXciCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgInJfMDAyIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb3Vyc2VfYWRtaW5fbGlzdCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2NvbW1pdCIsCgkJCQkJInhjX3N5c3RlbV9jYXRlZ29yeSIsCgkJCQkJInhjX21fbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX2F1ZGl0IgogICAgICAgICAgICAgICAgXQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfSwKICAgICJ1c2VyX25hbWUiOiAieGMtdXNlci1maXJzdCIsCiAgICAic2NvcGUiOiBbCiAgICAgICAgInJlYWQiCiAgICBdLAogICAgIm1vYmlsZSI6ICIxNTAxMjM0NTY3OCIsCiAgICAiZXhwIjogMTYwNjUyNTEyMiwKICAgICJjbGllbnRfYXV0aG9yaXRpZXMiOiBbCiAgICAgICAgIlJPTEVfVVNFUiIKICAgIF0sCiAgICAianRpIjogIjFlYjdlOTg3LWQ3YzItNDBmNS1iMGQ2LWNkNjEzOWNiMThlMCIsCiAgICAiY2xpZW50X2lkIjogInhjLWNvbS1wbGF0Zm9ybSIsCiAgICAiY29tcGFueUlkIjogMTIzMjE0MTQyNQp9
●添加和修改二级课程计划(示例数据,具体依据数据库数据)
{
"pname":"测试第一章",
"courseId": 47
}
添加和修改三级课程计划(示例数据,具体依据数据库数据)
{
"pname":"测试第一个小节",
"courseId": 47,
"parentid" : 99
}
4.课程计划信息删除-实战
教育机构中的老师对录入课程计划信息,平台来管理应提供对其行删除的业务操作,针对课程计划信息数据删除的需求,我们下面要开发课程计划删除的功能实现。
业务操作:
1.判断关键
teachplanId companyId
2.判断业务数据
课程基础信息
判断是否存在、判断是否是同一家教学机构、判断是否删除、判断审核状态(未提交、审核未通过)
课程计划
判断删除的课程计划是几级数据
二级
判断是否存在子级数据,如果有不让删除
三级
如果是录播课程
判断课程计划是否有关联数据(查询TeachplanMedia表数据)
4.1 课程计划删除接口业务需求
4.1.1 接口业务需求
●操作界面展示如下:
录播课程计划删除示例图
上图解释:
1.删除大章节(二级课程计划)数据时,如果大章节下有小章节不允许删除。
2.删除章节(三级课程计划)数据时,如果有关联的资源,不允许删除。
●具体需要如下:
1.机构老师在课程基本信息列表中点击“删除” 按钮。
2.页面弹出确认模态窗口,需要用户进行确认删除消息。
3.用户在确认模块窗口中点击 ‘确认’ 按钮,对数据进行删除操作。
4.删除成功后,提示删除结果操作。并返回信息列表界面中。
5.对课程计划大章节下如果有小章节,是不能被删除的。
6.对于课程计划小章节下如果有关联视频等资源,是不能被删除的。
4.2 课程计划删除功能业务实现
下面将会实现课程基本信息删除功能,课程基本信息的删除是在内容管理系统服务,所以此接口的实现将在内容管理微服务中开发,接口信息依然定义在 Api 工程中。
4.2.1 课程计划信息删除接口定义
在 ‘项目开发规范文档.md — 接口开发规范’ 文档声明,参入的参数为删除数据的 ID 集合数据,无接口响应数据。
1.接口参数列表
根据前后端传入参数列表来定义接口
Http接口地址
接口传入传出列表
2. 接口编写
在 xc-api 接口工程中的 TeachPlanApi 接口定义方法:
package com.xuecheng.api.content;
import com.xuecheng.api.content.model.qo.QueryCourseBaseModel;
import com.xuecheng.common.domain.page.PageVO;
/**
* <p>
* 课程基本信息 Api 接口路径定义
* </p>
*
* @Description: 课程基本信息 Api 接口路径定义
*/
@Api(tags = "课程基本信息服务接口",description = "对课程基本信息业务操作")
public interface TeachPlanApi {
//其他代码省略
@ApiOperation(value = "删除课程计划")
@ApiImplicitParam(name = "teachPlanId" ,
value = "课程计划Id值",required = true,
dataType = "Long", paramType = "path", example = "1")
void removeTeachPlan(Long teachPlanId);
}
4.2.2 课程计划信息删除接口开发
1.DAO编写
Mybatis Plus 已经简化了单表操作,它提供的 Api 就可以完成添加数据操作,所有不需要进行编写。
2. service编写
●接口
public interface TeachplanService extends IService<Teachplan> {
//其他代码省略
/**
* 根据课程计划Id删除课程计划信息
* @param teachPlanId {@link Long} 课程计划Id
* @param companyId {@link Long} 公司Id
*/
void removeTeachPlan(Long teachPlanId,Long companyId);
}
●实现类
@Service
public class TeachplanServiceImpl extends ServiceImpl<TeachplanMapper, Teachplan> implements TeachplanService {
@Autowired
private TeachplanMediaService mediaService;
//其他代码省略
@Override
public void removeTeachPlan(Long teachPlanId, Long companyId) {
//1.判断关键数据的合法性
Teachplan teachplan = getById(teachPlanId);
if (ObjectUtils.isEmpty(teachplan)) ExceptionCast.cast(ContentErrorCode.E_120402);
//1.2 判断课程基本信息是否存在
CourseBase courseBase = baseService.getById(teachplan.getCourseId());
if (ObjectUtils.isEmpty(courseBase))
ExceptionCast.cast(ContentErrorCode.E_120013);
//1.3 判断公司 Id是否和课程中的公司Id一致
if (!(ObjectUtils.nullSafeEquals(courseBase.getCompanyId(),companyId)))
ExceptionCast.cast(CommonErrorCode.E_100108);
// 2. 根据课程计划等级进行业务判断
if (teachplan.getGrade() == TeachPlanEnum.SECEND_LEVEL) {
// 判断二级课程计划是否有子级课程计划信息
LambdaQueryWrapper<Teachplan> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Teachplan::getParentid, teachPlanId);
int count = count(queryWrapper);
if (count > 0)
ExceptionCast.cast(ContentErrorCode.E_120409);
} else {
// 判断三级课程计划是否关联课程媒资信息
LambdaQueryWrapper<TeachplanMedia>
mediaLambdaQueryWrapper = new LambdaQueryWrapper<>();
mediaLambdaQueryWrapper.eq(TeachplanMedia::getTeachplanId,teachPlanId);
int mediaCount = mediaService.count(mediaLambdaQueryWrapper);
if (mediaCount > 0) ExceptionCast.cast(ContentErrorCode.E_120413);
}
// 4.根据Id删除课程计划
removeById(teachPlanId);
}
}
3. controller编写
@RestController
public class TeachplanController implements TeachplanApi {
@Autowired
private TeachplanService teachplanService;
//其他代码省略
@DeleteMapping("teachplan/{teachplanId}")
public void removeTeachPlan(@PathVariable Long teachplanId) {
// 1.获得公司Id
Long companyId = SecurityUtil.getCompanyId();
// 2.调用service方法
teachplanService.removeTeachPlan(teachplanId,companyId);
}
}
4.2.3 信息接口测试
测试环境需要启动的微服务有:
1.注册中心 xc-discover-service (端口:63000)
2.服务网关 xc-gateway-service (端口:63010)
3.内容管理 xc-content-service (端口:63040)
1. 使用 postman 在请求信息中添加请求参数
DELETE http://localhost:63010/content/teachplan/要删除的课程计划id值
●请求头(访问令牌)
请求头的 key 值: authorization
请求头的 value 值:
Bearer ewogICAgImF1ZCI6IFsKICAgICAgICAieHVlY2hlbmctcmVzb3VyY2UiCiAgICBdLAogICAgInBheWxvYWQiOiB7CiAgICAgICAgIjExNzcxNDQyMDk0NjMxMjgxMjUiOiB7CiAgICAgICAgICAgICJyZXNvdXJjZXMiOiBbCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJ1c2VyX2F1dGhvcml0aWVzIjogewogICAgICAgICAgICAgICAgInJfMDAxIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb21wYW55X21vZGlmeSIsCgkJCQkJInhjX2NvbXBhbnlfdmlldyIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2RlbCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2VkaXQiLAoJCQkJCSJ4Y19jb3Vyc2VfYmFzZV9saXN0IiwKCQkJCQkieGNfY291cnNlX2Jhc2Vfc2F2ZSIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX3ZpZXciLAoJCQkJCSJ4Y19jb3Vyc2VfcHVibGlzaCIsCgkJCQkJInhjX21hcmtldF9zYXZlX21vZGlmeSIsCgkJCQkJInhjX21hcmtldF92aWV3IiwKCQkJCQkieGNfbWVkaWFfZGVsIiwKCQkJCQkieGNfbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX3ByZXZpZXciLAoJCQkJCSJ4Y19tZWRpYV9zYXZlIiwKCQkJCQkieGNfdGVhY2hlcl9saXN0IiwKCQkJCQkieGNfdGVhY2hlcl9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaGVyX3NhdmUiLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2NvcnJlY3Rpb24iLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2xpc3QiLAoJCQkJCSJ4Y190ZWFjaHBsYW53b3JrX2RlbCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfbGlzdCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfc2F2ZV9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaHBsYW5fZGVsIiwKCQkJCQkieGNfdGVhY2hwbGFuX3NhdmVfbW9kaWZ5IiwKCQkJCQkieGNfdGVhY2hwbGFuX3ZpZXciCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgInJfMDAyIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb3Vyc2VfYWRtaW5fbGlzdCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2NvbW1pdCIsCgkJCQkJInhjX3N5c3RlbV9jYXRlZ29yeSIsCgkJCQkJInhjX21fbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX2F1ZGl0IgogICAgICAgICAgICAgICAgXQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfSwKICAgICJ1c2VyX25hbWUiOiAieGMtdXNlci1maXJzdCIsCiAgICAic2NvcGUiOiBbCiAgICAgICAgInJlYWQiCiAgICBdLAogICAgIm1vYmlsZSI6ICIxNTAxMjM0NTY3OCIsCiAgICAiZXhwIjogMTYwNjUyNTEyMiwKICAgICJjbGllbnRfYXV0aG9yaXRpZXMiOiBbCiAgICAgICAgIlJPTEVfVVNFUiIKICAgIF0sCiAgICAianRpIjogIjFlYjdlOTg3LWQ3YzItNDBmNS1iMGQ2LWNkNjEzOWNiMThlMCIsCiAgICAiY2xpZW50X2lkIjogInhjLWNvbS1wbGF0Zm9ybSIsCiAgICAiY29tcGFueUlkIjogMTIzMjE0MTQyNQp9