# 绩效管理模块 - 测试场景文档

> **module**: performance
> **doc_type**: TestScenarios
> **status**: Active
> **owner**: FFOA 开发团队
> **upstream_docs**: 01-prd.md, 04-state-machine.md, 07-api.md, 08-error-codes.md
> **last_verified**: 2026-03-20

---

## 场景总览

### 周期管理

| 编号 | 用例 | 类型 | 优先级 | 入口 | 预期结果 |
|------|------|------|--------|------|----------|
| TC-CYCLE-001 | 周期列表加载 | E2E | P0 | `/performance/cycles` | 周期列表正常显示 |
| TC-CYCLE-002 | 创建周期 | API | P0 | `POST /cycles` | 201, status=DRAFT |
| TC-CYCLE-003 | 发布周期 | API | P0 | `POST /cycles/:id/publish` | 200, status 从 DRAFT 变为 GOAL_SETTING |
| TC-CYCLE-004 | 非法阶段推进 | API | P0 | `POST /cycles/:id/start-execution` | 409, PERF_CYCLE_004 |
| TC-CYCLE-005 | 日期冲突校验 | API | P1 | `POST /cycles` | 400, PERF_CYCLE_003 |
| TC-CYCLE-006 | 启动执行 | API | P1 | `POST /cycles/:id/start-execution` | 200, status=IN_PROGRESS |
| TC-CYCLE-007 | 非 HR 无权创建 | API | P1 | `POST /cycles` | 403, PERF_COMMON_002 |
| TC-CYCLE-008 | 删除非草稿周期 | API | P2 | `DELETE /cycles/:id` | 409, PERF_CYCLE_005 |
| TC-CYCLE-009 | 周期归档 | API | P1 | `POST /cycles/:id/archive` | 200, status=ARCHIVED |
| TC-CYCLE-010 | 重复阶段推进（幂等性） | API | P1 | `POST /cycles/:id/publish`（已发布周期重复调用） | 409, PERF_COMMON_006 或幂等返回当前状态 |

### KPI 管理

| 编号 | 用例 | 类型 | 优先级 | 入口 | 预期结果 |
|------|------|------|--------|------|----------|
| TC-KPI-001 | 创建 KPI | API | P0 | `POST /kpi/assignments` | 201, KPI 创建成功 |
| TC-KPI-002 | 权重不等于 100% 时提交 | API | P0 | `POST /kpi/assignments/submit-all` | 400, PERF_KPI_004 |
| TC-KPI-003 | 依赖未确认时提交 | API | P0 | `POST /kpi/assignments/submit-all` | 409, PERF_KPI_018 |
| TC-KPI-004 | 经理审批通过 | API | P0 | `POST /kpi/assignments/:id/approve` | 200, status=APPROVED |
| TC-KPI-005 | 经理驳回 | API | P0 | `POST /kpi/assignments/:id/reject` | 200, status=REJECTED |
| TC-KPI-006 | 我的 KPI 列表加载 | E2E | P0 | `/performance/kpi` | KPI 列表正常显示 |
| TC-KPI-007 | 行内创建 KPI | E2E | P1 | `/performance/kpi` | KPI 创建成功 |
| TC-KPI-008 | 依赖方确认 KPI | E2E | P1 | `/performance/kpi` | 依赖确认状态变为已确认（业务规则推导） |
| TC-KPI-010 | KPI 名称重复 | API | P2 | `POST /kpi/assignments` | 400, PERF_KPI_014 |

### 评估

