SpringBoot+Vue3 企业人事调动全流程设计:调动申请+BPM审批+生效日回写档案——从“纸面调岗”到“审批即调动”

🌐 文档地址http://ruoyioffice.com | 📦 源码1https://gitcode.com/zhouzhongyan/ruoyi-office-vben.git | 📦 源码2https://gitcode.com/zhouzhongyan/ruoyi-office.git | 📦 源码3https://github.com/yuqing2026/ruoyi-office.git | 💬 微信:17156169080(备注「RuoYi Office」)

人事调动是企业组织活力最直接的体现——员工从测试部转到产品部、从深圳公司调到北京公司、从普通岗位转到管理岗位,看起来只是改几个字段,真正做成系统却必须回答很多问题:调动是否立即生效?审批通过前能不能改档案?跨公司调动谁来审批?如果生效日是下个月 1 号,今天审批通过要不要立刻更新员工信息?RuoYi Office 用 1 张调动申请单 + BPM 审批 + 生效日回写策略 + XXL-Job 定时补写机制,把“调动申请”和“档案更新”彻底打通。
employee-transfer-flow.png

▲ 人事调动业务流转全景:申请单记录原/新组织与岗位信息,BPM 审批驱动状态流转,通过后可立即回写档案,也可按生效日期由 XXL-Job 定时生效

引言:人事调动到底难在哪?

“不就是把部门改一下吗?”很多人第一次做调动管理时都会低估它的复杂度。事实上,它是 HRM 里非常典型的“审批数据”和“正式档案”双轨问题。

调动信息不是简单覆盖:员工当前属于哪个部门、哪家公司、什么职位、什么职务,都必须在审批前完整保留,供审批人对照“原状态”和“目标状态”。

生效时间不一定等于审批时间:今天 4 月 15 日审批通过,不代表调动一定今天生效。很多企业要求“次月 1 日统一生效”,这就需要把“审批通过”和“档案变更”拆成两步。

调动范围是复合变更:一次调动可能同时变更公司、部门、职位、职务,也可能只调部门不调岗位。系统必须允许“按字段局部回写”,而不是全量覆盖员工档案。

组织信息要级联联动:选择“变更为部门”后,往往能推导出“变更为公司”。如果前端不能自动带出,HR 就得重复选择两遍,还容易选错。

审批和档案更新必须强一致:如果档案先改了、流程后走,审批被拒绝就会出现严重脏数据;如果流程通过了但档案没有更新,也会导致员工信息滞后。

痛点 传统做法 后果
原/新岗位信息不留痕 直接修改员工档案 事后无法审计“调前是什么、调后是什么”
审批和生效时间混淆 审批通过立即人工改档案 无法支持“下月生效”等真实场景
调动范围不完整 只改部门,不改公司/岗位 档案字段彼此矛盾
部门/公司联动靠人记 HR 手动二次选择 选错公司或部门的概率很高
流程与档案脱节 审批通过后再人工同步 忙忘了就出现数据滞后

本文就以 RuoYi Office 的人事调动模块为例,拆解它如何通过调动申请单 + BPM 审批 + 立即生效/定时生效双策略,把组织异动这件事做成真正可靠的系统能力。


一、业务设计:审批通过不一定立刻生效

1.1 业务全景

一次完整的人事调动涉及 4 个阶段:

阶段 角色 操作 系统行为
发起 HR / 直属上级 选择员工,填写异动类型、原因、新组织与岗位 创建调动申请单
审批 HR / 主管 / 领导 审核调动合理性 BPM 流程推进
生效 系统自动 根据生效策略执行 立即回写或按日期定时回写
归档 HR 查看申请单与档案结果 保留完整调动留痕

1.2 调动单为什么要冗余“原信息”和“新信息”?

人事调动单不是简单地引用一份员工档案,而是必须沉淀一份审批快照:

字段组 代表字段 作用
原信息 原公司、原部门、原职位、原职务 审批对照、留痕审计
新信息 新公司、新部门、新职位、新职务 审批目标、回写来源
控制信息 是否立即生效、生效日期 决定档案何时更新

如果只保存“新值”,审批人永远看不到“原来在哪里”;如果直接读取当前员工档案,当员工在流程审批期间又发生其他变动,单据就会失真。

1.3 两种生效策略

这是本方案最重要的业务抽象:审批通过档案生效可以相同,也可以不同。

策略 字段组合 适用场景
立即生效 effectiveImmediately = true 小团队快速调岗,当天审批当天生效
按生效日生效 effectiveImmediately = false + effectiveDate 集团统一调岗、月初组织调整

业务语义非常清楚:

  • BPM 只负责判断“这次调动是否被批准”。
  • 员工档案何时更新,由生效策略决定。

1.4 立即生效和定时生效如何共存?

RuoYi Office 采用“双通道”实现:

审批通过
  ├── 立即生效 = true
  │     └── 立刻调用 updateEmployeeFromTransferBill()
  └── 立即生效 = false
        └── 由 employeeTransferByEffectiveDateJob
            在 effectiveDate 当天补写员工档案

这比“审批通过后先写入档案,再用计划任务覆盖”干净得多,因为系统只会在真正应该生效的时刻更新正式档案。


二、系统设计:申请单、档案、定时任务的三方协同

2.1 模块定位

人事调动位于 人力 → 人事管理 → 人事调动 目录下,是员工全生命周期中的重要流程单据之一。

模块 功能定位 面向角色
调动申请单 承载审批前的异动信息 HR / 直属上级
员工档案 承载正式组织与岗位信息 HR 管理员
XXL-Job 生效任务 处理延迟生效单据 系统后台

2.2 核心设计决策

决策点 方案 理由
申请模型 单表承载原/新信息 调动信息结构相对稳定,无需子表
生效策略 立即生效 + 生效日补写 同时满足灵活调岗与月度统一生效
审批回调 FlowBillService.updateProcessStatus BPM 统一驱动单据状态
档案回写 updateEmployeeFromTransferBill 只回写新部门/公司/职位/职务
定时任务 employeeTransferByEffectiveDateJob 处理审批通过但未到生效日的单据
前端联动 选员工带出原信息、选部门带出新公司 减少人工重复录入

2.3 数据结构

表名 职责 关键字段
hrm_employee_transfer_bill 调动申请单 employee_id, old_dept_id, new_dept_id, effective_immediately, effective_date, process_status
hrm_employee 员工档案 dept_id, company_id, job_post, job_position

单据和档案的关系不是“谁替代谁”,而是“申请数据驱动正式数据更新”。


三、PC 端功能实现:调前、调后信息一眼看清

3.1 调动申请列表页

employee-transfer-list.png

▲ 人事调动列表页:直接展示员工工号、姓名、所属部门、异动类型、原部门与变更后部门,方便 HR 快速回顾调动轨迹

这个列表页最大的价值,不是简单罗列申请单,而是把“调前 → 调后”的核心变化直接摊开给 HR 看:

  • 员工工号、姓名保证快速定位人。
  • 原部门、变更后部门帮助判断调动方向。
  • 单据状态显示流程进度。
  • 默认只查当前创建人的申请,天然符合“我的申请”视角。

3.2 调动申请详情页

employee-transfer-form.png

▲ 人事调动表单页:上半部分是员工当前档案快照,下半部分是调动信息与生效策略,既能看清“从哪里来”,也能明确“要到哪里去”

这个页面明显不是普通表单,而是“主表 + 调动信息子表单”的组合式单据:

  • 选择员工后,自动带出工号、性别、手机号、原部门、原公司、原职位、原职务。
  • 下方“调动信息”区域只填写变更项,不会强迫用户重复录入所有字段。
  • “是否立即生效”与“生效日期”联动,符合真实 HR 业务习惯。

3.3 交互设计亮点

