flow-audit-report.md 17 KB

OKR 绩效系统流程诊断报告

日期:2026-06-18

严重程度图例

  • 严重:流程出错、权限漏洞、数据不一致,会影响真实上线使用。
  • 中等:边界场景、体验缺陷、性能或运维风险。
  • 轻微:口径说明、可维护性、性能余量或文案一致性。

严重问题

严重-1:前端自评评分口径与后端权威口径不一致

问题陈述:后端 PerformanceService.validateScoreParts 明确限制 OKR 为 0-40、KPI/工作成果为 0-60;上级打分页也按 OKR 40、工作成果 60 计算。但员工自评页曾按 OKR 60、工作成果 40 展示和计算。

证据

  • backend/src/main/java/com/emoon/okr/service/PerformanceService.java:328requireRange("OKR得分", okrScore, 0, 40)
  • backend/src/main/java/com/emoon/okr/service/PerformanceService.java:330requireRange("KPI得分", kpiScore, 0, 60)
  • frontend/src/views/scoring/ScoringEditView.vue:130:上级终评 OKR 满分 40。
  • frontend/src/views/scoring/ScoringEditView.vue:157:上级终评工作成果满分 60。
  • frontend/src/views/scoring/MyResultView.vue 自评表单是本轮重点修复对象。

影响:员工 OKR 自评满分时会向后端提交 60 分,触发后端 0-40 校验失败;即便未满分,也会造成自评与终评不可比。

建议修复方向:以后端为权威,前端所有自评、终评、结果展示统一为 OKR 40 + 工作成果 60。本轮已纳入直接修复,并增加前端评分契约检查脚本。

严重-2:结果页把分值占比误写成加权系数

问题陈述:系统真实计算是 OKR 分 + 工作成果分 + 加分 - 扣分,不是结果页展示的 OKR × 0.6 / 工作成果 × 0.4

证据

  • backend/src/main/java/com/emoon/okr/service/PerformanceService.java:319calculateTotal 直接相加。
  • frontend/src/views/scoring/MyResultView.vue 结果页是本轮重点修复对象。
  • frontend/src/views/scoring/ScoringEditView.vue:192 的 “OKR(40%) + 工作成果(60%)” 容易被理解为二次加权,也需要改为分值制描述。

影响:员工查看绩效结果时会误解评分算法,降低绩效系统可信度。

建议修复方向:结果页展示改为 “OKR X / 40”“工作成果 Y / 60”;评分预览文案改为 “OKR 40 分制 + 工作成果 60 分制 + 加分 - 扣分”。本轮已纳入直接修复。

严重-3:周期快照参与人中的离职或调岗人员可能卡死状态流转

问题陈述:周期发布后会生成参与人快照。进入执行或考评前,系统要求所有参与人均完成并通过 OKR、锁定 KPI。如果某个参与人在周期内离职、调岗或不再参与,但没有被豁免,周期可能无法推进。

证据

  • backend/src/main/java/com/emoon/okr/service/AssessmentPeriodService.java:169:流转检查读取参与人 ID。
  • backend/src/main/java/com/emoon/okr/service/AssessmentPeriodService.java:179:统计未提交或未通过 OKR 人数。
  • backend/src/main/java/com/emoon/okr/service/AssessmentPeriodService.java:189:统计未配置并锁定 KPI 人数。
  • backend/src/main/java/com/emoon/okr/service/AssessmentPeriodService.java:511:周期发布时生成参与人快照。

影响:真实公司运行中,单个离职或豁免人员就可能阻塞全公司周期。

建议修复方向:保留“参与人快照”的审计价值,但 HR/管理员必须能在参与人弹窗中把人员标记为豁免。本轮前端已提供树形参与人勾选/豁免入口;后续还应在流转检查文案中列出具体阻塞人员,而不只是人数。

中等问题

中等-1:评分权限缺少评分人在职状态校验

问题陈述:上级评分主要校验 evaluatorId == targetUser.superiorId,未在评分服务内显式校验评分人仍为有效在职用户。

证据

  • backend/src/main/java/com/emoon/okr/service/PerformanceService.java:83:只取目标用户并校验其直属上级。
  • backend/src/main/java/com/emoon/okr/service/PerformanceService.java:86:权限判断依赖 superiorId

影响:若离职或禁用账号的 JWT 仍有效,并且组织关系未清理,理论上存在继续评分风险。

建议修复方向:在评分、发布、确认等敏感操作中统一通过用户状态守卫,校验 actor 未删除、未禁用、未离职。若当前依赖登录态拦截,也应补服务层纵深防御。

