SpringBoot + Low-Code + JSON 表单引擎:5 分钟配置一套审批流,告别重复 CRUD

前言

在企业级应用开发中,审批流是一个高频需求。无论是请假申请、费用报销,还是采购审批,都需要一套完整的表单和流程系统。传统开发模式下,每个审批流都需要单独开发表单页面、验证逻辑、数据存储和流程控制,不仅耗时耗力,还容易出现重复造轮子的情况。今天,我将和大家分享一个基于SpringBoot的低代码表单引擎解决方案,通过JSON配置,实现5分钟配置一套审批流,彻底告别重复的CRUD开发。

为什么需要低代码表单引擎?

1. 开发效率问题

传统审批流开发需要经历以下步骤:

  • 设计表单UI界面
  • 实现前端交互逻辑
  • 开发后端API接口
  • 编写数据验证逻辑
  • 集成工作流引擎
  • 实现审批节点配置
  • 部署和测试

整个过程可能需要几天甚至几周时间,而且每个新流程都要重复这些步骤。

2. 维护成本高昂

随着业务发展,表单字段经常需要调整,流程节点需要变更,每次修改都需要开发人员介入,增加了维护成本和响应时间。

3. 业务人员参与度低

业务人员无法直接参与表单和流程的设计,只能被动接受开发结果,导致最终产品与实际需求存在偏差。

核心技术方案

1. 架构设计

我们的解决方案采用以下核心技术栈:

  • Spring Boot: 快速开发框架
  • JSON Schema: 定义表单结构
  • Activiti: 工作流引擎
  • Thymeleaf: 模板引擎用于动态表单渲染
  • Redis: 缓存表单定义和流程定义
  • MySQL: 数据持久化存储

2. 核心组件

表单定义(FormDefinition)

{
  "formKey": "leave_application",
  "formName": "请假申请",
  "formTitle": "员工请假申请表",
  "formSchema": {
    "type": "object",
    "properties": {
      "employeeName": {
        "type": "string",
        "title": "员工姓名",
        "required": true
      },
      "department": {
        "type": "string",
        "title": "所属部门",
        "enum": ["技术部", "市场部", "人事部", "财务部"],
        "enumNames": ["技术部", "市场部", "人事部", "财务部"]
      },
      "leaveType": {
        "type": "string",
        "title": "请假类型",
        "enum": ["年假", "病假", "事假", "婚假", "产假"],
        "enumNames": ["年假", "病假", "事假", "婚假", "产假"]
      },
      "startDate": {
        "type": "string",
        "format": "date",
        "title": "开始日期"
      },
      "endDate": {
        "type": "string",
        "format": "date",
        "title": "结束日期"
      },
      "reason": {
        "type": "string",
        "title": "请假原因",
        "maxLength": 200
      }
    },
    "required": ["employeeName", "leaveType", "startDate", "endDate"]
  },
  "formUiSchema": {
    "employeeName": {
      "ui:widget": "input"
    },
    "department": {
      "ui:widget": "select"
    },
    "leaveType": {
      "ui:widget": "radio"
    },
    "reason": {
      "ui:widget": "textarea"
    }
  },
  "processDefinitionKey": "leave_approval_process"
}

流程定义(ApprovalProcess)

{
  "processKey": "leave_approval_process",
  "processName": "请假审批流程",
  "processDefinition": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>...",
  "description": "员工请假申请审批流程"
}

3. 核心实现

表单渲染服务

@Service
@Slf4j
public class FormRenderingService {
    
    /**
     * 根据表单定义渲染HTML表单
     */
    public String renderForm(FormDefinition formDefinition) {
        try {
            JSONObject schema = JSON.parseObject(formDefinition.getFormSchema());
            JSONObject uiSchema = JSON.parseObject(formDefinition.getFormUiSchema());
            
            StringBuilder html = new StringBuilder();
            html.append("<form id=\"").append(formDefinition.getFormKey()).append("\" class=\"low-code-form\">\n");
            
            // 添加表单标题
            if (formDefinition.getFormTitle() != null && !formDefinition.getFormTitle().isEmpty()) {
                html.append("  <h3>").append(formDefinition.getFormTitle()).append("</h3>\n");
            }
            
            // 解析schema中的属性并生成对应的HTML元素
            if (schema.containsKey("properties")) {
                JSONObject properties = schema.getJSONObject("properties");
                
                for (String fieldName : properties.keySet()) {
                    JSONObject fieldSchema = properties.getJSONObject(fieldName);
                    JSONObject fieldUiSchema = uiSchema != null ? 
                        uiSchema.getJSONObject(fieldName) : null;
                    
                    String fieldHtml = generateFieldHtml(fieldName, fieldSchema, fieldUiSchema);
                    html.append(fieldHtml).append("\n");
                }
            }
            
            // 添加提交按钮
            html.append("  <div class=\"form-actions\">\n");
            html.append("    <button type=\"submit\" class=\"btn btn-primary\">提交</button>\n");
            html.append("    <button type=\"reset\" class=\"btn btn-secondary\">重置</button>\n");
            html.append("  </div>\n");
            html.append("</form>\n");
            
            return html.toString();
        } catch (Exception e) {
            log.error("渲染表单失败: {}", formDefinition.getFormKey(), e);
            return "<div class=\"error\">表单渲染失败</div>";
        }
    }
    
