Files
AI-Health/健康管家-报告模块实施文档.md
MingNian c2399b952f refactor: 4层架构重构 + 饮食VLM接入 + 多项修复
- 后端: remaining_endpoints拆分为6个独立文件
- 后端: AI Agent Handler从ai_chat_endpoints抽取为7个独立处理器
- 后端: 食物识别prompt改为输出结构化JSON
- 前端: 饮食识别从Mock替换为真实VLM API调用
- 前端: 首页图片上传URL修复(/api/upload→/api/files/upload)
- 前端: 拍饮食按钮导航到独立DietCapturePage
- 前端: 删除无用agent_bar.dart
- 前端: 修复widget_test.dart过期属性名
- 前端: 恢复ServicePackageCard和详情页
- 新增6份实施文档(情况/问诊/报告/建档/日历/视觉统一)
2026-06-03 23:17:37 +08:00

12 KiB
Raw Blame History

报告模块 — 实施文档

一、现状与差距

层级 当前状态 目标状态
后端 report_endpoints.cs 只有 GET 列表/详情,无创建接口 新增 POST 创建报告
后端 report_agent_handler.cs analyze_report 工具是空壳 接入 VisionClient 分析报告图片
前端 report_pages.dart 全 Mock假数据 + Future.delayed 接真实 API 上传→分析→展示
前端 ai_analysis_page.dart 硬编码 4 个指标 展示 AI 返回的真实指标
医生审核 不存在 留占位,等医生后台开发后再接

二、完整流程(带医生审核占位)

患者上传报告(拍照/相册/PDF
       │
       ▼
① 上传文件 → POST /api/files/upload → 得到 fileUrl
       │
       ▼
② 创建报告 → POST /api/reports → 得到 reportId
       │
       ▼
③ AI 预解读 → SSE /api/ai/report/chat → 流式返回指标+摘要
       │
       ▼
④ 展示结果 ─┬─ 提取的指标表格(正常/偏高/偏低 标注)
            ├─ AI 摘要分析
            ├─ 🏷️ "AI预解读待医生确认"
            └─ 🔒 医生审核区(占位:"等待医生审核中..."
       │
       ▼
⑤ 医生审核(未来) → 医生 Web 后台审阅 → 状态变为 DoctorReviewed
       │
       ▼
⑥ 推送通知患者(未来)→ 患者看到"医生已确认"标记

本次实施范围:① ② ③ ④(患者侧完整闭环)
留占位:⑤ ⑥(等医生后台)

三、后端改动

3.1 report_endpoints.cs — 新增 POST

在现有文件末尾追加一个 POST 端点:

group.MapPost("/", async (CreateReportRequest req, HttpContext http, AppDbContext db, CancellationToken ct) =>
{
    var userId = GetUserId(http);
    var report = new Report
    {
        Id = Guid.NewGuid(),
        UserId = userId,
        FileUrl = req.FileUrl,
        FileType = req.FileType,
        Category = req.Category ?? ReportCategory.Other,
        Status = ReportStatus.PendingDoctor,
        CreatedAt = DateTime.UtcNow,
    };
    db.Reports.Add(report);
    await db.SaveChangesAsync(ct);
    return Results.Ok(new { code = 0, data = new { report.Id, report.Category, report.Status }, message = (string?)null });
});

新增 DTO放在 report_endpoints.cs 底部):

public sealed record CreateReportRequest(string FileUrl, ReportFileType FileType, ReportCategory? Category);

3.2 report_agent_handler.cs — 实现 analyze_report

// 改为依赖注入 VisionClient改为非 static 或通过参数传递)
// 方案:接受 VisionClient 参数
public static async Task<object> Execute(
    string toolName, JsonElement args, AppDbContext db, Guid userId,
    VisionClient? visionClient = null)
{
    return toolName switch
    {
        "analyze_report" => await ExecuteAnalyzeReport(db, userId, args, visionClient),
        "query_health_records" => await CommonAgentHandler.Execute(toolName, args, db, userId),
        _ => new { success = false, message = $"未知工具: {toolName}" }
    };
}

private static async Task<object> ExecuteAnalyzeReport(
    AppDbContext db, Guid userId, JsonElement args, VisionClient? visionClient)
{
    var imageUrl = args.TryGetProperty("image_url", out var u) ? u.GetString()! : "";
    if (string.IsNullOrEmpty(imageUrl))
        return new { success = false, message = "缺少报告图片" };

    if (visionClient == null)
        return new { success = false, message = "VLM 服务未配置" };

    var prompt = """
        你是一个医学报告解读专家。请分析以下检查报告图片:
        1. 识别报告类型(血常规/生化全项/心电图/彩超/出院小结/其他)
        2. 提取所有指标名称、数值、单位、参考范围
        3. 标注每个指标状态:normal(正常) / high(偏高) / low(偏低)
        4. 给出初步分析摘要
        5. 如果是图像类报告(彩超/CT/心电图),注明"需医生人工审阅"
        """;

    var response = await visionClient.VisionAsync(prompt, [imageUrl],
        userText: "请分析这份检查报告", ct: CancellationToken.None);

    var content = response.Choices?.FirstOrDefault()?.Message?.Content ?? "{}";

    // 保存 AI 分析结果到报告
    // 需要根据 conversation 上下文找到对应的 reportId...
    // 简化方案:直接返回分析结果,由前端保存

    return new { success = true, analysis = content };
}

3.3 ai_chat_endpoints.cs 改动

ExecuteToolCallanalyze_report 需要传入 VisionClient。改动 MapAiChatEndpoints 方法签名的 lambda 中,把 visionClient 注入并传入 Handler

// 当前:
toolResult = await ExecuteToolCall(tc.Function.Name, tc.Function.Arguments, db, userId.Value);

// 改为:
toolResult = await ExecuteToolCall(tc.Function.Name, tc.Function.Arguments,
    db, userId.Value, visionClient);

ExecuteToolCall 方法签名追加 VisionClient? visionClient = null 参数,analyze_report 分支传入。

四、前端改动

4.1 文件拆分

当前 report_pages.dart 包含 Provider + ReportListPage + ReportDetailPage535 行),拆分为:

