学成在线项目笔记第二期 全局异常处理 异常问题分析 在service方法中有很多的参数合法性校验,当参数不合法则抛出异常。我们的需求是当正常操作时按接口要求返回数据,当非正常流程时要获取异常信息进行记录,并提示给用户 。所以,异常处理除了输出在日志中,还需要提示给用户,前端和后端需要作一些约定:
错误提示信息统一以json格式返回给前端。
以HTTP状态码决定当前是否出错,非200为操作异常。
如何规范异常信息?
代码中统一抛出项目的自定义异常类型
规范了异常类型就可以统一去捕获这一类或几类的异常,获取异常信息。
如果捕获了非项目自定义的异常类型 统一向用户提示“执行过程异常,请重试”的错误信息。
如何捕获异常?
代码统一用try/catch方式去捕获代码比较臃肿,可以通过SpringMVC提供的控制器增强类统一由一个类去完成异常的捕获。
全局异常处理器实现
在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>
在 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;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;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;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;@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完成对请求参数的基本合法性进行校验。
实现统一校验
在Base工程添加spring-boot-starter-validation的依赖
1 2 3 4 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
对 对象接收参数 添加校验规则
以新增课程为例,此接口使用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;@Data public class AddCourseDto { @NotEmpty(groups = {ValidationGroups.Inster.class},message = "添加课程名称不能为空") @NotEmpty(groups = {ValidationGroups.Update.class},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; private String qq; private String wechat; private String phone; private Integer validDays; }
在javax.validation.constraints包下有很多这样的校验注解,上边用到了@NotEmpty和@Size两个注解,@NotEmpty表示属性不能为空,@Size表示限制属性内容的长短。
开启校验,在controller方法中添加@Validated注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @PostMapping("/course") public CourseBaseInfoDto createCourseBase (@RequestBody @Validated AddCourseDto addCourseDto) { Long companyId = 1232141425L ; return courseBaseInfoService.createCourseBase(companyId, addCourseDto); }
在统一异常处理器中捕获异常,解析出异常信息。在全局异常处理器中添加:
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()); }
重启内容管理服务
使用httpclient进行测试,将必填项设置为空,“适用人群” 属性的内容设置1个字,执行测试
接口响应结果应如下,说明校验器生效:
1 2 3 { "errMessage": "课程名称不能为空 课程分类不能为空 课程分类不能为空 适用人群内容过少 " }
实现分组校验 有时候在同一个属性上设置一个校验规则不能满足要求,比如:订单编号由系统生成,在添加订单时要求订单编号为空,在更新 订单时要求订单编写不能为空。此时就用到了分组校验,同一个属性定义多个校验规则属于不同的分组。
在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;public class ValidationGroups { public interface Inster { } public interface Update { } public interface Delete { } }
在定义校验规则时指定分组,仍是以 新增课程的接收对象为例:
1 2 3 4 @NotEmpty(groups = {ValidationGroups.Inster.class},message = "添加课程名称不能为空") @NotEmpty(groups = {ValidationGroups.Update.class},message = "修改课程名称不能为空") private String name;
在Controller方法中启动校验规则指定要使用的分组名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @PostMapping("/course") public CourseBaseInfoDto createCourseBase (@RequestBody @Validated({ValidationGroups.Inster.class}) AddCourseDto addCourseDto) { Long companyId = 1232141425L ; return courseBaseInfoService.createCourseBase(companyId, addCourseDto); }
再次测试
由于这里指定了Insert分组,所以抛出 异常信息:添加课程名称不能为空。
如果修改分组为ValidationGroups.Update.class,异常信息为:修改课程名称不能为空。
内容管理模块content 修改课程信息接口实现 需求分析
进入课程列表查询存在“编辑”按钮
实现编辑按钮功能
因为课程审核通过方可发布,任何时候都 可以编辑,进入编辑界面显示出当前课程的信息。下图是编辑课程的界面:
修改成功自动进入课程计划编辑页面
数据模型 修改课程的涉及到的数据表是课程基本信息表(course_base) 和 课程营销表(course_category)
进入课程编辑界面
界面中显示了课程的当前信息,需要根据课程id查询课程基本和课程营销信息,显示在表单 上。
编辑、提交
修改课程提交的数据比新增课程多了一项课程id,因为修改课程需要针对某个课程进行修改。
保存数据
编辑完成保存课程基础信息和课程营销信息。更新课程基本信息表中的修改人、修改时间。
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;@Data public class EditCourseDto extends AddCourseDto { 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 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 public CourseBaseInfoDto getCourseBaseInfo (Long courseId) ;
修改课程信息功能实现
创建service接口
1 2 3 4 5 6 7 8 9 public CourseBaseInfoDto updateCourseBase (Long companyId, EditCourseDto dto) ;
创建 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;@Service @Slf4j public class CourseMarketServiceImpl extends ServiceImpl <CourseMarketMapper, CourseMarket> implements CourseMarketService {}
保存营销信息的校验
相比于视频,我个人增加了一些判断条件(不加的话数据库会有 “原价为空,现价有值” 或者 “定义免费,实际有价格” 的离谱场面):
设置免费时的原价和现价的金额设置
判空时原价和现价的金额设定
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 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 ; }
实现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; @Transactional @Override public CourseBaseInfoDto updateCourseBase (Long companyId, EditCourseDto dto) { 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); }
接口测试 直接前后端联调打开前端页面,修改前面自行定义的新增课程的信息
查询课程计划(课程目录)接口实现 需求分析 课程基本信息添加或修改成功将自动进入课程计划编辑器界面,课程计划分为两级:章节和小节。
数据模型 课程计划主要关联两张表,课程计划表(teachplan) 和 课程视频表(teachplan_media),两张表是一对一关系,每个课程计划只能在teachplan_media表中存在一个视频。
课程计划有两个级别,第一级为章,grade为1、第二级为小节,grade为2
第二级的parentid为第一级的id
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;@Data public class TeachplanDto extends Teachplan { TeachplanMedia teachplanMedia; List<TeachplanDto> teachPlanTreeNodes; }
service–接口开发 修改Mapper
在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;public interface TeachplanMapper extends BaseMapper <Teachplan> { public List<TeachplanDto> selectTreeNodes (Long courseId) ; }
修改 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 >
注意:
sql文档两个都是 left join ,视频挖的坑是用了 inner join,会导致章 这一级无法显示
association 内出现无法找到该类的报错,但运行是没有问题的,不清楚是idea的原因还是什么,暂时忽略
单元测试
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;public interface TeachplanService { 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;@RestController public class TeachplanController { @Autowired TeachplanService teachplanService; @GetMapping("/teachplan/{courseId}/tree-nodes") public List<TeachplanDto> getTreeNodes (@PathVariable Long courseId) { return teachplanService.findTeachplayTree(courseId); } }
单元测试
使用httpclient测试,找一个有课程计划的课程进行测试
1 2 3 4 ### 查询某个课程的课程计划 GET {{content_host}}/content/teachplan/117/tree-nodes Content-Type: application/json
前后端联调,观察课程计划获取是否成功。
由于是新增的课程,课程计划为空。
新增/修改课程计划接口实现 需求分析
进入课程计划界面
点击“添加章”新增第一级课程计划,新增成功自动刷新课程计划列表。
点击“添加小节”向某个第一级课程计划下添加小节,新增成功自动刷新课程计划列表,且新增的课程计划自动排序到最后。
点击“章”、“节”的名称,可以修改名称、选择是否免费。
数据模型
新增第一级课程计划
名称默认为:新章名称 [点击修改]
grade:1
orderby: 所属课程中同级别下排在最后
新增第二级课程计划
名称默认为:新小节名称 [点击修改]
grade:2
orderby: 所属课程计划中排在最后
修改第一级、第二级课程计划的名称,修改第二级课程计划是否免费
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;@Data @ToString public class SaveTeachplanDto { private Long id; private String pname; private Long parentid; private Integer grade; private String mediaType; private Long courseId; private Long coursePubId; private String isPreview; }
service–接口开发 定义service接口 定义保存课程计划的Service接口
1 2 3 4 5 6 7 8 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) { 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); } } 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 @PostMapping("/teachplan") public void saveTeachplan ( @RequestBody SaveTeachplanDto teachplan) { teachplanService.saveTeachplan(teachplan); }
单元测试 这里我直接使用前后端联调,在前端页面进行课程计划的创建和修改,检查数据库的变更情况。
注意:day03 的bug修改已在代码中体现——主要是对Mapper文件的sql进行修改
第二期OVER~~~~ 下期预告:
TODO 媒资管理 Nacos