学成在线--内容管理模块

  • 完整版请移步至我的个人博客查看:https://cyborg2077.github.io/

  • 学成在线–项目环境搭建

  • 学成在线–内容管理模块

  • 学成在线–媒资管理模块

  • 学成在线–课程发布模块

  • 学成在线–认证授权模块

  • 学成在线–选课学习模块

  • 学成在线–项目优化

  • Git仓库:https://github.com/Cyborg2077/xuecheng-plus

模块需求分析

什么是需求分析

  • 百度百科汇总对需求分析的定义如下

需求分析也称为软件需求分析、系统需求分析或需求分析工程等,是开发人员经过深入细致的调研和分析,准确理解用户和项目的功能、性能、可靠性等具体需求,将用户非形式的需求表叔转化为完整的需求定义,从而确定系统必须做什么的过程

  • 简单理解就是要高清问题域,问题域就是用户的需求,软件要为用户解决什么问题,实现哪些业务功能,满足什么样的性能要求

  • 那么如何做需求分析?

    1. 首先确认用户需求
      • 用户需求即用户的原始需求。通过用户访谈、问卷调查、开会讨论、查阅资料等调研手段梳理用户的原始需求,产品人员根据用户需求绘制界面原型,再通过界面原型让用户确认需求是否符合预期
    2. 确认关键问题
      • 用户的原始需求可能是含糊不清的,需求分析要从繁杂的问题中梳理出关键问题。比如:俭学机构的老师想要将课程发布到网上,这是原始需求,根据这个用户需求我们需要进行扩展分析,扩展出以下几个点:
        1. 课程发布需要哪些信息?
        2. 如果用户发布了不良信息怎么办?
        3. 课程发布后用户怎么查看?
      • 课程发布需要课程名称、价格、介绍、图片(封面)、师资信息等。继续延伸分析:这么多课程信息进行归类,方便用户编辑,可以分为课程基本信息、课程营销信息、课程师资信息。
      • 按照这样的思路对用户需求逐项分析,梳理出若干问题,再从中找到关键信息。比如:上边对课程信息分类后,哪些是关键信息,课程名称、课程图片、课程介绍扥基本信息为关键信息,所以发布课程的第一步要编写课程基本信息。
      • 找到了关键问题,下一步就是进行数据建模,创建课程基本信息表,并设计其中的字段
    3. 数据建模
      • 数据建模要根据分析的关键问题将其相关的信息全部建模。比如:根据发布课程的用户需求,可创建课程基本信息表、课程营销信息表、课程师资表、课程发布记录表、课程审核记录表等
    4. 编写需求规格说明书
      • 针对每一个关键问题最终都需要编写需求规格说明书,包括:功能名称、功能描述、参与者、基本时间流程、可选事件流、数据描述、前置条件、后置条件等
      • 比如添加课程的需求规格如下
项目 添加课程
功能名称 添加课程
功能描述 添加课程基本信息
参与者 教学机构管理员
前置条件 教学机构管理只允许向自己机构添加课程
拥有添加课程的权限
基本事件流程 1、登录教学机构平台
2、进入课程列表页面
3、点击添加课程按钮进入添加课程界面
4、填写课程基本信息
5、点击提交。
可选事件流程 成功:提示添加成功,跳转到课程营销信息添加界面
失败:提示具体的失败信息,用户根据失败信息进行修改。
数据描述 课程基本信息:课程id、课程名称、课程介绍、课程大分类、课程小分类、课程等级、课程图片、所属机构、课程创建时间、课程修改时间、课程状态
后置条件 向课程基本信息插入一条记录
补充说明

模块介绍

  • 内容管理这个词存在于很多软件系统,什么是内容管理?
  • 内容管理系统(content management system,CMS),是一种位于WEB前端(Web服务器)和后端办公系统或流程(内容创作、编辑)之间的软件系统。内容的创作人员,编辑人员、发布人员使用内容管理系统来提交、修改、审批、发布内容。这里的内容可能包括文件、表格、图片、数据库中的数据甚至视频等一切你想法不到网站的信息
  • 本项目作为一个大型的在线教育平台,其内容管理模块主要对课程及相关内容进行管理,包括:课程的基本信息、课程图片、课程师资信息、课程的授课计划、课程视频、课程文档等内容的管理

业务流程

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

界面原型

  • 产品工程师根据用户需求制作产品界面原型,开发工程师除了根据用户需求进行需求分析以外,还会根据界面原型上的元素信息进行需求分析

数据模型

  • 数据模型就是对应的数据库表

创建模块工程

模块工程结构

  • 在之前我们已经创建好了项目父工程和基础工程
    学成在线--内容管理模块_第1张图片

  • 那下面我们继续来创建内容管理模块的工程结构。本项目是一个前后端分离项目,前端与后端开发人员之间主要依据接口进行开发,前后端交互流程如下

    1. 前端请求后端服务提供的接口
    2. 后端服务的Controller层接收前端的请求
    3. Controller层调用Service层进行业务处理
    4. Service层调用Dao持久层对数据持久化
  • 流程分为前端、接口层、业务层三部分,所以模块工程结构如下图所示

    • xuecheng-plus-content-api:接口工程,为前端提供接口
    • xuecheng-plus-content-service:业务工程,为接口工程提供业务支撑
    • xuecheng-plus-content-model:数据模型工程,存储数据模型类、数据传输类型等
      学成在线--内容管理模块_第2张图片
  • 结合项目父工程、项目基础工程后,如下图

    • xuecheng-plus-content:内容管理模块工程,负责聚合xuecheng-plus-content-api、xuecheng-plus-content-service、xuecheng-plus-content-model
      学成在线--内容管理模块_第3张图片

创建模块工程

  1. 创建内容管理模块父工程xuecheng-plus-content,修改pom.xml,声明为聚合工程,且有三个子模块


    4.0.0
    
        com.xuecheng
        xuecheng-plus-parent
        0.0.1-SNAPSHOT
        ../xuecheng-plus-parent
    
    com.xuecheng
    xuecheng-plus-content
    0.0.1-SNAPSHOT
    xuecheng-plus-content
    xuecheng-plus-content
    pom
    
        1.8
    
    
        xuecheng-plus-content-api
        xuecheng-plus-content-model
        xuecheng-plus-content-service
    


  1. 创建xuecheng-plus-content-api工程,设置父工程为xuecheng-plus-content,按照上图中的依赖关系修改pom文件


    4.0.0
    
        com.xuecheng
        xuecheng-plus-content
        0.0.1-SNAPSHOT
    
    xuecheng-plus-content-api
    0.0.1-SNAPSHOT
    xuecheng-plus-content-api
    xuecheng-plus-content-api
    
        1.8
    
    
        
            org.springframework.boot
            spring-boot-starter
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            com.xuecheng
            xuecheng-plus-content-service
            0.0.1-SNAPSHOT
        
        
            com.xuecheng
            xuecheng-plus-content-model
            0.0.1-SNAPSHOT
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    


  1. 创建xuecheng-plus-content-model工程,设置父工程为xuecheng-plus-content,按照上图中的依赖关系修改pom文件


    4.0.0
    
        com.xuecheng
        xuecheng-plus-content
        0.0.1-SNAPSHOT
    
    xuecheng-plus-content-model
    0.0.1-SNAPSHOT
    xuecheng-plus-content-model
    xuecheng-plus-content-model
    
        1.8
    
    
        
            com.xuecheng
            xuecheng-plus-base
            0.0.1-SNAPSHOT
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    


  1. 创建xuecheng-plus-content-service工程,设置父工程为xuecheng-plus-content,按照上图中的依赖关系修改pom文件


    4.0.0
    
        com.xuecheng
        xuecheng-plus-content
        0.0.1-SNAPSHOT
    
    xuecheng-plus-content-service
    0.0.1-SNAPSHOT
    xuecheng-plus-content-service
    xuecheng-plus-content-service
    
        1.8
    
    
        
            org.springframework.boot
            spring-boot-starter
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            com.xuecheng
            xuecheng-plus-content-model
            0.0.1-SNAPSHOT
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    


  • 创建好的目录结构如下图所示
    学成在线--内容管理模块_第4张图片