| 编号 | 用例 | 类型 | 优先级 | 入口 | 预期结果 |
|------|------|------|--------|------|----------|
| TC-EVAL-001 | 非 EVALUATING 阶段自评 | API | P0 | `POST /kpi/assessments/:id/self-evaluate` | 409, PERF_CYCLE_004 |
| TC-EVAL-002 | 员工自评提交 | API | P0 | `POST /kpi/assessments/:id/self-evaluate` | 200, status=SELF_EVALUATED |
| TC-EVAL-003 | 经理评估提交 | API | P0 | `POST /kpi/assessments/:id/manager-evaluate` | 200, status=MANAGER_EVALUATED |
| TC-EVAL-004 | 员工自评页面 | E2E | P0 | `/performance/kpi` | 自评保存成功 |
| TC-EVAL-005 | 经理评分页面 | E2E | P0 | `/performance/kpi/team` | 经理评分保存成功 |
| TC-EVAL-006 | 自评已提交不可重复 | API | P1 | `POST /kpi/assessments/:id/self-evaluate` | 400, PERF_KPI_007 |
| TC-EVAL-007 | 未自评经理不可提交 | API | P1 | `POST /kpi/assessments/:id/manager-evaluate` | 409, PERF_KPI_008 |
| TC-EVAL-008 | 评分超出范围 | API | P2 | `POST /kpi/assessments/:id/self-evaluate` | 400, PERF_KPI_009 |
| TC-EVAL-009 | 整体自评评语保存 | API | P0 | `PATCH /results/overall-comment` | 200, selfOverallComment 保存成功 |
| TC-EVAL-010 | 整体经理评语保存 | API | P0 | `PATCH /results/overall-comment` | 200, managerOverallComment 保存成功 |
| TC-EVAL-011 | 整体评语读取 | API | P1 | `GET /results/overall-comment?cycleId=&employeeId=` | 200, 返回 selfOverallComment 和 managerOverallComment |

### 校准

| 编号 | 用例 | 类型 | 优先级 | 入口 | 预期结果 |
|------|------|------|--------|------|----------|
| TC-CAL-001 | 调整等级（必须填原因） | API | P0 | `POST /calibration/adjust` | 200, 等级更新并生成日志 |
| TC-CAL-002 | 不填原因调整 | API | P0 | `POST /calibration/adjust` | 400, PERF_CAL_003 |
| TC-CAL-003 | 校准列表加载 | E2E | P0 | `/performance/calibration` | 列表正常显示 |
| TC-CAL-004 | 查看分布 | E2E | P0 | `/performance/calibration/[id]` | 分布正常渲染 |
| TC-CAL-005 | 非 CALIBRATING 阶段调整 | API | P1 | `POST /calibration/adjust` | 409, PERF_CYCLE_004 |
| TC-CAL-006 | 无权校准 | API | P1 | `POST /calibration/adjust` | 403, PERF_CAL_005 |

### 360 评估

| 编号 | 用例 | 类型 | 优先级 | 入口 | 预期结果 |
|------|------|------|--------|------|----------|
| TC-360-001 | 配置评估人 | API | P0 | `POST /360/evaluations/:id/evaluators` | 201, 评估关系创建成功 |
| TC-360-002 | 同一评估者重复提交 | API | P0 | `POST /360/tasks/:id/submit` | 409, PERF_E360_015 |
| TC-360-003 | 评估完成后自动可见 | API | P0 | `GET /360/evaluations/:id/results` | 200, 达到最低提交数后结果自动可见 |
| TC-360-004 | 评估列表加载 | E2E | P0 | `/performance/360` | 列表正常显示 |
| TC-360-005 | 提交问卷 | E2E | P0 | `/performance/360/[id]` | 问卷提交成功 |
| TC-360-006 | 查看结果汇总 | E2E | P0 | `/performance/360/[id]` | 汇总正确显示 |
| TC-360-007 | 创建评估 | E2E | P1 | `/performance/360` | 创建成功 |
| TC-360-008 | 截止日期校验 | API | P1 | `POST /360/tasks/:id/submit` | 400, PERF_E360_006 |
| TC-360-009 | 同级评人数校验 | API | P1 | `POST /360/evaluations` | 400, PERF_E360_004 |
| TC-360-010 | 评估关系已存在 | API | P2 | `POST /360/evaluations/:id/evaluators` | 400, PERF_E360_013 |

### 绩效结果

