创建内容管理工程

是上一节中,我们讲到了后端工程结构,各个微服务工程依赖于基础(base)工程

下面我们来做第一个微服务工程 “内容管理工程” (content)

其中这个内容管理工程下面也可以再进行细分

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

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

img

创建内容管理父工程

现在开始创建内容管理工程,注意基于根目录创建xuecheng-plus-content

image-20230526093953309

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

在pom文件中引入对父工程的依赖

<?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-model)

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

image-20230526094811382

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

修改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>

按照上述步骤依次创建 service工程以及 api工程

service pom文件

<?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>

api工程pom文件

<?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>

创建完毕后,项目的基础目录如下:

image-20230526095744019

内容管理功能实现

功能一:课程查询

建库建表

创建了数据库(这时内容管理的数据库,一般一个微服务就对应一个数据库即微服务之间的分库分表)xc_content

导入第一节课中sql脚本,创建好数据库表

image-20230526101648309

生成PO类(mybatis-plus代码生成器)

本项目使用mybatis-plus的generator工程生成PO类、Mapper接口、Mapper的xml文件,地址在:https://github.com/baomidou/generator

将课程资料目录下的xuecheng-plus-generator.zip解压后拷贝至项目工程根目录

打开IDEA将其导入项目工程 ,打开xuecheng-plus-generator工程的pom.xml,右键 点击“Add as Maven Project” 自动识别maven工程。

找到ContentCodeGenerator类

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

//数据库账号
private static final String DATA_SOURCE_USER_NAME  = "root";
//数据库密码
private static final String DATA_SOURCE_PASSWORD  = "mysql";
//生成的表
private static final String[] TABLE_NAMES = new String[]{
        "course_base",
        "course_market",
        "course_teacher",
        "course_category",
        "teachplan",
        "teachplan_media",
        "course_publish",
        "course_publish_pre"
};
// TODO 默认生成entity,需要生成DTO修改此变量
// 一般情况下要先生成 DTO类 然后修改此参数再生成 PO 类。
private static final Boolean IS_DTO = false;

public static void main(String[] args) {
        ....
        //生成路径
        gc.setOutputDir(System.getProperty("user.dir") + "/xuecheng-plus-generator/src/main/java");
        
....
// 数据库配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setDbType(DbType.MYSQL);
        dsc.setUrl("jdbc:mysql://192.168.101.65:3306/xc_" + SERVICE_NAME+"166"
                        + "?serverTimezone=UTC&useUnicode=true&useSSL=false&characterEncoding=utf8");
        ...

运行完毕后生成如下目录

image-20230526105448632

在该包下自动生成了内容管理模块的controller、mapper、po及service相关代码,这里我们只需要po类。

将po类拷贝到model工程

image-20230526105629277

打开一个PO类发现编译报错,这是缺少依赖包导致,本项目使用的持久层框架是MyBatisPlus,在生成的po类中加了一些MyBatisPlus框架的注解,这里需要添加MyBatisPlus框架的依赖,消除错误。

下边在model工程添加依赖

接口模型类

分析接口

1、 协议

  • 确定协议,一般都是http协议
  • 查询请求方式:查询一般用post 或者get,

  • 确定contenttype 数据以什么形式提交,一般情况下都json格式回应

2、分析请求参数(这是一个分页查询)

  • 课程名称
  • 课程审核状态
  • 当前页码、每页显示记录数

3、分析响应结果

  • 课程id、课程名称、任务数、创建时间、审核状态、类型。
  • 审核状态为数据字典中的代码字段,前端会根据审核状态代码 找到对应的名称显示。

4、分析完成,使用SpringBoot注解开发一个Http接口

5、使用接口文档工具查看接口的内容

6、接口中调用Service方法完成业务处理

请求响应示例:

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
}

可以观察到,返回的数组是一种分页数据结果,一般需要单独设计一种数据模型(类)来暴露在外面,供api封装返回

话不多说,我们来定义个这个

分页模型

首先定义分页查询类,以后都可以调用到的(所以放在 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类

QueryCourseParamsDto

package com.xuecheng.content.model.dto;

import lombok.Data;
import lombok.ToString;

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

}

(分页)响应模型类

针对分页查询结果经过分析也存在固定的数据和格式,所以在base工程定义一个基础的模型类。


package com.xuecheng.base.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;

import java.io.Serializable;
import java.util.List;

@Data
@ToString
@AllArgsConstructor
public class PageResult<T> implements Serializable {

    // 数据列表
    private List<T> items;

    //总记录数
    private long counts;

    //当前页码
    private long page;

    //每页记录数
    private long pageSize;

}

image-20230526130445955

我们发现此模型类中定义了List属性,此属性存放数据列表,且支持泛型,课程查询接口的返回类型可以是此模型类型。

List中的数据类型用什么呢?根据需求分析使用生成的PO类即可,所以课程查询接口返回结果类型如下:

定义接口

controller实现

根据分析,此接口提供 HTTP post协议,查询条件以json格式提交,响应结果为json 格式。

可使用SpringBoot注解在Controller类中实现。


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

之后定义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<CourseBase> list(PageParams pageParams, @RequestBody QueryCourseParamDto queryCourseParams) {
        CourseBase courseBase = new CourseBase();
        courseBase.setId(15L);
        courseBase.setDescription("测试课程");
        PageResult<CourseBase> result = new PageResult<>();
        result.setItems(Arrays.asList(courseBase));
        result.setPage(1);
        result.setPageSize(10);
        result.setCounts(1);
        return result;
    }
}

定义启动类

  • 定义启动类
package com.xuecheng;


import com.spring4all.swagger.EnableSwagger2Doc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableSwagger2Doc 
public class ContentApplication {
   public static void main(String[] args) {
      SpringApplication.run(ContentApplication.class, args);
   }
}

添加配置文件

  • 添加配置文件

src/main/resources 下添加文件 log4j2-dev.xml

(用于输出日志)

<?xml version="1.0" encoding="UTF-8"?>
<Configuration monitorInterval="180" packages="">
    <properties>
        <property name="logdir">logs</property>
        <property name="PATTERN">%date{YYYY-MM-dd HH:mm:ss,SSS} %level [%thread][%file:%line] - %msg%n%throwable</property>
    </properties>
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="${PATTERN}"/>
        </Console>

        <RollingFile name="ErrorAppender" fileName="${logdir}/error.log"
            filePattern="${logdir}/$${date:yyyy-MM-dd}/error.%d{yyyy-MM-dd-HH}.log" append="true">
            <PatternLayout pattern="${PATTERN}"/>
            <ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1" modulate="true" />
            </Policies>
        </RollingFile>

        <RollingFile name="DebugAppender" fileName="${logdir}/info.log"
            filePattern="${logdir}/$${date:yyyy-MM-dd}/info.%d{yyyy-MM-dd-HH}.log" append="true">
            <PatternLayout pattern="${PATTERN}"/>
            <ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1" modulate="true" />
            </Policies>
        </RollingFile>
        
        <!--异步appender-->
        <Async name="AsyncAppender" includeLocation="true">
            <AppenderRef ref="ErrorAppender"/>
            <AppenderRef ref="DebugAppender"/>
        </Async>
    </Appenders>
    
    <Loggers>
        <!--过滤掉spring和mybatis的一些无用的debug信息-->
        <logger name="org.springframework" level="INFO">
        </logger>
        <logger name="org.mybatis" level="INFO">
        </logger>
        <logger name="cn.itcast.wanxinp2p.consumer.mapper" level="DEBUG">
        </logger>

        <logger name="springfox" level="INFO">
        </logger>
        <logger name="org.apache.http" level="INFO">
        </logger>
        <logger name="com.netflix.discovery" level="INFO">
        </logger>
        
        <logger name="RocketmqCommon"  level="INFO" >
        </logger>
        
        <logger name="RocketmqRemoting" level="INFO"  >
        </logger>
        
        <logger name="RocketmqClient" level="WARN">
        </logger>

        <logger name="org.dromara.hmily" level="WARN">
        </logger>

        <logger name="org.dromara.hmily.lottery" level="WARN">
        </logger>

        <logger name="org.dromara.hmily.bonuspoint" level="WARN">
        </logger>
        
        <!--OFF   0-->
        <!--FATAL   100-->
        <!--ERROR   200-->
        <!--WARN   300-->
        <!--INFO   400-->
        <!--DEBUG   500-->
        <!--TRACE   600-->
        <!--ALL   Integer.MAX_VALUE-->
        <Root level="DEBUG" includeLocation="true">
            <AppenderRef ref="AsyncAppender"/>
            <AppenderRef ref="Console"/>
            <AppenderRef ref="DebugAppender"/>
        </Root>
    </Loggers>