本轮处理:已在上级评分与自评提交链路增加用户有效性守卫,拒绝 deleted=1DISABLEDLEFTINACTIVEdisabledAtleftAt 命中的用户继续提交绩效评分,并补充单元测试。

中等-2:下属评分列表存在 N+1 查询

问题陈述:直属上级查看下属评分列表时,对每个下属调用一次 getScoringPage,每次都会拉取 OKR、评分、反馈等数据。

证据

  • backend/src/main/java/com/emoon/okr/service/PerformanceService.java:222:查询下属列表。
  • backend/src/main/java/com/emoon/okr/service/PerformanceService.java:225:逐个调用 getScoringPage

影响:50 人内企业短期可接受,但部门 20 人以上会明显放大数据库查询次数。

建议修复方向:列表页使用轻量 DTO,批量查询下属用户、评分状态、最终评分和必要 OKR 摘要;详情页再加载完整证据。

中等-3:OKR 富集全表扫描用户和部门

问题陈述OkrService.enrichObjectives 为少量 OKR 富集用户名时拉取全部用户、全部部门。

证据

  • backend/src/main/java/com/emoon/okr/service/OkrService.java:584userMapper.selectList(null)
  • backend/src/main/java/com/emoon/okr/service/OkrService.java:586departmentMapper.selectList(null)

影响:50 人内影响有限,但属于典型可优化点,后续组织规模扩大后会浪费查询和内存。

建议修复方向:按 objectives、aligned objectives、source KR 涉及的 userId / departmentId 集合批量查询。

本轮处理:已把用户名和部门名富集从全表扫描改为按引用 ID 批量查询,并把 source KR / source objective 查询改为批量加载,补充单元测试防止回退到 selectList(null)

中等-4:考评阶段评分证据未在进入 ASSESSING 时冻结

问题陈述:进入考评阶段后,业务层禁止员工修改 OKR,但评分页仍读取实时 OKR 数据;归档时才写 snapshot。

证据

  • backend/src/main/java/com/emoon/okr/service/PerformanceService.java:159:评分页读取 okrService.getMyOkr 实时数据。
  • backend/src/main/java/com/emoon/okr/service/AssessmentPeriodService.java:533:归档时才执行 snapshotPeriod

影响:若管理员或脚本直接修改数据库,考评依据会变化。对真实绩效上线而言,证据冻结时间应早于评分。

建议修复方向:进入 ASSESSING 时创建评分证据快照;评分页优先读取快照,归档只负责最终归档副本。

严重-4:申诉受理后未联动改分,闭环断裂

问题陈述:员工申诉经 HR 处理为 ACCEPTED 后,系统只更新申诉记录状态,不修改原评分。员工申诉成功,但绩效结果仍是原分,必须 HR 另走 audit correction 流程手动改分。

证据

  • backend/src/main/java/com/emoon/okr/service/ScoreAppealService.java:89resolveAppealappeal.setStatus(status)setReplysetResolvedBy,无评分回写。
  • backend/src/main/java/com/emoon/okr/service/ScoreAppealService.java:93:更新后只发通知,未触发 PerformanceService 改分。
  • backend/src/main/java/com/emoon/okr/entity/ScoreAppeal.java:申诉记录关联 scoreId,但受理后无任何反向写入 performance_score

影响:申诉"成功"是名义上的,员工看不到分数变化,绩效系统可信度受损。HR 需记住"受理后还要去改分",容易遗漏,造成申诉已处理但分数不对的脏数据。

建议修复方向:明确制度规则——申诉受理后是否自动改分、还是仅作为 HR 改分凭据。建议至少在受理为 ACCEPTED 时:(a) 在 UI 上提示"需通过绩效修正流程调整分数";(b) 在申诉详情页提供直达 audit correction 的入口。是否自动改分涉及绩效制度,需业务确认,本轮不静默实现。

中等-5:定时任务向全表用户发送周期截止通知,含离职/禁用账号

问题陈述PeriodScheduler.checkPeriodDeadlines 在周期结束前 3 天,对所有用户(userMapper.selectList(null))逐一发通知,未过滤在职状态。

证据

  • backend/src/main/java/com/emoon/okr/schedule/PeriodScheduler.java:36List<SysUser> users = userMapper.selectList(null)
  • backend/src/main/java/com/emoon/okr/schedule/PeriodScheduler.java:37-42:遍历全部用户发 PERIOD_DEADLINE 通知。

影响:已离职、已禁用账号仍会收到通知(虽不登录,但通知表会写入无效数据);组织规模增长后每次扫描全表。

