大数跨境

SpringBoot+Vue3 人力假期余额设计:预占、扣减、回滚与重复回调怎么保证一致

SpringBoot+Vue3 人力假期余额设计:预占、扣减、回滚与重复回调怎么保证一致 企业软件源码
2026-09-27
4
导读:假期余额不是 remainingDays 做减法。本文基于 Spring Boot、Vue3 与 Flowable,拆解假期规则、年度账户、变动流水、审批中预占、通过确认、拒绝撤回释放、销假退回、到期

SpringBoot3+Vue3 人力假期余额设计:预占、扣减、回滚与重复回调怎么保证一致

🌐 文档地址:https://ruoyioffice.com 👇 文章底部获取源码和演示地址 👇 💬 :17156169080(获取产品咨询)

员工看到“年假还剩 6 天”,背后其实不是一道减法。审批中的 3 天算不算可用?流程拒绝要不要自动退?请了 5 天只休 3 天怎么返还?同一条 Flowable 回调执行两次,会不会再扣一次?真正可靠的假期余额,需要按账户系统来设计。

▲ 规则负责算额度,账户保存当前值,流水解释每次变化,请假单只通过余额服务改变账户

引言:一个 remainingDays 字段为什么撑不住真实请假

最初版假勤系统往往只有员工、假种和剩余天数:

employee_id | leave_type | remaining_days

它能支持“提交后减 1 天”,却回答不了下面的问题:

  • 两张审批中的请假单能否同时占用同一份余额;
  • 审批拒绝、员工撤回、管理员取消是不是同一种退回;
  • 流程通过后,预占怎样变成正式使用;
  • 销假实际天数变少,差额回到哪一个年度账户;
  • 工龄调整导致年假额度变化,为什么从 10 天变成 12 天;
  • 消息重试让回调重复执行时,怎样避免重复扣减。

所以假期余额的正确抽象是:一套带业务来源、状态迁移和审计流水的额度账户。

一、员工余额由三套配置合成

余额不从账户表凭空产生。系统先合成假种、部门方案和员工参数,再计算某员工某年度应得额度。

1. 假种决定是否管理余额

年假、调休假、福利假通常需要余额;事假、婚假等可能只校验条件,不做账户扣减。假种层应明确是否启用余额、默认额度、最小请假单位、是否允许透支和有效期。

2. 部门方案承载企业差异

总部、制造基地和项目团队可能适用不同方案。系统按员工部门查找生效方案,必要时向上继承,而不是在请假表单里写死规则。

3. 员工参数处理个体差异

员工入职日期、工龄起算日和奖励年假会改变最终额度。规则更新后,不应直接覆盖旧数字,而应生成一条调整流水。

▲ HR 配的是规则,员工拿到的是规则在某个年度计算后的账户结果

核心边界是:规则是计算依据,账户是计算结果。历史账户不能随着规则页面每次打开而漂移,否则系统无法解释余额变化。

二、账户看当前,流水看原因

年度账户建议至少保留五个口径:

口径
含义
什么时候变化
发放额度
本年度累计获得
年度发放、人工调整、工龄重算
可用余额
还能发起多少
预占时减少,释放或退回时增加
预占余额
已提交、尚未最终通过
提交增加,通过或拒绝减少
已用额度
已确认消耗
审批通过时增加,销假时减少
结转额度
来自上一周期
年度初始化或结转任务
@TableName("hrm_leave_account")public class LeaveAccountDO extends TenantBaseDO {    @TableId    private Long id;    private Long employeeId;    private Integer leaveType;    private Integer grantYear;    private BigDecimal grantedDays;    private BigDecimal usedDays;    private BigDecimal reservedDays;    private BigDecimal availableDays;    private BigDecimal carryForwardDays;    private LocalDate expireDate;}

账户只回答“现在是多少”。每一次变化还要写入 hrm_leave_ledger,记录变动类型、前后余额、业务类型、业务 ID 和备注。这样 HR 可以从“剩 6 天”继续追到“年度发放 10 天、请假确认 3 天、审批中预占 1 天”。

三、提交时先预占,不要等通过后才扣

