Files
my-springboot-project/src/main/java/top/tqx/demo_1/service/AsyncExamService.java

266 lines
13 KiB
Java
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.
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<File> 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<File> likeWrapper =
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
likeWrapper.like(File::getFilePath, "3.docx");
java.util.List<File> 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<String, Integer> types) {
if (types == null || types.isEmpty()) {
return "{}";
}
StringBuilder sb = new StringBuilder("{");
java.util.Map<String, String> 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<String, Integer> 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();
}
}