企业请假销假系统设计实战:一张表、一套流程、两段生命周期------BPM节点驱动的表单变形术
📦 源码1 :ruoyi-office-vben |📦 源码2 :ruoyi-office |📦 源码3 :ruoyi-office | 💬 :17156169080(备注「RuoYi Office」)
请假管理几乎是每个企业管理系统的"标配"功能,但做到"好用"的很少。大多数系统把请假和销假设计成两个独立模块、两张表、两套流程,不仅增加了开发复杂度,还让员工和 HR 在两个界面之间反复切换。本文拆解一种更优雅的方案:一张表承载请假+销假两段数据,一条 BPMN 流程贯穿全生命周期,前端表单根据流程节点名称自动"变形"------请假阶段填请假字段,流程流转到销假节点时自动切换为销假表单。
引言:请假销假到底难在哪?
"请假不就是填个表、领导批一下吗?"------初次接到需求的开发者大多这么想。但真正动手时会发现,企业假勤管理的复杂度远超预期:
假期类型多样:年假、事假、病假、婚假、产假、陪产假、丧假、育儿假、调休假......不同类型的审批流程、扣薪规则、余额管理方式可能完全不同。
请假和销假的关系:员工请了 3 天假,但实际只用了 2 天就提前返岗,需要"销假"归还多余的假期天数。请假和销假是同一次假勤的两个阶段,不是两件独立的事。
审批流程分段:请假需要领导审批,销假也可能需要确认(尤其是涉及薪资结算的假期类型)。两段审批如何衔接?是两个独立流程还是一个流程的不同阶段?
多端体验一致:员工可能在 PC 端发起请假,在手机上收到销假提醒并填写实际天数。PC 端和移动端的表单逻辑如何统一?
数据一致性:请假的预计天数和销假的实际天数需要存在同一个业务上下文中,方便 HR 做考勤统计和薪资计算。
本文以一个开源企业管理系统的请假销假模块为例,完整拆解其数据模型、流程设计、前端实现和移动端适配方案。
一、业务设计:请假与销假是一枚硬币的两面
1.1 传统方案的问题
很多系统把请假和销假设计成两个独立的业务实体:
请假申请表(leave_application)
↓ 审批通过
销假申请表(leave_cancel_application)
↑ 关联原请假单
这种设计的问题在于:
- 两张表、两套 CRUD、两套权限、两条流程------开发量翻倍
- 销假单需要通过外键关联原请假单,查询和统计需要 JOIN
- 请假信息散落在两张表中,HR 统计实际假期天数需要汇总两张表
1.2 一张表的设计理念
本方案采用了一种更简洁的设计------一张表 hrm_leave_cancel_bill 同时承载请假信息和销假信息:
| 区域 | 字段 | 填写阶段 |
|---|---|---|
| 请假信息 | 请假类型、假期余额、预计开始/结束时间、预计天数、请假原因 | 请假发起时填写 |
| 销假信息 | 实际开始/结束时间、实际天数、销假备注 | 销假确认时填写 |
| 公共信息 | 单据编号、流程状态、申请人、部门、附件、备注 | 全流程共享 |
核心优势:
- 一张表搞定:请假和销假数据在同一行中,无需 JOIN
- 一条流程贯穿:请假审批和销假确认是同一个 BPMN 流程的不同阶段
- 统计简单:预计天数和实际天数在同一行,差值一目了然
1.3 支持的假期类型
系统通过字典 hrm_leave_type 配置假期类型,当前支持 10 种:
| 编码 | 类型 | 编码 | 类型 |
|---|---|---|---|
| 1 | 年假 | 6 | 产假 |
| 2 | 带薪事假 | 7 | 陪产假 |
| 3 | 病假 | 8 | 丧假 |
| 4 | 事假 | 9 | 育儿假 |
| 5 | 婚假 | 10 | 调休假 |
假期类型通过数据字典管理,无需修改代码即可扩展新类型。前端通过 getDictOptions(DICT_TYPE.HRM_LEAVE_TYPE) 获取字典选项渲染为下拉列表。
二、流程设计:一条 BPMN 流程的两段人生
2.1 流程编排
请假销假使用 Flowable 引擎编排,流程定义 Key 为 hr_leave_cancel_bill。整个 BPMN 流程分为两大阶段:
请假审批阶段
- 员工填写请假信息 → 提交流程
- 直属负责人审批
- 人力资源审批
- 请假审批通过
销假确认阶段
- 流程自动流转到 「发起人销假」 节点 → 发起人(即请假员工)填写实际休假天数
- 负责人销假审批
- 人力销假审批
- 流程结束
关键设计:整个流程是一条 BPMN ,不是两个独立流程。请假审批通过后,流程不会结束,而是继续流转到销假阶段。这意味着请假和销假共享同一个流程实例、同一个
businessKey。
2.2 节点驱动表单变形
这是本方案最核心的设计------前端表单根据当前流程节点名称动态切换行为:
| 流程节点 | 前端行为 |
|---|---|
| 发起 / 负责人审批 / 人力审批 | 请假字段可编辑且必填 ,销假字段隐藏或只读 |
| 发起人销假 | 请假字段全部只读 ,销假字段可编辑且必填 |
| 负责人销假审批 / 人力销假审批 | 全部字段只读,仅审批操作 |
前端通过 nodeKeyName 判断当前节点:
typescript
const isCancelNode = nodeKeyName?.value === '发起人销假';
当 isCancelNode 为 true 时:
- 请假类型、预计时间等字段自动设为
disabled: true,取消required校验 - 实际开始/结束时间、实际天数字段切换为
required,可编辑状态 - 系统自动将预计时间预填到实际时间字段(若实际字段为空),减少重复输入
这种"一个表单、多种形态"的设计模式,在 BPM 业务中非常实用------避免了为每个节点创建独立页面的冗余。
三、PC 端功能实现
3.1 请假销假列表
列表页位于 人力资源管理 → 假勤管理 → 请假销假 ,展示当前用户创建的所有请假销假单据。