    /**
     * 根据字段类型生成HTML元素
     */
    private String generateFieldHtml(String fieldName, JSONObject fieldSchema, JSONObject fieldUiSchema) {
        StringBuilder fieldHtml = new StringBuilder();
        
        // 获取字段标签
        String label = fieldSchema.getString("title");
        if (label == null || label.isEmpty()) {
            label = fieldName;
        }
        
        // 获取字段类型
        String type = fieldSchema.getString("type");
        String widget = fieldUiSchema != null ? 
            fieldUiSchema.getString("ui:widget") : null;
        
        // 生成字段HTML
        fieldHtml.append("  <div class=\"form-group\">\n");
        fieldHtml.append("    <label for=\"").append(fieldName).append("\">")
                 .append(label).append("</label>\n");
        
        if ("string".equals(type)) {
            if ("textarea".equals(widget)) {
                // 文本域
                fieldHtml.append("    <textarea id=\"").append(fieldName)
                         .append("\" name=\"").append(fieldName).append("\"")
                         .append(" class=\"form-control\"");
                
                // 添加验证规则
                addValidationAttributes(fieldHtml, fieldSchema);
                
                fieldHtml.append(">");
                if (fieldSchema.containsKey("default")) {
                    fieldHtml.append(fieldSchema.getString("default"));
                }
                fieldHtml.append("</textarea>\n");
            } else {
                // 输入框
                fieldHtml.append("    <input type=\"text\" id=\"").append(fieldName)
                         .append("\" name=\"").append(fieldName).append("\"")
                         .append(" class=\"form-control\"");
                
                // 添加默认值
                if (fieldSchema.containsKey("default")) {
                    fieldHtml.append(" value=\"").append(fieldSchema.getString("default")).append("\"");
                }
                
                // 添加验证规则
                addValidationAttributes(fieldHtml, fieldSchema);
                
                fieldHtml.append(" />\n");
            }
        }
        // ... 其他类型的字段处理
        
        fieldHtml.append("  </div>\n");
        
        return fieldHtml.toString();
    }
}

表单验证服务

@Service
@Slf4j
public class FormValidationService {
    
    /**
     * 根据表单定义验证表单数据
     */
    public ValidationResult validateFormData(String formKey, String formData, FormDefinition formDefinition) {
        ValidationResult result = new ValidationResult();
        
        try {
            JSONObject data = JSON.parseObject(formData);
            JSONObject schema = JSON.parseObject(formDefinition.getFormSchema());
            
            // 获取必填字段
            if (schema.containsKey("required")) {
                JSONArray requiredFields = schema.getJSONArray("required");
                
                for (int i = 0; i < requiredFields.size(); i++) {
                    String fieldName = requiredFields.getString(i);
                    if (!data.containsKey(fieldName) || data.getString(fieldName) == null || 
                        data.getString(fieldName).trim().isEmpty()) {
                        result.addError(fieldName, fieldName + " 是必填字段");
                    }
                }
            }
            
            // 验证字段类型和约束
            if (schema.containsKey("properties")) {
                JSONObject properties = schema.getJSONObject("properties");
                
                for (String fieldName : properties.keySet()) {
                    JSONObject fieldSchema = properties.getJSONObject(fieldName);
                    Object fieldValue = data.get(fieldName);
                    
                    if (fieldValue != null) {
                        validateField(fieldName, fieldValue, fieldSchema, result);
                    }
                }
            }
            
        } catch (Exception e) {
            log.error("验证表单数据失败: {}", formKey, e);
            result.addError("system", "表单验证系统错误");
        }
        
        return result;
    }
    
