2026-08-10-report-blocks-renderer-design.md 10 KB

通用报告块渲染引擎(report-blocks)设计

日期:2026-08-10 状态:已确认(用户已逐节批准)

背景与问题

肠道菌群报告详情页存在字段错位 bug:buildReportFromPayload(HealthReportController:1553-1593)只填充第二套评分字段(diversityScore 等),而前端 gut-flora-detail.vuebuildScores(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 统一外壳 + <report-blocks-renderer> 通用渲染

表结构

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 种块类型)

[
  { "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-forblock.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(实测样本)