23 KiB
V39:测评结果驱动岗位推荐设计
状态:编码完成,真实 MySQL/Docker 关键黑盒已通过,待 Git 检查点。 功能版本:V39。 当前基线:Flyway 迁移新增
V37__job_recommendation_profiles.sql;静态 OpenAPI 为154 paths / 190 operations。 文档用途:V39 第一版规则、接口、安全边界与验收口径的权威设计说明。
1. 目标与边界
1.1 目标
为学生提供“可解释、可复现、可关闭”的岗位推荐列表。推荐只使用学生已授权产生的偏好、V35 RIASEC 职业兴趣结果和 V36 四维能力结果,并结合企业为岗位维护的结构化推荐画像。
推荐的含义是“当前岗位与学生已知偏好的匹配程度”,不保证录用概率,不构成职业、心理或就业诊断。
1.2 第一版范围
- 仅 STUDENT 查看自己的推荐岗位。
- 仅推荐状态为
PUBLISHED且学生尚未投递的岗位。 - 使用学生偏好、最新完成的 RIASEC 结果、最新有效的四维能力结果和岗位推荐画像。
- 企业只能维护自己岗位的推荐画像;不能查询任何学生的测评明细、推荐分或推荐理由。
- 返回有限、白名单化的推荐理由和缺口提示。
- 规则实时计算;不保存学生级推荐历史、点击画像或行为画像。
1.3 明确不在本版范围
- 不使用 AI、大模型、简历文本向量或隐式行为画像。
- 不自动投递、不替学生收藏、不发送推荐通知。
- 不增加企业人才推荐或企业查看学生测评详情。
- 不接入 V38 之外的
GROWTH评分维度,不写candidate_dimension_scores。 - 不把 RIASEC 结果当作职业诊断,不根据测评结果拒绝学生投递。
- 实现“我不感兴趣”持久化,以及推荐解释的有帮助/无帮助反馈;首版不将反馈用于模型训练。
2. 现状核查结论
| 已有能力 | 当前状态 | 对 V39 的影响 |
|---|---|---|
| 岗位搜索 | /jobs 已支持城市、薪资、用工类型、关键词和分页 |
可复用公开岗位视图和发布状态口径 |
| 单岗位匹配诊断 | GET /jobs/{jobId}/match,按学生偏好计算 |
只适合单岗位说明;内部会写审计,不应在推荐列表中逐岗位循环调用 |
| 学生偏好 | 城市、薪资、用工类型、行业、岗位类型、远程意愿 | 可作为硬过滤和软匹配数据 |
| V35 RIASEC | 最新完成结果可提供六维兴趣聚合分 | 可作为兴趣匹配输入;不读取答案和题目 |
| V36 四维能力 | 最新有效完成结果可提供四维聚合分和有效期 | 可作为能力适配输入;不依赖 V38 开关 |
| V38 评分接入 | ABILITY 可有限进入候选评分 | V39 直接读 V36 聚合结果,不读取候选评分快照,避免推荐随评分开关改变 |
jobs 表 |
只有标题、描述、要求、城市、薪资、用工类型 | 缺少可靠的 RIASEC/能力要求字段,必须新增结构化岗位推荐画像 |
3. 总体方案
3.1 方案选择
采用“规则推荐 + 岗位结构化画像 + 实时只读计算”方案。
学生偏好 ─────────────┐
V35 RIASEC 聚合结果 ──┼─> RecommendationService ─> 推荐岗位列表 + 理由/缺口
V36 能力聚合结果 ─────┤ ↑
岗位推荐画像 ─────────┘ └─ 仅查询 PUBLISHED、未投递岗位
不能使用岗位标题、岗位描述或 requirements 进行关键词猜测来代替岗位画像:该做法不可解释、难以测试,也会把同义词、否定语句和描述质量差异误当成职业要求。
3.2 推荐画像的维护主体
企业在创建或维护岗位后,为本人岗位维护一份结构化推荐画像。画像只描述岗位,不包含学生数据;它包含:
- 行业代码与岗位族代码,用于与学生偏好精确比较;
- RIASEC 六维权重,用于表达该岗位偏好的兴趣侧重点;
- 四维能力期望值,用于表达完成该岗位所需的基础能力水平;
- 是否启用测评推荐。
企业画像是“岗位推荐参考”,不是招聘门槛。企业仍需通过原有岗位说明、投递、面试与 Offer 流程进行评估。
3.3 平台受控分类与审核流
行业代码和岗位族代码采用“平台维护、企业选择、不得自由输入”的两级受控字典:
行业大类(industry)
└─ 岗位族(job family)
└─ 企业保留自由填写的岗位标题(jobs.title)
该选择参考了主流招聘产品按大类、细分职类组织职位浏览和筛选的做法:分类用于匹配与检索,岗位标题保留企业对具体职位的表达。第一版字典由产品负责人确定编码、后端随 Flyway 种子发布;ADMIN 只审核企业的岗位画像,不可在线任意新增分类。分类变更必须通过代码评审、版本化种子和兼容迁移完成。
建议首批受控岗位族覆盖当前产品最常见的求职方向:互联网技术、产品、设计、运营/市场、销售/客户成功、人力/行政/财务/法务、生产制造/工程、教育/医疗、供应链/物流、零售/生活服务和综合管理。每个岗位族必须隶属唯一行业大类,编码一经发布不得重用。
企业首次保存或修改已审核画像后,画像进入 PENDING_REVIEW;只有 APPROVED 且 enabled=true 的画像可用于个性化推荐。ADMIN 使用固定理由码审批或驳回。企业主动停用画像可立即生效,不需要等待审核。
3.4 时间一致性
一次推荐请求在服务入口生成唯一的 capturedNow,V36 结果有效性、规则版本和返回时间均使用该时间点。不得在同一请求内多次调用 now() 导致结果边界不一致。
4. 数据来源与有效性
| 来源 | 选择规则 | 无数据/异常处理 | 禁止读取 |
|---|---|---|---|
| 学生偏好 | 本人 student_preferences,至多一条 |
缺字段时该偏好项不计入分母 | 其他学生偏好 |
| RIASEC | HOLLAND_RIASEC 类型、COMPLETED、按完成时间倒序和答卷 ID 倒序取一条 |
JSON 损坏、缺维、分数越界时兴趣信号不可用 | 原始答案、题目、questionCode |
| 四维能力 | WORKPLACE_ABILITY、COMPLETED、valid_until > capturedNow、按完成时间倒序和答卷 ID 倒序取一条 |
过期、缺维、不是 24/24 完成时能力信号不可用 | 原始答案、题目反馈、答案键、评分规则 |
| 岗位画像 | job_recommendation_profiles.enabled=true、review_status=APPROVED 且关联 PUBLISHED 岗位 |
无画像或待审核画像不宣称“测评匹配”;仅进入兜底列表 | 企业内部非公开数据 |
| 投递记录 | 当前学生与岗位的任何已有投递记录 | 已投递岗位从推荐候选中排除 | 其他学生投递数据 |
| 推荐反馈 | 本人对岗位的有效 NOT_INTERESTED 反馈 |
仅排除本人明确不感兴趣的岗位;理由反馈不改变排序 | 其他学生反馈 |
RIASEC 只使用六维聚合百分比;四维能力只使用 LOGIC、COMMUNICATION、EXECUTION、LEARNING 四个聚合分。两类结果均须通过服务端结构校验后才可参与排序。
5. 推荐候选、过滤与排序
5.1 硬过滤
推荐候选必须同时满足:
- 岗位状态为
PUBLISHED; - 当前学生不存在该岗位的任何投递记录,避免推荐一个无法再次投递的岗位;
- 若学生已设置期望城市,岗位城市必须命中;
- 若学生已设置期望薪资下限且岗位有薪资上限,学生下限不得高于岗位上限;
- 当前学生没有对该岗位提交有效的
NOT_INTERESTED反馈; - 岗位推荐画像存在、已启用且审核状态为
APPROVED时,才进入“个性化推荐”排序。
城市和薪资口径与现有单岗位匹配诊断保持一致。用工类型、行业和岗位族是软信号,不作为拒绝或隐藏条件。
5.2 三类软信号
| 信号 | 默认权重 | 计算方式 |
|---|---|---|
偏好适配 P |
40% | 城市 30、用工类型 20、薪资 20、行业 15、岗位族 15;只对学生已设置且岗位有对应结构化值的字段纳入分母 |
兴趣适配 I |
35% | Σ(RIASEC学生维度分 × 岗位该维权重) / Σ岗位兴趣权重 |
能力适配 A |
25% | 对每个已配置能力期望值:学生分数大于等于期望值记 100,否则 学生分 / 期望分 × 100;再取已配置维度平均值 |
总分公式:
recommendationScore =
(P × 40 + I × 35 + A × 25) / 可用信号权重之和
结果保留两位小数,使用 HALF_UP。缺失或无效信号不记为 0,而是从分母中移除;这样不会因为学生尚未完成某项测评而被无理由降分。
如果偏好、兴趣、能力三者都不可用,则该岗位不能标为个性化推荐。
5.3 排序与兜底
个性化岗位按以下顺序排序:
recommendationScore DESC;published_at DESC;job_id DESC。
若没有任何可用个性化结果,接口返回 mode=FALLBACK 的公开岗位兜底列表,只按发布时间排序,recommendationScore=null,并明确说明“完善偏好或完成测评后可获得个性化推荐”。兜底列表绝不伪装为个性化结果。
5.4 理由与缺口
每个个性化岗位最多返回 3 条理由和 2 条建议,均由固定白名单模板生成,例如:
PREFERENCE_CITY_MATCH:工作城市符合你的偏好;PREFERENCE_SALARY_MATCH:薪资范围满足你的期望;RIASEC_INTEREST_MATCH:职业兴趣与岗位侧重点较匹配;ABILITY_STRENGTH_MATCH:你的能力表现满足该岗位的主要侧重点;ABILITY_GAP:可优先提升与岗位相关的某项能力;PREFERENCE_GAP:用工类型或岗位族与当前偏好存在差异。
不返回 RIASEC 原始六维向量、能力答卷 ID、结果 ID、题目、答案、答案键、评分规则或企业配置原文。
6. 数据模型与 Flyway(建议,待确认)
6.1 迁移
当前最新迁移为 V36;建议新增:
V37__job_recommendation_profiles.sql
功能版本仍为 V39。该单一迁移创建岗位画像、受控分类和学生反馈三张相关表,不修改 V1–V36 的语义。
6.2 job_recommendation_taxonomies
平台受控字典表,随 job-taxonomy-v0.1 种子初始化。
| 字段 | 说明 |
|---|---|
code |
不可重用的稳定编码,主键 |
parent_code |
岗位族指向所属行业大类;行业大类为 NULL |
taxonomy_type |
INDUSTRY 或 JOB_FAMILY |
display_name |
前端展示名 |
active |
是否仍允许企业新选用 |
sort_order、catalog_version |
展示顺序与目录版本 |
服务端必须校验 industry_code 是有效行业大类、job_family_code 是该行业下有效岗位族;企业不得传任意文本或跨层级组合。
6.3 job_recommendation_profiles
| 字段 | 说明 |
|---|---|
id |
主键 |
job_id |
外键至 jobs.id,唯一,一岗一份画像 |
enabled |
是否参与个性化推荐,默认 false |
review_status |
PENDING_REVIEW、APPROVED、REJECTED、DISABLED |
industry_code |
必填、受控行业大类编码 |
job_family_code |
必填、受控岗位族编码 |
riasec_r 至 riasec_c |
六维兴趣权重,0..100,至少一个非零才可启用兴趣信号 |
logic_requirement 等四列 |
四维能力期望值,0..100;NULL 表示该维不参与能力适配 |
reviewed_by_user_id、reviewed_at |
最近一次审核人和审核时间,仅内部使用 |
review_reason_code |
审核/驳回的固定理由码,不存自由文本 |
profile_version |
乐观锁版本 |
created_at、updated_at |
审计时间 |
约束与索引:
UNIQUE(job_id);FOREIGN KEY(job_id) REFERENCES jobs(id);- 所有权重和能力期望必须在
0..100; - 建立
(enabled, review_status, job_id)索引,支撑可推荐画像的候选岗位查询; - 使用 JPA
@Version和expectedVersion防止企业并发覆盖配置。
6.4 student_job_recommendation_feedback
仅保存学生主动提交的最小化反馈,不保存自动点击画像。
| 字段 | 说明 |
|---|---|
id |
主键 |
student_user_id、job_id |
反馈所属学生和岗位 |
feedback_type |
NOT_INTERESTED、REASON_HELPFUL、REASON_NOT_HELPFUL |
reason_code |
可选固定理由码;不得提交自由文本、测评内容或个人信息 |
feedback_key |
student + job + type + reasonCode 组成的非空去重键 |
status |
ACTIVE 或 REVOKED;撤销不物理删除 |
idempotency_key |
同一学生请求幂等键唯一;同键异体返回冲突 |
created_at、updated_at、version |
审计时间与乐观锁 |
NOT_INTERESTED 有效时排除该岗位;理由是否“有帮助”只用于后续人工优化规则,不自动改变学生分、岗位分或排序公式。
7. API 设计(建议,待确认)
7.1 学生推荐列表
GET /profile/me/job-recommendations?page=0&size=20
权限:仅 STUDENT 本人。page 从 0 开始,size 范围 1–50。
响应核心字段:
{
"mode": "PERSONALIZED",
"rulesVersion": "job-recommendation-v0.1",
"items": [
{
"job": { "id": 101, "title": "后端开发工程师", "city": "合肥" },
"recommendationScore": 82.50,
"reasons": ["PREFERENCE_CITY_MATCH", "RIASEC_INTEREST_MATCH"],
"suggestions": ["ABILITY_GAP"],
"availableSignals": ["PREFERENCE", "RIASEC", "ABILITY"]
}
],
"page": 0,
"size": 20,
"total": 1,
"totalPages": 1,
"generatedAt": "2026-09-04T12:00:00"
}
岗位详情继续复用既有 GET /jobs/{jobId};如需查看单个岗位的偏好诊断,继续复用既有 GET /jobs/{jobId}/match。
7.2 企业岗位画像查询与更新
GET /companies/me/jobs/{jobId}/recommendation-profile
PUT /companies/me/jobs/{jobId}/recommendation-profile
仅岗位所属 COMPANY 可查询和更新。更新请求需带 expectedVersion,并仅接受固定字段:enabled、industryCode、jobFamilyCode、riasecWeights、abilityRequirements。新建/更新后进入 PENDING_REVIEW;只有企业主动 enabled=false 时可转为 DISABLED。
不提供 DELETE;企业通过 enabled=false 停用画像,保留配置和审计链路。
7.3 受控分类、学生反馈与 ADMIN 审核
GET /job-recommendation-taxonomies
POST /profile/me/job-recommendations/{jobId}/feedback
DELETE /profile/me/job-recommendations/{jobId}/feedback
GET /admin/job-recommendation-profiles?page=0&size=20&status=PENDING_REVIEW
POST /admin/job-recommendation-profiles/{jobId}/decision
分类接口只返回 code、parentCode、type、displayName、sortOrder 和 catalogVersion;无需登录即可让岗位发布和岗位搜索复用同一字典。
反馈创建必须携带 idempotencyKey。NOT_INTERESTED 的撤销只把对应记录改为 REVOKED;不删除审计记录。ADMIN 审核请求只接受 APPROVE 或 REJECT 与固定 reasonCode,不接受自由文本。
7.4 OpenAPI 预估
新增 6 个路径、8 个 operation:
148 paths / 182 operations → 154 paths / 190 operations
编码时必须以运行时 OpenAPI 为准更新 docs/openapi-rc.json 与 docs/verify_openapi.py,不允许仅修改静态计数。
8. 权限、隐私与审计
| 操作 | STUDENT | COMPANY | ADMIN | 未登录 |
|---|---|---|---|---|
| 查看本人推荐列表 | 200 | 403/2002 | 403/2002 | 401 |
| 提交/撤销本人推荐反馈 | 200 | 403/2002 | 403/2002 | 401 |
| 查看本人岗位匹配诊断 | 既有行为 | 既有行为 | 既有行为 | 既有行为 |
| 查看自己岗位画像 | 403/2002 | 仅所属岗位 200 | 403/2002 | 401 |
| 更新自己岗位画像 | 403/2002 | 仅所属岗位 200,进入待审核 | 403/2002 | 401 |
| 查询受控分类 | 200 | 200 | 200 | 200 |
| 查看待审核岗位画像 | 403/2002 | 403/2002 | 200 | 401 |
| 审批岗位画像 | 403/2002 | 403/2002 | 200 | 401 |
审计动作建议:
JOB_RECOMMENDATION_PROFILE_UPDATED:仅记录jobId、画像版本、是否启用、请求 ID 和时间;JOB_RECOMMENDATION_PROFILE_DECIDED:仅记录jobId、决定、固定理由码、画像版本、请求 ID 和时间;JOB_RECOMMENDATION_FEEDBACK_CREATED/JOB_RECOMMENDATION_FEEDBACK_REVOKED:仅记录jobId、反馈类型、固定理由码、请求 ID 和时间;JOB_RECOMMENDATIONS_VIEWED:可选,仅记录返回数量、规则版本和请求 ID;不得记录完整排序、测评分、题目、答案、手机、邮箱、IP、User-Agent 或 token。
公司端绝不获得学生的 RIASEC、四维能力、候选评分、推荐分、推荐理由或缺口。学生端也不获得企业配置的完整权重和能力期望值。
9. 模块边界、性能与并发
9.1 建议模块划分
JobRecommendationProfile/ Repository:岗位画像配置;JobRecommendationTaxonomy/ Repository:平台受控行业与岗位族字典;StudentJobRecommendationFeedback/ Repository:学生显式反馈及撤销状态;JobRecommendationProfileService:企业画像读取、所有权校验和乐观锁更新;JobRecommendationAdminService:待审核画像分页和固定决定流转;StudentRecommendationSource:一次读取学生偏好、最新有效 V35/V36 聚合结果;JobRecommendationCalculator:纯函数,执行结构校验、分数公式和理由模板;JobRecommendationService:过滤、分页、排序和响应白名单;JobRecommendationController:仅从SecurityContext获取当前用户 ID。
9.2 N+1 防护
推荐列表不得对每个岗位调用现有 MatchService.diagnose():该方法会逐岗位读数据并写审计,既会造成 N+1,也会污染审计。
推荐请求的目标查询上限为固定次数:
- 学生偏好一次;
- 最新 RIASEC 结果一次;
- 最新有效四维能力结果一次;
- 分页候选岗位及画像一次;
- 当前学生已投递岗位 ID 集合一次;
- 当前学生有效
NOT_INTERESTED岗位 ID 集合一次; - 页面企业信息批量加载一次。
岗位排序应使用单条参数化 SQL/JPA 查询按推荐表达式完成,或在有明确候选上限时使用固定批量加载;禁止“每岗位再查画像、公司、投递或测评”的循环查询。编码后必须用 Hibernate/JDBC Spy 断言查询次数不随页面 items 数线性增加。
9.3 并发
- 推荐查询只读、无幂等键要求;
- 企业更新画像、学生反馈撤销和 ADMIN 审核均使用
expectedVersion+@Version,冲突返回 409; - 岗位关闭、画像停用或学生完成投递后,下一次查询自然不再返回该岗位;不缓存旧结果。
10. 错误码(建议)
建议使用新段 10001–10009:
| 错误码 | HTTP | 含义 |
|---|---|---|
10001 |
404 | JOB_RECOMMENDATION_PROFILE_NOT_FOUND |
10002 |
400 | JOB_RECOMMENDATION_PROFILE_INVALID |
10003 |
409 | JOB_RECOMMENDATION_PROFILE_CONCURRENT_UPDATE |
10004 |
400 | JOB_RECOMMENDATION_INVALID_QUERY |
10005 |
400 | JOB_RECOMMENDATION_TAXONOMY_INVALID |
10006 |
409 | JOB_RECOMMENDATION_FEEDBACK_CONCURRENT_UPDATE |
10007 |
409 | JOB_RECOMMENDATION_FEEDBACK_IDEMPOTENCY_CONFLICT |
10008 |
404 | JOB_RECOMMENDATION_FEEDBACK_NOT_FOUND |
10009 |
409 | JOB_RECOMMENDATION_PROFILE_INVALID_REVIEW_STATE |
现有岗位不存在、岗位归属不匹配、未登录和角色越权继续使用既有错误码,不重复造码。
11. 功能开关、兼容与回滚
建议新增默认关闭的开关:
recommendation:
assessment:
enabled: ${RECOMMENDATION_ASSESSMENT_ENABLED:false}
开关关闭时,学生推荐接口只返回 FALLBACK 公开岗位列表,不查询测评结果、不生成个性化分;企业仍可维护岗位画像,为灰度开启做准备。
开关开启时规则版本为 job-recommendation-v0.1。回滚只需关闭开关;画像数据保留,不删除岗位、测评、投递、评分或审计数据。
V39 不改变:
- V35/V36 的题库、答题、重测、有效期和审计语义;
- V38 的候选评分开关、ABILITY 快照和 V32 排名口径;
- 既有
/jobs搜索、/jobs/{jobId}/match、投递和收藏接口语义。
12. 测试与验收计划
12.1 单元测试
- RIASEC 与四维能力来源选择:最新优先、过期回退、结构异常不抛 500;
- 公式:单信号、双信号、三信号、缺失重归一化、
HALF_UP、稳定排序; - 硬过滤:未发布、已投递、城市不符、薪资不符;
- 理由/建议白名单和最大数量;
- 受控分类父子校验、画像字段边界、启用条件和审核状态机;
- 反馈幂等、撤销、不感兴趣过滤和理由反馈不改变推荐分;
- 画像、反馈和审核的乐观锁冲突。
12.2 MockMvc 与持久化测试
- STUDENT/COMPANY/ADMIN/未登录权限矩阵;
- 企业只能读写自己的岗位画像,更新后必须进入待审核状态;
- ADMIN 只能审核、不可修改企业画像内容;非 ADMIN 审核返回 403;
- 分类接口仅返回受控字典;自由行业/岗位族编码返回 400;
- 推荐响应不含测评题目、答案、答案键、结果 ID、原始画像权重;
- 无画像/无测评返回
FALLBACK,不伪造个性化分; - 分页参数、空列表、推荐总数和稳定排序;
- 查询次数固定,禁止推荐列表 N+1;
- 不写
candidate_dimension_scores、不改变 V32 排名。
12.3 MySQL/Docker 黑盒
- 执行 V37 迁移,校验三张表、唯一约束、外键、范围约束、分类种子和索引;
- 企业创建/更新自己岗位画像,其他企业返回 403,ADMIN 审批后才可用于推荐;
- 学生完成 RIASEC 和能力测评后,推荐分与理由符合固定 fixture;
- 验证历史高分、最新低分和能力过期三种选择口径;
- 开关关闭返回
FALLBACK,开启后才产生PERSONALIZED; - 已投递、明确不感兴趣、关闭、城市/薪资不符岗位不会进入个性化列表;
- 反馈重复提交、同键重放、撤销重试和审核并发均满足预期业务码;
- 响应和审计不泄露题目、答案、评分规则、结果 ID 或凭据;
- RabbitMQ 队列终态正常,并回归 V34–V38 关键接口。
冻结条件:全量 mvn clean verify、运行时/静态 OpenAPI 对账、真实 MySQL/Docker 黑盒关键项全部 PASS,且 FAIL=0。
13. 已确认决策与编码前最后口径
负责人已确认:
- 新增岗位推荐画像与 Flyway
V37__job_recommendation_profiles.sql; - 接受默认排序权重:偏好 40%、RIASEC 35%、能力 25%;
- 无结构化岗位画像时仅返回
FALLBACK,不伪造测评推荐; - 任何已有投递的岗位均不再推荐;
- 默认关闭
recommendation.assessment.enabled,支持灰度; - 第一版加入学生“不感兴趣”持久化和推荐理由反馈;
- 按负责人第 8 项“是”处理:第一版需要 ADMIN 审核企业岗位画像。
基于第 6、7 项,原先的 3 个 operation 扩展为 8 个 operation,且迁移包含三个相关表。这是已确认产品范围变化,不是无关扩张。
编码开始前只需最终确认以下两项具体口径:
- 受控分类采用“平台种子维护、企业只能选择、不允许自由输入”,并按
job-taxonomy-v0.1首批目录落库; - ADMIN 审核语义确为“企业提交/更新画像后进入
PENDING_REVIEW,审核通过才参与推荐”。
若以上两项确认,即可进入 V39 编码;在此之前不得创建迁移、Controller 或前端接口实现。