介绍

项目介绍套用模板

  • 我最近参与的项目是我们公司自研的专门针对成人职业技能教育的网络课堂系统,网站提供了成人职业技能培训的相关课程,如:软件开发培训、职业资格证书培训、成人学历教育培训等课程。项目基于B2B2C的业务模式,培训机构可以在平台入驻、发布课程,我们公司作为运营方由专门的人员对发布的课程进行审核,审核通过后课程才可以发布成功,课程包括免费和收费两种形式,对于免费课程普通用户可以直接选课学习,对于收费课程在选课后需要支付成功才可以继续学习。
  • 本项目包括三个端:用户端(学生端)、机构端、运营端。
  • 核心模块包括:内容管理、媒资管理、课程搜索、订单支付、选课管理、认证授权等。
  • 本项目采用前后端分离架构,后端采用SpringBoot、SpringCloud技术栈开发,数据库使用了MySQL,还使用的Redis、消息队列、分布式文件系统、Elasticsearch等中间件系统。
  • 划分的微服务包括:内容管理服务、媒资管理服务、搜索服务、订单支付服务、 学习中心服务、系统管理服务、认证授权服务、网关服务、注册中心服务、配置中心服务等。
  • 我在这个项目中负责了内容管理、媒资管理、订单支付模块的设计与开发。
  • 内容管理模块,是对平台上的课程进行管理,课程的相关信息比较多这里在数据库设计了课程基本信息表、课程营销表、课程计划、课程师资表进行存储 ,培训机构要发布一门课程需要填写课程基本信息、课程营销信息、课程计划信息、课程师资信息,填写完毕后需要提交审核,由运营人员进行课程信息的审核,整个审核过程是程序自动审核加人工确认的方式,通常24小时审核完成。课程审核通过即可发布课程,课程的相关信息会聚合到课程发布表中,这里不仅要将课程信息写到课程发布表还要将课程信息写到索引库、分布式文件系统中,所以这里存在分布式事务的问题,项目使用本地消息表加任务调度的方式去解决这里的分布式事务,保存数据的最终一致性。

项目开发环境搭建

后端的工程结构:使用 Maven 来进行项目的管理和构建。整个项目分为三大类工程:父工程、基础工程 和微服务工程

img

  • 父工程
    • 依赖包的版本进行管理
    • 本身为Pom工程,对子工程进行聚合管理
  • 基础工程
    • 继承父类工程
    • 提供基础类库
    • 提供工具类库
  • 微服务工程
    • 分别从业务、技术方面划分模块,每个模块构建为一个微服务。
    • 每个微服务工程依赖基础工程,间接继承父工程。
    • 包括:内容管理服务、媒资管理服务、搜索服务、缓存服务、消息服务等。

内容管理模块

模块需求整理

如:

功能: 添加课程

使用者: 机构端(需要提交发布上传课程发布课程) 平台端(需要审核课程)

描述:添加课程基本信息,添加课程营销表,添加课程计划表,填写授课机构/老师信息

前置约束条件:仅允许向自己的教学机构的课程增删查改(关于身份认证后续认证授权再做,先用一个固定id代替)

创建模块

  • 教学机构人员的业务流程如下:

    1. 登录教学机构
    2. 维护课程信息,添加一门课程需要编辑课程的基本信息(内容管理)、上传课程图片(内容管理、媒资)、课程营销信息(内容管理)、课程计划(内容管理)、上传课程视频(内容管理、媒资)、课程师资信息(内容管理)等内容
    3. 课程信息编辑完成,通过课程预览确认无误后 提交审核。
    4. 待运营人员课程审核通过后方可进行课程 发布
  • 运用人员的业务流程如下:

    1. 查询待审核的课程信息
    2. 审核课程信息
    3. 提交审核结果

流程分为前端、接口层、业务层三部分,所以模块工程结构如下图所示

  • xuecheng-plus-content-api:接口工程,为前端提供接口
  • xuecheng-plus-content-service:业务工程,为接口工程提供业务支撑
  • xuecheng-plus-content-model:数据模型工程,存储数据模型类、数据传输类型等

结合项目父工程、项目基础工程后,如下图

  • xuecheng-plus-content:内容管理模块工程,负责聚合xuecheng-plus-content-api、xuecheng-plus-content-service、xuecheng-plus-content-model

