SpringBoot3+Vue3 低代码业务表单:从拖拽设计、独立建表到审批与数据查询
🌐 文档地址 :https://ruoyioffice.com
👇 文章底部获取源码和演示地址 👇
💬 :17156169080(获取产品咨询)
采购部门让你做一张申请表,拖几个控件很快就能完成。一个月后,他们开始追问:能按项目统计预算吗?能关联采购订单吗?增加"交付日期"后,以前的申请还看得懂吗?低代码真正进入业务,往往不是从拖拽完成开始,而是从这些问题开始。

▲ 设计器负责表达输入,模型负责管理定义,独立表负责保存业务事实。三者分开,才有演进空间。
一、先把目标说清:我们要的不是另一张电子表格
业务表单是结构化信息的入口。
它既可能只是一次审批所需的输入,也可能成为后续采购、核算和统计的业务依据。
这两种用途看起来都在"填表",但对数据的要求完全不同。
如果只是申请参加一次活动,审批结束后查阅一下原始内容,流程变量可能已经够用。
如果是采购申请,后面需要逐行关联商品、汇总金额、核对到货情况,表单数据就不能只被当成审批引擎的附属信息。
采购负责人关心"这个项目到底申请了多少预算",不是"有哪些流程实例的 JSON 包含这个项目名称"。
开发人员需要先理解这种差别,再决定存储方式。
RuoYi Office 的流程表单提供了设计、存储方式选择、独立表模型管理和流程绑定等能力。
本文沿着一张采购申请的业务需求,解释这些能力如何组合。
截图使用本地已有的出差申请表单,展示同一套设计器和独立表能力;采购金额与明细是贯穿文章的说明案例,不伪装成截图中的真实单据。
| 使用者 | 真正需要的能力 | 仅有拖拽页面还缺什么 |
|---|---|---|
| 业务配置人员 | 调整字段、明细与填写规则 | 变更影响和发布边界 |
| 申请人与审批人 | 填写、查看、按节点处理 | 单据身份与审批状态绑定 |
| 管理人员 | 按项目、部门、期间查询 | 稳定字段与可控查询口径 |
| 系统开发人员 | 接入采购、打印和其他模块 | 数据契约与版本兼容 |
本文不把低代码描述为"完全不需要开发"。
更合理的目标是:重复的数据采集与基础审批可以配置完成,复杂计算、跨系统一致性和访问治理仍保留明确的工程入口。
1.1 一张采购申请会经历什么
假设申请人要购买三种办公设备,每一行都有名称、数量、单价、预计交付日期。
申请单上还有申请部门、项目、用途和总预算。
审批人可以核减数量,但必须留下最终核准结果。
后续采购订单要引用申请单,管理者还要按项目汇总已批准金额。
这时至少存在四种不同的对象:
- 页面定义:用户看到哪些控件、怎样排版。
- 业务模型:哪些字段进入主表,哪些进入明细表。
- 流程定义:由谁审批、何时允许修改、使用哪版表单。
- 业务单据:某次申请实际填写了什么、现在处于什么状态。
如果四者共用一份不断修改的 JSON,最先出现的问题通常不是性能,而是解释不清历史。
今天设计器里的字段,未必就是上个月申请人实际填写的字段。
1.2 先做数据承诺,再谈配置效率
所谓数据承诺,是系统愿意向下游保证哪些内容长期稳定。
例如申请单主键不变、明细关联有依据、审批结果可以识别、历史字段能够解释。
拖拽设计可以非常灵活,业务数据却不能无限随意。
字段标识、金额精度、日期含义和关联键一旦被其他模块使用,就已经变成了接口契约。
低代码平台的成熟程度,不只是控件数量多,而是能否把这种契约显式管理起来。
二、选择存储方式,就是选择这张表单的未来
独立数据表是为某类表单建立可识别的主表与明细表,用业务列保存单据内容。
它与流程变量、绑定已有物理表,是不同的使用策略。
| 方式 | 适合的需求 | 需要接受的边界 |
|---|---|---|
| 流程变量 | 数据主要服务于审批和回看 | 跨单统计、复杂关联需另做处理 |
| 独立数据表 | 新业务需要稳定存储和扩展查询 | 必须管理发布、字段与数据库适配 |
| 绑定已有物理表 | 已有业务数据需要接入流程 | 需要对齐原有主键、字段和写入责任 |
这不是"独立表一定优于流程变量"的排名。
如果只需要一个短期报名审批,独立建表反而可能带来不必要的维护成本。
如果已有完整采购模块,再新建一张功能相同的独立表,则可能形成两个相互竞争的申请台账。
2.1 用三个问题做判断
第一,审批结束之后,是否还有其他业务使用这些数据?
如果采购订单、合同或者费用核算还会引用它,就需要把单据身份和数据契约摆到前面。
第二,查询是否依赖稳定的业务字段?
按部门、金额区间、交付日期筛选,与搜索一段描述文字,是两种完全不同的查询。
第三,是否已经存在业务表及其责任服务?
如果有,就应评估复用或者绑定,而不是因为设计器支持建表就复制一套存储。
这三个问题能帮团队把"想要低代码"转化为更具体的架构选择。
2.2 新建、保存、发布应当分开理解
保存设计意味着保留配置草稿。
发布模型意味着把确认后的定义转成可执行的数据模型,并登记版本。
把表单绑定到流程后,还要关注流程定义是否已经使用新的版本。
这些操作的用户界面可以做得很顺畅,但语义不能混在一起。
否则业务人员以为"刚刚点了保存,所有在途申请就都升级了",开发人员却以为"只保存了设计器草稿",双方会对同一动作产生完全不同的预期。