课程查询

需求分析

业务流程

  1. 教学机构恩怨点击课程管理,进入课程查询界面
  2. 在课程查询页面输入查询条件查询课程信息
    • 当不输入查询条件时,输出全部课程信息
    • 输入查询条件,查询符合条件的课程信息
    • 约束:教学机构只允许查询本教学机构的课程信息

数据模型

  • 课程查询功能涉及到的数据表有;课程基本信息表,教学计划表
    学成在线--内容管理模块_第5张图片
    学成在线--内容管理模块_第6张图片
  • 下面从查询条件、查询列表两方面进行分析
    1. 查询条件
      • 包括:课程名称、课程审核状态、课程发布状态
      • 课程名称:可以模糊搜索
      • 课程审核状态:未提交、已提交、审核通过、审核未通过
      • 课程发布状态:未发布、已发布、已下线
      • 因为是分页查询,所以查询条件中还要包括当前页码、每页显示记录数
    2. 查询结果
      • 包括:课程id、课程名称、任务数、创建时间、审核状态、类型
      • 从结果上看基本来源于课程基本信息表,任务书需要关联教学计划表查询
      • 因为是分页查询,所以查询结果中还要包括总记录数、当前页码、每页显示记录数

生成PO

  • PO即持久对象(Persistent Object),它们是由一组属性和属性的get/set方法组成的。PO对应于数据库的表

  • 在开发持久层代码需要根据数据表编写PO类,在实际开发中通常使用代码生成器工具来生成PO类代码(人人开源、MP代码生成器等)

  • 这里是使用的MP的generator工程生成PO类,详细操作可以参考我这篇文章
    {% link MyBatisPlus, https://cyborg2077.github.io/2022/09/20/MyBatisPlus/, https://pic.imgdb.cn/item/6335135c16f2c2beb100182d.jpg %}

  • 将生成好的PO类拷贝至xuecheng-plus-content-model工程的com.xuecheng.content.model.po包下,同时在model工程的pom.xml中添加MP的依赖,版本控制在父工程已经完成了


    com.baomidou
    mybatis-plus-annotation
    ${mybatis-plus-boot-starter.version}

接口定义

接口定义分析

  • 定义一个接口需要包括以下几个方面

    1. 协议
      • 通常使用HTTP协议,查询类的接口请求方式通常为GET或POST,查询条件较少的时候使用GET,较多的时候使用POST
      • 本接口使用http post
      • 同时也要确定content-type,参数以什么数据格式提交,结果以什么数据格式响应
      • 一般情况下都以json格式响应
    2. 分析请求参数
      • 根据前面对数据模型的分析,请求参数为:课程名称、课程审核状态、当前页码、每页显示的记录数
      • 根据分析的请求参数定义模型类
    3. 分析响应结果
      • 根据前面对数据模型的分析,响应结果为数据列表和一些分页信息(总记录数、当前页码、每页显示记录数)
      • 数据列表中数据的属性包括:课程id、课程名称、任务数、创建时间、审核状态、类型
        {% note warning no-icon %}注意:查询结果中的审核状态为数据字典中的代码字段,前端会根据审核状态代码字段找到对应的名称显示(例如错误响应码404/401/502等){% endnote %}
      • 根据分析的相应结果定义模型类
    4. 分析完成,使用SpringBoot注解开发一个Http接口
    5. 使用接口文档工具查看接口的内容
    6. 接口中调用Service方法完成业务处理
  • 接口请求示例

POST /content/course/list?pageNo=2&pageSize=1
Content-Type: application/json

{
  "auditStatus": "202002",
  "courseName": ""
}
//成功响应结果
{
  "items": [
    {
      "id": 26,
      "companyId": 1232141425,
      "companyName": null,
      "name": "spring cloud实战",
      "users": "所有人",
      "tags": null,
      "mt": "1-3",
      "mtName": null,
      "st": "1-3-2",
      "stName": null,
      "grade": "200003",
      "teachmode": "201001",
      "description": "本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud 基础入门 3.实战Spring Boot 4.注册中心eureka。",
      "pic": "https://cdn.educba.com/academy/wp-content/uploads/2018/08/Spring-BOOT-Interview-questions.jpg",
      "createDate": "2019-09-04 09:56:19",
      "changeDate": "2021-12-26 22:10:38",
      "createPeople": null,
      "changePeople": null,
      "auditStatus": "202002",
      "auditMind": null,
      "auditNums": 0,
      "auditDate": null,
      "auditPeople": null,
      "status": 1,
      "coursePubId": null,
      "coursePubDate": null
    }
  ],
  "counts": 23,
  "page": 2,
  "pageSize": 1
}

课程查询接口定义

  1. 定义请求模型类

    • 对于查询条件较多的接口定义单独的模型类接收参数
    • 由于分页查询这一类的接口在项目中很多地方都会用到,这里针对分页查询的参数(当前页码、每页显示的记录数)单独在xuecheng-plus-base基础工程中定义
    package com.xuecheng.base.model;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    
    @Data
    @AllArgsConstructor
    public class PageParams {
        // 默认起始页码
        public static final long DEFAULT_PAGE_CURRENT = 1L;
        // 默认每页记录数
        public static final long DEFAULT_PAGE_SIZE = 10L;
    
        // 当前页码
        private Long pageNo = DEFAULT_PAGE_CURRENT;
    
        // 当前每页记录数
        private Long pageSize = DEFAULT_PAGE_SIZE;
    }
    
    • 除了分页查询参数,剩下的就是课程查询的特有参数,此时需要在内容管理的model工程中定义课程查询的参数模型类
    • 定义DTO包,DTO即数据传输对象(Data Transfer Object),用于接口层和业务层之间传输数据
    package com.xuecheng.content.model.dto;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    
    @Data
    @AllArgsConstructor
    public class QueryCourseParamDto {
        // 审核状态
        private String auditStatus;
        // 课程名称
        private String courseName;
        // 发布状态
        private String publishStatus;
    }
    
  2. 多样化的模型类

    • 现在项目中有两种模型类:DTO数据传输对象、PO持久化对象。
      • DTO用于接口层向业务层之间传输数据
      • PO用于业务层与持久层之间传输数据
      • 有些公司还会设置VO对象,VO对象用在前端和接口层之间传输数据
      • 当前端有多个平台且接口存在差异时,就需要设置VO对象用于前端和接口层传输数据。比如:课程列表查询接口,根据用户需求,用户在手机端也要查询课程信息,此时课程查询接口是否需要编写手机端和PC端两个接口呢?
      • 如果用户要求通过手机和PC的查询条件或查询结果不一样,那此时就需要定义两个Controller课程查询接口,每个接口定义VO对象与前端传输数据
        • 手机查询:根据课程状态查询,查询结果只有课程名称和课程状态
        • PC查询:可以改根据课程名称、课程状态、课程审核状态等条件查询,查询结果也比手机查询的结果内容多
      • 此时,Service业务层尽量提供一个业务接口,即使两个前端接口需要的数据不一样,Service可以提供一个最群的查询结果,由Controller层进行数据整合。
        学成在线--内容管理模块_第7张图片
    • 如果前端接口没有多样性,且比较固定,此时可以取消VO,只用DTO即可
  3. 定义响应模型类

    • 根据接口分析,下面定义响应结果模型类
    • 针对分页查询结果经过分析,也存在固定的数据和格式,所以还是在base工程定义一个基础的结果模型类
    import lombok.AllArgsConstructor;
    import lombok.Data;
    
    import java.io.Serializable;
    import java.util.List;
    
    @Data
    @AllArgsConstructor
    public class PageResult implements Serializable {
        // 数据列表
        private List items;
        // 总记录数
        private long counts;
        // 当前页码
        private long page;
        // 每页记录数
        private long pageSize;
    }   
    
  4. 定义接口

    • 根据分析,此接口提供http post协议,查询条件以json格式提交,响应结果为json格式
    • 首先咋xuecheng-plus-content-api中添加依赖
    
        
            com.xuecheng
            xuecheng-plus-content-model
            0.0.1-SNAPSHOT
        
        
            com.xuecheng
            xuecheng-plus-content-service
            0.0.1-SNAPSHOT
        
        
        
            org.springframework.cloud
            spring-cloud-context
        
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
        
            org.springframework.boot
            spring-boot-starter
            
                
                    org.springframework.boot
                    spring-boot-starter-logging
                
            
        
            
                org.springframework.boot
                spring-boot-starter-validation
            
        
        
            org.springframework.boot
            spring-boot-starter-log4j2
        
    
        
        
            com.spring4all
            swagger-spring-boot-starter
            1.9.0.RELEASE
        
    
    
    
    • 之后定义Controller方法
      • 说明:pageParams分页参数通过url的key/value传入,queryCourseParams通过json数据传入,所以queryCourseParams前面需要用@RequestBody注解将json转为QueryCourseParamDto对象。这里的两个@Api注解是swagger的,用于描述接口的
    @RestController
    @Api(value = "课程信息编辑接口", tags = "课程信息编辑接口")
    public class CourseBaseInfoController {
        @PostMapping("/course/list")
        @ApiOperation("课程查询接口")
        public PageResult list(PageParams pageParams, @RequestBody QueryCourseParamDto queryCourseParams) {
            CourseBase courseBase = new CourseBase();
            courseBase.setId(15L);
            courseBase.setDescription("测试课程");
            PageResult result = new PageResult<>();
            result.setItems(Arrays.asList(courseBase));
            result.setPage(1);
            result.setPageSize(10);
            result.setCounts(1);
            return result;
        }
    }
    
    • 定义启动类,使用@EnableSwagger2Doc注解,启用Swagger
    
    
    • 添加配置文件
      {% tabs 日志和最高优先级配置项 %}
    
    
        
            logs
            %date{YYYY-MM-dd HH:mm:ss,SSS} %level [%thread][%file:%line] - %msg%n%throwable
        
        
            
                
            
    
            
                
                
                
                    
                
            
    
            
                
                
                
                    
                
            
            
            
            
                
                
            
        
        
        
            
            
            
            
            
            
            
    
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
    
            
            
    
            
            
    
            
            
            
            
            
            
            
            
            
            
            
            
                
                
                
            
        
    
    
    server:
      servlet:
        context-path: /content
      port: 63040
    # 微服务配置
    spring: 
      application:
        name: content-api
    
    # 日志文件配置路径
    logging:
      config: classpath:log4j2-dev.xml
    
    # swagger 文档配置
    swagger:
      title: "学成在线内容管理系统"
      description: "内容系统管理系统对课程相关信息进行业务管理数据"
      base-package: com.xuecheng.content
      enabled: true
      version: 1.0.0
    

    {% endtabs %}

    • 运行启动类,访问http://localhost:63040/content/swagger-ui.html, 查看接口信息
      学成在线--内容管理模块_第8张图片
    • 使用PostMan测试我们的接口也能返回我们的测试数据
      学成在线--内容管理模块_第9张图片

Swagger介绍

  • 什么是Swagger?
    • OpenAPI规范(OpenAPI Specification 简称OAS)是Linux基金会的一个项目,试图通过定义一种用来描述API格式或API定义的语言,来规范RESTful服务开发过程,并且已经发布并开源在GitHub上:https://github.com/OAI/OpenAPI-Specification
    • Swagger是全球最大的OpenAPI规范(OAS)API开发工具框架,Swagger是一个在线接口文档的生成工具,前后端开发人员依据接口文档进行开发,只要添加Swagger的依赖和配置信息即可使用它
      {% tabs swagger的xml和yml %}
    
    
        com.spring4all
        swagger-spring-boot-starter
    
    
    • base-package为包扫描路径,扫描Controller类
    swagger:
      title: "学成在线内容管理系统"
      description: "内容系统管理系统对课程相关信息进行业务管理数据"
      base-package: com.xuecheng.content
      enabled: true
      version: 1.0.0
    
    {% endtabs %}
  • SpringBoot可以集成Swagger,Swagger根据Controller类中的注解生成接口文档,在模型类上也可以添加注解对模型类的属性进行说明,方便对接口文档的阅读,例如在我们之前编写的PageParams模型类上添加注解
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageParams {
    // 默认起始页码
    public static final long DEFAULT_PAGE_CURRENT = 1L;
    // 默认每页记录数
    public static final long DEFAULT_PAGE_SIZE = 10L;

    // 当前页码
+   @ApiModelProperty("当前页码")
    private Long pageNo = DEFAULT_PAGE_CURRENT;

    // 当前每页记录数
+   @ApiModelProperty("每页记录数")
    private Long pageSize = DEFAULT_PAGE_SIZE;
}
  • 重启服务,再次进入接口文档,可以看到添加的描述
    学成在线--内容管理模块_第10张图片
  • Swagger常用的注解如下
@Api 修饰整个类,描述Controller的作用
@ApiOperation 描述一个类的一个方法,或者说一个接口
@ApiParam 单个参数描述
@ApiModel 用对象来接收参数
@ApiModelProperty 用对象接收参数时,描述对象的一个字段
@ApiResponse HTTP响应其中1个描述
@ApiResponses HTTP响应整体描述
@ApiIgnore 使用该注解忽略这个API
@ApiError 发生错误返回的信息
@ApiImplicitParam 一个请求参数
@ApiImplicitParams 多个请求参数

接口开发

DAO开发

  • 业务层为接口层提供业务处理支撑,本项目业务层包括了持久层代码,一些大型公司的团队职责划分更细,会将持久层和业务层分为两个工程,不过这需要增加成本
  • DAO即数据访问对象,通过DAO去访问数据库对数据进行持久化,本项目持久层使用MyBatisPlus进行开发
  • 持久层的基础代码我们使用MP提供的代码生成器生成,将生成的Mapper和对应的xml拷贝纸service工程的com.xuecheng.content.mapper包下
  • 同时在service的pom.xml中添加MP和日志的一些依赖

    
        com.xuecheng
        xuecheng-plus-content-model
        0.0.1-SNAPSHOT
    

    
    
        mysql
        mysql-connector-java
        runtime
    

    
    
        com.baomidou
        mybatis-plus-boot-starter
    
    
    
        org.springframework.boot
        spring-boot-starter-test
        test
    
    
    
        org.springframework.boot
        spring-boot-starter
        
            
                org.springframework.boot
                spring-boot-starter-logging
            
        
    

    
    
        org.springframework.boot
        spring-boot-starter-log4j2
    


  • 在com.xuecheng.content.config包下创建MP配置类,配置分页拦截器
/**
 * MybatisPlus配置
 */
@Configuration
@MapperScan("com.xuecheng.content.mapper")
public class MybatisPlusConfig {
    /**
     * 定义分页拦截器
     * @return
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}
  • 然后在yml中配置数据库连接信息和日志信息等
spring:
  application:
    name: content-service
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/xc_content?serverTimezone=UTC&userUnicode=true&useSSL=false
    username: root
    password: root
logging:
  config: classpath:log4j2-dev.xml
  • 最后编写测试方法并进行测试,控制台可以输出查到的数据(前提保证你的id真的对应有数据)
@Slf4j
@SpringBootTest(classes = XuechengPlusContentServiceApplication.class)
class XuechengPlusContentServiceApplicationTests {
    @Autowired
    CourseBaseMapper courseBaseMapper;

    @Test
    void contextLoads() {
        CourseBase courseBase = courseBaseMapper.selectById(22);
        log.info("查询到数据:{}", courseBase);
        Assertions.assertNotNull(courseBase);
    }
}

Service开发

  1. 数据字典表

    • 课程基本信息查询的主要数据来源是课程基本信息表,这里有一点需要注意:课程的审核状态、发布状态
    • 审核状态在查询条件和查询结果中都存在,包括:未审核、审核通过、审核未通过这三种
    • 那么我们思考一个问题:直接在数据库表中的字段填充审核未通过这几个大字,合适吗?
      • 如果将审核未通过这五个字记录在课程基本信息表中,查询出来的状态就是审核未通过这几个字,那么如果有一天客户想把审核未通过改为未通过,怎么办?
      • 词汇我们可以批量处理数据库中的数据,写一个update语句,将所有的审核未通过更新为未通过。看起来解决了问题,但是万一后期客户抽风又想改呢?真实情况就是这样,但是这一类数据也有共同点:它有一些分类项,且这些分类项比较固定,大致的意思都是一样的,只是表述方式不一样。
    • 那么针对这一类数据,为了提高系统的可扩展性,专门定义数据字典去维护,例如
      [
          {"code":"202001","desc":"审核未通过"},
          {"code":"202002","desc":"未审核"},
          {"code":"202003","desc":"审核通过"}
      ]
      
    • 那么我们创建系统管理数据库xc_system,在其中创建管理系统服务的数据表,导入黑马提供的SQL脚本就好了。这样查询出的数据在前端展示时,就根据代码取出它对应的内容显示给用户。如果客户需要修改审核未通过的显示内容,直接在数据字典中修改就好了,无需修改课程基本信息表
      学成在线--内容管理模块_第11张图片
  2. Service开发

    • 首先创建Service接口
    public interface CourseBaseInfoService {
        /**
        * 课程查询接口
        * @param pageParams 分页参数
        * @param queryCourseParams 查询条件
        * @return
        */
        PageResult queryCourseBaseList(PageParams pageParams, QueryCourseParamDto queryCourseParams);
    }
    
    • 然后创建实现类
    @Service
    public class CourseBaseInfoServiceImpl implements CourseBaseInfoService {
        @Resource
        CourseBaseMapper courseBaseMapper;
    
        @Override
        public PageResult queryCourseBaseList(PageParams pageParams, QueryCourseParamDto queryCourseParams) {
            // 构建条件查询器
            LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
            // 构建查询条件:按照课程名称模糊查询
            queryWrapper.like(StringUtils.isNotEmpty(queryCourseParams.getCourseName()), CourseBase::getCompanyName, queryCourseParams.getCourseName());
            // 构建查询条件,按照课程审核状态查询
            queryWrapper.eq(StringUtils.isNotEmpty(queryCourseParams.getAuditStatus()), CourseBase::getAuditStatus, queryCourseParams.getAuditStatus());
            // 构建查询条件,按照课程发布状态查询
            queryWrapper.eq(StringUtils.isNotEmpty(queryCourseParams.getPublishStatus()), CourseBase::getStatus, queryCourseParams.getPublishStatus());
            // 分页对象
            Page page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
            // 查询数据内容获得结果
            Page pageInfo = courseBaseMapper.selectPage(page, queryWrapper);
            // 获取数据列表
            List items = pageInfo.getRecords();
            // 获取数据总条数
            long counts = pageInfo.getTotal();
            // 构建结果集
            return new PageResult<>(items, counts, pageParams.getPageNo(), pageParams.getPageSize());
        }
    }
    
    • 使用单元测试类进行测试,
    @Resource
    CourseBaseInfoService courseBaseInfoService;
    
    @Test
    void contextQueryCourseTest() {
        PageResult result = courseBaseInfoService.queryCourseBaseList(new PageParams(1L, 10L), new QueryCourseParamDto());
        log.info("查询到数据:{}", result);
    }
    

接口代码完善

  • 控制层、业务层、持久层三层通常可以面向接口并行开发,比如:业务层开发的同时可以先只编写一个Service接口,接口层的同时即可以面向Service接口去开发,待接口层和业务层完成后进行联调。
  • 下面是课程查询接口的实现
    @Resource
    CourseBaseInfoService courseBaseInfoService;
    @ApiOperation("课程查询接口")
    @PostMapping("/course/list")
    public PageResult list(PageParams pageParams, @RequestBody QueryCourseParamDto queryCourseParams) {
        PageResult result = courseBaseInfoService.queryCourseBaseList(pageParams, queryCourseParams);
        return result;
    }
  • 我们可以在Swagger中进行测试,也可以在PostMan中进行测试
    • 这里演示一下在Swagger中测试,输入参数,这里查询条件均设为空,当前页码1,页大小2
      学成在线--内容管理模块_第12张图片
    • 测试成功,查询到了数据
      学成在线--内容管理模块_第13张图片

接口测试

HttpClient测试

  • Swagger是一个在线接口文档,虽然使用它也能测试,但是需要浏览器进入Swagger,最关键的是它不能保存测试数据。
  • PostMan对内存的消耗也比较大,而且需要下载客户端。
  • 在IDEA中有一个非常方便的http接口测试工具HTTP Client,下面介绍它的使用方法,后面我们使用它来进行接口测试
    {% note warning no-icon %}
    先检查一下自己的IDEA是否已经安装了HTTP Client插件
    {% endnote %}
  • 进入Controller类,找到HTTP接口对应的方法
    学成在线--内容管理模块_第14张图片
  • 点击在HTTP客户端中生成请求,即可生成一个测试用例,IDEA会为我们生成一个.http结尾的文件,我们可以添加请求参数进行测试
### 课程查询列表
POST http://localhost:63040/content/course/list?pageNo=1&pageSize=10
Content-Type: application/json

{
  "auditStatus": "",
  "courseName": "",
  "publishStatus": ""
}
  • 同样通过测试,可以查询到数据
    学成在线--内容管理模块_第15张图片
  • .http文件即测试用例文档,它可以随着项目工程一起保存(也可以提交git),这样测试数据就可以保存下来,方便进行测试
  • 为了方便保存.http文件,我们单独在项目工程的根目录下创建一个目录单独来存放他们
  • 同时为了将来方便和网关集成测试,这里把测试主机地址在配置文件http-client.env.json中配置
{
  "dev": {
    "host": "localhost:63010",
    "content_host": "localhost:63040",
    "system_host": "localhost:63110",
    "media_host": "localhost:63050",
    "cache_host": "localhost:63035"
  }
}
  • 那么现在就可以用{{content_host}}替换掉原来的http://localhost:63040 了,同时环境改为dev
    学成在线--内容管理模块_第16张图片

导入系统管理服务

  • 要进行前后端联调首先启动前端工程,浏览器访问http://localhost:8601/ ,此时会报错,因为还有一个接口我们还没有完成:http://localhost:63110/system/dictionary/all
  • 该接口指向的是系统管理服务,次链接是前端请求后端获取数据字典数据的接口地址
  • 拷贝黑马提供的xuecheng-plus-system工程到项目根目录即可,然后修改数据库连接配置,该工程仅包含数据字典对应的PO类,Mapper映射和一些配置类,然后提供了两个简单的接口,查询全部数据文档。
  • 启动系统管理服务,浏览器访问http://localhost:63110/system/dictionary/all ,如果可以正常读取数据字典的信息,则说明导入成功

解决跨域问题

  • 启动前端工程,工程首页不能正常显示,查看浏览器报错如下
Access to XMLHttpRequest at 'http://harib-eir.info/xuecheng-plus.com?adTagId=dbb6a410-0bec-11ec-8010-0a70670a1f67&fallbackUrl=ww87.xuecheng-plus.com' (redirected from 'http://localhost:8601/api/content/course/list?pageNo=1&pageSize=10') from origin 'http://localhost:8601' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
  • 提示:从http://localhost:8601访问http://localhost:63110/system/dictionary/all被CORS policy阻止,因为没有Access-Control-Allow-Origin 头信息。CORS全称是 cross origin resource share 表示跨域资源共享。

  • 比如:

    • 从http://localhost:8601 到 http://localhost:8602 由于端口不同,是跨域。
    • 从http://192.168.101.10:8601 到 http://192.168.101.11:8601 由于主机不同,是跨域。
    • 从http://192.168.101.10:8601 到 https://192.168.101.11:8601 由于协议不同,是跨域。

{% note warning no-icon %}
注意:服务器之间不存在跨域请求。
{% endnote %}

  • 浏览器判断是跨域请求会在请求头上添加origin,表示这个请求来源哪里,例如
GET / HTTP/1.1
Origin: http://localhost:8601
  • 服务器接收到请求判断这个Origin是否跨域,如果允许则在响应头中说明允许该来源的跨域请求,如下
Access-Control-Allow-Origin:http://localhost:8601
  • 如果允许域名来源的跨域请求,则响应如下
Access-Control-Allow-Origin:*
  • 解决跨域的方法

    1. JSONP
      • 通过script标签的src属性进行跨域请求,如果服务端要响应内容,则先读取请求参数callback值,callback是一个回调函数的名称,服务端读取callback的值后将响应内容通过调用callback函数的方式告诉请求方
        学成在线--内容管理模块_第17张图片
    2. 添加响应头
      • 服务端在响应头添加Access-Control-Allow-Origin: *
    3. 通过nginx代理跨域
      • 由于服务端之间没有跨域,浏览器通过nginx去访问跨域地址
      • 浏览器先访问http://192.168.101.10:8601 nginx提供的地址,进入页面
      • 此页面要跨域访问http://192.168.101.11:8601 ,不能直接跨域访问http://www.baidu.com:8601 ,而是访问nginx的一个同源地址,比如:http://192.168.101.11:8601/api ,通过http://192.168.101.11:8601/api 的代理去访问http://www.baidu.com:8601。
      • 这样就实现了跨域访问。
      • 浏览器到http://192.168.101.11:8601/api 没有跨域
      • nginx到http://www.baidu.com:8601 通过服务端通信,没有跨域。
        学成在线--内容管理模块_第18张图片
  • 这里采用添加请求头的方式解决跨域问题。在xuecheng-plus-system-api模块下新建配置类GlobalCorsConfig

@Configuration
public class GlobalCorsConfig {

    @Bean
    public CorsFilter getCorsFilter() {
        CorsConfiguration configuration = new CorsConfiguration();
        //添加哪些http方法可以跨域,比如:GET,Post,(多个方法中间以逗号分隔),*号表示所有
        configuration.addAllowedMethod("*");
        //添加允许哪个请求进行跨域,*表示所有,可以具体指定http://localhost:8601 表示只允许http://localhost:8601/跨域
        configuration.addAllowedOrigin("*");
        //所有头信息全部放行
        configuration.addAllowedHeader("*");
        //允许跨域发送cookie
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", configuration);
        return new CorsFilter(urlBasedCorsConfigurationSource);
    }
}
  • 此配置类实现了跨域过滤器,在响应头添加Access-Control-Allow-Origin
  • 重启系统管理服务,前端工程可以正常进入http://localhost:8601 ,查看NetWork选项卡,跨域问题成功解决
    学成在线--内容管理模块_第19张图片

前后端联调

  • 前端启动完毕,在启动内容管理服务端。
  • 前端默认连接的是项目的网关地址,由于现在网关工程还没有创建,这里需要更改前端工程的参数配置文件,修改网关地址为内容管理服务的地址
  • 编辑.env文件
# 前台管理页面-端口
VUE_APP_CLIENT_MANAGE_PORT=8601
# 首页、列表、学习
VUE_APP_CLIENT_PORTAL_URL=http://www.xuecheng-plus.com
# 后台服务网关
#VUE_APP_SERVER_API_URL=http://www.xuecheng-plus.com/api
#VUE_APP_SERVER_API_URL=http://localhost:63010
#VUE_APP_SERVER_API_URL=http://172.16.63.20:63010
VUE_APP_SERVER_API_URL=http://localhost:63040
# 权限认证
#VUE_APP_SERVER_AUTHORIZATION=ewogICAgImF1ZCI6IFsKICAgICAgICAieHVlY2hlbmctcmVzb3VyY2UiCiAgICBdLAogICAgInBheWxvYWQiOiB7CiAgICAgICAgIjExNzcxNDQyMDk0NjMxMjgxMjUiOiB7CiAgICAgICAgICAgICJyZXNvdXJjZXMiOiBbCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJ1c2VyX2F1dGhvcml0aWVzIjogewogICAgICAgICAgICAgICAgInJfMDAxIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb21wYW55X21vZGlmeSIsCgkJCQkJInhjX2NvbXBhbnlfdmlldyIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2RlbCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2VkaXQiLAoJCQkJCSJ4Y19jb3Vyc2VfYmFzZV9saXN0IiwKCQkJCQkieGNfY291cnNlX2Jhc2Vfc2F2ZSIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX3ZpZXciLAoJCQkJCSJ4Y19jb3Vyc2VfcHVibGlzaCIsCgkJCQkJInhjX21hcmtldF9zYXZlX21vZGlmeSIsCgkJCQkJInhjX21hcmtldF92aWV3IiwKCQkJCQkieGNfbWVkaWFfZGVsIiwKCQkJCQkieGNfbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX3ByZXZpZXciLAoJCQkJCSJ4Y19tZWRpYV9zYXZlIiwKCQkJCQkieGNfdGVhY2hlcl9saXN0IiwKCQkJCQkieGNfdGVhY2hlcl9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaGVyX3NhdmUiLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2NvcnJlY3Rpb24iLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2xpc3QiLAoJCQkJCSJ4Y190ZWFjaHBsYW53b3JrX2RlbCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfbGlzdCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfc2F2ZV9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaHBsYW5fZGVsIiwKCQkJCQkieGNfdGVhY2hwbGFuX3NhdmVfbW9kaWZ5IiwKCQkJCQkieGNfdGVhY2hwbGFuX3ZpZXciCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgInJfMDAyIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb3Vyc2VfYWRtaW5fbGlzdCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2NvbW1pdCIsCgkJCQkJInhjX3N5c3RlbV9jYXRlZ29yeSIsCgkJCQkJInhjX21fbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX2F1ZGl0IgogICAgICAgICAgICAgICAgXQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfSwKICAgICJ1c2VyX25hbWUiOiAieGMtdXNlci1maXJzdCIsCiAgICAic2NvcGUiOiBbCiAgICAgICAgInJlYWQiCiAgICBdLAogICAgIm1vYmlsZSI6ICIxNTAxMjM0NTY3OCIsCiAgICAiZXhwIjogMTYwNjUyNTEyMiwKICAgICJjbGllbnRfYXV0aG9yaXRpZXMiOiBbCiAgICAgICAgIlJPTEVfVVNFUiIKICAgIF0sCiAgICAianRpIjogIjFlYjdlOTg3LWQ3YzItNDBmNS1iMGQ2LWNkNjEzOWNiMThlMCIsCiAgICAiY2xpZW50X2lkIjogInhjLWNvbS1wbGF0Zm9ybSIsCiAgICAiY29tcGFueUlkIjogMTIzMjE0MTQyNQp9
VUE_APP_SERVER_AUTHORIZATION=
#ewogICAgImF1ZCI6IFsKICAgICAgICAieHVlY2hlbmctcmVzb3VyY2UiCiAgICBdLAogICAgInBheWxvYWQiOiB7CiAgICAgICAgIjExNzcxNDQyMDk0NjMxMjgxMjUiOiB7CiAgICAgICAgICAgICJyZXNvdXJjZXMiOiBbCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJ1c2VyX2F1dGhvcml0aWVzIjogewogICAgICAgICAgICAgICAgInJfMDAxIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb21wYW55X21vZGlmeSIsCgkJCQkJInhjX2NvbXBhbnlfdmlldyIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2RlbCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2VkaXQiLAoJCQkJCSJ4Y19jb3Vyc2VfYmFzZV9saXN0IiwKCQkJCQkieGNfY291cnNlX2Jhc2Vfc2F2ZSIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX3ZpZXciLAoJCQkJCSJ4Y19jb3Vyc2VfcHVibGlzaCIsCgkJCQkJInhjX21hcmtldF9zYXZlX21vZGlmeSIsCgkJCQkJInhjX21hcmtldF92aWV3IiwKCQkJCQkieGNfbWVkaWFfZGVsIiwKCQkJCQkieGNfbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX3ByZXZpZXciLAoJCQkJCSJ4Y19tZWRpYV9zYXZlIiwKCQkJCQkieGNfdGVhY2hlcl9saXN0IiwKCQkJCQkieGNfdGVhY2hlcl9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaGVyX3NhdmUiLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2NvcnJlY3Rpb24iLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2xpc3QiLAoJCQkJCSJ4Y190ZWFjaHBsYW53b3JrX2RlbCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfbGlzdCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfc2F2ZV9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaHBsYW5fZGVsIiwKCQkJCQkieGNfdGVhY2hwbGFuX3NhdmVfbW9kaWZ5IiwKCQkJCQkieGNfdGVhY2hwbGFuX3ZpZXciCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgInJfMDAyIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb3Vyc2VfYWRtaW5fbGlzdCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2NvbW1pdCIsCgkJCQkJInhjX3N5c3RlbV9jYXRlZ29yeSIsCgkJCQkJInhjX21fbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX2F1ZGl0IgogICAgICAgICAgICAgICAgXQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfSwKICAgICJ1c2VyX25hbWUiOiAieGMtdXNlci1maXJzdCIsCiAgICAic2NvcGUiOiBbCiAgICAgICAgInJlYWQiCiAgICBdLAogICAgIm1vYmlsZSI6ICIxNTAxMjM0NTY3OCIsCiAgICAiZXhwIjogMTYwNjUyNTEyMiwKICAgICJjbGllbnRfYXV0aG9yaXRpZXMiOiBbCiAgICAgICAgIlJPTEVfVVNFUiIKICAgIF0sCiAgICAianRpIjogIjFlYjdlOTg3LWQ3YzItNDBmNS1iMGQ2LWNkNjEzOWNiMThlMCIsCiAgICAiY2xpZW50X2lkIjogInhjLWNvbS1wbGF0Zm9ybSIsCiAgICAiY29tcGFueUlkIjogMTIzMjE0MTQyNQp9
# Cookie域
VUE_APP_SERVER_DOMAIN=xuecheng-plus.com
# 七牛云静态页
VUE_APP_SERVER_QINIU_URL=http://localhost11
# 图片服务器地址
VUE_APP_SERVER_PICSERVER_URL=http://192.168.101.65:9000
  • 启动前端工程,进入课程管理,可以看到界面显示的课程信息,那么到此就基本完成了前后端联调
    学成在线--内容管理模块_第20张图片

课程分类查询

需求分析

  • 下面我们进行添加课程的接口开发,在新增课程界面,有三处信息需要选择,课程分类、课程等级、课程类型
    学成在线--内容管理模块_第21张图片
  • 其中,课程等级和课程类型都来源于数字字典表,此部分的信息前端已从系统管理服务中读取。但是课程类型的数据是通过另外一个接口来读取的,现在还没有编写
请求网址: http://localhost:8601/api/content/course-category/tree-nodes
请求方法: GET
状态代码: 404 Not Found
远程地址: 127.0.0.1:8601
  • 课程分类信息没有在数据字典表中存储,而是有单独一张课程分类表,下面我们来看看课程分类表的结构
    学成在线--内容管理模块_第22张图片
  • 这张表是一个树形结构,通过父节点id将各元素组成一个树,下面是一部分数据
    学成在线--内容管理模块_第23张图片
  • 那么现在的需求就是:在内容管理服务中编写一个接口,读取课程分类表的数据,组成一个树形结构返回给前端

接口定义

  • 通过查看前端的请求记录,可以得出该接口的协议为:HTTP GET,请求参数为空
请求网址: http://localhost:8601/api/content/course-category/tree-nodes
请求方法: GET
状态代码: 404 Not Found
远程地址: 127.0.0.1:8601
  • 此接口要返回全部课程分类,以树状结构返回,下面是示例
[
    {
    "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"
        },
        {
            "childrenTreeNodes" : null,
            "id" : "1-1-3",
            "isLeaf" : null,
            "isShow" : null,
            "label" : "jQuery",
            "name" : "jQuery",
            "orderby" : 3,
            "parentid" : "1-1"
        }
    ],
    "id" : "1-1",
    "isLeaf" : null,
    "isShow" : null,
    "label" : "前端开发",
    "name" : "前端开发",
    "orderby" : 1,
    "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" : "iOS",
            "name" : "iOS",
            "orderby" : 2,
            "parentid" : "1-2"
        },
        {
            "childrenTreeNodes" : null,
            "id" : "1-2-3",
            "isLeaf" : null,
            "isShow" : null,
            "label" : "其它",
            "name" : "其它",
            "orderby" : 8,
            "parentid" : "1-2"
        }
    ],
    "id" : "1-2",
    "isLeaf" : null,
    "isShow" : null,
    "label" : "移动开发",
    "name" : "移动开发",
    "orderby" : 2,
    "parentid" : "1"
    }
]
  • 可以看到,上面的数据格式是一个数组结构,数组的元素即为分类信息,分类信息设计两级分类
    • 第一级的分类信息示例如下,这部分字段其实就是课程分类信息表的属性,即我们之前生成的CourseCategory类
        "id" : "1-2",
        "isLeaf" : null,
        "isShow" : null,
        "label" : "移动开发",
        "name" : "移动开发",
        "orderby" : 2,
        "parentid" : "1"
    
    • 第二级的分类是第一级分类中的childrenTreeNode属性,它是一个数组结构
        "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" : "iOS",
                "name" : "iOS",
                "orderby" : 2,
                "parentid" : "1-2"
            },
            {
                "childrenTreeNodes" : null,
                "id" : "1-2-3",
                "isLeaf" : null,
                "isShow" : null,
                "label" : "其它",
                "name" : "其它",
                "orderby" : 8,
                "parentid" : "1-2"
            }
        ]
    
  • 那么我们可以定义一个DTO类表示分类信息的模型类,让其继承CourseCategory(对应一级分类),然后设置一个List属性(对应二级分类)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CourseCategoryTreeDto extends CourseCategory {
    List childrenTreeNodes;
}
  • 这样模型类就定义好了,下面我们来定义接口
