阅读建议:本流程同时涉及外包人员、项目、协议、业务线、年度计划、项目编制和审批等模块。建议先理解这些模块的概念,再阅读下面的完整流程图。这样更容易看懂人员为什么需要校验、项目编制如何变化,以及申请在审批、生效和失败重试之间如何流转。------------本文是该项目的核心。
1. 业务流程图(产品和测试版,不含数据库读写)
本图隐藏表名、字段、状态码和系统接口,仅呈现用户操作、业务校验、审批、生效、异常及重试流程,供产品和测试理解、评审及设计用例。
2. 完整流程图(含数据库读写)
图中“查”表示读取本地 HRMS 数据库,“写”包含新增、更新、删除和归档;流程中心、SSO、ACL、消息中心属于外部系统调用,不对应 HRMS 本地表。
2.1 查校验数据的用途
项目:project_detail,project_code = project_id
协议:agreement,agreement_no = agreement_id
机构:t_abs_org_basic
业务线:business_line;角色权限另查 ACL 外部系统
这些数据主要用于校验“当前人员是否有资格发起这次申请”,不是单纯为了页面展示。
| 查询对象 | 查出来的用途 |
|---|---|
project_detail | 根据人员的 project_id 确认项目存在;取得项目额度、业务线和项目年度;入场时计算申请后剩余编制 |
agreement | 根据 agreement_id 确认人员关联的协议存在;入场申请还要求协议状态为已生效;协议编号同时写入人员申请明细作为快照 |
t_abs_org_basic | 查询人员所属机构和本次操作机构的管理路径,判断当前用户是否有权操作该机构下的人员 |
business_line | 取得项目所属业务线信息,并与当前用户可管理的业务线进行比较 |
| ACL 外部系统 | 判断当前账号是否属于 HR 等特殊角色;如果不是,则限制其只能操作有权限的业务线 |
整体校验逻辑可以理解为:
人员关联的项目是否存在?
→ 人员关联的协议是否有效?
→ 当前操作人是否有人员所属机构的权限?
→ 当前操作人是否有项目所属业务线的权限?
→ 全部通过后,人员才能进入本次入场/离场申请
其中:
- 项目和协议属于“业务数据有效性校验”。
- 机构、业务线和 ACL 属于“操作权限校验”。
project_detail.project_quota还用于入场编制计算,并非只做存在性校验。
2.2 为什么要防止人员重复申请
查询条件实际表示:
account_id IN (本次选择的 staffId 集合)
AND application_batch_id != 本次 requestId
AND status = 0
各条件含义:
account_id IN staffIds:查本次选中的这些人员。这里的account_id实际保存的是outsourced_staff.id,也就是staffId。status = 0:该人员申请明细仍处于“待处理”,还没有实际入场或离场完成。application_batch_id != 本次 requestId:排除当前正在编辑的这张申请,避免把自己误判为重复申请。
例如:
人员:张三,staffId = S001
已有申请:requestId = R001,status = 0
当前新申请:requestId = R002
系统查询到张三已经在 R001 中待处理,就不会再把他加入 R002,并提示:
已在入场申请流程中
如果当前只是在继续编辑 R001,因为查询排除了本次 requestId = R001,所以不会误报重复。
核心目的就是:避免同一个人同时存在于两张未完成的申请中,防止两条审批流程同时修改人员状态、账号和项目编制。
2.3 requestId 的用途
requestId 是 HRMS 为这一次入场或离场申请生成的业务申请单编号,用于把整张申请涉及的数据关联起来。
它的主要用途有:
- 作为申请主表主键
outsourced_staff_onoffboard_application.id = requestId
一张入场或离场申请对应一个 requestId。
- 关联人员申请明细
outsourced_staff_onoffboard_application_record.application_batch_id
= requestId
通过它可以知道这张申请包含哪些人员。
- 关联项目编制明细
outsourced_staff_onoffboard_application_project.application_batch_id
= requestId
通过它可以知道这张申请占用或释放了多少项目编制。
- 关联附件
attachment.biz_id = requestId
用于查询这张申请上传的附件。
- 支持后续操作
前端后续查询、继续编辑、正式提交、取消申请和失败重试,都通过 requestId 定位申请单。
- 关联审批中心
正式提交时,将它传给流程中心:
businessNum = requestId
审批变量 request_id = requestId
这样流程中心的审批记录可以反查 HRMS 业务申请。
例如:
requestId = R001
申请主表
└── id = R001
人员明细
├── 张三,application_batch_id = R001
└── 李四,application_batch_id = R001
项目编制明细
└── application_batch_id = R001,request_num = -2
附件
└── biz_id = R001
简单理解:requestId 就是整张入场或离场申请的“订单号”,负责串联申请主表、人员明细、项目编制、附件和审批流程。
它和 flowInstanceId 不一样:
requestId = HRMS 业务申请编号,初始化申请时生成
flowInstanceId = 流程中心审批实例编号,正式提交审批后生成
2.4 project_quota 的用途
这一步是在计算:项目总编制是多少、已经占用了多少、本次入场后还剩多少。
系统没有直接修改 project_detail.project_quota,而是使用项目申请明细表中的正负数流水计算编制。
2.4.1 查询项目总编制
project_detail.project_code = 人员的 project_id
读取:
project_quota:项目总编制。business_line_code:所属业务线,用于权限校验和确定审批业务线。- 起止年度:确定本次申请对应的项目年度。
例如:
project_code = P001
project_quota = 10
表示项目 P001 总共允许配置 10 人。
2.4.2 汇总项目已有的编制变动
查询:
outsourced_staff_onoffboard_application_project
WHERE project_id = P001
然后汇总:
SUM(request_num)
request_num 使用正负数表示编制变化:
| 场景 | request_num | 含义 |
|---|---|---|
| 入场 1 人 | -1 | 占用一个编制 |
| 入场 3 人 | -3 | 占用三个编制 |
| 离场 1 人 | +1 | 释放一个编制 |
| 离场 2 人 | +2 | 释放两个编制 |
2.4.3 计算本次申请前后的剩余编制
计算方式:
本次申请前可用编制
= project_quota + 已有 request_num 汇总值
本次申请后剩余编制
= 本次申请前可用编制 + 本次 request_num
例如项目总编制为 10 人:
历史入场 6 人:request_num = -6
历史离场 1 人:request_num = +1
已有 request_num 汇总
= -6 + 1
= -5
当前可用编制:
available_num
= 10 + (-5)
= 5
现在又申请入场 2 人:
本次 request_num = -2
remain_num
= 5 + (-2)
= 3
最终写入本次项目申请明细:
project_num = 10 项目总编制
available_num = 5 本次申请前可用编制
request_num = -2 本次占用两个编制
remain_num = 3 本次申请后剩余编制
核心目的就是:把 project_detail.project_quota 作为总额度,通过汇总项目申请明细的 request_num,计算本次入场是否会超出项目编制。
如果计算结果:
remain_num < 0
正式提交入场审批时就会报错:
本次申请后剩余编制小于 0
2.5 business_line_code 和项目起止年度的用途
可以把 business_line_code 理解成:项目属于哪个业务条线的标签。
比如公司可能有:
车险业务线
非车险业务线
农险业务线
健康险业务线
某个外包项目属于“车险业务线”,那么项目表中可能保存:
project_id = P001
business_line_code = CAR
办理人员入场时,系统主要用它做两件事。
2.5.1 判断当前用户是否有权办理
例如:
项目 P001 属于车险业务线
当前用户只能管理非车险业务线
那么系统会阻止当前用户为这个项目办理人员入场。
如果当前用户拥有车险业务线权限,才能继续申请。
2.5.2 决定申请由谁审批
正式提交后,系统把项目的 business_line_code 传给流程中心。
流程中心根据业务线找到对应的审批人:
车险项目
→ 走车险业务线审批人员
农险项目
→ 走农险业务线审批人员
所以它并不负责计算编制人数,而是负责:
business_line_code
├── 判断当前用户能不能操作这个项目
└── 告诉流程中心应该由哪条业务线的人审批
一句话理解:business_line_code 就像项目的“所属部门标签”,既用来限制操作权限,也用来找到正确的审批人。
关键点是:“启用年度”和 start_year 不是两个同等优先级的选择。启用年度是首选,start_year 只是当前代码的兜底值。
2.5.3 什么是启用年度
plan_year 表保存公司已经创建的年度计划:
2025,status = 0,停用
2026,status = 1,启用
2027,status = 0,停用
status = 1 表示当前公司正在使用、正在核算编制的计划年度。
可以把它理解成:
启用年度 = 当前打开使用的年度账本
系统设计上通常只有一个启用年度。启用新年度时,会先把原来的启用年度改成停用。
2.5.4 正常情况下为什么取启用年度
假设项目有效期是:
start_year = 2025
end_year = 2027
当前启用年度是 2026:
2025—2027 包含 2026
→ 本次人员申请归入 2026 年度
→ project_year = 2026
意思是:
虽然项目跨了三年,但这次入场申请发生在当前启用的 2026 年度,所以给申请标记 2026。
2.5.5 为什么又能取 start_year
假设年度表是:
2025,status = 0
2026,status = 0
2027,status = 0
项目范围内有年度计划,但是没有任何年度处于启用状态。
当前代码没有报错,而是兜底:
project_year = start_year = 2025
这里并不表示 2025 已经启用,也不表示 2025 一定是正确的业务年度。
它只是表示:
没有找到启用年度
→ 为了保证 project_year 有值
→ 暂时使用项目开始年度
2.5.6 三种情况对比
| 年度表情况 | 最终取值 | 原因 |
|---|---|---|
| 2026 是启用年度,并且位于项目起止范围内 | 2026 | 正常归入当前启用年度 |
| 项5—2027 年度都存在,但全部停用 | 2025 | 没有启用年度,代码用 start_year 兜底 |
| 2025—2027 一个年度计划都没有创建 | 报错 | 项目范围内没有任何年度计划 |
2.5.7 这个兜底合理吗
从代码上看,它能保证程序继续执行;但从业务语义上看,有一点不严谨:
没有启用年度
却把申请记录到已经停用的 start_year
因此它不是“启用年度和开始年度都可以随便选”,而是:
优先:项目范围内的启用年度
兜底:项目 start_year
完全没有年度计划:报错
如果业务要求“人员申请必须属于当前启用年度”,更严格的处理应该是:
项目起止范围内没有启用年度
→ 直接报错
→ 提示先启用计划年度
而不是回退到 start_year。
另外,当前入离场流程计算剩余编制时只按 project_id 汇总,没有按 project_year 分年度计算。因此这里的 project_year 更像一个年度归属标签或快照字段,不会因为取了 2025 或 2026 就重新生成一套项目编制。
2.6 申请确认页为什么要查四张表
这是什么申请?
申请里有哪些人?
人员详细信息是什么?
本次申请占用多少编制?
申请后还剩多少编制?
所以需要查询多张表。
2.6.1 四张表分别负责什么
| 查询表 | 回答的问题 |
|---|---|
| 申请主表 | 这是一张什么申请? |
| 人员申请明细 | 这张申请选了哪些人? |
| 人员表 | 这些人的详细资料是什么? |
| 项目申请明细 | 这次申请占用多少编制? |
1. 申请主表
outsourced_staff_onoffboard_application
根据 requestId 查询:
application_type:入场还是离场
status:草稿、审批中、成功还是失败
application_date:计划入场或离场日期
remark:申请备注
flow_instance_id:对应哪个审批流程
它回答的是:
这是一张什么申请,目前进行到哪一步?
2. 人员申请明细
outsourced_staff_onoffboard_application_record
查询条件:
application_batch_id = requestId
它保存:
这张申请包含哪些 staffId
人员申请处理成功还是失败
失败原因是什么
申请时所属的项目和协议
它回答的是:
这张申请选了哪些人?
只查询 outsourced_staff 不够,因为人员表里没有 requestId,无法判断某个人属于哪张申请。
3. 人员表
outsourced_staff
人员申请明细里主要只有 staffId,没有完整的人员资料,所以还要回查人员表,取得:
姓名
手机号
人员状态
所属机构
所属项目
所属协议
工作地点
其他人员信息
两张表的分工是:
申请人员明细
→ 确定这张申请选了哪些 staffId
人员表
→ 根据 staffId 查询这些人的完整资料
4. 项目申请明细
outsourced_staff_onoffboard_application_project
它保存这张申请生成时的编制快照:
project_num:项目总编制
available_num:本次申请前可用编制
request_num:本次申请人数
remain_num:本次申请后剩余编制
project_year:归属计划年度
business_line_code:所属业务线
它回答的是:
这次申请会占用或释放多少项目编制?
仅查询人员名单,只能知道申请了几个人,不能直接还原发起申请时的总编制和剩余编制。
2.6.2 举个例子
requestId = R001
申请类型 = 入场
申请主表告诉系统:
R001 是一张入场申请
当前状态是 init
人员申请明细告诉系统:
R001 包含 S001、S002 两个人
人员表告诉系统:
S001 = 张三
S002 = 李四
项目申请明细告诉系统:
项目总编制 = 10
申请前可用 = 5
本次申请 = 2
申请后剩余 = 3
确认页最终展示的是:
入场申请 R001
├── 人员:张三、李四
├── 本次申请:2 人
├── 项目总编制:10 人
├── 申请前可用:5 人
└── 申请后剩余:3 人
2.6.3 最简单的理解
申请主表
→ 查“什么申请”
人员申请明细
→ 查“申请选了谁”
人员表
→ 查“这些人是谁”
项目申请明细
→ 查“用了多少编制”
因此,不是所有页面都必须查这么多表。如果页面只展示人员名单,确实只需要:
人员申请明细 → 人员表
当前流程图把它们放在一起,是因为描述的是完整的申请确认页面,而不只是人员名单查询。
2.7 为什么入场是 application_date ≤ 今天,离场是 application_date < 今天
因为当前代码把两个日期理解成不同的边界:
入场日期 = 第一天
离场日期 = 最后一天2.7.1 入场:application_date ≤ 今天
到入场日期当天,人员就应该生效。
例如:
入场日期:9 月 10 日
今天:9 月 10 日当天就创建或返聘账号,并把人员状态修改为“已入场”。
2.7.2 离场:application_date < 今天
离场日期被当作人员的“最后有效日”。离场日期当天人员仍然有效,等这一天结束、日期过去后,才真正执行离场。
例如:
离场日期:9 月 10 日
9 月 10 日:仍是在场状态
9 月 11 日:离场日期已经小于今天,执行账号失效并改为“已离场”因此可以简单理解为:
入场日期:从这一天开始生效
离场日期:到这一天结束为止这不是技术上必须如此,而是当前代码采用的业务口径。如果产品定义的“离场日期”表示“当天一到就离场”,判断条件就应该改成 application_date ≤ 今天;但按照当前实现,它表示最后工作日或最后有效日。
2.8 为什么入场任职快照的 event_type = AGREEMENT
这里的 AGREEMENT 不是说“本次入场发生了协议变更”,而是表示:这条任职快照记录的是人员使用某份协议的任职区间。
人员入场后,系统会形成一段关系:
某个人
在某个项目下
使用某份协议
从入场日期开始任职因此,入场时会向 outsourced_staff_movement 写入:
event_type = AGREEMENT
agreement_id = 当前协议编号
agreement_start_date = 入场日期
agreement_end_date = 离场日期,入场时通常为 9999-12-31例如:
人员:张三
协议:A001
入场日期:2026-09-10写入的任职快照相当于:
张三使用协议 A001
任职开始:2026-09-10
任职结束:9999-12-31
event_type:AGREEMENT以后张三离场时,系统会按照以下条件找到这条协议任职记录:
account_id
+ agreement_id
+ event_type = AGREEMENT找到记录后,再把 agreement_end_date 修改为实际离场日期。
这类记录还有一个重要用途:后续按协议结算或查询人员时,可以根据以下信息判断某个人在结算周期内是否使用过这份协议:
agreement_id
+ event_type = AGREEMENT
+ agreement_start_date / agreement_end_date所以它的实际含义更接近:
AGREEMENT = 协议任职区间记录而不是:
AGREEMENT = 本次发生了协议变更流程图中的这一步可以理解为:
写任职快照表:outsourced_staff_movement
新增人员本次协议任职区间:
event_type = AGREEMENT
agreement_id = 当前协议
agreement_start_date = 入场日期
agreement_end_date = 默认离场日期
用途:
后续按协议查询任职历史
离场时找到并结束当前协议任职区间需要注意:实体注释虽然定义了 DEFAULT / AGREEMENT / PROJECT,但当前代码中多个场景都写成了 AGREEMENT,包括部分项目续延和终止处理。因此,当前实现中的 AGREEMENT 已经带有“协议维度任职记录”的泛化含义,并不完全是严格的事件来源。
2.9 为什么部分人员离场失败仍释放整批编制
核心原因是:当前代码按“整张离场申请”释放编制,而不是按“实际离场成功人数”释放编制。
执行过程是:
逐个人员办理离场
├── 成功:人员明细 status = 1
└── 失败:人员明细 status = 2
全部人员处理结束
→ 不判断是否全部成功
→ 仍然调用项目编制处理
→ 按申请人员明细总数写入 request_num代码统计的是申请内全部人员:
按 project_id 对所有申请人员明细计数离场时:
request_num = +申请人员总数它没有先过滤:
人员明细.status = 1例如,一张离场申请有 3 人:
张三:离场成功
李四:离场成功
王五:SSO 账号失效失败当前代码仍然写入:
request_num = +3而不是:
request_num = +2可能的设计意图是:
- 离场申请已经审批通过并到达生效日,业务上认为整批人员都应退出项目;
- 单个人员失败通常被当成 SSO、消息或数据处理异常,不改变其应该离场的业务决定;
- 编制只在首次离场执行时统一释放一次;
- 后续失败重试不会再次增加
request_num,避免重复释放编制。
因此,目前的口径是:
编制释放依据 = 已生效的离场申请人数
不是 = 技术处理成功人数但这里存在一个风险:
人员实际离场失败
但项目编制已经释放如果失败人员一直没有重试成功,就会出现:
人员仍未完成离场
项目却已经可以使用这个空出的编制所以,这只能说明“当前代码为什么这么执行”,不能证明业务规则一定合理。如果实际要求是“人员真正离场成功后才能释放编制”,当前实现需要调整为:
首次处理:只释放 status = 1 的成功人数
失败重试成功:再释放本次新增成功人数
同时增加防重复释放控制流程图中的这一步可以理解为:
当前实现:按整张离场申请释放项目编制
查 project_detail
查 outsourced_staff_onoffboard_application_project
并按 project_id 汇总 request_num
写项目申请明细:
request_num = +申请人员总数
不按人员处理结果过滤
部分人员失败时仍释放整批编制
失败重试不再重复释放
注意:
如果业务要求实际离场成功后才释放编制,
当前实现存在提前释放风险,需要进一步确认