模块介绍

1.课程列表

  1. 添加课程

  1. 选择录播, 填写课程信息

  1. 填写课程计划信息

  1. 填写师资信息

  1. 课程信息填写完毕, 先进行提交审核, 审核通过后可以发布课程

  1. 内容管理模块的基础表涉及9张,如下:

使用 PowerDesigner打开课程资料下的"数据库\模型\学成在线项目.sws”

创建模块

模块关系

内容管理模块独立为一个模块, 模块结构如图

  1. xuecheng-plus-content-api:接口工程,为前端提供接口。
  2. xuecheng-plus-content-service: 业务工程,为接口工程提供业务支撑。
  3. xuecheng-plus-content-model: 数据模型工程,存储数据模型类、数据传输类型等。
  4. xuecheng-plus-content:内容管理模块工程,负责聚合xuecheng-plus-content-api、xuecheng-plus-content-service、xuecheng-plus-content-model。

内容管理模块和基础模块以及父工程的关系

创建模块工程

首先在项目根目录创建内容管理模块的父工程xuecheng-plus-content

  1. 该工程只负责模块聚合, 只保留pom.xml文件,删除多余的文件。
  2. 内容管理父工程的主要职责是聚合内容管理接口和内容管理接口实现两个工程
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>xuecheng-plus-parent</artifactId>
        <groupId>com.xuecheng</groupId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>../xuecheng-plus-parent</relativePath>
    </parent>
    <artifactId>xuecheng-plus-content</artifactId>
    <name>xuecheng-plus-content</name>
    <description>xuecheng-plus-content</description>
    <packaging>pom</packaging>

    <modules>
        <module>xuecheng-plus-content-api</module>
        <module>xuecheng-plus-content-model</module>
        <module>xuecheng-plus-content-service</module>
    </modules>
</project>

在xuecheng-plus-content下创建xuecheng-plus-content-model数据模型工程。

  1. 创建完成,只保留包和pom.xml文件,删除多余的文件。

  1. 修改pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>xuecheng-plus-content</artifactId>
        <groupId>com.xuecheng</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>xuecheng-plus-content-model</artifactId>


    <dependencies>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xuecheng-plus-base</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

在xuecheng-plus-content下创建xuecheng-plus-content-service接口实现工程。

  1. 创建完成,只保留包和pom.xml文件,删除多余的文件
  2. 修改pom.xml如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>xuecheng-plus-content</artifactId>
        <groupId>com.xuecheng</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>xuecheng-plus-content-service</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xuecheng-plus-content-model</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

在xuecheng-plus-content下创建xuecheng-plus-content-api接口工程。

  1. 创建完成,只保留包和pom.xml文件,删除多余的文件
  2. 编辑pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>xuecheng-plus-content</artifactId>
        <groupId>com.xuecheng</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>xuecheng-plus-content-api</artifactId>


    <dependencies>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xuecheng-plus-content-service</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

课程列表查询

业务介绍

教学机构人员点击课程管理首先进入课程查询界面

在课程进行列表查询页面输入查询条件查询课程信息

  1. 当不输入查询条件时输入全部课程信息。
  2. 输入查询条件查询符合条件的课程信息。
  3. 约束:本教学机构查询本机构的课程信息。

数据模型

下边从查询条件、查询列表两个方面分析数据模型

1、查询条件:

  • 包括:课程名称、课程审核状态、课程发布状态
  • 课程名称:可以模糊搜索
  • 课程审核状态:未提交、已提交、审核通过、审核未通过
  • 课程发布状态:未发布、已发布、已下线
  • 因为是分页查询, 所以查询条件中还要包括当前页码、每页显示记录数。

2、查询结果:

查询结果中包括:课程id、课程名称、任务数、创建时间、是否付费、审核状态、类型,操作

  • 任务数:该课程所包含的课程计划数,即课程章节数。
  • 是否付费:课程包括免费、收费两种。
  • 类型:录播、直播。
  • 因为是分页查询所以查询结果中还要包括总记录数、当前页、每页显示记录数。

准备工作

了解课程基本信息表

生成PO类

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

  1. 在开发持久层代码时需要根据数据表编写PO类,在实际开发中通常使用代码生成器(工具)生成PO类的代码。
  2. 本项目使用mybatis-plus的generator工程生成PO类、Mapper接口、Mapper的xml文件,下载地址在:GitHub - baomidou/generator: Any Code generator, 也可以使用资料中下载好的
  3. 将资料中的 xuecheng-plus-generator.zip 解压后拷贝至项目工程根目录

  1. 手动添加模块到工程中, 不会被maven识别, 打开pom文件, 右键选择"Add as Maven Project", 将模块添加为maven工程

  1. 要生成内容管理模块的PO类、Mapper接口和Mapper的xml文件,找到ContentCodeGenerator类

  1. 修改ContentCodeGenerator类中的信息,包括:数据库地址、数据库账号、数据库密码、生成的表、生成路径

  1. 修改完成,执行该类的main方法,自动生成content包

  1. 在该包下自动生成了内容管理模块的controller、mapper、po及service相关代码

  1. 将po类拷贝到model工程

  1. PO类编译报错,这是缺少依赖包导致,本项目使用的持久层框架是MyBatisPlus,在生成的po类中使用了MyBatisPlus框架的注解,这里需要添加MyBatisPlus框架的依赖,消除错误。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    ... ...
    <artifactId>xuecheng-plus-content-model</artifactId>

    <dependencies>
        ... ... 

        <!--存在mybatisplus注解添加相关注解保证不报错-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-annotation</artifactId>
            <version>${mybatis-plus-boot-starter.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-core</artifactId>
            <version>${mybatis-plus-boot-starter.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

</project>

接口设计

设计一个接口需要包括以下几个方面:

1)协议

  • 通常协议采用HTTP,查询类接口通常为get或post,查询条件较少的使用get,较多的使用post。
  • 本接口使用 http post。
  • 还要确定content-type,参数以什么数据格式提交,结果以什么数据格式响应。
  • 一般情况没有特殊情况结果以json 格式响应。

2)分析请求参数

  • 根据前边对数据模型的分析,请求参数为:课程名称、课程审核状态、当前页码、每页显示记录数。