交互 设计方式 价值
选员工自动带出原信息 EmployeeSelectModal 省去 HR 手动填写当前岗位信息
选新部门自动带出新公司 部门弹窗联动公司 避免跨公司/跨部门录错
保存和提交分离 草稿可不完整,提交才强校验 符合单据类产品习惯
审批只读控制 computeBusinessFormReadonly 审批态禁止任意篡改单据

四、流程设计:调动通过后,什么时候改档案?

4.1 流程定义 Key 与业务键

人事调动流程定义 Key 为 hr_employee_transfer_bill,业务键是调动申请单主键 ID。这意味着 BPM 只需要保存“流程实例 ↔ 业务单据”的映射,不需要知道员工档案细节。

4.2 审批通过后的两条路径

流程通过后,系统不会一刀切直接改员工档案,而是分两种情况:

  1. 立即生效:审批通过的当下就更新员工档案。
  2. 延迟生效:仅把单据状态置为通过,等生效日任务执行。

这样可以完美覆盖“月底审批、月初统一生效”的组织调整场景。

4.3 为什么要引入 XXL-Job?

因为“生效日期当天自动更新档案”不是 BPM 自身要负责的能力,它更适合交给定时任务:

  • BPM 负责审批路径。
  • XXL-Job 负责时间到点执行。
  • 员工档案服务只负责真正的更新动作。

这个分工非常清晰,也更容易排障。


五、后端核心实现

5.1 提交调动申请:生成单号、发起流程、保存附件

submitEmployeeTransferBill() 的职责和入职、转正等单据保持一致:编号生成、校验、流程发起、附件保存。

@Transactional(rollbackFor = Exception.class)
public Long submitEmployeeTransferBill(EmployeeTransferBillSaveReqVO saveReqVO) {
    if (StringUtils.isBlank(saveReqVO.getBillCode())) {
        saveReqVO.setBillCode(BillCodeUtils.generateBillCode(
                SystemEnum.HRM, HrmBillTypeEnum.HRM_EMPLOYEE_TRANSFER_BILL));
    }
    validateEmployeeExists(saveReqVO.getEmployeeId());

    EmployeeTransferBillDO transferBill = BeanUtils.toBean(saveReqVO, EmployeeTransferBillDO.class)
            .setProcessStatus(BpmTaskStatusEnum.RUNNING.getStatus());
    employeeTransferBillMapper.insertOrUpdate(transferBill);

    Map<String, Object> processInstanceVariables = BpmProcessVariableUtils.buildBillVariables(saveReqVO);
    processInstanceVariables.put(BpmProcessVariableConstants.CAUSE, transferBill.getName() + "人事调动申请");
    String processInstanceId = processInstanceApi.submitProcessInstance(
            Long.valueOf(saveReqVO.getCreator()),
            new BpmProcessInstanceCreateReqDTO()
                    .setProcessDefinitionKey(HrmBillTypeEnum.HRM_EMPLOYEE_TRANSFER_BILL.getProcessDefinitionKey())
                    .setVariables(processInstanceVariables)
                    .setBusinessKey(String.valueOf(transferBill.getId()))
    ).getCheckedData();

    employeeTransferBillMapper.updateById(new EmployeeTransferBillDO()
            .setId(transferBill.getId())
            .setProcessInstanceId(processInstanceId));
    return transferBill.getId();
}

这段代码体现了调动单作为典型“业务单据”的标准模式:先形成申请事实,再把审批系统挂上去。

5.2 审批回调:立即生效的单据直接改档案

updateProcessStatus() 是整个模块最关键的“分流点”:

@Override
@Transactional(rollbackFor = Exception.class)
public void updateProcessStatus(String businessKey, Integer status) {
    Long id = Long.parseLong(businessKey);
    EmployeeTransferBillDO bill = validateEmployeeTransferBillExists(id);

    EmployeeTransferBillDO updateObj = new EmployeeTransferBillDO();
    updateObj.setId(id);
    updateObj.setProcessStatus(status);

    if (APPROVE.getStatus().equals(status)) {
        if (Boolean.TRUE.equals(bill.getEffectiveImmediately())) {
            updateEmployeeFromTransferBill(id);
        }
    }

    employeeTransferBillMapper.updateById(updateObj);
}

