SpringBoot3+Vue3 招聘 Offer:发出、审批通过、转入职怎么走完

SpringBoot3+Vue3 招聘 Offer:发出、审批通过、转入职怎么走完

🌐 文档地址https://ruoyioffice.com

📦 源码1·GitHubhttps://github.com/yuqing2026/ruoyi-office

📦 源码2·GitCodehttps://gitcode.com/zhouzhongyan/ruoyi-office

📦 源码3·Giteehttps://gitee.com/yqzy1688/ruoyi-office

💬 微信:17156169080(备注「RuoYi Office」)
面试过了,Offer 在聊天软件里,入职表又是空白。HR 手抄身份证和部门,抄错一个字档案就对不上。RuoYi Office 把录用审批和入职单接起来:通过才建入职草稿,候选人和职位一起改状态。

▲ 左:陈雨薇已录用。右:投递 → Offer 审批 → 自动入职单 → 入职单再审。已有入职单 ID 不重复建


引言:Offer 难在「过了还要手建档」

缺的不是录用审批,是通过之后人落不到入职环节

现场 配错之后
Offer 通过就当入职 到岗日还没定,档案已经建了
HR 另开入职单手抄 姓名、手机、部门和候选人页对不上
候选人状态还停在面试 招聘漏斗假,职位已招满看不出来
回调重放再建一张入职 同一人两张入职单,报到日打架

本文只讲录用单怎么转到入职。需求提报、职位发布、简历初筛、面试评价是上游,不在这里展开。

菜单:人力资源 → 招聘管理 → 录用审批。流程 key:hr_recruitment_offer_bill。入职单自己的 key 是 hr_employee_entry_bill(以枚举为准)。


一、产品能力:Offer 是闸门,入职是下一张单

录用页 /hrm/recruitment/offer-info 不是「发一封邮件」的页面,是一张审批单:候选人、职位、部门、计划入职日、试用期月数、职级职务、汇报人、薪资说明。

能力 面向角色 和「只做一张审批单」差在哪
从投递开 Offer 招聘专员 带着 candidateId、applicationId、positionId
录用审批 用人部门 / HR 通过前候选人不改 hired
自动建入职单 系统 抄候选人档案 + Offer 任职字段
入职单再审 HR 到岗日、试用期还能改,再提交入职流程
职位已录用 +1 招聘负责人 编制是否招满能看见

候选人页 /hrm/recruitment/candidate-info 看的是人。录用页看的是「这家公司准备用什么条件收下他」。入职页 /hrm/employee-relation/entry-info 看的是「报到材料齐了没有」。三页不要合成一页长表。


二、业务怎么走:发出、通过、落到入职

1. 先有人,再有 Offer

候选人面试通过后,从投递关系开录用单。没有 applicationId 提交会失败。开发库 HR208-2026041800006(id=6)对应候选人陈雨薇,投递已能对上 Java 开发岗。

2. 审批中:人还是候选人

提交走 Flowable,processStatus=审批中。这时不建入职单、不加 hired、职位人数不加。驳回或撤回,人仍停在原阶段,避免「没谈拢却进了入职池」。

HR208-2026041800005(id=5)是审批中的对照单。

3. 通过:三处一起改

回调里做四件事,同一事务:

  1. 若还没有 employeeEntryBillId,按 Offer 生成入职草稿。
  2. 候选人 status=hired
  3. 投递 stage=offer_approved
  4. 职位 hiredCount + 1
  5. Offer 自己记下入职单 ID。

陈雨薇这张单通过后,入职单是 HR201-2026041800003(id=31),部门研发,流程已通过。入职单还可以是未提交:另一张 HR201-2026041800001(id=30)就是 Offer 通过后停在草稿,等人改到岗日再提交。

这就是产品上最重要的一句:Offer 通过 ≠ 当天入职。

/hrm/recruitment/offer-info?id=6 HR208-2026041800006。审批通过后才生成入职单,薪资说明会写进入职备注

/hrm/employee-relation/entry-info?id=31 HR201-2026041800003 陈雨薇。姓名、手机、身份证、开发工程师从 Offer 带过来,入职单再走自己的审批

