初始化项目代码

This commit is contained in:
2026-06-03 12:43:48 +08:00
commit 5b12356567
303 changed files with 40729 additions and 0 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,468 @@
# 出题批题接口 - 后端对接指南
## 一、接口概览
| 接口 | 方法 | 功能 | 超时建议 |
|------|------|------|----------|
| `/exam/generate` | POST | 生成题目 | 120秒 |
| `/exam/grade` | POST | 批阅答案 | 60秒 |
---
## 二、出题接口
### 2.1 请求
```
POST /exam/generate
Content-Type: application/json
```
**请求体字段说明:**
| 字段 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `file_path` | string | ✅ | 文档路径(相对于 documents 目录) |
| `collection` | string 或 string[] | ✅ | 向量库名称,支持数组(按优先级顺序检索,找到文件即停止) |
| `question_types` | object | ✅ | 题型及数量 |
| `difficulty` | int | ❌ | 难度等级 1-5默认 3 |
| `request_id` | string | ❌ | 请求ID相同ID返回缓存结果幂等性 |
**请求示例:**
```json
{
"file_path": "薪酬制度.docx",
"collection": ["dept_hr", "public_kb"],
"question_types": {
"single_choice": 3,
"multiple_choice": 2,
"true_false": 2,
"fill_blank": 2,
"subjective": 1
},
"difficulty": 3,
"request_id": "uuid-for-idempotency"
}
```
> **collection 检索逻辑说明**
> - 出题接口采用**按优先级顺序检索**策略
> - 先在 `dept_hr` 中查找文件,找到则停止
> - 未找到则继续在 `public_kb` 中查找
> - 这与 RAG 问答接口的**并行检索+融合**策略不同
> - 原因:出题需要特定文件的完整内容,而非广泛搜索
### 2.2 响应
**成功响应:**
```json
{
"success": true,
"status": "success",
"status_code": 2011,
"message": "出题完成",
"data": {
"request_id": "uuid-xxx",
"total": 10,
"source_chunks_used": 25,
"questions": [
{
"question_type": "single_choice",
"difficulty": 3,
"content": {
"stem": "题干内容",
"data": {
"options": [
{"key": "A", "content": "选项A"},
{"key": "B", "content": "选项B"},
{"key": "C", "content": "选项C"},
{"key": "D", "content": "选项D"}
]
},
"answer": "B",
"explanation": "答案解析"
},
"source_trace": {
"document_name": "薪酬制度.docx",
"chunks_count": 3
}
}
]
}
}
```
**失败响应:**
```json
{
"success": false,
"status": "failed",
"error_code": "FILE_NOT_FOUND",
"message": "文件不存在"
}
```
### 2.3 返回字段说明
**RAG 服务返回的字段(后端需存储):**
| 字段 | 类型 | 说明 | 后端是否需要存储 |
|------|------|------|------------------|
| `question_type` | string | 题型 | ✅ 存储 |
| `difficulty` | int | 难度 1-5 | ✅ 存储 |
| `content` | object | 题目内容 | ✅ 存储 |
| `content.stem` | string | 题干 | ✅ |
| `content.data` | object | 附加数据(选项、评分标准等) | ✅ |
| `content.answer` | any | 正确答案 | ✅ 批阅时需要 |
| `content.explanation` | string | 答案解析 | ✅ |
| `source_trace` | object | 来源追踪 | ✅ 存储 |
**后端需要自己生成的字段:**
| 字段 | 说明 |
|------|------|
| `question_id` | UUID唯一标识 |
| `score` | 满分,根据业务需求设定 |
| `tags` | 标签 |
| `status` | 状态(待审核/已通过/已拒绝) |
> **重要**RAG 服务不返回 `score` 字段,分值由后端在入库时指定。
### 2.4 题型格式对照表
| 题型 | question_type | content.answer 格式 | content.data |
|------|---------------|---------------------|--------------|
| 单选题 | `single_choice` | `"B"` | `{options: [{key, content}]}` |
| 多选题 | `multiple_choice` | `["A", "C"]` | `{options: [{key, content}]}` |
| 判断题 | `true_false` | `"T"``"F"` | 无 |
| 填空题 | `fill_blank` | `[["答案1"], ["答案2", "同义词"]]` | `{blank_count: 2}` |
| 简答题 | `subjective` | `"参考范文..."` | `{scoring_points: [...]}` |
### 2.5 错误码
| 错误码 | HTTP | 说明 |
|--------|------|------|
| `FILE_NOT_FOUND` | 404 | 指定文件不存在 |
| `COLLECTION_NOT_FOUND` | 404 | 指定向量库不存在 |
| `NO_CONTENT` | 400 | 文件内容为空,无法出题 |
| `LLM_ERROR` | 500 | LLM 调用失败 |
| `PARSE_ERROR` | 500 | 解析失败 |
---
## 三、批阅接口
### 3.1 请求
```
POST /exam/grade
Content-Type: application/json
```
**请求体字段说明:**
| 字段 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `request_id` | string | ❌ | 请求ID用于追踪 |
| `answers` | array | ✅ | 答案列表 |
**answers 数组中每个对象的字段:**
| 字段 | 类型 | 必需 | 说明 | 来源 |
|------|------|------|------|------|
| `question_id` | string | ✅ | 题目ID | 后端数据库 |
| `question_type` | string | ✅ | 题型 | 后端数据库 |
| `question_content` | object | ✅ | 题目内容(含正确答案) | 后端数据库 |
| `student_answer` | any | ✅ | 学生答案 | 学生提交 |
| `max_score` | number | ✅ | 满分 | 后端数据库 |
**请求示例:**
```json
{
"request_id": "grade-uuid-xxx",
"answers": [
{
"question_id": "q-001",
"question_type": "single_choice",
"question_content": {
"stem": "根据公司规定,员工薪资由哪几部分组成?",
"data": {
"options": [
{"key": "A", "content": "基本工资+奖金"},
{"key": "B", "content": "基本工资+绩效奖金+津贴补贴"},
{"key": "C", "content": "基本工资+加班费"},
{"key": "D", "content": "基本工资"}
]
},
"answer": "B"
},
"student_answer": "B",
"max_score": 5.0
},
{
"question_id": "q-002",
"question_type": "multiple_choice",
"question_content": {
"stem": "以下哪些属于绩效奖金的评定因素?",
"data": {
"options": [
{"key": "A", "content": "工作质量"},
{"key": "B", "content": "工作态度"},
{"key": "C", "content": "考勤情况"},
{"key": "D", "content": "团队协作"}
]
},
"answer": ["A", "B", "D"]
},
"student_answer": ["A", "B"],
"max_score": 4.0
},
{
"question_id": "q-003",
"question_type": "true_false",
"question_content": {
"stem": "公司规定员工每月绩效奖金上限为工资的20%。",
"answer": "F"
},
"student_answer": "T",
"max_score": 2.0
},
{
"question_id": "q-004",
"question_type": "fill_blank",
"question_content": {
"stem": "员工薪资由___、___和___三部分组成。",
"data": {"blank_count": 3},
"answer": [["基本工资"], ["绩效奖金", "绩效"], ["津贴补贴", "补贴"]]
},
"student_answer": ["基本工资", "绩效奖金", "交通补贴"],
"max_score": 6.0
},
{
"question_id": "q-005",
"question_type": "subjective",
"question_content": {
"stem": "请简述公司薪酬制度的核心原则。",
"data": {
"scoring_points": [
{"point": "公平性", "weight": 0.3},
{"point": "激励性", "weight": 0.3},
{"point": "竞争力", "weight": 0.2},
{"point": "合法性", "weight": 0.2}
]
},
"answer": "公司薪酬制度遵循公平、激励、竞争、合法四大原则..."
},
"student_answer": "我认为公司的薪酬制度应该公平公正...",
"max_score": 10.0
}
]
}
```
### 3.2 响应
**成功响应:**
```json
{
"success": true,
"status": "success",
"status_code": 2012,
"message": "批阅完成",
"data": {
"request_id": "grade-uuid-xxx",
"total_score": 14.0,
"total_max_score": 27.0,
"score_rate": 51.85,
"results": [
{
"question_id": "q-001",
"question_type": "single_choice",
"score": 5.0,
"max_score": 5.0,
"is_correct": true,
"student_answer": "B",
"correct_answer": "B",
"feedback": "回答正确"
},
{
"question_id": "q-002",
"question_type": "multiple_choice",
"score": 2.0,
"max_score": 4.0,
"is_correct": false,
"student_answer": ["A", "B"],
"correct_answer": ["A", "B", "D"],
"feedback": "少选了 D 选项,正确答案: A, B, D"
},
{
"question_id": "q-003",
"question_type": "true_false",
"score": 0.0,
"max_score": 2.0,
"is_correct": false,
"student_answer": "T",
"correct_answer": "F",
"feedback": "正确答案: F"
},
{
"question_id": "q-004",
"question_type": "fill_blank",
"score": 4.0,
"max_score": 6.0,
"is_correct": false,
"student_answer": ["基本工资", "绩效奖金", "交通补贴"],
"correct_answer": [["基本工资"], ["绩效奖金", "绩效"], ["津贴补贴", "补贴"]],
"feedback": "第1、2空正确第3空答案应为津贴补贴或补贴"
},
{
"question_id": "q-005",
"question_type": "subjective",
"score": 3.0,
"max_score": 10.0,
"is_correct": false,
"student_answer": "我认为公司的薪酬制度应该公平公正...",
"correct_answer": "公司薪酬制度遵循公平、激励、竞争、合法四大原则...",
"feedback": "得分点:提到公平性概念,但未展开说明激励性和竞争力原则"
}
]
}
}
```
### 3.3 返回字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `total_score` | number | 总得分 |
| `total_max_score` | number | 总满分 |
| `score_rate` | number | 得分率(百分比) |
| `results` | array | 每道题的批阅结果 |
| `results[].question_id` | string | 题目ID |
| `results[].score` | number | 该题得分 |
| `results[].max_score` | number | 该题满分 |
| `results[].is_correct` | boolean | 是否完全正确 |
| `results[].feedback` | string | 批阅反馈 |
### 3.4 批阅规则
| 题型 | 批阅方式 | 得分规则 |
|------|----------|----------|
| 单选题 | 精确匹配 | 正确得满分,错误得 0 |
| 多选题 | 集合比对 | 全对满分,少选得一半,错选得 0 |
| 判断题 | 精确匹配 | 正确得满分,错误得 0 |
| 填空题 | 多答案匹配 | 每空独立评分,支持同义词匹配 |
| 简答题 | LLM 评分 | 根据得分点评分,不超过 `max_score` |
### 3.5 错误码
| 错误码 | HTTP | 说明 |
|--------|------|------|
| `INVALID_ANSWER_FORMAT` | 400 | 答案格式不正确 |
| `GRADING_ERROR` | 500 | 批阅过程出错 |
---
## 四、完整调用流程
### 4.1 出题流程
```
后端调用 /exam/generate
RAG 返回题目
(question_type, difficulty, content, source_trace)
后端生成 question_id
后端设置 score (满分)
后端设置 status
后端存入数据库
```
### 4.2 批阅流程
```
学生提交答案
后端从数据库查询:
- question_id
- question_type
- question_content (含正确答案)
- score (满分)
后端组装请求调用 /exam/grade
RAG 返回批阅结果
(score, feedback, is_correct)
后端存入成绩表
```
---
## 五、字段来源速查表
### 5.1 出题接口
| 字段 | 返回方 | 存储方 | 说明 |
|------|--------|--------|------|
| question_id | ❌ 不返回 | 后端生成 | UUID |
| question_type | ✅ RAG | 后端存储 | 题型 |
| difficulty | ✅ RAG | 后端存储 | 难度 1-5 |
| content | ✅ RAG | 后端存储 | 题目内容 |
| source_trace | ✅ RAG | 后端存储 | 来源追踪 |
| **score** | ❌ 不返回 | **后端设定** | 满分 |
| tags | ❌ 不返回 | 后端设定 | 标签 |
| status | ❌ 不返回 | 后端设定 | 状态 |
### 5.2 批阅接口
| 字段 | 来源 | 说明 |
|------|------|------|
| question_id | 后端数据库 | 题目唯一标识 |
| question_type | 后端数据库 | 题型 |
| question_content | 后端数据库 | 题目内容(含正确答案) |
| student_answer | 学生提交 | 学生作答 |
| **max_score** | **后端数据库** | 满分(决定得分上限) |
---
## 六、常见问题
### Q1: 出题接口返回的题目需要审核吗?
建议后端设置 `status` 字段进行审核流程,审核通过后再用于组卷。
### Q2: 分值如何动态调整?
后端在入库时自由设定 `score` 字段,批阅时传入 `max_score` 即可。RAG 服务不干预分值。
### Q3: 填空题同义词匹配是如何实现的?
填空题答案格式为 `[["主答案", "同义词1", "同义词2"], ...]`,学生答案匹配任意一个即视为正确。
### Q4: 批阅接口超时怎么办?
建议:
- 单次批阅不超过 50 道题
- 设置 60 秒超时
- 主观题较多时适当延长
---
**文档版本**: v1.1
**更新时间**: 2026-05-17
**相关文档**: [后端对接规范.md](./后端对接规范.md)

