Browse Source

refactor(health): 数据库迁移DDL+seed, HealthReportController集成refreshFromGutReport, 前端7维字段修复

Sisyphus 3 months ago
parent
commit
2579bb401a

+ 99 - 47
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -2576,58 +2576,102 @@ log.info("已添加template_id列到tasks表");
 
 
     private void migrateHealthDimensionsTable() {
     private void migrateHealthDimensionsTable() {
         try {
         try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `health_dimension_score` (\n" +
+            jdbcTemplate.execute("RENAME TABLE IF EXISTS `health_dimension_score` TO `health_dimension_score_old`");
+        } catch (Exception e) {
+            log.warn("health_dimension_score rename skipped: {}", e.getMessage());
+        }
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `health_dimension_scores` (\n" +
                 "  `id` bigint NOT NULL AUTO_INCREMENT,\n" +
                 "  `id` bigint NOT NULL AUTO_INCREMENT,\n" +
                 "  `member_id` bigint NOT NULL COMMENT '家庭成员ID',\n" +
                 "  `member_id` bigint NOT NULL COMMENT '家庭成员ID',\n" +
-                "  `dimension` varchar(50) NOT NULL COMMENT '维度: body/fitness/cardio/blood/sleep/psyche/diet',\n" +
-                "  `score` decimal(5,2) NOT NULL COMMENT '得分 0-100',\n" +
-                "  `data_source` varchar(50) NOT NULL COMMENT '数据来源: manual/assessment/report/device',\n" +
-                "  `record_date` date DEFAULT NULL COMMENT '记录日期',\n" +
-                "  `source_id` bigint DEFAULT NULL COMMENT '来源记录ID(体检报告等)',\n" +
-                "  `is_latest` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否最新',\n" +
+                "  `dimension` varchar(20) NOT NULL COMMENT '维度: growth/sleep/vision/immunity/nutrition/gut/exercise',\n" +
+                "  `score` int NOT NULL COMMENT '0-100分',\n" +
+                "  `percentile` int DEFAULT NULL COMMENT '人群百分位 0-100',\n" +
+                "  `data_source` varchar(20) NOT NULL COMMENT '数据源: REPORT/PHOTO/VOICE/MANUAL',\n" +
+                "  `tier` tinyint NOT NULL DEFAULT 3 COMMENT '数据层级: 1自动/2语音/3手动',\n" +
+                "  `raw_data` json DEFAULT NULL COMMENT '原始数据快照',\n" +
+                "  `assess_date` date NOT NULL COMMENT '评估日期',\n" +
+                "  `expire_date` date DEFAULT NULL COMMENT '数据过期日',\n" +
                 "  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n" +
                 "  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n" +
+                "  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n" +
                 "  PRIMARY KEY (`id`),\n" +
                 "  PRIMARY KEY (`id`),\n" +
-                "  KEY `idx_member_dimension` (`member_id`,`dimension`,`is_latest`),\n" +
-                "  KEY `idx_record_date` (`record_date`)\n" +
+                "  UNIQUE KEY `uk_member_dim_date` (`member_id`, `dimension`, `assess_date`)\n" +
                 ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康维度评分表'");
                 ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康维度评分表'");
         } catch (Exception e) {
         } catch (Exception e) {
-            log.warn("health_dimension_score 表创建失败: {}", e.getMessage());
+            log.warn("health_dimension_scores 表创建失败: {}", e.getMessage());
         }
         }
 
 
         try {
         try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `health_data_source_record` (\n" +
+            jdbcTemplate.execute("RENAME TABLE IF EXISTS `health_data_source_record` TO `health_data_source_record_old`");
+        } catch (Exception e) {
+            log.warn("health_data_source_record rename skipped: {}", e.getMessage());
+        }
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `health_data_sources` (\n" +
                 "  `id` bigint NOT NULL AUTO_INCREMENT,\n" +
                 "  `id` bigint NOT NULL AUTO_INCREMENT,\n" +
-                "  `member_id` bigint NOT NULL COMMENT '家庭成员ID',\n" +
-                "  `data_source` varchar(50) NOT NULL COMMENT '数据源: manual/assessment/report/device',\n" +
-                "  `source_id` bigint NOT NULL COMMENT '来源记录ID',\n" +
-                "  `dimension` varchar(50) DEFAULT NULL COMMENT '关联维度',\n" +
-                "  `record_time` datetime NOT NULL COMMENT '记录时间',\n" +
-                "  `is_latest` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否最新',\n" +
+                "  `member_id` bigint NOT NULL,\n" +
+                "  `source_type` varchar(20) NOT NULL COMMENT 'PDF/PHOTO/VOICE/MANUAL/WEIXIN_STEP',\n" +
+                "  `dimension_codes` json NOT NULL COMMENT '影响的维度列表',\n" +
+                "  `parsed_result` json DEFAULT NULL COMMENT '解析结果全量',\n" +
+                "  `file_url` varchar(500) DEFAULT NULL COMMENT '原始文件URL',\n" +
+                "  `confidence` decimal(3,2) DEFAULT NULL COMMENT 'AI解析置信度',\n" +
                 "  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n" +
                 "  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n" +
                 "  PRIMARY KEY (`id`),\n" +
                 "  PRIMARY KEY (`id`),\n" +
-                "  KEY `idx_member_source` (`member_id`,`data_source`,`is_latest`)\n" +
-                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康数据来源记录表'");
+                "  KEY `idx_member_source` (`member_id`, `source_type`)\n" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康数据源记录表'");
         } catch (Exception e) {
         } catch (Exception e) {
-            log.warn("health_data_source_record 表创建失败: {}", e.getMessage());
+            log.warn("health_data_sources 表创建失败: {}", e.getMessage());
         }
         }
 
 
+        try {
+            jdbcTemplate.execute("RENAME TABLE IF EXISTS `health_norm_reference` TO `health_norm_reference_old`");
+        } catch (Exception e) {
+            log.warn("health_norm_reference rename skipped: {}", e.getMessage());
+        }
         try {
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `health_norm_reference` (\n" +
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `health_norm_reference` (\n" +
                 "  `id` bigint NOT NULL AUTO_INCREMENT,\n" +
                 "  `id` bigint NOT NULL AUTO_INCREMENT,\n" +
-                "  `dimension` varchar(50) NOT NULL COMMENT '维度',\n" +
-                "  `indicator` varchar(100) NOT NULL COMMENT '指标名',\n" +
-                "  `min_value` decimal(10,3) DEFAULT NULL COMMENT '正常最小值',\n" +
-                "  `max_value` decimal(10,3) DEFAULT NULL COMMENT '正常最大值',\n" +
-                "  `unit` varchar(20) DEFAULT NULL COMMENT '单位',\n" +
-                "  `age_group` varchar(20) DEFAULT NULL COMMENT '年龄段: child/adolescent/adult',\n" +
-                "  `gender` varchar(10) DEFAULT NULL COMMENT '性别: male/female/unisex',\n" +
-                "  `source` varchar(100) DEFAULT NULL COMMENT '参考标准来源',\n" +
-                "  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n" +
+                "  `dimension` varchar(20) NOT NULL COMMENT '维度',\n" +
+                "  `gender` varchar(10) NOT NULL COMMENT 'male/female/all',\n" +
+                "  `age_min` int NOT NULL COMMENT '年龄下限(月)',\n" +
+                "  `age_max` int NOT NULL COMMENT '年龄上限(月)',\n" +
+                "  `percentile_5` int DEFAULT NULL,\n" +
+                "  `percentile_15` int DEFAULT NULL,\n" +
+                "  `percentile_25` int DEFAULT NULL,\n" +
+                "  `percentile_50` int DEFAULT NULL COMMENT '中位值',\n" +
+                "  `percentile_75` int DEFAULT NULL,\n" +
+                "  `percentile_85` int DEFAULT NULL,\n" +
+                "  `percentile_95` int DEFAULT NULL,\n" +
+                "  `source` varchar(50) DEFAULT NULL COMMENT '常模来源',\n" +
+                "  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n" +
                 "  PRIMARY KEY (`id`),\n" +
                 "  PRIMARY KEY (`id`),\n" +
-                "  KEY `idx_dimension_indicator` (`dimension`,`indicator`)\n" +
-                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康指标参考标准表'");
-        } catch (Exception e) {
-            log.warn("health_norm_reference 表创建失败: {}", e.getMessage());
+                "  UNIQUE KEY `uk_dim_gender_age` (`dimension`, `gender`, `age_min`, `age_max`)\n" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康常模对照表'");
+        } catch (Exception e) {
+            log.warn("health_norm_reference 表重建失败: {}", e.getMessage());
+        }
+
+        try {
+            jdbcTemplate.execute("INSERT IGNORE INTO health_dimension_scores (member_id, dimension, score, data_source, tier, assess_date, created_at)\n" +
+                " SELECT member_id,\n" +
+                "  CASE dimension\n" +
+                "    WHEN 'body' THEN 'growth'\n" +
+                "    WHEN 'fitness' THEN 'exercise'\n" +
+                "    WHEN 'cardio' THEN 'exercise'\n" +
+                "    WHEN 'blood' THEN 'immunity'\n" +
+                "    WHEN 'sleep' THEN 'sleep'\n" +
+                "    WHEN 'psyche' THEN 'growth'\n" +
+                "    WHEN 'diet' THEN 'nutrition'\n" +
+                "    ELSE dimension\n" +
+                "  END,\n" +
+                "  score,\n" +
+                "  UPPER(data_source),\n" +
+                "  3,\n" +
+                "  record_date,\n" +
+                "  NOW()\n" +
+                " FROM health_dimension_score_old WHERE is_latest = 1");
+        } catch (Exception e) {
+            log.warn("旧维度评分数据迁移跳过: {}", e.getMessage());
         }
         }
     }
     }
 
 
@@ -2635,23 +2679,31 @@ log.info("已添加template_id列到tasks表");
         try {
         try {
             int count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM health_norm_reference", Integer.class);
             int count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM health_norm_reference", Integer.class);
             if (count == 0) {
             if (count == 0) {
-                jdbcTemplate.execute("INSERT INTO `health_norm_reference` (`dimension`, `indicator`, `min_value`, `max_value`, `unit`, `age_group`, `gender`, `source`) VALUES\n" +
-                    "('body', 'bmi', 18.5, 24.0, 'kg/m²', 'child', 'unisex', '中国儿童青少年BMI标准'),\n" +
-                    "('body', 'bmi', 18.5, 24.0, 'kg/m²', 'adolescent', 'unisex', '中国儿童青少年BMI标准'),\n" +
-                    "('body', 'bmi', 18.5, 24.0, 'kg/m²', 'adult', 'unisex', '中国成人BMI标准'),\n" +
-                    "('sleep', 'duration', 8.0, 12.0, '小时', 'child', 'unisex', '中国儿童睡眠指南'),\n" +
-                    "('sleep', 'duration', 9.0, 11.0, '小时', 'adolescent', 'unisex', '中国儿童青少年睡眠指南'),\n" +
-                    "('sleep', 'duration', 7.0, 9.0, '小时', 'adult', 'unisex', '中国成人睡眠指南'),\n" +
-                    "('fitness', 'situp', 30.0, NULL, '次', 'child', 'male', '国家学生体质健康标准'),\n" +
-                    "('fitness', 'situp', 25.0, NULL, '次', 'child', 'female', '国家学生体质健康标准'),\n" +
-                    "('fitness', 'situp', 40.0, NULL, '次', 'adolescent', 'male', '国家学生体质健康标准'),\n" +
-                    "('fitness', 'situp', 35.0, NULL, '次', 'adolescent', 'female', '国家学生体质健康标准')");
-                log.info("health_norm_reference 种子数据已加载 (10条)");
+                jdbcTemplate.execute("INSERT INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES\n" +
+                    "('growth', 'male', 72, 84, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    "('growth', 'female', 72, 84, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    "('growth', 'male', 84, 96, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    "('growth', 'female', 84, 96, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    "('growth', 'male', 96, 120, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    "('growth', 'female', 96, 120, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    "('sleep', 'all', 0, 12, 40, 50, 60, 70, 80, 88, 95, 'NSF婴幼儿睡眠→百分制'),\n" +
+                    "('sleep', 'all', 12, 36, 40, 50, 60, 70, 80, 88, 95, 'NSF幼儿睡眠→百分制'),\n" +
+                    "('sleep', 'all', 36, 72, 40, 50, 60, 70, 80, 88, 95, 'NSF学龄前睡眠→百分制'),\n" +
+                    "('sleep', 'all', 72, 156, 40, 50, 60, 70, 80, 88, 95, 'NSF学龄儿童睡眠→百分制'),\n" +
+                    "('vision', 'male', 72, 144, 45, 55, 65, 75, 85, 90, 95, '视力发育标准→百分制'),\n" +
+                    "('vision', 'female', 72, 144, 45, 55, 65, 75, 85, 90, 95, '视力发育标准→百分制'),\n" +
+                    "('immunity', 'all', 0, 72, 15, 30, 45, 65, 80, 90, 95, '年患病次数→百分制'),\n" +
+                    "('immunity', 'all', 72, 192, 15, 35, 50, 70, 85, 92, 98, '年患病次数→百分制'),\n" +
+                    "('nutrition', 'all', 36, 192, 20, 30, 40, 50, 60, 70, 80, '膳食多样性→百分制'),\n" +
+                    "('gut', 'all', 0, 192, 40, 50, 60, 70, 80, 85, 95, '菌群人群分布(平台自建)'),\n" +
+                    "('exercise', 'male', 72, 144, 30, 40, 50, 60, 70, 80, 90, '国家学生体质健康标准'),\n" +
+                    "('exercise', 'female', 72, 144, 25, 35, 45, 55, 65, 75, 85, '国家学生体质健康标准')");
+                log.info("health_norm_reference 常模种子数据已加载 (18条, 统一0-100百分制)");
             } else {
             } else {
-                log.info("health_norm_reference 种子数据已存在 ({}条), 跳过", count);
+                log.info("health_norm_reference 常模数据已存在 ({}条), 跳过", count);
             }
             }
         } catch (Exception e) {
         } catch (Exception e) {
-            log.warn("health_norm_reference 种子数据初始化失败: {}", e.getMessage());
+            log.warn("health_norm_reference 常模种子数据初始化失败: {}", e.getMessage());
         }
         }
     }
     }
 
 

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

@@ -360,24 +360,6 @@ public class HealthReportController {
             // 8. 创建完整报告(subjectId = null,尚未绑定成员)
             // 8. 创建完整报告(subjectId = null,尚未绑定成员)
             HealthReport created = healthReportService.createReport(report, indicators, gutFloraList, diseaseRiskList);
             HealthReport created = healthReportService.createReport(report, indicators, gutFloraList, diseaseRiskList);
 
 
-            // 10. 保存肠道健康评分到维度表
-            if (parsed.getGutHealthScore() != null) {
-                try {
-                    com.etotem.cfc.dto.DimensionUploadVO vo = new com.etotem.cfc.dto.DimensionUploadVO();
-                    vo.setMemberId(userId);
-                    vo.setDimension("body");
-                    vo.setScore(parsed.getGutHealthScore() != null ? BigDecimal.valueOf(parsed.getGutHealthScore()) : null);
-                    vo.setDataSource("report");
-                    if (parsed.getReportDate() != null && !parsed.getReportDate().isEmpty()) {
-                        vo.setRecordDate(parsed.getReportDate());
-                    }
-                    dimensionScoreService.uploadScore(vo);
-                    log.info("已保存肠道健康评分到维度表: childId={}, score={}", userId, parsed.getGutHealthScore());
-                } catch (Exception e) {
-                    log.warn("保存肠道健康评分到维度表失败: {}", e.getMessage());
-                }
-            }
-
             // 11. 保存食材推荐数据
             // 11. 保存食材推荐数据
             if (parsed.getFoodSuitability() != null && !parsed.getFoodSuitability().isEmpty()) {
             if (parsed.getFoodSuitability() != null && !parsed.getFoodSuitability().isEmpty()) {
                 int sortOrder = 0;
                 int sortOrder = 0;
@@ -412,6 +394,13 @@ public class HealthReportController {
                     healthReportService.bindReportToMember(created.getId(), matchedMemberId);
                     healthReportService.bindReportToMember(created.getId(), matchedMemberId);
                     matchedMemberName = parsed.getPersonName();
                     matchedMemberName = parsed.getPersonName();
                     log.info("报告{}自动匹配到成员{}", created.getId(), matchedMemberId);
                     log.info("报告{}自动匹配到成员{}", created.getId(), matchedMemberId);
+                    // 刷新7维评分(基于菌群报告5维映射矩阵)
+                    try {
+                        dimensionScoreService.refreshFromGutReport(matchedMemberId, created.getId());
+                        log.info("已刷新成员{}的7维健康评分", matchedMemberId);
+                    } catch (Exception e) {
+                        log.warn("刷新7维健康评分失败: {}", e.getMessage());
+                    }
                 }
                 }
             }
             }
 
 

+ 7 - 7
cfc-backend/src/main/java/com/etotem/cfc/dto/DimensionScoreVO.java

@@ -23,12 +23,12 @@ public class DimensionScoreVO {
     }
     }
 
 
     public static List<DimensionScoreVO> DEFAULT_DIMENSIONS = java.util.Arrays.asList(
     public static List<DimensionScoreVO> DEFAULT_DIMENSIONS = java.util.Arrays.asList(
-        new DimensionScoreVO("body", "身体", null, null, null),
-        new DimensionScoreVO("fitness", "运动", null, null, null),
-        new DimensionScoreVO("cardio", "心肺", null, null, null),
-        new DimensionScoreVO("blood", "血液", null, null, null),
-        new DimensionScoreVO("sleep", "睡眠", null, null, null),
-        new DimensionScoreVO("psyche", "心理", null, null, null),
-        new DimensionScoreVO("diet", "饮食", null, null, null)
+        new DimensionScoreVO("growth", "生长发育", null, null, null),
+        new DimensionScoreVO("sleep", "睡眠质量", null, null, null),
+        new DimensionScoreVO("vision", "视力健康", null, null, null),
+        new DimensionScoreVO("immunity", "免疫力", null, null, null),
+        new DimensionScoreVO("nutrition", "营养均衡", null, null, null),
+        new DimensionScoreVO("gut", "肠胃健康", null, null, null),
+        new DimensionScoreVO("exercise", "运动活力", null, null, null)
     );
     );
 }
 }