3)分析响应结果

  • 根据前边对数据模型的分析,响应结果为数据列表加一些分页信息
  • 数据列表中数据的属性包括:课程id、课程名称、任务数、创建时间、审核状态、类型。
  • 查询结果中的审核状态为数据字典中的代码字段,前端会根据审核状态代码找到对应的名称显示。

4)接口请求示例

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

{
  "auditStatus": "202002",
  "courseName": "",
  "publishStatus":""
}

###成功响应结果
{
  "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
}

分页查询模型类: 用于封装分页查询的参数

/**
 * @description 分页查询通用参数
 * @author Mr.M
 * @date 2022/9/6 14:02
 * @version 1.0
 */
@Data
@ToString
public class PageParams {

    //当前页码
    private Long pageNo = 1L;

    //每页记录数默认值
    private Long pageSize =10L;

    public PageParams(){

    }

    public PageParams(long pageNo,long pageSize){
        this.pageNo = pageNo;
        this.pageSize = pageSize;
    }

}

查询条件模型类: 用于封装课程查询时的筛选参数

/**
 * @description 课程查询参数Dto
 * @author Mr.M
 * @date 2022/9/6 14:36
 * @version 1.0
 */
@Data
@ToString
public class QueryCourseParamsDto {

    //审核状态
    private String auditStatus;
    //课程名称
    private String courseName;
    //发布状态
    private String publishStatus;

}

响应模型类: 用于封装查询结果

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
     ... ...

    <dependencies>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xuecheng-plus-content-service</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <!--cloud的基础环境包-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-context</artifactId>
        </dependency>
        <!-- Spring Boot 的 Spring Web MVC 集成 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 排除 Spring Boot 依赖的日志包冲突 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <!-- Spring Boot 集成 log4j2 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>

        <!-- Spring Boot 集成 swagger -->
        <dependency>
            <groupId>com.spring4all</groupId>
            <artifactId>swagger-spring-boot-starter</artifactId>
            <version>1.9.0.RELEASE</version>
        </dependency>

    </dependencies>

</project>

定义接口

引入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>xuecheng-plus-content</artifactId>
        <groupId>com.xuecheng</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>xuecheng-plus-content-api</artifactId>


    <dependencies>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xuecheng-plus-content-service</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <!--cloud的基础环境包-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-context</artifactId>
        </dependency>
        <!-- Spring Boot 的 Spring Web MVC 集成 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 排除 Spring Boot 依赖的日志包冲突 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <!-- Spring Boot 集成 log4j2 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>

        <!-- Spring Boot 集成 swagger -->
        <dependency>
            <groupId>com.spring4all</groupId>
            <artifactId>swagger-spring-boot-starter</artifactId>
            <version>1.9.0.RELEASE</version>
        </dependency>

    </dependencies>

</project>

定义controller方法

/**
 * 课程信息管理接口
 */
@RestController
public class CourseBaseInfoController {

    // 课程查询接口
    @RequestMapping("/course/list")
    public PageResult<CourseBase> list(PageParams pageParams, @RequestBody QueryCourseParamsDto queryCourseParamsDto) {
        return null;
    }
}
  1. pageParams分页参数通过url的key/value传入,
  2. queryCourseParams通过json数据传入,使用@RequestBody注解将json转成Dto对象。

定义启动类

/**
 * 内容管理服务启动类
 */
@SpringBootApplication
public class ContentApplication {
    public static void main(String[] args) {
        
        SpringApplication.run(ContentApplication.class, args);
    }
}

添加SpringBoot框架的配置文件

server:
  servlet:
    context-path: /content
  port: 63040
#微服务配置
spring:
  application:
    name: content-api
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.101.65:3306/xcplus_content?serverTimezone=UTC&userUnicode=true&useSSL=false&
    username: root
    password: mysql
# 日志文件配置路径
logging:
  config: classpath:log4j2-dev.xml

添加日志框架的配置文件: 从课程资料中复制到工程中

启动服务,测试接口是否可以正常请求

  1. 在controller方法中打断点,debug启动微服务,
  2. 在浏览器访问http://localhost:63040/content/course/list?pageNo=1&pageSize=10
  3. 浏览器报400错误,400错误通常由于你访问的页面域名不存在或者请求错误。一般是因为我们输入的语法格式有错误,服务器无法理解用户的请求,我们需要认真检查下请求参数是否有误,不然再怎么刷新都没有用
  4. 口接收两部分参数,一部分是分页参数,它是通过http url传递key/value串,另一部分是业务查询条件,通过http body传入json内容。服务端使用RequestBody接收json内容,我们在测试并没有传递json内容这里导致错误。
  5. 下边在@RequestBody后添加(required=false)表示此参数不是必填项,如下

  1. 重新启动服务, 进行测试, 请求可以到达断点, 说明我们的接口定义没有问题

模型类的作用

现在项目中有两类模型类:DTO数据传输对象、PO持久化对象,

  1. DTO用于接口层向业务层之间传输数据,
  2. PO用于业务层与持久层之间传输数据,

有些项目还会设置VO对象(视图数据对象),VO对象用在前端与接口层之间传输数据

什么时候使用VO

当前端有多个平台且接口存在差异时, 就需要设置VO对象用于前端和接口层传输数据。

