学成在线项目笔记第二期

全局异常处理

异常问题分析

在service方法中有很多的参数合法性校验,当参数不合法则抛出异常。我们的需求是当正常操作时按接口要求返回数据,当非正常流程时要获取异常信息进行记录,并提示给用户。所以,异常处理除了输出在日志中,还需要提示给用户,前端和后端需要作一些约定:

  1. 错误提示信息统一以json格式返回给前端。

  2. 以HTTP状态码决定当前是否出错,非200为操作异常。

如何规范异常信息?

  1. 代码中统一抛出项目的自定义异常类型
  2. 规范了异常类型就可以统一去捕获这一类或几类的异常,获取异常信息。
  3. 如果捕获了非项目自定义的异常类型统一向用户提示“执行过程异常,请重试”的错误信息。

如何捕获异常?

代码统一用try/catch方式去捕获代码比较臃肿,可以通过SpringMVC提供的控制器增强类统一由一个类去完成异常的捕获。

全局异常处理器实现

  1. 在base基础工程添加需要依赖的包
1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
  1. 在 base工程中创建exception文件夹,用于存放异常处理相关的文件
  • 定义通用的异常信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.xuecheng.base.exception;

/**
* @author xioaming
* @version 1.0
* @description 通用错误信息
* @date 2023/1/20 17:16
*/
public enum CommonError {
UNKOWN_ERROR("执行过程异常,请重试。"),
PARAMS_ERROR("非法参数"),
OBJECT_NULL("对象为空"),
QUERY_NULL("查询结果为空"),
REQUEST_NULL("请求参数为空");

private String errMessage;

public String getErrMessage() {
return errMessage;
}

private CommonError( String errMessage) {
this.errMessage = errMessage;
}
}
  • 自定义异常类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.xuecheng.base.exception;

/**
* @author xioaming
* @version 1.0
* @description 学成在线项目异常类
* @date 2023/1/20 17:17
*/
public class XueChengException extends RuntimeException{

private static final long serialVersionUID = 3572545756501312360L;
private String errMessage;

public XueChengException() {
super();
}

public XueChengException(String errMessage) {
super(errMessage);
this.errMessage = errMessage;
}

public String getErrMessage() {
return errMessage;
}

public static void cast(CommonError commonError){
throw new XueChengException(commonError.getErrMessage());
}
public static void cast(String errMessage){
throw new XueChengException(errMessage);
}

}
  • 响应用户的统一类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.xuecheng.base.exception;

import java.io.Serializable;

/**
* @author xioaming
* @version 1.0
* @description 错误响应参数包装
* @date 2023/1/20 17:18
*/
public class RestErrorResponse implements Serializable {

private String errMessage;

public RestErrorResponse(String errMessage) {
this.errMessage = errMessage;
}

public String getErrMessage() {
return errMessage;
}

public void setErrMessage(String errMessage) {
this.errMessage = errMessage;
}
}
  • 全局异常处理器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.xuecheng.base.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
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;

import java.util.List;

/**
* @author xioaming
* @version 1.0
* @description 全局异常处理器
* @date 2023/1/20 17:20
*/
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

@ResponseBody
@ExceptionHandler(XueChengException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse customException(XueChengException e){
log.error("[系统异常]{}", e.getErrMessage(), e);
return new RestErrorResponse(e.getErrMessage());
}

@ResponseBody
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse exception(Exception e){
log.error("[系统异常]{}", e.getMessage(), e);
return new RestErrorResponse(CommonError.UNKOWN_ERROR.getErrMessage());
}
}

异常处理测试

