EmotionAlertService.java 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. package com.etotem.cfc.service;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.etotem.cfc.dto.EmotionAlertVO;
  4. import com.etotem.cfc.entity.EmotionAlert;
  5. import com.etotem.cfc.entity.EmotionCheckin;
  6. import com.etotem.cfc.mapper.EmotionAlertMapper;
  7. import com.etotem.cfc.mapper.EmotionCheckinMapper;
  8. import lombok.extern.slf4j.Slf4j;
  9. import org.springframework.stereotype.Service;
  10. import javax.annotation.Resource;
  11. import java.util.*;
  12. import java.util.stream.Collectors;
  13. /**
  14. * 情绪风险告警服务 —— 在每次情绪打卡完成后自动检查风险模式:
  15. * 1. 连续低情绪(3天+ moodScore <= 4)
  16. * 2. 情绪锐降(比前一天下降 4+ 分)
  17. * 3. 备注中的危机关键词
  18. */
  19. @Slf4j
  20. @Service
  21. public class EmotionAlertService {
  22. @Resource
  23. private EmotionAlertMapper emotionAlertMapper;
  24. @Resource
  25. private EmotionCheckinMapper emotionCheckinMapper;
  26. /**
  27. * 检查情绪打卡是否存在风险,返回新创建的告警列表
  28. */
  29. public List<EmotionAlert> checkRiskAfterCheckin(EmotionCheckin checkin) {
  30. List<EmotionAlert> newAlerts = new ArrayList<>();
  31. // 1. 检查备注中是否含危机关键词
  32. if (checkin.getNote() != null) {
  33. Set<String> matched = matchCrisisKeywords(checkin.getNote());
  34. if (!matched.isEmpty()) {
  35. EmotionAlert alert = createAlert(
  36. checkin.getChildId(),
  37. "crisis_keyword",
  38. "high",
  39. "⚠️ 危机关键词预警",
  40. "情绪打卡备注中检测到危机关键词:" + String.join(", ", matched) + ",建议及时关注。",
  41. checkin.getId()
  42. );
  43. newAlerts.add(alert);
  44. }
  45. }
  46. // 2. 检查是否有 moodScore 数据
  47. if (checkin.getMoodScore() == null) {
  48. return newAlerts;
  49. }
  50. // 3. 检查连续低情绪
  51. List<EmotionCheckin> recent = getRecentCheckins(checkin.getChildId(), 7);
  52. List<EmotionCheckin> lowMoodDays = recent.stream()
  53. .filter(e -> e.getMoodScore() != null && e.getMoodScore() <= 4)
  54. .collect(Collectors.toList());
  55. if (lowMoodDays.size() >= 3) {
  56. // 检查是否已有未解决的连续低情绪告警
  57. boolean hasExisting = hasActiveAlert(checkin.getChildId(), "consecutive_low_mood");
  58. if (!hasExisting) {
  59. EmotionAlert alert = createAlert(
  60. checkin.getChildId(),
  61. "consecutive_low_mood",
  62. lowMoodDays.size() >= 5 ? "high" : "medium",
  63. "⚡ 连续低情绪提醒",
  64. "最近" + recent.size() + "天中有" + lowMoodDays.size() + "天情绪偏低(≤4分),建议关注孩子心理状态。",
  65. checkin.getId()
  66. );
  67. newAlerts.add(alert);
  68. }
  69. }
  70. // 4. 检查情绪锐降 (与最近一次打卡比较)
  71. if (recent.size() >= 2) {
  72. EmotionCheckin prev = recent.get(1); // 上一次
  73. if (prev.getMoodScore() != null && (prev.getMoodScore() - checkin.getMoodScore()) >= 4) {
  74. boolean hasDeclineAlert = hasActiveAlert(checkin.getChildId(), "sharp_decline");
  75. if (!hasDeclineAlert) {
  76. EmotionAlert alert = createAlert(
  77. checkin.getChildId(),
  78. "sharp_decline",
  79. "medium",
  80. "📉 情绪明显下降",
  81. "情绪评分从" + prev.getMoodScore() + "分降至" + checkin.getMoodScore() + "分,下降幅度较大。",
  82. checkin.getId()
  83. );
  84. newAlerts.add(alert);
  85. }
  86. }
  87. }
  88. return newAlerts;
  89. }
  90. /**
  91. * 批量创建检查告警(用于后台定时任务或全量扫描)
  92. */
  93. public List<EmotionAlertVO> getActiveAlerts(Long childId) {
  94. LambdaQueryWrapper<EmotionAlert> wrapper = new LambdaQueryWrapper<EmotionAlert>()
  95. .eq(EmotionAlert::getChildId, childId)
  96. .eq(EmotionAlert::getResolved, false)
  97. .orderByDesc(EmotionAlert::getCreatedAt);
  98. return emotionAlertMapper.selectList(wrapper)
  99. .stream().map(this::toVO).collect(Collectors.toList());
  100. }
  101. public void resolveAlert(Long alertId) {
  102. EmotionAlert alert = emotionAlertMapper.selectById(alertId);
  103. if (alert != null) {
  104. alert.setResolved(true);
  105. alert.setResolvedAt(new Date());
  106. emotionAlertMapper.updateById(alert);
  107. }
  108. }
  109. // === 内部方法 ===
  110. private static final Set<String> CRISIS_KEYWORDS = new HashSet<>(Arrays.asList(
  111. "自杀", "不想活", "死了算了", "活不下去", "自残", "伤害自己",
  112. "好痛苦", "没意思", "撑不下去了", "想死", "结束",
  113. "suicide", "kill myself", "end my life", "self-harm", "cutting"
  114. ));
  115. private Set<String> matchCrisisKeywords(String text) {
  116. Set<String> matched = new HashSet<>();
  117. for (String keyword : CRISIS_KEYWORDS) {
  118. if (text.contains(keyword)) {
  119. matched.add(keyword);
  120. }
  121. }
  122. return matched;
  123. }
  124. private List<EmotionCheckin> getRecentCheckins(Long childId, int limit) {
  125. return emotionCheckinMapper.selectLatestByChildId(childId, limit);
  126. }
  127. private boolean hasActiveAlert(Long childId, String alertType) {
  128. LambdaQueryWrapper<EmotionAlert> wrapper = new LambdaQueryWrapper<EmotionAlert>()
  129. .eq(EmotionAlert::getChildId, childId)
  130. .eq(EmotionAlert::getAlertType, alertType)
  131. .eq(EmotionAlert::getResolved, false);
  132. return emotionAlertMapper.selectCount(wrapper) > 0;
  133. }
  134. private EmotionAlert createAlert(Long childId, String alertType, String severity,
  135. String title, String message, Long relatedCheckinId) {
  136. EmotionAlert alert = new EmotionAlert();
  137. alert.setChildId(childId);
  138. alert.setAlertType(alertType);
  139. alert.setSeverity(severity);
  140. alert.setTitle(title);
  141. alert.setMessage(message);
  142. alert.setRelatedCheckinId(relatedCheckinId);
  143. alert.setResolved(false);
  144. alert.setCreatedAt(new Date());
  145. emotionAlertMapper.insert(alert);
  146. log.info("情绪告警创建: childId={}, type={}, severity={}", childId, alertType, severity);
  147. return alert;
  148. }
  149. private EmotionAlertVO toVO(EmotionAlert alert) {
  150. EmotionAlertVO vo = new EmotionAlertVO();
  151. vo.setId(alert.getId());
  152. vo.setChildId(alert.getChildId());
  153. vo.setAlertType(alert.getAlertType());
  154. vo.setSeverity(alert.getSeverity());
  155. vo.setTitle(alert.getTitle());
  156. vo.setMessage(alert.getMessage());
  157. vo.setResolved(alert.getResolved());
  158. vo.setCreatedAt(alert.getCreatedAt());
  159. return vo;
  160. }
  161. }