# W2-T4 独立评审报告 ## 状态 完成。本轮只评审提交 `0c77824`、修订后的契约与规划产物、V35/V36,并独立执行指定测试;未修改实现、测试、迁移或配置文件,仅写入本报告。 ## Verdict **FAIL** 未发现 Critical。发现 1 项 Important:技能投影查询没有保证 `occupation_skills.category_id` 对应的分类属于同一个 `occupation_id`,在数据库出现逻辑错配关系时会返回跨职位数据。该问题会阻断 T4 通过,修复并复审后可进入 T5。 ## 独立测试输出 环境: - `JAVA_HOME=D:\java\jdk` - Java:OpenJDK 17.0.16 - Maven:`D:\maven\apache-maven-3.9.7\bin\mvn.cmd` - 工作目录:`D:\1\zhi\zhilai\mydreamjob-server` 命令: ```powershell $env:JAVA_HOME='D:\java\jdk' & 'D:\maven\apache-maven-3.9.7\bin\mvn.cmd' '-Dtest=SkillTaxonomyMigrationTest,SkillCloudSeedDataIntegrationTest,SkillTaxonomyRepositoryTest' test ``` 结果: ```text SkillCloudSeedDataIntegrationTest: Tests run: 8, Failures: 0, Errors: 0, Skipped: 0 SkillTaxonomyMigrationTest: Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 SkillTaxonomyRepositoryTest: Tests run: 5, Failures: 0, Errors: 0, Skipped: 0 Total: Tests run: 15, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS Total time: 44.358 s ``` ## 必须检查项逐项结论 ### 1. 实体与 V35 映射:通过 7 个实体均映射到 V35 对应表,主键为 `Long` + `GenerationType.IDENTITY`。已逐表核对表名、列名、长度、可空性和类型: - `SkillSourceVersion` 与 `skill_source_versions`:字段、长度和可空性一致。 - `SkillIndustry` 与 `skill_industries`:字段、长度和可空性一致。 - `SkillOccupation` 与 `skill_occupations`:字段、长度和可空性一致。 - `SkillCategory` 与 `skill_categories`:字段、长度和可空性一致。 - `Skill` 与 `skills`:字段、长度和可空性一致。 - `SkillAlias` 与 `skill_aliases`:字段、长度和可空性一致;该表无 `updated_at`,实体也未映射该列。 - `OccupationSkill` 与 `occupation_skills`:`DECIMAL(4,2)`、来源字段长度、`TINYINT(1)` 代理标记及可空性匹配;`importanceIsProxy` 使用 `@JdbcTypeCode(SqlTypes.TINYINT)`。 本次测试在 `ddl-auto=validate` 下执行通过,证明 V35 七张表实际参与了 Hibernate 校验。 ### 2. Schema Filter 与生产配置:通过 `SkillTaxonomyRepositoryTest` 的 validate filter 明确包含全部七个 V35 表: `D:\1\zhi\zhilai\mydreamjob-server\src\test\java\com\mydreamjob\skill\SkillTaxonomyRepositoryTest.java:182-231` 因此该 filter 隔离的是仓库历史中与本任务无关的表,不会跳过 V35 实体的列类型或可空性校验。测试通过 `@TestPropertySource` 覆盖测试数据库和 `ddl-auto`,提交未修改生产 `application.yml`、`SecurityConfig` 或其他配置。 ### 3. 行业、职位、分类关联校验:通过 以下 Repository 方法正确表达了单节点关系约束: - `SkillOccupationRepository.existsByIdAndIndustryId` / `findByIdAndIndustryId` - `SkillCategoryRepository.existsByIdAndOccupationId` / `findByIdAndOccupationId` 测试覆盖了正确组合、跨行业职位、跨职位分类和不存在的 ID: `D:\1\zhi\zhilai\mydreamjob-server\src\test\java\com\mydreamjob\skill\SkillTaxonomyRepositoryTest.java:78-88` ### 4. occupation + 可选 category 查询的同职位约束:不通过 两个投影查询都只连接 `os.category_id = c.id`,没有断言 `c.occupation_id` 与 `os.occupation_id` 相同: `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\repository\OccupationSkillRepository.java:36-40` `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\repository\OccupationSkillRepository.java:62-66` V35 对 `occupation_skills.category_id` 只有单列外键,不阻止一条关系的 `occupation_id` 指向职位 A、`category_id` 指向职位 B 的分类: `D:\1\zhi\zhilai\mydreamjob-server\src\main\resources\db\migration\V35__skill_cloud_taxonomy.sql:114-117` 这违反了契约要求“分类必须属于职位”: `D:\1\zhi\zhilai\changes\n03-skill-cloud\execution-contract.md:59` 也违反了设计中的“category_id 必须属于 occupation_id”: `D:\1\zhi\zhilai\changes\n03-skill-cloud\design.md:119` 现有测试 `findSkillProjectionsByOccupationIdAndCategoryId(2L, 101L)` 只证明当前种子数据中不存在 `职位 2 + 分类 101` 关系,不能发现上述缺列条件;即使插入一条 `occupation_id=2, category_id=101` 的逻辑错配关系,该查询仍会返回它: `D:\1\zhi\zhilai\mydreamjob-server\src\test\java\com\mydreamjob\skill\SkillTaxonomyRepositoryTest.java:113-115` 修复方向明确:两个查询均增加分类所属职位约束,例如 `AND c.occupationId = os.occupationId`;同时增加可证伪测试,用跨职位分类关系验证查询不会返回数据。 ### 5. 投影 14 个字段:通过 `SkillCloudSkillProjection` 包含 14 个字段,覆盖技能身份、名称、来源、分类、`importance`、`importanceSourceCode`、`importanceElementId`、`importanceIsProxy`、技能排序值和分类排序值。构造表达式与字段顺序一致,`importance` 使用 `BigDecimal`,代理标记使用 `Boolean`。 `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\dto\SkillCloudSkillProjection.java:5-20` ### 6. 未提前实现 T5–T8:通过 提交只新增 7 个实体、1 个投影、7 个 Repository 和专项测试;未新增排名服务、匹配服务、学习路径服务、Controller、安全规则或 OpenAPI 变更。Repository 查询明确未实现最终排序,符合 T5 边界。 ### 7. 测试数据覆盖与可证伪性:除跨职位查询外通过 测试使用了 V36 种子数据,覆盖计数、关联校验、整职位查询、可选分类过滤、O*NET 来源、空 Importance、代理标记、别名和按 ID 查询。查找均为精确断言,不是仅计数。 但第 4 项的跨职位分类错配缺少可证伪用例;即使 Repository 查询遗漏同职位条件,当前 15 项测试仍全部通过。 ### 8. 提交范围与历史迁移:通过 提交只包含允许的 16 个文件。独立哈希核对确认 V35/V36 在 `0c77824` 中未发生变化,提交未修改其他模块、已有迁移、生产配置或安全配置。 ## Findings ### Critical 无。 ### Important #### W2-T4-I1:分类过滤查询未强制 category 与 occupation 属于同一职位 证据: - `OccupationSkillRepository` 两个查询均只有 `os.categoryId = c.id`,缺少 `c.occupationId = os.occupationId`: - `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\repository\OccupationSkillRepository.java:36-40` - `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\repository\OccupationSkillRepository.java:62-66` - V35 外键不提供复合 `(occupation_id, category_id)` 完整性: - `D:\1\zhi\zhilai\mydreamjob-server\src\main\resources\db\migration\V35__skill_cloud_taxonomy.sql:114-117` - 契约明确要求分类必须属于职位: - `D:\1\zhi\zhilai\changes\n03-skill-cloud\execution-contract.md:59` - `D:\1\zhi\zhilai\changes\n03-skill-cloud\design.md:119` 影响:一旦数据写入出现逻辑错配,整职位查询会返回其他职位的分类技能;带分类查询也可能在对应关系存在时返回跨职位数据,破坏词云筛选边界,并影响后续匹配和学习路径。当前种子数据恰好一致,因此测试未暴露问题。 ### Minor 无额外阻断项。 ## 未决问题 无。修复语义明确:查询必须同时绑定 category 所属 occupation;修复后应增加一条跨职位分类关系的失败用例并重新评审。