轻松聘AI开发
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

同岗位竞争力排名 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}。禁止返回任何其他候选人的:

  • studentUserId
  • candidateId
  • name
  • school
  • phone
  • email
  • resumeUrl
  • applicationId
  • 其他任何身份字段

12. 数据库与 Flyway

  • 不需要新表。
  • 不新增 Flyway V32。
  • 无 DDL、无数据变更。

13. 查询与 N+1 风险

沿用企业看板已验收的“批量 JOIN 最新期快照”模式:

  1. applications 按 job_id + 状态过滤取 cohort(1 条查询)。
  2. candidate_dimension_scores 按 student_user_id IN (...) 且 period <= asOfPeriod 批量读取,Java 侧取每个学生最新周期(1 条查询)。
  3. 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 黑盒

  1. 起基础设施 + 最新 JAR;/health=200;Flyway 无新迁移;OpenAPI=117。
  2. mock 登录 STUDENT;选 PUBLISHED 岗位且 ≥2 位非无效候选人。
  3. 本人 GET → 校验各字段;抓体验证 topCandidates 匿名。
  4. 越权:COMPANY/ADMIN→403/2002;未投递→403/2002;匿名→401;岗位不存在或非 PUBLISHED→404/7001;jobId=0→400/1001。
  5. 数据库核验最新期快照、业务数据无改动、无新表。
  6. 回归: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(黑盒阶段除外)。