</Configuration>

bootstrap.yml

(启动时优先读取的配置)

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/xc_content?serverTimezone=UTC&userUnicode=true&useSSL=false&
    username: root
    password: mysql
# 日志文件配置路径
logging:
  config: classpath:log4j2-dev.xml
  
# swagger 文档配置
swagger:
  title: "学成在线内容管理系统"
  description: "内容系统管理系统对课程相关信息进行业务管理数据"
  base-package: com.xuecheng.content
  enabled: true
  version: 1.0.0

swagger介绍

运行启动类:

访问:http://localhost:63040/content/swagger-ui.html

image-20230526141846226

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

重启服务,再次进入接口文档,可以看到添加的描述

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

接口开发

上面我们并没有开发持久层建立于数据库的连接,下面我们来真正开发这些功能。

持久层的代码一般是固定的,我们可以用mybatis自动生成mapper

下边将使用generator工程成的mapper接口和mapper映射文件 拷贝到service工程对应目录 ,如下图:

image-20230526144040964

测试mapper

先对mapper进行测试看看能不能用

测试前对service添加依赖

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

    <!-- 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>
    <!-- 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>

</dependencies>

在com.xuecheng.content.config包下创建MP配置类,配置分页拦截器

到 service工程的com.xuecheng.content.config包下:

package com.xuecheng.content.config;

@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每页大小和当前页的数据,是否为首页,是否为尾页,总页数等。

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

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

其中bootstrap.yml

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

image-20230528140216853

package com.xuecheng.content;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;



@SpringBootApplication
public class ContentApplication {
    public static void main(String[] args) {
        SpringApplication.run(ContentApplication.class,args);
    }
}

编写测试类

@SpringBootTest
public class ContentMapperTest {
    @Autowired
    CourseBaseMapper courseBaseMapper;
    @Test
    public void TestMapper(){
        CourseBase courseBase=courseBaseMapper.selectById(74L);
        System.out.println(JSON.toJSON(courseBase));
    }
}

没想到一直报错,说数据源没配置好。。。

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class


Action:

Consider the following:
	If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
	If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).

实在是想不知道是哪出错了,索性重新做了一遍项目,好了。。。

我也是醉了

数据字典表

目的:为了提高系统的可扩展性,专门定义数据字典去维护,例如

[
    {"code":"202001","desc":"审核未通过"},
    {"code":"202002","desc":"未审核"},
    {"code":"202003","desc":"审核通过"}
]

那么我们创建系统管理数据库xc_system,在其中创建管理系统服务的数据表,导入黑马提供的SQL脚本就好了。这样查询出的数据在前端展示时,就根据代码取出它对应的内容显示给用户。如果客户需要修改审核未通过的显示内容,直接在数据字典中修改就好了,无需修改课程基本信息表

查询下拉框中的数据也可以从数据字典表中获取

编写Servcie代码

在service工程下面 新建 service包

service接口

public interface CourseBaseInfoService {

    // 课程分页查询
    /**
     * 参数查询条件 queryCourseParamsDto
     * 查询分页参数 pageParams
     */
    public PageResult<CourseBase> queryCourseBaseList (PageParams pageParams,QueryCourseParamsDto queryCourseParamsDto);


}

实现该接口:


@Slf4j
@Service
public class CourseBaseInfoServiceImpl implements CourseBaseInfoService {
    @Autowired
    CourseBaseMapper courseBaseMapper;


    @Override
    public PageResult<CourseBase> queryCourseBaseList(PageParams pageParams, QueryCourseParamsDto queryCourseParamsDto) {
        // 构造查询条件
        LambdaQueryWrapper<CourseBase> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(StringUtils.isNotEmpty(queryCourseParamsDto.getAuditStatus()),CourseBase::getAuditStatus,queryCourseParamsDto.getAuditStatus());
        lambdaQueryWrapper.like(StringUtils.isNotEmpty(queryCourseParamsDto.getCourseName()),CourseBase::getName,queryCourseParamsDto.getCourseName());
        lambdaQueryWrapper.eq(StringUtils.isNotEmpty(queryCourseParamsDto.getPublishStatus()),CourseBase::getStatus,queryCourseParamsDto.getPublishStatus());
        // 构造分页
        long pageNo = pageParams.getPageNo();
        long pageSize = pageParams.getPageSize();
        Page<CourseBase> page =new Page<>(pageNo,pageSize);
        // 分页查询
        Page<CourseBase> courseBasePage = courseBaseMapper.selectPage(page, lambdaQueryWrapper);
        // 将查询道德数据封装到结果类型中
        List<CourseBase> items = courseBasePage.getRecords();
        long total = courseBasePage.getTotal();
        PageResult<CourseBase> pageResult = new PageResult<>(items,total,pageNo,pageSize);
        return  pageResult;
    }
}

再实现controller

 @PostMapping("/course/list")
  public PageResult<CourseBase> list(PageParams pageParams, @RequestBody(required = false) QueryCourseParamsDto queryCourseParams){
     log.info("pageParams:{},QueryParams:{}", JSON.toJSONString(pageParams),JSON.toJSONString(queryCourseParams));

     PageResult<CourseBase> pageResult = courseBaseInfoService.queryCourseBaseList(pageParams,queryCourseParams);
//     log.info("结果集:{}",pageResult);
//     System.out.println(pageResult);
     return pageResult;
  }

Httpclient测试

(需要下载Httpclient插件)

在项目目录下新建http文件

image-20230529222317342

输入测试案例:

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

{
  "auditStatus": "202004",
  "courseName": "java",
  "publishStatus": "203001"
}

image-20230529222544638

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

注意:文件名称http-client.env.json保持一致,否则无法读取dev环境变量的内容。

内容如下:

{
  "dev": {
    "access_token": "",
    "gateway_host": "localhost:63010",
    "content_host": "localhost:63040",
    "system_host": "localhost:63110",
    "media_host": "localhost:63050",
    "search_host": "localhost:63080",
    "auth_host": "localhost:63070",
    "checkcode_host": "localhost:63075",
    "learning_host": "localhost:63020"
  }
}

再回到xc-content-api.http文件,将http://localhost:63040 用变量代替

image-20230529223638294

前后端联调

准备环境

安装nodejs

把课程前端工程project-xczx2-portal-vue-ts.zip解压放到项目平级目录用idea打开

一些小知识点

package.json相当于前端工程的pom文件(依赖文件)

右键点击project-xczx2-portal-vue-ts目录下的package.json文件

点击Show npm Scripts打开npm窗口

image-20230529224933494

点击“Edit ‘serve’” setting,下边对启动项目的一些参数进行配置,选择nodejs、npm。

image-20230529224948371

image-20230529225116110

右键点击Serve,点击“Run serve”启动工程。

访问http://localhost:8601即可访问前端工程。

