返回博客

审批表单字段集来源读错节点,四轮修复全部打在症状上

在一个内部审批平台里,我们的工单详情页支持多节点工作流——发起 → 主管分配 → 评审 → 审批 → 结束。每个节点有自己独立的必填/选填/自动字段配置。理论上,审批人打开表单时,只该看到「当前节点」对应的字段集。

但是用户反馈说:在主管分配节点,页面却展示了 20 多个「数据录入员」才需要填的字段,评审节点同理。每轮补丁修复后,新 bug 照常出现,连续四轮都没彻底解决。

现象

  • 主管分配节点(node-2)页面上渲染出 24 个必填 + 18 个选填字段
  • 其中大量字段是 node-1(发起节点)才该显示的内容
  • 级联联动(cascade)、脏字段 diff、锁定 icon 的逻辑后来各改了一轮,症状仍然部分存在
  • 四次修复均无法根治,只是让部分字段的交互行为"看上去稍好一点"

根因

审批详情页在计算「渲染哪些字段」时,代码里写了这样一段:

// 反模式:硬读发起节点
const startNode = workflow.nodes.find((n) => n.type === 'start')
const requiredFields = startNode?.requiredFields ?? []
const optionalFields = startNode?.optionalFields ?? []

approval.current_node_id 随工作流推进会不断移动(node-1 → node-2 → node-3 …),但字段集来源始终钉在 start。后续的所有逻辑——级联 Provider、脏 diff 计算、锁定状态——都以「这就是当前节点的字段」为前提在运行。它们都做对了,只是对着一份错误的数据在操作。

坑:后续所有层的正确性建立在 Layer 0 正确的前提上

把根因分层来看:

Layer 0(字段集来源)  ← 读错节点 → 以下所有层都在操作错误数据
  ↓
Layer 1(级联联动)    ← 读 "columns" 来自 Layer 0
  ↓
Layer 2(脏字段 diff) ← 读 formValues 的 key 来自 Layer 0
  ↓
Layer 3(锁定 icon)   ← 读 fieldStatus 来自 Layer 0
  ↓
Layer 4(渲染)        ← FieldValueEditor 按字段列表渲染

Layer 0 错了,Layer 1-4 行为都「正确地执行了错误任务」。在 Layer 1/2/3 各打一轮补丁,本质上是在移动甲板上的椅子。

还有一种「安全兜底」写法更危险:

// 更隐蔽的反模式:fallback 到 start
const node =
  workflow.nodes.find((n) => n.id === approval.currentNodeId) ??
  workflow.nodes.find((n) => n.type === 'start')

这段代码对「恰好在发起节点的用户(通常是 QA 或管理员)」完全正常,却对所有处于下游节点的真实用户静默返回错误字段集。测试通过,线上炸。

正解

approval.current_node_id 出发,精准匹配当前节点,匹配不到就返回空配置,绝不 fallback 到 start:

export function resolveCurrentNodeFieldConfig(
  nodes: WorkflowNodeFieldConfig[] | undefined | null,
  currentNodeId: string | undefined | null
): ResolvedNodeFieldConfig {
  if (!nodes || nodes.length === 0) return EMPTY_CONFIG
  if (!currentNodeId) return EMPTY_CONFIG
 
  const nodeIdx = nodes.findIndex((n) => n.id === currentNodeId)
  if (nodeIdx < 0) return EMPTY_CONFIG  // 不 fallback,直接返回空
  const node = nodes[nodeIdx]
  return {
    requiredFields: node.requiredFields ?? [],
    optionalFields: node.optionalFields ?? [],
    autoFields: node.autoFields ?? [],
  }
}

调用时:

const currentNodeCfg = resolveCurrentNodeFieldConfig(
  wf.nodes as WorkflowNodeFieldConfig[],
  found.currentNodeId   // ← 来自 approval 记录,随流程实时更新
)
requiredFields = currentNodeCfg.requiredFields
optionalFields = currentNodeCfg.optionalFields
autoFields = currentNodeCfg.autoFields
正解:Layer 0 修对,所有下游不用动

修完字段集来源后,级联联动、脏 diff、锁定 icon 一个补丁都没有再动,行为全部自动正确。这印证了:四轮症状修复积累的代码复杂度,本可以用一次根因定位来替代。

验收清单:

  • grep -n "nodes.find\|nodes\[0\]\|type === 'start'" 在审批详情页无结果
  • 存在测试:「node-2 返回 node-2 配置,而非 node-1 配置」
  • currentNodeId 匹配失败时函数返回 EMPTY_CONFIG,而非 start 节点字段

一句话外卖

在多层数据流中,一旦最上游的「数据来源」是错的,下游所有层的修复都是在对着错误数据做正确操作——先定位 Layer 0,再动任何 Layer N。