# 菌群报告页面重构 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 重构肠道菌群报告详情页,全覆盖参考 JSON 数据(200+ 菌属、50+ 指标、224 食物),支持查看/编辑双模式,独立知识库弹出框,异常值颜色+箭头。 **Architecture:** 4 页架构(主页 + 菌群详情 + 食物推荐 + 疾病风险),后端新增 health_knowledge_base 表 + 编辑 API,前端新增通用知识库弹出框组件。 **Tech Stack:** Java 8 + Spring Boot 2.7.18 + MyBatis-Plus / uni-app (Vue 2 Options API) / WeChat Mini-Program / Playwright E2E > **✅ 实施状态(2026-08-22 回填核实):** 全部任务已完成并通过代码核实(后端 entity/mapper/service/controller、迁移98、`/report/edit`、前端组件与页面、E2E spec 均在仓库中)。初版存在的 1 处落地路径偏差(食物推荐页)已于同日修复归位,详见文末「附录:实施偏差记录」。文中代码块为计划原稿,实际代码以仓库为准。 --- ## 文件结构总览 ### 后端(cfc-backend) | 文件 | 操作 | 职责 | |------|------|------| | `entity/HealthKnowledgeBase.java` | NEW | 知识库实体 | | `mapper/HealthKnowledgeBaseMapper.java` | NEW | MyBatis-Plus Mapper | | `service/HealthKnowledgeBaseService.java` | NEW | 知识库查询(单条+批量) | | `controller/HealthKnowledgeBaseController.java` | NEW | `POST /api/health/knowledge/query` | | `controller/HealthReportController.java` | MODIFY | 新增 `POST /api/health/report/edit` | | `service/HealthReportService.java` | MODIFY | 新增 `updateReportFromPayload` | | `config/DatabaseInitializer.java` | MODIFY | 迁移93: 创建 health_knowledge_base 表 | | `resources/schema.sql` | MODIFY | 追加 health_knowledge_base DDL | ### 前端(cfc-frontend) | 文件 | 操作 | 职责 | |------|------|------| | `components/health-knowledge-popup.vue` | NEW | 通用知识库弹出框组件 | | `pages/health/gut-flora-detail.vue` | REWRITE | 主页(评分+手风琴+指标+编辑) | | `pages/health/gut-flora-species-detail.vue` | NEW | 菌群详情(12 Tab + 卡片) | | `pages/health/gut-flora-foods-detail.vue` | NEW ✅ | 食物推荐(筛选+卡片+编辑)。2026-08-22 已自 `pages/growth/foods-detail.vue` 迁移归位(附录偏差①已修复) | | `pages/health/gut-flora-risks-detail.vue` | NEW | 疾病风险(分类+卡片+编辑) | | `pages.json` | MODIFY | 注册 gut-flora-species-detail / gut-flora-risks-detail / gut-flora-foods-detail,4 页架构齐备 | | `utils/api.js` | MODIFY | 新增 editHealthReport、queryKnowledgeBase、batchQueryKnowledgeBase | ### 测试 | 文件 | 操作 | 职责 | |------|------|------| | `tests/e2e/flora-microbiome-flow.spec.js` | MODIFY | 验证知识库交互+新选择器 | --- ### Task 1: 后端 — HealthKnowledgeBase 实体类 **Files:** - Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/HealthKnowledgeBase.java` - [x] **Step 1: 创建实体类** ```java 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; /** * 健康知识库 — 菌属/营养素/指标标准说明 * 独立于报告数据存储,通过 itemType + itemName 精确匹配 */ @Data @TableName("health_knowledge_base") public class HealthKnowledgeBase implements Serializable { @TableId(type = IdType.AUTO) private Long id; /** 类型: bacteria/nutrient/indicator/vitamin/amino_acid/disease */ private String itemType; /** 项目名称(精确匹配,如 "双歧杆菌属 Bifidobacterium") */ private String itemName; /** 分类(如 有益菌/有害菌) */ private String category; /** 正常范围参考(如 "0.19-14.59") */ private String normalRange; /** 详细说明文字 */ private String description; /** 相关建议(如补充来源) */ private String suggestion; /** 数据来源 */ private String source; private Date createdAt; private Date updatedAt; } ``` - [x] **Step 2: 编译验证** Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile` Expected: BUILD SUCCESS - [x] **Step 3: Commit** ```bash cd /sc-data/cfc git add cfc-backend/src/main/java/com/etotem/cfc/entity/HealthKnowledgeBase.java git commit -m "feat(backend): add HealthKnowledgeBase entity" ``` --- ### Task 2: 后端 — HealthKnowledgeBase Mapper **Files:** - Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/HealthKnowledgeBaseMapper.java` - [x] **Step 1: 创建 Mapper 接口** ```java package com.etotem.cfc.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.etotem.cfc.entity.HealthKnowledgeBase; public interface HealthKnowledgeBaseMapper extends BaseMapper { } ``` - [x] **Step 2: 编译验证** Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile` Expected: BUILD SUCCESS - [x] **Step 3: Commit** ```bash cd /sc-data/cfc git add cfc-backend/src/main/java/com/etotem/cfc/mapper/HealthKnowledgeBaseMapper.java git commit -m "feat(backend): add HealthKnowledgeBaseMapper" ``` --- ### Task 3: 后端 — HealthKnowledgeBase Service **Files:** - Create: `cfc-backend/src/main/java/com/etotem/cfc/service/HealthKnowledgeBaseService.java` - [x] **Step 1: 创建 Service 类** ```java package com.etotem.cfc.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.etotem.cfc.entity.HealthKnowledgeBase; import com.etotem.cfc.mapper.HealthKnowledgeBaseMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.HashMap; import java.util.List; import java.util.Map; @Slf4j @Service public class HealthKnowledgeBaseService { @Resource private HealthKnowledgeBaseMapper healthKnowledgeBaseMapper; /** * 单条查询 */ public HealthKnowledgeBase query(String itemType, String itemName) { return healthKnowledgeBaseMapper.selectOne( new LambdaQueryWrapper() .eq(HealthKnowledgeBase::getItemType, itemType) .eq(HealthKnowledgeBase::getItemName, itemName) ); } /** * 批量查询 — queries 为 [{itemType, itemName}, ...] * 返回 Map.key = "itemType:itemName" */ public Map batchQuery(List> queries) { Map result = new HashMap<>(); for (Map q : queries) { String itemType = q.get("itemType"); String itemName = q.get("itemName"); if (itemType == null || itemName == null) { continue; } String key = itemType + ":" + itemName; HealthKnowledgeBase entry = query(itemType, itemName); if (entry != null) { result.put(key, entry); } } return result; } /** * 插入单条(用于数据导入) */ public void insert(HealthKnowledgeBase entry) { healthKnowledgeBaseMapper.insert(entry); } } ``` - [x] **Step 2: 编译验证** Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile` Expected: BUILD SUCCESS - [x] **Step 3: Commit** ```bash cd /sc-data/cfc git add cfc-backend/src/main/java/com/etotem/cfc/service/HealthKnowledgeBaseService.java git commit -m "feat(backend): add HealthKnowledgeBaseService (query + batchQuery)" ``` --- ### Task 4: 后端 — HealthKnowledgeBase Controller **Files:** - Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/HealthKnowledgeBaseController.java` - Bean 名冲突检查: `grep -rn 'class HealthKnowledgeBaseController' src/main/java/com/etotem/cfc/controller/` 确保无重名 - [x] **Step 1: 检查路由重复** Run: `cd /sc-data/cfc/cfc-backend && grep -rn '@Mapping' src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u` Expected: /api/health/knowledge/query 未出现 - [x] **Step 2: 创建 Controller** ```java package com.etotem.cfc.controller; import com.etotem.cfc.common.Result; import com.etotem.cfc.entity.HealthKnowledgeBase; import com.etotem.cfc.service.HealthKnowledgeBaseService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.List; import java.util.Map; @Slf4j @Tag(name = "健康知识库") @RestController @RequestMapping("/api/health/knowledge") public class HealthKnowledgeBaseController { @Resource private HealthKnowledgeBaseService healthKnowledgeBaseService; @Operation(summary = "查询知识库条目") @PostMapping("/query") public Result query(@RequestBody Map params) { String itemType = params.get("itemType"); String itemName = params.get("itemName"); if (itemType == null || itemName == null) { return Result.error("itemType和itemName不能为空"); } HealthKnowledgeBase entry = healthKnowledgeBaseService.query(itemType, itemName); return Result.success(entry); } @Operation(summary = "批量查询知识库") @PostMapping("/batch-query") public Result> batchQuery(@RequestBody Map params) { @SuppressWarnings("unchecked") List> queries = (List>) params.get("queries"); if (queries == null || queries.isEmpty()) { return Result.error("queries不能为空"); } Map result = healthKnowledgeBaseService.batchQuery(queries); return Result.success(result); } } ``` - [x] **Step 3: 编译验证** Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile` Expected: BUILD SUCCESS - [x] **Step 4: Commit** ```bash cd /sc-data/cfc git add cfc-backend/src/main/java/com/etotem/cfc/controller/HealthKnowledgeBaseController.java git commit -m "feat(backend): add HealthKnowledgeBaseController (query + batch-query)" ``` --- ### Task 5: 后端 — 数据库迁移(创建 health_knowledge_base 表) **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` - Modify: `cfc-backend/src/main/resources/schema.sql` - [x] **Step 1: 在 DatabaseInitializer 添加迁移 93** 在 `runMigrations()` 末尾(`// 迁移92` 之后)追加: ```java // 迁移93: 创建 health_knowledge_base 表(菌群/营养知识库) try { jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `health_knowledge_base` (" + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, " + "`item_type` VARCHAR(50) NOT NULL COMMENT '类型: bacteria/nutrient/indicator/vitamin/amino_acid/disease', " + "`item_name` VARCHAR(200) NOT NULL COMMENT '项目名称(精确匹配)', " + "`category` VARCHAR(100) COMMENT '分类(如 有益菌/有害菌)', " + "`normal_range` VARCHAR(200) COMMENT '正常范围参考', " + "`description` TEXT COMMENT '详细说明', " + "`suggestion` TEXT COMMENT '相关建议', " + "`source` VARCHAR(100) COMMENT '数据来源', " + "`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, " + "`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " + "UNIQUE KEY `uk_item_type_name` (`item_type`, `item_name`), " + "KEY `idx_item_type` (`item_type`)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康知识库 - 菌属/营养素/指标说明'"); log.info("已创建health_knowledge_base表"); } catch (Exception e) { // 表已存在,忽略错误 } ``` **Location detail:** 在 DatabaseInitializer.java 末尾的 `runMigrations()` 中,最后一个 try-catch 之后、`}` 闭合之前插入。当前第 5884 行附近。 - [x] **Step 2: 在 schema.sql 末尾追加 DDL** ```sql -- 健康知识库 — 菌属/营养素/指标说明 CREATE TABLE IF NOT EXISTS `health_knowledge_base` ( `id` BIGINT AUTO_INCREMENT PRIMARY KEY, `item_type` VARCHAR(50) NOT NULL COMMENT '类型: bacteria/nutrient/indicator/vitamin/amino_acid/disease', `item_name` VARCHAR(200) NOT NULL COMMENT '项目名称(精确匹配)', `category` VARCHAR(100) COMMENT '分类(如 有益菌/有害菌)', `normal_range` VARCHAR(200) COMMENT '正常范围参考', `description` TEXT COMMENT '详细说明', `suggestion` TEXT COMMENT '相关建议', `source` VARCHAR(100) COMMENT '数据来源', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_item_type_name` (`item_type`, `item_name`), KEY `idx_item_type` (`item_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康知识库 - 菌属/营养素/指标说明'; ``` - [x] **Step 3: 编译验证** Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile` Expected: BUILD SUCCESS - [x] **Step 4: Commit** ```bash cd /sc-data/cfc git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java git add cfc-backend/src/main/resources/schema.sql git commit -m "feat(backend): add health_knowledge_base table (migration 93)" ``` --- ### Task 6: 后端 — 报告编辑 API(POST /api/health/report/edit) **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java` - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java` - [x] **Step 1: 在 Controller 中新增编辑端点** 在 `HealthReportController` 中(`getReportDetail` 方法之后)追加: ```java /** * 编辑已发布报告 — 修正 AI 解析错误的值 * 只修改已有条目(不增删),保存后刷新 7 维健康评分 */ @Operation(summary = "编辑已发布报告数据") @PostMapping("/report/edit") public Result editReport( @RequestBody Map params, @RequestAttribute("userId") Long userId) { Long reportId = params.get("reportId") != null ? Long.valueOf(params.get("reportId").toString()) : null; if (reportId == null) { return Result.error("reportId不能为空"); } Long subjectId = params.get("subjectId") != null ? Long.valueOf(params.get("subjectId").toString()) : null; if (subjectId == null) { return Result.error("subjectId不能为空"); } @SuppressWarnings("unchecked") Map payload = (Map) params.get("payload"); if (payload == null) { return Result.error("payload不能为空"); } healthReportService.updateReportFromPayload(reportId, payload, subjectId); return Result.success("已保存"); } ``` - [x] **Step 2: 在 Service 中新增 `updateReportFromPayload`** ```java /** * 更新已发布报告的编辑数据 * 逻辑: 只更新已有字段的值,不增删条目 */ @Transactional(rollbackFor = Exception.class) public void updateReportFromPayload(Long reportId, Map payload, Long subjectId) { // 1. 更新 health_report 评分数值 HealthReport report = healthReportMapper.selectById(reportId); if (report == null) { throw new RuntimeException("报告不存在: " + reportId); } if (payload.get("overallScore") != null) { report.setOverallScore(Integer.valueOf(payload.get("overallScore").toString())); } if (payload.get("gutHealthScore") != null) { report.setGutHealthScore(Integer.valueOf(payload.get("gutHealthScore").toString())); } if (payload.get("chronicDiseaseScore") != null) { report.setChronicDiseaseScore(Integer.valueOf(payload.get("chronicDiseaseScore").toString())); } if (payload.get("nutritionScore") != null) { report.setNutritionScore(Integer.valueOf(payload.get("nutritionScore").toString())); } if (payload.get("gutBalanceScore") != null) { report.setGutBalanceScore(Integer.valueOf(payload.get("gutBalanceScore").toString())); } if (payload.get("gutDiversityScore") != null) { report.setGutDiversityScore(Integer.valueOf(payload.get("gutDiversityScore").toString())); } if (payload.get("beneficialBacteriaScore") != null) { report.setBeneficialBacteriaScore(Integer.valueOf(payload.get("beneficialBacteriaScore").toString())); } if (payload.get("harmfulBacteriaScore") != null) { report.setHarmfulBacteriaScore(Integer.valueOf(payload.get("harmfulBacteriaScore").toString())); } if (payload.get("coreSpeciesScore") != null) { report.setCoreSpeciesScore(Integer.valueOf(payload.get("coreSpeciesScore").toString())); } healthReportMapper.updateById(report); // 2. 更新 indicators(删除旧→插入新) @SuppressWarnings("unchecked") List> indicators = (List>) payload.get("indicators"); if (indicators != null) { healthIndicatorMapper.delete(new LambdaQueryWrapper() .eq(HealthIndicator::getReportId, reportId)); for (int i = 0; i < indicators.size(); i++) { Map item = indicators.get(i); HealthIndicator ind = new HealthIndicator(); ind.setReportId(reportId); ind.setCategory((String) item.get("category")); ind.setIndicatorName((String) item.get("indicatorName")); ind.setIndicatorValue(item.get("indicatorValue") != null ? item.get("indicatorValue").toString() : null); ind.setUnit((String) item.get("unit")); ind.setRefRange((String) item.get("refRange")); ind.setStatus((String) item.get("status")); ind.setSymptoms((String) item.get("symptoms")); ind.setSortOrder(i); healthIndicatorMapper.insert(ind); } } // 3. 更新 gutFlora @SuppressWarnings("unchecked") List> gutFlora = (List>) payload.get("gutFlora"); if (gutFlora != null) { healthGutFloraMapper.delete(new LambdaQueryWrapper() .eq(HealthGutFlora::getReportId, reportId)); for (int i = 0; i < gutFlora.size(); i++) { Map item = gutFlora.get(i); HealthGutFlora flora = new HealthGutFlora(); flora.setReportId(reportId); flora.setBacteriaName((String) item.get("bacteriaName")); flora.setBacteriaValue(item.get("bacteriaValue") != null ? item.get("bacteriaValue").toString() : null); flora.setNormalRange((String) item.get("normalRange")); flora.setStatus((String) item.get("status")); flora.setCategory((String) item.get("category")); flora.setPopulationLevel((String) item.get("populationLevel")); flora.setDetectionRate((String) item.get("detectionRate")); flora.setLevel((String) item.get("level")); flora.setSortOrder(i); healthGutFloraMapper.insert(flora); } } // 4. 更新 diseaseRisks @SuppressWarnings("unchecked") List> diseaseRisks = (List>) payload.get("diseaseRisks"); if (diseaseRisks != null) { healthDiseaseRiskMapper.delete(new LambdaQueryWrapper() .eq(HealthDiseaseRisk::getReportId, reportId)); for (Map item : diseaseRisks) { HealthDiseaseRisk risk = new HealthDiseaseRisk(); risk.setReportId(reportId); risk.setDiseaseName((String) item.get("diseaseName")); risk.setRiskValue(item.get("riskValue") != null ? item.get("riskValue").toString() : null); risk.setRiskLevel((String) item.get("riskLevel")); healthDiseaseRiskMapper.insert(risk); } } // 5. 刷新 7 维健康评分 try { dimensionScoreService.refreshFromGutReport(subjectId, reportId); log.info("编辑后已刷新成员{}的7维健康评分", subjectId); } catch (Exception e) { log.warn("刷新7维健康评分失败: {}", e.getMessage()); } } ``` - [x] **Step 3: 在 Controller 上方追加所需的 import** 确认文件顶部已有: ```java import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.transaction.annotation.Transactional; ``` 如果 `@Transactional` 没有 import,在文件顶部追加: ```java import org.springframework.transaction.annotation.Transactional; ``` - [x] **Step 4: 确保 HealthReportService 中有需要的 Mapper 注入** 在 `HealthReportService.java` 中确认以下字段已存在,若不存在则添加: ```java @Resource private HealthIndicatorMapper healthIndicatorMapper; @Resource private HealthGutFloraMapper healthGutFloraMapper; @Resource private HealthDiseaseRiskMapper healthDiseaseRiskMapper; @Resource private HealthReportMapper healthReportMapper; @Resource private DimensionScoreService dimensionScoreService; ``` - [x] **Step 5: 编译验证** Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile` Expected: BUILD SUCCESS - [x] **Step 6: Commit** ```bash cd /sc-data/cfc git add cfc-backend/ git commit -m "feat(backend): add POST /api/health/report/edit + updateReportFromPayload" ``` --- ### Task 7: 后端 — 导入参考 JSON 知识库数据 **Files:** - Create: `cfc-backend/src/main/java/com/etotem/cfc/service/KnowledgeBaseDataImporter.java` - 数据源: `docs/superpowers/refs/501999942-某人.json`, `530010234-侯.json`, `儿童示例.json` - [x] **Step 1: 创建数据导入 Service(一次性工具)** ```java package com.etotem.cfc.service; import com.etotem.cfc.entity.HealthKnowledgeBase; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.io.InputStream; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * 知识库数据导入器 — 从参考 JSON 导入菌属/指标说明 * 仅在 health_knowledge_base 为空时执行一次 */ @Slf4j @Service public class KnowledgeBaseDataImporter { @Resource private HealthKnowledgeBaseService knowledgeBaseService; @Resource private ObjectMapper objectMapper; private static final String[] REF_JSON_FILES = { "/data/refs/501999942-某人.json", "/data/refs/530010234-侯.json", "/data/refs/儿童示例.json" }; @PostConstruct public void init() { // 延迟执行,等待数据库表就绪 new Thread(() -> { try { Thread.sleep(5000); importAll(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); } public void importAll() { try { // 检查是否已有数据 HealthKnowledgeBase existing = knowledgeBaseService.query("bacteria", "双歧杆菌属"); if (existing != null) { log.info("知识库已有数据,跳过导入"); return; } Set dedupSet = new HashSet<>(); for (String jsonFile : REF_JSON_FILES) { importFromRef(jsonFile, dedupSet); } log.info("知识库数据导入完成"); } catch (Exception e) { log.error("知识库数据导入失败", e); } } private void importFromRef(String classpathFile, Set dedupSet) { try (InputStream is = getClass().getResourceAsStream(classpathFile)) { if (is == null) { log.warn("参考JSON文件不存在: {}", classpathFile); return; } Map root = objectMapper.readValue(is, new TypeReference>() {}); // 导入菌属说明(gutFlora 数组含 description 字段) importGutFloraDescriptions(root, dedupSet); // 导入指标说明(indicators 数组含 status/refRange 等) importIndicatorDescriptions(root, dedupSet); } catch (Exception e) { log.error("读取参考JSON失败: {}", classpathFile, e); } } private void importGutFloraDescriptions(Map root, Set dedupSet) { Object raw = root.get("gutFlora"); if (raw instanceof List) { for (Object item : (List) raw) { if (item instanceof Map) { Map flora = (Map) item; String name = (String) flora.get("bacteriaName"); String desc = (String) flora.get("description"); if (name != null && desc != null && !desc.isEmpty()) { String key = "bacteria:" + name; if (dedupSet.add(key)) { HealthKnowledgeBase entry = new HealthKnowledgeBase(); entry.setItemType("bacteria"); entry.setItemName(name); entry.setDescription(desc); entry.setCategory((String) flora.get("category")); entry.setNormalRange((String) flora.get("normalRange")); knowledgeBaseService.insert(entry); } } } } } } private void importIndicatorDescriptions(Map root, Set dedupSet) { Object raw = root.get("indicators"); if (raw instanceof List) { for (Object item : (List) raw) { if (item instanceof Map) { Map ind = (Map) item; String name = (String) ind.get("indicatorName"); String category = (String) ind.get("category"); if (name == null) continue; // 将 category 映射为 itemType String itemType = mapIndicatorCategory(category); String key = itemType + ":" + name; if (dedupSet.add(key)) { HealthKnowledgeBase entry = new HealthKnowledgeBase(); entry.setItemType(itemType); entry.setItemName(name); entry.setCategory(category); entry.setNormalRange((String) ind.get("refRange")); entry.setDescription((String) ind.get("symptoms")); // 部分 JSON 用 status 字段描述健康状况 knowledgeBaseService.insert(entry); } } } } } private String mapIndicatorCategory(String category) { if (category == null) return "nutrient"; switch (category) { case "维生素": return "vitamin"; case "氨基酸": return "amino_acid"; case "微量元素": return "indicator"; case "肠道屏障与代谢物": return "indicator"; case "短链脂肪酸": return "indicator"; case "神经递质与激素": return "indicator"; case "抗生素风险评估": return "indicator"; default: return "nutrient"; } } } ``` - [x] **Step 2: 创建资源目录并将参考 JSON 复制到 resources/data/refs/** ```bash mkdir -p /sc-data/cfc/cfc-backend/src/main/resources/data/refs cp /sc-data/cfc/docs/superpowers/refs/501999942-某人.json /sc-data/cfc/cfc-backend/src/main/resources/data/refs/ cp /sc-data/cfc/docs/superpowers/refs/530010234-侯.json /sc-data/cfc/cfc-backend/src/main/resources/data/refs/ cp /sc-data/cfc/docs/superpowers/refs/儿童示例.json /sc-data/cfc/cfc-backend/src/main/resources/data/refs/ ``` - [x] **Step 3: 编译验证** Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile` Expected: BUILD SUCCESS - [x] **Step 4: Commit** ```bash cd /sc-data/cfc git add cfc-backend/ git commit -m "feat(backend): add KnowledgeBaseDataImporter + ref JSONs" ``` --- ### Task 8: 前端 — api.js 新增方法 - [x] **Step 1: 在 utils/api.js 中追加 3 个方法**(在 `getReportDetail` 附近,约第 1515 行) ```javascript export const editHealthReport = (reportId, payload, subjectId) => request('/api/health/report/edit', 'POST', { reportId, payload, subjectId }) export const queryKnowledgeBase = (itemType, itemName) => request('/api/health/knowledge/query', 'POST', { itemType, itemName }) export const batchQueryKnowledgeBase = (queries) => request('/api/health/knowledge/batch-query', 'POST', { queries }) ``` - [x] **Step 2: Commit** ```bash cd /sc-data/cfc git add cfc-frontend/utils/api.js git commit -m "feat(frontend): add editHealthReport, queryKnowledgeBase, batchQueryKnowledgeBase API" ``` --- ### Task 9: 前端 — 知识库弹出框组件 **Files:** - Create: `cfc-frontend/components/health-knowledge-popup.vue` - [x] **Step 1: 创建弹出框组件** ```vue ``` - [x] **Step 2: Commit** ```bash cd /sc-data/cfc git add cfc-frontend/components/health-knowledge-popup.vue git commit -m "feat(frontend): add health-knowledge-popup component" ``` --- ### Task 10: 前端 — pages.json 注册 3 个新页面 > ✅ **实施记录(2026-08-22 更新):** 初版仅注册 species/risks 两条;同日已补齐——`gut-flora-foods-detail` 注册至 health 分包(页面自 growth/foods-detail 迁移而来,见附录偏差①)。现 pages.json 含全部 3 条 gut-flora 子页 + 主页,与计划一致。 - [x] **Step 1: 在 pages.json 中 health 分组内追加 3 条路径** 在 `"path": "pages/health/gut-flora-detail"` 条目之后追加: ```json { "path": "pages/health/gut-flora-species-detail", "style": { "navigationBarTitleText": "菌群详情" } }, { "path": "pages/health/gut-flora-foods-detail", "style": { "navigationBarTitleText": "饮食推荐" } }, { "path": "pages/health/gut-flora-risks-detail", "style": { "navigationBarTitleText": "疾病风险" } } ``` - [x] **Step 2: Commit** ```bash cd /sc-data/cfc git add cfc-frontend/pages.json git commit -m "feat(frontend): register 3 new health detail pages in pages.json" ``` --- ### Task 11: 前端 — 食物推荐详情页 > ✅ **实施记录(2026-08-22 更新):** 页面现已存在于计划路径 `cfc-frontend/pages/health/gut-flora-foods-detail.vue`——由 `pages/growth/foods-detail.vue` 经 git mv 迁移归位,功能含知识库营养数据合并增强;主页跳转已同步更新。详见附录偏差①修复记录。 **Files:** - Create: `cfc-frontend/pages/health/gut-flora-foods-detail.vue` - [x] **Step 1: 创建页面(最简单页面,先做以验证编辑流)** ```vue ``` - [x] **Step 2: Commit** ```bash cd /sc-data/cfc git add cfc-frontend/pages/health/gut-flora-foods-detail.vue git commit -m "feat(frontend): add gut-flora-foods-detail page" ``` --- ### Task 12: 前端 — 疾病风险详情页 **Files:** - Create: `cfc-frontend/pages/health/gut-flora-risks-detail.vue` - [x] **Step 1: 创建页面** ```vue ``` - [x] **Step 2: Commit** ```bash cd /sc-data/cfc git add cfc-frontend/pages/health/gut-flora-risks-detail.vue git commit -m "feat(frontend): add gut-flora-risks-detail page with knowledge popup" ``` --- ### Task 13: 前端 — 菌群详情页(12 Tab + 颜色箭头) **Files:** - Create: `cfc-frontend/pages/health/gut-flora-species-detail.vue` - [x] **Step 1: 创建页面** ```vue ``` - [x] **Step 2: Commit** ```bash cd /sc-data/cfc git add cfc-frontend/pages/health/gut-flora-species-detail.vue git commit -m "feat(frontend): add gut-flora-species-detail page (12 tabs + color arrows + knowledge popup)" ``` --- ### Task 14: 前端 — 主页 gut-flora-detail.vue 重写 **Files:** - Modify: `cfc-frontend/pages/health/gut-flora-detail.vue`(完全重写) - [x] **Step 1: 重写主页 — 完整长 scroll + 评分圆环 + 手风琴 + 颜色箭头 + 知识库 + 编辑模式** ```vue ``` - [x] **Step 2: Commit** ```bash cd /sc-data/cfc git add cfc-frontend/pages/health/gut-flora-detail.vue git commit -m "feat(frontend): rewrite gut-flora-detail with full data, accordion, colors, knowledge popup, edit mode" ``` --- ### Task 15: E2E 测试适配 **Files:** - Modify: `tests/e2e/flora-microbiome-flow.spec.js` - [x] **Step 1: 检查并更新现有 E2E 测试** 读取现有测试文件,确认 `.kb-link`、`.kb-popup-overlay`、`.flora-name` 等新选择器是否涵盖。若无,添加新断言并在关键操作步骤增加超时。 关键检查项: - 报告头部显示(`.report-title`, `.badge`) - 评分圆环存在(`.score-item` 数量 >= 3) - 快速导航入口可见(`.nav-card` 数量 = 4) - 手风琴可折叠(`.accordion-header`点击后 `.accordion-body` 可见) - 知识库弹出框触发(`.indicator-name` 点击后 `.kb-popup-overlay` 可见) - 颜色状态类名(`.val-偏高`, `.status-偏低` 等) - FAB 编辑按钮(`.fab`) - 导航到详情页路径正确 为 Playwright 配置 viewport: `{ width: 375, height: 667 }`(iPhone 6/7/8 尺寸) - [x] **Step 2: Commit** ```bash cd /sc-data/cfc git add tests/e2e/flora-microbiome-flow.spec.js git commit -m "test(e2e): update flora-microbiome-flow tests for restructured pages" ``` --- ### Task 16: 最终验证 - [x] **Step 1: 后端编译** Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile` Expected: BUILD SUCCESS - [x] **Step 2: 检查小程序项目完整注册** Run: `cd /sc-data/cfc && grep -c 'gut-flora' cfc-frontend/pages.json` Expected: 4(gut-flora-detail, gut-flora-species-detail, gut-flora-risks-detail, gut-flora-foods-detail;✅ 2026-08-22 偏差①修复后达标) - [x] **Step 3: 检查所有新文件** Run: `cd /sc-data/cfc && git ls-files -- 'cfc-backend/' 'cfc-frontend/' | grep -E '(KnowledgeBase|gut-flora|knowledge-popup)'` Expected: 所有新文件已跟踪 - [x] **Step 4: 最终 push** ```bash cd /sc-data/cfc git push origin cfclub ``` --- ## Self-Review Checklist ### 1. 规范覆盖度 | 规范需求 | 对应 Task | |---------|-----------| | 知识库独立表 health_knowledge_base | Task 1 (Entity) + Task 5 (Migration) | | 知识库查询 API(单条+批量) | Task 3 (Service) + Task 4 (Controller) | | 报告编辑 API | Task 6 | | 仅展示个性化数据(无内嵌说明) | Task 12-14(所有卡片不显示 description) | | 菌名/指标名可点击弹出知识库 | Task 12-14(`@tap="showKnowledge"`) | | 颜色+箭头表示异常值 | Task 13-14(`:class="'status-' + item.status"` + ``) | | 主页手风琴 | Task 14(`toggleAccordion`) | | 4 页架构 | Task 10 (pages.json) + Task 11-14 | | 12 分类 Tab + 卡片 | Task 13 | | 编辑模式切换 | Task 11-14(`toggleEdit`/`cancelEdit`/`saveEdit`) | | 数据库迁移 | Task 5(迁移93) | | schema.sql 同步 | Task 5 | | 异常值颜色: 偏高=红↑, 偏低=琥珀↓ | Task 13-14 | | E2E 测试 | Task 15 | ### 2. 占位符扫描 - 所有代码块包含完整实现,无 `TBD`/`TODO` - 所有文件路径使用绝对(相对于项目根) - 所有 API 方法签名在前后端间一致 ### 3. 类型一致性 - `HealthKnowledgeBase` 的字段名与数据库列名一致(`itemType` ↔ `item_type` via MyBatis-Plus 自动映射) - Controller 方法参数名与前端 `api.js` 调用参数一致 - `batchQuery` 的 Map key 格式 `"itemType:itemName"` 在前端 `/utils/api.js` 和 Service 中一致 --- ## Execution Handoff **Plan complete and saved to `docs/superpowers/plans/2026-07-17-flora-microbiome-report-implementation.md`.** Two execution options: 1. **Subagent-Driven (recommended)** — I dispatch a fresh subagent per task, review between tasks, fast iteration 2. **Inline Execution** — Execute tasks in this session with checkpoints Which approach? --- ## 附录:实施偏差记录(2026-08-22 核实回填) ### 偏差① 食物推荐页落地路径变更 —— ✅ 已修复(2026-08-22) **偏差原状**:初版实施将食物推荐页落在 `pages/growth/foods-detail.vue`,未按计划创建 `pages/health/gut-flora-foods-detail.vue`。 **修复动作(2026-08-22)**: | 项 | 修复内容 | |----|---------| | 文件迁移 | `git mv cfc-frontend/pages/growth/foods-detail.vue cfc-frontend/pages/health/gut-flora-foods-detail.vue`(git 历史保留) | | pages.json | 移除 growth 分包注册;health 分包新增 `gut-flora-foods-detail`(gut-flora-risks-detail 之后),4 页架构齐备 | | 主页入口 | gut-flora-detail.vue `goToFoods()` 改跳 `/pages/health/gut-flora-foods-detail?reportId=` | | 功能保留 | 知识库「食物库」营养数据合并增强(getKnowledgeSection)原样保留 | | 验证 | script 块 node --check 通过;pages.json JSON 校验通过;全仓无 `growth/foods-detail` 残留引用;E2E spec 无 foods 路径断言、无需变更 | 注:`growth/caution-detail`(戒备食材页)与 `diet/index.vue`(独立饮食推荐系统)不属菌群报告流,维持原位。 ### 偏差② 迁移编号变化 计划编写时为「迁移93」,实际落地时该编号已被其它迁移占用,health_knowledge_base 表创建落在 **迁移98**(DatabaseInitializer.java:6795,schema.sql:3293 已同步)。 ### 核实证据清单 - 后端四件套存在:`entity/HealthKnowledgeBase.java`、`mapper/HealthKnowledgeBaseMapper.java`、`service/HealthKnowledgeBaseService.java`、`controller/HealthKnowledgeBaseController.java`(且扩展了 v3/unified 批量端点) - 编辑链路存在:`HealthReportController` `/report/edit`(L357) + `updateReportFromPayload` + `DimensionScoreService.refreshFromGutReport` - 前端存在:`components/health-knowledge-popup.vue`、`gut-flora-detail.vue`(重写版)、`gut-flora-species-detail.vue`、`gut-flora-risks-detail.vue`、api.js 三个方法(L1941-1947) - E2E 存在:`tests/e2e/flora-microbiome-flow.spec.js` - 五维页面接入:身(body-detail 营养模块+肠胃健康评分+菌群评分面板)、心(mind-detail 情绪与肠胃 + `/api/mind/neurotransmitters`)、智(wisdom-detail 肠胃与认知 + `/api/wisdom/gut-cognition`);行/富无菌群业务(符合预期)