学成在线笔记+踩坑(3)——【内容模块】课程分类查询、课程增改删、课程计划增删改查,统一异常处理+JSR303校验

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 课程分类查询、课程新增、统一异常处理、统一封装结果类、JSR303校验、修改课程、查询课程计划、新增/修改课程计划

 导航:

【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析

目录

1【内容模块】课程分类查询

1.1 需求分析

1.2 查询的sql语句,内连接查询

1.2.1【自连接查询】查询两层的课程分类

1.2.2 回顾内连接查询

1.2.3 回顾自连接查询

1.2.4 回顾MySQL递归查询

1.2.5【最终sql】层序遍历查询多层的课程分类

1.2.6 mysql递归特点,对比Java递归的优势

1.3 dto+mapper+api+Service

1.4 httpClient测试

2【内容模块】课程新增

2.1 业务流程

2.2 数据模型

2.3 请求响应数据

2.4 dto+service+api

2.5 httpclient测试、前后端联调

3【基础模块】统一异常处理

3.1 通用异常信息的枚举类

3.2 自定义异常类

3.3 异常信息模型类

3.4 全局异常处理器,@RestControllerAdvice,@ExceptionHandler

4【基础模块】统一封装结果类

5 JSR303校验

5.1 controller实现JSR303校验

5.2 MethodArgumentNotValidException捕获处理

5.3 分组校验

5.3.1 基础模块创建分组类

5.3.2 实体类分组校验

5.3.3 Controller指定分组,@Validated

5.4【内容模块】修改课程

5.5【内容模块】查询课程计划

5.5.1 预览

5.5.2 数据模型

5.5.3 dto+sql+mapper+service+api

6【内容模块】新增/修改课程计划

6.1 业务流程

6.2 请求

6.3 dto

6.4 Service

6.5 api


1【内容模块】课程分类查询

1.1 需求分析

新增课程界面需要查询课程分类:

image.gif

课程等级、课程类型来源于数据字典表,此部分的信息前端已从系统管理服务读取。

course_category课程分类表的结构

image.gif

这张表是一个树型结构,通过父结点id将各元素组成一个树。

表的数据:

image.gif

请求:

image.gif

http://localhost:8601/api/content/course-category/tree-nodes

image.gif

请求参数为空。

响应数据:

[
         {
            "childrenTreeNodes" : [
               {
                  "childrenTreeNodes" : null,
                  "id" : "1-1-1",
                  "isLeaf" : null,
                  "isShow" : null,
                  "label" : "HTML/CSS",
                  "name" : "HTML/CSS",
                  "orderby" : 1,
                  "parentid" : "1-1"
               },
               {
                  "childrenTreeNodes" : null,
                  "id" : "1-1-2",
                  "isLeaf" : null,
                  "isShow" : null,
                  "label" : "JavaScript",
                  "name" : "JavaScript",
                  "orderby" : 2,
                  "parentid" : "1-1"
               },
               ...
            ],
            "id" : "1-2",
            "isLeaf" : null,
            "isShow" : null,
            "label" : "移动开发",
            "name" : "移动开发",
            "orderby" : 2,
            "parentid" : "1"
         }
   ]
image.gif

1.2 查询的sql语句,内连接查询

1.2.1【自连接查询】查询两层的课程分类

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 as one
            inner join course_category as two on one.id = two.parentid    #内连接自连接
   where one.parentid = 1        #加条件,只查one的一级分类
     and one.is_show = 1        #加条件,只查显示状态的分类
     and two.is_show = 1
   order by one.orderby,            #根据排序字段排序
            two.orderby

image.gif

tip:as起别名时可以省略。

查询结果:

image.gif 对比原分类表:

image.gif

1.2.2 回顾内连接查询

image.gif

  • 内连接查询 :相当于查询AB交集数据
  • 外连接查询
  • 左外连接查询 :相当于查询A表所有数据和交集部门数据
  • 右外连接查询 : 相当于查询B表所有数据和交集部分数据

内连接查询

相当于查询AB交集数据。

语句:

-- 隐式内连接。没有JOIN关键字,条件使用WHERE指定。书写简单,多表时效率低
SELECT 字段列表 FROM 表1,表2… WHERE 条件;
-- 显示内连接。使用INNER JOIN ... ON语句, 可以省略INNER。书写复杂,多表时效率高
SELECT 字段列表 FROM 表1 [INNER] JOIN 表2 ON 条件;

