using Health.Domain; using Health.Domain.Entities; namespace Health.Application.Exercises; public sealed class ExerciseService(IExerciseRepository exercises) : IExerciseService { private const int MaxDurationMinutes = 1440; // 单次运动时长上限 24 小时 private static readonly TimeOnly DefaultReminderTime = new(19, 0); private readonly IExerciseRepository _exercises = exercises; public async Task GetCurrentAsync(Guid userId, CancellationToken ct) { var today = BeijingToday(); var plans = await _exercises.ListActiveOnAsync(userId, today, ct); var plan = plans.FirstOrDefault(p => p.Items.Any(i => i.ScheduledDate == today)); return plan == null ? null : ToDto(plan); } public async Task CreateAsync(Guid userId, ExercisePlanCreateRequest request, CancellationToken ct) { ValidateDuration(request.DurationMinutes); var startDate = request.StartDate; var endDate = request.EndDate < startDate ? startDate : request.EndDate; if (endDate.DayNumber - startDate.DayNumber > 365) endDate = startDate.AddDays(365); var exerciseType = string.IsNullOrWhiteSpace(request.ExerciseType) ? "运动" : request.ExerciseType.Trim(); var duration = request.DurationMinutes > 0 ? request.DurationMinutes : 30; var plan = NewPlan(userId, startDate, endDate, request.ReminderTime); for (var date = startDate; date <= endDate; date = date.AddDays(1)) { plan.Items.Add(new ExercisePlanItem { Id = Guid.NewGuid(), ScheduledDate = date, ExerciseType = exerciseType, DurationMinutes = duration, IsRestDay = false, }); } await SaveNewAsync(plan, ct); return plan.Id; } // 时长上限校验:下限交由各方法兜底为 30,这里只拦截不合理的超大值 private static void ValidateDuration(int durationMinutes) { if (durationMinutes > MaxDurationMinutes) throw new ValidationException($"单次运动时长不能超过 {MaxDurationMinutes} 分钟(24 小时),当前值 {durationMinutes} 不合理"); } public async Task> ListAsync(Guid userId, CancellationToken ct) { var plans = await _exercises.ListAsync(userId, 20, ct); return plans.Select(ToSummaryDto).ToList(); } public async Task> ListAllAsync(Guid userId, CancellationToken ct) { var plans = await _exercises.ListAllAsync(userId, ct); return plans.Select(ToSummaryDto).ToList(); } public async Task GetByIdAsync(Guid userId, Guid planId, CancellationToken ct) { var plan = await _exercises.GetOwnedPlanAsync(userId, planId, ct); return plan == null ? null : ToDto(plan); } public async Task DeleteAsync(Guid userId, Guid planId, CancellationToken ct) { var plan = await _exercises.GetOwnedPlanAsync(userId, planId, ct); if (plan == null) return false; _exercises.Delete(plan); await _exercises.SaveChangesAsync(ct); return true; } public async Task ToggleCheckInAsync(Guid userId, Guid itemId, CancellationToken ct) { var item = await _exercises.GetOwnedItemAsync(userId, itemId, ct); if (item == null) return null; if (item.ScheduledDate != BeijingToday()) throw new ValidationException("只能打卡今天的运动任务"); if (item.IsRestDay) throw new ValidationException("休息日无需打卡"); item.IsCompleted = !item.IsCompleted; item.CompletedAt = item.IsCompleted ? DateTime.UtcNow : null; item.Plan.UpdatedAt = DateTime.UtcNow; await _exercises.SaveChangesAsync(ct); return item.IsCompleted; } public async Task CheckInAsync(Guid userId, Guid itemId, CancellationToken ct) { var item = await _exercises.GetOwnedItemAsync(userId, itemId, ct); if (item == null) return false; if (item.ScheduledDate != BeijingToday()) throw new ValidationException("只能打卡今天的运动任务"); if (item.IsRestDay) throw new ValidationException("休息日无需打卡"); item.IsCompleted = true; item.CompletedAt = DateTime.UtcNow; item.Plan.UpdatedAt = DateTime.UtcNow; await _exercises.SaveChangesAsync(ct); return true; } private async Task SaveNewAsync(ExercisePlan plan, CancellationToken ct) { await _exercises.AddAsync(plan, ct); await _exercises.SaveChangesAsync(ct); } private static ExercisePlan NewPlan(Guid userId, DateOnly startDate, DateOnly endDate, TimeOnly? reminderTime) => new() { Id = Guid.NewGuid(), UserId = userId, StartDate = startDate, EndDate = endDate, ReminderTime = reminderTime ?? DefaultReminderTime, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, }; private static ExercisePlanDto ToDto(ExercisePlan plan) => new( plan.Id, plan.StartDate, plan.EndDate, plan.ReminderTime, plan.CreatedAt, plan.UpdatedAt, plan.Items.OrderBy(i => i.ScheduledDate).Select(ToItemDto).ToList()); private static ExercisePlanSummaryDto ToSummaryDto(ExercisePlan plan) => new( plan.Id, plan.StartDate, plan.EndDate, plan.ReminderTime, plan.CreatedAt, plan.Items.Count, plan.Items.Count(i => i.IsCompleted), plan.Items.OrderBy(i => i.ScheduledDate).Select(ToItemDto).ToList()); private static ExercisePlanItemDto ToItemDto(ExercisePlanItem item) => new( item.Id, item.ScheduledDate, item.ExerciseType, item.DurationMinutes, item.IsCompleted, item.CompletedAt, item.IsRestDay); private static DateOnly BeijingToday() => DateOnly.FromDateTime(DateTime.UtcNow.AddHours(8)); }