SpringBoot+Vue3 绩效“谁评谁”设计:1类主评、2种规则、3类来源破解评审错派
🌐 演示地址:https://ruoyioffice.com | 📦 源码1·GitHub:ruoyi-office | 📦 源码2·GitCode:ruoyi-office | 📦 源码3·Gitee:ruoyi-office | 💬 微信:17156169080(备注「RuoYi Office」)
绩效系统里最危险的错误,往往不是 90 分算成了 89.9 分,而是评价任务发给了错误的人。主管调整、部门负责人缺失、指定评委变更、批次重生成覆盖人工结果,都会让“谁评谁”失真。RuoYi Office 把评估关系做成独立能力:方案层定义规则,周期层固化实例,发布前先预览,异常统一进池,人工改派再同步计划、评价单与必要的 BPM 变量。

▲ 绩效评估关系全景:L1 方案规则 → 发布前预览 → L2 周期实例 → 异常池/人工修正 → 计划、评价单与 BPM 同步;当前边界是单主评,不是完整 360 多角色。
结论前置:先建立可执行关系,再启动评价流程
核心结论只有一句话:绩效流程不能靠临时推断主评人,必须先生成一条可审计、可修正、可同步的“被评人 → 主评人”关系。
当前产品已落地:
relationType=1,当前关系类型是主评。assessorType=1/2,当前主评规则支持部门负责人与指定用户。source=1/2/3,关系来源支持 AUTO / MANUAL / IMPORT。- 草稿批次发布前可预览
matchedCount / unmatchedCount / relationCount / abnormalCount。 - 无法解析主评时保留异常码和异常说明,并进入工作台异常池。
- 已锁定、手工或导入的关系在重生成时会被保留。
- HR 可更换主评人,系统同步回写计划、评价单,并在必要时更新 BPM 的
leaderUserId。 - 校准及之后阶段禁止改边,避免历史责任链被重写。
但边界也必须说清楚:
当前落地的是“单主评关系闭环”,不是完整 360 多角色评价。
代码里虽然已经把关系抽象成独立表,也保留了 relationType、roleLabel 等扩展位置,但目前不能据此宣称同级、下属、跨部门评委、多角色权重汇总已经全部完成。
二、两层模型:L1 定规则,L2 固化本周期结果
2.1 为什么要拆成两层?
L1 解决“通常应该由谁评”,L2 解决“这个周期最终由谁评”。
如果只有 L1:
- 每次查询都要重新解析组织关系;
- 负责人变化会让历史批次的主评漂移;
- 无法记录手工改派;
- 无法对异常关系做闭环处置。
如果只有 L2:
- 每个周期都要手工维护全部关系;
- 无法复用方案规则;
- 批量生成成本过高;
- 很难保证不同批次口径一致。
两层组合后,规则可复用,实例可修正。
2.2 L1:PerformanceRelationRuleDO
L1 规则挂在绩效方案 schemeId 下。
当前真实字段表达的能力如下:
| 字段 | 当前语义 | 已落地取值 |
|---|---|---|
schemeId | 所属绩效方案 | 方案 ID |
assesseeScopeType | 被评人范围 | 1=ALL |
assessorType | 主评人解析类型 | 1=部门负责人、2=指定用户 |
assessorParam | 解析参数 | 指定用户 ID 等 |
sort | 规则顺序 | 当前按顺序取首条 |
fallbackPolicy | 找不到主评时的策略 | 1=EXCEPTION |
这张表的角色是“规则模板”,不是本周期最终名单。
当方案没有配置关系规则时,服务会构造一条默认规则:
- 被评范围为全部;
- 主评人按部门负责人解析;
- 找不到时记录异常。
2.3 L2:PerformanceEvalRelationDO
L2 关系挂在绩效周期 periodId 下。
它记录的是发布后真正参与执行的数据:
| 字段 | 作用 | 业务价值 |
|---|---|---|
periodId | 归属周期 | 隔离不同考核批次 |
assesseeEmployeeId | 被考核员工 | 关系左端 |
assessorUserId | 主评用户 | 关系右端 |
relationType | 关系类型 | 当前固定为主评 |
source | 来源 | 区分自动、手工、导入 |
locked | 是否锁定 | 控制重生成是否覆盖 |
status | 有效/排除 | 支持关系状态治理 |
roleLabel | 角色标签 | 当前默认“主评” |
exceptionCode | 异常码 | 机器可识别原因 |
exceptionMsg | 异常说明 | HR 可读原因 |
2.4 真实源码:当前数据边界
下面片段来自 PerformanceEvalRelationDO,裁剪后保留了最关键的边界字段。
public class PerformanceEvalRelationDO extends BaseDO {
@TableId
private Long id;
private Long periodId;
private Long assesseeEmployeeId;
private Long assesseeUserId;
private Long assessorUserId;
private String assessorUserName;
/** 关系类型:1=主评 */
private Integer relationType;
/** 来源:1=AUTO 2=MANUAL 3=IMPORT */
private Integer source;
/** 锁定/手工边在自动生成时保留 */
private Boolean locked;
/** 状态:0=有效 1=排除 */
private Integer status;
private String roleLabel;
private String exceptionCode;
private String exceptionMsg;
}这段代码明确证明了三件事:
- 当前关系实例是周期级数据;
- 当前关系类型只定义了主评;
- 异常与来源都作为正式字段落库。
三、两种主评规则:部门负责人或指定用户
当前主评规则只有两种:默认按员工所属部门负责人解析,或从 assessorParam 读取指定用户 ID。解析失败不会静默猜人,而是返回异常码,交由 HR 处理。
3.3 当前异常码
| 异常码 | 触发条件 | 建议处理 |
|---|---|---|
EMPLOYEE_NOT_FOUND | 被考核员工记录不存在 | 检查计划与员工档案 |
DEPT_MISSING | 员工未配置部门 | 完善员工组织归属 |
DEPT_LEADER_NOT_FOUND | 部门未配置负责人 | 补部门负责人或人工改派 |
DESIGNATED_USER_INVALID | 指定用户参数无法解析 | 修正规则参数 |
DESIGNATED_USER_NOT_FOUND | 指定用户不存在 | 更换有效用户 |
异常码面向程序筛选,异常说明面向 HR 阅读。
两者同时存在,异常池才能既可统计又可处置。
四、发布前预览:把失败提前到批次落库之前
4.1 预览不是“看看名单”
发布前预览是一次不落库的关系彩排。
它同时回答四个问题:
- 有多少员工匹配到绩效模板?
- 有多少员工未匹配模板?
- 能预生成多少条主评关系?
- 其中有多少条关系异常?
对应响应字段分别是:
matchedCountunmatchedCountrelationCountabnormalCount
除此之外,响应还带:
- 参与人明细
participants; - 关系明细
relations; - 是否需要目标确认
requireGoalConfirm; - 发布时是否自动确认
autoConfirmOnPublish。
4.2 为什么先匹配模板,再预览关系?
绩效周期不是把所有在职员工都直接塞进流程。
系统先判断员工是否匹配绩效模板。
只有匹配成功的员工,才进入主评关系预览。
因此:
unmatchedCount解决“谁没有考核内容”;abnormalCount解决“有考核内容但找不到谁来评”。
这两类异常不能混成一个数字。
前者通常要补模板或调整范围。
后者通常要补组织负责人、修规则或人工改派。
4.3 真实源码:逐人试算主评
下面是 previewPrimaryRelations 的核心路径。
public List<PerformancePeriodPreviewRespVO.RelationItem>
previewPrimaryRelations(Long schemeId, List<EmployeeDO> employees) {
PerformanceRelationRuleDO primaryRule = resolvePrimaryRule(schemeId);
Map<Long, DeptRespDTO> deptCache = new HashMap<>();
List<PerformancePeriodPreviewRespVO.RelationItem> rows = new ArrayList<>();
if (employees == null || employees.isEmpty()) {
return rows;
}
for (EmployeeDO employee : employees) {
AssessorResolveResult result =
resolveAssessor(primaryRule, employee, deptCache);
PerformancePeriodPreviewRespVO.RelationItem row =
new PerformancePeriodPreviewRespVO.RelationItem();
row.setAssesseeEmployeeId(employee.getId());
row.setAssesseeEmployeeName(employee.getName());
row.setAssessorUserId(result.assessorUserId());
row.setAssessorUserName(result.assessorUserName());
row.setRoleLabel("主评");
row.setExceptionCode(result.exceptionCode());
row.setExceptionMsg(result.exceptionMsg());
rows.add(row);
}
return rows;
}这里还做了一个朴素但有效的优化:
deptCache 在批次预览期间缓存部门信息。
同一部门有多名员工时,不必重复请求部门接口。
4.4 工作台如何呈现预览
草稿状态下,前端不会读取已落库关系。
它把预览响应中的 relations 映射为临时展示行:
- 临时负数 ID 仅用于表格行键;
roleLabel默认“主评”;source展示为自动;- 异常码非空的行进入异常池;
- 发布后才允许更换主评。

