SpringBoot+Activiti7+React构建低代码审批流:从零实现钉钉式流程设计器

张开发
2026/4/12 22:07:10 15 分钟阅读

分享文章

SpringBoot+Activiti7+React构建低代码审批流:从零实现钉钉式流程设计器
1. 为什么需要低代码审批流设计器在企业日常运营中审批流程无处不在。传统的审批流开发模式存在几个明显痛点首先流程配置需要技术人员介入业务人员无法自主调整其次修改流程需要重新部署响应速度慢最后复杂的工作流引擎学习曲线陡峭。这些问题直接影响了企业的运营效率。我去年参与过一个OA系统改造项目客户最大的抱怨就是每次调整审批流程都要找IT部门排队。财务部的报销审批要加个会签节点等两周人事部的入职流程要调整下个月排期这种状况催生了我们对低代码审批流设计器的探索。目前主流的流程设计解决方案主要有三种Activiti原生的Modeler、BPMN.js以及类似钉钉的设计器。Activiti Modeler功能最强大但它的操作界面复杂得像个飞机驾驶舱非技术人员根本无从下手。BPMN.js算是折中方案但依然需要理解BPMN规范。而钉钉式的设计器把复杂度隐藏在背后给用户呈现的是发起人→审批人→抄送这样直观的流程视图这才是业务人员真正需要的。2. 技术选型与整体架构要实现一个易用的流程设计器技术栈的选择至关重要。我们采用SpringBoot作为后端框架它的自动配置特性让集成Activiti7变得非常简单。前端选择React它的组件化开发模式特别适合构建可复用的流程节点组件。而Activiti7作为工作流引擎提供了稳定的流程执行能力。整个系统的架构可以分为三层展示层React实现的可视化设计器提供拖拽式操作体验业务逻辑层SpringBoot处理业务规则和流程控制流程引擎层Activiti7负责流程的运行时管理这种架构最精妙的地方在于职责划分清晰。前端只需要关心如何生成合规的BPMN XML后端只需要存储和部署这些XML二者通过定义良好的接口协作。我在项目中实测发现当流程需要调整时业务人员在前端拖拽几下就能完成完全不需要后端介入。3. 核心实现可视化设计器开发设计器的核心是要把复杂的BPMN规范隐藏起来给用户最简单的操作界面。我们参考钉钉的设计将流程抽象为几个基础节点const nodeTypes [ { type: start, name: 发起人, icon: user }, { type: approver, name: 审批人, icon: check }, { type: notifier, name: 抄送人, icon: mail }, { type: condition, name: 条件分支, icon: fork }, { type: end, name: 结束, icon: flag } ]实现拖拽功能时我们使用了React-DnD库。这里有个小技巧每个节点组件都设计成智能的它们知道自己能连接哪些类型的节点。比如审批节点后面不能直接接发起节点这些校验规则都封装在组件内部const canConnect (sourceType, targetType) { const rules { start: [approver, notifier, condition], approver: [approver, notifier, condition, end], notifier: [approver, notifier, condition, end], condition: [approver, notifier, end], end: [] } return rules[sourceType].includes(targetType) }对于条件分支这种复杂节点我们采用了递归组件的设计方式。每个分支都是一个独立的设计区域可以继续添加子节点。在保存时这些嵌套结构会被扁平化处理成Activiti能理解的排他网关(Exclusive Gateway)。4. 与Activiti引擎的深度集成设计器生成的XML需要与Activiti完美配合。我们在后端设计了几个关键接口RestController RequestMapping(/api/process) public class ProcessController { PostMapping(/deploy) public String deploy(RequestBody String bpmnXml) { Deployment deployment repositoryService.createDeployment() .addString(process.bpmn20.xml, bpmnXml) .deploy(); return deployment.getId(); } GetMapping(/definition/{id}) public String getDefinition(PathVariable String id) { ProcessDefinition definition repositoryService.createProcessDefinitionQuery() .deploymentId(id) .singleResult(); InputStream stream repositoryService.getResourceAsStream( definition.getDeploymentId(), definition.getResourceName()); return IOUtils.toString(stream, UTF-8); } }这里特别要注意的是多实例任务的处理。比如当审批设置为会签时需要在XML中配置multiInstanceLoopCharacteristicsbpmn2:userTask idtask1 name会签审批 bpmn2:extensionElements activiti:collectionsigners/activiti:collection activiti:elementVariablesigner/activiti:elementVariable /bpmn2:extensionElements bpmn2:multiInstanceLoopCharacteristics activiti:collection${signers} activiti:elementVariablesigner bpmn2:completionCondition ${nrOfCompletedInstances/nrOfInstances 0.5} /bpmn2:completionCondition /bpmn2:multiInstanceLoopCharacteristics /bpmn2:userTask5. 动态任务分配的实现技巧审批流程最复杂的部分莫过于动态指定审批人。我们通过实现ExecutionListener接口支持了多种分配方式Service public class TaskAssignmentListener implements ExecutionListener { Override public void notify(DelegateExecution execution) { String assigneeType (String) execution.getVariable(assigneeType); switch(assigneeType) { case SPECIFIED_USER: handleSpecifiedUser(execution); break; case DEPARTMENT_HEAD: handleDepartmentHead(execution); break; case ROLE_BASED: handleRoleBased(execution); break; case INITIATOR: handleInitiator(execution); break; } } private void handleDepartmentHead(DelegateExecution execution) { Integer startDeptId (Integer) execution.getVariable(startDeptId); Integer level (Integer) execution.getVariable(approvalLevel); ListUser heads userService.findDepartmentHeads(startDeptId, level); execution.setVariable(approvers, heads); } }对于抄送功能我们采用了ServiceTask加监听器的方案。抄送人配置保存在流程变量中流程到达抄送节点时会调用对应的JavaDelegate实现Service(copyService) public class CopyServiceDelegate implements JavaDelegate { Override public void execute(DelegateExecution execution) { ListInteger userIds (ListInteger) execution.getVariable(copyToUsers); // 保存抄送记录 copyService.saveCopyTasks(execution.getProcessInstanceId(), userIds); // 发送通知 notificationService.notifyUsers(userIds); } }6. 前端数据结构的巧妙设计流程设计器的核心难点在于如何管理节点之间的关系。我们借鉴图论中的邻接表思想设计了一套专门的数据结构class ProcessGraph { constructor() { this.nodes new Map(); // 节点ID到节点的映射 this.edges []; // 所有连线 } addNode(node) { this.nodes.set(node.id, { ...node, incoming: [], // 入边 outgoing: [] // 出边 }); } addEdge(sourceId, targetId, condition) { const edge { id: generateId(), source: sourceId, target: targetId, condition }; this.edges.push(edge); // 更新节点关系 this.nodes.get(sourceId).outgoing.push(edge.id); this.nodes.get(targetId).incoming.push(edge.id); return edge; } }这种设计使得我们可以轻松实现各种复杂操作。比如插入一个新节点insertNodeAfter(existingNodeId, newNode) { const existingNode this.nodes.get(existingNodeId); this.addNode(newNode); // 重定向原有连线 existingNode.outgoing.forEach(edgeId { const edge this.edges.find(e e.id edgeId); this.addEdge(newNode.id, edge.target, edge.condition); this.removeEdge(edgeId); }); // 添加新连线 this.addEdge(existingNodeId, newNode.id); }7. 性能优化与踩坑经验在实际项目中我们遇到了几个性能瓶颈。首先是大型流程的渲染问题。当节点超过50个时前端会出现明显卡顿。我们最终采用的解决方案是实现画布的懒加载只渲染可视区域内的节点使用Web Worker处理复杂的布局计算对React组件进行memo优化另一个坑是条件分支的验证。Activiti要求每个分支都必须有明确的出口条件否则部署时会报错。我们的解决方案是在前端做预校验validateConditions(gateway) { const hasDefault gateway.outgoing.some(edge !edge.condition || edge.condition.trim() ); if (!hasDefault gateway.outgoing.length 1) { throw new Error(必须有一个无条件分支作为默认路径); } gateway.outgoing.forEach(edge { if (edge.condition) { try { new Function(variables, return ${edge.condition}); } catch (e) { throw new Error(条件表达式错误: ${edge.condition}); } } }); }数据库设计也有讲究。流程定义和实例数据要分开存储我们采用的表结构包括ACT_RE_PROCDEF存储部署的流程定义ACT_RU_TASK运行中的任务ACT_HI_TASKINST历史任务记录CUSTOM_PROCESS_FORM自定义的业务表单关联8. 从开发到部署的全流程完整的流程生命周期包括几个关键阶段设计阶段业务人员在设计器中拖拽配置流程保存为BPMN XML部署阶段将XML发布到Activiti引擎生成流程定义运行阶段用户发起流程实例引擎按照定义执行监控阶段管理员可以查看流程运行状态必要时进行干预部署环节有个实用技巧——版本控制。我们通过在流程定义key后附加版本号的方式实现多版本共存public String deployProcess(String bpmnXml, String processKey) { // 查询当前最新版本 long version repositoryService.createProcessDefinitionQuery() .processDefinitionKey(processKey) .latestVersion() .singleResult() .getVersion() 1; // 部署新版本 Deployment deployment repositoryService.createDeployment() .addString(processKey -v version .bpmn20.xml, bpmnXml) .name(processKey v version) .deploy(); // 关联业务表单 formService.saveFormRelation(processKey, version, formId); return deployment.getId(); }对于已经运行的流程实例可以通过流程迁移API将其切换到新版本public void migrateRunningInstances(String oldDefinitionId, String newDefinitionId) { runtimeService.createProcessInstanceQuery() .processDefinitionId(oldDefinitionId) .list() .forEach(instance - { runtimeService.createChangeActivityStateBuilder() .processInstanceId(instance.getId()) .moveActivityIdTo( getCurrentActivityId(instance.getId()), getCorrespondingActivityId(newDefinitionId) ) .changeState(); }); }9. 权限控制与安全性设计企业级审批流必须考虑权限问题。我们设计了三级权限控制流程设计权限指定哪些角色可以创建/修改流程定义流程发起权限控制哪些人可以看到并使用特定流程任务处理权限验证处理人是否有权审批当前任务在Spring Security中的实现示例PreAuthorize(hasPermission(#processKey, PROCESS_DEFINITION, EDIT)) PostMapping(/deploy/{processKey}) public ResponseEntity? deployProcess( PathVariable String processKey, RequestBody DeployRequest request) { // 部署逻辑 } PreAuthorize(hasPermission(#taskId, TASK, COMPLETE)) PostMapping(/task/{taskId}/complete) public ResponseEntity? completeTask( PathVariable String taskId, RequestBody TaskCompleteRequest request) { // 任务完成逻辑 }对于敏感操作如删除流程定义我们还添加了审计日志Aspect Component public class ProcessAuditAspect { AfterReturning( pointcut execution(* org.activiti.engine.RepositoryService.delete*(..)), returning result) public void logDeletion(JoinPoint jp, Object result) { Object[] args jp.getArgs(); String userId SecurityContextHolder.getContext() .getAuthentication().getName(); auditService.logAction( userId, DELETE_PROCESS, args[0].toString()); } }10. 扩展性与二次开发一个好的流程设计器应该易于扩展。我们通过以下几种机制支持二次开发自定义节点类型通过注册机制添加新节点// 注册自定义节点 designer.registerNodeType(custom-node, { template: div classcustom-node.../div, properties: [ {name: customProp, label: 自定义属性} ], validator: (node) { if(!node.customProp) { return 必须设置自定义属性; } } });后端插件通过SPI机制扩展Activiti行为public interface ProcessPlugin { default void beforeDeploy(DeploymentBuilder builder) {} default void afterStart(DelegateExecution execution) {} default void beforeTaskCreate(TaskEntity task) {} } // META-INF/services/org.activiti.plugin.ProcessPlugin com.example.plugin.NotificationPlugin业务规则引擎集成与Drools等规则引擎结合实现智能路由Service public class RuleTaskListener implements TaskListener { Autowired private KieContainer kieContainer; Override public void notify(DelegateTask task) { KieSession session kieContainer.newKieSession(); session.insert(task.getVariables()); session.fireAllRules(); session.dispose(); } }在实际项目中我们还集成了消息通知、电子签名、OCR识别等扩展功能这些都可以通过类似的插件机制实现。11. 测试策略与质量保障流程引擎的测试需要特别关注以下几个方面单元测试验证单个节点的行为Test public void testApprovalTaskAssignment() { // 准备测试数据 MapString, Object variables new HashMap(); variables.put(assigneeType, DEPARTMENT_HEAD); variables.put(startDeptId, 101); // 执行测试 runtimeService.startProcessInstanceByKey( testProcess, variables); // 验证结果 Task task taskService.createTaskQuery().singleResult(); assertThat(task.getAssignee()).isEqualTo(user101); }集成测试验证完整流程执行SpringBootTest public class ProcessIntegrationTest { Test public void testHolidayApprovalProcess() { // 部署流程 deployTestProcess(); // 发起流程 ProcessInstance instance runtimeService .startProcessInstanceByKey(holidayRequest); // 验证任务分配 Task task taskService.createTaskQuery() .processInstanceId(instance.getId()) .singleResult(); assertThat(task.getName()).isEqualTo(主管审批); // 模拟审批通过 taskService.complete(task.getId(), singletonMap(approved, true)); // 验证流程结束 assertThat(runtimeService .createProcessInstanceQuery() .processInstanceId(instance.getId()) .count()).isZero(); } }性能测试模拟高并发场景Test public void testConcurrentProcessStart() { int threadCount 100; ExecutorService executor Executors.newFixedThreadPool(threadCount); CountDownLatch latch new CountDownLatch(threadCount); AtomicInteger successCount new AtomicInteger(); for (int i 0; i threadCount; i) { executor.execute(() - { try { runtimeService.startProcessInstanceByKey(concurrentProcess); successCount.incrementAndGet(); } finally { latch.countDown(); } }); } latch.await(); assertThat(successCount.get()).isEqualTo(threadCount); }12. 最佳实践与项目经验经过多个项目的实践我们总结出以下经验模板化设计为常见流程(如请假、报销)创建模板用户可以在模板基础上调整而不是从头创建版本回滚机制保存每次部署的历史版本当新版本出现问题时可以快速回退测试环境验证重要流程修改先在测试环境验证通过后再部署到生产环境监控告警对长时间卡住的流程实例设置自动告警文档自动化根据流程定义自动生成操作文档保持文档与实现同步一个特别实用的技巧是使用流程变量存储业务上下文// 启动流程时携带业务数据 runtimeService.startProcessInstanceByKey( expenseApproval, businessKey, Map.of( amount, expense.getAmount(), category, expense.getCategory(), applicant, expense.getApplicantId() ) ); // 在监听器中获取业务数据 public void notify(DelegateTask task) { BigDecimal amount (BigDecimal) task.getVariable(amount); if (amount.compareTo(new BigDecimal(10000)) 0) { task.setVariable(requireCFOApproval, true); } }13. 与现代前端框架的深度集成为了让设计器体验更流畅我们充分利用了现代前端框架的特性状态管理使用Redux管理复杂的流程状态const processReducer (state initialState, action) { switch (action.type) { case ADD_NODE: return { ...state, nodes: [...state.nodes, action.payload], edges: addEdgesForNewNode(state, action.payload) } case UPDATE_NODE: return { ...state, nodes: state.nodes.map(node node.id action.payload.id ? action.payload : node ) } // 其他case... } }动态表单根据节点类型渲染不同的配置表单function NodePropertiesPanel({ node }) { const formMap { approver: ApproverForm, notifier: NotifierForm, condition: ConditionForm }; const FormComponent formMap[node.type] || DefaultForm; return FormComponent node{node} /; }实时协作通过WebSocket实现多用户同时编辑const socket new WebSocket(/api/process-collab); function handleNodeUpdate(update) { socket.send(JSON.stringify({ type: NODE_UPDATE, payload: update })); } socket.onmessage (event) { const message JSON.parse(event.data); if (message.type REMOTE_UPDATE) { store.dispatch(applyRemoteUpdate(message.payload)); } };14. 移动端适配与响应式设计随着移动办公的普及流程设计器也需要适配移动设备。我们采用了几种技术方案响应式布局使用CSS Grid和Flexbox实现自适应的画布布局.process-designer { display: grid; grid-template-columns: 80px 1fr 300px; height: 100vh; } media (max-width: 768px) { .process-designer { grid-template-columns: 60px 1fr; } .properties-panel { position: absolute; right: 0; width: 100%; } }手势操作支持触摸屏上的拖拽、缩放等操作const zoomable new PinchZoom({ element: document.getElementById(canvas), onZoom: (scale) updateViewport(scale) }); draggable.on(drag, (event) { if (isTouchDevice()) { // 处理触摸拖拽逻辑 } });离线支持通过Service Worker缓存关键资源支持断网操作// service-worker.js self.addEventListener(install, (event) { event.waitUntil( caches.open(process-designer-v1).then((cache) { return cache.addAll([ /, /static/js/main.js, /static/css/styles.css, /api/process-templates ]); }) ); });15. 与现有系统的集成方案在实际企业环境中流程设计器需要与多个现有系统集成单点登录通过OAuth2/OIDC与企业身份提供商集成Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .oauth2Login() .userInfoEndpoint() .userService(customOAuth2UserService); } }组织架构同步定期从HR系统同步用户和部门数据Scheduled(cron 0 0 3 * * ?) public void syncOrganizationData() { ListDepartment departments hrServiceClient.getAllDepartments(); departmentRepository.sync(departments); ListUser users hrServiceClient.getAllUsers(); userRepository.sync(users); }消息通知集成支持邮件、企业微信、钉钉等多种通知方式public interface NotificationService { void sendNotification(NotificationRequest request); } Service public class CompositeNotificationService implements NotificationService { Override public void sendNotification(NotificationRequest request) { notificationServices.forEach(service - { try { service.sendNotification(request); } catch (Exception e) { log.error(通知发送失败, e); } }); } }16. 项目演进与未来规划随着项目的持续发展我们规划了几个演进方向智能流程推荐基于历史数据使用机器学习推荐流程优化方案# 示例伪代码 def recommend_optimization(process_def_id): historical_data load_execution_data(process_def_id) bottlenecks detect_bottlenecks(historical_data) return generate_optimization_suggestions(bottlenecks)流程挖掘从日志数据中发现实际执行路径与设计差异public class ProcessMiningAnalyzer { public ProcessDiscoveryResult analyze(String processDefinitionId) { ListHistoricActivityInstance instances historyService .createHistoricActivityInstanceQuery() .processDefinitionId(processDefinitionId) .list(); return new ProcessDiscoveryEngine() .discover(instances); } }跨系统流程编排通过API网关集成多个系统的业务流程# 流程定义示例 - step: Call CRM API type: http config: url: https://crm.api/update method: POST body: customerId: ${variables.customerId} status: APPROVED - step: Notify ERP type: kafka config: topic: order-updates message: eventType: APPROVAL_COMPLETE processId: ${execution.id}17. 总结与实用建议在多个项目实施过程中我们发现以下几个要点对成功至关重要渐进式复杂度对用户隐藏不必要的复杂性随着用户熟练度提升逐步展示高级功能即时反馈在设计器中提供实时校验和预览避免用户等到部署时才发现问题性能监控对流程执行进行全方位监控特别是长时间运行的任务文档即代码将流程文档与实现保持同步可以考虑使用类似Swagger的机制自动生成文档用户培训即使是最简单的设计器适当的培训也能显著提高使用效率一个特别实用的技巧是建立流程资产库将经过验证的流程片段保存为可复用的组件。当用户创建新流程时可以从资产库中快速组合现有组件而不是每次都从头开始。这不仅提高了效率也保证了流程设计的质量一致性。

更多文章