# 统一求职进度聚合 API(V34) > 版本:V34 设计锁定版 > 状态:已编码实现 > 适用范围:学生端**只读**接口,为「求职进度」页聚合投递、面试、结构化面试评分、Offer、同岗位竞争力排名与通知计数。 ## 1. 定位与边界 为 STUDENT 提供「求职进度」页所需的单次聚合数据,避免前端串行调用多个接口。与既有接口边界: | 接口 | 是否改动 | 关系 | |---|---|---| | `GET /jobs/{jobId}/competition-ranking` | 不改 | 复用同套候选人池/排序/百分位口径(共享 `CompetitionRankingCalculator`),但本接口只返回摘要,不返回 dimensions/gaps/topCandidates | | `GET /notifications` | 不改 | 本接口只返回通知**计数**,通知标题/正文/详情仍由 `/notifications` 提供 | | `GET /interviews/{id}/score` 等 | 不改 | 仅读取已提交(SUBMITTED)评分摘要,不返回 DRAFT、不返回 comment | | `GET /offers/...` | 不改 | 仅返回 Offer 摘要,不返回薪资/条款细节 | | `GET /profile/me/...`、投递、面试 | 不改 | 本接口为其只读聚合,不重写既有语义 | ## 2. 接口 ``` GET /api/v1/profile/me/career-progress ``` Query 参数: | 参数 | 类型 | 必填 | 默认 | 说明 | |---|---|---|---|---| | `page` | int | 否 | `0` | 页码,从 0 开始,`>= 0` | | `size` | int | 否 | `20` | 每页条数,`1–50` | | `status` | string | 否 | — | 投递状态过滤,仅 `SUBMITTED/VIEWED/INTERVIEW/HIRED/REJECTED/WITHDRAWN` | - 角色:仅 STUDENT。 - 身份从 JWT 获取,不接收 `userId`/`studentUserId`/`candidateId` 等身份注入参数。 - 新增 1 条 GET 路径,OpenAPI 125 → 126。 ## 3. 权限矩阵 | 身份/场景 | HTTP | code | 说明 | |---|---|---|---| | 未登录 | 401 | 2001 | 既有安全机制 | | COMPANY / ADMIN | 403 | 2002 | Service 层 `assertStudentRole` | | STUDENT 正常 | 200 | — | 正常返回 | | `page < 0` / `size < 1` / `size > 50` / 非法 `status` | 400 | 1001 | BAD_REQUEST | 不新增错误码,复用既有 ErrorCode。 ## 4. 响应结构 ``` ApiResponse CareerProgressResponse { ProgressTodoSummary todo; PaginatedApplications applications; Instant generatedAt; } ProgressTodoSummary { long pendingInterviewResponseCount; // 状态 SCHEDULED 的面试数量 long upcomingConfirmedInterviewCount; // 状态 CONFIRMED 且 startAt > 当前时间 long pendingOfferResponseCount; // 状态 SENT/NEGOTIATING 且 expireAt > 当前时间 long unreadNotificationCount; // 未读通知总数 long unreadReportDecisionCount; // 未读 REPORT_DECISION 通知数 } PaginatedApplications { List items; long total; int page; int size; int totalPages; } CareerProgressItem { Long applicationId; String applicationStatus; LocalDateTime appliedAt; LocalDateTime updatedAt; JobSummary job; InterviewSummary nextInterview; // 可空 ScoreSummary latestSubmittedInterviewScore; // 可空 OfferSummary offer; // 可空 CompetitionSummary competition; } JobSummary { Long jobId; String title; String companyName; String city; String employmentType; String jobStatus; } InterviewSummary { Long interviewId; Integer roundNo; String title; String interviewType; LocalDateTime startAt; LocalDateTime endAt; String status; } ScoreSummary { Long interviewId; BigDecimal overallScore; String recommendation; LocalDateTime submittedAt; } OfferSummary { Long offerId; String status; String title; LocalDateTime expireAt; LocalDate onboardingDate; } CompetitionSummary { boolean eligible; Integer rank; // 有分才返回;WITHDRAWN/REJECTED 或本人无分 → null Integer cohortSize; // 仅 eligible=true 时返回 BigDecimal overallScore; // 本人无分 → null BigDecimal percentile; // 有分者 < 2 → null String status; // OK / PARTIAL / INSUFFICIENT_DATA / PENDING String mySnapshotPeriod; // 本人实际使用快照月;无快照 → null String rulesVersion; } ``` ### 明确不返回字段 - 不返回 `studentUserId`、`candidateId`、`companyUserId`、`evaluatorUserId`、`phone`、`email`、`resumeUrl`、`passwordHash`、JWT。 - 不返回通知标题/正文(`title`/`content`)、举报详情、面试评分 `comment`、Offer 薪资/条款细节(`salaryMin/salaryMax/baseSalary/housingAllowance/annualBonusMonths/benefits/description/otherAllowances`)。 - 不返回其他候选人的任何身份信息,也不返回 V32 的 `dimensions`、`gaps`、`topCandidates`。 ## 5. 聚合口径 ### 5.1 投递 - 读取当前学生全部投递,按 `createdAt DESC` 分页。 - `status` 存在时按该状态筛选。 - 不过滤 `WITHDRAWN/REJECTED/HIRED`,均属于进度历史。 ### 5.2 Todo | 字段 | 口径 | |---|---| | `pendingInterviewResponseCount` | 当前学生 `SCHEDULED` 面试数量 | | `upcomingConfirmedInterviewCount` | 当前学生 `CONFIRMED` 且 `startAt > now` 的面试数量 | | `pendingOfferResponseCount` | 当前学生 `SENT/NEGOTIATING` 且 `expireAt > now` 的 Offer 数量 | | `unreadNotificationCount` | 复用 `NotificationRepository` 未读总数 | | `unreadReportDecisionCount` | 未读且 `type='REPORT_DECISION'` 的通知数量 | ### 5.3 nextInterview - 只看该投递的 `SCHEDULED / CONFIRMED` 面试,取 `startAt` 最早一场。 - `RESCHEDULE_REQUESTED` 不算已确定;已完成/取消/拒绝不作为 nextInterview。 ### 5.4 latestSubmittedInterviewScore - 只读 `SUBMITTED` 状态的 `InterviewScore`,每个 `applicationId` 取 `submittedAt` 最新一条。 - 不返回 DRAFT、不返回 `comment`。 ### 5.5 Offer - 仅学生本人 `candidateId` 的 Offer;`DRAFT` 永不返回。 - 优先展示 `SENT/NEGOTIATING` 且 `expireAt > now` 的有效 Offer,按 `expireAt ASC` 取最早到期一条。 - 无有效 Offer 时,展示该投递 `createdAt` 最新的一条非 DRAFT Offer。 - 不返回薪资/条款细节。 ### 5.6 competition - 投递状态 `SUBMITTED/VIEWED/INTERVIEW/HIRED` → `eligible=true`;`WITHDRAWN/REJECTED` → `eligible=false`(不返回其它排名字段)。 - 复用 V32 候选池口径、最新快照口径、`CandidateScoringCalculator#overallScoreFromSnapshots`、稳定排序与百分位公式。 - `percentile = 严格低于本人分数的人数 / (有分候选人数 - 1) × 100`,`scale=2`、`HALF_UP`;有分者 `< 2` 时 `null`。 - 不返回 V32 的 `dimensions`/`gaps`/`topCandidates`。 ## 6. 综合分 / 排序 / 百分位 - 全部复用 `CompetitionRankingCalculator`(V32/V34 共享),禁止复制第二套公式、禁止 SQL 重算。 - `status` 复用引擎语义:`OK / PARTIAL / INSUFFICIENT_DATA / PENDING`,不把 `PENDING/INSUFFICIENT_DATA` 折算为 0。 ## 7. 只读与性能 - `CareerProgressService` 标注 `@Transactional(readOnly=true)`,不写业务表/审计表/通知表/缓存。 - 所有附属数据批量读取(jobs/companies/interviews/scores/offers/cohort/快照/通知计数),查询次数与分页 `items` 数量无关,目标约 8–10 次,无 N+1。 - `size` 上限固定 50。 ## 8. 数据库与 Flyway - 不需要新表。 - 不新增 Flyway V34。 - 无 DDL、无数据变更。 ## 9. 测试 - `CareerProgressServiceTest`:权限/参数、空数据、完整聚合、DRAFT 不泄露、WITHDRAWN/REJECTED、无快照、批量查询无 N+1。 - `CareerProgressControllerTest`:STUDENT 正常、COMPANY/ADMIN 403、未登录 401、参数非法 400、脱敏断言。 - `CareerProgressPersistenceIntegrationTest`:真实仓库批量读取 + 只读性(关键表计数前后不变)。 ## 10. 禁止事项(实现阶段) - 不修改 Candidate Scoring Engine、Competition Ranking 对外接口/权限/状态口径。 - 不修改 Offer、Interview、Notification、Report/Block 的既有接口语义。 - 不新增数据库表、不新增 Flyway、不新增 Redis 缓存、不新增异步任务。 - 不新增错误码。 - 不做企业回复率、推荐算法、AI 分析、薪资建议、通知内容聚合。 - 不启停 Docker(黑盒阶段除外)。