diff --git a/changes/n03-skill-cloud/proposal.md b/changes/n03-skill-cloud/proposal.md new file mode 100644 index 0000000..930da97 --- /dev/null +++ b/changes/n03-skill-cloud/proposal.md @@ -0,0 +1,59 @@ +# N03 行业技能词云与学习路径接口 + +## Why + +当前系统已经具备学生资料、技能标签、岗位匹配和求职进度能力,但学生无法从行业与职位的视角看到“市场在要求哪些技能”,也无法知道自己相对热门技能 TOP10 的掌握程度,更无法直接得到按优先级排列的学习补缺清单。仓库截至 V34 没有技能词云、行业技能热度或个人学习路径实现,现有 `student_profiles.skills_json` 与 `jobs.requirements` 也尚未被统一为可查询的技能分类体系。 + +本变更通过引入 ESCO + O*NET 的开源知识底座,为“行业 → 职位 → 技能分类 → 技能”建立可版本化数据,并为已登录学生提供三个确定性接口:查看词云与 TOP10、计算个人技能匹配度、生成缺失技能学习路径。 + +## What Changes + +- 新增 Flyway 词库表,保存行业、职位、技能分类、技能、别名、职位技能关系、O*NET Importance 热度及数据来源版本。 +- 首版导入互联网、金融、教育行业及代表性职位;中文名称、别名和细粒度技能分类由项目维护映射。 +- 新增 `GET /api/v1/skills/word-cloud`:按行业、职位、可选技能分类返回全部技能,按热度降序排列。 +- 新增 `GET /api/v1/skills/match`:用当前学生技能命中当前筛选结果 TOP10,返回匹配度、命中技能和缺失技能。 +- 新增 `GET /api/v1/skills/learning-path`:对缺失 TOP10 技能按热度生成优先级和通用学习建议。 +- 三个接口仅允许已登录 STUDENT 访问,统一使用现有 JWT、角色校验和 `ApiResponse` 规范。 +- 更新 OpenAPI 契约、前端联调文档和词库来源说明。 + +## Scope + +### In Scope + +- ESCO 与 O*NET 的代表性数据导入、中文映射、来源版本和许可归属。 +- 行业、职位、技能分类、技能、别名、热度及职位技能关系查询。 +- 当前筛选范围内的完整技能词云,以及可由前端直接截取的 TOP10。 +- 个人 `skills_json` 与 TOP10 的别名归一化匹配。 +- 匹配度按 `命中数 / min(10, 实际技能数)` 计算,实际技能数为 0 时返回 0。 +- 缺失技能按同一热度排序生成优先级 1、2、3…。 +- 按技能分类生成通用学习建议,不依赖 AI 或外部服务。 +- 单元测试、MockMvc 测试、H2 持久化测试和 OpenAPI 静态校验。 + +### Out of Scope + +- 平台岗位 `jobId`、岗位发布频率或实时文本抽取参与热度计算。 +- ESCO/O*NET 全量导入、自动翻译全量技能名称、在线同步开源词库。 +- 后台词库维护页面、运营导入工具或运行时更新接口。 +- AI 生成课程大纲、课程链接、讲师、学习时长或培训平台对接。 +- 保存个人匹配历史、学习进度、完成状态或提醒任务。 +- 企业和管理员查看学生个人匹配结果。 +- 前端页面、交互设计和可视化实现。 + +## Impact + +- `mydreamjob-server/src/main/resources/db/migration`:新增词库结构迁移和代表性种子数据。 +- `mydreamjob-server/src/main/java/com/mydreamjob`:新增技能词云领域包、实体、Repository、Service、DTO 和 Controller,并调整安全配置。 +- `mydreamjob-server/src/test/java/com/mydreamjob`:新增查询排序、匹配、学习路径和权限场景测试。 +- `docs/openapi-rc.json`、`docs/frontend-integration.md`:登记三个接口和响应示例。 +- 新增词库来源与版本说明文档,保留 ESCO/O*NET 的许可和数据版本。 +- 不修改已有岗位匹配、候选人评分、求职进度等已验收接口语义。 + +## Proof of Completion + +- Flyway 在 MySQL 模式测试库成功创建词库表并导入代表性数据。 +- 三个接口返回统一 `ApiResponse`,匿名访问为 401,非 STUDENT 为 403。 +- 词云结果按 O*NET Importance 降序;无权重技能保留且排在有权重技能之后;同权重排序稳定。 +- TOP10 与词云使用同一排序结果,匹配度和学习路径复用该结果。 +- 匹配度覆盖 0、少于 10 项、恰好 10 项、全部命中和部分命中场景。 +- 学习路径优先级连续,且只包含未命中的 TOP10 技能;无缺口时返回空列表。 +- `mvn test`、`mvn clean verify` 和 `python docs/verify_openapi.py` 全部通过。 \ No newline at end of file diff --git a/changes/n03-skill-cloud/specs/learning-path/spec.md b/changes/n03-skill-cloud/specs/learning-path/spec.md new file mode 100644 index 0000000..f95e8e5 --- /dev/null +++ b/changes/n03-skill-cloud/specs/learning-path/spec.md @@ -0,0 +1,84 @@ +# Learning Path Specification + +## ADDED Requirements + +### Requirement: Learning path consumes unmatched TOP10 skills + +学习路径接口 MUST 复用个人技能匹配结果,并只包含当前 TOP10 中未命中的技能;不得包含已命中技能或 TOP10 之外的技能。 + +#### Scenario: Student has skill gaps + +- **WHEN** 当前 TOP10 中部分技能未命中 +- **THEN** 学习路径 MUST 包含全部未命中技能且不含已命中技能 + +#### Scenario: Student has no skill gaps + +- **WHEN** 当前 TOP10 全部命中 +- **THEN** 学习路径 MUST 返回空列表和总数 0 + +#### Scenario: Student has no matching skill evidence + +- **WHEN** 学生没有技能数据 +- **THEN** 学习路径 MUST 包含当前 TOP10 的全部技能 + +### Requirement: Learning priority follows heat order + +学习路径项 MUST 按与 TOP10 相同的热度顺序排列,并从 1 开始连续编号;空热度技能继续遵守词云的末位排序规则。 + +#### Scenario: Multiple missing skills are ordered + +- **WHEN** 缺失技能包含多个热度不同的技能 +- **THEN** 热度最高的技能 MUST 获得优先级 1,后续技能依次编号 + +#### Scenario: Missing skills have equal heat + +- **WHEN** 缺失技能热度相同 +- **THEN** 优先级 MUST 沿用词云的稳定次级排序,重复请求结果一致 + +#### Scenario: Only one skill is missing + +- **WHEN** 当前 TOP10 只有一项未命中 +- **THEN** 该技能 MUST 返回优先级 1 + +### Requirement: Learning suggestions are deterministic and industry-neutral + +每项学习建议 MUST 由后端按技能分类的通用模板生成,不调用 AI、知识库问答或外部课程服务;建议不得包含课程链接、讲师、价格、学习时长或完成度。 + +#### Scenario: Programming language gap is returned + +- **WHEN** 缺失技能属于编程语言分类 +- **THEN** 建议 MUST 描述基础语法学习和编码练习方向 + +#### Scenario: Framework gap is returned + +- **WHEN** 缺失技能属于框架分类 +- **THEN** 建议 MUST 描述核心概念学习和小型项目实践方向 + +#### Scenario: Tool gap is returned + +- **WHEN** 缺失技能属于工具分类 +- **THEN** 建议 MUST 描述安装配置和工程实践方向 + +#### Scenario: Unknown category is returned + +- **WHEN** 缺失技能无法匹配特定模板分类 +- **THEN** 系统 MUST 返回一条行业无关的通用学习建议 + +### Requirement: Learning path response is actionable and minimal + +每个学习路径项 MUST 返回技能 ID、技能名称、技能分类、优先级和简化学习建议;响应 MUST 不包含个人隐私、简历原文或模型推理内容。 + +#### Scenario: A learning item is generated + +- **WHEN** 存在未命中技能 +- **THEN** 每个路径项 MUST 同时具备技能身份、分类、连续优先级和建议文本 + +#### Scenario: Repeated request is made + +- **WHEN** 学生资料、词库数据和筛选条件均未变化 +- **THEN** 接口 MUST 返回相同顺序、相同优先级和相同建议 + +#### Scenario: Response contains no private profile data + +- **WHEN** 学习路径成功返回 +- **THEN** 响应 MUST 不含手机号、邮箱、简历文件、原始 `skills_json` 或其他无关个人数据 \ No newline at end of file diff --git a/changes/n03-skill-cloud/specs/skill-cloud/spec.md b/changes/n03-skill-cloud/specs/skill-cloud/spec.md new file mode 100644 index 0000000..f601c13 --- /dev/null +++ b/changes/n03-skill-cloud/specs/skill-cloud/spec.md @@ -0,0 +1,98 @@ +# Skill Cloud Specification + +## ADDED Requirements + +### Requirement: STUDENT-only skill cloud access + +系统 MUST 只允许已登录 STUDENT 调用技能词云接口,并从 JWT 解析当前用户,不接受客户端传入学生身份参数。 + +#### Scenario: Anonymous request is rejected + +- **WHEN** 未携带有效 JWT 请求 `GET /api/v1/skills/word-cloud` +- **THEN** 系统 MUST 返回 401,且不得返回任何词库数据 + +#### Scenario: Company or administrator request is rejected + +- **WHEN** COMPANY 或 ADMIN 用户携带有效 JWT 请求技能词云 +- **THEN** 系统 MUST 返回 403,且不得返回任何词库数据 + +#### Scenario: Student request is accepted + +- **WHEN** STUDENT 用户携带有效 JWT 请求技能词云 +- **THEN** 系统 MUST 按筛选条件返回词库数据 + +### Requirement: Skill cloud filter validation + +接口 MUST 要求 `industryId` 和 `occupationId` 为必填正整数,`categoryId` 为可选正整数;行业与职位必须存在且互为关联,技能分类存在时必须属于该职位。 + +#### Scenario: Required filters are missing + +- **WHEN** 请求未传 `industryId` 或 `occupationId` +- **THEN** 系统 MUST 返回 400,并说明缺失的筛选参数 + +#### Scenario: Occupation does not belong to industry + +- **WHEN** 请求传入存在但不属于目标行业的职位 +- **THEN** 系统 MUST 返回 400,且不得返回技能数据 + +#### Scenario: Category does not belong to occupation + +- **WHEN** 请求传入存在但不属于目标职位的技能分类 +- **THEN** 系统 MUST 返回 400,且不得返回技能数据 + +#### Scenario: Category is omitted + +- **WHEN** 请求只传行业和职位 +- **THEN** 系统 MUST 返回该职位下所有技能分类的技能 + +### Requirement: Complete ranked skill cloud + +系统 MUST 返回当前筛选范围内全部技能,并按 `heat` 降序排列;`heat` 为空时排在非空值之后。同 `heat` 或同为空值时,系统 MUST 使用稳定的中文技能名称和技能 ID 作为次级排序条件。 + +#### Scenario: Skills are ordered by heat + +- **WHEN** 筛选结果存在多个带 O*NET Importance 的技能 +- **THEN** 返回列表 MUST 按数值从高到低排列 + +#### Scenario: Skills without heat are retained + +- **WHEN** 筛选结果包含无 O*NET Importance 的 ESCO 技能 +- **THEN** 这些技能 MUST 保留在列表中,`heat` 为 null,并排在所有非 null 热度技能之后 + +#### Scenario: Equal heat has deterministic order + +- **WHEN** 两个技能具有相同 `heat` +- **THEN** 系统 MUST 按稳定次级规则返回一致顺序,重复请求不得随机变化 + +#### Scenario: Empty scope returns empty list + +- **WHEN** 合法筛选范围内没有技能 +- **THEN** 系统 MUST 返回成功响应、空技能列表和总数 0 + +### Requirement: TOP10 is derived from the skill cloud + +系统 MUST NOT 为 TOP10 维护独立排序或独立数据源;前端必须能够直接使用词云返回列表的前十条得到 TOP10。 + +#### Scenario: Scope has at least ten skills + +- **WHEN** 词云结果包含至少十条技能 +- **THEN** 前十条 MUST 与同一筛选条件下的热门技能 TOP10 一致 + +#### Scenario: Scope has fewer than ten skills + +- **WHEN** 词云结果少于十条技能 +- **THEN** 全部结果 MUST 构成实际 TOP10 集合 + +### Requirement: Skill cloud response exposes source-linked metadata + +每个技能项 MUST 至少返回技能 ID、中文名称、技能分类 ID、技能分类名称、`heat` 和稳定排名。技能仍可通过 ID 追溯至 ESCO/O*NET 来源。 + +#### Scenario: Skill item is returned + +- **WHEN** 查询返回任一技能 +- **THEN** 响应 MUST 包含该技能的身份、分类、热度和排名字段 + +#### Scenario: Response is wrapped consistently + +- **WHEN** 请求成功 +- **THEN** 响应 MUST 使用项目统一 `ApiResponse` 结构,并在 `data` 中返回筛选条件和技能列表 \ No newline at end of file diff --git a/changes/n03-skill-cloud/specs/skill-match/spec.md b/changes/n03-skill-cloud/specs/skill-match/spec.md new file mode 100644 index 0000000..2b9ff04 --- /dev/null +++ b/changes/n03-skill-cloud/specs/skill-match/spec.md @@ -0,0 +1,98 @@ +# Skill Match Specification + +## ADDED Requirements + +### Requirement: Personal matching uses the current ranked TOP10 + +个人技能匹配接口 MUST 复用技能词云相同的筛选参数、数据范围和热度排序;匹配集合 MUST 是当前筛选结果的前 `min(10, 实际技能数)` 项。 + +#### Scenario: Filtered result has at least ten skills + +- **WHEN** 当前筛选返回十条或更多技能 +- **THEN** 匹配集合 MUST 恰好包含热度最高的十条技能 + +#### Scenario: Filtered result has fewer than ten skills + +- **WHEN** 当前筛选返回六条技能 +- **THEN** 匹配集合 MUST 包含全部六条技能,分母 MUST 为 6 + +#### Scenario: Filtered result is empty + +- **WHEN** 当前合法筛选范围内没有技能 +- **THEN** 匹配度 MUST 返回 0,命中和缺失列表 MUST 均为空 + +### Requirement: Student skills are read from the authenticated profile + +系统 MUST 从当前登录 STUDENT 的 `student_profiles.skills_json` 读取技能;不得接受客户端另行提交的技能列表作为个人匹配依据。 + +#### Scenario: Student profile has valid skills + +- **WHEN** 当前学生存在合法技能数组 +- **THEN** 系统 MUST 使用该数组计算匹配结果 + +#### Scenario: Student profile is absent or has no skills + +- **WHEN** 当前学生没有资料记录、技能字段为空或技能数组为空 +- **THEN** 匹配度 MUST 返回 0,命中列表为空,缺失列表为当前 TOP10 + +#### Scenario: Skills JSON is malformed + +- **WHEN** 当前学生技能字段无法解析为字符串数组 +- **THEN** 系统 MUST 按无技能处理,不得导致 500 响应或泄露原始字段内容 + +### Requirement: Skill matching normalizes aliases + +系统 MUST 使用技能标准和别名进行匹配,归一化至少覆盖首尾空白、大小写、连续空格和常见标点差异;每个标准技能命中一次即可。 + +#### Scenario: Alias matches canonical skill + +- **WHEN** 学生技能为 `SpringBoot`,TOP10 标准技能为 `Spring Boot` 且存在对应别名 +- **THEN** 系统 MUST 将该技能计为命中 + +#### Scenario: Case and spacing differ + +- **WHEN** 学生技能与标准技能的字母大小写或空格数量不同 +- **THEN** 系统 MUST 在归一化后判断为同一技能 + +#### Scenario: Duplicate student skills exist + +- **WHEN** 学生技能数组包含多个指向同一标准技能的别名 +- **THEN** 系统 MUST 只计一次命中 + +### Requirement: Match score uses the actual TOP10 size + +匹配度 MUST 使用公式 `round(命中数 × 100 / min(10, 实际技能数))` 计算,并返回 0 到 100 的整数。实际技能数为 0 时,匹配度 MUST 为 0。 + +#### Scenario: All skills match + +- **WHEN** 当前 TOP10 有 8 项,学生命中全部 8 项 +- **THEN** 匹配度 MUST 返回 100 + +#### Scenario: Partial match is rounded + +- **WHEN** 当前 TOP10 有 3 项,学生命中 1 项 +- **THEN** 匹配度 MUST 返回 33 + +#### Scenario: No skill matches + +- **WHEN** 当前 TOP10 非空且学生没有命中任何技能 +- **THEN** 匹配度 MUST 返回 0 + +### Requirement: Match response separates matched and missing skills + +响应 MUST 返回匹配度、命中数量、实际匹配集合大小、命中技能列表和缺失技能列表;缺失技能 MUST 保持 TOP10 的热度排序。 + +#### Scenario: Some skills are matched + +- **WHEN** 学生命中 TOP10 的部分技能 +- **THEN** 命中列表和缺失列表 MUST 分别返回对应技能,且并集等于 TOP10 + +#### Scenario: All skills are matched + +- **WHEN** 学生命中 TOP10 的全部技能 +- **THEN** 缺失技能列表 MUST 为空,匹配度 MUST 为 100 + +#### Scenario: No skills are matched + +- **WHEN** 学生没有命中 TOP10 的任何技能 +- **THEN** 命中技能列表 MUST 为空,缺失列表 MUST 等于 TOP10 \ No newline at end of file