@Slf4j
@RestController("/course-category/tree-nodes")
@Api(value = "课程分类相关接口", tags = "课程分类相关接口")
public class CourseCategoryController {
    @ApiOperation("课程分类相关接口")
    public List queryTreeNodes() {
        return null;
    }
}

接口开发

  • 如何生成一个树形结构对象?这里有两种方案
    {% tabs 两种树形结构对象生成方案 %}
  • 可以将数据从数据库中读取出来,在Java程序中遍历数据组成一个树形结构对象
  • 那这里先简单介绍一下如何编写SQL语句,注意到表中设置了每条数据的id和parentid,我们可以据此来创建内连接查询
    学成在线--内容管理模块_第24张图片
  • 连接条件为two.parentid = one.id(二级分类的父节点为一级分类),同时one.parentid = 1(一级分类的父节点为根节点),这样就可以查询出所有的数据
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;
  • 它会把查询出来的结果再次代入到查询子句中继续查询,这里的SELECT 1 AS n,就是给一个递归的初始值,WHERE n < 5 则是终止条件
  • 那么我们现在来使用递归查询全部数据
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;

学成在线--内容管理模块_第25张图片

  • 成功查询到数据了之后,我们现在就需要用Java代码将其组装成树形结构,在此之前,我们先来编写mapper(采用更灵活的递归查询)
    • 现在service中编写mapper对应的接口
    public interface CourseCategoryMapper extends BaseMapper {
        List selectTreeNodes();
    }
    
    • 编写xml
    
    
  • mapper写好了之后,我们来编写Service,首先定义接口
