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

feat: add precision nutrition & health report survey flow

Add HealthGutFlora/HealthDiseaseRisk/ReportSurvey entities, mappers, services, controllers. Add survey page with 6-question flow, wire into body page and AI chat context. Add report detail endpoint with gut flora + disease risk analysis.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
User 4 месяцев назад
Родитель
Сommit
94af2ff1b4
22 измененных файлов с 2975 добавлено и 149 удалено
  1. 162 55
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 16 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java
  3. 80 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/ReportSurveyController.java
  4. 20 3
      cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java
  5. 34 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/HealthDiseaseRisk.java
  6. 44 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/HealthGutFlora.java
  7. 39 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ReportSurvey.java
  8. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/HealthDiseaseRiskMapper.java
  9. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/HealthGutFloraMapper.java
  10. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/ReportSurveyMapper.java
  11. 84 0
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyContextService.java
  12. 86 0
      cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java
  13. 93 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ReportSurveyService.java
  14. 51 51
      cfc-backend/src/main/resources/schema.sql
  15. 29 5
      cfc-frontend/pages/ai/chat.vue
  16. 3 3
      cfc-frontend/pages/body/health-report.vue
  17. 213 3
      cfc-frontend/pages/body/index.vue
  18. 247 0
      cfc-frontend/pages/health/report-list.vue
  19. 432 0
      cfc-frontend/pages/health/report-survey.vue
  20. 352 0
      cfc-frontend/pages/health/report-upload.vue
  21. 70 29
      cfc-frontend/utils/api.js
  22. 893 0
      docs/需求分析/精准营养与健康报告功能需求.md

+ 162 - 55
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -888,7 +888,7 @@ log.info("已添加template_id列到tasks表");
                 "  intro TEXT," +
                 "  intro TEXT," +
                 "  cover_image VARCHAR(500)," +
                 "  cover_image VARCHAR(500)," +
                 "  images TEXT," +
                 "  images TEXT," +
-                "  price DECIMAL(10,2) NOT NULL DEFAULT 0," +
+                "  price INT NOT NULL DEFAULT 0 COMMENT '价格(分)'," +
                 "  stock INT NOT NULL DEFAULT 0," +
                 "  stock INT NOT NULL DEFAULT 0," +
                 "  sales_count INT NOT NULL DEFAULT 0," +
                 "  sales_count INT NOT NULL DEFAULT 0," +
                 "  product_type VARCHAR(32) NOT NULL COMMENT 'activity/course/physical/digital/service'," +
                 "  product_type VARCHAR(32) NOT NULL COMMENT 'activity/course/physical/digital/service'," +
@@ -896,7 +896,8 @@ log.info("已添加template_id列到tasks表");
                 "  vendor_name VARCHAR(100)," +
                 "  vendor_name VARCHAR(100)," +
                 "  status VARCHAR(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/pending/approved/rejected/on_shelf/off_shelf'," +
                 "  status VARCHAR(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/pending/approved/rejected/on_shelf/off_shelf'," +
                 "  reject_reason VARCHAR(500)," +
                 "  reject_reason VARCHAR(500)," +
-                "  member_price DECIMAL(10,2)," +
+                "  member_price INT COMMENT '会员价(分)'," +
+                "  profit_rate INT DEFAULT 0 COMMENT '平台利润率(千分比)'," +
                 "  member_eligible VARCHAR(32) DEFAULT 'none' COMMENT 'all/budget/none'," +
                 "  member_eligible VARCHAR(32) DEFAULT 'none' COMMENT 'all/budget/none'," +
                 "  domain VARCHAR(32) COMMENT 'action/mind/body/wisdom/wealth'," +
                 "  domain VARCHAR(32) COMMENT 'action/mind/body/wisdom/wealth'," +
                 "  external_source VARCHAR(64)," +
                 "  external_source VARCHAR(64)," +
@@ -922,8 +923,8 @@ log.info("已添加template_id列到tasks表");
                 "  buyer_id BIGINT NOT NULL," +
                 "  buyer_id BIGINT NOT NULL," +
                 "  family_id BIGINT," +
                 "  family_id BIGINT," +
                 "  quantity INT NOT NULL DEFAULT 1," +
                 "  quantity INT NOT NULL DEFAULT 1," +
-                "  unit_price DECIMAL(10,2) NOT NULL," +
-                "  total_amount DECIMAL(10,2) NOT NULL," +
+                "  unit_price INT NOT NULL COMMENT '单价(分)'," +
+                "  total_amount INT NOT NULL COMMENT '总金额(分)'," +
                 "  status VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/completed/cancelled/refunded'," +
                 "  status VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/completed/cancelled/refunded'," +
                 "  payment_method VARCHAR(32) COMMENT 'wechat/points/membership'," +
                 "  payment_method VARCHAR(32) COMMENT 'wechat/points/membership'," +
                 "  remark VARCHAR(500)," +
                 "  remark VARCHAR(500)," +
@@ -953,7 +954,7 @@ log.info("已添加template_id列到tasks表");
 
 
         // SysConfig 种子数据
         // SysConfig 种子数据
         try {
         try {
-            insertSysConfigSeed("points_exchange_rate", "0.01", "积分兑换汇率(1积分=0.01元)");
+            insertSysConfigSeed("points_exchange_rate", "1", "积分兑换汇率(1积分=1分,即0.01元)");
             insertSysConfigSeed("points_deduct_cap", "0.5", "积分抵扣上限(50%)");
             insertSysConfigSeed("points_deduct_cap", "0.5", "积分抵扣上限(50%)");
             insertSysConfigSeed("member_fee_standard", "1314", "标准会员年费(元)");
             insertSysConfigSeed("member_fee_standard", "1314", "标准会员年费(元)");
             insertSysConfigSeed("member_fee_premium", "3650", "高级会员年费(元)");
             insertSysConfigSeed("member_fee_premium", "3650", "高级会员年费(元)");
@@ -979,11 +980,11 @@ log.info("已添加template_id列到tasks表");
 
 
         // Product 种子数据(5 个示例商品,覆盖全部 5 种 productType)
         // Product 种子数据(5 个示例商品,覆盖全部 5 种 productType)
         try {
         try {
-            insertProductSeed("亲子关系工作坊", "线下半日工作坊,重建家庭对话模式", "activity", new java.math.BigDecimal("388.00"), "action", "all");
-            insertProductSeed("情绪管理21天训练营", "21天视频课程,识别并管理情绪", "course", new java.math.BigDecimal("199.00"), "mind", "all");
-            insertProductSeed("肠道菌群检测套装", "家用检测 + 报告解读", "physical", new java.math.BigDecimal("599.00"), "body", "budget");
-            insertProductSeed("儿童多模态认知测评", "在线测评 + 个性化建议", "digital", new java.math.BigDecimal("99.00"), "wisdom", "none");
-            insertProductSeed("成长规划师1对1咨询", "60分钟专业规划", "service", new java.math.BigDecimal("500.00"), "wealth", "budget");
+            insertProductSeed("亲子关系工作坊", "线下半日工作坊,重建家庭对话模式", "activity", 38800, "action", "all");
+            insertProductSeed("情绪管理21天训练营", "21天视频课程,识别并管理情绪", "course", 19900, "mind", "all");
+            insertProductSeed("肠道菌群检测套装", "家用检测 + 报告解读", "physical", 59900, "body", "budget");
+            insertProductSeed("儿童多模态认知测评", "在线测评 + 个性化建议", "digital", 9900, "wisdom", "none");
+            insertProductSeed("成长规划师1对1咨询", "60分钟专业规划", "service", 50000, "wealth", "budget");
             log.info("Product 种子数据已加载");
             log.info("Product 种子数据已加载");
         } catch (Exception e) {
         } catch (Exception e) {
             log.warn("Product 种子数据初始化失败: {}", e.getMessage());
             log.warn("Product 种子数据初始化失败: {}", e.getMessage());
@@ -1073,7 +1074,7 @@ log.info("已添加template_id列到tasks表");
                 "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'products' AND COLUMN_NAME = 'profit_rate'",
                 "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'products' AND COLUMN_NAME = 'profit_rate'",
                 Integer.class);
                 Integer.class);
             if (exists == null || exists == 0) {
             if (exists == null || exists == 0) {
-                jdbcTemplate.execute("ALTER TABLE products ADD COLUMN profit_rate DECIMAL(5,2) DEFAULT 0.00 COMMENT '平台利润率(%)'");
+                jdbcTemplate.execute("ALTER TABLE products ADD COLUMN profit_rate INT DEFAULT 0 COMMENT '平台利润率(bps,100=1%)'");
                 log.info("已添加profit_rate列到products表");
                 log.info("已添加profit_rate列到products表");
             } else {
             } else {
                 log.info("profit_rate列已存在,跳过");
                 log.info("profit_rate列已存在,跳过");
@@ -1091,9 +1092,9 @@ log.info("已添加template_id列到tasks表");
                 "referrer_id BIGINT NOT NULL COMMENT '推荐人ID', " +
                 "referrer_id BIGINT NOT NULL COMMENT '推荐人ID', " +
                 "buyer_id BIGINT NOT NULL COMMENT '购买人ID', " +
                 "buyer_id BIGINT NOT NULL COMMENT '购买人ID', " +
                 "commission_type VARCHAR(16) NOT NULL COMMENT '佣金类型: member/profit', " +
                 "commission_type VARCHAR(16) NOT NULL COMMENT '佣金类型: member/profit', " +
-                "order_amount DECIMAL(10,2) NOT NULL COMMENT '订单金额', " +
-                "profit_rate DECIMAL(5,2) DEFAULT 0.00 COMMENT '利润率(%)', " +
-                "commission_amount DECIMAL(10,2) NOT NULL COMMENT '佣金金额', " +
+                "order_amount INT NOT NULL COMMENT '订单金额(分)', " +
+                "profit_rate INT DEFAULT 0 COMMENT '利润率(bps,100=1%)', " +
+                "commission_amount INT NOT NULL COMMENT '佣金金额(分)', " +
                 "status VARCHAR(16) NOT NULL DEFAULT 'settled' COMMENT '状态: pending/settled', " +
                 "status VARCHAR(16) NOT NULL DEFAULT 'settled' COMMENT '状态: pending/settled', " +
                 "remark VARCHAR(255) DEFAULT NULL, " +
                 "remark VARCHAR(255) DEFAULT NULL, " +
                 "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
                 "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
@@ -1111,7 +1112,7 @@ log.info("已添加template_id列到tasks表");
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS withdrawal_requests (" +
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS withdrawal_requests (" +
                 "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                 "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                 "user_id BIGINT NOT NULL COMMENT '用户ID', " +
                 "user_id BIGINT NOT NULL COMMENT '用户ID', " +
-                "amount DECIMAL(10,2) NOT NULL COMMENT '提现金额', " +
+                "amount INT NOT NULL COMMENT '提现金额(分)', " +
                 "account_info TEXT COMMENT '账户信息(JSON)', " +
                 "account_info TEXT COMMENT '账户信息(JSON)', " +
                 "status VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/approved/rejected', " +
                 "status VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/approved/rejected', " +
                 "audit_by BIGINT DEFAULT NULL COMMENT '审核人ID', " +
                 "audit_by BIGINT DEFAULT NULL COMMENT '审核人ID', " +
@@ -1283,7 +1284,7 @@ log.info("已添加template_id列到tasks表");
                 "source_id BIGINT NOT NULL COMMENT '来源业务表ID', " +
                 "source_id BIGINT NOT NULL COMMENT '来源业务表ID', " +
                 "source_name VARCHAR(200) COMMENT '冗余名称', " +
                 "source_name VARCHAR(200) COMMENT '冗余名称', " +
                 "dimension_id BIGINT NOT NULL COMMENT '维度ID', " +
                 "dimension_id BIGINT NOT NULL COMMENT '维度ID', " +
-                "ratio DECIMAL(5,4) NOT NULL COMMENT '占比如0.6000=60%', " +
+                "ratio INT NOT NULL COMMENT '占比(bps,10000=100%)', " +
                 "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
                 "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
                 "INDEX idx_source (source_type, source_id)" +
                 "INDEX idx_source (source_type, source_id)" +
                 ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务-维度比例配置表'"
                 ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务-维度比例配置表'"
@@ -1366,7 +1367,7 @@ log.info("已添加template_id列到tasks表");
                 for (String[] mapping : domainMapping) {
                 for (String[] mapping : domainMapping) {
                     jdbcTemplate.execute(
                     jdbcTemplate.execute(
                         "INSERT INTO energy_source_config (source_type, source_id, source_name, dimension_id, ratio) " +
                         "INSERT INTO energy_source_config (source_type, source_id, source_name, dimension_id, ratio) " +
-                        "SELECT 'product', p.id, p.name, d.id, 1.0000 " +
+                        "SELECT 'product', p.id, p.name, d.id, 10000 " +
                         "FROM products p " +
                         "FROM products p " +
                         "JOIN energy_dimension d ON d.code = '" + mapping[1] + "' " +
                         "JOIN energy_dimension d ON d.code = '" + mapping[1] + "' " +
                         "WHERE p.domain = '" + mapping[0] + "' " +
                         "WHERE p.domain = '" + mapping[0] + "' " +
@@ -1382,6 +1383,58 @@ log.info("已添加template_id列到tasks表");
             log.warn("初始化商品能量比例配置失败: {}", e.getMessage());
             log.warn("初始化商品能量比例配置失败: {}", e.getMessage());
         }
         }
 
 
+        // ==================== 精准营养 P0: health_report_survey ====================
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_report_survey (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "report_id BIGINT NOT NULL COMMENT '关联报告ID', " +
+                "user_id BIGINT NOT NULL COMMENT '用户ID', " +
+                "child_id BIGINT NOT NULL COMMENT '孩子ID', " +
+                "survey_data JSON COMMENT '调研问卷结果JSON', " +
+                "status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/completed', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                "UNIQUE KEY uk_report (report_id)" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报告调研问卷'");
+            log.info("已创建 health_report_survey 表");
+        } catch (Exception e) {
+            log.warn("创建 health_report_survey 表失败: {}", e.getMessage());
+        }
+
+        // ==================== 精准营养 P0: health_gut_flora + health_disease_risk ====================
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_gut_flora (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "report_id BIGINT NOT NULL COMMENT '关联报告ID', " +
+                "bacteria_name VARCHAR(100) NOT NULL COMMENT '菌种名称', " +
+                "bacteria_value VARCHAR(50) COMMENT '菌种丰度值', " +
+                "normal_range VARCHAR(100) COMMENT '正常范围', " +
+                "status VARCHAR(20) COMMENT '状态: 正常/偏低/偏高', " +
+                "category VARCHAR(50) COMMENT '分类: 有益菌/有害菌/中性菌', " +
+                "sort_order INT DEFAULT 0 COMMENT '排序', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "INDEX idx_report (report_id)" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菌群检测专项数据'");
+            log.info("已创建 health_gut_flora 表");
+        } catch (Exception e) {
+            log.warn("创建 health_gut_flora 表失败: {}", e.getMessage());
+        }
+
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_disease_risk (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "report_id BIGINT NOT NULL COMMENT '关联报告ID', " +
+                "disease_name VARCHAR(100) NOT NULL COMMENT '疾病名称', " +
+                "risk_value VARCHAR(50) COMMENT '风险值(0-1)', " +
+                "risk_level VARCHAR(20) COMMENT '风险等级: 高风险/需注意/低风险', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "INDEX idx_report (report_id)" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疾病风险评估数据'");
+            log.info("已创建 health_disease_risk 表");
+        } catch (Exception e) {
+            log.warn("创建 health_disease_risk 表失败: {}", e.getMessage());
+        }
+
         // ==================== 迁移: guides 表统一 ====================
         // ==================== 迁移: guides 表统一 ====================
 
 
         // 迁移: guides 添加 type 字段(区分服务商类型)
         // 迁移: guides 添加 type 字段(区分服务商类型)
@@ -1415,7 +1468,7 @@ log.info("已添加template_id列到tasks表");
         } catch (Exception e) {}
         } catch (Exception e) {}
 
 
         try {
         try {
-            jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN rating DECIMAL(3,2) DEFAULT 5.00 COMMENT '评分'");
+            jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN rating INT DEFAULT 500 COMMENT '评分(百分位,500=5.00)'");
         } catch (Exception e) {}
         } catch (Exception e) {}
 
 
         try {
         try {
@@ -1515,6 +1568,27 @@ log.info("已添加template_id列到tasks表");
             log.warn("检查/添加 dan_assessment_results EMI 心理子维度字段失败: {}", e.getMessage());
             log.warn("检查/添加 dan_assessment_results EMI 心理子维度字段失败: {}", e.getMessage());
         }
         }
 
 
+        // 迁移: dan_assessment_results 表添加大五人格维度字段
+        try {
+            Integer colExists = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'dan_assessment_results' AND COLUMN_NAME = 'openness_score'",
+                Integer.class);
+            if (colExists == null || colExists == 0) {
+                jdbcTemplate.execute("ALTER TABLE dan_assessment_results " +
+                    "ADD COLUMN openness_score INT COMMENT '开放性(0-100)', " +
+                    "ADD COLUMN conscientiousness_score INT COMMENT '尽责性(0-100)', " +
+                    "ADD COLUMN extraversion_score INT COMMENT '外向性(0-100)', " +
+                    "ADD COLUMN agreeableness_score INT COMMENT '宜人性(0-100)', " +
+                    "ADD COLUMN neuroticism_score INT COMMENT '神经质(0-100)', " +
+                    "ADD COLUMN big_five_overall_score INT COMMENT '大五人格综合分'");
+                log.info("已为 dan_assessment_results 表添加大五人格维度字段");
+            } else {
+                log.info("dan_assessment_results 大五人格维度字段已存在,跳过");
+            }
+        } catch (Exception e) {
+            log.warn("检查/添加 dan_assessment_results 大五人格维度字段失败: {}", e.getMessage());
+        }
+
         // 迁移: users 表添加出生时辰字段
         // 迁移: users 表添加出生时辰字段
         try {
         try {
             jdbcTemplate.execute("ALTER TABLE users ADD COLUMN birth_hour VARCHAR(10) COMMENT '出生时辰: 子丑寅卯辰巳午未申酉戌亥'");
             jdbcTemplate.execute("ALTER TABLE users ADD COLUMN birth_hour VARCHAR(10) COMMENT '出生时辰: 子丑寅卯辰巳午未申酉戌亥'");
@@ -1541,8 +1615,8 @@ log.info("已添加template_id列到tasks表");
                     "wuxing_elements JSON COMMENT '五行星盘元素JSON', " +
                     "wuxing_elements JSON COMMENT '五行星盘元素JSON', " +
                     "zodiac VARCHAR(16) COMMENT '生肖: rat/ox/tiger/rabbit/dragon/snake/horse/sheep/monkey/rooster/dog/pig', " +
                     "zodiac VARCHAR(16) COMMENT '生肖: rat/ox/tiger/rabbit/dragon/snake/horse/sheep/monkey/rooster/dog/pig', " +
                     "blood_type VARCHAR(5) COMMENT '血型: A/B/AB/O', " +
                     "blood_type VARCHAR(5) COMMENT '血型: A/B/AB/O', " +
-                    "mind_base_score DECIMAL(5,2) DEFAULT 0 COMMENT '心维度基础分(先天)', " +
-                    "wisdom_base_score DECIMAL(5,2) DEFAULT 0 COMMENT '智维度基础分(先天)', " +
+                    "mind_base_score INT DEFAULT 0 COMMENT '心维度基础分(百分位,500=5.00)', " +
+                    "wisdom_base_score INT DEFAULT 0 COMMENT '智维度基础分(百分位,500=5.00)', " +
                     "behavior_modifier JSON COMMENT '行为修正因子JSON', " +
                     "behavior_modifier JSON COMMENT '行为修正因子JSON', " +
                     "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
                     "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
                     "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
                     "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
@@ -1616,9 +1690,9 @@ log.info("已添加template_id列到tasks表");
                 "description TEXT COMMENT '商品详细描述', " +
                 "description TEXT COMMENT '商品详细描述', " +
                 "cover_image VARCHAR(512) COMMENT '封面图URL', " +
                 "cover_image VARCHAR(512) COMMENT '封面图URL', " +
                 "images TEXT COMMENT '多图(JSON数组)', " +
                 "images TEXT COMMENT '多图(JSON数组)', " +
-                "price DECIMAL(10,2) NOT NULL COMMENT '售价(元)', " +
-                "member_price DECIMAL(10,2) DEFAULT NULL COMMENT '会员价', " +
-                "cost_price DECIMAL(10,2) DEFAULT NULL COMMENT '成本价', " +
+                "price INT NOT NULL COMMENT '售价(分)', " +
+                "member_price INT DEFAULT NULL COMMENT '会员价(分)', " +
+                "cost_price INT DEFAULT NULL COMMENT '成本价(分)', " +
                 "stock INT DEFAULT 0 COMMENT '库存数量', " +
                 "stock INT DEFAULT 0 COMMENT '库存数量', " +
                 "sales_count INT DEFAULT 0 COMMENT '销量', " +
                 "sales_count INT DEFAULT 0 COMMENT '销量', " +
                 "product_type VARCHAR(32) NOT NULL COMMENT '类型: physical/virtual/coupon/course', " +
                 "product_type VARCHAR(32) NOT NULL COMMENT '类型: physical/virtual/coupon/course', " +
@@ -1626,7 +1700,7 @@ log.info("已添加template_id列到tasks表");
                 "vendor_name VARCHAR(64) COMMENT '供应商名称', " +
                 "vendor_name VARCHAR(64) COMMENT '供应商名称', " +
                 "status VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/approved/rejected/on_shelf/off_shelf', " +
                 "status VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/approved/rejected/on_shelf/off_shelf', " +
                 "reject_reason VARCHAR(255) DEFAULT NULL COMMENT '审核拒绝原因', " +
                 "reject_reason VARCHAR(255) DEFAULT NULL COMMENT '审核拒绝原因', " +
-                "profit_rate DECIMAL(5,2) DEFAULT 0.00 COMMENT '平台利润率(%)', " +
+                "profit_rate INT DEFAULT 0 COMMENT '平台利润率(bps,100=1%)', " +
                 "external_source VARCHAR(32) COMMENT '外部商品来源', " +
                 "external_source VARCHAR(32) COMMENT '外部商品来源', " +
                 "external_id VARCHAR(64) COMMENT '外部商品ID', " +
                 "external_id VARCHAR(64) COMMENT '外部商品ID', " +
                 "external_data TEXT COMMENT '外部商品完整数据(JSON)', " +
                 "external_data TEXT COMMENT '外部商品完整数据(JSON)', " +
@@ -1648,9 +1722,9 @@ log.info("已添加template_id列到tasks表");
                 "order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号(DS+时间戳+6位随机)', " +
                 "order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号(DS+时间戳+6位随机)', " +
                 "buyer_id BIGINT NOT NULL COMMENT '买家ID', " +
                 "buyer_id BIGINT NOT NULL COMMENT '买家ID', " +
                 "family_id BIGINT DEFAULT NULL COMMENT '家庭ID', " +
                 "family_id BIGINT DEFAULT NULL COMMENT '家庭ID', " +
-                "total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总金额(标价)', " +
-                "discount_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '优惠金额', " +
-                "actual_amount DECIMAL(10,2) NOT NULL COMMENT '实付金额', " +
+                "total_amount INT NOT NULL COMMENT '订单总金额(分)', " +
+                "discount_amount INT DEFAULT 0 COMMENT '优惠金额(分)', " +
+                "actual_amount INT NOT NULL COMMENT '实付金额(分)', " +
                 "status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/shipped/completed/refunding/refunded/cancelled', " +
                 "status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/shipped/completed/refunding/refunded/cancelled', " +
                 "payment_method VARCHAR(16) DEFAULT 'wechat' COMMENT 'wechat/alipay', " +
                 "payment_method VARCHAR(16) DEFAULT 'wechat' COMMENT 'wechat/alipay', " +
                 "transaction_id VARCHAR(64) DEFAULT NULL COMMENT '微信/支付宝交易号', " +
                 "transaction_id VARCHAR(64) DEFAULT NULL COMMENT '微信/支付宝交易号', " +
@@ -1662,8 +1736,8 @@ log.info("已添加template_id列到tasks表");
                 "receiver_name VARCHAR(32) DEFAULT NULL COMMENT '收件人姓名', " +
                 "receiver_name VARCHAR(32) DEFAULT NULL COMMENT '收件人姓名', " +
                 "receiver_phone VARCHAR(16) DEFAULT NULL COMMENT '收件人电话', " +
                 "receiver_phone VARCHAR(16) DEFAULT NULL COMMENT '收件人电话', " +
                 "receiver_address VARCHAR(256) DEFAULT NULL COMMENT '收件人地址', " +
                 "receiver_address VARCHAR(256) DEFAULT NULL COMMENT '收件人地址', " +
-                "freight DECIMAL(10,2) DEFAULT 0.00 COMMENT '运费', " +
-                "profit_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '分润金额', " +
+                "freight INT DEFAULT 0 COMMENT '运费(分)', " +
+                "profit_amount INT DEFAULT 0 COMMENT '分润金额(分)', " +
                 "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
                 "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
                 "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
                 "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
                 "INDEX idx_buyer (buyer_id), " +
                 "INDEX idx_buyer (buyer_id), " +
@@ -1684,10 +1758,10 @@ log.info("已添加template_id列到tasks表");
                 "product_id BIGINT NOT NULL COMMENT '商品ID', " +
                 "product_id BIGINT NOT NULL COMMENT '商品ID', " +
                 "product_name VARCHAR(128) NOT NULL COMMENT '商品名称(快照)', " +
                 "product_name VARCHAR(128) NOT NULL COMMENT '商品名称(快照)', " +
                 "cover_image VARCHAR(512) COMMENT '封面图(快照)', " +
                 "cover_image VARCHAR(512) COMMENT '封面图(快照)', " +
-                "unit_price DECIMAL(10,2) NOT NULL COMMENT '单价(快照)', " +
-                "price DECIMAL(10,2) NOT NULL COMMENT '单价(别名,兼容)', " +
+                "unit_price INT NOT NULL COMMENT '单价(分,快照)', " +
+                "price INT NOT NULL COMMENT '单价(分,别名兼容)', " +
                 "quantity INT NOT NULL DEFAULT 1 COMMENT '购买数量', " +
                 "quantity INT NOT NULL DEFAULT 1 COMMENT '购买数量', " +
-                "subtotal DECIMAL(10,2) NOT NULL COMMENT '小计', " +
+                "subtotal INT NOT NULL COMMENT '小计(分)', " +
                 "product_type VARCHAR(32) COMMENT '商品类型(快照)', " +
                 "product_type VARCHAR(32) COMMENT '商品类型(快照)', " +
                 "vendor_id BIGINT COMMENT '供应商ID(快照)', " +
                 "vendor_id BIGINT COMMENT '供应商ID(快照)', " +
                 "vendor_name VARCHAR(64) COMMENT '供应商名称(快照)', " +
                 "vendor_name VARCHAR(64) COMMENT '供应商名称(快照)', " +
@@ -1754,10 +1828,10 @@ log.info("已添加template_id列到tasks表");
                 "rule_name VARCHAR(64) NOT NULL COMMENT '规则名称', " +
                 "rule_name VARCHAR(64) NOT NULL COMMENT '规则名称', " +
                 "rule_type VARCHAR(32) NOT NULL COMMENT '规则类型: product_category/vendor_level/global', " +
                 "rule_type VARCHAR(32) NOT NULL COMMENT '规则类型: product_category/vendor_level/global', " +
                 "target_id BIGINT DEFAULT NULL COMMENT '关联目标ID', " +
                 "target_id BIGINT DEFAULT NULL COMMENT '关联目标ID', " +
-                "platform_profit_rate DECIMAL(5,2) NOT NULL COMMENT '平台利润率(%)', " +
-                "referrer_l1_rate DECIMAL(5,2) DEFAULT 0.00 COMMENT '直接推荐人分润比例(占利润%)', " +
-                "referrer_l2_rate DECIMAL(5,2) DEFAULT 0.00 COMMENT '间接推荐人分润比例(占利润%)', " +
-                "platform_service_rate DECIMAL(5,2) DEFAULT 0.00 COMMENT '平台服务费率(占订单金额%)', " +
+                "platform_profit_rate INT NOT NULL COMMENT '平台利润率(bps,100=1%)', " +
+                "referrer_l1_rate INT DEFAULT 0 COMMENT '直接推荐人分润比例(bps,占利润%)', " +
+                "referrer_l2_rate INT DEFAULT 0 COMMENT '间接推荐人分润比例(bps,占利润%)', " +
+                "platform_service_rate INT DEFAULT 0 COMMENT '平台服务费率(bps,占订单金额%)', " +
                 "is_active INT DEFAULT 1 COMMENT '1启用/0禁用', " +
                 "is_active INT DEFAULT 1 COMMENT '1启用/0禁用', " +
                 "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
                 "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
                 "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
                 "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
@@ -1775,13 +1849,13 @@ log.info("已添加template_id列到tasks表");
                 "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                 "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                 "order_id BIGINT NOT NULL COMMENT '订单ID', " +
                 "order_id BIGINT NOT NULL COMMENT '订单ID', " +
                 "order_no VARCHAR(32) NOT NULL COMMENT '订单号', " +
                 "order_no VARCHAR(32) NOT NULL COMMENT '订单号', " +
-                "order_amount DECIMAL(10,2) NOT NULL COMMENT '订单实付金额', " +
-                "cost_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '成本金额', " +
-                "profit_amount DECIMAL(10,2) NOT NULL COMMENT '利润金额', " +
-                "platform_retained DECIMAL(10,2) NOT NULL COMMENT '平台留存金额', " +
-                "vendor_income DECIMAL(10,2) NOT NULL COMMENT '供应商实收金额', " +
-                "commission_l1 DECIMAL(10,2) DEFAULT 0.00 COMMENT 'L1推荐人佣金', " +
-                "commission_l2 DECIMAL(10,2) DEFAULT 0.00 COMMENT 'L2推荐人佣金', " +
+                "order_amount INT NOT NULL COMMENT '订单实付金额(分)', " +
+                "cost_amount INT DEFAULT 0 COMMENT '成本金额(分)', " +
+                "profit_amount INT NOT NULL COMMENT '利润金额(分)', " +
+                "platform_retained INT NOT NULL COMMENT '平台留存金额(分)', " +
+                "vendor_income INT NOT NULL COMMENT '供应商实收金额(分)', " +
+                "commission_l1 INT DEFAULT 0 COMMENT 'L1推荐人佣金(分)', " +
+                "commission_l2 INT DEFAULT 0 COMMENT 'L2推荐人佣金(分)', " +
                 "rule_id BIGINT DEFAULT NULL COMMENT '使用的分润规则ID', " +
                 "rule_id BIGINT DEFAULT NULL COMMENT '使用的分润规则ID', " +
                 "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
                 "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
                 "INDEX idx_order (order_id), " +
                 "INDEX idx_order (order_id), " +
@@ -1829,6 +1903,21 @@ log.info("已添加template_id列到tasks表");
             log.warn("创建 danshop_addresses 表失败: {}", e.getMessage());
             log.warn("创建 danshop_addresses 表失败: {}", e.getMessage());
         }
         }
 
 
+        // ==================== 迁移: activities表添加visible_scope列 ====================
+        try {
+            Integer colExists = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activities' AND COLUMN_NAME = 'visible_scope'",
+                Integer.class);
+            if (colExists == null || colExists == 0) {
+                jdbcTemplate.execute("ALTER TABLE activities ADD COLUMN visible_scope VARCHAR(20) DEFAULT NULL COMMENT '可见范围: family(家庭)/role(角色)/user(用户)' AFTER visibility");
+                log.info("已添加visible_scope列到activities表");
+            } else {
+                log.info("activities表visible_scope列已存在,跳过");
+            }
+        } catch (Exception e) {
+            log.warn("检查/添加activities表visible_scope列失败: {}", e.getMessage());
+        }
+
         log.info("数据库迁移完成");
         log.info("数据库迁移完成");
     }
     }
 
 