访问网址F12 发现他要访问system微服务的资源,但是这个微服务我们还没有写

http://localhost:8601/system/dictionary/all

这个有现成的代码,导入即可

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

启动system服务再次访问网址就可以显示页面了

跨域资源访问

等等还是有问题,发现虽然返回正常(200 OK),,但是依然报错?

错误的

错误名称是“CORS错误”,

其实返回的信息都是正确的,你双击能看到所有的返回结果。这就表明,并非服务端发生的错误,而是前端报的错。

错误的原因:

你访问的网址是(打开的网址) http://localhost:8601,也就是浏览器默认你拿所有的资源的时候都默认从http://localhost:8601这个地方拿

但是这里访问http://localhost:8601时,网站却自动向(异步访问)http://localhost:63110/system/dictionary/all 这个网址拿资源,这就触发了浏览器自带的一种保护策略即CORS协议,这种协议默认你不能跨域(非同源是指协议、主机、端口号有任意一个不同的都叫跨域)我们这里端口号不同,发生了跨域。

如果发生了跨域访问(或者说,一定要跨域),需要

  • 在跨域的请求请求头添加Origin字段,给服务器说明,这是一个跨域请求
  • 服务端根据设定的规则,判断是否允许跨域如果允许,则会在响应头中添加 Access-Control-Allow-Origin字段,如
    • Access-Control-Allow-Origin:http://localhost:8601
    • Access-Control-Allow-Origin:* (允许任何网址的跨域资源共享)

如果客户端发现响应头没有上述允许的标识,就会报错,即使接收到了完整信息,浏览器也会报错

默认浏览器不支持跨域请求,解决方案有很多种

1、前端 JSONP

  • 采用了一种很巧妙的规避方式:跨域策略资源不限制 图片标签 script标签 等
  • 可以将跨域请求的资源 放在 script标签的 src属性当中

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

image-20230530175502553

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

在 Spring Boot 中为特定响应添加标头

参考以上博文,我将system查询代码做了如下修改,果然成功了

@GetMapping("/dictionary/all")
public List<Dictionary> queryAll(HttpServletResponse httpServletResponse) {
    httpServletResponse.setHeader("Access-Control-Allow-Origin","*");
    return dictionaryService.queryAll();
}

这样只是在单个请求实现了 跨域访问,若想使该服务的所有资源都享受跨域资源共享,那么可以设计一个过滤器,过滤所有的资源访问请求,将响应结果添加允许跨域的标识

仅需在system系统api工程下config目录添加配置类GlobalCorsConfig.java,

这是Spring已经考虑到过跨域访问情况而设计的过滤器,直接拿来用即可。

代码如下:

package com.xuecheng.system.config;

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

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

image-20230530202223840

1)浏览器先访问http://192.168.101.10:8601 nginx提供的地址,进入页面
2)此页面要跨域访问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通过服务端通信,没有跨域。

测试接口

同时开启 content 和 system 服务, 可以看到 查询功能生效

image-20230530210038810

功能二:课程分类查询

课程分类表存储在存储在内容管理数据库中,需要单独查询

此外,比较棘手的是,课程分类信息信息有一定的层次结构,如何从表中提取层次结构是实现接口的关键

image-20230530212041488

这是一种典型的树形结构

定义接口

受限分析接口

请求时 HTTP GET请求:http://localhost:8601/api/content/course-category/tree-nodes

查询接口文档,需要返回的是这样的格式(为了看着方便,经过简化,删去了部分属性)

[
        {
           "childrenTreeNodes" : [
              {
                 "childrenTreeNodes" : null,
                 "id" : "1-1-1",
                 "name" : "HTML/CSS",
                 "parentid" : "1-1"
              },
              {
                 "childrenTreeNodes" : null,
                 "id" : "1-1-10",
                 "name" : "其它",
                 "parentid" : "1-1"
              }
           ],
           "id" : "1-1",
           "name" : "前端开发",
           "parentid" : "1"
        },
        {
           "childrenTreeNodes" : [
              {
                 "childrenTreeNodes" : null,
                 "id" : "1-2-1",
                 "name" : "微信开发",
                 "parentid" : "1-2"
              }
           ],
           "id" : "1-2",
           "name" : "移动开发",
           "parentid" : "1"
        }
  ]

所以,在 content model dto 下定义一个DTO类表示分类信息的模型类,如下:

它是一种嵌套结构

@Data
public class CourseCategoryTreeDto extends CourseCategory implements Serializable {

  List<CourseCategoryTreeDto> childrenTreeNodes;
}

定义控制器返回树形结构。(列表)

@Slf4j
@RestController
public class CourseCategoryController {

    
    @GetMapping("/course-category/tree-nodes")
    public List<CourseCategoryTreeDto> queryTreeNodes() {
       return null;
    }
}

接口开发

树型表查询

课程分类表是一个树型结构,其中parentid字段为父结点ID,它是树型结构的标志字段。
如果树的层级固定可以使用表的自链接去查询,比如:我们只查询两级课程分类,可以用下边的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

如果树的层级不确定,此时可以使用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 :公共表达式包含的列名,可以写也可以不写

实现:

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 相当于整体表名

查询结果如下

image-20230531091633294

通过这种方法就找到了id=’1’的所有下级节点,下级节点包括了所有层级的节点。

拓展,上面是向下递归,如何向上递归?

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

mysql为了避免无限递归默认递归次数为1000,可以通过设置cte_max_recursion_depth参数增加递归深度,还可以通过max_execution_time限制执行时间,超过此时间也会终止递归操作。

开发Mapper

一般情况下mapper都不用写,都是用代码生成器帮我们自动生成,只有这种特殊情况的查询需要我们来手动写

在content service工程下 找到mapper目录新建CourseCategoryMapper接口

public interface CourseCategoryMapper extends BaseMapper<CourseCategory> {
    public List<CourseCategoryTreeDto> selectTreeNodes(String id);
}

找到对应 的mapper.xml文件,编写sql语句。

<?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">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.xuecheng.content.model.po.CourseCategory">
        <id column="id" property="id" />
        <result column="name" property="name" />
        <result column="label" property="label" />
        <result column="parentid" property="parentid" />
        <result column="is_show" property="isShow" />
        <result column="orderby" property="orderby" />
        <result column="is_leaf" property="isLeaf" />
    </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, name, label, parentid, is_show, orderby, is_leaf
    </sql>

+    <!-- 自定义查询结果列 -->
+    <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文件中:

id=”selectTreeNodes” 是你在mapper接口中定义的 查询函数的名称

resultType=”com.xuecheng.content.model.dto.CourseCategoryTreeDto” 是返回结果

parameterType=”string” 是传参类型

mapper测试 略。

开发Service

Mapper只是把所有的课程(根节点为1)的分类查询了出来,我们想要的是一种树状结构,

所以我们需要从结果中构建这种树状结构。

开始开发 service

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

编写实现类

在编写之前,缕一缕怎么编写才对

1、排除根节点 :返回的类型应该没有根节点 1 的存在

2、根节点直接子节点是 我们要的 第一层 节点,也就是 返回结果第一层列表中的元素是这些第一层节点

3、 第一层节点 的 子节点属性是一个列表 ,列表中的元素是 第二层节点 往下递归。

好了开始编写吧

@Service
public class CourseCategoryServiceImpl implements CourseCategoryService {
    /**
     * 实现 查询传入id的所有节点,以及将其封装成为一个树状结构
     * @param id
     * @return
     */