img

img

为 api模块配置访问端口、日志 等信息

server:
  servlet:
    context-path: /content
  port: 63040
# 微服务配置
spring: 
  application:
    name: content-api

课程查询

根据 课程名称、课程审核状态、课程发布状态 (条件可空) 分页查询

Dao层 PO对象的生成 :代码生成器工具 MP的generator工程生成PO类(一组属性和属性的get/set方法组成)

接口的编写技巧

  • 一般定义一个DTO对象(最全的查询,可以整合多表)来作为前后端的连接中介
  • 分析 步骤
    • 1、看协议 方式 如HTTP: POST
    • 2、content-type : 参数以什么数据格式提交,结果以什么数据格式响应 如json
    • 3、分析请求参数
    • 4、分析响应结果 ,结合上述分析定义模型类
  • 开发

DTO数据传输对象、PO持久化对象

  • DTO用于接口层向业务层之间传输数据
  • PO用于业务层与持久层之间传输数据

Service业务层尽量提供一个业务接口,即使两个前端接口(如手机端只有俩条件、PC端需要三个条件)需要的数据不一样,Service可以提供一个最全的查询结果,由Controller层进行数据整合

service层提供

  • 增加MP依赖
  • 添加MP配置类配置分页拦截器
  • 然后在yml中配置数据库连接信息和日志信息等(后续迁移到nacos)

PS: 查询结果中包含课程状态信息:

  • 状态一般使用代号表示

  • 一个项目中应该有多种状态,课程发布状态、媒资处理状态、错误码、课程等级 等

  • 为了统一管理 ,新建一个微服务xc_system,以及系统管理数据库,专门查询这个状态字典

分页查询MP实现步骤:

  • 引入依赖,添加分页拦截器,连接数据库

  • 代码生成器生成 po 类(放在 model ) 、 mapper 以及对象的mapper.xml (放在 service)

  • 编写自己的service类

    • 一般根据 DTO类 编写(一个DTO类一个Service)

    • 对于分页查询:

      • 有分页参数(编写专门的一个类,页码 每页记录数)和 查询条件(DTO)
    • 查询分页 service 的实现函数

      • 构建查询条件 :

        LambdaQueryWrapper<CourseBase> queryWrapper = new LambdaQueryWrapper<>();

        • 构建查询条件,注意判空 ,可链式添加

          queryWrapper.like(StringUtils.isNotEmpty(queryCourseParams.getCourseName()), CourseBase::getCompanyName, queryCourseParams.getCourseName());

      • 分页

        • 构建分页

          Page<CourseBase> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());

        • 查询(将结果放进MP自带的封装类)

          Page<CourseBase> pageInfo = courseBaseMapper.selectPage(page, queryWrapper);

        • 获取Page中封装的数据,是一个列表

          List<CourseBase> items = pageInfo.getRecords();

          long counts = pageInfo.getTotal();

        • 封装到自己的分页类

          return new PageResult<>(items, counts ,pageParams.getPageNo(), pageParams.getPageSize());

  • 完善接口

  • 前后端联调,关于跨域(8601前端 异步 请求了 system 63110 资源 被 CORS禁止) 查看我的文章 跨域问题解决方案及原理 (分类 技术 - 前后端)

课程分类查询

新建课程时要选择: 课程分类、课程等级、课程类型,课程等级和课程类型都来源于数字字典表,此部分的信息前端已从系统管理服务中读取。所以仅需完成课程分类的查询

课程分类表如下:

img

这张表是一个树形结构,通过父节点id将各元素组成一个树,下面是一部分数据

img

  • 那么现在的需求就是:在内容管理服务中编写一个接口,读取课程分类表的数据,组成一个树形结构返回给前端

    • 分析格式:

      • 最外一层时数组结构,数组的元素即为分类信息(一级分类)

        "id" : "1-2",
        "isLeaf" : null,
        "isShow" : null,
        "label" : "移动开发",
        "name" : "移动开发",
        "orderby" : 2,
        "parentid" : "1"
        "childrenTreeNodes":[ { 子分类信息。。。}]