public interface CourseCategoryService {
    /**
     * 课程分类查询
     * @param id 根节点id
     * @return 根节点下面的所有子节点
     */
    List queryTreeNodes(String id);
}

  • 具体实现,我们的mapper接口只返回了所有子节点的数据,那么现在我们要将这些子节点封装成一个树形结构
@Slf4j
@Service
public class CourseCategoryServiceImpl implements CourseCategoryService {
    @Autowired
    private CourseCategoryMapper courseCategoryMapper;

    @Override
    public List queryTreeNodes(String id) {
        // 获取所有的子节点
        List categoryTreeDtos = courseCategoryMapper.selectTreeNodes(id);
        // 定义一个List,作为最终返回的数据
        List result = new ArrayList<>();
        // 为了方便找子节点的父节点,这里定义一个HashMap,key是节点的id,value是节点本身
        HashMap 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());
                }
                // 将子节点设置给父节点
                parentNode.getChildrenTreeNodes().add(item);
            }
        });
        // 返回根节点的直接下属节点(1-1、1-2 ···)
        return result;
    }
}
  • 编写测试方法,进行调试
    @Resource
    CourseCategoryService courseCategoryService;
    
    @Test
    void contextCourseCategoryTest() {
        List courseCategoryTreeDtos = courseCategoryService.queryTreeNodes("1");
        System.out.println(courseCategoryTreeDtos);
    }

