# V38:四维能力测评有限接入候选人评分设计 > 状态:**已实现并通过真实 MySQL/Docker 关键黑盒验收,待建立 Git 检查点**。 > 功能版本:V38。 > 基线:Flyway `V1–V36`、OpenAPI `148 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 提交后跨模块写入候选评分表。 ```text V36 完成测评 └─ 写 ability_attempts / ability_results(既有行为,不调用评分引擎) 学生或企业读取既有候选评分卡 └─ CandidateScoringCalculator └─ AbilityAssessmentScoreSource(只读选择最新有效 ability_result) └─ AbilityScoreMapper(按 V38 固定权重计算 ABILITY) └─ 既有批量 upsert candidate_dimension_scores(既有评分快照机制) ``` 这样做的原因: 1. V36 仍可独立工作,测评提交失败或评分开关关闭都不会影响其完成、结果和历史查询。 2. 评分只在既有评分查询/重算时产生快照,沿用当前 `candidate_dimension_scores` 的写入模型。 3. 不需要异步消息、定时回填或跨服务事务;结果到期后下一次评分自然变为 `PENDING`。 4. 评分行的 `sub_scores_json` 保留最小化、可追溯的来源版本信息,不需新建投影表。 ## 4. 数据选择与有效性判定 ### 4.1 单一查询契约 新增一个只读仓库查询,语义为: ```text 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`(学习),则: ```text 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` 仅允许下列字段: ```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. 功能开关与上线策略 新增配置: ```yaml scoring: ability-assessment: enabled: false ``` | 模式 | 行为 | |---|---| | `false`(默认) | 不查询 V36 结果,`ABILITY=PENDING`;无评分行为变更 | | `true` | 查询最新有效结果,按本设计映射;仅在既有评分计算/重算时写候选评分快照 | 发布顺序: 1. 合入代码且以 `false` 发布;验证所有既有候选评分卡仍是 V37 行为。 2. 在验收/小范围环境设为 `true`,核验映射、脱敏、排名影响与回滚。 3. 经负责人确认后才在正式环境启用;启用后仍可立即设回 `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)` 索引进行学生级最新结果查询;实现前必须用 MySQL `EXPLAIN` 验证不扫全表。若实际查询计划无法命中索引,停止编码并单独评审最小索引迁移,不得临时修改既有 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 的顶层契约保持兼容: ```text 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 黑盒 1. 启动真实 MySQL/Redis/RabbitMQ 和最新 JAR;确认 health 200、Flyway 仍止于 V36、OpenAPI 148/182。 2. 使用独立学生账号完成一份 V36 测评,先验证提交后 `candidate_dimension_scores` 行数不变。 3. 开关 `false` 调用候选评分:`ABILITY=PENDING`;再在隔离环境将开关设为 `true`,调用评分:`ABILITY=OK` 且总分符合公式。 4. 核对 `sub_scores_json` 白名单;企业 API 不含四维、结果 ID、题目、答案、标签或评分规则。 5. 将结果有效期置为过去时间(只在独立验收数据上操作),重算后验证 `ABILITY=PENDING`;历史 V36 结果仍可由本人读取。 6. 创建两份符合 V36 重测规则的历史结果,验证取最新而不是最高;验证并发评分刷新无 500。 7. 队列最终 `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 确认以下边界,编码必须严格遵守: 1. 开关开启时,允许 `ABILITY` 参与候选综合分,并允许其按既有评分口径影响 V32 同岗位排名。 2. 仅在开关开启时,候选评分规则版本允许由 `candidate-scoring-v0.1` 升级为 `candidate-scoring-v0.2`。 3. 学生单维评分详情可展示四维聚合分、权重和有效期;企业端始终只见 `ABILITY` 聚合分与状态。 4. 本版采用无新 Flyway、无新公网接口的只读投影方案。 后续若要新增人工改分、企业端明细、V35/RIASEC 接入、`GROWTH` 映射、历史回填或任何公开接口,必须另行设计和授权,不得作为 V38 编码的附带改动。