package top.tqx.demo_1.service; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import top.tqx.demo_1.dto.ExamGenerateRequest; import top.tqx.demo_1.dto.ExamGenerateResponse; import top.tqx.demo_1.entity.File; import top.tqx.demo_1.enums.FileProcessStepStatus; import top.tqx.demo_1.mapper.FileMapper; import java.time.LocalDateTime; @Slf4j @Service public class AsyncExamService { private final ObjectMapper objectMapper = new ObjectMapper(); @Autowired private ExamService examService; @Autowired private FileMapper fileMapper; public static final String EXAM_STATUS_UNGENERATED = "UNGENERATED"; public static final String EXAM_STATUS_GENERATING = "GENERATING"; public static final String EXAM_STATUS_GENERATED = "GENERATED"; public static final String EXAM_STATUS_FAILED = "FAILED"; @Async("examTaskExecutor") public void generateQuestionsAsync(ExamGenerateRequest request) { log.info("========== 异步任务开始执行 =========="); log.info("requestId={}, filePath={}, fileId={}, 当前线程: {}", request.getRequestId(), request.getFilePath(), request.getFileId(), Thread.currentThread().getName()); String filePath = request.getFilePath(); Long fileId = null; String errorMessage = null; try { log.info("步骤1: 查找文件ID"); // 优先使用 request 中的 fileId(来自 collection_file) if (request.getFileId() != null) { log.info("优先使用 request 中的 fileId: {}", request.getFileId()); fileId = request.getFileId(); } // 如果没有 request 中的 fileId,尝试通过路径查询 if (fileId == null && filePath != null && !filePath.isEmpty()) { fileId = findFileIdByPath(filePath, null); log.info("通过路径查询得到 fileId: {}", fileId); } log.info("最终确定的 fileId: {}", fileId); // 步骤2: 更新状态为 GENERATING(只要有 fileId 就更新) if (fileId != null) { log.info("步骤2: 更新状态为 GENERATING"); updateFileExamStatus(fileId, EXAM_STATUS_GENERATING, "正在生成题目,请稍后..."); } log.info("步骤3: 调用 generateQuestions"); ExamGenerateResponse response = examService.generateQuestions(request); log.info("步骤3完成: total={}", response.getTotal()); // 步骤4: 更新状态为完成 if (fileId != null) { log.info("步骤4: 更新状态为 GENERATED"); if (response.getTotal() == null || response.getTotal() == 0) { log.warn("RAG服务返回0道题目,记录警告信息"); updateFileExamStatus(fileId, EXAM_STATUS_GENERATED, "警告:未生成任何题目。可能原因:文档内容为空、文档内容无法提取题目、或文档格式不支持。source_chunks_used=" + response.getSourceChunksUsed()); } else { String processMessage = "生成成功,共" + response.getTotal() + "道题"; // 追加题型统计信息 if (response.getRequestedTypes() != null || response.getActualTypes() != null) { processMessage += "\n\n【题型统计】"; if (response.getRequestedTypes() != null) { processMessage += "\n请求题型: " + formatQuestionTypes(response.getRequestedTypes()); } if (response.getActualTypes() != null) { processMessage += "\n实际生成: " + formatQuestionTypes(response.getActualTypes()); } } // 追加短缺警告 if (response.getWarnings() != null && !response.getWarnings().isEmpty()) { processMessage += "\n\n【短缺警告】"; for (String warning : response.getWarnings()) { processMessage += "\n- " + warning; } } log.info("response.getAiAnalysis() = {}", response.getAiAnalysis()); log.info("response.getAiAnalysis() == null? {}", response.getAiAnalysis() == null); if (response.getAiAnalysis() != null) { try { String aiAnalysisJson = objectMapper.writeValueAsString(response.getAiAnalysis()); log.info("序列化成功的 aiAnalysisJson = {}", aiAnalysisJson); processMessage += "\n\n【AI分析结果】\n" + aiAnalysisJson; } catch (Exception e) { log.warn("序列化 aiAnalysis 失败: {}", e.getMessage(), e); String aiAnalysisString = response.getAiAnalysis().toString(); log.info("使用 toString(): {}", aiAnalysisString); processMessage += "\n\n【AI分析结果】" + aiAnalysisString; } } else { log.warn("response.getAiAnalysis() 为 null,不追加 AI 分析结果"); } log.info("最终 processMessage = {}", processMessage); updateFileExamStatus(fileId, EXAM_STATUS_GENERATED, processMessage); } } log.info("========== 异步任务成功完成 =========="); } catch (Exception e) { String errorType = e.getMessage() != null && (e.getMessage().contains("500") || e.getMessage().contains("INTERNAL SERVER ERROR")) ? "[AI端错误]" : "[本地错误]"; errorMessage = String.format("生成失败%s: %s", errorType, e.getMessage()); log.error("========== 异步任务执行失败 ==========", e); if (fileId != null) { updateFileExamStatus(fileId, EXAM_STATUS_FAILED, errorMessage); } // 不抛出RuntimeException,避免产生"Unexpected exception occurred invoking async method"日志 log.info("异步任务已优雅结束,文件状态已更新为FAILED"); } } private Long findFileIdByPath(String filePath, Long requestFileId) { try { log.info("开始查询 file 表: filePath={}, requestFileId={}", filePath, requestFileId); // 优先通过 ID 查询(如果提供了 requestFileId) if (requestFileId != null) { log.info("尝试通过 ID 查询: fileId={}", requestFileId); File fileById = fileMapper.selectById(requestFileId); if (fileById != null) { log.info("通过 ID 查询成功: id={}, fileName={}, filePath={}", fileById.getId(), fileById.getFileName(), fileById.getFilePath()); return fileById.getId(); } else { log.warn("通过 ID 查询失败,尝试通过路径查询"); } } // 其次通过路径查询 log.info("尝试通过路径查询: filePath={}", filePath); com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper wrapper = new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>(); wrapper.eq(File::getFilePath, filePath); File file = fileMapper.selectOne(wrapper); if (file != null) { log.info("通过路径查询成功: id={}, fileName={}, filePath={}", file.getId(), file.getFileName(), file.getFilePath()); return file.getId(); } else { log.warn("通过路径查询失败: filePath={}", filePath); // 尝试模糊查询,看看是否有相似的路径 com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper likeWrapper = new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>(); likeWrapper.like(File::getFilePath, "3.docx"); java.util.List similarFiles = fileMapper.selectList(likeWrapper); if (similarFiles != null && !similarFiles.isEmpty()) { log.info("找到 {} 条相似的文件记录:", similarFiles.size()); for (File f : similarFiles) { log.info(" - id={}, fileName={}, filePath={}", f.getId(), f.getFileName(), f.getFilePath()); } } return null; } } catch (Exception e) { log.error("查询文件失败: filePath={}, requestFileId={}, error={}", filePath, requestFileId, e.getMessage(), e); return null; } } @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void updateFileExamStatus(Long fileId, String status, String message) { if (fileId == null) { log.warn("更新文件状态失败: fileId为null"); return; } try { File file = fileMapper.selectById(fileId); if (file != null) { log.info("更新文件状态: fileId={}, 从 {} 变为 {}", fileId, file.getExamStatus(), status); file.setExamStatus(status); if (message != null) { file.setProcessMessage(message); } // 更新流程状态 if (EXAM_STATUS_GENERATING.equals(status)) { file.setProcessStepStatus(FileProcessStepStatus.EXAM_GENERATING.getCode()); file.setProcessStepMessage(FileProcessStepStatus.EXAM_GENERATING.getDescription()); } else if (EXAM_STATUS_GENERATED.equals(status)) { // 如果向量化已完成且题目生成完成,则标记为全部完成 if (FileProcessStepStatus.VECTORIZED.getCode().equals(file.getProcessStepStatus())) { file.setProcessStepStatus(FileProcessStepStatus.COMPLETED.getCode()); file.setProcessStepMessage(FileProcessStepStatus.COMPLETED.getDescription()); } else { file.setProcessStepStatus(FileProcessStepStatus.EXAM_GENERATED.getCode()); file.setProcessStepMessage(FileProcessStepStatus.EXAM_GENERATED.getDescription()); } } else if (EXAM_STATUS_FAILED.equals(status)) { file.setProcessStepStatus(FileProcessStepStatus.EXAM_GENERATE_FAILED.getCode()); String stepMsg = FileProcessStepStatus.EXAM_GENERATE_FAILED.getDescription() + ": " + (message != null ? message : ""); file.setProcessStepMessage(stepMsg.length() > 500 ? stepMsg.substring(0, 500) : stepMsg); } file.setUpdateTime(LocalDateTime.now()); int result = fileMapper.updateById(file); if (result > 0) { log.info("文件状态更新成功: fileId={}", fileId); } else { log.warn("文件状态更新失败: fileId={}, status={}, 未影响任何行", fileId, status); } } else { log.warn("更新文件状态失败: fileId={} 对应的文件不存在", fileId); } } catch (Exception e) { log.error("更新文件题目状态失败: fileId={}, status={}, error={}", fileId, status, e.getMessage(), e); throw new RuntimeException("更新文件状态失败", e); } } /** * 格式化题型统计信息 */ private String formatQuestionTypes(java.util.Map types) { if (types == null || types.isEmpty()) { return "{}"; } StringBuilder sb = new StringBuilder("{"); java.util.Map typeNames = new java.util.HashMap<>(); typeNames.put("single_choice", "单选题"); typeNames.put("multiple_choice", "多选题"); typeNames.put("true_false", "判断题"); typeNames.put("fill_blank", "填空题"); typeNames.put("subjective", "主观题"); boolean first = true; for (java.util.Map.Entry entry : types.entrySet()) { if (!first) { sb.append(", "); } String typeName = typeNames.getOrDefault(entry.getKey(), entry.getKey()); sb.append(typeName).append(": ").append(entry.getValue()).append("道"); first = false; } sb.append("}"); return sb.toString(); } }