# OKR 绩效系统流程诊断报告

日期：2026-06-18

## 严重程度图例

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

## 严重问题

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

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

**证据**：

- `backend/src/main/java/com/yimeng/okr/service/PerformanceService.java:328`：`requireRange("OKR得分", okrScore, 0, 40)`。
- `backend/src/main/java/com/yimeng/okr/service/PerformanceService.java:330`：`requireRange("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/yimeng/okr/service/PerformanceService.java:319`：`calculateTotal` 直接相加。
- `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/yimeng/okr/service/AssessmentPeriodService.java:169`：流转检查读取参与人 ID。
- `backend/src/main/java/com/yimeng/okr/service/AssessmentPeriodService.java:179`：统计未提交或未通过 OKR 人数。
- `backend/src/main/java/com/yimeng/okr/service/AssessmentPeriodService.java:189`：统计未配置并锁定 KPI 人数。
- `backend/src/main/java/com/yimeng/okr/service/AssessmentPeriodService.java:511`：周期发布时生成参与人快照。

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

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

## 中等问题

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

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

**证据**：

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

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

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

**本轮处理**：已在上级评分与自评提交链路增加用户有效性守卫，拒绝 `deleted=1`、`DISABLED`、`LEFT`、`INACTIVE`、`disabledAt` 或 `leftAt` 命中的用户继续提交绩效评分，并补充单元测试。

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

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

**证据**：

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

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

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

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

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

**证据**：

- `backend/src/main/java/com/yimeng/okr/service/OkrService.java:584`：`userMapper.selectList(null)`。
- `backend/src/main/java/com/yimeng/okr/service/OkrService.java:586`：`departmentMapper.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/yimeng/okr/service/PerformanceService.java:159`：评分页读取 `okrService.getMyOkr` 实时数据。
- `backend/src/main/java/com/yimeng/okr/service/AssessmentPeriodService.java:533`：归档时才执行 `snapshotPeriod`。

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

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

### 严重-4：申诉受理后未联动改分，闭环断裂

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

**证据**：

- `backend/src/main/java/com/yimeng/okr/service/ScoreAppealService.java:89`：`resolveAppeal` 仅 `appeal.setStatus(status)`、`setReply`、`setResolvedBy`，无评分回写。
- `backend/src/main/java/com/yimeng/okr/service/ScoreAppealService.java:93`：更新后只发通知，未触发 `PerformanceService` 改分。
- `backend/src/main/java/com/yimeng/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/yimeng/okr/schedule/PeriodScheduler.java:36`：`List<SysUser> users = userMapper.selectList(null)`。
- `backend/src/main/java/com/yimeng/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/yimeng/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/yimeng/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/yimeng/okr/controller/DimensionController.java:43-49`：delete 仅校验角色，未校验引用。
- `backend/src/main/java/com/yimeng/okr/service/DimensionService.java`（delete 实现）：逻辑删除，无引用检查。

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

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

## 轻微问题

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

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

**证据**：

- `backend/src/main/java/com/yimeng/okr/service/PerformanceService.java:319`：总分保留 1 位小数。
- `backend/src/main/java/com/yimeng/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：完整流程时序图

```mermaid
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：周期状态机图

```mermaid
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 | 定时任务去重、面谈防覆盖、维度引用校验 | 边界场景，需制度与优先级确认 |