▲ 绩效批次工作台总览:草稿期先展示参与人数、关系数量与异常数量;发布动作建立在预览结果之上,避免“先启动、后补洞”。
4.5 发布门禁
当前前端至少要求:
草稿状态 && matchedCount > 0才允许点击发布。
如果没有员工匹配模板,按钮会给出明确提示:
“当前范围下无在职员工可参与考核,请调整公司/部门/人员状态后再发布。”
这比发布后生成一个空批次更可控。
五、L2 生成:自动关系允许失败,但失败必须留下原因
5.2 真实源码:生成与保留
下面代码来自 generateByPeriod,保留了生成、异常落库和回写计划三条主线。
public void generateByPeriod(Long periodId) {
PerformanceRelationRuleDO rule =
resolvePrimaryRule(validatePeriodExists(periodId).getSchemeId());
List<PerformancePlanDO> plans = performancePlanMapper.selectListByPeriodId(periodId);
Map<Long, DeptRespDTO> deptCache = new HashMap<>();
for (PerformancePlanDO plan : plans) {
PerformanceEvalRelationDO existed = performanceEvalRelationMapper
.selectByPeriodIdAndAssesseeEmployeeId(periodId, plan.getEmployeeId());
if (existed != null && shouldPreserve(existed)) {
fillPlanLeaderFromRelation(plan, existed);
continue;
}
EmployeeDO employee = employeeMapper.selectById(plan.getEmployeeId());
AssessorResolveResult result = resolveAssessor(rule, employee, deptCache);
PerformanceEvalRelationDO relation = new PerformanceEvalRelationDO()
.setPeriodId(periodId)
.setAssesseeEmployeeId(plan.getEmployeeId())
.setAssessorUserId(result.assessorUserId())
.setRelationType(RELATION_TYPE_PRIMARY)
.setSource(SOURCE_AUTO)
.setExceptionCode(result.exceptionCode())
.setExceptionMsg(result.exceptionMsg());
fillPlanLeaderFromRelation(plan, relation);
}
}5.3 为什么关系要回写计划?
独立关系表是主评治理入口。
但计划和评价单仍需要冗余主评字段:
- 列表查询不必每次联表;
- 评价单创建时可直接得到上级评办理人;
- BPM 启动变量可以从业务单据准备;
- 历史执行记录更容易还原。
因此,这不是“重复存储没设计好”。
它是一种有同步约束的执行冗余。
关键不在于是否冗余,而在于修改主评时必须同步。
六、重生成策略:为什么“尽量保留”比全量覆盖更安全?
6.1 三类需要保留的关系
shouldPreserve 的真实逻辑非常明确:locked=true,或来源为 SOURCE_MANUAL / SOURCE_IMPORT 时返回 true。
也就是说,以下关系不会被自动重生成覆盖:
locked=true的关系;source=MANUAL的关系;source=IMPORT的关系。
| 关系来源/状态 | 重生成行为 | 原因 |
|---|---|---|
| AUTO 且未锁定 | 允许重算 | 跟随当前规则与组织数据 |
| MANUAL | 保留 | 尊重 HR 明确修正 |
| IMPORT | 保留 | 尊重外部确认名单 |
| locked=true | 保留 | 显式冻结本周期关系 |
七、异常池:让“找不到主评”成为可处理工单
7.1 异常池不是错误日志
异常池是批次工作台内的业务处置区,不是开发人员看的服务器日志。
前端通过 displayRelations.filter((item) => !!item.exceptionCode) 把异常码非空的关系筛进异常池。
这让 HR 不需要理解堆栈或数据库。
HR 看到的是:
- 被考核人;
- 当前主评人;
- 角色;
- 来源;
- 异常码;
- 异常说明;
- 更换主评操作。

