用 OpenAPI 驱动 Postman Native Git

1. 为什么我不想再手工维护 Postman

Postman collection 手工维护到最后,最麻烦的不是点几下 UI,而是你不知道哪一份才是真的。

代码里有一份路由,docs/openapi.json 里有一份接口契约,Postman 里又有一份 collection。接口改过几轮以后,这三份东西很容易开始互相打架。路径少一个前缀、body 字段改了名字、认证方式忘了更新,这些问题单独看都不大,但排查时很烦。

我这次想收掉的是这个问题:Postman 只能是生成物,不能再当第二个接口源头。

最后落下来的链路是:

1
2
3
4
5
6
7
Gin runtime routes
-> OpenAPI generator
-> docs/openapi.json
-> openapi-to-postmanv2
-> postman collection migrate
-> Native Git collection directory
-> postprocess script

日常入口也只保留两个:

1
2
make postman-collection
make postman-sync

前一个只在本地生成。后一个才推到 Postman Cloud。

1.1 这篇不是 Postman 教程

这篇文章不讲 Postman 每个按钮怎么点。我真正关心的是三个边界:

第一,接口源头只能有一个。这个项目里是 docs/openapi.json

第二,本地生成和云端同步必须分开。生成 collection 不应该依赖 Postman 登录状态,也不应该因为网络问题影响 OpenAPI。

第三,所有项目定制逻辑都要写进脚本。比如 Bearer Token、登录后保存 token、删除 examples,不要靠 UI 手工改。

这三个边界收住以后,Postman 才不会变成一个看起来方便、实际不可 review 的黑盒。

2. Makefile 先把副作用分开

Makefile 里相关的 target 很短:

1
2
3
4
5
6
7
8
9
openapi:
go run ./tool/openapi -root .

postman-collection: openapi
@find postman .postman -name .DS_Store -delete 2>/dev/null || true
bash script/generate_postman_collection.sh

postman-sync: postman-collection
postman workspace push -y

这里最重要的不是命令本身,而是依赖方向。

postman-collection 依赖 openapi,所以每次生成 collection 前都会刷新 docs/openapi.json。这能避免一个很隐蔽的问题:你以为自己在调最新接口,其实 Postman 是拿旧 spec 生成的。

postman-sync 依赖 postman-collection,但 openapi 不依赖 postman-sync。这点我觉得必须坚持。postman workspace push -y 有外部副作用,它依赖账号、网络、workspace 权限,也会影响团队在云端看到的资源。它不应该藏在普通生成命令里。

本地生成就是本地生成。云端同步必须显式执行。

3. 主脚本只做一件事:从 OpenAPI 生成 Native Git 目录

script/generate_postman_collection.sh 的输入和输出很清楚。

输入是:

1
2
docs/openapi.json
.postman/openapi-to-postman-options.json

输出是:

1
postman/collections/CurveTool Backend API

脚本里这些变量都可以用环境变量覆盖:

1
2
3
4
OPENAPI_SPEC="${OPENAPI_SPEC:-$ROOT_DIR/docs/openapi.json}"
POSTMAN_COLLECTION_DIR="${POSTMAN_COLLECTION_DIR:-$ROOT_DIR/postman/collections/CurveTool Backend API}"
CONVERTER_PACKAGE="${OPENAPI_TO_POSTMAN_PACKAGE:-openapi-to-postmanv2@5.0.0}"
CONVERTER_OPTIONS_CONFIG="${OPENAPI_TO_POSTMAN_OPTIONS_CONFIG:-$ROOT_DIR/.postman/openapi-to-postman-options.json}"

这个设计对测试很有用。测试脚本可以塞一个临时 OpenAPI、临时 collection 目录、假的 npx 和假的 postman,不用动真实项目。

生成过程分几步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
npx --yes openapi-to-postmanv2@5.0.0 \
--spec "$OPENAPI_SPEC" \
--output "$tmp_collection" \
--pretty \
--options-config "$CONVERTER_OPTIONS_CONFIG"

postman collection migrate "$tmp_collection" --output "$tmp_collection_dir"

POSTMAN_COLLECTION_DIR="$tmp_collection_dir" \
node "$ROOT_DIR/script/postprocess_postman_collection.js"

