Files
aue/后端问题诊断报告_考试提交AI批改服务崩溃.md
2026-06-03 13:16:30 +08:00

346 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 后端问题诊断报告
> 生成时间2026-05-31
> 涉及模块:考察训练系统 - 考试提交 / AI批改服务
> 问题级别:**P1 - 高(功能不可用)**
---
## 一、问题描述
### 1.1 现象
用户在正式考试中提交试卷(含空白答案或已作答答案)时,前端收到 **HTTP 500 错误**,考试记录无法正常结束。
### 1.2 复现步骤
1. 进入「考察和训练」→ 选择试卷 → 开始考试
2. 不作答(或部分作答)任何题目
3. 点击「提交试卷」按钮
4. 前端控制台输出错误日志(见下方)
5. 页面提示「提交失败」或「系统异常」
### 1.3 前端控制台错误日志
```
[提交] ⚠️ 空白试卷提交 — recordId: fa779367-bebb-48e7-af75-68e582c8ae7b 总题数: 5
[examAPI] submit(空白提交) - recordId: fa779367-bebb-48e7-af75-68e582c8ae7b
[提交] submit(结束考试) 失败:
message: 系统异常
无response对象(可能是网络错误/CORS)
[examAPI] 正式考试提交批改 - recordId: fa779367-bebb-48e7-af75-68e582c8ae7b 题目数: 5
[提交] gradeExamRecord(空答案) 失败:
message: 提交失败: Service层批改题目失败: 调用AI服务失败:
POST请求失败: 500 INTERNAL SERVER ERROR:
"{"error_code":"GRADE_ERROR",
"message":"'NoneType' object has no attribute 'get'",
"status":"failed",
"status_code":5021,
"success":false}"
[提交] 所有2种提交方式均失败
```
---
## 二、根因分析
### 2.1 直接原因Python AI 批改服务崩溃
后端 Java 服务在处理考试提交时,调用 Python AI 服务进行自动批改。Python 服务在接收到**空答案数据**时发生 `TypeError`
```
'NoneType' object has no attribute 'get'
```
**错误位置**Python AI 批改服务的答案解析/评分逻辑中
**触发条件**`answers` 参数为空列表、空字典或 `None` 时,代码对 `None` 对象调用了 `.get()` 方法
### 2.2 根本原因:所有提交接口均强制调用 AI 批改
| 接口 | HTTP方法 | 路径 | 内部行为 | 受影响 |
|------|---------|------|---------|--------|
| `submitExam()` | POST | `/admin/exam/paper/{recordId}/submit` | → `gradeAnswersForRecord()`**调用Python AI** | ✅ 全部用户 |
| `gradeAnswers()` | POST | `/admin/exam/paper/{recordId}/grade` | → **调用Python AI** | ✅ 全部用户 |
| `gradeAnswersForRecord()` | POST | `/exam/record/{recordId}/grade` | → **调用Python AI** | ✅ 全部用户 |
**关键发现**:后端**不存在**"仅更新状态、不触发AI批改"的纯提交接口。`submit` 接口虽然名称为"提交"但内部实际执行的是完整的AI批改流程。**
### 2.3 数据流图
```
┌──────────┐ POST /record/{id}/submit ┌─────────────────────┐
│ 前端 │ ──────────────────────────────→ │ AdminExamController │
│ 提交 │ body: {} (空白) │ .submitExam() │
└──────────┘ └──────────┬──────────┘
examService.gradeAnswersForRecord()
┌─────────▼──────────┐
│ Java Service层 │
│ 组装answers参数 │
└─────────┬──────────┘
HTTP POST (内部调用)
┌─────────▼──────────┐
│ Python AI 批改服务 │
│ │
│ answers = None/[] │ ← 空值传入
│ ↓ │
│ data.get('xxx') │ ← 💥 TypeError!
│ 'NoneType' object │
│ has no attribute │
│ 'get' │
└─────────┬──────────┘
返回 HTTP 500 + error_code=5021
┌─────────▼──────────┐
│ AdminExamController │
│ catch(Exception) │
│ return Result.error │
│ (500, e.getMessage())│
└─────────┬──────────┘
HTTP 500 { code:500, message:"..." }
axios拦截器包装 Error("系统异常")
前端显示 "提交失败: 系统异常"
```
---
## 三、影响范围
### 3.1 功能影响
| 功能 | 影响 | 说明 |
|------|------|------|
| 正式考试提交(空白卷) | ❌ 完全不可用 | 所有题型答案为空时必现 |
| 正式考试提交(有答案) | ⚠️ 可能不可用 | 取决于Python服务是否对所有题型都做了空值保护 |
| 日常练习提交 | ⚠️ 可能受影响 | 同样走AI批改流程 |
| 试卷发布 | ⚠️ 需单独验证 | 发布接口不涉及AI批改但当前也报错需进一步排查 |
### 3.2 用户影响
- **全部角色用户**受影响(系统管理员、部门管理员、普通用户)
- 无法完成考试流程闭环(开始考试 → 提交 → 查看成绩)
- 已开始的考试记录卡在 `not_started` 或中间状态
---
## 四、修复方案
### 方案APython AI 服务空值防御(推荐 - 快速修复)
**修改文件**Python AI 批改服务入口文件(具体路径待确认)
**修复内容**:在接收 `answers` 参数后立即做空值检查
```python
# === 修复前 ===
def grade_answers(questions, answers):
for q in questions:
ans = answers.get(q['id']) # ← answers=None时崩溃
score = calculate_score(ans)
# === 修复后 ===
def grade_answers(questions, answers):
# 防御性检查空值直接返回0分
if not answers or not isinstance(answers, (dict, list)):
return {
"score": 0,
"total_score": 0,
"details": [],
"success": True,
"message": "空白试卷,无需批改"
}
if isinstance(answers, dict) and len(answers) == 0:
return {"score": 0, "total_score": 0, "details": [], "success": True}
if isinstance(answers, list) and all(
a is None or a == '' or a.get('answer', '') == ''
for a in answers if isinstance(a, dict)
):
return {"score": 0, "total_score": 0, "details": [], "success": True}
# 正常批改逻辑...
for q in questions:
ans = answers.get(q['id']) if isinstance(answers, dict) else None
if ans is None or ans == '':
continue # 跳过无答案的题目
score = calculate_score(ans)
```
**优点**
- 改动最小只修改Python侧
- 不影响现有正常答题的批改逻辑
- 空白试卷自动得0分符合业务预期
**缺点**
- 未解决架构问题submit接口仍强依赖AI服务
---
### 方案B新增纯提交接口推荐 - 完整修复)
**修改文件**
- `AdminExamController.java` — 新增接口
- `ExamPaperService.java` / `ExamPaperServiceImpl.java` — 新增方法
#### B.1 后端新增接口
```java
// AdminExamController.java - 新增
@Operation(summary = "结束考试(不批改)", description = "仅更新考试状态为已完成不做AI批改")
@PostMapping("/admin/exam/paper/{recordId}/finish")
public Result<Void> finishExam(
@PathVariable String recordId,
HttpServletRequest httpRequest) {
try {
Long userId = getCurrentUserId(httpRequest);
ExamRecord record = examRecordMapper.selectById(recordId);
if (record == null) {
return Result.error(404, "考试记录不存在");
}
// 仅更新状态不调用AI批改
record.setStatus("completed");
record.setScore(0); // 空白卷默认0分
record.setSubmitTime(LocalDateTime.now());
record.setUpdatedAt(LocalDateTime.now());
examRecordMapper.updateById(record);
log.info("考试结束(未批改): recordId={}, userId={}", recordId, userId);
return Result.success();
} catch (Exception e) {
log.error("结束考试失败: {}", e.getMessage(), e);
return Result.error(500, e.getMessage());
}
}
```
#### B.2 前端对接
```javascript
// useExam.js - 空白试卷优先使用finish接口
if (isBlankSubmission) {
_variants.push({
label: 'finishExam(结束考试)',
fn: () => examAPI.finishExam(recordId) // 新增API
})
_variants.push({
label: 'submit(结束考试)',
fn: () => examAPI.submit(recordId) // 兜底
})
} else {
// 有答案的正常走AI批改
_variants.push({
label: 'gradeExamRecord(批改)',
fn: () => examAPI.gradeExamRecord(recordId, formattedAnswers)
})
// ...
}
```
```javascript
// exam.js API层新增
finishExam: (recordId) => {
if (!recordId) throw new Error('考试记录ID不能为空')
return examClient.post(`/admin/exam/paper/${encodeURIComponent(recordId)}/finish`, {}, { timeout: 15000 })
.then(handleExamResponse)
},
```
**优点**
- 彻底解耦"提交"与"批改"
- 空白试卷不再触发AI服务
- 符合单一职责原则
- 可后续异步补批改
**缺点**
- 需要前后端配合改动
- 空白卷暂无成绩明细(可后续补充手动打分功能)
---
### 方案CJava Service 层增加空值保护(折中方案)
**修改文件**`ExamServiceImpl.java``gradeAnswersForRecord()` 方法
```java
@Override
public ExamRecordResponse gradeAnswersForRecord(String recordId, ExamSaveRequest request, Long userId, String answerType) {
// 新增:前置空值检查
List<AnswerItem> answers = request.getAnswers();
if (answers == null || answers.isEmpty() ||
answers.stream().allMatch(a -> a.getAnswer() == null || a.getAnswer().isBlank())) {
log.info("检测到空白试卷跳过AI批改: recordId={}", recordId);
// 直接更新状态为已完成分数为0
ExamRecord record = examRecordMapper.selectOne(
new LambdaQueryWrapper<ExamRecord>().eq(ExamRecord::getRecordId, recordId));
if (record != null) {
record.setStatus("completed");
record.setScore(0);
record.setSubmitTime(LocalDateTime.now());
examRecordMapper.updateById(record);
}
return ExamRecordResponse.builder()
.recordId(recordId)
.score(0)
.totalScore(0)
.status("completed")
.build();
}
// 原有AI批改逻辑...
}
```
**优点**
- 只改Java层不动Python
- 对前端透明,无需前端改动
- 快速上线
**缺点**
- submit接口语义仍混乱名为提交实为批改
- 未从根本上解决问题
---
## 五、推荐实施计划
| 优先级 | 方案 | 工作量 | 建议 |
|--------|------|--------|------|
| **P0 - 立即** | **方案C** | 30min | 先恢复基本功能 |
| **P1 - 本周** | **方案A** | 1h | Python侧彻底修复 |
| **P2 - 迭代** | **方案B** | 3h | 架构优化 |
**建议先实施方案C快速止血再按方案A+方案B逐步完善。**
---
## 六、相关附件
### 6.1 涉及的后端文件清单
| 文件 | 路径 | 作用 |
|------|------|------|
| `AdminExamController.java` | `demo/src/main/java/top/tqx/demo_1/controller/` | 考试管理REST接口 |
| `ExamController.java` | `demo/src/main/java/top/tqx/demo_1/controller/` | 用户侧考试接口 |
| `ExamPaperService.java` | `demo/src/main/java/top/tqx/demo_1/service/` | 考试业务接口定义 |
| `ExamPaperServiceImpl.java` | `demo/src/main/java/top/tqx/demo_1/service/impl/` | 考试业务实现含AI调用 |
| Python AI批改服务 | 待确认路径 | 自动评分核心逻辑 |
### 6.2 涉及的前端文件清单(已优化)
| 文件 | 路径 | 已完成的优化 |
|------|------|------------|
| `useExam.js` | `src/components/exam/composables/` | 空白试卷优先走submit增强错误诊断日志 |
| `PaperManagementPanel.vue` | `src/components/exam/` | 发布错误增强诊断originalError回溯 |