using Health.Domain.Entities; using Health.Domain.Enums; using Health.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace Health.WebApi.Endpoints; /// /// 健康数据 API 端点 /// public static class HealthEndpoints { public static void MapHealthEndpoints(this WebApplication app) { var group = app.MapGroup("/api/health-records").RequireAuthorization(); // 查询健康记录 group.MapGet("/", async ( string? type, int? days, HttpContext http, AppDbContext db, CancellationToken ct) => { var userId = GetUserId(http); var query = db.HealthRecords.Where(r => r.UserId == userId); if (!string.IsNullOrEmpty(type) && Enum.TryParse(type, ignoreCase: true, out var mt)) query = query.Where(r => r.MetricType == mt); if (days.HasValue) query = query.Where(r => r.RecordedAt >= DateTime.UtcNow.AddDays(-days.Value)); var records = await query.OrderByDescending(r => r.RecordedAt).Take(100) .Select(r => new { r.Id, Type = r.MetricType.ToString(), r.Systolic, r.Diastolic, r.Value, r.Unit, Source = r.Source.ToString(), r.IsAbnormal, r.RecordedAt }).ToListAsync(ct); return Results.Ok(new { code = 0, data = records, message = (string?)null }); }); // 新增健康记录 group.MapPost("/", async (CreateHealthRecordRequest req, HttpContext http, AppDbContext db, CancellationToken ct) => { var userId = GetUserId(http); var record = new HealthRecord { Id = Guid.NewGuid(), UserId = userId, MetricType = req.Type, Systolic = req.Systolic, Diastolic = req.Diastolic, Value = req.Value, Unit = req.Unit, Source = req.Source, RecordedAt = req.RecordedAt ?? DateTime.UtcNow, IsAbnormal = CheckAbnormal(req), }; db.HealthRecords.Add(record); await db.SaveChangesAsync(ct); return Results.Ok(new { code = 0, data = new { record.Id }, message = (string?)null }); }); // 修改健康记录 group.MapPut("/{id:guid}", async (Guid id, CreateHealthRecordRequest req, HttpContext http, AppDbContext db, CancellationToken ct) => { var userId = GetUserId(http); var record = await db.HealthRecords.FirstOrDefaultAsync(r => r.Id == id && r.UserId == userId, ct); if (record == null) return Results.Ok(new { code = 40004, data = (object?)null, message = "记录不存在" }); record.Systolic = req.Systolic; record.Diastolic = req.Diastolic; record.Value = req.Value; record.Unit = req.Unit; record.RecordedAt = req.RecordedAt ?? record.RecordedAt; record.IsAbnormal = CheckAbnormal(req); await db.SaveChangesAsync(ct); return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null }); }); // 获取各指标最新值 group.MapGet("/latest", async (HttpContext http, AppDbContext db, CancellationToken ct) => { var userId = GetUserId(http); var types = new[] { HealthMetricType.BloodPressure, HealthMetricType.HeartRate, HealthMetricType.Glucose, HealthMetricType.SpO2, HealthMetricType.Weight }; var result = new Dictionary(); foreach (var t in types) { var latest = await db.HealthRecords .Where(r => r.UserId == userId && r.MetricType == t) .OrderByDescending(r => r.RecordedAt) .FirstOrDefaultAsync(ct); result[t.ToString()] = latest == null ? null : new { latest.Systolic, latest.Diastolic, latest.Value, latest.Unit, latest.RecordedAt }; } return Results.Ok(new { code = 0, data = result, message = (string?)null }); }); // 趋势数据 group.MapGet("/trend", async (string type, int period, HttpContext http, AppDbContext db, CancellationToken ct) => { var userId = GetUserId(http); if (!Enum.TryParse(type, ignoreCase: true, out var mt)) return Results.Ok(new { code = 40001, data = (object?)null, message = "不支持的指标类型" }); var days = period switch { 7 => 7, 30 => 30, 90 => 90, _ => 7 }; var records = await db.HealthRecords .Where(r => r.UserId == userId && r.MetricType == mt && r.RecordedAt >= DateTime.UtcNow.AddDays(-days)) .OrderBy(r => r.RecordedAt) .Select(r => new { r.Id, r.Systolic, r.Diastolic, r.Value, r.IsAbnormal, r.RecordedAt }) .ToListAsync(ct); return Results.Ok(new { code = 0, data = records, message = (string?)null }); }); } private static bool CheckAbnormal(CreateHealthRecordRequest req) => req.Type switch { HealthMetricType.BloodPressure => req.Systolic >= 140 || req.Diastolic >= 90 || req.Systolic <= 89 || req.Diastolic <= 59, HealthMetricType.HeartRate => req.Value > 100 || req.Value < 60, HealthMetricType.Glucose => req.Value >= 7.0m || req.Value <= 3.8m, HealthMetricType.SpO2 => req.Value <= 94, _ => false }; private static Guid GetUserId(HttpContext http) => Guid.TryParse(http.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value, out var id) ? id : Guid.Empty; } public sealed record CreateHealthRecordRequest( HealthMetricType Type, int? Systolic, int? Diastolic, decimal? Value, string? Unit, HealthRecordSource Source, DateTime? RecordedAt);