| 编号 | 用例 | 类型 | 优先级 | 入口 | 预期结果 |
|------|------|------|--------|------|----------|
| TC-RES-001 | 发布结果 | API | P0 | `POST /cycles/:id/start-confirming` | 200, 周期进入 CONFIRMING |
| TC-RES-002 | 确认结果 | API | P0 | `POST /results/my/:id/confirm` | 200, confirmStatus=CONFIRMED |
| TC-RES-003 | 申诉（未填原因） | API | P0 | `POST /results/my/:id/appeal` | 400, PERF_RESULT_006 |
| TC-RES-003a | 申诉（原因过短 <10字符） | API | P1 | `POST /results/my/:id/appeal` | 400, PERF_RESULT_006 |
| TC-RES-004 | 结果管理页加载 | E2E | P0 | `/performance/results/admin` | 列表正常显示 |
| TC-RES-005 | 员工查看结果 | E2E | P0 | `/performance` | 显示最终等级和得分 |
| TC-RES-006 | 员工确认结果 | E2E | P0 | `/performance` | 确认状态更新 |
| TC-RES-007 | 申诉（填写原因） | API | P1 | `POST /results/my/:id/appeal` | 200, confirmStatus=APPEALED |
| TC-RES-008 | 非 CONFIRMING 阶段确认 | API | P1 | `POST /results/my/:id/confirm` | 409, PERF_CYCLE_004 |

### 统计分析