- 分类信息中`childrenTreeNodes`的又时下一层**分类数组**,数组中是下一层分类信息,子分类信息中的childrenTreeNodes为空
  • 设计DTO类

    • 设计一级分类DTO 继承 CourseCategory,多出来一个childrenTreeNodes 属性 是一个list,到时候与用于存放改一级分类下的子分类信息。
  • 查询,填充数据,(如何形成树状结构?)

    • 方案一:利用SQL语句的内连接
    SELECT * FROM 
    course_category one
    JOIN course_category two 
    ON two.parentid = one.id
    WHERE one.parentid = '1'

    缺点:我们有三级分类的话,那么还得继续修改SQL语句

        SELECT * FROM 
        course_category one
        JOIN course_category two  
        ON two.parentid = one.id
        JOIN course_category three 
    +   ON three.parentid = two.id
        WHERE one.parentid = '1'

    所以如果当树的层级不固定时,此时就可以使用MySQL的递归实现,使用with语法,下面举一个简单的例子

    WITH RECURSIVE t1 AS (
        SELECT 1 AS n
        UNION ALL
        SELECT n + 1 FROM t1 WHERE n < 5
    )
    SELECCT * FROM t1;

    那么把上面的SQL语句用with实现

    WITH RECURSIVE t1 AS (
        SELECT p.* FROM course_category p WHERE p.id = '1'
        UNION ALL
        SELECT c.* FROM course_category c JOIN t1 WHERE c.parentid = t1.id
    )
    SELECT * FROM t1;
查询结果:



![img](https://differencer.oss-cn-beijing.aliyuncs.com/img/pSDJXSe.png)

​    成功查询到数据了之后,我们现在就需要用Java代码将其组装成树形结构,在此之前,我们先来编写mapper(采用更灵活的递归查询)

编写service中的mapper接口

public interface CourseCategoryMapper extends BaseMapper<CourseCategory> {
    List<CourseCategoryTreeDto> selectTreeNodes(String id);
}
再别写其对应的xml
<select id="selectTreeNodes" parameterType="string" resultMap="com.xuecheng.content.model.dto.CourseCategoryTreeDto">
    WITH RECURSIVE t1 AS (
        SELECT p.* FROM course_category p WHERE p.id = #{id}
        UNION ALL
        SELECT c.* FROM course_category c JOIN t1 WHERE c.parentid = t1.id
    )
    SELECT * FROM t1;
</select>
编写queryTreeNodes(String id)函数 实现
@Slf4j
@Service
public class CourseCategoryServiceImpl implements CourseCategoryService {
    @Autowired
    private CourseCategoryMapper courseCategoryMapper;

    @Override
    public List<CourseCategoryTreeDto> queryTreeNodes(String id) {
        // 获取所有的子节点
        List<CourseCategoryTreeDto> categoryTreeDtos = courseCategoryMapper.selectTreeNodes(id);
        // 定义一个List,作为最终返回的数据
        List<CourseCategoryTreeDto> result = new ArrayList<>();
        // 为了方便找子节点的父节点,这里定义一个HashMap,key是节点的id,value是节点本身
        HashMap<String, CourseCategoryTreeDto> nodeMap = new HashMap<>();
        // 将数据封装到List中,只包括根节点的下属节点(1-1、1-2 ···),这里遍历所有节点
        categoryTreeDtos.stream().forEach(item -> {
            // 这里寻找父节点的直接下属节点(1-1、1-2 ···)
            if (item.getParentid().equals(id)) {
                nodeMap.put(item.getId(), item);
                result.add(item);
            }
            // 获取每个子节点的父节点
            String parentid = item.getParentid();
            CourseCategoryTreeDto parentNode = nodeMap.get(parentid);
            // 判断HashMap中是否存在该父节点(按理说必定存在,以防万一)
            if (parentNode != null) {
                // 为父节点设置子节点(将1-1-1设为1-1的子节点)
                List childrenTreeNodes = parentNode.getChildrenTreeNodes();
                // 如果子节点暂时为null,则初始化一下父节点的子节点(给个空集合就行)
                if (childrenTreeNodes == null) {
                    parentNode.setChildrenTreeNodes(new ArrayList<CourseCategoryTreeDto>());
                }
                // 将子节点设置给父节点
                parentNode.getChildrenTreeNodes().add(item);
            }
        });
        // 返回根节点的直接下属节点(1-1、1-2 ···)
        return result;
    }
}
效果 ![img](https://differencer.oss-cn-beijing.aliyuncs.com/img/pSDUpwR.png)

新增课程