    @Autowired
    CourseCategoryMapper courseCategoryMapper;
    @Override
    public List<CourseCategoryTreeDto> queryTreeNodes(String id) {
        // 未处理的数据
        List<CourseCategoryTreeDto> courseCategoryTreeDtos = courseCategoryMapper.selectTreeNodes(id);



        // 开始封装
        // 将list 转成 Map 方便查询使用 (在这一步排除根节点)
        Map<String,CourseCategoryTreeDto> mapTemp =
                courseCategoryTreeDtos
                        .stream()
                        .filter(item -> !(item.getId().equals(id))) // 过滤根节点
                        .collect(Collectors.toMap(key->key.getId(),value->value,(key1,key2)->key2));
                        //(key1,key2)->key2) 的含义是 : 当 list集合元素重复时选择哪一个?
        // 返回 的 list
        List<CourseCategoryTreeDto> result = new ArrayList<>();
        // 遍历元素(排除根节点) 将将其放入结果集当中
        courseCategoryTreeDtos
                .stream()
                .filter(item->!(item.getId().equals(id)))
                .forEach(item->{    // 遍历元素
                    // 第一层
                    if(item.getParentid().equals(id)){
                        result.add(item);
                    }
                    // 不是第一层,找到了他的父节点 (一定可以找到,因为时按顺序拍的)
                    // 父节点
                    CourseCategoryTreeDto parentDto = mapTemp.get(item.getParentid());
                    // 在父节点的 子节点类表中添加 子节点
                    if(parentDto!=null){ //其实不用加的,因为已经排除了根节点
                        if(parentDto.getChildrenTreeNodes()==null){
                            parentDto.setChildrenTreeNodes(new ArrayList<CourseCategoryTreeDto>());
                        }
                        parentDto.getChildrenTreeNodes().add(item);
                    }
                });
                return result;

    }
}

测试service

@Test
public void ServiceTest(){
    List<CourseCategoryTreeDto> courseCategoryTreeDtos = service.queryTreeNodes("1");
    System.out.println(JSON.toJSON(courseCategoryTreeDtos));
}

显示成功!!!

完善接口

@Autowired
CourseCategoryService courseCategoryService;
@GetMapping("/course-category/tree-nodes")
public List<CourseCategoryTreeDto> queryTreeNodes() {

    return courseCategoryService.queryTreeNodes("1");

}

分别在 httpclient 打开页面进行测试 ,ogay完成!

image-20230531112719410

功能三:添加课程

根据前边对内容管理模块的数据模型分析,课程相关的信息有:课程基本信息、课程营销信息、课程图片信息、课程计划、课程师资信息,所以新增一门课程需要完成这几部分信息的填写。

定义接口

新增课程信息的两个模型DTO在资料中提供了,只需复制到content model工程的 dto当中即可

注意关于价格这里用float接收,当参与计算时一般转成bigdecimal

定义接口

@ApiOperation("新增课程基础信息")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto addCourseDto){
    return null;
}

接口开发

mapper 不用再写了,因为MP生成的基本的增删已经满足需求了

现在直接写service

在 content service下编写service 以及实现类 (service 已经写了其实,增加新增的函数即可)

CourseBaseInfoDto createCourseBase(Long companyId,AddCourseDto addCourseDto);

这里 的 companyId主要用于后续单点登录,权限校验等,新增课程只能新增本机构的课程。

此外,还需要对传入的参数进行合法性校验。

假设对传入的参数一个一个的进行校验是这样写的(很麻烦)

@Override
  @Transactional
  public CourseBaseInfoDto createCourseBase(Long companyId, AddCourseDto dto) {

          //合法性校验
          if (StringUtils.isBlank(dto.getName())) {
              throw new RuntimeException("课程名称为空");
          }

          if (StringUtils.isBlank(dto.getMt())) {
              throw new RuntimeException("课程分类为空");
          }

          if (StringUtils.isBlank(dto.getSt())) {
              throw new RuntimeException("课程分类为空");
          }

          if (StringUtils.isBlank(dto.getGrade())) {
              throw new RuntimeException("课程等级为空");
          }

          if (StringUtils.isBlank(dto.getTeachmode())) {
              throw new RuntimeException("教育模式为空");
          }

          if (StringUtils.isBlank(dto.getUsers())) {
              throw new RuntimeException("适应人群为空");
          }

          if (StringUtils.isBlank(dto.getCharge())) {
              throw new RuntimeException("收费规则为空");
          }
          //新增对象
          CourseBase courseBaseNew = new CourseBase();
          //将填写的课程信息赋值给新增对象
          BeanUtils.copyProperties(dto,courseBaseNew);
          //设置审核状态
          courseBaseNew.setAuditStatus("202002");
          //设置发布状态
          courseBaseNew.setStatus("203001");
          //机构id
          courseBaseNew.setCompanyId(companyId);
          //添加时间
          courseBaseNew.setCreateDate(LocalDateTime.now());
          //插入课程基本信息表
          int insert = courseBaseMapper.insert(courseBaseNew);
          if(insert<=0){
              throw new RuntimeException("新增课程基本信息失败");
          }
  }

先这样写着,后续,我想将在controller 进行校验,controller将使用统一的校验框架进行自动校验。

这部分就是基本的CRUD我就不写了。

就是返回结果里面因该有大分类小分类的名字,传进去的只是代号,所以还需要去课程分类表中查询对应的名称。

测试接口

在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": "测试课程103",
  "pic": "",
  "teachmode": "200002",
  "users": "初级人员",
  "tags": "",
  "grade": "204001",
  "description": ""
 }

看看是否正常插入。

可以输入一些不合规的参数,看看校验功能是否正常

功能: 异常处理

(自定义异常以及统一异常处理)

我们上面校验的时候采用抛出运行时异常(throw new RuntimeException(“”))的方式报错,这种报错虽然可以返回给调用方(逐层传到controller层),但是,前端接收到只会发现500异常,并不会输出我们抛出的具体的异常信息。这样在开发中并不会使用。

为了能将特定的异常信息抛给前端,我们需要和前端做一些约定

1、错误提示信息统一以json格式返回给前端。

2、以HTTP状态码决定当前是否出错,非200为操作异常。

那么如何捕获异常并统一返回呢?

如果代码用try/catch方式去在所有的controller接口捕获的话比较臃肿

可以通过SpringMVC提供的控制器增强类统一由一个类去完成异常的捕获。

这样,项目的整体结构就是这样的。

image-20230531140357899

具体来说就是利用@ControllerAdvice,他就是我们说的控制器增强类,基于AOP(面向切面编程)的思想来增强控制器。

具体来说,为了能完成上面的约定,我们后端主要采用了三个比较核心的注解

1、@ControllerAdvice & @RestControllerAdvice

Spring3.2提供的新注解,从名字上可以看出大体意思是控制器增强, 在项目中来增强SpringMVC中的Controller,。通常和@ExceptionHandler 结合使用(@ControllerAdvice主要用于捕获异常@ExceptionHandler用来处理异常),来处理SpringMVC的异常信息

2、@ExceptionHandler(“异常类.class”)

Spring3.0提供的标识在方法上或类上的注解,用来表明方法的处理异常类型

3、@ResponseStatus(“异常错误信息”)

Spring3.0提供的标识在方法上或类上的注解,用状态代码和应返回的原因标记方法或异常类。

前两个更核心

注意:

(注意不能自己try和catch异常,否则就不会被全局异常处理捕获到)

统一异常处理的实现

这套异常处理流程应该是整体架构层面的事,各个微服务模块都能用到,所以,他应该放在base工程当中

定义异常返回结果

在base 工程 新建 一个 exception包,定义一个异常对象模型 (用于返回错误结果)

package com.xuecheng.base.exception;

public class RestErrorResponse implements Serializable {
    private String errMessage;

    public RestErrorResponse(String errMessage) {
        this.errMessage = errMessage;
    }

    public String getErrHessage() {
        return errMessage;
    }

    public void setErrHessage(String errMessage) {
        this.errMessage = errMessage;
    }
}

自定义异常以及一些常用异常

