尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
工作流请假实例:从零搭建审批流的完整指南与避坑实践
简介这是一份面向工作流初学者与Java Web开发入门者的请假流程实例源码围绕企业管理中常见的请假审批场景演示如何用工作流思想组织任务分配、审批与通知等环节并以Struts2框架承载请求处理与业务逻辑同时涉及中文编码过滤等常见问题的处理思路。压缩包共42个文件约51KB以xml配置、jsp页面、properties属性文件为主辅以java源码、class字节码及少量图片与工程配置分别对应流程定义、视图展示、字符编码设置和业务实现等用途结构紧凑便于按模块查阅。目前已有253人学习下载。读者可从中获得一套可运行的请假流程示例理解Action、拦截器与持久化层的协作方式并借鉴中文乱码的排查与配置经验适合作为工作流与Struts2入门练手项目。1. 工作流请假实例从零搭一套能跑通的审批流到底要填多少坑刚接手审批流开发那会儿我以为“请假”是最简单的场景——员工提交、主管审批、结束。结果真动手才发现光是“请假三天以上要不要总监审批”“审批人离职了流程卡死怎么办”“驳回后能不能修改重提”这几个问题就够写一整天。工作流请假实例之所以被反复拿来练手不是因为它简单而是因为它把流程定义、节点跳转、条件分支、任务分配、状态回滚这些核心机制全串起来了。你如果正在找一个能跑通、能改、能扩展的最小闭环这个实例就是干这个的。它适合刚接触工作流引擎的后端开发也适合需要快速验证审批模型的产品或测试同学。下面我按实际拆解的顺序把流程定义、引擎选型、代码落地和踩坑记录一次讲透。2. 流程定义与引擎选型为什么请假单最适合拿来试刀2.1 请假单里藏着的四个工作流核心概念很多人一上来就急着写代码结果流程定义文件写到一半发现节点类型选错了返工重来。请假单虽然业务简单但它天然覆盖了工作流最基础的四个概念流程定义、任务节点、条件网关、流转条件。流程定义就是整张请假单的“地图”它规定了从提交到结束一共经过哪些节点。任务节点分两种用户任务和系统任务。用户任务需要人参与比如“主管审批”系统任务自动执行比如“发送通知”。条件网关是请假单里最容易写错的地方——比如“请假天数大于3天走总监审批否则直接到人事备案”这个判断就挂在网关上。流转条件则是连接线上的表达式决定流程往哪走。我一般建议新手先把这四个概念在纸上画一遍再打开建模器拖拽。画的时候重点标三个东西谁发起、谁审批、什么条件下换人。这三样定了流程定义基本不会大改。2.2 引擎选型别为了请假单上重型武器工作流引擎的选择直接决定你后面写代码的体量。常见做法是分三档轻量级用状态机自己撸中量级用 Activiti 或 Flowable重量级用 Camunda 或 Zeebe。请假单这个场景我一般会先问一句后面要不要接会签、加签、驳回任意节点、动态改流程如果只是固定两三级审批状态机足够代码量少、调试直观。如果预期半年内会加会签和条件分支直接上 Flowable它的 BPMN 2.0 支持完整社区资料也多。这里有个选型误区很多人觉得 Camunda 功能强就无脑上结果请假单跑起来发现光配置就写了三百行后面维护的人看不懂。引擎不是越重越好是跟你的流程复杂度匹配。请假单的流程节点通常不超过十个Flowable 的轻量嵌入模式完全够用启动快、依赖少跟 Spring Boot 整合也顺。提示选型时先确认团队里有没有人写过 BPMN 文件。如果没人写过选带图形化建模器的引擎能省掉大量手写 XML 的时间。2.3 用 BPMN 画一张能跑的请假流程图下面这张流程定义我改过很多版最终稳定下来的结构是开始事件 → 提交请假申请用户任务→ 天数判断网关 → 主管审批用户任务→ 总监审批用户任务仅大于3天走→ 人事备案系统任务→ 结束事件。用 Flowable 的 BPMN XML 表示核心片段如下process idleaveProcess name请假审批流程 startEvent idstart / userTask idsubmitLeave name提交请假申请 / exclusiveGateway iddaysGateway / userTask idmanagerApprove name主管审批 / userTask iddirectorApprove name总监审批 / serviceTask idhrRecord name人事备案 / endEvent idend / sequenceFlow sourceRefstart targetRefsubmitLeave / sequenceFlow sourceRefsubmitLeave targetRefdaysGateway / sequenceFlow sourceRefdaysGateway targetRefmanagerApprove conditionExpression xsi:typetFormalExpression ${leaveDays 3} /conditionExpression /sequenceFlow sequenceFlow sourceRefdaysGateway targetRefdirectorApprove conditionExpression xsi:typetFormalExpression ${leaveDays 3} /conditionExpression /sequenceFlow sequenceFlow sourceRefmanagerApprove targetRefhrRecord / sequenceFlow sourceRefdirectorApprove targetRefhrRecord / sequenceFlow sourceRefhrRecord targetRefend / /process这段 XML 的逻辑很直白daysGateway是排他网关它只选一条路走。leaveDays是流程变量在提交申请时由前端传入。注意条件表达式里用的是${}包裹的布尔判断Flowable 会在网关节点自动求值。如果两个条件都不满足引擎会抛异常所以实际项目里我会再加一条默认流兜底。参数说明leaveDays建议用整数类型避免浮点数比较时出现精度问题。conditionExpression里的变量名必须和流程启动时传入的 key 完全一致大小写敏感。serviceTask的hrRecord需要绑定一个 Java 委托类或表达式否则流程走到这里会报“未实现”错误。3. 代码落地从提交申请到审批通过的完整链路3.1 启动流程实例与变量传递流程定义部署好之后第一步是启动实例。下面这段代码是 Spring Boot 里常见的写法我把它拆成三步拿引擎、设变量、启动。Autowired private RuntimeService runtimeService; public String startLeaveProcess(LeaveRequest request) { // 1. 准备流程变量 MapString, Object variables new HashMap(); variables.put(applicant, request.getApplicant()); variables.put(leaveDays, request.getLeaveDays()); variables.put(reason, request.getReason()); // 2. 用业务单号作为 businessKey方便后续查询 String businessKey LEAVE- request.getId(); // 3. 启动流程实例 ProcessInstance instance runtimeService.startProcessInstanceByKey( leaveProcess, businessKey, variables); return instance.getId(); }逻辑说明startProcessInstanceByKey的第一个参数是 BPMN 文件里process idleaveProcess的值必须完全一致。第二个参数businessKey是业务主键我习惯用“前缀单号”的格式后面查历史记录时直接用它过滤比用流程实例 ID 可读性好得多。第三个参数variables就是流程变量它在整个流程生命周期内都能被网关和任务监听器读取。参数坑点leaveDays如果传的是字符串 3网关里的${leaveDays 3}会抛类型转换异常。我一般在入口做一次强制类型转换或者用Integer.parseInt兜底。另外变量名不要用start、end这类跟 BPMN 元素 id 重名的词引擎解析时可能冲突。3.2 查询待办任务与完成审批流程启动后主管登录系统应该能看到待办。查询待办用TaskService完成审批也是用它。Autowired private TaskService taskService; // 查询某人的待办 public ListTask getTodoList(String assignee) { return taskService.createTaskQuery() .taskAssignee(assignee) .orderByTaskCreateTime().desc() .list(); } // 完成审批 public void approve(String taskId, boolean pass, String comment) { MapString, Object vars new HashMap(); vars.put(approved, pass); vars.put(comment, comment); // 先添加审批意见再完成任务 taskService.addComment(taskId, null, comment); taskService.complete(taskId, vars); }逻辑说明taskAssignee对应 BPMN 里userTask的assignee属性如果流程定义里没写死就需要在启动时通过变量动态指定或者用监听器在任务创建时设置。complete方法会把流程推进到下一个节点同时把vars合并进流程变量。approved这个变量在后面的网关或监听器里可以用来判断通过还是驳回。参数说明addComment的第二个参数是流程实例 ID传 null 时 Flowable 会自动关联当前任务所在的实例。审批意见建议单独存一张业务表引擎自带的 comment 表字段有限复杂意见比如带附件还是自己管理更灵活。3.3 驳回与重新提交的状态处理驳回是请假流里最容易出 bug 的地方。常见做法有两种一种是驳回到发起人流程回到“提交申请”节点另一种是驳回到上一个审批节点。我一般推荐第一种因为请假单的修改权在申请人手里驳回到上一级审批人没有意义。public void rejectToApplicant(String taskId, String reason) { // 1. 获取当前流程实例 Task task taskService.createTaskQuery().taskId(taskId).singleResult(); String processInstanceId task.getProcessInstanceId(); // 2. 给流程实例打上驳回标记 runtimeService.setVariable(processInstanceId, rejected, true); runtimeService.setVariable(processInstanceId, rejectReason, reason); // 3. 通过跳转指令回到提交节点 runtimeService.createChangeActivityStateBuilder() .processInstanceId(processInstanceId) .moveActivityIdTo(task.getTaskDefinitionKey(), submitLeave) .changeState(); }逻辑说明createChangeActivityStateBuilder是 Flowable 提供的动态跳转 API它能把流程从当前节点强制拉到指定节点。moveActivityIdTo的第一个参数是当前任务的定义 key第二个是目标节点 id。执行后流程会回到“提交请假申请”节点申请人可以修改天数后重新提交。注意动态跳转不会自动清理历史任务如果业务上需要展示“驳回记录”得自己查历史表。另外跳转后原来的流程变量还在重新提交时记得覆盖leaveDays否则网关判断用的还是旧值。这个坑我踩过申请人改了天数但审批人没变查了半天才发现变量没更新。4. 避坑与排查请假流跑不通时先看这五条4.1 流程启动报“找不到流程定义”现象调用startProcessInstanceByKey时抛FlowableObjectNotFoundException提示 no processes deployed with key。原因BPMN 文件没有部署成功或者部署时指定的 key 和代码里写的不一致。常见情况是文件放在resources/processes/下但没被自动扫描或者 XML 里process id写成了别的名字。解决先查act_re_procdef表看key_字段的值到底是什么。如果表是空的检查flowable.check-process-definitions配置是否为 true以及 BPMN 文件路径是否在flowable.process-definition-location-prefix指定的目录下。4.2 网关条件不生效流程走错分支现象请假天数明明大于3天流程却直接走到人事备案跳过了总监审批。原因条件表达式里的变量名写错或者变量类型不匹配。比如 BPMN 里写的是${leaveDays 3}但启动时传的 key 是leave_days引擎取不到值就默认 false于是走了另一条路。解决在网关节点前加一个执行监听器把当前所有流程变量打印出来。确认变量名和类型后再核对 BPMN 里的表达式。如果用的是字符串比较记得加引号比如${leaveType 事假}。4.3 审批人离职后任务卡死现象主管离职后他名下的待办任务没人能处理流程一直停在“主管审批”节点。原因任务分配时写死了 assignee没有考虑人员变动。解决把 assignee 改成 candidateUsers 或 candidateGroups让一组人都能看到待办。如果必须指定个人加一个定时任务扫描超时未处理的任务自动转交给上级或管理员。Flowable 的taskService.createTaskQuery().taskUnassigned()可以查出所有未签收的任务配合claim方法重新分配。4.4 驳回后重新提交历史记录重复现象申请人驳回后修改再提交待办列表里出现了两条相同的审批任务。原因动态跳转时没有结束当前任务或者跳转后旧任务的历史记录没清理干净。解决跳转前先调用taskService.complete或taskService.resolveTask把当前任务标记为已完成。如果业务上需要保留驳回痕迹就在业务表里单独记一条驳回日志不要依赖引擎的历史任务表。4.5 流程变量在并发审批时被覆盖现象会签场景下两个审批人同时提交意见后提交的把先提交的变量覆盖了。原因runtimeService.setVariable是全局变量多个任务同时写同一个 key 会互相覆盖。解决用setVariableLocal把变量绑定到当前任务或执行实例上或者把审批意见存到业务表流程变量只保留最终结果。如果必须用全局变量加乐观锁或分布式锁但请假单场景一般用不上这么重。5. 进阶技巧用监听器把审批日志和业务表串起来流程能跑通只是第一步真正上线还要解决“审批记录怎么查”“流程状态怎么同步到业务表”这两个问题。我一般会在流程定义里挂两类监听器执行监听器和任务监听器。执行监听器挂在sequenceFlow上用来记录节点流转时间。任务监听器挂在userTask上用来在任务创建和完成时写业务日志。下面这段代码是一个任务监听器的实现它把审批人、审批时间、审批意见写进自定义的leave_approve_log表。Component(leaveTaskListener) public class LeaveTaskListener implements TaskListener { Autowired private ApproveLogMapper approveLogMapper; Override public void notify(DelegateTask delegateTask) { String eventName delegateTask.getEventName(); if (create.equals(eventName)) { // 任务创建时记录待办生成时间 ApproveLog log new ApproveLog(); log.setTaskId(delegateTask.getId()); log.setProcessInstanceId(delegateTask.getProcessInstanceId()); log.setAssignee(delegateTask.getAssignee()); log.setAction(CREATE); log.setCreateTime(new Date()); approveLogMapper.insert(log); } else if (complete.equals(eventName)) { // 任务完成时更新审批结果 ApproveLog log new ApproveLog(); log.setTaskId(delegateTask.getId()); log.setAction(COMPLETE); log.setComment((String) delegateTask.getVariable(comment)); log.setCompleteTime(new Date()); approveLogMapper.insert(log); } } }逻辑说明delegateTask.getEventName()返回当前触发的事件名常见的有create、assignment、complete、delete。我一般只处理create和complete前者用来生成待办记录后者用来落审批结果。delegateTask.getVariable可以读取流程变量但注意在complete事件里变量可能还没合并进去稳妥做法是从delegateTask.getVariables()里取。参数说明监听器类需要用Component注册成 Spring Bean然后在 BPMN 里通过flowable:taskListener引用写法是flowable:taskListener eventcreate delegateExpression${leaveTaskListener}。如果不想用表达式也可以直接写全类名但那样没法注入 Mapper。验证监听器是否生效最简单的办法是看日志表里有没有数据。如果create事件没触发检查 BPMN 里监听器的event属性是否拼写正确如果complete事件没触发确认任务是不是通过complete方法结束的用resolveTask不会触发 complete 事件。还有一个实用技巧把流程状态同步到业务表时不要在每个监听器里单独写更新逻辑而是用一个统一的ProcessStatusSyncListener挂在流程结束事件上一次性把最终状态写回去。这样业务表的状态字段永远和流程实例的最终状态一致查历史数据时不用再关联引擎表。从那以后我每次搭新的审批流都强制先把监听器配好再写业务代码宁可前期多花半小时也不想到处补日志。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

Jupyter Notebook转Python脚本:.ipynb转.py的完整指南

Jupyter Notebook转Python脚本:.ipynb转.py的完整指南

我接触过不少刚开始用Python做数据分析或者机器学习的朋友,几乎每个人都会遇到同一个问题:辛辛苦苦在Jupyter Notebook里调试好的代码,转头要交付或者部署的时候,对面只要一份干净的.py文件。很多人第一反应是直接把.ipynb后缀改成…

📅 2026/10/19 5:30:42
基于深度学习的商品图像检索系统毕业设计源码(源码+lw+部署文档+讲解等)

基于深度学习的商品图像检索系统毕业设计源码(源码+lw+部署文档+讲解等)

博主介绍:✌ 专注于VUE,小程序,安卓,Java,python,物联网专业, 从事毕业指导,项目实战✌选取一个适合的毕业设计题目很重要。✌关注✌私信我✌具体的问题,我会尽力帮助你。一、研究目的在当今电子商务与数字…

📅 2026/10/19 5:30:42
Wireshark抓包分析IEC 104报文:从链路机制到现场排查实战

Wireshark抓包分析IEC 104报文:从链路机制到现场排查实战

干电力自动化、SCADA系统调试这一行,几乎都会遇到这样的场景:站端设备与调度主站之间的通信突然中断,或者遥测数据出现跳变,厂家说没问题,主站说收到的是错的,两边来回拉扯,最后才发现问题出在链…

📅 2026/10/19 5:30:42
MORE NEWS

更多资讯

📰

基于YOLOv26的绝缘子缺陷检测系统:从数据到部署全流程实践

电力绝缘子缺陷检测这几年在巡检圈子里真的是绕不开的话题,输电线路上的绝缘子数量大、分布广,尤其是山区和林区的杆塔,人工巡检一趟成本极高,而且很多缺陷在肉眼看来并不明显。我这次要分享的,是同事前段时间做的一个…

📰

作业车间调度中深度强化学习状态特征设计实战与避坑指南

作业车间调度是制造系统里最经典的优化问题之一。这些年我一直在帮工厂做排产相关的算法落地,最深的体会是:调度算法从理论到能用,中间隔着一条叫"状态表达"的河。模型选得再花哨,超参调得再好,状态特征设计…

📰

视频智能分析算法实战:从模型选型到部署优化的关键细节

这些年做视频智能分析算法,从最早的帧差法、背景建模,到现在的深度学习端到端推理,技术路线换了好几代,踩过的坑也足够写一本小册子了。视频智能分析算法,说白了就是让计算机"看懂"摄像头画面里正在发生什么…

📰

Qt5 WebEngine SEO行为模拟实验框架

简介:本资源是一套基于Qt5框架开发的SEO快排点击软件源码,面向具备C和Qt基础的开发者,用于研究黑帽SEO中模拟用户点击行为的技术实现原理。代码通过多浏览器UA切换、随机停留与点击逻辑、网络请求调度等机制,尝试影响搜索引擎对目…

📰

Gemma 4本地部署实战指南:从硬件评估到参数调优

1. 为什么要在本地跑 Gemma 4:价值与适用场景1.1 本地部署解决的三大核心问题把 Gemma 4 跑在本地,这件事本身就有很强的吸引力。先说一个大白话结论:大模型本地部署不是你非得有八张专业显卡才能碰的领域。Gemma 4 属于开放权重模型里对消费…

📰

二自由度横摆动力学与自适应参数MPC路径跟踪控制

做自动驾驶路径跟踪的人,基本都会遇到一个尴尬场景:同一套MPC参数,在低速园区绕个圆道又稳又准,车速一旦提到80甚至120,过连续弯道就明显开始抖,要么横向误差变大,要么方向盘来回修正。这个问题…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