| 编号 | 用例 | 类型 | 优先级 | 入口 | 预期结果 |
|------|------|------|--------|------|----------|
| TC-STAT-001 | 统计分析页面加载 | E2E | P0 | `/performance/analytics`（页面入口为 analytics，后端接口为 /reports/*） | 页面正常渲染 |
| TC-STAT-002 | 部门绩效报表 | API | P1 | `GET /reports/department/:deptId` | 200, 返回部门等级分布数据 |
| TC-STAT-003 | 公司绩效报表 | API | P1 | `GET /reports/company` | 200, 返回公司整体等级分布数据 |
| TC-STAT-004 | 非 HR 无权查看报表 | API | P1 | `GET /reports/department/:deptId` | 403, PERF_REPORT_002 |

### 战略目标

| 编号 | 用例 | 类型 | 优先级 | 入口 | 预期结果 |
|------|------|------|--------|------|----------|
| TC-STRAT-001 | 战略目标列表加载 | E2E | P0 | `/performance/strategic-objectives` | 列表正常显示 |
| TC-STRAT-002 | 创建战略目标 | API | P1 | `POST /strategic-objectives` | 201, 目标创建成功 |
| TC-STRAT-003 | 行内编辑目标 | E2E | P1 | `/performance/strategic-objectives` | 编辑保存成功 |
| TC-STRAT-004 | 分配目标到部门/员工 | API | P1 | `POST /strategic-objectives/:id/assign` | 200, 分配成功 |
| TC-STRAT-005 | 非 HR 无权管理战略目标 | API | P1 | `POST /strategic-objectives` | 403, PERF_COMMON_002 |

### 等级配置与模板

| 编号 | 用例 | 类型 | 优先级 | 入口 | 预期结果 |
|------|------|------|--------|------|----------|
| TC-CONF-001 | 等级配置列表加载 | E2E | P0 | `/performance/settings/grades` | 列表正常显示 |
| TC-CONF-002 | 创建等级配置 | E2E | P1 | `/performance/settings/grades` | 创建成功 |
| TC-CONF-003 | 设置默认等级配置 | E2E | P1 | `/performance/settings/grades` | 默认配置更新 |
| TC-CONF-004 | 360 模板列表加载 | E2E | P0 | `/performance/settings/360-templates` | 列表正常显示 |
| TC-CONF-005 | 创建 360 模板 | E2E | P1 | `/performance/settings/360-templates` | 模板创建成功 |

### 权限控制

| 编号 | 用例 | 类型 | 优先级 | 入口 | 预期结果 |
|------|------|------|--------|------|----------|
| TC-AUTH-001 | 员工访问管理接口 | API | P0 | `POST /cycles` | 403, PERF_COMMON_002 |
| TC-AUTH-002 | 非经理审批 KPI | API | P0 | `POST /kpi/assignments/:id/approve` | 403, PERF_KPI_010 |
| TC-AUTH-003 | 跨组织访问 | API | P0 | `GET /kpi/assignments/team` | 403, PERF_COMMON_007 |
| TC-AUTH-004 | 员工仅见本人 KPI | E2E | P0 | `/performance/kpi` | 只显示本人数据 |
| TC-AUTH-005 | 经理可见团队 KPI | E2E | P0 | `/performance/kpi/team` | 可见下属数据 |
| TC-AUTH-006 | HR 可见管理菜单 | E2E | P1 | `/performance/admin` | 管理菜单完整显示 |
| TC-AUTH-007 | 非 HR 无权管理周期 | E2E | P1 | `/performance/cycles` | 无创建权限或返回无权限 |
| TC-AUTH-008 | 非 HR 无权校准 | E2E | P1 | `/performance/calibration` | 无访问权限 |

---

## 场景详情（P0）

### 周期管理

#### TC-CYCLE-002: 创建周期

| 项目 | 内容 |
|------|------|
| 前置条件 | HR 用户已登录；无日期冲突的已有周期 |
| 操作 | `POST /api/v1/performance/cycles` |
| 输入 | `{ "name": "2026-H1 绩效周期", "type": "SEMI_ANNUAL", "startDate": "2026-01-01", "endDate": "2026-06-30", "organizationId": "<orgId>" }` |
| 期望输出 | `201, body.data.status === "DRAFT"` |

#### TC-CYCLE-003: 发布周期

| 项目 | 内容 |
|------|------|
| 前置条件 | 存在 status=DRAFT 的周期；必填字段完整 |
| 操作 | `POST /api/v1/performance/cycles/:id/publish` |
| 输入 | 无 body（路径参数传 cycleId） |
| 期望输出 | `200, body.data.status === "GOAL_SETTING"` |

#### TC-CYCLE-004: 非法阶段推进

| 项目 | 内容 |
|------|------|
| 前置条件 | 存在 status=DRAFT 的周期（未经过 publish） |
| 操作 | `POST /api/v1/performance/cycles/:id/start-execution` |
| 输入 | 无 body |
| 期望输出 | `409, body.error.code === "PERF_CYCLE_004"` |

### KPI 管理

#### TC-KPI-001: 创建 KPI

| 项目 | 内容 |
|------|------|
| 前置条件 | 周期处于 GOAL_SETTING；员工已登录 |
| 操作 | `POST /api/v1/performance/kpi/assignments` |
| 输入 | `{ "cycleId": "<cycleId>", "name": "代码质量", "weight": 30, "targetValue": "90%", "description": "代码质量达标率目标" }` |
| 期望输出 | `201, body.data.id 存在, body.data.status === "DRAFT"` |

#### TC-KPI-002: 权重不等于 100% 时提交

| 项目 | 内容 |
|------|------|
| 前置条件 | 周期处于 GOAL_SETTING；员工有 KPI 但权重合计为 80% |
| 操作 | `POST /api/v1/performance/kpi/assignments/submit-all` |
| 输入 | `{ "cycleId": "<cycleId>" }` |
| 期望输出 | `400, body.error.code === "PERF_KPI_004"` |

#### TC-KPI-003: 依赖未确认时提交

| 项目 | 内容 |
|------|------|
| 前置条件 | 周期处于 GOAL_SETTING；员工有 KPI 且存在未确认的 KpiDependency 记录；权重合计 100% |
| 操作 | `POST /api/v1/performance/kpi/assignments/submit-all` |
| 输入 | `{ "cycleId": "<cycleId>" }` |
| 期望输出 | `409, body.error.code === "PERF_KPI_018"` |

#### TC-KPI-004: 经理审批通过

| 项目 | 内容 |
|------|------|
| 前置条件 | 员工已提交 KPI（status=SUBMITTED）；经理已登录且为该员工经理（manager_id） |
| 操作 | `POST /api/v1/performance/kpi/assignments/:id/approve` |
| 输入 | 无 body（路径参数传 assignmentId） |
| 期望输出 | `200, body.data.status === "APPROVED"` |

#### TC-KPI-005: 经理驳回

| 项目 | 内容 |
|------|------|
| 前置条件 | 员工已提交 KPI（status=SUBMITTED）；经理已登录 |
| 操作 | `POST /api/v1/performance/kpi/assignments/:id/reject` |
| 输入 | `{ "reason": "目标值设定过低，请重新调整" }` |
| 期望输出 | `200, body.data.status === "REJECTED"` |

### 评估

#### TC-EVAL-001: 非 EVALUATING 阶段自评

| 项目 | 内容 |
|------|------|
| 前置条件 | 周期处于 GOAL_SETTING 或 IN_PROGRESS（非 EVALUATING）；员工已登录 |
| 操作 | `POST /api/v1/performance/kpi/assessments/:id/self-evaluate` |
| 输入 | `{ "selfScore": 85, "selfComment": "完成情况良好" }` |
| 期望输出 | `409, body.error.code === "PERF_CYCLE_004"` |

#### TC-EVAL-002: 员工自评提交

| 项目 | 内容 |
|------|------|
| 前置条件 | 周期处于 EVALUATING；KPI assessment 状态为 PENDING；员工已登录 |
| 操作 | `POST /api/v1/performance/kpi/assessments/:id/self-evaluate` |
| 输入 | `{ "selfScore": 85, "selfComment": "按时完成所有目标" }` |
| 期望输出 | `200, body.data.status === "SELF_EVALUATED"` |

#### TC-EVAL-003: 经理评估提交

| 项目 | 内容 |
|------|------|
| 前置条件 | 周期处于 EVALUATING；KPI assessment 状态为 SELF_EVALUATED；经理已登录 |
| 操作 | `POST /api/v1/performance/kpi/assessments/:id/manager-evaluate` |
| 输入 | `{ "managerScore": 80, "managerComment": "整体表现良好，部分指标可进一步提升" }` |
| 期望输出 | `200, body.data.status === "MANAGER_EVALUATED"` |

#### TC-EVAL-009: 整体自评评语保存

| 项目 | 内容 |
|------|------|
| 前置条件 | 周期处于 EVALUATING；员工已登录 |
| 操作 | `PATCH /api/v1/performance/results/overall-comment` |
| 输入 | `{ "cycleId": "<cycleId>", "employeeId": "<employeeId>", "selfOverallComment": "本周期整体完成情况良好，超额达成核心 KPI" }` |
| 期望输出 | `200, 评语保存成功` |

#### TC-EVAL-010: 整体经理评语保存

| 项目 | 内容 |
|------|------|
| 前置条件 | 周期处于 EVALUATING；经理已登录；目标员工已完成自评 |
| 操作 | `PATCH /api/v1/performance/results/overall-comment` |
| 输入 | `{ "cycleId": "<cycleId>", "employeeId": "<targetUserId>", "managerOverallComment": "该员工本周期表现突出，建议重点培养" }` |
| 期望输出 | `200, 评语保存成功` |

### 校准

#### TC-CAL-001: 调整等级（必须填原因）

| 项目 | 内容 |
|------|------|
| 前置条件 | 周期处于 CALIBRATING；HR 已登录；目标员工有绩效结果 |
| 操作 | `POST /api/v1/performance/calibration/adjust` |
| 输入 | `{ "resultId": "<resultId>", "newGradeCode": "A", "reason": "综合考虑项目贡献后上调" }` |
| 期望输出 | `200, 等级已更新, 校准日志已生成` |

#### TC-CAL-002: 不填原因调整

| 项目 | 内容 |
|------|------|
| 前置条件 | 周期处于 CALIBRATING；HR 已登录 |
| 操作 | `POST /api/v1/performance/calibration/adjust` |
| 输入 | `{ "resultId": "<resultId>", "newGradeCode": "A", "reason": "" }` |
| 期望输出 | `400, body.error.code === "PERF_CAL_003"` |

### 360 评估

#### TC-360-001: 配置评估人

| 项目 | 内容 |
|------|------|
| 前置条件 | 存在 status=DRAFT 的 360 评估；HR/经理已登录 |
| 操作 | `POST /api/v1/performance/360/evaluations/:id/evaluators` |
| 输入 | `{ "evaluatorId": "<userId>", "relationType": "PEER" }` |
| 期望输出 | `201, 评估关系创建成功` |

#### TC-360-002: 同一评估者重复提交

| 项目 | 内容 |
|------|------|
| 前置条件 | 360 评估处于 IN_PROGRESS/COLLECTING；评估者已提交过该任务（task status=SUBMITTED） |
| 操作 | `POST /api/v1/performance/360/tasks/:id/submit` |
| 输入 | `{ "scores": [{ "dimensionId": "<dimId>", "score": 4, "comment": "表现优秀" }] }` |
| 期望输出 | `409, body.error.code === "PERF_E360_015"` |

#### TC-360-003: 评估完成后自动可见

| 项目 | 内容 |
|------|------|
| 前置条件 | 360 评估处于 COLLECTING；达到最低提交数后系统自动标记 COMPLETED |
| 操作 | `GET /api/v1/performance/360/evaluations/:id/results` |
| 输入 | 无 body |
| 期望输出 | `200, body.data 包含汇总评分; 评估 status === "COMPLETED"` |

### 绩效结果

#### TC-RES-001: 发布结果

| 项目 | 内容 |
|------|------|
| 前置条件 | 周期处于 CALIBRATING；校准已完成；HR 已登录 |
| 操作 | `POST /api/v1/performance/cycles/:id/start-confirming` |
| 输入 | 无 body |
| 期望输出 | `200, 周期 status === "CONFIRMING"` |

#### TC-RES-002: 确认结果

| 项目 | 内容 |
|------|------|
| 前置条件 | 周期处于 CONFIRMING；员工有已发布的绩效结果（confirmStatus=PENDING）；员工已登录 |
| 操作 | `POST /api/v1/performance/results/my/:id/confirm` |
| 输入 | 无 body |
| 期望输出 | `200, body.data.confirmStatus === "CONFIRMED"` |

#### TC-RES-003: 申诉（未填原因）

| 项目 | 内容 |
|------|------|
| 前置条件 | 周期处于 CONFIRMING；员工有已发布的绩效结果（confirmStatus=PENDING）；员工已登录 |
| 操作 | `POST /api/v1/performance/results/my/:id/appeal` |
| 输入 | `{ "reason": "" }` |
| 期望输出 | `400, body.error.code === "PERF_RESULT_006"` |

### 权限控制

#### TC-AUTH-001: 员工访问管理接口

| 项目 | 内容 |
|------|------|
| 前置条件 | 使用 Employee 角色账号登录（如 test.dev1） |
| 操作 | `POST /api/v1/performance/cycles` |
| 输入 | `{ "name": "测试周期", "type": "ANNUAL", "startDate": "2026-01-01", "endDate": "2026-12-31" }` |
| 期望输出 | `403, body.error.code === "PERF_COMMON_002"` |

#### TC-AUTH-002: 非经理审批 KPI

| 项目 | 内容 |
|------|------|
| 前置条件 | 使用非经理账号登录；目标 KPI 处于 SUBMITTED 状态 |
| 操作 | `POST /api/v1/performance/kpi/assignments/:id/approve` |
| 输入 | 无 body |
| 期望输出 | `403, body.error.code === "PERF_KPI_010"` |

#### TC-AUTH-003: 跨组织访问

| 项目 | 内容 |
|------|------|
| 前置条件 | 使用组织 A 的经理账号登录；请求组织 B 的团队 KPI 数据 |
| 操作 | `GET /api/v1/performance/kpi/assignments/team?organizationId=<orgB_Id>` |
| 输入 | 无 body（query 参数指定目标组织） |
| 期望输出 | `403, body.error.code === "PERF_COMMON_007"` |

---

## 当前版本排除范围

以下能力保留为后续版本或历史接口，不纳入当前主测试口径：

- 绩效面谈（INTERVIEW 相关）
- 持续反馈（FEEDBACK 相关）
- 独立 KPI 指标库页面（KPI 仅支持行内创建）
- 独立员工结果页（结果在个人概览页展示）
- 个人/团队绩效报表（当前仅有部门/公司报表）
- KPI 从模板导入
- KPI 目标对齐（无上级目标关联）
- 年度 KPI 分解为季度目标（父子周期仅作为组织维度）
- 校准会议管理（创建/开始/完成会议，当前校准为直接等级调整）
- 阶段自动流转/阶段时间配置（阶段由 HR 手动推进）
- OverallEvalComment 独立实体已废弃，当前评语通过 PerformanceResult 字段存取

---

## 用途说明

- 本文定义"测什么"，场景总览列出所有用例，场景详情为 P0 场景提供结构化的输入/输出规格
- E2E 具体执行编排见 `10-e2e-test-spec.md`
- API 变更应先跑契约校验，再依据本文补齐 L1/L2 验证
- 错误码定义见 `08-error-codes.md`，状态机约束见 `04-state-machine.md`

---

**创建时间**: 2025-12-25
**更新时间**: 2026-03-20
**状态**: Active
**版本**: v6.2