image.gif

  • 隐式连接好理解好书写,语法简单,担心的点较少。
  • 但是显式连接可以减少字段的扫描,有更快的执行速度。这种速度优势在3张或更多表连接时比较明显

示例:

#隐式内连接
SELECT
  emp. NAME,
  emp.gender,
  dept.dname
FROM
  emp,
  dept
WHERE
  emp.dep_id = dept.did;

image.gif

#显式内连接
select * from emp inner join dept on emp.dep_id = dept.did;

image.gif

1.2.3 回顾自连接查询

自连接是一种特殊的内连接,它是指相互连接的表在物理上为同一张表,但可以在逻辑上分为两张表。

注意:自连接查询的列名必须是“表名.*”,而不是直接写“*”

案例:

要求检索出学号为20210的学生的同班同学的信息

SELECT stu.*        #一定注意是stu.*,不是*
FROM stu JOIN stu AS stu1 ON stu.grade= stu1.grade
WHERE stu1.id='20210'

image.gif

1.2.4 回顾MySQL递归查询

with语法:

WITH [RECURSIVE]
        cte_name [(col_name [, col_name] ...)] AS (subquery)
        [, cte_name [(col_name [, col_name] ...)] AS (subquery)] ...

image.gif

recurslve译为递归。

with:在mysql中被称为公共表达式,可以作为一个临时表然后在其他结构中调用.如果是自身调用那么就是后面讲的递归.

cte_name :公共表达式的名称,可以理解为表名,用来表示as后面跟着的子查询

col_name :公共表达式包含的列名,可以写也可以不写

例子:使用MySQL临时表遍历1~5

with RECURSIVE t1  AS    #这里t1函数名,也是临时表的表名
(
  SELECT 1 as n        #n是列的别名,1是初始记录
  UNION ALL        #把递归结果(2,3,4,5)合并到t1表中
  SELECT n + 1 FROM t1 WHERE n < 5    #n+1是参数,t1是函数名,n<5是遍历终止条件
)
SELECT * FROM t1;        #正常查询t1这个临时表,相当于调用这个函数。

image.gif

image.gif

说明:

t1 相当于一个表名

select 1 相当于这个表的初始值,这里使用UNION ALL 不断将每次递归得到的数据加入到表中。

n<5为递归执行的条件,当n>=5时结束递归调用。

1.2.5【最终sql】层序遍历查询多层的课程分类

with recursive t1 as (        #t1是函数名、临时表名
select * from  course_category where  id= '1'   #初始记录,也就是根节点
union all         #把递归结果合并到t1表中
 select t2.* from course_category as t2 inner join t1 on t1.id = t2.parentid    #递归,用分类表t和临时表t1内连接查询
)
select *  from t1 order by t1.id, t1.orderby    #查t1表,相当于调用这个函数。

image.gif

排序顺序为层序遍历树:

第一行是根节点,紧跟着的几行是根节点的直接子节点,以此类推。

image.gif

1.2.6 mysql递归特点,对比Java递归的优势

mysql递归次数限制:

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

对比Java递归的优势:

mysql递归相当于在存储过程中执行若干次sql语句,java程序仅与数据库建立一次链接执行递归操作。相比之下,Java递归性能就很差,每次递归都会建立一次数据库连接。

1.3 dto+mapper+api+Service

dto

package com.xuecheng.content.model.dto;
//继承分类实体类的基础上,多了子节点列表
@Data
public class CourseCategoryTreeDto extends CourseCategory implements Serializable {
  List<CourseCategoryTreeDto> childrenTreeNodes;    //多了子节点列表
}

image.gif

也可以不加dto,在分类实体类加属性:

@TableField(exist = false) //表示数据库表中不存在
  private List<CategoryEntity> children;
image.gif

mapper

public interface CourseCategoryMapper extends BaseMapper<CourseCategory> {
    public List<CourseCategoryTreeDto> selectTreeNodes(String id);    //层序遍历查询所有分类
}

image.gif

mapper.xml

把sql语句中的数值改成#{}就行,防止sql注入。

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

