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:
@@ -18,13 +18,11 @@ public static class HealthDataAgentHandler
|
||||
|
||||
public static List<ToolDefinition> Tools => [RecordHealthDataTool, CommonAgentHandler.QueryHealthRecordsTool];
|
||||
|
||||
public static async Task<object> Execute(string toolName, JsonElement args, AppDbContext db, Guid userId, IHealthRecordService? healthRecords = null)
|
||||
public static async Task<object> Execute(string toolName, JsonElement args, Guid userId, IHealthRecordService healthRecords)
|
||||
{
|
||||
return toolName switch
|
||||
{
|
||||
"record_health_data" => healthRecords == null
|
||||
? await ExecuteRecordHealthData(db, userId, args)
|
||||
: await ExecuteRecordHealthData(healthRecords, userId, args),
|
||||
"record_health_data" => await ExecuteRecordHealthData(healthRecords, userId, args),
|
||||
_ => new { success = false, message = $"未知工具: {toolName}" }
|
||||
};
|
||||
}
|
||||
@@ -62,63 +60,6 @@ public static class HealthDataAgentHandler
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<object> ExecuteRecordHealthData(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var type = args.TryGetProperty("type", out var t) ? t.GetString()! : "";
|
||||
var record = new HealthRecord
|
||||
{
|
||||
Id = Guid.NewGuid(), UserId = userId, Source = HealthRecordSource.AiEntry,
|
||||
RecordedAt = args.TryGetProperty("recorded_at", out var ra) && ra.TryGetDateTime(out var dt) ? dt : DateTime.UtcNow,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "blood_pressure":
|
||||
record.MetricType = HealthMetricType.BloodPressure;
|
||||
record.Systolic = args.TryGetProperty("systolic", out var s) ? s.GetInt32() : null;
|
||||
record.Diastolic = args.TryGetProperty("diastolic", out var d) ? d.GetInt32() : null;
|
||||
record.Unit = "mmHg";
|
||||
record.IsAbnormal = record.Systolic >= 140 || record.Diastolic >= 90 || record.Systolic <= 89 || record.Diastolic <= 59;
|
||||
break;
|
||||
case "heart_rate":
|
||||
record.MetricType = HealthMetricType.HeartRate;
|
||||
record.Value = args.TryGetProperty("heart_rate", out var hr) ? hr.GetDecimal() : null;
|
||||
record.Unit = "次/分";
|
||||
record.IsAbnormal = record.Value > 100 || record.Value < 60;
|
||||
break;
|
||||
case "glucose":
|
||||
record.MetricType = HealthMetricType.Glucose;
|
||||
record.Value = args.TryGetProperty("glucose", out var g) ? g.GetDecimal() : null;
|
||||
record.Unit = "mmol/L";
|
||||
record.IsAbnormal = record.Value >= 7.0m || record.Value <= 3.8m;
|
||||
break;
|
||||
case "spo2":
|
||||
record.MetricType = HealthMetricType.SpO2;
|
||||
record.Value = args.TryGetProperty("spo2", out var o) ? o.GetDecimal() : null;
|
||||
record.Unit = "%";
|
||||
record.IsAbnormal = record.Value <= 94;
|
||||
break;
|
||||
case "weight":
|
||||
record.MetricType = HealthMetricType.Weight;
|
||||
record.Value = args.TryGetProperty("weight", out var w) ? w.GetDecimal() : null;
|
||||
record.Unit = "kg";
|
||||
record.IsAbnormal = false;
|
||||
break;
|
||||
default:
|
||||
return new { success = false, message = $"未知指标类型: {type}" };
|
||||
}
|
||||
|
||||
db.HealthRecords.Add(record);
|
||||
await db.SaveChangesAsync();
|
||||
var valStr = record.MetricType switch
|
||||
{
|
||||
HealthMetricType.BloodPressure => $"{record.Systolic}/{record.Diastolic}",
|
||||
_ => record.Value?.ToString() ?? ""
|
||||
};
|
||||
return new { success = true, record_id = record.Id, type = record.MetricType.ToString(), value = valStr, unit = record.Unit, isAbnormal = record.IsAbnormal };
|
||||
}
|
||||
|
||||
private static (HealthMetricType? Type, decimal? Value, string Unit, int? Systolic, int? Diastolic) ParseMetric(string type, JsonElement args)
|
||||
{
|
||||
return type switch
|
||||
|
||||
@@ -71,7 +71,8 @@ public static class MedicationAgentHandler
|
||||
var times = ReadTimes(args);
|
||||
var durationDays = args.TryGetProperty("duration_days", out var dd) ? dd.GetInt32() : 0;
|
||||
var startDate = ReadStartDate(args);
|
||||
var endDate = durationDays > 0 ? startDate.AddDays(durationDays) : (DateOnly?)null;
|
||||
// 结束日为包含式(IsActiveOn 用 EndDate >= date 判断),故 N 天疗程结束日 = 起始日 + (N-1)
|
||||
var endDate = durationDays > 0 ? startDate.AddDays(durationDays - 1) : (DateOnly?)null;
|
||||
|
||||
var medicationId = await medications.CreateAsync(userId, new MedicationUpsertRequest(
|
||||
name,
|
||||
|
||||
@@ -31,7 +31,7 @@ public sealed class AiToolExecutionService(
|
||||
var root = json.RootElement;
|
||||
return await (toolName switch
|
||||
{
|
||||
"record_health_data" => HealthDataAgentHandler.Execute(toolName, root, _db, userId, _healthRecords),
|
||||
"record_health_data" => HealthDataAgentHandler.Execute(toolName, root, userId, _healthRecords),
|
||||
"query_health_records" => CommonAgentHandler.Execute(toolName, root, userId, _healthRecords, _healthArchives, ct),
|
||||
"check_archive" => CommonAgentHandler.Execute(toolName, root, userId, _healthRecords, _healthArchives, ct),
|
||||
"manage_medication" => MedicationAgentHandler.Execute(toolName, root, _db, userId, _medications, ct),
|
||||
|
||||
Reference in New Issue
Block a user