【网课平台】Day2.异常的统一处理与JSR303入参校验

有时候,不是因为你没有能力,也不是因为你缺少勇气,只是因为你付出的努力还太少,所以,成功便不会走向你。而你所需要做的,就是坚定你的梦想,你的目标,你的未来,然后以不达目的誓不罢休的那股劲,去付出你的努力,成功就会慢慢向你靠近。

导读:本篇文章讲解 【网课平台】Day2.异常的统一处理与JSR303入参校验,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

1、课程分类选项查询

1.1 需求分析

先看新增页面的UI设计图:
在这里插入图片描述

  • 课程等级、课程类型这两个来源于字典表,这里的数据前端从system服务拿
  • 课程分类不是简单的几个选项,而是可以展开的一个树形结构,通过父结点id将各元素组成一个树,得单独存一张表:
  • 精髓就是:每条数据,根据parentId能知道它爹是谁,而它的id就是它儿子的parentId,WHERE parentId = Id就知道了它的儿子是谁。
    在这里插入图片描述
    数据表字段说明:
    在这里插入图片描述

1.2 开发PO类

根据课程分类表创建课程分类表的po类:

在这里插入图片描述

1.3 接口分析与定义

课程分类要返回全部的课程分类,以树形结构

JSON
{
"id" : "1-2",
"isLeaf" : null,
"isShow" : null,
"label" : "移动开发",
"name" : "移动开发",
"orderby" : 2,
"parentid" : "1",
"childrenTreeNodes" : [
               {
                  "childrenTreeNodes" : null,
                  "id" : "1-2-1",
                  "isLeaf" : null,
                  "isShow" : null,
                  "label" : "微信开发",
                  "name" : "微信开发",
                  "orderby" : 1,
                  "parentid" : "1-2"
               },
               {
                  "childrenTreeNodes" : null,
                  "id" : "1-2-2",
                  "isLeaf" : null,
                  "isShow" : null,
                  "label" : "app开发",
                  "name" : "app开发",
                  "orderby" : 1,
                  "parentid" : "1-2"
               }
               ]
 }

即除了返回表中的基本数据外,还要返回他的子节点属性,即childrenTreeNode属性,这是一个数组。注意这里的子节点也可能有自己的子节点,因此子节点也要有childrenTreeNodes属性,不过这里值为null,由此考虑定义一个dto类:

/**
 * @description 课程分类树型结点dto
 * @version 1.0
 */
@Data
public class CourseCategoryTreeDto extends CourseCategory{

  List<CourseCategoryTreeDto> childrenTreeNodes;
}

注意这里的dto对po的继承.接下来定义接口:

/**
 * 数据字典 前端控制器
 */
@Slf4j
@RestController
public class CourseCategoryController {

    
    @GetMapping("/course-category/tree-nodes")
    public List<CourseCategoryTreeDto> queryTreeNodes() {
       return null;
    }
}

注意这里的dto也即vo

1.4 开发Mapper

当前树形结构的层级固定,都是只有两级,可以使用表的自联结查询:

select
       one.id            one_id,
       one.name          one_name,
       one.parentid      one_parentid,
       one.orderby       one_orderby,
       one.label         one_label,
       two.id            two_id,
       two.name          two_name,
       two.parentid      two_parentid,
       two.orderby       two_orderby,
       two.label         two_label
   from course_category one
            inner join course_category two on one.id = two.parentid
   where one.parentid = 1
     and one.is_show = 1
     and two.is_show = 1
   order by one.orderby,
            two.orderby;

如果层级不固定,有的两级,有的三级,应该MySql递归查询:

with recursive t1 as (
select * from  course_category p where  id= '1'
union all
 select t.* from course_category t inner join t1 on t1.id = t.parentid
)
select *  from t1 order by t1.id, t1.orderby

  • t1是一个表名
  • 使用UNION ALL 不断将每次递归得到的数据加入到表t1中
  • select * from course_category p where id= ‘1’即t1表中的初始数据是id=1的记录,即根节点
  • 通过inner join t1 on t1.id = t.parentid 找到id=’1’的下级节点
  • 最后select * from t1拿递归得到的所有数据

在这里插入图片描述
这种方法是向下递归,即找到初始节点的所有下级节点,向上递归即:

with recursive t1 as (
select * from  course_category p where  id= '1-1-1'
union all
 select t.* from course_category t inner join t1 on t1.parentid = t.id
)
select *  from t1 order by t1.id, t1.orderby

此时:初始节点为1-1-1,通过递归找到它的父级节点。

mysql为了避免无限递归默认递归次数为1000,可以通过设置cte_max_recursion_depth参数增加递归深度,还可以通过max_execution_time限制执行时间,超过此时间也会终止递归操作

定义mapper接口

public interface CourseCategoryMapper extends BaseMapper<CourseCategory> {

    public List<CourseCategoryTreeDto> selectTreeNodes(String id);
    
}

mapper.xml文件:

<select id="selectTreeNodes" resultType="com.xuecheng.content.model.dto.CourseCategoryTreeDto" parameterType="string">
    with recursive t1 as (
        select * from  course_category p where  id= #{id}
        union all
        select t.* from course_category t inner join t1 on t1.id = t.parentid
    )
    select *  from t1 order by t1.id, t1.orderby
</select>

1.5 开发service层

此时需要对数据层返回的结果:
在这里插入图片描述

进行包装,得到需要的一个需要的形式,即含有子节点属性的

public interface CourseCategoryService {
    /**
     * 课程分类树形结构查询
     *
     * @return
     */
    public List<CourseCategoryTreeDto> queryTreeNodes(String id);
}