比如:

  1. 课程列表查询接口,根据需求用户在手机端也要查询课程信息,此时课程查询接口是否需要编写手机端和PC端两个接口呢?如果用户要求通过手机和PC的查询条件或查询结果不一样,此时就需要定义两个Controller课程查询接口,每个接口定义VO对象与前端传输数据。
  2. 手机查询:根据课程状态查询,查询结果只有课程名称和课程状态。
  3. PC查询:可以根据课程名称、课程状态、课程审核状态等条件查询,查询结果也比手机查询结果内容多。
  4. 此时,Service业务层尽量提供一个业务接口,即使两个前端接口需要的数据不一样,Service可以提供一个最全查询结果,由Controller进行数据整合。
  5. 如下图:

  1. 如果前端的接口没有多样性且比较固定,此时可以取消VO,只用DTO即可。

集成Swagger

什么是Swagger?

OpenAPI规范(OpenAPI Specification 简称OAS)是Linux基金会的一个项目,试图通过定义一种用来描述API格式或API定义的语言,来规范RESTful服务开发过程,目前版本是V3.0,并且已经发布并开源在github上。(GitHub - OAI/OpenAPI-Specification: The OpenAPI Specification Repository

Swagger是全球最大的OpenAPI规范(OAS)API开发工具框架,Swagger是一个在线接口文档的生成工具,前后端开发人员依据接口文档进行开发。 (API Documentation & Design Tools for Teams | Swagger)

集成Swagger

Spring Boot 可以集成Swagger,Swaager根据Controller类中的注解生成接口文档,只要添加Swagger的依赖和配置信息即可使用它。

1、在API工程添加swagger-spring-boot-starter依赖

<!-- Spring Boot 集成 swagger -->
<dependency>
    <groupId>com.spring4all</groupId>
    <artifactId>swagger-spring-boot-starter</artifactId>
</dependency>

2、在 bootstrap.yml中配置swagger的扫描包路径及其它信息,base-package为扫描的包路径,扫描Controller类。

# swagger配置
swagger:
  title: "学成在线内容管理系统"
  description: "内容系统管理系统对课程相关信息进行管理"
  base-package: com.xuecheng.content
  enabled: true
  version: 1.0.0

3、在启动类中添加@EnableSwagger2Doc注解

4、再次启动服务,工程启动起来,访问http://localhost:63040/content/swagger-ui.html

5、优化接口文档

这个文档存在两个问题:

  • 接口名称显示course-base-info-controller名称不直观
  • 课程查询是post方式只显示post /course/list即可。

下边进行修改,添加一些接口说明的注解,并且将RequestMapping改为PostMapping,如下:

/**
 * 课程信息管理接口
 */
@RestController
@Api(value = "课程信息管理接口",tags = "课程信息管理接口")
public class CourseBaseInfoController {

    // 课程查询接口
    @ApiOperation("课程查询接口")
    @PostMapping("/course/list")
    public PageResult<CourseBase> list(PageParams pageParams, @RequestBody(required = false) QueryCourseParamsDto queryCourseParamsDto) {
        return null;
    }
}

6、接口文档中会有关于接口参数的说明,在模型类上也可以添加注解对模型类中的属性进行说明,方便对接口文档的阅读。

/**
 * @description 课程查询参数Dto
 * @author Mr.M
 * @date 2022/9/6 14:36
 * @version 1.0
 */
@Data
@ToString
public class QueryCourseParamsDto {

    //审核状态
    @ApiModelProperty("审核状态")
    private String auditStatus;
    //课程名称
    @ApiModelProperty("课程名称")
    private String courseName;
    //发布状态
    @ApiModelProperty("发布状态")
    private String publishStatus;

}
/**
 * @description 分页查询通用参数
 * @author Mr.M
 * @date 2022/9/6 14:02
 * @version 1.0
 */
@Data
@ToString
public class PageParams {

    //当前页码
    @ApiModelProperty("当前页码")
    private Long pageNo = 1L;

    //每页记录数默认值
    @ApiModelProperty("每页记录数默认值")
    private Long pageSize =10L;

    public PageParams(){

    }

    public PageParams(long pageNo,long pageSize){
        this.pageNo = pageNo;
        this.pageSize = pageSize;
    }

}

Swaager的常用注解如下:

 @Api:修饰整个类,描述Controller的作用
 @ApiOperation:描述一个类的一个方法,或者说一个接口
 @ApiParam:单个参数描述
 @ApiModel:用对象来接收参数
 @ApiModelProperty:用对象接收参数时,描述对象的一个字段
 @ApiResponse:HTTP响应其中1个描述
 @ApiResponses:HTTP响应整体描述
 @ApiIgnore:使用该注解忽略这个API
 @ApiError :发生错误返回的信息
 @ApiImplicitParam:一个请求参数
 @ApiImplicitParams:多个请求参数

@ApiImplicitParam属性如下

7、使用Swagger可以进行接口的测试

/**
 * 课程信息管理接口
 */
@RestController
@Api(value = "课程信息管理接口",tags = "课程信息管理接口")
public class CourseBaseInfoController {

    // 课程查询接口
    @ApiOperation("课程查询接口")
    @PostMapping("/course/list")
    public PageResult<CourseBase> list(PageParams pageParams, @RequestBody(required = false) QueryCourseParamsDto queryCourseParamsDto) {
        // 模拟查询结果
        CourseBase courseBase = new CourseBase();
        courseBase.setName("测试名称");
        courseBase.setCreateDate(LocalDateTime.now());
        List<CourseBase> courseBases = new ArrayList();
        courseBases.add(courseBase);
        PageResult pageResult = new PageResult<CourseBase>(courseBases,10,1,10);
        return pageResult;

    }
}

8、不过存在一个问题就是LocalDateTime类型的数据转json后数据格式并不是我们要的年月日时分秒格式,

在base工程com.xuecheng.base.config包下加配置LocalDateTimeConfig 类实现转json时字符串与LocalDateTime类型的转换,LocalDateTimeConfig 类可从课程资料下的项目工程目录中直接拷贝。

@Configuration
public class LocalDateTimeConfig {

    /*
     * 序列化内容
     *   LocalDateTime -> String
     * 服务端返回给客户端内容
     * */
    @Bean
    public LocalDateTimeSerializer localDateTimeSerializer() {
        return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }

    /*
     * 反序列化内容
     *   String -> LocalDateTime
     * 客户端传入服务端数据
     * */
    @Bean
    public LocalDateTimeDeserializer localDateTimeDeserializer() {
        return new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }


    // 配置
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
        return builder -> {
            builder.serializerByType(LocalDateTime.class, localDateTimeSerializer());
            builder.deserializerByType(LocalDateTime.class, localDateTimeDeserializer());
        };
    }

}