+ 2 - 1
cfc-backend/src/main/java/com/etotem/cfc/dto/DimensionUploadVO.java

@@ -8,6 +8,7 @@ public class DimensionUploadVO {
     private Long memberId;
     private Long memberId;
     private String dimension;
     private String dimension;
     private BigDecimal score;
     private BigDecimal score;
-    private String dataSource;
+    private String sourceType;
+    private BigDecimal confidence;
     private String recordDate;
     private String recordDate;
 }
 }

+ 4 - 2
cfc-backend/src/main/java/com/etotem/cfc/dto/HealthDimensionVO.java

@@ -17,10 +17,12 @@ public class HealthDimensionVO {
         private String dimension;
         private String dimension;
         private String label;
         private String label;
         private BigDecimal score;
         private BigDecimal score;
-        private BigDecimal normMin;
-        private BigDecimal normMax;
+        private Integer percentile;
+        private String dataSource;
+        private Integer tier;
         private String level;
         private String level;
         private String trend;
         private String trend;
         private String recordDate;
         private String recordDate;
+        private String expireDate;
     }
     }
 }
 }

+ 11 - 8
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthNormReference.java

@@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 import lombok.Data;
 import java.io.Serializable;
 import java.io.Serializable;
-import java.math.BigDecimal;
 import java.util.Date;
 import java.util.Date;
 
 
 @Data
 @Data
