266 lines
13 KiB
Java
266 lines
13 KiB
Java
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();
|
||
}
|
||
}
|