持久层开发

本项目使用MyBatis-Plus开发持久层,需要创建PO类、Mapper接口、Mapper的xml文件,每个PO类对应数据库的每张表,每张表需要创建一个Mapper接口和Mapper的xml映射文件。

本项目使用持久层框架MyBatis-Plus进行开发,下边将mapper接口和xml文件拷贝到 service工程,拷贝后如下图所示:

测试mapper

下边对mapper进行单元测试,测试course_base表的查询接口。

  1. 下边在service工程的pom.xml中添加依赖
<!-- MySQL 驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- mybatis plus的依赖 -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
    </dependency>
   <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-context</artifactId>
  </dependency>
    <!-- Spring Boot 集成 Junit -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!-- 排除 Spring Boot 依赖的日志包冲突 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <!-- Spring Boot 集成 log4j2 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>
  1. 配置扫描mapper及分页插件

/**
 * Mybatis-Plus 配置
 */
@Configuration
@MapperScan("com.xuecheng.content.mapper")
public class MybatisPlusConfig {
    /**
     * 定义分页拦截器
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

}

分页插件的原理:

首先分页参数放到ThreadLocal中,拦截执行的sql,根据数据库类型添加对应的分页语句重写sql,例如:(select * from table where a) 转换为 (select count(*) from table where a)和(select * from table where a limit ,)

计算出了total总条数、pageNum当前第几页、pageSize每页大小和当前页的数据,是否为首页,是否为尾页,总页数等。

  1. 单元测试所需要的配置文件

在test/resources下创建 log4j2-dev.xml、bootstrap.yml:

log4j2-dev.xml: 从课程资料中获取

#微服务配置
spring:
  application:
    name: content-api
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.101.65:3306/xcplus_content?serverTimezone=UTC&userUnicode=true&useSSL=false&
    username: root
    password: mysql
# 日志文件配置路径
logging:
  config: classpath:log4j2-dev.xml
  1. 编写启动类

@SpringBootApplication
public class ContentApplication {
    public static void main(String[] args) {
        SpringApplication.run(ContentApplication.class, args);
    }
}
  1. 编写测试类
@SpringBootTest
class CourseBaseMapperTests {

    @Autowired
    CourseBaseMapper courseBaseMapper;


    @Test
    void testCourseBaseMapper() {
        // 根据id查询
        CourseBase courseBase = courseBaseMapper.selectById(18L);
        Assertions.assertNotNull(courseBase); // 结果断言

        // 测试分页条件查询
        // 1.1构建查询条件
        QueryCourseParamsDto courseParamsDto = new QueryCourseParamsDto();
        courseParamsDto.setCourseName("java"); // 课程名称
        // 1.2拼接查询条件
        LambdaQueryWrapper<CourseBase> queryWrapper = new LambdaQueryWrapper<>();
        // 根据名称模糊查询
        queryWrapper.like(StringUtils.isNotEmpty(courseParamsDto.getCourseName()), CourseBase::getName, courseParamsDto.getCourseName());
        // 根据课程审核状态查询
        queryWrapper.eq(StringUtils.isNotEmpty(courseParamsDto.getAuditStatus()), CourseBase::getAuditStatus, courseParamsDto.getAuditStatus());

        // 2.1创建分页参数对象
        PageParams pageParams = new PageParams();
        pageParams.setPageNo(1L);
        pageParams.setPageSize(5L);
        // 2.2创建page分页参数对象
        Page<CourseBase> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
        // 2.3开始分页查询
        Page<CourseBase> pageResult = courseBaseMapper.selectPage(page, queryWrapper);
        // 2.4得到分页数据
        List<CourseBase> items = pageResult.getRecords();
        long total = pageResult.getTotal();
        // 2.5返回分页数据
        PageResult<CourseBase> courseBasePageResult = new PageResult<>(items, total, pageParams.getPageNo(), pageParams.getPageSize());
        System.out.println(courseBasePageResult + "条件分页查询结果");

    }


}
  1. 运行测试类的测试方法进行测试,测试成功:

数据字典表

为了提高系统的可扩展性, 将系统中一些固定的参数定义到数据字典表去维护。

业务层开发

接下来开发Service方法,首先创建Service接口:

/**
 * 课程基本信息管理业务接口
 */
public interface CourseBaseInfoService {

    /**
     * 课程分页查询接口
     * @param pageParams 分页参数
     * @param queryCourseParamsDto 查询条件
     * @return 返回查询结果
     */
    public PageResult<CourseBase> queryCourseBaseList(PageParams pageParams, QueryCourseParamsDto queryCourseParamsDto);
}

再创建接口实现类

@Slf4j
@Service
public class CourseBaseInfoServiceImpl implements CourseBaseInfoService {

    @Autowired
    private CourseBaseMapper courseBaseMapper;

