- Backend: .NET 10 Minimal API + EF Core + PostgreSQL - Frontend: Flutter + Riverpod + GoRouter + Dio - AI: DeepSeek LLM + Qwen VLM (OpenAI-compatible) - Auth: SMS + JWT (access/refresh tokens) - Features: AI chat, health tracking, medication management, diet analysis, exercise plans, doctor consultations, report analysis
133 lines
5.8 KiB
C#
133 lines
5.8 KiB
C#
using Health.Domain.Entities;
|
|
using Health.Domain.Enums;
|
|
using Health.Infrastructure.Data;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Health.WebApi.Endpoints;
|
|
|
|
/// <summary>
|
|
/// 健康数据 API 端点
|
|
/// </summary>
|
|
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<HealthMetricType>(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<string, object?>();
|
|
|
|
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<HealthMetricType>(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);
|