▲ 请假销假列表页:支持按单据编号、单据状态、请假类型、创建时间搜索筛选
列表设计要点:
- 我的单据过滤 :列表默认只显示当前登录用户创建的单据,后端在查询时自动注入
creator = 当前用户ID - 单据编号链接:点击编号列自动跳转到详情页
- 状态标签 :通过
CellDict组件渲染 BPM 流程状态(草稿/审批中/通过/拒绝/已取消),不同状态用不同颜色区分 - 预计与实际对照:表格同时展示预计开始时间/天数和实际开始时间/天数,方便快速对比
- 工具栏操作:新建、导出 Excel、批量删除;批量删除前会过滤掉不允许删除状态(如审批中)的记录并给出提示
3.2 请假销假详情
详情页集 表单填写、审批信息、流程图 于一体,通过 Tab 页签切换:
▲ 详情页:顶部展示单据编号、申请人、日期、所属部门等摘要;下方分三个 Tab(单据信息、审批信息、流程图)
「单据信息」Tab包含三个区域:
基本信息区
| 字段 | 说明 |
|---|---|
| 请假类型 | 下拉选择(年假/事假/病假等 10 种) |
| 假期余额(天) | 手动输入,展示当前假期剩余天数 |
| 预计开始时间 | 日期时间选择器,30 分钟步长 |
| 预计结束时间 | 与开始时间交叉校验(结束 ≤ 开始则报错并清空) |
| 预计申请天数 | 数值输入,精确到 0.1 天(支持半天) |
| 项目名称 / 编码 | 可选,用于关联项目的请假统计 |
| 请假原因 | 多行文本,必填 |
销假信息区
| 字段 | 说明 |
|---|---|
| 实际开始时间 | 销假节点必填,默认预填预计开始时间 |
| 实际结束时间 | 销假节点必填,默认预填预计结束时间 |
| 实际天数 | 销假节点必填,精确到 0.1 天 |
| 销假备注 | 选填,说明实际休假情况 |
附件信息区
支持上传请假相关的附件文档(如医院证明、事假说明等),通过通用附件组件 AttachmentList 管理。
3.3 表单的节点级权限控制
详情页的表单权限不是简单的"可编辑/只读"二态,而是根据当前流程节点动态调整每个字段的状态:
| 场景 | 请假字段 | 销假字段 | 备注字段 |
|---|---|---|---|
| 新建/草稿编辑 | 可编辑,必填 | 可编辑,非必填 | 可编辑 |
| 待办 - 发起人销假节点 | 只读 | 可编辑,必填 | 只读 |
| 其他审批节点 | 只读 | 只读 | 只读 |
| 已办/抄送查看 | 只读 | 只读 | 只读 |
当待办任务的节点名为 发起人销假 时,前端会自动将预计时间预填到实际时间字段,员工只需确认或调整即可。
3.4 审批前自动保存
在 BPM 审批场景中,详情页以组件形式嵌入审批页面。当审批人点击"通过"时,系统会先调用 beforeApproval() 方法:
- 校验当前表单的必填项
- 调用
saveLeaveCancelBill将表单数据落库 - 校验通过后才允许流程继续
这个设计确保了审批通过时业务数据已经完整保存,避免了"流程通过了但数据没存"的问题。
四、移动端功能实现
移动端基于 UniApp + wot-design-uni 构建,支持 H5、微信小程序、APP 多端运行。
4.1 请假销假列表

▲ 移动端列表:每条单据以卡片形式展示,包含编号、标题、请假类型、预计时间、天数、申请人和流程状态标签
移动端列表的设计特点:
- 卡片式布局:每条单据独占一张卡片,信息层次分明,适合手机屏幕阅读
- 下拉刷新 + 触底加载 :使用
scroll-view的refresher-enabled实现下拉刷新,监听scrolltolower触底加载下一页 - 搜索筛选 :顶部搜索栏点击展开筛选弹窗(
wd-popup),支持按流程状态和创建时间范围筛选 - FAB 新建按钮 :右下角蓝色
+悬浮按钮,点击跳转到发起页面 - 点击跳转审批 :卡片点击不是跳到业务详情页,而是跳转到通用 BPM 流程详情页 (传入
processInstanceId),由 BPM 详情页加载业务表单组件
4.2 发起请假

▲ 移动端发起页:请假信息表单 + 备注 + 流程预览时间线 + 底部提交按钮
发起页面的功能亮点:
- 请假信息表单 :请假类型(
wd-picker)、假期余额、预计开始/结束时间(wd-datetime-picker)、预计天数、项目名称/编码、请假原因 - 流程预览 :底部展示
ProcessInstanceTimeline组件,显示即将走过的审批节点和审批人 - 发起人自选审批人 :部分流程节点支持发起人指定审批人,通过
startUserSelectAssignees字段传递 - 提交前确认:点击返回键时弹出确认对话框,防止误退出丢失表单数据
4.3 移动端销假
移动端的销假流程通过 BPM 待办入口触发------当流程流转到「发起人销假」节点时,原请假人会在待办列表中收到任务。点击进入后:
- 业务表单以
embedded模式嵌入 BPM 详情页 - 请假信息区域自动切换为只读状态
- 销假信息区域(实际开始/结束时间、实际天数)变为可编辑
- 预计时间自动预填到实际时间字段
- 审批前调用
beforeApproval先保存销假数据
4.4 PC 端与移动端的差异
| 维度 | PC 端 (Vben Admin) | 移动端 (UniApp) |
|---|---|---|
| 表单框架 | BasicForm + Schema 驱动 |
wd-form + wd-cell 手写 |
| 附件管理 | AttachmentList 完整 CRUD |
当前版本未集成 |
| 删除/导出 | 列表支持删除和 Excel 导出 | 仅展示和发起 |
| 流程集成 | Tab 页签内嵌审批信息和流程图 | ProcessInstanceTimeline 预览 |
| 销假编辑 | Schema dependencies 动态控制 |
v-if + canCancelEdit 控制 |
五、后端核心实现
5.1 单据编号生成
每张请假销假单都有唯一的单据编号,格式为 HR205-{YYYYMMDD}{序号}(如 HR205-2026031400003)。编号在首次保存或提交时自动生成:
java
if (StringUtils.isBlank(saveReqVO.getBillCode())) {
saveReqVO.setBillCode(BillCodeUtils.generateBillCode(
SystemEnum.HRM, HrmBillTypeEnum.HRM_LEAVE_CANCEL_BILL));
}
205 是 HRM 模块中请假销假单的类型编码,由 HrmBillTypeEnum.HRM_LEAVE_CANCEL_BILL 定义。
5.2 提交流程
提交请假时,后端做三件事:
- 保存业务数据 :将表单数据写入
hrm_leave_cancel_bill表,设置processStatus = RUNNING - 发起流程实例:调用 BPM API 创建 Flowable 流程实例,传入流程变量(申请原因、制单人等)
- 回写流程实例 ID :将 Flowable 返回的
processInstanceId存入业务表,建立业务数据与流程的关联
java
@Override
@Transactional(rollbackFor = Exception.class)
public Long submitLeaveCancelBill(LeaveCancelBillSaveReqVO saveReqVO) {
// 1. 生成编号 + 保存业务数据
LeaveCancelBillDO leaveCancelBill = BeanUtils.toBean(saveReqVO, LeaveCancelBillDO.class)
.setProcessStatus(BpmTaskStatusEnum.RUNNING.getStatus());
leaveCancelBillMapper.insertOrUpdate(leaveCancelBill);
// 2. 构建流程变量 + 发起流程
Map<String, Object> variables = BpmProcessVariableUtils.buildBillVariables(saveReqVO);
variables.put(BpmProcessVariableConstants.CAUSE, saveReqVO.getCreatorName() + "请假申请");
String processInstanceId = processInstanceApi.submitProcessInstance(
Long.valueOf(saveReqVO.getCreator()),
new BpmProcessInstanceCreateReqDTO()
.setProcessDefinitionKey("hr_leave_cancel_bill")
.setVariables(variables)
.setBusinessKey(String.valueOf(leaveCancelBill.getId()))
).getCheckedData();
// 3. 回写流程实例 ID
leaveCancelBillMapper.updateById(
new LeaveCancelBillDO().setId(leaveCancelBill.getId())
.setProcessInstanceId(processInstanceId));
return leaveCancelBill.getId();
}
5.3 FlowBillService 接口
请假销假服务实现了框架统一的 FlowBillService<HrmBillTypeEnum> 接口,用于接收 BPM 引擎的状态回调:
java
@Override
public HrmBillTypeEnum getSupportedBillType() {
return HrmBillTypeEnum.HRM_LEAVE_CANCEL_BILL;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateProcessStatus(String businessKey, Integer status) {
Long id = Long.parseLong(businessKey);
validateLeaveCancelBillExists(id);
LeaveCancelBillDO updateObj = new LeaveCancelBillDO();
updateObj.setId(id);
updateObj.setProcessStatus(status);
leaveCancelBillMapper.updateById(updateObj);
}
当 Flowable 流程状态变化(审批通过、拒绝、撤回等)时,BPM 框架自动通过 processDefinitionKey 找到对应的 FlowBillService 实现,调用 updateProcessStatus 同步更新业务表的流程状态字段。
5.4 删除时清理流程
删除请假单时,如果该单据已经发起了流程,需要同时清理 Flowable 中的流程实例:
java
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteLeaveCancelBill(Long id) {
LeaveCancelBillDO bill = leaveCancelBillMapper.selectById(id);
if (bill == null) throw exception(LEAVE_CANCEL_BILL_NOT_EXISTS);
if (StringUtils.isNotBlank(bill.getProcessInstanceId())) {
try {
processInstanceApi.deleteProcessInstance(
Long.valueOf(bill.getCreator()), bill.getProcessInstanceId());
} catch (Exception e) {
log.warn("清理流程实例失败: {}", e.getMessage());
}
}
attachmentService.deleteAttachmentByBusiness(
HrmBillTypeEnum.HRM_LEAVE_CANCEL_BILL.getTypeCode(), id);
leaveCancelBillMapper.deleteById(id);
}
流程清理使用 try-catch 包裹,即使流程实例已不存在(如已被管理员手动终止),也不会影响业务数据的删除。
六、前端表单变形的实现细节
6.1 Schema 驱动的字段控制
PC 端使用 useFormSchema 函数生成表单 Schema,根据 nodeKeyName 参数动态调整每个字段的行为:
typescript
export function useFormSchema(
readonly?: Ref<boolean>,
nodeKeyName?: Ref<string>,
canCancelEdit?: Ref<boolean>,
): VbenFormSchema[] {
const isCancelNode = nodeKeyName?.value === '发起人销假';
return [
{
fieldName: 'leaveType',
label: '请假类型',
rules: isCancelNode ? undefined : 'required', // 销假节点取消必填
componentProps: {
disabled: isCancelNode ? true : undefined, // 销假节点禁用
},
},
// ... 其他请假字段同理
{
fieldName: 'actualStartTime',
label: '实际开始时间',
rules: isCancelNode ? 'required' : undefined, // 销假节点必填
componentProps: {
disabled: () => canCancelEdit?.value ? false : readonly?.value,
},
},
// ... 其他销假字段同理
];
}
6.2 时间交叉校验
预计开始/结束时间和实际开始/结束时间都配置了交叉校验------当修改一个时间字段时,自动校验与对应的另一个时间字段的关系:
typescript
{
fieldName: 'expectedStartTime',
dependencies: {
triggerFields: ['expectedEndTime'],
trigger: (values, formApi) => {
if (values.expectedEndTime && values.expectedStartTime
&& values.expectedEndTime <= values.expectedStartTime) {
message.error('预计开始时间不能晚于或等于预计结束时间');
formApi?.setFieldValue('expectedEndTime', undefined);
}
},
},
}
当检测到时间冲突时,自动清空对端字段(而非当前正在编辑的字段),避免打断用户的输入流。
七、数据结构
7.1 表结构 hrm_leave_cancel_bill
| 字段 | 类型 | 说明 |
|---|---|---|
id |
bigint | 主键 |
bill_code |
varchar(50) | 单据编号(唯一约束:bill_code + deleted + tenant_id) |
process_instance_id |
varchar(64) | Flowable 流程实例 ID |
process_status |
tinyint | 流程状态(0草稿 1审批中 2通过 3拒绝 4已取消) |
leave_type |
tinyint | 请假类型(字典 hrm_leave_type) |
leave_balance |
decimal(10,1) | 假期余额(天) |
expected_start_time |
datetime | 预计开始时间 |
expected_end_time |
datetime | 预计结束时间 |
expected_days |
decimal(10,1) | 预计申请天数 |
project_name |
varchar(200) | 项目名称 |
project_code |
varchar(100) | 项目编码 |
leave_reason |
varchar(500) | 请假原因 |
actual_start_time |
datetime | 实际开始时间(销假填写) |
actual_end_time |
datetime | 实际结束时间(销假填写) |
actual_days |
decimal(10,1) | 实际天数(销假填写) |
cancel_remark |
varchar(500) | 销假备注 |
creator_name |
varchar(100) | 申请人姓名 |
company_id / company_name |
bigint / varchar | 所属公司 |
dept_id / dept_name |
bigint / varchar | 所属部门 |
remark |
varchar(500) | 备注 |
tenant_id |
bigint | 租户编号(多租户隔离) |
7.2 设计要点
- 单表设计 :请假字段(
expected_*、leave_*)和销假字段(actual_*、cancel_*)共存于同一张表,由流程阶段决定哪些字段被填充 - 唯一约束 :
bill_code + deleted + tenant_id三字段联合唯一,支持软删除和多租户 - 精度设计 :天数字段使用
decimal(10,1),支持 0.5 天等半天假期 - 冗余存储 :
creator_name、dept_name、company_name做了冗余存储,避免查询时 JOIN 用户/部门/公司表
八、技术亮点总结
| 设计要点 | 实现方式 | 价值 |
|---|---|---|
| 一张表两段数据 | expected_* + actual_* 字段共存 | 避免双表 JOIN,统计简单 |
| 一条流程两段审批 | BPMN 中包含请假审批节点和销假审批节点 | 全生命周期在一个流程实例中 |
| 节点驱动表单变形 | nodeKeyName === '发起人销假' 切换 required/disabled |
一个表单适配多个阶段 |
| 预填减少输入 | 销假节点自动预填预计时间到实际字段 | 常见场景零修改直接提交 |
| 交叉时间校验 | dependencies 互相监听 + 清空对端 | 防止时间逻辑错误 |
| 审批前保存 | beforeApproval → validate → save | 确保审批通过时数据完整 |
| FlowBillService 标准化 | 统一接口接收 BPM 状态回调 | 新增业务单据只需实现接口 |
| 我的单据过滤 | 后端强制注入 creator = 当前用户 | 数据安全,用户只看自己的 |
| 流程清理容错 | try-catch 包裹流程实例删除 | 业务删除不被流程异常阻塞 |
| 多租户隔离 | 全表 tenant_id | SaaS 场景开箱即用 |
结语
请假销假管理的技术挑战不在于"表单多复杂",而在于如何用最少的实体和最简的架构覆盖请假→审批→销假→确认的全生命周期。
"一张表两段数据 + 一条流程两段审批 + 节点驱动表单变形"的设计模式,不仅适用于请假销假,还可以推广到其他"申请→确认"类的业务场景(如出差申请+出差报告、采购申请+验收确认等)。核心思想是:让流程引擎驱动表单行为,而不是用代码硬编码每个阶段的逻辑。
如果你正在设计类似的假勤管理模块,或者对 BPM 节点驱动的表单设计感兴趣,欢迎参考源码实现。
🌐 演示地址 :http://ruoyioffice.com