以新增课程的service方法为例进行代码修改,修改 content工程下的service工程的 新增课程信息代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 @Override
public CourseBaseInfoDto createCourseBase(Long companyId,AddCourseDto dto) {
...
//合法性校验
if (StringUtils.isBlank(dto.getName())) {
throw new XueChengPlusException("课程名称为空");
}

if (StringUtils.isBlank(dto.getMt())) {
throw new XueChengPlusException("课程分类为空");
}

if (StringUtils.isBlank(dto.getSt())) {
throw new XueChengPlusException("课程分类为空");
}

if (StringUtils.isBlank(dto.getGrade())) {
throw new XueChengPlusException("课程等级为空");
}

if (StringUtils.isBlank(dto.getTeachmode())) {
throw new XueChengPlusException("教育模式为空");
}

if (StringUtils.isBlank(dto.getUsers())) {
throw new XueChengPlusException("适应人群");
}

if (StringUtils.isBlank(dto.getCharge())) {
throw new XueChengPlusException("收费规则为空");
}
。。。
if ("201001".equals(charge)) {
BigDecimal price = dto.getPrice();
if (ObjectUtils.isEmpty(price)) {
throw new XueChengPlusException("收费课程价格不能为空");
}
courseMarketNew.setPrice(dto.getPrice().floatValue());
}
。。。

再进行单元测试,故意将必填项改为 空值,查看项目报错的返回值是否为自己定义的文案。

JSR303校验

前端请求后端接口传输参数,在controller中和 在Service中 都需要校验

Contoller中校验请求参数的合法性,包括:必填项校验,数据格式校验,比如:是否是符合一定的日期格式,等。

Service中要校验的是业务规则相关的内容,比如:课程已经审核通过所以提交失败。

Service中根据业务规则去校验不方便写成通用代码,Controller中则可以将校验的代码写成通用代码。

JSR-303 定义了Bean Validation,即对bean属性进行校验。SpringBoot提供了JSR-303的支持,它就是spring-boot-starter-validation,它的底层使用Hibernate Validator,Hibernate Validator是Bean Validation 的参考实现。

所以,统一校验就是 在Controller层使用spring-boot-starter-validation完成对请求参数的基本合法性进行校验。

实现统一校验

  1. 在Base工程添加spring-boot-starter-validation的依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  1. 对象接收参数 添加校验规则

以新增课程为例,此接口使用AddCourseDto模型对象接收参数,所以进入AddCourseDto类,在属性上添加校验规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.xuecheng.contentModel.dto;

import com.xuecheng.base.exception.ValidationGroups;
import lombok.Data;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import java.math.BigDecimal;

/**
* @author xioaming
* @version 1.0
* @description 课程添加DTO
* @date 2023/1/18 21:22
*/
@Data
public class AddCourseDto {

@NotEmpty(groups = {ValidationGroups.Inster.class},message = "添加课程名称不能为空")
@NotEmpty(groups = {ValidationGroups.Update.class},message = "修改课程名称不能为空")
// @NotEmpty(message = "课程名称不能为空")
private String name;

@NotEmpty(message = "适用人群不能为空")
@Size(message = "适用人群内容过少",min = 10)
private String users;

//课程标签
private String tags;

@NotEmpty(message = "课程分类(大)不能为空")
private String mt;

@NotEmpty(message = "课程分类(小)不能为空")
private String st;

@NotEmpty(message = "课程等级不能为空")
private String grade;

//教学模式
private String teachmode;

//课程介绍
private String description;

//课程图片
private String pic;

@NotEmpty(message = "收费规则不能为空")
private String charge;

//价格
private Float price;

//原价
private Float originalPrice;

//qq
private String qq;

//微信
private String wechat;

//电话
private String phone;

//有效期
private Integer validDays;
}

在javax.validation.constraints包下有很多这样的校验注解,上边用到了@NotEmpty和@Size两个注解,@NotEmpty表示属性不能为空,@Size表示限制属性内容的长短。

image-20230124220557117
  1. 开启校验,在controller方法中添加@Validated注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @description 新增课程接口
* @param @Validated 校验注释(提供统一校验方法(什么都不加) 和 分组校验方法(ValidationGroups.Inster.class))
* @return
* @author xiaoming
* @date 2023/1/20 21:06
*/
//todo 新增课程中途取消新增,数据库信息怎么删除
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody @Validated AddCourseDto addCourseDto){
//机构id,由于认证系统没有上线暂时硬编码
Long companyId = 1232141425L;
return courseBaseInfoService.createCourseBase(companyId, addCourseDto);
}
  1. 在统一异常处理器中捕获异常,解析出异常信息。在全局异常处理器中添加:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ResponseBody
