AI会干活 / 免费教程
给接口加返回字段,先让 Codex 把旧客户端护住
给接口响应加字段,看起来是后端工作里最安全的一类改动。新增字段不影响旧字段,旧客户端不读它,新页面可以直接用它。很多团队会把这类需求当成“顺手加一下”:DTO 里多一个属性,序列化结果里多一段 JSON,OpenAPI 文档补一下,然后发版。
适合人群
维护公共接口的后端工程师
先解决什么
新增返回字段给新版页面使用,但旧客户端仍在调用。
学完结果
接口扩展方案和兼容验证清单。
你会学到什么
让 Codex 查序列化、默认值、类型生成和兼容测试。
准备材料:接口定义、调用方列表、返回样例、版本兼容要求。
交付物:接口扩展方案和兼容验证清单。
边界:聚焦向后兼容的接口小改动。
教程定位
这篇教程解决什么问题
给接口响应加字段,看起来是后端工作里最安全的一类改动。新增字段不影响旧字段,旧客户端不读它,新页面可以直接用它。很多团队会把这类需求当成“顺手加一下”:DTO 里多一个属性,序列化结果里多一段 JSON,OpenAPI 文档补一下,然后发版。
真正出问题的地方,往往不在“多一个字段”本身,而在围绕这个字段的一圈隐含契约。老版本 Android 客户端可能用严格 JSON 反序列化,遇到未知字段直接失败;老版本 iOS 可能把 `null` 映射成非可选类型后崩溃;某个 BFF 会把后端响应再转一次,字段名从 snake_case 变成 camelCase;第三方客户用生成的 TypeScript SDK,生成器把新增必填字段当成破坏性变更;缓存层用响应体 hash 做兼容判断;灰度期间新版页面读到了字段,旧服务实例还没有返回,前端又把 `undefined` 当成真实状态展示。
这篇教程讲的不是“让 Codex 帮你写一个字段”,而是让 Codex 在小改动里做兼容审查。场景是订单详情接口 `GET /api/v1/orders/{orderId}` 要为新版页面增加发票状态字段:`invoice_status`、`invoice_title`、`invoice_failed_reason`。旧客户端仍在调用这个接口,有移动端、管理后台、第三方 SDK 和内部任务脚本。你的交付物不是一段漂亮代码,而是一份接口扩展方案和兼容验证清单:字段怎么序列化,默认值怎么取,哪些类型要重新生成,哪些旧客户端要跑兼容测试,回滚时字段缺失会不会让新版页面出错。
Codex 在这里的价值是耐心。它可以帮你沿着代码查:响应 DTO 在哪里定义,序列化策略是否忽略空值,OpenAPI schema 是否会把字段标成 required,哪些调用方在严格解析,生成类型落在哪些包,mock 和契约测试有没有覆盖旧响应。你要做的是给它边界:先只读调查,不要改;发现生成文件先找源头;新增字段必须 optional;不能删除、重命名或改变旧字段;默认值和空值策略要写清;兼容测试失败时先停下来说明,不要为了过测试改旧客户端逻辑。
这类小改动最适合用 Codex 做“向后兼容护栏”。它不会替你决定产品语义,但能把容易漏看的技术边界摊开:字段是否总返回,旧客户端是否容忍未知字段,文档和类型是否同步,测试是否同时覆盖新旧响应,回滚后新版页面是否降级。接口越公共,越不能靠“JSON 多字段应该没事”的经验主义。
使用场景
什么情况下最适合用这一套
你是维护公共接口的后端工程师。产品要在新版订单页展示发票状态,需求说得很轻:“订单详情接口加几个字段就行,老版本页面不用管。”你打开接口,看到现在的响应大概是这样:
新版页面希望多拿到:
需求不难,但你知道这个接口不是只有新版页面在用。旧 App 还在调用 `GET /api/v1/orders/{orderId}` 展示支付成功页;旧 Web 管理台会把订单详情 JSON 透传到一个状态面板;客服系统有一个 Node 脚本每天拉订单详情做对账;还有第三方客户使用你们发布的 OpenAPI 生成 SDK。接口 URL 里虽然有 `v1`,但团队并不想为了三个展示字段开 `v2`。这就变成了一个典型的向后兼容小改动。
风险通常集中在六个地方。
第一,序列化策略不清。后端 DTO 里字段是 `invoiceStatus`,响应 JSON 要不要输出成 `invoice_status`?字段为 `null` 时是否输出?旧缓存是否依赖响应字段集合?如果全局配置是 `JsonInclude.NON_NULL`,新版页面在未开票订单里可能完全拿不到 `invoice_failed_reason`,这和“字段存在但值为空”不是一回事。
第二,默认值容易乱。`invoice_status` 是不是每笔订单都有?历史订单没有发票记录时返回 `not_requested`、`none`、`null` 还是不返回字段?如果 Codex 不查业务规则,可能会自己造一个看起来合理的默认值,结果把“未申请开票”和“暂时查不到发票数据”混成一类。
第三,类型生成链可能不止一处。服务端有 Kotlin/Java/Go DTO,OpenAPI 有 schema,前端有生成的 `OrderDetailResponse`,移动端可能也有生成模型。手改生成文件能让当前编译通过,但下次生成会被覆盖;只改后端不改 schema,新版页面拿不到类型;把字段标成 required,旧 mock 和旧客户端契约测试可能全红。
第四,旧客户端兼容不是一句“它不读就没事”。有些 JSON parser 默认忽略未知字段,有些项目为了发现接口漂移会开启严格模式;有些客户端不会因为未知字段失败,但会因为字段类型变化失败。新增字段如果嵌在旧对象里,一般安全;如果顺手把旧字段从 `invoice` 对象里改名或重组,就不是新增了。
第五,灰度和回滚要考虑双向缺失。后端先发、新页面后发时,新字段多出来,旧客户端要安全;新页面先发、部分后端还没发时,新页面要能处理字段缺失;后端回滚时,新页面也不能把缺失字段展示成“开票失败”。只验证“新后端 + 新页面”是不够的。
第六,测试边界容易太乐观。只加一条 happy path 测试,说明不了兼容。你至少要覆盖:旧响应仍可被旧客户端解析;新响应在旧客户端不报错;新页面面对字段缺失或 `null` 能降级;OpenAPI 生成类型没有把字段变成必填;回滚时没有把未知状态当成失败状态。
这就是本文的工作方式:让 Codex 先做接口兼容调查,再输出最小扩展方案,最后补兼容验证清单。你可以让它写代码,但不能让它跳过调查。
{
"order_id": "ORDER_DEMO_1001",
"status": "paid",
"amount_cents": 129900,
"paid_at": "2026-06-20T10:12:00Z",
"customer": {
"name": "demo_customer"
}
}{
"invoice_status": "pending",
"invoice_title": "demo_company",
"invoice_failed_reason": null
}材料准备
开始前先把材料和边界备齐
开始前不要只给 Codex 一句“给订单详情加发票字段”。要把它当成一个要审查接口契约的同事,给足输入材料。
第一类材料是接口定义。包括接口路径、当前响应 DTO、OpenAPI schema 或 protobuf/thrift 定义、序列化注解、响应样例。如果项目有 BFF 或 gateway,也要给它对应的响应转换文件。你要让 Codex 看清字段从数据库或服务层出来后,经过哪些层变成外部 JSON。
第二类材料是调用方列表。不要只写“前端会用”。至少列出调用方名称、版本、解析方式和是否还活跃。例如:
第三类材料是返回样例。至少准备旧响应、新响应、无发票记录、开票失败、历史订单缺发票数据这几种。样例要脱敏,但结构要真实。不要省略嵌套层级,因为兼容问题常常和字段位置有关。
第四类材料是版本兼容要求。比如“不能开 v2”“不能让旧客户端升级才能继续用”“新增字段必须 optional”“字段为未知状态时新版页面显示 `-`”“后端回滚后新版页面不能报错”“本次不删除旧字段、不改变旧字段类型、不改变旧字段含义”。这些限制要写在提示词里,不要期待 Codex 自己猜。
第五类材料是项目里的检查命令。包括后端单测、契约测试、OpenAPI 生成命令、前端类型生成命令、旧客户端兼容测试或至少旧响应 fixture 的解析测试。Codex 需要知道它能跑什么,不能跑什么。跑不了 Android/iOS 本地测试时,也要让它输出人工验证项,而不是假装已经覆盖。
还要特别说明保密边界。接口样例里不要出现真实订单号、真实客户名、发票抬头、税号、手机号、地址、邮箱或支付流水号。用 `ORDER_DEMO_1001`、`demo_company`、`TAX_ID_DEMO_MASKED` 这类占位值就够了。Codex 需要结构,不需要真实用户数据。
- 新版 Web 订单页:需要读取 `invoice_status`,由 TypeScript SDK 生成类型。
- 老版 Web 管理台:仍调用同一接口,不读取新字段,使用手写 fetch 类型。
- Android 5.8 以下:使用 Moshi,历史上对未知字段是否宽松不确定。
- iOS 6.2 以下:使用 Codable,默认忽略未知字段,但非可选字段不能为 `null`。
- 客服导出脚本:Node.js 脚本,直接保存完整 JSON 到对象存储。
- 第三方 SDK:由 OpenAPI schema 生成,字段可选性会影响客户升级。
实操流程
按这套步骤把工作跑起来
第一步,让 Codex 只读调查接口契约。
第一轮提示词里明确写“不要修改文件”。让它搜索接口路径、响应 DTO、OpenAPI schema、序列化配置、生成类型、旧客户端解析测试和调用方。它要输出一张字段链路表:服务层字段从哪里来,DTO 怎么命名,JSON 输出名是什么,schema 里字段是否 optional,前端类型从哪里生成,旧客户端是否有解析测试。
一个好的调查结果应该具体到文件和判断。例如:
| 层级 | 发现 | 兼容判断 | | --- | --- | --- | | 服务层 | `InvoiceService.getInvoiceSummary(orderId)` 可能返回 `null` | 历史订单要区分无发票记录和查询失败 | | API DTO | `OrderDetailResponse` 是手写 DTO | 可以新增 nullable 字段,但不能改旧字段 | | 序列化 | 全局开启 `NON_NULL` | `null` 字段不会输出,新页面必须处理缺失 | | OpenAPI | `OrderDetailResponse` schema 手写维护 | 新字段应放入 `properties`,不能加入 `required` | | Web SDK | `pnpm generate:api` 生成 | 不要手改 `src/generated/api.ts` | | 旧客户端 | 有 `order-detail-v1-response.json` fixture | 应加入“新响应旧解析”测试 |
第二步,让 Codex 判断字段语义,不让它自创默认值。
发票字段看似简单,但默认值是业务语义。你要让 Codex根据现有业务代码和需求材料列出几种状态:
如果材料里没有定义“未申请开票”的返回值,Codex 应该停下来标成待确认,而不是随手补 `none`。接口字段一旦发出去,枚举值就是契约,后面改起来比现在问一句要贵得多。
第三步,制定最小扩展方案。
方案不要一上来就写一堆代码。先让 Codex 输出“必须改”和“不改”。本例的最小方案通常是:
不改清单同样重要:
第四步,让 Codex 按层改动,并把生成源头和生成产物分开。
如果类型文件是生成的,Codex 最容易犯的错是直接编辑生成产物。你要要求它先找到源头:OpenAPI YAML、后端注解、schema 文件或 proto 文件。源头改完后再运行生成命令。生成结果如果很大,也要让它说明哪些生成文件变化是新字段带来的,不要顺手格式化整个 SDK。
后端 DTO 的字段建议保持 nullable 或 optional。以 TypeScript schema 为例,不要写成:
更稳的写法是让新字段只出现在 `properties`,不进 `required`。如果业务允许空值,要明确 `nullable` 或使用项目约定的可空表达:
如果项目使用 Java/Kotlin DTO,也要检查序列化注解。比如 `@JsonInclude(Include.NON_NULL)` 会让 `null` 字段消失。字段消失不一定错,但文档和前端降级要按“可能缺失”写。不要让文档承诺“总是返回 null”,代码却把 null 字段省略。
第五步,补兼容测试,而不是只补新字段 happy path。
你可以让 Codex 设计四类测试。
第一类是后端响应测试。构造有发票、无发票、失败发票和历史订单,断言新字段的输出策略。重点不是字段出现一次,而是默认值符合确认后的语义。
第二类是 schema 或契约测试。断言 OpenAPI 里新字段没有进入 `required`,字段类型和枚举值符合约定。如果项目没有专门测试,也可以在 PR 检查清单里写明人工检查位置。
第三类是生成类型测试。重新生成 SDK 后,新字段应该是可选或可空,例如 `invoice_status?: InvoiceStatus | null`,而不是 `invoice_status: InvoiceStatus`。如果生成器输出的是 camelCase,也要确认命名和序列化层一致。
第四类是旧客户端兼容测试。最轻量的做法是拿旧客户端已有 response fixture,分别跑“旧响应解析”和“新响应解析”。如果旧客户端开启严格未知字段,这一步会直接暴露风险。跑不了真实移动端时,也要至少把风险写进清单,交给对应端确认。
第六步,做灰度和回滚检查。
兼容不是只向过去兼容,也要向部署现实兼容。灰度时可能出现四种组合:
| 组合 | 风险 | 需要验证 | | --- | --- | --- | | 旧后端 + 旧客户端 | 基线 | 不应受影响 | | 新后端 + 旧客户端 | 未知字段 | 旧客户端解析不失败 | | 旧后端 + 新页面 | 字段缺失 | 新页面显示 `-` 或“暂无发票信息” | | 新后端 + 新页面 | 完整链路 | 新字段展示正确 |
回滚风险也要提前写清。如果后端因为故障回滚,新页面仍可能已经发布。新版页面对 `invoice_status` 的读取必须是可选的,不能直接写 `switch(order.invoice_status)` 后默认把 `undefined` 当作 `failed`。如果字段控制重要按钮,比如“申请重开票”,更要确认缺字段时按钮是隐藏、禁用还是显示兜底文案。
第七步,让 Codex 输出最终交付说明。
交付说明不需要很长,但要能让评审者快速看到兼容边界:
这比“已加字段,请 review”有用得多。公共接口的评审重点不是代码行数,而是契约有没有被悄悄改变。
- 在后端响应 DTO 新增 nullable 字段:`invoice_status`、`invoice_title`、`invoice_failed_reason`。
- 在 DTO 组装层从发票摘要读取字段,历史订单或无发票记录按确认后的默认策略返回。
- 在 OpenAPI schema 的 `properties` 中新增字段,全部保持 optional,不加入 `required`。
- 重新生成 Web SDK 类型,让新版页面读取可选字段。
- 增加契约或 fixture 测试:旧响应解析、新响应解析、字段缺失解析、`null` 或未知枚举降级。
- 更新返回样例和兼容说明。
- 有发票记录且待开票:`invoice_status: "pending"`。
- 已开票:`invoice_status: "issued"`,`invoice_title` 可能有值。
- 开票失败:`invoice_status: "failed"`,`invoice_failed_reason` 可选。
- 用户未申请开票:是否返回 `not_requested`,还是 `null`,需要从产品或现有枚举确认。
- 历史订单缺数据:不能擅自当作 `not_requested`,除非业务明确。
- 不改接口路径。
- 不开新版本,除非发现旧客户端不能容忍未知字段。
- 不删除、不重命名、不改变旧字段类型。
- 不把旧字段移动进新的 `invoice` 对象。
- 不手改生成文件。
- 不把新字段标成 required。
- 不在本次顺手新增筛选、导出、发票详情接口。
- 本次新增哪些 response 字段。
- 字段是否 optional/nullable。
- 旧字段是否保持不变。
- OpenAPI 和生成类型是否同步。
- 旧客户端兼容验证跑了哪些。
- 新页面字段缺失时如何降级。
- 灰度和回滚有哪些注意事项。
required:
- order_id
- status
- amount_cents
- invoice_status
properties:
invoice_status:
type: string
enum: [not_requested, pending, issued, failed]properties:
invoice_status:
type: string
nullable: true
description: "发票状态。旧订单或未申请开票时可能为空,客户端必须兼容字段缺失。"
enum:
- not_requested
- pending
- issued
- failed
invoice_title:
type: string
nullable: true
description: "发票抬头。仅在已有发票信息时返回。"
invoice_failed_reason:
type: string
nullable: true
description: "开票失败原因。仅 invoice_status 为 failed 时可能返回。"输入示例
可以直接参考的输入材料
下面是一份可以直接交给 Codex 的材料。它是脱敏的,但保留了真实工程里的结构和风险。
这份输入刻意把“不确定”写进材料里。尤其是 `not_requested` 和 `null` 的关系,不要让 Codex 用自己的常识定契约。公共接口里,含糊的默认值比少写几行代码危险。
你先不要改代码,只做只读调查和接口扩展方案。
任务:
给订单详情接口 GET /api/v1/orders/{orderId} 新增发票相关返回字段,供新版 Web 订单页展示。旧 App、旧 Web 管理台、客服脚本和第三方 SDK 仍在调用同一接口。
计划新增字段:
- invoice_status:发票状态,枚举候选 not_requested / pending / issued / failed。是否允许 null 需要你根据现有代码和材料判断,不能自创默认值。
- invoice_title:发票抬头,可能为空。
- invoice_failed_reason:开票失败原因,仅 failed 时可能有值。
当前旧响应样例:
{
"order_id": "ORDER_DEMO_1001",
"status": "paid",
"amount_cents": 129900,
"paid_at": "2026-06-20T10:12:00Z",
"customer": {
"name": "demo_customer"
}
}
新版页面期望响应样例:
{
"order_id": "ORDER_DEMO_1001",
"status": "paid",
"amount_cents": 129900,
"paid_at": "2026-06-20T10:12:00Z",
"customer": {
"name": "demo_customer"
},
"invoice_status": "pending",
"invoice_title": "demo_company",
"invoice_failed_reason": null
}
候选文件:
- server/orders/OrderController.ts
- server/orders/OrderDetailResponse.ts
- server/orders/OrderResponseMapper.ts
- server/invoices/InvoiceService.ts
- api/openapi/order.yaml
- web/src/generated/api.ts
- web/src/pages/orders/OrderDetailPage.tsx
- mobile/android/fixtures/order_detail_v1_response.json
- mobile/ios/Fixtures/OrderDetailV1Response.json
调用方:
- 新版 Web 订单页:会读取 invoice_status 展示发票状态。
- 老版 Web 管理台:仍调用同一接口,不读取新字段。
- Android 5.8 以下:是否忽略未知字段不确定,需要查测试或配置。
- iOS 6.2 以下:Codable,通常忽略未知字段,但要确认字段类型变化没有影响。
- 客服脚本:Node.js,保存完整 JSON。
- 第三方 SDK:通过 OpenAPI schema 生成。
兼容要求:
- 不开新接口版本。
- 不能删除、重命名、移动或改变任何旧字段类型。
- 新字段必须对旧客户端向后兼容。
- 如果 OpenAPI 需要新增字段,不能把新字段放入 required,除非有明确兼容证据。
- 不要手改生成文件,要找到生成源头。
- 后端回滚后,新版页面面对字段缺失必须安全降级。
请输出:
1. 字段链路调查:DTO、序列化、OpenAPI、生成类型、调用方兼容点。
2. 待确认问题:尤其是默认值、null、字段缺失和枚举值。
3. 最小扩展方案:必须改哪些文件,为什么。
4. 不改清单:哪些功能和旧契约本次不碰。
5. 兼容验证清单:旧响应、新响应、字段缺失、null、未知枚举、回滚。
限制:
只读调查,不要改代码。如果发现旧客户端不能容忍未知字段,停止并说明是否需要新版本接口或网关兼容策略。提示词
可复制使用的提示词
下面这组提示词建议分三轮使用。第一轮只调查,第二轮写方案,第三轮才动代码。
这三轮提示词的关键是把“写字段”拆成“查契约、定方案、再实施”。Codex 很擅长沿代码链路跑,但你要把它的速度关进正确的顺序里。
第一轮:只读兼容调查
你是公共 API 兼容审查助手。请先不要修改文件。
我要给一个已有接口的响应新增字段。这个接口仍被旧客户端、内部脚本和生成 SDK 使用。你的任务不是马上写代码,而是先找清楚响应字段从哪里来、如何序列化、哪些类型由 schema 生成、旧客户端是否能容忍未知字段。
接口:
[粘贴接口路径、当前响应样例、新字段说明]
调用方:
[粘贴调用方列表、版本、解析方式、是否活跃]
候选文件:
[粘贴 DTO、mapper、OpenAPI、生成类型、旧客户端 fixture、测试文件路径]
兼容要求:
- 不删除、不重命名、不移动、不改变旧字段类型。
- 新字段默认 optional/nullable,除非有明确证据证明所有调用方都能接受 required。
- 不手改生成文件。
- 不自创默认值或枚举。
- 如果旧客户端严格解析未知字段,先停下来说明风险。
请输出:
1. 字段链路表:服务层、DTO、序列化、schema、生成类型、调用方。
2. 兼容风险表:未知字段、null、字段缺失、枚举扩展、灰度、回滚。
3. 待确认问题:需要产品、前端、移动端或接口 owner 确认的点。
4. 建议的最小改动范围。第二轮:接口扩展方案
根据刚才的调查结果,请输出接口扩展方案,仍然不要改代码。
方案必须包含:
1. 新增字段的 JSON 名称、类型、可选性、null 策略、字段缺失策略。
2. 默认值语义:未申请开票、历史订单缺数据、查询失败分别怎么处理。
3. OpenAPI/schema 改法:properties、required、nullable、enum、description。
4. 类型生成链:源头文件、生成命令、生成产物,不允许手改生成文件。
5. 兼容测试计划:旧响应旧客户端、新响应旧客户端、旧响应新页面、新响应新页面。
6. 回滚计划:后端回滚或字段缺失时,新页面如何降级。
7. 不改清单:本次不碰接口版本、旧字段、筛选、导出、发票详情功能。
如果有任何默认值或客户端行为没有证据,请放入“待确认”,不要在方案里假定。第三轮:按方案小步实施
现在开始按已确认的方案实施。请只做本次接口扩展,不做相邻功能。
实施要求:
1. 先改源头定义和后端 DTO/mapper,再运行或说明类型生成。
2. 新字段保持 optional/nullable,不进入 required,除非方案里已有明确确认。
3. 不手改生成产物;如果必须生成,请说明命令和变更范围。
4. 补兼容测试:旧响应解析、新响应解析、字段缺失、null、未知枚举或至少其中有风险的部分。
5. 不改接口路径,不改旧字段,不改旧客户端业务逻辑。
6. 完成后输出兼容验证清单和回滚风险说明。
如果测试失败,不要为了过测试扩大修改范围。先说明失败暴露的是兼容风险、测试数据问题,还是实现错误。输出样例
AI 应该输出到什么程度
下面是一份理想的 Codex 输出片段。它不应该只说“可以加字段”,而应该把兼容判断说清楚。
这样的输出有几个值得保留的习惯。它会把“没证据”的地方标出来,而不是猜;它会区分源头文件和生成文件;它会提醒 `NON_NULL` 导致字段缺失;它会把回滚后的缺失字段当成正常场景测试。你可以让 Codex 按这个方案继续改代码,也可以把这份方案先发给前端和移动端 owner 确认。
字段链路调查结果:
1. 后端响应 DTO
- 文件:server/orders/OrderDetailResponse.ts
- 当前 OrderDetailResponse 是手写 DTO,不是生成文件。
- 响应使用 snake_case 序列化策略,DTO 字段 invoiceStatus 会输出为 invoice_status。
- 全局 JsonInclude 为 NON_NULL,因此 null 字段不会出现在 JSON 中。新版页面必须兼容字段缺失。
2. 发票数据来源
- 文件:server/invoices/InvoiceService.ts
- getInvoiceSummary(orderId) 返回 InvoiceSummary | null。
- 现有枚举为 PENDING / ISSUED / FAILED,没有找到 NOT_REQUESTED。
- 未申请开票和历史订单缺数据的区分不明确,需要产品或发票模块 owner 确认,不能直接新增 not_requested。
3. OpenAPI/schema
- 文件:api/openapi/order.yaml
- OrderDetailResponse schema 手写维护。
- required 当前包含 order_id/status/amount_cents/paid_at/customer。
- 新字段应只加到 properties,不加入 required。
4. 生成类型
- Web SDK 来源:api/openapi/order.yaml。
- 生成命令:pnpm generate:api。
- web/src/generated/api.ts 是生成产物,不应手改。
- 预期生成类型:invoice_status?: "pending" | "issued" | "failed" | null。
5. 旧客户端兼容
- Android fixture 存在 order_detail_v1_response.json,但没有新响应解析测试。
- iOS Codable 默认忽略未知字段,但仍建议用新响应 fixture 跑一次。
- Node 客服脚本保存完整 JSON,不依赖固定字段列表,低风险。
待确认问题:
- 未申请开票是否需要单独枚举值。
- 历史订单缺发票数据时应返回字段缺失、null,还是业务状态。
- 开票失败原因是否允许展示给用户,是否需要脱敏。
最小改动方案:
- OrderDetailResponse 新增 nullable invoiceStatus/invoiceTitle/invoiceFailedReason。
- OrderResponseMapper 从 InvoiceSummary 映射字段;InvoiceSummary 为 null 时不输出发票字段。
- order.yaml properties 新增字段,不进入 required。
- 运行 pnpm generate:api 更新 Web SDK。
- 补后端响应测试和 Web 降级测试。
不改清单:
- 不改 GET /api/v1/orders/{orderId} 路径。
- 不改已有字段名称、类型和嵌套结构。
- 不新增发票详情接口。
- 不改导出、筛选、排序。
- 不手改 web/src/generated/api.ts。
兼容验证清单:
- 旧响应仍可被新版页面解析,发票区域显示“暂无发票信息”。
- 新响应可被旧 Web 管理台解析,不报未知字段错误。
- 新响应可被 Android/iOS fixture 解析测试接受。
- OpenAPI 新字段不是 required。
- 后端回滚导致字段缺失时,新页面不显示“开票失败”。人工验收
人要怎么检查和改到可用
Codex 做完兼容调查和小步实现后,人工检查要集中在“契约有没有被悄悄改变”。不要只看代码能不能编译。
第一,检查旧字段。确认旧响应中的字段名、类型、嵌套位置、枚举值含义没有变化。尤其要防止 Codex 为了“结构更清晰”把平铺字段整理进 `invoice` 对象,或者把旧字段 `status` 的语义顺手扩展。接口兼容的底线是旧调用方看到的旧世界不变。
第二,检查新字段可选性。OpenAPI、后端 DTO、生成 SDK、前端读取都要一致。如果 schema 里 optional,生成类型却变成必填,要查生成器配置;如果后端可能省略 null 字段,前端就不能只按 `null` 处理,还要按 `undefined` 处理。可选性不一致会在灰度和回滚时放大。
第三,检查默认值语义。不要让历史订单被自动归为“未申请开票”,除非业务明确允许。技术上最省事的默认值,经常会变成业务报表里的脏数据。建议把“无发票记录”“未申请开票”“发票服务查询失败”“历史数据不可判定”分开看,至少在文档里写清哪些会返回字段缺失或空值。
第四,检查枚举扩展。新增 `invoice_status` 时,不要直接复制内部枚举。内部可能有 `CANCELLED`、`RETRYING`、`MANUAL_REVIEW` 这类状态,不一定适合暴露给客户端。对外枚举要稳定,未知内部状态可以映射成 `pending` 或不返回字段,但这个决定必须有 owner 确认。
第五,检查生成文件。生成产物变化大时,不要被 diff 吓到,也不要盲目接受。让 Codex 说明哪些变化来自新字段,哪些是生成器版本或格式化造成的。如果生成器版本变化带来大量无关 diff,本轮应该停下来,先固定生成环境,不要把“接口加字段”和“生成器升级”混在一起。
第六,检查旧客户端。最好不要只问“移动端应该没事吧”。拿新响应 fixture 跑一次旧解析测试。如果没有现成测试,至少让 Android/iOS owner 确认解析库配置:是否忽略未知字段,是否对缺失字段宽松,是否对 `null` 宽松。未知字段和 `null` 是两件事,不能混为一谈。
第七,检查回滚路径。新版页面必须把字段缺失当成正常输入。建议人工打开或写测试覆盖:
缺失时的界面文案应该是“暂无发票信息”或 `-`,而不是“开票失败”。失败状态只能来自明确枚举,不能来自空值推断。
第八,检查日志和监控。新增字段可能需要埋点,但本次不一定要做。至少确认不会把发票抬头、税号或失败原因里的敏感信息打进普通日志。Codex 如果顺手加了 debug log,要仔细删掉或改成安全字段。
人工检查的最终结果可以整理成一张 PR 附表:
| 检查项 | 结果 | 证据 | | --- | --- | --- | | 旧字段未变化 | 通过 | 响应快照对比 | | 新字段未加入 required | 通过 | OpenAPI schema | | 生成 SDK 类型可选 | 通过 | `invoice_status?: ...` | | 旧客户端解析新响应 | 待 Android/iOS 确认 | fixture 已准备 | | 新页面兼容字段缺失 | 通过 | 单测或本地截图 | | 后端回滚风险 | 可接受 | 缺字段降级为 `-` |
这张表不是形式主义。它让一个小改动在评审时变得可判断,也让将来出问题时知道当时检查过什么、没检查什么。
- `invoice_status` 完全不存在。
- `invoice_status` 为 `null`。
- `invoice_status` 为未知字符串。
- `invoice_failed_reason` 存在但为空。
- `invoice_status` 为 `failed` 但原因缺失。
失败反例
这些失败反例要提前避开
反例 1:把新字段标成 required。
最常见的错误是 Codex 看到新版页面“需要”字段,就在 OpenAPI schema 里把 `invoice_status` 加进 `required`。这会让生成 SDK 把字段变成必填,旧 mock、旧响应 fixture、部分客户端测试立刻失败。更麻烦的是,required 暗示后端永远返回这个字段,但灰度、回滚、历史订单和发票服务异常都可能让字段缺失。
正确做法是先把字段设计成 optional/nullable。新版页面读取时写降级逻辑,后端只在有明确数据时输出。等所有调用方都升级、数据补齐、契约稳定后,再讨论是否需要收紧。大多数展示型字段其实不需要变成 required。
反例 2:手改生成类型,绕过 schema。
Codex 为了让前端编译通过,直接修改 `web/src/generated/api.ts`,加上 `invoice_status?: string | null`。短期看能用,但下次运行生成命令就会丢失;后端文档也不会更新;第三方 SDK 继续不知道这个字段。生成文件被手改,还会让评审者误以为 schema 已经同步。
正确做法是找到生成源头。源头可能是 `api/openapi/order.yaml`,也可能是后端注解生成的 JSON schema。改源头,运行生成命令,再检查生成产物。交付说明里写清“源头改了什么,生成了什么”,不要把生成文件当成业务源文件。
反例 3:把 `null`、缺失和默认枚举混成一件事。
另一个高频错误是为了让前端好写,把所有没有发票记录的订单都返回 `invoice_status: "not_requested"`。听起来友好,实际可能污染语义。历史订单缺数据、发票服务超时、用户从未申请开票、申请记录被撤销,这些状态对业务和客服并不一样。如果后端随手归一,前端展示是简单了,报表和排障会变难。
正确做法是先确认语义。没有证据时,用字段缺失或 `null` 表示“不确定/无可展示信息”,由新版页面降级显示。只有产品和发票模块 owner 明确“无记录就等于未申请开票”时,才增加 `not_requested`。接口默认值不是代码审美,是业务承诺。
反例 4:只测新页面,不测旧客户端。
有些团队做完后端和新版页面联调,看到发票状态展示正常,就认为完成。结果旧 Android 版本使用严格解析库,新响应多了未知字段后崩溃;或者旧管理台把完整 JSON 交给一个白名单校验器,新增字段被当成异常接口漂移。
正确做法是至少拿新响应 fixture 跑旧解析路径。如果真实移动端测试跑不动,也要把“未知字段容忍度”作为发布前确认项。公共接口的兼容不是靠相信,而是靠解析测试或 owner 明确确认。
反例 5:没有考虑回滚,导致新版页面把缺字段当失败。
后端发版后,新版页面开始读取 `invoice_status`。一小时后后端因为别的问题回滚,字段消失。前端代码写了一个默认分支:`default: return "开票失败"`,于是所有订单都显示失败。这个事故不是接口新增字段本身导致的,而是没有把“字段缺失”当成灰度和回滚的正常输入。
正确做法是把缺失、`null`、未知枚举都做成安全降级。缺失不是失败,未知也不一定是失败。失败只能来自明确的 `failed` 状态。Codex 写前端消费逻辑时,要明确要求它覆盖这几个输入。
主题边界
它和相邻主题的区别
这篇文章只讨论“公共接口响应新增字段”的向后兼容小改动,重点是序列化、默认值、类型生成和兼容测试。它和几个相邻 Codex 主题容易混在一起,但边界不同。
它不同于“只改一个列表字段展示”。列表字段展示更偏前端数据链路:接口已有字段,重点是类型、mapper、表格列和截图验收。本文的字段还没有进入公共契约,重点在后端响应和调用方兼容。列表展示可以只关心当前页面;公共接口新增字段要关心旧 App、SDK、脚本、灰度和回滚。
它也不同于“给高风险改动加 feature flag”。Feature flag 适合控制行为变化,比如新计算规则、新权限路径、新按钮入口。响应新增字段通常不应该靠开关解决全部问题,因为字段契约一旦出现在公共接口里,就需要 schema、类型和客户端兼容。你可以用灰度控制新版页面读取字段,但不能用开关替代接口兼容设计。
它还不同于“重构前锁住现有行为”。重构关注的是内部结构变化时保持行为不变;本文关注的是外部契约扩大时不破坏旧调用方。重构可以通过现有输入输出锁住函数;接口扩展必须额外检查 schema、生成 SDK、旧客户端解析和发布顺序。
它也不是“升级接口到 v2”。如果新增字段会破坏旧客户端,或者你需要改变旧字段含义、重组响应结构、删除字段、把可选字段改必填,那就不是本文的小改动了。那应该进入版本化接口设计:新路径、新 schema、迁移计划、双写或兼容层、弃用周期。本文的前提是:旧字段保持不变,新字段可选,新旧调用方可以在同一接口上共存。
最后,它不是让 Codex 替代接口 owner 做决定。Codex 可以查代码、找风险、生成测试、列出待确认问题;但默认值语义、对外枚举、失败原因是否可展示、旧客户端能否要求升级,这些仍然需要人来确认。好的用法是让 Codex 把问题问完整,让你在发布前少靠运气。
可直接套用的流程
1. 先写清楚任务目标:这次要让 AI 帮你完成什么工作,而不是泛泛地问一个问题。
2. 再给资料边界:哪些背景、数据、约束、口径必须被使用,哪些内容不能编。
3. 最后规定输出格式:用清单、表格、方案、话术还是复盘报告,并保留人工检查。