Ver Fonte

feat(health): implement health questionnaire with 9.9 membership gate, health_status unlock, diet questionnaire integration; fix audit error; add diet_structure fields and lifestyle step; final verification passed

Sisyphus há 1 semana atrás
pai
commit
6a400ef9f8

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -3059,6 +3059,12 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
 
 
         // 迁移374: 创建 user_content_weights 表(用户内容偏好权重表)
         // 迁移374: 创建 user_content_weights 表(用户内容偏好权重表)
         addUserContentWeightsTable();
         addUserContentWeightsTable();
+
+        // 迁移376: health_status 添加 lifestyle JSON 列(心情/睡眠/排便/抗生素/过敏表现)
+        ensureColumn("health_status", "lifestyle", "JSON DEFAULT NULL COMMENT '生活方式与肠道健康 {mood,sleepDuration,bedtime,bowelFrequency,bowelForm,antibioticHistory,allergySymptoms}'");
+
+        // 迁移377: diet_preferences 添加 diet_structure JSON 列(蔬果量/乳制品/含糖饮料频率)
+        ensureColumn("diet_preferences", "diet_structure", "JSON DEFAULT NULL COMMENT '饮食结构现状 {vegetableFruit,dairy,sugaryDrinks}'");
     }
     }
 
 
     private void migrateHealthDimensionsTable() {
     private void migrateHealthDimensionsTable() {

+ 2 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthStatusController.java

@@ -62,6 +62,7 @@ public class HealthStatusController {
         if (params.get("diseaseHistory") != null) status.setDiseaseHistory(params.get("diseaseHistory").toString());
         if (params.get("diseaseHistory") != null) status.setDiseaseHistory(params.get("diseaseHistory").toString());
         if (params.get("medications") != null) status.setMedications(params.get("medications").toString());
         if (params.get("medications") != null) status.setMedications(params.get("medications").toString());
         if (params.get("notes") != null) status.setNotes(params.get("notes").toString());
         if (params.get("notes") != null) status.setNotes(params.get("notes").toString());
+        if (params.get("lifestyle") != null) status.setLifestyle(params.get("lifestyle").toString());
         if (isEmpty(status)) {
         if (isEmpty(status)) {
             return Result.error("请至少填写一项");
             return Result.error("请至少填写一项");
         }
         }
@@ -79,7 +80,7 @@ public class HealthStatusController {
         return s.getHeightCm() == null && s.getWeightKg() == null
         return s.getHeightCm() == null && s.getWeightKg() == null
                 && s.getBloodPressure() == null && s.getBloodGlucose() == null
                 && s.getBloodPressure() == null && s.getBloodGlucose() == null
                 && s.getBloodLipids() == null && s.getDiseaseHistory() == null
                 && s.getBloodLipids() == null && s.getDiseaseHistory() == null
-                && s.getMedications() == null && s.getNotes() == null;
+                && s.getMedications() == null && s.getNotes() == null && s.getLifestyle() == null;
     }
     }
 
 
     /** 数值校验,返回错误信息或 null */
     /** 数值校验,返回错误信息或 null */

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/DietPreferencesDTO.java

@@ -19,4 +19,13 @@ public class DietPreferencesDTO {
     private Integer goalConfirmed;
     private Integer goalConfirmed;
     private String filledAt;
     private String filledAt;
     private Boolean noPreference;
     private Boolean noPreference;
+
+    /** 每日蔬果量(单选,如 '<2份'/'2-3份'/'4份以上') */
+    private String vegetableFruit;
+
+    /** 乳制品频率(单选,如 '每天'/'每周'/'很少'/'不喝') */
+    private String dairy;
+
+    /** 含糖饮料频率(单选,如 '几乎不喝'/'每周1-2次'/'每天1次以上') */
+    private String sugaryDrinks;
 }
 }

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/DietPreferences.java

@@ -25,6 +25,10 @@ public class DietPreferences implements Serializable {
     private String goalSource;
     private String goalSource;
     private Integer goalConfirmed;
     private Integer goalConfirmed;
     private Boolean noPreference;
     private Boolean noPreference;
+
+    /** 饮食结构现状:{vegetableFruit, dairy, sugaryDrinks} */
+    private String dietStructure;
+
     private Date filledAt;
     private Date filledAt;
     private Date updatedAt;
     private Date updatedAt;
 }
 }

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthStatus.java