实现类:!!!!!!!!!

@Slf4j
@Service
public class CourseCategoryServiceImpl implements CourseCategoryService {

    @Autowired
    CourseCategoryMapper courseCategoryMapper;

    public List<CourseCategoryTreeDto> queryTreeNodes(String id) {
        List<CourseCategoryTreeDto> courseCategoryTreeDtos = courseCategoryMapper.selectTreeNodes(id);
    	//将list转map,以备使用,排除根节点
   	    Map<String, CourseCategoryTreeDto> mapTemp = courseCategoryTreeDtos.stream().filter(item->!id.equals(item.getId())).collect(Collectors.toMap(key -> key.getId(), value -> value, (key1, key2) -> key2));
    	//定义最终要返回的list
    	List<CourseCategoryTreeDto> categoryTreeDtoList = new ArrayList<>();
    	//依次遍历每个元素,排除根节点
    	courseCategoryTreeDtoList.stream().filter(item->!id.equals(item.getId())).forEach(item->{
    	//父节点是我们传入的id,即父节点是1,如1-1前端开发,那就塞进List
        if(item.getParentid().equals(id)){
            categoryTreeDtoList.add(item);
        }
        //找到当前节点的父节点
        CourseCategoryTreeDto courseCategoryTreeDto = mapTemp.get(item.getParentid());
        if(courseCategoryTreeDto!=null){
            if(courseCategoryTreeDto.getChildrenTreeNodes() ==null){
                courseCategoryTreeDto.setChildrenTreeNodes(new ArrayList<CourseCategoryTreeDto>());
            }
            //下边开始往ChildrenTreeNodes属性中放子节点
            courseCategoryTreeDto.getChildrenTreeNodes().add(item);
        }
    });
    return categoryTreeDtoList;
    }

}

关于流:

将集合转换为这么一种叫做 “流” 的元素序列,能够对集合中的每个元素进行一系列并行或串行的流水线操作。
在这里插入图片描述

  • xxx.stream().filter(item -> xx布尔条件)即过滤掉集合中满足这个布尔条件的元素.

关于List转Map:

【1】转型的背景:

在这里插入图片描述

【2】转型的代码手工实现:遍历List+put方法

在这里插入图片描述
【3】直接使用Collectors.toMap()方法,直接实现List转Map

在这里插入图片描述
【4】关于toMap方法的三个参数:

  • key -> key.getId() 即使用对象的id属性做为map的key值
  • value -> value 即选择原来的对象做为map的value值
  • (key1, key2) -> key2) 即如果v1与v2的key值相同,选择v1作为那个key所对应的value值

1.6 完善controller层

@Slf4j
@RestController
public class CourseCategoryController {

    @Autowired
    CourseCategoryService courseCategoryService;

    @GetMapping("/course-category/tree-nodes")
    public List<CourseCategoryTreeDto> queryTreeNodes() {
       return courseCategoryService.queryTreeNodes("1");
    }
}

接口正确返回结果,前端效果如下:

在这里插入图片描述

2、新增课程

2.1 需求分析

UI上来看:

  • 点击添加课程
    在这里插入图片描述

  • 选择课程形式为录播
    在这里插入图片描述

  • 点击下一步,到课程信息页面–包括课程基本信息和课程营销信息在这里插入图片描述课程营销信息:
    在这里插入图片描述

  • 点击下一步到达课程计划信息页面
    在这里插入图片描述。课程计划即课程的大纲目录
    。课程计划分为两级,章节和小节
    。每个小节需要上传课程视频,用户点击 小节的标题即开始播放视频
    。如果是直播课程则会进入直播间

  • 课程计划填写完后进入师资管理页面
    在这里插入图片描述

  • 可在这里添加教师信息
    在这里插入图片描述

至此,课程新增完成。即一门课程信息涉及:课程基本信息、课程营销信息、课程计划信息、课程师资信息。

2.2 设计表与开发po类

此处先写课程基本信息页面的接口,只向课程基本信息、课程营销信息添加记录。

表设计:

除了页面上已有的字段,还要设计一些必要字段和逻辑上的字段
在这里插入图片描述

  • 新建课程的初始审核状态为“未提交”、初始发布状态为“未发布”
  • 两张表通过id关联,一对一的关系

在这里插入图片描述

生成course_base、course_market表的PO类

2.3 接口分析与定义

分析:

  • 接口协议:HTTP POST,Content-Type为application/json
  • 传参:
    在这里插入图片描述
  • 响应:
    在这里插入图片描述
    即:
### 创建课程
POST {{content_host}}/content/course
Content-Type: application/json

{

  "mt": "",
  "st": "",
  "name": "",
  "pic": "",
  "teachmode": "200002",
  "users": "初级人员",
  "tags": "",
  "grade": "204001",
  "description": "",
  "charge": "201000",
  "price": 0,
  "originalPrice":0,
  "qq": "",
  "wechat": "",
  "phone": "",
  "validDays": 365
}

###响应结果如下
#成功响应结果如下
{
  "id": 109,
  "companyId": 1,
  "companyName": null,
  "name": "测试课程103",
  "users": "初级人员",
  "tags": "",
  "mt": "1-1",
  "mtName": null,
  "st": "1-1-1",
  "stName": null,
  "grade": "204001",
  "teachmode": "200002",
  "description": "",
  "pic": "",
  "createDate": "2022-09-08 07:35:16",
  "changeDate": null,
  "createPeople": null,
  "changePeople": null,
  "auditStatus": "202002",
  "status": 1,
  "coursePubId": null,
  "coursePubDate": null,
  "charge": "201000",
  "price": null,
  "originalPrice":0,
  "qq": "",
  "wechat": "",
  "phone": "",
  "validDays": 365
}