这段逻辑非常值得借鉴:审批通过并不等于无条件立刻写档案,而是先判断业务策略。

5.3 档案回写:只更新确实发生变化的字段

updateEmployeeFromTransferBill() 不是粗暴地全量覆盖,而是按字段有条件更新:

public void updateEmployeeFromTransferBill(Long transferBillId) {
    EmployeeTransferBillRespVO transferBillRespVO = getEmployeeTransferBillInfo(transferBillId);
    EmployeeDO employee = employeeMapper.selectById(transferBillRespVO.getEmployeeId());

    EmployeeDO updateEmployee = new EmployeeDO();
    updateEmployee.setId(employee.getId());

    if (transferBillRespVO.getNewDeptId() != null) {
        updateEmployee.setDeptId(transferBillRespVO.getNewDeptId());
        updateEmployee.setDeptName(transferBillRespVO.getNewDeptName());
    }
    if (transferBillRespVO.getNewCompanyId() != null) {
        updateEmployee.setCompanyId(transferBillRespVO.getNewCompanyId());
        updateEmployee.setCompanyName(transferBillRespVO.getNewCompanyName());
    }
    if (StringUtils.isNotBlank(transferBillRespVO.getNewJobPost())) {
        updateEmployee.setJobPost(transferBillRespVO.getNewJobPost());
    }
    if (StringUtils.isNotBlank(transferBillRespVO.getNewJobPosition())) {
        updateEmployee.setJobPosition(transferBillRespVO.getNewJobPosition());
    }

    employeeMapper.updateById(updateEmployee);
}

它的价值在于:一次调动只改需要改的部分,不会误伤员工档案里的其他信息。

5.4 延迟生效:用 XXL-Job 到点补写档案

这段任务代码就是“月初统一生效”策略真正落地的地方:

@XxlJob("employeeTransferByEffectiveDateJob")
@TenantJob
public String execute() {
    LocalDate today = LocalDate.now();
    List<EmployeeTransferBillDO> bills = employeeTransferBillMapper.selectList(
            new LambdaQueryWrapperX<EmployeeTransferBillDO>()
                    .eq(EmployeeTransferBillDO::getProcessStatus, APPROVE.getStatus())
                    .eq(EmployeeTransferBillDO::getEffectiveImmediately, false)
                    .eq(EmployeeTransferBillDO::getEffectiveDate, today)
    );

    for (EmployeeTransferBillDO bill : bills) {
        employeeTransferBillService.updateEmployeeFromTransferBill(bill.getId());
    }
    return "处理完成";
}

这里的关键点有两个:

  1. 只处理“已审批通过 + 非立即生效 + 生效日期 = 今天”的单据。
  2. 使用 @TenantJob 保证多租户场景下按租户安全执行。

5.5 前端表单:主表和调动信息表单组合保存

前端不是一个单纯的 BasicForm,而是“员工基础信息表单 + 调动信息子表单”双表单合并提交:

async function handleSaveAndSubmit(isSubmit: boolean) {
  const formValues = isSubmit
    ? ((await basicFormRef.value.getFormValues()) as EmployeeTransferBillApi.EmployeeTransferBill)
    : ((await basicFormRef.value.getFormValues(false)) as EmployeeTransferBillApi.EmployeeTransferBill);

  const transferValues = await transferFormApi.getValues();

  const data = {
    ...formData.value,
    ...formValues,
    ...transferValues,
  };

  id = await (isSubmit
    ? submitEmployeeTransferBill(data)
    : saveEmployeeTransferBill(data));
}

这段实现很好地说明了为什么人事调动前端会拆成上下两块:员工现状是一组字段,调动目标又是一组字段,但提交时仍然应该是同一张业务单据。