109
docs/error.txt Normal file
View File

@@ -0,0 +1,109 @@
index-DxCaeH_z.js:504 POST http://47.116.16.222/api/exam/record/103edf6a-e026-4314-889f-4187f6b44147/submit 500 (Internal Server Error)
(匿名) @ index-DxCaeH_z.js:504
xhr @ index-DxCaeH_z.js:504
dispatchRequest @ index-DxCaeH_z.js:506
Promise.then
_request @ index-DxCaeH_z.js:510
request @ index-DxCaeH_z.js:506
(匿名) @ index-DxCaeH_z.js:510
(匿名) @ index-DxCaeH_z.js:502
submit @ index-DxCaeH_z.js:659
fn @ index-DxCaeH_z.js:707
ft @ index-DxCaeH_z.js:707
Bt @ index-DxCaeH_z.js:709
createBaseVNode.onClick.fn.<computed>.fn.<computed> @ index-DxCaeH_z.js:709
callWithErrorHandling @ index-DxCaeH_z.js:16
callWithAsyncErrorHandling @ index-DxCaeH_z.js:16
ee @ index-DxCaeH_z.js:22
index-DxCaeH_z.js:707 [提交] submit(结束考试) 失败:
yt @ index-DxCaeH_z.js:707
ft @ index-DxCaeH_z.js:707
await in ft
Bt @ index-DxCaeH_z.js:709
createBaseVNode.onClick.fn.<computed>.fn.<computed> @ index-DxCaeH_z.js:709
callWithErrorHandling @ index-DxCaeH_z.js:16
callWithAsyncErrorHandling @ index-DxCaeH_z.js:16
ee @ index-DxCaeH_z.js:22
index-DxCaeH_z.js:707 message: 系统异常
yt @ index-DxCaeH_z.js:707
ft @ index-DxCaeH_z.js:707
await in ft
Bt @ index-DxCaeH_z.js:709
createBaseVNode.onClick.fn.<computed>.fn.<computed> @ index-DxCaeH_z.js:709
callWithErrorHandling @ index-DxCaeH_z.js:16
callWithAsyncErrorHandling @ index-DxCaeH_z.js:16
ee @ index-DxCaeH_z.js:22
index-DxCaeH_z.js:707 HTTP状态: 500
yt @ index-DxCaeH_z.js:707
ft @ index-DxCaeH_z.js:707
await in ft
Bt @ index-DxCaeH_z.js:709
createBaseVNode.onClick.fn.<computed>.fn.<computed> @ index-DxCaeH_z.js:709
callWithErrorHandling @ index-DxCaeH_z.js:16
callWithAsyncErrorHandling @ index-DxCaeH_z.js:16
ee @ index-DxCaeH_z.js:22
index-DxCaeH_z.js:707 响应数据: {"code":500,"message":"系统异常","data":null,"timestamp":"2026-06-01 08:44:52","success":false}
yt @ index-DxCaeH_z.js:707
ft @ index-DxCaeH_z.js:707
await in ft
Bt @ index-DxCaeH_z.js:709
createBaseVNode.onClick.fn.<computed>.fn.<computed> @ index-DxCaeH_z.js:709
callWithErrorHandling @ index-DxCaeH_z.js:16
callWithAsyncErrorHandling @ index-DxCaeH_z.js:16
ee @ index-DxCaeH_z.js:22
index-DxCaeH_z.js:707 响应头: {"connection":"keep-alive","content-type":"application/json","date":"Mon, 01 Jun 2026 08:44:52 GMT","server":"nginx/1.18.0 (Ubuntu)","transfer-encoding":"chunked","vary":"Origin, Access-Control-Request-Method, Access-Control-Request-Headers"}
yt @ index-DxCaeH_z.js:707
ft @ index-DxCaeH_z.js:707
await in ft
Bt @ index-DxCaeH_z.js:709
createBaseVNode.onClick.fn.<computed>.fn.<computed> @ index-DxCaeH_z.js:709
callWithErrorHandling @ index-DxCaeH_z.js:16
callWithAsyncErrorHandling @ index-DxCaeH_z.js:16
ee @ index-DxCaeH_z.js:22
index-DxCaeH_z.js:707 请求URL: /api/exam/record/103edf6a-e026-4314-889f-4187f6b44147/submit
yt @ index-DxCaeH_z.js:707
ft @ index-DxCaeH_z.js:707
await in ft
Bt @ index-DxCaeH_z.js:709
createBaseVNode.onClick.fn.<computed>.fn.<computed> @ index-DxCaeH_z.js:709
callWithErrorHandling @ index-DxCaeH_z.js:16
callWithAsyncErrorHandling @ index-DxCaeH_z.js:16
ee @ index-DxCaeH_z.js:22
index-DxCaeH_z.js:707 请求方法: post
yt @ index-DxCaeH_z.js:707
ft @ index-DxCaeH_z.js:707
await in ft
Bt @ index-DxCaeH_z.js:709
createBaseVNode.onClick.fn.<computed>.fn.<computed> @ index-DxCaeH_z.js:709
callWithErrorHandling @ index-DxCaeH_z.js:16
callWithAsyncErrorHandling @ index-DxCaeH_z.js:16
ee @ index-DxCaeH_z.js:22
index-DxCaeH_z.js:659 [examAPI] 正式考试提交批改 - recordId: 103edf6a-e026-4314-889f-4187f6b44147 题目数: 5
index-DxCaeH_z.js:707 [提交] gradeExamRecord(空答案) 失败:
yt @ index-DxCaeH_z.js:707
ft @ index-DxCaeH_z.js:707
await in ft
Bt @ index-DxCaeH_z.js:709
createBaseVNode.onClick.fn.<computed>.fn.<computed> @ index-DxCaeH_z.js:709
callWithErrorHandling @ index-DxCaeH_z.js:16
callWithAsyncErrorHandling @ index-DxCaeH_z.js:16
ee @ index-DxCaeH_z.js:22
index-DxCaeH_z.js:707 message: 提交失败: Index 0 out of bounds for length 0
yt @ index-DxCaeH_z.js:707
ft @ index-DxCaeH_z.js:707
await in ft
Bt @ index-DxCaeH_z.js:709
createBaseVNode.onClick.fn.<computed>.fn.<computed> @ index-DxCaeH_z.js:709
callWithErrorHandling @ index-DxCaeH_z.js:16
callWithAsyncErrorHandling @ index-DxCaeH_z.js:16
ee @ index-DxCaeH_z.js:22
index-DxCaeH_z.js:707 完全无响应对象, error keys:
yt @ index-DxCaeH_z.js:707
ft @ index-DxCaeH_z.js:707
await in ft
Bt @ index-DxCaeH_z.js:709
createBaseVNode.onClick.fn.<computed>.fn.<computed> @ index-DxCaeH_z.js:709
callWithErrorHandling @ index-DxCaeH_z.js:16
callWithAsyncErrorHandling @ index-DxCaeH_z.js:16
ee @ index-DxCaeH_z.js:22
index-DxCaeH_z.js:707 [提交] 所有2种提交方式均失败

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,172 @@
# 前端图片 URL 使用指南
## 概述
后端已优化图片访问方式,现在支持通过 Nginx 静态资源直接访问图片,提高了图片加载效率。
## 图片数据结构
### 聊天消息中的图片
```json
{
"images": [
{
"id": "abc123",
"url": "/files/img/abc123.jpg",
"type": "image",
"source": "文档名.pdf",
"page": 1,
"description": "图片描述"
}
]
}
```
## URL 格式说明
### 两种 URL 格式
后端会返回以下两种格式的 URL
1. **新格式(推荐):`/files/img/xxx.jpg` - 通过 Nginx 静态资源访问
2. **旧格式(兼容)**`/api/image/xxx/data` - 通过后端 API 代理访问
## 前端适配说明
### 无需修改的情况
如果您的前端目前使用如下方式处理图片,则无需修改代码:
```javascript
// 直接使用后端返回的 URL
<img src={image.url} alt={image.description} />
```
### 推荐的处理方式
```javascript
// 通用图片加载器
function loadImage(imageData) {
const imageUrl = imageData.url;
// 直接使用后端返回的 URL
const img = document.createElement('img');
img.src = imageUrl;
img.alt = imageData.description || '图片';
img.onerror = function() {
// 图片加载失败时的处理
console.error('图片加载失败:', imageUrl);
};
return img;
}
```
### 检查 URL 类型
```javascript
function isNginxUrl(url) {
return url && url.startsWith('/files/');
}
function isApiUrl(url) {
return url && url.includes('/api/image/');
}
```
## SSE 流处理示例
### JavaScript 示例
```javascript
const eventSource = new EventSource('/api/chat/stream');
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === 'finish') {
// 处理图片
if (data.images && data.images.length > 0) {
renderImages(data.images);
}
}
};
function renderImages(images) {
const container = document.getElementById('image-container');
images.forEach(image => {
const img = document.createElement('img');
img.src = image.url;
img.alt = image.description || '图片';
img.style.maxWidth = '100%';
container.appendChild(img);
// 显示图片描述
if (image.description) {
const desc = document.createElement('p');
desc.textContent = image.description;
container.appendChild(desc);
}
});
}
```
### React 示例
```jsx
function ChatImages({ images }) {
return (
<div className="chat-images">
{images && images.map((image, index) => (
<div key={index} className="image-item">
<img
src={image.url}
alt={image.description}
className="chat-image"
/>
{image.description && (
<div className="image-description">
{image.description}
</div>
)}
</div>
))}
</div>
);
}
```
### Vue 示例
```vue
<template>
<div class="chat-images">
<div v-for="(image, index) in images" :key="index" class="image-item">
<img
:src="image.url"
:alt="image.description"
class="chat-image"
/>
<div v-if="image.description" class="image-description">
{{ image.description }}
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ChatImages',
props: {
images: {
type: Array,
default: () => []
}
}
}
</script>
```
## 历史记录查看
在查看历史会话时,后端已自动将图片 URL 转换为 Nginx 格式,无需额外处理。
## 注意事项
1. **向后兼容**:后端保留了对旧 URL 格式的支持,以确保旧数据仍然可用
2. **错误处理**:建议添加图片加载失败的错误处理逻辑
3. **相对路径**`/files/` 开头的 URL 是相对路径,会自动根据当前域名拼接完整地址
## 兼容性
- 无需对现有前端代码进行大量修改
- 两种格式都支持