@@ -14,12 +13,16 @@ public class HealthNormReference implements Serializable {
     @TableId(type = IdType.AUTO)
     @TableId(type = IdType.AUTO)
     private Long id;
     private Long id;
     private String dimension;
     private String dimension;
-    private String indicator;
-    private BigDecimal minValue;
-    private BigDecimal maxValue;
-    private String unit;
-    private String ageGroup;
-    private String gender;
+    private String gender;       // male/female/all
+    private Integer ageMin;     // 月
+    private Integer ageMax;     // 月
+    private Integer percentile5;
+    private Integer percentile15;
+    private Integer percentile25;
+    private Integer percentile50;
+    private Integer percentile75;
+    private Integer percentile85;
+    private Integer percentile95;
     private String source;
     private String source;
-    private Date updatedAt;
+    private Date createdAt;
 }
 }

+ 12 - 7
cfc-backend/src/main/java/com/etotem/cfc/mapper/HealthDataSourceRecordMapper.java

@@ -5,18 +5,23 @@ import com.etotem.cfc.entity.HealthDataSourceRecord;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Select;
-import org.apache.ibatis.annotations.Update;
 import java.util.List;
 import java.util.List;
 
 
 @Mapper
 @Mapper
 public interface HealthDataSourceRecordMapper extends BaseMapper<HealthDataSourceRecord> {
 public interface HealthDataSourceRecordMapper extends BaseMapper<HealthDataSourceRecord> {
 
 
-    @Select("SELECT * FROM health_data_source_record WHERE member_id = #{memberId} AND data_source = #{dataSource} AND is_latest = 1 ORDER BY record_time DESC LIMIT 1")
-    HealthDataSourceRecord selectLatest(@Param("memberId") Long memberId, @Param("dataSource") String dataSource);
+    @Select("SELECT * FROM health_data_sources WHERE member_id = #{memberId} " +
+        "AND source_type = #{sourceType} ORDER BY created_at DESC LIMIT 1")
+    HealthDataSourceRecord selectLatestBySource(@Param("memberId") Long memberId,
+        @Param("sourceType") String sourceType);
 
 
-    @Select("SELECT * FROM health_data_source_record WHERE member_id = #{memberId} AND data_source = #{dataSource} ORDER BY record_time DESC")
-    List<HealthDataSourceRecord> selectByMemberAndSource(@Param("memberId") Long memberId, @Param("dataSource") String dataSource);
+    @Select("SELECT * FROM health_data_sources WHERE member_id = #{memberId} " +
+        "ORDER BY created_at DESC")
+    List<HealthDataSourceRecord> selectByMember(@Param("memberId") Long memberId);
 
 
-    @Update("UPDATE health_data_source_record SET is_latest = 0 WHERE member_id = #{memberId} AND data_source = #{dataSource} AND id != #{sourceId}")
-    void updateNotLatest(@Param("memberId") Long memberId, @Param("sourceId") Long sourceId);
+    @Select("SELECT * FROM health_data_sources WHERE member_id = #{memberId} " +
+        "AND JSON_CONTAINS(dimension_codes, #{dimensionCode}) " +
+        "ORDER BY created_at DESC LIMIT #{limit}")
+    List<HealthDataSourceRecord> selectByDimension(@Param("memberId") Long memberId,
+        @Param("dimensionCode") String dimensionCode, @Param("limit") int limit);
 }
 }

