类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 !结束!