类MOOC系统开发1-内容管理模块开发
创建内容管理工程
是上一节中,我们讲到了后端工程结构,各个微服务工程依赖于基础(base)工程
下面我们来做第一个微服务工程 “内容管理工程” (content)
其中这个内容管理工程下面也可以再进行细分
流程分为前端、接口层、业务层三部分,所以模块工程结构如下图所示
- xuecheng-plus-content-api:接口工程,为前端提供接口
 - xuecheng-plus-content-service:业务工程,为接口工程提供业务支撑
 - xuecheng-plus-content-model:数据模型工程,存储数据模型类、数据传输类型等
 
创建内容管理父工程
现在开始创建内容管理工程,注意基于根目录创建xuecheng-plus-content
创建完成,只保留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数据模型工程。
创建完成,只保留包和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>
创建完毕后,项目的基础目录如下:
内容管理功能实现
功能一:课程查询
建库建表
创建了数据库(这时内容管理的数据库,一般一个微服务就对应一个数据库即微服务之间的分库分表)xc_content
导入第一节课中sql脚本,创建好数据库表
生成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");
        ...
运行完毕后生成如下目录
在该包下自动生成了内容管理模块的controller、mapper、po及service相关代码,这里我们只需要po类。
将po类拷贝到model工程
打开一个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;
}
我们发现此模型类中定义了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
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工程对应目录 ,如下图:
测试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
编写启动类/测试类:
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文件
输入测试案例:
POST http://localhost:63040/content/course/list?pageNo=1&pageSize=2
Content-Type: application/json
{
  "auditStatus": "202004",
  "courseName": "java",
  "publishStatus": "203001"
}
为了方便将来和网关集成测试,这里我们把测试主机地址在配置文件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 用变量代替
前后端联调
准备环境
安装nodejs
把课程前端工程project-xczx2-portal-vue-ts.zip解压放到项目平级目录用idea打开
一些小知识点
package.json相当于前端工程的pom文件(依赖文件)
右键点击project-xczx2-portal-vue-ts目录下的package.json文件
点击Show npm Scripts打开npm窗口
点击“Edit ‘serve’” setting,下边对启动项目的一些参数进行配置,选择nodejs、npm。
右键点击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字段,给服务器说明,这是一个跨域请求
- Origin: http://localhost:8601
 
 - 服务端根据设定的规则,判断是否允许跨域如果允许,则会在响应头中添加 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函数的方式告诉请求方。如下图:
2、添加响应头
服务端在响应头添加 Access-Control-Allow-Origin:*
参考以上博文,我将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去访问跨域地址。
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 服务, 可以看到 查询功能生效
功能二:课程分类查询
课程分类表存储在存储在内容管理数据库中,需要单独查询
此外,比较棘手的是,课程分类信息信息有一定的层次结构,如何从表中提取层次结构是实现接口的关键
这是一种典型的树形结构
定义接口
受限分析接口
请求时 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 相当于整体表名
查询结果如下
通过这种方法就找到了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完成!
功能三:添加课程
根据前边对内容管理模块的数据模型分析,课程相关的信息有:课程基本信息、课程营销信息、课程图片信息、课程计划、课程师资信息,所以新增一门课程需要完成这几部分信息的填写。
定义接口
新增课程信息的两个模型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提供的控制器增强类统一由一个类去完成异常的捕获。
这样,项目的整体结构就是这样的。
具体来说就是利用@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");
}    
成功显示错误信息!
改进:参数校验 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类中添加 相关注解即可
2、在controller传入参数时激活
测试。把原来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;
}
再次测试,发现成功了,这种校验的好处是,能够一次把所有的这种校验错误发现出来
分组校验
但是别高兴太早:
如果任何时候都是对 这个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
成功显示计划信息。
功能六 :修改课程计划
包括新增章节,新增小节
点击“章”、“节”的名称,可以修改名称、选择是否免费。
新增课程计划
数据模型
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 !结束!






































