|
|
@@ -0,0 +1,421 @@
|
|
|
+package com.etotem.cfc.service.impl;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.etotem.cfc.entity.*;
|
|
|
+import com.etotem.cfc.mapper.*;
|
|
|
+import com.etotem.cfc.service.ProfileComputeService;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import java.sql.Timestamp;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class ProfileComputeServiceImpl implements ProfileComputeService {
|
|
|
+
|
|
|
+ @Autowired private ProfileSnapshotMapper snapshotMapper;
|
|
|
+ @Autowired private ProfileHistoryMapper historyMapper;
|
|
|
+ @Autowired private HealthSleepRecordMapper sleepMapper;
|
|
|
+ @Autowired private HealthExerciseRecordMapper exerciseMapper;
|
|
|
+ @Autowired private HealthMealRecordMapper mealMapper;
|
|
|
+ @Autowired private HealthWaterRecordMapper waterMapper;
|
|
|
+ @Autowired private EmotionCheckinMapper emotionMapper;
|
|
|
+ @Autowired private TaskMapper taskMapper;
|
|
|
+ @Autowired private DanAssessmentResultMapper danMapper;
|
|
|
+ @Autowired private GameRecordMapper gameMapper;
|
|
|
+ @Autowired private FinanceCheckinMapper financeMapper;
|
|
|
+ @Autowired private FamilyMemberMapper memberMapper;
|
|
|
+ @Autowired private PointsLogMapper pointsLogMapper;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void computeAndSave(Long memberId) {
|
|
|
+ LocalDate now = LocalDate.now();
|
|
|
+
|
|
|
+ JSONObject bodyMetrics = computeBodyMetrics(memberId, now);
|
|
|
+ JSONObject mindMetrics = computeMindMetrics(memberId, now);
|
|
|
+ JSONObject wisdomMetrics = computeWisdomMetrics(memberId);
|
|
|
+ JSONObject actionMetrics = computeActionMetrics(memberId, now);
|
|
|
+ JSONObject wealthMetrics = computeWealthMetrics(memberId, now);
|
|
|
+ JSONObject dimensionScores = computeDimensionScores(bodyMetrics, mindMetrics, wisdomMetrics, actionMetrics, wealthMetrics);
|
|
|
+ String problemDomains = computeProblemDomains(memberId);
|
|
|
+
|
|
|
+ LocalDateTime computedAt = LocalDateTime.now();
|
|
|
+ JSONObject allMetrics = new JSONObject();
|
|
|
+ allMetrics.put("dimension_scores", dimensionScores);
|
|
|
+ allMetrics.put("body_metrics", bodyMetrics);
|
|
|
+ allMetrics.put("mind_metrics", mindMetrics);
|
|
|
+ allMetrics.put("wisdom_metrics", wisdomMetrics);
|
|
|
+ allMetrics.put("action_metrics", actionMetrics);
|
|
|
+ allMetrics.put("wealth_metrics", wealthMetrics);
|
|
|
+ allMetrics.put("problem_domains", problemDomains);
|
|
|
+ allMetrics.put("computed_at", computedAt.toString());
|
|
|
+
|
|
|
+ // upsert snapshot
|
|
|
+ LambdaQueryWrapper<ProfileSnapshot> qw = new LambdaQueryWrapper<>();
|
|
|
+ qw.eq(ProfileSnapshot::getMemberId, memberId);
|
|
|
+ ProfileSnapshot snapshot = snapshotMapper.selectOne(qw);
|
|
|
+
|
|
|
+ if (snapshot == null) {
|
|
|
+ snapshot = new ProfileSnapshot();
|
|
|
+ snapshot.setMemberId(memberId);
|
|
|
+ }
|
|
|
+ snapshot.setDimensionScores(dimensionScores.toJSONString());
|
|
|
+ snapshot.setBodyMetrics(bodyMetrics.toJSONString());
|
|
|
+ snapshot.setMindMetrics(mindMetrics.toJSONString());
|
|
|
+ snapshot.setWisdomMetrics(wisdomMetrics.toJSONString());
|
|
|
+ snapshot.setActionMetrics(actionMetrics.toJSONString());
|
|
|
+ snapshot.setWealthMetrics(wealthMetrics.toJSONString());
|
|
|
+ snapshot.setProblemDomains(problemDomains);
|
|
|
+ snapshot.setComputedAt(computedAt);
|
|
|
+ snapshotMapper.insert(snapshot);
|
|
|
+
|
|
|
+ // upsert history
|
|
|
+ LambdaQueryWrapper<ProfileHistory> hqw = new LambdaQueryWrapper<>();
|
|
|
+ hqw.eq(ProfileHistory::getMemberId, memberId)
|
|
|
+ .eq(ProfileHistory::getSnapshotDate, new java.sql.Date(now.atStartOfDay().toEpochDay() * 86400000));
|
|
|
+ ProfileHistory history = historyMapper.selectOne(hqw);
|
|
|
+ if (history == null) {
|
|
|
+ history = new ProfileHistory();
|
|
|
+ history.setMemberId(memberId);
|
|
|
+ history.setSnapshotDate(new java.sql.Date(now.atStartOfDay().toEpochDay() * 86400000));
|
|
|
+ history.setAllMetrics(allMetrics.toJSONString());
|
|
|
+ historyMapper.insert(history);
|
|
|
+ } else {
|
|
|
+ history.setAllMetrics(allMetrics.toJSONString());
|
|
|
+ historyMapper.updateById(history);
|
|
|
+ }
|
|
|
+
|
|
|
+ // cleanup old history
|
|
|
+ cleanOldHistory(memberId, now);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void computeAll() {
|
|
|
+ LambdaQueryWrapper<FamilyMember> qw = new LambdaQueryWrapper<>();
|
|
|
+ qw.select(FamilyMember::getId);
|
|
|
+ List<FamilyMember> members = memberMapper.selectList(qw);
|
|
|
+ for (FamilyMember m : members) {
|
|
|
+ try {
|
|
|
+ computeAndSave(m.getId());
|
|
|
+ } catch (Exception e) {
|
|
|
+ // ignore individual failure
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== 各维度计算方法 ==========
|
|
|
+
|
|
|
+ private JSONObject computeBodyMetrics(Long memberId, LocalDate now) {
|
|
|
+ JSONObject m = new JSONObject();
|
|
|
+ LocalDate thirtyDaysAgo = now.minusDays(30);
|
|
|
+ LocalDate sevenDaysAgo = now.minusDays(7);
|
|
|
+
|
|
|
+ // 睡眠:近30天
|
|
|
+ LambdaQueryWrapper<HealthSleepRecord> sqw = new LambdaQueryWrapper<>();
|
|
|
+ sqw.eq(HealthSleepRecord::getMemberId, memberId)
|
|
|
+ .ge(HealthSleepRecord::getCreatedAt, java.sql.Timestamp.valueOf(thirtyDaysAgo.atStartOfDay()));
|
|
|
+ List<HealthSleepRecord> sleeps = sleepMapper.selectList(sqw);
|
|
|
+ if (!sleeps.isEmpty()) {
|
|
|
+ int totalMin = sleeps.stream().mapToInt(r -> r.getDurationMinutes() != null ? r.getDurationMinutes() : 0).sum();
|
|
|
+ m.put("sleep_dur_avg", Math.round(totalMin / (double) sleeps.size() / 60 * 10) / 10.0);
|
|
|
+ int deepTotal = sleeps.stream().mapToInt(r -> r.getDeepSleepMinutes() != null ? r.getDeepSleepMinutes() : 0).sum();
|
|
|
+ int lightTotal = sleeps.stream().mapToInt(r -> r.getLightSleepMinutes() != null ? r.getLightSleepMinutes() : 0).sum();
|
|
|
+ int remTotal = sleeps.stream().mapToInt(r -> r.getRemMinutes() != null ? r.getRemMinutes() : 0).sum();
|
|
|
+ int totalSleepMin = deepTotal + lightTotal + remTotal;
|
|
|
+ m.put("deep_sleep_pct", totalSleepMin > 0 ? Math.round((double) deepTotal / totalSleepMin * 100) : 0);
|
|
|
+ m.put("sleep_records_count", sleeps.size());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 运动:近7天
|
|
|
+ LambdaQueryWrapper<HealthExerciseRecord> eqw = new LambdaQueryWrapper<>();
|
|
|
+ eqw.eq(HealthExerciseRecord::getMemberId, memberId)
|
|
|
+ .ge(HealthExerciseRecord::getCreatedAt, java.sql.Timestamp.valueOf(sevenDaysAgo.atStartOfDay()));
|
|
|
+ List<HealthExerciseRecord> exercises = exerciseMapper.selectList(eqw);
|
|
|
+ m.put("exercise_count_week", exercises.size());
|
|
|
+ int totalExMin = exercises.stream().mapToInt(r -> r.getDurationMinutes() != null ? r.getDurationMinutes() : 0).sum();
|
|
|
+ m.put("exercise_duration_week", totalExMin);
|
|
|
+
|
|
|
+ // 喝水:近30天
|
|
|
+ LambdaQueryWrapper<HealthWaterRecord> wqw = new LambdaQueryWrapper<>();
|
|
|
+ wqw.eq(HealthWaterRecord::getMemberId, memberId)
|
|
|
+ .ge(HealthWaterRecord::getCreatedAt, java.sql.Timestamp.valueOf(thirtyDaysAgo.atStartOfDay()));
|
|
|
+ List<HealthWaterRecord> waters = waterMapper.selectList(wqw);
|
|
|
+ if (!waters.isEmpty()) {
|
|
|
+ int totalMl = waters.stream().mapToInt(r -> r.getAmountMl() != null ? r.getAmountMl() : 0).sum();
|
|
|
+ m.put("water_intake_avg_ml", Math.round((double) totalMl / waters.size()));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 饮食记录数
|
|
|
+ LambdaQueryWrapper<HealthMealRecord> mqw = new LambdaQueryWrapper<>();
|
|
|
+ mqw.eq(HealthMealRecord::getMemberId, memberId)
|
|
|
+ .ge(HealthMealRecord::getCreatedAt, java.sql.Timestamp.valueOf(thirtyDaysAgo.atStartOfDay()));
|
|
|
+ long mealCount = mealMapper.selectCount(mqw);
|
|
|
+ m.put("meal_records_count", mealCount);
|
|
|
+
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONObject computeMindMetrics(Long memberId, LocalDate now) {
|
|
|
+ JSONObject m = new JSONObject();
|
|
|
+ LocalDate thirtyDaysAgo = now.minusDays(30);
|
|
|
+ LambdaQueryWrapper<EmotionCheckin> qw = new LambdaQueryWrapper<>();
|
|
|
+ qw.eq(EmotionCheckin::getChildId, memberId)
|
|
|
+ .ge(EmotionCheckin::getCreatedAt, java.sql.Timestamp.valueOf(thirtyDaysAgo.atStartOfDay()));
|
|
|
+ List<EmotionCheckin> emotions = emotionMapper.selectList(qw);
|
|
|
+ if (!emotions.isEmpty()) {
|
|
|
+ long total = emotions.size();
|
|
|
+ long joyCount = emotions.stream().filter(e -> "joy".equals(e.getEmotionType()) || "excited".equals(e.getEmotionType())).count();
|
|
|
+ m.put("emotion_joy_ratio", Math.round(joyCount / (double) total * 100) / 100.0);
|
|
|
+ long sadCount = emotions.stream().filter(e -> "sad".equals(e.getEmotionType())).count();
|
|
|
+ long angryCount = emotions.stream().filter(e -> "angry".equals(e.getEmotionType())).count();
|
|
|
+ m.put("negative_ratio", Math.round((sadCount + angryCount) / (double) total * 100) / 100.0);
|
|
|
+ double stressSum = emotions.stream().filter(e -> e.getStressLevel() != null).mapToDouble(e -> e.getStressLevel()).sum();
|
|
|
+ long stressCount = emotions.stream().filter(e -> e.getStressLevel() != null).count();
|
|
|
+ m.put("stress_avg", stressCount > 0 ? Math.round(stressSum / stressCount * 10) / 10.0 : 0);
|
|
|
+ double energySum = emotions.stream().filter(e -> e.getEnergyLevel() != null).mapToDouble(e -> e.getEnergyLevel()).sum();
|
|
|
+ long energyCount = emotions.stream().filter(e -> e.getEnergyLevel() != null).count();
|
|
|
+ m.put("energy_avg", energyCount > 0 ? Math.round(energySum / energyCount * 10) / 10.0 : 0);
|
|
|
+ double moodSum = emotions.stream().filter(e -> e.getMoodScore() != null).mapToDouble(e -> e.getMoodScore()).sum();
|
|
|
+ long moodCount = emotions.stream().filter(e -> e.getMoodScore() != null).count();
|
|
|
+ m.put("mood_score_avg", moodCount > 0 ? Math.round(moodSum / moodCount * 10) / 10.0 : 0);
|
|
|
+ m.put("emotion_records_count", total);
|
|
|
+ }
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONObject computeWisdomMetrics(Long memberId) {
|
|
|
+ JSONObject m = new JSONObject();
|
|
|
+ // 最近一次DAN测评
|
|
|
+ LambdaQueryWrapper<DanAssessmentResult> qw = new LambdaQueryWrapper<>();
|
|
|
+ qw.eq(DanAssessmentResult::getFamilyMemberId, memberId)
|
|
|
+ .isNotNull(DanAssessmentResult::getAssessmentDate)
|
|
|
+ .orderByDesc(DanAssessmentResult::getAssessmentDate)
|
|
|
+ .last("LIMIT 1");
|
|
|
+ DanAssessmentResult result = danMapper.selectOne(qw);
|
|
|
+ if (result != null) {
|
|
|
+ m.put("attention_score", result.getAttentionScore());
|
|
|
+ m.put("focus_score", result.getFocusScore());
|
|
|
+ m.put("memory_score", result.getMemoryScore());
|
|
|
+ m.put("logic_score", result.getLogicScore());
|
|
|
+ m.put("perception_score", result.getPerceptionScore());
|
|
|
+ m.put("spatial_score", result.getSpatialScore());
|
|
|
+ m.put("processing_speed_score", result.getProcessingSpeedScore());
|
|
|
+ m.put("overall_score", result.getOverallScore());
|
|
|
+ JSONObject bigFive = new JSONObject();
|
|
|
+ bigFive.put("openness", result.getOpennessScore());
|
|
|
+ bigFive.put("conscientiousness", result.getConscientiousnessScore());
|
|
|
+ bigFive.put("extraversion", result.getExtraversionScore());
|
|
|
+ bigFive.put("agreeableness", result.getAgreeablenessScore());
|
|
|
+ bigFive.put("neuroticism", result.getNeuroticismScore());
|
|
|
+ bigFive.put("overall", result.getBigFiveOverallScore());
|
|
|
+ m.put("big_five", bigFive);
|
|
|
+ JSONObject emi = new JSONObject();
|
|
|
+ emi.put("emotion_management", result.getEmotionManagementScore());
|
|
|
+ emi.put("empathy", result.getEmpathyScore());
|
|
|
+ emi.put("social_adaptability", result.getSocialAdaptabilityScore());
|
|
|
+ emi.put("self_motivation", result.getSelfMotivationScore());
|
|
|
+ m.put("emi", emi);
|
|
|
+ m.put("assessment_date", result.getAssessmentDate());
|
|
|
+ }
|
|
|
+ // 游戏分数(近7天)
|
|
|
+ LocalDate sevenDaysAgo = LocalDate.now().minusDays(7);
|
|
|
+ LambdaQueryWrapper<GameRecord> gqw = new LambdaQueryWrapper<>();
|
|
|
+ gqw.eq(GameRecord::getChildId, memberId)
|
|
|
+ .ge(GameRecord::getPlayedAt, java.sql.Timestamp.valueOf(sevenDaysAgo.atStartOfDay()));
|
|
|
+ List<GameRecord> games = gameMapper.selectList(gqw);
|
|
|
+ if (!games.isEmpty()) {
|
|
|
+ int avgScore = games.stream().mapToInt(g -> g.getScore() != null ? g.getScore() : 0).sum() / games.size();
|
|
|
+ m.put("game_avg_score_7d", avgScore);
|
|
|
+ m.put("game_count_7d", games.size());
|
|
|
+ }
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONObject computeActionMetrics(Long memberId, LocalDate now) {
|
|
|
+ JSONObject m = new JSONObject();
|
|
|
+ LocalDate sevenDaysAgo = now.minusDays(7);
|
|
|
+ Timestamp weekStart = java.sql.Timestamp.valueOf(sevenDaysAgo.atStartOfDay());
|
|
|
+
|
|
|
+ // 任务完成率
|
|
|
+ LambdaQueryWrapper<Task> tqw = new LambdaQueryWrapper<>();
|
|
|
+ tqw.and(w -> w.eq(Task::getExecutorId, memberId).or().eq(Task::getChildId, memberId))
|
|
|
+ .ge(Task::getCreatedAt, weekStart);
|
|
|
+ long totalTasks = taskMapper.selectCount(tqw);
|
|
|
+ LambdaQueryWrapper<Task> doneQw = new LambdaQueryWrapper<>();
|
|
|
+ doneQw.and(w -> w.eq(Task::getExecutorId, memberId).or().eq(Task::getChildId, memberId))
|
|
|
+ .eq(Task::getStatus, "completed")
|
|
|
+ .ge(Task::getCompletedAt, weekStart);
|
|
|
+ long doneTasks = taskMapper.selectCount(doneQw);
|
|
|
+ m.put("task_completion_rate", totalTasks > 0 ? Math.round(doneTasks / (double) totalTasks * 100) / 100.0 : 0);
|
|
|
+ m.put("task_total_7d", totalTasks);
|
|
|
+ m.put("task_done_7d", doneTasks);
|
|
|
+
|
|
|
+ // 打卡连续天数
|
|
|
+ m.put("checkin_streak", computeCheckinStreak(memberId));
|
|
|
+
|
|
|
+ // 积分速度
|
|
|
+ LambdaQueryWrapper<PointsLog> pqw = new LambdaQueryWrapper<>();
|
|
|
+ pqw.eq(PointsLog::getFamilyMemberId, memberId)
|
|
|
+ .gt(PointsLog::getAmount, 0)
|
|
|
+ .ge(PointsLog::getCreatedAt, weekStart);
|
|
|
+ List<PointsLog> pointsList = pointsLogMapper.selectList(pqw);
|
|
|
+ long pointsEarned = pointsList.stream().mapToInt(PointsLog::getAmount).sum();
|
|
|
+ m.put("points_velocity_7d", pointsEarned);
|
|
|
+
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONObject computeWealthMetrics(Long memberId, LocalDate now) {
|
|
|
+ JSONObject m = new JSONObject();
|
|
|
+ LocalDate thirtyDaysAgo = now.minusDays(30);
|
|
|
+ LambdaQueryWrapper<FinanceCheckin> qw = new LambdaQueryWrapper<>();
|
|
|
+ qw.eq(FinanceCheckin::getChildId, memberId)
|
|
|
+ .ge(FinanceCheckin::getCheckinDate, java.sql.Date.valueOf(thirtyDaysAgo));
|
|
|
+ List<FinanceCheckin> finances = financeMapper.selectList(qw);
|
|
|
+ if (!finances.isEmpty()) {
|
|
|
+ long income = finances.stream().filter(f -> "income".equals(f.getType())).count();
|
|
|
+ long expense = finances.stream().filter(f -> "expense".equals(f.getType())).count();
|
|
|
+ m.put("income_count", income);
|
|
|
+ m.put("expense_count", expense);
|
|
|
+ int totalIncome = finances.stream().filter(f -> "income".equals(f.getType()))
|
|
|
+ .mapToInt(f -> f.getAmount() != null ? f.getAmount() : 0).sum();
|
|
|
+ int totalExpense = finances.stream().filter(f -> "expense".equals(f.getType()))
|
|
|
+ .mapToInt(f -> f.getAmount() != null ? f.getAmount() : 0).sum();
|
|
|
+ m.put("savings_rate", totalIncome > 0 ? Math.round((totalIncome - totalExpense) / (double) totalIncome * 100) / 100.0 : 0);
|
|
|
+ }
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONObject computeDimensionScores(JSONObject body, JSONObject mind, JSONObject wisdom, JSONObject action, JSONObject wealth) {
|
|
|
+ JSONObject scores = new JSONObject();
|
|
|
+
|
|
|
+ // 身维度
|
|
|
+ int bodyScore = 50;
|
|
|
+ double sleepAvg = body.getDoubleValue("sleep_dur_avg", 0);
|
|
|
+ if (sleepAvg >= 8 && sleepAvg <= 10) bodyScore += 15;
|
|
|
+ else if (sleepAvg >= 7 && sleepAvg <= 11) bodyScore += 8;
|
|
|
+ int exerciseWeek = body.getIntValue("exercise_count_week", 0);
|
|
|
+ if (exerciseWeek >= 3) bodyScore += 15;
|
|
|
+ else if (exerciseWeek >= 1) bodyScore += 8;
|
|
|
+ int waterAvg = body.getIntValue("water_intake_avg_ml", 0);
|
|
|
+ if (waterAvg >= 1000) bodyScore += 10;
|
|
|
+ else if (waterAvg >= 500) bodyScore += 5;
|
|
|
+ scores.put("body", Math.min(bodyScore, 100));
|
|
|
+
|
|
|
+ // 心维度
|
|
|
+ int mindScore = 50;
|
|
|
+ double joyRatio = mind.getDoubleValue("emotion_joy_ratio", 0.5);
|
|
|
+ mindScore += (int) (joyRatio * 30);
|
|
|
+ double stressAvg = mind.getDoubleValue("stress_avg", 5);
|
|
|
+ mindScore -= (int) ((stressAvg - 3) * 5);
|
|
|
+ double negativeRatio = mind.getDoubleValue("negative_ratio", 0);
|
|
|
+ mindScore -= (int) (negativeRatio * 30);
|
|
|
+ scores.put("mind", Math.max(0, Math.min(100, mindScore)));
|
|
|
+
|
|
|
+ // 智维度
|
|
|
+ int wisdomScore = 50;
|
|
|
+ Integer overall = wisdom.getInteger("overall_score");
|
|
|
+ if (overall != null) wisdomScore = overall;
|
|
|
+ Integer gameAvg = wisdom.getInteger("game_avg_score_7d");
|
|
|
+ if (gameAvg != null) wisdomScore = Math.max(wisdomScore, gameAvg);
|
|
|
+ scores.put("wisdom", wisdomScore);
|
|
|
+
|
|
|
+ // 行维度
|
|
|
+ int actionScore = 50;
|
|
|
+ double compRate = action.getDoubleValue("task_completion_rate", 0.5);
|
|
|
+ actionScore += (int) (compRate * 30);
|
|
|
+ int streak = action.getIntValue("checkin_streak", 0);
|
|
|
+ actionScore += Math.min(streak, 10);
|
|
|
+ scores.put("action", Math.min(100, actionScore));
|
|
|
+
|
|
|
+ // 富维度
|
|
|
+ int wealthScore = 50;
|
|
|
+ double savingsRate = wealth.getDoubleValue("savings_rate", 0.5);
|
|
|
+ wealthScore += (int) (savingsRate * 30);
|
|
|
+ int financeCount = wealth.getIntValue("income_count", 0) + wealth.getIntValue("expense_count", 0);
|
|
|
+ if (financeCount >= 5) wealthScore += 20;
|
|
|
+ else if (financeCount >= 2) wealthScore += 10;
|
|
|
+ scores.put("wealth", Math.min(100, wealthScore));
|
|
|
+
|
|
|
+ return scores;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String computeProblemDomains(Long memberId) {
|
|
|
+ Set<String> domains = new LinkedHashSet<>();
|
|
|
+ LocalDate thirtyDaysAgo = LocalDate.now().minusDays(30);
|
|
|
+
|
|
|
+ // 睡眠问题
|
|
|
+ LambdaQueryWrapper<HealthSleepRecord> sqw = new LambdaQueryWrapper<>();
|
|
|
+ sqw.eq(HealthSleepRecord::getMemberId, memberId)
|
|
|
+ .ge(HealthSleepRecord::getCreatedAt, java.sql.Timestamp.valueOf(thirtyDaysAgo.atStartOfDay()))
|
|
|
+ .lt(HealthSleepRecord::getDurationMinutes, 480);
|
|
|
+ if (sleepMapper.selectCount(sqw) > 3) domains.add("sleep");
|
|
|
+
|
|
|
+ // 注意力问题
|
|
|
+ LambdaQueryWrapper<DanAssessmentResult> dwqw = new LambdaQueryWrapper<>();
|
|
|
+ dwqw.eq(DanAssessmentResult::getFamilyMemberId, memberId)
|
|
|
+ .isNotNull(DanAssessmentResult::getAttentionScore)
|
|
|
+ .lt(DanAssessmentResult::getAttentionScore, 60)
|
|
|
+ .orderByDesc(DanAssessmentResult::getAssessmentDate)
|
|
|
+ .last("LIMIT 1");
|
|
|
+ if (danMapper.selectCount(dwqw) > 0) domains.add("attention");
|
|
|
+
|
|
|
+ // 情绪问题
|
|
|
+ LambdaQueryWrapper<EmotionCheckin> eqw = new LambdaQueryWrapper<>();
|
|
|
+ eqw.eq(EmotionCheckin::getChildId, memberId)
|
|
|
+ .ge(EmotionCheckin::getCreatedAt, java.sql.Timestamp.valueOf(thirtyDaysAgo.atStartOfDay()))
|
|
|
+ .in(EmotionCheckin::getEmotionType, Arrays.asList("sad", "anxious", "angry"));
|
|
|
+ if (emotionMapper.selectCount(eqw) > 5) domains.add("emotion");
|
|
|
+
|
|
|
+ return domains.isEmpty() ? "[]" : JSON.toJSONString(new ArrayList<>(domains));
|
|
|
+ }
|
|
|
+
|
|
|
+ private int computeCheckinStreak(Long memberId) {
|
|
|
+ int streak = 0;
|
|
|
+ LocalDate today = LocalDate.now();
|
|
|
+ for (int i = 0; i < 60; i++) {
|
|
|
+ LocalDate d = today.minusDays(i);
|
|
|
+ Timestamp dayStart = java.sql.Timestamp.valueOf(d.atStartOfDay());
|
|
|
+ Timestamp dayEnd = java.sql.Timestamp.valueOf(d.plusDays(1).atStartOfDay());
|
|
|
+ boolean hasCheckin = false;
|
|
|
+
|
|
|
+ // sleep
|
|
|
+ LambdaQueryWrapper<HealthSleepRecord> sqw = new LambdaQueryWrapper<>();
|
|
|
+ sqw.eq(HealthSleepRecord::getMemberId, memberId)
|
|
|
+ .ge(HealthSleepRecord::getCreatedAt, dayStart).lt(HealthSleepRecord::getCreatedAt, dayEnd);
|
|
|
+ if (sleepMapper.selectCount(sqw) > 0) hasCheckin = true;
|
|
|
+
|
|
|
+ // exercise
|
|
|
+ if (!hasCheckin) {
|
|
|
+ LambdaQueryWrapper<HealthExerciseRecord> eqw = new LambdaQueryWrapper<>();
|
|
|
+ eqw.eq(HealthExerciseRecord::getMemberId, memberId)
|
|
|
+ .ge(HealthExerciseRecord::getCreatedAt, dayStart).lt(HealthExerciseRecord::getCreatedAt, dayEnd);
|
|
|
+ if (exerciseMapper.selectCount(eqw) > 0) hasCheckin = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // emotion
|
|
|
+ if (!hasCheckin) {
|
|
|
+ LambdaQueryWrapper<EmotionCheckin> emqw = new LambdaQueryWrapper<>();
|
|
|
+ emqw.eq(EmotionCheckin::getChildId, memberId)
|
|
|
+ .ge(EmotionCheckin::getCreatedAt, dayStart).lt(EmotionCheckin::getCreatedAt, dayEnd);
|
|
|
+ if (emotionMapper.selectCount(emqw) > 0) hasCheckin = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (hasCheckin) streak++;
|
|
|
+ else if (i > 0) break;
|
|
|
+ }
|
|
|
+ return streak;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void cleanOldHistory(Long memberId, LocalDate now) {
|
|
|
+ LocalDate cutoff = now.minusDays(365);
|
|
|
+ LambdaQueryWrapper<ProfileHistory> qw = new LambdaQueryWrapper<>();
|
|
|
+ qw.eq(ProfileHistory::getMemberId, memberId)
|
|
|
+ .lt(ProfileHistory::getSnapshotDate, java.sql.Date.valueOf(cutoff));
|
|
|
+ historyMapper.delete(qw);
|
|
|
+ }
|
|
|
+}
|