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

357 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 报告模块 — 实施文档
## 一、现状与差距
| 层级 | 当前状态 | 目标状态 |
|------|---------|---------|
| 后端 `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 端点:
```csharp
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 底部):
```csharp
public sealed record CreateReportRequest(string FileUrl, ReportFileType FileType, ReportCategory? Category);
```
### 3.2 report_agent_handler.cs — 实现 analyze_report
```csharp
// 改为依赖注入 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 改动
`ExecuteToolCall``analyze_report` 需要传入 `VisionClient`。改动 `MapAiChatEndpoints` 方法签名的 lambda 中,把 `visionClient` 注入并传入 Handler
```csharp
// 当前:
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 核心逻辑
```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``_mockAnalysis``Future.delayed`
- `build()``state.reports` 为空时调 `loadReports()`
- 上传按钮用真实文件选择 + API 调用
- 列表项点击 → pushRoute + 展示 AI 结果
进度显示(上传→分析中):
```dart
// 替换原来的 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.currentAnalysis`Mock → 真实 API 返回)
- 底部增加医生审核占位区域:
```dart
// 医生审核占位区(固定位置)
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 个指标:
```dart
final indicators = [
{'name': '红细胞 (RBC)', 'value': '4.68', ...},
...
];
```
改为从 `ReportAnalysis.indicators` 动态渲染,数据结构对齐后端 `AiIndicators` JSONB 字段:
```json
[{ "name": "白细胞计数", "value": "7.5", "unit": "×10^9/L", "range": "4.0-10.0", "status": "normal" }]
```
## 五、状态模型定义
```dart
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 字段
- ❌ 审核完成推送通知
- ❌ 报告分享/导出功能