rsync -a --delete "$tmp_collection_dir"/ "$POSTMAN_COLLECTION_DIR"/

postman collection lint "$POSTMAN_COLLECTION_DIR"

先生成临时 v2 collection JSON,再用 Postman CLI migrate 成 Native Git 的 v3 目录。后处理也在临时目录里做完,最后才 rsync 到目标目录。

我觉得这个顺序比直接写最终目录稳一点。旧做法如果先删目标目录再重建,Postman Desktop 的本地索引可能会短暂看到“collection 被删了”。现在保留目标目录本身,只同步里面的内容,至少不会主动制造这个问题。

3.1 转换配置按 tags 分组

.postman/openapi-to-postman-options.json 现在是:

1
2
3
4
5
6
{
"folderStrategy": "Tags",
"requestNameSource": "Fallback",
"parametersResolution": "Schema",
"alwaysInheritAuthentication": true
}

这里我最关心的是 folderStrategy: "Tags"

如果按 path 分组,/api/v1/user/token/query/device 这种路径会在 Postman 左侧变成很深的目录树。用 tags 分组以后,结构更接近业务域。比如用户相关、资源相关、任务相关接口会各自聚在一起,调试时不用一直展开路径目录。

这个配置不是架构问题,就是使用体验问题。但使用体验差了,最后大家还是会回到手工收藏、手工复制请求那套老路。

4. 后处理脚本解决项目自己的习惯

官方转换器只知道 OpenAPI,不知道这个项目怎么登录、怎么保存 token、哪些 examples 在左侧会变成噪音。

所以这些规则集中放到 script/postprocess_postman_collection.js

它现在主要做四件事:

1
2
3
4
1. 给需要认证的 request 注入 Bearer Auth
2. 给登录接口加 After response 脚本
3. 删除 Postman 生成的随机 id
4. 删除 response examples

这些都不适合在 Postman UI 里手工点。

4.1 Auth 来自 OpenAPI security

OpenAPI 里如果某个 operation 声明了:

1
2
3
4
5
6
7
{
"security": [
{
"BearerAuth": []
}
]
}

或者:

1
2
3
4
5
6
7
{
"security": [
{
"AdminAuth": []
}
]
}

后处理脚本就认为它需要认证。

脚本会先从 request YAML 里读出 method 和 url:

1
2
3
const method = yamlValue(content, 'method').toLowerCase();
const requestPath = stripBaseUrl(yamlValue(content, 'url'));
const operation = spec.paths?.[requestPath]?.[method];

然后把 auth 块插进去:

1
2
3
4
auth:
type: bearer
credentials:
token: "{{access_token}}"

这样 Postman 里带权限的接口会自动读当前 environment 的 access_token

这里有一个很好的副作用:如果某个接口没有带 Auth,不应该先去 Postman UI 里补,而应该回头看 OpenAPI 里有没有 security。这就把问题拉回了源头。

4.2 登录接口自动保存 token

项目里的登录接口是:

1
POST /api/v1/user/token/query/device

后处理脚本会给它加一个 After response:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const json = pm.response.json();
const accessToken = json?.data?.access_token;
const refreshToken = json?.data?.refresh_token;
const accountId = json?.data?.account_id;

if (accessToken) {
pm.environment.set('access_token', accessToken);
}
if (refreshToken) {
pm.environment.set('refresh_token', refreshToken);
}
if (accountId) {
pm.environment.set('account_id', accountId);
}

调试时流程就变成:

1
2
3
4
选择 Local 或 Dev environment
-> 调登录接口
-> environment 自动写入 access_token
-> 调其他带权限接口

这个动作如果靠每个人自己手工配置,迟早有人漏。写进生成脚本以后,collection 重新生成也不会丢。

4.3 删除随机 id,少看一些无意义 diff

postman collection migrate 会给 collection 和 request 生成 UUID:

1
id: ee99de05-8b46-4430-949a-7fda7c5e654a

这些 id 对接口调试没什么帮助,而且可能每次生成都变。后处理脚本会删掉它们:

1
content.replace(/^id: [0-9a-f-]{36}\n/m, '')

collection definition 里的 id 也会处理。

我不想在 review 里看到这种 diff。接口路径、参数、认证才值得看,工具生成的随机数不值得。

4.4 删除 examples,让左侧安静一点

