forked from baskbuddy/zhangshun
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
8.3 KiB
8.3 KiB
同岗位竞争力排名 API(V32)
版本:V32 设计锁定版 状态:已编码实现(含状态口径修复) 适用范围:学生端只读接口,不修改既有接口、百分位、评分引擎与企业看板。
⚠️ 状态口径(最终): 投递已 WITHDRAWN 或 REJECTED 的学生已退出候选人池,访问排名接口返回 403/2002。 “进入排名接口”的有效投递状态与 cohort 完全一致:仅
SUBMITTED / VIEWED / INTERVIEW / HIRED允许进入。
1. 定位与边界
为已投递某岗位的 STUDENT 提供该岗位候选人池内的竞争力排名。与既有接口边界:
| 接口 | 是否改动 | 关系 |
|---|---|---|
GET /profile/me/candidate-percentile |
不改 | 全局 cohort、基于 candidate_feedback 均分;本接口为单岗位、基于候选评分快照 |
GET /profile/me/candidate-scoring |
不改 | 复用其真实公式 overallScoreFromSnapshots |
GET /company/.../scoring/dashboard/candidate-rankings |
不改 | 企业端全公司视角(含姓名/学校);本接口学生端、匿名 |
GET /jobs/{jobId}/match |
不改 | 偏好确定性匹配诊断;本接口为候选人间竞争排名 |
2. 接口
GET /api/v1/jobs/{jobId}/competition-ranking
- 角色:仅 STUDENT。
- 身份从 JWT 获取,不接收
userId/studentUserId参数。 - 新增 1 条 GET 路径,OpenAPI 116 → 117。
3. 权限矩阵
| 身份/场景 | HTTP | code | 说明 |
|---|---|---|---|
| 未登录 | 401 | 2001 | SecurityConfig /jobs/** 为 authenticated |
| COMPANY / ADMIN | 403 | 2002 | Service 层 assertStudentRole |
| STUDENT 已投递该岗位且状态有效(SUBMITTED/VIEWED/INTERVIEW/HIRED) | 200 | — | 正常返回 |
| STUDENT 未投递该岗位 | 403 | 2002 | 岗位存在但不属于其候选人池,不泄露候选人数据 |
| STUDENT 唯一投递为 WITHDRAWN 或 REJECTED | 403 | 2002 | 已退出候选人池;与 cohort 排除口径一致,不返回 200 |
| jobId 为空或 <=0 | 400 | 1001 | BAD_REQUEST |
| 岗位不存在 | 404 | 7001 | JOB_NOT_FOUND |
| 岗位非 PUBLISHED(DRAFT/CLOSED 等) | 404 | 7001 | 不暴露岗位是否存在候选人池 |
不新增错误码,复用既有 ErrorCode。
4. 响应结构
ApiResponse<CompetitionRankingResponse>
CompetitionRankingResponse {
Long jobId;
Integer rank; // 本人名次(完整 cohort 内,含无分者排序);本人无分 → null
Integer cohortSize; // cohort 总人数(含无分者)
BigDecimal overallScore; // 本人真实综合分;无有效维度 → null
BigDecimal percentile; // 百分位,仅对有分 cohort 计算;有分者<2 → null
String status; // OK / PARTIAL / INSUFFICIENT_DATA / PENDING(复用引擎语义)
String asOfPeriod; // 本次排名计算截止月份 yyyy-MM(当前月)
String mySnapshotPeriod;// 当前学生实际使用的最新快照月份
List<DimensionItem> dimensions; // 本人各维度 {dimension,score,status,weight}
List<String> gaps; // 缺口建议(基于维度状态,简单实现)
List<RankingSnapshot> topCandidates; // 匿名 Top 5
String rulesVersion;
Instant generatedAt;
}
DimensionItem { String dimension; BigDecimal score; String status; BigDecimal weight; }
RankingSnapshot { Integer rank; BigDecimal overallScore; }
明确不返回字段
- 不返回
candidateId(本人内部标识,前端不需要)。 topCandidates[]每个元素仅{rank, overallScore}。
5. Cohort 口径
| 问题 | 决策 |
|---|---|
| 排除状态 | 排除 WITHDRAWN、REJECTED |
| 计入状态 | 计入 SUBMITTED、VIEWED、INTERVIEW、HIRED |
| 同学生多次投递 | 由 applications(job_id, student_user_id) 唯一约束天然去重,单岗位至多 1 条 |
| 是否要求已投递 | 必须存在有效投递(状态非 WITHDRAWN/REJECTED);仅 WITHDRAWN/REJECTED → 403/2002 |
| 无评分候选人 | 计入 cohortSize,排在最后,不参与 percentile |
| 岗位可查范围 | 仅 PUBLISHED;其余 → 404/7001 |
6. 排名规则
候选人排序(Java 侧排序,SQL 只负责批量查询,不使用 MySQL NULLS LAST):
1. 有分候选人优先(overallScore != null)
2. 有分者内部:overallScore DESC
3. overallScore 相同:application.createdAt ASC
4. 仍相同:student_user_id ASC
5. 无分候选人(overallScore == null)最后
- 本人
rank:在完整 cohort 排序中的位次;本人无分 →rank=null。 - 本地用
Comparator排序,明确把overallScore == null最后处理。 ownFlag不参与排名排序,仅用于在排序后定位当前学生。
7. 综合分与状态
- 复用
CandidateScoringCalculator#overallScoreFromSnapshots(真实公式,禁止 SQL 复算)。 status复用 Candidate Scoring Engine 状态语义:OK / PARTIAL / INSUFFICIENT_DATA / PENDING。- 不得把
PENDING/INSUFFICIENT_DATA当作 0 分。
8. 百分位
严格复用企业看板公式:
分母 = 有分候选人数 - 1
分子 = 综合分严格低于当前学生的有分候选人数
percentile = 分子 / 分母 * 100
- 用
BigDecimal,scale=2,RoundingMode.HALF_UP。 - 有分候选人 < 2 时:
percentile = null。
9. 周期
asOfPeriod= 当前月份yyyy-MM。- 每个候选人取自己
<= asOfPeriod的最新周期快照(cohort 各候选人的最新快照月份可能不同)。 - 当前学生单独返回
mySnapshotPeriod。
10. Gaps
- 不新增另一套缺口计算规则。
- 不把
PENDING/INSUFFICIENT_DATA转成 0。 - 当前阶段返回基于维度状态的简单缺口;若后续需要复杂建议另行设计。
11. 匿名化硬约束
topCandidates[] 仅返回 {rank, overallScore}。禁止返回任何其他候选人的:
studentUserIdcandidateIdnameschoolphoneemailresumeUrlapplicationId- 其他任何身份字段
12. 数据库与 Flyway
- 不需要新表。
- 不新增 Flyway V32。
- 无 DDL、无数据变更。
13. 查询与 N+1 风险
沿用企业看板已验收的“批量 JOIN 最新期快照”模式:
applications按job_id+ 状态过滤取 cohort(1 条查询)。candidate_dimension_scores按student_user_id IN (...)且period <= asOfPeriod批量读取,Java 侧取每个学生最新周期(1 条查询)。- jobs 验存在 + 用户身份读取(各 1 条)。
无 N+1,恒定时复杂度。
14. Top N
固定返回 Top 5,不增加 top 参数,避免通过参数批量探测候选人排名。分页需求另行设计。
15. 测试(设计)
MockMvc
- 权限矩阵(未登录/COMPANY/ADMIN/已投递/未投递/岗位不存在/非 PUBLISHED/jobId<=0)。
- 本人有分:rank/cohortSize/percentile/overallScore 数值正确(HALF_UP、scale2)。
- 本人无分:score=null、rank=null、percentile=null、仍返回 cohortSize。
- 无分候选人排最后且计入 cohort;同分按 createdAt ASC 稳定排序。
topCandidates[]匿名化(断言响应不含身份字段)。- 最新期快照读取(两期数据取一期)。
- cohort 状态过滤(排除 WITHDRAWN/REJECTED,计入 SUBMITTED/VIEWED/INTERVIEW/HIRED)。
Docker 黑盒
- 起基础设施 + 最新 JAR;
/health=200;Flyway 无新迁移;OpenAPI=117。 - mock 登录 STUDENT;选 PUBLISHED 岗位且 ≥2 位非无效候选人。
- 本人 GET → 校验各字段;抓体验证 topCandidates 匿名。
- 越权:COMPANY/ADMIN→403/2002;未投递→403/2002;匿名→401;岗位不存在或非 PUBLISHED→404/7001;jobId=0→400/1001。
- 数据库核验最新期快照、业务数据无改动、无新表。
- 回归:
mvn test、mvn clean verify、verify_openapi.py。
16. 文档变更
- 新增:
docs/competition-ranking-api.md(本文档)。 - 更新:
docs/openapi-rc.json(117 paths)、docs/verify_openapi.py、docs/frontend-integration.md。
17. 禁止事项(实现阶段)
- 不修改 Candidate Scoring Engine。
- 不修改 percentile。
- 不修改已验收接口(评分、看板、治理、面试、通知、匹配、Offer)。
- 不新增数据库表、不新增 Flyway。
- 不启停 Docker(黑盒阶段除外)。