前面的章节我们花了大力气整好了项目开发的基础框架,并完成了商品分类模块的开发。相信小伙伴了已经能胜任独自开发一个新的业务模块了。本小节开始小卷将带小伙伴一起来开发商品模块的接口,首先我们来定义web层 API,开干!
前面我们已经学习了通过swagger生成器的方式来生成controller的API接口,而不用自己再手动编码定义。看下商品模块相关的openApi3的文档定义吧。
商品保存接口
商品在进行后台的新增和更新操作时,我们将实现一些组件的复用,这里我们的服用方案是DTO对象的复用,而API的定义以及controller方法和后台service逻辑都是分开的。因为如果API实现为共用,使用validation框架进行参数校验时无法使用分组校验,且后台service逻辑混杂在一起并不好维护。因此我们仅采用DTO的共用,API接口分开,且可以实现参数的分组校验。
API定义
看下这块在openApi3文档的定义:
yaml
openapi: 3.0.1
...
paths:
...
/mall-api/admin/products:
post:
tags:
- productAdmin
summary: 商品新增
description: 新增一条商品记录
operationId: addProduct
x-group: Add
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/ProductSave'
responses:
200:
description: 新增成功
put:
tags:
- productAdmin
summary: 商品更新
description: 更新一条商品记录
operationId: updateProduct
x-group: Update
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/ProductSave'
responses:
200:
description: 更新成功
components:
schemas:
ProductSave:
description: 商品保存DTO类
type: object
x-groups:
- Add
- Update
properties:
id:
description: 商品id
type: integer
format: int64
x-validation: "@NotNull(message = MSG_PRODUCT_ID_REQUIRED, groups = Update.class)"
categoryId:
description: 三级分类id
type: integer
format: int64
x-validation: "@NotNull(message = MSG_CATEGORY_ID_REQUIRED, groups = Add.class)"
name:
description: 商品名称
type: string
x-validation: "@NotBlank(message = MSG_PRODUCT_NAME_REQUIRED, groups = Add.class)"
imgFile:
description: 图片文件
type: multipartFile
detail:
description: 商品详情
type: string
x-validation: "@Size(min = 50, max = 500, message = MSG_PRODUCT_DETAIL_RANGE)"
price:
description: 价格
type: integer
x-validation: "@NotNull(message = MSG_PRODUCT_PRICE_REQUIRED, groups = Add.class) @Min(value = 1, message = MSG_PRODUCT_PRICE_MIN)"
stock:
description: 库存
type: integer
x-validation: "@NotNull(message = MSG_PRODUCT_STOCK_REQUIRED, groups = Add.class) @Max(value = 10000, message = MSG_PRODUCT_STOCK_MAX)"
配置说明
这里针对新增商品和更新商品的两个请求API我们采用了标准的restful api的规范,对资源的不同操作映射到不同的请求方法。
注意这里请求内容的类型为
multipart/form-data
,也就是说,前端是以formData
的形式来提交的,可以同时将业务字段和文件类型的字段一并提交,而不用先异步上传文件再提交表单,我们杜绝这种做法。后台用一个ProductSaveDTO
来接收前端提交的可以包含文件域的formData
格式的数据,这样就要求swagger生成器能帮我们生成MultipartFile
类型的字段,而默认这是不支持的,我们需要修改swagger生成器的源码。
扩展swagger生成器
修改swagger-codegen-generators-1.0.39
源码工程,增加新的文件上传的类型和映射:
然后我们通过扩展openApi文档定义的方式,增加对分组校验的支持,我们在DTO的schema定义中增加了一个扩展的属性x-groups
,用来定义校验分组的列表,比如这里我们对商品进行新增和更新涉及这两个分组,对于公共的字段校验不用指定分组,而为特定于一个API的字段校验指定分组,比如更新时商品id
不能为空,看下x-validation
中指定的分组,分组的class
来自于x-groups
中的定义。不要忘了在API定义中我们同样要用扩展属性x-group
来应用哪个组别的校验。这些都是我们扩展的定义,而swagger生成器默认是不支持的,还是要我们修改源码。修改swagger-codegen-generators-1.0.39
的源码:
在处理模型的扩展属性时,我们加一个布尔类型的x-has-group
的扩展变量。这样我们只要在项目中的pojo.mustache
模板的最后加上这样的内容即可渲染出用于分组的类内部接口定义:
而要在api文件中生成的表单或者json body对应的DTO上应用校验分组注解定义,我们需要继续修改生成器源码:
针对API参数的代码生成逻辑,我们基于API操作对象codegenOperation
中是否能获取x-group
扩展属性的判断,为参数生成模型设置扩展属性x-has-validate-group
和新加的一个validateGroup
字段,前者用于判断是否对DTO参数启用分组校验,后者指定组名。还需要修改CodegenParamter
类源码:
源码改完后在本地install后在项目中更新依赖,最后调整项目中form参数和body参数对应的mustache
模板文件:
注意,form参数模板要在isFormObj
判断内调整,因为我们只关心接收类型为form的DTO对象,为其应用分组校验。
注意
swagger分组校验的在线文档并不会区分分组,都会被应用,这是一个比较坑的地方。
删除商品(批量)
这里我们要支持传入多个要删除的商品id数组:
yaml
/mall-api/admin/products:
...
delete:
tags:
- productAdmin
summary: 商品删除
description: 按照一个或多个id删除商品
operationId: delete
parameters:
- name: ids
in: query
schema:
type: array
items:
type: integer
format: int64
x-validation: "@NotEmpty(message = MSG_PRODUCT_IDS_REQUIRED)"
responses:
200:
description: 删除成功
生成的参数类型为List<Long>
,这里我们将生成@NotEmpty
注解对其进行校验,前端的提交形式:
http
### 商品删除
DELETE http://localhost:8080/mall-api/admin/products
Content-Type: application/x-www-form-urlencoded
ids=1&ids=2&ids=3
商品批量上下架
对于该接口,我们前端会传多个商品id以及上架或下架的操作类型字段oprType
,为了校验传入的操作类型值为我们后台期望的范围,这里我们将增加一个枚举值范围校验的自定义注解,注解可以参考以前的MyPattern
来定义,其区别的地方:
java
package com.xiaojuan.boot.common.validation.annotation;
import ...
@Constraint(validatedBy = InValidator.class)
...
public @interface In {
int[] value();
...
}
校验器实现逻辑很简单:
java
package com.xiaojuan.boot.common.validation.validator;
import ...
public class InValidator implements ConstraintValidator<In, Integer> {
private int[] arr;
@Override
public void initialize(In inAnnotation) {
arr = inAnnotation.value();
}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
if (value == null) return true;
return ArrayUtils.contains(arr, value);
}
}
现在我们来定义API:
yaml
/mall-api/admin/product/updateSellStatus:
post:
tags:
- productAdmin
summary: 商品上下架
description: 批量对商品上架或下架处理
operationId: batchUpdateSellStatus
parameters:
- name: ids
in: query
schema:
type: array
items:
type: integer
format: int64
x-validation: "@NotEmpty(message = MSG_PRODUCT_IDS_REQUIRED)"
- name: oprType
in: query
schema:
type: integer
x-validation: "@NotNull(message = MSG_PRODUCT_UPDATE_SELL_STATUS_OPR_TYPE_REQUIRED) @In(value = {0, 1}, message = MSG_PRODUCT_UPDATE_SELL_STATUS_OPR_TYPE_ILLEGAL)"
responses:
200:
description: 操作成功
这里我们对oprType
字段采用了双重校验。通过自定义注解@In
来校验操作类型必须要在指定的一组值中。如果传入为空,@In
注解的校验实现逻辑中将直接返回true,即校验通过,这样只会应用非空检查。
重新执行generateAPI
任务后启动服务,进行测试:
后台商品分页检索
该API只开放给后台管理员进行商品查询的,因此我们希望检索功能做的更细化些:
- 支持按照商品名称的模糊搜索
- 支持传入任意一个层级的分类检索
- 支持按上下架状态检索
- 按照更新时间倒序排列
API定义:
yaml
openapi: 3.0.1
...
paths:
...
/mall-api/admin/products:
get:
tags:
- productAdmin
summary: 商品检索
description: 按照条件查询商品分页列表
operationId: findProducts
requestBody:
content:
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/ProductBackQuery'
x-pageResult: true
responses:
200:
description: 查询成功
content:
application/json:
schema:
$ref: '#/components/schemas/ProductBackInfo'
...
components:
schemas:
...
ProductBackQuery:
description: 商品后台查询DTO
type: object
allOf:
- $ref: '#/components/schemas/PageQuery'
properties:
keyword:
description: 商品名称关键字
type: string
categoryId:
description: 分类id
type: integer
format: int64
status:
description: 上下架状态
type: integer
ProductBackInfo:
description: 商品后台信息DTO
type: object
properties:
id:
description: 商品id
type: integer
format: int64
categoryIds:
description: 分类id列表
type: array
items:
type: integer
format: int64
name:
description: 商品名称
type: string
imgUrl:
description: 商品图片url
type: string
detail:
description: 商品详情
type: string
price:
description: 商品价格(单位:分)
type: integer
stock:
description: 库存
type: integer
status:
description: 上下架状态
type: integer
createTime:
description: 创建时间
type: string
format: date
updateTime:
description: 更新时间
type: string
format: date
...
前台商品检索
这里我们给前端用户提供:
- 按商品名称模糊检索
- 按三级分类检索
- 排序方式:默认排序、价格递增、上架日期倒序
- 分页展示
API定义:
yaml
openapi: 3.0.1
...
paths:
...
/mall-api/portal/products:
get:
tags:
- productPortal
summary: 前台检索商品列表
description: 前台检索商品列表
operationId: findProducts
requestBody:
content:
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/ProductQuery'
x-pageResult: true
responses:
200:
description: 查询成功
content:
application/json:
schema:
$ref: '#/components/schemas/ProductInfo'
...
components:
schemas:
...
ProductQuery:
description: 商品前台查询DTO
type: object
allOf:
- $ref: '#/components/schemas/PageQuery'
properties:
keyword:
description: 商品名称关键字
type: string
categoryId:
description: 三级分类id
type: integer
format: int64
sortType:
description: 排序类型:1-价格升序 2-上架日期倒序
type: integer
...
ProductInfo:
description: 商品前台信息DTO
type: object
properties:
id:
description: 商品id
type: integer
format: int64
name:
description: 商品名称
type: string
imgUrl:
description: 商品图片url
type: string
price:
description: 商品价格(单位:分)
type: integer
...
前台商品详情
yaml
openapi: 3.0.1
...
paths:
...
/mall-api/portal/product-detail/{id}:
get:
tags:
- productPortal
summary: 查看商品详情
description: 按照id获取商品详情
operationId: getProduct
parameters:
- name: id
in: path
description: 商品id
schema:
type: integer
format: int64
responses:
200:
description: 查询成功
content:
application/json:
schema:
$ref: '#/components/schemas/ProductDetail'
...
components:
schemas:
...
ProductDetail:
description: 商品前台详情DTO
type: object
properties:
id:
description: 商品id
type: integer
format: int64
name:
description: 商品名称
type: string
categoryNames:
description: 多层级分类名称列表
type: array
items:
type: integer
format: int64
imgUrl:
description: 商品图片url
type: string
detail:
description: 商品详情
type: string
price:
description: 商品价格(单位:分)
type: integer
stock:
description: 库存
type: integer
...
按大类查最新6款商品
在商品门户的首页我们希望查找每个分类下的最新上架的6款商品,定义如下api:
yaml
/mall-api/portal/product-latest/{categoryId}:
get:
tags:
- productPortal
summary: 查找最新商品
description: 按照大类查找最新上架6款商品
operationId: getLatestProducts
parameters:
- name: categoryId
in: path
description: 大类id
schema:
type: integer
format: int64
responses:
200:
description: 查询成功
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ProductInfo'
以上就是本节的主要内容,我们对商品模块的API接口通过openApi3的形式进行了定义,并借助swagger生成器帮我们生成了相应的Java代码,然后我们开发相应的Controller
组件来实现这些接口即可。下一小节,我们将把主要精力放在API接口的实现上,大家加油!