商品分类查询接口
本节我们继续开发商品分类剩下的查询接口,包括了后台管理的分类分页查询列表和门户端的分类查询。话不多说,开干!
定义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缓存,对从数据库加载的分类信息进行缓存,大家加油!