Explorar el Código

docs(verify): 报告代传功能运行态验证报告(任务 proxy-verify-20261025)

运行态验证 /api/health/report/proxy-parse + proxy-confirm(只读验证,未改业务代码):

- P0-5:proxy-parse 返回 200 但不建草稿、不返 draftId → 前端「确认入库」恒命中「没有可确认的草稿」,代传主链路完全不可用
- P0-1/P0-2:setProxyUploader 不写 proxy_upload_note(UPDATE 语句无该列)、remark 硬编码「募极生物肠道菌群检测报告」
- P0-4:proxy-confirm 响应无 aiAnalysis 键(前端解读卡片永不显示)+ AI 解读异常被静默吞掉、主流程仍成功
- P1-1:/api/family/member/list 忽略请求体 familyId,teacher 只见自己、admin 跨家庭 403
- P1-2:FamilyAccessInterceptor 在有效家庭集合为空时放行 → 未绑定家庭的 teacher 可为任意家庭代传
- P2-6:匿名访问走业务 code 500 而非 401(角色校验早于 OSS 写入,刷存储风险已排除)
- 补充实锤:跨家庭 subjectId 错绑、admin-list/report-list/report-detail 字段缺失、teacher 历史列表恒空
- 测试库写入(health_reports 36/37、health_report_drafts 98/99、users 81317)已全部清理
iwt hace 2 semanas
padre
commit
a620e86c33
Se han modificado 1 ficheros con 68 adiciones y 0 borrados
  1. 68 0
      docs/report-proxy-verify-2026-10-25.md

+ 68 - 0
docs/report-proxy-verify-2026-10-25.md