学成在线--内容管理模块_第26张图片

  • 使用HTTP Client测试接口
    学成在线--内容管理模块_第27张图片
  • 编写Controller方法
@Slf4j
@RestController
@Api(value = "课程分类相关接口", tags = "课程分类相关接口")
public class CourseCategoryController {
    @Resource
    private CourseCategoryService courseCategoryService;
    @ApiOperation("课程分类相关接口")
    @GetMapping("/course-category/tree-nodes")
    public List queryTreeNodes() {
        return courseCategoryService.queryTreeNodes("1");
    }
}
  • 重启服务,已经可以看到课程分类的信息了
    学成在线--内容管理模块_第28张图片
  • 通过表的自连接查出数据,使用mybatis映射成一个树形结构
  • 定义mapper接口方法,编写SQL
public interface CourseCategoryMapper extends BaseMapper {
    List selectTreeNodes();
}
  • 编写对应的XML

  • 编写resultMap映射


    
    
    
    
    
    
    
    
        
        
        
        
        
        
    

  • service接口和对应的实现类,测试方法,均与方案一一致,这里就不予赘述了

{% endtabs %}

新增课程

需求分析

业务流程

  • 根据前面对内容管理模块的数据模型分析,课程相关的信息有:课程基本信息、课程营销信息、课程图片信息、课程计划、课程师资信息,所以新增一门课程就需要完成这几部分的信息的编写
  • 以下是业务流程
    1. 进入课程查询列表
    2. 点击添加课程,选择课程类型是直播还是录播,课程类型不同,课程的授课方式也不同
    3. 选择完毕,点击下一步,进入课程基本信息添加界面
      • 本界面分为两部分信息,一部分是课程基本信息,一部分是课程营销信息
    4. 添加课程计划信息
      • 课程计划即课程的大纲目录
      • 课程计划分为两级,章节和小节
      • 每个小节需要上传课程视频,用户点击小节标题即可开始播放视频
      • 如果是直播课程,则会进入直播间
    5. 课程计划填写完毕,进入课程师资管理
      • 在课程师资界面维护该课程的授课老师
      • 到此,一门课程新增完成