@@ -46,6 +46,9 @@ public class HealthStatus implements Serializable {
     /** 其他补充说明 */
     /** 其他补充说明 */
     private String notes;
     private String notes;
 
 
+    /** 生活方式与肠道健康:{mood, sleepDuration, bedtime, bowelFrequency, bowelForm, antibioticHistory, allergySymptoms} */
+    private String lifestyle;
+
     /** 首次填写时间 */
     /** 首次填写时间 */
     private Date filledAt;
     private Date filledAt;
 
 

+ 21 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/DietPreferencesService.java

@@ -57,7 +57,16 @@ public class DietPreferencesService {
         entity.setGoalSource(dto.getGoalSource());
         entity.setGoalSource(dto.getGoalSource());
         entity.setGoalConfirmed(dto.getGoalConfirmed() != null ? dto.getGoalConfirmed() : 0);
         entity.setGoalConfirmed(dto.getGoalConfirmed() != null ? dto.getGoalConfirmed() : 0);
         entity.setNoPreference(dto.getNoPreference() != null ? dto.getNoPreference() : false);
         entity.setNoPreference(dto.getNoPreference() != null ? dto.getNoPreference() : false);
-        
+
+        // 饮食结构现状:{vegetableFruit, dairy, sugaryDrinks}
+        java.util.Map<String, Object> dietStructureMap = new java.util.LinkedHashMap<>();
+        if (dto.getVegetableFruit() != null) dietStructureMap.put("vegetableFruit", dto.getVegetableFruit());
+        if (dto.getDairy() != null) dietStructureMap.put("dairy", dto.getDairy());
+        if (dto.getSugaryDrinks() != null) dietStructureMap.put("sugaryDrinks", dto.getSugaryDrinks());
+        if (!dietStructureMap.isEmpty()) {
+            entity.setDietStructure(toJSON(dietStructureMap));
+        }
+
         if (entity.getId() == null) {
         if (entity.getId() == null) {
             dietPreferencesMapper.insert(entity);
             dietPreferencesMapper.insert(entity);
         } else {
         } else {
@@ -90,6 +99,17 @@ public class DietPreferencesService {
         dto.setGoalConfirmed(entity.getGoalConfirmed());
         dto.setGoalConfirmed(entity.getGoalConfirmed());
         dto.setNoPreference(entity.getNoPreference());
         dto.setNoPreference(entity.getNoPreference());
         dto.setFilledAt(entity.getFilledAt() != null ? entity.getFilledAt().toString() : null);
         dto.setFilledAt(entity.getFilledAt() != null ? entity.getFilledAt().toString() : null);
+        // diet_structure JSON → {vegetableFruit, dairy, sugaryDrinks}
+        if (entity.getDietStructure() != null) {
+            try {
+                java.util.Map<String, Object> dsMap = objectMapper.readValue(entity.getDietStructure(), java.util.Map.class);
+                dto.setVegetableFruit(dsMap != null && dsMap.get("vegetableFruit") != null ? String.valueOf(dsMap.get("vegetableFruit")) : null);
+                dto.setDairy(dsMap != null && dsMap.get("dairy") != null ? String.valueOf(dsMap.get("dairy")) : null);
+                dto.setSugaryDrinks(dsMap != null && dsMap.get("sugaryDrinks") != null ? String.valueOf(dsMap.get("sugaryDrinks")) : null);
+            } catch (Exception e) {
+                log.warn("diet_structure 反序列化失败: {}", e.getMessage());
+            }
+        }
         return dto;
         return dto;
     }
     }
 
 

+ 2 - 0
cfc-backend/src/main/resources/schema.sql

@@ -4737,6 +4737,7 @@ CREATE TABLE IF NOT EXISTS diet_preferences (
     goal_source VARCHAR(20) DEFAULT NULL COMMENT 'manual/system_recommend',
     goal_source VARCHAR(20) DEFAULT NULL COMMENT 'manual/system_recommend',
     goal_confirmed TINYINT(1) DEFAULT 0 COMMENT '用户已确认',
     goal_confirmed TINYINT(1) DEFAULT 0 COMMENT '用户已确认',
     no_preference TINYINT(1) DEFAULT 0 COMMENT '无特殊偏好标记(1=用户明确选择无特殊偏好)',
     no_preference TINYINT(1) DEFAULT 0 COMMENT '无特殊偏好标记(1=用户明确选择无特殊偏好)',
+    diet_structure JSON DEFAULT NULL COMMENT '饮食结构现状 {vegetableFruit,dairy,sugaryDrinks}',
     filled_at TIMESTAMP NULL COMMENT '首次填写时间',
     filled_at TIMESTAMP NULL COMMENT '首次填写时间',
     updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     UNIQUE KEY uk_member (family_member_id),
     UNIQUE KEY uk_member (family_member_id),
@@ -5358,6 +5359,7 @@ CREATE TABLE IF NOT EXISTS health_status (
     disease_history JSON DEFAULT NULL COMMENT '疾病史 [{name,note}]',
     disease_history JSON DEFAULT NULL COMMENT '疾病史 [{name,note}]',
     medications JSON DEFAULT NULL COMMENT '在服药物/治疗 [{name,note}]',
     medications JSON DEFAULT NULL COMMENT '在服药物/治疗 [{name,note}]',
     notes VARCHAR(500) DEFAULT NULL COMMENT '其他补充说明',
     notes VARCHAR(500) DEFAULT NULL COMMENT '其他补充说明',
+    lifestyle JSON DEFAULT NULL COMMENT '生活方式与肠道健康 {mood,sleepDuration,bedtime,bowelFrequency,bowelForm,antibioticHistory,allergySymptoms}',
     filled_at DATETIME DEFAULT NULL COMMENT '首次填写时间',
     filled_at DATETIME DEFAULT NULL COMMENT '首次填写时间',
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
     INDEX idx_user_member (user_id, member_id)
     INDEX idx_user_member (user_id, member_id)

+ 41 - 7
cfc-frontend/pages/health/components/ConfirmStep.vue

@@ -56,6 +56,22 @@
       </view>
       </view>
     </view>
     </view>
 
 
+    <!-- 生活方式与肠道健康 -->
+    <view class="form-card" v-if="hasLifestyle">
+      <view class="card-head">
+        <text class="card-title">生活方式与肠道健康</text>
+      </view>
+      <view class="preview-row" v-if="healthStatus.mood"><text class="preview-label">心情</text><text class="preview-value">{{ healthStatus.mood }}</text></view>
+      <view class="preview-row" v-if="healthStatus.sleepDuration"><text class="preview-label">睡眠时长</text><text class="preview-value">{{ healthStatus.sleepDuration }}</text></view>
+      <view class="preview-row" v-if="healthStatus.bedtime"><text class="preview-label">入睡时间</text><text class="preview-value">{{ healthStatus.bedtime }}</text></view>
+      <view class="preview-row" v-if="healthStatus.bowelFrequency"><text class="preview-label">排便频率</text><text class="preview-value">{{ healthStatus.bowelFrequency }}</text></view>
+      <view class="preview-row" v-if="healthStatus.bowelForm"><text class="preview-label">粪便性状</text><text class="preview-value">{{ healthStatus.bowelForm }}</text></view>
+      <view class="preview-row" v-if="healthStatus.antibioticHistory"><text class="preview-label">近2年抗生素</text><text class="preview-value">{{ healthStatus.antibioticHistory }}</text></view>
+      <view class="preview-row" v-if="healthStatus.allergySymptoms && healthStatus.allergySymptoms.length > 0">
+        <text class="preview-label">过敏表现</text><text class="preview-value">{{ healthStatus.allergySymptoms.join('、') }}</text>
+      </view>
+    </view>
+
     <!-- 饮食习惯 -->
     <!-- 饮食习惯 -->
     <view class="form-card">
     <view class="form-card">
       <view class="card-head">
       <view class="card-head">
@@ -85,6 +101,18 @@
         <text class="preview-label">健康目标</text>
         <text class="preview-label">健康目标</text>
         <text class="preview-value">{{ dietPreferences.healthGoals.join('、') }}</text>
         <text class="preview-value">{{ dietPreferences.healthGoals.join('、') }}</text>
       </view>
       </view>
+      <view class="preview-row" v-if="dietPreferences.vegetableFruit">
+        <text class="preview-label">每日蔬果量</text>
+        <text class="preview-value">{{ dietPreferences.vegetableFruit }}</text>
+      </view>
+      <view class="preview-row" v-if="dietPreferences.dairy">
+        <text class="preview-label">乳制品</text>
+        <text class="preview-value">{{ dietPreferences.dairy }}</text>
+      </view>
+      <view class="preview-row" v-if="dietPreferences.sugaryDrinks">
+        <text class="preview-label">含糖饮料</text>
+        <text class="preview-value">{{ dietPreferences.sugaryDrinks }}</text>
+      </view>
       <text class="empty-hint" v-if="!hasDiet && noPreference">无特殊偏好</text>
       <text class="empty-hint" v-if="!hasDiet && noPreference">无特殊偏好</text>
       <text class="empty-hint" v-else-if="!hasDiet">未填写饮食习惯</text>
       <text class="empty-hint" v-else-if="!hasDiet">未填写饮食习惯</text>
     </view>
     </view>
@@ -96,13 +124,13 @@
         {{ submitting ? '保存中...' : '保存并生成方案' }}
         {{ submitting ? '保存中...' : '保存并生成方案' }}
       </button>
       </button>
     </view>
     </view>
-  </view>
-</template>
+   </view>
+ </template>
 
 
-<script>
-export default {
-  name: 'ConfirmStep',
-  props: {
+ <script>
+ export default {
+   name: 'ConfirmStep',
+   props: {
     healthStatus: {
     healthStatus: {
       type: Object,
       type: Object,
       default: function() { return {} }
       default: function() { return {} }
@@ -124,6 +152,11 @@ export default {
     hasMeds: function() {
     hasMeds: function() {
       return this.medRows.length > 0
       return this.medRows.length > 0
     },
     },
+    hasLifestyle: function() {
+      var hs = this.healthStatus || {}
+      return hs.mood || hs.sleepDuration || hs.bedtime || hs.bowelFrequency || hs.bowelForm || hs.antibioticHistory ||
+        (hs.allergySymptoms && hs.allergySymptoms.length > 0)
+    },
     hasDiet: function() {
     hasDiet: function() {
       var d = this.dietPreferences
       var d = this.dietPreferences
       return !!(d &&
       return !!(d &&
@@ -131,7 +164,8 @@ export default {
         (d.absoluteAvoid && d.absoluteAvoid.length > 0) ||
         (d.absoluteAvoid && d.absoluteAvoid.length > 0) ||
         (d.mustEat && d.mustEat.length > 0) ||
         (d.mustEat && d.mustEat.length > 0) ||
         (d.flavorPref && d.flavorPref.length > 0) ||
         (d.flavorPref && d.flavorPref.length > 0) ||
-        (d.healthGoals && d.healthGoals.length > 0)))
+        (d.healthGoals && d.healthGoals.length > 0) ||
+        d.vegetableFruit || d.dairy || d.sugaryDrinks))
     },
     },
     noPreference: function() {
     noPreference: function() {
       return !!(this.dietPreferences && this.dietPreferences.noPreference)
       return !!(this.dietPreferences && this.dietPreferences.noPreference)

+ 57 - 3
cfc-frontend/pages/health/components/DietHabitsStep.vue

@@ -14,6 +14,41 @@
       >{{ inner.noPreference ? '已选:无特殊偏好' : '无特殊偏好,直接下一步' }}</button>
       >{{ inner.noPreference ? '已选:无特殊偏好' : '无特殊偏好,直接下一步' }}</button>
     </view>
     </view>
 
 
+    <!-- 饮食结构现状:蔬果量/乳制品/含糖饮料频率 -->
+    <view class="form-card">
+      <view class="card-head">
+        <text class="card-title">饮食结构现状</text>
+        <text class="card-hint">单选</text>
+      </view>
+      <view class="struct-row">
+        <text class="struct-label">每日蔬果量</text>
+        <view class="tag-wrap">
+          <view v-for="opt in vegFruitOptions" :key="opt" class="tag-item"
+                :class="{ 'tag-active': inner.vegetableFruit === opt }" @click="setSingle('vegetableFruit', opt)">
+            <text>{{ opt }}</text>
+          </view>
+        </view>
+      </view>
+      <view class="struct-row">
+        <text class="struct-label">乳制品</text>
+        <view class="tag-wrap">
+          <view v-for="opt in dairyOptions" :key="opt" class="tag-item"
+                :class="{ 'tag-active': inner.dairy === opt }" @click="setSingle('dairy', opt)">
+            <text>{{ opt }}</text>
+          </view>
+        </view>
+      </view>
+      <view class="struct-row">
+        <text class="struct-label">含糖饮料频率</text>
+        <view class="tag-wrap">
+          <view v-for="opt in sugaryDrinkOptions" :key="opt" class="tag-item"
+                :class="{ 'tag-active': inner.sugaryDrinks === opt }" @click="setSingle('sugaryDrinks', opt)">
+            <text>{{ opt }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
     <!-- 过敏原 -->
     <!-- 过敏原 -->
     <view class="form-card">
     <view class="form-card">
       <view class="card-head">
       <view class="card-head">
@@ -221,7 +256,10 @@ export default {
         cuisinePref: [],
         cuisinePref: [],
         cookingMethods: [],
         cookingMethods: [],
         healthGoals: [],
         healthGoals: [],
-        noPreference: false
+        noPreference: false,
+        vegetableFruit: '',
+        dairy: '',
+        sugaryDrinks: ''
       },
       },
       customInput: '',
       customInput: '',
       spiceLabels: ['不辣', '微辣', '中辣', '偏辣', '很辣', '特辣']
       spiceLabels: ['不辣', '微辣', '中辣', '偏辣', '很辣', '特辣']
@@ -242,7 +280,10 @@ export default {
         { value: 'vegetarian', label: '素食' },
         { value: 'vegetarian', label: '素食' },
         { value: 'vegan', label: '纯素' }
         { value: 'vegan', label: '纯素' }
       ]
       ]
-    }
+    },
+    vegFruitOptions: function() { return ['<2份', '2-3份', '4份以上'] },
+    dairyOptions: function() { return ['每天', '每周3-5次', '很少', '不喝'] },
+    sugaryDrinkOptions: function() { return ['几乎不喝', '每周1-2次', '每周3-5次', '每天1次以上'] }
   },
   },
   watch: {
   watch: {
     value: {
     value: {
@@ -266,6 +307,9 @@ export default {
       self.inner.religiousDiet = val.religiousDiet || 'none'
       self.inner.religiousDiet = val.religiousDiet || 'none'
       self.inner.spiceLevel = val.spiceLevel != null ? val.spiceLevel : 0
       self.inner.spiceLevel = val.spiceLevel != null ? val.spiceLevel : 0
       self.inner.noPreference = !!val.noPreference
       self.inner.noPreference = !!val.noPreference
+      self.inner.vegetableFruit = val.vegetableFruit || ''
+      self.inner.dairy = val.dairy || ''
+      self.inner.sugaryDrinks = val.sugaryDrinks || ''
     },
     },
     emitInput: function() {
     emitInput: function() {
       this.$emit('input', {
       this.$emit('input', {
@@ -278,9 +322,16 @@ export default {
         cuisinePref: this.inner.cuisinePref.slice(),
         cuisinePref: this.inner.cuisinePref.slice(),
         cookingMethods: this.inner.cookingMethods.slice(),
         cookingMethods: this.inner.cookingMethods.slice(),
         healthGoals: this.inner.healthGoals.slice(),
         healthGoals: this.inner.healthGoals.slice(),
-        noPreference: this.inner.noPreference
+        noPreference: this.inner.noPreference,
+        vegetableFruit: this.inner.vegetableFruit,
+        dairy: this.inner.dairy,
+        sugaryDrinks: this.inner.sugaryDrinks
       })
       })
     },
     },
+    setSingle: function(field, value) {
+      this.inner[field] = (this.inner[field] === value) ? '' : value
+      this.emitInput()
+    },
     toggleChip: function(field, value) {
     toggleChip: function(field, value) {
       var list = this.inner[field]
       var list = this.inner[field]
       if (!list) return
       if (!list) return
@@ -325,6 +376,9 @@ export default {
       this.inner.cuisinePref = []
       this.inner.cuisinePref = []
       this.inner.cookingMethods = []
       this.inner.cookingMethods = []
       this.inner.healthGoals = []
       this.inner.healthGoals = []
+      this.inner.vegetableFruit = ''
+      this.inner.dairy = ''
+      this.inner.sugaryDrinks = ''
       this.inner.noPreference = true
       this.inner.noPreference = true
       this.emitInput()
       this.emitInput()
       this.$emit('skip-no-preference')
       this.$emit('skip-no-preference')

+ 40 - 19
cfc-frontend/pages/health/components/HealthQuestionnaireWizard.vue

@@ -34,8 +34,17 @@
         />
         />
       </view>
       </view>
 
 
-      <!-- Step 3: 饮食习惯 -->
+      <!-- Step 3: 生活方式与肠道健康 -->
       <view v-if="currentStep === 3">
       <view v-if="currentStep === 3">
+        <lifestyle-step
+          ref="stepRef"
+          :value="currentMemberForm"
+          @input="onLifestyleInput"
+        />
+      </view>
+
+      <!-- Step 4: 饮食习惯 -->
+      <view v-if="currentStep === 4">
         <diet-habits-step
         <diet-habits-step
           ref="stepRef"
           ref="stepRef"
           :value="currentMemberDiet"
           :value="currentMemberDiet"
@@ -44,8 +53,8 @@
         />
         />
       </view>
       </view>
 
 
-      <!-- Step 4: 确认 -->
-      <view v-if="currentStep === 4">
+      <!-- Step 5: 确认 -->
+      <view v-if="currentStep === 5">
         <confirm-step
         <confirm-step
           ref="stepRef"
           ref="stepRef"
           :health-status="currentMemberForm"
           :health-status="currentMemberForm"
@@ -58,7 +67,7 @@
     </view>
     </view>
 
 
     <!-- 底部导航:上一步 / 下一步(最后一步为保存并生成) -->
     <!-- 底部导航:上一步 / 下一步(最后一步为保存并生成) -->
-    <view class="wizard-footer" v-if="currentStep < 4">
+    <view class="wizard-footer" v-if="currentStep < 5">
       <button class="btn-prev" :disabled="currentStep === 1" @click="prev">
       <button class="btn-prev" :disabled="currentStep === 1" @click="prev">
         <text>上一步</text>
         <text>上一步</text>
       </button>
       </button>
@@ -72,6 +81,7 @@
 <script>
 <script>
 import BasicVitalsStep from './BasicVitalsStep.vue'
 import BasicVitalsStep from './BasicVitalsStep.vue'
 import MedicalHistoryStep from './MedicalHistoryStep.vue'
 import MedicalHistoryStep from './MedicalHistoryStep.vue'
+import LifestyleStep from './LifestyleStep.vue'
 import DietHabitsStep from './DietHabitsStep.vue'
 import DietHabitsStep from './DietHabitsStep.vue'
 import ConfirmStep from './ConfirmStep.vue'
 import ConfirmStep from './ConfirmStep.vue'
 
 
@@ -80,6 +90,7 @@ export default {
   components: {
   components: {
     BasicVitalsStep,
     BasicVitalsStep,
     MedicalHistoryStep,
     MedicalHistoryStep,
+    LifestyleStep,
     DietHabitsStep,
     DietHabitsStep,
     ConfirmStep
     ConfirmStep
   },
   },
@@ -113,7 +124,7 @@ export default {
   },
   },
   computed: {
   computed: {
     wizardSteps: function() {
     wizardSteps: function() {
-      return ['基础体征', '疾病史', '饮食习惯', '确认']
+      return ['基础体征', '疾病史', '生活方式', '饮食习惯', '确认']
     },
     },
     // 当前成员的健康表单(来自父组件快照)
     // 当前成员的健康表单(来自父组件快照)
     currentMemberForm: function() {
     currentMemberForm: function() {
@@ -129,7 +140,14 @@ export default {
         lipidCholesterol: snapshot.lipidCholesterol || '',
         lipidCholesterol: snapshot.lipidCholesterol || '',
         selectedDiseases: snapshot.selectedDiseases || [],
         selectedDiseases: snapshot.selectedDiseases || [],
         medRows: snapshot.medRows || [],
         medRows: snapshot.medRows || [],
-        noMedication: !!snapshot.noMedication
+        noMedication: !!snapshot.noMedication,
+        mood: snapshot.lifestyle && snapshot.lifestyle.mood || '',
+        sleepDuration: snapshot.lifestyle && snapshot.lifestyle.sleepDuration || '',
+        bedtime: snapshot.lifestyle && snapshot.lifestyle.bedtime || '',
+        bowelFrequency: snapshot.lifestyle && snapshot.lifestyle.bowelFrequency || '',
+        bowelForm: snapshot.lifestyle && snapshot.lifestyle.bowelForm || '',
+        antibioticHistory: snapshot.lifestyle && snapshot.lifestyle.antibioticHistory || '',
+        allergySymptoms: snapshot.lifestyle && Array.isArray(snapshot.lifestyle.allergySymptoms) ? snapshot.lifestyle.allergySymptoms : []
       }
       }
     },
     },
     // 当前成员的饮食习惯
     // 当前成员的饮食习惯
@@ -148,13 +166,11 @@ export default {
   methods: {
   methods: {
     // —— 步骤导航 ——
     // —— 步骤导航 ——
     goToStep: function(n) {
     goToStep: function(n) {
-      if (n < 1 || n > 4) return
+      if (n < 1 || n > 5) return
       if (n <= this.currentStep) {
       if (n <= this.currentStep) {
-        // 回退到已完成步骤,无需校验
         this.currentStep = n
         this.currentStep = n
         return
         return
       }
       }
-      // 向前跳需逐级校验中间每一步(保证"每步都校验")
       for (var s = this.currentStep; s < n; s++) {
       for (var s = this.currentStep; s < n; s++) {
         if (!this.validateStep(s)) return
         if (!this.validateStep(s)) return
       }
       }
@@ -168,17 +184,16 @@ export default {
     next: function() {
     next: function() {
       var pass = this.validateStep(this.currentStep)
       var pass = this.validateStep(this.currentStep)
       if (!pass) return
       if (!pass) return
-      if (this.currentStep < 4) {
+      if (this.currentStep < 5) {
         this.currentStep += 1
         this.currentStep += 1
       }
       }
     },
     },
-    // —— 每步校验(用户在确认需求时明确要求"每步都校验") ——
     validateStep: function(step) {
     validateStep: function(step) {
-      var self = this
       var validatorMap = {
       var validatorMap = {
         1: this.validateVitals,
         1: this.validateVitals,
         2: this.validateMedical,
         2: this.validateMedical,
-        3: this.validateDiet
+        3: this.validateLifestyle,
+        4: this.validateDiet
       }
       }
       var fn = validatorMap[step]
       var fn = validatorMap[step]
       if (fn) {
       if (fn) {
@@ -223,31 +238,37 @@ export default {
       }
       }
       return ''
       return ''
     },
     },
-    // Step3: 饮食习惯至少填写一项(忌口/偏好/必吃/菜系/烹饪/辣度/宗教饮食/目标任一)
+    // Step3: 生活方式 — 心情/睡眠/排便/抗生素/过敏,所有字段可选,无强制校验
+    validateLifestyle: function() {
+      return ''
+    },
+    // Step4: 饮食习惯至少填写一项(忌口/偏好/必吃/菜系/烹饪/辣度/宗教饮食/目标/饮食结构任一)
     validateDiet: function() {
     validateDiet: function() {
       var diet = this.currentMemberDiet || {}
       var diet = this.currentMemberDiet || {}
-      // 已明确选择「无特殊偏好」→ 直接通过
       if (diet.noPreference) return ''
       if (diet.noPreference) return ''
       var arrays = [diet.allergies, diet.absoluteAvoid, diet.mustEat, diet.flavorPref, diet.cuisinePref, diet.cookingMethods, diet.healthGoals]
       var arrays = [diet.allergies, diet.absoluteAvoid, diet.mustEat, diet.flavorPref, diet.cuisinePref, diet.cookingMethods, diet.healthGoals]
       var hasAny = arrays.some(function(a) { return a && a.length > 0 })
       var hasAny = arrays.some(function(a) { return a && a.length > 0 })
+      if (diet.vegetableFruit || diet.dairy || diet.sugaryDrinks) hasAny = true
       if (!hasAny && !(diet.spiceLevel && Number(diet.spiceLevel) > 0) && !(diet.religiousDiet && diet.religiousDiet !== 'none')) {
       if (!hasAny && !(diet.spiceLevel && Number(diet.spiceLevel) > 0) && !(diet.religiousDiet && diet.religiousDiet !== 'none')) {
-        return '请至少选择一项饮食习惯偏好'
+        return '请至少选择一项饮食习惯偏好或填写饮食结构'
       }
       }
       return ''
       return ''
     },
     },
-    // —— 子组件数据同步回父组件 ——
     onVitalsInput: function(payload) {
     onVitalsInput: function(payload) {
       this.$emit('update-vitals', payload)
       this.$emit('update-vitals', payload)
     },
     },
     onMedicalInput: function(payload) {
     onMedicalInput: function(payload) {
       this.$emit('update-medical', payload)
       this.$emit('update-medical', payload)
     },
     },
+    onLifestyleInput: function(payload) {
+      this.$emit('update-lifestyle', payload)
+    },
     onDietInput: function(payload) {
     onDietInput: function(payload) {
       this.$emit('update-diet', payload)
       this.$emit('update-diet', payload)
     },
     },
-    // 第3步「无特殊偏好」:跳过校验直接进入确认步
+    // 第4步「无特殊偏好」:跳过校验直接进入确认步
     onDietSkipNoPreference: function() {
     onDietSkipNoPreference: function() {
-      this.currentStep = 4
+      this.currentStep = 5
     },
     },
     // —— 确认步骤提交:聚合所有成员数据后交给父组件统一保存+生成 ——
     // —— 确认步骤提交:聚合所有成员数据后交给父组件统一保存+生成 ——
     handleSubmit: function() {
     handleSubmit: function() {

+ 140 - 0
cfc-frontend/pages/health/components/LifestyleStep.vue

@@ -0,0 +1,140 @@
+<template>
+  <view class="lifestyle-step">
+    <view class="step-title-row">
+      <text class="step-title">生活方式与肠道健康</text>
+      <text class="step-sub">问卷核心:心情/睡眠/排便/抗生素/过敏,帮助精准识别肠道状态</text>
+    </view>
+
+    <!-- 心情状态 -->
+    <view class="form-card">
+      <view class="card-head">
+        <text class="card-title">心情状态</text>
+        <text class="card-hint">单选</text>
+      </view>
+      <view class="chip-row">
+        <view v-for="opt in moodOptions" :key="opt" class="chip-item" :class="{ 'chip-active': inner.mood === opt }" @click="setField('mood', opt)"><text>{{ opt }}</text></view>
+      </view>
+    </view>
+
+    <!-- 睡眠 -->
+    <view class="form-card">
+      <view class="card-head"><text class="card-title">睡眠时长</text><text class="card-hint">单选</text></view>
+      <view class="chip-row">
+        <view v-for="opt in sleepOptions" :key="opt" class="chip-item" :class="{ 'chip-active': inner.sleepDuration === opt }" @click="setField('sleepDuration', opt)"><text>{{ opt }}</text></view>
+      </view>
+    </view>
+    <view class="form-card">
+      <view class="card-head"><text class="card-title">入睡时间</text><text class="card-hint">单选</text></view>
+      <view class="chip-row">
+        <view v-for="opt in bedtimeOptions" :key="opt" class="chip-item" :class="{ 'chip-active': inner.bedtime === opt }" @click="setField('bedtime', opt)"><text>{{ opt }}</text></view>
+      </view>
+    </view>
+
+    <!-- 排便情况 -->
+    <view class="form-card">
+      <view class="card-head"><text class="card-title">排便频率</text><text class="card-hint">单选</text></view>
+      <view class="chip-row">
+        <view v-for="opt in bowelFreqOptions" :key="opt" class="chip-item" :class="{ 'chip-active': inner.bowelFrequency === opt }" @click="setField('bowelFrequency', opt)"><text>{{ opt }}</text></view>
+      </view>
+    </view>
+    <view class="form-card">
+      <view class="card-head"><text class="card-title">粪便性状</text><text class="card-hint">单选</text></view>
+      <view class="chip-row">
+        <view v-for="opt in bowelFormOptions" :key="opt" class="chip-item" :class="{ 'chip-active': inner.bowelForm === opt }" @click="setField('bowelForm', opt)"><text>{{ opt }}</text></view>
+      </view>
+    </view>
+
+    <!-- 抗生素用药史 -->
+    <view class="form-card">
+      <view class="card-head"><text class="card-title">近 2 年抗生素用药史</text><text class="card-hint">单选</text></view>
+      <view class="chip-row">
+        <view v-for="opt in antibioticOptions" :key="opt" class="chip-item" :class="{ 'chip-active': inner.antibioticHistory === opt }" @click="setField('antibioticHistory', opt)"><text>{{ opt }}</text></view>
+      </view>
+    </view>
+
+    <!-- 过敏表现 -->
+    <view class="form-card">
+      <view class="card-head"><text class="card-title">过敏表现</text><text class="card-hint">多选(可多个)</text></view>
+      <view class="chip-row-wrap">
+        <view v-for="opt in allergyOptions" :key="opt" class="chip-item-wrap" :class="{ 'chip-active': inner.allergySymptoms && inner.allergySymptoms.indexOf(opt) >= 0 }" @click="toggleChip('allergySymptoms', opt)"><text>{{ opt }}</text></view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'LifestyleStep',
+  props: {
+    value: { type: Object, default: function() { return {} } }
+  },
+  data: function() {
+    return {
+      inner: { mood: '', sleepDuration: '', bedtime: '', bowelFrequency: '', bowelForm: '', antibioticHistory: '', allergySymptoms: [] }
+    }
+  },
+  computed: {
+    moodOptions: function() { return ['良好', '一般', '低落', '焦虑'] },
+    sleepOptions: function() { return ['<6小时', '6-7小时', '7-8小时', '>8小时'] },
+    bedtimeOptions: function() { return ['22:00前', '22:00-24:00', '24:00后'] },
+    bowelFreqOptions: function() { return ['每天1次(正常)', '隔天1次', '每周2-3次', '每周<2次'] },
+    bowelFormOptions: function() { return ['干硬颗粒', '条状干硬', '正常香蕉状', '软便', '水样'] },
+    antibioticOptions: function() { return ['无', '曾使用过(未用完疗程)', '按疗程使用完', '频繁使用(近2年≥3次)'] },
+    allergyOptions: function() { return ['无', '反复皮疹', '鼻痒/打喷嚏', '眼痒/流泪', '哮喘样症状', '食物过敏'] }
+  },
+  watch: {
+    value: { deep: true, handler: function(val) { this.syncFromValue(val) } }
+  },
+  created: function() { this.syncFromValue(this.value) },
+  methods: {
+    syncFromValue: function(val) {
+      if (!val) return
+      this.inner.mood = val.mood || ''
+      this.inner.sleepDuration = val.sleepDuration || ''
+      this.inner.bedtime = val.bedtime || ''
+      this.inner.bowelFrequency = val.bowelFrequency || ''
+      this.inner.bowelForm = val.bowelForm || ''
+      this.inner.antibioticHistory = val.antibioticHistory || ''
+      this.inner.allergySymptoms = Array.isArray(val.allergySymptoms) ? val.allergySymptoms.slice() : []
+    },
+    setField: function(field, value) {
+      this.inner[field] = value
+      this.emitInput()
+    },
+    toggleChip: function(field, value) {
+      var list = this.inner[field] || []
+      var idx = list.indexOf(value)
+      if (idx >= 0) { this.inner[field] = list.slice(0, idx).concat(list.slice(idx + 1)) } else { this.inner[field] = list.concat([value]) }
+      this.emitInput()
+    },
+    emitInput: function() {
+      this.$emit('input', {
+        mood: this.inner.mood,
+        sleepDuration: this.inner.sleepDuration,
+        bedtime: this.inner.bedtime,
+        bowelFrequency: this.inner.bowelFrequency,
+        bowelForm: this.inner.bowelForm,
+        antibioticHistory: this.inner.antibioticHistory,
+        allergySymptoms: (this.inner.allergySymptoms || []).slice()
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.lifestyle-step { padding: 10rpx 0 30rpx; }
+.step-title-row { margin-bottom: 20rpx; }
+.step-title { font-size: 34rpx; font-weight: 700; color: #333; }
+.step-sub { display: block; font-size: 24rpx; color: #999; margin-top: 8rpx; }
+.form-card { background: #fff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 20rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04); }
+.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16rpx; }
+.card-title { font-size: 28rpx; font-weight: 600; color: #333; }
+.card-hint { font-size: 22rpx; color: #94A3B8; }
+.chip-row { display: flex; flex-wrap: wrap; }
+.chip-row-wrap { display: flex; flex-wrap: wrap; }
+.chip-item, .chip-item-wrap { padding: 12rpx 28rpx; border-radius: 999rpx; background: #F1F5F9; font-size: 24rpx; color: #334155; margin: 0 16rpx 14rpx 0; }
+.chip-active { background: #FFF3E0; color: #F97316; font-weight: 600; }
+.custom-row { margin-top: 10rpx; }
+.custom-hint { font-size: 22rpx; color: #F97316; }
+</style>

+ 41 - 4
cfc-frontend/pages/health/health-plan-summary.vue

@@ -60,6 +60,7 @@
         :submitting="loading"
         :submitting="loading"
         @update-vitals="onWizardVitals"
         @update-vitals="onWizardVitals"
         @update-medical="onWizardMedical"
         @update-medical="onWizardMedical"
+        @update-lifestyle="onWizardLifestyle"
         @update-diet="onWizardDiet"
         @update-diet="onWizardDiet"
         @submit="onWizardSubmit"
         @submit="onWizardSubmit"
       />
       />
@@ -391,7 +392,8 @@ selectedMembers: function() {
             lipidCholesterol: self.parseLipidCholesterol(d.bloodLipids),
             lipidCholesterol: self.parseLipidCholesterol(d.bloodLipids),
             selectedDiseases: self.parseDiseases(d.diseaseHistory),
             selectedDiseases: self.parseDiseases(d.diseaseHistory),
             medRows: self.parseMeds(d.medications),
             medRows: self.parseMeds(d.medications),
-            noMedication: false
+            noMedication: false,
+            lifestyle: d.lifestyle ? self.parseLifestyle(d.lifestyle) : {}
           })
           })
         }
         }
       }).catch(function() {})
       }).catch(function() {})
@@ -420,7 +422,10 @@ selectedMembers: function() {
         cuisinePref: (d && d.cuisinePref) || [],
         cuisinePref: (d && d.cuisinePref) || [],
         cookingMethods: (d && d.cookingMethods) || [],
         cookingMethods: (d && d.cookingMethods) || [],
         healthGoals: (d && d.healthGoals) || [],
         healthGoals: (d && d.healthGoals) || [],
-        noPreference: !!(d && d.noPreference)
+        noPreference: !!(d && d.noPreference),
+        vegetableFruit: (d && d.vegetableFruit) || '',
+        dairy: (d && d.dairy) || '',
+        sugaryDrinks: (d && d.sugaryDrinks) || ''
       }
       }
     },
     },
     hasDietContent: function(diet) {
     hasDietContent: function(diet) {
@@ -429,6 +434,7 @@ selectedMembers: function() {
       for (var i = 0; i < arrs.length; i++) {
       for (var i = 0; i < arrs.length; i++) {
         if (arrs[i] && arrs[i].length > 0) return true
         if (arrs[i] && arrs[i].length > 0) return true
       }
       }
+      if (diet.vegetableFruit || diet.dairy || diet.sugaryDrinks) return true
       if (diet.spiceLevel && Number(diet.spiceLevel) > 0) return true
       if (diet.spiceLevel && Number(diet.spiceLevel) > 0) return true
       if (diet.religiousDiet && diet.religiousDiet !== 'none') return true
       if (diet.religiousDiet && diet.religiousDiet !== 'none') return true
       return false
       return false
@@ -436,7 +442,7 @@ selectedMembers: function() {
     // 保存用饮食对象:noPreference 为前端标记,但随表单落库(后端 diet_preferences.no_preference 持久化)
     // 保存用饮食对象:noPreference 为前端标记,但随表单落库(后端 diet_preferences.no_preference 持久化)
     dietForSave: function(diet) {
     dietForSave: function(diet) {
       var out = {}
       var out = {}
-      var keys = ['allergies', 'absoluteAvoid', 'mustEat', 'religiousDiet', 'spiceLevel', 'flavorPref', 'cuisinePref', 'cookingMethods', 'healthGoals', 'noPreference']
+      var keys = ['allergies', 'absoluteAvoid', 'mustEat', 'religiousDiet', 'spiceLevel', 'flavorPref', 'cuisinePref', 'cookingMethods', 'healthGoals', 'noPreference', 'vegetableFruit', 'dairy', 'sugaryDrinks']
       keys.forEach(function(k) { out[k] = diet[k] })
       keys.forEach(function(k) { out[k] = diet[k] })
       return out
       return out
     },
     },
@@ -492,6 +498,11 @@ selectedMembers: function() {
     onWizardMedical: function(flat) {
     onWizardMedical: function(flat) {
       this.mergeWizardForm(flat)
       this.mergeWizardForm(flat)
     },
     },
+    onWizardLifestyle: function(lifestyle) {
+      var key = String(this.activeFormMemberId || '__account')
+      var prev = this.memberFormMap[key] || {}
+      this.$set(this.memberFormMap, key, Object.assign({}, prev, { lifestyle: lifestyle }))
+    },
     onWizardDiet: function(diet) {
     onWizardDiet: function(diet) {
       var key = String(this.activeFormMemberId || '__account')
       var key = String(this.activeFormMemberId || '__account')
       this.$set(this.memberDietMap, key, diet)
       this.$set(this.memberDietMap, key, diet)
@@ -550,6 +561,16 @@ selectedMembers: function() {
       if (diseases.length > 0) payload.diseaseHistory = JSON.stringify(diseases)
       if (diseases.length > 0) payload.diseaseHistory = JSON.stringify(diseases)
       if (meds.length > 0) payload.medications = JSON.stringify(meds)
       if (meds.length > 0) payload.medications = JSON.stringify(meds)
       if (s.form.notes) payload.notes = s.form.notes
       if (s.form.notes) payload.notes = s.form.notes
+      var lifestyle = s.lifestyle || {}
+      var lifestyleKeys = ['mood', 'sleepDuration', 'bedtime', 'bowelFrequency', 'bowelForm', 'antibioticHistory', 'allergySymptoms']
+      var hasLifestyle = lifestyleKeys.some(function(k) {
+        return lifestyle[k] && (Array.isArray(lifestyle[k]) ? lifestyle[k].length > 0 : true)
+      })
+      if (hasLifestyle) {
+        var lsObj = {}
+        lifestyleKeys.forEach(function(k) { if (lifestyle[k]) lsObj[k] = lifestyle[k] })
+        payload.lifestyle = JSON.stringify(lsObj)
+      }
       return payload
       return payload
     },
     },
     // 当前成员快照(无数据时返回 null)
     // 当前成员快照(无数据时返回 null)
@@ -562,7 +583,23 @@ selectedMembers: function() {
     hasPayload: function(payload) {
     hasPayload: function(payload) {
       if (!payload) return false
       if (!payload) return false
       return payload.heightCm || payload.weightKg || payload.bloodPressure || payload.bloodGlucose ||
       return payload.heightCm || payload.weightKg || payload.bloodPressure || payload.bloodGlucose ||
-             payload.bloodLipids || payload.diseaseHistory || payload.medications || payload.notes
+             payload.bloodLipids || payload.diseaseHistory || payload.medications || payload.notes || payload.lifestyle
+    },
+    parseLifestyle: function(json) {
+      var obj = {}
+      try {
+        var parsed = JSON.parse(json)
+        if (parsed && typeof parsed === 'object') {
+          obj.mood = parsed.mood || ''
+          obj.sleepDuration = parsed.sleepDuration || ''
+          obj.bedtime = parsed.bedtime || ''
+          obj.bowelFrequency = parsed.bowelFrequency || ''
+          obj.bowelForm = parsed.bowelForm || ''
+          obj.antibioticHistory = parsed.antibioticHistory || ''
+          obj.allergySymptoms = Array.isArray(parsed.allergySymptoms) ? parsed.allergySymptoms.slice() : []
+        }
+      } catch (e) {}
+      return obj
     },
     },
     // 逐个成员保存健康档案 + 饮食习惯,最后生成方案
     // 逐个成员保存健康档案 + 饮食习惯,最后生成方案
     saveAndGenerate: function() {
     saveAndGenerate: function() {

+ 79 - 0
cfc-langgraph/app/api/adapter.py

@@ -667,6 +667,7 @@ PLAN_SYSTEM_PROMPT = """你是一个专业的家庭健康方案规划师。根
 3. 建议要具体可执行,避免空泛
 3. 建议要具体可执行,避免空泛
 4. 营养补充部分要具体到产品类型和用量
 4. 营养补充部分要具体到产品类型和用量
 5. 严重健康问题建议咨询医生
 5. 严重健康问题建议咨询医生
+6. 当问卷中某项关键信息缺失或不确定(如抗生素用药史、排便性状、过敏表现、饮食结构不明确)时,应在方案末尾明确提示用户:「为了获得更精准的饮食建议,建议您进行肠道菌群检测(GI Navigator 或其他权威检测),以获取确切的菌群组成数据」。
 
 
 ## 画像数据使用指南
 ## 画像数据使用指南
 如果提供了用户的画像数据(五维评分、身体指标、心理指标等),请结合这些真实数据给出更有针对性的建议。特别关注异常指标(如睡眠不足、压力偏高、运动频率低等),在方案中明确说明这些指标的现状和改善方向。
 如果提供了用户的画像数据(五维评分、身体指标、心理指标等),请结合这些真实数据给出更有针对性的建议。特别关注异常指标(如睡眠不足、压力偏高、运动频率低等),在方案中明确说明这些指标的现状和改善方向。
@@ -700,6 +701,49 @@ REGENERATE_SECTION_SYSTEM_PROMPT = """你是一个家庭健康方案规划师。
 - 建议要具体可执行"""
 - 建议要具体可执行"""
 
 
 
 
+def _format_diet_preferences(dp: dict) -> str:
+    if not dp:
+        return ""
+    parts = []
+    def _list(v):
+        if not v:
+            return ""
+        if isinstance(v, list):
+            return ", ".join(str(x) for x in v)
+        try:
+            parsed = json.loads(v)
+            if isinstance(parsed, list):
+                return ", ".join(str(x) for x in parsed)
+        except Exception:
+            pass
+        return str(v)
+    if dp.get("allergies"):
+        parts.append(f"过敏原:{_list(dp['allergies'])}")
+    if dp.get("absoluteAvoid"):
+        parts.append(f"忌口:{_list(dp['absoluteAvoid'])}")
+    if dp.get("mustEat"):
+        parts.append(f"必吃:{_list(dp['mustEat'])}")
+    if dp.get("religiousDiet") and dp["religiousDiet"] not in ("none", None, ""):
+        parts.append(f"宗教饮食:{dp['religiousDiet']}")
+    if dp.get("spiceLevel"):
+        parts.append(f"辣度:{dp['spiceLevel']}")
+    if dp.get("noPreference"):
+        parts.append("无特殊饮食偏好")
+    if dp.get("dietStructure"):
+        try:
+            ds = json.loads(dp["dietStructure"])
+            if isinstance(ds, dict):
+                if ds.get("vegetableFruit"):
+                    parts.append(f"每日蔬果量:{ds['vegetableFruit']}")
+                if ds.get("dairy"):
+                    parts.append(f"乳制品:{ds['dairy']}")
+                if ds.get("sugaryDrinks"):
+                    parts.append(f"含糖饮料:{ds['sugaryDrinks']}")
+        except Exception:
+            parts.append(f"饮食结构:{dp['dietStructure']}")
+    return "; ".join(parts)
+
+
 def _format_health_status(hs: dict) -> str:
 def _format_health_status(hs: dict) -> str:
     if not hs:
     if not hs:
         return ""
         return ""
@@ -735,6 +779,31 @@ def _format_health_status(hs: dict) -> str:
         out.append(f"用药:{meds}")
         out.append(f"用药:{meds}")
     if hs.get("notes"):
     if hs.get("notes"):
         out.append(f"备注:{hs['notes']}")
         out.append(f"备注:{hs['notes']}")
+    # 生活方式与肠道健康(心情/睡眠/排便/抗生素/过敏表现)
+    if hs.get("lifestyle"):
+        try:
+            ls = json.loads(hs["lifestyle"])
+            parts = []
+            if isinstance(ls, dict):
+                if ls.get("mood"):
+                    parts.append(f"心情状态:{ls['mood']}")
+                if ls.get("sleepDuration"):
+                    parts.append(f"睡眠时长:{ls['sleepDuration']}")
+                if ls.get("bedtime"):
+                    parts.append(f"入睡时间:{ls['bedtime']}")
+                if ls.get("bowelFrequency"):
+                    parts.append(f"排便频率:{ls['bowelFrequency']}")
+                if ls.get("bowelForm"):
+                    parts.append(f"粪便性状:{ls['bowelForm']}")
+                if ls.get("antibioticHistory"):
+                    parts.append(f"近2年抗生素用药史:{ls['antibioticHistory']}")
+                if ls.get("allergySymptoms") and ls["allergySymptoms"]:
+                    sym = ", ".join(ls["allergySymptoms"]) if isinstance(ls["allergySymptoms"], list) else str(ls["allergySymptoms"])
+                    parts.append(f"过敏表现:{sym}")
+            if parts:
+                out.append("生活方式:" + "; ".join(parts))
+        except Exception:
+            out.append(f"生活方式:{hs['lifestyle']}")
     return "; ".join(out)
     return "; ".join(out)
 
 
 
 
@@ -778,6 +847,10 @@ async def _collect_plan_data(java: JavaClient, retriever: RagRetriever, member_i
             member["health_status"] = await java.get_member_health_status(int(member["id"]))
             member["health_status"] = await java.get_member_health_status(int(member["id"]))
         except Exception:
         except Exception:
             member["health_status"] = {}
             member["health_status"] = {}
+        try:
+            member["diet_preferences"] = await java.get_member_diet_preferences(int(member["id"]))
+        except Exception:
+            member["diet_preferences"] = {}
 
 
     # 3. 知识库
     # 3. 知识库
     kb_results = []
     kb_results = []
@@ -931,6 +1004,9 @@ async def health_plan_generate(req: HealthPlanRequest):
         hs_text = _format_health_status(m.get("health_status"))
         hs_text = _format_health_status(m.get("health_status"))
         if hs_text:
         if hs_text:
             parts.append(f"  健康自检: {hs_text}")
             parts.append(f"  健康自检: {hs_text}")
+        dp_text = _format_diet_preferences(m.get("diet_preferences"))
+        if dp_text:
+            parts.append(f"  饮食偏好: {dp_text}")
         if dims:
         if dims:
             parts.append(f"  五维评分: 身{dims.get('body','?')} 智{dims.get('wisdom','?')} 心{dims.get('mind','?')} 行{dims.get('action','?')} 富{dims.get('wealth','?')}")
             parts.append(f"  五维评分: 身{dims.get('body','?')} 智{dims.get('wisdom','?')} 心{dims.get('mind','?')} 行{dims.get('action','?')} 富{dims.get('wealth','?')}")
         if body.get('sleep_dur_avg'):
         if body.get('sleep_dur_avg'):
@@ -991,6 +1067,9 @@ async def health_plan_regenerate(req: HealthPlanRegenerateRequest):
         hs_text = _format_health_status(m.get("health_status"))
         hs_text = _format_health_status(m.get("health_status"))
         if hs_text:
         if hs_text:
             ctx_parts.append(f"  健康自检: {hs_text}")
             ctx_parts.append(f"  健康自检: {hs_text}")
+        dp_text = _format_diet_preferences(m.get("diet_preferences"))
+        if dp_text:
+            ctx_parts.append(f"  饮食偏好: {dp_text}")
     for ind in all_indicators[:10]:
     for ind in all_indicators[:10]:
         if ind.get("status") in ("abnormal", "high", "low", "偏高", "偏低"):
         if ind.get("status") in ("abnormal", "high", "low", "偏高", "偏低"):
             ctx_parts.append(f"- 【异常】{ind.get('_member_name','')} - {ind.get('indicatorName','')}: {ind.get('indicatorValue','')}")
             ctx_parts.append(f"- 【异常】{ind.get('_member_name','')} - {ind.get('indicatorName','')}: {ind.get('indicatorValue','')}")

+ 9 - 0
cfc-langgraph/app/tools/java_client.py

@@ -208,3 +208,12 @@ class JavaClient:
             return data["data"]
             return data["data"]
         return {}
         return {}
 
 
+    async def get_member_diet_preferences(self, member_id: int) -> dict:
+        """获取家庭成员饮食偏好(diet_preferences,含饮食结构现状)"""
+        client = await self._get_client()
+        resp = await client.post("/api/diet/preferences/current-member", json={"memberId": member_id})
+        data = resp.json()
+        if data.get("code") == 200 and data.get("data"):
+            return data["data"]
+        return {}
+