@ExceptionHandler(value = MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse doValidException(MethodArgumentNotValidException argumentNotValidException) {

BindingResult bindingResult = argumentNotValidException.getBindingResult();
StringBuffer errMsg = new StringBuffer();

List<FieldError> fieldErrors = bindingResult.getFieldErrors();
fieldErrors.forEach(error -> {
errMsg.append(error.getDefaultMessage()).append(",");
});
log.error(errMsg.toString());
return new RestErrorResponse(errMsg.toString());
}
  1. 重启内容管理服务

使用httpclient进行测试,将必填项设置为空,“适用人群” 属性的内容设置1个字,执行测试

接口响应结果应如下,说明校验器生效:

1
2
3
{
"errMessage": "课程名称不能为空 课程分类不能为空 课程分类不能为空 适用人群内容过少 "
}

实现分组校验

有时候在同一个属性上设置一个校验规则不能满足要求,比如:订单编号由系统生成,在添加订单时要求订单编号为空,在更新 订单时要求订单编写不能为空。此时就用到了分组校验,同一个属性定义多个校验规则属于不同的分组。

  1. 在base工程中添加 校验分组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.xuecheng.base.exception;

/**
* @author xioaming
* @version 1.0
* @description 定义到实体类上的分组校验规则(就是针对同一个实体类,在不同的情况下需要进行校验的规则进行分类设置)
* @date 2023/1/20 21:01
*/
public class ValidationGroups {
/**
* @description 新增情况下的校验规则
* @author xiaoming
* @date 2023/1/20 21:09
*/
public interface Inster {
}

/**
* @description 修改情况下的校验规则
* @author xiaoming
* @date 2023/1/20 21:10
*/
public interface Update {
}

/**
* @description 删除情况下的校验规则
* @author xiaoming
* @date 2023/1/20 21:10
*/
public interface Delete {
}
}
  1. 在定义校验规则时指定分组,仍是以 新增课程的接收对象为例:
1
2
3
4
    @NotEmpty(groups = {ValidationGroups.Inster.class},message = "添加课程名称不能为空")
@NotEmpty(groups = {ValidationGroups.Update.class},message = "修改课程名称不能为空")
// @NotEmpty(message = "课程名称不能为空")
private String name;
  1. 在Controller方法中启动校验规则指定要使用的分组名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @description 新增课程接口
* @param @Validated 校验注释(提供统一校验方法(什么都不加) 和 分组校验方法(ValidationGroups.Inster.class))
* @return
* @author xiaoming
* @date 2023/1/20 21:06
*/
//todo 新增课程中途取消新增,数据库信息怎么删除
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody @Validated({ValidationGroups.Inster.class}) AddCourseDto addCourseDto){
//机构id,由于认证系统没有上线暂时硬编码
Long companyId = 1232141425L;
return courseBaseInfoService.createCourseBase(companyId, addCourseDto);
}
  1. 再次测试

由于这里指定了Insert分组,所以抛出 异常信息:添加课程名称不能为空。

如果修改分组为ValidationGroups.Update.class,异常信息为:修改课程名称不能为空。

内容管理模块content

修改课程信息接口实现

需求分析

  1. 进入课程列表查询存在“编辑”按钮
image-20230124221454775
  1. 实现编辑按钮功能

因为课程审核通过方可发布,任何时候都 可以编辑,进入编辑界面显示出当前课程的信息。下图是编辑课程的界面:

image-20230124221536822
  1. 修改成功自动进入课程计划编辑页面

数据模型

修改课程的涉及到的数据表是课程基本信息表(course_base) 和 课程营销表(course_category)

  1. 进入课程编辑界面

界面中显示了课程的当前信息,需要根据课程id查询课程基本和课程营销信息,显示在表单上。

  1. 编辑、提交

修改课程提交的数据比新增课程多了一项课程id,因为修改课程需要针对某个课程进行修改。

  1. 保存数据

