本节我们继续开发商品分类剩下的查询接口,包括了后台管理的分类分页查询列表和门户端的分类查询。话不多说,开干!
定义openApi3文档
现在我们来定义分类的分页查询API,看下在mall.yml中新增的内容:
openapi: 3.0.1
...
paths:
...
/mall-api/admin/categories:
get:
tags:
- category
summary: 查询分类列表
description: 分页查询分类列表
operationId: listCategories
requestBody:
content:
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/CategoryCondition'
x-pageResult: true
responses:
200:
description: 查询成功
content:
application/json:
schema:
$ref: '#/components/schemas/CategoryInfo'
...
...
components:
schemas:
...
PageQuery:
description: 分页基类DTO
type: object
properties:
pageNo:
description: 当前页
type: integer
pageSize:
description: 每页显示记录数
type: integer
CategoryCondition:
description: 分类分页查询条件DTO类
type: object
allOf:
- $ref: '#/components/schemas/PageQuery'
properties:
name:
description: 分类名称(模糊匹配)
type: string
CategoryInfo:
description: 分类信息DTO类
type: object
properties:
id:
description: 分类id
type: integer
format: int64
pid:
description: 父级分类id
type: integer
format: int64
name:
description: 分类名称
type: string
level:
description: 分类层级
type: integer
orderNum:
description: 排序
type: integer
createTime:
description: 创建时间
type: string
format: date
updateTime:
description: 更新时间
type: string
format: date
children:
description: 子分类列表
type: array
items:
$ref: '#/components/schemas/CategoryInfo'
...
注意,分页查询条件DTO,我们用openApi3文档规范中提供的allOf
来实现对schema
的重用,生成的类则会实现继承。
而返回类型将作为我们包装的一个分页结果PageResultDTO
的泛型类型,因此这里也采用了扩展定义:x-pageResult: true
。
看下我们对分页信息类的设计:
package com.xiaojuan.boot.common.dto;
import ...
@Data
@AllArgsConstructor
public class PageResultDTO {
private Long total;
private List data;
}
这里还涉及到java.util.Date
日期类型的字段,注意我们这里的字段声明形式:
createTime:
type: string
format: date
还要在生成器配置中设置dateLibrary
为custom
:
生成的CategoryInfoDTO
我们将用作两个用途,一个是给后台管理返回查询的分页列表,还有就是给门户端查询分类展示嵌套结构,因此这里我们多定义了一个List
类型的children
字段,注意下定义形式:
CategoryInfo:
description: 分类信息DTO类
type: object
properties:
...
children:
description: 子分类列表
type: array
items:
$ref: '#/components/schemas/CategoryInfo'
修改returnTypes模板
{{#returnContainer}}{{#isMapContainer}}Map{{/isMapContainer}}{{#isListContainer}}List{{/isListContainer}}{{/returnContainer}}{{^returnContainer}}{{#vendorExtensions.x-pageResult}}PageResultDTO{{/vendorExtensions.x-pageResult}}{{^vendorExtensions.x-pageResult}}{{{returnType}}}{{/vendorExtensions.x-pageResult}}{{/returnContainer}}
这里我们对扩展的定义进行了判断:
{{#vendorExtensions.x-pageResult}}PageResultDTO{{/vendorExtensions.x-pageResult}}{{^vendorExtensions.x-pageResult}}{{{returnType}}}{{/vendorExtensions.x-pageResult}}
修改生成器源码
为了在API接口头部按需导入PageResultDTO
类型,我们还需要修改swagger生成器源码。
这样就可以在api.mustache中按需导入了:
{{#needImportPageResultDTO}}
import com.xiaojuan.boot.common.dto.PageResultDTO;
{{/needImportPageResultDTO}}
最后生成的是我们想要的代码:
开发分页查询接口
接下来我们将开发分页查询的mapper接口,因为之前我们已经完成了mybatis与spring boot的整合,现在我们只专注相关模块的开发流程。
因为涉及到动态查询条件的传入,我们将通过xml的mapper查询语句块的形式来实现动态查询条件,我们先新建一个category-mapper.xml
:
select t.id, t.pid, t.name, t.level, t.order_num, t.create_time, t.update_time
from TB_CATEGORY t
t.NAME like concat('%', #{name}, '%')
对应的mapper接口:
package com.xiaojuan.boot.dao.mapper;
import ...
public interface CustomCategoryMapper {
List listCategories(CategoryConditionDTO conditionDTO);
}
Service接口:
package com.xiaojuan.boot.service;
import ...
public interface CategoryService {
...
PageResultDTO listCategoriesByPage(CategoryConditionDTO conditionDTO);
}
注意service接口的方法名要尽量起的见名知意。
Service接口实现:
package com.xiaojuan.boot.service.impl;
import ...
@RequiredArgsConstructor
@Service
public class CategoryServiceImpl implements CategoryService {
...
private final CustomCategoryMapper customCategoryMapper;
...
@Override
public PageResultDTO listCategoriesByPage(CategoryConditionDTO conditionDTO) {
PageHelper.startPage(conditionDTO.getPageNo(), conditionDTO.getPageSize());
List categories = customCategoryMapper.listCategories(conditionDTO);
PageInfo pageInfo = new PageInfo(categories);
return new PageResultDTO(pageInfo.getTotal(), pageInfo.getList());
}
}
分页API前面我们已经实践过,这里派上了用场。
Controller组件调用service则更简单了:
package com.xiaojuan.boot.web.controller.admin;
import ...
@RequiredArgsConstructor
@RestController
public class CategoryController implements CategoryApi {
private final CategoryService categoryService;
...
@Override
public PageResultDTO listCategories(CategoryConditionDTO body) {
return categoryService.listCategoriesByPage(body);
}
}
最后的测试我们用http client的方式,调用下:
相应的日志输出:
开发门户端分类查询
完成了后台管理的分类分页查询,现在我们再回到门户网站的分类展示功能。我们将开发一个以嵌套结构展示的分类查询功能。
先来定义api:
/mall-api/portal/category/listTree:
get:
summary: 门户端查询分类树接口
description: 查询分类嵌套结构
operationId: listCategories
responses:
200:
description: 查询成功
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/CategoryInfo'
当我们生成API接口后,我们发现命名为:
这种命名规则其实是通过我们在openApi3中设置的tag来生成的。
tag定义与api接口名
当我们这样定义tag时:
...
paths:
/mall-api/admin/users:
get:
tags:
- admin
- user
summary: 用户列表查询接口
...
/mall-api/user/profile:
get:
tags:
- user
summary: 用户信息查询接口
...
/mall-api/user/admin/login:
post:
tags:
- user
summary: 管理员登录接口
...
/mall-api/user/login:
post:
tags:
- user
summary: 普通用户登录接口
...
/mall-api/user/signature:
post:
tags:
- user
summary: 更新个性签名接口
...
/mall-api/user/logout:
post:
tags:
- user
summary: 退出登录接口
...
/mall-api/user/register:
post:
tags:
- user
summary: 用户注册接口
...
/mall-api/admin/categories:
get:
tags:
- admin
- category
summary: 查询分类列表
...
post:
tags:
- admin
- category
summary: 新增分类
...
put:
tags:
- admin
- category
summary: 更新分类
...
/mall-api/admin/categories/{id}:
delete:
tags:
- admin
- category
summary: 删除分类
...
/mall-api/portal/category/listTree:
get:
tags:
- portal
- category
summary: 门户端查询分类树接口
...
components:
schemas:
...
这里我们只是粗略的把接口分成了后台管理、门户和用户认证相关三大类(这里是以第一个tag
来生成API名称):
而对于多个标签的api接口,我们更希望能在命名API接口时都体现出来,对接口进行细粒度的划分,因此我们可以这样定义组合的标签:
...
paths:
/mall-api/admin/users:
get:
tags:
- userAdmin
summary: 用户列表查询接口
...
...
/mall-api/admin/categories:
get:
tags:
- categoryAdmin
summary: 查询分类列表
...
post:
tags:
- categoryAdmin
summary: 新增分类
...
put:
tags:
- categoryAdmin
summary: 更新分类
...
/mall-api/admin/categories/{id}:
delete:
tags:
- categoryAdmin
summary: 删除分类
...
/mall-api/portal/category/listTree:
get:
tags:
- categoryPortal
summary: 门户端查询分类树接口
...
components:
schemas:
...
我们为多标签生成一个组合标签名。
这样再看生成的API接口的命名,就划分更合理了:
api分组
现在我们调整下controller的包和类:
这里我们将Rest API分三组,对应的swagger分组配置进行调整:
springdoc:
group-configs:
- group: '用户认证模块'
packagesToScan: com.xiaojuan.boot.web.controller.user
pathsToMatch: /**
- group: '管理模块'
packagesToScan: com.xiaojuan.boot.web.controller.admin
pathsToMatch: /**
- group: '门户模块'
packagesToScan: com.xiaojuan.boot.web.controller.portal
pathsToMatch: /**
这样我们看一个模块的API就会比较清晰:
开发接口
这次我们按照从调用层到接口定义的方向来做,然后用idea帮助我们根据修复建议自动生成接口定义。
controller调用:
package com.xiaojuan.boot.web.controller.portal;
import ...
@RequiredArgsConstructor
@RestController
public class CategoryPortalController implements CategoryPortalApi {
private final CategoryService categoryService;
@Override
public List listCategories() {
return categoryService.listCategoryTree();
}
}
生成service接口:
package com.xiaojuan.boot.service;
import ...
public interface CategoryService {
...
List listCategoryTree();
}
完成service接口实现:
package com.xiaojuan.boot.service.impl;
import ...
@RequiredArgsConstructor
@Service
public class CategoryServiceImpl implements CategoryService {
...
private final CustomCategoryMapper customCategoryMapper;
...
@Override
public List listCategoryTree() {
List children = new ArrayList();
loadSubcategories(0, children);
return children;
}
private void loadSubcategories(long pid, List children) {
CategoryConditionDTO conditionDTO = new CategoryConditionDTO();
conditionDTO.setPid(pid);
List subCategories = customCategoryMapper.listCategories(conditionDTO);
if (!CollectionUtils.isEmpty(subCategories)) {
for (CategoryInfoDTO subCategory : subCategories) {
children.add(subCategory);
if (subCategory.getChildren() == null) {
subCategory.setChildren(new ArrayList());
}
loadSubcategories(subCategory.getId(), subCategory.getChildren());
}
}
}
}
这里我们采用了递归的调用方式从pid
为0的分类开始依次加载其子分类列表。而mapper层我们并没有新开发接口,而是重用了之前为分页提供的接口。只不过我们要为查询条件DTO额外增加一个pid
的字段,在openApi3文档中定义即可,并在category-mapper.xml
的中新增基于pid
查询的动态条件定义。这里代码就省略了。
最后通过swagger测试,该API无需用户登录,直接调用:
本节我们边开发分类查询接口,边对swagger生成器进行进一步修改源码的完善。在实现门户端的查询时我们采用了递归方式,每次递归都要按照父级id查询子列表。下一节我们将来实践spring缓存,对从数据库加载的分类信息进行缓存,大家加油!