    /**
     * 验证单个字段
     */
    private void validateField(String fieldName, Object value, JSONObject fieldSchema, ValidationResult result) {
        String type = fieldSchema.getString("type");
        
        if ("string".equals(type)) {
            if (!(value instanceof String)) {
                result.addError(fieldName, fieldName + " 必须是字符串类型");
                return;
            }
            
            String strValue = (String) value;
            
            // 长度验证
            if (fieldSchema.containsKey("minLength")) {
                int minLength = fieldSchema.getIntValue("minLength");
                if (strValue.length() < minLength) {
                    result.addError(fieldName, fieldName + " 长度不能少于 " + minLength + " 个字符");
                }
            }
            
            if (fieldSchema.containsKey("maxLength")) {
                int maxLength = fieldSchema.getIntValue("maxLength");
                if (strValue.length() > maxLength) {
                    result.addError(fieldName, fieldName + " 长度不能超过 " + maxLength + " 个字符");
                }
            }
            
            // 格式验证
            if (fieldSchema.containsKey("pattern")) {
                String pattern = fieldSchema.getString("pattern");
                if (!strValue.matches(pattern)) {
                    result.addError(fieldName, fieldName + " 格式不正确");
                }
            }
        }
        // ... 其他类型的字段验证
    }
    
    public static class ValidationResult {
        private boolean valid = true;
        private Map<String, String> errors = new HashMap<>();
        
        public void addError(String field, String message) {
            valid = false;
            errors.put(field, message);
        }
        
        public boolean isValid() {
            return valid;
        }
        
        public Map<String, String> getErrors() {
            return errors;
        }
    }
}

工作流服务

@Service
@Slf4j
public class WorkflowService {
    
    @Autowired
    private RuntimeService runtimeService;
    
    @Autowired
    private TaskService taskService;
    
    /**
     * 启动审批流程实例
     */
    @Transactional
    public String startProcessInstance(String processKey, String businessKey, Map<String, Object> variables) {
        try {
            // 启动流程实例
            ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(
                processKey, businessKey, variables);
            
            String processInstanceId = processInstance.getId();
            
            log.info("启动审批流程实例: {}, 业务键: {}", processInstanceId, businessKey);
            
            return processInstanceId;
        } catch (Exception e) {
            log.error("启动审批流程实例失败: {}", processKey, e);
            throw new RuntimeException("启动审批流程实例失败", e);
        }
    }
    
    /**
     * 完成审批任务
     */
    @Transactional
    public void completeTask(String taskId, String assignee, Map<String, Object> variables) {
        try {
            // 设置办理人
            taskService.setAssignee(taskId, assignee);
            
            // 完成任务
            taskService.complete(taskId, variables);
            
            log.info("完成审批任务: {}, 办理人: {}", taskId, assignee);
        } catch (Exception e) {
            log.error("完成审批任务失败: {}", taskId, e);
            throw new RuntimeException("完成审批任务失败", e);
        }
    }
    
    /**
     * 提交表单并启动审批流程
     */
    @Transactional
    public String submitFormWithWorkflow(String formKey, String formData, String submitter) {
        // 1. 提交表单实例
        FormInstance formInstance = formService.submitFormInstance(formKey, formData, submitter);
        
        // 2. 获取表单关联的流程定义
        FormDefinition formDefinition = formService.getFormDefinition(formKey);
        
        if (formDefinition == null || formDefinition.getProcessDefinitionKey() == null) {
            throw new RuntimeException("表单未关联审批流程或表单定义不存在: " + formKey);
        }
        
        // 3. 启动审批流程
        Map<String, Object> variables = new HashMap<>();
        variables.put("assignee", submitter);
        variables.put("formInstanceKey", formInstance.getInstanceKey());
        
        String processInstanceId = startProcessInstance(
            formDefinition.getProcessDefinitionKey(), 
            formInstance.getInstanceKey(), 
            variables);
        
        return processInstanceId;
    }
}

实际应用案例

让我们以请假审批为例,看看如何在5分钟内配置一套完整的审批流。

第一步:定义表单结构(1分钟)

{
  "formKey": "leave_app_001",
  "formName": "请假申请",
  "formTitle": "员工请假申请表",
  "formSchema": {
    "type": "object",
    "properties": {
      "employeeId": {
        "type": "string",
        "title": "工号",
        "pattern": "^\\d{6}$",
        "required": true
      },
      "employeeName": {
        "type": "string",
        "title": "姓名",
        "required": true
      },
      "leaveType": {
        "type": "string",
        "title": "请假类型",
        "enum": ["年假", "病假", "事假", "婚假", "产假"],
        "enumNames": ["年假", "病假", "事假", "婚假", "产假"],
        "required": true
      },
      "startTime": {
        "type": "string",
        "format": "date",
        "title": "开始时间",
        "required": true
      },
      "endTime": {
        "type": "string",
        "format": "date",
        "title": "结束时间",
        "required": true
      },
      "reason": {
        "type": "string",
        "title": "请假事由",
        "maxLength": 200
      }
    },
    "required": ["employeeId", "employeeName", "leaveType", "startTime", "endTime"]
  },
  "formUiSchema": {
    "employeeId": { "ui:widget": "input" },
    "employeeName": { "ui:widget": "input" },
    "leaveType": { "ui:widget": "select" },
    "reason": { "ui:widget": "textarea" }
  },
  "processDefinitionKey": "leave_approval_wf"
}