定义模型类.

请求参数相比course_base表的CourseBase类相比,不一致,得定义dto

package com.xuecheng.content.model.dto;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

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

/**
 * @description 添加课程dto
 */
@Data
@ApiModel(value="AddCourseDto", description="新增课程基本信息")
public class AddCourseDto {

 @NotEmpty(message = "课程名称不能为空")
 @ApiModelProperty(value = "课程名称", required = true)
 private String name;

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

 @ApiModelProperty(value = "课程标签")
 private String tags;

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

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

 @NotEmpty(message = "课程等级不能为空")
 @ApiModelProperty(value = "课程等级", required = true)
 private String grade;

 @ApiModelProperty(value = "教学模式(普通,录播,直播等)", required = true)
 private String teachmode;

 @ApiModelProperty(value = "课程介绍")
 private String description;

 @ApiModelProperty(value = "课程图片", required = true)
 private String pic;

 @NotEmpty(message = "收费规则不能为空")
 @ApiModelProperty(value = "收费规则,对应数据字典", required = true)
 private String charge;

 @ApiModelProperty(value = "价格")
 private Float price;
 @ApiModelProperty(value = "原价")
 private Float originalPrice;


 @ApiModelProperty(value = "qq")
 private String qq;

 @ApiModelProperty(value = "微信")
 private String wechat;
 @ApiModelProperty(value = "电话")
 private String phone;

 @ApiModelProperty(value = "有效期")
 private Integer validDays;
}

对比响应结果,CourseBase类,即po类不能满足要求,因此加vo(继承po后再补补)类:

package com.xuecheng.content.model.dto;

import com.xuecheng.content.model.po.CourseBase;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.math.BigDecimal;

/**
 * @description 课程基本信息vo
 */
@Data
public class CourseBaseInfoVo extends CourseBase {


 /**
  * 收费规则,对应数据字典
  */
 private String charge;

 /**
  * 价格
  */
 private Float price;


 /**
  * 原价
  */
 private Float originalPrice;

 /**
  * 咨询qq
  */
 private String qq;

 /**
  * 微信
  */
 private String wechat;

 /**
  * 电话
  */
 private String phone;

 /**
  * 有效期天数
  */
 private Integer validDays;

 /**
  * 大分类名称
  */
 private String mtName;

 /**
  * 小分类名称
  */
 private String stName;

}

定义接口:

@ApiOperation("新增课程基础信息")
@PostMapping("/course")
public CourseBaseInfoVo createCourseBase(@RequestBody AddCourseDto addCourseDto){
    return null;
}

2.4 开发Mapper层

直接extends BaseMapper<CourseBasePo>

2.5 开发Service层

定义接口:

/**
  * @description 添加课程基本信息
  * @param companyId  教学机构id,以后通过登录获取
  * @param addCourseDto  课程基本信息
 */
CourseBaseInfoVo createCourseBase(Long companyId,AddCourseDto addCourseDto);

写实现类:1.参数的合法性校验 2.业务逻辑处理

校验这里的异常,后续再优化

@Transactional
@Override
public CourseBaseInfoVo createCourseBase(Long companyId,AddCourseDto dto) {

 //合法性校验
 if (StringUtils.isBlank(dto.getName())) {
  throw new RuntimeException("课程名称为空");
 }

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

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

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

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

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

 if (StringUtils.isBlank(dto.getCharge())) {
  throw new RuntimeException("收费规则为空");
 }
   //新增Po对象,以后要向数据库写数据
   //将页面传入的dto对象中的值放入po中
  CourseBase courseBaseNew = new CourseBase();
  //将填写的课程信息赋值给新增对象
  BeanUtils.copyProperties(dto,courseBaseNew);
  //设置审核状态
  courseBaseNew.setAuditStatus("202002");
  //设置发布状态
  courseBaseNew.setStatus("203001");
  //机构id
  courseBaseNew.setCompanyId(companyId);
  //添加时间
  courseBaseNew.setCreateDate(LocalDateTime.now());
  //插入课程基本信息表
  //处理业务
  int insert = courseBaseMapper.insert(courseBaseNew);
  if(insert<=0){
    throw new RuntimeException("新增课程基本信息失败");
}

  //new课程营销po对象,向课程营销表保存课程营销信息
  CourseMarket courseMarketNew = new CourseMarket();
  BeanUtils.copyProperties(dto,courseMarketNew);
  Long courseId = courseBaseNew.getId();
  courseMarketNew.setId(courseId);
  //调用单独定义的保存营销信息方法
  if(saveCourseMarket(courseMarketNew)<=0){
        throw new RuntimeException("保存课程营销信息失败");
    }

  //查询课程基本信息及营销信息并返回
 return getCourseBaseInfo(courseId);
}



//单独定义一个方法,保存营销信息
private int saveCourseMarket(CourseMarket courseMarketNew){
    //收费规则
    String charge = courseMarketNew.getCharge();
    if(StringUtils.isBlank(charge)){
        throw new RuntimeException("收费规则没有选择");
    }
    //收费规则为收费
    if(charge.equals("201001")){
        if(courseMarketNew.getPrice() == null || courseMarketNew.getPrice().floatValue()<=0){
            throw new RuntimeException("课程为收费价格不能为空且必须大于0");
        }
    }
    //根据id从课程营销表查询
    CourseMarket courseMarketObj = courseMarketMapper.selectById(courseMarketNew.getId());
    if(courseMarketObj == null){
    	//插入新的营销规则
        return courseMarketMapper.insert(courseMarketNew);
    }else{
	    //更新营销规则,拷贝新传入的属性值
        BeanUtils.copyProperties(courseMarketNew,courseMarketObj);
        //id被覆盖,别忘了set一下
        courseMarketObj.setId(courseMarketNew.getId());
        //更新营销信息
        return courseMarketMapper.updateById(courseMarketObj);
    }
}