▲ 本地已有出差申请设计器:关注独立数据表标识、已发布版本和数据表入口,而不是把截图中的表单名称当作本文采购案例。
三、把页面字段落成业务模型,先解决"它是谁"
字段标题用于阅读,字段标识用于程序访问,组件身份用于识别设计变更。
这三者不应被当成同一个值。
"预估金额"改成"申请预算",可能只是业务文案变化。
把文本输入框改成金额控件,则可能涉及类型、精度和历史值转换。
删除一个字段后,又新建一个同名字段,也未必是恢复了原来的字段。
因此,识别变更不能只比较标题。
3.1 主表和明细表回答不同问题
申请部门、申请事由、项目和申请日期,通常属于单据主表。
设备名称、数量、单价和交付日期,属于可重复的明细行。
总预算即使可以由明细计算,也仍然需要明确它是实时派生值,还是审批时确认的快照值。
这不是"多存一个字段还是少存一个字段"的小优化,而是后续解释金额的依据。
| 业务信息 | 建模位置 | 要明确的含义 |
|---|---|---|
| 申请部门 | 主表 | 申请时部门还是当前部门 |
| 设备数量 | 明细表 | 申请数量还是核准数量 |
| 交付日期 | 明细表 | 纯日期,不应自动加入时区偏移 |
| 核准总预算 | 主表或核准快照 | 是否允许随着明细变化重算 |
原有设计器中,独立表字段会经过规则解析,形成主表、明细表和列的模型描述。
发布时再与登记模型比较,计算哪些内容新增、变更、停用或移除。
对于已发布字段,前端还会围绕组件标识和字段标识做保护。
这样,用户修改控件表现时,不至于轻易让系统把它当成另一个全新字段。
3.2 一张 ER 图,分清定义和事实

