配置了字段却没存进去——配置驱动系统的「死字段」陷阱
做低代码或元数据驱动的后台系统时,有一类 bug 极其隐蔽:用户在配置表里增加了一个字段,前端表单也正常渲染、提交也返回 200,但数据库里那列永远是空的。排查半天才发现——这个字段从来没有落盘,是个「死字段」。
现象
某后台管理系统有一张「报表字段配置表」,通过 key、type、preset_enum_id 等元信息驱动前端动态渲染表单。运营配置了一个关联订单的下拉字段,开发上线后一切看起来正常:字段有显示、选了值、接口也返回 200。但翻数据库——那列一直是 NULL。
更诡异的变体出现在一个内部信息编辑器上:编辑器共 18 个字段,保存后立刻能看到新值(页面未刷新)。一旦刷新页面重新拉取,其中 7 个字段全部归零。
根因
这两个场景的根因是同一件事:「metadata 能配 ≠ 能落盘」。
配置层(report_columns / field_definitions)负责告诉前端「渲染什么字段」,但持久化层(orderService.create、formService.save 等)在写库时只认:
- 主表白名单(
MAIN_TABLE_FIELDS/allowedColumns)——严格比对 snake_case key - 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 表列数
# 差集就是死字段清单新增字段的唯一合法顺序:migration 加列 → 白名单/ext 感知 → metadata 配置。
同时在 create/update 通用 service 里对「被过滤掉的 key」打 warn log(开发环境可直接 throw),让「死字段」在开发期就暴露,而不是等到线上用户反复填了数据却丢失。
一句话外卖
配置层的字段定义只管渲染,持久化层有独立的白名单或表 schema;两者不同步,用户的输入就是无声消失。