数据模型

  • 新增课程功能,只向课程基本信息表、课程营销信息表添加记录
    • 课程基本信息表
      学成在线--内容管理模块_第29张图片
    • 课程营销信息表
      学成在线--内容管理模块_第30张图片
  • 新建课程的初始审核状态为未提交,初始发布状态为未发布

接口定义

  • 根据业务流程,这里先定义提交课程基本信息的接口
请求网址: http://localhost:8601/api/content/course
请求方法: POST
状态代码: 404 Not Found
远程地址: 127.0.0.1:8601
  • 接口协议:HTTP POST
  • 接口请求示例如下
### 创建课程
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
}
  • 请求参数和CourseBase模型类不一致,所以我们需要定义一个模型类来接收请求参数;同时,响应结果中包含了课程基本信息和课程营销信息还有课程分类信息,所以我们也需要定义一个响应结果模型类
    {% tabs 定义新增课程接收参数模型类和响应结果模型类 %}
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;

/**
 * @version 1.0
 * @description 添加课程dto
 */
@Data
@ApiModel(value = "AddCourseDto", description = "新增课程基本信息")
public class AddCourseDto {

    @ApiModelProperty(value = "课程名称", required = true)
    private String name;

    @ApiModelProperty(value = "适用人群", required = true)
    private String users;

