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`**。
- **只读**:`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 文本、学习时长或进度记录。

698
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": {

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'))
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)")

Loading…
Cancel
Save