# 行业技能词云与学习路径设计 ## 事实与约束 - 仓库当前为 Spring Boot 3.5.16、Java 17、MySQL/Flyway/JPA,最新已存在迁移为 `V34`。 - 学生技能存储在 `student_profiles.skills_json`,历史数据可能为空、重复或无法解析。 - 企业行业与岗位业务数据是自由文本,不能作为稳定的“行业 → 职位 → 技能”分类源。 - ESCO 提供职位、技能和技能分类关系;O*NET 提供 Importance 数值。两者均需保留来源版本与许可归属。 - 首版只导入互联网、金融、教育及代表性职位,避免全量翻译和超大迁移。 - 三个接口均为只读、仅 STUDENT,身份只取 JWT,响应统一使用 `ApiResponse`。 - 词云热度是静态 O*NET Importance,不随平台岗位发布和投递行为变化。 - 计算完全确定,不调用 AI、网络课程服务或第三方运行时接口。 ## Goals - 建立可持续扩展的开源技能知识库,并允许项目维护中文名称、别名和细粒度分类。 - 提供完整、稳定、可筛选的热度排序技能词云。 - 以同一排序结果计算个人 TOP10 匹配度,避免不同接口结果不一致。 - 基于未命中技能生成最小可用的学习优先级和建议。 - 保持现有模块接口语义不变,并满足项目测试、OpenAPI 和黑盒验收约定。 ## Non-Goals - 不做岗位实时文本挖掘、平台岗位热度聚合或用户行为周榜。 - 不导入 ESCO/O*NET 全量数据,不建设后台维护界面。 - 不保存匹配快照、学习进度、完成状态或提醒。 - 不提供课程、讲师、价格、时长、链接和 AI 对话。 - 不向 COMPANY/ADMIN 暴露个人匹配和学习路径。 ## Decisions ### D1: 以 ESCO + O*NET 作为静态知识底座 **Choice**:ESCO 作为职位、通用技能和分类主表;当具体技术(如 React、Docker、Spring Boot)没有可靠 ESCO 节点时,允许使用 O*NET `software_skills` 作为补充技能来源。O*NET 继续提供可追溯 Importance;项目只维护中文标签、别名和映射。 **Rationale**:能用开源数据覆盖技能层级,同时保留 O*NET 明确的重要度字段,不需要自行创造热度权重。 **Alternatives**: - 仅 O*NET:重要度完整,但英文和美国职业体系适配成本高。 - 严格仅 ESCO:来源单一,但会丢失 React、Docker 等 ESCO 未收录的具体技能。 - 自建词典:可控性高,但需要长期运营且缺少来源可信度。 **Consequences**:需要明确区分 ESCO 主来源技能与 O*NET 补充技术;每条热度必须记录 O*NET SOC 和 Element ID,无法映射时保留技能并写空热度。 ### D2: 词库使用关系表落 Flyway **Choice**:新增行业、职位、技能分类、技能、别名、职位技能关系和来源版本表,通过 Flyway 迁移导入代表性数据。 **Rationale**:接口需要多条件过滤、稳定排序和可追溯来源;关系表可利用外键、唯一约束和索引保证数据质量。 **Alternatives**: - 静态 JSON:实现快,但无法方便地按节点过滤、校验引用和演进。 - 图数据库:适合复杂关系,但超出当前技术栈和首版范围。 **Consequences**:迁移文件需要严格审查,后续词库更新必须创建新迁移,不能修改已执行迁移。 ### D3: 三个接口共享同一条排名管道 **Choice**:先查询筛选范围内的技能列表并按热度排序,再从同一列表截取 TOP10;匹配和学习路径均调用同一领域服务,不重新排序。 **Rationale**:词云、匹配度和学习路径必须对“热门技能集合”保持一致,减少重复逻辑和边界分歧。 **Alternatives**: - 每个接口独立查询和排序:实现分散,容易出现排序不一致。 - 持久化统一排名快照:可缓存,但当前数据规模不需要额外调度和一致性成本。 **Consequences**:排序规则、空热度规则和别名规则只有一个实现位置;单元测试必须聚焦该共享管道。 ### D4: TOP10 不单独存储或返回 **Choice**:词云接口返回完整排序列表,前端直接取前十条;匹配服务在内部最多取前十条。 **Rationale**:用户已确认不需要独立 TOP10 数据源,列表本身是唯一事实来源。 **Alternatives**:响应同时返回 `top10` 字段或提供独立接口;会重复数据并增加一致性风险。 **Consequences**:前端必须按约定截取前十条;接口文档需明确列表已按排名排序。 ### D5: 匹配和建议按请求实时计算且不持久化 **Choice**:每次读取当前学生技能、词库和筛选条件后计算匹配度和学习路径,不保存计算结果。 **Rationale**:数据量小、算法确定,且用户未要求历史记录;避免引入无用状态和隐私数据留存。 **Alternatives**:保存每日匹配快照或学习计划;会增加表、清理与一致性复杂度,首版无收益。 **Consequences**:学生技能或词库更新后,结果立即变化;响应必须包含足够字段让前端独立展示。 ### D6: 学习建议使用分类模板 **Choice**:技能分类映射到编程语言、框架、工具和通用四类建议模板。 **Rationale**:满足“技能 + 优先级 + 简化建议”,无需依赖外部课程数据。 **Alternatives**:调用 AI 或维护课程表;前者不确定且超出约束,后者缺少可用授权数据。 **Consequences**:建议保持简短、行业无关和可重复;后续可在不改变接口结构的前提下细化模板。 ## Data Model | 表 | 关键字段 | 作用 | | --- | --- | --- | | `skill_industries` | `id`, `code`, `name_zh`, `source_code`, `sort_order` | 行业节点 | | `skill_occupations` | `id`, `industry_id`, `code`, `name_zh`, `source_code`, `sort_order` | 行业下职位 | | `skill_categories` | `id`, `occupation_id`, `code`, `name_zh`, `suggestion_template_code`, `sort_order` | 职位下技能分类 | | `skills` | `id`, `code`, `name_zh`, `name_en`, `source_type`, `source_code` | 标准技能 | | `skill_aliases` | `id`, `skill_id`, `alias`, `normalized_alias` | 中英文、大小写和写法别名 | | `occupation_skills` | `occupation_id`, `skill_id`, `category_id`, `importance`, `importance_source_code`, `importance_element_id`, `importance_is_proxy`, `relationship_type`, `source_version` | 职位技能关系、热度及其机器可追溯来源 | | `skill_source_versions` | `source_name`, `source_version`, `license`, `attribution_url`, `imported_at` | 来源与许可追溯 | 约束与索引: - `skill_occupations.industry_id + code` 唯一,防止同一行业内职位重复。 - `skill_categories.occupation_id + code` 唯一。 - `skill_aliases.normalized_alias` 全局唯一,避免同一写法映射到多个技能。 - `occupation_skills.occupation_id + skill_id` 唯一。 - 每条非空 `importance` 必须有 O*NET `importance_source_code` 和 `importance_element_id`;代理映射必须设置 `importance_is_proxy=1`。 - 查询索引覆盖 `occupation_id + category_id` 和 `occupation_id + importance`。 - `category_id` 必须属于 `occupation_id`,通过服务校验并在必要测试中验证。 ## API Contract ### `GET /api/v1/skills/word-cloud` 查询参数:`industryId` 必填,`occupationId` 必填,`categoryId` 可选。 返回 `data`: - `industry`、`occupation`、`category`:实际筛选节点。 - `total`:技能总数。 - `items`:按 `heat DESC, nameZh ASC, skillId ASC` 排序,包含 `rank`、`skillId`、`code`、`nameZh`、`nameEn`、`categoryId`、`categoryName`、`heat`、`heatSource`。 ### `GET /api/v1/skills/match` 使用与词云相同的三个查询参数。返回 `data`: - `matchScore`:0–100 整数。 - `matchedCount` 与 `totalCount`。 - `matchedSkills` 与 `missingSkills`:使用精简技能项,缺失技能保持热度顺序。 ### `GET /api/v1/skills/learning-path` 使用与词云相同的三个查询参数。返回 `data`: - `total`。 - `items`:每项包含 `priority`、`skillId`、`skillName`、`categoryId`、`categoryName`、`suggestion`。 ## Processing Rules 1. Flyway 先建立表和约束,再导入代表性词库数据。 2. 查询服务校验行业、职位、分类关系;ESCO 作为职位与通用技能主表,O*NET `software_skills` 仅补充 ESCO 缺失的具体技术,构建统一的排序技能列表。 3. 匹配服务解析当前学生 `skills_json`;解析失败按空技能处理并记录告警,不输出原始 JSON。 4. 学生技能和技能别名统一执行 Unicode 规范化、去首尾空白、大小写折叠、连续空白折叠及常见标点归一化。 5. TOP10 大小取 `min(10, 列表大小)`;匹配度按 `round(matched * 100.0 / size)` 计算,`size=0` 时返回 0。 6. 学习路径遍历缺失技能,按列表顺序生成连续优先级,并依据 `suggestion_template_code` 产生建议。 7. 所有计算在只读事务中完成,不写入匹配或学习路径记录。 ## Security and Privacy - 在 SecurityFilterChain 中将 `/skills/**` 限制为已认证请求,Controller/Service 再断言 STUDENT 角色。 - 身份只取 SecurityContext/JWT,不接受 `studentUserId`、`userId` 或 `skills` 请求参数。 - 响应不包含原始技能 JSON、手机号、邮箱、简历内容或词库内部敏感字段。 - 日志只记录异常类型与请求上下文,不记录完整学生技能数组。 ## Risks and Verification | 风险 | 处理 | 验证证据 | | --- | --- | --- | | ESCO/O*NET 中文覆盖不足 | 首版只导入代表性职位,所有入选技能必须有中文标准名 | 种子数据检查:中文名非空率 100% | | 别名冲突导致匹配不确定 | `normalized_alias` 全局唯一,迁移失败即阻断 | Flyway 唯一约束测试和别名单测 | | Importance 缺失 | `heat=null`,排序末位,保留技能 | 排序单元测试和 H2 Repository 测试 | | 分类关系错配 | 校验分类属于职位、职位属于行业 | 非法组合 MockMvc 400 场景 | | 迁移过大影响启动 | 首版限制三类行业和代表性职位,种子数据可控 | 迁移在 H2/MySQL 测试库执行耗时记录 | | 学生技能 JSON 损坏 | 捕获解析异常并按空技能处理 | 异常 JSON 服务测试,断言 200 和匹配度 0 | | 三个接口结果漂移 | 使用共享排名服务与同一 DTO 排序 | 集成测试断言 TOP10、匹配和学习路径顺序一致 | | 许可或版本遗漏 | 保存来源版本、许可和归属 URL | 来源说明文档审查与迁移数据断言 | ## Completion Boundary 设计完成的标准是:四个规划产物一致、所有需求有可测试场景、任务覆盖三个接口和 Flyway 数据变更、没有未确认的兼容性或权限决策。实现和验收不在本设计阶段启动。