    @ApiModelProperty(value = "课程标签")
    private String tags;

    @ApiModelProperty(value = "大分类", required = true)
    private String mt;

    @ApiModelProperty(value = "小分类", required = true)
    private String st;

    @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;

    @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;
}
package com.xuecheng.content.model.dto;

import com.xuecheng.content.model.po.CourseBase;
import lombok.Data;

/**
 * @version 1.0
 * @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;

}

{% endtabs %}

  • 定义接口如下,返回类型和请求参数类型均为我们刚刚创建的模型类
    @ApiOperation("新增课程基础信息接口")
    @PostMapping("/course")
    public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto addCourseDto) {
        return null;
    }

接口开发

  1. 定义service接口,这里额外需要一个机构id,因为我们的业务是教学机构登录账号,然后添加该教学机构的下属课程
    /**
     * 新增课程基本信息
     * @param companyId 教学机构id
     * @param addCourseDto 课程基本信息
     * @return
     */
    CourseBaseInfoDto createCourseBase(Long companyId, AddCourseDto addCourseDto);
  1. 编写service接口实现类
    • 首先我们需要对请求参数做合法性校验,判断一下用户是否输入了必填项,还有一些项的默认值
    • 然后对请求参数进行封装,调用mapper进行数据持久化
    • 组装返回结果
        @Resource
        CourseBaseMapper courseBaseMapper;
        
        @Resource
        CourseMarketMapper courseMarketMapper;
        
        @Resource
        CourseCategoryMapper courseCategoryMapper;
    
        @Override
        @Transactional
        public CourseBaseInfoDto createCourseBase(Long companyId, AddCourseDto addCourseDto) {
            // 1. 合法性校验
            if (StringUtils.isBlank(addCourseDto.getName())) {
                throw new RuntimeException("课程名称为空");
            }
            if (StringUtils.isBlank(addCourseDto.getMt())) {
                throw new RuntimeException("课程分类为空");
            }
            if (StringUtils.isBlank(addCourseDto.getSt())) {
                throw new RuntimeException("课程分类为空");
            }
            if (StringUtils.isBlank(addCourseDto.getGrade())) {
                throw new RuntimeException("课程等级为空");
            }
            if (StringUtils.isBlank(addCourseDto.getTeachmode())) {
                throw new RuntimeException("教育模式为空");
            }
            if (StringUtils.isBlank(addCourseDto.getUsers())) {
                throw new RuntimeException("适应人群为空");
            }
            if (StringUtils.isBlank(addCourseDto.getCharge())) {
                throw new RuntimeException("收费规则为空");
            }
            // 2. 封装请求参数
            // 封装课程基本信息
            CourseBase courseBase = new CourseBase();
            BeanUtils.copyProperties(addCourseDto, courseBase);
            // 2.1 设置默认审核状态(去数据字典表中查询状态码)
            courseBase.setAuditStatus("202002");
            // 2.2 设置默认发布状态
            courseBase.setStatus("203001");
            // 2.3 设置机构id
            courseBase.setCompanyId(companyId);
            // 2.4 设置添加时间
            courseBase.setCreateDate(LocalDateTime.now());
            // 2.5 插入课程基本信息表
            int baseInsert = courseBaseMapper.insert(courseBase);
            Long courseId = courseBase.getId();
            // 封装课程营销信息
            CourseMarket courseMarket = new CourseMarket();
            BeanUtils.copyProperties(addCourseDto, courseMarket);
            courseMarket.setId(courseId);
            // 2.6 判断收费规则,若课程收费,则价格必须大于0
            String charge = courseMarket.getCharge();
            if ("201001".equals(charge)) {
                Float price = addCourseDto.getPrice();
                if (price == null || price.floatValue() <= 0) {
                    throw new RuntimeException("课程设置了收费,价格不能为空,且必须大于0");
                }
            }
            // 2.7 插入课程营销信息表
            int marketInsert = courseMarketMapper.insert(courseMarket);
            if (baseInsert <= 0 || marketInsert <= 0) {
                throw new RuntimeException("新增课程基本信息失败");
            }
            // 3. 返回添加的课程信息
            return getCourseBaseInfo(courseId);
        }
    
        private CourseBaseInfoDto getCourseBaseInfo(Long courseId) {
            CourseBaseInfoDto courseBaseInfoDto = new CourseBaseInfoDto();
            // 1. 根据课程id查询课程基本信息
            CourseBase courseBase = courseBaseMapper.selectById(courseId);
            if (courseBase == null)
                return null;
            // 1.1 拷贝属性
            BeanUtils.copyProperties(courseBase, courseBaseInfoDto);
            // 2. 根据课程id查询课程营销信息
            CourseMarket courseMarket = courseMarketMapper.selectById(courseId);
            // 2.1 拷贝属性
            if (courseMarket != null)
                BeanUtils.copyProperties(courseMarket, courseBaseInfoDto);
            // 3. 查询课程分类名称,并设置属性
            // 3.1 根据小分类id查询课程分类对象
            CourseCategory courseCategoryBySt = courseCategoryMapper.selectById(courseBase.getSt());
            // 3.2 设置课程的小分类名称
            courseBaseInfoDto.setStName(courseCategoryBySt.getName());
            // 3.3 根据大分类id查询课程分类对象
            CourseCategory courseCategoryByMt = courseCategoryMapper.selectById(courseBase.getMt());
            // 3.4 设置课程大分类名称
            courseBaseInfoDto.setMtName(courseCategoryByMt.getName());
            return courseBaseInfoDto;
        }
    
  • 下面编写Controller方法
@ApiOperation("新增课程基础信息接口")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto addCourseDto) {
    // 机构id,暂时硬编码模拟假数据
    Long companyId = 22L;
    return courseBaseInfoService.createCourseBase(companyId, addCourseDto);
}

完整版请移步我的个人博客

学成在线–内容管理模块

你可能感兴趣的:(软件工程,需求分析)