▲ 绩效评估关系列表:关系来源区分自动、手工与导入;异常码和异常说明帮助 HR 定位问题,发布后可通过“更换主评”完成修正。
7.2 异常池的运营顺序
建议 HR 按以下顺序处理:
- 先看
unmatchedCount,补绩效模板覆盖; - 再看
abnormalCount,判断组织配置还是规则参数问题; - 可修主数据的,优先补部门或负责人;
- 本周期必须立即推进的,直接人工更换主评;
- 修复后重新生成关系;
- 确认异常池归零或剩余异常已有明确处理意见;
- 再进入自评、上级评与校准运营。
7.3 为什么不自动找上级部门负责人?
当前真实代码没有实现“本部门无负责人就逐级向上找”的逻辑。
这不是遗漏描述,而是事实边界。
自动向上寻找看似聪明,实际可能产生新的歧义:
- 子部门绩效是否应该由事业部负责人评价?
- 空部门负责人是配置错误还是业务规则?
- 跨层级主评是否符合绩效制度?
- 上级部门负责人是否有权限查看被评人的全部指标?
在规则未明确前,进入异常池比擅自决定更安全。
八、人工改派:一次操作同步关系、计划、评价单与 BPM
8.1 改派不是只更新一列
更换主评人的真正难点是保持执行链一致。
如果只改 hrm_performance_eval_relation.assessor_user_id:
- 计划详情仍显示旧主管;
- 评价单仍记录旧主评;
- 当前办理人可能仍是旧人;
- BPM 变量仍可能把任务派给旧人。
所以改派必须是一条事务链。
8.2 真实源码:主评人同步
下面片段裁剪自 updatePrimaryAssessor,展示业务单据和流程变量同步。
performanceEvalRelationMapper.updateById(
new PerformanceEvalRelationDO()
.setId(relation.getId())
.setAssessorUserId(newAssessorUserId)
.setSource(SOURCE_MANUAL)
.setLocked(true)
.setExceptionCode(null)
.setExceptionMsg(null));
performancePlanMapper.updateById(new PerformancePlanDO()
.setId(plan.getId())
.setLeaderUserId(newAssessorUserId));
performanceAssessmentBillMapper.updateById(new PerformanceAssessmentBillDO()
.setId(bill.getId())
.setLeaderUserId(newAssessorUserId));
if (StringUtils.isNotBlank(bill.getMainProcessInstanceId())) {
processInstanceApi.updateProcessInstanceVariables(
bill.getMainProcessInstanceId(),
Map.of("leaderUserId", newAssessorUserId));
}真实实现还包含一个更细的条件:
如果评价单当前正处于 leader_review,或者状态为待上级评 20,系统还会同步:
currentAssigneeIdcurrentAssigneeName
这样工作台办理人与主评关系立即一致。
8.3 为什么 BPM 更新是“必要时尝试”?
代码仅在 mainProcessInstanceId 非空时更新流程变量。
并且流程变量更新放在 try/catch 中:
- 成功:BPM 的
leaderUserId与业务关系一致; - 失败:记录 warn 日志,不回滚已经完成的关系和业务表更新。
因此,准确表述应是:
更换主评会回写关系、计划与评价单;主流程已存在时,系统会尝试更新 BPM
leaderUserId。
不应写成“任何情况下 BPM 任务都必然自动改派成功”。
流程变量变化与已创建用户任务的实际候选人刷新,还受流程引擎节点状态和任务实现影响。
8.4 改派时机边界
当前服务允许在校准前修改关系。
以下状态禁止改边:
- 待校准
30; - 待结果确认
40; - 已完成及更后状态
>=50。
原因是:
主评责任已经完成后再换人,会让“实际评分人”和“关系表当前主评”不一致。
禁止改边是为了保护审计语义。
十三、技术亮点总结
| 设计点 | 实现方式 | 业务价值 |
|---|---|---|
| L1/L2 两层关系 | 方案规则 + 周期实例 | 兼顾复用与历史快照 |
| 单主评边界明确 | relationType=1 | 不夸大当前能力 |
| 两种主评规则 | 部门负责人 / 指定用户 | 覆盖层级组织与特殊评委 |
| 三类来源 | AUTO / MANUAL / IMPORT | 可审计、可制定覆盖策略 |
| 发布前四项预览 | matched/unmatched/relation/abnormal | 把失败提前暴露 |
| 显式异常池 | code + message + 独立 Tab | HR 可定位、可处置 |
| 重生成保护 | locked / MANUAL / IMPORT 保留 | 不覆盖人工确认结果 |
| 改派同步 | relation / plan / bill | 业务数据一致 |
| 节点办理人同步 | 上级评阶段更新 assignee | 防止待办仍指向旧人 |
| BPM 变量同步 | 必要时更新 leaderUserId | 流程候选人与业务关系对齐 |
| 校准后禁改边 | 状态门禁 | 保护历史责任链 |
| 部门缓存 | deptCache | 降低批量解析重复调用 |
技术栈锚点:
- 后端:Spring Boot 3.5、MyBatis-Plus、Flowable;
- 前端:Vue3、TypeScript、Vben Admin、Ant Design Vue;
- 业务位置:HRM 绩效方案、周期工作台、评估关系与评价单。
十四、快速体验
14.1 在线演示
地址:https://ruoyioffice.com/web/
账号:admin
密码:admin123
14.2 推荐体验路径
操作路径:HRM 人力资源 → 绩效管理 → 绩效周期 → 批次工作台
建议按以下顺序体验:
- 新建或打开一个草稿绩效周期;
- 进入批次工作台;
- 查看参与人匹配数量;
- 打开“评估关系”页签;
- 对照被评人、主评人、角色与异常说明;
- 查看未匹配模板与异常关系是否分开提示;
- 发布批次,让预览关系正式落库;
- 修改部门负责人后尝试重新生成自动关系;
- 选择一条关系执行“更换主评”;
- 再次重生成,观察手工锁定关系是否保留;
- 查看计划与评价单上的主评是否同步;
- 在待上级评阶段核对当前办理人。
14.3 源码仓库
| 平台 | 地址 | 适合场景 |
|---|---|---|
| GitHub | https://github.com/yuqing2026/ruoyi-office | 浏览源码、Issue 与 Star |
| GitCode | https://gitcode.com/zhouzhongyan/ruoyi-office | 国内网络访问 |
| Gitee | https://gitee.com/yqzy1688/ruoyi-office | 国内镜像与协作 |
说明:在线演示用于功能体验;不同版本的模块范围、源码授权、商业能力与持续维护政策,请以仓库说明和正式咨询结果为准。
十五、常见问题(FAQ)
绩效评估关系到底解决什么问题?
绩效评估关系解决“某个绩效周期里,谁负责评价谁”的执行问题。
它把主评人从临时组织查询结果,升级为周期级业务数据,支持生成、预览、异常、改派、锁定和同步。
当前是否已经支持完整 360 度评价?
没有。
当前真实落地的是 relationType=主评 的单主评关系,主评可来自部门负责人或指定用户。
完整的同级、下属、多角色、多权重 360 关系仍属于后续扩展方向。
部门没有负责人时,系统会自动找上级部门负责人吗?
当前不会。
系统会生成 DEPT_LEADER_NOT_FOUND 异常,并在工作台异常池展示,由 HR 补组织配置或人工更换主评。
重新生成关系会覆盖 HR 手工改派吗?
按当前 shouldPreserve 逻辑,已锁定、手工来源或导入来源的关系会被保留。
未锁定的自动关系允许按最新规则和组织数据重新计算。
更换主评后,绩效计划和流程会同步吗?
会同步计划与评价单的主评字段。
如果评价单处于上级评节点,还会同步当前办理人;主流程实例存在时,系统会尝试更新 BPM 变量 leaderUserId。
为什么校准后不能再更换主评?
因为主评责任已经完成。
校准后再改关系,会导致历史评分人与当前关系不一致,破坏审计语义,所以服务在待校准、待结果确认和完成阶段禁止改边。
结语:绩效系统先要把责任链做对
绩效系统的第一性问题,不是“用了 KPI 还是 OKR”,也不是“有几套评分公式”,而是:
每一位员工的评价责任,是否在流程启动前就已经明确。
这套设计用一条很务实的路径回答了这个问题:
- L1 方案规则定义通常由谁评;
- 发布前预览暴露模板与关系异常;
- L2 周期实例固化本批次主评;
- 异常池承接无法自动判断的情况;
- 手工改派锁定 HR 的最终决定;
- 计划、评价单与 BPM 同步保持执行一致;
- 状态门禁保护已经完成的责任链。
它没有假装一次性解决完整 360 多角色评价。
但它把最基础、也最容易卡住绩效运营的“谁评谁”做成了可执行闭环。
这种“规则模板 + 周期快照 + 异常显式化 + 人工兜底 + 执行链同步”的模式,也可以迁移到:
- 试用期转正评估;
- 人才盘点评委分配;
- 项目成员评价;
- 供应商考评;
- 能力测评;
- 述职评审。
如果你正在建设绩效系统,建议先问团队一个问题:
当主管调岗、部门负责人缺失、HR 临时换评委时,你们的系统能否准确解释“现在是谁评、为什么是他、改完后哪些地方同步了”?
欢迎在评论区分享你们遇到过的评审错派、主管变更和批次重生成问题。
💡 想要体验 RuoYi Office 的绩效批次工作台?
🌐 在线演示:https://ruoyioffice.com/web/(账号 admin / admin123)
📦 源码仓库:GitHub | GitCode | Gitee
💬 技术咨询:添加微信 17156169080,备注「RuoYi Office」
⭐ 如果本文对你设计绩效“谁评谁”有帮助,欢迎关注项目并给个 Star。