+ 11 - 8
cfc-backend/src/main/java/com/etotem/cfc/mapper/HealthDimensionScoreMapper.java

@@ -5,21 +5,24 @@ import com.etotem.cfc.entity.HealthDimensionScore;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Select;
-import org.apache.ibatis.annotations.Update;
 import java.util.List;
 import java.util.List;
 
 
 @Mapper
 @Mapper
 public interface HealthDimensionScoreMapper extends BaseMapper<HealthDimensionScore> {
 public interface HealthDimensionScoreMapper extends BaseMapper<HealthDimensionScore> {
 
 
-    @Select("SELECT * FROM health_dimension_score WHERE member_id = #{memberId} AND is_latest = 1 ORDER BY record_date DESC")
+    @Select("SELECT h.* FROM health_dimension_scores h " +
+        "INNER JOIN (SELECT dimension, MAX(assess_date) max_date FROM health_dimension_scores " +
+        "WHERE member_id = #{memberId} GROUP BY dimension) latest " +
+        "ON h.dimension = latest.dimension AND h.assess_date = latest.max_date " +
+        "WHERE h.member_id = #{memberId}")
     List<HealthDimensionScore> selectLatestByMember(@Param("memberId") Long memberId);
     List<HealthDimensionScore> selectLatestByMember(@Param("memberId") Long memberId);
 
 
-    @Select("SELECT * FROM health_dimension_score WHERE member_id = #{memberId} AND dimension = #{dimension} AND is_latest = 1 LIMIT 1")
+    @Select("SELECT * FROM health_dimension_scores WHERE member_id = #{memberId} " +
+        "AND dimension = #{dimension} ORDER BY assess_date DESC LIMIT 1")
     HealthDimensionScore selectLatest(@Param("memberId") Long memberId, @Param("dimension") String dimension);
     HealthDimensionScore selectLatest(@Param("memberId") Long memberId, @Param("dimension") String dimension);
 
 
-    @Select("SELECT * FROM health_dimension_score WHERE member_id = #{memberId} AND dimension = #{dimension} ORDER BY record_date DESC LIMIT #{limit}")
-    List<HealthDimensionScore> selectHistory(@Param("memberId") Long memberId, @Param("dimension") String dimension, @Param("limit") int limit);
-
-    @Update("UPDATE health_dimension_score SET is_latest = 0 WHERE member_id = #{memberId} AND dimension = #{dimension} AND is_latest = 1")
-    void updateExpired(@Param("memberId") Long memberId, @Param("dimension") String dimension);
+    @Select("SELECT * FROM health_dimension_scores WHERE member_id = #{memberId} " +
+        "AND dimension = #{dimension} ORDER BY assess_date DESC LIMIT #{limit}")
+    List<HealthDimensionScore> selectHistory(@Param("memberId") Long memberId,
+        @Param("dimension") String dimension, @Param("limit") int limit);
 }
 }

+ 6 - 9
cfc-backend/src/main/java/com/etotem/cfc/mapper/HealthNormReferenceMapper.java