    // 课程分页查询
    @Override
    public PageResult<CourseBase> queryCourseBaseList(PageParams pageParams, QueryCourseParamsDto courseParamsDto) {
        // 测试分页条件查询
        // 1.2拼接查询条件
        LambdaQueryWrapper<CourseBase> queryWrapper = new LambdaQueryWrapper<>();
        // 根据课程名称模糊查询
        queryWrapper.like(StringUtils.isNotEmpty(courseParamsDto.getCourseName()), CourseBase::getName, courseParamsDto.getCourseName());
        // 根据课程审核状态查询
        queryWrapper.eq(StringUtils.isNotEmpty(courseParamsDto.getAuditStatus()), CourseBase::getAuditStatus, courseParamsDto.getAuditStatus());
        // 根据课程发布状态查询
        queryWrapper.eq(StringUtils.isNotEmpty(courseParamsDto.getPublishStatus()), CourseBase::getStatus, courseParamsDto.getPublishStatus());

        // 2.2创建page分页参数对象
        Page<CourseBase> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
        // 2.3开始分页查询
        Page<CourseBase> pageResult = courseBaseMapper.selectPage(page, queryWrapper);
        // 2.4得到分页数据
        List<CourseBase> items = pageResult.getRecords();
        long total = pageResult.getTotal();
        // 2.5返回分页数据
        PageResult<CourseBase> courseBasePageResult = new PageResult<>(items, total, pageParams.getPageNo(), pageParams.getPageSize());
        System.out.println(courseBasePageResult + "条件分页查询结果");
        return courseBasePageResult;
    }
}

下边对service进行单元测试,编写单元测试类:

@SpringBootTest
class CourseBaseServiceTests {

    @Autowired
    CourseBaseInfoService courseBaseInfoService;

    @Test
    void testCourseBaseInfoService() {
        //查询条件
        QueryCourseParamsDto queryCourseParamsDto = new QueryCourseParamsDto();
        queryCourseParamsDto.setCourseName("java");
        queryCourseParamsDto.setAuditStatus("202004");
        queryCourseParamsDto.setPublishStatus("203001");

        //分页参数
        PageParams pageParams = new PageParams();
        pageParams.setPageNo(1L);//页码
        pageParams.setPageSize(3L);//每页记录数

        PageResult<CourseBase> courseBasePageResult = courseBaseInfoService.queryCourseBaseList(pageParams, queryCourseParamsDto);
        System.out.println(courseBasePageResult);

    }

}

接口测试

业务层已经开发开发完成, 在接口中调用业务层完成接口的开发

/**
 * 课程信息管理接口
 */
@RestController
@Api(value = "课程信息管理接口",tags = "课程信息管理接口")
public class CourseBaseInfoController {

    @Autowired
    private CourseBaseInfoService courseBaseInfoService;

    // 课程查询接口
    @ApiOperation("课程查询接口")
    @PostMapping("/course/list")
    public PageResult<CourseBase> list(PageParams pageParams, @RequestBody(required = false) QueryCourseParamsDto queryCourseParamsDto) {
        PageResult<CourseBase> courseBasePageResult = courseBaseInfoService.queryCourseBaseList(pageParams, queryCourseParamsDto);
        return courseBasePageResult;

    }
}

使用Swagger测试接口

使用HttpClient测试接口

  1. Swagger是一个在线接口文档,虽然使用它也能测试但需要浏览器进入Swagger,最关键的是它并不能保存测试数据。
  2. 在IDEA中有一个非常方便的http接口测试工具httpclient,下边介绍它的使用方法,后边我们会用它进行接口测试。
  3. 新版的idea都自带该插件

  1. 进入controller类,找到http接口对应的方法, 点击小地球, 选择Generate request in HTTP Client即可生成的一个测试用例。

  1. 我们可以添加请求参数进行测试

  1. .http文件即测试用例文档,它可以随着项目工程一起保存,这样测试的数据就可以保存下来,方便进行测试。

  1. 为了方便保存.http文件,我们单独在项目工程的根目录创建一个目录单独存放它们

  1. 我们以模块为单位创建.http文件。把刚才测试数据拷贝上去

  1. 为了方便将来和网关集成测试,我们把测试主机地址在配置文件http-client.env.json 中配置

前端工程

解压前端工程, 运行前端工程

前端工程需要使用字典数据, 按照下边的步骤安装系统管理服务即可。

  1. 解压后, 引入system工程到项目中

  1. 进入pom.xml右键转为pom工程

  1. 进入xuecheng-plus-system-service工程,找到resources下的application.yml修改数据库连接参数。

  1. 启动系统管理服务,启动成功后,在浏览器请求

  1. 请求成功, 只是还要解决跨域问题

解决跨域

什么是跨域

CORS全称是 cross origin resource share 表示跨域资源共享。

同源策略是浏览器的一种安全机制,从一个地址请求另一个地址,如果协议、主机、端口三者全部一致则不属于跨域,否则有一个不一致就是跨域请求。比如:

  1. http://localhost:8601http://localhost:8602 由于端口不同,是跨域。
  2. http://192.168.101.10:8601http://192.168.101.11:8601 由于主机不同,是跨域。
  3. http://192.168.101.10:8601https://192.168.101.10:8601 由于协议不同,是跨域。
  4. 注意:服务器之间不存在跨域请求。

跨域时的交互

浏览器判断是跨域请求会在请求头上添加origin,表示这个请求来源哪里。比如:

GET / HTTP/1.1
Origin: http://localhost:8601

服务器收到请求判断这个Origin是否允许跨域,如果允许则在响应头中说明允许该来源的跨域请求,如下:

Access-Control-Allow-Origin:http://localhost:8601

解决跨域的方法:

1、JSONP

通过script标签的src属性进行跨域请求,如果服务端要响应内容则首先读取请求参数callback的值,callback是一个回调函数的名称,服务端读取callback的值后将响应内容通过调用callback函数的方式告诉请求方。如下图:

2、添加响应头

服务端在响应头添加 Access-Control-Allow-Origin:*

// 意思就是允许所有的请求跨域
Access-Control-Allow-Origin:*

3、通过nginx代理跨域

由于服务端之间没有跨域限制,浏览器通过nginx去访问跨域地址。

本项目使用方案2解决跨域问题

在系统管理的api工程config包下编写GlobalCorsConfig.java

/**
 * @author Mr.M
 * @version 1.0
 * @description 跨域过虑器
 * @date 2022/9/7 11:04
 */
@Configuration
public class GlobalCorsConfig {