六、RuoYi Office 的创新设计

6.1 审批通过与档案生效解耦

这是整个模块最有价值的设计。很多系统把“审批通过”直接等同于“立刻改档案”,RuoYi Office 则允许企业按管理制度选择是否延迟生效。

6.2 调动单保留完整原/新快照

原部门、原公司、原职位、原职务全部保留,使调动单天然具备审计价值,而不是一张只记录“结果”的表。

6.3 前端联动减少 HR 重复录入

员工选择带出原信息、部门选择带出公司,这些看似小细节,实际上决定了 HR 是否愿意长期使用系统。

6.4 XXL-Job 补齐“流程之外的时间逻辑”

审批系统擅长处理“谁来批”,定时任务擅长处理“什么时候生效”。把两者拆开,是非常成熟的架构分工。


七、数据结构

7.1 hrm_employee_transfer_bill

字段 含义 设计要点
employee_id 员工ID 关联正式员工档案
old_dept_id / new_dept_id 原/新部门 保留调动前后对照
old_company_id / new_company_id 原/新公司 支撑跨公司调动
new_job_post / new_job_position 新职位/新职务 部分字段允许局部更新
effective_immediately 是否立即生效 区分两种生效策略
effective_date 生效日期 定时任务筛选条件
process_status 流程状态 BPM 回调驱动

7.2 hrm_employee

字段 含义 调动时是否回写
dept_id / dept_name 所属部门
company_id / company_name 所属公司
job_post 职位
job_position 职务
其他个人信息 手机、身份证、学历等

八、技术亮点总结

设计要点 实现方式 价值
调动留痕 单据冗余原/新组织与岗位 审批与审计更清晰
即时生效 updateProcessStatus() 中直接回写档案 适合快速调岗场景
延迟生效 employeeTransferByEffectiveDateJob 支持月初统一生效
安全回写 updateEmployeeFromTransferBill() 按字段更新 避免误覆盖其他档案字段
前端联动 员工/部门选择弹窗自动带值 降低 HR 录入成本
多租户任务 @TenantJob 保证跨租户安全执行

九、快速体验

9.1 操作路径

  • 人事调动列表:人力 → 人事管理 → 人事调动
  • 调动申请详情:/hrm/employee-relation/transfer-info

9.2 推荐体验流程

  1. 进入人事调动列表,新建调动申请。
  2. 选择员工,确认系统自动带出的原公司、原部门、原职位信息。
  3. 填写异动类型、异动原因、变更后部门和岗位。
  4. 选择“立即生效”或“按生效日期生效”。
  5. 提交流程并完成审批。
  6. 如果选择立即生效,回到员工档案确认字段已更新。
  7. 如果选择延迟生效,等待生效日期后由任务自动更新。

9.3 源码仓库

类型 路径
前端 ruoyi-office-vben/apps/web-antd/src/views/hrm/employee-relation/transfer
后端 ruoyi-office/yudao-module-hrm/.../service/employee/EmployeeTransferBillServiceImpl.java
定时任务 ruoyi-office/yudao-module-hrm/.../job/employee/EmployeeTransferByEffectiveDateJob.java

结语

人事调动最容易做错的地方,不是表单字段不全,而是审批通过、何时生效、如何更新正式档案三者之间的关系处理不清。

RuoYi Office 这套“调动单承载审批事实、员工档案承载正式结果、XXL-Job 兜住延迟生效”的设计,实际上为很多 HRM 流程都提供了通用范式。凡是“审批通过后未必立刻更新主数据”的场景,比如岗位调整、编制变更、薪资调档,都可以复用这套思路。


💡 想要体验 RuoYi Office 的强大功能?

🌐 在线演示http://ruoyioffice.com/web/(账号 admin / admin123)

💬 技术咨询:添加💬 17156169080,备注「RuoYi Office」

如果觉得不错,请给个 Star 支持一下!


Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