# 双向匹配基础模块接口说明 > 职来 (mydreamjob) · 双向匹配基础流程(规则阈值版)与匹配后沟通解锁 > 基础路径:`/api/v1` · 全部接口需 `Authorization: Bearer ` ## 1. 概述 企业与求职者围绕同一条投递 `application` 形成双向匹配,**不依赖 AI 或综合算法**: - `candidateInterestScore`(0-100):求职者对岗位的**意愿分**(人工录入,来源 `MANUAL_INTEREST`), **不是**教育/能力/诚信/算法综合分;未来算法服务可替换该分数来源。 - `hrSuitabilityScore`(0-100):企业对候选人的 **HR 适配分**(人工录入,来源 `HR_MANUAL`)。 - `companyRequiredCandidateScore`(0-100,默认 60):企业设置的"最低意愿分"阈值。 - `studentRequiredHrScore`(0-100,默认 60):求职者设置的"最低 HR 适配分"阈值。 **匹配成功条件**:`candidateInterestScore >= companyRequiredCandidateScore 且 hrSuitabilityScore >= studentRequiredHrScore`。 匹配成功(状态 `MATCHED`)后:写入 `matchedAt`;双方各收到一次 `MATCH_SUCCEEDED` 通知; 双方可基于该 application 创建/获取直聊会话;本期**不再允许修改双方评分**。 未达标时状态为 `NOT_MATCHED`,双方可重提/改分,每次修改后重新计算。**不做** 7 天冷却期等额外限制。 ## 2. 状态机 | 状态 | 含义 | |---|---| | `PENDING_BOTH` | 双方都未评分 | | `PENDING_HR` | 求职者已评分,等待企业 HR 分 | | `PENDING_CANDIDATE` | 企业已评分,等待求职者意愿分 | | `NOT_MATCHED` | 双方已有评分但未同时达到阈值 | | `MATCHED` | 双方达到阈值(终态之一,评分锁定) | | `CLOSED` | 投递已撤回/拒绝、岗位关闭等,不再允许匹配(终态) | 重算规则(每次评分提交后执行): - 两侧评分缺失 → `PENDING_BOTH`;仅缺候选人分 → `PENDING_CANDIDATE`;仅缺 HR 分 → `PENDING_HR`; - 两侧都有且同时达标 → `MATCHED`;否则 → `NOT_MATCHED`。 - 仅**有效投递状态**(`SUBMITTED`/`VIEWED`/`INTERVIEW`)可评分;`REJECTED`/`HIRED`/`WITHDRAWN` 或岗位 `CLOSED` 时评分为 `CLOSED` 并拒绝写入。 - 重复提交相同分数不产生重复事件/通知;并发评分经乐观锁 + `MATCH_SUCCEEDED` 事件唯一约束, 保证**仅一次**从非 MATCHED 进入 MATCHED。 ## 3. 接口清单 ### 3.1 企业匹配阈值 ``` GET /companies/me/match-rule PUT /companies/me/match-rule ``` - 角色:企业本人(COMPANY 且绑定企业);未配置读取返回默认 `60`;首次更新创建配置。 - PUT 请求:`{"requiredCandidateScore": 70}`(0-100,非法值 400)。 - 响应:`{"companyId": 1, "requiredCandidateScore": 70}`。 - 阈值更新**只影响后续重新计算**;匹配记录保留本次计算实际使用的阈值快照。 ### 3.2 学生匹配阈值 ``` GET /profile/me/match-rule PUT /profile/me/match-rule ``` - 角色:学生本人;同上,PUT 字段为 `requiredHrScore`。 ### 3.3 提交求职者岗位意愿分 ``` PUT /applications/{id}/candidate-interest-score ``` - 角色:该投递的求职者本人(STUDENT);请求 `{"score": 80}`(0-100)。 - 前置:投递存在(404)、投递归属本人(403)、投递状态有效(否则 409 `MATCH_CLOSED`)、 匹配记录非 MATCHED/CLOSED(409 `MATCH_SCORES_LOCKED`/`MATCH_CLOSED`)。 - 成功 200,返回匹配记录视图(状态、双分、阈值快照、matchedAt)。 ### 3.4 提交企业 HR 适配分 ``` PUT /applications/{id}/hr-suitability-score ``` - 角色:岗位所属企业(COMPANY,经 `company_id` 归属校验);请求 `{"score": 80}`。 - 权限/校验同上;他人企业 403。 ### 3.5 查询投递匹配状态 ``` GET /applications/{id}/match ``` - 角色:该投递求职者或岗位所属企业;其他 403,不存在 404。 - 响应(脱敏,不含内部路径/事件 ID): `applicationId, jobId, companyId, candidateId, status, candidateInterestScore, hrSuitabilityScore, companyRequiredCandidateScore, studentRequiredHrScore, candidateScoreSource, hrScoreSource, matchedAt, matchVersion`。 - 无记录返回 `PENDING_BOTH` + 当前阈值;投递终态/岗位关闭返回 `CLOSED`。 ### 3.6 匹配后创建/获取沟通会话 ``` POST /applications/{id}/conversation ``` - 角色:该投递求职者或岗位所属企业;`application` 必须为 `MATCHED`,否则 409 `APPLICATION_NOT_MATCHED`。 - 幂等:已有对应会话则返回原会话;否则创建 DIRECT 会话并记录 `application_match_conversations`; 并发冲突返回既有会话。 - 响应:会话视图 `{id, title, type, createdBy, createdAt, updatedAt}`。 ## 4. 权限与越权矩阵 | 接口 | 该投递求职者 | 岗位所属企业 | 其他企业 | 其他求职者 | 未登录 | |---|---|---|---|---|---| | 企业阈值 GET/PUT | 403 | ✅ | 403 | 403 | 401 | | 学生阈值 GET/PUT | ✅ | 403 | 403 | 403 | 401 | | candidate-interest-score | ✅(本人) | 403 | 403 | 403 | 401 | | hr-suitability-score | 403 | ✅(本人岗位) | 403 | 403 | 401 | | 匹配状态查询 | ✅(本人) | ✅(本人岗位) | 403 | 403 | 401 | | 匹配会话创建 | ✅(且 MATCHED) | ✅(且 MATCHED) | 403 | 403 | 401 | 原则:身份一律来自 JWT,不信任请求体/查询参数中的 userId/companyId/studentId; 401 未登录;403 存在但越权;404 资源不存在;分数/阈值非法值 400。 ## 5. 防绕过:旧 `/conversations` 收紧 - 现有 `POST /conversations` 在**新建会话**时,若双方为"企业账号 + 求职者账号"组合, 必须存在一条同时归属双方且状态为 `MATCHED` 的投递,否则 **403 `MATCH_REQUIRED_FOR_CONVERSATION`**。 - 已有 DIRECT 会话:仅允许原成员继续访问,**不删除、不失效**(命中既有会话直接返回,不校验)。 - 学生-学生、企业-企业及其他组合:保持原语义。 - 无法判定身份时(上游已校验存在性)防御性放行。 ## 6. 通知与 RabbitMQ(Outbox) - 首次进入 `MATCHED` 时,求职者与企业各收到一次 `MATCH_SUCCEEDED` 通知。 - 通知内容含 `applicationId`、`jobId`、对方展示信息(企业名/岗位名、候选人姓名/昵称), **不暴露手机号、JWT、内部存储路径**。 - 防重复三层保障:① `application_match_events` 对同一 application 的 `MATCH_SUCCEEDED` 事件唯一(dedup_key); ② 事件 `notification_status` 仅 `PENDING → SENT`,发送后不再重发; ③ 通知幂等键 `match-succeeded-{applicationId}-{candidate|company}`。 - 可靠发送:评分事务提交后 `afterCommit` 立即发送;失败保持 `PENDING`,由 `MatchNotificationScheduler` (`app.match.notification.enabled`/`retry-ms`,默认 60s)重试;`sendMatchSucceeded` 为 `REQUIRES_NEW` 独立事务,重试/并发不产生重复通知。 - 复用现有 `NotificationService`(幂等键落库),不新增队列、不清空任何 RabbitMQ 队列;不影响 Offer 通知语义。 ## 7. 数据库迁移(V16) `src/main/resources/db/migration/V16__bidirectional_match.sql`(兼容 MySQL 8 / H2 MODE=MySQL)。 | 表 | 关键字段/约束 | |---|---| | `company_match_rules` | company_id 唯一 FK→companies;required_candidate_score 0-100 默认 60 | | `student_match_rules` | student_user_id 唯一 FK→users;required_hr_score 0-100 默认 60 | | `application_match_records` | application_id 唯一 FK→applications;双分可空(0-100 CHECK)、阈值快照、评分来源、status、matched_at、version 乐观锁;索引(company_id,status)/(candidate_id,status)/(status,matched_at)/(job_id) | | `application_match_events` | application_id+dedup_key 唯一(保证 MATCH_SUCCEEDED 仅一次);event_type/old_status/new_status/old_score/new_score/score_field/notification_status/request_id;索引(application_id,created_at)/(event_type,notification_status) | | `application_match_conversations` | application_id 唯一;conversation_id FK→conversations;解锁会话幂等 | 不改任何既有表;`/jobs/{jobId}/match` 岗位匹配诊断、`candidate_feedback`、`POST /conversations` 语义保持不变。 ## 8. Apifox 测试步骤 1. 登录学生与企业账号(Mock 验证码)。 2. 企业 `PUT /companies/me/match-rule` 设阈值 70;学生 `PUT /profile/me/match-rule` 设 70。 3. 学生投递岗位,记录 applicationId。 4. 学生 `PUT /applications/{id}/candidate-interest-score` `{"score":80}` → 200,状态 `PENDING_HR`。 5. 企业 `PUT /applications/{id}/hr-suitability-score` `{"score":80}` → 200,状态 `MATCHED`,matchedAt 非空。 6. `GET /applications/{id}/match` 查看双方评分与快照。 7. 双方各收到 `MATCH_SUCCEEDED` 通知(通知列表可查)。 8. `POST /applications/{id}/conversation` → 创建会话;重复调用返回同一会话。 9. 再提分 → 409 `MATCH_SCORES_LOCKED`。 10. 未匹配投递 `POST /applications/{id}/conversation` → 409 `APPLICATION_NOT_MATCHED`。 11. 未匹配的企业+学生调旧 `POST /conversations` → 403 `MATCH_REQUIRED_FOR_CONVERSATION`。 ## 9. 已知限制 1. 评分与阈值均为人工录入(来源 MANUAL_INTEREST/HR_MANUAL),非算法评分;算法服务接入时替换写入方并升级 `matchVersion`/评分来源。 2. `MATCHED` 后评分锁定;如需协商调整需先 CLOSED 或产品确认撤销规则。 3. 阈值更新只影响后续重算,匹配记录保留计算时点快照(可审计)。 4. 通知在评分事务提交后发送,极端情况下依赖调度任务补发(默认 60s 周期)。 5. OpenAPI 快照:`docs/openapi-rc.json` 由 H2 集成测试上下文导出(**55 路径**),现网可用 `GET /api/v1/v3/api-docs` 重新导出。 ## 10. 错误码(新增 83xx) | 错误码 | 枚举 | HTTP | 含义 | |---|---|---|---| | 8301 | `MATCH_CLOSED` | 409 | 投递终态/岗位关闭,匹配已关闭 | | 8302 | `MATCH_SCORES_LOCKED` | 409 | 已 MATCHED,评分锁定 | | 8303 | `MATCH_REQUIRED_FOR_CONVERSATION` | 403 | 企业-求职者建会话需匹配成功 | | 8304 | `APPLICATION_NOT_MATCHED` | 409 | 投递未匹配,不可建会话 |