自定义异常(用于抛出异常)

public class XuechengPlusException extends RuntimeException{
    private String errMessage;

    public XuechengPlusException() {
    }

    public XuechengPlusException(String message) {
        super(message);
        this.errMessage=message;
    }
    public String getErrHessage() {
        return errMessage;
    }

    public void setErrHessage(String errMessage) {
        this.errMessage = errMessage;
    }

    // 为了方便抛出异常,定义一个静态方法,就是替换原来的 throw new 语句
    public static void cast(String message){
        throw new XuechengPlusException(message);
    }
    public static void cast(CommonError commonError){
        throw new XuechengPlusException(commonError.getErrMessage());
    }
    
}

利用枚举类再列举一些通用异常

public enum CommonError {

   UNKOWN_ERROR("执行过程异常,请重试。"),
   PARAMS_ERROR("非法参数"),
   OBJECT_NULL("对象为空"),
   QUERY_NULL("查询结果为空"),
   REQUEST_NULL("请求参数为空");

   private String errMessage;

   public String getErrMessage() {
      return errMessage;
   }

   private CommonError( String errMessage) {
      this.errMessage = errMessage;
   }

}

自定义异常处理器

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 针对自定义异常处理
    @ExceptionHandler(XuechengPlusException.class) // 截取并处理异常信息(截取什么养的异常)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)// 返回的状态码
    public RestErrorResponse customException(XuechengPlusException e){


        log.error("系统异常:{}",e.getErrMessage(),e);
        // 解析出
        String errorMessage = e.getErrMessage();
        RestErrorResponse errorResponse = new RestErrorResponse(errorMessage);
        return errorResponse;
    }

    // 针对 其他异常处理
    @ExceptionHandler(Exception.class) // 截取并处理异常信息(截取什么养的异常)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)// 返回的状态码
    public RestErrorResponse otherException(Exception e){
        // 记录
        log.error("系统异常:{}",e.getMessage(),e);
        // 解析出
        RestErrorResponse errorResponse = new RestErrorResponse(CommonError.UNKOWN_ERROR.getErrMessage());
        return errorResponse;
    }
}

重启尝试一下

注意之前抛出的异常修改一下,如

if(courseMarket.getPrice() == null || courseMarket.getPrice().floatValue()!=0){
    throw new XuechengPlusException("课程为免费 价格必须等于0");
}

/----------修改为------------->
if(courseMarket.getPrice() == null || courseMarket.getPrice().floatValue()!=0){
    XuechengPlusException.cast("课程为免费 价格必须等于0");
}    

成功显示错误信息!

image-20230531190350561

image-20230531190314338

改进:参数校验 JSR303

上述编写代码时,我们在services实现中对参数的合法性一一判断,

前端请求后端接口传输参数,是在controller中校验还是在Service中校验?答案是都需要校验,只是分工不同

Contoller中校验请求参数的合法性,包括:必填项校验,数据格式校验,比如:是否是符合一定的日期格式,等。

Service中要校验的是业务规则相关的内容,比如:课程已经审核通过所以提交失败

Service中根据业务规则去校验不方便写成通用代码,Controller中则可以将校验的代码写成通用代码。

早在JavaEE6规范中就定义了参数校验的规范,它就是JSR-303,它定义了Bean Validation,即对bean属性进行校验。

SpringBoot提供了JSR-303的支持,它就是spring-boot-starter-validation,

JSR303 校验使用方式

base 工程下 pom文件添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

使用方式:

1、在 controller接受的DTO类中添加 相关注解即可

image-20230531203930353

image-20230531194938895

2、在controller传入参数时激活

image-20230531200005852

测试。把原来service层写校验屏蔽了,然后重新启动服务

测试一个课程名为空的添加测试。

会发现虽然报错了,而且就是校验失败导致的错误,但是却没有抛出我们想要的异常信息,这是因为 验证失败异常抛出的异常MethodArgumentNotValidException 是不是我们的自定义异常不会提取异常信息。

所以我们再单独写一个提取 校验失败异常的异常处理方法

@ResponseBody
@ExceptionHandler(MethodArgumentNotValidException.class) // 截取并处理异常信息(截取什么养的异常)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)// 返回的状态码
public RestErrorResponse validateFailException(MethodArgumentNotValidException e){


    log.error("校验异常:{}",e.getMessage(),e);
    // 解析出
    String errorMessage = e.getMessage();
    RestErrorResponse errorResponse = new RestErrorResponse(errorMessage);
    return errorResponse;
}

再次测试,发现成功了,这种校验的好处是,能够一次把所有的这种校验错误发现出来

image-20230531201901780

分组校验

但是别高兴太早:

如果任何时候都是对 这个dto对象使用一套校验规则是否合理?

答案肯定不合理 ,如,新增课程与修改课程就应该不一样。

新增课程:需要更全的用户信息

修改课程:只需要特定的修改信息以及辅助信息。

怎么解决?

1、解决方案 : 定义不同的DTO 。。。。麻烦

2、分组校验:JSR 303 校验提供的

分组校验实现方式

定义分组

public class ValidationGroups {
 public interface Inster{};
 public interface Update{};
 public interface Delete{};

}

分组注释

+@NotEmpty(groups = {ValidationGroups.Inster.class},message = "添加课程名称不能为空")
+@NotEmpty(groups = {ValidationGroups.Update.class},message = "修改课程名称不能为空")
// @NotEmpty(message = "课程名称不能为空")
 @ApiModelProperty(value = "课程名称", required = true)
 private String name;

在调用段 指定校验的分组

@ApiOperation("新增课程基础信息")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody @Validated({ValidationGroups.Inster.class}) AddCourseDto addCourseDto){
    //机构id,由于认证系统没有上线暂时硬编码
    Long companyId = 1L;
  return courseBaseInfoService.createCourseBase(companyId,addCourseDto);
}

再次测试,由于这里指定了Insert分组,所以抛出 异常信息:添加课程名称不能为空。

如果修改分组为ValidationGroups.Update.class,异常信息为:修改课程名称不能为空。

功能四 :修改课程

修改相对于新增来说,多了一个课程id 因为修改课程需要针对某个课程进行修改。

你若修改,肯定是已经存在的课程,已经存在的课程必有一个id

所以编辑的第一步,就是根据id 查询课程的基本信息。

接口定义

查询课程

@ApiOperation("根据课程id查询课程基础信息")
@GetMapping("/course/{courseId}")
public CourseBaseInfoDto getCourseBaseById(@PathVariable Long courseId){
    return null;
}

修改课程

@ApiOperation("修改课程基础信息")
@PutMapping("/course")
public CourseBaseInfoDto modifyCourseBase(@RequestBody @Validated EditCourseDto editCourseDto){
    
}

接口开发

查询接口开发

查询课程的函数之前已经编写了,不用再写,只需将其提到接口上即可

完善接口

测试接口

修改课程开发

@Transactional
@Override
public CourseBaseInfoDto updateCourseBase(Long companyId, EditCourseDto dto) {

    //课程id
    Long courseId = dto.getId();
    CourseBase courseBase = courseBaseMapper.selectById(courseId);
    if(courseBase==null){
        XueChengPlusException.cast("课程不存在");
    }

    //校验本机构只能修改本机构的课程
    if(!courseBase.getCompanyId().equals(companyId)){
        XueChengPlusException.cast("本机构只能修改本机构的课程");
    }

    //封装基本信息的数据
    BeanUtils.copyProperties(dto,courseBase);
    courseBase.setChangeDate(LocalDateTime.now());

    //更新课程基本信息
    int i = courseBaseMapper.updateById(courseBase);

    //封装营销信息的数据
    CourseMarket courseMarket = new CourseMarket();
    BeanUtils.copyProperties(dto,courseMarket);
    saveCourseMarket(courseMarket);
    //查询课程信息
    CourseBaseInfoDto courseBaseInfo = this.getCourseBaseInfo(courseId);
    return courseBaseInfo;

}