▲ 表单版本记录定义,业务主表记录一次申请,明细通过父单据关联;图中的关系是逻辑关系,不代表都存在数据库外键。
这里最重要的不是表名,而是生命周期不同。
定义可以反复编辑,版本必须有确定含义,单据需要可追溯。
把三种生命周期拆开,才能讨论兼容,而不是每次改字段都碰运气。
当前源码中可以看到模型表、模型列、表单版本以及业务独立表各自承担不同职责。
版本记录包含配置、字段、模型快照和变更摘要。
实际单据也会记录 form_version。
不过,保存版本号不意味着系统已经自动解决了一切历史问题。
历史页面渲染、物理列保留、旧类型转换和打印模板兼容,仍需要分别验证。
3.3 金额、日期与关联对象不要交给默认猜测
金额字段需要确定精度和舍入规则。
数量可能允许小数,设备件数与服务工时也不一定共用精度。
同一张表单的两个数字控件,不必天然映射成同一种业务数字。
日期同样需要区分纯日期和时间点。
"预计交付日期"通常表达日历上的某一天,"提交时间"表达实际发生时刻。
前者不应因为浏览器时区不同变成前一天。
关联项目也不能只存项目名称。
名称会变化,身份引用却应稳定。
如果业务需要复原申请当时的名称,可以在关联 ID 之外保留名称快照,但应说明两个字段分别服务于什么场景。
这些规则最好在设计阶段提示,而不是等建表后再靠人工修正。
四、发布不是保存:变更计划才是安全边界
模型发布应回答四件事:准备改什么、是否允许、用户确认的是否仍是同一份变更、执行后如何记录。
当前发布服务会先取得表单级发布锁,重新计算发布计划,再检查阻断项和预览哈希。
只有这些条件满足,才执行 DDL 并登记版本。
下面是按现有方法压缩的核心摘录,保留检查顺序,省略返回对象组装:
java
public BpmFormManagedPublishRespVO publish(
BpmFormManagedPublishReqVO reqVO) {
RLock lock = redissonClient.getLock(
PUBLISH_LOCK_KEY + reqVO.getFormId());
if (!lock.tryLock()) {
throw exception(FORM_MANAGED_PUBLISH_LOCKED);
}
try {
BpmFormDO form = validateManagedForm(reqVO.getFormId());
var plan = computePlan(form);
var preview = buildPreview(form, plan);
if (preview.getBlockedCount() > 0) {
throw exception(FORM_MANAGED_PUBLISH_BLOCKED);
}
if (!Objects.equals(preview.getPreviewHash(), reqVO.getPreviewHash())) {
throw exception(FORM_MANAGED_PUBLISH_EXPIRED);
}
executeDdl(form.getId(), plan);
transactionTemplate.executeWithoutResult(
status -> applyPublish(form, plan));
return buildResponse(plan); // 返回组装在此用示意方法代替
} finally {
lock.unlock();
}
}
这段逻辑拦的不是一种风险,而是三种风险。
发布锁减少同一表单并发发布的冲突。
阻断检查拒绝不允许的模型变更。
预览哈希则避免用户确认了旧计划,系统却执行了另一份新计划。
4.1 为什么确认过预览,还要重新比较
设想两名管理员同时打开设计器。
甲看到的是增加交付日期的预览,乙随后修改了金额字段。
如果甲点击确认后直接执行"当前最新草稿",甲实际确认的内容和系统执行内容就不相同。
预览哈希的价值,是把确认对象锁定为某一份确定的变更内容。
发生变化时让用户重新确认,比悄悄帮用户合并更可信。

