Initial commit: 健康管家 AI 健康陪伴助手

- 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
This commit is contained in:
MingNian
2026-06-02 11:11:29 +08:00
commit 14d7c30d3d
144 changed files with 11436 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
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);