Просмотр исходного кода

docs: 通用报告块渲染引擎设计文档

asus 1 месяц назад
Родитель
Сommit
76d1f07398
1 измененных файлов с 191 добавлено и 0 удалено
  1. 191 0
      docs/superpowers/specs/2026-08-10-report-blocks-renderer-design.md

+ 191 - 0
docs/superpowers/specs/2026-08-10-report-blocks-renderer-design.md

@@ -0,0 +1,191 @@
+# 通用报告块渲染引擎(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 统一外壳 + <report-blocks-renderer> 通用渲染
+```
+
+## 表结构
+
+```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`(实测样本)