从零构建动态表单引擎,告别重复劳动

15 阅读9分钟

一、引子:我们为何要“自找麻烦”?

在最近的几个项目中,我们反复遭遇这样一个场景:活动报名环节需要收集的用户信息,因活动而异,变化莫测。

活动A要求收集:姓名、性别、年龄、学校,以及单位信息和家长信息(这两者本身又是独立的复杂模型)。

活动B则要求收集:站点、规划、进度,以及同伴信息等。

...以此类推,每次都有新花样。

最初的应对策略:专题定制模式

起初,我们的做法简单“粗暴”:每来一个新活动,就为其量身定制一个专题项目。从数据库表结构,到后端接口,再到前端页面,全套流程都得修改一遍。虽然主体流程大同小异,但绝大部分修改都耗费在适配这些千变万化的信息结构上。

这种模式的真实写照

走得通,但很累: 如果活动不多、参与人少,尚可勉强支撑。但从开发到运营,整个团队都在“遭罪”。

定制与妥协并存: 好处是能快速响应大部分“临时需求”,但大方向不能动,某些需求仍需妥协。

边际成本过高: 说起来都是“报名”,但每次都要“全套服务”。尽管复制粘贴不会消耗太多脑力劳动,依然需要投入宝贵的时间和精力,因为量太大,牵一发而动全身,投入产出比极低。这终究不是长久之计。

二、破局思路:能否“配置”出一个表单?

从实际业务出发,我们开始思考:能否在每次活动开始前,通过一个后台系统,像搭积木一样配置好本次活动需要采集的信息?然后让客户端系统自动识别这些配置,并动态渲染出对应的表单。

核心目标:

  • 大流程不变: 主体业务流程保持稳定,这也是动态表单落地的大前提
  • 降低边际成本: 践行编程界的DRY原则,让类似的活动不再产生重复的开发工作。
  • 提升效率: 将开发人员从繁琐的重复劳动中解放出来。

理想很丰满,但挑战接踵而至:

  • 数据存储: 动态表单的数据怎么存?存到哪里?
  • 数据检索: 信息收集上来后,如何高效检索?
  • 数据验证: 提交的数据如何保证规范性?
  • 数据导出: 之前固定表单下,导出PDF/Excel相对简单,动态化之后如何实现?
  • 业务关联: 如何与后续复杂的业务流程无缝衔接?

这些问题在定制模式下都不是问题,但在新路线下,却成了我们必须攻克的道道难关。

三、解决方案:“元数据”驱动的动态表单引擎

我的解决思路是构建一个 “元数据”驱动的表单引擎。其核心生命周期如下图所示:

  1. 表单定义
  2. 动态获取
  3. 表单渲染
  4. 数据存储
  5. 业务关联

整个流程可以概括为以下几个核心阶段:

阶段一:表单定义 - 系统的基石

这相当于我们定义“积木”和“图纸”的过程。

定义表单元素

我们预先定义了一系列基础组件的元数据模板,例如:

  • 独立输入框
  • 级联输入框
  • 独立下拉框
  • 级联下拉框
  • 独立单选框
  • 上传组件(用于照片等场景)

每个元素都通过一个结构化的JSON Schema来描述。例如,一个“独立输入框”的元数据如下:

{
  "key": "name",
  "label": "姓名",
  "type": "input",
  "props": {
    "placeholder": "请输入您的姓名",
    "maxlength": 10,
    "dataType": "string"
  },
  "validation": {
    "required": true,
    "message": "姓名不能为空",
    "pattern": "^[\u4e00-\u9fa5]{2,10}$"
  }
}

关键点: 在实际配置时,运营人员无需接触复杂的JSON,只需在友好的表单页面中填写关键信息,系统会自动生成最终的Schema。

test.gif

组合表单角色

将定义好的表单元素,像拼图一样组合到不同的“角色”中。

  • 例如,将“姓名”、“联系方式”、“家庭住址”组合成 “个人信息” 角色。
  • 将“职称”、“职务”、“技能”组合成 “个人能力” 角色。

最终,系统会产出一个完整的、可供前端直接使用的表单Schema。

{
    "DecMainId": xxx,
    "AssociatedName": "DecProjectDetail",
    "DynamicRoleName": "\u8EAB\u4EFD\u4FE1\u606F",
    "DynamicRoleId": xxx,
    "Fields": [
        {
            "Key": "IdCardType",
            "Label": "\u8BC1\u4EF6\u7C7B\u578B",
            "Type": "select",
            "Props": {
                "Placeholder": "\u8BF7\u8F93\u5165\u8BC1\u4EF6\u7C7B\u578B",
                "Maxlength": null,
                "Format": null,
                "Default": null,
                "DataType": "string",
                "Rows": null,
                "Multiple": false,
                "Options": [
                    {
                        "Value": "0",
                        "Label": "\u8BF7\u9009\u62E9\u9002\u7528\u7684\u8BC1\u4EF6\u7C7B\u578B",
                        "Meta": null
                    },
                    //...其他
                ],
                "FieldMapping": {
                    "Value": "",
                    "Label": ""
                },
                //...其他属性
            },
            "Validation": {
                "Required": true,
                //...其他属性
            }
        },
        {
            "Key": "IdCard",
            "Label": "\u8BC1\u4EF6\u53F7\u7801",
            //...其他属性
        },
        {
            "Key": "Name",
            "Label": "\u59D3\u540D",
            //...其他属性
        }
    ]
}

