17 KiB
V41 企业人才发现推荐——详细设计(编码前评审)
状态:已实现并通过自动化与真实 MySQL/Docker 黑盒验收。自动化测试 1461/1461 通过,真实黑盒
PASS=37 / FAIL=0。本文是 V41 的唯一设计基线。
1. 目标与边界
1.1 目标
让已认证企业针对自己已发布的岗位,发现主动开启人才发现的学生候选人。系统用已批准的岗位画像和学生自愿提供的聚合信号做规则排序,返回数据最小化的候选人卡片。
这是一项“人才发现”能力,不替代投递、面试、Offer 或现有企业候选人看板:
- 现有
/company/candidates/**只展示已经投递的候选人; - V41 只展示尚未向当前企业投递、且显式同意被发现的候选人;
- V41 不创建投递、不发送消息、不创建邀约,也不让企业获得联系方式或简历文件。
1.2 确认范围
- 学生人才发现默认关闭,学生可随时自行开启或关闭;
- 只有
verified=true的 COMPANY,才可对其自有、PUBLISHED的岗位查询; - 仅已启用且 ADMIN 审核通过的 V39 岗位画像可以计算推荐;
- 复用 V39 的规则信号权重:学生偏好 40%、RIASEC 聚合兴趣 35%、四维能力聚合 25%;
- 已向当前企业任一岗位投递过的学生均从候选池排除,包含已撤回或已拒绝的历史投递,避免重复触达;
- 公司只得到匹配档位与安全概述,不得到 RIASEC、四维能力、候选评分、答卷、答案、题目或原始权重;
- 本版没有企业私信、邀约、收藏/淘汰反馈、企业端联系方式、学生被动通知、AI 解释或推荐历史快照。
1.3 明确不做
- 不改变 V35/V36 的测评题库、提交、重测、有效期或审计语义;
- 不改变 V38 的候选评分写入、排名和企业评分卡;
- 不改变 V39 的“学生找岗位”推荐及其岗位画像审核流;
- 不改变现有
resumeVisibility/contactVisible的含义; - 不按性别、年龄、籍贯、手机号、邮箱、学校排名、姓名或头像作为排序信号;
- 不生成“录用概率”“胜任结论”“适合/不适合”之类确定性结论。
2. 现状核查与设计前提
| 项目 | 现状 | V41 处理 |
|---|---|---|
| 最新迁移 | V38 为企业评价官方回复 | V41 建议下一编号 V39__enterprise_talent_discovery.sql |
| V39 岗位画像 | job_recommendation_profiles,仅 enabled=true 且 APPROVED 可用 |
作为岗位侧唯一的结构化推荐输入 |
| 学生偏好 | student_preferences 含城市、行业、岗位类型、用工类型、远程与薪资期望 |
只用于内部偏好匹配,不原样返回企业 |
| 兴趣测评 | V35 RIASEC 聚合结果 | 仅可作为内部聚合信号,绝不下发维度分 |
| 能力测评 | V36 完整且未过期的聚合结果 | 仅可作为内部聚合信号,绝不下发维度分 |
| 简历隐私 | resumeVisibility 只控制简历可见性,默认 PRIVATE |
不复用为人才发现授权;V41 另设明确授权 |
| 企业候选人看板 | 仅已投递的 application |
V41 只发现未向本企业投递过的候选人 |
resumeVisibility 与“是否允许被人才发现”是不同目的的授权:前者控制简历,后者控制是否进入企业发现候选池。两者不可混用或隐式推导。
3. 角色、权限与隐私
3.1 权限矩阵
| 能力 | STUDENT | COMPANY(未认证) | COMPANY(认证且岗位所属) | ADMIN | 未登录 |
|---|---|---|---|---|---|
| 查询/更新本人人才发现开关 | ✓ | 403/2002 | 403/2002 | 403/2002 | 401 |
| 查询岗位人才推荐 | 403/2002 | 403/2002 | ✓ | 403/2002 | 401 |
所有身份从 SecurityContext.getAuthentication().getName() 的 JWT subject 推导。请求中不得接受 companyId、studentUserId、candidateId 或任何越权身份参数。
3.2 候选人可见性规则
一个学生进入某企业某岗位的候选池,必须同时满足:
- 用户角色为
STUDENT,账号未进入注销终态; - 存在
student_talent_discovery_preferences且enabled=true;无记录一律视为关闭; - 存在可用于候选卡的基础学生档案;
- 与当前公司不存在任何历史
applications记录; - 当前 COMPANY 已认证,目标岗位归属该 COMPANY 且为
PUBLISHED; - 目标岗位有
APPROVED + enabled=true的 V39 岗位画像。
关闭开关后,后续查询立即不再返回该学生;不删除其档案、投递、测评、评分或审计历史。V41 首版不保存企业查询历史与候选人曝光历史,避免把“被浏览”固化为可追踪个人行为数据。
3.3 响应白名单
企业候选卡仅允许:
{
"candidateKey": "tdc_opaque_token",
"candidateLabel": "开放人才候选人",
"city": "合肥",
"education": "本科",
"graduationYear": 2027,
"skills": ["Java", "Spring Boot"],
"fitBand": "HIGH",
"summary": "岗位画像与候选人公开资料存在较高匹配度",
"signalsAvailable": ["PREFERENCE", "RIASEC", "ABILITY"]
}
candidateKey 是服务端签名的、与公司和岗位绑定的短期不透明标识,只用于前端列表稳定 key;它不等于 studentUserId,也不能在本版换取详情、联系方式或简历。
企业响应禁止出现:
studentUserId、用户 ID、简历 ID、手机号、邮箱、住址、简历 URL;- 姓名、头像、出生日期、性别、身份证明、学校排名;
- 测评 attempt/result ID、维度分、原始分、题目、答案、答案键、解析、评分规则;
candidate_dimension_scores明细、候选综合分、排名、百分位;- 画像原始权重、能力期望、内部规则版本、SQL/审计数据。
学生端开关响应只返回 enabled、version、createdAt、updatedAt;不返回企业查询过该学生的记录。
4. 接口契约(提案)
4.1 学生人才发现偏好
GET /profile/me/talent-discovery
PUT /profile/me/talent-discovery
GET 无记录时返回默认值,不写库:
{ "enabled": false, "version": 0 }
PUT 请求:
{ "enabled": true, "expectedVersion": 0 }
enabled必填;expectedVersion可选,传入时必须与当前版本一致;- 首次创建仅接受
expectedVersion缺失或0; - 并发冲突复用现有乐观锁错误
9201,不新增无必要错误码; - 成功只写
TALENT_DISCOVERY_PREFERENCE_UPDATED审计,details 仅有开关状态、版本、requestId 与时间。
4.2 企业岗位人才推荐
GET /companies/me/jobs/{jobId}/talent-recommendations?page=0&size=20
参数规范:page 从 0 起;size 为 1–50;jobId > 0。无候选人返回 HTTP 200:
{
"mode": "NO_ELIGIBLE_CANDIDATE",
"items": [],
"total": 0,
"page": 0,
"size": 20,
"totalPages": 0,
"message": "暂未发现已授权且满足基础条件的候选人"
}
目标岗位缺少可用画像时也返回 HTTP 200,但明确:
{
"mode": "UNAVAILABLE",
"items": [],
"message": "该岗位尚无审核通过的推荐画像"
}
不得把空候选池或不可用画像伪装成评分为 0 的推荐列表。
4.3 预计 OpenAPI
新增 2 个唯一 path / 3 个 operation(学生 GET/PUT 共用同一 path):
| 方法 | 路径 | 角色 |
|---|---|---|
| GET | /profile/me/talent-discovery |
STUDENT |
| PUT | /profile/me/talent-discovery |
STUDENT |
| GET | /companies/me/jobs/{jobId}/talent-recommendations |
已认证 COMPANY,且岗位归属本人 |
以当前 159 paths / 196 operations 为基线,预计更新到 161 paths / 199 operations。三个 operation 只形成两个唯一 URL path;实际以运行时 /v3/api-docs 和静态 openapi-rc.json 的对账结果为准。
5. 数据模型与迁移
5.1 新迁移
建议新增、且只新增:
V39__enterprise_talent_discovery.sql
不修改 V1–V38 已执行迁移;不向 candidate_dimension_scores 写入任何数据。
5.2 student_talent_discovery_preferences
| 字段 | 类型/约束 | 含义 |
|---|---|---|
id |
BIGINT PK | 主键 |
student_user_id |
BIGINT NOT NULL UNIQUE,FK users(id) |
仅本人一条授权记录 |
enabled |
TINYINT NOT NULL DEFAULT 0 | 人才发现显式授权 |
version |
BIGINT NOT NULL DEFAULT 0 | 乐观锁 |
created_at |
DATETIME NOT NULL | 创建时间 |
updated_at |
DATETIME NOT NULL | 最后更新 |
索引:(enabled, student_user_id)。关闭的记录不参与候选查询;不存在记录等效关闭。
不存:公司 ID、岗位 ID、企业浏览记录、联系方式、简历路径、测评细节、候选评分或算法分。
6. 规则计算与数据一致性
6.1 capturedNow
企业一次查询在服务起点生成唯一 capturedNow。同一次计算的能力有效期、候选过滤与输出必须复用该值,避免边界时刻同页结果不一致。
6.2 信号选择
对每位候选人仅读取以下最小聚合数据:
| 信号 | 有效条件 | 不可用处理 |
|---|---|---|
| 偏好 P | 可解析的城市、行业、岗位类型、用工类型、远程或薪资偏好 | 该信号不计入分母 |
| 兴趣 I | 最新 HOLLAND_RIASEC、COMPLETED,按 completed_at DESC, attempt_id DESC 取一条,六维完整且 0–100 |
不计入分母 |
| 能力 A | 最新 WORKPLACE_ABILITY、COMPLETED、valid_until > capturedNow,四维完整、每维 6 题、总计 24 题且 0–100 |
不计入分母 |
禁止取历史最高分。JSON 损坏、缺维、越界、空值、重复维或无效时间都只能令该信号不可用,不能造成 HTTP 500。
6.3 分数与排序
仅当至少一项可用信号存在时,计算:
fitScore =
(P × 40 + I × 35 + A × 25) / 可用信号权重之和
中间与最终结果使用 BigDecimal,最终保留两位、HALF_UP。fitScore 仅用于后端排序与映射档位,不对企业返回数值。
| 匹配档位 | 内部区间 | 企业展示 |
|---|---|---|
| HIGH | ≥ 75 | 较高匹配度 |
| MEDIUM | 60–74.99 | 中等匹配度 |
| BASIC | < 60 | 基础匹配度 |
排序:fitScore DESC → 候选档案最后更新时间 DESC → student_user_id DESC。最后一项只用于稳定排序,不对外暴露。
若没有任何有效信号,候选人不进入 V41 首版列表;不使用学校、姓名、性别等字段做“兜底排序”。
6.4 安全概述
summary 固定来自模板,只允许说明总体匹配等级。signalsAvailable 仅披露存在的类别 PREFERENCE / RIASEC / ABILITY,不披露任何单项高低、维度或权重。详细理由与可解释反馈明确留给后续“推荐理由和解释”模块评审。
7. 查询、性能与并发
7.1 查询策略
V41 第一版采用实时计算、全局排序后分页:不能先在数据库分页、只对当前页计算分数再排序,否则第 2 页候选人可能比第 1 页分数更高,分页结果会错误。
- 先用一次批量资格查询读取全部候选 ID 与必要的基础档案字段:授权开启、
STUDENT、档案存在、账号未处于注销限制状态、与当前公司不存在任何历史投递;历史投递通过NOT EXISTS或批量集合排除,不能只排除当前岗位,也不能按候选人逐条查询。 - 按固定批次(建议每批 500 个 ID)批量读取学生偏好、最新可用 RIASEC、最新有效能力结果和展示档案;候选 ID 必须使用参数绑定,禁止拼接 SQL。
- 服务层以内存 Map 汇总每个候选人的信号,计算所有候选人的
fitScore,再按fitScore DESC、档案updatedAt DESC、studentUserId DESC做全局排序,最后执行page/size切片。
这是第一版实时方案;当候选池达到规模阈值,应升级为异步推荐索引/快照,但不能以页内排序牺牲当前版本的全局排序正确性。查询需预留真实 MySQL EXPLAIN 验收:student_user_id、definition_id、completed_at、valid_until 与授权开关索引均应参与过滤。
7.2 并发
人才推荐列表是只读、实时计算,无持久化推荐记录,故无创建推荐的并发写入问题。学生开关通过 @Version + expectedVersion 控制并发,两个竞争更新只能有一个成功;另一个返回 409/9201。
查询过程中学生关闭开关的竞态允许下一次请求生效;同一已开始的只读事务可在当前页继续返回当时快照,不延迟或阻塞用户操作。
8. 错误语义与审计
8.1 错误
| 场景 | HTTP / 业务码 |
|---|---|
| 非认证 | 401 |
| 角色不符 | 403 / 2002 |
| 企业不存在 | 404 / 5001 |
| 岗位不存在/非发布 | 复用既有岗位错误码 |
| 岗位不属于当前企业 | 403 / 2002(不泄露额外信息) |
| 企业未认证 | 403 / 2002 |
| page/size/jobId 非法 | 400 / 1001 |
| 学生开关版本冲突 | 409 / 9201 |
本版不新增 V41 专属错误码。UNAVAILABLE 和 NO_ELIGIBLE_CANDIDATE 是成功响应模式,不是错误。
8.2 审计
仅学生开关写审计:
{
"enabled": true,
"version": 2,
"requestId": "..."
}
GET 推荐列表不写逐候选人或逐企业浏览审计,避免形成候选人追踪链路。不得审计候选人清单、matchScore、信号细节、学生 ID、联系方式、简历地址、答案、IP、User-Agent、token 或密码。
9. 安全与公平性门禁
- 学生显式同意是进入企业发现池的前置条件;默认关闭;
- 企业必须认证、岗位必须属于自己且公开发布;
- 推荐不是自动淘汰或录用依据,企业仍需在后续合法招聘流程中自行评估;
- 不使用受保护特征或联系方式排序;
- 任何测评聚合仅在学生自愿进入发现池后作为内部排序信号;企业侧不见测评详情;
- 若后续加入解释、邀约、反馈或模型训练,必须单列模块并重新做隐私、风控和产品评审。
10. 建议代码结构
com.mydreamjob.talentdiscovery
StudentTalentDiscoveryPreference
StudentTalentDiscoveryPreferenceRepository
TalentDiscoveryService
TalentDiscoveryCandidateSource # 批量只读数据来源
TalentDiscoveryScoreCalculator # 纯函数,无数据库访问
TalentDiscoveryController
dto/
允许复用 V39 的安全 JSON 解析与岗位画像校验,但不得复制 Controller 中的权限逻辑或把 V41 放入 V39 Controller 而扩大其职责。
11. 测试与验收方案
11.1 自动化测试
TalentDiscoveryScoreCalculatorTest- 三信号权重与缺项重归一化;
- 最新低分优先于历史高分;
- 能力恰在
capturedNow过期; - 缺维、非法 JSON、越界均不 500;
- 不使用姓名、性别、学校或联系方式作为输入。
TalentDiscoveryServiceTest- 默认关闭不入池;开启后入池;关闭后立即排除;
- 当前企业任一历史投递均排除;其他企业投递不排除;
- 未认证企业、跨企业岗位、无审核画像、未发布岗位;
- 候选卡白名单;无数据模式正确;N+1 查询次数固定。
TalentDiscoveryControllerTest- STUDENT/COMPANY/ADMIN/未登录矩阵;
- 分页校验;不泄露
studentUserId、联系方式、测评字段或分数; - 学生开关 PUT 乐观锁冲突。
TalentDiscoveryPersistenceIntegrationTest- V39 迁移、唯一约束、FK、默认关闭、
@Version冲突; - 批量查询与已投递排除在 H2 MySQL 模式工作。
- V39 迁移、唯一约束、FK、默认关闭、
11.2 真实 MySQL/Docker 黑盒
- V39 迁移成功且无影响 V1–V38;
- 默认关闭、开启、关闭后的立即可见性变化;
- 已认证/未认证企业、岗位所有权、PUBLISHED 与已审核画像门禁;
- 同企业历史投递排除、其他企业投递不误排;
- 最新低分优先、能力过期、无有效信号和无候选人模式;
- 响应脱敏、审计不写候选清单;
EXPLAIN与常数批量查询验证;- RabbitMQ 队列终态与 V39/V40 关键接口回归。
12. 文档、OpenAPI 与发布门禁
编码后更新:
docs/enterprise-talent-discovery-api.md;docs/frontend-integration.md;docs/backend-development-roadmap.md;docs/openapi-rc.json(V41 实际 161 paths / 199 operations,两个唯一 path 承载三个 operation);docs/verify_openapi.py。
发布门禁已完成:专项与全量自动化测试通过(1461/1461),OpenAPI 静态/运行时均为 161 paths / 199 operations,真实 MySQL/Docker 黑盒 PASS=37 / FAIL=0。黑盒覆盖 V39 迁移、授权状态机、最新版测评选择、同企业历史投递排除、全局排序后分页、权限与脱敏、队列终态及 V39 回归。
13. 编码前确认结论
负责人已确认本设计的业务范围。进入编码前还必须按本设计完成只读核查:最新 Flyway 为 V38、V39 岗位画像审核字段存在、认证企业判断可用、学生档案字段可满足候选卡白名单;如任一项与当前代码冲突,停止并报告,不自行改变接口、迁移号、隐私边界或排序口径。