目录
实验内容
- 搭建Spring Boot后台,使用
Mapper
,Service
,Controller
三层架构。 - 使用
Mybatis-Plus
访问数据库。 - 必须用到
Mybatis-Plus
自带的增删改查功能,还有分页查询功能。 - 查询不能直接写
SQL
语句,必须用到条件构造器(QueryWrapper
或LambdaQueryWrapper
)。 - 必须用到参数校验。
- 根据接口文档实现处理请求的方法。
前言
实验内容的前三个和上一个实验:搭建Spring Boot+MyBatis-Plus后台一样,具体如何创建SpringBoot
项目、添加依赖和创建Mapper
、Service
和Controller
,还有添加分页和配置类就不详细说明了,还有数据库连接参数的设置。
不一样的地方就是实体类由Person
换成了Article
,大差不差的。
一、添加新的依赖
跟上次实验不一样的是,添加了新的依赖,首先是lombok
用于自动生成Getter
和Setter
方法
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
然后是参数校验用的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
二、配置连接MySQL数据库
修改了src/main/resources
文件夹下的application.properties
文件的扩展名为层级分明的.yml
或.yaml
文件,添加/修改内容为如下配置。
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver # 连接操作MySQL数据库的类
url: jdbc:mysql://localhost:3306/exp2 # 最后为数据库的名字、再前一个是端口
username: root # 数据库用户名
password: 123456 # 数据库密码
接下来就是在数据库中创建后面会用到的表,根据接口文档中获取文章详情的响应数据样例中返回的文章data
内容可以推断出article
表所拥有的字段,顺便再创建一个数据库,所生成的SQL如下。
DROP DATABASE IF EXISTS exp2;
CREATE DATABASE exp2 CHARACTER SET utf8;
USE exp2;
CREATE TABLE article (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
cover_img VARCHAR(255),
state VARCHAR(50),
category_id INT,
create_date DATETIME DEFAULT CURRENT_TIMESTAMP,
update_date DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) CHARSET=utf8;
三、创建实体类以及Mapper、Service和Controller三层架构
POJO
实体类Article
相对于上次实验手动一个一个地添加Getter
和Setter
方法,这次就用了lombok
来简化代码,让其自动生成,另外使用了@JsonFormat
注解规定了Date
在转换成Json
后的格式。。
package org.peanut.exp2.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
@Data
public class Article {
@TableId(type = IdType.AUTO)
private Integer id;
private String title;
private String content;
private String coverImg;
private String state;
private Integer categoryId;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createDate;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateDate;
}
Mapper
package org.peanut.exp2.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.peanut.exp2.pojo.Article;
public interface ArticleMapper extends BaseMapper<Article> {
}
Service
IService
package org.peanut.exp2.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.peanut.exp2.pojo.Article;
import java.util.List;
public interface IArticleService extends IService<Article> {
}
ServiceImpl
package org.peanut.exp2.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.peanut.exp2.mapper.ArticleMapper;
import org.peanut.exp2.pojo.Article;
import org.peanut.exp2.service.IArticleService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article>
implements IArticleService {
}
Controller
package org.peanut.exp2.controller;
import org.peanut.exp2.service.IArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
public class ArticleController {
@Autowired
IArticleService articleService;
}
四、添加配置类、响应类和全局异常处理类
配置类主要是Mybatis-Plus
的,用于添加它的分页插件以及相关配置,如Mapper
扫描配置。
package org.peanut.exp2.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("org.peanut.exp2.mapper") // 记得改成自己Mapper所在的路径
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
然后根据接口文档的响应数据样例,可以发现返回的内容有很高的相似性,为此创建了响应类Result<T>
,而且用到了泛型,为了提高通用性,因为里面的数据类型是多样的,以及创建了快速生成成功或失败响应的静态方法,添加的@Getter
注解是为了SpringBoot
在响应时能够正常地生成Json
格式的内容。
package org.peanut.exp2.common;
import lombok.Getter;
@Getter
public class Result<T> {
Integer code;
String message;
T data;
public Result(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public static <T> Result<T> success(T data) {
return new Result<>(0, "操作成功", data);
}
public static <T> Result<T> error(String message) {
return new Result<>(1, message, null);
}
}
为了让接口的调用者能够知道SpringBoot
发出的所有异常,并通过Result
中的message
来响应,所以创建了GlobalExceptionHandler
类。
package org.peanut.exp2.handler;
import org.peanut.exp2.common.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result<Void> exception(Exception e) {
e.printStackTrace();
return Result.error(e.getMessage());
}
}
五、根据接口文档编写控制器方法并测试接口
1.新增文章接口
1.1 基本信息
接口描述:该接口用于新增文章(发布文章)
请求路径:/article
请求方式: POST
1.2 请求参数
请求参数格式:application/json
请求参数说明:
参数名称 | 说明 | 类型 | 是否必须 | 备注 |
---|---|---|---|---|
title | 文章标题 | string | 是 | 1~10个非空字符 |
content | 文章正文 | string | 是 | - |
coverImg | 封面图像地址 | string | 是 | 必须是url地址 |
state | 发布状态 | string | 是 | 已发布 | 草稿 |
categoryId | 文章分类ID | number | 是 | - |
请求参数样例:
{
"title": "陕西旅游攻略",
"content": "兵马俑,华清池,法门寺,华山...爱去哪去哪...",
"coverImg": "https://big-event-gwd.oss-cn-asdbeijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
"state": "草稿",
"categoryId": 2
}
1.3 响应数据
响应数据类型:application/json
响应参数说明:
名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
---|---|---|---|---|---|
code | number | 必须 | 响应码, 0-成功,1-失败 | ||
message | string | 非必须 | 提示信息 | ||
data | object | 非必须 | 返回的数据 |
响应数据样例:
{
"code": 0,
"message": "操作成功",
"data": null
}
处理请求的方法
根据接口文档可以得知请求路径为/article
,且请求方法为POST
,那么用的就是@PostMapping
注解,然后根据请求格式application/json
以及请求样例,可以确定传的参数为实体类,所以参数使用了@RequestBody
注解,只是在数据库创建一条数据,所以直接用MyBatis-Plus
写好的save
方法就行。最后根据响应样例,判断是否返回数据,然后返回相应处理结果的响应。
@PostMapping("/article")
public Result<Void> addArticle(@RequestBody Article article) {
judgeArticleValid(article);
if(articleService.save(article)) {
return Result.success(null);
}
return Result.error("操作失败");
}
而调用的judgeArticleValid
方法是用来校验传过来的实体类参数,根据文档的参数说明进行校验,否则抛出带有message
的校验异常。
private void judgeArticleValid(Article article) {
if(article.getTitle().isEmpty() || article.getTitle().length() > 10 || article.getTitle().isBlank()) {
throw new ValidationException("文章标题不允许为空或只有空字符,且长度需要在1~10内!");
}
if (article.getContent() == null) {
throw new ValidationException("文章正文不允许为空!");
}
if (article.getCoverImg() == null
|| !article.getCoverImg().matches("^(https?|ftp)://[\\w.-]+(?:/[\\w.-]*)*")) {
throw new ValidationException("封面图像地址不允许为空,且必须为URL地址!");
}
if (article.getState() == null || !article.getState().matches("已发布|草稿")) {
throw new ValidationException("发布状态不能为空,且只能是“已发布”或“草稿”!");
}
if(article.getCategoryId() == null) {
throw new ValidationException("文章分类ID不允许为空!");
}
}
测试接口
然后就是使用Postman
进行接口测试,首先是成功请求的效果,如下图所示,选择POST
方法,填入请求链接并添加JSON
的参数。
其他就再演示一个使用正则校验的封面图像地址校验效果,如下图所示。
2.文章列表(条件分页)接口
1.1 基本信息
接口描述:该接口用于根据条件查询文章,带分页
请求路径:/article
请求方式: GET
1.2 请求参数
请求参数格式:queryString
请求参数说明:
参数名称 | 说明 | 类型 | 是否必须 | 备注 |
---|---|---|---|---|
pageNum | 当前页码 | number | 是 | |
pageSize | 每页条数 | number | 是 | |
categoryId | 文章分类ID | number | 否 | |
state | 发布状态 | string | 否 | 已发布|草稿 |
请求参数样例:
?pageNum=1&pageSize=3&categoryId=2&state=草稿
1.3 响应数据
响应数据类型:application/json
响应参数说明:
名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
---|---|---|---|---|---|
code | number | 必须 | 响应码, 0-成功,1-失败 | ||
message | string | 非必须 | 提示信息 | ||
data | object | 必须 | 返回的数据 | ||
|-total | number | 必须 | 总记录数 | ||
|-items | array | 必须 | 数据列表 | ||
|-id | number | 非必须 | 主键ID | ||
|-title | string | 非必须 | 文章标题 | ||
|-content | string | 非必须 | 文章正文 | ||
|-coverImg | string | 非必须 | 文章封面图像地址 | ||
|-state | string | 非必须 | 发布状态 | 已发布 | 草稿 | |
|-categoryId | number | 非必须 | 文章分类ID | ||
|-createTime | string | 非必须 | 创建时间 | ||
|-updateTime | string | 非必须 | 更新时间 |
响应数据样例:
{
"code": 0,
"message": "操作成功",
"data": {
"total": 1,
"items": [
{
"id": 5,
"title": "陕西旅游攻略",
"content": "兵马俑,华清池,法门寺,华山...爱去哪去哪...",
"coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
"state": "草稿",
"categoryId": 2,
"createTime": "2025-06-01 11:55:30",
"updateTime": "2025-06-01 11:55:30"
}
]
}
}
处理请求的方法
请求路径一样为/article
,且请求方法为GET
,那么用的就是@GetMapping
注解,然后根据请求格式queryString
以及请求样例,可以确定需要传的参数,并且参数使用需要用@RequestParam
注解,且非必要的参数设置required
为false
。其中用到了@Pattern
注解来对参数进行校验,用了这个就得在控制类前添加@Validated
注解。最后根据响应样例,判断是否返回数据,然后返回相应处理结果的响应。
@GetMapping("/article")
public Result<ArticleList> getArticleList(
@RequestParam Integer pageNum,
@RequestParam Integer pageSize,
@RequestParam(required = false) Integer categoryId,
@RequestParam(required = false) @Pattern(regexp = "已发布|草稿", message = "发布状态只能是“已发布”或“草稿”") String state) {
Page<Article> page = new Page<>(pageNum, pageSize);
ArticleList al = new ArticleList(articleService.getArticleList(page, categoryId, state));
return Result.success(al);
}
其中不仅到了分页,还有自定义条件查询,为此在IArticleService
接口创建了一个带条件的分页查询的抽象方法,来获取满足条件的文章列表。
List<Article> getArticleList(Page<Article> page, Integer categoryId, String state);
然后是在ArticleServiceImpl
实现这个新添加的方法,里面用到了条件构造器LambdaQueryWrapper
来进行条件查询,只有在条件参数不为空才作为其中一个过滤条件。
@Override
public List<Article> getArticleList(Page<Article> page, Integer categoryId, String state) {
LambdaQueryWrapper<Article> lqw = new LambdaQueryWrapper<>();
// 当条件不为空才添加到生成的 SQL 中
lqw.eq(categoryId != null, Article::getCategoryId, categoryId);
lqw.eq(state != null, Article::getState, state);
return baseMapper.selectList(page, lqw);
}
根据响应数据样例,返回的数据不能只是普通的List,还多了一个属性,为此创建了一个新类ArticleList来作为响应的data类型。
package org.peanut.exp2.pojo;
import lombok.Data;
import java.util.List;
@Data
public class ArticleList {
int total;
List<Article> items;
public ArticleList(List<Article> list) {
this.total = list.size();
this.items = list;
}
}
测试接口
接下来就是测试这个接口了,首先是成功请求的效果,选择POST方法,填入请求链接并添加查询参数。
然后是缺少查询参数的效果。
2.获取文章详情接口
1.1 基本信息
接口描述:该接口用于根据ID获取文章详细信息
请求路径:/article/detail
请求方式: GET
1.2 请求参数
请求参数格式:queryString
请求参数说明:
参数名称 | 说明 | 类型 | 是否必须 | 备注 |
---|---|---|---|---|
id | 主键ID | number | 是 |
请求参数样例:
?id=1
1.3 响应数据
响应数据类型:application/json
响应参数说明:
名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
---|---|---|---|---|---|
code | number | 必须 | 响应码, 0-成功,1-失败 | ||
message | string | 非必须 | 提示信息 | ||
data | object | 必须 | 返回的数据 | ||
|-id | number | 非必须 | 主键ID | ||
|-title | string | 非必须 | 文章标题 | ||
|-content | string | 非必须 | 文章正文 | ||
|-coverImg | string | 非必须 | 文章封面图像地址 | ||
|-state | string | 非必须 | 发布状态 | 已发布 | 草稿 | |
|-categoryId | number | 非必须 | 文章分类ID | ||
|-createTime | string | 非必须 | 创建时间 | ||
|-updateTime | string | 非必须 | 更新时间 |
{
"code": 0,
"message": "操作成功",
"data": {
"id": 4,
"title": "北京旅游攻略",
"content": "天安门,颐和园,鸟巢,长城...爱去哪去哪...",
"coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
"state": "已发布",
"categoryId": 2,
"createTime": "2025-06-01 11:57:30",
"updateTime": "2025-06-01 11:57:30"
}
}
处理请求的方法
请求路径为/article/detail
,且请求方法也为GET
,那么用的就是@GetMapping
注解,然后根据请求格式queryString
以及请求样例,可以确定需要传的参数,并且为参数添加@RequestParam
注解。根据响应样例确定data
的数据类型。
@GetMapping("/article/detail")
public Result<Article> getArticle(@RequestParam Integer id) {
return Result.success(articleService.getById(id));
}
测试接口
测试查询成功效果
无论是否查询到对应的数据,都会是成功的,除非是没有传入查询参数。
4.更新文章接口
1.1 基本信息
接口描述:该接口用于更新文章信息
请求路径:/article
请求方式: PUT
1.2 请求参数
请求参数格式:application/json
请求参数说明:
参数名称 | 说明 | 类型 | 是否必须 | 备注 |
---|---|---|---|---|
id | 主键ID | number | 是 | |
title | 文章标题 | string | 是 | |
content | 文章正文 | string | 是 | |
coverImg | 封面图像地址 | string | 是 | |
state | 发布状态 | string | 是 | 已发布 | 草稿 |
categoryId | 文章分类ID | number | 是 |
请求参数样例:
{
"id": 4,
"title": "北京旅游攻略",
"content": "天安门,颐和园,鸟巢,长城...爱去哪去哪...",
"coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
"state": "已发布",
"categoryId": 2
}
1.3 响应数据
响应数据类型:application/json
响应参数说明:
名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
---|---|---|---|---|---|
code | number | 必须 | 响应码, 0-成功,1-失败 | ||
message | string | 非必须 | 提示信息 | ||
data | object | 非必须 | 返回的数据 |
响应数据样例:
{
"code": 0,
"message": "操作成功",
"data": null
}
处理请求的方法
请求路径为/article
,且请求方法为PUT
,那么用的就是@PutMapping
注解,根据请求格式以及请求样例,可以确定传的参数为实体类,虽然参数说明中没有明确地对参数进行内容限制,不过应该还是要和新建文章的限制保持一致,所以也调用了judgeArticleValid
进行参数校验,另外还需要校验id是否为空,因为是靠id
来更新数据库内容的。
@PutMapping("/article")
public Result<Void> updateArticle(@RequestBody Article article) {
if (article.getId() == null) {
throw new ValidationException("文章id不允许为空!");
}
judgeArticleValid(article);
if(articleService.updateById(article)) {
return Result.success(null);
}
return Result.error("操作失败");
}
测试接口
更新文章成功效果。
缺少文章id
参数的校验效果。
5.删除文章接口
1.1 基本信息
接口描述:该接口用于根据ID
删除文章
请求路径:/article
请求方式: DELETE
1.2 请求参数
请求参数格式:queryString
请求参数说明:
参数名称 | 说明 | 类型 | 是否必须 | 备注 |
---|---|---|---|---|
id | 主键ID | number | 是 |
请求参数样例:
?id=1
1.3 响应数据
响应数据类型:application/json
名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
---|---|---|---|---|---|
code | number | 必须 | 响应码, 0-成功,1-失败 | ||
message | string | 非必须 | 提示信息 | ||
data | object | 非必须 | 返回的数据 |
响应数据样例:
{
"code": 0,
"message": "操作成功",
"data": null
}
处理请求的方法
请求路径为/article
,且请求方法也为DELETE
,那么用的就是@DeleteMapping
注解,然后根据请求格式queryString
以及请求样例,可以确定需要传的参数,并且为参数添加@RequestParam
注解。
@DeleteMapping("/article")
public Result<Void> deleteArticle(@RequestParam Integer id) {
if (articleService.removeById(id)) {
return Result.success(null);
}
return Result.error("操作失败");
}
测试接口
删除文章成功效果。
当对应id
的文章不存在时,会响应失败。
缺少id
参数也是不会成功的。
总结
项目文件结构如下图所示。
有新增内容的代码总览。
IArticleService
package org.peanut.exp2.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import org.peanut.exp2.pojo.Article;
import java.util.List;
public interface IArticleService extends IService<Article> {
List<Article> getArticleList(Page<Article> page, Integer categoryId, String state);
}
ArticleServiceImpl
package org.peanut.exp2.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.peanut.exp2.mapper.ArticleMapper;
import org.peanut.exp2.pojo.Article;
import org.peanut.exp2.service.IArticleService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article>
implements IArticleService {
@Override
public List<Article> getArticleList(Page<Article> page, Integer categoryId, String state) {
LambdaQueryWrapper<Article> lqw = new LambdaQueryWrapper<>();
// 当条件不为空才添加到生成的 SQL 中
lqw.eq(categoryId != null, Article::getCategoryId, categoryId);
lqw.eq(state != null, Article::getState, state);
return baseMapper.selectList(page, lqw);
}
}
ArticleController
package org.peanut.exp2.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.validation.ValidationException;
import jakarta.validation.constraints.Pattern;
import org.peanut.exp2.pojo.Article;
import org.peanut.exp2.common.Result;
import org.peanut.exp2.pojo.ArticleList;
import org.peanut.exp2.service.IArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@Validated
public class ArticleController {
@Autowired
IArticleService articleService;
@PostMapping("/article")
public Result<Void> addArticle(@RequestBody Article article) {
judgeArticleValid(article);
if(articleService.save(article)) {
return Result.success(null);
}
return Result.error("操作失败");
}
@GetMapping("/article")
public Result<ArticleList> getArticleList(
@RequestParam Integer pageNum,
@RequestParam Integer pageSize,
@RequestParam(required = false) Integer categoryId,
@RequestParam(required = false) @Pattern(regexp = "已发布|草稿", message = "发布状态只能是“已发布”或“草稿”") String state) {
Page<Article> page = new Page<>(pageNum, pageSize);
ArticleList al = new ArticleList(articleService.getArticleList(page, categoryId, state));
return Result.success(al);
}
@GetMapping("/article/detail")
public Result<Article> getArticle(@RequestParam Integer id) {
return Result.success(articleService.getById(id));
}
@PutMapping("/article")
public Result<Void> updateArticle(@RequestBody Article article) {
if (article.getId() == null) {
throw new ValidationException("文章id不允许为空!");
}
judgeArticleValid(article);
if(articleService.updateById(article)) {
return Result.success(null);
}
return Result.error("操作失败");
}
@DeleteMapping("/article")
public Result<Void> deleteArticle(@RequestParam Integer id) {
if (articleService.removeById(id)) {
return Result.success(null);
}
return Result.error("操作失败");
}
private void judgeArticleValid(Article article) {
if(article.getTitle().isEmpty() || article.getTitle().length() > 10 || article.getTitle().isBlank()) {
throw new ValidationException("文章标题不允许为空或只有空字符,且长度需要在1~10内!");
}
if (article.getContent() == null) {
throw new ValidationException("文章正文不允许为空!");
}
if (article.getCoverImg() == null
|| !article.getCoverImg().matches("^(https?|ftp)://[\\w.-]+(?:/[\\w.-]*)*")) {
throw new ValidationException("封面图像地址不允许为空,且必须为URL地址!");
}
if (article.getState() == null || !article.getState().matches("已发布|草稿")) {
throw new ValidationException("发布状态不能为空,且只能是“已发布”或“草稿”!");
}
if(article.getCategoryId() == null) {
throw new ValidationException("文章分类ID不允许为空!");
}
}
}
参考
官方文档
安装 | MyBatis-Plus
分页插件 | MyBatis-Plus
条件构造器 | MyBatis-Plus