轻松聘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.
 
 
 

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 硬过滤

推荐候选必须同时满足:

  1. 岗位状态为 PUBLISHED;
  2. 当前学生不存在该岗位的任何投递记录,避免推荐一个无法再次投递的岗位;
  3. 若学生已设置期望城市,岗位城市必须命中;
  4. 若学生已设置期望薪资下限且岗位有薪资上限,学生下限不得高于岗位上限;
  5. 当前学生没有对该岗位提交有效的 NOT_INTERESTED 反馈;
  6. 岗位推荐画像存在、已启用且审核状态为 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 排序与兜底

个性化岗位按以下顺序排序:

  1. recommendationScore DESC;
  2. published_at DESC;
  3. 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,也会污染审计。

推荐请求的目标查询上限为固定次数:

  1. 学生偏好一次;
  2. 最新 RIASEC 结果一次;
  3. 最新有效四维能力结果一次;
  4. 分页候选岗位及画像一次;
  5. 当前学生已投递岗位 ID 集合一次;
  6. 当前学生有效 NOT_INTERESTED 岗位 ID 集合一次;
  7. 页面企业信息批量加载一次。

岗位排序应使用单条参数化 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 黑盒

  1. 执行 V37 迁移,校验三张表、唯一约束、外键、范围约束、分类种子和索引;
  2. 企业创建/更新自己岗位画像,其他企业返回 403,ADMIN 审批后才可用于推荐;
  3. 学生完成 RIASEC 和能力测评后,推荐分与理由符合固定 fixture;
  4. 验证历史高分、最新低分和能力过期三种选择口径;
  5. 开关关闭返回 FALLBACK,开启后才产生 PERSONALIZED;
  6. 已投递、明确不感兴趣、关闭、城市/薪资不符岗位不会进入个性化列表;
  7. 反馈重复提交、同键重放、撤销重试和审核并发均满足预期业务码;
  8. 响应和审计不泄露题目、答案、评分规则、结果 ID 或凭据;
  9. RabbitMQ 队列终态正常,并回归 V34–V38 关键接口。

冻结条件:全量 mvn clean verify、运行时/静态 OpenAPI 对账、真实 MySQL/Docker 黑盒关键项全部 PASS,且 FAIL=0。

13. 已确认决策与编码前最后口径

负责人已确认:

  1. 新增岗位推荐画像与 Flyway V37__job_recommendation_profiles.sql;
  2. 接受默认排序权重:偏好 40%、RIASEC 35%、能力 25%;
  3. 无结构化岗位画像时仅返回 FALLBACK,不伪造测评推荐;
  4. 任何已有投递的岗位均不再推荐;
  5. 默认关闭 recommendation.assessment.enabled,支持灰度;
  6. 第一版加入学生“不感兴趣”持久化和推荐理由反馈;
  7. 按负责人第 8 项“是”处理:第一版需要 ADMIN 审核企业岗位画像。

基于第 6、7 项,原先的 3 个 operation 扩展为 8 个 operation,且迁移包含三个相关表。这是已确认产品范围变化,不是无关扩张。

编码开始前只需最终确认以下两项具体口径:

  1. 受控分类采用“平台种子维护、企业只能选择、不允许自由输入”,并按 job-taxonomy-v0.1 首批目录落库;
  2. ADMIN 审核语义确为“企业提交/更新画像后进入 PENDING_REVIEW,审核通过才参与推荐”。

若以上两项确认,即可进入 V39 编码;在此之前不得创建迁移、Controller 或前端接口实现。