研发团队使用的工具很多:代码托管、持续集成、项目管理、部署系统和企业通知。每个工具解决局部问题,完整流程却依赖开发者在多个页面之间复制信息、等待结果和确认状态。

内部研发平台的目标不是再造这些工具,而是连接它们,让一次变更从创建到发布拥有连续、可追踪的过程。听起来像接口聚合,真正开始设计后,核心问题很快变成:平台究竟是一层界面,还是流程本身的状态拥有者?

从动作清单转向领域模型

最初的需求通常以功能表达:创建项目、触发构建、查看发布、发送通知。如果直接按页面开发,后端会成为一组转发接口,业务规则散落在 React 页面、Node 服务和外部系统里。

我们先整理流程中的稳定对象:工程、迭代、变更、构建、发布窗口、环境和审批。每个对象有自己的状态与约束,例如变更只有在检查通过后才能进入发布窗口,发布任务必须关联确定的构建产物。

领域模型并不需要复杂术语。它的价值是让团队使用同一种语言,也让状态变化有唯一位置。页面不再自行判断“是否可发布”,而是展示服务端根据完整规则给出的能力。

服务端返回状态和允许动作,React 页面只负责呈现:

type ReleaseView = {
  id: string;
  status: "draft" | "checking" | "ready" | "deploying" | "failed" | "succeeded";
  artifactId?: string;
  blockers: Array<{ code: string; message: string; owner?: string }>;
  capabilities: Array<"submit" | "approve" | "deploy" | "retry" | "rollback">;
  version: number;
};

capabilities 不是权限的替代,动作提交时服务端仍会重新校验。它的作用是让页面和服务共享同一决策结果,避免前端复制“检查通过且有审批角色且在发布窗口”这串规则。

平台不能成为唯一故障点

连接更多系统后,平台也拥有更大故障面。GitLab、部署服务或消息接口任何一个变慢,都可能拖住整个请求。如果所有调用都在用户点击后同步完成,页面会长时间等待,重试还可能重复创建任务。

我们将需要较长时间的动作转成异步任务。用户请求只负责验证输入、创建流程记录并入队;Worker 执行外部调用,持续更新状态;前端通过轮询或推送看到进度。

异步并不自动可靠。任务必须拥有幂等键,避免重复点击或重试造成多次发布;每一步要记录开始、成功、失败与外部标识;可重试错误和业务拒绝需要区分;超过次数后进入人工处理,而不是无限循环。

最重要的是,即使平台短暂不可用,也不能破坏原有研发工具的基本能力。平台应该提供更顺畅的路径,而不是把所有系统锁死在自己身后。

React 页面应反映流程,而不是拼接表单

内部系统常被认为“不需要设计”,结果是大量表单和状态文本堆在一起。研发平台的用户虽然是工程师,同样需要快速判断当前发生了什么、下一步由谁处理、失败后如何恢复。

页面围绕流程记录组织,而不是围绕接口组织。顶部给出当前阶段和阻塞原因,时间线展示关键事件,操作只在满足条件时出现。外部系统链接作为证据保留,让用户可以进入原系统确认细节。

前端状态也严格区分服务器事实与临时交互。流程状态来自服务端,不能因为前端乐观更新就假装发布完成;表单展开、筛选条件则属于本地。混淆二者会让刷新后出现矛盾。

Node.js 服务的边界

Node.js 适合整合大量 I/O 密集接口,也让前端团队能够快速参与全栈开发。但语言统一不代表职责可以混乱。

服务按入口、应用流程、领域规则和基础设施适配拆分。控制器只处理协议,应用层编排用例,领域层保存不依赖外部工具的规则,适配层负责 GitLab、部署与消息系统。这样更换外部接口时,不必改写流程语义。

数据一致性是另一个难点。数据库事务无法覆盖外部系统,我们不能假装一次跨系统操作具有真正原子性。更现实的做法是本地先记录意图,再通过任务推进外部状态;每一步可重复执行,并使用补偿或人工介入处理无法自动恢复的结果。

我们采用本地事务 + Outbox,避免数据库已经创建发布任务但队列消息丢失:

await db.transaction(async (tx) => {
  const release = await tx.release.create({ data: command });
  await tx.outbox.create({
    data: {
      topic: "release.requested",
      aggregateId: release.id,
      payload: JSON.stringify({ releaseId: release.id, version: release.version }),
    },
  });
});

独立发布器扫描未发送 Outbox,投递成功后标记。消息可能重复,因此 Worker 仍按 releaseId + version 幂等消费。Outbox 解决的是“不丢意图”,不保证只执行一次。

权限不是菜单显示

研发平台连接生产发布和代码权限,前端隐藏按钮远远不够。权限判断必须在服务端执行,并基于用户、组织、工程、环境和动作共同决定。

同时,权限模型需要能解释拒绝原因。用户被拒绝时应该知道缺少什么角色、由谁审批,而不是只看到“无权限”。这会减少大量线下沟通。

企业身份可以帮助统一登录,但外部系统中的账号映射仍要被管理。映射错误可能让操作以错误身份执行,因此关键动作保留操作者、审批者和外部执行身份。

内部平台也要有产品指标

上线功能数量不能说明平台价值。我们更关心重复操作是否减少、流程等待时间是否缩短、失败是否更容易恢复、人工沟通是否下降。

指标需要从事件中自然产生,而不是事后统计。创建、审批、构建、发布和失败都记录结构化事件,既用于用户时间线,也用于分析流程瓶颈。这样一套数据可以回答:需求主要卡在哪里,哪类构建最常失败,哪些环节仍然依赖人工。

{
  "releaseId": "rel_812",
  "event": "approval.requested",
  "actor": "user_17",
  "occurredAt": "2021-09-25T08:20:00Z",
  "metadata": { "environment": "production", "approverGroup": "ops" }
}

时间线告诉用户“正在等待 ops 审批”,聚合数据则能发现发布周期主要耗在等待而非构建。没有领域事件,团队只能从日志里事后拼流程。

但研发数据很容易被误用为个人绩效。平台应当分析系统效率,而不是把代码量或提交次数包装成生产力。指标一旦影响个人评价,行为会迅速围绕指标优化。

平台工程首先是约束设计

内部平台的吸引力在于它能把零散经验固化为默认路径:新项目自动获得仓库、检查与发布配置;变更沿着统一状态流转;失败拥有标准恢复入口。

它的风险也在这里。如果默认路径设计错误,错误会被放大到整个团队。因此平台不能只追求覆盖率,还要允许例外、保留逃生通道,并持续从真实使用中修正规则。

这个项目让我第一次系统地从页面走到流程、服务和组织协作。React 与 Node.js 是实现工具,真正需要构建的是一套可被理解、执行和改进的研发秩序。

研发平台由用户工作流、控制面和证据面组成的架构

图:操作入口只是表面,自助恢复和可靠执行才是平台价值。

内部平台也要用任务结果衡量

我们不再以页面访问量证明平台价值,而是看创建一次发布需要多少人工步骤、失败后平均多久恢复、多少任务能自助完成、多少问题仍要找平台同学手工处理。

每次新增能力都要减少一个明确摩擦:少复制一份配置、少等一次权限、少进入一台服务器。指标如果只让平台更忙,却没有让使用者更独立,功能数量再多也不算进步。