@@ -5,17 +5,14 @@ import com.etotem.cfc.entity.HealthNormReference;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Select;
-import java.util.List;
 
 
 @Mapper
 @Mapper
 public interface HealthNormReferenceMapper extends BaseMapper<HealthNormReference> {
 public interface HealthNormReferenceMapper extends BaseMapper<HealthNormReference> {
 
 
-    @Select("SELECT * FROM health_norm_reference WHERE dimension = #{dimension} LIMIT 1")
-    HealthNormReference selectByDimension(@Param("dimension") String dimension);
-
-    @Select("SELECT * FROM health_norm_reference WHERE dimension = #{dimension} AND indicator = #{indicator} LIMIT 1")
-    HealthNormReference selectByDimensionIndicator(@Param("dimension") String dimension, @Param("indicator") String indicator);
-
-    @Select("SELECT * FROM health_norm_reference WHERE dimension = #{dimension} AND age_group = #{ageGroup} AND (gender = #{gender} OR gender = 'unisex') LIMIT 1")
-    HealthNormReference selectNorm(@Param("dimension") String dimension, @Param("ageGroup") String ageGroup, @Param("gender") String gender);
+    @Select("SELECT * FROM health_norm_reference WHERE dimension = #{dimension} " +
+        "AND (gender = #{gender} OR gender = 'all') " +
+        "AND age_min <= #{ageMonths} AND age_max > #{ageMonths} " +
+        "ORDER BY gender DESC LIMIT 1")
+    HealthNormReference selectNorm(@Param("dimension") String dimension,
+        @Param("gender") String gender, @Param("ageMonths") Integer ageMonths);
 }
 }

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DimensionScoreService.java

@@ -10,4 +10,6 @@ public interface DimensionScoreService {
     void uploadScore(DimensionUploadVO vo);
     void uploadScore(DimensionUploadVO vo);
     void submitQuestionnaire(HealthDimensionQuestionnaireVO vo);
     void submitQuestionnaire(HealthDimensionQuestionnaireVO vo);
     List<HealthDimensionVO.DimensionItem> getDimensionHistory(Long memberId, String dimension, int limit);
     List<HealthDimensionVO.DimensionItem> getDimensionHistory(Long memberId, String dimension, int limit);
+
+    void refreshFromGutReport(Long memberId, Long reportId);
 }
 }

+ 2 - 3
cfc-backend/src/main/java/com/etotem/cfc/service/HealthDataSourceRecordService.java

@@ -5,7 +5,6 @@ import java.util.List;
 
 
 public interface HealthDataSourceRecordService {
 public interface HealthDataSourceRecordService {
     void saveRecord(HealthDataSourceRecord record);
     void saveRecord(HealthDataSourceRecord record);
-    void markLatest(Long memberId, Long sourceId);
-    List<HealthDataSourceRecord> getRecords(Long memberId, String dataSource);
-    HealthDataSourceRecord getLatest(Long memberId, String dataSource);
+    List<HealthDataSourceRecord> getRecords(Long memberId, String sourceType);
+    HealthDataSourceRecord getLatest(Long memberId, String sourceType);
 }
 }

+ 224 - 74
cfc-backend/src/main/java/com/etotem/cfc/service/impl/DimensionScoreServiceImpl.java

@@ -4,34 +4,37 @@ import com.etotem.cfc.dto.HealthDimensionVO;
 import com.etotem.cfc.dto.DimensionUploadVO;
 import com.etotem.cfc.dto.DimensionUploadVO;
 import com.etotem.cfc.dto.HealthDimensionQuestionnaireVO;
 import com.etotem.cfc.dto.HealthDimensionQuestionnaireVO;
 import com.etotem.cfc.entity.HealthDimensionScore;
 import com.etotem.cfc.entity.HealthDimensionScore;
+import com.etotem.cfc.entity.HealthDataSourceRecord;
+import com.etotem.cfc.entity.HealthGutFlora;
+import com.etotem.cfc.entity.HealthIndicator;
+import com.etotem.cfc.entity.HealthDiseaseRisk;
 import com.etotem.cfc.entity.HealthNormReference;
 import com.etotem.cfc.entity.HealthNormReference;
+import com.etotem.cfc.entity.HealthReport;
 import com.etotem.cfc.mapper.HealthNormReferenceMapper;
 import com.etotem.cfc.mapper.HealthNormReferenceMapper;
 import com.etotem.cfc.service.DimensionScoreService;
 import com.etotem.cfc.service.DimensionScoreService;
 import com.etotem.cfc.service.HealthDimensionScoreService;
 import com.etotem.cfc.service.HealthDimensionScoreService;
