Quellcode durchsuchen

feat(body): 身页面新增肠道菌群指标卡片

- 后端新增 POST /api/health/gut-flora 接口,从最新菌群报告提取核心菌属/益生菌/有害菌
- 前端 body-detail 页面新增🦠肠道菌群区块:有报告时显示指标明细,无报告时引导上传
- 显示菌群名称、丰度值、状态(正常/偏高/偏低)及摘要提示
- 心/智页面已有肠脑轴数据展示,无需改动
iwt vor 2 Monaten
Ursprung
Commit
57ce472ef5

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

@@ -342,6 +342,26 @@ public class HealthReportController {
         }
     }
 
+    /**
+     * 获取成员最新菌群报告核心指标(用于身页面肠道菌群卡片)
+     */
+    @Operation(summary = "获取菌群指标")
+    @PostMapping("/gut-flora")
+    public Result<List<Map<String, Object>>> getGutFlora(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
+        if (memberId == null) return Result.error("memberId不能为空");
+        try {
+            List<Map<String, Object>> list = healthReportService.getGutFloraIndicators(memberId);
+            return Result.success(list);
+        } catch (Exception e) {
+            log.warn("获取菌群指标失败: {}", e.getMessage());
+            return Result.success(java.util.Collections.emptyList());
+        }
+    }
+
     /**
      * 获取孩子身体维度数据详情(健康指标解读)
      */

+ 38 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java

@@ -1686,4 +1686,42 @@ public class HealthReportService {
         }
         return result;
     }
+
+    /**
+     * 获取成员的最新菌群报告核心指标(用于身页面"肠道菌群"卡片)
+     * 返回:益生菌(core/probiotic)、有害菌(harmful)、核心菌属(taxonomy)各取前3条
+     */
+    public List<Map<String, Object>> getGutFloraIndicators(Long memberId) {
+        LambdaQueryWrapper<HealthReport> reportW = new LambdaQueryWrapper<>();
+        reportW.eq(HealthReport::getSubjectId, memberId)
+               .or().eq(HealthReport::getUserId, memberId);
+        reportW.eq(HealthReport::getReportType, "gut_flora")
+               .eq(HealthReport::getStatus, "active")
+               .orderByDesc(HealthReport::getCreatedAt)
+               .last("LIMIT 1");
+        HealthReport report = healthReportMapper.selectOne(reportW);
+        if (report == null) return Collections.emptyList();
+
+        LambdaQueryWrapper<HealthGutFlora> w = new LambdaQueryWrapper<>();
+        w.eq(HealthGutFlora::getReportId, report.getId())
+         .in(HealthGutFlora::getCategory, "core", "probiotic", "harmful", "other")
+         .orderByAsc(HealthGutFlora::getSortOrder);
+        List<HealthGutFlora> flora = healthGutFloraMapper.selectList(w);
+        if (flora.isEmpty()) return Collections.emptyList();
+
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (HealthGutFlora f : flora) {
+            Map<String, Object> item = new HashMap<>();
+            item.put("name", f.getBacteriaName());
+            item.put("value", f.getBacteriaValue());
+            item.put("status", f.getStatus());
+            item.put("category", f.getCategory());
+            item.put("populationLevel", f.getPopulationLevel());
+            item.put("detectionRate", f.getDetectionRate());
+            item.put("description", f.getDescription());
+            item.put("level", f.getLevel());
+            result.add(item);
+        }
+        return result;
+    }
 }

+ 69 - 2
cfc-frontend/pages/body-detail/index.vue

@@ -34,7 +34,7 @@
     </view>
 
     <!-- 身体指标卡片 -->
-    <view class="indicator-cards-section" v-if="isLoggedIn && bodyIndicators.length > 0">
+    <view class="indicator-cards-section" v-if="isLoggedIn && (bodyIndicators.length > 0 || gutFloraIndicators.length > 0)">
       <view class="section-header">
         <text class="section-title">📊 身体指标</text>
         <text class="section-more" @click="goToBodyDetail">更多 ›</text>
@@ -64,6 +64,40 @@
       </view>
     </view>
 
