目录
3.4 全局异常处理器,@RestControllerAdvice,@ExceptionHandler
5.2 MethodArgumentNotValidException捕获处理
5.3.3 Controller指定分组,@Validated
5.5.3 dto+sql+mapper+service+api
1【内容模块】课程分类查询
1.1 需求分析
新增课程界面需要查询课程分类:
课程等级、课程类型来源于数据字典表,此部分的信息前端已从系统管理服务读取。
course_category课程分类表的结构
这张表是一个树型结构,通过父结点id将各元素组成一个树。
表的数据:
请求:
http://localhost:8601/api/content/course-category/tree-nodes
请求参数为空。
响应数据:
[ { "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" } ]
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
tip:as起别名时可以省略。
查询结果:
对比原分类表:
1.2.2 回顾内连接查询
-
内连接查询 :相当于查询AB交集数据
-
外连接查询
-
左外连接查询 :相当于查询A表所有数据和交集部门数据
-
右外连接查询 : 相当于查询B表所有数据和交集部分数据
-
内连接查询
相当于查询AB交集数据。
语句:
-- 隐式内连接。没有JOIN关键字,条件使用WHERE指定。书写简单,多表时效率低
SELECT 字段列表 FROM 表1,表2… WHERE 条件;
-- 显示内连接。使用INNER JOIN ... ON语句, 可以省略INNER。书写复杂,多表时效率高
SELECT 字段列表 FROM 表1 [INNER] JOIN 表2 ON 条件;
- 隐式连接好理解好书写,语法简单,担心的点较少。
- 但是显式连接可以减少字段的扫描,有更快的执行速度。这种速度优势在3张或更多表连接时比较明显
示例:
#隐式内连接
SELECT
emp. NAME,
emp.gender,
dept.dname
FROM
emp,
dept
WHERE
emp.dep_id = dept.did;
#显式内连接
select * from emp inner join dept on emp.dep_id = dept.did;
1.2.3 回顾自连接查询
自连接是一种特殊的内连接,它是指相互连接的表在物理上为同一张表,但可以在逻辑上分为两张表。
注意:自连接查询的列名必须是“表名.*”,而不是直接写“*”
案例:
要求检索出学号为20210的学生的同班同学的信息
SELECT stu.* #一定注意是stu.*,不是*
FROM stu JOIN stu AS stu1 ON stu.grade= stu1.grade
WHERE stu1.id='20210'
1.2.4 回顾MySQL递归查询
with语法:
WITH [RECURSIVE]
cte_name [(col_name [, col_name] ...)] AS (subquery)
[, cte_name [(col_name [, col_name] ...)] AS (subquery)] ...
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这个临时表,相当于调用这个函数。
说明:
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表,相当于调用这个函数。
排序顺序为层序遍历树:
第一行是根节点,紧跟着的几行是根节点的直接子节点,以此类推。
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; //多了子节点列表
}
也可以不加dto,在分类实体类加属性:
@TableField(exist = false) //表示数据库表中不存在 private List<CategoryEntity> children;
mapper
public interface CourseCategoryMapper extends BaseMapper<CourseCategory> {
public List<CourseCategoryTreeDto> selectTreeNodes(String id); //层序遍历查询所有分类
}
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>
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");
}
}
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;
}
}
1.4 httpClient测试
使用httpclient测试:
定义.http文件
运行测试。
完成前后端连调:
打开前端工程,进入新增课程页面。
课程分类下拉框可以正常显示
2【内容模块】课程新增
2.1 业务流程
课程基本信息:
课程营销信息:
在这个界面中填写课程的基本信息、课程营销信息上。
填写完毕,保存并进行下一步。
在此界面填写课程计划信息
课程计划即课程的大纲目录。
课程计划分为两级,章节和小节。
每个小节需要上传课程视频,用户点击 小节的标题即开始播放视频。
如果是直播课程则会进入直播间。
课程计划填写完毕进入课程师资的管理。
在课程师资界面维护该课程的授课老师。
至此,一门课程新增完成。
2.2 数据模型
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
}
2.4 dto+service+api
略。
Service注意
- Service添加课程方法要添加事务,Service要加@Transactional,启动类加@EnableTransactionManagement
- Service要校验参数,毕竟@Valid只能controller用
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网络编程高级"
}
前后端联调
打开新增课程页面,除了课程图片其它信息全部输入。
点击保存,观察浏览器请求接口参数及响应结果是否正常。
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;
}
}
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);
}
}
使用自定义的异常处理:
if (StringUtils.isBlank(dto.getName())) { // throw new RuntimeException("课程名称为空"); XueChengPlusException.cast("课程名称为空"); }
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;
}
}
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());
}
}
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;
}
}
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;
}
}
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>
2.实体类注解
示例,实体类注解:
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; }
3.controller方法中添加@Validated注解
@ApiOperation("新增课程基础信息")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody @Validated AddCourseDto addCourseDto){
//机构id,由于认证系统没有上线暂时硬编码
Long companyId = 1L;
return courseBaseInfoService.createCourseBase(companyId,addCourseDto);
}
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);
}
此时发新增课程请求,name属性为空,运行:
测试JSR303异常要暂时先把Service里的参数校验注释掉:
5.3 分组校验
5.3.1 基础模块创建分组类
package com.xuecheng.base.execption;
public class ValidationGroups {
public interface Inster{};
public interface Update{};
public interface Delete{};
}
5.3.2 实体类分组校验
@NotEmpty(groups = {ValidationGroups.Inster.class},message = "添加课程名称不能为空")
@NotEmpty(groups = {ValidationGroups.Update.class},message = "修改课程名称不能为空")
// @NotEmpty(message = "课程名称不能为空")
@ApiModelProperty(value = "课程名称", required = true)
private String name;
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);
}
5.4【内容模块】修改课程
略。
- 请求参数比新增时多了id,EditCourseDto继承AddCourseDto,多了一个id属性。
- controller和Service多了一个回显方法(根据id查询课程信息)。
- 修改时给数据添加更新时间。
5.5【内容模块】查询课程计划
5.5.1 预览
课程基本信息添加或修改成功将自动进入课程计划编辑器界面,如下图:
课程计划即课程的大纲目录。
课程计划是树形结构,分为两级:第一级为大章节,grade为1、第二级为小章节,grade为2
5.5.2 数据模型
课程计划表teachplan:
每个课程计划都有所属课程“课程标识course_id”。
课程计划关联的视频信息在teachplan_media表,结构如下:
teachplan_media表最重要的是“媒体id”和“计划id”两个字段,绑定单个计划和单个媒体的关系。
两张表是一对一关系,每个课程计划只能在teachplan_media表中存在一个视频。
5.5.3 dto+sql+mapper+service+api
GET /teachplan/22/tree-nodes
dto
除了课程计划实体类的数据,多了该计划的“计划与媒体关系”和“子分类列表”数据。
@Data
@ToString
public class TeachplanDto extends Teachplan {
//与媒资管理的信息
private TeachplanMedia teachplanMedia;
//小章节list
private List<TeachplanDto> teachPlanTreeNodes;
}
mapper
public interface TeachplanMapper extends BaseMapper<Teachplan> {
public List<TeachplanDto> selectTreeNodes(long courseId);
}
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
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>
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);
}
}
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);
}
测试
### 查询某个课程的课程计划
GET {
{content_host}}/content/teachplan/74/tree-nodes
6【内容模块】新增/修改课程计划
6.1 业务流程
添加包括:添加章、添加节
修改包括:点击章节名称,显示输入框进行修改。
1、进入课程计划界面
2、点击“添加章”新增第一级课程计划。
新增成功自动刷新课程计划列表。
3、点击“添加小节”向某个第一级课程计划下添加小节。
新增成功自动刷新课程计划列表。
新增的课程计划自动排序到最后。
4、点击“章”、“节”的名称,可以修改名称、选择是否免费。
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" : "小节名称 [点击修改]"
}
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;
}
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;
}
6.5 api
@ApiOperation("课程计划创建或修改")
@PostMapping("/teachplan")
public void saveTeachplan( @RequestBody SaveTeachplanDto teachplan){
teachplanService.saveTeachplan(teachplan);
}