13 KiB
13 KiB
后端问题诊断报告
生成时间:2026-05-31
涉及模块:考察训练系统 - 考试提交 / AI批改服务
问题级别:P1 - 高(功能不可用)
一、问题描述
1.1 现象
用户在正式考试中提交试卷(含空白答案或已作答答案)时,前端收到 HTTP 500 错误,考试记录无法正常结束。
1.2 复现步骤
- 进入「考察和训练」→ 选择试卷 → 开始考试
- 不作答(或部分作答)任何题目
- 点击「提交试卷」按钮
- 前端控制台输出错误日志(见下方)
- 页面提示「提交失败」或「系统异常」
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或中间状态
四、修复方案
方案A:Python AI 服务空值防御(推荐 - 快速修复)
修改文件:Python AI 批改服务入口文件(具体路径待确认)
修复内容:在接收 answers 参数后立即做空值检查
# === 修复前 ===
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 后端新增接口
// 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 前端对接
// 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)
})
// ...
}
// 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服务
- 符合单一职责原则
- 可后续异步补批改
缺点:
- 需要前后端配合改动
- 空白卷暂无成绩明细(可后续补充手动打分功能)
方案C:Java Service 层增加空值保护(折中方案)
修改文件:ExamServiceImpl.java 的 gradeAnswersForRecord() 方法
@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回溯) |