完善接口

@ApiOperation("修改课程基础信息")
@PutMapping("/course")
public CourseBaseInfoDto modifyCourseBase(@RequestBody @Validated EditCourseDto editCourseDto){
    //机构id,由于认证系统没有上线暂时硬编码
    Long companyId = 1232141425L;
    return courseBaseInfoService.updateCourseBase(companyId,editCourseDto);
}

测试接口

### 修改课程
PUT {{content_host}}/content/course
Content-Type: application/json

{
  "id": 40,
  "name": "SpringBoot核心",
  "users": "Spring Boot初学者",
  "tags": "Spring项目的快速构建",
  "mt": "1-3",
  "st": "1-3-2",
  "grade": "200003",
  "teachmode": "201001",
  "description": "课程系统性地深度探讨 Spring Boot 核心特性,引导小伙伴对 Java 规范的重视,启发对技术原理性的思考,掌握排查问题的技能,以及学习阅读源码的方法和技巧,全面提升研发能力,进军架构师队伍。",
  "pic": "https://cdn.educba.com/academy/wp-content/uploads/2018/08/Spring-BOOT-Interview-questions.jpg",
  "charge": "201001",
  "price": 0.01
}

功能五:查询课程计划

从课程计划表teachplan 上可以看出整体上是 一个树型结构

每个课程计划都有所属课程。

第二级的parentid为第一级的id。

根据业务流程中的界面原型,课程计划列表展示时还有课程计划关联的视频信息。课程计划关联的视频信息在teachplan_media表

接口定义

和之前的课程分类表一样,查询课程计划也是一种属性结构的数据,需要自定义模型类

package com.xuecheng.content.model.dto;

@Data
@ToString
public class TeachplanDto extends Teachplan {

  //课程计划关联的媒资信息
  TeachplanMedia teachplanMedia;

  //子结点
  List<TeachplanDto> teachPlanTreeNodes;

}

接口定义

package com.xuecheng.content.api;


 @Api(value = "课程计划编辑接口",tags = "课程计划编辑接口")
 @RestController
public class TeachplanController {

    @ApiOperation("查询课程计划树形结构")
    @ApiImplicitParam(value = "courseId",name = "课程Id",required = true,dataType = "Long",paramType = "path")
    @GetMapping("/teachplan/{courseId}/tree-nodes")
    public List<TeachplanDto> getTreeNodes(@PathVariable Long courseId){
        return null;
    }

}

接口开发

由于是树状结构,需要查询对应课程的所计划,然后封装,所以 需要对mapper进行改造

编写mapper

定义mapper的方法

编写SQL语句

  • 一级分类和二级分类通过teachplan表的自连接进行,如果一级分类旗下没有二级分类,此时也需要显示一级分类,所以这里使用左连接,左边是一级分类,右边是二级分类
  • 同时课程的媒资信息teachplan_media也需要和teachplan左连接,左边是teachplan,右边是媒资信息teachplan_media
SELECT
	p.id p_id,
	p.pname p_pname,
	p.parentid p_parentid,
	p.grade p_grade,
	p.media_type p_mediaType,
	p.start_time p_stratTime,
	p.end_time p_endTime,
	p.orderby p_orderby,
	p.course_id p_courseId,
	p.course_pub_id p_coursePubId,
	c.id c_id,
	c.pname c_pname,
	c.parentid c_parentid,
	c.grade c_grade,
	c.media_type c_mediaType,
	c.start_time c_stratTime,
	c.end_time c_endTime,
	c.orderby c_orderby,
	c.course_id c_courseId,
	c.course_pub_id c_coursePubId,
	tm.media_fileName mediaFilename,
	tm.id teachplanMeidaId,
	tm.media_id mediaId 
FROM
	teachplan p
	LEFT JOIN teachplan c ON c.parentid = p.id
	LEFT JOIN teachplan_media tm ON tm.teachplan_id = c.id 
WHERE
	p.parentid = '0' AND p.course_id = #{value}
ORDER BY p.orderby, c.orderby

根据响应结果编写 mapper映射文件

相应结果

{
    "changeDate": null,              // 一级分类              
    "courseId": 74,                                           
    "cousePubId": null,                                           
    "createDate": null,                                           
    "endTime": null,                                              
    "grade": "2",                                             
    "isPreview": "0",                                             
    "mediaType": null,                                            
    "orderby": 1,                                             
    "parentid": 112,                                              
    "pname": "第1章基础知识",                                             
    "startTime": null,                                            
    "status": null,                                           
    "id": 113,                                            
    "teachPlanTreeNodes": [{         // 二级分类,其中可能存在多个       
        "changeDate": null,             
        "courseId": 74,
        "cousePubId": null,
        "createDate": null,
        "endTime": null,
        "grade": "3",
        "isPreview": "1",
        "mediaType": "001002",
        "orderby": 1,
        "parentid": 113,
        "pname": "第1节项目概述",
        "startTime": null,
        "status": null,
        "id": 115,
        "teachPlanTreeNodes": null,
        "teachplanMedia": {         // 媒资信息
            "courseId": 74,
            "coursePubId": null,
            "mediaFilename": "2.avi",
            "mediaId": 41,
            "teachplanId": 115,
            "id": null
        }
    }],
    "teachplanMedia": null
}

映射文件

<?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.TeachplanMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.xuecheng.content.model.po.Teachplan">
        <id column="id" property="id" />
        <result column="pname" property="pname" />
        <result column="parentid" property="parentid" />
        <result column="grade" property="grade" />
        <result column="media_type" property="mediaType" />
        <result column="start_time" property="startTime" />
        <result column="end_time" property="endTime" />
        <result column="description" property="description" />
        <result column="timelength" property="timelength" />
        <result column="orderby" property="orderby" />
        <result column="course_id" property="courseId" />
        <result column="course_pub_id" property="coursePubId" />
        <result column="status" property="status" />
        <result column="is_preview" property="isPreview" />
        <result column="create_date" property="createDate" />
        <result column="change_date" property="changeDate" />
    </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, pname, parentid, grade, media_type, start_time, end_time, description, timelength, orderby, course_id, course_pub_id, status, is_preview, create_date, change_date
    </sql>

    <resultMap id="selectTreeNodesMap" type="com.xuecheng.content.model.dto.TeachplanDto">

        <id column="one_id" property="id"></id>
        <result column="one_pname" property="pname"></result>
        <result column="one_parentid"     property="parentid" />
        <result column="one_grade"  property="grade" />
        <result column="one_mediaType"   property="mediaType" />
        <result column="one_stratTime"   property="startTime" />
        <result column="one_endTime"   property="endTime" />
        <result column="one_orderby"   property="orderby" />
        <result column="one_courseId"   property="courseId" />
        <result column="one_coursePubId"   property="coursePubId" />
        <collection property="teachPlanTreeNodes" ofType="com.xuecheng.content.model.dto.TeachplanDto">
            <id column="two_id" property="id"></id>
            <result column="two_pname" property="pname"></result>
            <result column="two_parentid"     property="parentid" />
            <result column="two_grade"  property="grade" />
            <result column="two_mediaType"   property="mediaType" />
            <result column="two_stratTime"   property="startTime" />
            <result column="two_endTime"   property="endTime" />
            <result column="two_orderby"   property="orderby" />
            <result column="two_courseId"   property="courseId" />
            <result column="two_coursePubId"   property="coursePubId" />
            <association property="teachplanMedia" javaType="com.xuecheng.content.model.po.TeachplanMedia">
                <id column="teachplanMeidaId" property="id"></id>
                <result column="mediaFilename" property="mediaFilename"></result>
                <result column="mediaId" property="mediaId"></result>
            </association>
        </collection>

    </resultMap>

    <select id="selectTreeNodes" parameterType="long" resultMap="selectTreeNodesMap">

        SELECT
            one.id one_id,
            one.pname one_pname,
            one.parentid one_parentid,
            one.grade one_grade,
            one.media_type one_mediaType,
            one.start_time one_stratTime,
            one.end_time one_endTime,
            one.orderby one_orderby,
            one.course_id one_courseId,
            one.course_pub_id one_coursePubId,
            two.id two_id,
            two.pname two_pname,
            two.parentid two_parentid,
            two.grade two_grade,
            two.media_type two_mediaType,
            two.start_time two_stratTime,
            two.end_time two_endTime,
            two.orderby two_orderby,
            two.course_id two_courseId,
            two.course_pub_id two_coursePubId,
            m.media_fileName mediaFilename,
            m.id teachplanMeidaId,
            m.media_id mediaId
        FROM
            teachplan one
                LEFT JOIN teachplan two ON two.parentid = one.id
                LEFT JOIN teachplan_media m ON m.teachplan_id = two.id
        WHERE  one.parentid='0' and one.course_id = #{value}
        ORDER BY
            one.orderby,
            two.orderby
    </select>