编辑完成保存课程基础信息和课程营销信息。更新课程基本信息表中的修改人、修改时间。

model–生成DTO

定义修改课程提交的数据模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.xuecheng.contentModel.dto;

import lombok.Data;

/**
* @author xioaming
* @version 1.0
* @description 添加课程DTO
* @date 2023/1/21 19:49
*/
@Data
public class EditCourseDto extends AddCourseDto{

//课程id
private Long id;
}

service–接口开发

根据id查询课程信息

查询课程信息的Service方法在新增课程接口开发中已实现,无需实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* @description 根据课程id查询课程基本信息,包括基本信息和营销信息
* @param courseId 课程id
* @return com.xuecheng.contentModel.dto.CourseBaseInfoDto
* @author xiaoming
* @date 2023/1/18 21:55
*/
public CourseBaseInfoDto getCourseBaseInfo(Long courseId){
//课程基本信息
CourseBase courseBase = courseBaseMapper.selectById(courseId);
//课程营销信息
CourseMarket courseMarket = courseMarketMapper.selectById(courseId);

if(courseBase == null){
return null;
}

//组成要返回的数据
CourseBaseInfoDto courseBaseInfoDto = new CourseBaseInfoDto();
BeanUtils.copyProperties(courseBase,courseBaseInfoDto);
if(courseMarket != null){
BeanUtils.copyProperties(courseMarket,courseBaseInfoDto);
}

//查询分类名称
CourseCategory courseCategoryBySt = courseCategoryMapper.selectById(courseBase.getSt());//小分类
courseBaseInfoDto.setStName(courseCategoryBySt.getName());
CourseCategory courseCategoryByMt = courseCategoryMapper.selectById(courseBase.getMt());//大分类
courseBaseInfoDto.setMtName(courseCategoryByMt.getName());

return courseBaseInfoDto;
}

只需要将这个方法提到service接口上,使 controller 能直接通过接口调用该方法:

1
2
3
4
5
6
7
8
/**
* @description 根据id查询课程基本信息
* @param courseId 课程id
* @return com.xuecheng.contentModel.dto.CourseBaseInfoDto
* @author xiaoming
* @date 2023/1/21 19:52
*/
public CourseBaseInfoDto getCourseBaseInfo(Long courseId);

修改课程信息功能实现

  1. 创建service接口
1
2
3
4
5
6
7
8
9
/**
* @description 修改课程信息
* @param companyId 机构id,需要校验:只能修改本机构的课程
* @param dto 课程信息
* @return com.xuecheng.contentModel.dto.CourseBaseInfoDto
* @author xiaoming
* @date 2023/1/21 19:57
*/
public CourseBaseInfoDto updateCourseBase(Long companyId, EditCourseDto dto);
  1. 创建 courseMarketServiceImpl 接口实现类,便于调用 courseMarketService 的 saveOrUpdate方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.xuecheng.contentService.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xuecheng.contentModel.po.CourseMarket;
import com.xuecheng.contentService.mapper.CourseMarketMapper;
import com.xuecheng.contentService.service.CourseMarketService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
* @author xioaming
* @version 1.0
* @description 课程营销信息service实体类,主要为courseBase的service业务层提供saveOrUpdate方法
* @date 2023/1/21 20:04
*/
@Service
@Slf4j
public class CourseMarketServiceImpl extends ServiceImpl<CourseMarketMapper, CourseMarket> implements CourseMarketService {

}
  1. 保存营销信息的校验

相比于视频,我个人增加了一些判断条件(不加的话数据库会有 “原价为空,现价有值” 或者 “定义免费,实际有价格” 的离谱场面):

  • 设置免费时的原价和现价的金额设置
  • 判空时原价和现价的金额设定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @description 保存营销信息的校验