image.gif

api

package com.xuecheng.content.api;
@Slf4j
@RestController
public class CourseCategoryController {
    @Autowired
    CourseCategoryService courseCategoryService;
    @GetMapping("/course-category/tree-nodes")
    public List<CourseCategoryTreeDto> queryTreeNodes() {
       return courseCategoryService.queryTreeNodes("1");
    }
}

image.gif

service

下面这方法麻烦,建议多写个方法getChildren(),递归寻找指定节点的子分类。

package com.xuecheng.content.service.impl;
@Slf4j
@Service
public class CourseCategoryServiceImpl implements CourseCategoryService {
    @Autowired
    CourseCategoryMapper courseCategoryMapper;
    @Override
    public List<CourseCategoryTreeDto> queryTreeNodes(String id) {
        //1.调用mapper层序遍历,递归查询出分类信息。此时列表的childrenTreeNodes属性为null
        List<CourseCategoryTreeDto> courseCategoryTreeDtos = courseCategoryMapper.selectTreeNodes(id);
        //2.找到每个节点的子节点,最终封装成List<CourseCategoryTreeDto>
        //先将list转成map,key就是结点的id,value就是CourseCategoryTreeDto对象,目的就是为了方便从map获取结点,filter(item->!id.equals(item.getId()))把根结点排除
//Collectors.toMap()第三个参数(key1, key2) -> key2)意思是键重复时,以后添加的为准。
        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
        List<CourseCategoryTreeDto> courseCategoryList = new ArrayList<>();
        //从头遍历 List<CourseCategoryTreeDto> ,一边遍历一边找子节点放在父节点的childrenTreeNodes
        courseCategoryTreeDtos.stream().filter(item -> !id.equals(item.getId())).forEach(item -> {
            if (item.getParentid().equals(id)) {
                courseCategoryList.add(item);
            }
            //找到节点的父节点
            CourseCategoryTreeDto courseCategoryParent = mapTemp.get(item.getParentid());
            if(courseCategoryParent!=null){
                if(courseCategoryParent.getChildrenTreeNodes()==null){
                    //如果该父节点的ChildrenTreeNodes属性为空要new一个集合,因为要向该集合中放它的子节点
                    courseCategoryParent.setChildrenTreeNodes(new ArrayList<CourseCategoryTreeDto>());
                }
                //到每个节点的子节点放在父节点的childrenTreeNodes属性中
                courseCategoryParent.getChildrenTreeNodes().add(item);
            }
        });
        //3.返回分类dto列表
        return courseCategoryList;
    }
}

image.gif

1.4 httpClient测试

使用httpclient测试:

定义.http文件

image.gif

运行测试。

完成前后端连调:

打开前端工程,进入新增课程页面。

课程分类下拉框可以正常显示

image.gif

2【内容模块】课程新增

2.1 业务流程

课程基本信息:

image.gif

课程营销信息:

image.gif

在这个界面中填写课程的基本信息、课程营销信息上。

填写完毕,保存并进行下一步。

在此界面填写课程计划信息

image.gif

课程计划即课程的大纲目录。

课程计划分为两级,章节和小节。

每个小节需要上传课程视频,用户点击 小节的标题即开始播放视频。

如果是直播课程则会进入直播间。

课程计划填写完毕进入课程师资的管理。

image.gif

在课程师资界面维护该课程的授课老师。

image.gif

至此,一门课程新增完成。

2.2 数据模型

image.gif

2.3 请求响应数据

### 创建课程
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
}

image.gif

2.4 dto+service+api

略。

Service注意

  1. Service添加课程方法要添加事务,Service要加@Transactional,启动类加@EnableTransactionManagement
  2. Service要校验参数,毕竟@Valid只能controller用

image.gif

2.5 httpclient测试、前后端联调

### 新增课程
POST {{content_host}}/content/course
Content-Type: application/json
{
  "name" : "新课程",
  "charge": "201001",
  "price": 10,
  "originalPrice":100,
  "qq": "22333",
  "wechat": "223344",
  "phone": "13333333",
  "validDays": 365,
  "mt": "1-1",
  "st": "1-1-1",
  "pic": "fdsf",
  "teachmode": "200002",
  "users": "初级人员",
  "tags": "tagstagstags",
  "grade": "204001",
  "description": "java网络编程高级java网络编程高级java网络编程高级"
}

