前言
在企业级应用开发中,审批流是一个高频需求。无论是请假申请、费用报销,还是采购审批,都需要一套完整的表单和流程系统。传统开发模式下,每个审批流都需要单独开发表单页面、验证逻辑、数据存储和流程控制,不仅耗时耗力,还容易出现重复造轮子的情况。本文将分享一个基于 SpringBoot 的低代码表单引擎解决方案,通过 JSON 配置,实现快速配置审批流,提升开发效率。
一种基于 SpringBoot、JSON Schema 和 Activiti 工作流引擎的低代码表单引擎解决方案。该方案允许通过 JSON 配置快速定义表单结构和审批流程,替代传统的硬编码开发模式。核心功能包括动态表单渲染、数据验证、流程实例启动与任务完成。文章详细阐述了架构设计、核心组件实现及实际案例,展示了如何通过配置在几分钟内完成一套审批流,从而显著提升开发效率,降低维护成本,并支持业务人员参与流程设计。
在企业级应用开发中,审批流是一个高频需求。无论是请假申请、费用报销,还是采购审批,都需要一套完整的表单和流程系统。传统开发模式下,每个审批流都需要单独开发表单页面、验证逻辑、数据存储和流程控制,不仅耗时耗力,还容易出现重复造轮子的情况。本文将分享一个基于 SpringBoot 的低代码表单引擎解决方案,通过 JSON 配置,实现快速配置审批流,提升开发效率。
传统审批流开发需要经历以下步骤:
整个过程可能需要几天甚至几周时间,而且每个新流程都要重复这些步骤。
随着业务发展,表单字段经常需要调整,流程节点需要变更,每次修改都需要开发人员介入,增加了维护成本和响应时间。
业务人员无法直接参与表单和流程的设计,只能被动接受开发结果,导致最终产品与实际需求存在偏差。
我们的解决方案采用以下核心技术栈:
{
"formKey": "leave_application",
"formName": "请假申请",
"formTitle": "员工请假申请表",
"formSchema": {
"type": "object",
"properties": {
"employeeName": { "type": "string", "title": "员工姓名", "required": true },
"department": { "type": "string", "title": "所属部门", "enum": ["技术部", "市场部", "人事部", "财务部"] },
"leaveType": { "type": "string", "title": "请假类型", "enum": ["年假", "病假", "事假", "婚假", "产假"] },
"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"
}
{
"processKey": "leave_approval_process",
"processName": "请假审批流程",
"processDefinition": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>...",
"description": "员工请假申请审批流程"
}
@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;
}
}
让我们以请假审批为例,看看如何配置一套完整的审批流。
{
"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": ["年假", "病假", "事假", "婚假", "产假"], "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"
}
使用 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>
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" }'
前端页面只需一行代码即可渲染表单:
<div id="dynamic-form-container">
<!-- 从后端 API 获取渲染后的表单 HTML -->
${renderedForm}
</div>
通过 SpringBoot + Low-Code + JSON 表单引擎的方案,我们成功实现了快速配置审批流的目标。这个方案不仅大幅提升了开发效率,还降低了维护成本,让业务人员能够更好地参与到表单和流程的设计中。
在实际项目中,这个方案已经帮助我们:
低代码不是万能的,但在合适的场景下,它确实能发挥巨大价值。希望这个方案能对大家有所帮助。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online