|
|
@@ -0,0 +1,307 @@
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.etotem.cfc.dto.EmotionAlertVO;
|
|
|
+import com.etotem.cfc.dto.ScreenResultVO;
|
|
|
+import com.etotem.cfc.dto.ScreenSubmitDTO;
|
|
|
+import com.etotem.cfc.entity.EmotionAlert;
|
|
|
+import com.etotem.cfc.entity.MentalHealthScreen;
|
|
|
+import com.etotem.cfc.mapper.EmotionAlertMapper;
|
|
|
+import com.etotem.cfc.mapper.MentalHealthScreenMapper;
|
|
|
+import com.fasterxml.jackson.core.JsonProcessingException;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.util.*;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class MentalHealthScreenService {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private MentalHealthScreenMapper mentalHealthScreenMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private EmotionAlertMapper emotionAlertMapper;
|
|
|
+
|
|
|
+ private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+
|
|
|
+ // === PHQ-9 量表定义 ===
|
|
|
+ private static final List<String> PHQ9_QUESTIONS = Arrays.asList(
|
|
|
+ "做事时提不起劲或没有兴趣",
|
|
|
+ "感到心情低落、沮丧或绝望",
|
|
|
+ "入睡困难、睡不安稳或睡眠过多",
|
|
|
+ "感觉疲倦或没有活力",
|
|
|
+ "食欲不振或吃太多",
|
|
|
+ "觉得自己很糟——或觉得自己很失败,或让自己或家人失望",
|
|
|
+ "对事物专注有困难,例如阅读报纸或看电视时",
|
|
|
+ "行动或说话速度缓慢到别人已经觉察?或正好相反——比平常更多话",
|
|
|
+ "有不如死掉或用某种方式伤害自己的念头"
|
|
|
+ );
|
|
|
+
|
|
|
+ private static final Map<Integer, String> PHQ9_SEVERITY_MAP = new LinkedHashMap<>();
|
|
|
+
|
|
|
+ static {
|
|
|
+ PHQ9_SEVERITY_MAP.put(4, "none");
|
|
|
+ PHQ9_SEVERITY_MAP.put(9, "mild");
|
|
|
+ PHQ9_SEVERITY_MAP.put(14, "moderate");
|
|
|
+ PHQ9_SEVERITY_MAP.put(19, "moderately_severe");
|
|
|
+ // >=20 -> "severe"
|
|
|
+ }
|
|
|
+
|
|
|
+ // === GAD-7 量表定义 ===
|
|
|
+ private static final List<String> GAD7_QUESTIONS = Arrays.asList(
|
|
|
+ "感到紧张、焦虑或烦躁",
|
|
|
+ "无法停止或控制担忧",
|
|
|
+ "对各种各样的事情担忧过多",
|
|
|
+ "很难放松下来",
|
|
|
+ "由于不安而无法静坐",
|
|
|
+ "变得容易烦恼或急躁",
|
|
|
+ "感到害怕,好像可能发生可怕的事情"
|
|
|
+ );
|
|
|
+
|
|
|
+ private static final Map<Integer, String> GAD7_SEVERITY_MAP = new LinkedHashMap<>();
|
|
|
+
|
|
|
+ static {
|
|
|
+ GAD7_SEVERITY_MAP.put(4, "none");
|
|
|
+ GAD7_SEVERITY_MAP.put(9, "mild");
|
|
|
+ GAD7_SEVERITY_MAP.put(14, "moderate");
|
|
|
+ // >=15 -> "severe"
|
|
|
+ }
|
|
|
+
|
|
|
+ // === 风险关键词 (Phase 1.2) ===
|
|
|
+ private static final Set<String> CRISIS_KEYWORDS = new HashSet<>(Arrays.asList(
|
|
|
+ "自杀", "不想活", "死了算了", "活不下去", "自残", "伤害自己",
|
|
|
+ "suicide", "kill myself", "end my life", "self-harm", "cutting"
|
|
|
+ ));
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 提交心理筛查问卷并计算评分
|
|
|
+ */
|
|
|
+ @Transactional
|
|
|
+ public ScreenResultVO submitScreen(ScreenSubmitDTO dto) {
|
|
|
+ int totalScore = dto.getAnswers().stream().mapToInt(Integer::intValue).sum();
|
|
|
+ String severity = calculateSeverity(dto.getScreenType(), totalScore);
|
|
|
+
|
|
|
+ // 检测风险标记
|
|
|
+ List<String> riskFlags = detectRiskFlags(dto.getScreenType(), dto.getAnswers(), totalScore);
|
|
|
+
|
|
|
+ // 生成建议文案
|
|
|
+ String suggestions = generateSuggestions(dto.getScreenType(), severity, riskFlags);
|
|
|
+
|
|
|
+ MentalHealthScreen screen = new MentalHealthScreen();
|
|
|
+ screen.setChildId(dto.getChildId());
|
|
|
+ screen.setScreenType(dto.getScreenType());
|
|
|
+ try {
|
|
|
+ screen.setAnswers(objectMapper.writeValueAsString(dto.getAnswers()));
|
|
|
+ } catch (JsonProcessingException e) {
|
|
|
+ screen.setAnswers(dto.getAnswers().toString());
|
|
|
+ }
|
|
|
+ screen.setTotalScore(totalScore);
|
|
|
+ screen.setSeverityLevel(severity);
|
|
|
+ try {
|
|
|
+ screen.setRiskFlags(objectMapper.writeValueAsString(riskFlags));
|
|
|
+ } catch (JsonProcessingException e) {
|
|
|
+ screen.setRiskFlags(String.join(",", riskFlags));
|
|
|
+ }
|
|
|
+ screen.setSuggestions(suggestions);
|
|
|
+ screen.setCompletedAt(new Date());
|
|
|
+ mentalHealthScreenMapper.insert(screen);
|
|
|
+
|
|
|
+ // 高风险则自动创建告警
|
|
|
+ if ("moderately_severe".equals(severity) || "severe".equals(severity) || !riskFlags.isEmpty()) {
|
|
|
+ createAlertFromScreen(screen, severity, riskFlags);
|
|
|
+ }
|
|
|
+
|
|
|
+ return toVO(screen);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取筛查历史
|
|
|
+ */
|
|
|
+ public List<ScreenResultVO> getScreenHistory(Long childId, String screenType, int limit) {
|
|
|
+ LambdaQueryWrapper<MentalHealthScreen> wrapper = new LambdaQueryWrapper<MentalHealthScreen>()
|
|
|
+ .eq(MentalHealthScreen::getChildId, childId)
|
|
|
+ .eq(screenType != null, MentalHealthScreen::getScreenType, screenType)
|
|
|
+ .orderByDesc(MentalHealthScreen::getCompletedAt)
|
|
|
+ .last("LIMIT " + limit);
|
|
|
+ return mentalHealthScreenMapper.selectList(wrapper)
|
|
|
+ .stream().map(this::toVO).collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取最新一次筛查结果
|
|
|
+ */
|
|
|
+ public ScreenResultVO getLatestScreen(Long childId, String screenType) {
|
|
|
+ LambdaQueryWrapper<MentalHealthScreen> wrapper = new LambdaQueryWrapper<MentalHealthScreen>()
|
|
|
+ .eq(MentalHealthScreen::getChildId, childId)
|
|
|
+ .eq(MentalHealthScreen::getScreenType, screenType)
|
|
|
+ .orderByDesc(MentalHealthScreen::getCompletedAt)
|
|
|
+ .last("LIMIT 1");
|
|
|
+ MentalHealthScreen screen = mentalHealthScreenMapper.selectOne(wrapper);
|
|
|
+ return screen != null ? toVO(screen) : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取活跃告警(未解决的)
|
|
|
+ */
|
|
|
+ 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::toAlertVO).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 String calculateSeverity(String screenType, int totalScore) {
|
|
|
+ if ("PHQ9".equals(screenType) || "phq9".equalsIgnoreCase(screenType)) {
|
|
|
+ if (totalScore >= 20) return "severe";
|
|
|
+ for (Map.Entry<Integer, String> entry : PHQ9_SEVERITY_MAP.entrySet()) {
|
|
|
+ if (totalScore <= entry.getKey()) return entry.getValue();
|
|
|
+ }
|
|
|
+ return "none";
|
|
|
+ } else if ("GAD7".equals(screenType) || "gad7".equalsIgnoreCase(screenType)) {
|
|
|
+ if (totalScore >= 15) return "severe";
|
|
|
+ for (Map.Entry<Integer, String> entry : GAD7_SEVERITY_MAP.entrySet()) {
|
|
|
+ if (totalScore <= entry.getKey()) return entry.getValue();
|
|
|
+ }
|
|
|
+ return "none";
|
|
|
+ }
|
|
|
+ return "unknown";
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<String> detectRiskFlags(String screenType, List<Integer> answers, int totalScore) {
|
|
|
+ List<String> flags = new ArrayList<>();
|
|
|
+ if ("PHQ9".equals(screenType) || "phq9".equalsIgnoreCase(screenType)) {
|
|
|
+ // Q9: 自杀/自伤念头 (answers index 8)
|
|
|
+ if (answers.size() > 8 && answers.get(8) > 0) {
|
|
|
+ flags.add("suicide_ideation");
|
|
|
+ }
|
|
|
+ // Q1+Q2 high: 核心抑郁症状显著
|
|
|
+ if (answers.size() > 1 && answers.get(0) >= 2 && answers.get(1) >= 2) {
|
|
|
+ flags.add("core_depression_symptoms");
|
|
|
+ }
|
|
|
+ } else if ("GAD7".equals(screenType) || "gad7".equalsIgnoreCase(screenType)) {
|
|
|
+ // Q1+Q2 high: 核心焦虑症状显著
|
|
|
+ if (answers.size() > 1 && answers.get(0) >= 2 && answers.get(1) >= 2) {
|
|
|
+ flags.add("core_anxiety_symptoms");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return flags;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String generateSuggestions(String screenType, String severity, List<String> riskFlags) {
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+
|
|
|
+ if (riskFlags.contains("suicide_ideation")) {
|
|
|
+ sb.append("⚠️ 请注意:您提到了一些与自伤/自杀相关的想法。请立即联系心理危机热线(400-161-9995)或前往就近医院寻求帮助。");
|
|
|
+ return sb.toString(); // 危机建议优先级最高
|
|
|
+ }
|
|
|
+
|
|
|
+ if ("severe".equals(severity) || "moderately_severe".equals(severity)) {
|
|
|
+ sb.append("建议尽快联系专业心理咨询师或精神科医生进行进一步评估。");
|
|
|
+ } else if ("moderate".equals(severity)) {
|
|
|
+ sb.append("建议关注自身心理状态,适当进行放松练习,如有需要可咨询心理专业人员。");
|
|
|
+ } else if ("mild".equals(severity)) {
|
|
|
+ sb.append("目前状态基本良好,建议保持规律作息和适量运动,关注情绪变化。");
|
|
|
+ } else {
|
|
|
+ sb.append("状态良好,继续保持!");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!riskFlags.isEmpty()) {
|
|
|
+ sb.append(" 部分指标需要关注,建议定期进行心理自评。");
|
|
|
+ }
|
|
|
+
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void createAlertFromScreen(MentalHealthScreen screen, String severity, List<String> riskFlags) {
|
|
|
+ EmotionAlert alert = new EmotionAlert();
|
|
|
+ alert.setChildId(screen.getChildId());
|
|
|
+ alert.setResolved(false);
|
|
|
+ alert.setCreatedAt(new Date());
|
|
|
+
|
|
|
+ if (riskFlags.contains("suicide_ideation")) {
|
|
|
+ alert.setAlertType("crisis_keyword");
|
|
|
+ alert.setSeverity("high");
|
|
|
+ alert.setTitle("⚠️ 危机预警:自杀/自伤风险");
|
|
|
+ alert.setMessage("心理筛查结果显示存在自杀/自伤相关念头,建议立即联系危机干预热线。");
|
|
|
+ } else if ("severe".equals(severity)) {
|
|
|
+ alert.setAlertType("consecutive_low_mood");
|
|
|
+ alert.setSeverity("high");
|
|
|
+ String typeLabel = "PHQ9".equals(screen.getScreenType()) ? "抑郁" : "焦虑";
|
|
|
+ alert.setTitle("⚠️ " + typeLabel + "水平严重");
|
|
|
+ alert.setMessage("" + screen.getScreenType() + "筛查得分为" + screen.getTotalScore() + "分,建议尽快寻求专业帮助。");
|
|
|
+ } else if ("moderately_severe".equals(severity)) {
|
|
|
+ alert.setAlertType("consecutive_low_mood");
|
|
|
+ alert.setSeverity("medium");
|
|
|
+ String typeLabel = "PHQ9".equals(screen.getScreenType()) ? "抑郁" : "焦虑";
|
|
|
+ alert.setTitle("⚡ " + typeLabel + "水平中重度");
|
|
|
+ alert.setMessage("" + screen.getScreenType() + "筛查得分为" + screen.getTotalScore() + "分,建议关注心理状态。");
|
|
|
+ } else {
|
|
|
+ alert.setAlertType("consecutive_low_mood");
|
|
|
+ alert.setSeverity("low");
|
|
|
+ alert.setTitle("心理状态提醒");
|
|
|
+ alert.setMessage("筛查结果显示一些需要关注的指标,建议定期自评。");
|
|
|
+ }
|
|
|
+
|
|
|
+ emotionAlertMapper.insert(alert);
|
|
|
+ }
|
|
|
+
|
|
|
+ private ScreenResultVO toVO(MentalHealthScreen screen) {
|
|
|
+ ScreenResultVO vo = new ScreenResultVO();
|
|
|
+ vo.setId(screen.getId());
|
|
|
+ vo.setChildId(screen.getChildId());
|
|
|
+ vo.setScreenType(screen.getScreenType());
|
|
|
+ vo.setTotalScore(screen.getTotalScore());
|
|
|
+ vo.setSeverityLevel(screen.getSeverityLevel());
|
|
|
+ vo.setSuggestions(screen.getSuggestions());
|
|
|
+ vo.setCompletedAt(screen.getCompletedAt());
|
|
|
+
|
|
|
+ // 解析 riskFlags JSON
|
|
|
+ if (screen.getRiskFlags() != null) {
|
|
|
+ try {
|
|
|
+ List<String> flags = objectMapper.readValue(screen.getRiskFlags(), List.class);
|
|
|
+ vo.setRiskFlags(flags);
|
|
|
+ } catch (Exception e) {
|
|
|
+ vo.setRiskFlags(Collections.singletonList(screen.getRiskFlags()));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ vo.setRiskFlags(Collections.emptyList());
|
|
|
+ }
|
|
|
+
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ private EmotionAlertVO toAlertVO(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;
|
|
|
+ }
|
|
|
+}
|