【Java实战篇】Day2.在线教育网课平台

文章目录

  • 1、课程分类选项查询
    • 1.1 需求分析
    • 1.2 开发PO类
    • 1.3 接口分析与定义
    • 1.4 开发Mapper
    • 1.5 开发service层
    • 1.6 完善controller层
  • 2、新增课程
    • 2.1 需求分析
    • 2.2 设计表与开发po类
    • 2.3 接口分析与定义
    • 2.4 开发Mapper层
    • 2.5 开发Service层
    • 2.6 完善controller层
  • 3、异常处理
    • 3.1 枚举定义通用的异常信息
    • 3.2 自定义异常类型
    • 3.3 定义响应用户统一类型
    • 3.4 全局异常处理器
  • 4、 `JSR303校验`
    • 4.1 JSR303相关注解:
    • 4.2 `统一校验的实现`:
    • 4.3 分组校验
  • 5、修改课程
    • 5.1 需求分析
    • 5.2 表设计和po类
    • 5.3 接口分析与定义
    • 5.4 开发Mapper层--Service层--完善controller层
  • 6、查询课程计划
    • 6.1 需求分析
    • 6.2 表设计与po类
    • 6.3 接口分析与定义
    • 6.4 开发mapper层
    • 6.5 开发service层
    • 6.6 完善controller层
  • 7、新增/修改课程计划
    • 7.1 需求分析
    • 7.2 数据表与模型
    • 7.3 接口分析与设计
    • 7.4 mapper层开发
    • 7.5 开发service层
    • 7.6 完善controller层

1、课程分类选项查询

1.1 需求分析

先看新增页面的UI设计图:
【Java实战篇】Day2.在线教育网课平台_第1张图片

  • 课程等级、课程类型这两个来源于字典表,这里的数据前端从system服务拿
  • 课程分类不是简单的几个选项,而是可以展开的一个树形结构,通过父结点id将各元素组成一个树,得单独存一张表:
    【Java实战篇】Day2.在线教育网课平台_第2张图片
    数据表字段说明:
    【Java实战篇】Day2.在线教育网课平台_第3张图片

1.2 开发PO类

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

【Java实战篇】Day2.在线教育网课平台_第4张图片

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拿递归得到的所有数据

【Java实战篇】Day2.在线教育网课平台_第5张图片
这种方法是向下递归,即找到初始节点的所有下级节点,向上递归即:

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层

此时需要对数据层返回的结果:
【Java实战篇】Day2.在线教育网课平台_第6张图片

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

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

}

关于流:

将集合转换为这么一种叫做 “流” 的元素序列,能够对集合中的每个元素进行一系列并行或串行的流水线操作。
【Java实战篇】Day2.在线教育网课平台_第7张图片

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

关于List转Map:

【1】转型的背景:

【Java实战篇】Day2.在线教育网课平台_第8张图片

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

【Java实战篇】Day2.在线教育网课平台_第9张图片
【3】直接使用Collectors.toMap()方法,直接实现List转Map

【Java实战篇】Day2.在线教育网课平台_第10张图片
【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");
    }
}

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

【Java实战篇】Day2.在线教育网课平台_第11张图片

2、新增课程

2.1 需求分析

UI上来看:

  • 点击添加课程
    【Java实战篇】Day2.在线教育网课平台_第12张图片

  • 选择课程形式为录播
    【Java实战篇】Day2.在线教育网课平台_第13张图片

  • 点击下一步,到课程信息页面–包括课程基本信息和课程营销信息【Java实战篇】Day2.在线教育网课平台_第14张图片课程营销信息:
    【Java实战篇】Day2.在线教育网课平台_第15张图片

  • 点击下一步到达课程计划信息页面
    【Java实战篇】Day2.在线教育网课平台_第16张图片。课程计划即课程的大纲目录
    。课程计划分为两级,章节和小节
    。每个小节需要上传课程视频,用户点击 小节的标题即开始播放视频
    。如果是直播课程则会进入直播间

  • 课程计划填写完后进入师资管理页面
    【Java实战篇】Day2.在线教育网课平台_第17张图片

  • 可在这里添加教师信息
    【Java实战篇】Day2.在线教育网课平台_第18张图片

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

2.2 设计表与开发po类

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

表设计:

除了页面上已有的字段,还要设计一些必要字段和逻辑上的字段
【Java实战篇】Day2.在线教育网课平台_第19张图片

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

【Java实战篇】Day2.在线教育网课平台_第20张图片

生成course_base、course_market表的PO类

2.3 接口分析与定义

分析:

  • 接口协议:HTTP POST,Content-Type为application/json
  • 传参:
    【Java实战篇】Day2.在线教育网课平台_第21张图片
  • 响应:
    【Java实战篇】Day2.在线教育网课平台_第22张图片
    即:
### 创建课程
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

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提供的控制器增强类统一由一个类去完成异常的捕获并处理:

【Java实战篇】Day2.在线教育网课平台_第23张图片

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 定义响应用户统一类型

【Java实战篇】Day2.在线教育网课平台_第24张图片

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

3.4 全局异常处理器

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

【Java实战篇】Day2.在线教育网课平台_第25张图片
异常处理的思路:
【Java实战篇】Day2.在线教育网课平台_第26张图片
到此,controller层直接return成功,出现异常统一给异常处理器去返回。

4、 JSR303校验

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

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

4.1 JSR303相关注解:

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

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

【Java实战篇】Day2.在线教育网课平台_第27张图片
具体用法含义:

【Java实战篇】Day2.在线教育网课平台_第28张图片

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图上来看:

  • 点击编辑
    【Java实战篇】Day2.在线教育网课平台_第29张图片
  • 进入编辑页面(其中,审核通过后才可编辑)
    【Java实战篇】Day2.在线教育网课平台_第30张图片
  • 从逻辑上来说:点击编辑,就要显示当前课程的信息,即需要一个根据id查询课程基本和课程营销信息,显示在表单上
  • 修改课程提交的数据比新增课程多了一项课程id,因为修改课程需要针对某个课程进行修改
  • 编辑完成保存课程基础信息和课程营销信息,更新课程基本信息表中的修改人、修改时间

5.2 表设计和po类

还是之前的旧表,课程基本信息表:
【Java实战篇】Day2.在线教育网课平台_第31张图片
营销信息表:
【Java实战篇】Day2.在线教育网课平台_第32张图片

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 需求分析

课程基本信息添加或修改成功将自动进入课程计划编辑器界面:
【Java实战篇】Day2.在线教育网课平台_第33张图片
这里需要完成课程计划信息的查询

6.2 表设计与po类

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

【Java实战篇】Day2.在线教育网课平台_第34张图片
课程计划列表展示时还有课程计划关联的视频信息,课程计划关联的视频信息在teachplan_media表:

【Java实战篇】Day2.在线教育网课平台_第35张图片

【Java实战篇】Day2.在线教育网课平台_第36张图片
两张表是一对一关系,每个课程计划只能在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语句:
【Java实战篇】Day2.在线教育网课平台_第37张图片
加入媒资表:

【Java实战篇】Day2.在线教育网课平台_第38张图片

定义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 需求分析

【Java实战篇】Day2.在线教育网课平台_第39张图片

看交互:

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

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

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

7.2 数据表与模型

还是之前的课程表teachplan:
【Java实战篇】Day2.在线教育网课平台_第40张图片
分析:

【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类
}

效果:

【Java实战篇】Day2.在线教育网课平台_第41张图片

你可能感兴趣的:(Spring,java,开发语言)