# 结构化面试评分 API(V31) 本文档描述「结构化面试评分」模块。**这与候选人反馈(`candidate_feedback`)是两套完全独立的机制**,请勿混淆: - candidate_feedback / Candidate Scoring Engine:既有的候选人反馈与评分治理模块,语义保持不变。 - 结构化面试评分:企业针对某一场**已完成(`COMPLETED`)**的面试,按固定五个维度填写并提交评分。独立落库到 `interview_scores` / `interview_score_history` 两张新表,不写 candidate_feedback。 只支持单面试官评分模型(一场面试一条评分);不新增多面试官模型。 OpenAPI 路径总数 113 → **116**(3 个 path、4 个 operation)。 --- ## 一、接口总览 | 操作 | 路径 | 说明 | | --- | --- | --- | | PUT | `/interviews/{interviewId}/score` | 企业 owner 保存评分草稿(幂等更新) | | GET | `/interviews/{interviewId}/score` | 查看评分(企业见 DRAFT/SUBMITTED;学生仅 SUBMITTED) | | POST | `/interviews/{interviewId}/score/submit` | 提交草稿 → SUBMITTED(不可逆) | | GET | `/interviews/{interviewId}/score/history` | 评分快照历史(无记录返回 200 空数组) | ### 身份与权限 - 所有接口都需要登录(JWT);身份只从 JWT/SecurityContext 获取,**不接受 `userId`/`studentUserId`/`companyId` 参数**。 - 归属校验:`companyRepository.findByOwnerUserId(userId)` 解析企业;`interview.companyUserId == company.ownerUserId` 且经由 application→job 的 `job.companyId == company.id` 且 `application.studentUserId == interview.studentUserId`,否则 `403/8402`。 - 企业 owner(COMPANY):可写可读草稿与已提交。 - 目标学生(STUDENT):只能查看**已提交(SUBMITTED)**的评分;查看 DRAFT 或他人评分返回 `403/8402`。 - ADMIN:不允许访问(`403/8402`)。 - 面试不存在 `404/8401`。 --- ## 二、保存评分草稿 `PUT /interviews/{interviewId}/score` 请求体: ```json { "technicalScore": null, "communicationScore": 4, "problemSolvingScore": 3, "roleFitScore": null, "professionalismScore": 5, "recommendation": "HIRE", "comment": "综合素质较好" } ``` 字段: - 五个维度 `technicalScore` / `communicationScore` / `problemSolvingScore` / `roleFitScore` / `professionalismScore`,取值 1-5;草稿阶段可留空(`null`)。 - `recommendation`(可选):`STRONG_HIRE` / `HIRE` / `NEUTRAL` / `NO_HIRE`,企业手动选择,不由 overallScore 推导;非法值 `400/1001`。 - `comment`(可选):≤2000 字符。 - `overallScore`:**DTO 中不提供该字段**,由服务端计算并忽略客户端传入值。 约束: - 无评分时创建 `DRAFT`;已有 DRAFT 时更新同一行(`@Version` 递增)。 - `SUBMITTED` 后再次 PUT → `409/9503`。 - 保存草稿不写历史节点(历史仅写 `NULL→DRAFT`、`DRAFT→SUBMITTED` 两次),但写审计日志 `INTERVIEW_SCORE_SAVED`。 - 并发冲突 → `409/9501`。 成功响应(脱敏): ```json { "code": 200, "message": "ok", "data": { "id": 1, "interviewId": 10, "applicationId": 20, "status": "DRAFT", "technicalScore": null, "communicationScore": 4, "problemSolvingScore": 3, "roleFitScore": null, "professionalismScore": 5, "overallScore": null, "recommendation": "HIRE", "comment": "综合素质较好", "submittedAt": null, "version": 1, "createdAt": "2026-08-26T10:00:00", "updatedAt": "2026-08-26T10:00:00" } } ``` > 脱敏:响应不返回 `companyUserId` / `studentUserId` / `evaluatorUserId` / 手机号 / 邮箱。 --- ## 三、查看评分 `GET /interviews/{interviewId}/score` - 企业 owner:可看 `DRAFT` 与 `SUBMITTED`。 - 目标学生:仅可看 `SUBMITTED`;查看 DRAFT → `403/8402`。 - 无评分 → `404/9504`。 - 面试未完成 → `409/9502`。 --- ## 四、提交评分 `POST /interviews/{interviewId}/score/submit` 无请求体。将 `DRAFT` 提交为 `SUBMITTED`: - 五个维度必须全部非空,否则 `400/1001`。 - `overallScore` 由服务端计算:`(t + c + ps + rf + p) / 5`,`BigDecimal`,保留两位小数,`RoundingMode.HALF_UP`。 - 提交成功后 `submittedAt` 落库;不可逆。 - 写历史节点 `DRAFT→SUBMITTED`;写审计日志 `INTERVIEW_SCORE_SUBMITTED`。 - **幂等**:重复 submit 已 SUBMITTED → `200` 返回原评分;不新增历史、不重复通知。 - **通知**:仅通知目标学生一次,`businessType=INTERVIEW`、`businessId=interviewId`、幂等键 `interview-score-submitted-{interviewId}`;草稿不发通知;不通知企业自身;无 Push。 - 并发冲突 → `409/9501`。 --- ## 五、查看评分历史 `GET /interviews/{interviewId}/score/history` - 企业 owner:可查看全部历史。 - 目标学生:仅在评分 `SUBMITTED` 后可查看。 - 无历史记录 → `200` 空数组 `items=[]`。 - 历史快照 `snapshot` 为解析后的维度分 + `overallScore`(JSON 字符串落库于 `snapshot_text` TEXT 字段,不返回内部用户标识)。 响应示例: ```json { "code": 200, "message": "ok", "data": [ { "id": 1, "scoreId": 1, "interviewId": 10, "fromStatus": null, "toStatus": "DRAFT", "actorRole": "COMPANY", "snapshot": { "technicalScore": null, "overallScore": null }, "createdAt": "2026-08-26T10:00:00" }, { "id": 2, "scoreId": 1, "interviewId": 10, "fromStatus": "DRAFT", "toStatus": "SUBMITTED", "actorRole": "COMPANY", "snapshot": { "technicalScore": 4, "overallScore": 4.20 }, "createdAt": "2026-08-26T10:05:00" } ] } ``` > 脱敏:历史条目不返回 `actorUserId`、手机号、邮箱。 --- ## 六、状态机与历史 ``` 无评分 → DRAFT → SUBMITTED ``` - PUT 无评分 → 创建 `DRAFT`(写历史 `NULL→DRAFT`)。 - PUT 已有 DRAFT → 更新同一行,不写历史,写审计 `INTERVIEW_SCORE_SAVED`。 - POST submit:DRAFT → SUBMITTED(写历史 `DRAFT→SUBMITTED`,写审计 `INTERVIEW_SCORE_SUBMITTED`)。 - `SUBMITTED` 不可逆;重复 submit 幂等。 - 草稿每次 PUT `version` 递增;`@Version` 乐观锁防并发。 --- ## 七、错误码 | 错误码 | 含义 | HTTP | | --- | --- | --- | | 9501 | 并发冲突(更新或首次并发创建撞唯一约束) | 409 | | 9502 | 面试未完成(非 COMPLETED),不可评分 | 409 | | 9503 | 评分已提交,不可再修改 | 409 | | 9504 | 评分不存在 | 404 | | 8401 | 面试不存在 | 404 | | 8402 | 归属校验失败 / 越权访问 | 403 | | 2002 | 非 COMPANY(对写入),未授权访问 | 403 | | 1001 | 参数校验失败(非 1-5 维度、非法推荐值、提交缺维度) | 400 | --- ## 八、数据库(V31) 新增两张表,均 `ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`: - `interview_scores`:`interview_id` 唯一;五个评分字段允许 NULL 且在 1-5;`overall_score` 1.00-5.00;`status` CHECK(DRAFT/SUBMITTED);`recommendation` CHECK;`SUBMITTED` 时五维、`overallScore`、`submittedAt` 均非空;`@Version`;外键 `ON DELETE CASCADE`。 - `interview_score_history`:`snapshot_text TEXT`(不使用 JSON 类型,避免 MySQL/H2 兼容问题);外键 `ON DELETE CASCADE`。 不修改 V1-V30、不修改 CandidateFeedback、不修改 Interview 状态机、不修改 Candidate Scoring Engine、不修改个人数据导出/账号注销/隐私设置。 --- ## 九、文档与验证 - OpenAPI 路径数 113 → 116;`docs/openapi-rc.json` 已同步。 - 验证:`python docs/verify_openapi.py`;项目 `mvn test`、`mvn clean verify`。