▲ 数据表视图用于核对表单字段对应的结构及版本信息。真实表名和列应从这里或模型接口取得,不靠控件标题猜测。
4.2 不能把 DDL 当成普通更新语句
建表、改列等 DDL 的事务语义依赖数据库。
MySQL 中的许多 DDL 会引发隐式提交,因此不能认为给发布方法加一个事务注解,就能把物理结构和模型登记一起无条件回滚。
当前代码先执行 DDL,再在事务中应用发布登记。
这使故障恢复成为必须解释的边界:如果结构已变更而登记失败,下次发布应如何识别现状,如何避免再次错误执行?
工程上可以进一步考虑发布执行日志、变更步骤状态、执行后结构复核和受控恢复入口。
这些是强化建议,不把它们描述成现有服务已经提供的一整套迁移平台。
4.3 删除字段,不应默认等于销毁历史
对于已经进入业务的字段,停用与物理删除是不同动作。
停用意味着新单不再使用,但历史解释可能仍需要它。
物理删除意味着数据本身不再存在,恢复成本和审批要求都不一样。
源码存在停用登记和允许移除的分支,说明平台不是简单地把设计器里删掉的东西全部丢弃。
具体哪一种变更被允许,需要看发布计划及阻断规则,不能只凭"删字段"三个字做结论。
对于客户环境,建议把删除、类型缩窄和长度缩短归入高风险变更,先做备份、抽样验证和影响分析。
这是数据治理要求,不只是界面上的二次确认。
五、进入审批后,单据身份和流程身份各做各的事
一张业务单据回答"发生了什么业务",一次流程实例回答"这次审批怎样推进"。
它们需要关联,但不应相互替代。
独立表单据创建时,除了业务字段,还会写入租户、状态、表单版本、单据编号和审计字段。
随后,流程使用业务行 ID 作为 businessKey,启动成功后再回写流程实例 ID。
下面是创建数据方法的主线摘录:
java
public Long createData(BpmProcessDefinitionInfoDO info,
Map<String, Object> variables, Long userId) {
Model model = loadModel(info);
Map<String, Object> values = new LinkedHashMap<>();
values.put("tenant_id", currentTenantId());
values.put("process_status",
BpmProcessInstanceStatusEnum.RUNNING.getStatus());
values.put("form_version", info.getFormVersion());
values.put("bill_no",
variables.get(BpmProcessVariableConstants.BILL_CODE));
putAudit(values, userId, true);
putFieldValues(values, model.mainColumns, variables);
Long id = insert(model.main.getTableName(), values);
for (var detail : model.details.entrySet()) {
String field = detail.getKey().getContainerField();
if (variables.containsKey(field)) {
insertDetailRows(detail.getKey(), detail.getValue(),
id, variables.get(field), userId);
}
}
return id;
}
摘录省略部门字段的转换细节,核心是"先有业务行,再建立流程关联"。
主表与明细也不是任意 Map 全量灌入数据库,而是通过模型列挑选、转换后写入。
5.1 状态回写应发生在明确的业务事件上

▲ 创建业务行后,以业务行 ID 启动流程,再绑定流程实例 ID;节点修改与状态变化走各自的同步入口。
当前服务具有创建数据、绑定流程实例、更新业务数据和更新流程状态等入口。
因此,独立数据表不是流程结束后才一次性导出的附属台账。
审批过程中如果允许修改采购数量,业务数据更新需要跟随受控的节点操作。
如果只是流程状态变更,就不应随手把整张表单重写一遍。
把这两种更新分开,能减少无关覆盖。
这里还要区分"代码中存在更新入口"和"所有业务字段都可以在所有节点修改"。
字段是否可写仍应服从流程和业务权限,不能因为底层支持更新就放开任意写入。
5.2 在途单据不能盲目读取最新设计
流程发布时会保存表单字段与配置,并记录绑定的表单版本。
这使流程定义能够携带它当时使用的表单形态。
例如 v1 没有交付日期,v2 增加交付日期。
旧流程实例仍应按照它所属定义解释输入,不能要求已经提交的申请人补填一个当时不存在的必填项。
还要注意,表单发布到新版本后,已有流程模型可能仍绑定旧版本。
设计器中的提示会提醒仍使用旧版的模型,需要重新发布相关流程模型,后续新单才会使用新版本。