+import com.etotem.cfc.service.HealthReportService;
+import com.etotem.cfc.service.HealthDataSourceRecordService;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
 import javax.annotation.Resource;
 import java.math.BigDecimal;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.math.RoundingMode;
 import java.text.SimpleDateFormat;
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
+import java.util.*;
+import java.util.stream.Collectors;
 
 
 @Service
 @Service
 public class DimensionScoreServiceImpl implements DimensionScoreService {
 public class DimensionScoreServiceImpl implements DimensionScoreService {
 
 
-    private static final List<String> DIMENSION_ORDER = java.util.Arrays.asList(
-        "body", "fitness", "cardio", "blood", "sleep", "psyche", "diet");
-
-    private static final List<String[]> DIMENSION_LABELS = java.util.Arrays.asList(
-        new String[]{"body", "身体"},
-        new String[]{"fitness", "运动"},
-        new String[]{"cardio", "心肺"},
-        new String[]{"blood", "血液"},
-        new String[]{"sleep", "睡眠"},
-        new String[]{"psyche", "心理"},
-        new String[]{"diet", "饮食"}
+    private static final List<String[]> DIMENSIONS = Arrays.asList(
+        new String[]{"growth",    "生长发育"},
+        new String[]{"sleep",     "睡眠质量"},
+        new String[]{"vision",    "视力健康"},
+        new String[]{"immunity",  "免疫力"},
+        new String[]{"nutrition", "营养均衡"},
+        new String[]{"gut",       "肠胃健康"},
+        new String[]{"exercise",  "运动活力"}
     );
     );
 
 
     @Resource
     @Resource
@@ -40,44 +43,55 @@ public class DimensionScoreServiceImpl implements DimensionScoreService {
     @Resource
     @Resource
     private HealthNormReferenceMapper normMapper;
     private HealthNormReferenceMapper normMapper;
 
 
+    @Resource
+    private HealthReportService healthReportService;
+
+    @Resource
+    private HealthDataSourceRecordService dataSourceRecordService;
+
     @Override
     @Override
     public HealthDimensionVO getHealthDimension(Long memberId) {
     public HealthDimensionVO getHealthDimension(Long memberId) {
         List<HealthDimensionScore> latest = scoreService.getLatestScores(memberId);
         List<HealthDimensionScore> latest = scoreService.getLatestScores(memberId);
-        BigDecimal healthIndex = calculateHealthIndex(latest);
+        Map<String, HealthDimensionScore> scoreMap = latest.stream()
+            .collect(Collectors.toMap(HealthDimensionScore::getDimension, s -> s, (a, b) -> a));
 
 
         List<HealthDimensionVO.DimensionItem> items = new ArrayList<>();
         List<HealthDimensionVO.DimensionItem> items = new ArrayList<>();
-        for (String[] dim : DIMENSION_LABELS) {
+        BigDecimal totalScore = BigDecimal.ZERO;
+        int validCount = 0;
+
+        for (String[] dim : DIMENSIONS) {
             String code = dim[0];
             String code = dim[0];
             String label = dim[1];
             String label = dim[1];
-            HealthDimensionScore found = latest.stream()
-                .filter(s -> code.equals(s.getDimension()))
-                .findFirst().orElse(null);
+            HealthDimensionScore found = scoreMap.get(code);
 
 
             HealthDimensionVO.DimensionItem item = new HealthDimensionVO.DimensionItem();
             HealthDimensionVO.DimensionItem item = new HealthDimensionVO.DimensionItem();
             item.setDimension(code);
             item.setDimension(code);
             item.setLabel(label);
             item.setLabel(label);
+
             if (found != null) {
             if (found != null) {
-                item.setScore(found.getScore());
+                item.setScore(BigDecimal.valueOf(found.getScore()));
+                item.setPercentile(found.getPercentile());
+                item.setDataSource(found.getDataSource());
+                item.setTier(found.getTier());
+                item.setLevel(calcLevel(found.getScore()));
                 item.setTrend("stable");
                 item.setTrend("stable");
-                String level = calcLevel(found.getScore());
-                item.setLevel(level);
+                if (found.getExpireDate() != null) {
+                    item.setExpireDate(new SimpleDateFormat("yyyy-MM-dd").format(found.getExpireDate()));
+                }
+                totalScore = totalScore.add(BigDecimal.valueOf(found.getScore()));
+                validCount++;
             } else {
             } else {
                 item.setScore(null);
                 item.setScore(null);
                 item.setLevel("none");
                 item.setLevel("none");
                 item.setTrend(null);
                 item.setTrend(null);
             }
             }
-
-            HealthNormReference norm = normMapper.selectByDimension(code);
-            if (norm != null) {
-                item.setNormMin(norm.getMinValue());
-                item.setNormMax(norm.getMaxValue());
-            }
             items.add(item);
             items.add(item);
         }
         }
 
 
         HealthDimensionVO vo = new HealthDimensionVO();
         HealthDimensionVO vo = new HealthDimensionVO();
         vo.setMemberId(memberId);
         vo.setMemberId(memberId);
-        vo.setHealthIndex(healthIndex);
+        vo.setHealthIndex(validCount > 0
+            ? totalScore.divide(BigDecimal.valueOf(validCount), 2, RoundingMode.HALF_UP) : null);
         vo.setDimensions(items);
         vo.setDimensions(items);
         vo.setUpdateTime(new Date());
         vo.setUpdateTime(new Date());
         return vo;
         return vo;
@@ -86,20 +100,18 @@ public class DimensionScoreServiceImpl implements DimensionScoreService {
     @Override
     @Override
     @Transactional
     @Transactional
     public void uploadScore(DimensionUploadVO vo) {
     public void uploadScore(DimensionUploadVO vo) {
-        scoreService.markExpired(vo.getMemberId(), vo.getDimension());
-
         HealthDimensionScore score = new HealthDimensionScore();
         HealthDimensionScore score = new HealthDimensionScore();
         score.setMemberId(vo.getMemberId());
         score.setMemberId(vo.getMemberId());
         score.setDimension(vo.getDimension());
         score.setDimension(vo.getDimension());
-        score.setScore(vo.getScore());
-        score.setDataSource(vo.getDataSource() != null ? vo.getDataSource() : "manual");
+        score.setScore(vo.getScore() != null ? vo.getScore().intValue() : 0);
+        score.setDataSource(vo.getSourceType() != null ? vo.getSourceType() : "MANUAL");
+        score.setTier(3);
         if (vo.getRecordDate() != null) {
         if (vo.getRecordDate() != null) {
-            score.setRecordDate(java.sql.Date.valueOf(vo.getRecordDate()));
+            score.setAssessDate(java.sql.Date.valueOf(vo.getRecordDate()));
         } else {
         } else {
-            score.setRecordDate(new java.sql.Date(System.currentTimeMillis()));
+            score.setAssessDate(new java.sql.Date(System.currentTimeMillis()));
         }
         }
-        score.setIsLatest(true);
-        score.setSourceId(null);
+        score.setPercentile(calcPercentile(vo.getDimension(), vo.getScore() != null ? vo.getScore().intValue() : 0));
         scoreService.saveScore(score);
         scoreService.saveScore(score);
     }
     }
 
 
@@ -114,19 +126,18 @@ public class DimensionScoreServiceImpl implements DimensionScoreService {
                 count++;
                 count++;
             }
             }
         }
         }
-        BigDecimal avg = count > 0 ? total.divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP) : null;
+        if (count == 0) return;
+        int avgScore = total.divide(BigDecimal.valueOf(count), 0, RoundingMode.HALF_UP).intValue();
 
 
-        if (avg != null) {
-            scoreService.markExpired(vo.getMemberId(), vo.getDimension());
-            HealthDimensionScore score = new HealthDimensionScore();
-            score.setMemberId(vo.getMemberId());
-            score.setDimension(vo.getDimension());
-            score.setScore(avg);
-            score.setDataSource("questionnaire");
-            score.setRecordDate(new java.sql.Date(System.currentTimeMillis()));
-            score.setIsLatest(true);
-            scoreService.saveScore(score);
-        }
+        HealthDimensionScore score = new HealthDimensionScore();
+        score.setMemberId(vo.getMemberId());
+        score.setDimension(vo.getDimension());
+        score.setScore(avgScore);
+        score.setDataSource("MANUAL");
+        score.setTier(3);
+        score.setAssessDate(new java.sql.Date(System.currentTimeMillis()));
+        score.setPercentile(calcPercentile(vo.getDimension(), avgScore));
+        scoreService.saveScore(score);
     }
     }
 
 
     @Override
     @Override
@@ -137,41 +148,180 @@ public class DimensionScoreServiceImpl implements DimensionScoreService {
         for (HealthDimensionScore s : history) {
         for (HealthDimensionScore s : history) {
             HealthDimensionVO.DimensionItem item = new HealthDimensionVO.DimensionItem();
             HealthDimensionVO.DimensionItem item = new HealthDimensionVO.DimensionItem();
             item.setDimension(s.getDimension());
             item.setDimension(s.getDimension());
-            item.setScore(s.getScore());
+            item.setScore(BigDecimal.valueOf(s.getScore()));
+            item.setPercentile(s.getPercentile());
+            item.setDataSource(s.getDataSource());
+            item.setTier(s.getTier());
             item.setLevel(calcLevel(s.getScore()));
             item.setLevel(calcLevel(s.getScore()));
-            item.setTrend("stable");
-            if (s.getRecordDate() != null) {
-                item.setRecordDate(sdf.format(s.getRecordDate()));
-            }
-            HealthNormReference norm = normMapper.selectByDimension(dimension);
-            if (norm != null) {
-                item.setNormMin(norm.getMinValue());
-                item.setNormMax(norm.getMaxValue());
-            }
+            item.setRecordDate(sdf.format(s.getAssessDate()));
             items.add(item);
             items.add(item);
         }
         }
         return items;
         return items;
     }
     }
 
 
