# 题目更新接口文档
> 对应接口:`PUT /api/question/{id}`
> 功能说明:更新题目信息,支持部分字段更新(只传需要修改的字段即可)
> 接口版本:V1.0
> 更新日期:2026-06-08
---
## 目录
- [1. 接口定义](#1-接口定义)
- [2. 请求参数](#2-请求参数)
- [3. 请求示例](#3-请求示例)
- [4. 响应说明](#4-响应说明)
- [5. 错误码说明](#5-错误码说明)
- [6. 边界情况说明](#6-边界情况说明)
- [7. curl 测试命令](#7-curl-测试命令)
---
## 1. 接口定义
| 项目 | 内容 |
|------|------|
| **请求方法** | `PUT` |
| **接口路径** | `/api/question/{id}` |
| **Content-Type** | `application/json` |
| **认证方式** | Bearer Token (JWT) |
| **基础URL** | `http://localhost:8080`(开发环境) |
| **完整URL示例** | `PUT http://localhost:8080/api/question/378` |
### 1.1 路径参数说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | String | **是** | 题目ID,支持两种格式:
① 自增数字ID(如 `378`)
② UUID格式的 questionId(如 `e6bb2566-90a8-4651-b553-083a1d695b3c`) |
### 1.2 认证要求
需要在请求头中携带 JWT Token:
```
Authorization: Bearer
```
Token 通过 `/api/auth/login` 接口登录获取。
---
## 2. 请求参数
所有请求字段均为**可选**(部分更新),只传需要修改的字段即可。
### 2.1 请求体结构
```json
{
"question_type": "single_choice",
"difficulty": 3,
"content": {
"stem": "题目标题",
"data": {},
"answer": "A"
},
"tags": ["数学", "代数"],
"score": 5.00,
"status": "pending",
"reviewer_comment": "审核意见",
"document_id": "uuid-string",
"document_name": "文件名.doc",
"knowledge_base_path": "public/dept_1",
"source_context": "原文上下文内容",
"chunk_ids": ["chunk_001", "chunk_002"],
"source_chunks_used": 3,
"page_numbers": [12, 15]
}
```
### 2.2 字段详情
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|---------|
| `question_type` | String | 否 | 题目类型 | 必须是以下之一:`single_choice`、`multiple_choice`、`true_false`、`fill_blank`、`subjective` |
| `difficulty` | Integer | 否 | 难度等级 | 1-5 |
| `content` | Object | 否 | 题目内容(JSON对象) | 不能为空的JSON对象 |
| `content.stem` | String | 视题型 | 题干 | 主观题必填 |
| `content.data` | Object | 视题型 | 额外数据(如选择题的选项列表) | - |
| `content.answer` | String | 视题型 | 答案 | 选择题为选项字母,判断题 "T"/"F" |
| `tags` | Array\ | 否 | 知识点标签 | - |
| `score` | BigDecimal | 否 | 分值 | 范围:`-999.99` ~ `999.99`(数据库 decimal(5,2) 约束) |
| `status` | String | 否 | 审核状态 | `pending`、`approved`、`rejected` |
| `reviewer_comment` | String | 否 | 审核评论 | - |
| `document_id` | String | 否 | 关联文档ID | - |
| `document_name` | String | 否 | 文档名称 | - |
| `knowledge_base_path` | String | 否 | 知识库路径 | 最大500字符 |
| `source_context` | String | 否 | 原文上下文 | - |
| `chunk_ids` | Array\ | 否 | 切片ID列表 | - |
| `source_chunks_used` | Integer | 否 | 使用的源切片数量 | - |
| `page_numbers` | Array\ | 否 | 页码列表 | - |
---
## 3. 请求示例
### 3.1 修改题型和难度
```json
// PUT /api/question/378
{
"question_type": "multiple_choice",
"difficulty": 4
}
```
### 3.2 修改题目内容和分值
```json
// PUT /api/question/378
{
"content": {
"stem": "以下哪项是正确的?",
"data": {
"options": [
{ "label": "A", "text": "选项A" },
{ "label": "B", "text": "选项B" },
{ "label": "C", "text": "选项C" }
]
},
"answer": "B"
},
"score": 10.00
}
```
### 3.3 审核通过题目
```json
// PUT /api/question/378
{
"status": "approved",
"reviewer_comment": "内容正确,通过审核"
}
```
### 3.4 使用 UUID 更新
```json
// PUT /api/question/e6bb2566-90a8-4651-b553-083a1d695b3c
{
"difficulty": 5,
"score": 8.00
}
```
---
## 4. 响应说明
### 4.1 通用响应格式
```json
{
"code": 200,
"message": "操作成功",
"data": {},
"timestamp": "2026-06-08 10:00:00",
"success": true
}
```
### 4.2 更新成功响应(code=200)
```json
{
"code": 200,
"message": "题目更新成功",
"data": {
"id": 378,
"questionId": "41a890f2-c9b9-466f-b9f6-e0bd237329b7",
"questionType": "multiple_choice",
"difficulty": 4,
"tags": null,
"score": 5.00,
"version": "1.0",
"documentId": null,
"excludeStems": null,
"knowledgeBasePath": null,
"sourceContext": null,
"chunkIds": null,
"sourceChunksUsed": 0,
"examCount": 0,
"correctCount": 0,
"lastExamTime": null,
"pageNumbers": null,
"content": {
"stem": "以下哪项是正确的?",
"data": {
"options": [
{ "label": "A", "text": "选项A" },
{ "label": "B", "text": "选项B" }
]
},
"answer": "A"
},
"status": "pending",
"reviewerComment": null,
"createdAt": "2026-06-08 09:00:00",
"updatedAt": "2026-06-08 10:00:00",
"isDeleted": 0
},
"timestamp": "2026-06-08 10:00:00",
"success": true
}
```
### 4.3 data 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | Long | 自增主键ID |
| `questionId` | String | 题目UUID(全局唯一标识) |
| `questionType` | String | 题目类型 |
| `difficulty` | Integer | 难度等级 1-5 |
| `tags` | JsonNode | 知识点标签(JSON数组或null) |
| `score` | BigDecimal | 分值 |
| `version` | String | 版本号(默认"1.0") |
| `documentId` | String | 关联文档ID |
| `excludeStems` | String | 题干内容(AI去重用) |
| `knowledgeBasePath` | String | 知识库路径 |
| `sourceContext` | String | 原文上下文 |
| `chunkIds` | JsonNode | 切片ID列表 |
| `sourceChunksUsed` | Integer | 使用切片数 |
| `examCount` | Integer | 出题次数 |
| `correctCount` | Integer | 正确次数 |
| `lastExamTime` | DateTime | 最后作答时间 |
| `pageNumbers` | JsonNode | 页码列表 |
| `content` | JsonNode | 题目内容(JSON对象) |
| `status` | String | 审核状态 |
| `reviewerComment` | String | 审核评论 |
| `createdAt` | DateTime | 创建时间 |
| `updatedAt` | DateTime | 最后更新时间 |
| `isDeleted` | Integer | 逻辑删除标志(0=正常,1=已删除) |
---
## 5. 错误码说明
### 5.1 业务错误码
| HTTP状态码 | code | message | 触发场景 |
|-----------|------|---------|---------|
| **200** | 200 | 题目更新成功 | 更新成功 |
| **404** | 404 | 题目不存在: {id} | 指定ID的题目不存在 |
| **400** | 400 | 分值超出范围,允许范围: -999.99 ~ 999.99 | score 超出 decimal(5,2) 精度范围 |
| **400** | 400 | 知识库路径过长,最大允许500字符 | knowledge_base_path 超过500字符 |
| **400** | 400 | 无效的题目类型: {type} | question_type 不在枚举值内 |
| **400** | 400 | 题目内容不能为空 | content 传了空对象 `{}` |
| **400** | 400 | 题目可能已被删除或归档 | 记录已被逻辑删除,updateById 影响行数为0 |
| **400** | 400 | 题目不存在 | getById 返回 null |
### 5.2 异常错误码
| HTTP状态码 | code | message | 触发场景 |
|-----------|------|---------|---------|
| **500** | 500 | 更新题目失败: {异常信息} | 数据库异常、JSON序列化异常等(Controller 兜底catch) |
| **401** | 401 | 用户未登录 | 未携带 Token 或 Token 无效 |
| **403** | 403 | 无权限访问 | 当前用户无操作权限 |
---
## 6. 边界情况说明
### 6.1 ID 格式自动识别
接口自动识别传入的 `{id}` 格式:
- **纯数字**(如 `378`):按自增主键 `id` 查询
- **非数字**(如 `e6bb2566-...`):按 `question_id`(UUID)查询
- **非法格式**:抛出 `NumberFormatException`,返回 400
### 6.2 部分更新机制
接口支持**部分字段更新**(PATCH 语义),请求体中只需包含要修改的字段:
- 未传的字段保持原值不变
- 传了 `null` 的字段会被设为 `null`
- `content` 为完整替换,非合并更新
### 6.3 防御性校验
Service 层对以下输入进行校验:
| 校验项 | 拦截条件 | 错误信息 |
|--------|---------|---------|
| score 范围 | `score > 999.99 \|\| score < -999.99` | "分值超出范围,允许范围: -999.99 ~ 999.99" |
| 路径长度 | `knowledge_base_path.length > 500` | "知识库路径过长,最大允许500字符" |
| 题型枚举 | `question_type ∉ validTypes` | "无效的题目类型: {type}" |
| 内容非空 | `content != null && content.isEmpty()` | "题目内容不能为空" |
| 存在性检查 | `getById(id) == null` | "题目不存在" |
| 更新影响行数 | `updateById 返回 false` | "题目可能已被删除或归档" |
### 6.4 逻辑删除
- 已逻辑删除的记录(`is_deleted = 1`)仍可通过 `getById` 查到
- 但 `updateById` 由于 MyBatis-Plus 自动追加 `WHERE is_deleted = 0` 条件,将返回 `false`
- 此时接口会抛出 `IllegalStateException`,返回 400 错误
### 6.5 JSON 序列化
- 后台使用 `JacksonTypeHandler` 将 `content` 字段序列化为 JSON 存入数据库
- 使用独立的 `ObjectMapper` 实例(非 Spring 管理的全局配置)
- 避免传入 `NaN`、`Infinity` 等非法 JSON 值,否则可能触发 `JsonProcessingException`
---
## 7. curl 测试命令
### 7.1 获取 Token
```bash
curl -X POST "http://localhost:8080/api/auth/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
```
### 7.2 正常更新(数字ID)
```bash
curl -X PUT "http://localhost:8080/api/question/378" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer " \
-d '{
"question_type": "multiple_choice",
"difficulty": 4,
"score": 5.00,
"content": {
"stem": "更新后的题干",
"data": {
"options": [
{"label": "A", "text": "选项A"},
{"label": "B", "text": "选项B"}
]
},
"answer": "A"
}
}'
```
### 7.3 使用 UUID 更新
```bash
curl -X PUT "http://localhost:8080/api/question/e6bb2566-90a8-4651-b553-083a1d695b3c" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer " \
-d '{"difficulty": 3}'
```
### 7.4 score 超出范围(测试防御性校验)
```bash
curl -X PUT "http://localhost:8080/api/question/378" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer " \
-d '{"score": 1000}'
# 期望响应: {"code": 500, "message": "更新题目失败: 分值超出范围,允许范围: -999.99 ~ 999.99"}
```
### 7.5 无效题型(测试防御性校验)
```bash
curl -X PUT "http://localhost:8080/api/question/378" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer " \
-d '{"question_type": "invalid_type"}'
# 期望响应: {"code": 500, "message": "更新题目失败: 无效的题目类型: invalid_type"}
```
### 7.6 不存在的题目
```bash
curl -X PUT "http://localhost:8080/api/question/99999" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer " \
-d '{"difficulty": 3}'
# 期望响应: {"code": 404, "message": "题目不存在: 99999"}
```