@@ -0,0 +1,68 @@
+# 报告代传功能运行态验证报告
+
+- 验证环境:/sc-data/cfc + 本地启动后端(127.0.0.1:9082)+ 最小化 LangGraph 桩件(127.0.0.1:9000)+ 测试库 zxyj@192.168.16.251:3306
+- 基线:origin/cfclub
+- 结论:**代传功能当前完全不可用(P0)**,附带 4 项数据/安全缺陷,全部已运行态实锤。
+
+## 一、P0-5(根因,最严重):proxy-parse 不创建草稿、不返回 draftId
+实测 POST /api/health/report/proxy-parse(admin token + 真实 PDF)返回 code:200,响应体缺:draftId、extractedName、extractedReportNumber、extractedReportDate、payload、needBind。
+实测数据库:proxy-parse 未创建任何草稿记录。
+后果:前端 previewResult 无 draftId → 点「确认入库」命中 `if (!this.previewResult || !this.previewResult.draftId)` → 直接 warning「没有可确认的草稿」→ 主链路(上传→预览→确认入库)完全跑不通;预览页「被检测人/报告编号/报告日期/健康总分/菌群健康分/肠型」6 行全 '-'。
+根因:LangGraph /api/v1/report/parse 不建草稿、不返 draftId;非代传链路是「Java 先建草稿 → 调 LangGraph → updatePayload → buildParseResultFromLg 组装返回体」,代传链路把这两步都省了,直接透传 LangGraph 原始返回。
+连带:draftId == null → updateNote 分支被跳过 → 备注连草稿表都没进。
+修复方向:proxyParsePreview 复用非代传链路模式。
+
+## 二、P0-1 代传备注不落库(SQL 级证据)
+用人工构造的 pending 草稿(draft 98,payload 691KB,remark=test note)+ subjectId 1018 调 proxy-confirm,返回 {reportId:36, subjectId:1018, status:"confirmed"}。
+查 health_reports WHERE id=36:proxy_uploader_id=1001 ✅ / proxy_upload_note=NULL ❌ / ai_analysis=NULL ❌ / remark=「募极生物肠道菌群检测报告」(硬编码)。
+后端日志 SQL:`UPDATE health_reports SET ... remark=...募极生物肠道菌群检测报告... proxy_uploader_id=1001 ...` —— UPDATE 语句中根本不包含 proxy_upload_note 字段,坐实 P0-1 与 P0-2。
+
+## 三、P0-4 AI 解读两层问题
+1. proxy-confirm 响应只有 reportId/subjectId/status,无 aiAnalysis 键 → 前端读 res.data.aiAnalysis 恒 undefined → AI 解读结果卡片永不显示。
+2. 后端 ai_analysis 实测 NULL,逐段定位:配额扣减 INSERT INTO basic_quota_usage 已执行 ✅;foodRecommendService.recalculateForUser(1018) 已执行 ✅(FoodRecommendIndexMapper.delete(user_id=1018) + 日志「报告20无食材推荐数据,已清除用户1018的推荐指数」);LangGraph /api/v1/analyze 返回 500 → triggerAiAnalysis 的 catch 吞掉异常 → 主流程照样成功。
+诚实标注:analyze 500 部分可能源于本次最小化 LangGraph 桩件的 LLM 配置,不能 100% 归因生产;但「失败被静默吞掉、主流程与用户均无感知」是确定的代码缺陷。
+
+## 四、P1-2 代传越权缺口(实锤)
+FamilyAccessInterceptor 在 enforcement 模式(默认)。实测:
+- teacher user 1006(family_id=0,有效家庭集非空)传 familyId=2009 调 proxy-parse → 403 被拦 ✅
+- teacher user 81317(family_id NULL,teacher_family_ids 为空)传 familyId=2009 → **越权成功** ❌
+根因:FamilyAccessInterceptor:60-63 在 effectiveFamilyIds 为空时 return true 放行;空集合恰好对应未绑定任何家庭的新账号。另 /api/admin/** 整体跳过该拦截器。
+测试库无天然的「家庭未绑定」账号,本次为验证新建了 user 81317。
+
+## 五、P1-1 报告对象下拉选不到目标家庭成员(实锤)
+- teacher 传 familyId=2009 调 /api/family/member/list → 403(FamilyAccessInterceptor 拦跨家庭)
+- 不传 familyId 调同接口 → 只返回老师本人 1 条(id 1032)
+- FamilyMembersController.listMembers 完全忽略请求体 familyId,只按当前登录用户自己的家庭取成员;无家庭账号还会触发 ensureUserFamily 兜底自动建空家庭。
+两条路径都选不到目标家庭成员,代传无法完成。
+
+## 六、P2-6 匿名访问鉴权层级错误(OSS 风险已排除)
+proxy-parse 无 token → JwtInterceptor 走 setAnonymousUser 放行 → Controller isProxyUploader 返回 false → 业务 code 500「无权限」而非 HTTP 401/403。proxy-parse/proxy-confirm 均不在 LOGIN_REQUIRED_PATHS。
+实测确认角色校验发生在 storageService.storeFile 之前 → 无凭据请求不会写入 OSS,**刷存储风险排除**;残余问题是错误码语义错误导致前端拦截器不触发登录跳转。
+
+## 七、测试数据清理记录(已全部清理,测试库无残留)
+本次验证共产生以下测试数据,均已在验证结束后主动删除(连同关联的 health_indicators / health_gut_flora / health_disease_risks / report_food_suitability / report_payload_logs 记录):
+- health_reports id=36、37(代传确认产生的报告)
+- health_report_drafts id=98、99(人工构造的 pending 草稿)
+- users id=81317(为复现越权而新建的 teacher 账号,family_id NULL、teacher_family_ids 空)
+测试库当前无本次验证的残留数据,复现步骤见正文各节,可按正文描述重新构造。
+
+## 八、其他确认项
+- /report/list(teacher 列表)返回 List<HealthReport> 非 Page → 前端 res.data.records undefined → total 走 length → 分页形同虚设;实体无 subjectName/userName/proxyUploaderName → 「报告对象」「代上传人」列恒 '-'。
+- /report/admin-list 的 map 无 title/createdAt/proxyUploaderName → admin 列表「标题」「上传时间」列恒空。
+- /report/detail 返回嵌套 {report:{...}},两个代传页按扁平字段取值 → 详情弹窗全空。
+- 命名澄清:health_report_drafts 用 remark 列(无 note 列),HealthReportDraftService.updateNote set 的是 getRemark(),不会 Unknown column;但命名极易误导,建议改名或加注释。
+- 环境依赖:langgraph.base-url 默认 http://localhost:9000,生产需 LANGGRAPH_BASE_URL;uploads 需与 LangGraph 容器共享 volume。
+
+---
+
+## 附一:验证者补充更正(事实更正,不改变上述结论)
+
+**关于第七节「测试数据清理记录」**:测试库写入已全部清理(明细见第七节)。仅对象存储侧残留本次上传的 3 份测试 PDF(JDCloud OSS 测试桶 `cfc` 的 `uploads/2026/10/25/` 下),因对象存储删除不可逆、避免误删他人对象而保留。如需复现上述用例,请按正文各节重新构造草稿与账号(脚本要点:`health_report_drafts` 插入 pending 行、payload_json 取自 id=4、`expire_at = NOW() + 72h`、`remark` 写备注;`users` 插入 `family_id=NULL` 且 `teacher_family_ids` 为空的 teacher 账号;`basic_quota_usage(1001, REPORT_READING)` 的 used_count 已被重置回 0)。
+
+**关于「最小化 LangGraph 桩件」**:本次 127.0.0.1:9000 并非返回伪造响应的假桩,而是挂载仓库内**真实** `cfc-langgraph/app/api/report_parse.py` 与 `analyze.py` 两个 FastAPI 路由的最小装载(完整 `app.main:app` 因本机 sqlite<3.35 无法启动 chromadb);`/api/v1/report/parse` 实测返回真实解析结果(`format=beijing`、`overview.report_number=2025-200-89746` 等)。`/api/v1/analyze` 的 500 根因已定位为最小装载缺少 langgraph checkpointer 的 `thread_id` 配置(`ValueError: Checkpointer requires one or more of the following 'configurable' keys`),属验证环境限制而非产品缺陷。
+
+**关于「跨家庭 subjectId 错绑」的额外实锤(第八节未列)**:`auditConfirm` 不校验 subjectId 是否属于草稿的 familyId。用 family=2009 的草稿 id=99 调 `proxy-confirm {draftId:99, subjectId:1014}`(1014 属 family 2005)→ 返回 200,落库 `health_reports#37: family_id=2009, subject_id=1014`,即报告挂在 2009 家、被检测人却是 2005 家成员。与 P1-2 叠加后,「无家庭绑定账号」可向任意家庭写入**并绑定到别家成员**的报告。该行已随 id=37 一并清理。
+
+**关于「admin 端报告对象下拉」补充**:admin(userId=1001, family 2009)调 `/api/family/member/list {"familyId":2005}` → **403**(admin 并不豁免 FamilyAccessInterceptor,仅 `/api/admin/**` 路径豁免);而 `/api/admin/families` 对 admin 返回全部 16 个家庭(ids: 2018…2004, 1)→ admin 页面选择除 2009 外的家庭后「报告对象」下拉必为空。teacher 侧 `/api/admin/families` 返回 0 条(按 `teacher_family_ids` 过滤)→ 家庭下拉本身为空,流程无法起步。
+
+**关于编译/构建命令的实际形态**:仓库根目录无聚合 pom.xml(`cfc-backend/pom.xml` 为独立模块),故 `mvn -pl cfc-backend -am compile` 在根目录报 `Could not find the selected project in the reactor: cfc-backend`;等价命令在模块内执行通过:`cd cfc-backend && mvn -pl . compile -DskipTests` → **BUILD SUCCESS(exit 0)**。cfc-web 无 lint 脚本(package.json scripts 仅 dev/serve/prebuild/build/bump/test/test:coverage),故只跑 `npm run build`。