Browse Source

docs: publish skill cloud api contract

backup/master-local-20261012
pwklppg 1 month ago
parent
commit
cd9220b506
  1. 19
      docs/frontend-integration.md
  2. 698
      docs/openapi-rc.json
  3. 275
      docs/skill-cloud-api.md
  4. 39
      docs/verify_openapi.py

19
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`**。 - **脱敏**:不返回 `studentUserId/candidateId/companyUserId/evaluatorUserId/phone/email/resumeUrl/passwordHash/JWT`、通知标题/正文、评分 `comment`、Offer 薪资/条款、他人任何身份字段。**本接口只返回通知计数,通知详情仍用 `/notifications`**。
- **只读**:`GET`,不写任何业务表;`size` 上限 50,聚合查询次数与单页条数无关(无 N+1)。 - **只读**:`GET`,不写任何业务表;`size` 上限 50,聚合查询次数与单页条数无关(无 N+1)。
- API 详见 [docs/career-progress-api.md](career-progress-api.md)。 - 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 文本、学习时长或进度记录。

698
docs/openapi-rc.json

@ -6296,6 +6296,455 @@
"409": { "description": "重复决定 9604 / 非法流转 9605", "content": { "*/*": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } } "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": { "components": {
@ -9679,6 +10128,255 @@
"requestId": { "type": "string" }, "requestId": { "type": "string" },
"timestamp": { "type": "string", "format": "date-time" } "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": { "securitySchemes": {

275
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 <accessToken>`,身份只从 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 <accessToken>
```
`categoryId` 可省略:
```http
GET /api/v1/skills/word-cloud?industryId=1&occupationId=2
```
成功响应为 `ApiResponse<SkillCloudResponse>`。`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 <accessToken>
```
匹配集合严格取当前筛选结果的 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<SkillMatchResponse>`:
```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 <accessToken>
```
学习路径只消费个人匹配结果中的 `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`。

39
docs/verify_openapi.py

@ -6,7 +6,7 @@ p = PROJECT_ROOT / 'docs' / 'openapi-rc.json'
d = json.load(open(p, encoding='utf-8')) d = json.load(open(p, encoding='utf-8'))
print('paths:', len(d['paths'])) 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'] paths = d['paths']
required = [ required = [
@ -66,6 +66,9 @@ required = [
'/admin/reports', '/admin/reports',
'/admin/reports/{id}', '/admin/reports/{id}',
'/admin/reports/{id}/decision', '/admin/reports/{id}/decision',
'/skills/word-cloud',
'/skills/match',
'/skills/learning-path',
] ]
for path in required: for path in required:
assert path in paths, f"Missing path: {path}" 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 '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" assert 'post' in paths['/admin/reports/{id}/decision'], "Missing POST /admin/reports/{id}/decision"
print(" V33 report/block endpoints present (9 operations / 8 paths)") 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(" Offer advance endpoints all present")
print(" 26 operations all present") print(" 26 operations all present")
print(" Account deletion endpoints present (3 operations)") print(" Account deletion endpoints present (3 operations)")

Loading…
Cancel
Save