image.gif

前后端联调

打开新增课程页面,除了课程图片其它信息全部输入。

点击保存,观察浏览器请求接口参数及响应结果是否正常。

3【基础模块】统一异常处理

3.1 通用异常信息的枚举类

package com.xuecheng.base.execption;
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;
   }
}

image.gif

3.2 自定义异常类

package com.xuecheng.base.execption;
public class XueChengPlusException extends RuntimeException {
   private String errMessage;
   public XueChengPlusException() {
      super();
   }
   public XueChengPlusException(String errMessage) {
      super(errMessage);
      this.errMessage = errMessage;
   }
   public String getErrMessage() {
      return errMessage;
   }
   public static void cast(CommonError commonError){
       throw new XueChengPlusException(commonError.getErrMessage());
   }
   public static void cast(String errMessage){
       throw new XueChengPlusException(errMessage);
   }
}

image.gif

使用自定义的异常处理:

if (StringUtils.isBlank(dto.getName())) {
//            throw new RuntimeException("课程名称为空");
            XueChengPlusException.cast("课程名称为空");
        }
image.gif

3.3 异常信息模型类

package com.xuecheng.base.execption;
/**
 * 错误响应参数包装
 */
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;
    }
}

image.gif

3.4 全局异常处理器,@RestControllerAdvice,@ExceptionHandler

package com.xuecheng.base.execption;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
   @ExceptionHandler(XueChengPlusException.class)
   @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
   public RestErrorResponse customException(XueChengPlusException e) {
      log.error("【系统异常】{}",e.getErrMessage(),e);
      return new RestErrorResponse(e.getErrMessage());
   }
   @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());
   }
}

image.gif

4【基础模块】统一封装结果类

controller用的结果类

package com.xuecheng.base.model;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
import java.util.List;
/**
 * @author Mr.M
 * @version 1.0
 * @description 分页查询结果模型类
 * @date 2023/2/11 15:40
 */
@Data
@ToString
public class PageResult<T> implements Serializable {
    // 数据列表
    private List<T> items;
    //总记录数
    private long counts;
    //当前页码
    private long page;
    //每页记录数
    private long pageSize;
    public PageResult(List<T> items, long counts, long page, long pageSize) {
        this.items = items;
        this.counts = counts;
        this.page = page;
        this.pageSize = pageSize;
    }
}

image.gif

service用的结果类

package com.xuecheng.base.model;
/**
 * @description 通用结果类型
 */
 @Data
 @ToString
public class RestResponse<T> {
  /**
   * 响应编码,0为正常,-1错误
   */
  private int code;
  /**
   * 响应提示信息
   */
  private String msg;
  /**
   * 响应内容
   */
  private T result;
  public RestResponse() {
   this(0, "success");
  }
  public RestResponse(int code, String msg) {
   this.code = code;
   this.msg = msg;
  }
  /**
   * 错误信息的封装
   *
   * @param msg
   * @param <T>
   * @return
   */
  public static <T> RestResponse<T> validfail(String msg) {
   RestResponse<T> response = new RestResponse<T>();
   response.setCode(-1);
   response.setMsg(msg);
   return response;
  }
  public static <T> RestResponse<T> validfail(T result,String msg) {
   RestResponse<T> response = new RestResponse<T>();
   response.setCode(-1);
   response.setResult(result);
   response.setMsg(msg);
   return response;
  }
  /**
   * 添加正常响应数据(包含响应内容)
   *
   * @return RestResponse Rest服务封装相应数据
   */
  public static <T> RestResponse<T> success(T result) {
   RestResponse<T> response = new RestResponse<T>();
   response.setResult(result);
   return response;
  }
  public static <T> RestResponse<T> success(T result,String msg) {
   RestResponse<T> response = new RestResponse<T>();
   response.setResult(result);
   response.setMsg(msg);
   return response;
  }
  /**
   * 添加正常响应数据(不包含响应内容)
   *
   * @return RestResponse Rest服务封装相应数据
   */
  public static <T> RestResponse<T> success() {
   return new RestResponse<T>();
  }
  public Boolean isSuccessful() {
   return this.code == 0;
  }
 }

