|
|
@@ -6,6 +6,8 @@ import com.etotem.cfc.dto.EmotionCheckinDTO;
|
|
|
import com.etotem.cfc.dto.EmotionCheckinVO;
|
|
|
import com.etotem.cfc.dto.EmotionStatsVO;
|
|
|
import com.etotem.cfc.dto.EmotionTrendVO;
|
|
|
+import com.etotem.cfc.dto.MoodTrendPoint;
|
|
|
+import com.etotem.cfc.dto.AffectPoint;
|
|
|
import com.etotem.cfc.entity.Child;
|
|
|
import com.etotem.cfc.entity.EmotionCheckin;
|
|
|
import com.etotem.cfc.mapper.ChildMapper;
|
|
|
@@ -31,6 +33,9 @@ public class EmotionCheckinService {
|
|
|
@Resource
|
|
|
private EnergyService energyService;
|
|
|
|
|
|
+ @Resource
|
|
|
+ private EmotionAlertService emotionAlertService;
|
|
|
+
|
|
|
@Transactional
|
|
|
public EmotionCheckinVO createCheckin(EmotionCheckinDTO dto, Long userId) {
|
|
|
Child child = childMapper.selectById(dto.getChildId());
|
|
|
@@ -46,10 +51,28 @@ public class EmotionCheckinService {
|
|
|
checkin.setNote(dto.getNote());
|
|
|
checkin.setEnergyAwarded(5);
|
|
|
checkin.setCheckinDate(new Date());
|
|
|
- emotionCheckinMapper.insert(checkin);
|
|
|
|
|
|
+ // Phase 1.1: 精细情绪字段
|
|
|
+ if (dto.getMoodScore() != null) checkin.setMoodScore(dto.getMoodScore());
|
|
|
+ if (dto.getEmotionType() != null) checkin.setEmotionType(dto.getEmotionType());
|
|
|
+ if (dto.getStressLevel() != null) checkin.setStressLevel(dto.getStressLevel());
|
|
|
+ if (dto.getArousalLevel() != null) checkin.setArousalLevel(dto.getArousalLevel());
|
|
|
+ if (dto.getEnergyLevel() != null) checkin.setEnergyLevel(dto.getEnergyLevel());
|
|
|
+
|
|
|
+ // Phase 3.1: 睡眠字段预埋
|
|
|
+ if (dto.getSleepHours() != null) checkin.setSleepHours(dto.getSleepHours());
|
|
|
+ if (dto.getSleepQuality() != null) checkin.setSleepQuality(dto.getSleepQuality());
|
|
|
+
|
|
|
+ emotionCheckinMapper.insert(checkin);
|
|
|
awardEnergy(checkin.getId(), dto.getChildId());
|
|
|
|
|
|
+ // Phase 1.2: 创建打卡后自动检测风险
|
|
|
+ try {
|
|
|
+ emotionAlertService.checkRiskAfterCheckin(checkin);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("情绪风险检测失败: checkinId={}", checkin.getId(), e);
|
|
|
+ }
|
|
|
+
|
|
|
return toVO(checkin, child.getNickname(), false);
|
|
|
}
|
|
|
|
|
|
@@ -116,7 +139,11 @@ public class EmotionCheckinService {
|
|
|
|
|
|
Map<String, Integer> distribution = new HashMap<>();
|
|
|
List<Map<String, Object>> dailyData = new ArrayList<>();
|
|
|
+ List<MoodTrendPoint> moodTrend = new ArrayList<>();
|
|
|
+ List<AffectPoint> scatterData = new ArrayList<>();
|
|
|
int totalEnergy = 0;
|
|
|
+ double moodSum = 0;
|
|
|
+ int moodCount = 0;
|
|
|
|
|
|
for (EmotionCheckin e : filtered) {
|
|
|
String weather = e.getMoodWeather();
|
|
|
@@ -126,15 +153,46 @@ public class EmotionCheckinService {
|
|
|
Map<String, Object> day = new HashMap<>();
|
|
|
day.put("date", e.getCheckinDate());
|
|
|
day.put("weather", e.getMoodWeather());
|
|
|
+ // Include fine-grained data in daily payload
|
|
|
+ if (e.getMoodScore() != null) day.put("moodScore", e.getMoodScore());
|
|
|
+ if (e.getArousalLevel() != null) day.put("arousalLevel", e.getArousalLevel());
|
|
|
+ if (e.getEmotionType() != null) day.put("emotionType", e.getEmotionType());
|
|
|
+ if (e.getStressLevel() != null) day.put("stressLevel", e.getStressLevel());
|
|
|
dailyData.add(day);
|
|
|
+
|
|
|
+ // Mood trend for line chart
|
|
|
+ if (e.getMoodScore() != null) {
|
|
|
+ MoodTrendPoint pt = new MoodTrendPoint();
|
|
|
+ pt.setDate(e.getCheckinDate().toString());
|
|
|
+ pt.setMoodScore(e.getMoodScore());
|
|
|
+ moodTrend.add(pt);
|
|
|
+ moodSum += e.getMoodScore();
|
|
|
+ moodCount++;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Scatter data for valence-arousal plot
|
|
|
+ if (e.getMoodScore() != null && e.getArousalLevel() != null) {
|
|
|
+ AffectPoint ap = new AffectPoint();
|
|
|
+ ap.setDate(e.getCheckinDate().toString());
|
|
|
+ ap.setValence(e.getMoodScore());
|
|
|
+ ap.setArousal(e.getArousalLevel());
|
|
|
+ scatterData.add(ap);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+ // Sort by date ascending (currently descending from query)
|
|
|
+ Collections.reverse(moodTrend);
|
|
|
+ Collections.reverse(scatterData);
|
|
|
+
|
|
|
EmotionTrendVO vo = new EmotionTrendVO();
|
|
|
vo.setPeriod(period);
|
|
|
vo.setWeatherDistribution(distribution);
|
|
|
vo.setDailyData(dailyData);
|
|
|
vo.setTotalCheckins(filtered.size());
|
|
|
vo.setTotalEnergy(totalEnergy);
|
|
|
+ vo.setMoodScoreAvg(moodCount > 0 ? Math.round(moodSum / moodCount * 10.0) / 10.0 : null);
|
|
|
+ vo.setMoodTrend(moodTrend);
|
|
|
+ vo.setScatterData(scatterData);
|
|
|
return vo;
|
|
|
}
|
|
|
|
|
|
@@ -164,6 +222,72 @@ public class EmotionCheckinService {
|
|
|
return vo;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 周度情绪报告汇总
|
|
|
+ */
|
|
|
+ public Map<String, Object> getWeeklyReport(Long childId) {
|
|
|
+ LambdaQueryWrapper<EmotionCheckin> wrapper = new LambdaQueryWrapper<EmotionCheckin>()
|
|
|
+ .eq(EmotionCheckin::getChildId, childId)
|
|
|
+ .orderByDesc(EmotionCheckin::getCheckinDate);
|
|
|
+ List<EmotionCheckin> all = emotionCheckinMapper.selectList(wrapper);
|
|
|
+
|
|
|
+ Calendar cal = Calendar.getInstance();
|
|
|
+ cal.add(Calendar.DAY_OF_YEAR, -7);
|
|
|
+ Date cutoff = cal.getTime();
|
|
|
+
|
|
|
+ List<EmotionCheckin> weekly = all.stream()
|
|
|
+ .filter(e -> e.getCheckinDate() != null && e.getCheckinDate().after(cutoff))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ Map<String, Object> report = new HashMap<>();
|
|
|
+ report.put("totalCheckins", weekly.size());
|
|
|
+
|
|
|
+ // Average mood score
|
|
|
+ OptionalDouble avgMood = weekly.stream()
|
|
|
+ .filter(e -> e.getMoodScore() != null)
|
|
|
+ .mapToInt(EmotionCheckin::getMoodScore)
|
|
|
+ .average();
|
|
|
+ report.put("avgMoodScore", avgMood.isPresent() ? Math.round(avgMood.getAsDouble() * 10.0) / 10.0 : null);
|
|
|
+
|
|
|
+ // Dominant emotion type
|
|
|
+ Map<String, Long> typeCount = weekly.stream()
|
|
|
+ .filter(e -> e.getEmotionType() != null)
|
|
|
+ .collect(Collectors.groupingBy(EmotionCheckin::getEmotionType, Collectors.counting()));
|
|
|
+ report.put("emotionTypeDistribution", typeCount);
|
|
|
+
|
|
|
+ // Average stress
|
|
|
+ OptionalDouble avgStress = weekly.stream()
|
|
|
+ .filter(e -> e.getStressLevel() != null)
|
|
|
+ .mapToInt(EmotionCheckin::getStressLevel)
|
|
|
+ .average();
|
|
|
+ report.put("avgStressLevel", avgStress.isPresent() ? Math.round(avgStress.getAsDouble() * 10.0) / 10.0 : null);
|
|
|
+
|
|
|
+ // Count of low-mood days (moodScore <= 4)
|
|
|
+ long lowMoodDays = weekly.stream()
|
|
|
+ .filter(e -> e.getMoodScore() != null && e.getMoodScore() <= 4)
|
|
|
+ .count();
|
|
|
+ report.put("lowMoodDays", lowMoodDays);
|
|
|
+
|
|
|
+ report.put("totalEnergy", weekly.stream()
|
|
|
+ .filter(e -> e.getEnergyAwarded() != null)
|
|
|
+ .mapToInt(EmotionCheckin::getEnergyAwarded)
|
|
|
+ .sum());
|
|
|
+
|
|
|
+ // Mood trend (last 7 data points)
|
|
|
+ List<Map<String, Object>> weekTrend = new ArrayList<>();
|
|
|
+ for (EmotionCheckin e : weekly) {
|
|
|
+ Map<String, Object> pt = new HashMap<>();
|
|
|
+ pt.put("date", e.getCheckinDate());
|
|
|
+ pt.put("moodScore", e.getMoodScore());
|
|
|
+ pt.put("weather", e.getMoodWeather());
|
|
|
+ weekTrend.add(pt);
|
|
|
+ }
|
|
|
+ weekTrend.sort(Comparator.comparing(m -> ((Map<String, Object>) m).get("date") != null ? ((Date) ((Map<String, Object>) m).get("date")).getTime() : 0));
|
|
|
+ report.put("dailyTrend", weekTrend);
|
|
|
+
|
|
|
+ return report;
|
|
|
+ }
|
|
|
+
|
|
|
private int calculateStreak(List<EmotionCheckin> list) {
|
|
|
if (list.isEmpty()) return 0;
|
|
|
Calendar cal = Calendar.getInstance();
|
|
|
@@ -213,6 +337,17 @@ public class EmotionCheckinService {
|
|
|
vo.setEnergyAwarded(ec.getEnergyAwarded());
|
|
|
vo.setCheckinDate(ec.getCheckinDate());
|
|
|
vo.setCreatedAt(ec.getCreatedAt());
|
|
|
+
|
|
|
+ // Phase 1.1: 精细情绪字段
|
|
|
+ vo.setMoodScore(ec.getMoodScore());
|
|
|
+ vo.setEmotionType(ec.getEmotionType());
|
|
|
+ vo.setStressLevel(ec.getStressLevel());
|
|
|
+ vo.setArousalLevel(ec.getArousalLevel());
|
|
|
+ vo.setEnergyLevel(ec.getEnergyLevel());
|
|
|
+
|
|
|
+ // Phase 3.1: 睡眠字段
|
|
|
+ vo.setSleepHours(ec.getSleepHours());
|
|
|
+ vo.setSleepQuality(ec.getSleepQuality());
|
|
|
return vo;
|
|
|
}
|
|
|
}
|