# R01-C 独立代码复验报告(评分与推荐规则结构收束) 日期:2026-09-18 分支:`refactor/r01-structure-performance` HEAD:`9954600650c3e7cd40a5f4fa55bc38595973f214`(复验全程未变) 复验对象:`docs/r01-c-scoring-recommendation-rules-refactor-report.md` 性质:**仅复验** —— 未修改生产代码、未放宽既有断言、未暂存、未提交、未推送、未进入 R01-E --- ## 0. 结论 > **R01-C独立代码复验通过,可以进入精确暂存与独立提交检查点。** - `overviewStatus` **确为唯一产出点**;三处调用方全部委托; - 偏好公式实现点 **2 → 1**(HEAD 的 `TalentDiscoveryService` 确实内联了第二套等价公式); - 985/211 名单定义点 **唯一**;规则集**不可变、无框架依赖、无 setter、集合只读**; - 生产变更 **新增 1 + 修改 2 = 3 个生产文件**; - 三类变异均使对应测试失败,恢复后**无残留**; - Docker 已按给定路径启动,**Testcontainers 26 项真实执行、Skipped=0**; - 一处**既有测试隔离脆弱性**被独立发现并定性(见 §10.3),**与 R01-C 无关**。 --- ## 1. 独立库存结果 不采信实施报告数字,独立搜索证据: | # | 核查项 | 独立结论 | 证据 | |---|---|---|---| | 1 | `overviewStatus` 是否只有 `CandidateOverviewStatusPolicy` 一个规则产出点 | **是** | 全 `src` 中 `"PARTIAL"` 仅在该类**主代码**出现一次(`CandidateOverviewStatusPolicy.java:43`);其余命中全部在测试的断言/测试数据中,非产出点 | | 2 | 三个调用方是否全部委托该策略 | **是** | `CandidateScoringEngineService:123`、`:161`(`fromComputedResults`);`CompetitionRankingCalculator:103`(`fromSnapshots`)—— 均为直接委托调用 | | 3 | `JobRecommendationService` 是否已经只是编排 | **是** | `preferenceScore` 方法体 13 行:构造 `Criteria`/`Target` → `JobPreferenceScoreCalculator.calculate(...)`,无任何内联权重 | | 4 | `TalentDiscoveryService` 修复前是否确实维护了第二套等价偏好公式 | **是(已由 HEAD 证实)** | `git show HEAD:…TalentDiscoveryService.java` 的 `preferenceScore` 为内联 `total += 30/20/20/15/15` + `BigDecimal.valueOf(got*100L).divide(...,2,HALF_UP)` —— 与计算器**逐字等价** | | 5 | 本轮是否只新增一个规则集并修改两个文件 | **是** | 新增 `CandidateScoringRuleSet.java`;修改 `CandidateScoringCalculator.java`、`TalentDiscoveryService.java` | | 6 | **生产代码变更总数** | **新增 1 + 修改 2 = 3 个生产文件** | 与 `git diff --name-status` + 未跟踪文件清单一致 | --- ## 2. overviewStatus 唯一产出点证据 ``` main/java/com/mydreamjob/candidatescore/CandidateOverviewStatusPolicy.java:43: return "PARTIAL"; ``` `"PARTIAL"` 在**主代码**中的唯一出现点即为该策略类(第 43 行)。 状态聚合器**必须**能够产出 `PARTIAL`,因此该字符串的出现位置即为产出点的充要证据。 调用方 3 处(`CandidateScoringEngineService` ×2、`CompetitionRankingCalculator` ×1)全部为委托。 **结构门禁已加入独立复验测试**:扫描 `src/main/java/com/mydreamjob` 全部 `.java`, 断言含 `"PARTIAL"` 的文件**恰为** `CandidateOverviewStatusPolicy.java`。 --- ## 3. 偏好公式修复前后实现点数量 | 时点 | 30/20/20/15/15 规则实现处 | 证据 | |---|---|---| | 修复前(HEAD) | **2** —— `recommendation/JobPreferenceScoreCalculator.java` + `talentdiscovery/TalentDiscoveryService.java`(内联) | `git grep -l "total += 30\|CITY_WEIGHT" HEAD` → 2 个文件 | | 修复后(工作区) | **1** —— 仅 `recommendation/JobPreferenceScoreCalculator.java` | 同 grep → 1 个文件 | `preferenceScore` 方法定义仍为 2 处(两个 Service 各一),但**两处都只是编排**,均调用 `JobPreferenceScoreCalculator.calculate(...)`;规则**实现**唯一。 --- ## 4. 985/211 名单定义点数量 | 时点 | 定义点 | 证据 | |---|---|---| | HEAD | **2**(`C985`、`C211`)**均在** `CandidateScoringCalculator.java` | `git grep "Set C985\|Set C211" HEAD` | | 工作区 | **2**(`C985`、`C211`)**均在** `candidatescore/rules/CandidateScoringRuleSet.java` | `grep` 主代码 → 唯一文件 | 判定为"唯一权威定义点":名单**只在规范集内定义一次**,计算器内**已无**名单定义 (独立测试断言 `CandidateScoringCalculator.java` 不含 `Set C985` / `C211`)。 | 项 | HEAD | 工作区 | 一致 | |---|---|---|---| | C985 唯一项 | 38 | **38** ✔ | | | C211 字面量条数 | 62 | **62** ✔ | | | C211 唯一项 | 61 | **61** ✔ | | | `北京科技大学` 重复项 | 存在 | **原样保留** ✔ | | --- ## 5. 规则集不可变性和依赖边界 逐项核验 `CandidateScoringRuleSet.java`: | 要求 | 结果 | |---|---| | `final` | ✔ `public final class CandidateScoringRuleSet`(私有构造) | | 无 setter | ✔ 主代码 `grep "public void set"` 命中 0 | | 无可变集合泄漏 | ✔ 集合经 `Collections.unmodifiableSet(new HashSet<>(...))` 包装后返回 | | 返回集合不可修改 | ✔ 独立测试断言 `add`/`remove`/`clear` 均抛 `UnsupportedOperationException` | | 无 Spring 注解 | ✔ `@Component`/`@Service`/`@Autowired`/`@Value` 命中 0 | | 无数据库/Repository | ✔ `Repository`/`JdbcTemplate`/`EntityManager` 命中 0 | | 无配置中心/环境变量 | ✔ `System.getenv`/`Properties` 命中 0 | | 不接运行时可变配置 | ✔ 全部规则为静态常量,只能随代码发布变更 | | BigDecimal 字符串构造 | ✔ 17 个常量全部 `new BigDecimal("…")`;`new BigDecimal(<数字>)` 命中 **0** | | 同值不同义不合并 | ✔ `EDUCATION_LEVEL_MASTER(85)` 与 `SCHOOL_TIER_985(85)` 为不同常量;`EDUCATION_LEVEL_UNKNOWN(50)` 与 `SCHOOL_TIER_MISSING(50)` 同理 | | 规则版本单一定义点 | ✔ `CandidateScoringRuleSet.VERSION` 为权威;`CandidateScoringCalculator.RULES_VERSION` 现**指向该常量**(赋值而非字面量) | --- ## 6. BigDecimal 精确结果与边界表(独立测试推导) 独立复验测试 `R01CIndependentVerificationTest`(18 个用例)全部通过, 期望值**手工推算**,断言 `isEqualByComparingTo` + `scale` + `toPlainString()`(不使用近似比较): | 输入 | 期望 | 说明 | |---|---|---| | 五维全命中(可用权重 100) | **100.00** | 上界 | | 五维全缺失 / 零分母 | **null** | 非 0.00 | | 城市(30) 命中、其余缺失 | **100.00** | 单维独立 | | 用工类型(20) 命中、其余缺失 | **100.00** | | | 薪资(20) 命中(边界相等 20000 == 20000) | **100.00** | 相等视为命中 | | 行业(15) 命中、其余缺失 | **100.00** | | | 岗位族(15) 命中、其余缺失 | **100.00** | | | 城市命中(30) + 行业未命中(15) → 30/45 | **66.67** | HALF_UP 舍入 | | 城市未命中(30) + 行业命中(15) → 15/45 | **33.33** | | | 四方存在命中两项 → 15/65 | **23.08** | | | 薪资低一单位(19999 < 20000) | **0.00** | 下界 | | 薪资高一单位(20001) | **100.00** | | | 岗位 salaryMax 为 null | **0.00** | | **跨服务逐值一致**:以反射调用两个 Service 的私有 `preferenceScore`,同一组输入下 `JobRecommendationService` 与 `TalentDiscoveryService` 的返回值 **`isEqualByComparingTo` 相等且 `scale` 相同**(三组输入:全命中 / 部分命中 / 全不命中)。 **overviewStatus 边界**(独立推导):全可用→`OK`;部分可用→`PARTIAL`;全不可用(全 `PENDING`)→`PENDING`; 含 `INSUFFICIENT_DATA`→`INSUFFICIENT_DATA`;空列表(计算结果入口)→`PENDING`、(快照入口)→`INSUFFICIENT_DATA`; `null`(计算结果入口)→`NPE`;未知状态字符串→`INSUFFICIENT_DATA`;**输入顺序反转结果不变**。 --- ## 7. 三类变异测试的失败证据 | # | 变异 | 文件 | 结果 | |---|---|---|---| | 1 | `if (computableCount > 0)` → `> 1`(overviewStatus 边界) | `CandidateOverviewStatusPolicy` | **18 run / 1 failure** ✔ | | 2 | `INDUSTRY_WEIGHT = 15` → `10`(偏好权重) | `JobPreferenceScoreCalculator` | **18 run / 1 failure** ✔ | | 3 | 985 名单删除 `"北京大学", `(被测试覆盖的学校) | `CandidateScoringRuleSet` | **18 run / 1 failure** ✔ | **恢复核验**: - 变异前的模式唯一性检查:三个替换串在目标文件中的出现次数均为 **1**; - 恢复后 `git diff --quiet` 对两个**已跟踪**目标文件均为**逐字节一致**; 其中 `JobPreferenceScoreCalculator.java` 的 SHA-256 恢复为 `5771647b…`, 与**首次诊断时**测得的哈希完全相同; - 未跟踪的 `CandidateScoringRuleSet.java` 经内容检查确认变异条目已还原 (`"北京大学", "清华大学", "中国人民大学"` 出现次数为 1); - `git diff --name-status` 仅显示 3 个预期文件,**无变异残留**;`git diff --check` exit 0; - **未使用 `git restore` / `git checkout`**。 > 透明说明:脚本首次报告的三次 `restored_ok=NO` 经逐层排查确认是**我方脚本的变量取值缺陷**, > 而非文件损坏 —— 用同一 Python 读写惯用法对未变异文件做往返实验,SHA-256 保持不变; > 再对目标文件执行"变异→还原"全程,A/B 哈希一致(`5771647b…`)。 > 上述三项内容级核验(git 逐字节一致 + 哈希比对 + 变异条目还原)均已独立完成。 --- ## 8. Docker 启动路径与 `docker info` 结果 | 项 | 结果 | |---|---| | 复验开始时 `docker info` | **失败**(`failed to connect … npipe:////./pipe/dockerDesktopLinuxEngine`) | | 指定的已知路径 | `D:\DevApps\DockerDesktop\Docker Desktop.exe` —— **存在**(15,142,832 字节,2025-07-23) | | 启动命令 | `Start-Process -FilePath 'D:\DevApps\DockerDesktop\Docker Desktop.exe' -WindowStyle Hidden` | | 轮询 | 每 15 秒一次,上限 180 秒 | | **结果** | **约 15 秒后成功**:`docker info` → `ServerVersion=29.6.2` ✔ | 未重建、删除或清理任何容器、volume 或 image。 --- ## 9. Testcontainers 实际执行数 ``` mvn -Dtest="JobSearchSpecificationMySqlIntegrationTest,CandidateScoringRefreshMySqlIntegrationTest,JobSearchQuerySideEffectMySqlIntegrationTest,JobSearchQueryExplainMySqlIntegrationTest" test → Tests run: 26, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS ``` | 测试类 | Tests | Skipped | |---|---|---| | `JobSearchSpecificationMySqlIntegrationTest` | 15 | **0** | | `CandidateScoringRefreshMySqlIntegrationTest` | 6 | **0** | | `JobSearchQuerySideEffectMySqlIntegrationTest` | 4 | **0** | | `JobSearchQueryExplainMySqlIntegrationTest` | 1 | **0** | | **合计** | **26** | **0** ✔ | 这 26 项正是此前因 Docker 不可用而跳过的全部用例,现已**真实执行并通过**。 --- ## 10. 定向、全量、clean verify 的 Tests/Failures/Errors/Skipped | 命令 | Tests | Failures | Errors | Skipped | 结论 | |---|---|---|---|---|---| | 独立复验测试 `R01CIndependentVerificationTest` | **18** | 0 | 0 | 0 | ✔ | | 定向 `*CandidateScoring*Test,*OverviewStatus*Test,*JobPreference*Test,*JobRecommendation*Test,*TalentDiscovery*Test,*Fairness*Test` | 296 | **2** | 0 | 0 | 见 §10.3(既有隔离脆弱性) | | `mvn test` | 见 §10.1 | | | | | | `mvn clean verify` | 见 §10.1 | | | | | ### 10.1 全量与 clean verify ``` mvn test → Tests run: 2743, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS mvn clean verify → Tests run: 2743, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS ``` | 项 | 数值 | |---|---| | 测试总数 | **2743** | | 下限校验 | 实施基线 **2725** + 本轮独立新增 **18** = **2743** ✔ **恰好等于下限** | | Failures / Errors / **Skipped** | **0 / 0 / 0** ✔ | | BUILD | **SUCCESS** ✔ | **Skipped=0 的达成路径**:实施阶段因 Docker 不可用而跳过的 26 个 Testcontainers 用例, 在本轮 Docker 启动后**全部真实执行**(见 §9),因此不再计入 Skipped; `2743 = 2725(实施阶段总数,含 26 项跳过)+ 18(本轮独立新增)= 2699 执行 + 26 执行 + 18`。 **该结果同时反证 §10.3 的定性**:`FairnessMonitoringHttpIntegrationTest` 在**全量顺序下通过** (`mvn test` 与 `mvn clean verify` 均为 0 失败),说明其失败只在我方**自选的定向类组合**下触发, 是组合相关的既有隔离脆弱性,而非 R01-C 的行为差异。 ### 10.2 Testcontainers 已执行 见 §9(26 项,Skipped=0)。 ### 10.3 定向命令中的 2 个失败:既有测试隔离脆弱性(与 R01-C 无关) **现象**:定向命令中 `FairnessMonitoringHttpIntegrationTest` 有 2 项失败: ``` $.data.populationSize expected:<100> but was:<115> $.data.populationSize expected:<200> but was:<215> ``` **定性证据链**: 1. **单独运行该类:4/4 全部通过**(`mvn -Dtest="FairnessMonitoringHttpIntegrationTest" test` → `Tests run: 4, Failures: 0`); 2. 失败仅在**与其他类同批**时出现,且两次都恰好多 **15** 行 → 典型的共享 H2 库数据泄漏; 3. **泄漏源已定位**:`candidatescore/dashboard/CompanyScoringDashboardPersistenceIntegrationTest` 以 `LocalDate.now()` 生成 **当前期间**(`period = String.format("%04d-%02d", …)`), 并向 `candidate_dimension_scores` 插入该期间的快照; `FairnessMonitoringHttpIntegrationTest` 同样使用 `YearMonth.now()`,且其 `populationSize` **未按自身前缀限定范围**,因此把同批类的行一并计入; 4. **该泄漏源不在 R01-C 改动范围内**(`git diff --name-only` 中无该文件); 5. R01-C 的生产改动仅为"教育规则常量改读规则集"与"偏好分改委托共用计算器", **不涉及任何持久化、清理或计数逻辑**。 **判定**:属**既有测试隔离脆弱性**(与 A6/R01-B 定位的同类问题同源),**非 R01-C 行为差异**, 不构成 R01-C 阻断项。按任务要求**如实记录、不写成通过**,也不修改该类测试。 **明确结论**: 1. **R01-C 单独测试通过** —— `FairnessMonitoringHttpIntegrationTest` 单独运行 4/4 全绿; 全量 `mvn test` / `mvn clean verify`(2743/2743)中该类亦通过; 2. **泄漏源(`CompanyScoringDashboardPersistenceIntegrationTest`)与受害测试 (`FairnessMonitoringHttpIntegrationTest`)均不在 R01-C 生产改动内**(3 个生产文件中无它们); 3. **该问题将由下一独立纯测试提交修复**(修污染源:独占 period/studentId + 精确清理), **不并入 R01-C 提交**; 4. **该次定向组合命令(296/2 失败)不得写成全绿** —— 上表如实保留 `Failures=2`。 --- ## 11. OpenAPI 数量 ``` python ..\docs\verify_openapi.py → paths: 176 / operations: 214 → All checks passed! ``` `git diff -- docs/openapi-rc.json` → **空**,`openapi-rc.json` **未被修改** ✔ --- ## 12. 精确文件清单 **本轮 R01-C 生产改动(新增 1 + 修改 2 = 3 个生产文件)** ``` 新增: mydreamjob-server/src/main/java/com/mydreamjob/candidatescore/rules/CandidateScoringRuleSet.java 修改: mydreamjob-server/src/main/java/com/mydreamjob/candidatescore/CandidateScoringCalculator.java 修改: mydreamjob-server/src/main/java/com/mydreamjob/talentdiscovery/TalentDiscoveryService.java ``` **实施者新增测试(4)**:`mydreamjob-server/src/test/java/com/mydreamjob/common/r01c/` 下 `OverviewStatusPolicyCharacterizationTest`、`PreferenceScoreCharacterizationTest`、 `CandidateScoringRuleSetCharacterizationTest`、`RecommendationOrderingCharacterizationTest` **本轮独立复验新增(2)** ``` mydreamjob-server/src/test/java/com/mydreamjob/common/r01c/R01CIndependentVerificationTest.java docs/r01-c-independent-verification-20260918.md ``` **文档(1 修改)**:`docs/backend-development-roadmap.md` **本轮未修改任何生产代码** —— 复验前后 `git diff --name-only -- …/src/main` 恒为上述 2 个文件。 --- ## 13. 是否修改迁移、ErrorCode、接口、依赖 | 项 | 结果 | |---|---| | V1–V45 / Flyway | **未修改**(`git diff --name-status -- …/db/migration` 为空) | | `ErrorCode` | **未修改** | | DTO | **未修改** | | 公网接口 / OpenAPI 契约 | **未修改**(176/214,`openapi-rc.json` 未变) | | `pom.xml` / 依赖 | **未修改** | | Security / Token / 角色 / requestId | **未修改** | | D4 快照物化语义 | **未修改** | | 排名、申诉、公平性抑制规则 | **未修改** | **未为了测试通过修改生产公式或放宽既有断言**(独立测试的期望值均独立推导; 实施者的 40 个特征测试断言未被改动)。 --- ## 14. Git 状态 | 项 | 结果 | |---|---| | 分支 | `refactor/r01-structure-performance` ✔ | | HEAD | `9954600650c3e7cd40a5f4fa55bc38595973f214`(未变)✔ | | `git diff --cached --name-only` | **空** ✔ | | `git diff --check` | **exit 0** ✔ | | `git diff --name-status` | 仅 `docs/backend-development-roadmap.md`、`CandidateScoringCalculator.java`、`TalentDiscoveryService.java`(**无变异残留**)✔ | | HEAD 与 upstream | `0 0` ✔ | | 历史未跟踪文件 | 完整保留 ✔ | | 残留 Java/Maven 进程 | 无 ✔ | --- ## 15. 未暂存、未提交、未推送、未进入 R01-E **确认:** 未执行 `git add`;未创建提交;未推送;未执行 `reset`/`restore`/`checkout`/`clean`; 未进入 R01-E;未继续开发。 --- > **R01-C独立代码复验通过,可以进入精确暂存与独立提交检查点。**