返回博客

配置了字段却没存进去——配置驱动系统的「死字段」陷阱

做低代码或元数据驱动的后台系统时,有一类 bug 极其隐蔽:用户在配置表里增加了一个字段,前端表单也正常渲染、提交也返回 200,但数据库里那列永远是空的。排查半天才发现——这个字段从来没有落盘,是个「死字段」。

现象

某后台管理系统有一张「报表字段配置表」,通过 keytypepreset_enum_id 等元信息驱动前端动态渲染表单。运营配置了一个关联订单的下拉字段,开发上线后一切看起来正常:字段有显示、选了值、接口也返回 200。但翻数据库——那列一直是 NULL

更诡异的变体出现在一个内部信息编辑器上:编辑器共 18 个字段,保存后立刻能看到新值(页面未刷新)。一旦刷新页面重新拉取,其中 7 个字段全部归零。

根因

这两个场景的根因是同一件事:「metadata 能配 ≠ 能落盘」

配置层(report_columns / field_definitions)负责告诉前端「渲染什么字段」,但持久化层(orderService.createformService.save 等)在写库时只认:

  1. 主表白名单MAIN_TABLE_FIELDS / allowedColumns)——严格比对 snake_case key
  2. ext / 动态表的真实列名——SHOW COLUMNS FROM <ext_table> 有没有这列

不在任何一个中的 key,value 会被静默丢弃。没有错误、没有警告、没有日志,就像那行代码从不存在。

第二个变体更具迷惑性:该系统的信息表是纯列存储,不像其他模块有 JSON blob 兜底。PATCH 接口把 7 个新值写进内存对象后直接 return company(内存里带着新值),前端看到 200 响应里有数据,界面也不会刷新——所以用户误以为保存成功了。等下次页面加载重新 GET,mapCompanyRow 把数据库行映射回对象时,7 个没有对应列的字段就全部消失了。

坑:配置层加了字段,持久化层不知道

report_columns / field_definitions 表里的 key 只决定前端渲染。持久化层按自己的白名单或实际表列做过滤,无匹配的 key 会被静默丢弃,不抛错、不日志。

PATCH 接口若返回内存对象(而非数据库回读),还会制造「保存成功」的假象——刷新页面后值才消失,很容易被当成缓存或网络问题追查。

正解

新增任何配置驱动字段,必须走「数据 schema 先行」的顺序:

1. 先加 DB 列(migration)
2. 再把 snake_case key 加进主表白名单 / ext 表 schema
3. 最后才在 metadata 配置表里新增字段记录

若是纯列存储,还要同步四处:

// 写路径
companyInsertSql   // INSERT 语句列表
companyUpsertSql   // UPDATE SET 语句列表
companySqlParams   // 参数绑定
 
// 读路径
SELECT <col> FROM wf_companies   // SELECT 列表
mapCompanyRow(row)               // 行→对象映射

漏掉任意一处,该字段仍是死字段。

快速排查「填了但没保存」的配置驱动字段:

# 1. 看字段 metadata
SELECT `key`, type, preset_enum_id FROM field_definitions WHERE ...
 
# 2. 看主表白名单
grep -A 30 'MAIN_TABLE_FIELDS\|allowedColumns' services/*-service.ts
 
# 3. 看 ext 表实际列
SHOW COLUMNS FROM <ext_table>
 
# 4. 比对差集:编辑器 FIELDS 数组 vs 表列数
# 差集就是死字段清单
正解:数据 schema 先行,白名单同步

新增字段的唯一合法顺序:migration 加列 → 白名单/ext 感知 → metadata 配置

同时在 create/update 通用 service 里对「被过滤掉的 key」打 warn log(开发环境可直接 throw),让「死字段」在开发期就暴露,而不是等到线上用户反复填了数据却丢失。

一句话外卖

配置层的字段定义只管渲染,持久化层有独立的白名单或表 schema;两者不同步,用户的输入就是无声消失。