员工有 5 天年假,同时提交两张各 4 天的申请。如果系统等审批通过才扣余额,两张单都能通过提交校验,最后必然透支。

更稳妥的状态迁移是:

提交:available -= days,reserved += days通过:reserved -= days,used += days拒绝/撤回:reserved -= days,available += days销假:used -= returnedDays,available += returnedDays

▲ 预占解决并发申请透支;确认、释放和销假退回分别对应不同业务结果

预占动作必须和请假单进入审批状态处于同一事务边界。后端不能只相信前端展示的余额,而要重新读取账户并完成原子扣减:

@Transactional(rollbackFor = Exception.class)public void reserveForBill(Long billId, Long employeeId,                           Integer leaveType, BigDecimal days) {    LeaveAccountDO account = getOrCreateAccount(employeeId, leaveType);    if (account.getAvailableDays().compareTo(days) < 0) {        throw exception(LEAVE_BALANCE_NOT_ENOUGH);    }    BigDecimal before = account.getAvailableDays();    account.setAvailableDays(before.subtract(days));    account.setReservedDays(account.getReservedDays().add(days));    leaveAccountMapper.updateById(account);    createLedger(account, "RESERVE", days.negate(), before,            account.getAvailableDays(), billId, "HRM_LEAVE_CANCEL_BILL");}

高并发场景还应增加乐观锁版本号或带余额条件的原子更新,让“余额足够”和“完成扣减”成为不可分割的数据库动作。

四、审批回调只做状态迁移,而且必须幂等

Flowable 审批通过后,不应再次从可用余额扣减,而是把预占转成已用:

@Transactional(rollbackFor = Exception.class)public void confirmForBill(Long billId) {    if (leaveLedgerMapper.existsByBizAndType(            "HRM_LEAVE_CANCEL_BILL", billId, "CONFIRM_USE")) {        return;    }    LeaveBillDO bill = validateBillExists(billId);    LeaveAccountDO account = getAccount(bill);    BigDecimal days = bill.getExpectedDays();    account.setReservedDays(account.getReservedDays().subtract(days));    account.setUsedDays(account.getUsedDays().add(days));    leaveAccountMapper.updateById(account);    createLedger(account, "CONFIRM_USE", BigDecimal.ZERO,            account.getAvailableDays(), account.getAvailableDays(),            billId, "HRM_LEAVE_CANCEL_BILL");}

确认使用时可用余额变化为 0,因为额度在提交时已经预占。确认动作改变的是 reservedDays 和 usedDays 的归属。

幂等不能只靠“当前流程状态已经通过”。更可靠的做法是:

  • 用 business_type + business_id + change_type 定义业务幂等键;
  • 数据库增加唯一约束或在落流水前检查;
  • 账户更新与流水写入同事务;
  • 重复调用直接返回已有结果;
  • 异常补偿只追加新流水,不修改历史流水。

五、拒绝、撤回与取消都释放预占,但保留不同原因

从余额数学上看,审批拒绝和员工撤回都执行:

reserved -= daysavailable += days

但审计含义不同。流水变动类型可以统一为 RELEASE_RESERVE,备注和业务状态要保留“审批拒绝”“发起人撤回”“管理员取消”的真实原因。

释放时还要防止 reservedDays 变成负数。如果旧数据没有成功预占,系统应先核对流水,再决定拒绝补偿还是记录告警,不能盲目加回余额。

六、销假不是撤销原单,而是按实际使用退差额

员工原申请 5 天,实际休 3 天,销假通过后应退回 2 天:

  • 原请假单仍保留“批准 5 天”的审批事实;
  • 销假单记录实际使用 3 天;
  • 年度账户 usedDays 减 2;
  • availableDays
     加 2;
  • 新增 CANCEL_RETURN 流水并关联销假业务。

▲ 表单在提交前展示余额;审批后的实际天数由销假动作修正,不回头篡改历史审批事实

BigDecimal returnedDays = bill.getExpectedDays()        .subtract(bill.getActualDays());if (returnedDays.signum() <= 0) {    return;}account.setUsedDays(account.getUsedDays().subtract(returnedDays));account.setAvailableDays(account.getAvailableDays().add(returnedDays));

