返回博客

组织拆分后接口只改了一半,另一侧悄悄坏掉

某后台系统在业务初期把两个审批角色合并管理,接口路径里直接刻入了「合组时代」的命名。半年后两个团队正式拆分,功能需求开始各走各路——但工程师只顺手改了提需求那一侧,另一侧的同名接口被默默遗忘,像一颗延迟引爆的地雷。

等用户报告「我怎么看不到自己这边待审的单?」,排查半天才意识到:那个接口压根就没有对称副本。

现象

# 2023 年两组合并时建的接口
GET  /api/group-a-b-leader/pending-reviews
POST /api/group-a-b-leader/batch-approve

# 2024 年拆分,只有 A 组提了需求
# → A 组的过滤逻辑被更新了 8 次,B 组调同一路径,拿到的数据逻辑已经偏

Git log 上看得很清楚:这个接口 A 组相关的 commit 有 8 条,B 组相关的 0 条。前端已经拆成了两个独立页面,但两个页面调用的是同一个路径。RBAC 规则里写的还是合组时代的 group-a-b-leader,B 组的角色根本没有对应权限配置。

根因

接口路径是对组织架构的一次快照。路径里编码了「建接口那一天」的 org chart。当组织变化时,有三种形态:

  • 角色重命名:风险低,改路径 + 更新 RBAC 即可
  • 角色合并(A + B → AB):风险低,其中一侧变成休眠状态
  • 角色拆分(AB → A + B):风险最高,也最容易漏

拆分之所以危险,是因为它的「安全幻觉」:旧接口继续对两侧都能返回数据,短期内看起来没坏。直到两侧需求开始分叉,第一个改动必然只改一侧,另一侧就悄悄偏了。PM 按团队维度跟需求,没有人持有一张「合组时代接口全量清单」,所以谁都没注意到这件事。

坑:每次单侧改动都是一次 50% 概率伤害对侧

拆分后的接口如果没有做显式的对称性决策,每一次功能迭代都是在「默认只改一侧」。累积几轮之后,两侧用户看到的数据语义已经完全不同,但日志里永远显示 200 OK——这是最难发现的静默漂移。

正解

把组织拆分时刻当成一次结构化审计事件,而不是背景迁移。

第一步:枚举所有合组时代接口

grep -rn "group-a-b\|groupAB\|GroupAB" src/ docs/

清单覆盖:路由路径、Controller/Service 命名、RBAC 规则、前端路由、数据库视图。

第二步:对每个接口做三选一决策

| 决策 | 适用场景 | 行动 | |------|---------|------| | 拆分 | 两侧逻辑或过滤条件会分叉 | 建 2 个独立接口,旧路径返回 410,更新所有调用方 | | 参数化 | 逻辑一致,仅需 ?category=a 区分 | 增加必填 query 参数并校验,保留单接口 | | 废弃 | 已无用或纯只读报表 | 标记 deprecated,排期删除 |

决策必须写进拆分 ticket,不允许「暂时先不管」作为默认选项。

第三步:为「没人提需求的那侧」单独建 issue

ISSUE-NNN: [B 组长] 补齐 /api/group-b-leader/* 与 /api/group-a-leader/* 的对称接口
Status: backlog(暂无用户需求,但对称性必须保证)
Blocks: A 侧任何后续功能迭代

这一步是关键。「等 B 组提需求再说」在实践中等价于「永远不做」。

第四步:加路由边界测试

// tests/routes-boundary.test.ts
it('禁止在新路由中重新引入合组时代复合命名', () => {
  const routes = enumerateAllRoutes()
  const forbidden = routes.filter(r => /group-a-b|dept-x-y/.test(r))
  expect(forbidden).toEqual([])  // 白名单:待迁移的存量路由
})

防止下一轮需求又在旧命名上叠加。

正解:拆分时刻必须是显式审计,不能是隐式迁移

在 ticket 里逐接口记录「拆 / 参数化 / 废弃」三选一决策,并为「另一侧」提前建 backlog issue——不管他们现在有没有需求。这是阻止接口不对称债务悄悄累积的唯一可靠手段。

一句话外卖

接口路径是组织架构的快照,org chart 一旦重组,旧快照就是技术债计时器——拆分时不做显式清点,每次单侧迭代都在给另一侧埋雷。