表单版本控制的实现方案与差异比较方法
一、表单版本控制的核心需求
在业务系统中,表单结构、字段规则、验证逻辑经常会随业务迭代发生变化,表单版本控制的核心目标是:记录每一次表单的变更历史,支持回溯任意历史版本,快速对比不同版本间的差异,同时保证新版本发布后不影响已提交的历史表单数据,避免数据兼容性问题。
二、表单版本控制的基础实现思路
1. 存储结构设计
表单版本控制的核心是存储每一版本的完整表单定义,通常采用「版本表+表单定义表」的结构,以下是通用的数据库表设计示例:
-- 表单基础信息表,存储表单的公共属性 CREATE TABLE form_base ( id BIGINT PRIMARY KEY AUTO_INCREMENT, form_code VARCHAR(64) NOT NULL COMMENT '表单唯一编码', form_name VARCHAR(128) NOT NULL COMMENT '表单名称', current_version_id BIGINT COMMENT '当前生效版本ID', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_form_code (form_code) ); -- 表单版本表,存储每一个版本的完整定义 CREATE TABLE form_version ( id BIGINT PRIMARY KEY AUTO_INCREMENT, form_id BIGINT NOT NULL COMMENT '关联form_base表ID', version_number VARCHAR(32) NOT NULL COMMENT '版本号,如v1.0.0、v2.1.3', form_definition TEXT NOT NULL COMMENT '表单完整定义,JSON格式存储', version_desc VARCHAR(512) COMMENT '版本变更说明', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, creator VARCHAR(64) COMMENT '版本创建人', UNIQUE KEY uk_form_version (form_id, version_number) ); -- 表单提交数据表,关联具体版本,保证数据按版本解析 CREATE TABLE form_submit_data ( id BIGINT PRIMARY KEY AUTO_INCREMENT, form_id BIGINT NOT NULL COMMENT '关联form_base表ID', version_id BIGINT NOT NULL COMMENT '关联form_version表ID,标记数据所属版本', submit_data TEXT NOT NULL COMMENT '提交的表单数据,JSON格式', submit_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, submitter VARCHAR(64) COMMENT '提交人' );
其中<form_definition>字段存储的JSON示例包含表单的字段列表、校验规则、布局配置等完整信息,示例如下:
{
"fields": [
{
"fieldId": "username",
"fieldName": "用户名",
"fieldType": "input",
"required": true,
"maxLength": 20
},
{
"fieldId": "age",
"fieldName": "年龄",
"fieldType": "number",
"required": false,
"min": 1,
"max": 120
}
],
"layout": "vertical",
"validateMode": "blur"
}2. 版本生成与发布逻辑
当表单发生变更时,需要生成新的版本,核心流程如下:
编辑表单时,基于当前生效版本克隆出一份草稿版本,所有修改仅在草稿中进行,不影响线上版本
草稿编辑完成后,校验表单定义的合法性(如字段ID不重复、必填规则无冲突)
校验通过后,将草稿版本号递增(如从v1.0.0升级到v1.0.1),写入<form_version>表,同时更新<form_base>表的<current_version_id>为新的版本ID
历史版本保持只读状态,不允许修改,仅支持查询和回溯
三、表单不同版本的差异比较实现
1. 差异比较的核心思路
由于表单定义以JSON格式存储,版本差异比较本质是两个JSON对象的差异对比,需要识别字段的增删改、规则变化等不同粒度的差异。通常分为三个层级:
字段级差异:新增字段、删除字段、字段属性(类型、必填、长度限制等)修改
规则级差异:校验规则、联动逻辑、提交规则的变化
布局级差异:表单布局方式、字段排序、分组配置的变化
2. 代码实现示例(Java版本)
以下是基于JSON对比的表单版本差异比较核心代码示例:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import java.util.*;
public class FormVersionDiffUtil {
/**
* 对比两个表单版本的差异
* @param oldVersionJson 旧版本表单定义JSON字符串
* @param newVersionJson 新版本表单定义JSON字符串
* @return 差异结果集合
*/
public static List<FormDiffResult> compareVersions(String oldVersionJson, String newVersionJson) {
List<FormDiffResult> diffResults = new ArrayList<>();
JSONObject oldObj = JSON.parseObject(oldVersionJson);
JSONObject newObj = JSON.parseObject(newVersionJson);
// 对比字段差异
compareFieldDiff(oldObj, newObj, diffResults);
// 对比布局差异
compareLayoutDiff(oldObj, newObj, diffResults);
// 对比校验规则差异
compareValidateRuleDiff(oldObj, newObj, diffResults);
return diffResults;
}
/**
* 对比字段差异
*/
private static void compareFieldDiff(JSONObject oldObj, JSONObject newObj, List<FormDiffResult> diffResults) {
JSONArray oldFields = oldObj.getJSONArray("fields");
JSONArray newFields = newObj.getJSONArray("fields");
// 用Map存储字段ID到字段定义的映射,方便快速查找
Map<String, JSONObject> oldFieldMap = new HashMap<>();
for (int i = 0; i < oldFields.size(); i++) {
JSONObject field = oldFields.getJSONObject(i);
oldFieldMap.put(field.getString("fieldId"), field);
}
Map<String, JSONObject> newFieldMap = new HashMap<>();
for (int i = 0; i < newFields.size(); i++) {
JSONObject field = newFields.getJSONObject(i);
newFieldMap.put(field.getString("fieldId"), field);
}
// 找出新增的字段
for (String fieldId : newFieldMap.keySet()) {
if (!oldFieldMap.containsKey(fieldId)) {
FormDiffResult diff = new FormDiffResult();
diff.setDiffType("FIELD_ADD");
diff.setFieldId(fieldId);
diff.setNewValue(newFieldMap.get(fieldId));
diffResults.add(diff);
}
}
// 找出删除的字段
for (String fieldId : oldFieldMap.keySet()) {
if (!newFieldMap.containsKey(fieldId)) {
FormDiffResult diff = new FormDiffResult();
diff.setDiffType("FIELD_DELETE");
diff.setFieldId(fieldId);
diff.setOldValue(oldFieldMap.get(fieldId));
diffResults.add(diff);
}
}
// 找出修改的字段
for (String fieldId : oldFieldMap.keySet()) {
if (newFieldMap.containsKey(fieldId)) {
JSONObject oldField = oldFieldMap.get(fieldId);
JSONObject newField = newFieldMap.get(fieldId);
if (!oldField.toJSONString().equals(newField.toJSONString())) {
FormDiffResult diff = new FormDiffResult();
diff.setDiffType("FIELD_MODIFY");
diff.setFieldId(fieldId);
diff.setOldValue(oldField);
diff.setNewValue(newField);
diffResults.add(diff);
}
}
}
}
/**
* 对比布局差异
*/
private static void compareLayoutDiff(JSONObject oldObj, JSONObject newObj, List<FormDiffResult> diffResults) {
String oldLayout = oldObj.getString("layout");
String newLayout = newObj.getString("layout");
if (!Objects.equals(oldLayout, newLayout)) {
FormDiffResult diff = new FormDiffResult();
diff.setDiffType("LAYOUT_CHANGE");
diff.setOldValue(oldLayout);
diff.setNewValue(newLayout);
diffResults.add(diff);
}
}
/**
* 对比校验规则差异
*/
private static void compareValidateRuleDiff(JSONObject oldObj, JSONObject newObj, List<FormDiffResult> diffResults) {
String oldValidateMode = oldObj.getString("validateMode");
String newValidateMode = newObj.getString("validateMode");
if (!Objects.equals(oldValidateMode, newValidateMode)) {
FormDiffResult diff = new FormDiffResult();
diff.setDiffType("VALIDATE_RULE_CHANGE");
diff.setOldValue(oldValidateMode);
diff.setNewValue(newValidateMode);
diffResults.add(diff);
}
}
/**
* 差异结果实体类
*/
static class FormDiffResult {
private String diffType; // 差异类型:FIELD_ADD、FIELD_DELETE、FIELD_MODIFY、LAYOUT_CHANGE等
private String fieldId; // 关联字段ID,非字段差异时为null
private Object oldValue; // 旧版本值
private Object newValue; // 新版本值
// 省略getter、setter方法
}
}3. 前端展示差异的示例
前端拿到差异结果后,可以清晰展示不同版本的变化,示例如下:
// 假设从后端获取到的差异结果
const diffResults = [
{ diffType: 'FIELD_ADD', fieldId: 'email', newValue: { fieldId: 'email', fieldName: '邮箱', fieldType: 'input', required: true } },
{ diffType: 'FIELD_DELETE', fieldId: 'age', oldValue: { fieldId: 'age', fieldName: '年龄', fieldType: 'number', required: false } },
{ diffType: 'FIELD_MODIFY', fieldId: 'username', oldValue: { maxLength: 20 }, newValue: { maxLength: 30 } },
{ diffType: 'LAYOUT_CHANGE', oldValue: 'vertical', newValue: 'horizontal' }
];
// 渲染差异列表
function renderDiffList(diffResults) {
const container = document.getElementById('diff-container');
container.innerHTML = '';
diffResults.forEach(diff => {
const item = document.createElement('div');
item.className = 'diff-item';
let content = '';
switch (diff.diffType) {
case 'FIELD_ADD':
content = `新增字段:${diff.newValue.fieldName}(${diff.fieldId})`;
break;
case 'FIELD_DELETE':
content = `删除字段:${diff.oldValue.fieldName}(${diff.fieldId})`;
break;
case 'FIELD_MODIFY':
content = `修改字段:${diff.fieldId},变更内容:${JSON.stringify(diff.oldValue)} → ${JSON.stringify(diff.newValue)}`;
break;
case 'LAYOUT_CHANGE':
content = `布局变更:${diff.oldValue} → ${diff.newValue}`;
break;
default:
content = '未知变更类型';
}
item.innerText = content;
container.appendChild(item);
});
}四、注意事项
版本号建议遵循语义化版本规范(主版本.次版本.修订号),主版本变更表示不兼容的历史变更,次版本表示新增功能兼容旧版本,修订号表示问题修复
历史版本对比时,需要考虑大版本的差异,避免对比不兼容的版本导致无意义的结果
表单提交数据必须关联对应的版本ID,解析数据时根据对应版本的表单定义解析,避免新版本结构导致历史数据无法读取
差异比较时可以根据业务需求扩展更多对比维度,比如字段的联动规则、权限配置等