OpenAPI responses 会被转换成 Postman examples,比如:

1
2
3
4
OK
Bad request
Unauthorized
Internal server error

它们会挂在每个 request 下面。接口少的时候还行,接口一多,左侧树会变得很吵。

后处理脚本会删掉 request YAML 里的 examples 引用:

1
content.replace(/^examples: .+\n(?:  .+\n)*/m, '')

也会删掉 .resources/**/examples/*.yaml

这不是说 examples 永远没价值。只是对我们当前的调试流程来说,它们比请求本身更容易制造噪音。如果以后要保留,可以把这段逻辑做成开关。

5. Local View 和 Cloud View 不要混在一起

.postman/resources.yaml 里记录了本地资源和云端资源的映射:

1
2
3
4
5
6
7
8
localResources:
specs:
- ../docs/openapi.json
environments:
- ../postman/environments/Dev.environment.yaml
- ../postman/environments/Local.environment.yaml
collections:
- ../postman/collections/CurveTool Backend API

这个文件让 Postman 知道本地的 spec、environment、collection 在哪里,也知道它们对应云端哪个 workspace 资源。

这里我踩到的点是:postman/collections/ 是生成物,文件数量又多,不适合提交进业务 repo。README 里也按这个口径写了:本地需要时运行 make postman-collection,团队要看就 make postman-sync 推到 Cloud View。

不过我实际核对本地 .gitignore 时,没有看到 postman/collections/ 的明确 ignore 规则。这个地方要小心,别只在文档里说“生成物不提交”,最后 git status 里还是冒出来一堆 YAML。真正落地时应该补上类似:

1
2
3
# Generated Postman Native Git collection. Regenerate with:
# cd backend && make postman-collection
postman/collections/

我不会为了让 Postman Desktop 的 Local View 更稳定,就把上千个 generated YAML 提交进 repo。这种提交看起来方便,后面每次工具升级、排序变化、examples 变化都会把 review 搞脏。

6. 测试脚本保护的是生成链路

script/generate_postman_collection_test.sh 不是业务接口测试。它测的是这条生成链路有没有断。

它做了几件很实际的事:

1
2
3
4
5
1. 创建一个临时 OpenAPI spec
2. fake npx,确认调用的是 openapi-to-postmanv2@5.0.0
3. fake postman,确认跑了 collection migrate 和 collection lint
4. 造出带随机 id、examples、登录接口、认证接口的 v3 目录
5. 验证后处理脚本真的改掉了这些文件

最后会检查:

1
2
3
4
grep -q 'token: "{{access_token}}"' protected.request.yaml
grep -q 'type: afterResponse' token.request.yaml
grep -R -q '^id: [0-9a-f-]\{36\}$' "$COLLECTION_DIR"
find "$COLLECTION_DIR" -path '*/examples/*.yaml' -type f

这个测试很接地气。它不关心 Postman 云端,也不关心真实接口有多少个。它只保护我们自己写的规则:转换器要被调用,migrate 要被调用,lint 要跑,后处理不能失效。

7. 日常怎么用

本地刷新 collection:

1
2
cd backend
make postman-collection

认证调试:

1
2
3
4
1. 选择 Local 或 Dev environment
2. 调用 POST /api/v1/user/token/query/device
3. 确认 environment 里有 access_token
4. 调用其他带权限接口

同步给团队:

1
2
cd backend
make postman-sync

如果 Postman 左侧没看到 collection,先确认本地确实生成了:

1
find postman/collections/CurveTool\ Backend\ API -type f | wc -l

文件存在但 Local View 不显示时,可以刷新 Postman、重新 Open Folder,或者推到 Cloud View 看。不要因为这个问题就把 generated collection 提交进去。

8. 我最后怎么理解这件事

这套流程真正解决的不是“怎么把 OpenAPI 导入 Postman”。那个命令其实很简单。

它解决的是接口调试的所有权问题。

路由从运行时代码来,OpenAPI 是接口契约,Postman collection 是生成物。项目里的调试习惯写进后处理脚本,云端同步单独执行,生成目录不进业务 review。

这样以后接口变了,只要把 OpenAPI 和生成脚本维护好,Postman 就会跟着变。人不用再去 UI 里补一遍,也不用猜团队里谁手上的 collection 才是最新的。