阶段二:动态获取与渲染

获取: 前端通过固定的API接口,传入活动ID和角色ID,即可获取到对应的表单Schema。

渲染: 前端根据Schema中的type、label、props等信息,动态渲染出真实的表单控件。这部分涉及具体的前端技术选型,本篇不做深入探讨

当用户填写后,提交的数据结构非常简单:

{
  "decProjectId": xx,
  "dynamicRoleId": xx,
  "decProjectDetailId": xxx,
  "formData": {
      "UnitName": "落云宗",
      "UnitAddress": "天南大陆",
      "Principal": "韩立",
      "Duty": "太上长老",
      "ConnectPhone": "xxx",
      "MobilePhone": "+xxx",
      "Email": "abc@xxt.com",
      "Fax": "",
      "PostCode": "xxx"
  }
}

阶段三:数据存储与验证 - 守护数据安全的防线

数据到达服务端后,将经历严格的“安检”流程。

非业务验证 - 数据规范性检查

这层验证完全依据我们之前定义的Schema规则来执行,是数据安全的第一道防线。我的验证器会按顺序执行以下检查:

  • 必填校验: 字段是否必须填写。

  • 格式校验: 日期、自定义格式等是否正确。

  • 数值范围校验: 数字是否在最小/最大值范围内。

  • 正则校验: 是否符合预设的正则表达式。

  • 级联动态验证: 例如,当“证件类型”选为“身份证”时,才按身份证规则验证“证件号码”。

业务验证 - 逻辑正确性检查

通过规范性验证后,数据进入业务层校验。例如:

  • 活动是否在报名期内?

  • 报名名额是否已满?

  • 信息是否重复?

关于“判重”的性能优化:

在动态表单场景下,精准判重非常复杂。为了提升性能,我们采用了“两级判重”策略:

粗略判重: 在精准判重前,先使用高性能的xxHash算法计算整个表单数据的哈希值,与历史记录对比。这能快速过滤掉完全相同的重复提交。

精准判重: 再根据具体业务规则进行更细致的检查。

public static ulong ComputeFormHash64(object formData, JsonSerializerOptions? options = null)
{
    if (formData == null)
        throw new ArgumentNullException(nameof(formData));

    var jsonBytes = SerializeToJsonBytes(formData, options);
    return XxHash64.HashToUInt64(jsonBytes);
}

常用Git的小伙伴应该对这个比较熟悉,我们用“git log --oneline”查看提交记录的时候,前面那个标记,就是用这个算法生成的,既能保证足够的运算空间避免产生碰撞,性能又快!目前大部分开发语言都原生支持xxHash系列算法了,在安全不敏感的场景,完全可以替代早先的md5,SH1等哈希算法。

数据脱敏 - 合规性保障

根据《网络安全法》等法规要求,敏感信息必须脱敏存储。

当前策略: 通过预定义的敏感字段关键词(如IdCard, Phone, Mobile)进行匹配和脱敏。

未来展望: 结合AI,对字段值内容进行智能识别,实现更精准的脱敏。

关于脱敏这一块,单拎出来也能讲好多,道友们需要注意的是,2026年起新的国家网络完全法就要实施了(www.ghxrd.gov.cn/zlk_0/zcfg/…),对数据安全的要求更为严格,建议早做打算。笔者之前有过一篇相关的博客,各位道友也可以自行找一下相关测材料。

保存

以上所有步骤后,数据最终被安全地存入数据库。

四、业务关联与AI增强*

动态表单的落地远不止于数据的收集。真正的挑战在于如何让这些动态数据重新赋能业务。

业务关联: 我们正在设计动态模板,使得导出PDF报名表、生成Excel统计报表等功能,也能像表单一样通过配置实现,确保管理效率不因动态化而降低。

AI增强: 我们计划引入AI能力,用于更智能的敏感信息识别、表单填写辅助,甚至自动生成表单模板,让系统变得更加智能。

这一趴,也是我们正在努力攻克的地方,所以目前还没什么能拿出来分享。

五、结语

构建动态表单引擎,是一个将不确定性封装为确定性的过程。它初期投入更大,挑战更多,但一旦建成,将从根本上扭转我们被动应对需求的局面。

这条路还很长,本文仅抛砖引玉,分享了我们在“表单定义”“数据存储与验证”方面的初步实践以及解决问题的方向,代码没有贴很多,实际上上面聊掉的那几块每一块单拎出来都能聊好久,后面有机会在接着聊,届时相关的代码也会贴出来请各位道友交流指正。

好了,这次就到这,算是及时记录一下开发进度,换换脑子,接着赶项目去啦😭~