* @param courseMarket 营销信息
* @return
* @author xiaoming
* @date 2023/1/21 21:46
*/
private int saveCourseMarket(CourseMarket courseMarket) {
String charge = courseMarket.getCharge();
if(StringUtils.isBlank(charge)){
XueChengException.cast("请设置收费规则");
}
if(charge.equals("201001")){
Float price = courseMarket.getPrice();
Float originalPrice = courseMarket.getOriginalPrice();
if(originalPrice == null || originalPrice <= 0 || price == null || price.floatValue() <= 0){
XueChengException.cast("课程设置了收费价格不能为空且必须大于0");
}
}
if (charge.equals("201000")){
Float price = courseMarket.getPrice();
Float originalPrice = courseMarket.getOriginalPrice();
if((originalPrice != null && originalPrice != 0) || (price != null && price.floatValue() != 0)){
XueChengException.cast("课程设置了免费,请勿添加金额");
}
courseMarket.setPrice(0f);
courseMarket.setOriginalPrice(0f);
}
boolean b = courseMarketService.saveOrUpdate(courseMarket);
return b ? 1 : -1;
}
  1. 实现service接口实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
   @Autowired
CourseMarketServiceImpl courseMarketService;

/**
* @description 修改课程信息
* @param companyId 机构id
* @param dto 编辑信息
* @return com.xuecheng.contentModel.dto.CourseBaseInfoDto
* @author xiaoming
* @date 2023/1/24 22:41
*/
@Transactional
@Override
public CourseBaseInfoDto updateCourseBase(Long companyId, EditCourseDto dto) {
// 校验课程id
if (courseBaseMapper.selectById(dto.getId()) == null){
XueChengException.cast("课程不存在");
}
//校验机构与课程是否对应
Long courseId = dto.getId();
CourseBase courseBaseUpdate = courseBaseMapper.selectById(courseId);
if (!companyId.equals(courseBaseUpdate.getCompanyId())){
XueChengException.cast("只允许修改本机构的课程");
}
//封装基本信息数据
BeanUtils.copyProperties(dto, courseBaseUpdate);
courseBaseUpdate.setChangeDate(LocalDateTime.now());

//更新课程基本信息
int insert = courseBaseMapper.updateById(courseBaseUpdate);

//封装营销信息数据
CourseMarket courseMarket = new CourseMarket();
BeanUtils.copyProperties(dto, courseMarket);

saveCourseMarket(courseMarket);

//查询课程信息
CourseBaseInfoDto courseBaseInfo = this.getCourseBaseInfo(dto.getId());
return courseBaseInfo;
}

api–定义接口

根据id查询课程信息

1
2
3
4
@GetMapping("/course/{courseId}")
public CourseBaseInfoDto getCourseBaseById(@PathVariable @Validated Long courseId){
return courseBaseInfoService.getCourseBaseInfo(courseId);
}

修改课程信息

1
2
3
4
5
@PutMapping("/course")
public CourseBaseInfoDto modifyCourseBase(@RequestBody @Validated EditCourseDto editCourseDto){
Long companyId = 1232141425L;
return courseBaseInfoService.updateCourseBase(companyId, editCourseDto);
}

接口测试

直接前后端联调打开前端页面,修改前面自行定义的新增课程的信息

查询课程计划(课程目录)接口实现

需求分析

课程基本信息添加或修改成功将自动进入课程计划编辑器界面,课程计划分为两级:章节和小节。

image-20230125123539885

数据模型

课程计划主要关联两张表,课程计划表(teachplan) 和 课程视频表(teachplan_media),两张表是一对一关系,每个课程计划只能在teachplan_media表中存在一个视频。

  • 课程计划有两个级别,第一级为章,grade为1、第二级为小节,grade为2
  • 第二级的parentid为第一级的id
image-20230125123834342 image-20230125123851024

model–生成DTO

根据前端响应结果,自定义模型类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.xuecheng.contentModel.dto;

import com.xuecheng.contentModel.po.Teachplan;
import com.xuecheng.contentModel.po.TeachplanMedia;
import lombok.Data;

import java.util.List;

/**
* @author xioaming
* @version 1.0
* @description 课程计划树型结构dto
* @date 2023/1/22 11:22
*/
@Data
public class TeachplanDto extends Teachplan {

//课程计划关联的媒资信息
TeachplanMedia teachplanMedia;

//子结点
List<TeachplanDto> teachPlanTreeNodes;
}