/hrm/recruitment/candidate-info?id=4。录用通过后这里变成 hired,不是 HR 再手改一次状态


三、设计怎么落地

设计思路

决策点 方案 理由
何时建入职 APPROVE 回调 驳回不会留下空入职单
字段从哪抄 候选人档案 + Offer 任职 身份证、学历跟人走,部门、试用期跟 Offer 走
重复回调 已有 employeeEntryBillId 直接返回 流程重放不建第二张
租户 TenantUtils.execute 再建入职 避免回调丢租户上下文
入职是否立即提交 save,不自动 submit 到岗日、材料还能补

#mermaid-svg-40cH3ONopyncxjqT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-40cH3ONopyncxjqT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-40cH3ONopyncxjqT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-40cH3ONopyncxjqT .error-icon{fill:#552222;}#mermaid-svg-40cH3ONopyncxjqT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-40cH3ONopyncxjqT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-40cH3ONopyncxjqT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-40cH3ONopyncxjqT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-40cH3ONopyncxjqT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-40cH3ONopyncxjqT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-40cH3ONopyncxjqT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-40cH3ONopyncxjqT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-40cH3ONopyncxjqT .marker.cross{stroke:#333333;}#mermaid-svg-40cH3ONopyncxjqT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-40cH3ONopyncxjqT p{margin:0;}#mermaid-svg-40cH3ONopyncxjqT .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-40cH3ONopyncxjqT .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-40cH3ONopyncxjqT .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-40cH3ONopyncxjqT .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-40cH3ONopyncxjqT .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-40cH3ONopyncxjqT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-40cH3ONopyncxjqT .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-40cH3ONopyncxjqT .node rect,#mermaid-svg-40cH3ONopyncxjqT .node circle,#mermaid-svg-40cH3ONopyncxjqT .node ellipse,#mermaid-svg-40cH3ONopyncxjqT .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-40cH3ONopyncxjqT .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-40cH3ONopyncxjqT .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-40cH3ONopyncxjqT .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-40cH3ONopyncxjqT .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-40cH3ONopyncxjqT .edgeLabel .label text{fill:#333;}#mermaid-svg-40cH3ONopyncxjqT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} delivers
岗位
开录用
通过后生成
CANDIDATE
bigint
id
PK
string
name
string
status
string
id_card
APPLICATION
POSITION
OFFER_BILL
bigint
id
PK
string
bill_code
int
process_status
bigint
employee_entry_bill_id
date
planned_entry_date
int
probation_period
ENTRY_BILL
bigint
id
PK
string
bill_code
string
name
date
entry_date
int
employee_status

