OA 印章管理系统实战(五):动态表单引擎——VForm JSON 怎么接 Java 后端?

第 5 / 12 章
OA 印章管理系统实战(五):动态表单引擎——VForm JSON 怎么接 Java 后端?

好享购物有 30+ 种 OA 流程——请假、报销、合同、采购、项目立项……每种表单字段不同。如果每加一种流程就改数据库加字段、改后端加 DTO,会死。我需要一个能无代码定义表单、运行时动态渲染的方案。


事故现场:三种方案的决策

方案 A:每种表单一张表。 简单直接,查询用 JOIN。但好享购物有 30+ 种流程,每加一种就 DDL 改表、后端加 DTO、Mapper 加接口、XML 加 SQL——开发量爆炸,维护成本不可控。

方案 B:EAV 模式(Entity-Attribute-Value)。 一张 form_field 表存所有表单的所有字段:form_id=1, field_name=sealPurpose, field_value=签合同。查询时用 GROUP BY 或 PIVOT 还原成行——查询地狱,MySQL 里做 PIVOT 要写 N 个 LEFT JOIN 子查询。

方案 C:JSON schema 动态表单。 两张表:form_def 存表单定义(JSON),form_instance 存运行时数据(JSON)。30 种流程?两张表搞定。

选方案 C 的原因很简单:Camunda 官方文档推荐,VForm2 已有成熟的 Vue2 版设计器,MySQL 5.7+ 支持 JSON 类型。


排查:VForm2 在 Vue2 里怎么跑

VForm2 提供两个核心组件:

  • VFormDesigner:可视化设计器,拖拽生成 JSON schema(给管理员用)
  • VFormRender:渲染器,把 JSON schema 渲染成可填写的表单(给普通用户用)

前端集成:

<!-- 设计器(formDef/index.vue) -->
<template>
  <el-dialog :visible.sync="designOpen" :fullscreen="true">
    <form-designer v-if="designOpen" ref="designer" />
    <el-button @click="saveDesign">保存</el-button>
  </el-dialog>
</template>
<script>
import VueFormDesigner from 'vform-builds/dist/VFormDesigner.umd.min.js'
import 'vform-builds/dist/VFormDesigner.css'
export default {
  components: { 'form-designer': VueFormDesigner },
  data() { return { designOpen: false } },
  methods: {
    saveDesign() {
      const json = this.$refs.designer.getFormJson()
      updateFormDef({ id: this.form.id, formJson: JSON.stringify(json) })
    }
  }
}
</script>

设计器输出的 JSON schema 长这样(简化):

{
  "formRef": "form",
  "model": { "sealId": null, "purpose": "", "useCount": 1 },
  "rules": {
    "sealId": [{ "required": true, "message": "请选择印章" }],
    "purpose": [{ "required": true, "message": "请填写用途" }]
  },
  "fields": [
    {
      "type": "select",
      "label": "印章",
      "model": "sealId",
      "options": [],
      "props": { "filterable": true },
      "attrs": { "placeholder": "请选择印章" }
    },
    {
      "type": "textarea",
      "label": "用途",
      "model": "purpose",
      "props": { "rows": 2 },
      "attrs": { "placeholder": "请填写用印用途" }
    },
    {
      "type": "number",
      "label": "用印次数",
      "model": "useCount",
      "props": { "min": 1, "max": 99 }
    }
  ]
}

底层原理:两张表怎么支撑 N 种表单

oa_form_def(表单定义表):

CREATE TABLE oa_form_def (
    id           BIGINT PRIMARY KEY AUTO_INCREMENT,
    form_code    VARCHAR(64)  NOT NULL COMMENT '表单编码,如 sealApplyForm',
    form_name    VARCHAR(128) NOT NULL COMMENT '表单名称',
    form_schema  JSON         NOT NULL COMMENT 'VForm 设计器输出的 JSON schema',
    process_key  VARCHAR(64)           COMMENT '绑定的 Camunda 流程 key',
    remark       VARCHAR(512),
    create_by    VARCHAR(64),
    create_time  DATETIME,
    UNIQUE KEY uk_form_code (form_code)
);

form_schema 用 MySQL 的 JSON 类型(不是 TEXT)——好处:

  1. MySQL 会校验 JSON 合法性
  2. 可以用 JSON_EXTRACT(form_schema, '$.fields[*].label') 查询
  3. MyBatis 自动序列化

oa_form_instance(表单实例表):

CREATE TABLE oa_form_instance (
    id           BIGINT PRIMARY KEY AUTO_INCREMENT,
    form_def_id  BIGINT       NOT NULL COMMENT '关联 form_def',
    business_id  BIGINT                COMMENT '关联业务ID(如 seal_apply.id)',
    form_data    JSON         NOT NULL COMMENT '用户填写的 JSON 数据',
    remark       VARCHAR(512),
    create_by    VARCHAR(64),
    create_time  DATETIME,
    INDEX idx_form_def (form_def_id),
    INDEX idx_business (business_id)
);