View File

@@ -0,0 +1,251 @@
# 前端图片渲染修复指南
## 问题分析
根据日志分析,**后端已完美修复**,图片数据已正确返回,但前端没有正确渲染:
### 后端返回的数据格式
```javascript
{
"success": true,
"data": [
{
"id": 175,
"role": "assistant",
"content": "...",
"images": [
{
"doc_name": "1.docx",
"page": 1,
"image_id": "49d2910b148d.jpg",
"url": "/files/img/49d2910b148d.jpg"
}
]
}
]
}
```
### 问题现象
从HTML片段可以看到页面中只有引用来源和文本内容**没有任何 `<img>` 标签渲染出来**。
---
## 修复步骤
### 1. 确认前端代码位置
首先找到渲染聊天消息的前端组件,通常在以下位置:
- `src/components/ChatMessage.vue`
- `src/views/ChatView.vue`
- 或其他类似位置
### 2. 修改消息渲染代码
在渲染消息的地方,添加对 `images` 字段的处理:
#### Vue 组件示例:
```vue
<template>
<div class="message-content">
<!-- 原有文本渲染 -->
<div v-html="message.content"></div>
<!-- 新增图片渲染 -->
<div v-if="message.images && message.images.length > 0" class="message-images">
<div v-for="(image, index) in message.images" :key="index" class="image-item">
<img
:src="image.url"
:alt="image.description || '图片'"
class="message-image"
loading="lazy"
@error="handleImageError($event, image)"
/>
<div v-if="image.doc_name" class="image-info">
<span class="doc-name">{{ image.doc_name }}</span>
<span v-if="image.page" class="page-num"> {{ image.page }} </span>
</div>
</div>
</div>
<!-- 原有引用卡片 -->
<div v-if="message.sources" class="references-card">
<!-- ... -->
</div>
</div>
</template>
<script>
export default {
name: 'ChatMessage',
props: {
message: {
type: Object,
required: true
}
},
methods: {
handleImageError(event, image) {
console.error('图片加载失败:', image);
// 可选:显示占位图
// event.target.src = '/placeholder-image.jpg';
}
}
}
</script>
<style scoped>
.message-images {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.image-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.message-image {
max-width: 100%;
max-height: 400px;
border-radius: 8px;
object-fit: contain;
cursor: pointer;
transition: transform 0.2s ease;
}
.message-image:hover {
transform: scale(1.02);
}
.image-info {
font-size: 12px;
color: #999;
display: flex;
gap: 8px;
}
</style>
```
### 3. 处理历史会话消息
确认在获取历史会话的接口调用后,正确地将数据传递给消息渲染组件:
```javascript
// 示例:在 API 调用处
async function loadSessionHistory(sessionId) {
try {
const response = await fetch(`/api/ai/session/${sessionId}`);
const result = await response.json();
if (result.success) {
// 确保 messages 数据正确传递给组件
messages.value = result.data;
}
} catch (error) {
console.error('加载历史会话失败:', error);
}
}
```
### 4. 添加图片点击预览功能(可选优化)
```vue
<!-- 在组件中添加图片预览模态框 -->
<div v-if="showPreview && previewImage" class="image-preview-overlay" @click="closePreview">
<img :src="previewImage.url" class="preview-image" alt="预览" />
<button class="close-btn" @click.stop="closePreview">×</button>
</div>
<script>
// 在 methods 中添加
openPreview(image) {
this.previewImage = image;
this.showPreview = true;
}
closePreview() {
this.showPreview = false;
this.previewImage = null;
}
</script>
<!-- 修改 img 标签添加点击事件 -->
<img
:src="image.url"
:alt="image.description || '图片'"
class="message-image"
@click="openPreview(image)"
@error="handleImageError($event, image)"
/>
```
---
## 验证步骤
1. **检查浏览器控制台**打开浏览器开发者工具F12查看 Console 标签,确认没有 JavaScript 错误
2. **检查网络请求**:在 Network 标签中,确认图片请求(如 `/files/img/49d2910b148d.jpg`)状态码为 200
3. **确认数据结构**:在 Network 标签中,检查 `/api/ai/session/{sessionId}` 接口的响应,确认 `images` 字段存在且格式正确
---
## 常见问题排查
### 问题1images 字段为 null 或 undefined
**原因**:可能旧版本的消息没有 images 字段
**解决**:添加空值检查
```vue
<div v-if="message.images && message.images.length > 0" class="message-images">
```
### 问题2图片加载 404
**原因**Nginx 路径配置问题
**解决**:确认 Nginx 配置正确映射了 `/files/img/` 路径
### 问题3图片跨域
**原因**:图片域名与前端域名不一致
**解决**:使用后端代理接口 `/api/image/{imageId}/data` 而不是 `/files/img/` 路径
---
## 后端返回的完整数据说明
后端返回的 ChatMessage 中 `images` 字段的完整结构:
```json
{
"images": [
{
"image_id": "49d2910b148d.jpg",
"url": "/files/img/49d2910b148d.jpg",
"doc_name": "1.docx",
"page": 1,
"description": "图片描述(可选)",
"type": "image"
}
]
}
```
主要字段说明:
- `url`: 图片的 Nginx 静态资源路径,直接使用即可
- `image_id`: 图片的唯一标识
- `doc_name`: 来源文档名称
- `page`: 来源页码
- `description`: 图片描述(可能为 null
---
## 修改总结
| 任务 | 优先级 |
|------|--------|
| 在消息渲染组件中添加 `images` 字段处理 | 🔴 高 |
| 确保正确解析和渲染图片列表 | 🔴 高 |
| 添加图片加载错误处理 | 🟡 中 |
| 添加图片预览功能 | 🟢 低 |
| 优化图片样式和交互 | 🟢 低 |

View File

@@ -0,0 +1,70 @@
# 图片 URL 优化更新文档
## 概述
本次更新优化了图片访问方式,从原来通过后端 API 代理访问改为直接通过 Nginx 静态资源访问,提高了图片加载效率。
## 修改内容
### 1. 配置文件
- 使用配置项 `file.upload.path` 确定本地保存路径
- 使用配置项 `file.upload.url-prefix` 确定 Nginx 访问前缀(默认为 `/files/`
### 2. ImageService 接口
- 新增 `getNginxUrlByImageId(String imageId)` 方法,用于根据图片 ID 获取 Nginx 格式的 URL
### 3. ImageServiceImpl 实现
- 使用配置的 `file.upload.path` + `/img` 作为图片存储目录
- 保存图片时返回相对路径(如 `img/xxx.jpg`)而非绝对路径
- 修改本地读取图片逻辑,支持相对路径
- 新增 `getNginxUrlByImageId` 方法
- 新增 `extractFilenameFromPath` 辅助方法
### 4. AiChatServiceImpl 修改
- 在保存图片后立即更新 imageMetadata 中的 url 为 Nginx 格式
- 在 SSE 的 finish 事件中,对无 metadata 的图片也尝试生成 Nginx 格式 URL
### 5. ChatServiceImpl 修改
- 查看历史记录时,将图片 URL 从 `/api/image/xxx/data` 转换为 Nginx 格式(`/files/img/xxx.jpg`
## 使用示例
### Nginx 配置示例
```nginx
server {
listen 80;
location /files/ {
alias /path/to/upload/;
expires 7d;
add_header Cache-Control "public, immutable";
}
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
### 配置文件 (application-prod.yml)
```yaml
file:
upload:
path: /opt/deploy/uploads/
url-prefix: /files/
```
## 部署说明
1. 确保 Nginx 配置正确,`/files/` 路径指向实际的上传目录
2. 确保应用配置文件中的 `file.upload.path``file.upload.url-prefix` 设置正确
3. 重新构建并部署应用
## 兼容性
- 保留了向后兼容,如果 Nginx 格式 URL 无法获取,会回退为原 `/api/image/xxx/data` 格式
- 旧数据仍然可以正常访问
## 图片 URL 格式对比
| 旧格式 | 新格式 |
|--------|--------|
| /api/image/xxx/data | /files/img/xxx.jpg |

File diff suppressed because it is too large Load Diff

2128
docs/完整接口文档.md Normal file

File diff suppressed because it is too large Load Diff

473
docs/批阅接口测试.md Normal file
View File

@@ -0,0 +1,473 @@
# 批阅接口测试文档
## 一、接口说明
| 接口 | 地址 | 功能 |
| -------- | ------------------------------------------------------------- | --------------------- |
| 后端通用接口 | POST <http://localhost:8080/api/exam/grade> | 批阅答案(日常练习/模拟考/正式考试通用) |
| 后端正式考试接口 | POST <http://localhost:8080/api/exam/record/{recordId}/grade> | 正式考试专用批阅接口 |
| AI端接口 | POST <http://localhost:5001/exam/grade> | AI端批阅答案 |
**answerType 参数说明**:后端接口支持 `answerType` 字段区分答题类型,前端传入什么值后端就使用什么值。
| answerType 值 | 说明 | 重复答题 |
| ------------- | ---- | ----------- |
| `practice` | 日常练习 | 允许(更新记录) |
| `mock_exam` | 模拟考 | 允许(更新记录) |
| `formal_exam` | 正式考试 | 不允许(唯一索引保护) |
***
## 二、接口路径规范
**注意**:所有接口路径都在 `/api` 前缀下,实际完整路径为:
- `http://localhost:8080/api/exam/grade`
- `http://localhost:8080/api/exam/record/{recordId}/grade`
***
## 三、获取 Token
在测试前,需要先获取有效的访问令牌:
```bash
curl -X POST http://localhost:8080/api/auth/login -H "Content-Type: application/json" -d '{"username":"admin","password":"123456"}'
```
**新 Token**有效期2小时
```
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsImlhdCI6MTc4MDAzMTgyNSwiZXhwIjoxNzgwMDM5MDI1fQ.an_bqtorziJ5Yb-otGPIIg400fCM1-VLLfX7mgQYHspOgSsTczcSY70evxsMUnfGmOr_euXgXwf2VkMQoXpG5w
```
***
## 四、后端接口测试
### 4.1 单选题测试(日常练习)
```bash
curl -X POST http://localhost:8080/api/exam/grade -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsImlhdCI6MTc4MDAzMTgyNSwiZXhwIjoxNzgwMDM5MDI1fQ.an_bqtorziJ5Yb-otGPIIg400fCM1-VLLfX7mgQYHspOgSsTczcSY70evxsMUnfGmOr_euXgXwf2VkMQoXpG5w" -d '{"answerType":"practice","requestId":"test_001","answers":[{"questionId":"9d05c475-b4fe-4679-a450-ee88a94b3b34","questionType":"single_choice","questionContent":{"stem":"根据文档内容,务求实效原则的核心要求聚焦于哪一根本目标?","data":{"options":[{"key":"A","content":"扩大终端数量规模"},{"key":"B","content":"提升终端信息化系统覆盖率"},{"key":"C","content":"着眼于终端功能发挥和服务水平提升"},{"key":"D","content":"统一各类终端建设外观标准"}]},"answer":"C"},"studentAnswer":"C","maxScore":2.0}]}'
```
### 4.2 多选题测试(日常练习)
```bash
curl -X POST http://localhost:8080/api/exam/grade -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsImlhdCI6MTc4MDAzMTgyNSwiZXhwIjoxNzgwMDM5MDI1fQ.an_bqtorziJ5Yb-otGPIIg400fCM1-VLLfX7mgQYHspOgSsTczcSY70evxsMUnfGmOr_euXgXwf2VkMQoXpG5w" -d '{"answerType":"practice","requestId":"test_002","answers":[{"questionId":"ee652f7e-da5e-4129-8766-c113277332f4","questionType":"multiple_choice","questionContent":{"stem":"依据务求实效原则,以下哪些做法符合其核心要求?(可多选)","data":{"options":[{"key":"A","content":"科学设定终端投入标准"},{"key":"B","content":"严格执行终端投入标准"},{"key":"C","content":"优先采购高配置但非必需的智能设备"},{"key":"D","content":"坚决杜绝不必要的支出和浪费"}]},"answer":["A","B","D"]},"studentAnswer":["A","B"],"maxScore":4.0}]}'
```
### 4.3 判断题测试(日常练习)
```bash
curl -X POST http://localhost:8080/api/exam/grade -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsImlhdCI6MTc4MDAzMTgyNSwiZXhwIjoxNzgwMDM5MDI1fQ.an_bqtorziJ5Yb-otGPIIg400fCM1-VLLfX7mgQYHspOgSsTczcSY70evxsMUnfGmOr_euXgXwf2VkMQoXpG5w" -d '{"answerType":"practice","requestId":"test_003","answers":[{"questionId":"76913ad8-5325-4f30-a03a-6e19e434b59e","questionType":"true_false","questionContent":{"stem":"务求实效原则允许在终端建设中适度预留冗余预算,以应对未来可能的功能升级需求。","data":{},"answer":"false"},"studentAnswer":"false","maxScore":1.0}]}'
```
### 4.4 填空题测试(日常练习)
```bash
curl -X POST http://localhost:8080/api/exam/grade -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsImlhdCI6MTc4MDAzMTgyNSwiZXhwIjoxNzgwMDM5MDI1fQ.an_bqtorziJ5Yb-otGPIIg400fCM1-VLLfX7mgQYHspOgSsTczcSY70evxsMUnfGmOr_euXgXwf2VkMQoXpG5w" -d '{"answerType":"practice","requestId":"test_004","answers":[{"questionId":"7ae3daef-0345-4852-bd37-336e74498cc4","questionType":"fill_blank","questionContent":{"stem":"务求实效原则强调以___为出发点通过科学设定和严格执行终端投入标准实现___与___的双重提升。","data":{"blank_count":3},"answer":["终端功能发挥和服务水平提升","终端功能发挥","服务水平提升"]},"studentAnswer":["终端功能发挥和服务水平提升","终端功能发挥","服务水平"],"maxScore":2.0}]}'
```
### 4.5 主观题测试(日常练习)
```bash
curl -X POST http://localhost:8080/api/exam/grade -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsImlhdCI6MTc4MDAzMTgyNSwiZXhwIjoxNzgwMDM5MDI1fQ.an_bqtorziJ5Yb-otGPIIg400fCM1-VLLfX7mgQYHspOgSsTczcSY70evxsMUnfGmOr_euXgXwf2VkMQoXpG5w" -d '{"answerType":"practice","requestId":"test_005","answers":[{"questionId":"ea6ba220-43ff-44d8-8b24-cd350eeb8b53","questionType":"subjective","questionContent":{"stem":"请结合文档内容,简述\"务求实效原则\"在终端建设管理中的具体体现,并说明其与形式主义倾向的根本区别。","data":{"answer_length_hint":"150200字"},"answer":"务求实效原则具体体现为:以终端功能发挥和服务水平提升为唯一导向,科学设定并严格执行投入标准,杜绝一切不必要的支出和浪费。"},"studentAnswer":"务求实效原则强调终端功能发挥和服务水平提升,杜绝浪费。形式主义只注重表面,不注重实际效果。","maxScore":6.0}]}'
```
### 4.6 批量测试(所有题型,包含错题,日常练习)
```bash
curl -X POST http://localhost:8080/api/exam/grade -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsImlhdCI6MTc4MDMwNDMwOCwiZXhwIjoxNzgwMzExNTA4fQ.5NLIrXfK-7MVrrmU7TZ7L_z2TXxc4OzaYIOmoFKBLOAkX9pjCBtxz915QQ1SGLekHTj5DBcgF-NhIZDSw-CPtQ" -d '{"answerType":"practice","requestId":"batch_test_001","answers":[{"questionId":"9d05c475-b4fe-4679-a450-ee88a94b3b34","questionType":"single_choice","questionContent":{"stem":"根据文档内容,务求实效原则的核心要求聚焦于哪一根本目标?","data":{"options":[{"key":"A","content":"扩大终端数量规模"},{"key":"B","content":"提升终端信息化系统覆盖率"},{"key":"C","content":"着眼于终端功能发挥和服务水平提升"},{"key":"D","content":"统一各类终端建设外观标准"}]},"answer":"C"},"studentAnswer":"","maxScore":2.0},{"questionId":"ee652f7e-da5e-4129-8766-c113277332f4","questionType":"multiple_choice","questionContent":{"stem":"依据务求实效原则,以下哪些做法符合其核心要求?(可多选)","data":{"options":[{"key":"A","content":"科学设定终端投入标准"},{"key":"B","content":"严格执行终端投入标准"},{"key":"C","content":"优先采购高配置但非必需的智能设备"},{"key":"D","content":"坚决杜绝不必要的支出和浪费"}]},"answer":["A","B","D"]},"studentAnswer":[],"maxScore":4.0},{"questionId":"76913ad8-5325-4f30-a03a-6e19e434b59e","questionType":"true_false","questionContent":{"stem":"务求实效原则允许在终端建设中适度预留冗余预算,以应对未来可能的功能升级需求。","data":{},"answer":"false"},"studentAnswer":"false","maxScore":1.0}]}'
```
**错题说明**
- 单选题学生选A正确答案是C
- 多选题学生选A、B、C正确答案是A、B、DC选项违背务求实效原则
- 判断题学生选true正确答案是false
### 4.7 模拟考测试answerType = mock\_exam
```bash
curl -X POST http://localhost:8080/api/exam/grade -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsImlhdCI6MTc4MDAzMTgyNSwiZXhwIjoxNzgwMDM5MDI1fQ.an_bqtorziJ5Yb-otGPIIg400fCM1-VLLfX7mgQYHspOgSsTczcSY70evxsMUnfGmOr_euXgXwf2VkMQoXpG5w" -d '{"answerType":"mock_exam","requestId":"mock_test_001","answers":[{"questionId":"9d05c475-b4fe-4679-a450-ee88a94b3b34","questionType":"single_choice","questionContent":{"stem":"根据文档内容,务求实效原则的核心要求聚焦于哪一根本目标?","data":{"options":[{"key":"A","content":"扩大终端数量规模"},{"key":"B","content":"提升终端信息化系统覆盖率"},{"key":"C","content":"着眼于终端功能发挥和服务水平提升"},{"key":"D","content":"统一各类终端建设外观标准"}]},"answer":"C"},"studentAnswer":"C","maxScore":2.0},{"questionId":"ee652f7e-da5e-4129-8766-c113277332f4","questionType":"multiple_choice","questionContent":{"stem":"依据务求实效原则,以下哪些做法符合其核心要求?(可多选)","data":{"options":[{"key":"A","content":"科学设定终端投入标准"},{"key":"B","content":"严格执行终端投入标准"},{"key":"C","content":"优先采购高配置但非必需的智能设备"},{"key":"D","content":"坚决杜绝不必要的支出和浪费"}]},"answer":["A","B","D"]},"studentAnswer":["A","B","D"],"maxScore":4.0}]}'
```
### 4.8 正式考测试(通过 recordId 接口,推荐方式)
> **注意**:正式考试推荐使用 `/api/exam/record/{recordId}/grade` 接口提交。这种方式会关联已存在的考试记录。
```bash
curl -X POST http://localhost:8080/api/exam/record/59759c40-ee4e-4ff5-ab03-4819a0499beb/grade -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsImlhdCI6MTc4MDAzMTgyNSwiZXhwIjoxNzgwMDM5MDI1fQ.an_bqtorziJ5Yb-otGPIIg400fCM1-VLLfX7mgQYHspOgSsTczcSY70evxsMUnfGmOr_euXgXwf2VkMQoXpG5w" -d '{"answers":[{"questionId":"9d05c475-b4fe-4679-a450-ee88a94b3b34","questionType":"single_choice","answer":"C"},{"questionId":"ee652f7e-da5e-4129-8766-c113277332f4","questionType":"multiple_choice","answer":["A","B","D"]}]}'
```
### 4.9 正式考测试(通过通用接口 + recordId 参数)
```bash
curl -X POST http://localhost:8080/api/exam/grade -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsImlhdCI6MTc4MDAzMTgyNSwiZXhwIjoxNzgwMDM5MDI1fQ.an_bqtorziJ5Yb-otGPIIg400fCM1-VLLfX7mgQYHspOgSsTczcSY70evxsMUnfGmOr_euXgXwf2VkMQoXpG5w" -d '{"answerType":"formal_exam","recordId":"59759c40-ee4e-4ff5-ab03-4819a0499beb","requestId":"formal_test_001","paperId":"0ef70443-67d3-48cd-9309-f5088c092fe1","answers":[{"questionId":"9d05c475-b4fe-4679-a450-ee88a94b3b34","questionType":"single_choice","questionContent":{"stem":"根据文档内容,务求实效原则的核心要求聚焦于哪一根本目标?","data":{"options":[{"key":"A","content":"扩大终端数量规模"},{"key":"B","content":"提升终端信息化系统覆盖率"},{"key":"C","content":"着眼于终端功能发挥和服务水平提升"},{"key":"D","content":"统一各类终端建设外观标准"}]},"answer":"C"},"studentAnswer":"C","maxScore":2.0}]}'
```
### 4.10 空白试卷测试 - 日常练习answers 为空数组)
```bash
curl -X POST http://localhost:8080/api/exam/grade -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsImlhdCI6MTc4MDAzMTgyNSwiZXhwIjoxNzgwMDM5MDI1fQ.an_bqtorziJ5Yb-otGPIIg400fCM1-VLLfX7mgQYHspOgSsTczcSY70evxsMUnfGmOr_euXgXwf2VkMQoXpG5w" -d '{"answerType":"practice","requestId":"blank_test_001","answers":[]}'
```
### 4.11 空白试卷测试 - 模拟考answers 为空数组)
```bash
curl -X POST http://localhost:8080/api/exam/grade -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsImlhdCI6MTc4MDAzMTgyNSwiZXhwIjoxNzgwMDM5MDI1fQ.an_bqtorziJ5Yb-otGPIIg400fCM1-VLLfX7mgQYHspOgSsTczcSY70evxsMUnfGmOr_euXgXwf2VkMQoXpG5w" -d '{"answerType":"mock_exam","requestId":"blank_test_002","answers":[]}'
```
### 4.12 空白试卷测试 - 正式考试(通过 recordId 接口)
```bash
curl -X POST http://localhost:8080/api/exam/record/4e34c752-1e0e-4207-8fdb-b0027b30521e/grade -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsImlhdCI6MTc4MDAzMTgyNSwiZXhwIjoxNzgwMDM5MDI1fQ.an_bqtorziJ5Yb-otGPIIg400fCM1-VLLfX7mgQYHspOgSsTczcSY70evxsMUnfGmOr_euXgXwf2VkMQoXpG5w" -d '{"answers":[]}'
```
### 4.13 空白试卷测试 - 正式考试(通过通用接口 + recordId
```bash
curl -X POST http://localhost:8080/api/exam/grade -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsImlhdCI6MTc4MDAzMTgyNSwiZXhwIjoxNzgwMDM5MDI1fQ.an_bqtorziJ5Yb-otGPIIg400fCM1-VLLfX7mgQYHspOgSsTczcSY70evxsMUnfGmOr_euXgXwf2VkMQoXpG5w" -d '{"answerType":"formal_exam","recordId":"4e34c752-1e0e-4207-8fdb-b0027b30521e","requestId":"blank_test_003","answers":[]}'
```
### 4.14 正式考试 - 只提交一道题(通过 recordId 接口)
```bash
curl -X POST http://localhost:8080/api/exam/record/2fa88db4-bc90-4096-9970-3286639c5b5b/grade -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsImlhdCI6MTc4MDMxOTgzMiwiZXhwIjoxNzgwMzI3MDMyfQ.azBEWs7-LiicNDA_O0qdm-TPmBtKLBUeCplRPnkGl7NFUhn-JMP5-G9gboP7eHxsaIvowoY6B2TVlB4z8bDIuQ" -d '{"answers":[{"questionId":"5af6fdce-dbf2-4a57-9ef4-bef176be6fb8","questionType":"single_choice","answer":"C"}]}'
```
### 4.15 正式考试 - 一道题没选(通过 recordId 接口)
```bash
curl -X POST http://localhost:8080/api/exam/record/389806c8-3b42-4b5a-adb8-23d166bb1053/grade -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsImlhdCI6MTc4MDMxOTgzMiwiZXhwIjoxNzgwMzI3MDMyfQ.azBEWs7-LiicNDA_O0qdm-TPmBtKLBUeCplRPnkGl7NFUhn-JMP5-G9gboP7eHxsaIvowoY6B2TVlB4z8bDIuQ" -d '{"answers":[{"questionId":"5af6fdce-dbf2-4a57-9ef4-bef176be6fb8","questionType":"single_choice","answer":""},{"questionId":"5e069067-a10d-4230-9564-e7503d72d738","questionType":"multiple_choice","answer":["A","B"]},{"questionId":"e6bb2566-90a8-4651-b553-083a1d695b3c","questionType":"true_false","answer":"true"}]}'
```
**answerType 测试总结**
| answerType 值 | 场景 | 重复提交同一题目 | 数据库行为 |
| ------------- | ---- | -------- | ------------ |
| `practice` | 日常练习 | 允许 | UPDATE 已有记录 |
| `mock_exam` | 模拟考 | 允许 | UPDATE 已有记录 |
| `formal_exam` | 正式考试 | 不允许 | 抛出异常(唯一索引保护) |
***
## 五、AI端接口测试
AI端接口**支持** **`explanation`** **字段**可以直接使用包含完整题目信息的JSON。
### 5.1 单选题测试
```bash
curl -X POST http://localhost:5001/exam/grade -H "Content-Type: application/json" -d '{"answers":[{"questionId":"q1","questionType":"single_choice","questionContent":{"stem":"根据文档内容,务求实效原则的核心要求聚焦于哪一根本目标?","data":{"options":[{"key":"A","content":"扩大终端数量规模"},{"key":"B","content":"提升终端信息化系统覆盖率"},{"key":"C","content":"着眼于终端功能发挥和服务水平提升"},{"key":"D","content":"统一各类终端建设外观标准"}]},"answer":"C","explanation":"文档明确指出要着眼于终端功能发挥和服务水平提升"},"studentAnswer":"C","maxScore":2}]}'
```
### 5.2 多选题测试
```bash
curl -X POST http://localhost:5001/exam/grade -H "Content-Type: application/json" -d '{"answers":[{"questionId":"q2","questionType":"multiple_choice","questionContent":{"stem":"依据务求实效原则,以下哪些做法符合其核心要求?(可多选)","data":{"options":[{"key":"A","content":"科学设定终端投入标准"},{"key":"B","content":"严格执行终端投入标准"},{"key":"C","content":"优先采购高配置但非必需的智能设备"},{"key":"D","content":"坚决杜绝不必要的支出和浪费"}]},"answer":["A","B","D"],"explanation":"A、B、D均符合务求实效原则"},"studentAnswer":["A","B","D"],"maxScore":4}]}'
```
### 5.3 空白试卷测试answers 为空数组)
```bash
curl -X POST http://localhost:5001/exam/grade -H "Content-Type: application/json" -d '{"answers":[]}'
```
**预期响应**
- **正常情况**Python AI 已做空值防御):
```json
{
"data": {
"requestId": null,
"results": [],
"scoreRate": 0.0,
"success": true,
"totalMaxScore": 0,
"totalScore": 0
},
"message": "批阅完成",
"status": "success",
"statusCode": 2012,
"success": true
}
```
- **异常情况**Python AI 未做空值防御):
```json
{
"error_code": "GRADE_ERROR",
"message": "'NoneType' object has no attribute 'get'",
"status": "failed",
"status_code": 5021,
"success": false
}
```
### 5.4 填空题空答案测试
```bash
curl -X POST http://localhost:5001/exam/grade -H "Content-Type: application/json" -d '{"answers":[{"questionId":"q3","questionType":"fill_blank","questionContent":{"stem":"务求实效原则强调以___为出发点通过科学设定和严格执行终端投入标准实现___与___的双重提升。","data":{"blank_count":3},"answer":["终端功能发挥和服务水平提升","终端功能发挥","服务水平提升"]},"studentAnswer":null,"maxScore":3}]}'
```
### 5.5 主观题空答案测试
```bash
curl -X POST http://localhost:5001/exam/grade -H "Content-Type: application/json" -d '{"answers":[{"questionId":"q4","questionType":"subjective","questionContent":{"stem":"请简述\"务求实效原则\"的核心要求。","data":{"scoring_points":[{"point":"功能发挥","weight":0.5},{"point":"服务水平","weight":0.3},{"point":"杜绝浪费","weight":0.2}]},"answer":"以终端功能发挥和服务水平提升为唯一导向"},"studentAnswer":"","maxScore":5}]}'
```
### 5.6 判断题空答案测试studentAnswer 为空字符串)
```bash
curl -X POST http://localhost:5001/exam/grade -H "Content-Type: application/json" -d '{"answers":[{"questionId":"q5","questionType":"true_false","questionContent":{"stem":"务求实效原则允许在终端建设中适度预留冗余预算,以应对未来可能的功能升级需求。","answer":"false"},"studentAnswer":"","maxScore":2}]}'
```
### 5.7 判断题空答案测试studentAnswer 为 null
```bash
curl -X POST http://localhost:5001/exam/grade -H "Content-Type: application/json" -d '{"answers":[{"questionId":"q6","questionType":"true_false","questionContent":{"stem":"务求实效原则强调以终端功能发挥和服务水平提升为唯一导向。","answer":"true"},"studentAnswer":null,"maxScore":2}]}'
```
### 5.8 单选题空答案测试
```bash
curl -X POST http://localhost:5001/exam/grade -H "Content-Type: application/json" -d '{"answers":[{"questionId":"q7","questionType":"single_choice","questionContent":{"stem":"根据文档内容,务求实效原则的核心要求聚焦于哪一根本目标?","data":{"options":[{"key":"A","content":"扩大终端数量规模"},{"key":"B","content":"提升终端信息化系统覆盖率"},{"key":"C","content":"着眼于终端功能发挥和服务水平提升"},{"key":"D","content":"统一各类终端建设外观标准"}]},"answer":"C"},"studentAnswer":null,"maxScore":2}]}'
```
### 5.9 多选题空答案测试
```bash
curl -X POST http://localhost:5001/exam/grade -H "Content-Type: application/json" -d '{"answers":[{"questionId":"q8","questionType":"multiple_choice","questionContent":{"stem":"依据务求实效原则,以下哪些做法符合其核心要求?(可多选)","data":{"options":[{"key":"A","content":"科学设定终端投入标准"},{"key":"B","content":"严格执行终端投入标准"},{"key":"C","content":"优先采购高配置但非必需的智能设备"},{"key":"D","content":"坚决杜绝不必要的支出和浪费"}]},"answer":["A","B","D"]},"studentAnswer":[],"maxScore":4}]}'
```
***
## 六、接口对比总结
| 特性 | 后端通用接口 (/api/exam/grade) | 后端正式考试接口 (/api/exam/record/{recordId}/grade) | AI端接口 (/exam/grade) |
| --------------- | -------------------------------------- | -------------------------------------------------------- | ---------------------------------- |
| 地址 | <http://localhost:8080/api/exam/grade> | <http://localhost:8080/api/exam/record/{recordId}/grade> | <http://localhost:5001/exam/grade> |
| 认证 | 需要 Bearer Token | 需要 Bearer Token | 不需要认证 |
| requestId | 必需 | 不需要 | 不需要 |
| recordId | 可选(正式考试时使用) | 路径参数(必需) | 不支持 |
| answerType | 前端传入,后端原样使用 | 固定为 "formal\_exam" | 不支持 |
| questionId | 使用数据库UUID | 使用数据库UUID | 可自定义ID |
| maxScore | 小数 (2.0) | 不需要(从数据库获取) | 整数 (2) |
| explanation字段 | **不支持** | **不支持** | 支持 |
| questionContent | 日常练习/模拟考时必需 | 不需要 | 支持 |
**recordId 参数说明**
- 当在通用接口中传入 `recordId` 时,系统会尝试查找并更新对应的考试记录
- 这种方式主要用于正式考试场景,配合 `answerType: "formal_exam"` 使用
- 对于日常练习或模拟考,可以不传入 `recordId`
- 推荐使用专门的 `/api/exam/record/{recordId}/grade` 接口进行正式考试
***
## 七、预期响应格式
### 7.1 后端通用接口响应示例(日常练习/模拟考)
```json
{
"code": 200,
"message": "批阅成功",
"data": {
"requestId": "test_001",
"results": [
{
"questionId": "9d05c475-b4fe-4679-a450-ee88a94b3b34",
"correct": true,
"feedback": "回答正确!",
"score": 2.0,
"maxScore": 2.0
}
],
"totalScore": 2.0,
"totalMaxScore": 2.0,
"scoreRate": 100.0
}
}
```
### 7.2 后端正式考试接口响应示例
```json
{
"code": 200,
"message": "提交成功",
"data": {
"recordId": "59759c40-ee4e-4ff5-ab03-4819a0499beb",
"paperId": "0ef70443-67d3-48cd-9309-f5088c092fe1",
"userId": 1,
"answerType": "formal_exam",
"status": "graded",
"totalScore": 12.0,
"paperTitle": "测试试卷",
"answers": [
{
"questionId": "9d05c475-b4fe-4679-a450-ee88a94b3b34",
"score": 2.0,
"isCorrect": true,
"feedback": "回答正确!"
}
],
"startTime": "2024-05-20T10:00:00",
"submitTime": "2024-05-20T10:30:00",
"gradedTime": "2024-05-20T10:30:05"
}
}
```
### 7.3 AI端接口响应示例
```json
{
"data": {
"requestId": null,
"results": [
{
"correct": true,
"feedback": "正确!",
"maxScore": 2,
"questionId": "9d05c475-b4fe-4679-a450-ee88a94b3b34",
"score": 2
}
],
"scoreRate": 100.0,
"success": true,
"totalMaxScore": 2,
"totalScore": 2
},
"message": "批阅完成",
"status": "success",
"statusCode": 2021,
"success": true
}
```
### 7.4 空白试卷响应示例(日常练习/模拟考)
```json
{
"code": 200,
"message": "批阅成功",
"data": {
"requestId": "blank_test_001",
"results": [],
"totalScore": 0.0,
"totalMaxScore": 0.0,
"scoreRate": 0.0
}
}
```
### 7.5 空白试卷响应示例(正式考试)
```json
{
"code": 200,
"message": "提交成功",
"data": {
"recordId": "59759c40-ee4e-4ff5-ab03-4819a0499beb",
"paperId": "0ef70443-67d3-48cd-9309-f5088c092fe1",
"userId": 1,
"answerType": "formal_exam",
"status": "graded",
"totalScore": 0.0,
"paperTitle": "测试试卷",
"answers": [],
"startTime": "2024-05-20T10:00:00",
"submitTime": "2024-05-20T10:30:00",
"gradedTime": "2024-05-20T10:30:05"
}
}
```
***
## 八、测试数据来源说明
本测试文档中的题目数据均来源于数据库表 `question`,具体来源如下:
| questionId | 题目类型 | 文档来源 |
| -------------------------------------- | ---------------- | ------------------- |
| `9d05c475-b4fe-4679-a450-ee88a94b3b34` | single\_choice | public\_kb - 务求实效原则 |
| `ee652f7e-da5e-4129-8766-c113277332f4` | multiple\_choice | public\_kb - 务求实效原则 |
| `76913ad8-5325-4f30-a03a-6e19e434b59e` | true\_false | public\_kb - 务求实效原则 |
| `7ae3daef-0345-4852-bd37-336e74498cc4` | fill\_blank | public\_kb - 务求实效原则 |
| `ea6ba220-43ff-44d8-8b24-cd350eeb8b53` | subjective | public\_kb - 务求实效原则 |
数据库文件路径:[knowledge\_management\_system.sql](file:///d:/Users/19211/Desktop/demo/docs/sql/knowledge_management_system.sql)
### 8.1 数据库题目查询 SQL
如需从数据库获取更多测试题目,可使用以下 SQL 查询:
```sql
-- 查询所有已审核通过的题目
SELECT question_id, question_type, difficulty, score, content
FROM question
WHERE status = 'approved'
ORDER BY question_type, id;
-- 查询指定类型的题目
SELECT question_id, question_type, content
FROM question
WHERE status = 'approved'
AND question_type = 'single_choice'
LIMIT 10;
-- 查询指定难度的题目
SELECT question_id, question_type, difficulty, content
FROM question
WHERE status = 'approved'
AND difficulty = 3;
```
***
## 九、重要提示
**后端接口 QuestionContent 字段定义**
```java
private String stem;
private Map<String, Object> data;
private Object answer;
```
**注意**:后端接口**不支持** **`explanation`** **字段**如果传入会导致JSON解析错误。
**字段命名规范**
- 所有字段使用驼峰命名camelCase而不是下划线命名snake\_case
- 例如:`answerType`、`recordId`、`questionId`、`studentAnswer`、`maxScore`
**使用建议**
1. **所有curl命令已改为单行格式**,可以直接复制使用
2. **Token有效期2小时**,过期后需要重新获取
3. **测试时请确保后端服务和AI服务都已启动**
4. **日常练习和模拟考可以重复提交**,会自动更新记录
5. **正式考试只能提交一次**,重复提交会报错
**空白试卷测试说明**
- 空白试卷(`answers` 为空数组)测试用于验证系统在无答案情况下的处理能力
- 正常情况下,空白试卷应返回总分 0.0,不触发错误
- 若 Python AI 批改服务未做空值防御,可能会出现 `'NoneType' object has no attribute 'get'` 错误500 状态码)
- 相关修复方案可参考:`后端问题诊断报告_考试提交AI批改服务崩溃.md`
**AI端空白试卷测试要点**
- AI端接口`http://localhost:5001/exam/grade`**支持完整的题目信息**,包含 `explanation` 字段
- AI端采用**小写驼峰命名**(如 `questionId`、`questionType`、`questionContent`、`studentAnswer`、`maxScore`
- 空白试卷测试重点验证 Python AI 服务的空值防御能力
- 填空题空答案(`studentAnswer: null`)和主观题空答案(`studentAnswer: ""`)也是常见的边界测试场景
***
**文档结束**