Offer 是闸门表。入职单是人事主数据的草稿。候选人 status 和投递 stage 是招聘漏斗,不要和入职 processStatus 混用一个字段。
候选人 入职服务 Flowable 录用服务 Vue3 录用页 候选人 入职服务 Flowable 录用服务 Vue3 录用页 #mermaid-svg-F7RPziSBE3x4ZyqL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-F7RPziSBE3x4ZyqL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-F7RPziSBE3x4ZyqL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-F7RPziSBE3x4ZyqL .error-icon{fill:#552222;}#mermaid-svg-F7RPziSBE3x4ZyqL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-F7RPziSBE3x4ZyqL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-F7RPziSBE3x4ZyqL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-F7RPziSBE3x4ZyqL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-F7RPziSBE3x4ZyqL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-F7RPziSBE3x4ZyqL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-F7RPziSBE3x4ZyqL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-F7RPziSBE3x4ZyqL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-F7RPziSBE3x4ZyqL .marker.cross{stroke:#333333;}#mermaid-svg-F7RPziSBE3x4ZyqL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-F7RPziSBE3x4ZyqL p{margin:0;}#mermaid-svg-F7RPziSBE3x4ZyqL .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-F7RPziSBE3x4ZyqL text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-F7RPziSBE3x4ZyqL .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-F7RPziSBE3x4ZyqL .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-F7RPziSBE3x4ZyqL .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-F7RPziSBE3x4ZyqL .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-F7RPziSBE3x4ZyqL #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-F7RPziSBE3x4ZyqL .sequenceNumber{fill:white;}#mermaid-svg-F7RPziSBE3x4ZyqL #sequencenumber{fill:#333;}#mermaid-svg-F7RPziSBE3x4ZyqL #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-F7RPziSBE3x4ZyqL .messageText{fill:#333;stroke:none;}#mermaid-svg-F7RPziSBE3x4ZyqL .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-F7RPziSBE3x4ZyqL .labelText,#mermaid-svg-F7RPziSBE3x4ZyqL .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-F7RPziSBE3x4ZyqL .loopText,#mermaid-svg-F7RPziSBE3x4ZyqL .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-F7RPziSBE3x4ZyqL .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-F7RPziSBE3x4ZyqL .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-F7RPziSBE3x4ZyqL .noteText,#mermaid-svg-F7RPziSBE3x4ZyqL .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-F7RPziSBE3x4ZyqL .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-F7RPziSBE3x4ZyqL .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-F7RPziSBE3x4ZyqL .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-F7RPziSBE3x4ZyqL .actorPopupMenu{position:absolute;}#mermaid-svg-F7RPziSBE3x4ZyqL .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-F7RPziSBE3x4ZyqL .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-F7RPziSBE3x4ZyqL .actor-man circle,#mermaid-svg-F7RPziSBE3x4ZyqL line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-F7RPziSBE3x4ZyqL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HR 提交录用submit + startProcesshr_recruitment_offer_bill审批通过回调save 入职草稿status=hired记下 entryBillId打开入职单补材料再提交入职流程 HR

提交只启录用流程。通过才碰入职和漏斗。入职单要报到,HR 自己再点提交。

java 复制代码
public Long submitRecruitmentOfferBill(RecruitmentOfferBillSaveReqVO saveReqVO) {
    RecruitmentOfferBillDO offerBill = buildOfferBill(saveReqVO, true);
    recruitmentOfferBillMapper.insertOrUpdate(offerBill);
    Map<String, Object> variables = BpmProcessVariableUtils.buildBillVariables(saveReqVO);
    String processInstanceId = processInstanceApi.submitProcessInstance(
            Long.valueOf(saveReqVO.getCreator()),
            new BpmProcessInstanceCreateReqDTO()
                    .setProcessDefinitionKey("hr_recruitment_offer_bill")
                    .setVariables(variables)
                    .setBusinessKey(String.valueOf(offerBill.getId())))
            .getCheckedData();
    offerBill.setProcessInstanceId(processInstanceId);
    return offerBill.getId();
}

回调里把入职单 ID 写回 Offer。已经有 ID 就当幂等成功。

java 复制代码
public void updateProcessStatus(String businessKey, Integer status) {
    Long id = Long.parseLong(businessKey);
    RecruitmentOfferBillDO updateObj = new RecruitmentOfferBillDO().setId(id).setProcessStatus(status);
    if (APPROVE.getStatus().equals(status)) {
        Long entryBillId = createEmployeeEntryBillFromOffer(bill);
        updateObj.setEmployeeEntryBillId(entryBillId);
    }
    recruitmentOfferBillMapper.updateById(updateObj);
}

建入职单时抄候选人自然信息,抄 Offer 的部门、职务、试用期;有试用期则员工状态先试岗。

java 复制代码
private Long createEmployeeEntryBillFromOffer(RecruitmentOfferBillDO offerBill) {
    if (offerBill.getEmployeeEntryBillId() != null) {
        return offerBill.getEmployeeEntryBillId();
    }
    EmployeeEntryBillSaveReqVO vo = buildEmployeeEntryBillSaveReqVO(offerBill, candidate);
    Long entryBillId = executeCreateEntryBill(offerBill.getTenantId(), vo);
    recruitmentCandidateMapper.updateById(new RecruitmentCandidateDO()
            .setId(candidate.getId()).setStatus("hired"));
    // 投递 stage=offer_approved;职位 hiredCount + 1
    return entryBillId;
}

