|
|
@@ -0,0 +1,241 @@
|
|
|
+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<Sentence> allSentences = new ArrayList<>(doc.sentences()); // 一次性加载所有句子到内存
|
|
|
+
|
|
|
+ // 后续操作都使用allSentences集合,避免频繁调用doc.sentences()方法
|
|
|
+ List<String> sentences = allSentences.stream()
|
|
|
+ .map(Sentence::text)
|
|
|
+ .toList();
|
|
|
+
|
|
|
+ log.info("分句完成,共{}个句子 - studentId: {}, assignmentId: {}",
|
|
|
+ sentences.size(), studentId, assignmentId);
|
|
|
+
|
|
|
+ // 3. 分词并获取词汇级别
|
|
|
+ List<TextAnalysis.WordLevel> 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<String, String> params = new LinkedMultiValueMap<>();
|
|
|
+ params.add("search", word);
|
|
|
+ params.add("database", DATABASE);
|
|
|
+ params.add("corpus", CORPUS);
|
|
|
+
|
|
|
+ HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
|
|
|
+
|
|
|
+ // 发送POST请求
|
|
|
+ ResponseEntity<String> 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|