▲ 本地已有流程实例展示填写结果与审批信息。它与设计器处于不同阶段,可用于核对业务单据和流程实例的关联。
不能由此推导出"任何历史结构变更都自动兼容"。
尤其是业务数据服务仍要使用模型登记来定位物理表和列;类型转换、列停用和明细更新行为,应加入版本回归测试。
六、进入查询阶段,独立表只是起点
独立存储提高了数据可访问性,但不会自动生成一套安全、准确、快速的报表系统。
要按项目统计已批准预算,至少需要明确项目身份、金额口径、流程状态、作废规则和时间维度。
如果只执行金额求和,草稿、驳回和撤销单也可能混进结果。
6.1 查询条件先有业务含义,再有 SQL
| 查询问题 | 必须确定的口径 | 常见误解 |
|---|---|---|
| 某项目批准了多少预算 | 核准金额、通过状态、有效单据 | 对所有申请金额求和 |
| 哪些设备未交付 | 申请与订单、交付数据的关联 | 认为审批通过等于交付完成 |
| 部门历史申请趋势 | 申请时组织或当前组织 | 不说明部门变更后的归属 |
| 某笔费用源于哪张申请 | 稳定单据 ID 与引用关系 | 仅通过名称或备注匹配 |
如果查询需要复杂的业务联动,就应该由对应业务服务提供稳定接口。
不要把数据库表名直接暴露给前端,让页面自由拼接筛选条件。
6.2 动态 SQL 更要保住两条边界
一条是标识符边界。
表名和列名来自经过校验的模型登记,而不是直接使用用户传来的任意字符串。
参数占位符能保护值,但不能替代对表名、列名的管理。
另一条是数据边界。
当前独立表数据服务使用 JdbcTemplate,不自动经过 MyBatis 的多租户插件。
所以它在更新和删除明细等语句中显式增加租户条件。
下面是更新方法的关键实现:
java
private void update(String tableName,
Map<String, Object> values, Long id) {
if (values.isEmpty()) {
return;
}
BpmFormDdlDialect dialect = formManagedService.getDialect();
List<String> names = new ArrayList<>(values.keySet());
String assignments = names.stream()
.map(name -> dialect.quote(name) + " = ?")
.collect(Collectors.joining(", "));
String sql = "UPDATE " + dialect.quote(tableName)
+ " SET " + assignments
+ " WHERE id = ? AND tenant_id = ?";
List<Object> args = new ArrayList<>();
names.forEach(name -> args.add(values.get(name)));
args.add(id);
args.add(currentTenantId());
jdbcTemplate.update(sql, args.toArray());
}
这段代码说明租户隔离不能只寄希望于某个插件自动生效。
但 tenant_id 也不是权限的全部:同一租户内,部门、本人、角色和单据授权仍可能有不同范围。
新增查询、导出和打印入口时,都应验证这些边界。
6.3 主子表更新还有一个容易忽略的问题
当前明细更新路径会在收到对应明细容器时,删除该父单在当前租户下的旧明细,再重新插入。
这种方式简单、明确,适合明细行主要依附于主单的场景。
但如果下游订单已经直接引用某条明细的数据库 ID,重建明细就可能破坏引用。
因此,是否允许外部业务依赖明细行 ID,必须提前约定。
需要稳定明细身份时,应评估按行标识做差异更新,或引入不会随重建改变的业务行编号。
这属于接入业务的额外设计,不应把"独立表可引用"理解成"所有内部明细 ID 都天然稳定"。
七、把演进能力做成可验收的约束
一套表单系统最有价值的测试,不是"输入框能不能输入",而是"设计变化后,业务事实是否仍然可信"。
| 决策点 | 推荐处理 | 为什么 |
|---|---|---|
| 页面标题与字段身份 | 分开管理 | 改文案不应变成新字段 |
| 保存与发布 | 独立动作 | 草稿不直接改变线上契约 |
| 发布确认 | 校验预览版本 | 防止确认内容与执行内容不一致 |
| 历史定义 | 保存快照并按绑定版本解释 | 防止新必填项污染旧单 |
| 数据访问 | 模型白名单与权限同时校验 | 有物理表不等于可任意查询 |
7.1 用一组变化检验设计
先建立 v1 表单并发起申请 A,保留它的业务主键和流程实例 ID。
增加交付日期后发布 v2,但暂时不重新发布流程模型,再发起申请 B,观察绑定版本。
随后重新发布流程模型,发起申请 C,核对新字段是否出现。
这组测试不是为了刻意制造复杂步骤,而是把"设计版本"和"流程部署版本"的差异显式验证出来。
A、B、C 的预期应写在测试记录里,不依赖开发人员口头解释。
再把一个字段从显示状态改为停用,查看历史单据是否仍能读懂。
如果改变金额精度,应准备具有边界小数位的历史值,核对页面、存储和导出是否一致。
7.2 不要遗漏失败与并发
- 两个管理员同时发布,后到者应得到明确结果。
- 预览后草稿变化,旧确认不能静默执行新计划。
- 写业务主行后流程启动失败,要核对事务边界及残留处理。
- 相同事件重复触发,状态同步不应制造重复业务单据。
- 从另一个租户访问相同业务 ID,不应读到数据。
- 同租户的普通员工,不应因为知道表名就绕过数据范围。
这些检查比"发布按钮成功提示"更接近企业系统的真实验收标准。
7.3 哪些需求应该退出低代码的舒适区
如果一张申请已经涉及库存预占、预算扣减、供应商准入和复杂分摊,核心规则就不宜散落在多个组件表达式里。
可以保留低代码负责输入和展示,让业务服务负责有副作用的执行。
判断标准不是字段多少,而是错误的代价和一致性边界。
一个只有三个字段的转账指令,可能比几十个字段的调查问卷更需要专门服务。
八、如何按本文路线体验
在线演示可从文末地址进入,公开演示账号为 admin / admin123。
本地已有环境优先访问管理端;没有环境时,按项目部署文档准备依赖并启动后端,再在 PC 工程执行 pnpm dev:antd。
仅启动前端不会自动获得业务数据。
建议按以下顺序体验,不要直接在共享环境发布结构变更:
- 进入流程中心的流程表单,查看不同存储方式。
- 打开已有独立表单的设计器,识别编码与已发布版本。
- 打开数据表视图,对照主表、明细表和字段模型。
- 查看关联流程模型,确认使用的表单及其版本。
- 打开已有流程实例,核对填写内容、单据编号与审批信息。
- 在自己的测试环境复制案例,验证保存、发布和流程重发的差别。
- 最后验证历史单据、查询权限和异常恢复,不只验证正常提交。
本文核对的主要实现包括 BpmFormManagedServiceImpl、BpmFormManagedDataServiceImpl、BpmProcessDefinitionServiceImpl 和 BpmProcessInstanceServiceImpl。
前端对应流程表单设计器、数据模型抽屉与发布预览。
代码摘录为阅读目的做了压缩,未列出的校验、响应组装和事务上下文应以完整源码为准。
常见问题
独立数据表能代替传统业务模块吗?
能承载一部分数据采集与审批需求,但不能自动代替复杂业务服务。
库存、资金、跨系统幂等和强一致性仍需按领域设计。
低代码减少重复工作,不意味着取消业务边界。
发布表单后,新字段为什么没有立刻出现在新流程里?
需要分别检查表单是否发布,以及流程模型是否重新部署并绑定了新版本。
设计器保存、表单发布和流程发布是不同动作。
不能只根据设计器当前画面判断运行中的定义。
表单有了物理表,是不是可以直接连报表?
可以把它作为报表的数据来源之一,但还要配置字段、状态口径、租户和数据权限。
独立建表本身不等于自动生成报表,也不等于授权所有人查询。
删除控件后,历史值是否一定保留?
不能做无条件保证。
需要看发布计划对该变更采用停用还是物理移除,以及历史页面、列类型和查询如何兼容。
正式变更前应验证历史样本与恢复方案。
业务查询应该读流程变量还是独立表?
先确定权威来源。
如果独立表承担业务台账,业务查询应围绕它建立明确接口,流程变量服务于执行需要。
两边数据都存在时,更要明确同步时点和冲突处理,不能让不同页面任意选一个来源。
结语
低代码最难的部分,不是让字段出现,而是让字段进入业务之后仍然有稳定含义。
当页面定义、发布模型、流程版本和业务单据各自拥有清晰职责,一张表单才有机会从短期工具成长为可持续维护的业务入口。
如果你正在建设企业表单平台,不妨先拿一张真正会改版、会被其他模块引用的单据做验证。
它会比十张只做展示的演示表单,更快暴露架构中需要补齐的部分。
延伸阅读:《SpringBoot3 企业数据导入:从 Excel 校验到批次处理、错误回执与安全重试》;《SpringBoot3 企业文件生命周期:上传、业务关联、权限、版本与安全清理》。
你们的表单系统中,最难处理的是历史版本、跨模块引用,还是查询权限?欢迎结合真实场景讨论。
如果这篇对你有用,点个「在看」或收藏。
🌐 演示地址 :https://ruoyioffice.com/web
📦 GitHub 源码 :https://github.com/yuqing2026/ruoyi-office
📦 Gitee 源码 :https://gitee.com/yqzy1688/ruoyi-office
💬 微信:17156169080(获取产品咨询)

打开演示地址直接查看系统。