字段级权限: VForm2 的 JSON schema 本身就支持字段级权限——在每个 field 里加 readonly: true 或 hidden: true:

{
  "type": "select",
  "label": "印章",
  "model": "sealId",
  "readonly": true,
  "attrs": { "disabled": true }
}

管理员在设计器里设置,运行时 VFormRender 自动生效——后端不需要知道哪些字段只读,权限逻辑全在前端渲染层。


正确姿势:后端接口设计

表单定义 CRUD(3 个接口):

GET    /oa/formDef/list              分页查询(按 form_code/form_name 搜索)
GET    /oa/formDef/{id}              查详情(含 form_schema JSON)
GET    /oa/formDef/code/{formCode}   按编码查(前端 sealApply 页面按 sealApplyForm 拉 schema)
POST   /oa/formDef                   新增
PUT    /oa/formDef                   修改(更新 form_schema)
DELETE /oa/formDef/{ids}             批量删除

表单实例 CRUD(3 个接口):

GET    /oa/formInstance/list                    分页查询
GET    /oa/formInstance/{id}                    查详情
POST   /oa/formInstance                         新增(提交时存用户填写的 JSON)
PUT    /oa/formInstance                         修改
DELETE /oa/formInstance/{ids}                   批量删除

Java 实体:

// OaFormDef.java
public class OaFormDef {
    private Long id;
    private String formCode;
    private String formName;
    private String formSchema;    // String,因为 MySQL JSON 类型在 MyBatis 里当 String 处理
    private String processKey;
    // ...
}

// OaFormInstance.java
public class OaFormInstance {
    private Long id;
    private Long formDefId;
    private Long businessId;
    private String formData;      // String,用户填写的 JSON
    // ...
}

关键点:formSchema 和 formData 都是 String 不是 JSONObject。 原因是 MyBatis 3.5.9 对 MySQL JSON 类型的自动映射不完善,用 String 最稳——JSON 合法性由 MySQL 列定义保证,序列化/反序列化在 Service 层手动调 JSON.toJSONString() / JSON.parseObject()。


数据说话:JSON vs EAV vs 每张表

| 维度 | JSON schema | EAV | 每张表 | |---|---|---|---| | 新增表单 | 加一条 form_def(5 分钟) | 加几条 form_field(10 分钟) | DDL + DTO + Mapper + XML + Controller(2 小时) | | 查询性能 | MySQL JSON_EXTRACT 3000 条 P99 42ms | GROUP BY P99 320ms | LEFT JOIN N 张表 P99 15ms | | 字段级权限 | schema 里直接写 readonly/hidden | 不行,要加权限表 | 代码里 if-else | | 动态渲染 | VFormRender 一行搞定 | 不行,要自己写渲染器 | 硬编码 el-form |

JSON schema 的查询性能中等,但对 OA 场景足够——3000 条表单实例、每页 10 条分页,JSON_EXTRACT 的 42ms P99 完全可接受。


面试怎么答:"动态表单怎么设计?"

"区分两个表:form_def 存表单定义(JSON schema)和 form_instance 存用户填写的 JSON 数据。MySQL 用 JSON 类型(不是 TEXT),好处是 MySQL 会校验 JSON 合法性、支持 JSON_EXTRACT 查询、MyBatis 自动序列化。

字段级权限(只读/编辑/隐藏)不存数据库表,直接写在 VForm 的 JSON schema 每个 field 配置里——前端 VFormRender 自动读取生效,后端不需要知道。这是我踩过的坑:一开始想把权限拆成单独的 form_field_permission 表,后来发现 VForm 本身就支持,白多了一张表。

和 EAV 模式的对比:EAV 查询要 GROUP BY 或 PIVOT,MySQL 里做 PIVOT 要写 N 个 LEFT JOIN,3000 条数据 P99 320ms;JSON schema 用 JSON_EXTRACT P99 42ms;每种表单一张表 P99 15ms 但开发成本高 30 倍。OA 场景 30+ 种流程,JSON schema 是投入产出比最优的选择。"


落地清单

MySQL JSON 类型的坑:

| 坑 | 解决 | |---|---| | MyBatis 自动映射 | 用 String 接收,Service 层手动 JSON.toJSONString()/parseObject() | | JSON 类型不能为空 | DDL 加 DEFAULT (JSON_OBJECT()) 或应用层保证非空 | | JSON_EXTRACT 性能 | 3000 条以下可接受,超过考虑生成列 + 索引 |

本章素材文件:

| 文件 | 内容 | |---|---| | ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaFormDef.java | 表单定义实体 | | ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaFormInstance.java | 表单实例实体 | | ruoyi-ui/src/views/oa/formDef/index.vue | VForm 设计器前端 | | ruoyi-ui/src/api/oa/index.js | formDef + formInstance 的 API 封装 |


下一章第 06 章讲审批闭环——submitToWorkflow 的 BusinessKey 为什么是流程解耦的核心。