    /**
     * 允许跨域调用的过滤器
     */
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        //允许白名单域名进行跨域调用
        config.addAllowedOrigin("*");
        //允许跨越发送cookie
        config.setAllowCredentials(true);
        //放行全部原始头信息
        config.addAllowedHeader("*");
        //允许所有请求方法跨域调用
        config.addAllowedMethod("*");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}
  1. 此配置类实现了跨域过虑器,在响应头添加Access-Control-Allow-Origin。
  2. 此跨域过滤器是由spring提供的, 我们只需要按照要求配置, 就可以完成跨域的处理
  3. 重启系统管理服务,前端工程可以正常进入http://localhost:8601,观察浏览器记录,成功解决跨域。

前后端联调

这里进行前后联调的目的是体会前后端联调的流程,测试的功能为课程查询功能。

  1. 访问前端首页,进入课程管理, 更改课程条件及分页参数测试课程查询列表是否正常显示。

课程分类查询

需求分析

要实现新增课程,需要先完成课程分类查询

课程分类信息有单独一张课程分类表

  1. 这张表是一个树型结构,通过父结点id将各元素组成一个树。

创建PO类

课程分类表的PO类之前生成好了, 如下

/**
 * <p>
 * 课程分类
 * </p>
 *
 * @author itcast
 */
@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

查看接口文档, 确定前端需要的数据结构, 然后定义DTO, 用于封装返回给前端的数据

// 前端需要的数据结构

 [
         {
            "childrenTreeNodes" : [
               {
                  "childrenTreeNodes" : null,
                  "id" : "1-1-1",
                  "isLeaf" : null,
                  "isShow" : null,
                  "label" : "HTML/CSS",
                  "name" : "HTML/CSS",
                  "orderby" : 1,
                  "parentid" : "1-1"
               },
               {
                  "childrenTreeNodes" : null,
                  "id" : "1-1-2",
                  "isLeaf" : null,
                  "isShow" : null,
                  "label" : "JavaScript",
                  "name" : "JavaScript",
                  "orderby" : 2,
                  "parentid" : "1-1"
               }
            ],
            "id" : "1-1",
            "isLeaf" : null,
            "isShow" : null,
            "label" : "前端开发",
            "name" : "前端开发",
            "orderby" : 1,
            "parentid" : "1"
         }
   ]

/**
 * 课程分类树型结点dto
 */
@Data
public class CourseCategoryTreeDto extends CourseCategory implements java.io.Serializable {
    // 子节点
    List<CourseCategoryTreeDto> childrenTreeNodes;
}
  1. Serializable是序列化接口, 网络传输时需要进行序列化, 所以要实现该接口

接口定义

定义接口

/**
 * 课程分类接口
 */
@Slf4j
@RestController
@Api(value = "课程分类接口",tags = "课程分类接口")
public class CourseCategoryController {

    // 查询课程分类列表
    @ApiOperation("课程分类查询")
    @GetMapping("/course-category/tree-nodes")
    public  CourseCategoryTreeDto queryTreeNodes() {
        return null;
    }

}

树型表查询

课程分类表是一个树型结构,其中parentid字段为父结点ID,它是树型结构的标志字段。

方案1:

如果树的层级固定可以使用表的自链接去查询,比如:我们只查询两级课程分类,可以用下边的SQL

select
       one.id            one_id,
       one.name          one_name,
       one.parentid      one_parentid,
       one.orderby       one_orderby,
       one.label         one_label,
       two.id            two_id,
       two.name          two_name,
       two.parentid      two_parentid,
       two.orderby       two_orderby,
       two.label         two_label
   from course_category one
            inner join course_category two on one.id = two.parentid
   where one.parentid = 1
     and one.is_show = 1
     and two.is_show = 1
   order by one.orderby,
            two.orderby

方案2:

如果树的层级不确定,此时可以使用MySQL递归实现,使用with语法,如下:

 WITH [RECURSIVE]
        cte_name [(col_name [, col_name] ...)] AS (subquery)
        [, cte_name [(col_name [, col_name] ...)] AS (subquery)] ...
  • cte_name :公共表达式的名称,可以理解为表名,用来表示as后面跟着的子查询
  • col_name :公共表达式包含的列名,可以写也可以不写
  • 下边是一个递归的简单例子:
/**
说明
● t1 相当于一个表名
● select 1 相当于这个表的初始值,这里使用UNION ALL 不断将每次递归得到的数据加入到表中。
● n<5为递归执行的条件,当n>=5时结束递归调用。
*/

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

下边我们使用递归实现课程分类的查询

with recursive t1 as (
select * from  course_category p where  id= '1'
union all
 select t.* from course_category t inner join t1 on t1.id = t.parentid
)
select *  from t1 order by t1.id, t1.orderby