-    private BigDecimal calculateHealthIndex(List<HealthDimensionScore> scores) {
-        if (scores.isEmpty()) return null;
-        BigDecimal sum = BigDecimal.ZERO;
-        int count = 0;
-        for (HealthDimensionScore s : scores) {
-            if (s.getScore() != null) {
-                sum = sum.add(s.getScore());
-                count++;
+    @Override
+    @Transactional
+    public void refreshFromGutReport(Long memberId, Long reportId) {
+        HealthReport report = healthReportService.getReportById(reportId);
+        if (report == null) return;
+
+        List<HealthGutFlora> gutFlora = healthReportService.getGutFloraByReportId(reportId);
+        List<HealthIndicator> indicators = healthReportService.getReportIndicators(reportId);
+        List<HealthDiseaseRisk> diseaseRisks = healthReportService.getDiseaseRisksByReportId(reportId);
+
+        Map<String, Integer> dimScores = mapGutFloraToDimensions(report, gutFlora, indicators, diseaseRisks);
+
+        Date assessDate = report.getReportDate() != null
+            ? report.getReportDate() : new java.sql.Date(System.currentTimeMillis());
+        for (Map.Entry<String, Integer> entry : dimScores.entrySet()) {
+            HealthDimensionScore score = new HealthDimensionScore();
+            score.setMemberId(memberId);
+            score.setDimension(entry.getKey());
+            score.setScore(entry.getValue());
+            score.setPercentile(calcPercentile(entry.getKey(), entry.getValue()));
+            score.setDataSource("REPORT");
+            score.setTier(1);
+            score.setAssessDate(assessDate);
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(assessDate);
+            cal.add(Calendar.MONTH, 6);
+            score.setExpireDate(cal.getTime());
+            score.setRawData("{\"reportId\":" + reportId + "}");
+            scoreService.saveScore(score);
+        }
+    }
+
+    private Map<String, Integer> mapGutFloraToDimensions(
+            HealthReport report,
+            List<HealthGutFlora> gutFlora,
+            List<HealthIndicator> indicators,
+            List<HealthDiseaseRisk> diseaseRisks) {
+
+        Map<String, List<Integer>> dimValues = new HashMap<>();
+        for (String[] dim : DIMENSIONS) {
+            dimValues.put(dim[0], new ArrayList<>());
+        }
+
+        if (report.getGutHealthScore() != null) {
+            dimValues.get("immunity").add(report.getGutHealthScore());
+            dimValues.get("gut").add(report.getGutHealthScore());
+        }
+        if (report.getChronicDiseaseScore() != null) {
+            int inverted = 100 - report.getChronicDiseaseScore();
+            dimValues.get("growth").add(inverted);
+            dimValues.get("nutrition").add(inverted);
+        }
+        if (report.getNutritionScore() != null) {
+            dimValues.get("nutrition").add(report.getNutritionScore());
+        }
+        if (report.getGutBalanceScore() != null) {
+            dimValues.get("gut").add(report.getGutBalanceScore());
+        }
+        if (report.getGutDiversityScore() != null) {
+            dimValues.get("immunity").add(report.getGutDiversityScore());
+            dimValues.get("gut").add(report.getGutDiversityScore());
+        }
+        if (report.getBeneficialBacteriaScore() != null) {
+            dimValues.get("immunity").add(report.getBeneficialBacteriaScore());
+            dimValues.get("gut").add(report.getBeneficialBacteriaScore());
+        }
+        if (report.getHarmfulBacteriaScore() != null) {
+            int inverted = 100 - report.getHarmfulBacteriaScore();
+            dimValues.get("immunity").add(inverted);
+            dimValues.get("gut").add(inverted);
+        }
+        if (report.getCoreSpeciesScore() != null) {
+            dimValues.get("gut").add(report.getCoreSpeciesScore());
+        }
+        if (report.getGutAge() != null) {
+            try {
+                int gutAgeYears = Integer.parseInt(report.getGutAge().replaceAll("[^0-9]", ""));
+                int gutAgeScore = Math.max(0, Math.min(100, 100 - gutAgeYears * 2));
+                dimValues.get("sleep").add(gutAgeScore);
+                dimValues.get("gut").add(gutAgeScore);
+            } catch (NumberFormatException ignored) {}
+        }
+
+        for (HealthIndicator ind : indicators) {
+            String cat = ind.getCategory();
+            String rawVal = ind.getIndicatorValue();
+            if (cat == null || rawVal == null) continue;
+            int val = tryParseNumericIndicator(rawVal);
+            if (val < 0) continue;
+
+            if (cat.contains("神经递质")) {
+                dimValues.get("sleep").add(val);
+            } else if ("短链脂肪酸".equals(cat)) {
+                dimValues.get("nutrition").add(val);
+                dimValues.get("gut").add(val);
+            } else if ("肠道屏障".equals(cat)) {
+                dimValues.get("immunity").add(val);
+                dimValues.get("gut").add(val);
+            } else if (cat.contains("维生素") || cat.contains("矿物质") || cat.contains("微量")) {
+                dimValues.get("nutrition").add(val);
+            }
+        }
+
+        for (HealthDiseaseRisk risk : diseaseRisks) {
+            if (risk.getRiskValue() == null) continue;
+            int riskVal = tryParseNumericIndicator(risk.getRiskValue());
+            if (riskVal < 0) {
+                if ("低风险".equals(risk.getRiskLevel())) riskVal = 20;
+                else if ("需注意".equals(risk.getRiskLevel())) riskVal = 50;
+                else if ("高风险".equals(risk.getRiskLevel())) riskVal = 80;
+                else continue;
+            }
+            int invertedScore = 100 - Math.min(riskVal, 100);
+            String name = risk.getDiseaseName();
+            if (name != null && (name.contains("代谢") || name.contains("肥胖") || name.contains("超重"))) {
+                dimValues.get("growth").add(invertedScore);
+                dimValues.get("nutrition").add(invertedScore);
+            }
+            if (name != null && (name.contains("感染") || name.contains("炎症"))) {
+                dimValues.get("immunity").add(invertedScore);
+            }
+            dimValues.get("gut").add(invertedScore);
+        }
+
+        Map<String, Integer> result = new HashMap<>();
+        for (String[] dim : DIMENSIONS) {
+            List<Integer> vals = dimValues.get(dim[0]);
+            if (vals != null && !vals.isEmpty()) {
+                int avg = (int) vals.stream().mapToInt(v -> v).average().orElse(0);
+                result.put(dim[0], Math.min(100, Math.max(0, avg)));
             }
             }
         }
         }
-        if (count == 0) return null;
-        return sum.divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP);
+        return result;
+    }
+
+    private int tryParseNumericIndicator(String val) {
+        if (val == null) return -1;
+        try {
+            String cleaned = val.replaceAll("[^0-9.]", "").trim();
+            if (cleaned.isEmpty()) return -1;
+            return (int) Double.parseDouble(cleaned);
+        } catch (NumberFormatException e) {
+            return -1;
+        }
+    }
+
+    private Integer calcPercentile(String dimension, int score) {
+        HealthNormReference norm = normMapper.selectNorm(dimension, "all", 120);
+        if (norm == null) return null;
+        if (norm.getPercentile95() != null && score >= norm.getPercentile95()) return 95;
+        if (norm.getPercentile85() != null && score >= norm.getPercentile85()) return 85;
+        if (norm.getPercentile75() != null && score >= norm.getPercentile75()) return 75;
+        if (norm.getPercentile50() != null && score >= norm.getPercentile50()) return 50;
+        if (norm.getPercentile25() != null && score >= norm.getPercentile25()) return 25;
+        if (norm.getPercentile15() != null && score >= norm.getPercentile15()) return 15;
+        if (norm.getPercentile5() != null && score >= norm.getPercentile5()) return 5;
+        return 1;
     }
     }
 
 
