144 lines
5.9 KiB
C#
144 lines
5.9 KiB
C#
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<ExercisePlanDto?> 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<Guid> 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<IReadOnlyList<ExercisePlanSummaryDto>> ListAsync(Guid userId, CancellationToken ct)
|
||
{
|
||
var plans = await _exercises.ListAsync(userId, 20, ct);
|
||
return plans.Select(ToSummaryDto).ToList();
|
||
}
|
||
|
||
public async Task<IReadOnlyList<ExercisePlanSummaryDto>> ListAllAsync(Guid userId, CancellationToken ct)
|
||
{
|
||
var plans = await _exercises.ListAllAsync(userId, ct);
|
||
return plans.Select(ToSummaryDto).ToList();
|
||
}
|
||
|
||
public async Task<ExercisePlanDto?> GetByIdAsync(Guid userId, Guid planId, CancellationToken ct)
|
||
{
|
||
var plan = await _exercises.GetOwnedPlanAsync(userId, planId, ct);
|
||
return plan == null ? null : ToDto(plan);
|
||
}
|
||
|
||
public async Task<bool> 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<bool?> 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<bool> 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));
|
||
}
|