image.gif

5 JSR303校验

5.1 controller实现JSR303校验

注意:controller和Service都需要校验。

Contoller使用JSR303校验请求参数的合法性。

Service中要校验的是业务规则相关的内容。

1.导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

image.gif

2.实体类注解 image.gif

示例,实体类注解:

package com.xuecheng.content.model.dto;
/**
 * @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 BigDecimal price;
}
image.gif

3.controller方法中添加@Validated注解

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

image.gif

5.2 MethodArgumentNotValidException捕获处理

MethodArgumentNotValidException方法参数不合法异常。

自定义异常类添加方法:

package com.xuecheng.base.execption.XueChengPlusException

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
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);
}

image.gif

此时发新增课程请求,name属性为空,运行:

image.gif

测试JSR303异常要暂时先把Service里的参数校验注释掉:

image.gif

5.3 分组校验

5.3.1 基础模块创建分组类

package com.xuecheng.base.execption;
public class ValidationGroups {
 public interface Inster{};
 public interface Update{};
 public interface Delete{};
}

image.gif

5.3.2 实体类分组校验

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

image.gif

5.3.3 Controller指定分组,@Validated

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

image.gif

5.4【内容模块】修改课程

略。

  • 请求参数比新增时多了id,EditCourseDto继承AddCourseDto,多了一个id属性。
  • controller和Service多了一个回显方法(根据id查询课程信息)。
  • 修改时给数据添加更新时间。

5.5【内容模块】查询课程计划

5.5.1 预览

课程基本信息添加或修改成功将自动进入课程计划器界面,如下图:

image.gif

课程计划即课程的大纲目录。

课程计划是树形结构,分为两级:第一级为大章节,grade为1、第二级为小章节,grade为2

5.5.2 数据模型

课程计划表teachplan

image.gif

image.gif

每个课程计划都有所属课程“课程标识course_id”。

课程计划关联的视频信息在teachplan_media表,结构如下:

image.gif

image.gif

teachplan_media表最重要的是“媒体id”和“计划id”两个字段,绑定单个计划和单个媒体的关系。

两张表是一对一关系,每个课程计划只能在teachplan_media表中存在一个视频。

5.5.3 dto+sql+mapper+service+api

GET /teachplan/22/tree-nodes

image.gif

dto

除了课程计划实体类的数据,多了该计划的“计划与媒体关系”和“子分类列表”数据。

@Data
@ToString
public class TeachplanDto extends Teachplan {
  //与媒资管理的信息
   private TeachplanMedia teachplanMedia;
  //小章节list
   private List<TeachplanDto> teachPlanTreeNodes;
}

image.gif

mapper

public interface TeachplanMapper extends BaseMapper<Teachplan> {
    public List<TeachplanDto> selectTreeNodes(long courseId);
}

image.gif

sql

1、一级分类和二级分类通过teachplan表的自链接进行,如果只有一级分类其下边没有二级分类,此时也需要显示一级分类,这里使用左连接,左边是一级分类,右边是二级分类。

2、由于当“还没有关联视频”时teachplan_media对应的记录为空,所以需要teachplan和teachplan_media左连接。

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

image.gif

mapper.xml

<!-- 课程分类树型结构查询映射结果 -->
    <resultMap id="treeNodeResultMap" type="com.xuecheng.content.model.dto.TeachplanDto">
        <!-- 一级数据映射 -->
        <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>

image.gif

Service

package com.xuecheng.content.service.impl;
 @Service
public class TeachplanServiceImpl implements TeachplanService {
  @Autowired
 TeachplanMapper teachplanMapper;
 @Override
 public List<TeachplanDto> findTeachplanTree(long courseId) {
  return teachplanMapper.selectTreeNodes(courseId);
 }
}

image.gif

api

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

image.gif

测试

### 查询某个课程的课程计划
GET {{content_host}}/content/teachplan/74/tree-nodes

image.gif

6【内容模块】新增/修改课程计划

6.1 业务流程

添加包括:添加章、添加节

修改包括:点击章节名称,显示输入框进行修改。

1、进入课程计划界面

image.gif

2、点击“添加章”新增第一级课程计划。

新增成功自动刷新课程计划列表。

3、点击“添加小节”向某个第一级课程计划下添加小节。

新增成功自动刷新课程计划列表。

新增的课程计划自动排序到最后。

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

image.gif

6.2 请求

1、新增第一级课程计划

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

grade:1

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

2、新增第二级课程计划

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

grade:2

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

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

新增章、节 的请求格式是一样的,主要章的等级是1,节的等级是2。

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

image.gif

6.3 dto

增改是一个dto,不同点是id是否为空。

保存dto的属性和教学计划实体类基本一样,只是少了几个属性。

package com.xuecheng.content.model.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;
}

image.gif

6.4 Service

  • 不用写mapper,因为就是基础的语句就能实现。
  • 新增和修改一个方法就行,通过判断id是否为空判断是增还是删
@Transactional
 @Override
 public void saveTeachplan(SaveTeachplanDto teachplanDto) {
  //课程计划id
  Long id = teachplanDto.getId();
  //修改课程计划
  if(id!=null){
    Teachplan teachplan = teachplanMapper.selectById(id);
   BeanUtils.copyProperties(teachplanDto,teachplan);
   teachplanMapper.updateById(teachplan);
  }else{
    //取出同父同级别的课程计划数量
   int count = getTeachplanCount(teachplanDto.getCourseId(), teachplanDto.getParentid());
   Teachplan teachplanNew = new Teachplan();
   //设置排序号
   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;
 }

image.gif

6.5 api

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

image.gif


相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
11天前
|
存储 缓存 网络协议
数据库执行查询请求的过程?
客户端发起TCP连接请求,服务端通过连接器验证主机信息、用户名及密码,验证通过后创建专用进程处理交互。服务端进程缓存以减少创建和销毁线程的开销。后续步骤包括缓存查询(8.0版后移除)、语法解析、查询优化及存储引擎调用,最终返回查询结果。
22 6
|
25天前
|
XML 数据库 数据格式
数据库 校验名称唯一性,用于新增和修改功能
数据库 校验名称唯一性,用于新增和修改功能
36 8
|
10天前
|
SQL JavaScript 程序员
数据库LIKE查询屡试不爽?揭秘大多数人都忽视的秘密操作符!
本文分析了因数据库中的不可见空白字符导致的数据查询问题,探讨了问题的成因与特性,并提出了使用 SQL 语句修复问题的有效方案。同时,总结了避免类似问题的经验和注意事项。
22 0
|
1月前
|
存储 缓存 固态存储
怎么让数据库查询更快
【10月更文挑战第28天】
33 2
|
1月前
|
存储 缓存 关系型数据库
怎么让数据库查询更快
【10月更文挑战第25天】通过以上综合的方法,可以有效地提高数据库查询的速度,提升应用程序的性能和响应速度。但在优化过程中,需要根据具体的数据库系统、应用场景和数据特点进行合理的调整和测试,以找到最适合的优化方案。
|
17天前
|
JSON JavaScript 关系型数据库
node.js连接GBase 8a 数据库 并进行查询代码示例
node.js连接GBase 8a 数据库 并进行查询代码示例
|
1月前
|
监控 关系型数据库 MySQL
数据库优化:MySQL索引策略与查询性能调优实战
【10月更文挑战第27天】本文深入探讨了MySQL的索引策略和查询性能调优技巧。通过介绍B-Tree索引、哈希索引和全文索引等不同类型,以及如何创建和维护索引,结合实战案例分析查询执行计划,帮助读者掌握提升查询性能的方法。定期优化索引和调整查询语句是提高数据库性能的关键。
209 1
|
1月前
|
Java 数据库连接 数据库
如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面
本文介绍了如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面。通过合理配置初始连接数、最大连接数和空闲连接超时时间,确保系统性能和稳定性。文章还探讨了同步阻塞、异步回调和信号量等并发控制策略,并提供了异常处理的最佳实践。最后,给出了一个简单的连接池示例代码,并推荐使用成熟的连接池框架(如HikariCP、C3P0)以简化开发。
51 2
|
1月前
|
XML 数据库 数据格式
数据库 校验名称唯一性,用于新增和修改功能
数据库 校验名称唯一性,用于新增和修改功能
33 1
|
1月前
|
存储 关系型数据库 MySQL
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
404 2