</mapper>

最后定义Service接口,ServiceImpl实现类,完善Controller层代码

public interface TeachplanService {
    List<TeachplanDto> findTeachplanTree(Long courseId);
}
@Slf4j
@Service
public class TeachplanServiceImpl implements TeachplanService {
    @Autowired
    private TeachplanMapper teachplanMapper;

    @Override
    public List<TeachplanDto> findTeachplanTree(Long courseId) {
        return teachplanMapper.selectTreeNodes(courseId);
    }
}
@Slf4j
@RestController
@Api(value = "课程计划编辑接口", tags = "课程计划编辑接口")
public class TeachplanController {
    @Autowired
    private TeachplanService teachplanService;
    @ApiOperation("查询课程计划树形结构")
    @GetMapping("/teachplan/{courseId}/tree-nodes")
    public List<TeachplanDto> getTreeNodes(@PathVariable Long courseId) {
        return teachplanService.findTeachplanTree(courseId);
    }
}

测试

### 根据课程id查询课程计划
GET {{content_host}}/content/teachplan/22/tree-nodes
Content-Type: application/json

成功显示计划信息。

功能六 :修改课程计划

包括新增章节,新增小节

点击“章”、“节”的名称,可以修改名称、选择是否免费。

image-20230601104309893

image-20230601104315047

新增课程计划

数据模型

POST {{content_host}}/content/teachplan
Content-Type: application/json

{
  "courseId" : 74,
  "parentid": 247,
  "grade" : 2,
  "pname" : "小节名称 [点击修改]"
}

定义SaveTeachplanDto

@Data
@ToString
public class SaveTeachplanDto {

 /***
  * 教学计划id
  */
 private Long id;

 /**
  * 课程计划名称
  */
 private String pname;

 /**
  * 课程计划父级Id
  */
 private Long parentid;

 /**
  * 层级,分为1、2、3级
  */
 private Integer grade;

 /**
  * 课程类型:1视频、2文档
  */
 private String mediaType;


 /**
  * 课程标识
  */
 private Long courseId;

 /**
  * 课程发布标识
  */
 private Long coursePubId;

 /**
  * 是否支持试学或预览(试看)
  */
 private String isPreview;


}

接口定义

@ApiOperation("课程计划创建或修改")
@PostMapping("/teachplan")
public void saveTeachplan( @RequestBody SaveTeachplanDto teachplan){

}

接口开发

mapper 以及满足要求了

现在写service

public void saveTeachplan(SaveTeachplanDto teachplanDto);

实现类

@Transactional
 @Override
 public void saveTeachplan(SaveTeachplanDto teachplanDto) {

  //课程计划id
  Long id = teachplanDto.getId();
  //修改课程计划
  if(id!=null){
    Teachplan teachplan = teachplanMapper.selectById(id);
   BeanUtils.copyProperties(teachplanDto,teachplan);
   teachplanMapper.updateById(teachplan);
  }else{
    //取出同父同级别的课程计划数量
   int count = getTeachplanCount(teachplanDto.getCourseId(), teachplanDto.getParentid());
   Teachplan teachplanNew = new Teachplan();
   //设置排序号
   teachplanNew.setOrderby(count+1);
   BeanUtils.copyProperties(teachplanDto,teachplanNew);

   teachplanMapper.insert(teachplanNew);

  }

 }
 /**
  * @description 获取最新的排序号
  * @param courseId  课程id
  * @param parentId  父课程计划id
  * @return int 最新排序号
  * @author Mr.M
  * @date 2022/9/9 13:43
 */
 private int getTeachplanCount(long courseId,long parentId){
  LambdaQueryWrapper<Teachplan> queryWrapper = new LambdaQueryWrapper<>();
  queryWrapper.eq(Teachplan::getCourseId,courseId);
  queryWrapper.eq(Teachplan::getParentid,parentId);
  Integer count = teachplanMapper.selectCount(queryWrapper);
  return count;
 }

完善接口

@ApiOperation("课程计划创建或修改")
@PostMapping("/teachplan")
public void saveTeachplan( @RequestBody SaveTeachplanDto teachplan){
    teachplanService.saveTeachplan(teachplan);
}

测试

上面在httpclinet都成功了,但是前后端联调时发现新增的第一级目录不能显示在列表中。

为什么?

原因查询的时候应该使用左外连接( 查询课程计划树状结构的那个 sql语句)

原来第一行是内连接,会导致没有子集的时候(只有一级目录时),不显示。

SELECT * FROM teachplan p
    LEFT JOIN teachplan c ON c.parentid = p.id
    LEFT JOIN teachplan_media tm ON tm.teachplan_id = c.id

内容管理模块实战

功能七:删除课程计划

删除结点
Request URL: /content/teachplan/246
Request Method: DELETE
如果失败:
{"errCode":"120409","errMessage":"课程计划信息还有子级信息,无法操作"}
如果成功:状态码200,不返回信息

接口定义

@ApiOperation("课程计划删除")
@DeleteMapping("teachplan/{teachplanId}")
public void deleteTeachplan( @PathVariable String teachplanId){
    
}

实现接口

servcie 定义

void deleteTeachplan(Long teachplanId);
@Override
public void deleteTeachplan(Long teachplanId) {
    if(teachplanId==null){
        XuechengPlusException.cast("删除课程id不能为空");
    }
    //查询是否有小节
    LambdaQueryWrapper<Teachplan> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.eq(Teachplan::getParentid,teachplanId);

    Integer subcount = teachplanMapper.selectCount(lambdaQueryWrapper);
    if (subcount>0){
        XuechengPlusException.cast("当前课程还有子课程未删除!");
    }else{
        // 课程计划下无小节,直接删除该课程计划和对应的媒资信息
        teachplanMapper.deleteById(teachplanId);
        // 条件构造器
        LambdaQueryWrapper<TeachplanMedia> mediaLambdaQueryWrapper = new LambdaQueryWrapper<>();
        // 删除媒资信息中对应teachplanId的数据
        mediaLambdaQueryWrapper.eq(TeachplanMedia::getTeachplanId, teachplanId);
        teachplanMediaMapper.delete(mediaLambdaQueryWrapper);
    }
}

完善

@ApiOperation("课程计划删除")
@DeleteMapping("/teachplan/{teachplanId}")
public void deleteTeachplan(@PathVariable Long teachplanId) {
    teachplanService.deleteTeachplan(teachplanId);
}

测试略

功能 八 课程计划排序

接口