建议修复方向:仅向周期参与人快照中 status=ACTIVE 的成员发通知;或按 sys_user.status NOT IN (DISABLED, LEFT) 过滤。

中等-6:KR 滞后通知无去重,连续多天重复推送

问题陈述PeriodScheduler.checkKrLagging 每天 10:07 扫描所有执行中周期,对进度落后时间线 20% 以上的 KR 推送通知,不检查当天是否已推过、也不检查历史通知,连续多天会产生重复通知轰炸。

证据

  • backend/src/main/java/com/emoon/okr/schedule/PeriodScheduler.java:78-86:滞后即 publishEvent,无去重逻辑。
  • 无任何"上次通知时间"或"通知次数"字段记录。

影响:一条滞后 KR 在执行期可能累计推送数十条相同通知,淹没重要通知,员工对告警脱敏。

建议修复方向:对同一 KR 的滞后通知加冷却期(如 3 天内不重复),或在通知表增加 type+refId+userId 的当日去重校验。

中等-7:面谈记录重复发起会重置已回复状态,可能丢失上下文

问题陈述FeedbackService.saveInterviewNotes 在面谈已存在时,直接覆盖 interviewNotes/highlights/issues/improvements,并把状态强制重置回 PENDING_EMPLOYEE、清空 closedAt。若员工已回复(状态为 REPLIED)或已关闭,重复发起会清空员工反馈状态。

证据

  • backend/src/main/java/com/emoon/okr/service/FeedbackService.java:42-51:existing 分支无条件 setStatus(PENDING_EMPLOYEE)setClosedAt(null)
  • 员工 employeeFeedback 字段未被清空(保留),但状态回退会让员工误以为需重新回复。

影响:上级修正面谈记录时,可能无意中把已沟通闭环的面谈重新打开,员工收到"新的绩效面谈"通知造成困惑;闭环状态丢失。

建议修复方向:重复发起时区分场景——若已是 REPLIED/CLOSED,应二次确认或保留状态;或在已关闭的面谈上禁止覆盖,要求先重新打开。

中等-8:维度配置删除为逻辑删除,但未校验是否被周期引用

问题陈述DimensionController.delete 调用 service.delete(id),未校验该维度是否已被某周期的 KPI 配置引用。若维度被已锁定 KPI 引用,删除后历史评分依据的解释会缺失(虽然 KPI 配置有快照,但维度管理页会显示空)。

证据

  • backend/src/main/java/com/emoon/okr/controller/DimensionController.java:43-49:delete 仅校验角色,未校验引用。
  • backend/src/main/java/com/emoon/okr/service/DimensionService.java(delete 实现):逻辑删除,无引用检查。

影响:删除被引用维度后,管理员维度列表不完整;虽然历史评分有 KPI 快照保护,但维度维护页与历史数据脱节。

建议修复方向:删除前校验是否有 KPI 配置引用,有则提示"已被 N 个周期引用,建议禁用而非删除",或改为软禁用。

轻微问题

轻微-1:等级临界值规则需要前端提示或制度说明

问题陈述:后端对总分四舍五入到 1 位小数后再判定等级,临界值附近可能引发解释争议。

证据

  • backend/src/main/java/com/emoon/okr/service/PerformanceService.java:319:总分保留 1 位小数。
  • backend/src/main/java/com/emoon/okr/enums/Grade.java:等级阈值为 S≥90、A≥75、B≥60。

影响:89.94 与 89.95 的等级差异会被放大。

建议修复方向:结果页展示“等级按四舍五入后一位小数判定”;或显示距下一等级差值。

轻微-2:UI 风格 token 使用不一致

问题陈述:多个核心页存在硬编码颜色、渐变徽章、emoji、营销式副标题,与内部医疗科技系统的专业感冲突。

证据

  • frontend/src/views/okr/MyOkrView.vue 存在营销口号、渐变徽章和大量硬编码色。
  • frontend/src/views/scoring/MyResultView.vue 存在 emoji、Material 色等级圆环和错误评分展示。
  • frontend/src/components/MainLayout.vue 侧边栏选中态和角色标签仍偏强视觉。

影响:系统看起来像生成式原型,不像真实企业内部工具。

建议修复方向:本轮按医疗科技样板页方案改造 LoginView、MainLayout、MyOkrView、ScoringEditView、MyResultView,并扩展 theme.css token。

