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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user