Request URL: http://localhost:8601/api/content/teachplan/movedown/43
Request Method: POST
43为课程计划id

接口定义

@ApiOperation("课程计划排序")
@PostMapping("/teachplan/{moveType}/{teachplanId}")
public void orderByTeachplan(@PathVariable String moveType, @PathVariable Long teachplanId) {
   
}

实现接口

void orderByTeachplan(String moveType, Long teachplanId);
@Transactional
@Override
public void orderByTeachplan(String moveType, Long teachplanId) {
    Teachplan teachplan = teachplanMapper.selectById(teachplanId);
    Integer grade = teachplan.getGrade();
    Integer orderby = teachplan.getOrderby();
    // 章节移动需要与 同一课程下的 讲解交换顺序
    Long courseId = teachplan.getCourseId();
    // 交接移动需要 与同一章节下的 交换顺序
    Long parentid = teachplan.getParentid();
    if ("moveup".equals(moveType)) {
        if (grade == 1) {
            // 章节上移,找到上一个章节的orderby,然后与其交换orderby
            // SELECT * FROM teachplan WHERE courseId = 117 AND grade = 1  AND orderby < 1 ORDER BY orderby DESC LIMIT 1
            LambdaQueryWrapper<Teachplan> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(Teachplan::getGrade, 1)
                    .eq(Teachplan::getCourseId, courseId)
                    .lt(Teachplan::getOrderby, orderby)
                    .orderByDesc(Teachplan::getOrderby)
                    .last("LIMIT 1");
            Teachplan tmp = teachplanMapper.selectOne(queryWrapper);
            exchangeOrderby(teachplan, tmp);
        } else if (grade == 2) {
            // 小节上移
            // SELECT * FROM teachplan WHERE parentId = 268 AND orderby < 5 ORDER BY orderby DESC LIMIT 1
            LambdaQueryWrapper<Teachplan> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(Teachplan::getParentid, parentid)
                    .lt(Teachplan::getOrderby, orderby)
                    .orderByDesc(Teachplan::getOrderby)
                    .last("LIMIT 1");
            Teachplan tmp = teachplanMapper.selectOne(queryWrapper);
            exchangeOrderby(teachplan, tmp);
        }

    } else if ("movedown".equals(moveType)) {
        if (grade == 1) {
            // 章节下移
            // SELECT * FROM teachplan WHERE courseId = 117 AND grade = 1 AND orderby > 1 ORDER BY orderby ASC LIMIT 1
            LambdaQueryWrapper<Teachplan> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(Teachplan::getCourseId, courseId)
                    .eq(Teachplan::getGrade, grade)
                    .gt(Teachplan::getOrderby, orderby)
                    .orderByAsc(Teachplan::getOrderby)
                    .last("LIMIT 1");
            Teachplan tmp = teachplanMapper.selectOne(queryWrapper);
            exchangeOrderby(teachplan, tmp);
        } else if (grade == 2) {
            // 小节下移
            // SELECT * FROM teachplan WHERE parentId = 268 AND orderby > 1 ORDER BY orderby ASC LIMIT 1
            LambdaQueryWrapper<Teachplan> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(Teachplan::getParentid, parentid)
                    .gt(Teachplan::getOrderby, orderby)
                    .orderByAsc(Teachplan::getOrderby)
                    .last("LIMIT 1");
            Teachplan tmp = teachplanMapper.selectOne(queryWrapper);
            exchangeOrderby(teachplan, tmp);
        }
    }
}

/**
 * 交换两个Teachplan的orderby
 * @param teachplan
 * @param tmp
 */
private void exchangeOrderby(Teachplan teachplan, Teachplan tmp) {
    if (tmp == null)
        XuechengPlusException.cast("已经到头啦,不能再移啦");
    else {
        // 交换orderby,更新
        Integer orderby = teachplan.getOrderby();
        Integer tmpOrderby = tmp.getOrderby();
        teachplan.setOrderby(tmpOrderby);
        tmp.setOrderby(orderby);
        teachplanMapper.updateById(tmp);
        teachplanMapper.updateById(teachplan);
    }
}

/**
 * @description 获取最新的排序号
 * @param courseId  课程id
 * @param parentId  父课程计划id
 * @return int 最新排序号
 * @author Mr.M
 * @date 2022/9/9 13:43
 */
private int getTeachplanCount(long courseId,long parentId){
    LambdaQueryWrapper<Teachplan> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(Teachplan::getCourseId,courseId);
    queryWrapper.eq(Teachplan::getParentid,parentId);
    Integer count = teachplanMapper.selectCount(queryWrapper);
    return count;
}

完善 controller

@ApiOperation("课程计划排序")
@PostMapping("/teachplan/{moveType}/{teachplanId}")
public void orderByTeachplan(@PathVariable String moveType, @PathVariable Long teachplanId) {
    teachplanService.orderByTeachplan(moveType, teachplanId);
}

测试略

功能九 师资管理

定义接口

查找 新增 修改 删除

@Slf4j
@RestController
@Api(value = "教师信息相关接口", tags = "教师信息相关接口")
public class CourseTeacherController {
    @Autowired
    private CourseTeacherService courseTeacherService;

    @ApiOperation("查询教师信息接口")
    @GetMapping("/courseTeacher/list/{courseId}")
    public List<CourseTeacher> getCourseTeacherList(@PathVariable Long courseId) {
        return courseTeacherService.getCourseTeacherList(courseId);
    }

    @ApiOperation("添加/修改教师信息接口")
    @PostMapping("/courseTeacher")
    public CourseTeacher saveCourseTeacher(@RequestBody CourseTeacher courseTeacher) {
        return courseTeacherService.saveCourseTeacher(courseTeacher);
    }

    @ApiOperation("删除教师信息接口")
    @DeleteMapping("/courseTeacher/course/{courseId}/{teacherId}")
    public void deleteCourseTeacher(@PathVariable Long courseId, @PathVariable Long teacherId) {
		courseTeacherService.deleteCourseTeacher(courseId,teacherId)
    }
    
}

实现接口

public interface CourseTeacherService {
    List<CourseTeacher> getCourseTeacherList(Long courseId);

    CourseTeacher saveCourseTeacher(CourseTeacher courseTeacher);

    void deleteCourseTeacher(Long courseId, Long teacherId);
    
}

功能十 删除课程

接口定义

delete /course/87
87为课程id

请求参数:课程id

响应:状态码200,不返回信息
@ApiOperation("删除课程")
@DeleteMapping("/course/{courseId}")
public void deleteCourse(@PathVariable Long courseId) {
    Long companyId = 1232141425L;
    courseBaseInfoService.delectCourse(companyId,courseId);
}

接口实现

@Transactional
@Override
public void delectCourse(Long companyId, Long courseId) {
    CourseBase courseBase = courseBaseMapper.selectById(courseId);
    if (!companyId.equals(courseBase.getCompanyId()))
        XueChengPlusException.cast("只允许删除本机构的课程");
    // 删除课程教师信息
    LambdaQueryWrapper<CourseTeacher> teacherLambdaQueryWrapper = new LambdaQueryWrapper<>();
    teacherLambdaQueryWrapper.eq(CourseTeacher::getCourseId, courseId);
    courseTeacherMapper.delete(teacherLambdaQueryWrapper);
    // 删除课程计划
    LambdaQueryWrapper<Teachplan> teachplanLambdaQueryWrapper = new LambdaQueryWrapper<>();
    teachplanLambdaQueryWrapper.eq(Teachplan::getCourseId, courseId);
    teachplanMapper.delete(teachplanLambdaQueryWrapper);
    // 删除营销信息
    courseMarketMapper.deleteById(courseId);
    // 删除课程基本信息
    courseBaseMapper.deleteById(courseId);
}

测试 略

ok !结束!