package com.njuzr.eaibackend.service.impl; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.njuzr.eaibackend.mapper.StudentAssignmentMapper; import com.njuzr.eaibackend.po.TextAnalysis; import com.njuzr.eaibackend.service.TextAnalysisService; import edu.stanford.nlp.simple.Document; import edu.stanford.nlp.simple.Sentence; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import java.util.ArrayList; import java.util.List; /** * 文本分析服务实现类 * 提供异步文本分析功能,包括分句、分词和词汇级别分析 * * @author AI Assistant * @date 2025-01-27 */ @Slf4j @Service public class TextAnalysisServiceImpl implements TextAnalysisService { private final StudentAssignmentMapper studentAssignmentMapper; private final MongoTemplate mongoTemplate; private final ObjectMapper objectMapper; private final RestTemplate restTemplate; // 外部API配置 private static final String WORD_LEVEL_API_URL = "https://laurenceanthony.net/software/wordfamilyfinder/get_result.php"; private static final String DATABASE = "basewords_130.db"; private static final String CORPUS = "bnc_freq"; private static final int MAX_RETRY_COUNT = 5; private static final long RETRY_DELAY_MS = 500; @Autowired public TextAnalysisServiceImpl(StudentAssignmentMapper studentAssignmentMapper, MongoTemplate mongoTemplate) { this.studentAssignmentMapper = studentAssignmentMapper; this.mongoTemplate = mongoTemplate; this.objectMapper = new ObjectMapper(); this.restTemplate = new RestTemplate(); } /** * 异步分析并保存文本内容 * 该方法会在后台异步执行,不会阻塞主流程 * * @param studentId 学生ID * @param assignmentId 作业ID */ @Async("textAnalysisTaskExecutor") @Override public void analyzeAndSaveTextContentAsync(Long studentId, Long assignmentId) { log.info("开始异步分析文本内容 - studentId: {}, assignmentId: {}", studentId, assignmentId); try { // 1. 获取最新的text_content String textContent = studentAssignmentMapper.getTextContent(studentId, assignmentId); if (textContent == null || textContent.trim().isEmpty()) { log.warn("文本内容为空,跳过分析 - studentId: {}, assignmentId: {}", studentId, assignmentId); return; } log.info("获取到文本内容,长度: {} - studentId: {}, assignmentId: {}", textContent.length(), studentId, assignmentId); // 2. 使用Stanford CoreNLP进行分句和分词 Document doc = new Document(textContent); List allSentences = new ArrayList<>(doc.sentences()); // 一次性加载所有句子到内存 // 后续操作都使用allSentences集合,避免频繁调用doc.sentences()方法 List sentences = allSentences.stream() .map(Sentence::text) .toList(); log.info("分句完成,共{}个句子 - studentId: {}, assignmentId: {}", sentences.size(), studentId, assignmentId); // 3. 分词并获取词汇级别 List wordLevels = new ArrayList<>(); for (Sentence sentence : allSentences) { for (String word : sentence.words()) { TextAnalysis.WordLevel wordLevel = new TextAnalysis.WordLevel(); wordLevel.setWord(word); // 判断是否为标点符号 if (isPunctuation(word)) { wordLevel.setBasewordLevel(0); } else { // 获取词汇级别(带重试机制) Integer level = fetchBasewordLevelWithRetry(word); wordLevel.setBasewordLevel(level); } wordLevels.add(wordLevel); } } log.info("分词完成,共{}个词汇 - studentId: {}, assignmentId: {}", wordLevels.size(), studentId, assignmentId); // 4. 组装TextAnalysis对象 TextAnalysis analysis = new TextAnalysis(); analysis.setStudentId(studentId); analysis.setAssignmentId(assignmentId); analysis.setTextContent(textContent); analysis.setSentences(sentences); analysis.setWordLevels(wordLevels); // 查找是否已有数据,若有则复用_id,实现数据的更新;若没有,则新插入 Query query = new Query(); query.addCriteria(Criteria.where("studentId").is(studentId).and("assignmentId").is(assignmentId)); TextAnalysis old = mongoTemplate.findOne(query, TextAnalysis.class, "textAnalyses"); if (old != null) { analysis.setId(old.getId()); } // 5. 保存到MongoDB mongoTemplate.save(analysis, "textAnalyses"); log.info("文本分析完成并保存到MongoDB - studentId: {}, assignmentId: {}", studentId, assignmentId); } catch (Exception e) { log.error("文本分析失败 - studentId: {}, assignmentId: {}, error: {}", studentId, assignmentId, e.getMessage(), e); } } /** * 判断字符串是否为标点符号 * * @param word 待判断的字符串 * @return 是否为标点符号 */ private boolean isPunctuation(String word) { return word.matches("\\p{Punct}"); } /** * 带重试机制的词汇级别获取 * * @param word 待查询的词汇 * @return 词汇级别,如果查询失败返回0 */ private Integer fetchBasewordLevelWithRetry(String word) { for (int retry = 0; retry < MAX_RETRY_COUNT; retry++) { try { Integer level = fetchBasewordLevel(word); if (level != null) { return level; } } catch (Exception e) { log.warn("获取词汇级别失败,第{}次重试 - word: {}, error: {}", retry + 1, word, e.getMessage()); } // 重试前等待 if (retry < MAX_RETRY_COUNT - 1) { try { Thread.sleep(RETRY_DELAY_MS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } log.warn("获取词汇级别失败,已达到最大重试次数 - word: {}", word); return 0; } /** * 调用外部API获取词汇级别 * * @param word 待查询的词汇 * @return 词汇级别,如果查询失败返回null */ private Integer fetchBasewordLevel(String word) { try { // 设置请求头 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); // 设置请求参数 MultiValueMap params = new LinkedMultiValueMap<>(); params.add("search", word); params.add("database", DATABASE); params.add("corpus", CORPUS); HttpEntity> request = new HttpEntity<>(params, headers); // 发送POST请求 ResponseEntity response = restTemplate.postForEntity( WORD_LEVEL_API_URL, request, String.class); if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { // 解析JSON响应 JsonNode jsonNode = objectMapper.readTree(response.getBody()); // 检查响应格式:[[VALUE, level, ...]] if (jsonNode.isArray() && jsonNode.size() > 0) { JsonNode firstElement = jsonNode.get(0); if (firstElement.isArray() && firstElement.size() > 1) { JsonNode levelNode = firstElement.get(1); try { return levelNode.asInt(); // 直接返回整数 } catch (NumberFormatException e) { log.warn("level不是有效数字: {}", levelNode); return 0; } } } } log.warn("API响应格式异常 - word: {}, response: {}", word, response.getBody()); return 0; } catch (Exception e) { log.error("调用词汇级别API失败 - word: {}, error: {}", word, e.getMessage()); return 0; } } }