新文件 内容
lib/providers/report_provider.dart ReportState, ReportNotifier从 report_pages.dart 迁出)
lib/pages/report/report_list_page.dart ReportListPage
lib/pages/report/report_detail_page.dart ReportDetailPage

report_pages.dart 删除,app_router.dart 的 import 更新。

4.2 report_provider.dart 核心逻辑

class ReportNotifier extends Notifier<ReportState> {

  // 初始化:从后端加载报告列表
  Future<void> loadReports() async {
    final api = ref.read(apiClientProvider);
    final res = await api.get('/api/reports');
    final list = (res.data['data'] as List?) ?? [];
    state = state.copyWith(reports: list.map(_fromApi).toList());
  }

  // 上传 + 创建报告 + AI 分析
  Future<void> uploadAndAnalyze(File file, {bool isPdf = false}) async {
    final api = ref.read(apiClientProvider);
    state = state.copyWith(isAnalyzing: true);

    try {
      // ① 上传文件
      final fileUrls = await api.uploadFile('/api/files/upload', file);
      final fileUrl = fileUrls; // 简化,实际取 data[0].url

      // ② 创建报告
      final res = await api.post('/api/reports', data: {
        'fileUrl': fileUrl,
        'fileType': isPdf ? 'Pdf' : 'Image',
        'category': 'Other',
      });
      final reportId = res.data['data']['id'];

      // ③ AI 预解读SSE 流式)
      final token = await api.accessToken;
      if (token == null) return;

      final stream = SseHandler.connect(
        agentType: 'report',
        message: fileUrl,  // 把图片 URL 作为消息传给 AI
        token: token,
      );

      String analysisContent = '';
      await for (final event in stream) {
        switch (event['action']) {
          case 'answer':
            analysisContent += event['data'] ?? '';
          case 'status':
            if (event['data'] == 'done') {
              // ④ 解析 AI 返回,更新报告
              final indicators = _parseIndicators(analysisContent);
              final summary = _parseSummary(analysisContent);
              state = state.copyWith(
                isAnalyzing: false,
                currentAnalysis: ReportAnalysis(
                  reportId: reportId,
                  reportType: '检查报告',
                  indicators: indicators,
                  summary: summary,
                ),
              );
              // 刷新列表
              loadReports();
            }
        }
      }
    } catch (e) {
      state = state.copyWith(isAnalyzing: false);
    }
  }
}

4.3 ReportListPage 改造

核心变化:

  • 删掉 _mockReports_mockAnalysisFuture.delayed
  • build()state.reports 为空时调 loadReports()
  • 上传按钮用真实文件选择 + API 调用
  • 列表项点击 → pushRoute + 展示 AI 结果

