返回博客

级联下拉永远「暂无选项」——parent_field_key 指向了 alias 而不是实际存储 key

我们做了一个元数据驱动的低代码表单平台,支持 preset_enum 级联下拉(类似省市区那种多级联动)。某天有用户反馈,某张单据的二级下拉展开后始终显示「暂无选项」,换了浏览器也没用,清缓存也没用。更诡异的是:用户明确说父字段已经选了值,而其他页面的省市区级联工作完全正常。

前端同学捋了半天 Context Provider 和 useCascadeField hook,逻辑一切看起来都对。后来数据库一查,真相让人哭笑不得。

现象

  • 级联子字段打开后显示「暂无选项」或 placeholder 停在「请先选择上级字段」
  • 用户确认父字段已有值(在前序审批节点或编辑态已保存)
  • form_data JSON,父字段的业务值确实存在
  • 其他同类型级联(比如省市区)工作正常
  • UI 架构层(Context / hook)看不出任何异常

根因

问题不在前端,在 L1 元数据配置层

同一个业务概念(比如「评审系列」)在字段配置表里有两条记录:一条 key 叫 titleSeries,另一条叫 reviewSeries。它们指向完全相同的 preset_enum_idlinked_enum_level,本质上是同一个东西的两个名字。

问题在于:

  • 子字段的 parent_field_key 配置指向了 titleSeries
  • 但实际写入 form_data 的父值,历史上走的是另一条入口,存的是 reviewSeries
  • 级联引擎拿 titleSeriesform_data 里找,什么都没有,于是子字段得不到父值,永远显示「暂无选项」

这是 SSOT(单一真源)在 DB 层的分裂:同一业务概念有多个 key,不同历史代码路径各走各的,cascade 链就这么断了。

坑:parent_field_key 指向的 key ≠ form_data 里实际存的 key

字段配置表里同一业务概念可能存在多条记录(alias key),不同写入路径历史上各自用了不同的 key。parent_field_key 只能指向一个,一旦配错,子字段永远读不到父值,但前端和 hook 层毫无报错,极难定位。

排查三步法

Step 1:查子字段的 parent_field_key 指向谁

SELECT `key`, title, parent_field_key, linked_enum_level, preset_enum_id
FROM field_config
WHERE `key` = '<子字段 key>' AND is_deleted = 0;

记下 parent_field_key 的值,假设是 titleSeries

Step 2:查 form_data 里实际存的 key

SELECT
  JSON_EXTRACT(form_data, '$.titleSeries') AS via_config_key,
  JSON_KEYS(form_data) AS all_keys
FROM submissions WHERE id = '<记录 id>';

如果 via_config_key 是 NULL,但 all_keys 里能看到另一个同义 key(比如 reviewSeries)——确诊

Step 3:确认 SSOT 分裂

SELECT `key`, title, preset_enum_id, linked_enum_level
FROM field_config
WHERE preset_enum_id = '<相同 enum id>'
  AND linked_enum_level = '<相同 level>'
  AND is_deleted = 0;

返回 > 1 行,说明同一业务概念被定义了多个 key,这是根因。

正解

救火(1 分钟):先统计哪个 key 在 form_data 里覆盖率更高,把 parent_field_key 改成实际存储的那个 key。

-- 先统计两个 key 的覆盖情况
SELECT
  SUM(JSON_EXTRACT(form_data, '$.titleSeries') IS NOT NULL) AS title_count,
  SUM(JSON_EXTRACT(form_data, '$.reviewSeries') IS NOT NULL) AS review_count
FROM submissions;
 
-- 确认哪个多,再改配置
UPDATE field_config
SET parent_field_key = 'reviewSeries'
WHERE `key` = '<子字段 key>' AND is_deleted = 0;

根治(小半天):选定 canonical key,迁移历史数据,软删 alias 定义,并在全仓库 grep 替换所有旧 key 引用。

-- 把 alias key 的历史值迁移到 canonical key
UPDATE submissions
SET form_data = JSON_SET(
  JSON_REMOVE(form_data, '$.titleSeries'),
  '$.reviewSeries',
  JSON_EXTRACT(form_data, '$.titleSeries')
)
WHERE JSON_EXTRACT(form_data, '$.titleSeries') IS NOT NULL
  AND JSON_EXTRACT(form_data, '$.reviewSeries') IS NULL;
 
-- 软删 alias 字段定义
UPDATE field_config SET is_deleted = 1 WHERE `key` = 'titleSeries';
正解:先统计覆盖率,再决定迁移方向

不要直接改配置——若生产数据里两个 key 都有历史存量,改配置只会让另一半失效。先 COUNT 统计各 key 的覆盖量,确定 canonical key 后再做数据迁移 + 配置收敛。

预防:新增字段前先扫一遍同义 key

每次新增字段配置前,先 SELECT * FROM field_config WHERE title LIKE '%业务概念%' 扫一遍,确认没有同义重复定义。有条件的话,给 preset_enum_id + linked_enum_level 组合加 CI 唯一性校验,从源头杜绝 SSOT 分裂。

一句话外卖

级联子字段读不到父值,八成不是前端 hook 的问题——先查 parent_field_key 指向的 key 和 form_data 里实际存的 key 是不是同一个。