SpringBoot3+Vue3 招聘 Offer:发出、审批通过、转入职怎么走完
🌐 文档地址 :https://ruoyioffice.com
📦 源码1·GitHub :https://github.com/yuqing2026/ruoyi-office
📦 源码2·GitCode :https://gitcode.com/zhouzhongyan/ruoyi-office
📦 源码3·Gitee :https://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. 通过:三处一起改
回调里做四件事,同一事务:
- 若还没有
employeeEntryBillId,按 Offer 生成入职草稿。 - 候选人
status=hired。 - 投递
stage=offer_approved。 - 职位
hiredCount + 1。 - 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)
招聘管理 → 候选人打开已面试通过的人。- 新建录用审批,带上部门、计划入职日、试用期。
- 走完审批,回到录用详情看是否出现入职单号。
- 打开
入转调离 → 入职,核对姓名和部门是不是带过来的。 - 再看候选人状态是否 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 支持一下!