-    private String calcLevel(BigDecimal score) {
-        if (score == null) return "none";
-        if (score.compareTo(new BigDecimal("85")) >= 0) return "excellent";
-        if (score.compareTo(new BigDecimal("70")) >= 0) return "good";
-        if (score.compareTo(new BigDecimal("60")) >= 0) return "fair";
+    private String calcLevel(int score) {
+        if (score >= 85) return "excellent";
+        if (score >= 60) return "good";
+        if (score >= 40) return "fair";
         return "poor";
         return "poor";
     }
     }
 }
 }

+ 4 - 10
cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthDataSourceRecordServiceImpl.java

@@ -21,18 +21,12 @@ public class HealthDataSourceRecordServiceImpl implements HealthDataSourceRecord
     }
     }
 
 
     @Override
     @Override
-    @Transactional
-    public void markLatest(Long memberId, Long sourceId) {
-        recordMapper.updateNotLatest(memberId, sourceId);
-    }
-
-    @Override
-    public List<HealthDataSourceRecord> getRecords(Long memberId, String dataSource) {
-        return recordMapper.selectByMemberAndSource(memberId, dataSource);
+    public List<HealthDataSourceRecord> getRecords(Long memberId, String sourceType) {
+        return recordMapper.selectByMember(memberId);
     }
     }
 
 
     @Override
     @Override
-    public HealthDataSourceRecord getLatest(Long memberId, String dataSource) {
-        return recordMapper.selectLatest(memberId, dataSource);
+    public HealthDataSourceRecord getLatest(Long memberId, String sourceType) {
+        return recordMapper.selectLatestBySource(memberId, sourceType);
     }
     }
 }
 }

+ 0 - 7
cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthDimensionScoreServiceImpl.java

@@ -3,7 +3,6 @@ package com.etotem.cfc.service.impl;
 import com.etotem.cfc.entity.HealthDimensionScore;
 import com.etotem.cfc.entity.HealthDimensionScore;
 import com.etotem.cfc.mapper.HealthDimensionScoreMapper;
 import com.etotem.cfc.mapper.HealthDimensionScoreMapper;
 import com.etotem.cfc.service.HealthDimensionScoreService;
 import com.etotem.cfc.service.HealthDimensionScoreService;
-import com.etotem.cfc.service.HealthDataSourceRecordService;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
 import javax.annotation.Resource;
@@ -15,14 +14,10 @@ public class HealthDimensionScoreServiceImpl implements HealthDimensionScoreServ
     @Resource
     @Resource
     private HealthDimensionScoreMapper scoreMapper;
     private HealthDimensionScoreMapper scoreMapper;
 
 
-    @Resource
-    private HealthDataSourceRecordService dataSourceRecordService;
-
     @Override
     @Override
     @Transactional
     @Transactional
     public void saveScore(HealthDimensionScore score) {
     public void saveScore(HealthDimensionScore score) {
         scoreMapper.insert(score);
         scoreMapper.insert(score);
-        dataSourceRecordService.markLatest(score.getMemberId(), score.getId());
     }
     }
 
 
     @Override
     @Override
@@ -41,8 +36,6 @@ public class HealthDimensionScoreServiceImpl implements HealthDimensionScoreServ
     }
     }
 
 
     @Override
     @Override
-    @Transactional
     public void markExpired(Long memberId, String dimension) {
     public void markExpired(Long memberId, String dimension) {
-        scoreMapper.updateExpired(memberId, dimension);
     }
     }
 }
 }

+ 12 - 12
cfc-frontend/pages/body/health-dimensions.vue

@@ -152,19 +152,19 @@ export default {
         dataSource: ''
         dataSource: ''
       },
       },
       dimensionOptions: [
       dimensionOptions: [
-        { code: 'body', label: '身体' },
-        { code: 'fitness', label: '运动' },
-        { code: 'cardio', label: '心肺' },
-        { code: 'blood', label: '血液' },
-        { code: 'sleep', label: '睡眠' },
-        { code: 'psyche', label: '心理' },
-        { code: 'diet', label: '饮食' }
+        { code: 'growth', label: '生长发育' },
+        { code: 'sleep', label: '睡眠质量' },
+        { code: 'vision', label: '视力健康' },
+        { code: 'immunity', label: '免疫力' },
+        { code: 'nutrition', label: '营养均衡' },
+        { code: 'gut', label: '肠胃健康' },
+        { code: 'exercise', label: '运动活力' }
       ],
       ],
       sourceOptions: [
       sourceOptions: [
-        { code: 'manual', label: '手动录入' },
-        { code: 'report', label: '健康报告' },
-        { code: 'device', label: '设备' },
-        { code: 'questionnaire', label: '问卷' }
+        { code: 'MANUAL', label: '手动录入' },
+        { code: 'REPORT', label: '健康报告' },
+        { code: 'PHOTO', label: '照片' },
+        { code: 'VOICE', label: '语音' }
       ]
       ]
     }
     }
   },
   },
@@ -254,7 +254,7 @@ export default {
         memberId: this.memberId,
         memberId: this.memberId,
         dimension: this.uploadForm.dimension,
         dimension: this.uploadForm.dimension,
         score: Number(this.uploadForm.score),
         score: Number(this.uploadForm.score),
-        dataSource: this.uploadForm.dataSource
+        sourceType: this.uploadForm.dataSource
       }
       }
       uploadDimensionScore(payload).then(function(res) {
       uploadDimensionScore(payload).then(function(res) {
         if (res.code === 200) {
         if (res.code === 200) {