service–接口开发

修改Mapper

  1. 在TeachplanMapper中自定义方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.xuecheng.contentService.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xuecheng.contentModel.dto.TeachplanDto;
import com.xuecheng.contentModel.po.Teachplan;

import java.util.List;

/**
* @author 15182
* @description 针对表【teachplan(课程计划)】的数据库操作Mapper
* @createDate 2023-01-16 16:07:42
* @Entity com.xuecheng.contentModel.po.Teachplan
*/
public interface TeachplanMapper extends BaseMapper<Teachplan> {

/**
* @description 查询某课程的课程计划,组成树型结构
* @param courseId 课程id
* @return com.xuecheng.contentModel.dto.TeachplanDto
* @author xiaoming
* @date 2023/1/23 14:51
*/
public List<TeachplanDto> selectTreeNodes(Long courseId);
}
  1. 修改 TeachplanMapper.xml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<?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.contentService.mapper.TeachplanMapper">

<sql id="Base_Column_List">
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
</sql>

<!-- 课程分类树型结构查询映射结果 -->
<resultMap id="treeNodeResultMap" type="com.xuecheng.contentModel.dto.TeachplanDto">
<id column="one_id" property="id"/>
<!-- 一级数据映射 -->
<id column="one_id" property="id"/>
<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="startTime"/>
<result column="one_endTime" property="endTime"/>
<result column="one_orderby" property="orderby"/>
<result column="one_courseId" property="courseId"/>
<result column="one_coursePubId" property="coursePubId"/>
<!-- 一级中包含多个二级数据 -->
<collection property="teachPlanTreeNodes" ofType="com.xuecheng.contentModel.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="startTime"/>
<result column="two_endTime" property="endTime"/>
<result column="two_orderby" property="orderby"/>
<result column="two_courseId" property="courseId"/>
<result column="two_coursePubId" property="coursePubId"/>
<association property="teachplanMedia" javaType="com.xuecheng.contentModel.po.TeachplanMedia">
<id 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>

<!--课程计划树型结构查询-->
<select id="selectTreeNodes" resultMap="treeNodeResultMap" parameterType="long">
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_startTime,
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,

m.id teachplanMediaId,
m.media_id mediaId,
m.teachplan_id teachplanId,
m.media_fileName mediaFileName
from teachplan one
LEFT JOIN teachplan two on two.parentid = one.id
LEFT JOIN teachplan_media m on m.teachplan_id = two.id
where one.parentid = 0
and one.course_id = #{value}
order by one.orderby,
two.orderby
</select>

</mapper>

注意:

  1. sql文档两个都是 left join,视频挖的坑是用了 inner join,会导致这一级无法显示
  2. association 内出现无法找到该类的报错,但运行是没有问题的,不清楚是idea的原因还是什么,暂时忽略
  1. 单元测试
1
2
3
4
5
@Test
void testCourseCategoryMapper(){
List<CourseCategoryTreeDto> courseCategoryTreeDtos = courseCategoryMapper.selectTreeNodes();
System.out.println(courseCategoryTreeDtos);
}

定义service接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.xuecheng.contentService.service;

import com.xuecheng.contentModel.dto.SaveTeachplanDto;
import com.xuecheng.contentModel.dto.TeachplanDto;

import java.util.List;

/**
* @author xioaming
* @version 1.0
* @description 课程基本信息管理业务接口
* @date 2023/1/22 13:54
*/
public interface TeachplanService {

/**
* @description 查询课程计划树型结构
* @param courseId
* @return List<TeachplanDto>
* @author xiaoming
* @date 2023/1/22 13:55
*/
public List<TeachplanDto> findTeachplayTree(Long courseId);

}

定义service接口实现类

1
2
3
4
@Override
public List<TeachplanDto> findTeachplayTree(Long courseId) {
return teachplanMapper.selectTreeNodes(courseId);
}

api–定义接口

