Просмотр исходного кода

perf(backend): batch queries in EnergyService to eliminate N+1 patterns

- calcTaskCompletionRate: single query with IN(category) instead of
  N queries (one per category) — eliminates N×categories DB roundtrips
- calcParentBody: batch-fetch health reports for all children via
  HealthReportMapper.selectOne with IN(userIds) instead of loop
Xiaogang Liao 2 месяцев назад
Родитель
Сommit
d912ebf7d4
1 измененных файлов с 32 добавлено и 26 удалено
  1. 32 26
      cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java

+ 32 - 26
cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java

@@ -51,6 +51,9 @@ public class EnergyService {
     @Resource
     private HealthReportService healthReportService;
 
+    @Resource
+    private HealthReportMapper healthReportMapper;
+
     @Resource
     private FamilyMemberAttributesMapper familyMemberAttributesMapper;
 
@@ -152,22 +155,31 @@ public class EnergyService {
     /**
      * 家长 身 — 运动健康类任务完成率
      * 注:家长如绑定了孩子(child ID),可通过该孩子的健康报告间接获取身体数据
+     * Optimized: batch-fetch all children's health scores in a single query
      */
     private int calcParentBody(User parent) {
-        // 尝试通过家庭中任意孩子的健康报告取分(家长关注的是家庭整体健康)
         if (parent.getFamilyId() != null) {
             List<Child> children = childMapper.selectList(
                     new LambdaQueryWrapper<Child>()
                             .eq(Child::getFamilyId, parent.getFamilyId())
             );
-            for (Child child : children) {
-                Integer healthScore = healthReportService.calculateBodyScore(child.getUserId());
-                if (healthScore != null) {
-                    return healthScore;
+            // Batch query: fetch health reports for ALL children at once
+            List<Long> childUserIds = children.stream()
+                    .map(Child::getUserId)
+                    .collect(Collectors.toList());
+            if (!childUserIds.isEmpty()) {
+                // Single query for all children — picks the first available health score
+                LambdaQueryWrapper<HealthReport> hrWrapper = new LambdaQueryWrapper<HealthReport>()
+                        .in(HealthReport::getUserId, childUserIds)
+                        .eq(HealthReport::getStatus, "active")
+                        .orderByDesc(HealthReport::getReportDate)
+                        .last("LIMIT 1");
+                HealthReport report = healthReportMapper.selectOne(hrWrapper);
+                if (report != null && report.getOverallScore() != null) {
+                    return report.getOverallScore();
                 }
             }
         }
-        // 降级:运动健康类任务完成率
         return calcTaskCompletionRate(parent.getId(), "parent",
                 Arrays.asList("运动", "体育", "户外", "健康"), 365);
     }
@@ -492,6 +504,7 @@ public class EnergyService {
 
     /**
      * 计算指定成员的任务完成率得分 (0-100)
+     * Optimized: single query for all categories instead of N queries per category
      *
      * @param memberId     executorId (家长=userId, 孩子=childId)
      * @param memberType   'parent' 或 'child'
@@ -504,26 +517,19 @@ public class EnergyService {
         cal.add(Calendar.DAY_OF_YEAR, -daysWindow);
         Date since = cal.getTime();
 
-        long total = 0;
-        long completed = 0;
-
-        for (String category : categories) {
-            LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
-                    .eq(Task::getExecutorType, memberType)
-                    .eq(Task::getExecutorId, memberId)
-                    .eq(Task::getCategory, category)
-                    .ne(Task::getIsTemplate, 1)
-                    .ne(Task::getStatus, "cancelled")
-                    .gt(Task::getCreatedAt, since);
-
-            List<Task> tasks = taskMapper.selectList(wrapper);
-            for (Task t : tasks) {
-                total++;
-                if ("completed".equals(t.getStatus())) {
-                    completed++;
-                }
-            }
-        }
+        // Single query for ALL categories — eliminates N+1
+        LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
+                .eq(Task::getExecutorType, memberType)
+                .eq(Task::getExecutorId, memberId)
+                .in(Task::getCategory, categories)
+                .ne(Task::getIsTemplate, 1)
+                .ne(Task::getStatus, "cancelled")
+                .gt(Task::getCreatedAt, since);
+
+        List<Task> tasks = taskMapper.selectList(wrapper);
+
+        long total = tasks.size();
+        long completed = tasks.stream().filter(t -> "completed".equals(t.getStatus())).count();
 
         if (total == 0) return 0;
         return (int) Math.round((double) completed / total * 100);