feat: 引入 EF Core Migrations + 后台任务/校验修复 + 前端四态

后端
- 改用 EF Core Migrations(InitialCreate),Program.cs 用 MigrateAsync + AUTO_MIGRATE 开关 + 显式 MigrationsAssembly;移除 EnsureCreated、删除手写 DatabaseSchemaMigrator
- 修复后台任务永久卡 Processing:三个队列对超时且达上限的任务原子标记 Failed
- 修复报告重试失效:基础设施异常向上抛激活 RetryAsync,停机取消不计失败
- 修复 AI 用药天数 off-by-one(结束日为包含式)
- AI 报告日志不再记录 VLM 健康正文
- 新增统一输入校验(ValidationException + 中间件映射 400):血压/心率/血糖/血氧/体重范围、药名/日期/服药时间去重、饮食热量与评分、运动时长上限
- 删除健康数据 AI 录入的影子直写路径,统一走 Service 校验

前端
- 新增 AppErrorState / AppFutureView,用药列表、服药打卡、运动计划、随访列表改为 加载/失败/空/数据 四态,失败可重试

瘦身
- 删除过时的 DataSeeder / DevDataSeeder 及调用
- 删除依赖实时服务的 ai_agent_tests 集成测试
This commit is contained in:
MingNian
2026-06-21 21:04:40 +08:00
parent aa44d6f0f0
commit b57d0d16f4
30 changed files with 4377 additions and 752 deletions

View File

@@ -1,4 +1,5 @@
using Health.Application.Reports;
using Health.Application.Notifications;
using Health.Infrastructure.AI;
using Microsoft.Extensions.Logging;
@@ -8,11 +9,13 @@ public sealed class ReportAnalysisService(
IReportRepository reports,
VisionClient vision,
DeepSeekClient llm,
IUserNotificationProducer notifications,
ILogger<ReportAnalysisService> logger) : IReportAnalysisService
{
private readonly IReportRepository _reports = reports;
private readonly VisionClient _vision = vision;
private readonly DeepSeekClient _llm = llm;
private readonly IUserNotificationProducer _notifications = notifications;
private readonly ILogger<ReportAnalysisService> _logger = logger;
public async Task AnalyzeAsync(ReportAnalysisJob job, CancellationToken ct)
@@ -32,6 +35,7 @@ public sealed class ReportAnalysisService(
report.AiIndicators = "[]";
report.Status = ReportStatus.AnalysisFailed;
await _reports.SaveChangesAsync(ct);
await NotifyAsync(report, job.TaskId, false, ct);
return;
}
@@ -44,8 +48,14 @@ public sealed class ReportAnalysisService(
report.Status = ReportStatus.PendingDoctor;
await _reports.SaveChangesAsync(ct);
await NotifyAsync(report, job.TaskId, true, ct);
_logger.LogInformation("报告 AI 分析完成: {ReportId}", job.ReportId);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
// 进程停机导致的取消:不算失败,不消耗重试,交还给恢复机制
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "报告 AI 分析失败: {ReportId}", job.ReportId);
@@ -53,6 +63,10 @@ public sealed class ReportAnalysisService(
report.AiSummary = "AI 暂时无法完成解读,可能是图片不清晰或服务暂时不可用。请稍后重试,或重新上传更清晰的报告图片。";
report.AiIndicators = "[]";
await _reports.SaveChangesAsync(ct);
// 重新抛出,使 Worker 进入 RetryAsync指数退避重试超限后任务置 Failed
// 若为可恢复的瞬时故障,重试成功会把状态改回 PendingDoctor。
throw;
}
}
@@ -91,7 +105,8 @@ public sealed class ReportAnalysisService(
ct: ct);
var content = response.Choices?.FirstOrDefault()?.Message?.Content ?? "";
_logger.LogInformation("报告 VLM 返回: {Content}", content[..Math.Min(content.Length, 200)]);
// 不记录 VLM 返回正文:可能包含检验指标等敏感医疗信息,生产日志只记录长度
_logger.LogInformation("报告 VLM 返回长度: {Length}", content.Length);
var json = TryParseJson(content);
return json ?? throw new InvalidOperationException("报告识别结果格式异常");
@@ -162,4 +177,17 @@ public sealed class ReportAnalysisService(
try { return JsonDocument.Parse(content).RootElement; }
catch (JsonException) { return null; }
}
private Task NotifyAsync(Report report, Guid sourceId, bool succeeded, CancellationToken ct) =>
_notifications.EnqueueAsync(
report.UserId,
sourceId,
new NotificationMessage(
succeeded ? "ReportAnalysisCompleted" : "ReportAnalysisFailed",
succeeded ? "报告分析完成" : "报告分析未完成",
succeeded ? "您的报告已完成 AI 预解读,点击查看分析结果。" : "这份报告暂时无法完成分析,您可以进入报告页面重新尝试。",
succeeded ? "success" : "warning",
"report",
report.Id.ToString()),
ct);
}