  • t1表中初始的数据是id等于1的记录,即根结点。
  • 通过inner join t1 t2 on t2.id = t.parentid 找到id='1'的下级节点。
  • 通过这种方法就找到了id='1'的所有下级节点,下级节点包括了所有层级的节点。
  • 上边这种方法是向下递归,即找到初始节点的所有下级节点。

如何向上递归?

下边的sql实现了向上递归:

with recursive t1 as (
select * from  course_category p where  id= '1-1-1'
union all
 select 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限制执行时间,超过此时间也会终止递归操作。

mysql递归相当于在存储过程中执行若干次sql语句,java程序仅与数据库建立一次链接执行递归操作,所以只要控制好递归深度,控制好数据量性能就没有问题。

持久层开发

自定义mapper方法查询课程分类,最终将查询结果映射到List<CourseCategoryTreeDto>中。

/**
 * <p>
 * 课程分类 Mapper 接口
 * </p>
 *
 * @author itcast
 */
public interface CourseCategoryMapper extends BaseMapper<CourseCategory> {
    // 使用递归查询分类
    public List<CourseCategoryTreeDto> selectTreeNodes(String id);

}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xuecheng.content.mapper.CourseCategoryMapper">
   
    <!-- 课程分类查询   -->
    <select id="selectTreeNodes" resultType="com.xuecheng.content.model.dto.CourseCategoryTreeDto" parameterType="string">
        with recursive t1 as (
            select * from  course_category p where  id= #{id}
            union all
            select t.* from course_category t inner join t1 on t1.id = t.parentid
        )
        select *  from t1 order by t1.id, t1.orderby

    </select>

</mapper>

测试mapper方法

@SpringBootTest
class CourseCategoryMapperTests {

    @Autowired
    CourseCategoryMapper courseCategoryMapper;


    @Test
    void testCourseCategoryMapper() {
        List<CourseCategoryTreeDto> cctd = courseCategoryMapper.selectTreeNodes("1");
        System.out.println(cctd);
    }

}

控制层开发

定义service接口,调用mapper查询课程分类,遍历数据按照接口要求对数据进行封装

/**
 * 课程分类信息管理接口
 */
public interface CourseCategoryService {
    /**
     * 课程分类树形结构查询
     *
     * @return
     */
    public List<CourseCategoryTreeDto> queryTreeNodes(String id);

}
@Slf4j
@Service
public class CourseCategoryServiceImpl implements CourseCategoryService {
    @Autowired
    private CourseCategoryMapper courseCategoryMapper;

    /**
     * 课程分类信息查询
     * @param id  根节点id
     * @return 树形结构对象
     */
    @Override
    public List<CourseCategoryTreeDto> queryTreeNodes(String id) {
        // 查询所有的课程分类信息
        List<CourseCategoryTreeDto> courseCategoryTreeDtos = courseCategoryMapper.selectTreeNodes(id);

        // 构建树形结构: 找到每个节点的子节点, 最终封装成List<CourseCategoryTreeDto>
        // 1.拆出每个节点: 先把list转成map, key是节点id, value是CourseCategoryTreeDto对象
        Map<String, CourseCategoryTreeDto> mapTemp = courseCategoryTreeDtos.stream().filter(item -> !id.equals(item.getId())).collect(Collectors.toMap(key -> key.getId(), value -> value, (key1, key2) -> key2));
        // 2.最终返回的树形结构
        List<CourseCategoryTreeDto> courseCategoryList = new ArrayList<>();
        // 3.重新组织数据结构: 遍历list, 获取每个节点的父节点id, 从map中获取父节点的子节点, 把当前节点添加到父节点的子节点集合中
        courseCategoryTreeDtos.stream().filter(item->!id.equals(item.getId())).forEach(item->{
            // a一级节点: 当前节点的父节点id等于根节点id, 则当前节点作为一级节点
            if(item.getParentid().equals(id)){
                courseCategoryList.add(item);
            }
            // b二级节点: 当前节点的父节点id不等于根节点id, 则当前节点一定是二级节点
            // b.1找到当前二级节点的父节点
            CourseCategoryTreeDto courseCategoryTreeDto = mapTemp.get(item.getParentid());
            if(courseCategoryTreeDto!=null){
                // b.2父节点的childrenTreeNodes属性是null, 先初始化一下
                if(courseCategoryTreeDto.getChildrenTreeNodes() ==null){
                    courseCategoryTreeDto.setChildrenTreeNodes(new ArrayList<CourseCategoryTreeDto>());
                }
                // b.3下边开始往ChildrenTreeNodes属性中放子节点
                courseCategoryTreeDto.getChildrenTreeNodes().add(item);
            }
        });


        return courseCategoryList;
    }
}

单元测试

@SpringBootTest
class CourseCategoryServiceTests {

    @Autowired
    CourseCategoryService categoryService;

    @Test
    void testCourseBaseInfoService() {
        List<CourseCategoryTreeDto> courseCategoryTreeDtos = categoryService.queryTreeNodes("1");
        System.out.println(courseCategoryTreeDtos);
    }


}

mapper查询到的数据

处理完成的树形结构

接口测试

完成接口

/**
 * 课程分类接口
 */
@Slf4j
@RestController
@Api(value = "课程分类接口", tags = "课程分类接口")
public class CourseCategoryController {

    @Autowired
    private CourseCategoryService courseCategoryService;

    // 查询课程分类列表
    @ApiOperation("课程分类查询")
    @GetMapping("/course-category/tree-nodes")
    public List<CourseCategoryTreeDto> queryTreeNodes() {
        List<CourseCategoryTreeDto> courseCategoryTreeDtos = courseCategoryService.queryTreeNodes("1");
        return courseCategoryTreeDtos;
    }

}

启动服务, 测试接口

前后端联调: 课程分类下拉框可以正常显示

新增课程

需求分析

课程基本信息是两部分组成,一部分是课程基本信息上,一部分是课程营销信息。

数据库中, 这两部分信息分别在course_base、course_market两张表存储。当点击保存按钮时向这两张表插入数据。这两张表是一对一关联关系。

  1. 新建课程的初始审核状态为“未提交”、初始发布状态为“未发布”。

导入DTO实体类, 分别封装请求参数和响应结果

/**
 * @description 课程基本信息dto
 * @author Mr.M
 * @date 2022/9/7 17:44
 * @version 1.0
 */
@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;

}
/**
 * @description 添加课程dto
 * @author Mr.M
 * @date 2022/9/7 17:40
 * @version 1.0
 */
@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;
}