进度显示(上传→分析中):

// 替换原来的 Center(child: CircularProgressIndicator)
Column(children: [
  _progressStep('🔍 扫描报告图片...', state.step >= 0),
  _progressStep('🏷️ 识别报告类型...', state.step >= 1),
  _progressStep('📊 提取关键指标...', state.step >= 2),
  _progressStep('📝 生成解读报告...', state.step >= 3),
])

4.4 ReportDetailPage 改造

保持不变的结构,但:

  • 数据来源从 state.currentAnalysisMock → 真实 API 返回)
  • 底部增加医生审核占位区域:
// 医生审核占位区(固定位置)
Container(
  margin: EdgeInsets.only(top: 24),
  padding: EdgeInsets.all(16),
  decoration: BoxDecoration(
    color: Color(0xFFF5F5F5),
    borderRadius: BorderRadius.circular(16),
    border: Border.all(color: Color(0xFFE0E0E0)),
  ),
  child: Column(children: [
    Row(children: [
      Icon(Icons.lock_outline, size: 16, color: Color(0xFFBBBBBB)),
      SizedBox(width: 8),
      Text('医生审核', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF999999))),
    ]),
    SizedBox(height: 12),
    Text('等待医生审核中...', style: TextStyle(fontSize: 14, color: Color(0xFFBBBBBB))),
    SizedBox(height: 4),
    Text('医生审核后将推送通知给您', style: TextStyle(fontSize: 12, color: Color(0xFFCCCCCC))),
  ]),
)

4.5 AiAnalysisPage 改造

当前硬编码 4 个指标:

final indicators = [
  {'name': '红细胞 (RBC)', 'value': '4.68', ...},
  ...
];

改为从 ReportAnalysis.indicators 动态渲染,数据结构对齐后端 AiIndicators JSONB 字段:

[{ "name": "白细胞计数", "value": "7.5", "unit": "×10^9/L", "range": "4.0-10.0", "status": "normal" }]

五、状态模型定义

class ReportState {
  final List<ReportItem> reports;
  final bool isAnalyzing;
  final int analysisStep;        // 0-3进度步骤
  final ReportAnalysis? currentAnalysis;
  final String? errorMessage;

  // ...copyWith
}

class ReportItem {
  final String id;
  final String fileUrl;          // ← 真实文件 URL
  final String fileType;         // 'Image' | 'Pdf'
  final String category;         // 报告类别
  final String status;           // 'PendingDoctor' | 'DoctorReviewed'
  final DateTime createdAt;

  // ...fromApi(Map)
}

class Indicator {
  final String name;
  final String value;
  final String unit;
  final String range;
  final String status;           // 'normal' | 'high' | 'low'

  // ...fromApi(Map)
}

六、边界情况

情况 处理
上传失败 Toast 提示 "上传失败,请重试"
AI 分析超时 显示超时提示 + "可稍后在报告详情中重新分析"
VLM 无法识别(图像类报告) 标记 indicators 为空 + summary 显示 "该报告为图像类报告,需医生人工审阅"
PDF 文件 暂不支持 VLM 分析,仅保存记录 → summary 显示 "PDF报告暂不支持AI预解读"
报告列表为空 显示空状态引导 "上传你的第一份报告"

七、文件改动清单

后端:
  修改: Endpoints/report_endpoints.cs       (+ POST 端点 + DTO, ~30 行)
  修改: AI/AgentHandlers/report_agent_handler.cs  (实现 analyze_report, ~60 行)
  修改: Endpoints/ai_chat_endpoints.cs      (ExecuteToolCall 传 VisionClient, ~5 行)

前端:
  新建: providers/report_provider.dart       (~200 行)
  新建: pages/report/report_list_page.dart   (从 report_pages.dart 迁出, ~180 行)
  新建: pages/report/report_detail_page.dart (从 report_pages.dart 迁出, ~150 行)
  修改: pages/report/ai_analysis_page.dart   (去掉硬编码, ~30 行改)
  修改: core/app_router.dart                (改 import 路径, ~3 行)
  删除: pages/report/report_pages.dart      (拆到上面两个文件了)

八、本次不做(医生端依赖)

  • 医生审核 → 状态变更PendingDoctor → DoctorReviewed
  • 医生审核意见 → DoctorComment 字段
  • 审核完成推送通知
  • 报告分享/导出功能