Преглед на файлове

docs: 报告类型采集指标定义与驱动解析设计(方案2 分层,P1+P2 本期)

Sisyphus преди 3 дни
родител
ревизия
c720dbf01e
променени са 1 файла, в които са добавени 162 реда и са изтрити 0 реда
  1. 162 0
      docs/superpowers/specs/2026-10-09-report-type-indicator-driven-parsing-design.md

+ 162 - 0
docs/superpowers/specs/2026-10-09-report-type-indicator-driven-parsing-design.md

@@ -0,0 +1,162 @@
+# 报告类型「采集指标」定义与驱动解析 — 设计文档
+
+- 日期:2026-10-09
+- 方案:方案2「分层」(DAN 保持 Java 解析器不动;指标清单驱动非 DAN 的 LLM/脚本解析;DAN 侧做归一 + 完整度校验)
+- 状态:待用户评审
+
+## 1. 背景与问题
+
+后台「报告类型管理」(`cfc-web` `ReportTypeManagement.vue` + `ReportParserAdminController`)目前只能维护两样东西:**报告类型 CRUD** 与**指纹规则 CRUD**。它**无法声明"某类报告应当采集哪些指标"**。
+
+由此导致的已观测问题(对 5 份已上传 DAN 报告审计):
+
+- `dan_b4`(wisdom)两份报告只采到 `selfConcept`(6) + `selfDriving`(3)=9 项;**`growthMindset`(成长型思维)0 项**——解析器 `parseGrowthMindset` 只提取"含数字行",而该章节为无数字图文,故整节漏采。
+- `dan_a2`(mind)两份报告:**情绪稳定性 4 分测验只采到 1 项**(自卑=4),缺 抑郁/焦虑/依赖;并**混入 5 项 `category=auto` 垃圾**(联系电话/报告编码/您测评的具体情况/身高/体重)。
+- 全部 DAN 上传 `parsed_summary` 长度为 0;`parsed_suggestions` 异常巨大(B4 21129 字符)→ 疑 `extractSection` 失配。
+
+根因:**没有"应采指标"的声明**,所以既不能驱动解析器去采全,也不能判断一次采集是否完整。本设计新增该声明能力,并让声明**驱动解析**与**完整性校验**。
+
+## 2. 目标 / 非目标
+
+### 目标
+1. 管理端可为每个报告类型定义一组成员指标(**采集指标**),成员来自既有指标目录 `indicator_definitions`。
+2. **驱动解析**:
+   - 非 DAN 类型(走 `cfc-langgraph` LLM/脚本)——把该类型的指标清单注入解析提示词/Schema,使 LLM 按清单提取。
+   - DAN 类型——保持现有 Java 解析器,但解析结果按清单**归一化**(补 `code`、对齐 `name`)并**计算缺失**。
+3. **完整度可见**:管理端可对已上传报告查看「应采 / 已采 / 缺失 / 多余」。
+4. 全程**幂等**(迁移可重复执行),不回归现有采集链路。
+
+### 非目标(本期不做)
+- 不改写 DAN Java 解析器的抽取逻辑(`parseA2Report/parseB4Report/parseGrowthMindset/...` 保持不变,避免回归)。
+- 不把 DAN 解析改为 LLM 驱动(列为 P3,目标态,另议)。
+- 不做报告展示的视觉/图表改版。
+- 不修复第 1 节列出的解析器缺陷本身(growthMindset 漏采、auto 垃圾、summary 失配)——**本设计只新增"声明 + 校验"能力把它们暴露出来**,缺陷修复单独排期。
+
+> 说明:本期交付后,第 1 节的漏采 / 垃圾项会在「完整度」中显示为"缺失 / 多余",但解析结果内容不会自动变化。若用户希望在 P1 同时修复这些缺陷,需单独确认(会触及 DAN 解析器,超出本 spec 的非目标)。
+
+## 3. 关键现状(实现事实)
+
+- **两条解析路径**(`ReportCollectService.collect`):
+  - `family=dan` → `collectDanReport` → `DanReportParseService.parse(fis, dimension)`(按维度硬编码),产出 `DanParsedReport{reportType, dimension, items:DataItem[]{code,name,value,category}, extra, summary, suggestions}`;经 `DanReportUploadService.confirmUpload` 入库(结果 / 展示块 / 汇总 / 五维)。
+  - 非 DAN → `LangGraphReportParseService.parseByScript / parseWithDetect`(Post `cfc-langgraph` `/api/v1/report/parse`;`postParse(path, body)` 只放 `file_path`),失败回退 `parseWithOpencode`。
+- **统一载荷**:`ParsedReportPayload.Payload`(含 `indicators: List<Indicator>`);`Indicator{category, indicatorName, indicatorValue, unit, refRange, status, symptoms, collectionDate}`,**无 `code` 字段**。
+- **指标目录**:`indicator_definitions(id, domain, category, code, name, data_type, options, unit, ref_range, sort_order, status)`,`code` 唯一;实体 `IndicatorDefinition`;既有端点 `/api/admin/indicator/{list,create,update,delete,values/list}`(`IndicatorAdminController`);管理页 `cfc-web-src/src/views/admin/indicators/index.vue`。
+- **报告类型表**:`report_type_registry`(`type_id` 唯一、`family`、`display_name`、`parser_path`…)与 `report_fingerprint`(`type_id` + `pattern` + `weight` + `is_exclusion`),DDL 见 `src/main/resources/schema.sql:4764` 起。
+- **迁移机制**:`DatabaseInitializer` 内顺序 `jdbcTemplate.execute(...)`(幂等 `CREATE TABLE IF NOT EXISTS` / `ALTER TABLE`,异常吞掉打 WARN);新迁移追加在末尾并同步 `schema.sql`。
+- **前端 API**:`cfc-web-src/src/api/reportParser.js`(`/api/admin/report-parser/*`)。
+- **DAN 展示分组**:`ReportBlockAssembler.assembleDanByType` 依据 `DAN_CATEGORY_TITLES`。
+
+## 4. 设计(方案2 分层)
+
+### 4.1 数据模型
+
+**A. 扩充指标目录 `indicator_definitions`(纯数据,幂等种子)**
+
+- 新增 `domain='dan'`,`category` 取 DAN 分类名(`bigFive/social/emotion/ability/overall/cognitive/executiveFunction/learningMotivation/learningStrategy/selfConcept/behavior/family/growthMindset/selfDriving/hollandInterest` 等)。
+- `code` 用稳定 slug(如 `dan_growth_mindset`),`name` **必须与 Java 解析器 `DataItem.name` 完全一致**(这是归一化匹配键),`data_type` 取 `number|text|score`,`status=1`。
+- 仅新增,不修改既有 body/mind/wisdom 指标。
+
+**B. 新增关联表 `report_type_indicator`**
+
+```sql
+CREATE TABLE IF NOT EXISTS report_type_indicator (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  type_id VARCHAR(64) NOT NULL COMMENT '关联 report_type_registry.type_id',
+  indicator_code VARCHAR(64) NOT NULL COMMENT '关联 indicator_definitions.code',
+  sort_order INT DEFAULT 0 COMMENT '展示/校验顺序',
+  status TINYINT(1) DEFAULT 1 COMMENT '0=停用 1=启用',
+  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  UNIQUE KEY uk_type_indicator (type_id, indicator_code),
+  INDEX idx_rti_type (type_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报告类型采集指标关联表';
+```
+
+**C. 迁移**:新增 `migrateReportTypeIndicator()` 于 `DatabaseInitializer`(建表 + 种子 DAN 指标,均幂等),末尾调用;`schema.sql` 同步追加。
+
+### 4.2 后端(Spring Boot)
+
+- 实体 `ReportTypeIndicator`(`@TableName("report_type_indicator")`)+ Mapper。
+- 服务 `ReportTypeIndicatorService`:
+  - `List<IndicatorDefinition> listByType(String typeId)`:按 `sort_order` 关联目录返回(过滤停用)。
+  - `saveForType(String typeId, List<String> codes)`:`@Transactional` 先删后插(整表替换该类型的清单)。
+  - `Map<String,Object> completeness(String typeId, String sourceTable, Long sourceId)`:见 4.5。
+- `ReportParserAdminController` 新增端点(沿用 `/api/admin/report-parser` 前缀与 `Result`):
+  - `POST /indicators/list` `{typeId}` → 已绑定指标(含目录 name/unit)。
+  - `POST /indicators/save` `{typeId, indicatorCodes:[...]}` → 整表替换。
+  - `POST /indicators/completeness` `{typeId, sourceTable, sourceId}`(或 `{summaryId}`,服务端据此解析 type_id 与数据源)→ 完整度:`{expected[], collected[], missing[], extra[]}`。
+  - (目录选择复用既有 `/api/admin/indicator/list`,不新增。)
+- `LangGraphReportParseService`:`postParse` 的 body 支持附加字段;新增重载 `parseByScript(String pdfPath, List<IndicatorDefinition> indicators)`,把 `indicators:[{code,name,category,unit}]` 放入请求体(老调用保持兼容)。
+- `ReportCollectService`:非 DAN 分支在调用 `parseByScript/parseWithDetect/parseWithOpencode` 前,按 `typeId` 取清单并下传(清单为空则行为不变)。
+- `ParsedReportPayload.Indicator` 新增 `code` 字段(Jackson 向后兼容;用于按 code 归一)。
+
+### 4.3 cfc-langgraph(非 DAN 的 LLM 驱动,P2)
+
+- `app/api/report_parse.py`:`ParseRequest` 增 `indicators: Optional[list] = None`,透传给 `ReportParseAgent.parse`。
+- `app/agents/report_parse_agent.py`:当 `indicators` 非空时,在解析提示词/Schema 中注入「仅提取以下指标;未能提取的返回 null」,输出 `indicators[]` 携带 `code`。
+- 兼容性:`indicators` 缺省时行为与现状一致。
+
+### 4.4 管理端前端(cfc-web)
+
+- `ReportTypeManagement.vue`:类型行「更多」菜单新增 **「采集指标」** 对话框:
+  - 左侧候选 = `getIndicatorList()`(`/api/admin/indicator/list`,可按 domain 过滤);右侧已选(可拖拽排序)。
+  - 保存调 `saveTypeIndicators({typeId, indicatorCodes})`。
+- `reportParser.js` 新增:`getTypeIndicators`、`saveTypeIndicators`、`getCompleteness`。
+- **「报告管理」页(`ReportManagement.vue`,路由 `/report-management`,权限 `health:reports`)**:表格新增「采集完整度」列(徽标:`已采/应采`,缺失>0 标红/hover 展示缺失明细),行操作「详情」中展示「应采/已采/缺失/多余」完整清单。数据由 `getCompleteness({summaryId})` 按需拉取。**该页仅展示,不改变解析行为。**
+
+### 4.5 归一化与完整度校验
+
+**归一化(DAN 与非 DAN 统一)**:对一条解析结果,逐个已采项按顺序匹配清单:①`code` 相等 → 命中;②否则 `name`(DAN 为 `DataItem.name`;非 DAN 为 `Indicator.indicatorName`)与目录 `name` 相等 → 命中;命中则写回 `code`,并采用目录 `name` 作为规范名。未命中的已采项视为**多余**(如 `category=auto` 垃圾)。
+
+**完整度**:`应采 = 清单成员`;`已采 = 命中的清单成员`;`缺失 = 应采 − 已采`;`多余 = 已采项 − 应采`。返回计数与明细。
+
+**计算时机**:**按需计算**(管理端查询时),不写入采集流程——零风险、不改 `collect` 事务边界。
+
+**type_id 解析**(报告管理页行 → 具体类型):行来自 `report_summary`(含 `source_table/source_id/detail_route`)。
+- DAN:`source_table='dan_report_uploads'`,由 upload 的 `report_type`(A2/B4…)映射到注册表 `type_id`(`dan_a2`/`dan_b4`…)。
+- 非 DAN:由 `report_summary` 的类型信息(或 `type_id` 列/关联草稿)解析到注册表 `type_id`。
+
+**数据源**:
+- DAN:`dan_report_uploads.parsed_items`(JSON `DataItem[]`)。
+- 非 DAN:草稿 `health_report_drafts.payload`(JSON `Payload.indicators`)或对应已入库报告的指标。
+- 清单为空 → 直接返回 `{expected:[], collected:[], missing:[], extra:[]}`,不报错。
+
+## 5. 数据流
+
+```
+[管理端] 报告类型 → 采集指标对话框 → /indicators/save → report_type_indicator
+                                                    ↑ 引用 indicator_definitions.code
+[采集] Draft.collect
+  ├─ family=dan  → Java 解析(不变)
+  └─ 非 dan      → 取清单 → LangGraph /report/parse(+indicators) → LLM 按清单抽取
+                        → 归一化(补 code / 对齐 name)
+[管理端] 报告管理页 → /indicators/completeness → 归一化对比 → 应采/已采/缺失/多余
+```
+
+## 6. 错误处理
+
+- 清单为空:解析与现状完全一致(不注入、不校验),不报错。
+- 指标 code 在目录中不存在(被删):`save` 拒绝或忽略,`completeness` 记为无法解析并告警日志。
+- LangGraph 不支持 `indicators`(旧版本):服务端兼容缺省;若调用失败按现有回退链处理,不新增失败点。
+- 迁移:沿用 `DatabaseInitializer` 幂等模式(WARN 非错误)。
+
+## 7. 测试策略
+
+- 后端单测:`saveForType` 整表替换语义;`completeness` 四种计数(应采/已采/缺失/多余)边界(空清单、全缺失、含垃圾项)。
+- 迁移:重复启动应用,确认建表/种子幂等、无 ERROR。
+- DAN 回归:对 5 份样本上传调用 `/indicators/completeness`,验证能识别出 `growthMindset` 缺失、A2 `auto` 多余(即"缺陷可被观测")。
+- LangGraph:pytest——传入 `indicators` 时输出含 `code`;不传时行为不变。
+- 前端:`npm run build` 通过;对话框保存后回显正确。
+
+## 8. 交付计划(P1 + P2 同属本期)
+
+- **P1 定义 + 校验(DAN 零改动)**:数据模型(表 + 种子)、后端实体/服务/端点(`indicators/list|save`、`completeness`)、`ReportTypeManagement.vue` 采集指标对话框、`ReportManagement.vue` 完整度列/明细。**不含解析行为变更**。
+- **P2 驱动 LLM 解析(本期)**:`cfc-langgraph` `indicators` 注入 + `LangGraphReportParseService`/`ReportCollectService` 下传清单 + 归一化写入 `code`。
+- **P3 目标态(另议,本期不做)**:DAN 解析器改为读清单驱动抽取(需评估回归)。
+
+## 9. 风险与待决
+
+- **匹配脆弱性**:归一化依赖 name 完全一致;种子 `name` 必须逐字对齐解析器输出。若不齐,会误报"缺失/多余"。缓解:种子以解析器实测输出为准生成。
+- **P3 回归风险**:DAN 换驱动 = 大改;本期不做。
+- **指标目录语义**:DAN 指标放入 `domain='dan'`,与 body/mind/wisdom 隔离。
+- **已决**:完整度展示在「报告管理」页(`/report-management`);P2 纳入本期交付(P1+P2 一并实现)。