+    <!-- 肠道菌群卡片(有菌群报告时显示) -->
+    <view class="gut-section" v-if="isLoggedIn">
+      <view class="section-header">
+        <text class="section-title">🦠 肠道菌群</text>
+        <text class="section-more" @click="goToBodyDetail">更多 ›</text>
+      </view>
+      <view class="gut-summary" v-if="gutSummary && gutFloraIndicators.length > 0">
+        <text class="gut-summary-text">{{ gutSummary }}</text>
+      </view>
+      <view class="gut-groups" v-if="gutFloraIndicators.length > 0">
+        <view class="gut-group" v-for="group in gutGrouped" :key="group.category">
+          <text class="gut-group-title">{{ group.title }}</text>
+          <view class="gut-items">
+            <view
+              v-for="(ind, i) in group.items"
+              :key="i"
+              class="gut-item"
+              :class="{ 'gut-abnormal': ind.status === '偏高' || ind.status === '偏低' }"
+            >
+              <text class="gut-item-name">{{ ind.name }}</text>
+              <view class="gut-item-right">
+                <text class="gut-item-value" v-if="ind.value">{{ ind.value }}</text>
+                <text class="gut-item-status" :class="'status-' + (ind.status || 'normal')">{{ ind.status || '正常' }}</text>
+              </view>
+            </view>
+          </view>
+        </view>
+      </view>
+      <view class="gut-empty-hint" v-if="!hasGutReport">
+        <text>上传菌群检测报告,查看肠道健康指标</text>
+        <text class="gut-upload-link" @click="goToUploadReport">去上传 →</text>
+      </view>
+    </view>
+
     <!-- 家庭成员选择条 -->
     <FamilyMemberStrip
       v-if="currentView === 'self' && isLoggedIn"
@@ -113,7 +147,7 @@ import RadarChart from '../../components/RadarChart.vue'
 import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
-import { getVisibleSections, getEnergyOverview, getChildren, getFamilyEnergySandbox, getEnergySandbox, getFamilyMemberList, getDimensionOverview } from '../../utils/api.js'
+import { getVisibleSections, getEnergyOverview, getChildren, getFamilyEnergySandbox, getEnergySandbox, getFamilyMemberList, getDimensionOverview, getGutFloraIndicators } from '../../utils/api.js'
 
 export default {
   components: { TabTransition, PageBanner, FamilyEnergyBar, RadarChart, FloatingAvatar, FamilyMemberStrip, DimensionIntroCard },
@@ -133,6 +167,8 @@ export default {
       membersChecked: false,
             healthSubDims: [],
       bodyIndicators: [],
+      gutFloraIndicators: [],
+      hasGutReport: false,
       sandboxData: null,
       currentView: 'self', // 'self' | 'family'
       dualDimension: null,
@@ -189,6 +225,29 @@ export default {
       }
       return result
     },
+    gutGrouped: function() {
+      var groups = {}
+      var order = ['core', 'probiotic', 'harmful', 'other']
+      var titles = { core: '核心菌属', probiotic: '益生菌', harmful: '有害菌', other: '其他重要菌属' }
+      for (var i = 0; i < this.gutFloraIndicators.length; i++) {
+        var ind = this.gutFloraIndicators[i]
+        var cat = ind.category || 'other'
+        if (!groups[cat]) groups[cat] = { category: cat, title: titles[cat] || cat, items: [] }
+        groups[cat].items.push(ind)
+      }
+      var result = []
+      for (var j = 0; j < order.length; j++) {
+        if (groups[order[j]]) result.push(groups[order[j]])
+      }
+      return result
+    },
+    gutSummary: function() {
+      if (!this.gutFloraIndicators.length) return ''
+      var abnormal = this.gutFloraIndicators.filter(function(i) { return i.status === '偏高' || i.status === '偏低' }).length
+      if (abnormal === 0) return '菌群整体平衡良好'
+      if (abnormal <= 2) return '部分菌群指标略有波动'
+      return '多项菌群指标异常,建议咨询专业营养师'
+    },
     /** 每月引导语 */
     monthlyGuidance: function() {
       var month = new Date().getMonth()
@@ -303,6 +362,8 @@ export default {
       })
       // 加载身体指标列表
       self.loadBodyIndicators(memberId)
+      // 加载菌群指标
+      self.loadGutFlora(memberId)
     },
     loadBodyIndicators: function(memberId) {
       var self = this
@@ -314,6 +375,12 @@ export default {
         console.log('[body] loadBodyIndicators failed', e)
       })
     },
+    loadGutFlora: function(memberId) {
+      var self = this
+      getGutFloraIndicators({ memberId: memberId }).then(function(res) {
+        if (res.code === 200) { self.gutFloraIndicators = res.data || []; self.hasGutReport = (res.data && res.data.length > 0) }
+      }).catch(function(e) { console.log('[body] loadGutFlora failed', e) })
+    },
     loadSandboxData: function() {
       var self = this
       getFamilyEnergySandbox().then(function(res) {

+ 1 - 0
cfc-frontend/utils/api.js

@@ -1930,6 +1930,7 @@ export const getProductsByDomain = (domain, page, size) => {
 export const getLatestHealthReport = (memberId) => request('/api/health/report/latest', 'POST', { memberId })
 export const getHealthIndicatorList = (memberId) => request('/api/health/indicator/list', 'POST', { memberId })
 export const getLatestIndicators = (data) => request('/api/health/indicators', 'POST', data)
+export const getGutFloraIndicators = (data) => request('/api/health/gut-flora', 'POST', data)
 
 
 export const createHealthReport = (data) => request('/api/health/report/create', 'POST', data)