# 通用报告块渲染引擎(report-blocks)设计 日期:2026-08-10 状态:已确认(用户已逐节批准) ## 背景与问题 肠道菌群报告详情页存在字段错位 bug:`buildReportFromPayload`(HealthReportController:1553-1593)只填充第二套评分字段(`diversityScore` 等),而前端 `gut-flora-detail.vue` 的 `buildScores`(242-252)读取第一套字段(`gutDiversityScore` 等)→ 5 个评分圆环(菌群多样性/平衡/有益菌/有害菌/核心菌属)永远为 null 不显示。 更深层的问题:系统存在多套并行报告链路(肠道菌群/体检/舌诊/DAN),每种报告各有一套页面、字段、接口,扩展成本高、易出字段错位类 bug。DAN 报告(独立 `dan_report_uploads` 表 + `DanReportController`)甚至没有独立详情页,只能复用上传页展示。 ## 目标 构建**通用报告块渲染引擎**: 1. 解析器输出统一 `blocks` 结构(JSON),前端按 `block.type` 通用渲染,**前端零字段名耦合** 2. 覆盖报告类型:`gut_flora` / `dan` / `physical_exam` / `tongue`(体检/舌诊二期) 3. 疾病风险按**风险等级分组**(重要风险 = 需注意/高风险/异常;其它风险 = 低风险),直接展示在详情页内 4. 修复肠道菌群 9 项评分圆环显示问题(从源头消灭键名错位) 5. 存量接口/页面零破坏(旧字段保留兼容,逐步迁移) ## 架构总览 ``` 解析器(LangGraph / Java fallback / DanReportParseService) ↓ 结构化数据(ParsedReportPayload / DanParsedReport) ReportBlockAssembler —— 按 reportType 组装 blocks[] ↓ confirm/编辑保存时落库 report_blocks 表(report_type + report_id 复合唯一键 + blocks JSON) ↓ 查询 getReportDetail / DanReportController.detail —— 返回 reportType + blocks(旧字段保留) ↓ 前端 report-detail 统一外壳 + 通用渲染 ``` ## 表结构 ```sql CREATE TABLE report_blocks ( id BIGINT AUTO_INCREMENT PRIMARY KEY, report_type VARCHAR(32) NOT NULL COMMENT 'gut_flora / dan / physical_exam / tongue', report_id BIGINT NOT NULL COMMENT '各类型报告主键ID(health_reports.id 或 dan_report_uploads.id,各自序列不冲突)', blocks JSON NOT NULL COMMENT '块数组 [{type,title,items,extra}]', version INT DEFAULT 1, created_at DATETIME, updated_at DATETIME, UNIQUE KEY uk_report_type_id (report_type, report_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报告通用展示块'; ``` - `(report_type, report_id)` 复合唯一键 → 各类型 id 序列互不干扰 - 落库用 **upsert**(存在则覆盖),幂等 - 迁移入口:`DatabaseInitializer.runMigrations()`(按 AGENTS.md 迁移工作流,同步 `schema.sql`) ## blocks schema(6 种块类型) ```json [ { "type": "score", "title": "健康评分", "items": [ {"label":"综合","value":57,"max":100}, {"label":"菌群多样性","value":50,"max":100} ] }, { "type": "risk_group", "title": "重要风险", "items": [ {"name":"心脑血管疾病","value":"0.37","level":"注意"}, {"name":"抑郁症","value":"0.27","level":"低风险"} ] }, { "type": "indicator", "title": "营养素", "items": [ {"name":"维生素D","value":"28.5","unit":"ng/mL","refRange":"30-100","status":"偏低"} ] }, { "type": "list", "title": "检出菌种", "columns": [ {"key":"name","label":"菌种"}, {"key":"value","label":"相对丰度"}, {"key":"status","label":"状态"} ], "items": [ {"name":"双歧杆菌","value":"12.4%","status":"正常"} ] }, { "type": "text", "title": "报告解读", "content": "..." }, { "type": "chart", "title": "维度雷达", "chartType": "radar", "data": {...} } ] ``` | 块类型 | 覆盖现有 | 前端渲染 | |--------|---------|---------| | `score` | 肠道 9 项评分、DAN total_score/percentile | 评分圆环组 | | `risk_group` | 疾病风险(按等级分"重要风险/其它风险"两组) | 风险卡片 + 等级徽章 | | `indicator` | 营养素/肠道屏障指标(status 高亮) | 指标卡片/表格 | | `list` | 菌种列表、DAN 各维度得分 items | 通用列表(columns 驱动) | | `text` | 报告解读、DAN summary/suggestions | 段落 | | `chart` | 雷达图(body-detail 的 radarDimensions) | 预留(二期) | ## 后端实现 ### ReportBlock 实体 + ReportBlockMapper MyBatis-Plus,`@TableName("report_blocks")`,blocks 列存 JSON 字符串。提供 `save(type, reportId, blocks)`(upsert)与 `getBlocks(type, reportId)`。 ### ReportBlockAssembler Service 按 reportType 分发组装: | 方法 | 输入 | 输出 blocks | |------|------|------------| | `assembleGutFlora(ParsedReportPayload)` | LangGraph/Java fallback 的 payload | `score`(9 项评分) + `risk_group`(重要/其它两组) + `indicator`(营养素/肠道屏障) + `list`(菌种/病原菌/食物推荐) + `text`(解读) | | `assembleDan(DanParsedReport)` | DAN 解析结果 | `score`(total/percentile) + `list`(各维度得分按 category 分组) + `text`(summary/suggestions) | | `assembleExam(payload)` / `assembleTongue(...)` | 体检/舌诊(二期) | 对应块 | 要点: - **命名注意**:`report_blocks.report_type` 是**报告类别**(`gut_flora`/`dan`/`physical_exam`/`tongue`);DAN 子类型 A2/B4(`DanReportUpload.reportType`)不是类别,由组装器放入 blocks 的 `extra`(如 `{"subType":"A2"}`)供前端展示徽章,两者不得混淆 - 评分圆环:组装器从 payload 的**第二套键**(`balanceScore`/`diversityScore`/`beneficialScore`/`harmfulScore`/`coreGenusScore` + `overallScore`/`gutHealthScore`/`chronicDiseaseScore`/`nutritionScore`)组装 9 项,**前端不再碰实体字段名** - 疾病风险分组:`需注意/高风险/异常` → "重要风险"组;`低风险` → "其它风险"组 - 组装失败不阻断 confirm:blocks 留空,详情页回退旧字段 ### 落库挂点(3 处) | 挂点 | 时机 | 写入 | |------|------|------| | `confirmDraft`(HealthReportController:888 createReport 后) | 肠道菌群/体检确认 | `save('gut_flora', report.id, blocks)` | | `confirmUpload`(DanReportUploadService) | DAN 确认 | `save('dan', upload.id, blocks)` | | `updateReportFromPayload` / `editHealthReport` / `DanReportController.edit` | 编辑保存后 | 重新组装覆盖 | ### 接口 - `getReportDetail`(HealthReportService:312)追加 `detail.put("blocks", ...)`,旧字段(report/indicators/gutFlora/diseaseRisks)全保留 - `DanReportController.detail` 追加 blocks - 现有页面(body-detail/health-report.vue 等)零破坏 ## 前端实现 ### 通用渲染器 `components/report-blocks-renderer.vue` `props: { blocks: [] }`,`v-for` 按 `block.type` 分发 6 个子渲染组件: - `score-block`:评分圆环组(圆环 + 数值) - `risk-group-block`:风险卡片 + 等级徽章(重要风险红/橙,其它风险绿) - `indicator-block`:指标卡片(status 高亮) - `list-block`:通用列表(columns 驱动) - `text-block`:段落 - `chart-block`:预留 纯展示组件,零字段名耦合。遵循小程序限制(禁可选链/禁 CSS Grid/禁 `:key` 表达式)。 ### 统一详情页 `pages/health/report-detail.vue` 外壳:报告头(类型徽章/姓名/日期)+ blocks 渲染区。**疾病风险直接展示在详情页内**(risk_group 块),不用再跳子页。 ### 入口切换 - `report-list.vue` goToDetail(79-87):`gut_flora`/`dan` → 新详情页 `?reportType=&reportId=`;`physical_exam`/`tongue` 暂留原页面 - DAN 维度页 `viewDanReport`(wisdom-detail:370 / mind-detail):从 `report-upload?draftId=` 改为新详情页 - 旧详情页(gut-flora-detail 等)保留,验证通过后再删除 ### 编辑策略 **blocks 是只读视图模型**。编辑走现有结构化接口(`editHealthReport` 等),保存后重新组装 blocks。渲染引擎不做编辑器。 风险编辑:新详情页内联等级/值编辑 → 调 `editHealthReport` → 刷新 blocks。 ## 分期实施 | 阶段 | 内容 | 验证 | |------|------|------| | 1 | 后端全链:迁移建表 + Assembler(gut_flora+dan) + 3 个落库挂点 + 接口返回 blocks | `mvn clean compile` + Assembler 单元测试(断言 9 项评分/风险分组/DAN items) | | 2 | 前端:blocks-renderer + 统一详情页 + report-list 切换 gut_flora/dan | 小程序模拟器打开 501999942 报告:9 项评分圆环全显示、风险两组卡片、营养素/菌种/解读 | | 3 | 疾病风险内联编辑 + DAN 维度页跳转 + 兼容验证 | 编辑风险保存后 blocks 刷新;DAN A2/B4 报告在维度页正常渲染 | | 4 | (可选)体检/舌诊接入 + 旧页面收尾删除 | 回归 | ## 错误处理 - blocks 空/缺失 → 详情页显示"暂无报告内容"并**回退旧字段渲染**(兼容期兜底) - 组装失败 → 不阻断 confirm,blocks 留空 - 落库幂等(upsert) ## 测试 - 后端:`ReportBlockAssemblerTest`(gut_flora payload → blocks 断言:9 项评分齐全、风险分组正确、DAN items 映射)+ `mvn clean compile` - 前端:语法/结构校验(node --check),不改动打包流程(HBuilderX 打包) ## 相关文件 - `cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java`(confirmDraft:880 / buildReportFromPayload:1553) - `cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java`(getReportDetail:312 / updateReportFromPayload:1134) - `cfc-backend/src/main/java/com/etotem/cfc/service/DanReportUploadService.java`(confirmUpload) - `cfc-backend/src/main/java/com/etotem/cfc/controller/DanReportController.java`(detail:63) - `cfc-backend/src/main/java/com/etotem/cfc/dto/ParsedReportPayload.java`(payload 结构) - `cfc-backend/src/main/java/com/etotem/cfc/service/DanReportParseService.java`(DanParsedReport:1645) - `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`(迁移入口) - `cfc-backend/src/main/resources/schema.sql`(同步建表) - `cfc-frontend/pages/health/report-list.vue`(goToDetail:79) - `cfc-frontend/pages/health/gut-flora-detail.vue`(buildScores:239) - `cfc-frontend/pages/health/gut-flora-risks-detail.vue`(风险页) - `cfc-frontend/pages/wisdom-detail/index.vue` / `pages/mind-detail/index.vue`(viewDanReport) - `cfc-frontend/components/`(新渲染器组件目录) - `docs/参考资料/501999942-某人.json`(实测样本)