# 后端问题诊断报告 > 生成时间: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` 或中间状态 --- ## 四、修复方案 ### 方案A:Python 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 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服务 - 符合单一职责原则 - 可后续异步补批改 **缺点**: - 需要前后端配合改动 - 空白卷暂无成绩明细(可后续补充手动打分功能) --- ### 方案C:Java Service 层增加空值保护(折中方案) **修改文件**:`ExamServiceImpl.java` 的 `gradeAnswersForRecord()` 方法 ```java @Override public ExamRecordResponse gradeAnswersForRecord(String recordId, ExamSaveRequest request, Long userId, String answerType) { // 新增:前置空值检查 List 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().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回溯) |