
简介这是一份面向工作流初学者与Java Web开发入门者的请假流程实例源码围绕企业管理中常见的请假审批场景演示如何用工作流思想组织任务分配、审批与通知等环节并借助Struts2框架完成请求处理与业务逻辑落地。压缩包共42个文件约51KB以xml配置、jsp页面、properties属性文件、java源码与class字节码为主另含少量图片与工程配置项分别承担流程定义、视图展示、字符编码设置及业务实现等职责。资源中涉及Struts2的Action与拦截器机制并针对中文编码乱码问题给出配置思路同时包含数据库脚本与测试用例便于读者理解从申请提交到审批记录的完整链路。目前已有253人学习下载适合希望以最小案例切入工作流与MVC开发、快速搭建可运行请假系统的读者参考。1. 工作流请假实例为什么“能跑通”和“敢上线”是两回事很多团队第一次做审批类需求都会从「工作流请假实例」入手。它看起来简单员工填单、主管审批、HR 归档三步走完。但真正把它放到生产环境你会发现真正难的不是画流程图而是状态怎么落库、驳回后能不能重新提交、审批人离职了怎么办、并发审批会不会把状态改乱。我见过太多 Demo 在本地跑得飞起一上测试环境就出现「同一张单子两个主管都能点通过」的玄学问题。这篇笔记面向两类人一是刚接触工作流引擎、想拿请假场景练手的开发者二是已经用过某款流程引擎、但被状态机和并发问题坑过的工程师。我会用一个最小可复现的请假流程把建模、状态流转、审批人解析、驳回重提、并发控制这几件事拆开讲清楚最后给出一个可以直接抄的验证清单。整套方案不依赖特定厂商用常见的工作流引擎或自研状态机都能落地。2. 请假流程建模从表单字段到状态机的映射2.1 先定状态再定节点顺序不能反很多人一上来就画流程图结果节点画了七八个状态却只有「审批中」和「已完成」两个。这是典型的翻车起点。工作流引擎里的节点是「谁来做」状态是「单子现在处于什么阶段」。请假实例里我一般先固定这几个状态状态码含义可执行动作DRAFT草稿仅发起人可见提交、删除PENDING审批中等待当前节点处理通过、驳回、撤回REJECTED已驳回可修改后重提重新提交、作废APPROVED审批通过待归档归档ARCHIVED已归档只读无CANCELED发起人主动撤销无状态定好之后再往流程节点上映射。一个典型的请假流程节点顺序是发起人提交 → 直属主管审批 → 部门负责人审批可选→ HR 备案。注意「可选」节点不要用条件分支硬编码在代码里而是用流程变量控制否则后面加一个「三天以上需要总监审批」的规则时你会想把整个流程重写。2.2 用流程变量承载业务规则而不是写死在节点里请假实例里最常见的业务规则是「请假天数影响审批层级」。我一般把天数、请假类型、申请人职级作为流程变量传入节点上用条件表达式判断。下面是一段伪代码展示如何构造启动参数# 构造请假流程启动参数 def build_leave_process_vars(leave_request): leave_request: 包含 days, leave_type, applicant_level 的业务对象 返回流程引擎需要的变量字典 vars { days: leave_request.days, # 请假天数浮点数 leave_type: leave_request.leave_type, # 事假/病假/年假 applicant_level: leave_request.level, # P5/P6/P7 等职级 need_director: leave_request.days 3, # 超过3天需要总监审批 need_hr: leave_request.leave_type 年假 # 年假需要HR备案 } return vars这段代码的关键点在于need_director和need_hr是布尔变量流程定义里直接用它们做网关条件而不是在 Java 或 Python 代码里写 if-else 去决定走哪个节点。这样做的好处是当规则从「超过3天」变成「超过5天」时你只需要改配置或改这一处变量计算不用动流程定义文件。参数说明days用浮点数是为了支持半天假leave_type建议用枚举字符串而不是数字排查问题时日志可读性高很多applicant_level如果职级体系复杂可以再拆成level_rank整数方便比较。2.3 审批人解析别把主管 ID 硬编码在流程里审批人怎么来是请假实例里第二个容易翻车的地方。常见做法有三种发起时指定、运行时查组织架构、用流程引擎的候选人接口。我一般推荐「运行时查组织架构 流程变量兜底」。具体来说流程定义里主管审批节点的处理人表达式写成${orgService.getDirectLeader(applicantId)}由引擎在进入节点时调用。如果组织架构服务挂了或查不到人流程变量里再放一个fallback_approver作为兜底。下面是一个查询审批人的示例# 审批人解析服务带兜底逻辑 def resolve_approver(applicant_id, fallback_approverNone): 优先查组织架构查不到时返回兜底审批人 leader org_service.get_direct_leader(applicant_id) if leader is None: if fallback_approver: log.warning(f申请人 {applicant_id} 无直属主管使用兜底审批人) return fallback_approver raise BizException(无法确定审批人请联系管理员) return leader逻辑说明先查直属主管查不到时不要直接抛异常让流程卡死而是用兜底人继续流转同时打警告日志。参数fallback_approver一般由发起人在提交时从 HR 提供的备选列表里选或者由系统默认指向部门负责人。这个设计能避免「新员工还没分配主管就提请假」这种边界场景把流程堵死。3. 状态流转与并发控制把「重复审批」按死在数据库层3.1 每次状态变更都要带上前置状态条件审批接口最常见的写法是「先查单子判断状态是 PENDING然后更新为 APPROVED」。这个写法在并发下必出问题两个主管同时点通过两个线程都查到 PENDING都更新成功最后单子被审批了两次假期余额扣了两次。正确做法是把前置状态作为更新条件让数据库来保证原子性。下面是一段 SQL 示例-- 审批通过只有当前状态为 PENDING 且当前处理人是该审批人时才更新 UPDATE leave_request SET status APPROVED, approver_id #{approverId}, approve_time NOW(), version version 1 WHERE id #{requestId} AND status PENDING AND current_assignee #{approverId};执行后检查影响行数如果为 0说明状态已经被别人改过或者当前处理人不对直接返回「该单子已被处理」即可。参数说明current_assignee字段记录当前节点处理人避免 A 主管去点 B 主管的单子version字段是乐观锁如果你们用的是 JPA 或 MyBatis-Plus可以直接用Version注解不用手写。3.2 驳回后重提是新建流程实例还是复用驳回后重新提交有两种做法一是作废原实例新建一个流程实例二是原实例回退到发起节点重新走一遍。我一般选第二种因为审批历史能串在一条时间线上HR 查起来清楚。但复用实例有个坑流程变量需要重置。比如第一次提交时need_director是 true驳回后申请人把天数从 4 天改成 2 天need_director应该变成 false。如果变量没重置流程还是会走总监审批节点。所以重提接口里必须重新计算一遍流程变量并覆盖旧值。# 驳回后重新提交重置流程变量 def resubmit_leave_request(request_id, new_leave_data): instance process_engine.get_instance_by_biz_id(request_id) if instance.status ! REJECTED: raise BizException(只有已驳回的单子可以重新提交) new_vars build_leave_process_vars(new_leave_data) # 覆盖旧变量注意不要合并直接替换 process_engine.set_variables(instance.id, new_vars) process_engine.signal(instance.id, resubmit)逻辑说明set_variables用替换而不是合并避免旧变量残留。signal是触发流程从驳回状态回到发起节点的信号名具体名称取决于你用的引擎常见的是resubmit或restart。参数new_leave_data必须重新做一次校验不能信任前端传过来的天数。3.3 撤回与作废的边界什么时候允许什么时候禁止撤回是发起人的权利但不是什么阶段都能撤。我一般规定只有当前节点还没被处理时才能撤回。如果主管已经点了通过进入部门负责人节点发起人就不能撤了只能走「作废」流程由 HR 介入。实现上撤回接口同样要用条件更新UPDATE leave_request SET status CANCELED WHERE id #{requestId} AND status PENDING AND current_assignee IS NULL; -- 当前节点无人处理如果影响行数为 0说明已经被审批过了返回「当前状态不允许撤回」。这个细节很多 Demo 会忽略导致发起人在主管审批后还能撤回数据对不上。4. 避坑与排查请假实例上线后最容易踩的五个坑4.1 坑一审批人离职后单子卡死现象某主管离职后他名下所有 PENDING 的请假单永远停在审批节点发起人催也没用。原因审批人解析时把 ID 写进了流程变量或任务表离职后账号被禁用任务无法转办。解决在组织架构服务里加离职监听主管离职时自动把他名下所有待办任务转交给其上级或指定代理人。如果用的是流程引擎一般有task transfer接口批量调用即可。同时审批人解析表达式里加一层「账号是否有效」的判断无效时走兜底审批人。4.2 坑二半天假扣成一天现象申请人请了 0.5 天假审批通过后假期余额扣了 1 天。原因天数在流程变量里是浮点数但扣减余额的接口用了整数除法或四舍五入。解决余额扣减统一用BigDecimal或整数「半天数」存储比如 0.5 天存为 1 个半天单位。流程变量里的days只用于展示和条件判断不直接参与扣减计算。扣减时从数据库重新读一次请假单的天数字段不要信任流程变量。4.3 坑三并发审批导致重复扣减现象两个审批人同时点通过假期余额被扣了两次。原因状态更新没有加前置条件或者扣减余额和状态更新不在同一个事务里。解决状态更新用条件 UPDATE影响行数为 0 时直接返回扣减余额放在同一个本地事务里或者用消息队列做最终一致。如果余额服务是独立的至少保证状态更新成功后才发扣减消息且消息消费端做幂等。4.4 坑四驳回后重新提交审批历史丢失现象申请人驳回后重提HR 查历史只看到最新一次审批记录。原因重提时新建了流程实例旧实例被标记为作废历史查询只查了当前实例。解决历史查询按业务单号request_id查而不是按流程实例 ID 查。所有实例的审批记录都关联同一个request_id这样无论重提多少次时间线都是完整的。4.5 坑五流程变量类型不匹配导致网关判断失效现象need_director明明是 true流程却跳过了总监审批节点。原因流程变量存进去是字符串true网关表达式按布尔值比较类型不匹配。解决启动流程前统一做变量类型转换布尔值就传布尔数字就传数字。如果用的是 JSON 序列化注意反序列化后的类型。排查时把流程变量打印出来看实际类型是什么不要凭感觉猜。5. 验证清单与一个提高排查效率的小技巧5.1 上线前必须跑一遍的六个验证场景请假实例看起来简单但下面这六个场景我每次上线前都会手动跑一遍能挡住八成以上的低级问题场景操作预期结果正常审批提交→主管通过→HR归档状态依次流转余额扣减一次主管驳回后重提提交→主管驳回→修改天数→重提流程回到主管节点变量已更新并发审批两个浏览器同时点通过只有一个成功另一个提示已处理撤回提交后主管未处理时撤回状态变为 CANCELED主管待办消失审批人离职主管离职后查看待办任务自动转交不卡死半天假提交 0.5 天请假余额扣减 0.5不是 1跑完这六个场景基本能确认流程定义、状态机、并发控制和审批人解析都没大问题。剩下的就是和 HR 确认假期余额的计算规则那属于业务范畴不是技术能兜住的。5.2 用「状态变更日志表」替代断点调试最后分享一个我自己的习惯所有状态变更都往一张leave_request_log表里插一条记录字段包括request_id、from_status、to_status、operator、operate_time、remark。这张表平时看起来多余但一旦线上出现「单子状态不对」的反馈直接按request_id查这张表就能还原出完整的操作时间线比翻应用日志快得多。-- 状态变更日志表建议加索引 (request_id, operate_time) CREATE TABLE leave_request_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, request_id BIGINT NOT NULL, from_status VARCHAR(32), to_status VARCHAR(32) NOT NULL, operator VARCHAR(64), operate_time DATETIME DEFAULT CURRENT_TIMESTAMP, remark VARCHAR(255), INDEX idx_request_time (request_id, operate_time) );这张表还有一个好处当业务方问「这个单子为什么走了总监审批」时你可以把日志拉出来结合流程变量快照给出确切答案而不是说「可能是配置问题」。我吃过这个亏后来所有审批类项目都强制加这张表排查效率至少翻一倍。希望帮到你。本文还有配套的精品资源点击获取