//定义一个方法,返回所有的课程信息,包括基本信息和营销信息,即VO类

public CourseBaseInfoVo getCourseBaseInfo(long courseId){

  CourseBase courseBase = courseBaseMapper.selectById(courseId);
  if(courseBase == null){
   return null;
  }
  CourseMarket courseMarket = courseMarketMapper.selectById(courseId);
  CourseBaseInfoVo courseBaseInfoVo = new CourseBaseInfoVo();
  BeanUtils.copyProperties(courseBase,courseBaseInfoVo);
  if(courseMarket != null){
   BeanUtils.copyProperties(courseMarket,courseBaseInfoVo);
  }

  //返回字段中要分类名称,要code换name,查询分类名称
  CourseCategory courseCategoryBySt = courseCategoryMapper.selectById(courseBase.getSt());
  courseBaseInfoVo.setStName(courseCategoryBySt.getName());
  CourseCategory courseCategoryByMt = courseCategoryMapper.selectById(courseBase.getMt());
  courseBaseInfoVo.setMtName(courseCategoryByMt.getName());

  return courseBaseInfoVo;

 }

  • 从dto对象get,往po对象set. 当属性很多时,这样很繁琐,直接使用BeanUtils.copyProperties(已有对象,目标对象)方法,只要二者属性名一致就可以拷贝
  • 注意,拷贝过程中,若po对象有值,而dto对象的这个属性为null,则po的这个属性会被覆盖为空,因此一些属性的set方copyProperties方法后

2.6 完善controller层

...

@ApiOperation("新增课程基础信息")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto addCourseDto){
    //机构id,由于认证系统没有上线暂时硬编码
    Long companyId = 1232141425L;
  return courseBaseInfoService.createCourseBase(companyId,addCourseDto);
}

3、异常处理

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

在这里插入图片描述

3.1 枚举定义通用的异常信息

在base下,定义出常用的异常信息:

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;
   }

}

3.2 自定义异常类型

public class MyServiceException extends RuntimeException {

   private String errMessage;

   public MyServiceException() {
      super();
   }

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

   public String getErrMessage() {
      return errMessage;
   }
	
	//定义静态方法throw异常,以后就在参数校验时直接调用
	//传入通用错误
   public static void cast(CommonError commonError){
       throw new MyServiceException(commonError.getErrMessage());
   }
	//传入个别特殊的错误msg
   public static void cast(String errMessage){
       throw new MyServiceException(errMessage);
   }
	//定义一个方法,给错误类型枚举对象继承并调用
	default String getMoudle(){
		return "Common:";
	}
	public 
	

}

3.3 定义响应用户统一类型

在这里插入图片描述

到此,可能出现异常的地方,使用枚举.异常方法—->throw异常—->全局异常处理器捕捉—->返回统一的错误类型
在这里插入图片描述

3.4 全局异常处理器

统一处理异常,并根据不同类型的异常,执行不同的操作,返回一个结果集对象。

在这里插入图片描述
异常处理的思路:
在这里插入图片描述
到此,controller层直接return成功,出现异常统一给异常处理器去返回。

4、 JSR303校验

前端请求后端接口传输参数,是在controller中校验还是在Service中校验?

都校验,分工不同:Contoller中校验请求参数的合法性,包括:必填项校验,数据格式校验,比如:是否是符合一定的日期格式。Service中要校验的是业务规则相关的内容,比如:课程已经审核通过所以提交失败。

4.1 JSR303相关注解:

//引入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

可以看到一些定义好校验规则的注解:

在这里插入图片描述
具体用法含义:

在这里插入图片描述

4.2 统一校验的实现

@ApiOperation("新增课程基础信息")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto addCourseDto){
    //机构id,由于认证系统没有上线暂时硬编码
    Long companyId = 1232141425L;
  	return courseBaseInfoService.createCourseBase(companyId,addCourseDto);
}

  • controller层传入的是一个dto对象,所以直接进入AddCourseDto类,在属性上添加校验规则
@Data
@ApiModel(value="AddCourseDto", description="新增课程基本信息")
public class AddCourseDto {

 @NotEmpty(message = "课程名称不能为空")
 @ApiModelProperty(value = "课程名称", required = true)
 private String name;

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

 @ApiModelProperty(value = "课程标签")
 private String tags;

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

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

 @NotEmpty(message = "课程等级不能为空")
 @ApiModelProperty(value = "课程等级", required = true)
 private String grade;

 @ApiModelProperty(value = "教学模式(普通,录播,直播等)", required = true)
 private String teachmode;

 @ApiModelProperty(value = "课程介绍")
 private String description;

 @ApiModelProperty(value = "课程图片", required = true)
 private String pic;

 @NotEmpty(message = "收费规则不能为空")
 @ApiModelProperty(value = "收费规则,对应数据字典", required = true)
 private String charge;

 @ApiModelProperty(value = "价格")
 private BigDecimal price;

}

  • 在controller方法中添加@Validated注解,告诉它开启校验
@ApiOperation("新增课程基础信息")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody @Validated AddCourseDto addCourseDto){
    Long companyId = 1L;
  	return courseBaseInfoService.createCourseBase(companyId,addCourseDto);
}

  • 校验出错时Spring会抛出MethodArgumentNotValidException异常,因此还要在全局控制器加上对这个异常的拦截和处理方法
