商品分类查询接口

本节我们继续开发商品分类剩下的查询接口,包括了后台管理的分类分页查询列表和门户端的分类查询。话不多说,开干!

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

还要在生成器配置中设置dateLibrarycustom

image.png

生成的CategoryInfoDTO我们将用作两个用途,一个是给后台管理返回查询的分页列表,还有就是给门户端查询分类展示嵌套结构,因此这里我们多定义了一个List类型的children字段,注意下定义形式:

CategoryInfo:
description: 分类信息DTO类
type: object
properties:
...
children:
description: 子分类列表
type: array
items:
$ref: '#/components/schemas/CategoryInfo'

修改returnTypes模板

image.png

{{#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生成器源码。

image.png

这样就可以在api.mustache中按需导入了:

{{#needImportPageResultDTO}}
import com.xiaojuan.boot.common.dto.PageResultDTO;
{{/needImportPageResultDTO}}

最后生成的是我们想要的代码:

image.png

image.png

开发分页查询接口

接下来我们将开发分页查询的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的方式,调用下:

image.png

相应的日志输出:

image.png

开发门户端分类查询

完成了后台管理的分类分页查询,现在我们再回到门户网站的分类展示功能。我们将开发一个以嵌套结构展示的分类查询功能。

先来定义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接口后,我们发现命名为:

image.png

这种命名规则其实是通过我们在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名称):

image.png

而对于多个标签的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接口的命名,就划分更合理了:

image.png

api分组

现在我们调整下controller的包和类:

image.png

这里我们将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就会比较清晰:

image.png

开发接口

这次我们按照从调用层到接口定义的方向来做,然后用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无需用户登录,直接调用:

image.png

本节我们边开发分类查询接口,边对swagger生成器进行进一步修改源码的完善。在实现门户端的查询时我们采用了递归方式,每次递归都要按照父级id查询子列表。下一节我们将来实践spring缓存,对从数据库加载的分类信息进行缓存,大家加油!