|
@@ -0,0 +1,183 @@
|
|
|
|
|
+package com.etotem.cfc.service;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import com.etotem.cfc.dto.EmotionAlertVO;
|
|
|
|
|
+import com.etotem.cfc.entity.EmotionAlert;
|
|
|
|
|
+import com.etotem.cfc.entity.EmotionCheckin;
|
|
|
|
|
+import com.etotem.cfc.mapper.EmotionAlertMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.EmotionCheckinMapper;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.Resource;
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 情绪风险告警服务 —— 在每次情绪打卡完成后自动检查风险模式:
|
|
|
|
|
+ * 1. 连续低情绪(3天+ moodScore <= 4)
|
|
|
|
|
+ * 2. 情绪锐降(比前一天下降 4+ 分)
|
|
|
|
|
+ * 3. 备注中的危机关键词
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+public class EmotionAlertService {
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private EmotionAlertMapper emotionAlertMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private EmotionCheckinMapper emotionCheckinMapper;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 检查情绪打卡是否存在风险,返回新创建的告警列表
|
|
|
|
|
+ */
|
|
|
|
|
+ public List<EmotionAlert> checkRiskAfterCheckin(EmotionCheckin checkin) {
|
|
|
|
|
+ List<EmotionAlert> newAlerts = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 检查备注中是否含危机关键词
|
|
|
|
|
+ if (checkin.getNote() != null) {
|
|
|
|
|
+ Set<String> matched = matchCrisisKeywords(checkin.getNote());
|
|
|
|
|
+ if (!matched.isEmpty()) {
|
|
|
|
|
+ EmotionAlert alert = createAlert(
|
|
|
|
|
+ checkin.getChildId(),
|
|
|
|
|
+ "crisis_keyword",
|
|
|
|
|
+ "high",
|
|
|
|
|
+ "⚠️ 危机关键词预警",
|
|
|
|
|
+ "情绪打卡备注中检测到危机关键词:" + String.join(", ", matched) + ",建议及时关注。",
|
|
|
|
|
+ checkin.getId()
|
|
|
|
|
+ );
|
|
|
|
|
+ newAlerts.add(alert);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 检查是否有 moodScore 数据
|
|
|
|
|
+ if (checkin.getMoodScore() == null) {
|
|
|
|
|
+ return newAlerts;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 检查连续低情绪
|
|
|
|
|
+ List<EmotionCheckin> recent = getRecentCheckins(checkin.getChildId(), 7);
|
|
|
|
|
+ List<EmotionCheckin> lowMoodDays = recent.stream()
|
|
|
|
|
+ .filter(e -> e.getMoodScore() != null && e.getMoodScore() <= 4)
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+
|
|
|
|
|
+ if (lowMoodDays.size() >= 3) {
|
|
|
|
|
+ // 检查是否已有未解决的连续低情绪告警
|
|
|
|
|
+ boolean hasExisting = hasActiveAlert(checkin.getChildId(), "consecutive_low_mood");
|
|
|
|
|
+ if (!hasExisting) {
|
|
|
|
|
+ EmotionAlert alert = createAlert(
|
|
|
|
|
+ checkin.getChildId(),
|
|
|
|
|
+ "consecutive_low_mood",
|
|
|
|
|
+ lowMoodDays.size() >= 5 ? "high" : "medium",
|
|
|
|
|
+ "⚡ 连续低情绪提醒",
|
|
|
|
|
+ "最近" + recent.size() + "天中有" + lowMoodDays.size() + "天情绪偏低(≤4分),建议关注孩子心理状态。",
|
|
|
|
|
+ checkin.getId()
|
|
|
|
|
+ );
|
|
|
|
|
+ newAlerts.add(alert);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 检查情绪锐降 (与最近一次打卡比较)
|
|
|
|
|
+ if (recent.size() >= 2) {
|
|
|
|
|
+ EmotionCheckin prev = recent.get(1); // 上一次
|
|
|
|
|
+ if (prev.getMoodScore() != null && (prev.getMoodScore() - checkin.getMoodScore()) >= 4) {
|
|
|
|
|
+ boolean hasDeclineAlert = hasActiveAlert(checkin.getChildId(), "sharp_decline");
|
|
|
|
|
+ if (!hasDeclineAlert) {
|
|
|
|
|
+ EmotionAlert alert = createAlert(
|
|
|
|
|
+ checkin.getChildId(),
|
|
|
|
|
+ "sharp_decline",
|
|
|
|
|
+ "medium",
|
|
|
|
|
+ "📉 情绪明显下降",
|
|
|
|
|
+ "情绪评分从" + prev.getMoodScore() + "分降至" + checkin.getMoodScore() + "分,下降幅度较大。",
|
|
|
|
|
+ checkin.getId()
|
|
|
|
|
+ );
|
|
|
|
|
+ newAlerts.add(alert);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return newAlerts;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 批量创建检查告警(用于后台定时任务或全量扫描)
|
|
|
|
|
+ */
|
|
|
|
|
+ public List<EmotionAlertVO> getActiveAlerts(Long childId) {
|
|
|
|
|
+ LambdaQueryWrapper<EmotionAlert> wrapper = new LambdaQueryWrapper<EmotionAlert>()
|
|
|
|
|
+ .eq(EmotionAlert::getChildId, childId)
|
|
|
|
|
+ .eq(EmotionAlert::getResolved, false)
|
|
|
|
|
+ .orderByDesc(EmotionAlert::getCreatedAt);
|
|
|
|
|
+ return emotionAlertMapper.selectList(wrapper)
|
|
|
|
|
+ .stream().map(this::toVO).collect(Collectors.toList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void resolveAlert(Long alertId) {
|
|
|
|
|
+ EmotionAlert alert = emotionAlertMapper.selectById(alertId);
|
|
|
|
|
+ if (alert != null) {
|
|
|
|
|
+ alert.setResolved(true);
|
|
|
|
|
+ alert.setResolvedAt(new Date());
|
|
|
|
|
+ emotionAlertMapper.updateById(alert);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === 内部方法 ===
|
|
|
|
|
+
|
|
|
|
|
+ private static final Set<String> CRISIS_KEYWORDS = new HashSet<>(Arrays.asList(
|
|
|
|
|
+ "自杀", "不想活", "死了算了", "活不下去", "自残", "伤害自己",
|
|
|
|
|
+ "好痛苦", "没意思", "撑不下去了", "想死", "结束",
|
|
|
|
|
+ "suicide", "kill myself", "end my life", "self-harm", "cutting"
|
|
|
|
|
+ ));
|
|
|
|
|
+
|
|
|
|
|
+ private Set<String> matchCrisisKeywords(String text) {
|
|
|
|
|
+ Set<String> matched = new HashSet<>();
|
|
|
|
|
+ for (String keyword : CRISIS_KEYWORDS) {
|
|
|
|
|
+ if (text.contains(keyword)) {
|
|
|
|
|
+ matched.add(keyword);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return matched;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<EmotionCheckin> getRecentCheckins(Long childId, int limit) {
|
|
|
|
|
+ return emotionCheckinMapper.selectLatestByChildId(childId, limit);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean hasActiveAlert(Long childId, String alertType) {
|
|
|
|
|
+ LambdaQueryWrapper<EmotionAlert> wrapper = new LambdaQueryWrapper<EmotionAlert>()
|
|
|
|
|
+ .eq(EmotionAlert::getChildId, childId)
|
|
|
|
|
+ .eq(EmotionAlert::getAlertType, alertType)
|
|
|
|
|
+ .eq(EmotionAlert::getResolved, false);
|
|
|
|
|
+ return emotionAlertMapper.selectCount(wrapper) > 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private EmotionAlert createAlert(Long childId, String alertType, String severity,
|
|
|
|
|
+ String title, String message, Long relatedCheckinId) {
|
|
|
|
|
+ EmotionAlert alert = new EmotionAlert();
|
|
|
|
|
+ alert.setChildId(childId);
|
|
|
|
|
+ alert.setAlertType(alertType);
|
|
|
|
|
+ alert.setSeverity(severity);
|
|
|
|
|
+ alert.setTitle(title);
|
|
|
|
|
+ alert.setMessage(message);
|
|
|
|
|
+ alert.setRelatedCheckinId(relatedCheckinId);
|
|
|
|
|
+ alert.setResolved(false);
|
|
|
|
|
+ alert.setCreatedAt(new Date());
|
|
|
|
|
+ emotionAlertMapper.insert(alert);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("情绪告警创建: childId={}, type={}, severity={}", childId, alertType, severity);
|
|
|
|
|
+ return alert;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private EmotionAlertVO toVO(EmotionAlert alert) {
|
|
|
|
|
+ EmotionAlertVO vo = new EmotionAlertVO();
|
|
|
|
|
+ vo.setId(alert.getId());
|
|
|
|
|
+ vo.setChildId(alert.getChildId());
|
|
|
|
|
+ vo.setAlertType(alert.getAlertType());
|
|
|
|
|
+ vo.setSeverity(alert.getSeverity());
|
|
|
|
|
+ vo.setTitle(alert.getTitle());
|
|
|
|
|
+ vo.setMessage(alert.getMessage());
|
|
|
|
|
+ vo.setResolved(alert.getResolved());
|
|
|
|
|
+ vo.setCreatedAt(alert.getCreatedAt());
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|