@ResponseBody
@ExceptionHandler(MethodArgumentNotValidException.class)
public RestErrorResponse methodArgumentNotValidException(MethodArgumentNotValidException e) {

----------------
    BindingResult bindingResult = e.getBindingResult();
    List<String> msgList = new ArrayList<>();
    //将错误信息放在msgList
    bindingResult.getFieldErrors().stream().forEach(item->msgList.add(item.getDefaultMessage()));
    //拼接错误信息
    String msg = StringUtils.join(msgList, ",");
-------------------
    log.error("【系统异常】{}",msg);
    return new RestErrorResponse(msg);
}

4.3 分组校验

当多个接口使用同一个模型类,如新增课程和修改课程接口,都使用AddCourseDto类,而它们对同一个参数的校验规则不一样,此时就需要分组校验

  • 定义不同的接口类型(空接口)表示不同的分组
 /**
 * @description 校验分组
 */
public class ValidationGroups {

 public interface Inster{};
 public interface Update{};
 public interface Delete{};

}

  • 定义校验规则时指定分组(注解新加groups属性)
@NotEmpty(groups = {ValidationGroups.Inster.class},message = "添加课程名称不能为空")

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

  • 在Controller方法中启动校验规则指定要使用的分组名
...

@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody @Validated({ValidationGroups.Inster.class}) AddCourseDto addCourseDto){

...

当JS303提供的校验注解不够用的时候,可以:

  • 如果这个校验规则不常用,直接在service或者controller层手写规则校验
  • 如果这个规则常见,则自定义一个校验注解

5、修改课程

5.1 需求分析

业务流程即操作流程,每一步的操作需要什么数据

从UI图上来看:

  • 点击编辑
    在这里插入图片描述
  • 进入编辑页面(其中,审核通过后才可编辑)
    在这里插入图片描述
  • 从逻辑上来说:点击编辑,就要显示当前课程的信息,即需要一个根据id查询课程基本和课程营销信息,显示在表单上
  • 修改课程提交的数据比新增课程多了一项课程id,因为修改课程需要针对某个课程进行修改
  • 编辑完成保存课程基础信息和课程营销信息,更新课程基本信息表中的修改人、修改时间

5.2 表设计和po类

还是之前的旧表,课程基本信息表:
在这里插入图片描述
营销信息表:
在这里插入图片描述

5.3 接口分析与定义

根据查询课程信息:

GET /content/course/40
Content-Type: application/json
#响应结果
#{
#  "id": 40,
#  "companyId": 1232141425,
#  "companyName": null,
#  "name": "SpringBoot核心",
#  "users": "Spring Boot初学者",
#  "tags": "Spring项目的快速构建",
#  "mt": "1-3",
#  "mtName": null,
#  "st": "1-3-2",
#  "stName": null,
#  "grade": "200003",
#  "teachmode": "201001",
#  "description": "课程系统性地深度探讨 Spring Boot 核心特性,引导小伙伴对 Java 规范的重视,启发对技术原理性的思考,掌握排查问题的技能,以及学习阅读源码的方法和技巧,全面提升研发能力,进军架构师队伍。",
#  "pic": "https://cdn.educba.com/academy/wp-content/uploads/2018/08/Spring-BOOT-Interview-questions.jpg",
#  "createDate": "2019-09-10 16:05:39",
#  "changeDate": "2022-09-09 07:27:48",
#  "createPeople": null,
#  "changePeople": null,
#  "auditStatus": "202004",
#  "status": "203001",
#  "coursePubId": 21,
#  "coursePubDate": null,
#  "charge": "201001",
#  "price": 0.01
#}

可以看到,之前的CourseBaseInfoVo类也能复用

@ApiOperation("根据课程id查询课程基础信息")
@GetMapping("/course/{courseId}")
public CourseBaseInfoVo getCourseBaseById(@PathVariable Long courseId){
    return null;
}

修改课程信息

修改课程提交的数据比新增多了课程id,我好去update xxx where ,

### 修改课程
PUT /content/course
Content-Type: application/json

{
  "id": 40,
  "companyName": null,
  "name": "SpringBoot核心",
  "users": "Spring Boot初学者",
  "tags": "Spring项目的快速构建",
  "mt": "1-3",
  "st": "1-3-2",
  "grade": "200003",
  "teachmode": "201001",
  "description": "课程系统性地深度探讨 Spring Boot 核心特性,引导小伙伴对 Java 规范的重视,启发对技术原理性的思考,掌握排查问题的技能,以及学习阅读源码的方法和技巧,全面提升研发能力,进军架构师队伍。",
  "pic": "https://cdn.educba.com/academy/wp-content/uploads/2018/08/Spring-BOOT-Interview-questions.jpg",
  "charge": "201001",
  "price": 0.01
}

###修改成功响应结果如下
#{
#  "id": 40,
#  "companyId": 1232141425,
#  "companyName": null,
#  "name": "SpringBoot核心",
#  "users": "Spring Boot初学者",
#  "tags": "Spring项目的快速构建",
#  "mt": "1-3",
#  "mtName": null,
#  "st": "1-3-2",
#  "stName": null,
#  "grade": "200003",
#  "teachmode": "201001",
#  "description": "课程系统性地深度探讨 Spring Boot 核心特性,引导小伙伴对 Java 规范的重视,启发对技术原理性的思考,掌握排查问题的技能,以及学习阅读源码的方法和技巧,全面提升研发能力,进军架构师队伍。",
#  "pic": "https://cdn.educba.com/academy/wp-content/uploads/2018/08/Spring-BOOT-Interview-questions.jpg",
#  "createDate": "2019-09-10 16:05:39",
#  "changeDate": "2022-09-09 07:27:48",
#  "createPeople": null,
#  "changePeople": null,
#  "auditStatus": "202004",
#  "status": "203001",
#  "coursePubId": 21,
#  "coursePubDate": null,
#  "charge": "201001",
#  "price": 0.01
#}

因此,重新定义修改课程的dto,继承新增课程的dto的基础上加id属性,做为修改接口的dto:

/**
 * @description 添加课程dto
 */
@Data
@ApiModel(value="EditCourseDto", description="修改课程基本信息")
public class EditCourseDto extends AddCourseDto {

 @ApiModelProperty(value = "课程id", required = true)
 private Long id;

}

接口定义:

@ApiOperation("修改课程信息")
@PutMapping("/course/")
public CourseBaseInfoVo modifyCourseBase(@RequestBody  @Validated EditCourseDto editCourseDto){
    return null;
}

5.4 开发Mapper层–Service层–完善controller层

根据id查询课程信息

在写新增的时候,最后要返回课程基本信息,这里已经有了这个方法,只需再暴露到interface中,这样在controller中通过接口调用此方法即可:

//上个接口中的旧方法

public CourseBaseInfoVo getCourseBaseInfo(long courseId){

  CourseBase courseBase = courseBaseMapper.selectById(courseId);
  if(courseBase == null){
   return null;
  }
  CourseMarket courseMarket = courseMarketMapper.selectById(courseId);
  CourseBaseInfoVo courseBaseInfoVo = new CourseBaseInfoVo();
  BeanUtils.copyProperties(courseBase,courseBaseInfoVo);
  if(courseMarket != null){
   BeanUtils.copyProperties(courseMarket,courseBaseInfoVo);
  }

  //返回字段中要分类名称,要code换name,查询分类名称
  CourseCategory courseCategoryBySt = courseCategoryMapper.selectById(courseBase.getSt());
  courseBaseInfoVo.setStName(courseCategoryBySt.getName());
  CourseCategory courseCategoryByMt = courseCategoryMapper.selectById(courseBase.getMt());
  courseBaseInfoVo.setMtName(courseCategoryByMt.getName());

  return courseBaseInfoVo;

提到接口中:

Java
   
public interface CourseBaseInfoService {
    ....
   /**
     * @description 根据id查询课程基本信息
     * @param courseId  课程id
    */
    public CourseBaseInfoVo getCourseBaseInfo(long courseId);
    ...

完善controller层:

@ApiOperation("根据课程id查询课程基础信息")
@GetMapping("/course/{courseId}")
public CourseBaseInfoVo getCourseBaseById(@PathVariable Long courseId){
    return courseBaseInfoService.getCourseBaseInfo(courseId);
}

编辑课程

mapper层中继承baseMapper就有根据id更新接口,所以mapper层不用再手敲。

接下来service层接口:

/**
 * @description 修改课程信息
 * 这里传参有个机构id,以后要做身份校验
*/
public CourseBaseInfoVo updateCourseBase(Long companyId,EditCourseDto dto);

写实现类,思路总结就是:

  • 部分数据合法性或者业务逻辑校验
  • 封装数据,set和copyProperties出一个po对象
  • 传入po给mapper层方法,更新数据库
Transactional
@Override
public CourseBaseInfoDto updateCourseBase(Long companyId, EditCourseDto dto) {

    //课程id
    Long courseId = dto.getId();
    CourseBase courseBase = courseBaseMapper.selectById(courseId);
    if(courseBase==null){
        XueChengPlusException.cast("课程不存在");
    }

    //校验本机构只能修改本机构的课程
    //这里以后可能也用token校验身份
    if(!courseBase.getCompanyId().equals(companyId)){
        XueChengPlusException.cast("本机构只能修改本机构的课程");
    }

    //封装基本信息的数据(覆盖查出来的课程信息)
    BeanUtils.copyProperties(dto,courseBase);
    courseBase.setChangeDate(LocalDateTime.now());

    //更新数据库中的课程基本信息
    int i = courseBaseMapper.updateById(courseBase);
    if(i < = 0){
    	 XueChengPlusException.cast("课程基本信息修改失败");
    }


    //new课程营销po对象,向课程营销表保存课程营销信息
  	CourseMarket courseMarketNew = new CourseMarket();
  	BeanUtils.copyProperties(dto,courseMarketNew);

  	if(saveCourseMarket(courseMarketNew)<=0){
        XueChengPlusException.cast("课程营销信息修改失败");
    }

    //查询课程全部信息
    CourseBaseInfoVo courseBaseInfoVo = this.getCourseBaseInfo(courseId);
    return courseBaseInfoVo;

}

完善controller层:

@ApiOperation("修改课程基础信息")
@PutMapping("/course")
public CourseBaseInfoVo modifyCourseBase(@RequestBody @Validated EditCourseDto editCourseDto){
    //机构id,由于认证系统没有上线暂时硬编码
    Long companyId = 1232141425L;
    //当然日常开发要把vo封装到AjaxResult类中
    return courseBaseInfoService.updateCourseBase(companyId,editCourseDto);
}

6、查询课程计划

6.1 需求分析

课程基本信息添加或修改成功将自动进入课程计划编辑器界面:
在这里插入图片描述
这里需要完成课程计划信息的查询

6.2 表设计与po类

从UI上看出整体上是 一个树型结构,课程计划表teachplan如下:

在这里插入图片描述
课程计划列表展示时还有课程计划关联的视频信息,课程计划关联的视频信息在teachplan_media表:

在这里插入图片描述

在这里插入图片描述
两张表是一对一关系,每个课程计划只能在teachplan_media表中存在一个视频。两张表的po类自动去生成。

6.3 接口分析与定义

协议、请求、响应:

GET /teachplan/22/tree-nodes

 [
      {
         "changeDate" : null,
         "courseId" : 74,
         "cousePubId" : null,
         "createDate" : null,
         "endTime" : null,
         "grade" : "2",
         "isPreview" : "0",
         "mediaType" : null,
         "orderby" : 1,
         "parentid" : 112,
         "pname" : "第1章基础知识",
         "startTime" : null,
         "status" : null,
         "id" : 113,
         "teachPlanTreeNodes" : [
            {
               "changeDate" : null,
               "courseId" : 74,
               "cousePubId" : null,
               "createDate" : null,
               "endTime" : null,
               "grade" : "3",
               "isPreview" : "1",
               "mediaType" : "001002",
               "orderby" : 1,
               "parentid" : 113,
               "pname" : "第1节项目概述",
               "startTime" : null,
               "status" : null,
               "id" : 115,
               "teachPlanTreeNodes" : null,
               "teachplanMedia" : {
                  "courseId" : 74,
                  "coursePubId" : null,
                  "mediaFilename" : "2.avi",
                  "mediaId" : 41,
                  "teachplanId" : 115,
                  "id" : null
               }
            }
         ],
         "teachplanMedia" : null
      },
      {
      ....
      }
]

定义Vo模型类:

/**
 * @description 课程计划树型结构dto
 */
@Data
@ToString
public class TeachplanVo extends Teachplan {

  //继承教学计划类的字段后,新加课程计划关联的媒资信息
  TeachplanMedia teachplanMedia;

  //子结点
  List<TeachplanDto> teachPlanTreeNodes;

}

定义接口:

/**
 * @description 课程计划接口
 */
 @Api(value = "课程计划接口",tags = "课程计划接口")
 @RestController
public class TeachplanController {

    @ApiOperation("查询课程计划树形结构")
    @ApiImplicitParam(value = "courseId",name = "课程Id",required = true,dataType = "Long",paramType = "path")
    @GetMapping("/teachplan/{courseId}/tree-nodes")
    public List<TeachplanVo> getTreeNodes(@PathVariable Long courseId){
        return null;
    }

}

6.4 开发mapper层

mapper接口:

public interface TeachplanMapper extends BaseMapper<Teachplan> {

    /**
     * @description 查询某课程的课程计划,组成树型结构 
    */
    public List<TeachplanVo> selectTreeNodes(long courseId);

}

在MySQL客户端试着写写SQL语句:
在这里插入图片描述
加入媒资表:

在这里插入图片描述

定义mapper.xml文件:

<!-- 课程分类树型结构查询映射结果 -->
    <resultMap id="treeNodeResultMap" type="com.xuecheng.content.model.dto.TeachplanVo">
        <!-- 一级数据映射 -->
        <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="stratTime" />
        <result column="one_endTime"   property="endTime" />
        <result column="one_orderby"   property="orderby" />
        <result column="one_courseId"   property="courseId" />
        <result column="one_coursePubId"   property="coursePubId" />
        <!-- 一级中包含多个二级数据 -->
        <collection property="teachPlanTreeNodes" ofType="com.xuecheng.content.model.dto.TeachplanDto">
            <!-- 二级数据映射 -->
            <id     column="two_id"        property="id" />
            <result column="two_pname"      property="pname" />
            <result column="two_parentid"     property="parentid" />
            <result column="two_grade"  property="grade" />
            <result column="two_mediaType"   property="mediaType" />
            <result column="two_stratTime"   property="stratTime" />
            <result column="two_endTime"   property="endTime" />
            <result column="two_orderby"   property="orderby" />
            <result column="two_courseId"   property="courseId" />
            <result column="two_coursePubId"   property="coursePubId" />
            <association property="teachplanMedia" javaType="com.xuecheng.content.model.po.TeachplanMedia">
                <result column="teachplanMeidaId"   property="id" />
                <result column="mediaFilename"   property="mediaFilename" />
                <result column="mediaId"   property="mediaId" />
                <result column="two_id"   property="teachplanId" />
                <result column="two_courseId"   property="courseId" />
                <result column="two_coursePubId"   property="coursePubId" />
            </association>
        </collection>
    </resultMap>
    <!--课程计划树型结构查询-->
    <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_stratTime,
            one.end_time       one_endTime,
            one.orderby        one_orderby,
            one.course_id      one_courseId,
            one.course_pub_id  one_coursePubId,
            two.id             two_id,
            two.pname          two_pname,
            two.parentid       two_parentid,
            two.grade          two_grade,
            two.media_type     two_mediaType,
            two.start_time     two_stratTime,
            two.end_time       two_endTime,
            two.orderby        two_orderby,
            two.course_id      two_courseId,
            two.course_pub_id  two_coursePubId,
            m1.media_fileName mediaFilename,
            m1.id teachplanMeidaId,
            m1.media_id mediaId

        from teachplan one
                 INNER JOIN teachplan two on one.id = two.parentid
                 LEFT JOIN teachplan_media m1 on m1.teachplan_id = two.id
        where one.parentid = 0 and one.course_id=#{value}
        order by one.orderby,
                 two.orderby
    </select>

6.5 开发service层

定义接口:

 interface TeachplanService {

/**
 * @description 查询课程计划树型结构
 * @param courseId  课程id
*/
 public List<TeachplanVo> findTeachplanTree(long courseId);

 }

写实现类:

@Service
public class TeachplanServiceImpl implements TeachplanService {

  @Autowired
 TeachplanMapper teachplanMapper;
 @Override
 public List<TeachplanVo> findTeachplanTree(long courseId) {
  return teachplanMapper.selectTreeNodes(courseId);
 }
}

6.6 完善controller层

@Autowired
TeachplanService teachplanService;

@ApiOperation("查询课程计划树形结构")
@ApiImplicitParam(value = "courseId",name = "课程基础Id值",required = true,dataType = "Long",paramType = "path")
@GetMapping("teachplan/{courseId}/tree-nodes")
public List<TeachplanVo> getTreeNodes(@PathVariable Long courseId){
    return teachplanService.findTeachplanTree(courseId);
}

7、新增/修改课程计划

7.1 需求分析

在这里插入图片描述

看交互:

  • 点击“添加章”新增第一级课程计划,新增成功自动刷新课程计划列表
  • 点击“添加小节”向某个第一级课程计划下添加小节,新增成功自动刷新课程计划列表。新增的课程计划自动排序到最后。
  • 点击“章”、“节”的名称,可以修改名称、选择是否免费
    在这里插入图片描述

对这种复杂的页面,分析梳理有哪些接口的思路

  • 点哪个按钮或者进行哪个操作,要和服务端有交互。比如点击章节名称,前端输入框可编辑,失焦后即保存更改,这就是一次和数据库的交互

7.2 数据表与模型

还是之前的课程表teachplan:
在这里插入图片描述
分析:

【1】当新增第一级课程计划:

  • 名称默认为:新章名称 [点击修改]
  • grade:1
  • orderby: 所属课程中同级别下排在最后(这个字段的值即在这个一级课程计划中,排第几)

【2】新增第二级课程计划

  • 名称默认为:新小节名称 [点击修改]
  • grade:2
  • orderby: 所属课程计划中排在最后

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

7.3 接口分析与设计

从页面分析请求时能收集到的传参:

### 新增课程计划--,当grade为1时parentid为0
POST /teachplan
Content-Type: application/json

{
  "courseId" : 74,
  "parentid": 0,
  "grade" : 1,
  "pname" : "新章名称 [点击修改]"
}
### 新增课程计划--节
POST /teachplan
Content-Type: application/json

{
  "courseId" : 74,
  "parentid": 247,
  "grade" : 2,
  "pname" : "小节名称 [点击修改]"
}

同一个接口接收新增和修改两个业务请求,以是否传递课程计划id 来判断是新增还是修改。如果传递了课程计划id说明当前是要修改该课程计划,否则是新增一个课程计划

定义一个dto模型类来接收前端传参:

/**
 * @description 保存课程计划dto,包括新增、修改
 */
@Data
@ToString
public class SaveTeachplanDto {

 /***
  * 教学计划id
  */
 private Long id;

 /**
  * 课程计划名称
  */
 private String pname;

 /**
  * 课程计划父级Id
  */
 private Long parentid;

 /**
  * 层级,分为1、2、3级
  */
 private Integer grade;

 /**
  * 课程类型:1视频、2文档
  */
 private String mediaType;


 /**
  * 课程标识
  */
 private Long courseId;

 /**
  * 课程发布标识
  */
 private Long coursePubId;


 /**
  * 是否支持试学或预览(试看)
  */
 private String isPreview;



}

定义接口:

@ApiOperation("课程计划创建或修改")
@PostMapping("/teachplan")
public void saveTeachplan( @RequestBody SaveTeachplanDto teachplan){
    
}

7.4 mapper层开发

针对课程计划表做更新和插入的,使用baseMapper中的方法足够了

7.5 开发service层

定义service层的接口中的方法:

public void saveTeachplan(SaveTeachplanDto teachplanDto);

写实现类:

Java
@Transactional
 @Override
 public void saveTeachplan(SaveTeachplanDto teachplanDto) {

  //课程计划id
  Long id = teachplanDto.getId();
  //id为空即修改课程计划
  if(id!=null){
    Teachplan teachplan = teachplanMapper.selectById(id);
    //赋值属性,封装出po
    BeanUtils.copyProperties(teachplanDto,teachplan);
    teachplanMapper.updateById(teachplan);
  }else{
    //取出同父同级别的课程计划数量
   int count = getTeachplanCount(teachplanDto.getCourseId(), teachplanDto.getParentid());
   Teachplan teachplanNew = new Teachplan();
   //设置排序号,+1即需求里的放到最后面
   teachplanNew.setOrderby(count+1);
   BeanUtils.copyProperties(teachplanDto,teachplanNew);

   teachplanMapper.insert(teachplanNew);

  }

 }
 /**
  * @description 获取最新的排序号
  * @param courseId  课程id
  * @param parentId  父课程计划id
  * @return int 最新排序号
 */
 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;
 }

精彩之处:

  • 根据id是否为空来判断是更改还是新增,if(id != null),即修改
  • 关于需求“新增的排到最后”,逻辑是获取所有同级课程计划量,加一后set给它代表位置的orderby字段即放在最后

7.6 完善controller层

@ApiOperation("课程计划创建或修改")
@PostMapping("/teachplan")
public void saveTeachplan( @RequestBody SaveTeachplanDto teachplanDto){
    teachplanService.saveTeachplan(teachplanDto);
    //实际开发这里可返回给前端一个添加成功的AjaxResult类
}

效果:

在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/146073.html

(0)

相关推荐

发表回复

登录后才能评论
半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!