写接口文档时,我把分页、排序、筛选参数声明复用了,结果每个接口都长得差不多,那差异化配置到底加在哪里
这就是一个典型的“接口文档模板化过度”问题。核心矛盾在于:你复用的是参数的声明(Declaration),但每个接口真正需要的,是参数的约束(Constraint)。
一个 /users 接口和一个 /orders 接口,都可以接收 sort 参数,但 /users 只允许按 created_at, name 排序,而 /orders 允许按 amount, order_time 排序。如果你在文档里只写“本接口支持 sort 参数,用法见通用分页排序章节”,那这个文档等于没写,调用方根本不知道该传什么值。
所以,差异化配置不是“加在哪里”的问题,而是你从一开始就应该把接口契约分成两层:结构层(复用)与约束层(差异)。
下面我直接用 OpenAPI 3.1 来演示这套思路。即使你用的是其他文档工具,这套分层逻辑也是通用的。
第一层:结构层复用,定义“参数长什么样”
结构层解决的是参数名称、类型、基本格式的问题。比如,全公司所有的排序参数都叫 sort,都是 array 类型,格式都是用逗号分隔的字段名,降序用 - 前缀。那么,你可以在 OpenAPI 的 components/parameters 里定义一个可复用的参数模板:
components:
parameters:
SortParam:
name: sort
in: query
description: 排序条件,降序字段前加 "-"
schema:
type: array
items:
type: string
default: []
style: form
explode: false
同样,分页参数 page 和 pageSize 也可以抽象出来:
PageParam:
name: page
in: query
schema:
type: integer
minimum: 1
default: 1
PageSizeParam:
name: pageSize
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
到此为止,你复用的只是“骨架”。真正的业务规则,一个都没写。
第二层:约束层配置,定义“这个接口能做什么”
这是差异化配置落地的关键。每个接口在自己的 parameters 列表里,通过 $ref 引用通用参数,然后立即用 description 或其他扩展字段,把该接口允许的取值范围、默认行为、业务规则写死。
1. 排序约束:明确可排序字段白名单
对于 /users 接口,它的排序参数引用 SortParam,但必须在 description 里清晰列出允许的字段,并给出枚举值供文档渲染工具使用。
paths:
/users:
get:
summary: 获取用户列表
parameters:
- $ref: '#/components/parameters/PageParam'
- $ref: '#/components/parameters/PageSizeParam'
- name: sort
in: query
description: |
排序条件。
允许字段: `name`, `created_at`, `age`。
降序示例: `-created_at`。
schema:
type: array
items:
type: string
enum: [name, -name, created_at, -created_at, age, -age]
style: form
explode: false
注意,这里我没有直接用 $ref 引用 SortParam,而是重新声明了一个同名的 sort 参数。这在 OpenAPI 里是合法的,并且会覆盖全局定义。这样做的好处是,我可以精确控制 enum 字段,让文档工具(如 Swagger UI)自动生成下拉列表,调用方不用猜。
如果你一定要坚持复用 SortParam 的引用,那就在 description 里用醒目的标记写清楚白名单,并在后端代码里做校验。但我的经验是,在文档层面直接给出 enum,是成本最低、效果最好的约束手段。
2. 筛选约束:定义过滤键的枚举
筛选参数通常更复杂,比如 filter 是一个对象。我建议不要用一个万能的 filter 对象到处复用,而是用 x- 扩展字段或独立的 schema 来定义每个资源的过滤键。
/orders:
get:
summary: 获取订单列表
parameters:
- name: status
in: query
description: 订单状态筛选
schema:
type: string
enum: [pending, paid, shipped, cancelled]
- name: amount_min
in: query
description: 最低金额(单位:分)
schema:
type: integer
minimum: 0
- name: amount_max
in: query
description: 最高金额(单位:分)
schema:
type: integer
minimum: 0
这里完全没有复用通用的 filter 参数,而是把每个过滤条件都拆成独立的查询参数。为什么?因为每个资源的过滤维度完全不同。/orders 有金额范围,/users 可能有注册日期范围、VIP 等级。强行用一个通用的 filter 对象去套,只会让文档变得模糊不清。
如果你团队有严格的规范,要求必须用一个 filter 参数,那么差异化就体现在 schema 上:为每个接口定义一个独立的 XxxFilter schema,不要共享。
3. 分页约束:覆盖默认值
即使是分页这种看起来最“通用”的参数,也可能有差异。比如,大部分接口的 pageSize 最大是 100,但导出接口可能允许到 1000。这时,直接覆盖 maximum 约束:
/orders/export:
get:
parameters:
- $ref: '#/components/parameters/PageParam'
- name: pageSize
in: query
description: 每页条数,最大 1000
schema:
type: integer
minimum: 1
maximum: 1000
default: 100
第三层:响应体的差异化
很多人只盯着请求参数,忽略了响应体也需要差异化。一个分页列表的响应结构可以复用,但 items 里的数据模型必须严格区分。
定义一个通用的分页响应外壳:
components:
schemas:
PaginatedResponse:
type: object
properties:
code:
type: integer
message:
type: string
data:
type: object
properties:
items:
type: array
items: {} # 留空,由具体接口覆盖
total:
type: integer
page:
type: integer
pageSize:
type: integer
在具体接口里,用 allOf 合并并覆盖 items 的类型:
/users:
get:
responses:
'200':
description: 成功
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/PaginatedResponse'
- type: object
properties:
data:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/User'
这样,通用结构(code, message, total)只定义一次,但 items 的具体类型在每个接口里是明确的、强约束的。
如果你的文档工具不支持 OpenAPI 这种精细控制
很多国内团队用的是 YApi、ShowDoc、或者直接在 Confluence 上写 Markdown。工具限制了你的表达力,但分层思想依然适用。
做法是:在通用参数模板旁边,加一个“接口约束”区块。
比如,你的 Confluence 模板可以这样设计:
通用排序参数说明(链接到公共章节)
本接口使用公司统一的排序参数
sort,格式为逗号分隔的字段名,降序用-前缀。本接口专属约束
允许排序字段 说明 name按用户名排序 created_at按注册时间排序 -created_at按注册时间倒序(最新注册在前) 不允许按
age排序,即使数据库有此字段。
这样,你复用的是“sort 参数的格式说明”这个链接,但每个接口文档里都有一块自己的“专属约束”表格。调用方看文档时,第一眼看到的就是这个表格,而不是去翻通用章节。
常见问题
我们团队要求所有接口的排序参数必须严格复用同一个组件定义,不能在每个接口里写死 enum,怎么办?
那就把约束逻辑后移到后端代码和集成测试里,但在文档里用 description 和示例(example)来弥补。在全局 SortParam 的 description 里,写一句“具体允许的排序字段,请参见各接口的详细说明”。然后,在每个接口的说明区域(比如 OpenAPI 的 operation.description 或 summary),用醒目的 markdown 表格列出白名单。同时,在 example 字段里只给出该接口合法的示例值。Swagger UI 会优先展示 example,调用方复制粘贴时不容易出错。
我们在用 gRPC 或 GraphQL,这套分层思想还适用吗?
完全适用,只是换了个形式。在 gRPC 里,你可以把通用的分页、排序消息定义在一个 common.proto 里。但每个接口的请求消息里,不要直接引用一个万能的 QueryParams,而是定义自己需要的字段,并在字段注释里写清楚约束。GraphQL 更直接,每个 type 的查询参数(arguments)本来就是独立的,你只需要在 schema 的注释里写清楚每个参数的可选值即可。本质上,都是“结构复用,约束独立”。