- Android 添加相机/存储权限,拍照和相册功能可用 - AI 回复支持 Markdown 渲染(加粗/表格不再显示**乱码) - 附件按钮接线,支持拍照/相册/文件选择 - 智能体面板按钮全部接线(拍照/上传/手动录入/导航) - 侧边栏 AI 录入后自动刷新健康数据 - 运动计划页增加创建按钮 + 打卡功能 - 后端运动计划支持 AI 创建和打卡(Tool Calling) - 修复 CreateExercisePlanRequest JSON 反序列化
286 lines
15 KiB
C#
286 lines
15 KiB
C#
namespace Health.WebApi.Endpoints;
|
||
|
||
/// <summary>
|
||
/// 饮食、用药、报告、问诊、运动、文件端点
|
||
/// </summary>
|
||
public static class RemainingEndpoints
|
||
{
|
||
public static void MapDietEndpoints(this WebApplication app)
|
||
{
|
||
var group = app.MapGroup("/api/diet-records").RequireAuthorization();
|
||
|
||
group.MapGet("/", async (string? date, string? mealType, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||
{
|
||
var userId = GetUserId(http);
|
||
var query = db.DietRecords.Include(d => d.FoodItems).Where(d => d.UserId == userId);
|
||
if (DateOnly.TryParse(date, out var d)) query = query.Where(r => r.RecordedAt == d);
|
||
if (Enum.TryParse<MealType>(mealType, ignoreCase: true, out var mt)) query = query.Where(r => r.MealType == mt);
|
||
var records = await query.OrderByDescending(r => r.RecordedAt).ToListAsync(ct);
|
||
return Results.Ok(new { code = 0, data = records, message = (string?)null });
|
||
});
|
||
|
||
group.MapPost("/", async (CreateDietRequest req, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||
{
|
||
var userId = GetUserId(http);
|
||
var record = new DietRecord
|
||
{
|
||
Id = Guid.NewGuid(), UserId = userId, MealType = req.MealType,
|
||
TotalCalories = req.TotalCalories, HealthScore = req.HealthScore, RecordedAt = req.RecordedAt ?? DateOnly.FromDateTime(DateTime.Now),
|
||
};
|
||
if (req.FoodItems != null)
|
||
foreach (var fi in req.FoodItems)
|
||
record.FoodItems.Add(new DietFoodItem { Id = Guid.NewGuid(), Name = fi.Name, Portion = fi.Portion, Calories = fi.Calories, ProteinGrams = fi.ProteinGrams, CarbsGrams = fi.CarbsGrams, FatGrams = fi.FatGrams, Warning = fi.Warning, SortOrder = fi.SortOrder });
|
||
db.DietRecords.Add(record);
|
||
await db.SaveChangesAsync(ct);
|
||
return Results.Ok(new { code = 0, data = new { record.Id }, message = (string?)null });
|
||
});
|
||
|
||
group.MapDelete("/{id:guid}", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||
{
|
||
var userId = GetUserId(http);
|
||
var record = await db.DietRecords.FirstOrDefaultAsync(r => r.Id == id && r.UserId == userId, ct);
|
||
if (record != null) { db.DietRecords.Remove(record); await db.SaveChangesAsync(ct); }
|
||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||
});
|
||
}
|
||
|
||
public static void MapMedicationEndpoints(this WebApplication app)
|
||
{
|
||
var group = app.MapGroup("/api/medications").RequireAuthorization();
|
||
|
||
group.MapGet("/", async (HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||
{
|
||
var userId = GetUserId(http);
|
||
var meds = await db.Medications.Where(m => m.UserId == userId).OrderByDescending(m => m.CreatedAt).ToListAsync(ct);
|
||
return Results.Ok(new { code = 0, data = meds, message = (string?)null });
|
||
});
|
||
|
||
group.MapPost("/", async (CreateMedicationRequest req, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||
{
|
||
var userId = GetUserId(http);
|
||
var med = new Medication
|
||
{
|
||
Id = Guid.NewGuid(), UserId = userId, Name = req.Name, Dosage = req.Dosage,
|
||
Frequency = req.Frequency, TimeOfDay = req.TimeOfDay ?? [],
|
||
StartDate = req.StartDate, EndDate = req.EndDate, IsActive = true, Source = req.Source,
|
||
};
|
||
db.Medications.Add(med);
|
||
await db.SaveChangesAsync(ct);
|
||
return Results.Ok(new { code = 0, data = new { med.Id }, message = (string?)null });
|
||
});
|
||
|
||
group.MapPut("/{id:guid}", async (Guid id, CreateMedicationRequest req, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||
{
|
||
var userId = GetUserId(http);
|
||
var med = await db.Medications.FirstOrDefaultAsync(m => m.Id == id && m.UserId == userId, ct);
|
||
if (med == null) return Results.Ok(new { code = 40004, message = "不存在" });
|
||
med.Name = req.Name; med.Dosage = req.Dosage; med.Frequency = req.Frequency;
|
||
med.TimeOfDay = req.TimeOfDay ?? med.TimeOfDay; med.StartDate = req.StartDate; med.EndDate = req.EndDate;
|
||
med.UpdatedAt = DateTime.UtcNow;
|
||
await db.SaveChangesAsync(ct);
|
||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||
});
|
||
|
||
group.MapDelete("/{id:guid}", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||
{
|
||
var userId = GetUserId(http);
|
||
var med = await db.Medications.FirstOrDefaultAsync(m => m.Id == id && m.UserId == userId, ct);
|
||
if (med != null) { db.Medications.Remove(med); await db.SaveChangesAsync(ct); }
|
||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||
});
|
||
|
||
group.MapPost("/{id:guid}/confirm", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||
{
|
||
var userId = GetUserId(http);
|
||
var log = new MedicationLog
|
||
{
|
||
Id = Guid.NewGuid(), MedicationId = id, UserId = userId,
|
||
Status = MedicationLogStatus.Taken, ScheduledTime = TimeOnly.FromDateTime(DateTime.Now), ConfirmedAt = DateTime.UtcNow,
|
||
};
|
||
db.MedicationLogs.Add(log);
|
||
await db.SaveChangesAsync(ct);
|
||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||
});
|
||
}
|
||
|
||
public static void MapReportEndpoints(this WebApplication app)
|
||
{
|
||
var group = app.MapGroup("/api/reports").RequireAuthorization();
|
||
|
||
group.MapGet("/", async (HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||
{
|
||
var userId = GetUserId(http);
|
||
var reports = await db.Reports.Where(r => r.UserId == userId).OrderByDescending(r => r.CreatedAt).ToListAsync(ct);
|
||
return Results.Ok(new { code = 0, data = reports, message = (string?)null });
|
||
});
|
||
|
||
group.MapGet("/{id:guid}", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||
{
|
||
var userId = GetUserId(http);
|
||
var report = await db.Reports.FirstOrDefaultAsync(r => r.Id == id && r.UserId == userId, ct);
|
||
return report == null ? Results.Ok(new { code = 40004, message = "不存在" }) : Results.Ok(new { code = 0, data = report, message = (string?)null });
|
||
});
|
||
}
|
||
|
||
public static void MapConsultationEndpoints(this WebApplication app)
|
||
{
|
||
var group = app.MapGroup("/api").RequireAuthorization();
|
||
|
||
group.MapGet("/doctors", async (AppDbContext db) =>
|
||
{
|
||
var doctors = await db.Doctors.Where(d => d.IsActive).Select(d => new { d.Id, d.Name, d.Title, d.Department, d.Introduction }).ToListAsync();
|
||
return Results.Ok(new { code = 0, data = doctors, message = (string?)null });
|
||
});
|
||
|
||
group.MapGet("/consultations", async (HttpContext http, AppDbContext db) =>
|
||
{
|
||
var userId = GetUserId(http);
|
||
var consultations = await db.Consultations.Where(c => c.UserId == userId).OrderByDescending(c => c.CreatedAt).ToListAsync();
|
||
return Results.Ok(new { code = 0, data = consultations, message = (string?)null });
|
||
});
|
||
|
||
group.MapPost("/consultations", async (CreateConsultationRequest req, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||
{
|
||
var userId = GetUserId(http);
|
||
var consultation = new Consultation
|
||
{
|
||
Id = Guid.NewGuid(), UserId = userId, DoctorId = req.DoctorId,
|
||
Status = ConsultationStatus.AiTalking,
|
||
Month = DateTime.UtcNow.Year * 100 + DateTime.UtcNow.Month,
|
||
};
|
||
db.Consultations.Add(consultation);
|
||
await db.SaveChangesAsync(ct);
|
||
return Results.Ok(new { code = 0, data = new { consultation.Id }, message = (string?)null });
|
||
});
|
||
|
||
group.MapGet("/consultations/{id:guid}/messages", async (Guid id, string? after, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||
{
|
||
var userId = GetUserId(http);
|
||
var query = db.ConsultationMessages.Where(m => m.ConsultationId == id && m.Consultation.UserId == userId);
|
||
if (Guid.TryParse(after, out var afterId))
|
||
query = query.Where(m => m.Id.CompareTo(afterId) > 0);
|
||
var messages = await query.OrderBy(m => m.CreatedAt).Take(50).Select(m => new { m.Id, SenderType = m.SenderType.ToString(), m.SenderName, m.Content, m.CreatedAt }).ToListAsync(ct);
|
||
return Results.Ok(new { code = 0, data = messages, message = (string?)null });
|
||
});
|
||
|
||
group.MapPost("/consultations/{id:guid}/messages", async (Guid id, SendMessageRequest req, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||
{
|
||
var userId = GetUserId(http);
|
||
var msg = new ConsultationMessage { Id = Guid.NewGuid(), ConsultationId = id, SenderType = ConsultationSenderType.User, Content = req.Content, SenderName = null, CreatedAt = DateTime.UtcNow };
|
||
db.ConsultationMessages.Add(msg);
|
||
await db.SaveChangesAsync(ct);
|
||
return Results.Ok(new { code = 0, data = new { msg.Id }, message = (string?)null });
|
||
});
|
||
|
||
group.MapGet("/user/consultation-quota", async (HttpContext http, AppDbContext db) =>
|
||
{
|
||
var userId = GetUserId(http);
|
||
var now = DateTime.UtcNow;
|
||
// 用年月组合值避免跨年问题:202601=2026年1月
|
||
var currentPeriod = now.Year * 100 + now.Month;
|
||
var used = await db.Consultations.CountAsync(c => c.UserId == userId && c.Month == currentPeriod);
|
||
return Results.Ok(new { code = 0, data = new { total = 3, used, remaining = 3 - used }, message = (string?)null });
|
||
});
|
||
}
|
||
|
||
public static void MapExerciseEndpoints(this WebApplication app)
|
||
{
|
||
var group = app.MapGroup("/api/exercise-plans").RequireAuthorization();
|
||
|
||
group.MapGet("/current", async (HttpContext http, AppDbContext db) =>
|
||
{
|
||
var userId = GetUserId(http);
|
||
var today = DateOnly.FromDateTime(DateTime.Now);
|
||
var monday = today.AddDays(-(int)today.DayOfWeek + 1);
|
||
var plan = await db.ExercisePlans.Include(p => p.Items).FirstOrDefaultAsync(p => p.UserId == userId && p.WeekStartDate == monday);
|
||
if (plan == null) return Results.Ok(new { code = 0, data = (object?)null, message = (string?)null });
|
||
return Results.Ok(new
|
||
{
|
||
code = 0,
|
||
data = new
|
||
{
|
||
plan.Id, plan.WeekStartDate, plan.CreatedAt, plan.UpdatedAt,
|
||
items = plan.Items.Select(i => new
|
||
{
|
||
i.Id, i.DayOfWeek, i.ExerciseType, i.DurationMinutes,
|
||
i.IsCompleted, i.CompletedAt, i.IsRestDay
|
||
})
|
||
},
|
||
message = (string?)null
|
||
});
|
||
});
|
||
|
||
group.MapPost("/", async (CreateExercisePlanRequest req, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||
{
|
||
var userId = GetUserId(http);
|
||
var plan = new ExercisePlan { Id = Guid.NewGuid(), UserId = userId, WeekStartDate = req.WeekStartDate };
|
||
if (req.Items != null)
|
||
foreach (var item in req.Items)
|
||
plan.Items.Add(new ExercisePlanItem { Id = Guid.NewGuid(), DayOfWeek = item.DayOfWeek, ExerciseType = item.ExerciseType, DurationMinutes = item.DurationMinutes, IsRestDay = item.IsRestDay });
|
||
db.ExercisePlans.Add(plan);
|
||
await db.SaveChangesAsync(ct);
|
||
return Results.Ok(new { code = 0, data = new { plan.Id }, message = (string?)null });
|
||
});
|
||
|
||
group.MapPost("/items/{itemId:guid}/checkin", async (Guid itemId, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||
{
|
||
var item = await db.ExercisePlanItems.FindAsync([itemId], ct);
|
||
if (item == null) return Results.Ok(new { code = 40004, message = "不存在" });
|
||
item.IsCompleted = true; item.CompletedAt = DateTime.UtcNow;
|
||
await db.SaveChangesAsync(ct);
|
||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||
});
|
||
}
|
||
|
||
public static void MapFileEndpoints(this WebApplication app)
|
||
{
|
||
var group = app.MapGroup("/api/files").RequireAuthorization();
|
||
|
||
group.MapPost("/upload", async (HttpRequest request) =>
|
||
{
|
||
var form = await request.ReadFormAsync();
|
||
var files = form.Files;
|
||
var results = new List<object>();
|
||
var uploadsDir = Path.Combine(Directory.GetCurrentDirectory(), "uploads");
|
||
Directory.CreateDirectory(uploadsDir);
|
||
|
||
foreach (var file in files)
|
||
{
|
||
var fileId = Guid.NewGuid().ToString();
|
||
var ext = Path.GetExtension(file.FileName);
|
||
var filePath = Path.Combine(uploadsDir, $"{fileId}{ext}");
|
||
using var stream = new FileStream(filePath, FileMode.Create);
|
||
await file.CopyToAsync(stream);
|
||
results.Add(new { id = fileId, name = file.FileName, size = file.Length });
|
||
}
|
||
|
||
return Results.Ok(new { code = 0, data = results, message = (string?)null });
|
||
});
|
||
}
|
||
|
||
private static Guid GetUserId(HttpContext http) =>
|
||
Guid.TryParse(http.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value, out var id) ? id : Guid.Empty;
|
||
}
|
||
|
||
// ---- 请求 DTO ----
|
||
public sealed record CreateDietRequest(MealType MealType, int? TotalCalories, int? HealthScore, DateOnly? RecordedAt, List<FoodItemDto>? FoodItems);
|
||
public sealed record FoodItemDto(string Name, string? Portion, int? Calories, decimal? ProteinGrams, decimal? CarbsGrams, decimal? FatGrams, string? Warning, int SortOrder);
|
||
|
||
public sealed record CreateMedicationRequest(string Name, string? Dosage, MedicationFrequency Frequency, List<TimeOnly>? TimeOfDay, DateOnly? StartDate, DateOnly? EndDate, MedicationSource Source);
|
||
|
||
public sealed record CreateConsultationRequest(Guid DoctorId);
|
||
public sealed record SendMessageRequest(string Content);
|
||
|
||
public sealed class CreateExercisePlanRequest
|
||
{
|
||
public DateOnly WeekStartDate { get; init; }
|
||
public List<ExerciseItemDto>? Items { get; init; }
|
||
}
|
||
public sealed class ExerciseItemDto
|
||
{
|
||
public int DayOfWeek { get; init; }
|
||
public string ExerciseType { get; init; } = "";
|
||
public int DurationMinutes { get; init; }
|
||
public bool IsRestDay { get; init; }
|
||
}
|