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

13 KiB
Raw Blame History

后端问题诊断报告

生成时间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 参数后立即做空值检查

# === 修复前 ===
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服务
  • 符合单一职责原则
  • 可后续异步补批改

缺点

  • 需要前后端配合改动
  • 空白卷暂无成绩明细(可后续补充手动打分功能)

方案CJava Service 层增加空值保护(折中方案)

修改文件ExamServiceImpl.javagradeAnswersForRecord() 方法

@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回溯