实现接口层代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.xuecheng.contentApi.controller;

import com.xuecheng.contentModel.dto.SaveTeachplanDto;
import com.xuecheng.contentModel.dto.TeachplanDto;
import com.xuecheng.contentService.service.TeachplanService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
* @author xioaming
* @version 1.0
* @description 课程计划编辑接口
* @date 2023/1/22 11:24
*/
@RestController
public class TeachplanController {

@Autowired
TeachplanService teachplanService;

/**
* @description 查询课程计划
* @param courseId 课程id
* @return
* @author xiaoming
* @date 2023/1/22 11:25
*/
@GetMapping("/teachplan/{courseId}/tree-nodes")
public List<TeachplanDto> getTreeNodes(@PathVariable Long courseId){
return teachplanService.findTeachplayTree(courseId);
}
}

单元测试

  1. 使用httpclient测试,找一个有课程计划的课程进行测试
1
2
3
4
### 查询某个课程的课程计划

GET {{content_host}}/content/teachplan/117/tree-nodes
Content-Type: application/json
  1. 前后端联调,观察课程计划获取是否成功。

    • 进入课程编辑页面

    • 保存进入下一步

        - 进入新增课程页面
      
        - 新增课程成功,自动进入课程计划编辑界面
      

​ 由于是新增的课程,课程计划为空。

新增/修改课程计划接口实现

需求分析

  1. 进入课程计划界面
  1. 点击“添加章”新增第一级课程计划,新增成功自动刷新课程计划列表。

  2. 点击“添加小节”向某个第一级课程计划下添加小节,新增成功自动刷新课程计划列表,且新增的课程计划自动排序到最后。

  3. 点击“章”、“节”的名称,可以修改名称、选择是否免费。

image-20230125125823863

数据模型

  1. 新增第一级课程计划

    • 名称默认为:新章名称 [点击修改]

    • grade:1

    • orderby: 所属课程中同级别下排在最后

  2. 新增第二级课程计划

    • 名称默认为:新小节名称 [点击修改]

    • grade:2

    • orderby: 所属课程计划中排在最后

  3. 修改第一级、第二级课程计划的名称,修改第二级课程计划是否免费

model–生成DTO

定义接收请求参数的数据模型类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.xuecheng.contentModel.dto;

import lombok.Data;
import lombok.ToString;

/**
* @author xioaming
* @version 1.0
* @description 保存课程计划dto,包括新增、修改
* @date 2023/1/23 21:01
*/
@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;

}

service–接口开发

定义service接口

定义保存课程计划的Service接口

1
2
3
4
5
6
7
8
/**
* @description 保存课程计划信息(新增/修改)
* @param teachplanDto 课程计划信息
* @return void
* @author xiaoming
* @date 2023/1/23 21:04
*/
public void saveTeachplan(SaveTeachplanDto teachplanDto);

定义service接口实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@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 newTeachplan = new Teachplan();

//设置排序号
newTeachplan.setOrderby(count + 1);
BeanUtils.copyProperties(teachplanDto, newTeachplan);
teachplanMapper.insert(newTeachplan);
}
}

/**
* @description 获取最新的排序号
* @param courseId 课程id
* @param parentId 父课程id
* @return int count(最新排序号)
* @author xiaoming
* @date 2023/1/23 21:13
*/
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;
}

api–定义接口

实现接口层代码

调用service方法完成课程计划的创建和修改

1
2
3
4
5
6
7
8
9
10
/**
* @description 创建或修改课程计划
* @param teachplan 课程计划
* @author xiaoming
* @date 2023/1/23 21:03
*/
@PostMapping("/teachplan")
public void saveTeachplan( @RequestBody SaveTeachplanDto teachplan){
teachplanService.saveTeachplan(teachplan);
}

单元测试

这里我直接使用前后端联调,在前端页面进行课程计划的创建和修改,检查数据库的变更情况。

注意:day03 的bug修改已在代码中体现——主要是对Mapper文件的sql进行修改

第二期OVER~~~~

下期预告:

TODO 媒资管理 Nacos