12 changed files with 983 additions and 9 deletions
@ -0,0 +1,130 @@ |
|||||
|
# W2-T4 修复复审报告 |
||||
|
|
||||
|
## 状态 |
||||
|
|
||||
|
完成。本轮仅复审修复提交 `52751e5`,对照第一轮报告 `W2-T4.md`,未修改任何实现、测试、迁移或配置文件;仅新增本报告。 |
||||
|
|
||||
|
## Verdict |
||||
|
|
||||
|
**PASS** |
||||
|
|
||||
|
第一轮阻断项 `W2-T4-I1` 已关闭。两个投影查询均已保证技能分类的 `occupation_id` 与职位技能关系的 `occupation_id` 一致,新增测试真实构造了跨职位分类错配数据并验证查询排除该数据。规定回归集全部通过,未发现新的 Critical、Important 或阻断项。 |
||||
|
|
||||
|
## Finding 关闭状态 |
||||
|
|
||||
|
### W2-T4-I1:分类过滤查询未强制 category 与 occupation 属于同一职位 |
||||
|
|
||||
|
状态:**已关闭**。 |
||||
|
|
||||
|
关闭证据: |
||||
|
|
||||
|
- 整职位查询在分类关联后增加了 `c.occupationId = os.occupationId`: |
||||
|
`D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\repository\OccupationSkillRepository.java:36-40` |
||||
|
- 职位加分类查询同样增加了该约束,并保留 `os.occupationId = :occupationId` 与 `os.categoryId = :categoryId`: |
||||
|
`D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\repository\OccupationSkillRepository.java:63-69` |
||||
|
- 两个查询均继续通过 `os.skillId = s.id` 和 `os.categoryId = c.id` 连接技能及分类,未改变投影字段或原过滤条件。 |
||||
|
|
||||
|
## 独立测试结果 |
||||
|
|
||||
|
环境: |
||||
|
|
||||
|
- `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: 6, Failures: 0, Errors: 0, Skipped: 0 |
||||
|
Total: Tests run: 16, Failures: 0, Errors: 0, Skipped: 0 |
||||
|
BUILD SUCCESS |
||||
|
Total time: 40.769 s |
||||
|
``` |
||||
|
|
||||
|
## 必须检查项逐项结论 |
||||
|
|
||||
|
### 1. 两个投影查询均约束 category 所属 occupation,且保留过滤:通过 |
||||
|
|
||||
|
两个查询都在连接 `os.categoryId = c.id` 后增加: |
||||
|
|
||||
|
```sql |
||||
|
AND c.occupationId = os.occupationId |
||||
|
``` |
||||
|
|
||||
|
整职位查询继续只按目标 `occupationId` 过滤;职位加分类查询继续同时按目标 `occupationId` 和 `categoryId` 过滤。因此: |
||||
|
|
||||
|
- 不会再通过职位 A 的关系读取仅属于职位 B 的分类; |
||||
|
- 分类参数仍必须与关系中的分类 ID 完全相等; |
||||
|
- 职位参数语义未改变。 |
||||
|
|
||||
|
位置: |
||||
|
|
||||
|
- `OccupationSkillRepository.java:36-43` |
||||
|
- `OccupationSkillRepository.java:63-72` |
||||
|
|
||||
|
### 2. 新增测试真实插入跨职位错配关系,且可证伪修复:通过 |
||||
|
|
||||
|
新增测试位于 `D:\1\zhi\zhilai\mydreamjob-server\src\test\java\com\mydreamjob\skill\SkillTaxonomyRepositoryTest.java:123-152`。 |
||||
|
|
||||
|
测试通过 `JdbcTemplate` 向真实 `occupation_skills` 表插入: |
||||
|
|
||||
|
- `occupation_id = 1` |
||||
|
- `category_id = 201` |
||||
|
- `skill_id = 1` |
||||
|
- `source_version_id = 2` |
||||
|
|
||||
|
其中分类 `201` 在 V36 中明确属于职位 `2`,见 `V36__skill_cloud_seed.sql:67`;技能 `1` 存在,见 `V36__skill_cloud_seed.sql:129`。插入使用合法外键值和完整来源字段,因此构造的是数据库允许保存、但职位与分类不属于同一节点的逻辑错配关系。 |
||||
|
|
||||
|
该测试没有断言 SQL 文本,而是调用真实 Repository 方法并检查返回结果。修复前的查询缺少同职位约束,插入关系会满足 `os.skillId = s.id`、`os.categoryId = c.id` 和 `os.occupationId = 1`,因此: |
||||
|
|
||||
|
- 整职位查询会包含技能 `1`,在第 `142` 行 `doesNotContain(1L)` 失败; |
||||
|
- `occupation=1, category=201` 查询会包含该关系,在第 `144-146` 行失败。 |
||||
|
|
||||
|
修复后该关系被 `c.occupationId = os.occupationId` 排除,测试通过,可证伪性成立。 |
||||
|
|
||||
|
测试方法带 `@Transactional`,见 `SkillTaxonomyRepositoryTest.java:123-124`,插入数据在测试结束后回滚,不污染其他测试或 V36 计数。 |
||||
|
|
||||
|
### 3. 三项断言均正确覆盖:通过 |
||||
|
|
||||
|
`SkillTaxonomyRepositoryTest.java:136-151` 分别验证: |
||||
|
|
||||
|
- 整职位查询仍包含正常技能 `3`,且不包含错配技能 `1`;当前断言在 `139-142` 行。 |
||||
|
- 错误分类组合 `occupation=1, category=201` 返回空列表;当前断言在 `144-146` 行。 |
||||
|
- 正确组合 `occupation=2, category=201` 仍返回技能 `1`,证明修复没有误伤合法数据;当前断言在 `148-151` 行。 |
||||
|
|
||||
|
三项断言覆盖了第一轮要求的安全边界和正常路径。 |
||||
|
|
||||
|
### 4. 正常查询、空 Importance、来源字段和计数:通过 |
||||
|
|
||||
|
- 原整职位和分类过滤测试继续验证职位 `1` 返回 15 项、分类 `101` 返回精确技能集合,以及错误职位分类返回空:`SkillTaxonomyRepositoryTest.java:95-120`。 |
||||
|
- O*NET 来源字段、`importance`、`importanceSourceCode`、`importanceElementId`、代理标记和排序字段的原断言仍通过:`SkillTaxonomyRepositoryTest.java:154-188`。 |
||||
|
- Figma 的空 Importance 场景仍通过:`SkillTaxonomyRepositoryTest.java:176-188`。 |
||||
|
- V36 计数仍为来源版本 2、行业 3、职位 10、分类 68、技能 72、别名 72、职位技能关系 139:`SkillTaxonomyRepositoryTest.java:72-81`。 |
||||
|
- 指定回归集 16 项全部通过,未观察到由新增约束造成的正常查询、投影字段或种子数据回归。 |
||||
|
|
||||
|
### 5. 提交范围与 T5 边界:通过 |
||||
|
|
||||
|
`git diff --name-only 0c77824..52751e5` 仅列出: |
||||
|
|
||||
|
- `mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/OccupationSkillRepository.java` |
||||
|
- `mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillTaxonomyRepositoryTest.java` |
||||
|
|
||||
|
未修改 V35/V36、生产配置、安全配置或其他模块,也未实现任何 T5 排名、匹配或学习路径代码。`git diff --check 0c77824..52751e5` 通过。 |
||||
|
|
||||
|
## 剩余阻断项 |
||||
|
|
||||
|
无。 |
||||
|
|
||||
|
## 非阻断备注 |
||||
|
|
||||
|
V35 仍允许数据库层面写入 `occupation_id` 与 `category_id` 逻辑不一致的关系,因为当前外键是单列外键;本次修复保证 Repository 查询不会返回此类错配数据。是否需要增加数据库复合完整性约束属于独立的数据模型变更,不影响 T4 通过。 |
||||
@ -0,0 +1,150 @@ |
|||||
|
# 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;修复后应增加一条跨职位分类关系的失败用例并重新评审。 |
||||
@ -0,0 +1,94 @@ |
|||||
|
# W2-T5 聚焦修复独立复审报告 |
||||
|
|
||||
|
- **评审对象**:T5 I-1 聚焦修复工作区差异(基于 `e3eecfe`) |
||||
|
- **前次评审**:`changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T5.md`,结论 `FAIL` |
||||
|
- **复审范围**:仅检查 `SkillCloudItem.java`、`SkillCloudQueryService.java`、`SkillCloudQueryServiceTest.java` 及其对既有行为的影响 |
||||
|
- **结论**:**PASS** |
||||
|
- **剩余阻断项**:无 |
||||
|
|
||||
|
## Finding 关闭状态 |
||||
|
|
||||
|
### [I-1] 响应项遗漏技能来源标识:已关闭 |
||||
|
|
||||
|
修复已同时补齐 DTO 字段和 Service 映射: |
||||
|
|
||||
|
- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\dto\SkillCloudItem.java:11-12` |
||||
|
- 新增 `sourceType` |
||||
|
- 新增 `sourceCode` |
||||
|
- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\SkillCloudQueryService.java:102-117` |
||||
|
- `toItem(...)` 在 `nameEn` 后依次映射 `projection.sourceType()` 和 `projection.sourceCode()`(第 109-110 行) |
||||
|
- 热度来源仍独立映射为 `heatSourceCode`、`heatElementId`、`heatProxy`(第 114-116 行) |
||||
|
|
||||
|
字段语义未混淆:技能自身来源是 `sourceType/sourceCode`,O*NET Importance 来源是 `heatSourceCode/heatElementId/heatProxy`。 |
||||
|
|
||||
|
### 双来源测试与可证伪性:已关闭 |
||||
|
|
||||
|
`D:\1\zhi\zhilai\mydreamjob-server\src\test\java\com\mydreamjob\skill\SkillCloudQueryServiceTest.java:198-236` 同时锁定两类来源: |
||||
|
|
||||
|
- O*NET 技术来源:第 219-220 行精确断言 |
||||
|
- `sourceType = "O*NET"` |
||||
|
- `sourceCode = "15-1254.00|React|2.E.7.h"` |
||||
|
- ESCO 技能来源:第 232-234 行精确断言 |
||||
|
- `sourceType = "ESCO"` |
||||
|
- `sourceCode = "http://data.europa.eu/esco/skill/3cd569a2-4f88-4c1e-9995-8dce8c5e51a7"` |
||||
|
- 来源隔离:第 235 行额外断言 JavaScript 的 `heatSourceCode = "15-1254.00"`,与 ESCO `sourceCode` 明显不同。 |
||||
|
|
||||
|
因此: |
||||
|
|
||||
|
- 若 `sourceType` 或 `sourceCode` 映射被遗漏并传为 `null`,上述精确断言会失败。 |
||||
|
- 若二者被交换,O*NET 与 ESCO 的精确值断言会失败。 |
||||
|
- 若技能来源与热度来源混用,React 的完整 O*NET 三元组断言及 JavaScript 的 URI/SOC 对照断言会失败。 |
||||
|
|
||||
|
## 原有行为回归 |
||||
|
|
||||
|
独立执行指定环境与命令: |
||||
|
|
||||
|
```powershell |
||||
|
$env:JAVA_HOME='D:\java\jdk' |
||||
|
& 'D:\maven\apache-maven-3.9.7\bin\mvn.cmd' '-Dtest=SkillTaxonomyMigrationTest,SkillCloudSeedDataIntegrationTest,SkillTaxonomyRepositoryTest,SkillCloudQueryServiceTest' test |
||||
|
``` |
||||
|
|
||||
|
结果: |
||||
|
|
||||
|
```text |
||||
|
Tests run: 24, Failures: 0, Errors: 0, Skipped: 0 |
||||
|
BUILD SUCCESS |
||||
|
``` |
||||
|
|
||||
|
Surefire 分类计数: |
||||
|
|
||||
|
- `SkillTaxonomyMigrationTest`:2 |
||||
|
- `SkillCloudSeedDataIntegrationTest`:8 |
||||
|
- `SkillTaxonomyRepositoryTest`:6 |
||||
|
- `SkillCloudQueryServiceTest`:8 |
||||
|
|
||||
|
已核验原有行为: |
||||
|
|
||||
|
- 全量查询、连续排名:`SkillCloudQueryServiceTest.java:62-81` |
||||
|
- 热度降序、同热度名称排序、null heat 末位:`SkillCloudQueryServiceTest.java:84-107` |
||||
|
- 同值排序的 `skillId` 稳定性:`SkillCloudQueryServiceTest.java:109-145` |
||||
|
- 分类过滤及缺省分类:`SkillCloudQueryServiceTest.java:166-179` |
||||
|
- 合法空分类返回空列表:`SkillCloudQueryServiceTest.java:181-196` |
||||
|
- 必填/正整数/未知节点/跨行业职位/跨职位分类校验:`SkillCloudQueryServiceTest.java:238-264` |
||||
|
- provenance 字段映射:`SkillCloudQueryServiceTest.java:198-236` |
||||
|
|
||||
|
本次修复没有改变排序、过滤、校验或热度来源映射逻辑,仅扩展响应字段与测试断言。 |
||||
|
|
||||
|
## 变更范围 |
||||
|
|
||||
|
自 `e3eecfe` 起,T5 实现差异仅包含: |
||||
|
|
||||
|
- `SkillCloudQueryService.java` |
||||
|
- `SkillCloudItem.java` |
||||
|
- `SkillCloudQueryServiceTest.java` |
||||
|
|
||||
|
共 `20 insertions, 3 deletions`。未修改 V35/V36、Repository、安全配置或应用配置;未发现 T6/T7/T8 的 Controller、匹配度 Service、学习路径 Service 或权限配置实现。 |
||||
|
|
||||
|
## 非阻断备注 |
||||
|
|
||||
|
- **M-1 仍为 Minor**:设计文档中的 `heatSource` 嵌套形态与当前三个扁平热度来源字段尚未统一,按前次评审要求留待 T8 明确 OpenAPI 和接口文档。 |
||||
|
- **M-2 仍为 Minor**:Service 层未单独断言无 heat 技能的 `sourceType/sourceCode`;Repository 层已在该场景断言 Figma 保留来源(`SkillTaxonomyRepositoryTest.java:183-188`),且 Service 映射无条件执行,不构成阻断。 |
||||
|
|
||||
|
## 最终裁决 |
||||
|
|
||||
|
`PASS`。I-1 已关闭,指定联合回归全部通过,未发现新的 Critical 或 Important 问题;T5 可进入后续任务门禁。 |
||||
@ -0,0 +1,94 @@ |
|||||
|
# W2-T5 独立评审报告 |
||||
|
|
||||
|
- **评审对象**:`e3eecfe feat: add skill cloud query service` |
||||
|
- **评审基线**:`52751e5`(T4 复审通过提交) |
||||
|
- **结论**:**FAIL** |
||||
|
- **评审范围**:仅检查提交 `e3eecfe` 的 5 个文件;未修改实现文件。 |
||||
|
|
||||
|
## 独立测试结果 |
||||
|
|
||||
|
环境:`JAVA_HOME=D:\java\jdk`,Maven `D:\maven\apache-maven-3.9.7\bin\mvn.cmd`。 |
||||
|
|
||||
|
执行: |
||||
|
|
||||
|
```powershell |
||||
|
mvn -Dtest=SkillTaxonomyMigrationTest,SkillCloudSeedDataIntegrationTest,SkillTaxonomyRepositoryTest,SkillCloudQueryServiceTest test |
||||
|
``` |
||||
|
|
||||
|
结果: |
||||
|
|
||||
|
```text |
||||
|
Tests run: 24, Failures: 0, Errors: 0, Skipped: 0 |
||||
|
BUILD SUCCESS |
||||
|
``` |
||||
|
|
||||
|
测试数量拆分为:迁移 2、种子数据 8、Repository 6、词云 Service 8。 |
||||
|
|
||||
|
## Important |
||||
|
|
||||
|
### [I-1] 响应项遗漏技能自身的来源标识,违反 T5 的“来源字段”和可追溯要求 |
||||
|
|
||||
|
`SkillCloudSkillProjection` 已查询并携带技能来源字段: |
||||
|
|
||||
|
- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\dto\SkillCloudSkillProjection.java:10-11` |
||||
|
- `sourceType` |
||||
|
- `sourceCode` |
||||
|
- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\repository\OccupationSkillRepository.java:25-26` |
||||
|
- 查询投影包含 `s.sourceType` 和 `s.sourceCode` |
||||
|
- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\repository\OccupationSkillRepository.java:52-53` |
||||
|
- 分类查询同样包含这两个来源字段 |
||||
|
|
||||
|
但 Service 转换成响应 DTO 时丢弃了这两个字段: |
||||
|
|
||||
|
- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\SkillCloudQueryService.java:102-115` |
||||
|
- `toItem(...)` 没有传入 `projection.sourceType()` 或 `projection.sourceCode()` |
||||
|
- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\dto\SkillCloudItem.java:5-17` |
||||
|
- Item 只有 `heatSourceCode`、`heatElementId`、`heatProxy`,没有技能来源类型或技能来源编码 |
||||
|
|
||||
|
这会使返回结果无法在接口层区分某个技能来自 ESCO 还是 O*NET `software_skills`,只剩通过数据库内部 ID 才能反查来源。T5 明确要求返回“来源字段”: |
||||
|
|
||||
|
- `D:\1\zhi\zhilai\changes\n03-skill-cloud\tasks.md:21` |
||||
|
- `D:\1\zhi\zhilai\changes\n03-skill-cloud\execution-contract.md:41` |
||||
|
- `D:\1\zhi\zhilai\changes\n03-skill-cloud\specs\skill-cloud\spec.md:86-93` |
||||
|
|
||||
|
当前测试也没有覆盖该缺口:`SkillCloudQueryServiceTest` 只断言了 React 的 `heatSourceCode`、`heatElementId` 和 `heatProxy`(`SkillCloudQueryServiceTest.java:198-223`),没有断言 Item 中应携带 `sourceType`/`sourceCode`。因此该遗漏是可复现但不可被现有测试发现的。 |
||||
|
|
||||
|
**修复要求**:在 `SkillCloudItem` 中补齐技能来源字段,在 `toItem` 中映射,并增加至少一个 ESCO 技能和一个 O*NET `software_skills` 技能的精确断言;同时确认无 heat 的技能仍保留其技能来源。 |
||||
|
|
||||
|
## Minor |
||||
|
|
||||
|
### [M-1] `heatSource` 的响应形态与设计文档不一致,需在 T8 前统一 |
||||
|
|
||||
|
设计文档将词云项描述为包含 `heatSource`: |
||||
|
|
||||
|
- `D:\1\zhi\zhilai\changes\n03-skill-cloud\design.md:131` |
||||
|
|
||||
|
当前实现使用三个扁平字段表达同一语义: |
||||
|
|
||||
|
- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\dto\SkillCloudItem.java:13-16` |
||||
|
- `heatSourceCode` |
||||
|
- `heatElementId` |
||||
|
- `heatProxy` |
||||
|
|
||||
|
这不是当前 T5 的运行时错误,但 T8 必须明确 OpenAPI 和接口文档采用扁平字段还是嵌套 `heatSource` 对象,避免契约漂移。 |
||||
|
|
||||
|
### [M-2] T5 服务测试对 null heat 的来源字段覆盖不足 |
||||
|
|
||||
|
`SkillCloudQueryServiceTest` 对 React 的非空 heat 来源做了精确断言,但没有对无 heat 技能(如当前种子数据中的 Figma)断言 `heatSourceCode`、`heatElementId` 和 `heatProxy` 的最终 DTO 值。T4 投影测试覆盖了该场景,因此暂不阻断;修复 I-1 时应顺带补齐服务层断言。 |
||||
|
|
||||
|
## 已通过检查 |
||||
|
|
||||
|
- 行业存在性、职位存在性、行业—职位归属校验均使用 `BusinessException(ErrorCode.BAD_REQUEST)`。 |
||||
|
- 非法 `industryId`、`occupationId`、`categoryId`(null、0、负数)均返回 400 语义;未知节点和跨行业/跨职位节点同样返回 400。 |
||||
|
- `categoryId` 缺省时调用整职位查询;传入时调用职位+分类查询,并保留 Repository 的职位—分类归属约束。 |
||||
|
- 排序实现为 `heat DESC`、null heat 最后、`nameZh ASC`、`skillId ASC`;当前种子数据测试能捕获名称比较器和 skillId 比较器写错,也覆盖了 null heat 末位和连续 rank。 |
||||
|
- 空分类返回成功、`total=0`、空 items。 |
||||
|
- `SkillCloudResponse` 对列表执行 `List.copyOf`,响应列表不可变。 |
||||
|
- 提交范围仅包含 T5 的 Service、3 个 DTO 和测试文件,没有实现 T6/T7/T8,也没有修改 V35/V36、安全配置或既有接口。 |
||||
|
- 未发现事务、空指针、可变集合、随机性或明显性能问题;当前静态词库规模下三次校验查询加一次投影查询是合理的。 |
||||
|
|
||||
|
## 未决问题 |
||||
|
|
||||
|
1. 技能来源字段的最终 JSON 名称应为 `sourceType`/`sourceCode`,还是采用带 `skillSource` 前缀的名称;需在修复 I-1 和 T8 OpenAPI 时统一。 |
||||
|
2. `heatSource` 是否需要改成嵌套对象;若保持扁平字段,应同步修订设计文档和后续接口文档。 |
||||
|
3. I-1 修复后必须重新执行本报告使用的 24 项联合回归,并重新评审;T5 评审通过前不得进入 T6。 |
||||
@ -0,0 +1,98 @@ |
|||||
|
# W2-T6 测试隔离修复独立复审报告 |
||||
|
|
||||
|
## Verdict |
||||
|
|
||||
|
**PASS** |
||||
|
|
||||
|
T6 原 Review 中唯一 Important finding 已关闭。新增的三个隔离测试均通过服务返回结果进行断言,能够分别证伪 `nameZh` 分支、alias 匹配链路和 alias 归属隔离的相关回归;类级事务确保测试数据回滚。未发现新的阻断项,T6 可进入下一任务。 |
||||
|
|
||||
|
## Finding 关闭状态 |
||||
|
|
||||
|
### T6-I1:`nameZh` 与 alias 匹配遗漏无法证伪 |
||||
|
|
||||
|
**状态:已关闭。** |
||||
|
|
||||
|
1. **nameZh-only 隔离成立** |
||||
|
- 测试:`matchesOnlyNameZhForV36JavaWhenOtherIdentityFieldsDoNotMatch` |
||||
|
- 位置:`D:\1\zhi\zhilai\mydreamjob-server\src\test\java\com\mydreamjob\skill\SkillMatchServiceTest.java:237-252` |
||||
|
- 第 239 行先将 V36 Java 的 `code` 改为 `JAVA_NAME_ZH_ISOLATED`。 |
||||
|
- Java 的 V36 `nameZh` 为 `Java`,`nameEn` 为 `Java (computer programming)`,alias 为 `Java (computer programming)`;学生输入仅为 `Java`。 |
||||
|
- 因此学生输入只能匹配 `nameZh`。删除生产代码的 `nameZh` 分支后,Java 必然进入 `missingSkills`,方法第 246-251 行会失败。 |
||||
|
|
||||
|
2. **alias-only 隔离成立** |
||||
|
- 测试:`matchesOnlyInsertedAliasForV36JavaWhenAliasIsTheOnlyIdentityMatch` |
||||
|
- 位置:`SkillMatchServiceTest.java:254-272` |
||||
|
- 第 256-259 行插入 Java 专属 alias `ONLY.ALIAS-JVM-XYZ`,与目标技能的 `nameZh`、`nameEn`、`skillCode` 均不相同。 |
||||
|
- 学生输入只有该 alias,并直接断言服务返回的 `matchedSkills` 包含 Java、`missingSkills` 包含 Python。 |
||||
|
- 删除 alias 查询或 alias 匹配分支后,Java 不会命中,测试必然失败。 |
||||
|
|
||||
|
3. **alias 归属隔离成立** |
||||
|
- 测试:`doesNotApplyAliasOwnedByAnotherTopSkillToV36Java` |
||||
|
- 位置:`SkillMatchServiceTest.java:274-292` |
||||
|
- 第 276-279 行将唯一 alias 关联到技能 2(Python),学生只输入该 alias。 |
||||
|
- 断言要求技能 2 命中、技能 1 缺失;若 alias 被错误共享给其他 TOP 技能,技能 1 也会命中,分数的断言会失败。 |
||||
|
|
||||
|
### 事务与回滚 |
||||
|
|
||||
|
**状态:满足要求。** |
||||
|
|
||||
|
- `SkillMatchServiceTest` 在类级使用 `@Transactional`:`SkillMatchServiceTest.java:51-52`。 |
||||
|
- 未发现 `@Commit` 或 `@Rollback(false)`。 |
||||
|
- 三个新增测试的 JDBC 插入/更新和 `service.match(...)` 调用均在测试事务内执行,测试结束后由 Spring Test 默认回滚,不会污染共享 H2 或 V36 数据。 |
||||
|
|
||||
|
### 服务结果断言 |
||||
|
|
||||
|
**状态:满足要求。** |
||||
|
|
||||
|
- 三个新增测试均直接对 `SkillMatchResponse` 的 `matchScore()`、`matchedSkills()` 和 `missingSkills()` 做断言,不是仅验证 helper。 |
||||
|
- alias-only 测试还会在第 265-271 行同时校验命中与缺失列表,能够发现“匹配分数正确但技能集合错误”的实现偏差。 |
||||
|
|
||||
|
## 独立测试结果 |
||||
|
|
||||
|
工作目录:`D:\1\zhi\zhilai\mydreamjob-server` |
||||
|
|
||||
|
环境: |
||||
|
|
||||
|
- `JAVA_HOME=D:\java\jdk` |
||||
|
- Maven:`D:\maven\apache-maven-3.9.7\bin\mvn.cmd` |
||||
|
|
||||
|
执行命令: |
||||
|
|
||||
|
```powershell |
||||
|
$env:JAVA_HOME='D:\java\jdk' |
||||
|
& 'D:\maven\apache-maven-3.9.7\bin\mvn.cmd' '-Dtest=SkillTaxonomyMigrationTest,SkillCloudSeedDataIntegrationTest,SkillTaxonomyRepositoryTest,SkillCloudQueryServiceTest,SkillMatchServiceTest' test |
||||
|
``` |
||||
|
|
||||
|
结果: |
||||
|
|
||||
|
```text |
||||
|
Tests run: 40, Failures: 0, Errors: 0, Skipped: 0 |
||||
|
BUILD SUCCESS |
||||
|
Total time: 47.828 s |
||||
|
``` |
||||
|
|
||||
|
测试组成: |
||||
|
|
||||
|
| 测试类 | 数量 | Failures | Errors | |
||||
|
|---|---:|---:|---:| |
||||
|
| `SkillTaxonomyMigrationTest` | 2 | 0 | 0 | |
||||
|
| `SkillCloudSeedDataIntegrationTest` | 8 | 0 | 0 | |
||||
|
| `SkillTaxonomyRepositoryTest` | 6 | 0 | 0 | |
||||
|
| `SkillCloudQueryServiceTest` | 8 | 0 | 0 | |
||||
|
| `SkillMatchServiceTest` | 16 | 0 | 0 | |
||||
|
|
||||
|
## 回归与范围核验 |
||||
|
|
||||
|
- T6 原有 13 项行为测试全部保留,新增 3 项隔离测试,专项测试总数为 16。 |
||||
|
- `nameEn`、`skillCode`、去重、TOP10、分母、非法 JSON、空数据、排序和跨职位隔离等既有测试均通过。 |
||||
|
- `git diff --name-only -- mydreamjob-server/src/main/java/com/mydreamjob/skill/SkillMatchService.java mydreamjob-server/src/main/java/com/mydreamjob/skill/SkillNameNormalizer.java` 无输出,生产实现相对当前审阅基线未被本次测试修复修改。 |
||||
|
- 源码搜索未发现 `LearningPath`、`SkillCloudController`、`SkillMatchController` 或相关接口实现;T7/T8 未实现。 |
||||
|
- `git diff --check` 未报告代码差异错误;输出中的无关 `.trae` 文档警告不属于 T6 范围。 |
||||
|
|
||||
|
## 剩余阻断项 |
||||
|
|
||||
|
无。 |
||||
|
|
||||
|
## 结论 |
||||
|
|
||||
|
T6-I1 已关闭,T6 测试隔离修复满足可证伪性、事务隔离和服务结果断言要求。允许 T6 进入后续流程,但在流程门禁正式记录 `PASS` 前不得启动 T7。 |
||||
@ -0,0 +1,71 @@ |
|||||
|
# W2-T6 独立评审报告 |
||||
|
|
||||
|
## Verdict |
||||
|
|
||||
|
**FAIL** |
||||
|
|
||||
|
T6 生产实现的主流程与当前契约基本一致,但测试未满足本次评审明确要求的可证伪性,尤其无法发现 `nameZh` 匹配或别名匹配被删除的回归。必须先补齐隔离型测试并通过复审,才能进入 T7。 |
||||
|
|
||||
|
## 独立测试结果 |
||||
|
|
||||
|
环境: |
||||
|
|
||||
|
- `JAVA_HOME=D:\java\jdk` |
||||
|
- 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,SkillCloudQueryServiceTest,SkillMatchServiceTest' test |
||||
|
``` |
||||
|
|
||||
|
结果: |
||||
|
|
||||
|
```text |
||||
|
Tests run: 37, Failures: 0, Errors: 0, Skipped: 0 |
||||
|
BUILD SUCCESS |
||||
|
Total time: 58.642 s |
||||
|
``` |
||||
|
|
||||
|
测试组成:迁移 2、种子数据 8、Repository 6、词云查询 8、个人匹配 13。 |
||||
|
|
||||
|
## 阻断项 |
||||
|
|
||||
|
### Important:无法证伪 `nameZh` 与 alias 匹配遗漏 |
||||
|
|
||||
|
文件: |
||||
|
|
||||
|
- `D:\1\zhi\zhilai\mydreamjob-server\src\test\java\com\mydreamjob\skill\SkillMatchServiceTest.java:212-235` |
||||
|
- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\SkillMatchService.java:137-142` |
||||
|
|
||||
|
问题: |
||||
|
|
||||
|
- 测试方法 `matchesNameZhNameEnSkillCodeAndAliasAndDeduplicatesStudentSkills` 试图一次覆盖 `nameZh`、`nameEn`、`skillCode` 和别名,但没有隔离各候选来源。 |
||||
|
- 技能 1 的 `nameZh` 为“数据库”,同时注册别名 `JVM Language`。学生技能数组同时包含“数据库”和 `jvm-language`。 |
||||
|
- 如果删除 `SkillMatchService.matches(...)` 中的 `nameZh` 判断,技能 1 仍可通过别名命中,测试继续通过。 |
||||
|
- 如果删除 alias 查询、alias 分支或整个别名匹配链路,技能 1 仍可通过 `nameZh` 命中,测试同样继续通过。 |
||||
|
- 因此现有测试不能证明 REQ-SM-3 和 TO-5 中“标准名称 + 别名”匹配能力存在;这是本次评审明确要求验证的回归保护,不能仅依赖实现代码的静态正确性。 |
||||
|
|
||||
|
要求的修复方向: |
||||
|
|
||||
|
- 增加独立技能/独立场景,确保某个标准技能只能通过 `nameZh` 命中,另一个只能通过 alias 命中。 |
||||
|
- 保留现有 `nameEn` 和 `skillCode` 隔离断言。 |
||||
|
- 建议再断言 alias 必须归属于目标 `skillId`,避免别名被错误应用于其他标准技能时测试仍通过。 |
||||
|
|
||||
|
## 已核验且未发现阻断的行为 |
||||
|
|
||||
|
- 匹配集合直接对 `SkillCloudQueryService.query(...)` 的结果取 `subList(0, min(10, size))`,服务内未重新排序。 |
||||
|
- 无 profile、空白 JSON、空数组和语法损坏 JSON 均按无技能处理;损坏 JSON 测试验证不抛 500 且日志不含原始 JSON。 |
||||
|
- 归一化覆盖 NFKC、trim、`Locale.ROOT` 小写、Unicode 连字符、空白折叠、句点移除,并保留 `+`、`#`。 |
||||
|
- `nameEn` 和 `skillCode` 在现有测试中有可区分断言;归一化后的学生技能使用 `Set` 去重。 |
||||
|
- 分数使用 `Math.round(matched * 100.0 / actualTopSize)`,`actualTopSize=0` 返回 0,`1/3=33`,少于十条和恰好十条均有断言。 |
||||
|
- 命中/缺失列表按 TOP 顺序构建,并无额外排序;无匹配场景的并集顺序与词云 TOP10 对齐。 |
||||
|
- 响应 DTO 只包含技能投影,不包含原始 `skills_json`、手机号、邮箱或简历内容。 |
||||
|
- T6 为只读事务,无持久化;未实现 T7/T8。 |
||||
|
|
||||
|
## 未决问题 |
||||
|
|
||||
|
- 无设计或接口语义待裁决。 |
||||
|
- 待修复 T6 测试可证伪性后,必须重新执行同一回归命令并进行独立复审;复审 `PASS` 前不得启动 T7。 |
||||
@ -0,0 +1,80 @@ |
|||||
|
# W2-T7 顺序测试修复复审报告 |
||||
|
|
||||
|
## Verdict |
||||
|
|
||||
|
**PASS** |
||||
|
|
||||
|
- 复审对象:`mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillLearningPathServiceTest.java` |
||||
|
- 对照报告:`changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T7.md` |
||||
|
- 生产服务基线:`fca1160 feat: add skill learning path service` |
||||
|
- 结论:T7-I1 已关闭;当前顺序测试可证伪,原有 T7 行为测试未回归,未发现 T8 实现。 |
||||
|
|
||||
|
## Finding 关闭状态 |
||||
|
|
||||
|
### T7-I1:测试无法阻止学习路径按名称重排 |
||||
|
|
||||
|
**已关闭。** |
||||
|
|
||||
|
1. Mockito 场景的输入名称和 ID 均不等于自然升序: |
||||
|
- `SkillLearningPathServiceTest.java:135-139` |
||||
|
- 名称输入为 `Skill Z -> Skill A -> Skill M`,自然升序为 `Skill A -> Skill M -> Skill Z`。 |
||||
|
- ID 输入为 `30 -> 10 -> 20`,自然升序为 `10 -> 20 -> 30`。 |
||||
|
- `SkillLearningPathServiceTest.java:155-156` 显式断言输入名称、ID 都不等于自然排序结果。 |
||||
|
- `SkillLearningPathServiceTest.java:157-165` 读取服务返回对象,并严格断言返回 ID 为 `30, 10, 20`、名称为 `Skill Z, Skill A, Skill M`。 |
||||
|
- 因此,生产实现若按名称或 ID 重排,现有测试必然失败。 |
||||
|
|
||||
|
2. 断言读取的是服务返回结果,而不是只验证 Mockito 输入: |
||||
|
- `SkillLearningPathServiceTest.java:157-165` 使用 `response.items()` 的 `skillId` 和 `skillName` 做精确顺序断言。 |
||||
|
- `SkillLearningPathServiceTest.java:160-162` 同时断言优先级严格为 `1, 2, 3`。 |
||||
|
- 满足“返回顺序、priority 和技能 ID”均被验证的要求。 |
||||
|
|
||||
|
3. 新增 V36 真实数据场景覆盖不同热度分组和名称顺序: |
||||
|
- `SkillLearningPathServiceTest.java:89-121` |
||||
|
- 使用教育行业 `3`、教学设计职位 `9` 调用真实 `SkillMatchService` 和 `SkillLearningPathService`。 |
||||
|
- `SkillLearningPathServiceTest.java:102-110` 断言缺失技能共 10 项、存在多个不同热度值,且名称和 ID 顺序均不等于自然升序。 |
||||
|
- `SkillLearningPathServiceTest.java:112-120` 将服务返回的 `skillId`、`skillName` 与 `matchResponse.missingSkills()` 的顺序做严格比较,并断言优先级为 `1..10`。 |
||||
|
- 因此,真实数据场景不是仅验证输入或数量,而是验证了学习路径严格复用匹配结果的缺失技能顺序。 |
||||
|
|
||||
|
4. 原有 T7 行为测试未回归: |
||||
|
- 四类模板测试:`SkillLearningPathServiceTest.java:176-265`,包括编程语言、框架、工具、通用能力、未知/空模板及分类不存在回退。 |
||||
|
- 空路径测试:`SkillLearningPathServiceTest.java:268-280`,断言 `total=0`、空列表且不访问分类仓储。 |
||||
|
- 重复调用测试:`SkillLearningPathServiceTest.java:282-309`,断言顺序、优先级和文本一致。 |
||||
|
- 本次修复仅修改测试文件;`SkillLearningPathService.java` 相对 `fca1160` 的生产差异为空。 |
||||
|
|
||||
|
5. 未发现 T8 实现: |
||||
|
- 当前 HEAD 仍为 `fca1160`。 |
||||
|
- 源码和测试目录未发现 `SkillCloudController`、`SkillLearningPathController`、`/skills` 词云或学习路径接口及对应安全配置变更。 |
||||
|
- 未发现超出 T7 范围的生产实现。 |
||||
|
|
||||
|
## 独立测试结果 |
||||
|
|
||||
|
环境:JDK 17.0.16、Maven 3.9.7。 |
||||
|
|
||||
|
执行命令: |
||||
|
|
||||
|
```powershell |
||||
|
$env:JAVA_HOME='D:\java\jdk' |
||||
|
& 'D:\maven\apache-maven-3.9.7\bin\mvn.cmd' '-Dtest=SkillTaxonomyMigrationTest,SkillCloudSeedDataIntegrationTest,SkillTaxonomyRepositoryTest,SkillCloudQueryServiceTest,SkillMatchServiceTest,SkillLearningPathServiceTest' test |
||||
|
``` |
||||
|
|
||||
|
结果: |
||||
|
|
||||
|
```text |
||||
|
Tests run: 50, Failures: 0, Errors: 0, Skipped: 0 |
||||
|
BUILD SUCCESS |
||||
|
``` |
||||
|
|
||||
|
分类统计: |
||||
|
|
||||
|
```text |
||||
|
SkillTaxonomyMigrationTest: 2 passed |
||||
|
SkillCloudSeedDataIntegrationTest: 8 passed |
||||
|
SkillTaxonomyRepositoryTest: 6 passed |
||||
|
SkillCloudQueryServiceTest: 8 passed |
||||
|
SkillMatchServiceTest: 16 passed |
||||
|
SkillLearningPathServiceTest: 10 passed |
||||
|
``` |
||||
|
|
||||
|
## 剩余阻断项 |
||||
|
|
||||
|
无。T7-I1 已关闭,允许进入 T8 评审前置流程。 |
||||
@ -0,0 +1,72 @@ |
|||||
|
# W2-T7 独立评审报告 |
||||
|
|
||||
|
## Verdict |
||||
|
|
||||
|
**FAIL** |
||||
|
|
||||
|
- 提交:`fca1160 feat: add skill learning path service` |
||||
|
- 评审对象:`SkillLearningPathService`、`SkillLearningPathItem`、`SkillLearningPathResponse`、`SkillLearningPathServiceTest` |
||||
|
- 阻断项:1 项 Important |
||||
|
- 结论:生产实现整体符合 T7 行为契约,但核心顺序行为缺少可证伪的回归测试,不能放行 T8。 |
||||
|
|
||||
|
## 独立测试 |
||||
|
|
||||
|
执行环境:JDK 17.0.16、Maven 3.9.7。 |
||||
|
|
||||
|
```powershell |
||||
|
$env:JAVA_HOME='D:\java\jdk' |
||||
|
& 'D:\maven\apache-maven-3.9.7\bin\mvn.cmd' '-Dtest=SkillTaxonomyMigrationTest,SkillCloudSeedDataIntegrationTest,SkillTaxonomyRepositoryTest,SkillCloudQueryServiceTest,SkillMatchServiceTest,SkillLearningPathServiceTest' test |
||||
|
``` |
||||
|
|
||||
|
结果: |
||||
|
|
||||
|
```text |
||||
|
Tests run: 49, Failures: 0, Errors: 0, Skipped: 0 |
||||
|
BUILD SUCCESS |
||||
|
Total time: 1:00 min |
||||
|
``` |
||||
|
|
||||
|
## Important |
||||
|
|
||||
|
### T7-I1:测试无法阻止学习路径按名称重排 |
||||
|
|
||||
|
- 文件:`mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillLearningPathServiceTest.java:98-117` |
||||
|
- 相关实现:`mydreamjob-server/src/main/java/com/mydreamjob/skill/SkillLearningPathService.java:35-38` |
||||
|
|
||||
|
现有单测的 `missing` 顺序为: |
||||
|
|
||||
|
- `First` / `skillId=31` / `heat=1.00` |
||||
|
- `Second` / `skillId=32` / `heat=10.00` |
||||
|
- `Third` / `skillId=33` / `heat=5.00` |
||||
|
|
||||
|
期望输出保持该输入顺序。但输入的名称和 `skillId` 本身均为升序,因此把实现改成: |
||||
|
|
||||
|
```java |
||||
|
missingSkills.stream() |
||||
|
.sorted(Comparator.comparing(SkillCloudItem::nameZh)) |
||||
|
.map(...) |
||||
|
``` |
||||
|
|
||||
|
后,单测仍会通过。真实数据集成测试 `SkillLearningPathServiceTest.java:62-76` 使用职位 1,其当前 TOP10 全部为相同热度,名称顺序也已符合词云次级排序,因此同样无法发现该重排缺陷。 |
||||
|
|
||||
|
T7 的要求是严格复用 `SkillMatchService` 返回的 `missingSkills` 顺序,不自行排序;该回归会破坏 REQ-LP-2。测试需要使用不会被名称排序或 `skillId` 排序偶然保持一致的缺失技能顺序,最好增加多热度分组的真实数据场景。 |
||||
|
|
||||
|
## 已通过检查 |
||||
|
|
||||
|
- 学习路径只遍历 `matchResponse.missingSkills()`,未消费已命中技能;优先级从 1 连续生成。 |
||||
|
- 编程语言、框架、工具、通用四类模板均已实现;未知、空模板编码及分类不存在时回退通用模板。 |
||||
|
- 建议包含技能名,精确断言中未出现 URL、课程链接、讲师、价格、时长、完成度或 AI 内容。 |
||||
|
- 无缺口时返回 `total=0` 和空列表,且不访问分类仓储。 |
||||
|
- 重复调用返回相同顺序、优先级和建议。 |
||||
|
- `SkillLearningPathItem` 仅包含技能 ID、技能名、分类 ID、分类名、优先级和建议,不含隐私字段。 |
||||
|
- 服务使用只读事务;未发现持久化或外部服务调用。 |
||||
|
- 提交只新增 4 个指定文件,未包含 T8 及后续实现。 |
||||
|
|
||||
|
## Minor |
||||
|
|
||||
|
- `SkillLearningPathServiceTest.java:195-221` 覆盖了 null 模板编码、未知模板和分类不存在,但未直接覆盖 `skill.categoryId()==null`;实现已在 `SkillLearningPathService.java:57-58` 回退 GENERAL。 |
||||
|
- 外部内容主要通过精确模板断言和禁用词断言约束;生产类没有外部客户端依赖,因此本项不构成阻断。 |
||||
|
|
||||
|
## 未决问题 |
||||
|
|
||||
|
无。修复测试后需重新执行指定回归并复审。 |
||||
@ -0,0 +1,175 @@ |
|||||
|
# N03 SDD W2 整批独立评审报告 |
||||
|
|
||||
|
## 评审对象 |
||||
|
|
||||
|
- 范围:`b960036..f296df4` |
||||
|
- Wave:W2 |
||||
|
- 批次:B2 / T4–T7 |
||||
|
- 提交: |
||||
|
- `0c77824 feat: add skill taxonomy repositories` |
||||
|
- `52751e5 fix: constrain skill projections by occupation category` |
||||
|
- `e3eecfe feat: add skill cloud query service` |
||||
|
- `1666162 fix: expose skill source metadata in cloud items` |
||||
|
- `6eb30e7 feat: add personal skill match service` |
||||
|
- `fca1160 feat: add skill learning path service` |
||||
|
- `f296df4 test: harden learning path ordering regression` |
||||
|
- 评审依据:修订后的 `execution-contract.md`、`design.md`、`tasks.md`、三份 spec、V35/V36,以及 `W2-T4*`、`W2-T5*`、`W2-T6*`、`W2-T7*` 报告。 |
||||
|
- 环境:`JAVA_HOME=D:\java\jdk`,Maven `D:\maven\apache-maven-3.9.7\bin\mvn.cmd`,工作目录 `D:\1\zhi\zhilai\mydreamjob-server`。 |
||||
|
|
||||
|
## Verdict |
||||
|
|
||||
|
**PASS** |
||||
|
|
||||
|
W2/T4–T7 的领域与查询实现完整覆盖 B2 范围;词云、个人匹配、学习路径确实共享同一排名管道;未发现 Critical 或 Important 阻断项。W3/T8 可以开始,但必须先处理报告中的 Minor 项,并在 B3 补齐 HTTP、权限、响应序列化、隐私和真实 MySQL 验证。 |
||||
|
|
||||
|
## 测试结果 |
||||
|
|
||||
|
独立执行: |
||||
|
|
||||
|
```powershell |
||||
|
$env:JAVA_HOME='D:\java\jdk' |
||||
|
& 'D:\maven\apache-maven-3.9.7\bin\mvn.cmd' '-Dtest=SkillTaxonomyMigrationTest,SkillCloudSeedDataIntegrationTest,SkillTaxonomyRepositoryTest,SkillCloudQueryServiceTest,SkillMatchServiceTest,SkillLearningPathServiceTest' test |
||||
|
``` |
||||
|
|
||||
|
结果: |
||||
|
|
||||
|
```text |
||||
|
Tests run: 50, Failures: 0, Errors: 0, Skipped: 0 |
||||
|
BUILD SUCCESS |
||||
|
``` |
||||
|
|
||||
|
Surefire 分类: |
||||
|
|
||||
|
| 测试类 | 通过数 | |
||||
|
| --- | ---: | |
||||
|
| `SkillTaxonomyMigrationTest` | 2 | |
||||
|
| `SkillCloudSeedDataIntegrationTest` | 8 | |
||||
|
| `SkillTaxonomyRepositoryTest` | 6 | |
||||
|
| `SkillCloudQueryServiceTest` | 8 | |
||||
|
| `SkillMatchServiceTest` | 16 | |
||||
|
| `SkillLearningPathServiceTest` | 10 | |
||||
|
|
||||
|
补充静态检查: |
||||
|
|
||||
|
- `git diff --check b960036..f296df4` 通过。 |
||||
|
- `git log --oneline b960036..f296df4` 只包含上述 7 个 W2/T4–T7 提交。 |
||||
|
- 范围内实现文件集中在 `mydreamjob-server/src/main/java/com/mydreamjob/skill/**` 与对应测试;未发现 `SkillCloudController`、`SecurityConfig` 或 OpenAPI 变更。 |
||||
|
|
||||
|
## 实现与需求覆盖矩阵 |
||||
|
|
||||
|
| 需求 | W2 结论 | 主要实现与证据 | |
||||
|
| --- | --- | --- | |
||||
|
| REQ-SC-1 | 不属于 W2,待 B3 | 未实现 Controller/SecurityConfig;由 T8 负责。W2 只提供可被 Controller 调用的服务。 | |
||||
|
| REQ-SC-2 | 通过(服务层) | `SkillCloudQueryService.query` 校验必填、正整数、行业—职位关系、分类—职位关系,非法组合抛 `BusinessException(BAD_REQUEST)`,见 `SkillCloudQueryService.java:57-79`、`150-158`;测试见 `SkillCloudQueryServiceTest.java:238-264`。 | |
||||
|
| REQ-SC-3 | 通过 | `heat DESC, nameZh ASC, skillId ASC`,`null` heat 末位,见 `SkillCloudQueryService.java:28-40`、`86-99`、`120-144`;测试覆盖空 heat、同 heat、同名称 skillId,见 `SkillCloudQueryServiceTest.java:84-145`。 | |
||||
|
| REQ-SC-4 | 通过(B2) | 词云返回完整列表;匹配直接取 `min(10, actualSize)`,未另建排序,见 `SkillMatchService.java:53-55`;测试见 `SkillMatchServiceTest.java:85-118`。 | |
||||
|
| REQ-SC-5 | B2 通过,B3 待补 | B2 已返回 rank、技能身份、分类、heat、技能来源及热度来源字段,见 `SkillCloudItem.java:5-19`、`SkillCloudQueryService.java:102-117`;`ApiResponse` 包装属于 T8。 | |
||||
|
| REQ-SM-1 | 通过 | 匹配服务复用 `SkillCloudQueryService.query` 的结果前 `min(10, actualSize)` 项,见 `SkillMatchService.java:53-55`;测试见 `SkillMatchServiceTest.java:85-137`。 | |
||||
|
| REQ-SM-2 | B2 通过,B3 待补 | 服务只通过 `studentUserId` 查询 `student_profiles.skills_json`,缺失、空、损坏 JSON 均按空技能处理,见 `SkillMatchService.java:81-116`;JWT 身份提取必须由 T8 Controller 完成。 | |
||||
|
| REQ-SM-3 | 通过 | NFKC、trim、Locale.ROOT、Unicode 连字符、连续空白、句点、`+`/`#` 规则见 `SkillNameNormalizer.java:12-27`;nameZh/nameEn/skillCode/alias 和别名归属有隔离测试,见 `SkillMatchServiceTest.java:237-292`。 | |
||||
|
| REQ-SM-4 | 通过 | `round(matched * 100.0 / actualTopSize)`,空集合返回 0,重复学生技能通过 `Set` 去重,见 `SkillMatchService.java:54-77`、`100-110`;测试覆盖 1/3=33、全命中、空集合。 | |
||||
|
| REQ-SM-5 | 通过 | 命中/缺失列表按 TOP 顺序分别构造,并集等于实际 TOP10,见 `SkillMatchService.java:59-78`;测试见 `SkillMatchServiceTest.java:294-370`。 | |
||||
|
| REQ-LP-1 | 通过 | 学习路径完全由 `SkillMatchResponse.missingSkills()` 驱动,不重新加入已命中或 TOP10 外技能,见 `SkillLearningPathService.java:29-40`;测试见 `SkillLearningPathServiceTest.java:131-174`。 | |
||||
|
| REQ-LP-2 | 通过 | 直接沿用 missingSkills 顺序,优先级从 1 连续递增,见 `SkillLearningPathService.java:35-38`;真实 V36 多热度分组及名称/ID 乱序测试见 `SkillLearningPathServiceTest.java:88-121`。 | |
||||
|
| REQ-LP-3 | 通过 | 四类确定性模板及未知模板回退见 `SkillLearningPathService.java:56-93`;模板精确断言见 `SkillLearningPathServiceTest.java:176-265`。 | |
||||
|
| REQ-LP-4 | 通过(B2) | 路径项包含技能 ID、名称、分类、优先级和建议,建议文本不含链接、课程、价格、时长、完成度或 AI 内容,见 `SkillLearningPathItem.java:3-10`、`SkillLearningPathServiceTest.java:358-362`。 | |
||||
|
|
||||
|
## 关键检查结论 |
||||
|
|
||||
|
### 1. 三服务共享同一排名管道:通过 |
||||
|
|
||||
|
`SkillCloudQueryService` 是唯一排序实现: |
||||
|
|
||||
|
- 过滤:`SkillCloudQueryService.java:57-84` |
||||
|
- 排序:`SkillCloudQueryService.java:28-40`、`86-88` |
||||
|
- 排名:`SkillCloudQueryService.java:89-91` |
||||
|
- 来源映射:`SkillCloudQueryService.java:102-117` |
||||
|
|
||||
|
`SkillMatchService` 直接调用词云服务并截取前 `min(10, actualSize)` 项,没有复制排序逻辑,见 `SkillMatchService.java:53-55`。`SkillLearningPathService` 直接消费匹配服务的 `missingSkills`,没有重新排序,见 `SkillLearningPathService.java:33-38`。因此 TOP10、匹配缺口和学习路径的顺序语义一致。 |
||||
|
|
||||
|
### 2. 跨任务接口一致性:通过,存在命名待收口项 |
||||
|
|
||||
|
DTO 字段和顺序语义互相衔接: |
||||
|
|
||||
|
- `SkillCloudItem` 提供词云和匹配共用的完整技能项,见 `SkillCloudItem.java:5-19`。 |
||||
|
- `SkillMatchResponse` 返回 `matchScore`、`matchedCount`、`totalCount`、`matchedSkills`、`missingSkills`,见 `SkillMatchResponse.java:5-15`。 |
||||
|
- `SkillLearningPathResponse` 返回 `total`、`items`;`SkillLearningPathItem` 返回 `priority`、技能、分类和建议,见 `SkillLearningPathResponse.java:5-11`、`SkillLearningPathItem.java:3-10`。 |
||||
|
|
||||
|
Minor M1:设计文档 `design.md:131` 使用概念字段名 `heatSource`,而实现采用扁平字段 `heatSourceCode`、`heatElementId`、`heatProxy`,见 `SkillCloudItem.java:15-18`。这不是 B2 行为错误,但 T8 必须在 OpenAPI、接口文档和前端契约中明确最终 JSON 命名,避免文档与运行时漂移。 |
||||
|
|
||||
|
### 3. 领域边界、事务、空值和异常:通过 |
||||
|
|
||||
|
- 三个业务服务均为 `@Transactional(readOnly = true)`,没有写操作,见三个服务类的类级注解。 |
||||
|
- 分类—职位约束同时存在于服务校验和 Repository 投影查询中;跨职位分类错配测试见 `SkillTaxonomyRepositoryTest.java:123-152`。 |
||||
|
- 空 heat 保留并末位排序:`SkillCloudQueryService.java:120-131`。 |
||||
|
- 空结果:词云返回 `total=0, items=[]`;匹配返回 `matchScore=0`、空命中/缺失;学习路径返回空列表,见对应测试。 |
||||
|
- 损坏 JSON 被捕获并仅记录异常类型和 profile ID,不输出原始 JSON,见 `SkillMatchService.java:91-114`。 |
||||
|
- 学习路径未知/空/缺失模板回退通用模板,见 `SkillLearningPathService.java:56-65`、`84-93`。 |
||||
|
|
||||
|
### 4. 确定性与性能:通过,Minor N+1 |
||||
|
|
||||
|
- 排序使用 `BigDecimal.compareTo`、`nameZh`、`skillId` 全序比较,重复请求结果确定。 |
||||
|
- 每个缺失技能最多一次分类模板查询,构建学习路径时最多 10 次查询,见 `SkillLearningPathService.java:61-64`。 |
||||
|
|
||||
|
Minor M2:这是有界的 N+1。当前任务是代表性小词库、最多 10 个路径项,不构成 W2 阻断;如果后续词库或 TOP 上限扩大,应在 T8/T10 或后续优化中改为一次批量读取模板。 |
||||
|
|
||||
|
### 5. 测试可证伪性:通过,有少量 Minor 缺口 |
||||
|
|
||||
|
已覆盖用户要求的关键反例: |
||||
|
|
||||
|
- 跨职位分类错配:`SkillTaxonomyRepositoryTest.java:123-152`、`SkillMatchServiceTest.java:347-370`。 |
||||
|
- 排序错误:`SkillCloudQueryServiceTest.java:84-145` 覆盖热度、同名/同热度的 skillId 稳定排序。 |
||||
|
- 名称、别名和别名归属遗漏:`SkillMatchServiceTest.java:237-292`。 |
||||
|
- 分母错误:`SkillMatchServiceTest.java:120-137`。 |
||||
|
- 优先级重排:`SkillLearningPathServiceTest.java:88-174`。 |
||||
|
- 模板遗漏:`SkillLearningPathServiceTest.java:176-265`。 |
||||
|
- 空路径:`SkillLearningPathServiceTest.java:268-280`。 |
||||
|
|
||||
|
Minor M3:B2 测试未直接覆盖 `SkillLearningPathService.resolveTemplate` 在 `skill.categoryId()==null` 时的通用模板分支;也未通过完整序列化验证所有响应对象不含原始 `skills_json`。生产 DTO 当前不含隐私字段,且 `SkillLearningPathServiceTest.java:358-362` 已约束建议文本;T8/T9 的 MockMvc/端到端测试必须补齐响应级隐私断言。 |
||||
|
|
||||
|
### 6. W2 未越过 T8 边界:通过 |
||||
|
|
||||
|
`git diff --name-only b960036..f296df4` 未出现: |
||||
|
|
||||
|
- `SkillCloudController` 或任何 `/skills/**` Controller; |
||||
|
- `SecurityConfig` 变更; |
||||
|
- OpenAPI 文件; |
||||
|
- `/api/v1/skills/*` 运行时端点。 |
||||
|
|
||||
|
因此 W2 没有实现 T8 的接口、权限或 OpenAPI 工作,边界符合 `tasks.md:24-25` 的后续安排。 |
||||
|
|
||||
|
## 严重级别与发现 |
||||
|
|
||||
|
### Critical |
||||
|
|
||||
|
无。 |
||||
|
|
||||
|
### Important |
||||
|
|
||||
|
无。 |
||||
|
|
||||
|
### Minor |
||||
|
|
||||
|
- **M1:设计中的 `heatSource` 与实际扁平来源字段未最终对齐。** |
||||
|
- 位置:`changes/n03-skill-cloud/design.md:131`;`mydreamjob-server/src/main/java/com/mydreamjob/skill/dto/SkillCloudItem.java:15-18`。 |
||||
|
- 处理方向:T8 明确选择扁平 `heatSourceCode`/`heatElementId`/`heatProxy` 或嵌套 `heatSource`,并同步 OpenAPI、接口文档和前端示例。 |
||||
|
|
||||
|
- **M2:学习路径模板查询是有界 N+1。** |
||||
|
- 位置:`mydreamjob-server/src/main/java/com/mydreamjob/skill/SkillLearningPathService.java:61-64`。 |
||||
|
- 处理方向:当前最多 10 次查询,不阻断;若词库或 TOP 上限扩大,应批量读取 `(categoryId, occupationId)` 模板映射。 |
||||
|
|
||||
|
- **M3:B2 测试未直接覆盖空 `categoryId` 模板回退和完整响应序列化隐私断言。** |
||||
|
- 位置:`mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillLearningPathServiceTest.java:240-265`、`358-362`。 |
||||
|
- 处理方向:T8 Controller 测试必须验证三个接口的响应不含 `skills_json`、手机号、邮箱和简历内容;可同时补 `categoryId=null` 的 Service 单测。 |
||||
|
|
||||
|
## 未决问题 |
||||
|
|
||||
|
1. `SkillCloudItem` 热度来源字段的最终 JSON 形态尚未在设计中收口,需在 T8 同时对齐实现、OpenAPI 和前端联调文档。 |
||||
|
2. B2 只验证 H2 `MODE=MySQL` 与 Service 层异常类型;真实 MySQL 迁移、HTTP 401/403/400、JWT 到 `studentUserId` 的绑定和 `ApiResponse` 包装仍属于 B3。 |
||||
|
3. B2 没有执行 `mvn clean verify` 或静态 OpenAPI 校验;这些属于 T9/T10 的退出条件,当前不作为 W2 阻断。 |
||||
|
|
||||
|
## 是否放行 W3 |
||||
|
|
||||
|
**放行。** |
||||
|
|
||||
|
W2 的领域行为、排名管道、个人匹配、学习路径和可证伪测试已达到 B2 完成定义。允许进入 W3/T8;W3 开始前必须在实现 Controller/SecurityConfig/OpenAPI 时处理 M1,并在 T8–T10 验证上述 HTTP、权限、隐私和真实数据库边界。 |
||||
Loading…
Reference in new issue