导航:
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
目录
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); }