18 KiB
V38:四维能力测评有限接入候选人评分设计
状态:已实现并通过真实 MySQL/Docker 关键黑盒验收,待建立 Git 检查点。 功能版本:V38。 基线:Flyway
V1–V36、OpenAPI148 paths / 182 operations、全量测试1427/1427(mvn clean verify,0 Failures / 0 Errors / 0 Skipped)。 本设计只接入 V36 四维能力测评,不接入 V35 RIASEC 兴趣测评。
1. 目标与边界
1.1 目标
在不改变 V36 测评主链路的前提下,将学生最新、完整、仍有效的四维能力测评结果映射为候选人评分中的 ABILITY 维度,使候选评分卡可有限地反映能力测评结果。
1.2 必须满足的边界
- 仅使用 V36
ability_results,不使用 V35 兴趣类型或职业方向结果。 - 仅映射到
ABILITY;不写入或推断GROWTH、ACTIVITY等其他维度。 - 不在测评提交时直接写
candidate_dimension_scores;V36 提交仍只写其ability_*表和既有审计。 - 不新增企业查看测评详情的接口。企业仅通过既有候选评分卡看到
ABILITY的聚合score/status。 - 不向任何评分接口返回题目、答案键、选项分值、题库标签、题目解析或测评答卷 ID。
- 功能开关默认关闭;关闭时现有
ABILITY=PENDING行为必须完全保持。 - 不引入 AI 评分、人工覆写、最高分挑选、自动封禁或对既有学生数据的回填。
2. 已确认的产品与规则口径
| 项目 | V38 锁定口径 |
|---|---|
| 可采纳来源 | V36 最新 COMPLETED 且 valid_until > capturedNow 的结果 |
| 四维权重 | LOGIC=35%、EXECUTION=30%、LEARNING=20%、COMMUNICATION=15% |
| 结果选择 | 只取最新有效完成结果;按 completed_at DESC, attempt_id DESC 稳定排序;绝不取历史最高分 |
| 有效期 | 复用 V36 的 7 天;过期结果仍可由学生查看历史,但不得参与新评分计算 |
| 重测治理 | 复用 V36:有效结果期间禁止重测,结果到期后可重测,滚动 90 天至多完成 3 次 |
| 企业可见性 | 仅既有公司评分卡的 dimensions[]{dimension,score,status};无四维细分、短板、答卷或题目反馈 |
| 默认发布状态 | OFF,不影响候选综合分、V32 排名或 V34 求职进度 |
3. 总体方案:评分时只读投影
采用评分时只读投影(read-through projection),而不是在 V36 提交后跨模块写入候选评分表。
V36 完成测评
└─ 写 ability_attempts / ability_results(既有行为,不调用评分引擎)
学生或企业读取既有候选评分卡
└─ CandidateScoringCalculator
└─ AbilityAssessmentScoreSource(只读选择最新有效 ability_result)
└─ AbilityScoreMapper(按 V38 固定权重计算 ABILITY)
└─ 既有批量 upsert candidate_dimension_scores(既有评分快照机制)
这样做的原因:
- V36 仍可独立工作,测评提交失败或评分开关关闭都不会影响其完成、结果和历史查询。
- 评分只在既有评分查询/重算时产生快照,沿用当前
candidate_dimension_scores的写入模型。 - 不需要异步消息、定时回填或跨服务事务;结果到期后下一次评分自然变为
PENDING。 - 评分行的
sub_scores_json保留最小化、可追溯的来源版本信息,不需新建投影表。
4. 数据选择与有效性判定
4.1 单一查询契约
新增一个只读仓库查询,语义为:
findLatestValidCompletedResult(studentUserId, typeCode, capturedNow)
查询必须同时满足:
ability_attempts.student_user_id = :studentUserId;ability_attempts.status = 'COMPLETED';ability_results.valid_until > :capturedNow;- 测评定义为 V36 能力测评类型且当前/答卷题集版本可识别;
- 排序固定为
completed_at DESC, ability_attempts.id DESC; - 最多取一条。
capturedNow 必须在一次评分计算开始时由后端生成并全程复用,不能在同一计算中多次调用 now(),以避免临界过期时出现不一致。
4.2 结果结构防御
即使查询到结果,也仅在以下条件全部成立时计算:
dimension_scores_json可被安全解析;- 恰有
LOGIC、COMMUNICATION、EXECUTION、LEARNING四维; - 每维分数均在
[0,100],且均为有限数值; - 每维已答题数均为 6,答卷总已答题数为 24;
overall_score、题集版本和 V36 规则版本存在。
实现细则(V38 发布前 P1):
answeredCount使用精确整数校验,仅接受数值意义上等于整数的表示 (如 JSON 整数 6 或 6.0、"6"、"6.0");6.9、6.5、"6.5"、溢出 int、空串、null均视为不可用 (stripTrailingZeros+intValueExact,不使用可能截断小数的intValue())。attemptAnsweredCount必须非空且严格等于 24,不允许null放行。
任一条件不满足时,不抛给前端 500,也不使用部分数据;该学生本次 ABILITY 返回 PENDING,原因写为“无可用的完整能力测评结果”。内部日志仅记录结果 ID 与结构错误类型,禁止记录题目、答案或选项分值。
4.3 过期与重测
- 无有效结果、仅有过期结果、或最新结果格式无效:
ABILITY=PENDING,score=null。 - 有效期内没有重测入口;到期后的新测评完成后,在下一次评分计算中自然替换为新结果。
- 结果选择不比较分数,因此不能通过“多次测到高分”挑选最高成绩。
- 已写入的旧月度快照保留为历史;当前月份按现有 upsert 语义在下一次评分查询/重算时刷新。
5. 分数映射
5.1 公式
设 V36 四维百分制结果为 L(逻辑)、C(沟通)、E(执行)、R(学习),则:
abilityScore = roundHalfUp(L × 0.35 + E × 0.30 + R × 0.20 + C × 0.15, 2)
分数范围为 0.00–100.00。所有中间计算使用 BigDecimal;最终 scale=2、HALF_UP。不得复用 V36 等权 overallScore 直接作为候选 ABILITY 分数,因为 V38 已确认使用不同的、显式的业务权重。
5.2 维度结果
| 条件 | ABILITY.status |
ABILITY.score |
对综合分的影响 |
|---|---|---|---|
| 开关关闭 | PENDING |
null |
与 V37 前完全一致,不参与平均 |
| 无有效完整结果 | PENDING |
null |
不参与平均 |
| 有效完整结果且开关开启 | OK |
映射后的 0–100 分 | 按既有候选评分规则参与可计算维度平均 |
| 来源读取异常/结构异常 | PENDING |
null |
不参与平均,记录脱敏内部告警 |
候选综合分继续遵循现有规则:只对 OK/ESTIMATED 且分数非空的维度重新归一化计算。故 V38 启用后会对拥有有效能力结果的学生产生真实影响;这是功能开关默认关闭的原因。
5.3 版本
- 能力映射规则版本:
ability-assessment-scoring-v0.1。 - 开关开启后的候选评分规则版本:
candidate-scoring-v0.2。 - 开关关闭时保留现有
candidate-scoring-v0.1。 - 每个写入的
ABILITY快照必须记录两类版本,保证规则升级后可解释、可重算。
6. 快照、溯源与数据最小化
不新增表、不新增 Flyway。复用 candidate_dimension_scores 现有的唯一键 (student_user_id, dimension, period) 与 sub_scores_json。
6.1 持久化的最小来源字段
ABILITY 行的内部 sub_scores_json 仅允许下列字段:
{
"source": "V36_ABILITY_ASSESSMENT",
"sourceResultId": 123,
"sourceCompletedAt": "2026-09-04T10:00:00Z",
"validUntil": "2026-09-11T10:00:00Z",
"questionSetVersion": "ability-v0.1",
"assessmentRulesVersion": "ability-v0.1",
"mappingRulesVersion": "ability-assessment-scoring-v0.1",
"weights": {"LOGIC": 35, "EXECUTION": 30, "LEARNING": 20, "COMMUNICATION": 15},
"dimensionScores": {"LOGIC": 80.00, "EXECUTION": 75.00, "LEARNING": 70.00, "COMMUNICATION": 65.00}
}
禁止写入:答卷 ID、题目 ID、题目编码、题库标签、题干、选项、用户选择、答案键、选项分值、题目解析、手机号、邮箱、令牌、请求体和完整审计详情。
sourceResultId 仅作内部溯源,任何 HTTP 响应均不得返回该字段。
6.2 响应投影
- 学生综合卡:沿用既有
ABILITY聚合分和状态。 - 学生单维详情:可返回四维聚合分、权重、有效期、规则版本和说明,用于解释自己的分数;不得返回
sourceResultId或题目级内容。 - 企业候选评分卡:保持既有字段,只返回
ABILITY的dimension/score/status。 - 趋势接口:只使用已写候选快照,不回读题目或 V36 原始答案。
7. 功能开关与上线策略
新增配置:
scoring:
ability-assessment:
enabled: false
| 模式 | 行为 |
|---|---|
false(默认) |
不查询 V36 结果,ABILITY=PENDING;无评分行为变更 |
true |
查询最新有效结果,按本设计映射;仅在既有评分计算/重算时写候选评分快照 |
发布顺序:
- 合入代码且以
false发布;验证所有既有候选评分卡仍是 V37 行为。 - 在验收/小范围环境设为
true,核验映射、脱敏、排名影响与回滚。 - 经负责人确认后才在正式环境启用;启用后仍可立即设回
false。
关闭开关只停止后续映射,不删除已存在的快照或 V36 数据。若需消除当前月份的已映射 ABILITY,必须走单独、经批准的重算流程,不能在开关切换时隐式删除数据。
8. 模块职责与实现边界
建议新增以下内部组件:
AbilityAssessmentScoreSource:一次查询并解析最新有效 V36 结果;不暴露实体给 Controller。AbilityScoreMapper:纯函数,校验四维、计算权重分、构造最小化来源描述。CandidateScoringCalculator#computeAbility(...):替换现有硬编码PENDING;开关关闭时保留旧分支。CandidateScoringService:每次评分计算只调用一次AbilityAssessmentScoreSource,不在八维循环内查询。
不得:
- 改动
AbilityAttemptService.submit()的事务边界、状态机、重测限制或审计。 - 让评分模块更新
ability_attempts、ability_answers、ability_results。 - 在 Controller 接受
studentUserId、能力分或结果 ID 来影响评分。 - 新增企业端能力测评详情、管理员修改能力分或公开的“重算某学生能力分”接口。
9. 并发、一致性与性能
- 评分计算使用一次
capturedNow和最多一次 V36 来源查询;不得按四维各查一次,也不得循环读取题目/答案。 - 已有候选评分批量 upsert 继续处理同月并发刷新;V38 不新增新的评分写表。
- V36 提交与评分查询并发时:评分要么读取提交前无结果并返回
PENDING,要么读取完整、已提交的结果并返回OK;不能读取半成品。 - 结果刚好到期时以
capturedNow判定;该次计算结束后不回滚同次快照,下一次计算按新时间重新判定。 - 利用既有
ability_attempts(student_user_id, definition_id, completed_at)索引进行学生级最新结果查询;实现前必须用 MySQLEXPLAIN验证不扫全表。若实际查询计划无法命中索引,停止编码并单独评审最小索引迁移,不得临时修改既有 V35 迁移。
10. 权限、隐私与审计
10.1 权限矩阵
| 数据 | STUDENT | COMPANY | ADMIN |
|---|---|---|---|
| 本人完整 V36 结果与题目反馈 | 仅本人 | 禁止 | 禁止(V36 原有口径) |
候选评分 ABILITY 聚合分 |
本人 | 仅有岗位归属的候选评分卡 | 禁止(沿用现有评分接口) |
| 四维聚合分与有效期 | 本人单维评分详情 | 禁止 | 禁止 |
| 题目/答案键/评分规则/标签 | 禁止通过评分接口返回 | 禁止 | 禁止 |
10.2 审计
V38 不对普通评分查询新增逐次审计,避免读取评分卡时产生审计噪声。现有评分治理申诉/回滚审计保持原样。
如内部结构无效需记录诊断日志,日志白名单仅为 studentUserId(内部日志)、sourceResultId、failureKind、mappingRulesVersion 和 requestId;禁止记录测评内容。不得将该诊断写入对企业可见的响应或 audit_logs.details。
11. 评分治理与申诉兼容
- 既有
ABILITY申诉/评分回滚若可用,回滚仍然是按当前数据确定性重算,不恢复人工指定的旧能力分。 - V38 不新增人工修改、豁免、补分或上传证明材料的入口。
- 来源结果在回滚时已过期或无效,则重算为
PENDING;这是规则结果,不属于回滚失败。 - V36 测评本身的题目、答案或判分争议不通过候选评分申诉接口处理,后续需单独定义测评申诉治理。
12. 接口与 OpenAPI 影响
本版不新增、删除或修改公网路径;既有候选评分 API 的顶层契约保持兼容:
GET /profile/me/candidate-scoring
GET /profile/me/candidate-scoring/dimensions/ABILITY
GET /profile/me/candidate-scoring/trend
GET /company/candidates/{applicationId}/scoring
因此静态和运行时 OpenAPI 均应继续为 148 paths / 182 operations。
学生的 ABILITY 单维详情可在既有通用 subScores/reasons/dataSources 容器中增加白名单字段;实现前须确认这些对象的现有 schema 允许兼容增加字段。若需要新增可见字段,必须同步 openapi-rc.json、verify_openapi.py 与联调文档,但不得改变公司端 schema。
13. 错误码
V38 不新增公网接口,正常的“无有效测评结果”属于 ABILITY=PENDING,不是 HTTP 错误。因此不新增 ErrorCode。
任何来源 JSON 异常、查询异常或规则版本无法识别都必须被隔离为 PENDING,并保留 200 的既有评分响应。数据库不可用等系统级故障仍按全局异常处理器既有语义处理,不伪造评分结果。
14. 测试计划
14.1 单元测试
- 开关关闭:不查询 V36 来源,
ABILITY=PENDING,候选评分规则版本仍为 v0.1。 - 无结果、仅过期结果、结构不完整结果、分数越界结果:均为
PENDING,无 500。 - 有效结果:严格验证权重公式、两位小数
HALF_UP、OK状态、来源版本。 - 多份有效历史:选
completed_at最新者,不选总分最高者;同时间用 attempt ID 稳定决胜。 - 来源字段白名单:持久化 JSON 和 HTTP 响应均不含答卷 ID、题目、答案、答案键、分值、标签。
computeAbility只触发一次来源查询;不得产生 N+1。
14.2 集成与 MockMvc 测试
- H2 MySQL 模式下完整 V36 有效结果 → 候选评分写入
ABILITY快照;过期后重算同周期ABILITY=PENDING。 - V36 提交本身不写
candidate_dimension_scores;只有既有候选评分查询/重算写快照。 - 学生单维详情可见聚合四维和有效期;公司端只见
ABILITY总分/状态;COMPANY/ADMIN/匿名权限回归。 - 并发 V36 提交与候选评分读取不出现半结果、500 或重复快照异常。
- V32 排名和 V34 职业进度在开关关闭时字节级/字段级保持现有结果;开关开启时仅允许因
ABILITY可计算带来的预期分数变化。
14.3 MySQL/Docker 黑盒
- 启动真实 MySQL/Redis/RabbitMQ 和最新 JAR;确认 health 200、Flyway 仍止于 V36、OpenAPI 148/182。
- 使用独立学生账号完成一份 V36 测评,先验证提交后
candidate_dimension_scores行数不变。 - 开关
false调用候选评分:ABILITY=PENDING;再在隔离环境将开关设为true,调用评分:ABILITY=OK且总分符合公式。 - 核对
sub_scores_json白名单;企业 API 不含四维、结果 ID、题目、答案、标签或评分规则。 - 将结果有效期置为过去时间(只在独立验收数据上操作),重算后验证
ABILITY=PENDING;历史 V36 结果仍可由本人读取。 - 创建两份符合 V36 重测规则的历史结果,验证取最新而不是最高;验证并发评分刷新无 500。
- 队列最终
ready=0/unacked=0;回归 V32 排名、V34 职业进度、V35 RIASEC、V36 能力结果、V37 风险概览。
15. 回滚与冻结门槛
15.1 回滚
| 情况 | 操作 |
|---|---|
| 映射分数不符合预期 | 设 scoring.ability-assessment.enabled=false,停止后续接入 |
| 需要恢复当前月评分 | 经批准后调用既有评分重算流程;不直接删表或手改快照 |
| 发现题库/测评结果质量问题 | 保持 V36 历史,关闭 V38 开关;不篡改历史题目或答案 |
V38 不新增迁移,故不存在本模块 DDL 回滚。不得修改已执行的 V35/V36 Flyway 文件。
15.2 冻结条件
- 所有 V38 单元、集成、MockMvc 测试通过;全量
mvn clean verify通过。 python docs/verify_openapi.py通过且 OpenAPI 仍为 148/182。- 真实 MySQL/Docker 黑盒为
FAIL=0,关键项不得 SKIP。 - 开关关闭和开启两条路径均通过;V36 主链路、V32/V34/V35/V37 回归通过。
- 不泄露题目、答案、答案键、评分规则、标签、来源结果/答卷 ID;企业端仅有聚合能力分。
- 未对 V35/V36 题库或既有 Flyway 做回写;没有未授权的业务数据删除或全量回填。
16. 已确认的编码授权边界
负责人已于 2026-09-04 确认以下边界,编码必须严格遵守:
- 开关开启时,允许
ABILITY参与候选综合分,并允许其按既有评分口径影响 V32 同岗位排名。 - 仅在开关开启时,候选评分规则版本允许由
candidate-scoring-v0.1升级为candidate-scoring-v0.2。 - 学生单维评分详情可展示四维聚合分、权重和有效期;企业端始终只见
ABILITY聚合分与状态。 - 本版采用无新 Flyway、无新公网接口的只读投影方案。
后续若要新增人工改分、企业端明细、V35/RIASEC 接入、GROWTH 映射、历史回填或任何公开接口,必须另行设计和授权,不得作为 V38 编码的附带改动。