点击“添加课程”,之后随便选一个“课程形式”
然后有一个“课程分类”,我们下面就要实现课程分类,这个地方缺少一个课程分类的下拉框
典型的树形分类结构
@Data
@TableName("course_category")
public class CourseCategory implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private String id;
/**
* 分类名称
*/
private String name;
/**
* 分类标签默认和名称一样
*/
private String label;
/**
* 父结点id(第一级的父节点是0,自关联字段id)
*/
private String parentid;
/**
* 是否显示
*/
private Integer isShow;
/**
* 排序字段
*/
private Integer orderby;
/**
* 是否叶子
*/
private Integer isLeaf;
}
创建一个Dto,方便之后向前端响应课程分类表数据
@Data
public class CourseCategoryTreeDto extends CourseCategory implements Serializable {
//Serializable:在网络传输需要序列化的时候,需要实现Serializable接口
private static final long serialVersionUID = 2950235607890841126L;
//下级节点
List<CourseCategoryTreeDto> childrenTreeNodes;
}
展示出来是下面这种格式
{
"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"
}
}
假如说数据层级比较固定,而且数据层级比较少,可以使用表自连接的方式
select one.id one_id, one.label one_label,
two.id two_id,two.label two_label,
three.id three_id,three.label three_label
from course_category one -- one是表的别名,表示一级分类
inner join course_category two -- two是表的别名,表示二级分类
on two.parentid = one.id -- 子节点的parentid是父节点的id
inner join course_category three
on three.parentid = two.id
查询结果
灵活的方式实现树形表的查询,比如使用MYSQL递归实现,使用with语法
递归时MySQL8之后才有的
递归语法
WITH [RECURSIVE]
cte_name [(col_name [, col_name] ...)] AS (subquery)
[, cte_name [(col_name [, col_name] ...)] AS (subquery)]
cte_name :公共表达式的名称,可以理解为表名,用来表示as后面跟着的子查询
col_name :公共表达式包含的列名,可以写也可以不写
有一个关键字RECURSIVE,就是递归的含义
cte_name相当于表的一个别名
(col_name [, col_name] …)]是表中的哪些字段
示例代码
-- t1J就是一个虚拟表
with RECURSIVE t1 AS
(
-- 这个t1表的初始数据就是1
SELECT 1 as n
-- 将下面查询出的数据结果集放入t1虚拟表中
UNION ALL
-- 下面是递归查询的内容
SELECT n + 1 FROM t1 WHERE n < 5
)
-- 查询最终结果
SELECT * FROM t1;
下面查询树形结果的SQL,向下递归
向下递归:先拿一级节点,拿到一级节点后找二级节点,拿到二级节点后找三级节点…
with RECURSIVE t1 AS(
-- 初始数据,就认为是根节点
select * from course_category as p where id = '1'
-- 每递归一次就把数据放入t1
union all
-- 由树根找叶子
select t2.*
from course_category as t2
INNER JOIN t1
ON t2.parentid = t1.id
-- 当我们拿到id为1的结点,递归后就可以拿到1-1等子结点的结果集
-- 当我们拿到id为1-1等结点后,递归后就可以拿到1-1-1等子结点结果集
-- .......
)
select * from t1
向上递归
向上递归:拿到最下一级的节点后找次下及节点…
由子节点找父节点
with RECURSIVE t1 AS(
-- 初始数据,就认为是根节点
select * from course_category as p where id = '1-1-1'
-- 每递归一次就把数据放入t1
union all
-- 由树根找叶子
select t2.*
from course_category as t2
INNER JOIN t1
ON t2.id = t1.parentid
-- 当我们拿到id为1的结点,递归后就可以拿到1-1等子结点的结果集
-- 当我们拿到id为1-1等结点后,递归后就可以拿到1-1-1等子结点结果集
-- .......
)
select * from t1
mysql为了避免无限递归默认递归次数为1000,可以通过设置cte_max_recursion_depth参数增加递归深度,还可以通过max_execution_time限制执行时间,超过此时间也会终止递归操作。
mysql递归相当于在存储过程中执行若干次sql语句,java程序仅与数据库建立一次链接执行递归操作,所以只要控制好递归深度,控制好数据量性能就没有问题。
//使用递归查询分类
public List<CourseCategoryTreeDto> selectTreeNodes(@Param("id") String id);
<!--查询课程分类-->
<select id="selectTreeNodes" resultType="com.xuecheng.content.model.dto.CourseCategoryTreeDto">
with RECURSIVE t1 AS (
select *
from course_category as p
where id = #{id}
union all
select t2.*
from course_category as t2
INNER JOIN t1
ON t2.parentid = t1.id
)
select *
from t1
order by t1.id, t1.orderby
</select>
我们现在从数据库中查到的数据是下列这个模样
代码中查询出来的数据如下所示
要将Mapper层返回的数据进行进一步的处理。
将根节点id=1舍弃不要,因为在业务上没什么需要了
我们要将子节点放入到父节点的childrenTreeNodes集合里面。比如将1-1-x的节点放入到1-1节点的childrenTreeNodes集合里面
@Slf4j
@Service
public class CourseCategoryServiceImpl implements CourseCategoryService {
@Autowired
private CourseCategoryMapper courseCategoryMapper;
@Override
public List<CourseCategoryTreeDto> queryTreeNodes(String id) {
//TODO 数据库递归查询出课程分类信息
List<CourseCategoryTreeDto> courseCategoryTreeDtos = courseCategoryMapper.selectTreeNodes(id);
//TODO 找到每个节点的子节点,最终封装成List<CourseCategoryTreeDto>
//将list转map,以备使用,排除根节点
Map<String, CourseCategoryTreeDto> mapTemp = courseCategoryTreeDtos.stream()
//!id.equals(item.getId()) 含义就是排除根节点
.filter(item -> !id.equals(item.getId()))
.collect(
//转Map是需要一个key,一个value的
//第一个key是代表元素的意思,key -> key.getId()是拿到key元素的id,然后充当Map的key
//value表示对象的本身,所以不需要任何的处理
//(key1, key2) -> key2 表示当key重复的时候(键相同),以后来的key为主
Collectors.toMap(key -> key.getId(), value -> value, (key1, key2) -> key2)
);
//最终返回的list
List<CourseCategoryTreeDto> categoryTreeDtos = new ArrayList<>();
//依次遍历每个元素,排除根节点
//courseCategoryTreeDtos是从数据库查询出来的全部的数据
courseCategoryTreeDtos.stream().filter(item -> !id.equals(item.getId())).forEach(item -> {
if (item.getParentid().equals(id)) {
//紧挨根节点下的节点
categoryTreeDtos.add(item);
}
//找到当前节点的父节点
CourseCategoryTreeDto courseCategoryTreeDto = mapTemp.get(item.getParentid());
if (courseCategoryTreeDto != null) {
if (courseCategoryTreeDto.getChildrenTreeNodes() == null) {
courseCategoryTreeDto.setChildrenTreeNodes(new ArrayList<CourseCategoryTreeDto>());
}
//下边开始往ChildrenTreeNodes属性中放子节点
courseCategoryTreeDto.getChildrenTreeNodes().add(item);
}
});
return categoryTreeDtos;
}
}
/**
* 课程分类相关接口
*/
@Slf4j
@RestController
public class CourseCategoryController {
@Autowired
private CourseCategoryService courseCategoryService;
@GetMapping("/course-category/tree-nodes")
public List<CourseCategoryTreeDto> queryTreeNodes() {
return courseCategoryService.queryTreeNodes("1");
}
}
将来这些信息都会存储到“course_base”表中
本界面分两部分信息,一部分是课程基本信息上,一部分是课程营销信息。
课程基本信息
课程营销信息:
下面的信息会存储在course_market表中
也就是说一个表单中的数据要存储到两张表中
在这个界面中填写课程的基本信息、课程营销信息上。
填写完毕,保存并进行下一步
course_base、course_market两张表存储
两张表是一对一的关系,一个课程只有一个营销信息
并且两张表的主键id是相同的
/**
* 课程基本信息
*/
@Data
@TableName("course_base")
public class CourseBase implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 机构ID
*/
private Long companyId;
/**
* 机构名称
*/
private String companyName;
/**
* 课程名称
*/
private String name;
/**
* 适用人群
*/
private String users;
/**
* 课程标签
*/
private String tags;
/**
* 大分类
*/
private String mt;
/**
* 小分类
*/
private String st;
/**
* 课程等级
*/
private String grade;
/**
* 教育模式(common普通,record 录播,live直播等)
*/
private String teachmode;
/**
* 课程介绍
*/
private String description;
/**
* 课程图片
*/
private String pic;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createDate;
/**
* 修改时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime changeDate;
/**
* 创建人
*/
private String createPeople;
/**
* 更新人
*/
private String changePeople;
/**
* 审核状态
*/
private String auditStatus;
/**
* 课程发布状态 未发布 已发布 下线
*/
private String status;
}
/**
* 课程营销信息
*/
@Data
@TableName("course_market")
public class CourseMarket implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键,课程id
*/
private Long id;
/**
* 收费规则,对应数据字典
*/
private String charge;
/**
* 现价
*/
private Float price;
/**
* 原价
*/
private Float originalPrice;
/**
* 咨询qq
*/
private String qq;
/**
* 微信
*/
private String wechat;
/**
* 电话
*/
private String phone;
/**
* 有效期天数
*/
private Integer validDays;
}
价格信息存Float是没有问题的,但是计算的时候是有问题的,我们要使用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 BigDecimal price;
@ApiModelProperty(value = "原价")
private BigDecimal originalPrice;
@ApiModelProperty(value = "qq")
private String qq;
@ApiModelProperty(value = "微信")
private String wechat;
@ApiModelProperty(value = "电话")
private String phone;
@ApiModelProperty(value = "有效期")
private Integer validDays;
}
添加成功之后我们查询出来的课程的详细信息
/**
* @description 课程基本信息dto
* 添加上课程信息后我们再查询返回的信息
*/
@Data
public class CourseBaseInfoDto 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;
}
/**
* 课程基本信息 Mapper 接口
*/
public interface CourseBaseMapper extends BaseMapper<CourseBase> {
}
/**
* 课程营销信息 Mapper 接口
*/
public interface CourseMarketMapper extends BaseMapper<CourseMarket> {
}
@ApiOperation("新增课程")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@Validated @RequestBody AddCourseDto addCourseDto) {
// 将来会集成SpringSecurity框架,用户登录之后就可以获取到用户所属机构的ID
// 先把机构ID写死
return courseBaseInfoService.createCourseBase(10086L,addCourseDto);
}
之后点击保存,这个时候会报错一个404,因为后面课程大纲的内容还没有编写
再查看数据库
再通过页面查看一下,还是挺带劲的
涉及的表也是course_base课程基本信息表、course_market课程营销表
其实就是比添加课程多了一个数据回显而已
点击编辑按钮就可以修改课程信息
要修改的表单内容其实是和添加课程时的表单是一个样子的
然后这个地方点击编辑的时候要做一个数据回显
修改课程的请求数据只是比添加课程的请求数据多了一个课程id而已
但是没有营销信息
这个方法之前其实写过
@Override
public CourseBaseInfoDto getCourseBaseInfo(Long courseId){
//TODO 查询课程基本信息
CourseBase courseBase = courseBaseMapper.selectById(courseId);
if(courseBase == null){
return null;
}
//TODO 查询课程营销信息
CourseMarket courseMarket = courseMarketMapper.selectById(courseId);
CourseBaseInfoDto courseBaseInfoDto = new CourseBaseInfoDto();
BeanUtils.copyProperties(courseBase,courseBaseInfoDto);
if(courseMarket != null){
BeanUtils.copyProperties(courseMarket,courseBaseInfoDto);
}
//TODO 查询分类名称,是哪一级的
CourseCategory courseCategoryBySt = courseCategoryMapper.selectById(courseBase.getSt());//小分类
courseBaseInfoDto.setStName(courseCategoryBySt.getName());//小分类名称
CourseCategory courseCategoryByMt = courseCategoryMapper.selectById(courseBase.getMt());//大分类
courseBaseInfoDto.setMtName(courseCategoryByMt.getName());//大分类名称
return courseBaseInfoDto;
}
@ApiOperation("根据课程id查询接口")
@GetMapping("/course/{courseId}")
public CourseBaseInfoDto getCourseBaseById(@PathVariable("courseId") Long courseId) {
return courseBaseInfoService.getCourseBaseInfo(courseId);
}
修改课程的请求数据只比增加课程的请求数据多一个课程id
/**
* 修改课程Dto
*/
@Data
@ApiModel(value = "EditCourseDto",description = "修改课程基本信息")
public class EditCourseDto extends AddCourseDto{
@ApiModelProperty(value = "课程id",required = true)
private Long id;
}
/**
* 修改课程
*
* @param companyId 机构id,后面做认证收取那使用
* @param editCourseDto 要修改的课程信息
* @return 修改之后的课程详细信息
*/
@Override
public CourseBaseInfoDto updateCourseBase(Long companyId, EditCourseDto editCourseDto) {
//TODO 课程基本信息
//获取到课程id
Long courseId = editCourseDto.getId();
//查询课程
CourseBase courseBase = courseBaseMapper.selectById(courseId);
if (courseBase == null) {
XueChengPlusException.cast("课程不存在");
}
//数据合法性校验
//根据具体的业务逻辑进行校验 - 本机构只能修改本机构的课程
if (!companyId.equals(courseBase.getCompanyId())) {
XueChengPlusException.cast("本机构只能修改本机构的课程");
}
//封装数据
BeanUtils.copyProperties(editCourseDto, courseBase);
//修改时间
courseBase.setChangeDate(LocalDateTime.now());
//更新数据库
int i = courseBaseMapper.updateById(courseBase);
if (i<=0){
XueChengPlusException.cast("修改课程失败");
}
//TODO 更新课程营销信息
CourseMarket courseMarketNew = new CourseMarket();
BeanUtils.copyProperties(editCourseDto, courseMarketNew);
int count = courseMarketMapper.updateById(courseMarketNew);
if (count <= 0) {
throw new RuntimeException("更新课程营销信息失败");
}
//查询课程信息
CourseBaseInfoDto courseBaseInfo = getCourseBaseInfo(courseId);
return courseBaseInfo;
}
@ApiOperation("修改课程接口")
@PutMapping("/course")
public CourseBaseInfoDto getCourseBaseById(@RequestBody EditCourseDto editCourseDto) {
//机构id先写死,后面授权认证的时候后会改过来
Long companyId = 1232141425L;
return courseBaseInfoService.updateCourseBase(companyId,editCourseDto);
}