前端录用页流程 key 写死 hr_recruitment_offer_bill,和后端枚举一致。入职页是另一张 CUSTOM 单据,不要在录用页里嵌整页入职材料。


四、边界与对照

设计要点 落地方式 对业务的价值
通过才建档 APPROVE 回调 黄了的 Offer 不进人事
幂等 已有入职单 ID 直接返回 避免双档
分两段流程 Offer / 入职两个 key 谈薪和报到材料分开审
漏斗回写 hired + offer_approved + hiredCount 招聘看板不用再对 Excel
租户包裹 TenantUtils 回调线程也能落到对的库

和「入职单一张表管全部」的差别:录用先确认编制和条件,入职再确认材料和到岗。不要把身份证扫描件做到 Offer 上。


五、快速体验

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

  1. 招聘管理 → 候选人 打开已面试通过的人。
  2. 新建录用审批,带上部门、计划入职日、试用期。
  3. 走完审批,回到录用详情看是否出现入职单号。
  4. 打开 入转调离 → 入职,核对姓名和部门是不是带过来的。
  5. 再看候选人状态是否 hired,职位已录用是否加一。

源码仓库:GitHub:https://github.com/yuqing2026/ruoyi-office | GitCode:https://gitcode.com/zhouzhongyan/ruoyi-office | Gitee:https://gitee.com/yqzy1688/ruoyi-office


常见问题(FAQ)

Offer 通过了为什么没有入职单?

看回调有没有进 APPROVE,以及候选人、投递、职位是否都还在。缺 applicationId 会在建档时失败。

入职单能不能改到岗日?

可以。生成的是草稿或独立流程单,计划入职日只是默认值。改完再提交入职审批。

同一候选人能开两张 Offer 吗?

产品上应避免并行录用。若历史数据有两张,以通过且已写 employeeEntryBillId 的为准,不要手删入职单只留 Offer。

和劳动合同是不是一张单?

不是。劳动合同在入职之后另走合同模块。Offer 备注里的薪资说明只进进入职备注,不是合同正文。

OA 裁剪版有招聘吗?

招聘和入职在 HRM。没有 HRM 模块就没有这条链,不要用 OA 入职登记代替。


结语

录用审批要守的是闸门:没通过,人就还是候选人;通过了,入职单从 Offer 长出来,漏斗一起动。读者能带走的三句:Offer 通过才建入职;入职单还要再审;回调必须幂等。

你们现在 Offer 过了是手建入职,还是系统带过去?评论区说一下抄错过几次身份证。


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

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

📦 源码仓库:GitHub:https://github.com/yuqing2026/ruoyi-office | GitCode:https://gitcode.com/zhouzhongyan/ruoyi-office | Gitee:https://gitee.com/yqzy1688/ruoyi-office

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

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

相关推荐
Bs_MoneyMagnet12 小时前
基于springboot+vue的医疗健康便民服务平台的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring
loser.with.m14 小时前
【AgentScope 2.0】05-从数据库配置到可运行 Agent:十个 Builder 开关怎么把一行 JSON 变成 HarnessAgent
人工智能·spring boot·ai
行百里er15 小时前
Spring Insight 里收到的 Span 是怎么存下来的
spring boot·spring·监控
xcl092517 小时前
智慧健身场馆系统开发实战:从架构设计到核心模块落地指南
java·spring boot
vx-程序开发18 小时前
【计算机毕设】基于Spring Boot的古城景区管理系统88564
java·数据库·spring boot·后端·spring·elasticsearch·课程设计
vx-Biye_Design18 小时前
springboot中国传统节日宣传平台49078-计算机课程设计、毕业设计
java·前端·vue.js·spring boot·后端·课程设计·idea
代码调试师20 小时前
【毕设分享】springboot攀枝花生鲜电商平台58990
java·vue.js·spring boot·后端·架构·eclipse·课程设计
xcl092520 小时前
全民健身解决方案小程序系统开发实战:从架构设计到上线指南
java·spring boot
洛阳泰山20 小时前
别卷 Python 了:我用 Java 21 + Spring Boot 3 打造了一个企业级 RAG + 智能体工作流引擎(附架构与源码解析)
java·spring boot·后端