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

Merge remote-tracking branch 'origin/cfclub' into cfclub

E2E Test Bot 2 месяцев назад
Родитель
Сommit
0a66391a1e

+ 20 - 7
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java

@@ -1404,14 +1404,9 @@ public class HealthReportController {
             // 指定报告:返回该报告的食材推荐
             // 指定报告:返回该报告的食材推荐
             foods = foodService.listByReportId(reportId);
             foods = foodService.listByReportId(reportId);
         } else {
         } else {
-            // 未指定报告:返回当前用户最新报告的食材推荐(当前最适合)
+            // 未指定报告:返回维护的"当前食材"推荐(food_recommend_idx,有新报告时 upsert 更新)
             Long uid = userId != null ? userId : -1L;
             Long uid = userId != null ? userId : -1L;
-            HealthReport latest = healthReportService.getLatestReport(uid);
-            if (latest == null) {
-                foods = java.util.Collections.emptyList();
-            } else {
-                foods = foodService.listByReportId(latest.getId());
-            }
+            foods = foodRecommendService.getCurrentFoodsAsFoodList(uid);
         }
         }
         return Result.success(foods);
         return Result.success(foods);
     }
     }
@@ -1597,6 +1592,14 @@ public class HealthReportController {
 
 
         try {
         try {
             Map<String, Object> result = healthReportService.auditConfirm(draftId, subjectId, auditorId);
             Map<String, Object> result = healthReportService.auditConfirm(draftId, subjectId, auditorId);
+            Long confirmedSubjectId = ParamUtils.getLong(result.get("subjectId"));
+            if (confirmedSubjectId != null) {
+                try {
+                    foodRecommendService.recalculateForUser(confirmedSubjectId);
+                } catch (Exception e) {
+                    log.warn("审核确认后刷新食材推荐失败: {}", e.getMessage());
+                }
+            }
             return Result.success(result);
             return Result.success(result);
         } catch (Exception e) {
         } catch (Exception e) {
             log.error("审核确认失败", e);
             log.error("审核确认失败", e);
@@ -2219,6 +2222,16 @@ public class HealthReportController {
             Map<String, Object> result = healthReportService.auditConfirm(draftId, subjectId, userId);
             Map<String, Object> result = healthReportService.auditConfirm(draftId, subjectId, userId);
             Long reportId = (Long) result.get("reportId");
             Long reportId = (Long) result.get("reportId");
 
 
+            // 维护"当前食材"推荐(有则更新,无则添加)
+            Long confirmedSubjectId = ParamUtils.getLong(result.get("subjectId"));
+            if (confirmedSubjectId != null) {
+                try {
+                    foodRecommendService.recalculateForUser(confirmedSubjectId);
+                } catch (Exception e) {
+                    log.warn("代确认后刷新食材推荐失败: {}", e.getMessage());
+                }
+            }
+
             // 设置代上传信息
             // 设置代上传信息
             if (reportId != null) {
             if (reportId != null) {
                 healthReportService.setProxyUploader(reportId, userId);
                 healthReportService.setProxyUploader(reportId, userId);

+ 88 - 6
cfc-backend/src/main/java/com/etotem/cfc/service/FoodRecommendService.java

@@ -15,6 +15,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
 import javax.annotation.Resource;
+import java.math.BigDecimal;
 import java.util.*;
 import java.util.*;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 
 
@@ -50,14 +51,29 @@ public class FoodRecommendService {
             return;
             return;
         }
         }
 
 
-        // 从 foods 表读取最新报告关联的食材推荐(正常确认流程写入 foods 表)
+        // 优先从 foods 表读取(正常确认流程写入 foods 表);
+        // 若 foods 表无数据(审核路径只写 report_food_suitability),回退到 report_food_suitability
         List<Food> reportFoods = foodService.listByReportId(report.getId());
         List<Food> reportFoods = foodService.listByReportId(report.getId());
-
         if (reportFoods.isEmpty()) {
         if (reportFoods.isEmpty()) {
-            foodRecommendIndexMapper.delete(new LambdaQueryWrapper<FoodRecommendIndex>()
-                    .eq(FoodRecommendIndex::getUserId, userId));
-            log.info("报告{}无食材推荐数据,已清除用户{}的推荐指数", report.getId(), userId);
-            return;
+            List<ReportFoodSuitability> fallback = reportFoodSuitabilityMapper.selectList(
+                    new LambdaQueryWrapper<ReportFoodSuitability>()
+                            .eq(ReportFoodSuitability::getReportId, report.getId()));
+            if (fallback.isEmpty()) {
+                foodRecommendIndexMapper.delete(new LambdaQueryWrapper<FoodRecommendIndex>()
+                        .eq(FoodRecommendIndex::getUserId, userId));
+                log.info("报告{}无食材推荐数据,已清除用户{}的推荐指数", report.getId(), userId);
+                return;
+            }
+            // 回退:将 report_food_suitability 映射为 Food 列表后继续处理
+            reportFoods = new ArrayList<>();
+            for (ReportFoodSuitability rfs : fallback) {
+                Food f = new Food();
+                f.setId(rfs.getFoodId() != null ? rfs.getFoodId() : 0L);
+                f.setName(rfs.getFoodName());
+                f.setCategory(rfs.getCategory());
+                f.setScore(rfs.getScore());
+                reportFoods.add(f);
+            }
         }
         }
 
 
         List<FoodRecommendIndex> existingIndices = foodRecommendIndexMapper.selectList(
         List<FoodRecommendIndex> existingIndices = foodRecommendIndexMapper.selectList(
@@ -139,6 +155,72 @@ public class FoodRecommendService {
         );
         );
     }
     }
 
 
+    /**
+     * 获取用户"当前食材"推荐列表(含分类与营养信息,供饮食推荐页展示)。
+     * 数据来自 food_recommend_idx(有新报告时 recalculateForUser 有则更新/无则添加),
+     * join foods 表补充 category/protein 等字段。
+     */
+    public List<Map<String, Object>> getCurrentFoods(Long userId) {
+        List<FoodRecommendIndex> indices = getIndexByUser(userId);
+        if (indices.isEmpty()) return new ArrayList<>();
+
+        Set<Long> foodIds = new HashSet<>();
+        for (FoodRecommendIndex idx : indices) {
+            if (idx.getFoodId() != null) foodIds.add(idx.getFoodId());
+        }
+        Map<Long, Food> foodMap = new HashMap<>();
+        if (!foodIds.isEmpty()) {
+            List<Food> foods = foodService.listByIds(foodIds);
+            for (Food f : foods) {
+                if (f != null) foodMap.put(f.getId(), f);
+            }
+        }
+
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (FoodRecommendIndex idx : indices) {
+            Food food = foodMap.get(idx.getFoodId());
+            Map<String, Object> m = new LinkedHashMap<>();
+            m.put("name", food != null && food.getName() != null ? food.getName() : idx.getFoodName());
+            m.put("foodName", m.get("name"));
+            m.put("score", idx.getIndexScore());
+            m.put("category", food != null ? food.getCategory() : "");
+            if (food != null) {
+                m.put("protein", food.getProtein());
+                m.put("fat", food.getFat());
+                m.put("carbs", food.getCarbs());
+                m.put("fiber", food.getFiber());
+                m.put("energyKj", food.getEnergyKj());
+            }
+            result.add(m);
+        }
+        return result;
+    }
+
+    /** 将当前食材数据转换为 Food 实体列表,供 /api/health/foods 无 reportId 时使用 */
+    public List<Food> getCurrentFoodsAsFoodList(Long userId) {
+        List<Map<String, Object>> current = getCurrentFoods(userId);
+        List<Food> result = new ArrayList<>();
+        for (Map<String, Object> m : current) {
+            Food food = new Food();
+            food.setName((String) m.get("name"));
+            food.setCategory((String) m.get("category"));
+            Integer score = (Integer) m.get("score");
+            if (score != null) food.setScore(score);
+            BigDecimal protein = (BigDecimal) m.get("protein");
+            if (protein != null) food.setProtein(protein);
+            BigDecimal fat = (BigDecimal) m.get("fat");
+            if (fat != null) food.setFat(fat);
+            BigDecimal carbs = (BigDecimal) m.get("carbs");
+            if (carbs != null) food.setCarbs(carbs);
+            BigDecimal fiber = (BigDecimal) m.get("fiber");
+            if (fiber != null) food.setFiber(fiber);
+            BigDecimal energyKj = (BigDecimal) m.get("energyKj");
+            if (energyKj != null) food.setEnergyKj(energyKj);
+            result.add(food);
+        }
+        return result;
+    }
+
     public List<FoodRecommendIdxLog> getIndexHistory(Long userId, Long foodId) {
     public List<FoodRecommendIdxLog> getIndexHistory(Long userId, Long foodId) {
         return foodRecommendIdxLogMapper.selectList(
         return foodRecommendIdxLogMapper.selectList(
                 new LambdaQueryWrapper<FoodRecommendIdxLog>()
                 new LambdaQueryWrapper<FoodRecommendIdxLog>()

+ 5 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FoodService.java

@@ -183,4 +183,9 @@ public class FoodService {
         }
         }
         log.info("已保存{}条食材推荐到报告{}", foods.size(), reportId);
         log.info("已保存{}条食材推荐到报告{}", foods.size(), reportId);
     }
     }
+
+    public List<Food> listByIds(Collection<Long> ids) {
+        if (ids == null || ids.isEmpty()) return new ArrayList<>();
+        return foodMapper.selectBatchIds(ids);
+    }
 }
 }

+ 2 - 2
cfc-frontend/components/wuxing-sandbox.vue

@@ -9,8 +9,8 @@
     </view>
     </view>
   </slot>
   </slot>
 
 
-  <!-- 个人/全家切换开关(仅 familyUnlocked 时显示) -->
-  <view class="sandbox-toggle" v-if="familyUnlocked && hasMultipleMembers && mode !== 'preview'">
+  <!-- 个人/全家切换开关(有 2+ 成员时显示) -->
+  <view class="sandbox-toggle" v-if="hasMultipleMembers && mode !== 'preview'">
     <view class="toggle-item" :class="activeClassName" @click="onToggle('personal')">
     <view class="toggle-item" :class="activeClassName" @click="onToggle('personal')">
       <text class="toggle-icon">👤</text>
       <text class="toggle-icon">👤</text>
       <text class="toggle-text">个人</text>
       <text class="toggle-text">个人</text>

+ 8 - 4
cfc-frontend/pages/home-pages/child-index.vue

@@ -543,8 +543,12 @@ export default {
         }
         }
         this.nickname = (userInfo && userInfo.nickname) ? userInfo.nickname : '小朋友'
         this.nickname = (userInfo && userInfo.nickname) ? userInfo.nickname : '小朋友'
 
 
-        const childrenRes = await getChildren()
-        const children = childrenRes.data || []
+        const childrenRes = await getFamilyMemberList({ visibleOnly: true })
+        var allMembers = childrenRes.data || []
+        if (!Array.isArray(allMembers)) allMembers = []
+        // 同步设置 familyMembersVisible,避免 loadFamilyMembersVisible() 再调一次
+        this.familyMembersVisible = allMembers
+        const children = allMembers.filter(function(m) { return m.effectiveRole === 'child' || m.source !== 'parent' })
         if (children.length === 0) {
         if (children.length === 0) {
           this.hasChildren = false
           this.hasChildren = false
           this.loading = false
           this.loading = false
@@ -653,8 +657,8 @@ export default {
           console.log('获取沙盘数据失败', e)
           console.log('获取沙盘数据失败', e)
         }
         }
 
 
-        // 加载家庭成员
-        this.loadFamilyMembersVisible()
+        // 家庭成员已在上方 loadData 开头通过 getFamilyMemberList 一次获取,不再重复调用
+        // this.loadFamilyMembersVisible()
 
 
         // 加载维度活动
         // 加载维度活动
         try {
         try {

+ 9 - 13
cfc-frontend/pages/home-pages/parent-index.vue

@@ -737,22 +737,29 @@ export default {
             if (dashData.familyId) {
             if (dashData.familyId) {
               this._familyId = dashData.familyId
               this._familyId = dashData.familyId
             }
             }
+            // 从仪表盘同步设置 familyMembersVisible(含家长本人),避免后续 loadFamilyMembersVisible() 再调 getFamilyMemberList
+            var dashMembers = dashData.members || dashData.familyMembers || filteredChildren
+            this.familyMembersVisible = dashMembers
+            _membersVisibleSet = true
             dashboardLoaded = true
             dashboardLoaded = true
           }
           }
         } catch (e) {
         } catch (e) {
           console.log('获取仪表盘数据失败,降级到单独接口', e)
           console.log('获取仪表盘数据失败,降级到单独接口', e)
         }
         }
 
 
-        // 降级:使用原有单独接口
+        // 降级:使用原有单独接口(仅调用一次 getFamilyMemberList,合并 children 与 familyMembers 派生)
         if (!dashboardLoaded) {
         if (!dashboardLoaded) {
           try {
           try {
             const currentUserId = uni.getStorageSync('userId')
             const currentUserId = uni.getStorageSync('userId')
             const membersRes = await getFamilyMemberList({ visibleOnly: true, includeFamily: true })
             const membersRes = await getFamilyMemberList({ visibleOnly: true, includeFamily: true })
             const data = membersRes.data || {}
             const data = membersRes.data || {}
-            const members = data.members || []
+            var members = data.members || data || []
+            if (!Array.isArray(members)) members = []
             const parents = members.filter(function(p) { return p.source === 'parent' && p.id && p.id !== currentUserId }).map(function(p) { return { ...p, role: 'parent' } })
             const parents = members.filter(function(p) { return p.source === 'parent' && p.id && p.id !== currentUserId }).map(function(p) { return { ...p, role: 'parent' } })
             const children = members.filter(function(c) { return c.source !== 'parent' && c.id }).map(function(c) { return { ...c, role: 'child' } })
             const children = members.filter(function(c) { return c.source !== 'parent' && c.id }).map(function(c) { return { ...c, role: 'child' } })
             this.familyMembers = [].concat(parents, children)
             this.familyMembers = [].concat(parents, children)
+            // 从同一响应派生 children,避免再调 getChildren()(= getFamilyMemberList({}))
+            this.children = children
             // 同步设置 familyMembersVisible,避免 loadFamilyMembersVisible() 重复调用 getFamilyMemberList
             // 同步设置 familyMembersVisible,避免 loadFamilyMembersVisible() 重复调用 getFamilyMemberList
             this.familyMembersVisible = members
             this.familyMembersVisible = members
             _membersVisibleSet = true
             _membersVisibleSet = true
@@ -764,17 +771,6 @@ export default {
           } catch (e) {
           } catch (e) {
             console.log('获取家庭成员失败', e)
             console.log('获取家庭成员失败', e)
           }
           }
-
-          if (this.children.length === 0) {
-            try {
-              const childrenRes = await getChildren()
-              const rawChildren = childrenRes.data || []
-              this.children = rawChildren
-              // 不自动选孩子:默认显示家长自己,等待用户从 FamilyMemberStrip 选择
-            } catch (e) {
-              console.log('获取孩子列表失败', e)
-            }
-          }
         }
         }
 
 
         const reviewRes = await getPendingReviewTasks()
         const reviewRes = await getPendingReviewTasks()

+ 19 - 28
cfc-frontend/pages/index-home/index.vue

@@ -790,41 +790,32 @@ membershipDays: 0,
     },
     },
     loadFamilyFeed: function() {
     loadFamilyFeed: function() {
       var self = this
       var self = this
-      // 仅对孩子成员(effectiveRole === 'child')拉取任务历史,避免对家长也发起请求
+      // 仅对第一个孩子拉取任务历史(避免 N 个孩子 N 次 /api/tasks/history 请求)
       var children = (self.children || []).filter(function(m) { return m.effectiveRole === 'child' })
       var children = (self.children || []).filter(function(m) { return m.effectiveRole === 'child' })
       if (children.length === 0) {
       if (children.length === 0) {
         self.familyFeed = []
         self.familyFeed = []
         return
         return
       }
       }
-      var promises = children.map(function(child) {
-        return getTaskHistory(child.id, 1, 3).then(function(res) {
-          var tasks = []
-          if (res.code === 200) {
-            var data = res.data
-            tasks = (data && data.records) ? data.records : (Array.isArray(data) ? data : [])
+      var child = children[0]
+      getTaskHistory(child.id, 1, 3).then(function(res) {
+        var tasks = []
+        if (res.code === 200) {
+          var data = res.data
+          tasks = (data && data.records) ? data.records : (Array.isArray(data) ? data : [])
+        }
+        var feed = tasks.filter(function(t) { return t.status === 'completed' }).map(function(t) {
+          var points = t.pointsEarned || t.points || 0
+          var timeStr = ''
+          if (t.completedAt && typeof t.completedAt === 'string') {
+            timeStr = t.completedAt.substring(11, 16)
           }
           }
-          return tasks.filter(function(t) { return t.status === 'completed' }).map(function(t) {
-            var points = t.pointsEarned || t.points || 0
-            var timeStr = ''
-            if (t.completedAt && typeof t.completedAt === 'string') {
-              timeStr = t.completedAt.substring(11, 16)
-            }
-            return {
-              text: '✅ ' + (child.nickname || '小名') + ' 完成「' + (t.title || '任务') + '」 +' + points + '分',
-              time: timeStr,
-              type: 'task'
-            }
-          })
-        }).catch(function() { return [] })
-      })
-      Promise.all(promises).then(function(results) {
-        var all = []
-        for (var i = 0; i < results.length; i++) {
-          for (var j = 0; j < results[i].length; j++) {
-            all.push(results[i][j])
+          return {
+            text: '✅ ' + (child.nickname || '小名') + ' 完成「' + (t.title || '任务') + '」 +' + points + '分',
+            time: timeStr,
+            type: 'task'
           }
           }
-        }
-        self.familyFeed = all.slice(0, 10)
+        })
+        self.familyFeed = feed.slice(0, 10)
       }).catch(function() {
       }).catch(function() {
         self.familyFeed = []
         self.familyFeed = []
       })
       })

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-35ed064b9440fc86d8000b406a7c0f89bf1552a5
+d4ef373c2fc4b0b751673ea92e371f18e2f35172

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
 {
   "name": "cfc-web",
   "name": "cfc-web",
-  "version": "1.0.1285",
+  "version": "1.0.1287",
   "lockfileVersion": 3,
   "lockfileVersion": 3,
   "requires": true,
   "requires": true,
   "packages": {
   "packages": {
     "": {
     "": {
       "name": "cfc-web",
       "name": "cfc-web",
-      "version": "1.0.1285",
+      "version": "1.0.1287",
       "dependencies": {
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "cfc-web",
   "name": "cfc-web",
-  "version": "1.0.1286",
+  "version": "1.0.1288",
   "private": true,
   "private": true,
   "scripts": {
   "scripts": {
     "dev": "vue-cli-service serve",
     "dev": "vue-cli-service serve",

+ 22 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,28 @@
 
 
 ---
 ---
 
 
+## v1.0.1288 (2026-09-03)
+
+### Bug 修复
+- 移除能量沙盘切换开关的 familyUnlocked 门禁条件
+
+### 其他
+- 切换开关仅依赖 hasMultipleMembers(有 2+ 成员时显示),
+- familyUnlocked 仍作为 prop 保留,不影响 GateLock 门禁展示。
+- 
+
+
+## v1.0.1287 (2026-09-03)
+
+### Bug 修复
+- 根治 family/member/list 与 tasks/history 重复调用——index-home loadFamilyFeed 改为只查第一个孩子、parent-index/child-index 合并 getChildren+loadFamilyMembersVisible 为单次 getFamilyMemberList
+
+### 其他
+- - child-index: loadData 中 getChildren() 改为 getFamilyMemberList({visibleOnly:true}) 一次调用,同步设置 familyMembersVisible,跳过后续 loadFamilyMembersVisible()
+- - parent-index: dashboard 成功路径同步设置 familyMembersVisible + _membersVisibleSet,降级路径合并 getFamilyMemberList+getChildren 为单次调用并派生 children
+- 
+
+
 ## v1.0.1286 (2026-09-02)
 ## v1.0.1286 (2026-09-02)
 
 
 ### Bug 修复
 ### Bug 修复

+ 23 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 # 更新日志
 
 
-> 当前版本: v1.0.1286
+> 当前版本: v1.0.1288
 
 
 ## 历史版本
 ## 历史版本
 
 
@@ -8,6 +8,28 @@
 
 
 ---
 ---
 
 
+## v1.0.1288 (2026-09-03)
+
+### Bug 修复
+- 移除能量沙盘切换开关的 familyUnlocked 门禁条件
+
+### 其他
+- 切换开关仅依赖 hasMultipleMembers(有 2+ 成员时显示),
+- familyUnlocked 仍作为 prop 保留,不影响 GateLock 门禁展示。
+- 
+
+
+## v1.0.1287 (2026-09-03)
+
+### Bug 修复
+- 根治 family/member/list 与 tasks/history 重复调用——index-home loadFamilyFeed 改为只查第一个孩子、parent-index/child-index 合并 getChildren+loadFamilyMembersVisible 为单次 getFamilyMemberList
+
+### 其他
+- - child-index: loadData 中 getChildren() 改为 getFamilyMemberList({visibleOnly:true}) 一次调用,同步设置 familyMembersVisible,跳过后续 loadFamilyMembersVisible()
+- - parent-index: dashboard 成功路径同步设置 familyMembersVisible + _membersVisibleSet,降级路径合并 getFamilyMemberList+getChildren 为单次调用并派生 children
+- 
+
+
 ## v1.0.1286 (2026-09-02)
 ## v1.0.1286 (2026-09-02)
 
 
 ### Bug 修复
 ### Bug 修复