# 菌群报告页面重构 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 --- ## 文件结构总览 ### 后端(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 | 食物推荐(筛选+卡片+编辑) | | `pages/health/gut-flora-risks-detail.vue` | NEW | 疾病风险(分类+卡片+编辑) | | `pages.json` | MODIFY | 注册 3 个新页面 | | `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` - [ ] **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; } ``` - [ ] **Step 2: 编译验证** Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile` Expected: BUILD SUCCESS - [ ] **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` - [ ] **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 { } ``` - [ ] **Step 2: 编译验证** Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile` Expected: BUILD SUCCESS - [ ] **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` - [ ] **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); } } ``` - [ ] **Step 2: 编译验证** Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile` Expected: BUILD SUCCESS - [ ] **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/` 确保无重名 - [ ] **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 未出现 - [ ] **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); } } ``` - [ ] **Step 3: 编译验证** Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile` Expected: BUILD SUCCESS - [ ] **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` - [ ] **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 行附近。 - [ ] **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='健康知识库 - 菌属/营养素/指标说明'; ``` - [ ] **Step 3: 编译验证** Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile` Expected: BUILD SUCCESS - [ ] **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` - [ ] **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("已保存"); } ``` - [ ] **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()); } } ``` - [ ] **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; ``` - [ ] **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; ``` - [ ] **Step 5: 编译验证** Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile` Expected: BUILD SUCCESS - [ ] **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` - [ ] **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"; } } } ``` - [ ] **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/ ``` - [ ] **Step 3: 编译验证** Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile` Expected: BUILD SUCCESS - [ ] **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 新增方法 - [ ] **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 }) ``` - [ ] **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` - [ ] **Step 1: 创建弹出框组件** ```vue ``` - [ ] **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 个新页面 - [ ] **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": "疾病风险" } } ``` - [ ] **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: 前端 — 食物推荐详情页 **Files:** - Create: `cfc-frontend/pages/health/gut-flora-foods-detail.vue` - [ ] **Step 1: 创建页面(最简单页面,先做以验证编辑流)** ```vue ``` - [ ] **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` - [ ] **Step 1: 创建页面** ```vue ``` - [ ] **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` - [ ] **Step 1: 创建页面** ```vue ``` - [ ] **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`(完全重写) - [ ] **Step 1: 重写主页 — 完整长 scroll + 评分圆环 + 手风琴 + 颜色箭头 + 知识库 + 编辑模式** ```vue ``` - [ ] **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` - [ ] **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 尺寸) - [ ] **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: 最终验证 - [ ] **Step 1: 后端编译** Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile` Expected: BUILD SUCCESS - [ ] **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-foods-detail, gut-flora-risks-detail) - [ ] **Step 3: 检查所有新文件** Run: `cd /sc-data/cfc && git ls-files -- 'cfc-backend/' 'cfc-frontend/' | grep -E '(KnowledgeBase|gut-flora|knowledge-popup)'` Expected: 所有新文件已跟踪 - [ ] **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?