第二步:配置审批流程(2分钟)

使用Activiti工作流引擎定义审批流程:

<process id="leave_approval_wf" name="请假审批流程" isExecutable="true">
  <startEvent id="startevent1" name="开始"></startEvent>
  <userTask id="dept_leader_approve" name="部门领导审批" activiti:assignee="${deptLeader}">
    <extensionElements>
      <activiti:taskListener event="create" class="com.example.listener.TaskCreateListener" />
    </extensionElements>
  </userTask>
  <exclusiveGateway id="check_duration" name="请假天数检查"></exclusiveGateway>
  <userTask id="hr_approve" name="HR审批" activiti:assignee="hr_user"></userTask>
  <endEvent id="endevent1" name="结束"></endEvent>
  
  <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="dept_leader_approve"></sequenceFlow>
  <sequenceFlow id="flow2" sourceRef="dept_leader_approve" targetRef="check_duration">
    <conditionExpression xsi:type="tFormalExpression"><![CDATA[${approvalResult == 'approved'}]]></conditionExpression>
  </sequenceFlow>
  <sequenceFlow id="flow3" sourceRef="check_duration" targetRef="hr_approve">
    <conditionExpression xsi:type="tFormalExpression"><![CDATA[${duration > 3}]]></conditionExpression>
  </sequenceFlow>
  <sequenceFlow id="flow4" sourceRef="check_duration" targetRef="endevent1">
    <conditionExpression xsi:type="tFormalExpression"><![CDATA[${duration <= 3}]]></conditionExpression>
  </sequenceFlow>
  <sequenceFlow id="flow5" sourceRef="hr_approve" targetRef="endevent1"></sequenceFlow>
</process>

第三步:API调用创建(1分钟)

curl -X POST "http://localhost:8081/api/forms" \
  -H "Content-Type: application/json" \
  -d '{
    "formKey": "leave_app_001",
    "formName": "请假申请",
    "formTitle": "员工请假申请表",
    "formSchema": "...",
    "formUiSchema": "...",
    "processDefinitionKey": "leave_approval_wf",
    "createdBy": "admin"
  }'

第四步:表单渲染和使用(1分钟)

前端页面只需一行代码即可渲染表单:

<div id="dynamic-form-container">
  <!-- 从后端API获取渲染后的表单HTML -->
  ${renderedForm}
</div>

核心优势

1. 极致开发效率

  • 配置即开发:通过JSON配置即可完成表单开发
  • 模板复用:相同类型的表单可以复用模板
  • 可视化配置:提供可视化配置界面,业务人员也能参与

2. 强大的扩展性

  • 字段类型丰富:支持文本、数字、日期、选择等多种类型
  • 验证规则灵活:支持必填、长度、格式、范围等多种验证
  • 流程自定义:支持复杂的审批流程配置

3. 降低维护成本

  • 配置驱动:表单修改无需代码发布
  • 版本管理:支持表单定义的版本控制
  • 权限控制:细粒度的权限管理

最佳实践

1. 性能优化

  • 使用Redis缓存表单定义,减少数据库查询
  • 对表单渲染结果进行适当缓存
  • 实现分页查询表单实例

2. 安全考虑

  • 对表单数据进行严格验证
  • 实现操作权限控制
  • 记录操作日志

3. 监控和运维

  • 监控表单提交成功率
  • 跟踪审批流程执行情况
  • 提供管理后台进行配置管理

总结

通过SpringBoot + Low-Code + JSON表单引擎的方案,我们成功实现了5分钟配置一套审批流的目标。这个方案不仅大幅提升了开发效率,还降低了维护成本,让业务人员能够更好地参与到表单和流程的设计中。

在实际项目中,这个方案已经帮助我们:

  • 减少80%的重复开发工作
  • 将表单配置时间从几天缩短到几分钟
  • 提升业务响应速度,增强系统灵活性

低代码不是万能的,但在合适的场景下,它确实能发挥巨大价值。希望这个方案能对大家有所帮助,让我们一起告别重复的CRUD开发!


服务端技术精选 - 专注后端技术分享,与你一起成长!


标题:SpringBoot + Low-Code + JSON 表单引擎:5 分钟配置一套审批流,告别重复 CRUD
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/01/10/1768030502710.html

    0 评论
avatar