From cd9220b506181d8747d4bd57936e3fbd268117e0 Mon Sep 17 00:00:00 2001 From: pwklppg <123456@.com> Date: Tue, 15 Sep 2026 11:21:57 +0800 Subject: [PATCH] docs: publish skill cloud api contract --- docs/frontend-integration.md | 19 + docs/openapi-rc.json | 698 +++++++++++++++++++++++++++++++++++ docs/skill-cloud-api.md | 275 ++++++++++++++ docs/verify_openapi.py | 39 +- 4 files changed, 1030 insertions(+), 1 deletion(-) create mode 100644 docs/skill-cloud-api.md diff --git a/docs/frontend-integration.md b/docs/frontend-integration.md index 599cdf3..d4b397e 100644 --- a/docs/frontend-integration.md +++ b/docs/frontend-integration.md @@ -715,3 +715,22 @@ GET /profile/me/career-progress?page=0&size=20&status=INTERVIEW # 仅 STUDENT - **脱敏**:不返回 `studentUserId/candidateId/companyUserId/evaluatorUserId/phone/email/resumeUrl/passwordHash/JWT`、通知标题/正文、评分 `comment`、Offer 薪资/条款、他人任何身份字段。**本接口只返回通知计数,通知详情仍用 `/notifications`**。 - **只读**:`GET`,不写任何业务表;`size` 上限 50,聚合查询次数与单页条数无关(无 N+1)。 - API 详见 [docs/career-progress-api.md](career-progress-api.md)。 + +## 21. 行业技能词云、个人匹配度与学习路径联调 + +三个接口共用行业、职位和技能分类筛选,仅 STUDENT 可调用,并统一使用 Bearer JWT。完整字段、错误示例和排序语义详见 [skill-cloud-api.md](skill-cloud-api.md)。 + +| 功能 | Method | Path | 关键返回/交互 | +|---|---|---|---| +| 行业技能词云 | `GET` | `/skills/word-cloud` | `data.items` 为完整排序结果;前端直接取前 10 条作为 TOP10。 | +| 个人技能匹配度 | `GET` | `/skills/match` | 服务端只读取 JWT 学生资料;返回匹配度、命中技能和缺失技能。 | +| 学习路径 | `GET` | `/skills/learning-path` | 只返回缺失技能、连续优先级和通用学习建议。 | + +共同 query 参数:`industryId`、`occupationId` 必填正整数;`categoryId` 可选正整数。非法关系返回 `400`,未登录 `401`,非 STUDENT `403`。 + +联调注意: + +- `heat=null` 的技能仍会返回,且必须排在所有非空 `heat` 之后;不要在前端丢弃这些技能或自行改变顺序。 +- TOP10 不是独立接口字段;统一使用 `data.items.slice(0, min(10, data.total))`。 +- 匹配度公式为 `round(matchedCount * 100 / min(10, total))`;范围 15 条、命中 5 条时为 `33`。 +- 学习路径不返回课程链接、AI 文本、学习时长或进度记录。 diff --git a/docs/openapi-rc.json b/docs/openapi-rc.json index 24c8fc5..0180660 100644 --- a/docs/openapi-rc.json +++ b/docs/openapi-rc.json @@ -6296,6 +6296,455 @@ "409": { "description": "重复决定 9604 / 非法流转 9605", "content": { "*/*": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } } } } + }, + "/skills/word-cloud": { + "get": { + "tags": [ + "行业技能词云" + ], + "summary": "查询行业技能词云", + "description": "返回筛选范围内的全部技能,按 heat DESC, nameZh ASC, skillId ASC 排序,heat=null 最后;前端取前十条作为 TOP10。仅 STUDENT 可访问。", + "operationId": "getWordCloud", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "industryId", + "in": "query", + "required": true, + "description": "行业 ID,必填正整数", + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "example": 1 + } + }, + { + "name": "occupationId", + "in": "query", + "required": true, + "description": "职位 ID,必填正整数;必须属于该行业", + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "example": 2 + } + }, + { + "name": "categoryId", + "in": "query", + "required": false, + "description": "技能分类 ID,可选正整数;省略时返回该职位下全部分类", + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "example": 101 + } + } + ], + "responses": { + "200": { + "description": "成功返回筛选范围内的完整技能列表", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseSkillCloudResponse" + }, + "example": { + "code": 200, + "message": "ok", + "data": { + "industry": { + "id": 1, + "code": "INTERNET", + "name": "互联网" + }, + "occupation": { + "id": 1, + "code": "FRONTEND_DEVELOPER", + "name": "前端开发工程师" + }, + "category": null, + "total": 1, + "items": [ + { + "rank": 1, + "skillId": 26, + "skillCode": "REACT", + "nameZh": "React", + "nameEn": "React", + "sourceType": "O*NET", + "sourceCode": "15-1254.00|React|2.E.7.h", + "categoryId": 103, + "categoryName": "前端框架", + "heat": 4.38, + "heatSourceCode": "15-1254.00", + "heatElementId": "2.E.7.h", + "heatProxy": true + } + ] + }, + "requestId": "req-word-cloud", + "timestamp": "2026-09-15T10:00:00Z" + } + } + } + }, + "400": { + "description": "筛选参数缺失、非正整数或节点关系非法", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + }, + "example": { + "code": 1001, + "message": "bad request", + "data": null, + "requestId": "req-1001", + "timestamp": "2026-09-15T10:03:00Z" + } + } + } + }, + "401": { + "description": "未登录或令牌无效", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + }, + "example": { + "code": 2001, + "message": "unauthorized", + "data": null, + "requestId": "req-2001", + "timestamp": "2026-09-15T10:03:00Z" + } + } + } + }, + "403": { + "description": "已登录但不是 STUDENT 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + }, + "example": { + "code": 2002, + "message": "forbidden", + "data": null, + "requestId": "req-2002", + "timestamp": "2026-09-15T10:03:00Z" + } + } + } + } + } + } + }, + "/skills/match": { + "get": { + "tags": [ + "行业技能词云" + ], + "summary": "查询个人技能匹配度", + "description": "使用 JWT 对应学生的 skills_json,与当前筛选结果 TOP10 比对;matchScore=round(matchedCount*100/min(10,total))。仅 STUDENT 可访问。", + "operationId": "getMatch", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "industryId", + "in": "query", + "required": true, + "description": "行业 ID,必填正整数", + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "example": 1 + } + }, + { + "name": "occupationId", + "in": "query", + "required": true, + "description": "职位 ID,必填正整数;必须属于该行业", + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "example": 2 + } + }, + { + "name": "categoryId", + "in": "query", + "required": false, + "description": "技能分类 ID,可选正整数;省略时使用该职位下全部技能", + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "example": 101 + } + } + ], + "responses": { + "200": { + "description": "成功返回个人技能匹配结果", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseSkillMatchResponse" + }, + "example": { + "code": 200, + "message": "ok", + "data": { + "matchScore": 50, + "matchedCount": 1, + "totalCount": 2, + "matchedSkills": [ + { + "rank": 1, + "skillId": 26, + "skillCode": "REACT", + "nameZh": "React", + "nameEn": "React", + "sourceType": "O*NET", + "sourceCode": "15-1254.00|React|2.E.7.h", + "categoryId": 103, + "categoryName": "前端框架", + "heat": 4.38, + "heatSourceCode": "15-1254.00", + "heatElementId": "2.E.7.h", + "heatProxy": true + } + ], + "missingSkills": [ + { + "rank": 2, + "skillId": 31, + "skillCode": "DOCKER", + "nameZh": "Docker", + "nameEn": "Docker", + "sourceType": "O*NET", + "sourceCode": "15-1299.08|Docker|2.E.7.h", + "categoryId": 105, + "categoryName": "工程化工具", + "heat": 4.12, + "heatSourceCode": "15-1299.08", + "heatElementId": "2.E.7.h", + "heatProxy": true + } + ] + }, + "requestId": "req-match", + "timestamp": "2026-09-15T10:01:00Z" + } + } + } + }, + "400": { + "description": "筛选参数缺失、非正整数或节点关系非法", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + }, + "example": { + "code": 1001, + "message": "bad request", + "data": null, + "requestId": "req-1001", + "timestamp": "2026-09-15T10:03:00Z" + } + } + } + }, + "401": { + "description": "未登录或令牌无效", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + }, + "example": { + "code": 2001, + "message": "unauthorized", + "data": null, + "requestId": "req-2001", + "timestamp": "2026-09-15T10:03:00Z" + } + } + } + }, + "403": { + "description": "已登录但不是 STUDENT 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + }, + "example": { + "code": 2002, + "message": "forbidden", + "data": null, + "requestId": "req-2002", + "timestamp": "2026-09-15T10:03:00Z" + } + } + } + } + } + } + }, + "/skills/learning-path": { + "get": { + "tags": [ + "行业技能词云" + ], + "summary": "生成个人学习路径", + "description": "只针对个人 TOP10 中未命中的技能,沿用热度顺序返回连续 priority 和确定性通用建议;不调用 AI,不返回课程链接、时长或进度。仅 STUDENT 可访问。", + "operationId": "getLearningPath", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "industryId", + "in": "query", + "required": true, + "description": "行业 ID,必填正整数", + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "example": 1 + } + }, + { + "name": "occupationId", + "in": "query", + "required": true, + "description": "职位 ID,必填正整数;必须属于该行业", + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "example": 2 + } + }, + { + "name": "categoryId", + "in": "query", + "required": false, + "description": "技能分类 ID,可选正整数;省略时使用该职位下全部技能", + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "example": 101 + } + } + ], + "responses": { + "200": { + "description": "成功返回缺失技能的学习路径;没有缺口时列表为空", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseSkillLearningPathResponse" + }, + "example": { + "code": 200, + "message": "ok", + "data": { + "total": 1, + "items": [ + { + "priority": 1, + "skillId": 26, + "skillName": "React", + "categoryId": 103, + "categoryName": "前端框架", + "suggestion": "先理解 React 的核心概念与生命周期,再通过一个小型项目练习集成。" + } + ] + }, + "requestId": "req-learning-path", + "timestamp": "2026-09-15T10:02:00Z" + } + } + } + }, + "400": { + "description": "筛选参数缺失、非正整数或节点关系非法", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + }, + "example": { + "code": 1001, + "message": "bad request", + "data": null, + "requestId": "req-1001", + "timestamp": "2026-09-15T10:03:00Z" + } + } + } + }, + "401": { + "description": "未登录或令牌无效", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + }, + "example": { + "code": 2001, + "message": "unauthorized", + "data": null, + "requestId": "req-2001", + "timestamp": "2026-09-15T10:03:00Z" + } + } + } + }, + "403": { + "description": "已登录但不是 STUDENT 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + }, + "example": { + "code": 2002, + "message": "forbidden", + "data": null, + "requestId": "req-2002", + "timestamp": "2026-09-15T10:03:00Z" + } + } + } + } + } + } } }, "components": { @@ -9679,6 +10128,255 @@ "requestId": { "type": "string" }, "timestamp": { "type": "string", "format": "date-time" } } + }, + "SkillCloudNode": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "SkillCloudItem": { + "type": "object", + "properties": { + "rank": { + "type": "integer", + "format": "int32", + "minimum": 1, + "description": "当前筛选范围内的稳定排名,从 1 开始。" + }, + "skillId": { + "type": "integer", + "format": "int64" + }, + "skillCode": { + "type": "string" + }, + "nameZh": { + "type": "string" + }, + "nameEn": { + "type": "string", + "nullable": true + }, + "sourceType": { + "type": "string", + "enum": [ + "ESCO", + "O*NET" + ], + "description": "技能身份主来源。" + }, + "sourceCode": { + "type": "string", + "description": "ESCO URI 或 O*NET 三元组标识。" + }, + "categoryId": { + "type": "integer", + "format": "int64" + }, + "categoryName": { + "type": "string" + }, + "heat": { + "type": "number", + "nullable": true, + "description": "O*NET Importance;无映射为 null,且排在非空热度之后。" + }, + "heatSourceCode": { + "type": "string", + "nullable": true, + "description": "热度来源 SOC。" + }, + "heatElementId": { + "type": "string", + "nullable": true, + "description": "热度来源 O*NET Element ID。" + }, + "heatProxy": { + "type": "boolean", + "description": "热度是否为代理映射。" + } + } + }, + "SkillCloudResponse": { + "type": "object", + "properties": { + "industry": { + "$ref": "#/components/schemas/SkillCloudNode" + }, + "occupation": { + "$ref": "#/components/schemas/SkillCloudNode" + }, + "category": { + "$ref": "#/components/schemas/SkillCloudNode", + "nullable": true + }, + "total": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SkillCloudItem" + } + } + } + }, + "SkillMatchResponse": { + "type": "object", + "properties": { + "matchScore": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 100, + "description": "round(matchedCount*100/min(10,total));total=0 时为 0。" + }, + "matchedCount": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "totalCount": { + "type": "integer", + "format": "int32", + "minimum": 0, + "description": "实际匹配集合大小 min(10,total)。" + }, + "matchedSkills": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SkillCloudItem" + } + }, + "missingSkills": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SkillCloudItem" + } + } + } + }, + "SkillLearningPathItem": { + "type": "object", + "properties": { + "priority": { + "type": "integer", + "format": "int32", + "minimum": 1 + }, + "skillId": { + "type": "integer", + "format": "int64" + }, + "skillName": { + "type": "string" + }, + "categoryId": { + "type": "integer", + "format": "int64" + }, + "categoryName": { + "type": "string" + }, + "suggestion": { + "type": "string", + "description": "按技能分类生成的通用建议,不含课程、AI、链接、时长或进度。" + } + } + }, + "SkillLearningPathResponse": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SkillLearningPathItem" + } + } + } + }, + "ApiResponseSkillCloudResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/SkillCloudResponse" + }, + "requestId": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + } + }, + "ApiResponseSkillMatchResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/SkillMatchResponse" + }, + "requestId": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + } + }, + "ApiResponseSkillLearningPathResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/SkillLearningPathResponse" + }, + "requestId": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + } } }, "securitySchemes": { diff --git a/docs/skill-cloud-api.md b/docs/skill-cloud-api.md new file mode 100644 index 0000000..6c1ba80 --- /dev/null +++ b/docs/skill-cloud-api.md @@ -0,0 +1,275 @@ +# 行业技能词云、个人匹配度与学习路径 API + +> 版本:N03 设计锁定版 +> 状态:已编码实现 +> 适用范围:学生端只读接口。本文描述 `GET /api/v1/skills/word-cloud`、`GET /api/v1/skills/match`、`GET /api/v1/skills/learning-path`。 +> Base URL:`http://127.0.0.1:8080/api/v1`(生产环境替换域名,路径不变)。 + +## 1. 权限与共同参数 + +三个接口均: + +- 仅允许 `STUDENT` 访问。 +- 必须携带 `Authorization: Bearer `,身份只从 JWT/SecurityContext 获取,不接受 `userId`、`studentId` 等身份注入参数。 +- 使用统一 `ApiResponse` 包装结构。 +- 只读,不写入学习进度、学生资料或词库数据。 + +查询参数: + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `industryId` | long | 是 | 词库行业 ID,必须为正整数,且必须是真实存在的行业。 | +| `occupationId` | long | 是 | 词库职位 ID,必须为正整数,且必须属于该行业。 | +| `categoryId` | long | 否 | 技能分类 ID,必须为正整数;省略时使用该职位下全部技能分类。传入时必须属于目标职位。 | + +任一必填参数缺失、非正整数,或行业—职位—分类关系非法时返回 `400`。如果未登录返回 `401`,非 STUDENT 用户返回 `403`。 + +## 2. 通用响应与排序 + +三个接口的 `data` 都使用 camelCase 字段。词云和 TOP10 使用同一份服务端排序结果: + +1. `heat` 从高到低;`heat` 为 `null` 的技能排在所有非空热度之后。 +2. 热度相同时按 `nameZh` 升序。 +3. 其余情况按 `skillId` 升序,保证重复请求顺序稳定。 +4. `rank` 从 1 连续递增。 + +`heat` 来自 O*NET Importance。无可靠 O*NET Importance 映射的 ESCO 技能仍返回,但 `heat` 为 `null`。TOP10 不在后端单独存储或单独排序;前端直接取词云 `data.items` 的前 `min(10, data.total)` 项。 + +技能身份来源: + +- `sourceType`:`ESCO` 或 `O*NET`,表示技能条目的主来源。 +- `sourceCode`:可追溯来源标识。ESCO 为技能 URI;O*NET 补充技能为 `SOC|Workplace Example|Element ID`。 +- `heatSourceCode`:热度来源 SOC;没有 O*NET 热度时为 `null`。 +- `heatElementId`:O*NET Importance 的来源 Element ID;没有时为 `null`。 +- `heatProxy`:该条热度是否为代理映射。代理值不改变排序规则,但前端可据此展示来源说明。 + +## 3. 查询词云 + +```http +GET /api/v1/skills/word-cloud?industryId=1&occupationId=2&categoryId=101 +Authorization: Bearer +``` + +`categoryId` 可省略: + +```http +GET /api/v1/skills/word-cloud?industryId=1&occupationId=2 +``` + +成功响应为 `ApiResponse`。`SkillCloudResponse` 返回筛选节点、总数和当前范围内的完整技能列表: + +```json +{ + "code": 200, + "message": "ok", + "data": { + "industry": { "id": 1, "code": "INTERNET", "name": "互联网" }, + "occupation": { "id": 1, "code": "FRONTEND_DEVELOPER", "name": "前端开发工程师" }, + "category": { "id": 103, "code": "FRONTEND_FRAMEWORK", "name": "前端框架" }, + "total": 1, + "items": [ + { + "rank": 1, + "skillId": 26, + "skillCode": "REACT", + "nameZh": "React", + "nameEn": "React", + "sourceType": "O*NET", + "sourceCode": "15-1254.00|React|2.E.7.h", + "categoryId": 103, + "categoryName": "前端框架", + "heat": 4.38, + "heatSourceCode": "15-1254.00", + "heatElementId": "2.E.7.h", + "heatProxy": true + } + ] + }, + "requestId": "req-123", + "timestamp": "2026-09-15T10:00:00Z" +} +``` + +`category` 在未传 `categoryId` 时为 `null`。合法范围但暂时没有技能时返回成功、`total=0`、`items=[]`。前端根据 `items` 渲染词云,并截取前十条作为 TOP10。 + +## 4. 查询个人技能匹配度 + +```http +GET /api/v1/skills/match?industryId=1&occupationId=2&categoryId=101 +Authorization: Bearer +``` + +匹配集合严格取当前筛选结果的 TOP10。系统从当前 JWT 学生的 `student_profiles.skills_json` 读取技能,使用标准技能名、英文名、技能代码和别名归一化匹配。大小为 `total` 的筛选范围使用 `min(10, total)` 作为分母: + +```text +matchScore = round(matchedCount * 100 / min(10, total)) +``` + +- `total=0` 时 `matchScore=0`,命中列表和缺失列表为空。 +- 范围少于 10 项时按实际技能数计算。 +- 重复技能或指向同一标准技能的多个别名只计一次。 +- 示例:范围 15 条、命中 5 条,`matchScore=33`。 +- 原始 `skills_json`、手机号、邮箱和简历内容不会出现在响应中。 + +成功响应为 `ApiResponse`: + +```json +{ + "code": 200, + "message": "ok", + "data": { + "matchScore": 50, + "matchedCount": 1, + "totalCount": 2, + "matchedSkills": [ + { + "rank": 1, + "skillId": 26, + "skillCode": "REACT", + "nameZh": "React", + "nameEn": "React", + "sourceType": "O*NET", + "sourceCode": "15-1254.00|React|2.E.7.h", + "categoryId": 103, + "categoryName": "前端框架", + "heat": 4.38, + "heatSourceCode": "15-1254.00", + "heatElementId": "2.E.7.h", + "heatProxy": true + } + ], + "missingSkills": [ + { + "rank": 2, + "skillId": 31, + "skillCode": "DOCKER", + "nameZh": "Docker", + "nameEn": "Docker", + "sourceType": "O*NET", + "sourceCode": "15-1299.08|Docker|2.E.7.h", + "categoryId": 105, + "categoryName": "工程化工具", + "heat": 4.12, + "heatSourceCode": "15-1299.08", + "heatElementId": "2.E.7.h", + "heatProxy": true + } + ] + }, + "requestId": "req-456", + "timestamp": "2026-09-15T10:01:00Z" +} +``` + +`matchedSkills` 与 `missingSkills` 的并集等于当前 TOP10,两个列表各自保持词云排序;`totalCount` 是实际匹配集合大小。 + +## 5. 生成学习路径 + +```http +GET /api/v1/skills/learning-path?industryId=1&occupationId=2&categoryId=101 +Authorization: Bearer +``` + +学习路径只消费个人匹配结果中的 `missingSkills`: + +- 只包含当前 TOP10 中未命中的技能,不包含已命中技能或 TOP10 之外的技能。 +- 顺序与 `missingSkills` 完全一致,`priority` 从 1 连续递增。 +- 每项只返回技能 ID、名称、分类、优先级和简化学习建议。 +- 后端不调用 AI、课程平台或外部知识库;不会返回课程链接、讲师、价格、学习时长或完成度。 +- 系统不持久化学习进度或完成状态。 +- 全部命中或合法范围没有技能时返回 `total=0`、`items=[]`。 + +建议模板按技能分类确定性生成: + +| 模板类别 | 建议方向 | +|---|---| +| 编程语言 | 基础语法、常见数据结构、至少一个编码练习。 | +| 框架 | 核心概念与生命周期、小型项目集成实践。 | +| 工具 | 安装与基础配置、结合工程完成实际操作。 | +| 通用 | 核心概念和常见应用场景、简短实践巩固。 | + +```json +{ + "code": 200, + "message": "ok", + "data": { + "total": 1, + "items": [ + { + "priority": 1, + "skillId": 26, + "skillName": "React", + "categoryId": 103, + "categoryName": "前端框架", + "suggestion": "先理解 React 的核心概念与生命周期,再通过一个小型项目练习集成。" + } + ] + }, + "requestId": "req-789", + "timestamp": "2026-09-15T10:02:00Z" +} +``` + +## 6. 错误响应示例 + +所有错误也使用统一 `ApiResponse`: + +```json +{ + "code": 1001, + "message": "bad request", + "data": null, + "requestId": "req-400", + "timestamp": "2026-09-15T10:03:00Z" +} +``` + +| HTTP | 场景 | 典型 `code` | +|---|---|---| +| `400` | `industryId`/`occupationId` 缺失、非正整数,或节点关系非法 | `1001` | +| `401` | 未携带有效 JWT 或令牌无效/过期 | `2001` | +| `403` | 已登录但不是 STUDENT | `2002` | + +示例 401: + +```json +{ + "code": 2001, + "message": "unauthorized", + "data": null, + "requestId": "req-401", + "timestamp": "2026-09-15T10:04:00Z" +} +``` + +示例 403: + +```json +{ + "code": 2002, + "message": "forbidden", + "data": null, + "requestId": "req-403", + "timestamp": "2026-09-15T10:05:00Z" +} +``` + +## 7. 字段参考 + +### SkillCloudNode + +`id`、`code`、`name`。`category` 节点在未指定分类时为 `null`。 + +### SkillCloudItem + +`rank`、`skillId`、`skillCode`、`nameZh`、`nameEn`、`sourceType`、`sourceCode`、`categoryId`、`categoryName`、`heat`、`heatSourceCode`、`heatElementId`、`heatProxy`。 + +### SkillMatchResponse + +`matchScore`、`matchedCount`、`totalCount`、`matchedSkills`、`missingSkills`。 + +### SkillLearningPathItem / SkillLearningPathResponse + +`SkillLearningPathItem`:`priority`、`skillId`、`skillName`、`categoryId`、`categoryName`、`suggestion`。 + +`SkillLearningPathResponse`:`total`、`items`。 diff --git a/docs/verify_openapi.py b/docs/verify_openapi.py index 7470085..c4e8318 100644 --- a/docs/verify_openapi.py +++ b/docs/verify_openapi.py @@ -6,7 +6,7 @@ p = PROJECT_ROOT / 'docs' / 'openapi-rc.json' d = json.load(open(p, encoding='utf-8')) print('paths:', len(d['paths'])) -assert len(d['paths']) == 126, f"Expected 126 paths, got {len(d['paths'])}" +assert len(d['paths']) == 129, f"Expected 129 paths, got {len(d['paths'])}" paths = d['paths'] required = [ @@ -66,6 +66,9 @@ required = [ '/admin/reports', '/admin/reports/{id}', '/admin/reports/{id}/decision', + '/skills/word-cloud', + '/skills/match', + '/skills/learning-path', ] for path in required: assert path in paths, f"Missing path: {path}" @@ -135,6 +138,40 @@ assert 'get' in paths['/admin/reports'], "Missing GET /admin/reports" assert 'get' in paths['/admin/reports/{id}'], "Missing GET /admin/reports/{id}" assert 'post' in paths['/admin/reports/{id}/decision'], "Missing POST /admin/reports/{id}/decision" print(" V33 report/block endpoints present (9 operations / 8 paths)") + +# Skill cloud endpoints (3 paths / 3 GET operations) +for skill_path in ('/skills/word-cloud', '/skills/match', '/skills/learning-path'): + assert 'get' in paths[skill_path], f"Missing GET {skill_path}" +assert paths['/skills/word-cloud']['get'].get('operationId') == 'getWordCloud' +assert paths['/skills/match']['get'].get('operationId') == 'getMatch' +assert paths['/skills/learning-path']['get'].get('operationId') == 'getLearningPath' +assert paths['/skills/word-cloud']['get']['responses']['200']['content']['application/json']['schema']['$ref'] == '#/components/schemas/ApiResponseSkillCloudResponse' +assert paths['/skills/match']['get']['responses']['200']['content']['application/json']['schema']['$ref'] == '#/components/schemas/ApiResponseSkillMatchResponse' +assert paths['/skills/learning-path']['get']['responses']['200']['content']['application/json']['schema']['$ref'] == '#/components/schemas/ApiResponseSkillLearningPathResponse' + +schemas = d['components']['schemas'] +skill_schema_fields = { + 'SkillCloudNode': {'id', 'code', 'name'}, + 'SkillCloudItem': {'rank', 'skillId', 'skillCode', 'nameZh', 'nameEn', 'sourceType', 'sourceCode', 'categoryId', 'categoryName', 'heat', 'heatSourceCode', 'heatElementId', 'heatProxy'}, + 'SkillCloudResponse': {'industry', 'occupation', 'category', 'total', 'items'}, + 'SkillMatchResponse': {'matchScore', 'matchedCount', 'totalCount', 'matchedSkills', 'missingSkills'}, + 'SkillLearningPathItem': {'priority', 'skillId', 'skillName', 'categoryId', 'categoryName', 'suggestion'}, + 'SkillLearningPathResponse': {'total', 'items'}, +} +for schema_name, expected_fields in skill_schema_fields.items(): + schema = schemas.get(schema_name) + assert schema is not None, f'Missing schema: {schema_name}' + assert expected_fields <= set(schema.get('properties', {})), f'Schema fields missing: {schema_name}' + +word_cloud_example = paths['/skills/word-cloud']['get']['responses']['200']['content']['application/json']['example']['data'] +assert word_cloud_example['total'] == len(word_cloud_example['items']) +match_example = paths['/skills/match']['get']['responses']['200']['content']['application/json']['example']['data'] +assert match_example['matchedCount'] == len(match_example['matchedSkills']) +assert match_example['totalCount'] == len(match_example['matchedSkills']) + len(match_example['missingSkills']) +assert match_example['matchScore'] == round(match_example['matchedCount'] * 100 / match_example['totalCount']) +learning_path_example = paths['/skills/learning-path']['get']['responses']['200']['content']['application/json']['example']['data'] +assert learning_path_example['total'] == len(learning_path_example['items']) +print(" Skill cloud endpoints and schemas present (3 GET operations)") print(" Offer advance endpoints all present") print(" 26 operations all present") print(" Account deletion endpoints present (3 operations)")