先看新增页面的UI设计图:
树形结构,通过父结点id将各元素组成一个树
,得单独存一张表:根据课程分类表创建课程分类表的po类:
课程分类要返回全部的课程分类,以树形结构
:
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 childrenTreeNodes;
}
注意这里的dto对po的继承
.接下来定义接口:
/*** 数据字典 前端控制器*/
@Slf4j
@RestController
public class CourseCategoryController {@GetMapping("/course-category/tree-nodes")public List queryTreeNodes() {return null;}
}
注意这里的dto也即vo
当前树形结构的层级固定,都是只有两级,可以使用表的自联结查询:
selectone.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_labelfrom course_category oneinner join course_category two on one.id = two.parentidwhere one.parentid = 1and one.is_show = 1and two.is_show = 1order by one.orderby,two.orderby;
如果层级不固定,有的两级,有的三级,应该MySql递归查询:
with recursive t1 as (
select * from course_category p where id= '1'
union allselect t.* from course_category t inner join t1 on t1.id = t.parentid
)
select * from t1 order by t1.id, t1.orderby
这种方法是向下递归,即找到初始节点的所有下级节点,向上递归即:
with recursive t1 as (
select * from course_category p where id= '1-1-1'
union allselect 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 {public List selectTreeNodes(String id);}
mapper.xml文件:
此时需要对数据层返回的结果:
进行包装,得到需要的一个需要的形式,即含有子节点属性的
public interface CourseCategoryService {/*** 课程分类树形结构查询** @return*/public List queryTreeNodes(String id);
}
实现类:!!!!!!!!!
@Slf4j
@Service
public class CourseCategoryServiceImpl implements CourseCategoryService {@AutowiredCourseCategoryMapper courseCategoryMapper;public List queryTreeNodes(String id) {List courseCategoryTreeDtos = courseCategoryMapper.selectTreeNodes(id);//将list转map,以备使用,排除根节点Map mapTemp = courseCategoryTreeDtos.stream().filter(item->!id.equals(item.getId())).collect(Collectors.toMap(key -> key.getId(), value -> value, (key1, key2) -> key2));//定义最终要返回的listList categoryTreeDtoList = new ArrayList<>();//依次遍历每个元素,排除根节点courseCategoryTreeDtoList.stream().filter(item->!id.equals(item.getId())).forEach(item->{//父节点是我们传入的id,即父节点是1,如1-1前端开发,那就塞进Listif(item.getParentid().equals(id)){categoryTreeDtoList.add(item);}//找到当前节点的父节点CourseCategoryTreeDto courseCategoryTreeDto = mapTemp.get(item.getParentid());if(courseCategoryTreeDto!=null){if(courseCategoryTreeDto.getChildrenTreeNodes() ==null){courseCategoryTreeDto.setChildrenTreeNodes(new ArrayList());}//下边开始往ChildrenTreeNodes属性中放子节点courseCategoryTreeDto.getChildrenTreeNodes().add(item);}});return categoryTreeDtoList;}}
关于流:
将集合转换为这么一种叫做 “流” 的元素序列,能够对集合中的每个元素进行一系列并行或串行的流水线操作。
xxx.stream().filter(item -> xx布尔条件)即过滤掉集合中满足这个布尔条件的元素
.
关于List转Map:
【1】转型的背景:
【2】转型的代码手工实现:遍历List+put方法
:
【3】直接使用Collectors.toMap()方法,直接实现List转Map
【4】关于toMap方法的三个参数:
key -> key.getId()
即使用对象的id属性做为map的key值value -> value
即选择原来的对象做为map的value值(key1, key2) -> key2)
即如果v1与v2的key值相同,选择v1作为那个key所对应的value值@Slf4j
@RestController
public class CourseCategoryController {@AutowiredCourseCategoryService courseCategoryService;@GetMapping("/course-category/tree-nodes")public List queryTreeNodes() {return courseCategoryService.queryTreeNodes("1");}
}
接口正确返回结果,前端效果如下:
UI上来看:
点击添加课程
选择课程形式为录播
点击下一步,到课程信息页面–包括课程基本信息和课程营销信息课程营销信息:
点击下一步到达课程计划信息页面
。课程计划即课程的大纲目录
。课程计划分为两级,章节和小节
。每个小节需要上传课程视频,用户点击 小节的标题即开始播放视频
。如果是直播课程则会进入直播间
课程计划填写完后进入师资管理页面
可在这里添加教师信息
至此,课程新增完成。即一门课程信息涉及:课程基本信息、课程营销信息、课程计划信息、课程师资信息。
此处先写课程基本信息页面的接口,只向课程基本信息、课程营销信息添加记录。
表设计:
除了页面上已有的字段,还要设计一些必要字段和逻辑上的字段
生成course_base、course_market表的PO类
分析:
### 创建课程
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;
}
直接extends BaseMapper
定义接口:
/*** @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");//机构idcourseBaseNew.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(已有对象,目标对象)
方法,只要二者属性名一致就可以拷贝...@ApiOperation("新增课程基础信息")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto addCourseDto){//机构id,由于认证系统没有上线暂时硬编码Long companyId = 1232141425L;return courseBaseInfoService.createCourseBase(companyId,addCourseDto);
}
代码统一用try/catch方式去捕获代码比较臃肿,可以通过SpringMVC提供的控制器增强类统一由一个类去完成异常的捕获并处理:
在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;}}
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());}//传入个别特殊的错误msgpublic static void cast(String errMessage){throw new MyServiceException(errMessage);}//定义一个方法,给错误类型枚举对象继承并调用default String getMoudle(){return "Common:";}public }
到此,可能出现异常的地方,使用枚举.异常方法---->throw异常---->全局异常处理器捕捉---->返回统一的错误类型
统一处理异常,并根据不同类型的异常,执行不同的操作,返回一个结果集对象。
异常处理的思路:
到此,controller层直接return成功,出现异常统一给异常处理器去返回。
JSR303校验
前端请求后端接口传输参数,是在controller中校验还是在Service中校验?
都校验,分工不同:Contoller中校验请求参数的合法性
,包括:必填项校验,数据格式校验,比如:是否是符合一定的日期格式。Service中要校验的是业务规则
相关的内容,比如:课程已经审核通过所以提交失败。
//引入依赖
org.springframework.boot spring-boot-starter-validation
可以看到一些定义好校验规则的注解:
具体用法含义:
统一校验的实现
:@ApiOperation("新增课程基础信息")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto addCourseDto){//机构id,由于认证系统没有上线暂时硬编码Long companyId = 1232141425L;return courseBaseInfoService.createCourseBase(companyId,addCourseDto);
}
进入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;}
@Validated
注解,告诉它开启校验@ApiOperation("新增课程基础信息")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody @Validated AddCourseDto addCourseDto){Long companyId = 1L;return courseBaseInfoService.createCourseBase(companyId,addCourseDto);
}
MethodArgumentNotValidException
异常,因此还要在全局控制器加上对这个异常的拦截和处理方法@ResponseBody
@ExceptionHandler(MethodArgumentNotValidException.class)
public RestErrorResponse methodArgumentNotValidException(MethodArgumentNotValidException e) {----------------BindingResult bindingResult = e.getBindingResult();List msgList = new ArrayList<>();//将错误信息放在msgListbindingResult.getFieldErrors().stream().forEach(item->msgList.add(item.getDefaultMessage()));//拼接错误信息String msg = StringUtils.join(msgList, ",");
-------------------log.error("【系统异常】{}",msg);return new RestErrorResponse(msg);
}
当多个接口使用同一个模型类,如新增课程和修改课程接口,都使用AddCourseDto类,而它们对同一个参数的校验规则不一样,此时就需要分组校验
/*** @description 校验分组*/
public class ValidationGroups {public interface Inster{};public interface Update{};public interface Delete{};}
@NotEmpty(groups = {ValidationGroups.Inster.class},message = "添加课程名称不能为空")@NotEmpty(groups = {ValidationGroups.Update.class},message = "修改课程名称不能为空")
// @NotEmpty(message = "课程名称不能为空")@ApiModelProperty(value = "课程名称", required = true)private String name;
...@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody @Validated({ValidationGroups.Inster.class}) AddCourseDto addCourseDto){...
当JS303提供的校验注解不够用的时候,可以:
业务流程即操作流程,每一步的操作需要什么数据
。
从UI图上来看:
点击编辑,就要显示当前课程的信息,即需要一个根据id查询课程
基本和课程营销信息,显示在表单上多了一项课程id
,因为修改课程需要针对某个课程进行修改更新课程基本信息表中的修改人、修改时间
还是之前的旧表,课程基本信息表:
营销信息表:
根据查询课程信息:
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;
}
根据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;
提到接口中:
Javapublic 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) {//课程idLong 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);
}
课程基本信息添加或修改成功将自动进入课程计划编辑器界面:
这里需要完成课程计划信息的查询
从UI上看出整体上是 一个树型结构,课程计划表teachplan如下:
课程计划列表展示时还有课程计划关联的视频信息,课程计划关联的视频信息在teachplan_media表:
两张表是一对一关系,每个课程计划只能在teachplan_media表中存在一个视频。两张表的po类自动去生成。
协议、请求、响应:
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 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 getTreeNodes(@PathVariable Long courseId){return null;}}
mapper接口:
public interface TeachplanMapper extends BaseMapper {/*** @description 查询某课程的课程计划,组成树型结构 */public List selectTreeNodes(long courseId);}
在MySQL客户端试着写写SQL语句:
加入媒资表:
定义mapper.xml文件:
定义接口:
interface TeachplanService {/*** @description 查询课程计划树型结构* @param courseId 课程id
*/public List findTeachplanTree(long courseId);}
写实现类:
@Service
public class TeachplanServiceImpl implements TeachplanService {@AutowiredTeachplanMapper teachplanMapper;@Overridepublic List findTeachplanTree(long courseId) {return teachplanMapper.selectTreeNodes(courseId);}
}
@Autowired
TeachplanService teachplanService;@ApiOperation("查询课程计划树形结构")
@ApiImplicitParam(value = "courseId",name = "课程基础Id值",required = true,dataType = "Long",paramType = "path")
@GetMapping("teachplan/{courseId}/tree-nodes")
public List getTreeNodes(@PathVariable Long courseId){return teachplanService.findTeachplanTree(courseId);
}
看交互:
对这种复杂的页面,分析梳理有哪些接口的思路:
点哪个按钮或者进行哪个操作,要和服务端有交互
。比如点击章节名称,前端输入框可编辑,失焦后即保存更改,这就是一次和数据库的交互还是之前的课程表teachplan:
分析:
【1】当新增第一级课程计划:
【2】新增第二级课程计划
【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){}
针对课程计划表做更新和插入的,使用baseMapper中的方法足够了
定义service层的接口中的方法:
public void saveTeachplan(SaveTeachplanDto teachplanDto);
写实现类:
Java
@Transactional@Overridepublic void saveTeachplan(SaveTeachplanDto teachplanDto) {//课程计划idLong id = teachplanDto.getId();//id为空即修改课程计划if(id!=null){Teachplan teachplan = teachplanMapper.selectById(id);//赋值属性,封装出poBeanUtils.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 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字段即放在最后@ApiOperation("课程计划创建或修改")
@PostMapping("/teachplan")
public void saveTeachplan( @RequestBody SaveTeachplanDto teachplanDto){teachplanService.saveTeachplan(teachplanDto);//实际开发这里可返回给前端一个添加成功的AjaxResult类
}
效果: