企业请假销假系统设计实战:一张表、一套流程、两段生命周期——BPM节点驱动的表单变形术

企业请假销假系统设计实战:一张表、一套流程、两段生命周期------BPM节点驱动的表单变形术

📦 源码1ruoyi-office-vben |📦 源码2ruoyi-office |📦 源码3ruoyi-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 流程分为两大阶段:

请假审批阶段

  1. 员工填写请假信息 → 提交流程
  2. 直属负责人审批
  3. 人力资源审批
  4. 请假审批通过

销假确认阶段

  1. 流程自动流转到 「发起人销假」 节点 → 发起人(即请假员工)填写实际休假天数
  2. 负责人销假审批
  3. 人力销假审批
  4. 流程结束

关键设计:整个流程是一条 BPMN ,不是两个独立流程。请假审批通过后,流程不会结束,而是继续流转到销假阶段。这意味着请假和销假共享同一个流程实例、同一个 businessKey

2.2 节点驱动表单变形

这是本方案最核心的设计------前端表单根据当前流程节点名称动态切换行为

流程节点 前端行为
发起 / 负责人审批 / 人力审批 请假字段可编辑且必填 ,销假字段隐藏或只读
发起人销假 请假字段全部只读 ,销假字段可编辑且必填
负责人销假审批 / 人力销假审批 全部字段只读,仅审批操作

前端通过 nodeKeyName 判断当前节点:

typescript 复制代码
const isCancelNode = nodeKeyName?.value === '发起人销假';

isCancelNodetrue 时:

  • 请假类型、预计时间等字段自动设为 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() 方法:

  1. 校验当前表单的必填项
  2. 调用 saveLeaveCancelBill 将表单数据落库
  3. 校验通过后才允许流程继续

这个设计确保了审批通过时业务数据已经完整保存,避免了"流程通过了但数据没存"的问题。


四、移动端功能实现

移动端基于 UniApp + wot-design-uni 构建,支持 H5、微信小程序、APP 多端运行。

4.1 请假销假列表

▲ 移动端列表:每条单据以卡片形式展示,包含编号、标题、请假类型、预计时间、天数、申请人和流程状态标签

移动端列表的设计特点:

  • 卡片式布局:每条单据独占一张卡片,信息层次分明,适合手机屏幕阅读
  • 下拉刷新 + 触底加载 :使用 scroll-viewrefresher-enabled 实现下拉刷新,监听 scrolltolower 触底加载下一页
  • 搜索筛选 :顶部搜索栏点击展开筛选弹窗(wd-popup),支持按流程状态和创建时间范围筛选
  • FAB 新建按钮 :右下角蓝色 + 悬浮按钮,点击跳转到发起页面
  • 点击跳转审批 :卡片点击不是跳到业务详情页,而是跳转到通用 BPM 流程详情页 (传入 processInstanceId),由 BPM 详情页加载业务表单组件

4.2 发起请假

▲ 移动端发起页:请假信息表单 + 备注 + 流程预览时间线 + 底部提交按钮

发起页面的功能亮点:

  • 请假信息表单 :请假类型(wd-picker)、假期余额、预计开始/结束时间(wd-datetime-picker)、预计天数、项目名称/编码、请假原因
  • 流程预览 :底部展示 ProcessInstanceTimeline 组件,显示即将走过的审批节点和审批人
  • 发起人自选审批人 :部分流程节点支持发起人指定审批人,通过 startUserSelectAssignees 字段传递
  • 提交前确认:点击返回键时弹出确认对话框,防止误退出丢失表单数据

4.3 移动端销假

移动端的销假流程通过 BPM 待办入口触发------当流程流转到「发起人销假」节点时,原请假人会在待办列表中收到任务。点击进入后:

  1. 业务表单以 embedded 模式嵌入 BPM 详情页
  2. 请假信息区域自动切换为只读状态
  3. 销假信息区域(实际开始/结束时间、实际天数)变为可编辑
  4. 预计时间自动预填到实际时间字段
  5. 审批前调用 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 提交流程

提交请假时,后端做三件事:

  1. 保存业务数据 :将表单数据写入 hrm_leave_cancel_bill 表,设置 processStatus = RUNNING
  2. 发起流程实例:调用 BPM API 创建 Flowable 流程实例,传入流程变量(申请原因、制单人等)
  3. 回写流程实例 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_namedept_namecompany_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


复制代码
相关推荐
鹤旗2 小时前
While语句,do-while语句,for语句
java·jvm·算法
小碗羊肉2 小时前
【从零开始学Java | 第十八篇】BigInteger
java·开发语言·新手入门
sky wide2 小时前
[特殊字符] Docker Swarm 集群搭建指南
java·docker·容器
wuqingshun3141592 小时前
谈谈你对springAop动态代理的理解?
java·jvm
执笔画流年呀2 小时前
PriorityQueue(堆)续集
java·开发语言
武超杰2 小时前
Spring Boot入门教程
java·spring boot·后端
左左右右左右摇晃2 小时前
JDK 1.7 ConcurrentHashMap——分段锁
java·开发语言·笔记
是小蟹呀^2 小时前
Java抽象类详解:从入门到精通
java·抽象类
KongHen022 小时前
uniapp-x实现自定义tabbar
前端·javascript·uni-app·unix