namespace Health.Application.Calendars; public sealed class CalendarService(ICalendarRepository calendar) : ICalendarService { private readonly ICalendarRepository _calendar = calendar; public async Task> GetMonthAsync(Guid userId, int year, int month, CancellationToken ct) { var start = new DateOnly(year, month, 1); var end = start.AddMonths(1); var snapshot = await _calendar.GetSnapshotAsync(userId, start, end, ct); var result = new List(); for (var date = start; date < end; date = date.AddDays(1)) { var entries = BuildEntries(snapshot, date); if (entries.Count == 0) continue; result.Add(new { date = date.ToString("yyyy-MM-dd"), events = entries.Select(entry => entry.Type).Distinct().ToList(), details = entries.Select(entry => entry.Value).ToList() }); } return result; } public async Task GetDayAsync(Guid userId, DateOnly date, CancellationToken ct) { var snapshot = await _calendar.GetSnapshotAsync(userId, date, date.AddDays(1), ct); var medications = snapshot.Medications .Where(m => IsMedicationActiveOn(m, date)) .Select(m => new { m.Name, m.Dosage, timeOfDay = m.TimeOfDay.Select(t => t.ToString(@"hh\:mm")).ToList() }) .ToList(); var exercises = snapshot.ExercisePlans .SelectMany(p => p.Items) .Where(i => i.ScheduledDate == date && !i.IsRestDay) .Select(i => new { type = i.ExerciseType, duration = i.DurationMinutes, isCompleted = i.IsCompleted, scheduledDate = i.ScheduledDate }) .ToList(); var followUps = snapshot.FollowUps .Where(f => BeijingDate(f.ScheduledAt) == date) .Select(f => new { f.Title, f.DoctorName, f.Department, status = f.Status.ToString() }) .ToList(); return new { medications, exercises, followUps }; } private static List BuildEntries(CalendarDataSnapshot snapshot, DateOnly date) { var entries = new List(); foreach (var medication in snapshot.Medications.Where(m => IsMedicationActiveOn(m, date))) { entries.Add(new CalendarEntry("medication", new { type = "medication", name = medication.Name, dosage = medication.Dosage, timeOfDay = medication.TimeOfDay.Select(t => t.ToString(@"hh\:mm")).ToList() })); } foreach (var plan in snapshot.ExercisePlans) { foreach (var item in plan.Items.Where(i => i.ScheduledDate == date && !i.IsRestDay)) entries.Add(new CalendarEntry("exercise", new { type = "exercise", name = item.ExerciseType, duration = item.DurationMinutes, isCompleted = item.IsCompleted, scheduledDate = item.ScheduledDate })); } foreach (var followUp in snapshot.FollowUps.Where(f => BeijingDate(f.ScheduledAt) == date)) { entries.Add(new CalendarEntry("followup", new { type = "followup", title = followUp.Title, doctorName = followUp.DoctorName, department = followUp.Department, status = followUp.Status.ToString() })); } return entries; } private static bool IsMedicationActiveOn(Health.Domain.Entities.Medication medication, DateOnly date) => medication.IsActive && medication.TimeOfDay.Count > 0 && (medication.StartDate == null || medication.StartDate <= date) && (medication.EndDate == null || medication.EndDate >= date); private static DateOnly BeijingDate(DateTime value) { // EF 从 timestamptz 读取的是 UTC;单元测试及旧内存对象可能是 Unspecified。 var beijing = value.Kind == DateTimeKind.Unspecified ? value : value.ToUniversalTime().AddHours(8); return DateOnly.FromDateTime(beijing); } private sealed record CalendarEntry(string Type, object Value); }