如果实际天数大于预计天数,不能静默把余额扣成负数,应走补充申请、变更流程或明确的透支规则。

七、前端负责解释余额,不负责决定余额

Vue3 表单在员工选择假种和预计天数后,调用摘要接口展示本年度发放、已使用、审批中预占、当前可用、本次申请和提交后预计剩余。

这能把“余额不足”从提交后的报错,提前变成员工填表过程中的反馈。但前端数值只用于体验,最终校验仍由后端事务完成。

▲ PC 与移动端共享同一余额摘要接口,避免两端各算一套口径

▲ 移动端负责快速发起与查看状态,余额变动仍统一由后端服务和流程回调驱动

八、年度发放、工龄重算和到期清零都要走流水

自动任务最容易被写成直接 update available_days = 0,这会让系统失去解释能力。正确做法是把定时任务也当作业务来源:

  • 年度发放:GRANT;
  • 工龄或奖励变化:ADJUST_ADD / ADJUST_SUB;
  • 上年度结转:CARRY_FORWARD;
  • 到期清零:EXPIRE_CLEAR。

任务应按员工、假种、年度建立幂等键。重复跑一次年度发放任务,不能再发一份年假;清零任务失败重试,也不能生成多条相同清零流水。

九、这套模型的边界在哪里

假期余额账户解决的是额度一致性,不会自动解决所有假勤问题:

  • 工作日和请假时长仍要读取班次、节假日方案;
  • 跨年度请假需要按日期拆分到不同年度账户;
  • 调休额度可能来自加班审批,不一定由年度任务发放;
  • 小时假与天数假要统一换算精度;
  • 多租户环境下,账户、流水和规则都必须保留租户边界。

余额服务应保持单一职责:接收已经算好的变动额度,执行可靠的账户迁移与流水留痕;日历、班次和劳动政策由上游规则服务负责。

十、上线前建议验证的 10 个场景

  1. 余额 5 天,同时提交两张 4 天申请,第二张必须失败;
  2. 提交 3 天后,可用减少 3 天、预占增加 3 天;
  3. 审批通过后,可用不再减少,预占转为已用;
  4. 同一通过回调执行两次,账户只变化一次;
  5. 审批拒绝后,预占释放且生成原因清晰的流水;
  6. 发起人撤回后,与拒绝使用相同数学动作但不同备注;
  7. 请 5 天、销假实际 3 天,退回 2 天;
  8. 工龄重算额度增加 2 天,保留调整前后余额;
  9. 到期清零任务重复运行,不重复清零或重复写流水;
  10. PC 与 App 查询到的可用、预占、已用口径一致。

结语

企业假期余额的难点不在字段多,而在业务动作多、审批时间长、回调可能重复、员工权益必须能解释。

一套可靠设计可以归纳为四句话:规则算额度,账户存当前,流水讲原因,流程做迁移。 再补上事务、并发控制和业务幂等,假期余额才不会在流程重试、销假和年度任务中越算越乱。

这套账户模型也可以复用到调休、福利额度、培训学时、补贴余额和会员积分等场景。区别只在发放规则,可靠性骨架是相通的。

如果这篇对你有用,点个「在看」或收藏。

🌐 演示地址
 https://ruoyioffice.com/web
 📦 GitHub 源码
 https://github.com/yuqing2026/ruoyi-office
 📦 Gitee 源码
 https://gitee.com/yqzy1688/ruoyi-office
 💬 微信:17156169080(获取产品咨询)

打开演示地址直接查看系统。

【声明】内容源于网络
0
0
企业软件源码
RuoyiOffice 是一套基于 Spring Boot + Vue3 +Uniapp 的企业一体化管理平台,集 OA、CRM、ERP、工作流、HR、资产、合同、项目、AI应用等业务于一体,帮助企业用一个系统协同管理多类核心业务。
内容 132
粉丝 0
企业软件源码 RuoyiOffice 是一套基于 Spring Boot + Vue3 +Uniapp 的企业一体化管理平台,集 OA、CRM、ERP、工作流、HR、资产、合同、项目、AI应用等业务于一体,帮助企业用一个系统协同管理多类核心业务。
总阅读2.0k
粉丝0
内容132