接口设计

### 创建课程
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
}

接口定义

/**
 * 课程信息管理接口
 */
@RestController
@Api(value = "课程信息管理接口", tags = "课程信息管理接口")
public class CourseBaseInfoController {

    // 添加课程
    @ApiOperation("添加课程")
    @PostMapping("/course")
    public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto addCourseDto) {

        return null;
    }
}

定义service

定义service接口和实现类, 向课程基本信息表、课程营销表保存数据。

/**
 * 课程基本信息管理业务接口
 */
public interface CourseBaseInfoService {

    /**
     * @description 添加课程基本信息
     * @param companyId  教学机构id
     * @param addCourseDto  课程基本信息
     * @return com.xuecheng.content.model.dto.CourseBaseInfoDto
     * @author Mr.M
     * @date 2022/9/7 17:51
     */
    CourseBaseInfoDto createCourseBase(Long companyId, AddCourseDto addCourseDto);
}
@Slf4j
@Service
public class CourseBaseInfoServiceImpl implements CourseBaseInfoService {

    @Autowired
    private CourseBaseMapper courseBaseMapper;

    @Autowired
    private CourseMarketMapper courseMarketMapper;

    @Autowired
    private CourseCategoryMapper courseCategoryMapper;

    // 课程添加
    @Transactional
    @Override
    public CourseBaseInfoDto createCourseBase(Long companyId, AddCourseDto dto) {
        //1.合法性校验
        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("收费规则为空");
        }

        //2.向课程基本信息表course_base写入数据
        CourseBase courseBase = new CourseBase();
        BeanUtils.copyProperties(dto, courseBase); // 只要属性名一致, 就能拷贝
        courseBase.setCompanyId(companyId);
        courseBase.setCreateDate(LocalDateTime.now());
        courseBase.setStatus("203001"); // 课程发布状态
        courseBase.setAuditStatus("202002"); // 课程审核状态
        int insert = courseBaseMapper.insert(courseBase);
        if (insert <= 0) {
            throw new RuntimeException("添加课程失败");
        }

        //3.向课程营销表course_market写入数据
        CourseMarket courseMarketNew = new CourseMarket();
        Long courseId = courseMarketNew.getId();
        BeanUtils.copyProperties(dto, courseMarketNew);
        courseMarketNew.setId(courseId);
        saveCourseMarket(courseMarketNew);

        //4.查询课程信息返回
        CourseBaseInfoDto courseBaseInfo = getCourseBaseInfo(courseId);

        return courseBaseInfo;
    }

    // 查询课程信息,根据课程id
    private CourseBaseInfoDto getCourseBaseInfo(Long courseId) {
        // 查询课程基本信息
        CourseBase courseBase = courseBaseMapper.selectById(courseId);
        if (courseBase == null) {
            return null;
        }

        // 查询课程营销信息
        CourseMarket courseMarket = courseMarketMapper.selectById(courseId);

        // 组装返回数据
        CourseBaseInfoDto courseBaseInfoDto = new CourseBaseInfoDto();
        BeanUtils.copyProperties(courseBase, courseBaseInfoDto);
        if (courseMarket != null) {
            BeanUtils.copyProperties(courseMarket, courseBaseInfoDto);
        }

        // 设置课程分类的名称(查出来是id)
        CourseCategory mtName = courseCategoryMapper.selectById(courseBaseInfoDto.getMt());
        courseBaseInfoDto.setMtName(mtName.getName());
        CourseCategory stName = courseCategoryMapper.selectById(courseBaseInfoDto.getSt());
        courseBaseInfoDto.setStName(stName.getName());

        return courseBaseInfoDto;
    }

    // 保存营销信息
    private int saveCourseMarket(CourseMarket courseMarketNew) {
        // 合法性校验
        String charge = courseMarketNew.getCharge();
        if (charge.equals("201001")) {
            // 如果是收费课程, 必须设置价格
            if (courseMarketNew.getPrice() == null || courseMarketNew.getPrice() <= 0) {
                throw new RuntimeException("收费课程,价格不能为空且必须大于0");
            }
        }

        // 查询课程营销表, 如果存在, 更新, 如果不存在, 添加
        Long id = courseMarketNew.getId(); // 主键
        CourseMarket courseMarket = courseMarketMapper.selectById(id);
        if(courseMarket == null) {
            // 插入数据
            return courseMarketMapper.insert(courseMarketNew);
        } else {
            // 更新数据
            BeanUtils.copyProperties(courseMarketNew, courseMarket);
            courseMarket.setId(courseMarketNew.getId());
            return courseMarketMapper.updateById(courseMarket);
        }
    }
}

完善controller

在controller中调用service方法

/**
 * 课程信息管理接口
 */
@RestController
@Api(value = "课程信息管理接口", tags = "课程信息管理接口")
public class CourseBaseInfoController {

    @Autowired
    private CourseBaseInfoService courseBaseInfoService;

    // 添加课程
    @ApiOperation("添加课程")
    @PostMapping("/course")
    public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto addCourseDto) {
        // todo: 机构id暂时硬编码
        Long companyId = 1232141425L;
        return courseBaseInfoService.createCourseBase(companyId, addCourseDto);
    }
}

接口测试

使用httpclient测试接口

### 添加课程
POST {{content_host}}/content/course
Content-Type: application/json

{
  "charge": "201000",
  "price": 0,
  "originalPrice":0,
  "qq": "22333",
  "wechat": "223344",
  "phone": "13333333",
  "validDays": 365,
  "mt": "1-1",
  "st": "1-1-1",
  "name": "java开发高级工程师",
  "pic": "",
  "teachmode": "200002",
  "users": "初级人员",
  "tags": "",
  "grade": "204001",
  "description": "键盘敲烂, 月入过万"
}

前后端联调: 添加课程后正常展示

Logo

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。

更多推荐