已验证正确的环节

  • 后端总分权威口径清晰:OKR 0-40 + KPI/工作成果 0-60 + 加分 - 扣分。
  • 绩效最终结果发布前,对员工本人隐藏未发布 FINAL,避免提前泄露。
  • FINAL 发布后需要员工确认,归档前检查未确认评分。
  • 操作日志与敏感操作审计已有基础覆盖。
  • 数据库已有版本化迁移 runner,初始化与兼容迁移职责分离。

附录 A:完整流程时序图

sequenceDiagram
  participant HR as HR/管理员
  participant CEO as CEO/高层
  participant Lead as 部门负责人
  participant Emp as 员工
  participant Sys as 系统

  HR->>Sys: 创建考核周期 DRAFT
  HR->>Sys: 发布 OKR 对齐
  Sys->>Sys: 生成参与人快照(默认全员)
  CEO->>Sys: 提交公司级 OKR
  Lead->>Sys: 对齐上级 OKR 并提交部门 OKR
  Emp->>Sys: 对齐上级 OKR 并提交个人 OKR
  Lead->>Sys: 审核下属 OKR
  HR->>Sys: 检查 OKR/KPI 就绪并进入 EXECUTING
  Emp->>Sys: 周期内更新 KR 进度
  HR->>Sys: 进入 ASSESSING
  Emp->>Sys: 提交自评
  Lead->>Sys: 上级终评并生成 FINAL
  HR->>Sys: 发布绩效结果
  Emp->>Sys: 确认或申诉
  HR->>Sys: 处理申诉/修正
  HR->>Sys: 归档周期

附录 B:周期状态机图

stateDiagram-v2
  [*] --> DRAFT
  DRAFT --> OKR_ALIGN: 发布 OKR 对齐 / 生成参与人快照
  OKR_ALIGN --> EXECUTING: 所有 ACTIVE 参与人 OKR 通过且 KPI 锁定
  EXECUTING --> OKR_ALIGN: 管理员回退
  EXECUTING --> ASSESSING: 进入考评
  EXECUTING --> ARCHIVED: 直接归档(需满足归档检查)
  ASSESSING --> ARCHIVED: 最终评分发布且员工确认
  ARCHIVED --> [*]

附录 C:评分口径对照表

项目 权威口径 本轮处理
OKR 得分 0-40 前端自评、终评、结果展示统一到 40 分制
工作成果/KPI 0-60 前端自评、终评、结果展示统一到 60 分制
加分 0-20(当前 UI 控制为 0-5) 后端允许 20,UI 后续可按制度再收紧或放开
扣分 0-20 后端权威校验
总分 0-100 直接相加,不做二次权重
等级 S≥90 / A≥75 / B≥60 / C<60 以保留 1 位小数后的总分判定

附录 D:本轮修复闭环

为可审计起见,本轮把诊断中发现的高优问题分两批处理:评分口径为确定性 bug,直接修复;权限守卫与性能优化为诊断建议中可安全落地的项,一并修复并附测试。其余涉及制度或数据模型的项(严重-3 参与人豁免、严重-4 申诉改分、中等-4 证据快照)需业务确认规则,列为后续高优,本轮不静默实现。

批次 1:诊断报告项(任务1 本应仅报告,但含确定性 bug)

报告编号 问题 本轮处理 验证
严重-1 自评口径 OKR 60 与后端 40 冲突 直接修复:前端自评/终评/结果统一 OKR 40 + 工作 60 verify:scoring 通过
严重-2 结果页误写 ×0.6/×0.4 加权 直接修复:改分值制直述 verify:scoring 通过

批次 2:诊断建议中可安全落地的后端修复(任务1 范围外,单独说明)

报告编号 问题 本轮处理 验证
中等-1 评分权限缺在职校验 已修复:自评/终评加 ensureActiveUser 守卫 PerformanceServiceTest
中等-3 enrich 全表扫描用户/部门 已修复:按引用 ID 批量查询 OkrServiceTest
—(验证暴露) SQLite WAL 下通知锁 已修复@TransactionalEventListener(AFTER_COMMIT) + 重试 NotificationEventListenerTest

列为后续高优(需业务确认规则,本轮不实现)

报告编号 问题 阻塞原因
严重-3 参与人豁免机制 需定义豁免规则与状态机回退语义
严重-4 申诉受理后改分 需定义自动改分 vs 凭据改分的制度规则
中等-2 下属评分 N+1 50 人内可接受,需 DTO 重构,单独排期
中等-4 进入 ASSESSING 冻结证据快照 改数据模型,需迁移方案
中等-5/6/7/8 定时任务去重、面谈防覆盖、维度引用校验 边界场景,需制度与优先级确认