@@ -2170,7 +2259,7 @@ try {
                 "content TEXT COMMENT '富文本内容', " +
                 "content TEXT COMMENT '富文本内容', " +
                 "duration INT COMMENT '建议时长(分钟)', " +
                 "duration INT COMMENT '建议时长(分钟)', " +
                 "age_range VARCHAR(50) COMMENT '适用年龄', " +
                 "age_range VARCHAR(50) COMMENT '适用年龄', " +
-                "price DECIMAL(10,2) COMMENT '参考价格', " +
+                "price INT COMMENT '参考价格(分)', " +
                 "status VARCHAR(20) DEFAULT 'DRAFT' COMMENT '状态:DRAFT/ACTIVE', " +
                 "status VARCHAR(20) DEFAULT 'DRAFT' COMMENT '状态:DRAFT/ACTIVE', " +
                 "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
                 "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
                 "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
                 "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
@@ -2185,8 +2274,8 @@ try {
                 "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                 "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                 "name VARCHAR(100) NOT NULL COMMENT '模板名称', " +
                 "name VARCHAR(100) NOT NULL COMMENT '模板名称', " +
                 "validity_months INT COMMENT '有效期(月)', " +
                 "validity_months INT COMMENT '有效期(月)', " +
-                "reference_price DECIMAL(10,2) COMMENT '参考价格', " +
-                "min_price DECIMAL(10,2) COMMENT '最低售价', " +
+                "reference_price INT COMMENT '参考价格(分)', " +
+                "min_price INT COMMENT '最低售价(分)', " +
                 "description VARCHAR(500) COMMENT '说明', " +
                 "description VARCHAR(500) COMMENT '说明', " +
                 "status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE/DISABLED', " +
                 "status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE/DISABLED', " +
                 "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
                 "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
@@ -2216,7 +2305,7 @@ try {
                 "teacher_id BIGINT NOT NULL COMMENT '成长规划师ID', " +
                 "teacher_id BIGINT NOT NULL COMMENT '成长规划师ID', " +
                 "template_id BIGINT COMMENT '关联模板', " +
                 "template_id BIGINT COMMENT '关联模板', " +
                 "name VARCHAR(100) NOT NULL COMMENT '套餐名称', " +
                 "name VARCHAR(100) NOT NULL COMMENT '套餐名称', " +
-                "price DECIMAL(10,2) NOT NULL COMMENT '销售价格', " +
+                "price INT NOT NULL COMMENT '销售价格(分)', " +
                 "validity_months INT COMMENT '有效期(月)', " +
                 "validity_months INT COMMENT '有效期(月)', " +
                 "description VARCHAR(500) COMMENT '套餐说明', " +
                 "description VARCHAR(500) COMMENT '套餐说明', " +
                 "status VARCHAR(20) DEFAULT 'DRAFT' COMMENT '状态:DRAFT/PUBLISHED/PAUSED', " +
                 "status VARCHAR(20) DEFAULT 'DRAFT' COMMENT '状态:DRAFT/PUBLISHED/PAUSED', " +
@@ -2241,7 +2330,7 @@ try {
         }
         }
 
 
         try {
         try {
-            jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN commission_rate DECIMAL(5,2) COMMENT '佣金比例' AFTER package_id");
+            jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN commission_rate INT COMMENT '佣金比例(bps,100=1%)' AFTER package_id");
         } catch (Exception e) {
         } catch (Exception e) {
         }
         }
 
 
@@ -2294,7 +2383,7 @@ try {
                 "teacher_id BIGINT NOT NULL COMMENT '成长规划师ID', " +
                 "teacher_id BIGINT NOT NULL COMMENT '成长规划师ID', " +
                 "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
                 "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
                 "package_id BIGINT NOT NULL COMMENT '套餐ID', " +
                 "package_id BIGINT NOT NULL COMMENT '套餐ID', " +
-                "amount DECIMAL(10,2) NOT NULL COMMENT '金额', " +
+                "amount INT NOT NULL COMMENT '金额(分)', " +
                 "status VARCHAR(20) DEFAULT 'PENDING' COMMENT '状态:PENDING/PAID/COMPLETED/REFUNDED', " +
                 "status VARCHAR(20) DEFAULT 'PENDING' COMMENT '状态:PENDING/PAID/COMPLETED/REFUNDED', " +
                 "pay_time DATETIME COMMENT '支付时间', " +
                 "pay_time DATETIME COMMENT '支付时间', " +
                 "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
                 "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
@@ -2310,8 +2399,8 @@ try {
                 "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                 "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                 "teacher_id BIGINT NOT NULL COMMENT '成长规划师ID', " +
                 "teacher_id BIGINT NOT NULL COMMENT '成长规划师ID', " +
                 "order_id BIGINT NOT NULL COMMENT '订单ID', " +
                 "order_id BIGINT NOT NULL COMMENT '订单ID', " +
-                "amount DECIMAL(10,2) NOT NULL COMMENT '佣金金额', " +
-                "rate DECIMAL(5,2) COMMENT '佣金比例', " +
+                "amount INT NOT NULL COMMENT '佣金金额(分)', " +
+                "rate INT COMMENT '佣金比例(bps,100=1%)', " +
                 "status VARCHAR(20) DEFAULT 'PENDING' COMMENT '状态:PENDING/COMPLETED', " +
                 "status VARCHAR(20) DEFAULT 'PENDING' COMMENT '状态:PENDING/COMPLETED', " +
                 "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
                 "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
                 ")");
                 ")");
@@ -2370,7 +2459,7 @@ try {
                 "child_id BIGINT NOT NULL COMMENT '孩子ID', " +
                 "child_id BIGINT NOT NULL COMMENT '孩子ID', " +
                 "checkin_type VARCHAR(20) NOT NULL COMMENT '打卡类型: daily_plan/saving/expense/review', " +
                 "checkin_type VARCHAR(20) NOT NULL COMMENT '打卡类型: daily_plan/saving/expense/review', " +
                 "checkin_date DATE NOT NULL COMMENT '打卡日期', " +
                 "checkin_date DATE NOT NULL COMMENT '打卡日期', " +
-                "amount DECIMAL(10,2) COMMENT '金额(收支记录)', " +
+                "amount INT COMMENT '金额(分,收支记录)', " +
                 "category VARCHAR(50) COMMENT '收支分类', " +
                 "category VARCHAR(50) COMMENT '收支分类', " +
                 "note VARCHAR(200) COMMENT '备注', " +
                 "note VARCHAR(200) COMMENT '备注', " +
                 "mood VARCHAR(20) COMMENT '心情', " +
                 "mood VARCHAR(20) COMMENT '心情', " +
@@ -2394,8 +2483,8 @@ try {
                 "insurance_company VARCHAR(100) COMMENT '保险公司', " +
                 "insurance_company VARCHAR(100) COMMENT '保险公司', " +
                 "policy_type VARCHAR(50) COMMENT '保单类型: medical/life/accident/education/endowment', " +
                 "policy_type VARCHAR(50) COMMENT '保单类型: medical/life/accident/education/endowment', " +
                 "insured_person VARCHAR(50) NOT NULL COMMENT '被保人姓名', " +
                 "insured_person VARCHAR(50) NOT NULL COMMENT '被保人姓名', " +
-                "premium DECIMAL(10,2) COMMENT '保费', " +
-                "sum_insured DECIMAL(10,2) COMMENT '保额', " +
+                "premium INT COMMENT '保费(分)', " +
+                "sum_insured INT COMMENT '保额(分)', " +
                 "start_date DATE COMMENT '生效日期', " +
                 "start_date DATE COMMENT '生效日期', " +
                 "end_date DATE COMMENT '到期日期', " +
                 "end_date DATE COMMENT '到期日期', " +
                 "renewal_type VARCHAR(20) DEFAULT 'annual' COMMENT '续费类型: annual/monthly/single', " +
                 "renewal_type VARCHAR(20) DEFAULT 'annual' COMMENT '续费类型: annual/monthly/single', " +
@@ -2512,6 +2601,24 @@ try {
             log.warn("创建contacts表可能已存在: {}", e.getMessage());
             log.warn("创建contacts表可能已存在: {}", e.getMessage());
         }
         }
 
 
+        // 创建文章阅读记录表
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS article_reading_records (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "user_id BIGINT COMMENT '用户ID', " +
+                "child_id BIGINT NOT NULL COMMENT '孩子ID', " +
+                "content VARCHAR(500) COMMENT '阅读内容摘要', " +
+                "duration_seconds INT DEFAULT 0 COMMENT '阅读时长(秒)', " +
+                "read_at DATETIME COMMENT '阅读时间', " +
+                "created_at DATETIME, " +
+                "INDEX idx_child_id (child_id), " +
+                "INDEX idx_read_at (read_at)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章阅读记录表'");
+            log.info("已创建article_reading_records表");
+        } catch (Exception e) {
+            log.warn("创建article_reading_records表可能已存在: {}", e.getMessage());
+        }
+
         // 迁移30: activities表添加visibility和visible_to字段
         // 迁移30: activities表添加visibility和visible_to字段
         try {
         try {
             jdbcTemplate.execute("ALTER TABLE activities ADD COLUMN visibility VARCHAR(20) DEFAULT 'all' COMMENT '可见性: all/member'");
             jdbcTemplate.execute("ALTER TABLE activities ADD COLUMN visibility VARCHAR(20) DEFAULT 'all' COMMENT '可见性: all/member'");
@@ -2539,7 +2646,7 @@ try {
     }
     }
 
 
     private void insertProductSeed(String name, String desc, String type,
     private void insertProductSeed(String name, String desc, String type,
-                                java.math.BigDecimal price,
+                                Integer price,
                                 String domain, String memberEligible) {
                                 String domain, String memberEligible) {
         Integer count = jdbcTemplate.queryForObject(
         Integer count = jdbcTemplate.queryForObject(
             "SELECT COUNT(*) FROM products WHERE name = ? AND product_type = ?", Integer.class, name, type);
             "SELECT COUNT(*) FROM products WHERE name = ? AND product_type = ?", Integer.class, name, type);

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

@@ -153,6 +153,22 @@ public class HealthReportController {
         return Result.success(detail);
         return Result.success(detail);
     }
     }
 
 
+    /**
+     * 获取报告完整详情(含指标明细 + 菌群数据 + 疾病风险评估)
+     */
+    @Operation(summary = "获取报告完整详情")
+    @PostMapping("/report/detail")
+    public Result<Map<String, Object>> getReportDetail(
+            @RequestBody Map<String, Object> params) {
+        Long reportId = params.get("reportId") != null
+                ? Long.valueOf(params.get("reportId").toString()) : null;
+        if (reportId == null) {
+            return Result.error("reportId不能为空");
+        }
+        Map<String, Object> detail = healthReportService.getReportDetail(reportId);
+        return Result.success(detail);
+    }
+
     /**
     /**
      * 归档报告
      * 归档报告
      */
      */

+ 80 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/ReportSurveyController.java

@@ -0,0 +1,80 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.ReportSurvey;
+import com.etotem.cfc.service.ReportSurveyService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+/**
+ * 报告调研问卷接口 — 上传报告后自动生成的问卷管理
+ */
+@Tag(name = "报告调研问卷", description = "报告上传后调研问卷的创建和提交")
+@RestController
+@RequestMapping("/api/health/survey")
+public class ReportSurveyController {
+
+    @Resource
+    private ReportSurveyService reportSurveyService;
+
+    /**
+     * 创建调研问卷(报告上传成功后自动调用)
+     */
+    @Operation(summary = "创建调研问卷")
+    @PostMapping("/create")
+    public Result<ReportSurvey> createSurvey(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId) {
+        Long reportId = params.get("reportId") != null
+                ? Long.valueOf(params.get("reportId").toString()) : null;
+        Long childId = params.get("childId") != null
+                ? Long.valueOf(params.get("childId").toString()) : null;
+        if (reportId == null || childId == null) {
+            return Result.error("reportId和childId不能为空");
+        }
+        ReportSurvey survey = reportSurveyService.createSurvey(reportId, userId, childId);
+        return Result.success(survey);
+    }
+
+    /**
+     * 提交调研问卷
+     */
+    @Operation(summary = "提交调研问卷")
+    @PostMapping("/submit")
+    public Result<ReportSurvey> submitSurvey(
+            @RequestBody Map<String, Object> params) {
+        Long surveyId = params.get("surveyId") != null
+                ? Long.valueOf(params.get("surveyId").toString()) : null;
+        String surveyData = params.get("surveyData") != null
+                ? params.get("surveyData").toString() : null;
+        if (surveyId == null || surveyData == null) {
+            return Result.error("surveyId和surveyData不能为空");
+        }
+        ReportSurvey survey = reportSurveyService.submitSurvey(surveyId, surveyData);
+        return Result.success(survey);
+    }
+
+    /**
+     * 检查报告的问卷状态
+     */
+    @Operation(summary = "检查问卷状态")
+    @PostMapping("/status")
+    public Result<Map<String, Object>> getSurveyStatus(
+            @RequestBody Map<String, Object> params) {
+        Long reportId = params.get("reportId") != null
+                ? Long.valueOf(params.get("reportId").toString()) : null;
+        if (reportId == null) {
+            return Result.error("reportId不能为空");
+        }
+        boolean completed = reportSurveyService.isSurveyCompleted(reportId);
+        ReportSurvey pending = reportSurveyService.getPendingSurvey(reportId);
+        java.util.Map<String, Object> result = new java.util.LinkedHashMap<>();
+        result.put("completed", completed);
+        result.put("pendingSurveyId", pending != null ? pending.getId() : null);
+        return Result.success(result);
+    }
+}

+ 20 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java

@@ -25,20 +25,37 @@ public class AIChatController {
     @Resource
     @Resource
     private FamilyContextService familyContextService;
     private FamilyContextService familyContextService;
 
 
-    @Operation(summary = "发送聊天消息")
+    @Operation(summary = "发送聊天消息(支持传入reportId以解读报告)")
     @PostMapping("/chat/send")
     @PostMapping("/chat/send")
     public Result<Map<String, Object>> sendMessage(
     public Result<Map<String, Object>> sendMessage(
             @RequestAttribute("userId") Long userId,
             @RequestAttribute("userId") Long userId,
             @RequestBody Map<String, String> params) {
             @RequestBody Map<String, String> params) {
         String query = params.get("query");
         String query = params.get("query");
         String conversationId = params.get("conversationId");
         String conversationId = params.get("conversationId");
+        String reportIdStr = params.get("reportId");
+        String surveyIdStr = params.get("surveyId");
 
 
         if (query == null || query.trim().isEmpty()) {
         if (query == null || query.trim().isEmpty()) {
             return Result.error("消息不能为空");
             return Result.error("消息不能为空");
         }
         }
 
 
-        // 组装家庭上下文
-        Map<String, Object> inputs = familyContextService.buildContext(userId);
+        // 组装家庭上下文(如果指定reportId/surveyId,注入报告+问卷数据)
+        Long reportId = null;
+        Long surveyId = null;
+        if (reportIdStr != null && !reportIdStr.trim().isEmpty()) {
+            reportId = Long.valueOf(reportIdStr);
+        }
+        if (surveyIdStr != null && !surveyIdStr.trim().isEmpty()) {
+            surveyId = Long.valueOf(surveyIdStr);
+        }
+        Map<String, Object> inputs;
+        if (reportId != null && surveyId != null) {
+            inputs = familyContextService.buildContext(userId, reportId, surveyId);
+        } else if (reportId != null) {
+            inputs = familyContextService.buildContext(userId, reportId);
+        } else {
+            inputs = familyContextService.buildContext(userId);
+        }
 
 
         // 调用 Dify
         // 调用 Dify
         Map<String, Object> difyResp = aiService.sendMessage(
         Map<String, Object> difyResp = aiService.sendMessage(

+ 34 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthDiseaseRisk.java

@@ -0,0 +1,34 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 疾病风险评估 — 从检测报告中提取的疾病风险数据
+ */
+@Data
+@TableName("health_disease_risk")
+public class HealthDiseaseRisk implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 关联报告ID */
+    private Long reportId;
+
+    /** 疾病名称 */
+    private String diseaseName;
+
+    /** 风险值 (0-1) */
+    private String riskValue;
+
+    /** 风险等级: 高风险/需注意/低风险 */
+    private String riskLevel;
+
+    private Date createdAt;
+}

+ 44 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthGutFlora.java

@@ -0,0 +1,44 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 菌群检测专项数据 — 针对 gut_flora 类型报告
+ * 存储每种菌种的丰度值和评估状态
+ */
+@Data
+@TableName("health_gut_flora")
+public class HealthGutFlora implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 关联报告ID */
+    private Long reportId;
+
+    /** 菌种名称 */
+    private String bacteriaName;
+
+    /** 菌种丰度值 */
+    private String bacteriaValue;
+
+    /** 正常范围 */
+    private String normalRange;
+
+    /** 状态: 正常/偏低/偏高 */
+    private String status;
+
+    /** 分类: 有益菌/有害菌/中性菌 */
+    private String category;
+
+    /** 排序 */
+    private Integer sortOrder;
+
+    private Date createdAt;
+}

+ 39 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ReportSurvey.java

@@ -0,0 +1,39 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 报告调研问卷 — 报告上传后自动生成,用户填写后进入AI报告解读
+ */
+@Data
+@TableName("health_report_survey")
+public class ReportSurvey implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 关联报告ID */
+    private Long reportId;
+
+    /** 用户ID */
+    private Long userId;
+
+    /** 孩子ID */
+    private Long childId;
+
+    /** 调研问卷结果JSON */
+    private String surveyData;
+
+    /** 状态: pending/completed */
+    private String status;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/HealthDiseaseRiskMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.HealthDiseaseRisk;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface HealthDiseaseRiskMapper extends BaseMapper<HealthDiseaseRisk> {
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/HealthGutFloraMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.HealthGutFlora;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface HealthGutFloraMapper extends BaseMapper<HealthGutFlora> {
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/ReportSurveyMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ReportSurvey;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ReportSurveyMapper extends BaseMapper<ReportSurvey> {
+}

+ 84 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyContextService.java

@@ -1,6 +1,8 @@
 package com.etotem.cfc.service;
 package com.etotem.cfc.service;
 
 
 import com.etotem.cfc.dto.ChildInfoDTO;
 import com.etotem.cfc.dto.ChildInfoDTO;
+import com.etotem.cfc.entity.HealthReport;
+import com.etotem.cfc.entity.ReportSurvey;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
@@ -24,6 +26,12 @@ public class FamilyContextService {
     @Resource
     @Resource
     private TaskService taskService;
     private TaskService taskService;
 
 
+    @Resource
+    private HealthReportService healthReportService;
+
+    @Resource
+    private ReportSurveyService reportSurveyService;
+
     /**
     /**
      * 构建当前用户的家庭上下文
      * 构建当前用户的家庭上下文
      *
      *
@@ -43,6 +51,22 @@ public class FamilyContextService {
             ctx.put("user", userInfo);
             ctx.put("user", userInfo);
         }
         }
 
 
+        // 1.5 当前用户的最新健康报告(家长查看自己体检报告时)
+        if (currentUser != null && "parent".equals(currentUser.getRole())) {
+            HealthReport myReport = healthReportService.getLatestReport(userId);
+            if (myReport != null) {
+                Map<String, Object> reportInfo = new LinkedHashMap<>();
+                reportInfo.put("id", myReport.getId());
+                reportInfo.put("reportDate", myReport.getReportDate() != null ? myReport.getReportDate().toString() : "");
+                reportInfo.put("reportType", myReport.getReportType());
+                reportInfo.put("overallScore", myReport.getOverallScore());
+                reportInfo.put("gutHealthScore", myReport.getGutHealthScore());
+                reportInfo.put("chronicDiseaseScore", myReport.getChronicDiseaseScore());
+                reportInfo.put("nutritionScore", myReport.getNutritionScore());
+                ctx.put("myHealthReport", reportInfo);
+            }
+        }
+
         // 2. 孩子列表
         // 2. 孩子列表
         List<ChildInfoDTO> children = userService.getChildren(userId);
         List<ChildInfoDTO> children = userService.getChildren(userId);
         List<Map<String, Object>> childList = new ArrayList<>();
         List<Map<String, Object>> childList = new ArrayList<>();
@@ -59,6 +83,21 @@ public class FamilyContextService {
                 Map<String, Object> taskStats = taskService.getChildTaskStats(child.getId());
                 Map<String, Object> taskStats = taskService.getChildTaskStats(child.getId());
                 c.put("taskCompletionRate", taskStats.getOrDefault("completionRate", 0));
                 c.put("taskCompletionRate", taskStats.getOrDefault("completionRate", 0));
 
 
+                // 孩子的最新健康报告
+                HealthReport childReport = healthReportService.getLatestReport(child.getId());
+                if (childReport != null) {
+                    Map<String, Object> reportInfo = new LinkedHashMap<>();
+                    reportInfo.put("id", childReport.getId());
+                    reportInfo.put("reportDate", childReport.getReportDate() != null ? childReport.getReportDate().toString() : "");
+                    reportInfo.put("reportType", childReport.getReportType());
+                    reportInfo.put("overallScore", childReport.getOverallScore());
+                    reportInfo.put("gutHealthScore", childReport.getGutHealthScore());
+                    reportInfo.put("chronicDiseaseScore", childReport.getChronicDiseaseScore());
+                    reportInfo.put("nutritionScore", childReport.getNutritionScore());
+                    reportInfo.put("gutType", childReport.getGutType());
+                    c.put("healthReport", reportInfo);
+                }
+
                 childList.add(c);
                 childList.add(c);
             }
             }
         }
         }
@@ -66,4 +105,49 @@ public class FamilyContextService {
 
 
         return ctx;
         return ctx;
     }
     }
+
+    /**
+     * 构建包含指定报告详情的上下文
+     *
+     * @param userId   当前用户ID
+     * @param reportId 指定报告ID(用于AI解读该报告)
+     * @return 上下文 Map
+     */
+    public Map<String, Object> buildContext(Long userId, Long reportId) {
+        Map<String, Object> ctx = buildContext(userId);
+
+        if (reportId != null) {
+            Map<String, Object> reportDetail = healthReportService.getReportDetail(reportId);
+            ctx.put("reportDetail", reportDetail);
+
+            // 注入已完成的调研问卷数据
+            ReportSurvey survey = reportSurveyService.getCompletedSurvey(reportId);
+            if (survey != null && survey.getSurveyData() != null) {
+                ctx.put("reportSurvey", survey.getSurveyData());
+            }
+        }
+
+        return ctx;
+    }
+
+    /**
+     * 构建包含指定报告详情和调研问卷的上下文
+     *
+     * @param userId   当前用户ID
+     * @param reportId 指定报告ID
+     * @param surveyId 指定问卷ID(提交后立即使用)
+     * @return 上下文 Map
+     */
+    public Map<String, Object> buildContext(Long userId, Long reportId, Long surveyId) {
+        Map<String, Object> ctx = buildContext(userId, reportId);
+
+        if (surveyId != null) {
+            ReportSurvey survey = reportSurveyService.getCompletedSurvey(reportId);
+            if (survey != null && survey.getSurveyData() != null) {
+                ctx.put("reportSurvey", survey.getSurveyData());
+            }
+        }
+
+        return ctx;
+    }
 }
 }

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

@@ -1,8 +1,12 @@
 package com.etotem.cfc.service;
 package com.etotem.cfc.service;
 
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.HealthDiseaseRisk;
+import com.etotem.cfc.entity.HealthGutFlora;
 import com.etotem.cfc.entity.HealthIndicator;
 import com.etotem.cfc.entity.HealthIndicator;
 import com.etotem.cfc.entity.HealthReport;
 import com.etotem.cfc.entity.HealthReport;
+import com.etotem.cfc.mapper.HealthDiseaseRiskMapper;
+import com.etotem.cfc.mapper.HealthGutFloraMapper;
 import com.etotem.cfc.mapper.HealthIndicatorMapper;
 import com.etotem.cfc.mapper.HealthIndicatorMapper;
 import com.etotem.cfc.mapper.HealthReportMapper;
 import com.etotem.cfc.mapper.HealthReportMapper;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
@@ -12,7 +16,9 @@ import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
 import javax.annotation.Resource;
 import java.util.Date;
 import java.util.Date;
 import java.util.Collections;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.List;
+import java.util.Map;
 
 
 /**
 /**
  * 健康检测报告服务 — 管理体检报告/菌群检测报告的录入与查询
  * 健康检测报告服务 — 管理体检报告/菌群检测报告的录入与查询
@@ -28,6 +34,12 @@ public class HealthReportService {
     @Resource
     @Resource
     private HealthIndicatorMapper healthIndicatorMapper;
     private HealthIndicatorMapper healthIndicatorMapper;
 
 
+    @Resource
+    private HealthGutFloraMapper healthGutFloraMapper;
+
+    @Resource
+    private HealthDiseaseRiskMapper healthDiseaseRiskMapper;
+
     /**
     /**
      * 创建健康报告(含指标列表)
      * 创建健康报告(含指标列表)
      */
      */
@@ -170,6 +182,80 @@ public class HealthReportService {
         }
         }
     }
     }
 
 
+    // ======================== 菌群+疾病风险专数据 ========================
+
+    /**
+     * 获取报告的菌群数据
+     */
+    public List<HealthGutFlora> getGutFloraByReportId(Long reportId) {
+        return healthGutFloraMapper.selectList(
+                new LambdaQueryWrapper<HealthGutFlora>()
+                        .eq(HealthGutFlora::getReportId, reportId)
+                        .orderByAsc(HealthGutFlora::getSortOrder)
+        );
+    }
+
+    /**
+     * 保存菌群数据
+     */
+    @Transactional
+    public void saveGutFlora(Long reportId, List<HealthGutFlora> floraList) {
+        if (floraList == null || floraList.isEmpty()) return;
+        for (int i = 0; i < floraList.size(); i++) {
+            HealthGutFlora item = floraList.get(i);
+            item.setReportId(reportId);
+            item.setSortOrder(i);
+            item.setCreatedAt(new Date());
+            healthGutFloraMapper.insert(item);
+        }
+    }
+
+    /**
+     * 获取报告的疾病风险评估
+     */
+    public List<HealthDiseaseRisk> getDiseaseRisksByReportId(Long reportId) {
+        return healthDiseaseRiskMapper.selectList(
+                new LambdaQueryWrapper<HealthDiseaseRisk>()
+                        .eq(HealthDiseaseRisk::getReportId, reportId)
+        );
+    }
+
+    /**
+     * 保存疾病风险评估
+     */
+    @Transactional
+    public void saveDiseaseRisks(Long reportId, List<HealthDiseaseRisk> riskList) {
+        if (riskList == null || riskList.isEmpty()) return;
+        for (HealthDiseaseRisk item : riskList) {
+            item.setReportId(reportId);
+            item.setCreatedAt(new Date());
+            healthDiseaseRiskMapper.insert(item);
+        }
+    }
+
+    /**
+     * 获取报告完整详情(含指标明细 + 菌群数据 + 疾病风险评估)
+     */
+    public Map<String, Object> getReportDetail(Long reportId) {
+        Map<String, Object> detail = new HashMap<>();
+
+        HealthReport report = healthReportMapper.selectById(reportId);
+        detail.put("report", report);
+
+        if (report != null) {
+            List<HealthIndicator> indicators = getReportIndicators(reportId);
+            detail.put("indicators", indicators);
+
+            List<HealthGutFlora> gutFlora = getGutFloraByReportId(reportId);
+            detail.put("gutFlora", gutFlora);
+
+            List<HealthDiseaseRisk> diseaseRisks = getDiseaseRisksByReportId(reportId);
+            detail.put("diseaseRisks", diseaseRisks);
+        }
+
+        return detail;
+    }
+
     private int clampScore(Integer score) {
     private int clampScore(Integer score) {
         if (score == null) return 0;
         if (score == null) return 0;
         return Math.min(Math.max(score, 0), 100);
         return Math.min(Math.max(score, 0), 100);

+ 93 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ReportSurveyService.java

@@ -0,0 +1,93 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.ReportSurvey;
+import com.etotem.cfc.mapper.ReportSurveyMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+
+/**
+ * 报告调研问卷服务 — 报告上传后生成问卷,收集用户健康信息
+ */
+@Slf4j
+@Service
+public class ReportSurveyService {
+
+    @Resource
+    private ReportSurveyMapper reportSurveyMapper;
+
+    /**
+     * 为报告创建调研问卷
+     */
+    @Transactional
+    public ReportSurvey createSurvey(Long reportId, Long userId, Long childId) {
+        ReportSurvey survey = new ReportSurvey();
+        survey.setReportId(reportId);
+        survey.setUserId(userId);
+        survey.setChildId(childId);
+        survey.setStatus("pending");
+        survey.setCreatedAt(new Date());
+        survey.setUpdatedAt(new Date());
+        reportSurveyMapper.insert(survey);
+        log.info("报告调研问卷已创建: surveyId={}, reportId={}", survey.getId(), reportId);
+        return survey;
+    }
+
+    /**
+     * 提交调研问卷答案
+     */
+    @Transactional
+    public ReportSurvey submitSurvey(Long surveyId, String surveyData) {
+        ReportSurvey survey = reportSurveyMapper.selectById(surveyId);
+        if (survey == null) {
+            throw new RuntimeException("调研问卷不存在: " + surveyId);
+        }
+        survey.setSurveyData(surveyData);
+        survey.setStatus("completed");
+        survey.setUpdatedAt(new Date());
+        reportSurveyMapper.updateById(survey);
+        log.info("报告调研问卷已提交: surveyId={}", surveyId);
+        return survey;
+    }
+
+    /**
+     * 检查指定报告的调研问卷是否已完成
+     */
+    public boolean isSurveyCompleted(Long reportId) {
+        ReportSurvey survey = reportSurveyMapper.selectOne(
+                new LambdaQueryWrapper<ReportSurvey>()
+                        .eq(ReportSurvey::getReportId, reportId)
+                        .eq(ReportSurvey::getStatus, "completed")
+                        .last("LIMIT 1")
+        );
+        return survey != null;
+    }
+
+    /**
+     * 获取报告的调研问卷(已完成状态)
+     */
+    public ReportSurvey getCompletedSurvey(Long reportId) {
+        return reportSurveyMapper.selectOne(
+                new LambdaQueryWrapper<ReportSurvey>()
+                        .eq(ReportSurvey::getReportId, reportId)
+                        .eq(ReportSurvey::getStatus, "completed")
+                        .last("LIMIT 1")
+        );
+    }
+
+    /**
+     * 获取报告待填写的调研问卷
+     */
+    public ReportSurvey getPendingSurvey(Long reportId) {
+        return reportSurveyMapper.selectOne(
+                new LambdaQueryWrapper<ReportSurvey>()
+                        .eq(ReportSurvey::getReportId, reportId)
+                        .eq(ReportSurvey::getStatus, "pending")
+                        .last("LIMIT 1")
+        );
+    }
+}

+ 51 - 51
cfc-backend/src/main/resources/schema.sql

@@ -174,7 +174,7 @@ CREATE TABLE IF NOT EXISTS task_reviews (
     photo_url VARCHAR(255),
     photo_url VARCHAR(255),
     voice_note_url VARCHAR(255),
     voice_note_url VARCHAR(255),
     ai_result ENUM('pass', 'review', 'pending'),
     ai_result ENUM('pass', 'review', 'pending'),
-    ai_confidence DECIMAL(3,2),
+    ai_confidence INT COMMENT 'AI置信度(0-100)',
     ai_suggestion VARCHAR(255),
     ai_suggestion VARCHAR(255),
     ai_threshold ENUM('strict', 'normal', 'loose') DEFAULT 'normal',
     ai_threshold ENUM('strict', 'normal', 'loose') DEFAULT 'normal',
     parent_result ENUM('pass', 'reject'),
     parent_result ENUM('pass', 'reject'),
@@ -287,7 +287,7 @@ CREATE TABLE IF NOT EXISTS guides (
     university VARCHAR(200) COMMENT '毕业院校',
     university VARCHAR(200) COMMENT '毕业院校',
     education_system VARCHAR(32) COMMENT '教育体系(参考education_systems表)',
     education_system VARCHAR(32) COMMENT '教育体系(参考education_systems表)',
     experience_years TINYINT DEFAULT 0 COMMENT '从业年限',
     experience_years TINYINT DEFAULT 0 COMMENT '从业年限',
-    rating DECIMAL(3,2) DEFAULT 5.00 COMMENT '评分',
+    rating INT DEFAULT 500 COMMENT '评分(百分位,500=5.00)',
     total_appointments INT DEFAULT 0 COMMENT '总服务次数',
     total_appointments INT DEFAULT 0 COMMENT '总服务次数',
     level VARCHAR(20) NOT NULL DEFAULT 'junior' COMMENT '等级: junior-初级, intermediate-中级, senior-高级',
     level VARCHAR(20) NOT NULL DEFAULT 'junior' COMMENT '等级: junior-初级, intermediate-中级, senior-高级',
     status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态: active-活跃, inactive-停用',
     status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态: active-活跃, inactive-停用',
@@ -297,7 +297,7 @@ CREATE TABLE IF NOT EXISTS guides (
     phone VARCHAR(20) COMMENT '联系电话',
     phone VARCHAR(20) COMMENT '联系电话',
     intro TEXT COMMENT '个人介绍',
     intro TEXT COMMENT '个人介绍',
     avatar VARCHAR(500) COMMENT '头像URL',
     avatar VARCHAR(500) COMMENT '头像URL',
-    commission_rate DECIMAL(5,4) DEFAULT 0.1000 COMMENT '佣金比例',
+    commission_rate INT DEFAULT 1000 COMMENT '佣金比例(基点,1000=10%)',
     street_id BIGINT COMMENT '街道ID',
     street_id BIGINT COMMENT '街道ID',
     province VARCHAR(50) COMMENT '省份',
     province VARCHAR(50) COMMENT '省份',
     city VARCHAR(50) COMMENT '城市',
     city VARCHAR(50) COMMENT '城市',
@@ -461,7 +461,7 @@ CREATE TABLE IF NOT EXISTS guide_families (
     guide_id BIGINT NOT NULL COMMENT '成长规划师用户ID',
     guide_id BIGINT NOT NULL COMMENT '成长规划师用户ID',
     family_id BIGINT NOT NULL COMMENT '家庭ID',
     family_id BIGINT NOT NULL COMMENT '家庭ID',
     service_type VARCHAR(20) DEFAULT 'consulting' COMMENT '服务类型: monthly/yearly/consulting',
     service_type VARCHAR(20) DEFAULT 'consulting' COMMENT '服务类型: monthly/yearly/consulting',
-    service_price DECIMAL(10,2) DEFAULT 0 COMMENT '服务价格',
+    service_price INT DEFAULT 0 COMMENT '服务价格(分)',
     status VARCHAR(20) DEFAULT 'binding' COMMENT '状态: binding/paused/cancelled',
     status VARCHAR(20) DEFAULT 'binding' COMMENT '状态: binding/paused/cancelled',
     bound_at DATETIME COMMENT '绑定时间',
     bound_at DATETIME COMMENT '绑定时间',
     expire_at DATETIME COMMENT '服务到期时间',
     expire_at DATETIME COMMENT '服务到期时间',
@@ -481,8 +481,8 @@ CREATE TABLE IF NOT EXISTS task_template_packages (
     cycle_days INT NOT NULL COMMENT '周期天数',
     cycle_days INT NOT NULL COMMENT '周期天数',
     category VARCHAR(50) COMMENT '分类',
     category VARCHAR(50) COMMENT '分类',
     tags JSON COMMENT '标签',
     tags JSON COMMENT '标签',
-    price DECIMAL(10,2) DEFAULT 0 COMMENT '售价',
-    platform_fee DECIMAL(5,2) DEFAULT 0 COMMENT '平台服务费',
+    price INT DEFAULT 0 COMMENT '售价(分)',
+    platform_fee INT DEFAULT 0 COMMENT '平台服务费(基点)',
     is_public TINYINT DEFAULT 0 COMMENT '是否公开',
     is_public TINYINT DEFAULT 0 COMMENT '是否公开',
     share_code VARCHAR(50) UNIQUE COMMENT '分享码',
     share_code VARCHAR(50) UNIQUE COMMENT '分享码',
     cover_image VARCHAR(500) COMMENT '封面图',
     cover_image VARCHAR(500) COMMENT '封面图',
@@ -555,8 +555,8 @@ CREATE TABLE IF NOT EXISTS package_orders (
     family_id BIGINT NOT NULL COMMENT '家庭ID',
     family_id BIGINT NOT NULL COMMENT '家庭ID',
     package_id BIGINT NOT NULL COMMENT '套餐ID',
     package_id BIGINT NOT NULL COMMENT '套餐ID',
     package_name VARCHAR(100) COMMENT '套餐名称',
     package_name VARCHAR(100) COMMENT '套餐名称',
-    price DECIMAL(10,2) NOT NULL COMMENT '支付金额',
-    platform_fee DECIMAL(10,2) DEFAULT 0 COMMENT '平台服务费',
+    price INT NOT NULL COMMENT '支付金额(分)',
+    platform_fee INT DEFAULT 0 COMMENT '平台服务费(分)',
     guide_id BIGINT COMMENT '成长规划师ID',
     guide_id BIGINT COMMENT '成长规划师ID',
     status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/paid/cancelled/refunded',
     status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/paid/cancelled/refunded',
     pay_method VARCHAR(20) COMMENT '支付方式',
     pay_method VARCHAR(20) COMMENT '支付方式',
@@ -733,7 +733,7 @@ CREATE TABLE IF NOT EXISTS assessment_materials (
 CREATE TABLE IF NOT EXISTS assessment_points_config (
 CREATE TABLE IF NOT EXISTS assessment_points_config (
     id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
     id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
     points_value INT DEFAULT 100 COMMENT '完成测评获得积分',
     points_value INT DEFAULT 100 COMMENT '完成测评获得积分',
-    points_value_yuan DECIMAL(10,2) DEFAULT 0.01 COMMENT '1积分等于多少元',
+    points_value_yuan INT DEFAULT 1 COMMENT '1积分等于多少分(原元→现分)',
     is_active TINYINT DEFAULT 1 COMMENT '是否启用',
     is_active TINYINT DEFAULT 1 COMMENT '是否启用',
     created_at DATETIME COMMENT '创建时间',
     created_at DATETIME COMMENT '创建时间',
     updated_at DATETIME COMMENT '更新时间'
     updated_at DATETIME COMMENT '更新时间'
@@ -808,7 +808,7 @@ CREATE TABLE IF NOT EXISTS guide_applications (
     current_level VARCHAR(20) DEFAULT NULL COMMENT '当前等级(升级时填写)',
     current_level VARCHAR(20) DEFAULT NULL COMMENT '当前等级(升级时填写)',
     package_id BIGINT COMMENT '申请的套餐ID',
     package_id BIGINT COMMENT '申请的套餐ID',
     payment_proof VARCHAR(500) COMMENT '付款凭证URL',
     payment_proof VARCHAR(500) COMMENT '付款凭证URL',
-    payment_amount DECIMAL(10,2) COMMENT '付款金额',
+    payment_amount INT COMMENT '付款金额(分)',
     address VARCHAR(500) COMMENT '地址',
     address VARCHAR(500) COMMENT '地址',
     bank_card VARCHAR(50) COMMENT '银行卡号',
     bank_card VARCHAR(50) COMMENT '银行卡号',
     bank_name VARCHAR(100) COMMENT '开户行',
     bank_name VARCHAR(100) COMMENT '开户行',
@@ -976,7 +976,7 @@ CREATE TABLE IF NOT EXISTS products (
     intro TEXT COMMENT '详细介绍',
     intro TEXT COMMENT '详细介绍',
     cover_image VARCHAR(500) COMMENT '封面图片',
     cover_image VARCHAR(500) COMMENT '封面图片',
     images TEXT COMMENT '多张图片JSON',
     images TEXT COMMENT '多张图片JSON',
-    price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '价格',
+    price INT NOT NULL DEFAULT 0 COMMENT '价格(分)',
     stock INT NOT NULL DEFAULT 0 COMMENT '库存',
     stock INT NOT NULL DEFAULT 0 COMMENT '库存',
     sales_count INT NOT NULL DEFAULT 0 COMMENT '销量',
     sales_count INT NOT NULL DEFAULT 0 COMMENT '销量',
     product_type VARCHAR(32) NOT NULL COMMENT '商品类型: activity/course/physical/digital/service',
     product_type VARCHAR(32) NOT NULL COMMENT '商品类型: activity/course/physical/digital/service',
@@ -984,10 +984,10 @@ CREATE TABLE IF NOT EXISTS products (
     vendor_name VARCHAR(100) COMMENT '供应商名称',
     vendor_name VARCHAR(100) COMMENT '供应商名称',
     status VARCHAR(32) NOT NULL DEFAULT 'draft' COMMENT '状态: draft/pending/approved/rejected/on_shelf/off_shelf',
     status VARCHAR(32) NOT NULL DEFAULT 'draft' COMMENT '状态: draft/pending/approved/rejected/on_shelf/off_shelf',
     reject_reason VARCHAR(500) COMMENT '审核拒绝原因',
     reject_reason VARCHAR(500) COMMENT '审核拒绝原因',
-    member_price DECIMAL(10,2) COMMENT '会员价',
+    member_price INT COMMENT '会员价(分)',
     member_eligible VARCHAR(32) DEFAULT 'none' COMMENT '会员资格: all/budget/none',
     member_eligible VARCHAR(32) DEFAULT 'none' COMMENT '会员资格: all/budget/none',
     domain VARCHAR(32) COMMENT '五维领域: action/mind/body/wisdom/wealth',
     domain VARCHAR(32) COMMENT '五维领域: action/mind/body/wisdom/wealth',
-    profit_rate DECIMAL(5,2) DEFAULT 0.00 COMMENT '平台利润率(%)',
+    profit_rate INT DEFAULT 0 COMMENT '平台利润率(千分比)',
     external_source VARCHAR(64) COMMENT '外部来源系统',
     external_source VARCHAR(64) COMMENT '外部来源系统',
     external_id VARCHAR(128) COMMENT '外部来源ID',
     external_id VARCHAR(128) COMMENT '外部来源ID',
     external_data TEXT COMMENT '外部来源数据JSON',
     external_data TEXT COMMENT '外部来源数据JSON',
@@ -1005,8 +1005,8 @@ CREATE TABLE IF NOT EXISTS product_orders (
     buyer_id BIGINT NOT NULL COMMENT '购买者ID',
     buyer_id BIGINT NOT NULL COMMENT '购买者ID',
     family_id BIGINT COMMENT '家庭ID',
     family_id BIGINT COMMENT '家庭ID',
     quantity INT NOT NULL DEFAULT 1 COMMENT '数量',
     quantity INT NOT NULL DEFAULT 1 COMMENT '数量',
-    unit_price DECIMAL(10,2) NOT NULL COMMENT '单价',
-    total_amount DECIMAL(10,2) NOT NULL COMMENT '总金额',
+    unit_price INT NOT NULL COMMENT '单价(分)',
+    total_amount INT NOT NULL COMMENT '总金额(分)',
     status VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/paid/completed/cancelled/refunded',
     status VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/paid/completed/cancelled/refunded',
     payment_method VARCHAR(32) COMMENT '支付方式: wechat/points/membership',
     payment_method VARCHAR(32) COMMENT '支付方式: wechat/points/membership',
     remark VARCHAR(500) COMMENT '备注',
     remark VARCHAR(500) COMMENT '备注',
@@ -1097,7 +1097,7 @@ CREATE TABLE IF NOT EXISTS energy_source_config (
     source_id BIGINT NOT NULL COMMENT '来源业务表ID',
     source_id BIGINT NOT NULL COMMENT '来源业务表ID',
     source_name VARCHAR(200) COMMENT '冗余名称',
     source_name VARCHAR(200) COMMENT '冗余名称',
     dimension_id BIGINT NOT NULL COMMENT '维度ID',
     dimension_id BIGINT NOT NULL COMMENT '维度ID',
-    ratio DECIMAL(5,4) NOT NULL COMMENT '占比如0.6000=60%',
+    ratio INT NOT NULL COMMENT '占比(基点,6000=60%)',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     INDEX idx_source (source_type, source_id)
     INDEX idx_source (source_type, source_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务-维度比例配置表';
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务-维度比例配置表';
@@ -1145,9 +1145,9 @@ CREATE TABLE IF NOT EXISTS commission_records (
     buyer_id BIGINT NOT NULL COMMENT '购买人ID',
     buyer_id BIGINT NOT NULL COMMENT '购买人ID',
     commission_type VARCHAR(16) NOT NULL COMMENT '佣金类型: member/profit',
     commission_type VARCHAR(16) NOT NULL COMMENT '佣金类型: member/profit',
     level INT DEFAULT 1 COMMENT '佣金层级: 1=直接推荐, 2=间接推荐',
     level INT DEFAULT 1 COMMENT '佣金层级: 1=直接推荐, 2=间接推荐',
-    order_amount DECIMAL(10,2) NOT NULL COMMENT '订单金额',
-    profit_rate DECIMAL(5,2) DEFAULT 0.00 COMMENT '利润率(%)',
-    commission_amount DECIMAL(10,2) NOT NULL COMMENT '佣金金额',
+    order_amount INT NOT NULL COMMENT '订单金额(分)',
+    profit_rate INT DEFAULT 0 COMMENT '利润率(千分比,如235=23.5%)',
+    commission_amount INT NOT NULL COMMENT '佣金金额(分)',
     status VARCHAR(16) NOT NULL DEFAULT 'settled' COMMENT '状态: pending/settled',
     status VARCHAR(16) NOT NULL DEFAULT 'settled' COMMENT '状态: pending/settled',
     remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
     remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
     created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@@ -1162,7 +1162,7 @@ CREATE TABLE IF NOT EXISTS commission_records (
 CREATE TABLE IF NOT EXISTS withdrawal_requests (
 CREATE TABLE IF NOT EXISTS withdrawal_requests (
     id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
     id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
     user_id BIGINT NOT NULL COMMENT '用户ID',
     user_id BIGINT NOT NULL COMMENT '用户ID',
-    amount DECIMAL(10,2) NOT NULL COMMENT '提现金额',
+    amount INT NOT NULL COMMENT '提现金额(分)',
     account_info TEXT COMMENT '账户信息(JSON)',
     account_info TEXT COMMENT '账户信息(JSON)',
     status VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/approved/rejected',
     status VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/approved/rejected',
     audit_by BIGINT DEFAULT NULL COMMENT '审核人ID',
     audit_by BIGINT DEFAULT NULL COMMENT '审核人ID',
@@ -1212,7 +1212,7 @@ CREATE TABLE IF NOT EXISTS service_contents (
     content TEXT COMMENT '富文本内容',
     content TEXT COMMENT '富文本内容',
     duration INT COMMENT '建议时长(分钟)',
     duration INT COMMENT '建议时长(分钟)',
     age_range VARCHAR(50) COMMENT '适用年龄',
     age_range VARCHAR(50) COMMENT '适用年龄',
-    price DECIMAL(10,2) COMMENT '参考价格',
+    price INT COMMENT '参考价格(分)',
     status VARCHAR(20) DEFAULT 'DRAFT' COMMENT '状态: DRAFT/ACTIVE',
     status VARCHAR(20) DEFAULT 'DRAFT' COMMENT '状态: DRAFT/ACTIVE',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
@@ -1223,8 +1223,8 @@ CREATE TABLE IF NOT EXISTS guide_package_templates (
     id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
     id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
     name VARCHAR(100) NOT NULL COMMENT '模板名称',
     name VARCHAR(100) NOT NULL COMMENT '模板名称',
     validity_months INT COMMENT '有效期(月)',
     validity_months INT COMMENT '有效期(月)',
-    reference_price DECIMAL(10,2) COMMENT '参考价格',
-    min_price DECIMAL(10,2) COMMENT '最低售价',
+    reference_price INT COMMENT '参考价格(分)',
+    min_price INT COMMENT '最低售价(分)',
     description VARCHAR(500) COMMENT '说明',
     description VARCHAR(500) COMMENT '说明',
     status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '状态: ACTIVE/DISABLED',
     status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '状态: ACTIVE/DISABLED',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@@ -1245,12 +1245,12 @@ CREATE TABLE IF NOT EXISTS guide_packages (
     id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
     id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
     guide_id BIGINT COMMENT '成长规划师ID',
     guide_id BIGINT COMMENT '成长规划师ID',
     package_id BIGINT COMMENT '关联模板',
     package_id BIGINT COMMENT '关联模板',
-    commission_rate DECIMAL(5,2) COMMENT '佣金比例',
+    commission_rate INT COMMENT '佣金比例(基点,1000=10%)',
     teacher_id BIGINT COMMENT '成长规划师ID(旧字段)',
     teacher_id BIGINT COMMENT '成长规划师ID(旧字段)',
     template_id BIGINT COMMENT '关联模板(旧字段)',
     template_id BIGINT COMMENT '关联模板(旧字段)',
     name VARCHAR(100) NOT NULL COMMENT '套餐名称',
     name VARCHAR(100) NOT NULL COMMENT '套餐名称',
     level VARCHAR(20) COMMENT '套餐等级',
     level VARCHAR(20) COMMENT '套餐等级',
-    price DECIMAL(10,2) NOT NULL COMMENT '销售价格',
+    price INT NOT NULL COMMENT '销售价格(分)',
     validity_months INT COMMENT '有效期(月)',
     validity_months INT COMMENT '有效期(月)',
     validity_days INT DEFAULT 0 COMMENT '有效期天数',
     validity_days INT DEFAULT 0 COMMENT '有效期天数',
     description VARCHAR(500) COMMENT '套餐说明',
     description VARCHAR(500) COMMENT '套餐说明',
@@ -1278,7 +1278,7 @@ CREATE TABLE IF NOT EXISTS guide_orders (
     teacher_id BIGINT NOT NULL COMMENT '成长规划师ID',
     teacher_id BIGINT NOT NULL COMMENT '成长规划师ID',
     family_id BIGINT NOT NULL COMMENT '家庭ID',
     family_id BIGINT NOT NULL COMMENT '家庭ID',
     package_id BIGINT NOT NULL COMMENT '套餐ID',
     package_id BIGINT NOT NULL COMMENT '套餐ID',
-    amount DECIMAL(10,2) NOT NULL COMMENT '金额',
+    amount INT NOT NULL COMMENT '金额(分)',
     status VARCHAR(20) DEFAULT 'PENDING' COMMENT '状态: PENDING/PAID/COMPLETED/REFUNDED',
     status VARCHAR(20) DEFAULT 'PENDING' COMMENT '状态: PENDING/PAID/COMPLETED/REFUNDED',
     pay_time DATETIME COMMENT '支付时间',
     pay_time DATETIME COMMENT '支付时间',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@@ -1290,8 +1290,8 @@ CREATE TABLE IF NOT EXISTS guide_commissions (
     id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
     id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
     teacher_id BIGINT NOT NULL COMMENT '成长规划师ID',
     teacher_id BIGINT NOT NULL COMMENT '成长规划师ID',
     order_id BIGINT NOT NULL COMMENT '订单ID',
     order_id BIGINT NOT NULL COMMENT '订单ID',
-    amount DECIMAL(10,2) NOT NULL COMMENT '佣金金额',
-    rate DECIMAL(5,2) COMMENT '佣金比例',
+    amount INT NOT NULL COMMENT '佣金金额(分)',
+    rate INT COMMENT '佣金比例(基点)',
     status VARCHAR(20) DEFAULT 'PENDING' COMMENT '状态: PENDING/COMPLETED',
     status VARCHAR(20) DEFAULT 'PENDING' COMMENT '状态: PENDING/COMPLETED',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='佣金明细表';
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='佣金明细表';
@@ -1334,7 +1334,7 @@ CREATE TABLE IF NOT EXISTS finance_checkins (
     child_id BIGINT NOT NULL COMMENT '孩子ID',
     child_id BIGINT NOT NULL COMMENT '孩子ID',
     checkin_type VARCHAR(20) NOT NULL COMMENT '打卡类型: daily_plan/saving/expense/review',
     checkin_type VARCHAR(20) NOT NULL COMMENT '打卡类型: daily_plan/saving/expense/review',
     checkin_date DATE NOT NULL COMMENT '打卡日期',
     checkin_date DATE NOT NULL COMMENT '打卡日期',
-    amount DECIMAL(10,2) COMMENT '金额(收支记录)',
+    amount INT COMMENT '金额(分,收支记录)',
     category VARCHAR(50) COMMENT '收支分类',
     category VARCHAR(50) COMMENT '收支分类',
     note VARCHAR(200) COMMENT '备注',
     note VARCHAR(200) COMMENT '备注',
     mood VARCHAR(20) COMMENT '心情',
     mood VARCHAR(20) COMMENT '心情',
@@ -1353,8 +1353,8 @@ CREATE TABLE IF NOT EXISTS insurance_policies (
     insurance_company VARCHAR(100) COMMENT '保险公司',
     insurance_company VARCHAR(100) COMMENT '保险公司',
     policy_type VARCHAR(50) COMMENT '保单类型: medical/life/accident/education/endowment',
     policy_type VARCHAR(50) COMMENT '保单类型: medical/life/accident/education/endowment',
     insured_person VARCHAR(50) NOT NULL COMMENT '被保人姓名',
     insured_person VARCHAR(50) NOT NULL COMMENT '被保人姓名',
-    premium DECIMAL(10,2) COMMENT '保费',
-    sum_insured DECIMAL(10,2) COMMENT '保额',
+    premium INT COMMENT '保费(分)',
+    sum_insured INT COMMENT '保额(分)',
     start_date DATE COMMENT '生效日期',
     start_date DATE COMMENT '生效日期',
     end_date DATE COMMENT '到期日期',
     end_date DATE COMMENT '到期日期',
     renewal_type VARCHAR(20) DEFAULT 'annual' COMMENT '续费类型: annual/monthly/single',
     renewal_type VARCHAR(20) DEFAULT 'annual' COMMENT '续费类型: annual/monthly/single',
@@ -1442,8 +1442,8 @@ CREATE TABLE IF NOT EXISTS family_member_attributes (
     wuxing_elements  JSON         COMMENT '五行星盘元素JSON',
     wuxing_elements  JSON         COMMENT '五行星盘元素JSON',
     zodiac           VARCHAR(16)  COMMENT '生肖: rat/ox/tiger/rabbit/dragon/snake/horse/sheep/monkey/rooster/dog/pig',
     zodiac           VARCHAR(16)  COMMENT '生肖: rat/ox/tiger/rabbit/dragon/snake/horse/sheep/monkey/rooster/dog/pig',
     blood_type       VARCHAR(5)   COMMENT '血型: A/B/AB/O',
     blood_type       VARCHAR(5)   COMMENT '血型: A/B/AB/O',
-    mind_base_score  DECIMAL(5,2) DEFAULT 0 COMMENT '心维度基础分(先天)',
-    wisdom_base_score DECIMAL(5,2) DEFAULT 0 COMMENT '智维度基础分(先天)',
+    mind_base_score  INT DEFAULT 0 COMMENT '心维度基础分(百分位)',
+    wisdom_base_score INT DEFAULT 0 COMMENT '智维度基础分(百分位)',
     behavior_modifier JSON        COMMENT '行为修正因子JSON',
     behavior_modifier JSON        COMMENT '行为修正因子JSON',
     created_at       DATETIME DEFAULT CURRENT_TIMESTAMP,
     created_at       DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     updated_at       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
@@ -1453,18 +1453,18 @@ CREATE TABLE IF NOT EXISTS family_member_attributes (
 -- 星座配置表
 -- 星座配置表
 CREATE TABLE IF NOT EXISTS zodiac_config (
 CREATE TABLE IF NOT EXISTS zodiac_config (
     id               BIGINT AUTO_INCREMENT PRIMARY KEY,
     id               BIGINT AUTO_INCREMENT PRIMARY KEY,
-    zodiac_code      VARCHAR(10)  NOT NULL UNIQUE COMMENT '星座代码',
+    zodiac_code      VARCHAR(20)  NOT NULL UNIQUE COMMENT '星座代码',
     zodiac_name      VARCHAR(10)  NOT NULL COMMENT '星座名称',
     zodiac_name      VARCHAR(10)  NOT NULL COMMENT '星座名称',
     element          VARCHAR(10)  NOT NULL COMMENT '五行: 金木水火土',
     element          VARCHAR(10)  NOT NULL COMMENT '五行: 金木水火土',
     start_month      INT          NOT NULL COMMENT '起始月',
     start_month      INT          NOT NULL COMMENT '起始月',
     start_day        INT          NOT NULL COMMENT '起始日',
     start_day        INT          NOT NULL COMMENT '起始日',
     end_month        INT          NOT NULL COMMENT '结束月',
     end_month        INT          NOT NULL COMMENT '结束月',
     end_day          INT          NOT NULL COMMENT '结束日',
     end_day          INT          NOT NULL COMMENT '结束日',
-    mind_base        DECIMAL(4,2) DEFAULT 0 COMMENT '心先天基础分',
-    wisdom_base      DECIMAL(4,2) DEFAULT 0 COMMENT '智先天基础分',
-    body_modifier    DECIMAL(4,2) DEFAULT 0 COMMENT '身修正系数(%)',
-    action_modifier  DECIMAL(4,2) DEFAULT 0 COMMENT '行修正系数(%)',
-    wealth_modifier  DECIMAL(4,2) DEFAULT 0 COMMENT '富修正系数(%)',
+    mind_base        INT DEFAULT 0 COMMENT '心先天基础分(百分位)',
+    wisdom_base      INT DEFAULT 0 COMMENT '智先天基础分(百分位)',
+    body_modifier    INT DEFAULT 0 COMMENT '身修正系数(千分比)',
+    action_modifier  INT DEFAULT 0 COMMENT '行修正系数(千分比)',
+    wealth_modifier  INT DEFAULT 0 COMMENT '富修正系数(千分比)',
     created_at       DATETIME DEFAULT CURRENT_TIMESTAMP
     created_at       DATETIME DEFAULT CURRENT_TIMESTAMP
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='星座配置表';
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='星座配置表';
 
 
@@ -1472,14 +1472,14 @@ CREATE TABLE IF NOT EXISTS zodiac_config (
 CREATE TABLE IF NOT EXISTS bazi_config (
 CREATE TABLE IF NOT EXISTS bazi_config (
     id               BIGINT AUTO_INCREMENT PRIMARY KEY,
     id               BIGINT AUTO_INCREMENT PRIMARY KEY,
     stem_or_branch   VARCHAR(10)  NOT NULL COMMENT '天干或地支',
     stem_or_branch   VARCHAR(10)  NOT NULL COMMENT '天干或地支',
-    type             VARCHAR(5)   NOT NULL COMMENT '类型: stem/branch',
+    type             VARCHAR(10)  NOT NULL COMMENT '类型: stem/branch',
     element          VARCHAR(10)  NOT NULL COMMENT '五行: 金木水火土',
     element          VARCHAR(10)  NOT NULL COMMENT '五行: 金木水火土',
     yin_yang         VARCHAR(5)   COMMENT '阴阳: yin/yang',
     yin_yang         VARCHAR(5)   COMMENT '阴阳: yin/yang',
-    mind_base        DECIMAL(4,2) DEFAULT 0 COMMENT '心先天基础分',
-    wisdom_base      DECIMAL(4,2) DEFAULT 0 COMMENT '智先天基础分',
-    body_modifier    DECIMAL(4,2) DEFAULT 0 COMMENT '身修正系数(%)',
-    action_modifier  DECIMAL(4,2) DEFAULT 0 COMMENT '行修正系数(%)',
-    wealth_modifier  DECIMAL(4,2) DEFAULT 0 COMMENT '富修正系数(%)',
+    mind_base        INT DEFAULT 0 COMMENT '心先天基础分(百分位)',
+    wisdom_base      INT DEFAULT 0 COMMENT '智先天基础分(百分位)',
+    body_modifier    INT DEFAULT 0 COMMENT '身修正系数(千分比)',
+    action_modifier  INT DEFAULT 0 COMMENT '行修正系数(千分比)',
+    wealth_modifier  INT DEFAULT 0 COMMENT '富修正系数(千分比)',
     created_at       DATETIME DEFAULT CURRENT_TIMESTAMP
     created_at       DATETIME DEFAULT CURRENT_TIMESTAMP
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='八字配置表(天干地支五行)';
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='八字配置表(天干地支五行)';
 
 
@@ -1488,11 +1488,11 @@ CREATE TABLE IF NOT EXISTS blood_type_config (
     id               BIGINT AUTO_INCREMENT PRIMARY KEY,
     id               BIGINT AUTO_INCREMENT PRIMARY KEY,
     blood_type       VARCHAR(5)   NOT NULL UNIQUE COMMENT '血型: A/B/AB/O',
     blood_type       VARCHAR(5)   NOT NULL UNIQUE COMMENT '血型: A/B/AB/O',
     description      VARCHAR(200) COMMENT '性格描述',
     description      VARCHAR(200) COMMENT '性格描述',
-    mind_base        DECIMAL(4,2) DEFAULT 0 COMMENT '心先天基础分',
-    wisdom_base      DECIMAL(4,2) DEFAULT 0 COMMENT '智先天基础分',
-    body_modifier    DECIMAL(4,2) DEFAULT 0 COMMENT '身修正系数(%)',
-    action_modifier  DECIMAL(4,2) DEFAULT 0 COMMENT '行修正系数(%)',
-    wealth_modifier  DECIMAL(4,2) DEFAULT 0 COMMENT '富修正系数(%)',
+    mind_base        INT DEFAULT 0 COMMENT '心先天基础分(百分位)',
+    wisdom_base      INT DEFAULT 0 COMMENT '智先天基础分(百分位)',
+    body_modifier    INT DEFAULT 0 COMMENT '身修正系数(千分比)',
+    action_modifier  INT DEFAULT 0 COMMENT '行修正系数(千分比)',
+    wealth_modifier  INT DEFAULT 0 COMMENT '富修正系数(千分比)',
     created_at       DATETIME DEFAULT CURRENT_TIMESTAMP
     created_at       DATETIME DEFAULT CURRENT_TIMESTAMP
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='血型配置表';
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='血型配置表';
 
 
@@ -1509,7 +1509,7 @@ CREATE TABLE IF NOT EXISTS activities (
     location         VARCHAR(200) COMMENT '活动地点',
     location         VARCHAR(200) COMMENT '活动地点',
     max_participants INT DEFAULT 0 COMMENT '最大参与人数',
     max_participants INT DEFAULT 0 COMMENT '最大参与人数',
     current_participants INT DEFAULT 0 COMMENT '当前参与人数',
     current_participants INT DEFAULT 0 COMMENT '当前参与人数',
-    price            DECIMAL(10,2) DEFAULT 0 COMMENT '活动价格',
+    price INT DEFAULT 0 COMMENT '活动价格(分)',
     vendor_id        BIGINT COMMENT '服务商ID',
     vendor_id        BIGINT COMMENT '服务商ID',
     vendor_name      VARCHAR(100) COMMENT '服务商名称',
     vendor_name      VARCHAR(100) COMMENT '服务商名称',
     created_at       DATETIME DEFAULT CURRENT_TIMESTAMP,
     created_at       DATETIME DEFAULT CURRENT_TIMESTAMP,

+ 29 - 5
cfc-frontend/pages/ai/chat.vue

@@ -53,7 +53,7 @@
           </view>
           </view>
         </view>
         </view>
 
 
-        <view class="message-group" v-for="(msg, idx) in messages" :key="msg.id || idx">
+        <view class="message-group" v-for="(msg, idx) in messages" :key="msg.id">
           <!-- 用户消息 -->
           <!-- 用户消息 -->
           <view class="message message-user" v-if="msg.query">
           <view class="message message-user" v-if="msg.query">
             <view class="message-bubble user-bubble">{{ msg.query }}</view>
             <view class="message-bubble user-bubble">{{ msg.query }}</view>
@@ -108,11 +108,28 @@ export default {
       sending: false,
       sending: false,
       showConversations: false,
       showConversations: false,
       scrollToId: '',
       scrollToId: '',
-      pageLoaded: false
+      pageLoaded: false,
+      reportId: null,
+      surveyId: null,
+      pendingFirstMessage: ''
     }
     }
   },
   },
-  onLoad() {
+  onLoad: function(options) {
+    this.reportId = options.reportId ? parseInt(options.reportId) : null
+    this.surveyId = options.surveyId ? parseInt(options.surveyId) : null
+    if (options.firstMessage) {
+      this.pendingFirstMessage = decodeURIComponent(options.firstMessage)
+    }
     this.loadConversations()
     this.loadConversations()
+    // 如果有预填消息,自动发送
+    var self = this
+    if (this.pendingFirstMessage) {
+      this.$nextTick(function() {
+        self.inputText = self.pendingFirstMessage
+        self.pendingFirstMessage = ''
+        self.sendMessage()
+      })
+    }
   },
   },
   onShow() {
   onShow() {
     if (this.pageLoaded) {
     if (this.pageLoaded) {
@@ -177,10 +194,17 @@ export default {
       })
       })
 
 
       try {
       try {
-        const res = await aiSendMessage({
+        var params = {
           query: text,
           query: text,
           conversationId: this.currentConversationId
           conversationId: this.currentConversationId
-        })
+        }
+        if (this.reportId) {
+          params.reportId = this.reportId
+        }
+        if (this.surveyId) {
+          params.surveyId = this.surveyId
+        }
+        const res = await aiSendMessage(params)
         const data = res.data || {}
         const data = res.data || {}
 
 
         // 更新最后一条消息的回复
         // 更新最后一条消息的回复

+ 3 - 3
cfc-frontend/pages/body/health-report.vue

@@ -46,7 +46,7 @@
           <text class="category-name">{{ group.name }}</text>
           <text class="category-name">{{ group.name }}</text>
           <text class="category-count">{{ group.items.length }}项</text>
           <text class="category-count">{{ group.items.length }}项</text>
         </view>
         </view>
-        <view class="indicator-card" v-for="ind in group.items" :key="ind.indicatorName + ind._idx">
+        <view class="indicator-card" v-for="ind in group.items" :key="ind._idx">
           <view class="ind-header">
           <view class="ind-header">
             <text class="ind-name">{{ ind.indicatorName }}</text>
             <text class="ind-name">{{ ind.indicatorName }}</text>
             <view class="ind-status" :class="ind._cls">
             <view class="ind-status" :class="ind._cls">
@@ -74,13 +74,13 @@
 </template>
 </template>
 
 
 <script>
 <script>
-const BASE_URL = 'http://localhost:8080'
+import config from '@/config.js'
 
 
 const healthRequest = function(url, data) {
 const healthRequest = function(url, data) {
   var token = uni.getStorageSync('token')
   var token = uni.getStorageSync('token')
   return new Promise(function(resolve, reject) {
   return new Promise(function(resolve, reject) {
     uni.request({
     uni.request({
-      url: BASE_URL + url,
+      url: config.API_BASE_URL + url,
       method: 'POST',
       method: 'POST',
       data: data,
       data: data,
       header: {
       header: {

+ 213 - 3
cfc-frontend/pages/body/index.vue

@@ -90,6 +90,76 @@
       </view>
       </view>
     </view>
     </view>
 
 
+    <!-- ===== 精准营养(上传报告+产品+文章) ===== -->
+    <view class="section nutrition-section" v-if="isLoggedIn">
+      <view class="section-header">
+        <text class="section-title">🥗 精准营养</text>
+      </view>
+
+      <!-- 上传报告入口 -->
+      <view class="nutrition-report-card" @click="goToUploadReport">
+        <view class="nutrition-report-left">
+          <text class="nutrition-icon-large">📋</text>
+        </view>
+        <view class="nutrition-report-right">
+          <text class="nutrition-title">上传健康报告</text>
+          <text class="nutrition-desc">上传体检报告或肠道检测报告,AI智能解读分析,获取个性化营养方案</text>
+          <view class="nutrition-report-tags">
+            <text class="tag">体检报告</text>
+            <text class="tag">肠道检测</text>
+            <text class="tag">AI解读</text>
+          </view>
+        </view>
+        <text class="nutrition-arrow">›</text>
+      </view>
+
+      <!-- 菌群检测产品链接 -->
+      <view class="nutrition-links">
+        <text class="links-label">🔥 相关产品</text>
+        <view class="links-row">
+          <view class="link-item" @click="goToShopProduct('gut_flora')">
+            <text class="link-icon">🧪</text>
+            <view class="link-texts">
+              <text class="link-name">肠道菌群检测</text>
+              <text class="link-desc">了解孩子的肠道健康状况</text>
+            </view>
+            <text class="link-arrow">›</text>
+          </view>
+          <view class="link-item" @click="goToShopProduct('probiotic')">
+            <text class="link-icon">🦠</text>
+            <view class="link-texts">
+              <text class="link-name">益生菌补充</text>
+              <text class="link-desc">平衡肠道菌群,增强免疫力</text>
+            </view>
+            <text class="link-arrow">›</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 相关文章链接 -->
+      <view class="nutrition-links">
+        <text class="links-label">📖 相关文章</text>
+        <view class="links-row">
+          <view class="link-item" @click="goToArticle('gut_health')">
+            <text class="link-icon">📝</text>
+            <view class="link-texts">
+              <text class="link-name">肠道菌群与儿童健康</text>
+              <text class="link-desc">肠道菌群如何影响孩子的免疫力</text>
+            </view>
+            <text class="link-arrow">›</text>
+          </view>
+          <view class="link-item" @click="goToArticle('nutrition_guide')">
+            <text class="link-icon">📝</text>
+            <view class="link-texts">
+              <text class="link-name">儿童精准营养指南</text>
+              <text class="link-desc">科学搭配饮食,促进健康成长</text>
+            </view>
+            <text class="link-arrow">›</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
     <!-- 维度任务 -->
     <!-- 维度任务 -->
     <DimensionTasks
     <DimensionTasks
       v-if="isLoggedIn"
       v-if="isLoggedIn"
@@ -100,7 +170,6 @@
 
 
     <!-- 维度活动 -->
     <!-- 维度活动 -->
     <DimensionActivities
     <DimensionActivities
-      v-if="isLoggedIn"
       dimensionCode="body"
       dimensionCode="body"
       :activities="dimensionActivities"
       :activities="dimensionActivities"
       @activityClick="goActivityDetail"
       @activityClick="goActivityDetail"
@@ -108,7 +177,6 @@
 
 
     <!-- 维度商品 -->
     <!-- 维度商品 -->
     <DimensionProducts
     <DimensionProducts
-      v-if="isLoggedIn"
       dimensionCode="body"
       dimensionCode="body"
       :products="dimensionProducts"
       :products="dimensionProducts"
       @productClick="goProductDetail"
       @productClick="goProductDetail"
@@ -155,8 +223,9 @@ import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 import { getVisibleSections, getEnergyOverview, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getVisibleFamilyMembers } from '../../utils/api.js'
 import { getVisibleSections, getEnergyOverview, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getVisibleFamilyMembers } from '../../utils/api.js'
+import config from '../../config.js'
 
 
-const BASE_URL = 'http://localhost:8080'
+var BASE_URL = config.api('')
 
 
 const healthRequest = function(url, data) {
 const healthRequest = function(url, data) {
   var token = uni.getStorageSync('token')
   var token = uni.getStorageSync('token')
@@ -236,6 +305,9 @@ export default {
       this.loadChildren()
       this.loadChildren()
       this.loadFamilyMembersVisible()
       this.loadFamilyMembersVisible()
     }
     }
+    // 游客也能浏览活动和商品
+    this.loadDimensionActivities()
+    this.loadDimensionProducts()
   },
   },
   methods: {
   methods: {
     loadSectionConfig: function() {
     loadSectionConfig: function() {
@@ -432,6 +504,27 @@ export default {
       }
       }
       uni.navigateTo({ url: '/pages/body/checkin?childId=' + this.activeChildId })
       uni.navigateTo({ url: '/pages/body/checkin?childId=' + this.activeChildId })
     },
     },
+    goToUploadReport: function() {
+      if (!this.activeChildId) {
+        uni.showToast({ title: '请先选择孩子', icon: 'none' })
+        return
+      }
+      uni.navigateTo({ url: '/pages/health/report-upload?childId=' + this.activeChildId + '&from=body' })
+    },
+    goToShopProduct: function(type) {
+      if (type === 'gut_flora') {
+        uni.navigateTo({ url: '/pages/discover/index?type=product&domain=body&keyword=' + encodeURIComponent('肠道菌群检测') })
+      } else if (type === 'probiotic') {
+        uni.navigateTo({ url: '/pages/discover/index?type=product&domain=body&keyword=' + encodeURIComponent('益生菌') })
+      }
+    },
+    goToArticle: function(topic) {
+      if (topic === 'gut_health') {
+        uni.navigateTo({ url: '/pages/mind/articles?tag=' + encodeURIComponent('肠道健康') })
+      } else if (topic === 'nutrition_guide') {
+        uni.navigateTo({ url: '/pages/mind/articles?tag=' + encodeURIComponent('精准营养') })
+      }
+    },
     formatDate: function(dateStr) {
     formatDate: function(dateStr) {
       if (!dateStr) return '--'
       if (!dateStr) return '--'
       try {
       try {
@@ -661,4 +754,121 @@ export default {
 .func-item:active, .action-btn:active {
 .func-item:active, .action-btn:active {
   opacity: 0.7;
   opacity: 0.7;
 }
 }
+
+/* ===== 精准营养 ===== */
+.nutrition-section {
+  margin-top: 20rpx;
+}
+.nutrition-report-card {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: linear-gradient(135deg, #E8F5E9, #F1F8E9);
+  border-radius: 20rpx;
+  padding: 28rpx 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.nutrition-report-left {
+  width: 100rpx;
+  height: 100rpx;
+  background: rgba(255,255,255,0.7);
+  border-radius: 20rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
+  flex-shrink: 0;
+}
+.nutrition-icon-large {
+  font-size: 52rpx;
+}
+.nutrition-report-right {
+  flex: 1;
+}
+.nutrition-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #2E7D32;
+  display: block;
+  margin-bottom: 6rpx;
+}
+.nutrition-desc {
+  font-size: 22rpx;
+  color: #558B2F;
+  line-height: 1.5;
+  display: block;
+  margin-bottom: 10rpx;
+}
+.nutrition-report-tags {
+  display: flex;
+  flex-direction: row;
+  gap: 10rpx;
+}
+.nutrition-report-tags .tag {
+  font-size: 20rpx;
+  color: #33691E;
+  background: rgba(255,255,255,0.6);
+  padding: 4rpx 14rpx;
+  border-radius: 20rpx;
+}
+.nutrition-arrow {
+  font-size: 40rpx;
+  color: #A5D6A7;
+  margin-left: 12rpx;
+}
+
+.nutrition-links {
+  margin-bottom: 20rpx;
+}
+.links-label {
+  font-size: 26rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 12rpx;
+}
+.links-row {
+  display: flex;
+  flex-direction: row;
+  gap: 16rpx;
+}
+.link-item {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 20rpx 18rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
+  gap: 12rpx;
+}
+.link-item:active {
+  opacity: 0.7;
+}
+.link-icon {
+  font-size: 36rpx;
+  flex-shrink: 0;
+}
+.link-texts {
+  flex: 1;
+}
+.link-name {
+  font-size: 24rpx;
+  font-weight: 500;
+  color: #333;
+  display: block;
+  margin-bottom: 4rpx;
+}
+.link-desc {
+  font-size: 20rpx;
+  color: #999;
+  display: block;
+}
+.link-arrow {
+  font-size: 32rpx;
+  color: #CCC;
+  flex-shrink: 0;
+}
 </style>
 </style>

+ 247 - 0
cfc-frontend/pages/health/report-list.vue

@@ -0,0 +1,247 @@
+<template>
+  <view class="list-container">
+    <!-- 顶部导航 -->
+    <view class="nav-bar">
+      <text class="nav-back" @click="goBack">‹ 返回</text>
+      <text class="nav-title">健康报告</text>
+      <view class="nav-placeholder"></view>
+    </view>
+
+    <!-- 加载状态 -->
+    <view class="loading-wrap" v-if="loading">
+      <text class="loading-text">加载中...</text>
+    </view>
+
+    <!-- 报告列表 -->
+    <view class="report-list" v-else-if="reports.length > 0">
+      <view
+        class="report-card"
+        v-for="item in reports"
+        :key="item.id"
+        @click="goToDetail(item.id)"
+      >
+        <view class="report-card-left">
+          <view class="report-type-badge" :class="'badge-' + (item.reportType === 'gut_flora' ? 'gut' : 'exam')">
+            <text>{{ item.reportType === 'gut_flora' ? '肠道检测' : '体检报告' }}</text>
+          </view>
+          <text class="report-date">{{ formatDate(item.reportDate) }}</text>
+        </view>
+        <view class="report-card-right">
+          <text class="report-score">{{ item.overallScore || '--' }}<text class="score-unit">分</text></text>
+          <text class="report-arrow">›</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty-wrap" v-else>
+      <text class="empty-icon">📋</text>
+      <text class="empty-title">暂无健康报告</text>
+      <text class="empty-desc">上传体检报告或肠道检测报告,获取 AI 解读</text>
+      <view class="empty-btn" @click="goToUpload">上传报告</view>
+    </view>
+
+    <!-- 底部占位 -->
+    <view class="bottom-spacer"></view>
+  </view>
+</template>
+
+<script>
+import { getReportList } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      reports: [],
+      loading: false,
+      childId: null
+    }
+  },
+  onLoad(options) {
+    this.childId = options.childId ? parseInt(options.childId) : null
+    this.loadData()
+  },
+  methods: {
+    loadData() {
+      var self = this
+      self.loading = true
+      getReportList(self.childId).then(function(res) {
+        self.reports = res.data || []
+      }).catch(function(e) {
+        console.log('获取报告列表失败', e)
+        uni.showToast({ title: '加载失败', icon: 'none' })
+      }).finally(function() {
+        self.loading = false
+      })
+    },
+    goBack() {
+      uni.navigateBack()
+    },
+    goToDetail(reportId) {
+      uni.navigateTo({ url: '/pages/body/health-report?reportId=' + reportId })
+    },
+    goToUpload() {
+      uni.navigateTo({ url: '/pages/health/report-upload' })
+    },
+    formatDate(dateStr) {
+      if (!dateStr) return '--'
+      try {
+        var d = new Date(dateStr)
+        var y = d.getFullYear()
+        var m = ('' + (d.getMonth() + 1)).padStart(2, '0')
+        var day = ('' + d.getDate()).padStart(2, '0')
+        return y + '-' + m + '-' + day
+      } catch (e) {
+        return dateStr
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.list-container {
+  min-height: 100vh;
+  background: #F5F7FA;
+}
+
+/* ===== 导航栏 ===== */
+.nav-bar {
+  position: sticky;
+  top: 0;
+  background: #fff;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20rpx 30rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
+  z-index: 100;
+}
+.nav-back {
+  font-size: 32rpx;
+  color: #5B9BD5;
+}
+.nav-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+}
+.nav-placeholder {
+  width: 80rpx;
+}
+
+/* ===== 加载状态 ===== */
+.loading-wrap {
+  display: flex;
+  justify-content: center;
+  padding: 80rpx 0;
+}
+.loading-text {
+  font-size: 28rpx;
+  color: #999;
+}
+
+/* ===== 报告列表 ===== */
+.report-list {
+  margin: 20rpx 30rpx;
+}
+.report-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 28rpx 24rpx;
+  margin-bottom: 16rpx;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
+}
+.report-card:active {
+  opacity: 0.8;
+}
+.report-card-left {
+  display: flex;
+  flex-direction: column;
+  gap: 8rpx;
+}
+.report-type-badge {
+  display: inline-block;
+  font-size: 22rpx;
+  padding: 4rpx 18rpx;
+  border-radius: 20rpx;
+  align-self: flex-start;
+}
+.badge-exam {
+  background: #E3F2FD;
+  color: #1565C0;
+}
+.badge-gut {
+  background: #E8F5E9;
+  color: #2E7D32;
+}
+.report-date {
+  font-size: 24rpx;
+  color: #888;
+}
+.report-card-right {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 12rpx;
+}
+.report-score {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+}
+.score-unit {
+  font-size: 22rpx;
+  color: #999;
+  font-weight: normal;
+}
+.report-arrow {
+  font-size: 36rpx;
+  color: #CCC;
+}
+
+/* ===== 空状态 ===== */
+.empty-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 120rpx 60rpx;
+}
+.empty-icon {
+  font-size: 80rpx;
+  margin-bottom: 24rpx;
+}
+.empty-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 12rpx;
+}
+.empty-desc {
+  font-size: 26rpx;
+  color: #999;
+  text-align: center;
+  margin-bottom: 40rpx;
+  line-height: 1.5;
+}
+.empty-btn {
+  background: #5B9BD5;
+  color: #fff;
+  font-size: 28rpx;
+  padding: 20rpx 60rpx;
+  border-radius: 40rpx;
+}
+.empty-btn:active {
+  opacity: 0.8;
+}
+
+/* ===== 底部 ===== */
+.bottom-spacer {
+  height: 60rpx;
+}
+</style>

+ 432 - 0
cfc-frontend/pages/health/report-survey.vue

@@ -0,0 +1,432 @@
+<template>
+  <view class="survey-container">
+    <!-- 导航 -->
+    <view class="nav-bar">
+      <text class="nav-back" @click="goBack">‹ 返回</text>
+      <text class="nav-title">健康调研问卷</text>
+      <view class="nav-placeholder"></view>
+    </view>
+
+    <!-- 进度条 -->
+    <view class="progress-bar">
+      <view class="progress-fill" :style="{ width: progressPercent + '%' }"></view>
+    </view>
+
+    <!-- 提示说明 -->
+    <view class="tip-banner">
+      <text class="tip-icon">💡</text>
+      <text class="tip-text">请根据孩子最近的实际情况填写,帮助AI更准确地解读报告并提供个性化方案</text>
+    </view>
+
+    <!-- 表单 -->
+    <view class="form-section">
+      <!-- Q1: 饮食情况 -->
+      <view class="question-card">
+        <text class="q-number">1/6</text>
+        <text class="q-title">孩子平时的饮食结构是怎样的?</text>
+        <view class="q-options">
+          <view class="q-option" :class="{ 'q-selected': survey.diet === 'balanced' }" @click="survey.diet = 'balanced'">
+            <text class="q-icon">🥗</text>
+            <text class="q-text">均衡饮食,荤素搭配</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.diet === 'meat' }" @click="survey.diet = 'meat'">
+            <text class="q-icon">🥩</text>
+            <text class="q-text">偏荤食,爱吃肉</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.diet === 'veggie' }" @click="survey.diet = 'veggie'">
+            <text class="q-icon">🥬</text>
+            <text class="q-text">偏素食,不爱吃肉</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.diet === 'junk' }" @click="survey.diet = 'junk'">
+            <text class="q-icon">🍟</text>
+            <text class="q-text">偏好零食/快餐</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- Q2: 消化状况 -->
+      <view class="question-card">
+        <text class="q-number">2/6</text>
+        <text class="q-title">孩子近期消化状况如何?</text>
+        <view class="q-options">
+          <view class="q-option" :class="{ 'q-selected': survey.digestion === 'good' }" @click="survey.digestion = 'good'">
+            <text class="q-icon">😊</text>
+            <text class="q-text">正常,消化良好</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.digestion === 'constipation' }" @click="survey.digestion = 'constipation'">
+            <text class="q-icon">😣</text>
+            <text class="q-text">容易便秘</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.digestion === 'diarrhea' }" @click="survey.digestion = 'diarrhea'">
+            <text class="q-icon">😟</text>
+            <text class="q-text">容易腹泻</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.digestion === 'irregular' }" @click="survey.digestion = 'irregular'">
+            <text class="q-icon">🤔</text>
+            <text class="q-text">不太规律</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- Q3: 睡眠质量 -->
+      <view class="question-card">
+        <text class="q-number">3/6</text>
+        <text class="q-title">孩子近期的睡眠质量如何?</text>
+        <view class="q-options">
+          <view class="q-option" :class="{ 'q-selected': survey.sleep === 'good' }" @click="survey.sleep = 'good'">
+            <text class="q-icon">😴</text>
+            <text class="q-text">睡得好,作息规律</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.sleep === 'late' }" @click="survey.sleep = 'late'">
+            <text class="q-icon">🦉</text>
+            <text class="q-text">睡得太晚</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.sleep === 'restless' }" @click="survey.sleep = 'restless'">
+            <text class="q-icon">😰</text>
+            <text class="q-text">翻来覆去睡不安稳</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.sleep === 'short' }" @click="survey.sleep = 'short'">
+            <text class="q-icon">⏰</text>
+            <text class="q-text">睡眠时间不足</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- Q4: 运动习惯 -->
+      <view class="question-card">
+        <text class="q-number">4/6</text>
+        <text class="q-title">孩子每周运动情况?</text>
+        <view class="q-options">
+          <view class="q-option" :class="{ 'q-selected': survey.exercise === 'frequent' }" @click="survey.exercise = 'frequent'">
+            <text class="q-icon">🏃</text>
+            <text class="q-text">经常运动(3次以上/周)</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.exercise === 'moderate' }" @click="survey.exercise = 'moderate'">
+            <text class="q-icon">🚶</text>
+            <text class="q-text">偶尔运动(1-2次/周)</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.exercise === 'rare' }" @click="survey.exercise = 'rare'">
+            <text class="q-icon">🪑</text>
+            <text class="q-text">很少运动</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- Q5: 过敏/不适 -->
+      <view class="question-card">
+        <text class="q-number">5/6</text>
+        <text class="q-title">孩子是否有以下过敏或不适症状?(可多选)</text>
+        <view class="q-options">
+          <view class="q-option" :class="{ 'q-selected': survey.allergies.indexOf('food') !== -1 }" @click="toggleAllergy('food')">
+            <text class="q-text">食物过敏</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.allergies.indexOf('pollen') !== -1 }" @click="toggleAllergy('pollen')">
+            <text class="q-text">花粉/尘螨过敏</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.allergies.indexOf('skin') !== -1 }" @click="toggleAllergy('skin')">
+            <text class="q-text">皮肤湿疹/皮疹</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.allergies.indexOf('none') !== -1 }" @click="toggleAllergy('none')">
+            <text class="q-text">无明显过敏</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- Q6: 健康目标 -->
+      <view class="question-card">
+        <text class="q-number">6/6</text>
+        <text class="q-title">您希望通过精准营养改善什么?(可多选)</text>
+        <view class="q-options">
+          <view class="q-option" :class="{ 'q-selected': survey.goals.indexOf('immunity') !== -1 }" @click="toggleGoal('immunity')">
+            <text class="q-icon">🛡️</text>
+            <text class="q-text">增强免疫力</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.goals.indexOf('digestion') !== -1 }" @click="toggleGoal('digestion')">
+            <text class="q-icon">🌿</text>
+            <text class="q-text">改善消化</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.goals.indexOf('growth') !== -1 }" @click="toggleGoal('growth')">
+            <text class="q-icon">📈</text>
+            <text class="q-text">促进生长发育</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.goals.indexOf('focus') !== -1 }" @click="toggleGoal('focus')">
+            <text class="q-icon">🎯</text>
+            <text class="q-text">提升专注力</text>
+          </view>
+          <view class="q-option" :class="{ 'q-selected': survey.goals.indexOf('weight') !== -1 }" @click="toggleGoal('weight')">
+            <text class="q-icon">⚖️</text>
+            <text class="q-text">体重管理</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 提交按钮 -->
+      <view class="submit-btn" :class="{ 'btn-disabled': !canSubmit }" @click="doSubmit">
+        <text v-if="submitting">提交中...</text>
+        <text v-else>提交并进入 AI 解读</text>
+      </view>
+    </view>
+
+    <view class="bottom-spacer"></view>
+  </view>
+</template>
+
+<script>
+import { createReportSurvey, submitReportSurvey, getSurveyStatus } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      reportId: null,
+      childId: null,
+      surveyId: null,
+      survey: {
+        diet: '',
+        digestion: '',
+        sleep: '',
+        exercise: '',
+        allergies: [],
+        goals: []
+      },
+      submitting: false
+    }
+  },
+  computed: {
+    progressPercent: function() {
+      var answered = 0
+      if (this.survey.diet) answered++
+      if (this.survey.digestion) answered++
+      if (this.survey.sleep) answered++
+      if (this.survey.exercise) answered++
+      if (this.survey.allergies.length > 0) answered++
+      if (this.survey.goals.length > 0) answered++
+      return Math.round((answered / 6) * 100)
+    },
+    canSubmit: function() {
+      return this.survey.diet && this.survey.digestion && this.survey.sleep && this.survey.exercise
+    }
+  },
+  onLoad: function(options) {
+    this.reportId = options.reportId ? parseInt(options.reportId) : null
+    this.childId = options.childId ? parseInt(options.childId) : null
+    this.initSurvey()
+  },
+  methods: {
+    initSurvey: function() {
+      var self = this
+      if (!self.reportId) return
+      // 先查状态
+      getSurveyStatus(self.reportId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          if (res.data.completed) {
+            uni.showToast({ title: '该报告问卷已提交', icon: 'none' })
+            return
+          }
+          if (res.data.pendingSurveyId) {
+            self.surveyId = res.data.pendingSurveyId
+            return
+          }
+        }
+        // 未创建,则创建
+        return createReportSurvey(self.reportId, self.childId || 0)
+      }).then(function(res) {
+        if (res && res.code === 200 && res.data && res.data.id) {
+          self.surveyId = res.data.id
+        }
+      }).catch(function(e) {
+        console.log('初始化问卷失败', e)
+      })
+    },
+  methods: {
+    toggleAllergy: function(val) {
+      var idx = this.survey.allergies.indexOf(val)
+      if (val === 'none') {
+        this.survey.allergies = ['none']
+      } else {
+        // 移除 none
+        var noneIdx = this.survey.allergies.indexOf('none')
+        if (noneIdx !== -1) this.survey.allergies.splice(noneIdx, 1)
+        if (idx === -1) {
+          this.survey.allergies.push(val)
+        } else {
+          this.survey.allergies.splice(idx, 1)
+        }
+        // 如果全删了,恢复 none
+        if (this.survey.allergies.length === 0) {
+          this.survey.allergies.push('none')
+        }
+      }
+    },
+    toggleGoal: function(val) {
+      var idx = this.survey.goals.indexOf(val)
+      if (idx === -1) {
+        this.survey.goals.push(val)
+      } else {
+        this.survey.goals.splice(idx, 1)
+      }
+    },
+    goBack: function() {
+      uni.navigateBack()
+    },
+    doSubmit: function() {
+      if (!this.canSubmit || this.submitting) return
+      var self = this
+      self.submitting = true
+      uni.showLoading({ title: '提交中...' })
+
+      // 确保 surveyId 存在
+      var doSubmit = function() {
+        submitReportSurvey(self.surveyId, JSON.stringify(self.survey)).then(function(res) {
+          uni.hideLoading()
+          if (res.code === 200) {
+            uni.showToast({ title: '提交成功', icon: 'success' })
+            // 跳转到 AI 对话页,带入报告和问卷数据
+            setTimeout(function() {
+              var params = 'reportId=' + self.reportId + '&surveyId=' + self.surveyId + '&firstMessage=' + encodeURIComponent('请帮我解读孩子的健康报告')
+              uni.redirectTo({ url: '/pages/ai/chat?' + params })
+            }, 500)
+          } else {
+            uni.showToast({ title: res.message || '提交失败', icon: 'none' })
+          }
+        }).catch(function(e) {
+          uni.hideLoading()
+          uni.showToast({ title: '提交失败,请重试', icon: 'none' })
+          console.log('提交问卷失败', e)
+        }).finally(function() {
+          self.submitting = false
+        })
+      }
+
+      if (this.surveyId) {
+        doSubmit()
+      } else {
+        // 先创建再提交
+        createReportSurvey(this.reportId, this.childId || 0).then(function(res) {
+          if (res.code === 200 && res.data && res.data.id) {
+            self.surveyId = res.data.id
+            doSubmit()
+          } else {
+            uni.hideLoading()
+            uni.showToast({ title: '创建问卷失败', icon: 'none' })
+            self.submitting = false
+          }
+        }).catch(function(e) {
+          uni.hideLoading()
+          uni.showToast({ title: '创建问卷失败', icon: 'none' })
+          self.submitting = false
+          console.log('创建问卷失败', e)
+        })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.survey-container {
+  min-height: 100vh;
+  background: #F3F7FA;
+}
+.nav-bar {
+  position: sticky;
+  top: 0;
+  background: #fff;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20rpx 30rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
+  z-index: 100;
+}
+.nav-back { font-size: 32rpx; color: #5B9BD5; }
+.nav-title { font-size: 32rpx; font-weight: bold; color: #333; }
+.nav-placeholder { width: 80rpx; }
+
+.progress-bar {
+  height: 6rpx;
+  background: #E2E8F0;
+  margin: 0 30rpx;
+  border-radius: 3rpx;
+  overflow: hidden;
+}
+.progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #66BB6A, #43A047);
+  border-radius: 3rpx;
+  transition: width 0.3s;
+}
+
+.tip-banner {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: #FFF8E1;
+  margin: 16rpx 30rpx;
+  padding: 16rpx 20rpx;
+  border-radius: 12rpx;
+}
+.tip-icon { font-size: 28rpx; margin-right: 12rpx; flex-shrink: 0; }
+.tip-text { font-size: 22rpx; color: #F57F17; line-height: 1.5; }
+
+.form-section { margin: 10rpx 30rpx 30rpx; }
+
+.question-card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx 24rpx;
+  margin-bottom: 16rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
+}
+.q-number {
+  font-size: 20rpx;
+  color: #66BB6A;
+  font-weight: bold;
+  display: block;
+  margin-bottom: 8rpx;
+}
+.q-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 16rpx;
+}
+.q-options {
+  display: flex;
+  flex-direction: column;
+  gap: 12rpx;
+}
+.q-option {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 12rpx;
+  padding: 16rpx 18rpx;
+  border-radius: 14rpx;
+  background: #F5F7FA;
+  border: 2rpx solid transparent;
+}
+.q-selected {
+  background: #E8F5E9;
+  border-color: #66BB6A;
+}
+.q-option:active { opacity: 0.7; }
+.q-icon { font-size: 32rpx; }
+.q-text { font-size: 26rpx; color: #333; }
+
+.submit-btn {
+  background: linear-gradient(135deg, #43A047, #2E7D32);
+  color: #fff;
+  font-size: 30rpx;
+  font-weight: bold;
+  text-align: center;
+  padding: 28rpx;
+  border-radius: 16rpx;
+  margin-top: 20rpx;
+}
+.submit-btn:active { opacity: 0.85; }
+.btn-disabled { opacity: 0.5; }
+
+.bottom-spacer { height: 60rpx; }
+</style>

+ 352 - 0
cfc-frontend/pages/health/report-upload.vue

@@ -0,0 +1,352 @@
+<template>
+  <view class="upload-container">
+    <!-- 顶部导航 -->
+    <view class="nav-bar">
+      <text class="nav-back" @click="goBack">‹ 返回</text>
+      <text class="nav-title">上传健康报告</text>
+      <view class="nav-placeholder"></view>
+    </view>
+
+    <!-- 说明提示 -->
+    <view class="tip-banner">
+      <text class="tip-icon">📋</text>
+      <text class="tip-text">上传体检报告或肠道检测报告,AI 将为您解读并提供个性化营养方案</text>
+    </view>
+
+    <!-- 表单 -->
+    <view class="form-section">
+      <!-- 报告类型 -->
+      <view class="form-item">
+        <text class="form-label">报告类型</text>
+        <view class="type-selector">
+          <view
+            class="type-option"
+            :class="{ 'type-selected': form.reportType === 'physical_exam' }"
+            @click="form.reportType = 'physical_exam'"
+          >
+            <text class="type-icon">🏥</text>
+            <text class="type-name">体检报告</text>
+          </view>
+          <view
+            class="type-option"
+            :class="{ 'type-selected': form.reportType === 'gut_flora' }"
+            @click="form.reportType = 'gut_flora'"
+          >
+            <text class="type-icon">🔬</text>
+            <text class="type-name">肠道检测</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 报告日期 -->
+      <view class="form-item">
+        <text class="form-label">报告日期</text>
+        <view class="date-display" @click="showDatePicker = true">
+          <text>{{ form.reportDate || '请选择日期' }}</text>
+          <text class="date-arrow">›</text>
+        </view>
+      </view>
+
+      <!-- 综合评分 -->
+      <view class="form-item">
+        <text class="form-label">综合评分(选填)</text>
+        <view class="score-input-wrap">
+          <slider
+            class="score-slider"
+            min="0"
+            max="100"
+            step="1"
+            :value="form.overallScore"
+            @changing="onScoreChanging"
+            @change="onScoreChange"
+            activeColor="#5B9BD5"
+            backgroundColor="#E2E8F0"
+            block-size="16"
+            show-value
+          />
+          <text class="score-value">{{ form.overallScore || 0 }}分</text>
+        </view>
+      </view>
+
+      <!-- 备注 -->
+      <view class="form-item">
+        <text class="form-label">备注(选填)</text>
+        <textarea
+          class="note-input"
+          v-model="form.description"
+          placeholder="请输入报告摘要或备注信息..."
+          maxlength="500"
+        />
+      </view>
+
+      <!-- 提交按钮 -->
+      <view class="submit-btn" :class="{ 'btn-disabled': !canSubmit }" @click="doSubmit">
+        <text v-if="submitting">提交中...</text>
+        <text v-else>提交报告</text>
+      </view>
+    </view>
+
+    <!-- 底部占位 -->
+    <view class="bottom-spacer"></view>
+  </view>
+</template>
+
+<script>
+import { createHealthReport } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      childId: null,
+      form: {
+        reportType: 'physical_exam',
+        reportDate: '',
+        overallScore: 0,
+        description: ''
+      },
+      showDatePicker: false,
+      submitting: false
+    }
+  },
+  computed: {
+    canSubmit() {
+      return this.form.reportType && this.form.reportDate
+    }
+  },
+  onLoad: function(options) {
+    this.childId = options.childId ? parseInt(options.childId) : null
+    // 默认设为今天
+    var now = new Date()
+    var y = now.getFullYear()
+    var m = ('' + (now.getMonth() + 1)).padStart(2, '0')
+    var d = ('' + now.getDate()).padStart(2, '0')
+    this.form.reportDate = y + '-' + m + '-' + d
+  },
+  methods: {
+    onScoreChanging(e) {
+      this.form.overallScore = e.detail.value
+    },
+    onScoreChange(e) {
+      this.form.overallScore = e.detail.value
+    },
+    goBack() {
+      uni.navigateBack()
+    },
+    doSubmit() {
+      if (!this.canSubmit || this.submitting) return
+      var self = this
+      self.submitting = true
+      uni.showLoading({ title: '提交中...' })
+      createHealthReport({
+        reportType: self.form.reportType,
+        reportDate: self.form.reportDate,
+        overallScore: self.form.overallScore,
+        description: self.form.description
+      }).then(function(res) {
+        uni.hideLoading()
+        if (res.code === 200) {
+          uni.showToast({ title: '提交成功', icon: 'success' })
+          // 跳转到报告详情
+          var reportId = res.data && res.data.id
+          if (reportId) {
+            setTimeout(function() {
+              if (self.form.reportType === 'gut_flora') {
+                // 肠道检测 → 调研问卷 → AI 解读
+                uni.redirectTo({
+                  url: '/pages/health/report-survey?reportId=' + reportId + '&childId=' + (self.childId || '')
+                })
+              } else {
+                uni.redirectTo({ url: '/pages/body/health-report?reportId=' + reportId })
+              }
+            }, 500)
+          } else {
+            setTimeout(function() {
+              uni.navigateBack()
+            }, 500)
+          }
+        } else {
+          uni.showToast({ title: res.message || '提交失败', icon: 'none' })
+        }
+      }).catch(function(e) {
+        uni.hideLoading()
+        uni.showToast({ title: '提交失败,请重试', icon: 'none' })
+        console.log('创建报告失败', e)
+      }).finally(function() {
+        self.submitting = false
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.upload-container {
+  min-height: 100vh;
+  background: #F5F7FA;
+}
+
+/* ===== 导航栏 ===== */
+.nav-bar {
+  position: sticky;
+  top: 0;
+  background: #fff;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20rpx 30rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
+  z-index: 100;
+}
+.nav-back {
+  font-size: 32rpx;
+  color: #5B9BD5;
+}
+.nav-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+}
+.nav-placeholder {
+  width: 80rpx;
+}
+
+/* ===== 提示条 ===== */
+.tip-banner {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: #E3F2FD;
+  margin: 20rpx 30rpx;
+  padding: 20rpx 24rpx;
+  border-radius: 16rpx;
+}
+.tip-icon {
+  font-size: 32rpx;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+}
+.tip-text {
+  font-size: 24rpx;
+  color: #1565C0;
+  line-height: 1.5;
+}
+
+/* ===== 表单 ===== */
+.form-section {
+  margin: 0 30rpx;
+}
+.form-item {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+}
+.form-label {
+  font-size: 26rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 16rpx;
+}
+
+/* 类型选择 */
+.type-selector {
+  display: flex;
+  flex-direction: row;
+  gap: 20rpx;
+}
+.type-option {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 24rpx;
+  border-radius: 16rpx;
+  background: #F5F7FA;
+  border: 2rpx solid transparent;
+}
+.type-selected {
+  background: #E3F2FD;
+  border-color: #5B9BD5;
+}
+.type-icon {
+  font-size: 40rpx;
+  margin-bottom: 8rpx;
+}
+.type-name {
+  font-size: 26rpx;
+  color: #333;
+  font-weight: 500;
+}
+
+/* 日期 */
+.date-display {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16rpx 20rpx;
+  background: #F5F7FA;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+  color: #333;
+}
+.date-arrow {
+  font-size: 32rpx;
+  color: #999;
+}
+
+/* 评分滑块 */
+.score-input-wrap {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+.score-slider {
+  flex: 1;
+}
+.score-value {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #5B9BD5;
+  margin-left: 16rpx;
+  min-width: 80rpx;
+  text-align: center;
+}
+
+/* 备注 */
+.note-input {
+  width: 100%;
+  min-height: 160rpx;
+  background: #F5F7FA;
+  border-radius: 12rpx;
+  padding: 20rpx;
+  font-size: 26rpx;
+  color: #333;
+  box-sizing: border-box;
+}
+
+/* 提交按钮 */
+.submit-btn {
+  background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
+  color: #fff;
+  font-size: 30rpx;
+  font-weight: bold;
+  text-align: center;
+  padding: 28rpx;
+  border-radius: 16rpx;
+  margin-top: 10rpx;
+}
+.submit-btn:active {
+  opacity: 0.85;
+}
+.btn-disabled {
+  opacity: 0.5;
+}
+
+/* ===== 底部 ===== */
+.bottom-spacer {
+  height: 60rpx;
+}
+</style>

+ 70 - 29
cfc-frontend/utils/api.js

@@ -3,6 +3,25 @@ import config from '@/config.js'
 const BASE_URL = config.API_BASE_URL
 const BASE_URL = config.API_BASE_URL
 const DANSHOP_BASE = config.DANSHOP_BASE_URL
 const DANSHOP_BASE = config.DANSHOP_BASE_URL
 
 
+var _redirectingToLogin = false
+
+function _clearAuthAndRedirect() {
+  if (_redirectingToLogin) return
+  _redirectingToLogin = true
+  uni.removeStorageSync('token')
+  uni.removeStorageSync('userId')
+  uni.removeStorageSync('currentRole')
+  uni.removeStorageSync('role')
+  uni.removeStorageSync('nickname')
+  uni.removeStorageSync('userName')
+  uni.removeStorageSync('currentChildId')
+  uni.showToast({ title: '登录已过期,请重新登录', icon: 'none' })
+  setTimeout(function() {
+    uni.reLaunch({ url: '/pages/login/login' })
+    _redirectingToLogin = false
+  }, 1500)
+}
+
 const request = (url, method = 'POST', data = {}) => {
 const request = (url, method = 'POST', data = {}) => {
   const token = uni.getStorageSync('token')
   const token = uni.getStorageSync('token')
   
   
@@ -17,6 +36,11 @@ const request = (url, method = 'POST', data = {}) => {
         'Authorization': token ? `Bearer ${token}` : ''
         'Authorization': token ? `Bearer ${token}` : ''
       },
       },
       success: (res) => {
       success: (res) => {
+        if (res.statusCode === 401 || res.data.code === 401) {
+          _clearAuthAndRedirect()
+          reject(res.data)
+          return
+        }
         if (res.data.code === 200) {
         if (res.data.code === 200) {
           resolve(res.data)
           resolve(res.data)
         } else {
         } else {
@@ -788,6 +812,16 @@ export const kickFamilyMember = (targetUserId) => {
   return request('/api/family/user/kick', 'POST', { targetUserId })
   return request('/api/family/user/kick', 'POST', { targetUserId })
 }
 }
 
 
+// 家庭邀请二维码
+export const generateFamilyInviteQrCode = () => {
+  return request('/api/family/invite/qrcode', 'POST')
+}
+
+// 通过邀请码接受邀请(扫码后调用)
+export const acceptFamilyInviteByCode = (inviteCode) => {
+  return request('/api/family/invite/accept-by-code', 'POST', { inviteCode })
+}
+
 // ===== AI 聊天 =====
 // ===== AI 聊天 =====
 export const aiSendMessage = (data) => {
 export const aiSendMessage = (data) => {
   return request('/api/ai/chat/send', 'POST', data)
   return request('/api/ai/chat/send', 'POST', data)
@@ -835,13 +869,7 @@ export const goodsCategory = (parentId = '0') => {
 
 
 // 商品列表(公开)
 // 商品列表(公开)
 export const productList = (params) => {
 export const productList = (params) => {
-  return new Promise((resolve, reject) => {
-    uni.request({
-      url: DANSHOP_BASE + '/buyer/goods/list',
-      method: 'GET',
-      data: params
-    })
-  })
+  return request('/api/product/list', 'POST', params)
 }
 }
 
 
 // 商品详情(公开)
 // 商品详情(公开)
@@ -1173,6 +1201,11 @@ const danshopRequest = (path, method = 'POST', data = {}) => {
         'Authorization': token ? `Bearer ${token}` : ''
         'Authorization': token ? `Bearer ${token}` : ''
       },
       },
       success: (res) => {
       success: (res) => {
+        if (res.statusCode === 401 || res.data.code === 401) {
+          _clearAuthAndRedirect()
+          reject(res.data)
+          return
+        }
         if (res.data.code === 200 || res.data.code === 0) {
         if (res.data.code === 200 || res.data.code === 0) {
           resolve(res.data)
           resolve(res.data)
         } else {
         } else {
@@ -1188,28 +1221,28 @@ const danshopRequest = (path, method = 'POST', data = {}) => {
   })
   })
 }
 }
 
 
-export const danshopCategoryList = (data) => danshopRequest('/danshop/category/list', 'POST', data)
-export const danshopCategoryTree = (data) => danshopRequest('/danshop/category/tree', 'POST', data)
-export const danshopProductList = (data) => danshopRequest('/danshop/goods/list', 'POST', data)
-export const danshopProductDetail = (data) => danshopRequest('/danshop/product/detail', 'POST', data)
-export const danshopCartList = (data) => danshopRequest('/danshop/cart/list', 'POST', data)
-export const danshopCartAdd = (data) => danshopRequest('/danshop/cart/add', 'POST', data)
-export const danshopCartRemove = (data) => danshopRequest('/danshop/cart/remove', 'POST', data)
-export const danshopCartClear = (data) => danshopRequest('/danshop/cart/clear', 'POST', data)
-export const danshopOrderCreate = (data) => danshopRequest('/danshop/order/create', 'POST', data)
-export const danshopOrderList = (data) => danshopRequest('/danshop/order/list', 'POST', data)
-export const danshopOrderDetail = (data) => danshopRequest('/danshop/order/detail', 'POST', data)
-export const danshopOrderConfirm = (data) => danshopRequest('/danshop/order/confirm', 'POST', data)
-export const danshopOrderCancel = (data) => danshopRequest('/danshop/order/cancel', 'POST', data)
-export const danshopAddressList = (data) => danshopRequest('/danshop/address/list', 'POST', data)
-export const danshopAddressSave = (data) => danshopRequest('/danshop/address/save', 'POST', data)
-export const danshopAddressDelete = (data) => danshopRequest('/danshop/address/delete', 'POST', data)
-export const danshopAddressSetDefault = (data) => danshopRequest('/danshop/address/setDefault', 'POST', data)
-export const danshopPaymentPrecreate = (data) => danshopRequest('/danshop/payment/precreate', 'POST', data)
-export const danshopLogisticsQuery = (data) => danshopRequest('/danshop/logistics/query', 'POST', data)
-export const danshopFavoriteList = (data) => danshopRequest('/danshop/favorite/list', 'POST', data)
-export const danshopFavoriteAdd = (data) => danshopRequest('/danshop/favorite/add', 'POST', data)
-export const danshopFavoriteRemove = (data) => danshopRequest('/danshop/favorite/remove', 'POST', data)
+export const danshopCategoryList = (data) => danshopRequest('/api/shop/category/list', 'POST', data)
+export const danshopCategoryTree = (data) => danshopRequest('/api/shop/category/tree', 'POST', data)
+export const danshopProductList = (data) => danshopRequest('/api/shop/product/list', 'POST', data)
+export const danshopProductDetail = (data) => danshopRequest('/api/shop/product/detail', 'POST', data)
+export const danshopCartList = (data) => danshopRequest('/api/shop/cart/list', 'POST', data)
+export const danshopCartAdd = (data) => danshopRequest('/api/shop/cart/add', 'POST', data)
+export const danshopCartRemove = (data) => danshopRequest('/api/shop/cart/remove', 'POST', data)
+export const danshopCartClear = (data) => danshopRequest('/api/shop/cart/clear', 'POST', data)
+export const danshopOrderCreate = (data) => danshopRequest('/api/shop/order/create', 'POST', data)
+export const danshopOrderList = (data) => danshopRequest('/api/shop/order/list', 'POST', data)
+export const danshopOrderDetail = (data) => danshopRequest('/api/shop/order/detail', 'POST', data)
+export const danshopOrderConfirm = (data) => danshopRequest('/api/shop/order/confirm', 'POST', data)
+export const danshopOrderCancel = (data) => danshopRequest('/api/shop/order/cancel', 'POST', data)
+export const danshopAddressList = (data) => danshopRequest('/api/shop/address/list', 'POST', data)
+export const danshopAddressSave = (data) => danshopRequest('/api/shop/address/save', 'POST', data)
+export const danshopAddressDelete = (data) => danshopRequest('/api/shop/address/delete', 'POST', data)
+export const danshopAddressSetDefault = (data) => danshopRequest('/api/shop/address/setDefault', 'POST', data)
+export const danshopPaymentPrecreate = (data) => danshopRequest('/api/shop/payment/precreate', 'POST', data)
+export const danshopLogisticsQuery = (data) => danshopRequest('/api/shop/logistics/query', 'POST', data)
+export const danshopFavoriteList = (data) => danshopRequest('/api/shop/favorite/list', 'POST', data)
+export const danshopFavoriteAdd = (data) => danshopRequest('/api/shop/favorite/add', 'POST', data)
+export const danshopFavoriteRemove = (data) => danshopRequest('/api/shop/favorite/remove', 'POST', data)
 
 
 // ===== 健康打卡 =====
 // ===== 健康打卡 =====
 export const healthCheckinList = (data) => request('/api/health/checkin/list', 'POST', data)
 export const healthCheckinList = (data) => request('/api/health/checkin/list', 'POST', data)
@@ -1236,6 +1269,14 @@ export const getProductsByDomain = (domain, page, size) => {
 // ===== 健康报告 =====
 // ===== 健康报告 =====
 export const getLatestHealthReport = (childId) => request('/api/health/report/latest', 'POST', { childId })
 export const getLatestHealthReport = (childId) => request('/api/health/report/latest', 'POST', { childId })
 export const getHealthIndicatorList = (childId) => request('/api/health/indicator/list', 'POST', { childId })
 export const getHealthIndicatorList = (childId) => request('/api/health/indicator/list', 'POST', { childId })
+export const createHealthReport = (data) => request('/api/health/report/create', 'POST', data)
+export const getReportList = (childId) => request('/api/health/report/list', 'POST', { childId })
+export const getReportDetail = (reportId) => request('/api/health/report/detail', 'POST', { reportId })
+
+// ===== 报告调研问卷 =====
+export const createReportSurvey = (reportId, childId) => request('/api/health/survey/create', 'POST', { reportId, childId })
+export const submitReportSurvey = (surveyId, surveyData) => request('/api/health/survey/submit', 'POST', { surveyId, surveyData })
+export const getSurveyStatus = (reportId) => request('/api/health/survey/status', 'POST', { reportId })
 
 
 // ===== 重要关系(联系人) =====
 // ===== 重要关系(联系人) =====
 export const getContactList = (data) => request('/api/contact/list', 'POST', data)
 export const getContactList = (data) => request('/api/contact/list', 'POST', data)

+ 893 - 0
docs/需求分析/精准营养与健康报告功能需求.md

@@ -0,0 +1,893 @@
+# 精准营养与健康报告功能 — 需求分析文档
+
+> **版本:** v1.1 (Oracle 审查修订)
+> **来源:** `docs/参考资料/` 肠菌健康评估报告 + 精准营养 Slide 方案
+> **关联设计:** 健康能量设计 (五维沙盘+账本)
+> **状态:** 分析完成 (v1.1 补充技术决策、数据隐私、闭环完整性)
+
+---
+
+## 一、背景与目标
+
+### 1.1 业务背景
+
+基于匿名用户 530010234 的肠道菌群健康评估报告(募极生物,2026-01-06)及"精准营养" Slide 方案,浠艾福平台需构建完整的**健康报告与精准营养闭环**。
+
+该功能是平台"身体(body)"维度的核心深度体验——从**报告上传 → AI 解析 → 调研问卷 → AI 对话 → 营养方案 → 产品推荐 → 导出/咨询 → 周期性复测迭代**的全闭环覆盖。
+
+> **范围说明:** 本分析覆盖"用户已有检测报告"的上传→解析→方案→推荐全流程。**居家采样**(平台售卖检测盒 → 居家采样 → 寄回实验室 → 报告自动同步)为二期扩展方向,本版本提供场景描述但不包含实现设计。
+
+### 1.2 用户场景
+
+| 场景 | 角色 | 触发条件 | 核心需求 |
+|------|------|---------|---------|
+| **上传报告** | 家长 | 拿到检测报告(医院/检测机构) | 通过小程序上传 PDF/图片,系统自动或手动录入关键指标 |
+| **解读报告** | 家长 | 报告上传完成 | 用通俗语言了解报告含义,关注重点指标和风险 |
+| **补充问卷** | 家长 | 报告信息不足以出方案 | 系统针对缺失信息生成调研问卷,用户填写补充 |
+| **制定方案** | 家长+全家 | 报告已解读、问卷已完成 | 结合全家情况(过敏、喜好、病史)生成家庭食谱和禁忌食品 |
+| **选购产品** | 家长 | 方案已生成 | 平台根据方案推荐适合的商品/服务 |
+| **咨询专家** | 家长 | 需要更专业的指导 | 在平台上找营养师出个性化方案(付费) |
+| **居家采样** ⏳二期 | 家长 | 没有现成报告,想从检测开始 | 在平台下单检测盒 → 居家采样 → 寄回实验室 → 平台自动同步报告 |
+| **复测对比** | 家长 | 已有第一次报告,间隔一段时间后 | 再次检测 → 系统自动对比前后指标变化趋势 |
+| **复用方案** | 平台 | 方案已存入系统 | 后续活动推荐、文章推荐、商品推荐基于方案中的标签 |
+
+### 1.3 参考数据类型
+
+以匿名 530010234 报告为参考,一份典型的肠道菌群健康评估报告包含:
+
+```
+┌──────────────────────────────────────────────┐
+│  健康总分: 73/100 (一般)                       │
+│  ├─ 菌群健康: 31/100    (⚠️ 偏低)             │
+│  ├─ 慢病控制: 85/100    (✅ 良好)             │
+│  ├─ 营养均衡: 68/100    (⚠️ 低于平均)         │
+│  ├─ 肠道年龄: 1.37 岁 (偏年轻)                │
+│  └─ 肠型: 拟杆菌型                           │
+├──────────────────────────────────────────────┤
+│  营养缺乏指标:                                 │
+│  ├─ 维生素B12: 14/100 (⬇️ 严重偏低)           │
+│  ├─ 苏氨酸: 14/100    (⬇️ 偏低)               │
+│  └─ 丙氨酸: 9/100     (⬇️ 显著偏低)           │
+├──────────────────────────────────────────────┤
+│  疾病风险评估:                                 │
+│  ├─ 感染性腹泻: 0.41  (⚠️ 需注意)              │
+│  └─ 其他: 低风险                               │
+├──────────────────────────────────────────────┤
+│  菌群结构:                                     │
+│  ├─ 有害菌: 78 (偏多)                         │
+│  ├─ 核心菌属: 77 (偏少)                       │
+│  ├─ 菌群平衡: 31 (中等)                       │
+│  └─ 菌种数: 313 (偏少, 均值约800)             │
+├──────────────────────────────────────────────┤
+│  个性化饮食方案 (基于拟杆菌型):                  │
+│  ├─ 增加膳食纤维 25-30g/天                     │
+│  ├─ 优选发酵蛋白                              │
+│  ├─ 控制高风险食物                            │
+│  └─ 推荐指数表 (牛蒡根 68, 南瓜 61, ...)       │
+└──────────────────────────────────────────────┘
+```
+
+---
+
+## 二、功能模块划分
+
+### 模块 A: 报告上传与解析
+
+#### A1. 报告上传
+
+| 项目 | 描述 |
+|------|------|
+| **用户入口** | 家长首页"精准营养"入口 → 上传报告页面 |
+| **支持格式** | PDF(初级优先)、图片(后续) |
+| **方式** | 手动选择文件上传,或从微信聊天/相册选择 |
+| **存储** | 文件 → 后端存储(本地/OSS),`health_reports.fileUrl` 存储路径 |
+| **归属** | 每份报告关联一个 childId(也可关联家长自己) |
+| **类型** | `physical_exam`(体检报告)、`gut_flora`(菌群检测报告)、其他可扩展 |
+
+#### A2. 报告解析(AI 提取关键指标)
+
+| 项目 | 描述 |
+|------|------|
+| **策略** | **初期**: 人工+模板录入(用户/运营填写关键字段)→ **后期**: AI OCR + 结构化提取 |
+| **AI 解析方案** | Dify 对接 OCR 模型(如 GPT-4o),读取 PDF 内容,提取结构化 JSON |
+| **提取字段** | 见下方数据模型(`health_reports` + `health_indicators` 表) |
+| **审核机制** | AI 提取结果 → 用户确认/修正 → 存入数据库 |
+| **已有 API** | `POST /api/health/report/create` — 创建报告含指标明细 |
+| **已有 API** | `POST /api/health/indicator/list` — 查看指标明细 |
+| **需要新增** | `POST /api/health/report/ai-parse` — AI 解析 PDF 并返回结构化建议数据 |
+
+#### A3. 关键指标记录
+
+**现有的 `health_reports` 表字段已覆盖:**
+- `overall_score` — 健康总分
+- `gut_health_score` — 菌群健康分
+- `chronic_disease_score` — 慢病控制分
+- `nutrition_score` — 营养均衡分
+- `gut_age` — 肠道预测年龄
+- `gut_type` — 肠型 (拟杆菌型/普氏菌型/混合型)
+
+**现有的 `health_indicators` 表字段已覆盖:**
+- `category` — 营养状况/疾病风险/肠道屏障/神经递质/短链脂肪酸/抗生素耐药/菌群状态
+- `indicator_name` — 指标名称 (如 "维生素B12")
+- `indicator_value` — 检测值 (如 "14")
+- `unit` — 单位
+- `ref_range` — 参考范围
+- `status` — 正常/偏低/偏高/过多/注意
+
+**需要补充的字段/表:**
+
+```sql
+-- 新增: 菌群检测专项数据(针对 gut_flora 类型报告)
+CREATE TABLE health_gut_flora (
+    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
+    report_id       BIGINT NOT NULL COMMENT '关联报告ID',
+    bacteria_name   VARCHAR(100) NOT NULL COMMENT '菌种名称',
+    bacteria_value  DECIMAL(10,4) COMMENT '菌种丰度值',
+    normal_range    VARCHAR(100) COMMENT '正常范围',
+    status          VARCHAR(20) COMMENT '正常/偏低/偏高',
+    category        VARCHAR(50) COMMENT '有益菌/有害菌/中性菌',
+    sort_order      INT,
+    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_report (report_id)
+);
+
+-- 新增: 疾病风险评估
+CREATE TABLE health_disease_risk (
+    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
+    report_id       BIGINT NOT NULL COMMENT '关联报告ID',
+    disease_name    VARCHAR(100) NOT NULL COMMENT '疾病名称',
+    risk_value      DECIMAL(5,2) COMMENT '风险值 (0-1)',
+    risk_level      VARCHAR(20) COMMENT '高风险/需注意/低风险',
+    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_report (report_id)
+);
+```
+
+---
+
+#### A4. 复测对比(P2)
+
+##### 对比逻辑
+
+当同一孩子(child_id)存在多份同类型报告时,系统支持前后对比:
+
+| 维度 | 对比方式 | 示例 |
+|------|---------|------|
+| **总分趋势** | 多份报告的 `overall_score` 按时间绘制折线 | 73 → 78 → 82(三次检测上升趋势) |
+| **子维度对比** | 同一子维度分值的柱状对比 | 菌群健康 31→45→52 |
+| **指标变化** | 同指标名的 `indicator_value` 前后变化 | 维生素B12 14→38(↑171%) |
+| **菌群变化** | 同菌种名 `bacteria_value` 丰度变化 | 双歧杆菌 2.3→4.1(↑78%) |
+| **疾病风险变化** | 同疾病 `risk_value` 变化 | 感染性腹泻 0.41→0.25(↓39%) |
+
+##### 后端 API
+
+```java
+// 获取同孩子同类型报告的对比数据
+POST /api/health/report/compare
+// 请求: { childId, reportType, limit?: 2 }
+// 响应: {
+//   reports: [ { reportId, overallScore, reportDate }, ... ],
+//   dimensions: [ { name: "菌群健康", scores: [31,45,52], changes: ["+45%","+16%"] }, ... ],
+//   indicators: [ {
+//     name: "维生素B12",
+//     series: [ { value: 14, status: "偏低" }, { value: 38, status: "正常" } ],
+//     changePercent: "+171%"
+//   }, ... ],
+//   gutFlora: [ { name: "双歧杆菌", series: [...] }, ... ],
+//   diseaseRisks: [ { name: "感染性腹泻", series: [...] }, ... ]
+// }
+```
+
+##### 前端对比页(`retest-compare.vue`)
+
+| 区域 | 展示方式 |
+|------|---------|
+| 选择对比的报告 | 下拉选择/时间轴选择 |
+| 总分趋势 | ECharts 折线图(`uni-ec-canvas`) |
+| 各维度对比 | 并列柱状图(前一次 vs 最近一次) |
+| 指标明细对比 | 表格:指标名 → 旧值(状态) → 新值(状态) → 变化率 → 变化方向箭头 |
+| 菌群对比 | 丰度变化条状图 |
+| 风险对比 | 热力图式高低变化 |
+
+---
+
+### 模块 B: 调研问卷
+
+#### B1. 触发时机
+
+报告 AI 解析完成后,系统判断信息缺口,自动生成定制化问卷:
+
+| 场景 | 问卷内容 | 补充目的 |
+|------|---------|---------|
+| 报告无饮食偏好 | 口味偏好、忌口、过敏原 | 精准匹配食物推荐 |
+| 报告无生活习惯 | 作息、运动频率、压力水平 | 完善生活方式干预 |
+| 报告无家庭病史 | 家庭成员慢性病/遗传病 | 风险评估 + 禁忌食品 |
+| 报告无目标 | 用户期望改善方向(减重/增肌/改善肠道/提升精力) | 方案目标设定 |
+| 报告无全家人信息 | 家庭成员年龄/性别/健康情况 | 生成全家食谱 |
+
+#### B2. 信息缺口检测算法
+
+检测机制分为三层,逐级评估报告信息的完整性:
+
+**L1 — 字段完整性检查(数据层)**
+
+| 检查项 | 检测方式 | 触发问卷 |
+|--------|---------|---------|
+| `user_nutrition_profile` 是否存在? | 查询 DB | 不存在 → 全量问卷 |
+| `taste_preference` 是否为空? | `IS NULL` | 饮食偏好表单 |
+| `dietary_restrictions` 是否为空? | `IS NULL` | 饮食限制/过敏原表单 |
+| `sleep_pattern` 是否为空? | `IS NULL` | 生活习惯表单 |
+| `family_history` 是否为空? | `IS NULL` | 家庭病史表单 |
+| `health_goals` 是否为空? | `IS NULL` | 健康目标表单 |
+| `family_members` 是否为空或无18岁以下成员? | `JSON_LENGTH` < 2 | 家庭成员表单 |
+| 以上全部已填写? | 全部非空 | 跳过问卷 |
+
+**L2 — 报告覆盖度分析(AI层,P1+)**
+
+当字段数据已存在但历史报告显示某些指标持续偏低时,Dify 分析报告维度覆盖度,判断是否需要补充问卷:
+- 报告仅覆盖菌群维度 → 补充饮食频率问卷
+- 报告无过敏原数据 → 补充过敏原筛查问卷
+- 报告无家族病史 → 补充遗传风险问卷
+
+**L3 — 用户主动触发**
+
+用户在 AI 对话过程中可主动请求补充填写某个维度("我想补充一下我的过敏信息"),系统唤起对应子表单。
+
+**检测流程:**
+```
+报告解析完成 → 执行 L1 字段检查
+    ├─ 全部已填 → 跳过问卷 → 直接进入方案生成
+    ├─ 部分缺失 → 标记缺失字段 → 生成对应子表单
+    └─ 全部缺失 → 生成全量问卷
+        ↓
+用户填写提交 → 存入 user_nutrition_profile
+        ↓
+(可选)P1+ AI 层检查报告覆盖度 → 补充专项问卷
+```
+
+```
+报告上传 → AI解析 → 检测信息缺口
+                         ↓
+              生成个性化调研问卷
+               ├─ 基本信息 (已填则跳过)
+               ├─ 饮食偏好 (必填)
+               ├─ 生活习惯 (必填)
+               ├─ 家庭病史 (必填)
+               ├─ 健康目标 (必填)
+               └─ 家庭成员 (必填)
+                         ↓
+              用户填写/提交 → 补充完成
+                         ↓
+              问卷数据存入 user_nutrition_profile
+```
+
+#### B3. 问卷模板系统
+
+问卷由 **模板定义 + 用户作答** 两部分组成。模板决定"问什么",作答表存"答什么"。
+
+**P0 MVP 方案:硬编码前端**
+- 问卷问题和表单 UI 直接写在 `questionnaire.vue` 中
+- 简单:不需要模板表、不需要管理后台
+- 限制:修改题目需要发版,不支持条件逻辑
+
+**模板数据结构(为 P1 迁移预留):**
+
+```sql
+-- 问卷模板表(P1+ 启用)
+CREATE TABLE questionnaire_template (
+    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
+    template_key    VARCHAR(50) NOT NULL COMMENT '模板标识: basic_info/diet/lifestyle/family_history/goals/family_members',
+    title           VARCHAR(200) COMMENT '问卷标题',
+    description     TEXT COMMENT '问卷说明',
+    
+    -- 问题列表 (JSON 数组, 含题型/选项/条件)
+    questions       TEXT NOT NULL COMMENT '[
+        {
+            "key": "taste_preference",
+            "type": "checkbox",
+            "label": "口味偏好",
+            "options": ["清淡","辛辣","甜","咸","无偏好"],
+            "required": true,
+            "depends_on": null
+        },
+        {
+            "key": "allergy_detail",
+            "type": "text",
+            "label": "请描述过敏详情",
+            "required": false,
+            "depends_on": {
+                "field": "dietary_restrictions",
+                "has_value": "过敏"
+            }
+        }
+    ]',
+    
+    sort_order      INT DEFAULT 0,
+    status          VARCHAR(20) DEFAULT 'active',
+    version         INT DEFAULT 1,
+    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+);
+```
+
+| 阶段 | 方案 | 说明 |
+|------|------|------|
+| **P0 MVP** | 前端 `questionnaire.vue` 硬编码 | 快速上线,不支持动态配置 |
+| **P1+** | 迁移到 `questionnaire_template` 表 | 支持运营配置题目、条件逻辑、多版本 |
+
+#### B4. 数据模型(新增)
+CREATE TABLE user_nutrition_profile (
+    id                BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id           BIGINT NOT NULL COMMENT '用户ID',
+    report_id         BIGINT COMMENT '关联报告ID',
+    
+    -- 饮食偏好
+    taste_preference  VARCHAR(200) COMMENT '口味偏好: 清淡/辛辣/甜/咸/无偏好',
+    dietary_restrictions VARCHAR(500) COMMENT '饮食限制/过敏原(JSON数组)',
+    cuisine_style     VARCHAR(100) COMMENT '偏好菜系',
+    
+    -- 生活习惯
+    sleep_pattern     VARCHAR(100) COMMENT '作息规律',
+    exercise_freq     VARCHAR(50) COMMENT '运动频率: 几乎不/1-2次/3-5次/每天',
+    stress_level      VARCHAR(20) COMMENT '压力水平: 低/中/高',
+    
+    -- 家庭病史 (JSON)
+    family_history    TEXT COMMENT '家庭成员疾病史',
+    
+    -- 健康目标
+    health_goals      VARCHAR(500) COMMENT '健康目标(JSON数组)',
+    
+    -- 家庭成员清单 (JSON, 含年龄/性别/关系)
+    family_members    TEXT COMMENT '家庭成员信息',
+    
+    -- 填表状态
+    questionnaire_status VARCHAR(20) DEFAULT 'pending' COMMENT 'pending/completed',
+    
+    created_at        DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at        DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_user (user_id),
+    INDEX idx_report (report_id)
+);
+```
+
+---
+
+### 模块 C: AI 对话解读报告
+
+#### C1. 现状
+
+已有 `AIService` + `FamilyContextService` + `AIChatController`,对接 Dify Chatbot"CFC 家庭助手"。现有上下文仅包含 `user` + `children` 基础信息(昵称、积分、DAN等级、任务完成率)。
+
+#### C2. 需要补充的上下文
+
+向 Dify inputs 增加报告数据,使 AI 能回答:
+
+| 用户问题 | 需要的数据 | 来源 |
+|---------|-----------|------|
+| "我的报告说维生素B12偏低,是什么意思?" | health_indicators (category=营养状况) | `health_indicators` |
+| "我这个菌群平衡31分算严重吗?" | health_reports.gut_health_score | `health_reports` |
+| "我家孩子应该吃什么补充B12?" | 营养指标 + 食物推荐数据 | `health_indicators` + 内置知识 |
+| "我的肠型是拟杆菌型,适合吃什么?" | health_reports.gut_type | `health_reports` |
+| "根据我的报告,有什么平台产品推荐?" | 报告指标 + 产品映射标签 | 见模块 E |
+
+#### C3. 后端接口新增
+
+```java
+// 新增: 获取AI聊天上下文时附带报告数据
+POST /api/ai/report-context
+// 请求: { reportId }
+// 响应: {
+//   report: { overallScore, gutType, nutritionScore, ... },
+//   indicators: [ { category, name, value, status }, ... ],
+//   diseaseRisks: [ { disease, riskValue, level }, ... ]
+// }
+
+// 修改: FamilyContextService.buildContext() 增加 reportId 参数
+// 当有最新报告时,自动将报告数据注入 Dify inputs
+```
+
+**Dify 应用的 System Prompt 需要更新:** 新增报告数据输入变量 `report_summary`、`indicators`、`disease_risks`,让 AI 能读懂报告内容并给出个性化建议。
+
+> **P0 能力范围(已覆盖完整指标级问答):** P0 即注入完整的报告摘要(总分、子分、肠型)**及所有非正常指标明细**(~500 tokens,远低于 6.2 节设定的 3000 tokens 上限)。因此 P0 即可支持"维生素B12=14偏低是什么意思?"等具体指标级问答。前端无需额外限制引导文案。
+
+---
+
+### 模块 D: 家庭食谱与禁忌食品
+
+#### D1. 核心逻辑
+
+基于 **报告中的肠型** + **调研问卷中的饮食偏好/过敏原/家庭病史**,生成个性化方案:
+
+```
+输入:
+  报告肠型: 拟杆菌型
+  + 营养缺乏指标: B12(⬇️), 苏氨酸(⬇️)
+  + 有害菌偏多: 78
+  + 饮食偏好: 清淡
+  + 过敏原: 无
+  + 家庭成员: 父亲(40), 母亲(38), 儿子(8, 无过敏)
+  + 健康目标: 改善肠道健康, 提升精力
+
+输出:
+  家庭食谱方案:
+    ├─ 早餐建议: 燕麦粥+蓝莓+无糖酸奶 (高纤维+益生菌)
+    ├─ 午餐建议: 糙米饭+蒸南瓜+清蒸鱼+炒菠菜 → 全家适配
+    ├─ 晚餐建议: 杂豆汤+西兰花+少量瘦肉
+    └─ 加餐: 苹果/坚果
+
+  禁忌食品:
+    ├─ ❌ 避免: 油炸食品、加工肉类(培根/香肠)
+    ├─ ⚠️ 限制: 红肉(每周≤2次)
+    └─ 原理: 基于拟杆菌型 + 有害菌偏多 + B12需求
+
+  推荐食品列表:
+    ├─ ✅ 强烈推荐: 牛蒡根(68), 南瓜(61), 四季豆(55)
+    ├─ ✅ 推荐: 大白菜(61), 秋葵(57), 甘薯(51)
+    └─ 每种食物附推荐指数(0-100)
+```
+
+#### D2. 生成策略
+
+| 层级 | 方式 | 说明 |
+|------|------|------|
+| **L1 模板** | 基于肠型的预设模板 | 拟杆菌型 → 高纤维+发酵蛋白模板;普氏菌型 → 植物基+低脂模板 |
+| **L2 AI 个性化** | Dify AI 根据指标+问卷微调 | 叠加缺乏营养素的针对性食物,排除过敏原 |
+| **L3 专家定制** | 人工营养师方案 | 付费用户由专业营养师手工出方案,覆盖 L1+L2 |
+
+#### D3. 数据模型(新增)
+
+```sql
+-- 营养方案主表
+CREATE TABLE nutrition_plan (
+    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id         BIGINT NOT NULL COMMENT '用户ID',
+    report_id       BIGINT COMMENT '关联报告ID',
+    plan_type       VARCHAR(20) DEFAULT 'ai' COMMENT 'ai(系统生成)/expert(专家方案)',
+    
+    -- 核心数据
+    gut_type        VARCHAR(50) COMMENT '肠型',
+    overall_score   INT COMMENT '健康总分',
+    
+    -- 方案内容 (结构化 JSON)
+    meal_plan       TEXT COMMENT '食谱方案 (JSON, 含早/午/晚/加餐及分量)',
+    forbidden_foods TEXT COMMENT '禁忌食品列表 (JSON)',
+    recommended_foods TEXT COMMENT '推荐食品列表 (JSON, 含推荐指数)',
+    nutrition_tips  TEXT COMMENT '营养建议 (Markdown)',
+    supplement_advice TEXT COMMENT '营养素补充建议 (JSON)',
+    
+    -- 标签 (用于后续推荐)
+    tags            VARCHAR(500) COMMENT '方案标签, 逗号分隔',
+    
+    -- 状态
+    status          VARCHAR(20) DEFAULT 'draft' COMMENT 'draft/active/archived',
+    expert_id       BIGINT COMMENT '营养师ID(plan_type=expert时)',
+    expert_notes    TEXT COMMENT '营养师备注',
+    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_user_status (user_id, status)
+);
+
+-- 方案-产品关联 (推荐产品)
+CREATE TABLE nutrition_plan_product (
+    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
+    plan_id         BIGINT NOT NULL,
+    product_id      BIGINT NOT NULL,
+    reason          VARCHAR(500) COMMENT '推荐理由',
+    sort_order      INT DEFAULT 0,
+    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_plan (plan_id)
+);
+```
+
+---
+
+### 模块 E: 平台产品推荐
+
+#### E1. 推荐机制
+
+| 推荐维度 | 数据来源 | 示例 |
+|---------|---------|------|
+| **营养缺乏** | `health_indicators` 中 status=偏低的指标 | B12偏低 → 推荐含B12的营养补充剂、相关食品 |
+| **肠型匹配** | `health_reports.gut_type` | 拟杆菌型 → 推荐高纤维食品、发酵食品 |
+| **健康目标** | `user_nutrition_profile.health_goals` | 改善肠道 → 推荐益生菌、膳食纤维商品 |
+| **禁忌排除** | `nutrition_plan.forbidden_foods` | 避免油炸 → 排除相关商品 |
+
+#### E2. 产品标签映射
+
+已有 `Product` 实体(`@TableName("products")`),相关字段分析:
+
+| Product 现有字段 | 是否可复用 | 说明 |
+|-----------------|-----------|------|
+| `productType` | ⚠️ 部分 | 商品类型分类(非营养维度),可作为推荐预过滤条件 |
+| `domain` | ⚠️ 部分 | 领域分类(非营养维度),可作为域过滤 |
+| `memberPrice` / `memberEligible` | ✅ 过滤条件 | 推荐时需校验用户会员资格和价格区间 |
+| `price` (分) | ✅ 过滤条件 | 价格约束 |
+
+**不足:** Product 表缺少营养维度的标签体系。需要补充 `nutrition_tags` 字段:
+
+- 方案A(推荐):Product 表新增 `nutrition_tags` 字段(JSON数组,如 `["高纤维", "益生菌", "维生素B12", "低脂"]`)
+- 方案B:新建 `product_nutrition_tag` 关联表(更灵活,但不急需)
+
+**推荐逻辑定位:** 建议在 `ProductService` 中扩展 `list()` 方法支持标签过滤参数,而非新建 `ProductRecommendService`。`ProductController` 已有 `/api/product/list` 端点,可通过新增查询参数实现推荐筛选。
+
+---
+
+### 模块 F: 报告导出
+
+#### F1. 导出内容
+
+| 项目 | 格式 | 说明 |
+|------|------|------|
+| 报告摘要 | PDF / 图片 | 健康总分 + 各维度得分概览 |
+| 指标明细 | PDF / CSV | 所有指标值 + 参考范围 + 状态 |
+| 营养方案 | PDF | 食谱 + 禁忌 + 推荐清单 |
+| 完整报告 | PDF | 以上全部内容打包 |
+
+#### F2. 导出方式
+
+```mermaid
+前端(小程序) → 请求导出 → 后端生成PDF → 返回文件URL → 前端预览/下载/微信分享
+```
+
+- **后端 PDF 生成**: iText / Apache PDFBox + HTML-to-PDF(模板渲染)
+- **前端**: 使用 `uni.downloadFile` + `uni.openDocument` 预览或保存
+
+---
+
+### 模块 G: 营养师咨询(付费)
+
+#### G1. 流程
+
+```
+用户 → "找营养师咨询"按钮
+    ↓
+选择咨询类型: 报告解读 / 方案定制 / 一对一
+    ↓
+查看营养师列表 (资质/评分/价格)
+    ↓
+选择营养师 → 下单支付 (创建咨询订单)
+    ↓
+营养师接单 → AI辅助生成初稿 → 营养师修改确认
+    ↓
+方案发布 → 用户收到通知 → 查看/下载
+    ↓
+存入 nutrition_plan (plan_type=expert)
+    供后续活动推荐、文章推荐、商品推荐使用
+```
+
+#### G2. 与现有测评预约体系的复用
+
+| 现有能力 | 复用方式 |
+|---------|---------|
+| `AssessmentOrder` 订单系统 | 创建营养咨询订单(复用或新建咨询订单类型) |
+| `PaymentOrder` 支付 | 支付咨询费用 |
+| `Guide` 师资体系 (`guides` 表) | 营养师纳入 `guide` 表 (`guides`),使用 `type='teacher'` + `specialty='nutrition'` 区分。`specialty` 字段已存在,无需新增 DDL |
+| `Appointment` 预约 | 排期预约咨询时间 |
+
+#### G3. 数据模型新增
+
+```sql
+-- 营养咨询订单
+CREATE TABLE nutrition_consultation_order (
+    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id         BIGINT NOT NULL,
+    nutritionist_id BIGINT NOT NULL COMMENT '营养师ID(关联guides表)',
+    report_id       BIGINT COMMENT '关联报告ID',
+    consultation_type VARCHAR(20) COMMENT 'report_reading/plan_customization/one_on_one',
+    status          VARCHAR(20) DEFAULT 'pending' COMMENT 'pending/paid/processing/completed/cancelled',
+    amount          DECIMAL(10,2) COMMENT '咨询费用',
+    payment_order_id BIGINT COMMENT '关联支付订单',
+    plan_id         BIGINT COMMENT '产出的方案ID',
+    remark          TEXT,
+    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_user (user_id),
+    INDEX idx_nutritionist (nutritionist_id)
+);
+```
+
+#### G4. 营养师端工作流程
+
+##### 营养师操作界面
+
+| 界面 | 载体 | 功能 |
+|------|------|------|
+| **咨询订单列表** | Web管理端 (`cfc-web`) → 新增 `views/teacher/nutrition/` 目录 | 查看待接单/进行中/已完成的咨询订单 |
+| **用户报告预览** | Web管理端 | 查看用户授权的 `health_reports` + `health_indicators` + `user_nutrition_profile` |
+| **AI辅助初稿** | Web管理端 → 调用 Dify 生成方案初稿 | 一键生成 `nutrition_plan`(plan_type=ai)作为参考 |
+| **方案编辑** | Web管理端 | 手工修改/确认方案,更新 `nutrition_plan.meal_plan`、`forbidden_foods` 等字段 |
+| **方案发布** | Web管理端 | 确认后设置 `status=active`、`plan_type=expert`,触发用户通知 |
+
+##### 营养师流程
+
+```
+用户下单 → 支付成功 → 订单状态变为 paid
+    ↓
+营养师在 Web管理端 看到新订单(状态 pending)
+    ↓
+营养师接单 → 状态变为 processing
+    ↓
+营养师查看用户报告 + AI 生成初稿
+    ↓
+营养师编辑/定制方案
+    ↓
+营养师发布方案 → 状态变为 completed
+    → nutrition_plan 写入数据库 (plan_type=expert, expert_id=营养师ID)
+    → 方案自动提取标签,进入推荐流转系统
+    ↓
+用户收到通知 → 在小程序查看/下载方案
+```
+
+##### 营养师角色区分
+
+> **注意:** 系统使用 `Guide` 实体 (`@TableName("guides")`) 作为统一服务商表。`specialty` 字段已存在,无需新建 DDL。`Teacher` 实体 (`teachers`) 为旧表,数据已迁移到 `guides`。
+
+| 字段 | 值 | 说明 |
+|------|----|------|
+| `Guide.specialty` | `nutrition` | 纯营养师 |
+| `Guide.specialty` | `assessment` | 纯成长规划师 |
+| `Guide.specialty` | `both` | 既是规划师又是营养师 |
+| `Guide.type` | `teacher` | 服务商子类型(默认值) |
+
+**菜单权限:** Web管理端根据 `specialty` 决定是否显示"营养咨询"菜单项。
+
+---
+
+### 模块 H: 方案复用与推荐流转
+
+#### H1. 标签系统
+
+每个 `nutrition_plan` 生成后提取标签,形成用户画像标签:
+
+```json
+{
+  "tags": ["拟杆菌型", "B12偏低", "高纤维需求", "肠道改善", "清淡口味", "无过敏"],
+  "diet_type": "high_fiber_fermented",
+  "health_goals": ["gut_health", "energy_boost"]
+}
+```
+
+#### H2. 营养标签词表(Tag Taxonomy)
+
+初始词表(运营维护,20-30个标签,P2 实施前定义完成):
+
+| 分类 | 标签 | 示例匹配产品 |
+|------|------|-------------|
+| **营养素类** | 维生素B12、维生素D、维生素C、铁、钙、锌、膳食纤维、益生菌、益生元、Omega-3、蛋白粉 | B12补充剂、钙片、益生菌粉 |
+| **饮食限制类** | 无麸质、低FODMAP、低脂、低糖、无乳糖、素食、高蛋白 | 无麸质食品、低脂酸奶 |
+| **健康状况类** | 肠道改善、免疫力提升、体重管理、控糖、护肝、抗氧化 | 益生菌、膳食纤维、护肝片 |
+| **食材类** | 发酵食品、全谷物、深色蔬菜、浆果、坚果 | 酸奶、糙米、蓝莓 |
+| **肠型类** | 拟杆菌型适配、普氏菌型适配、混合型适配 | —(影响食谱而非产品) |
+
+**打标责任方:** 运营人员手动打标(P2上线时对在售商品做一次全量回溯),后续新品上架时自动推荐标签。**标签新增流程:** 营养师/运营提报 → 管理员审核 → 生效。
+
+**匹配算法(增强版):**
+
+```
+匹配度 = Σ(命中标签权重) / Σ(方案标签基础权重) 
+  × 类目过滤(排除不相关类目)
+  × 年龄适宜性过滤
+  × 价格区间过滤(用户 budget 内)
+```
+
+> P2 MVP 先用简单交集/总数匹配,后续根据数据积累逐步引入权重和过滤条件。
+
+#### H3. 后续推荐场景
+
+| 场景 | 匹配逻辑 |
+|------|---------|
+| **活动推荐** | 标签匹配活动维度: `高纤维需求 → 烹饪课程`, `肠道改善 → 健康讲座` |
+| **文章推荐** | 标签匹配内容: `B12偏低 → 维生素B12科普文章`, `拟杆菌型 → 肠型饮食指南` |
+| **商品推荐** | 标签匹配产品标签: `益生菌`, `膳食纤维粉`, `B12补充剂` |
+| **任务推荐** | 系统自动下发相关任务: `每日一杯酸奶`, `午餐加入一份绿叶菜` |
+
+---
+
+## 三、数据流总图
+
+```
+用户上传报告(PDF/图片)
+    │
+    ▼
+后端存储文件 → POST /api/health/report/create
+    │
+    ├─ AI 解析(后期): POST /api/health/report/ai-parse
+    │   → Dify OCR + 提取结构化数据
+    │   → 存入 health_indicators + health_gut_flora + health_disease_risk
+    │   → 返回解析结果供用户确认
+    │
+    ├─ 信息缺口检测
+    │   → 生成调研问卷
+    │   → POST /api/nutrition/profile/save
+    │   → user_nutrition_profile 完成
+    │
+    ├─ AI 对话解读
+    │   → FamilyContextService 增加报告数据
+    │   → Dify inputs: { report_summary, indicators, disease_risks }
+    │   → 用户可对话提问
+    │
+    ├─ 方案生成
+    │   → AI/模板生成 nutrition_plan
+    │   → 含 meal_plan, forbidden_foods, recommended_foods
+    │   → 标签提取 → 后续推荐用
+    │
+    ├─ 产品推荐
+    │   → nutrition_plan.tags → 匹配 product.nutrition_tags
+    │   → 返回推荐产品列表
+    │
+    ├─ 导出
+    │   → 后端生成 PDF
+    │   → 用户下载/分享
+    │
+    └─ 专家咨询(付费)
+        → 创建 nutrition_consultation_order
+        → 支付 → 营养师接单 → 出方案
+        → nutrition_plan (expert) → 复用标签
+```
+
+---
+
+## 四、与现有系统的对接点
+
+| 现有模块 | 对接方式 | 工作量 |
+|---------|---------|--------|
+| `HealthReport` + `HealthIndicator` | 直接使用现有表和 API,扩展字段 | 小 |
+| `AIService` + `AIChatController` | 扩展 `FamilyContextService` 注入报告数据,更新 Dify Prompt | 中 |
+| `HealthReportController` | 可复用 `/report/create`, `/indicator/list` | 小 |
+| `AssessmentOrder` / `PaymentOrder` | 复用或扩展订单类型支持营养咨询 | 中 |
+| `Teacher` 表 | 扩展营养师角色(需区分 teacher 和 nutritionist) | 小 |
+| `Task` 系统 | 方案产出标签后,可自动下发配套任务(健康能量系统) | 中 |
+| `WuxingSandbox` | 报告数据纳入"body"维度评分(已实现 `calculateBodyScore`) | 小 |
+
+---
+
+## 五、实施优先级建议
+
+| 阶段 | 模块 | 核心交付 | 工作量估算 |
+|------|------|---------|-----------|
+| **P0 MVP** | A1 报告上传 + A3 指标记录 | 用户能上传报告、系统存储关键指标 | 2-3天 |
+| | C AI 对话解读报告 | 注入报告数据到 Dify,AI 能回答报告相关问题 | 1-2天 |
+| **P1 核心** | B 调研问卷 | 信息缺口检测 + 问卷交互页面 | 2-3天 |
+| | D 家庭食谱 | 基于肠型的模板方案生成 + 禁忌食品 | 2-3天 |
+| **P2 增值** | E 产品推荐 | 方案标签 + 产品匹配 | 1-2天 |
+| | F 报告导出 | PDF 生成 + 下载 | 1-2天 |
+| **P3 商业** | G 营养师咨询 | 咨询订单 + 支付 + 专家方案 | 3-5天 |
+| **持续** | H 标签复用 | 活动/文章/商品推荐链 | 2-3天 |
+
+---
+
+## 六、技术决策与风险评估
+
+### 6.1 文件存储方案
+
+| 决策项 | 推荐方案 | 备选 | 说明 |
+|--------|---------|------|------|
+| **存储位置** | 阿里云OSS(或腾讯云COS) | 本地文件系统 | 小程序用户上传PDF/图片,后端接收后上传OSS,`file_url`存OSS路径 |
+| **上传端点** | `POST /api/health/report/upload` | 复用现有 `/report/create` | 先上传文件返回URL,再调用 `/report/create` 写入结构化数据 |
+| **文件大小限制** | 单文件 ≤ 20MB | — | 微信小程序上传限制 + OSS 稳定性 |
+| **支持格式** | PDF (优先)、JPG/PNG | — | 一期仅支持PDF,二期开放图片 |
+
+### 6.2 Dify 上下文长度策略
+
+**风险:** 整份报告(135页PDF提取的数据)可能超过 Dify chat-messages API 的上下文窗口限制。
+
+| 层级 | 策略 | 内容 |
+|------|------|------|
+| **L1 摘要** | 始终注入 | `report_summary`: 总分、各子维度分、肠型、报告日期(~200 tokens) |
+| **L2 关键指标** | 按需注入 | `indicators`: 仅 status != "正常" 的指标(~500 tokens) |
+| **L3 详细数据** | 用户提问时附加 | 当用户问具体指标时,通过 Dify 工作流中的 API 工具查询详细数据 |
+| **兜底** | 截断告警 | 总输入限制视底层模型而定(GPT-4o 通常 8K-128K tokens),超出时截断并提示"如需更多详情,请具体询问某项指标"。**3000 tokens** 为实施时的初始保守阈值,上线后根据实际模型和效果调整 |
+
+### 6.3 PDF AI 解析策略
+
+| 阶段 | 方案 | 说明 |
+|------|------|------|
+| **P0 MVP** | 模板录入 + 人工辅助 | 用户上传PDF后,系统呈现字段模板,用户手动填入关键数值(总分、子分等),系统存入 `health_reports` + `health_indicators` |
+| **P1 增强** | Dify OCR + GPT-4o 提取 | 调用 Dify 知识库或 GPT-4o API 对PDF进行OCR和结构化提取,返回JSON供用户确认/修正 |
+| **降级** | 人工客服代录 | 用户不愿自己填 → 联系客服 → 运营后台代录 |
+
+**注意:** 中文PDF(含表格、图表、评分截图)的AI提取精度需在实施前用真实报告做 Pilot 测试。预计首版准确率在 70-80%,需要用户确认/修正机制。
+
+### 6.4 PDF 导出方案
+
+| 决策项 | 方案 |
+|--------|------|
+| **后端生成** | iText 7 (开放许可) + 中文字体包(思源黑体,SIL许可免费可商用) |
+| **中文字体** | 将 `SourceHanSansSC-Regular.ttf` 打包到 `resources/fonts/` |
+| **导出内容** | 报告摘要 + 指标明细 + 营养方案,分节生成 |
+| **触发方式** | 后端生成 → 上传OSS → 返回URL → 前端 `uni.downloadFile` + `uni.openDocument` |
+| **模板引擎** | iText 7 程序化构建(HTML-to-PDF 需额外引入 `pdfHTML` 依赖) |
+
+### 6.5 健康数据隐私与合规
+
+| 要求 | 措施 |
+|------|------|
+| **数据加密** | `health_reports` / `health_indicators` / `user_nutrition_profile` 等敏感表字段建议加密存储(AES-256),或至少限制数据库访问IP |
+| **访问控制** | 所有报告API必须 JWT 认证,仅本人/监护人可访问(`@RequestAttribute("userId")` 校验) |
+| **最小化存储** | PDF原件存储保留,但API返回时不包含原始文件内容,仅返回结构化提取结果 |
+| **合规参考** | 《个人信息保护法》《健康医疗大数据标准》——用户应可自主删除报告(提供删除API) |
+
+### 6.6 模块间依赖关系与风险
+
+| 依赖 | 风险 | 缓解措施 |
+|------|------|---------|
+| **AI对话(C) 依赖 报告解析(A)** | P0 中 C 需报告数据才能回答问题,但P0解析是手工录入,数据有限 | P0 AI 对话定位为"报告摘要问答",仅基于整体得分/肠型等有限字段对话。完整指标级问答放在 P1 |
+| **产品推荐(E) 依赖 方案标签(D)** | E中"方案标签匹配产品"需要Product表已有标签体系 | P2 前需完成 Product 表增加 `nutrition_tags` 字段(JSON数组)或新建 `product_nutrition_tag` 关联表 |
+| **方案复用(H) 影响多个现有模块** | Activity/Article/Task 均无标签系统 | H 分为两阶段:P2 仅做方案→产品推荐(标签匹配产品),P3 再做方案→活动/文章/任务推荐(需在对应模块加标签) |
+| **营养师咨询(G) 依赖 Guide 表和支付系统** | Guide 表已有 `specialty` 字段,需增加 `nutrition` 值。现有 `type='teacher'` 搭配 `specialty='nutrition'` 区分营养师 | 无需 DDL。`Guide.specialty` 字段已存在(line 38, Guide.java),直接设置值即可。现有规划师代码不受影响 |
+
+### 6.7 Product 表标签补充方案
+
+```sql
+-- 方案A(推荐):Product 表新增 nutrition_tags 字段
+-- 注意:实际表名为 `products`(对应 @TableName("products"))
+-- Product.java 已有 productType, domain, price 等字段可作为推荐过滤条件
+ALTER TABLE products ADD COLUMN nutrition_tags VARCHAR(500) DEFAULT NULL 
+    COMMENT '营养标签JSON数组, 如 ["高纤维","益生菌","维生素B12"]';
+
+-- 方案B(更灵活,但不急需):新建 product_nutrition_tag 关联表
+-- 推荐先做方案A,简单直接,后续需要再拆分为方案B
+```
+
+### 6.8 报告生命周期规则
+
+| 规则 | 内容 |
+|------|------|
+| **默认报告** | 同一孩子同一 `report_type` 的最新报告(按 `created_at` 倒序)为"当前活跃报告" |
+| **AI对话上下文** | 默认基于当前活跃报告。用户可在对话中指定 `reportId` 切换到历史报告 |
+| **方案关联** | `nutrition_plan.report_id` 记录方案基于哪份报告生成。新报告上传后旧方案不自动失效,用户可主动选择"基于新报告重新生成方案" |
+| **复测识别** | 同一 `child_id` + 同一 `report_type` 的报告 ≥ 2 份时,自动激活复测对比功能 |
+| **删除策略** | 用户删除报告 → 级联删除关联的 `health_indicators`, `health_gut_flora`, `health_disease_risk`,但保留 `nutrition_plan`(方案已独立于原始报告) |
+| **切换API** | `POST /api/health/report/switch-active` → `{ childId, reportId }` 切换当前活跃报告 |
+
+## 七、涉及文件清单(实施用)
+
+### 后端新增/修改
+
+| # | 文件 | 操作 | 阶段 |
+|---|------|------|------|
+| 1 | `entity/HealthGutFlora.java` | 新增 | P0 |
+| 2 | `entity/HealthDiseaseRisk.java` | 新增 | P0 |
+| 3 | `entity/UserNutritionProfile.java` | 新增 | P1 |
+| 4 | `entity/NutritionPlan.java` | 新增 | P1 |
+| 5 | `entity/NutritionPlanProduct.java` | 新增 | P2 |
+| 6 | `entity/NutritionConsultationOrder.java` | 新增 | P3 |
+| 7 | `mapper/` 对应 6 个 Mapper | 新增 | 分阶段 |
+| 8 | `service/HealthReportParseService.java` | 新增(AI 解析逻辑) | P0 |
+| 9 | `service/NutritionPlanService.java` | 新增 | P1 |
+| 10 | `service/ProductService.java` | 修改(`list()` 扩展标签过滤参数) | P2 |
+| 11 | `service/NutritionConsultationService.java` | 新增(封装咨询订单+通知逻辑) | P3 |
+| 12 | `service/FamilyContextService.java` | 修改(增加报告数据注入) | P0 |
+| 13 | `controller/HealthReportController.java` | 修改(增加 `/report/parse` 端点) | P0 |
+| 14 | `controller/nutrition/NutritionPlanController.java` | 新增 | P1 |
+| 15 | `controller/nutrition/NutritionConsultationController.java` | 新增 | P3 |
+| 16 | `controller/ai/AIChatController.java` | 修改(支持 reportId 参数) | P0 |
+| 17 | `config/DatabaseInitializer.java` | 修改(新增种子数据) | 分阶段 |
+| 18 | `controller/HealthReportUploadController.java` | 新增(文件上传) | P0 |
+
+### 前端新增/修改
+
+| # | 文件 | 操作 | 阶段 |
+|---|------|------|------|
+| 19 | `pages/health/report-upload.vue` | 新增 | P0 |
+| 20 | `pages/health/report-detail.vue` | 新增 | P0 |
+| 21 | `pages/health/questionnaire.vue` | 新增 | P1 |
+| 22 | `pages/health/nutrition-plan.vue` | 新增 | P1 |
+| 23 | `pages/health/product-recommend.vue` | 新增 | P2 |
+| 24 | `pages/health/nutritionist-list.vue` | 新增 | P3 |
+| 25 | `pages/health/nutritionist-chat.vue` | 新增 | P3 |
+| 26 | `pages/health/retest-compare.vue` | 新增(复测趋势对比) | P2 |
+| 27 | `pages/ai/chat.vue` | 修改(增加报告上下文) | P0 |
+| 28 | `pages/index/parent-index.vue` | 修改(增加精准营养入口) | P0 |
+| 29 | `utils/api.js` | 修改(新增 API 方法) | 分阶段 |
+| 30 | `pages.json` | 修改(注册新页面) | 分阶段 |
+
+### 数据库迁移
+
+| # | 内容 | 操作 | 阶段 |
+|---|------|------|------|
+| 31 | `Product` 表增加 `nutrition_tags` 列 | DDL | P2 |
+| 32 | `Guide` 表已有 `specialty` 字段,规划师入驻时设置 `specialty` 值(`assessment`/`nutrition`/`both`) | 配置(无需DDL) | P0 |
+| 33 | 新增 `health_gut_flora`、`health_disease_risk` 表 | DDL | P0 |
+| 34 | 新增 `user_nutrition_profile`、`nutrition_plan`、`nutrition_plan_product` 表 | DDL | P1 |
+| 35 | 新增 `nutrition_consultation_order` 表 | DDL | P3 |