/
afanasevn
/
RedisLab
Обзор
Документация
Войти
/
afanasevn
/
RedisLab
Код
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/RedisLab.Infrastructure/Tasks/TaskService.cs
186 строк
6 KB
IBS\NAfanasev
Solution commit
24 июн 2026, 16:45
24 июн 2026, 16:45
bab80b7
Код
Авторство
О чём код?
using Microsoft.EntityFrameworkCore; using RedisLab.Application.Abstractions; using RedisLab.Application.Contracts.Activity; using RedisLab.Application.Contracts.Tasks; using RedisLab.Domain.Entities; using RedisLab.Infrastructure.Persistence; using RedisLab.Infrastructure.Redis; using TaskStatus = RedisLab.Domain.Enums.TaskStatus; namespace RedisLab.Infrastructure.Tasks; /// <summary> /// Задачи: PostgreSQL + cache-aside Hash + Pub/Sub activity feed. /// </summary> public sealed class TaskService( AppDbContext db, TaskCardCache taskCardCache, IDashboardKpiCacheInvalidator dashboardCacheInvalidator, IActivityFeedService activityFeedService, ILeaderboardService leaderboardService) : ITaskService { /// <inheritdoc /> public async Task<IReadOnlyList<TaskListItemDto>> GetListAsync(CancellationToken ct) { return await db.Tasks .AsNoTracking() .Include(x => x.Assignee) .OrderByDescending(x => x.UpdatedAt) .Select(x => new TaskListItemDto( x.Id, x.Title, x.Status.ToString(), x.Assignee.DisplayName, x.DueDate)) .ToListAsync(ct); } /// <inheritdoc /> public async Task<TaskCardResult?> GetByIdAsync( Guid taskId, CancellationToken ct) { var cached = await taskCardCache.TryGetAsync(taskId, ct); if (cached is not null) { return new TaskCardResult(cached, CacheHit: true); } var task = await db.Tasks .AsNoTracking() .Include(x => x.Assignee) .FirstOrDefaultAsync(x => x.Id == taskId, ct); if (task is null) { return null; } var card = MapToCard(task); await taskCardCache.SetAsync(card, ct); return new TaskCardResult(card, CacheHit: false); } /// <inheritdoc /> public async Task<Guid> CreateAsync( CreateTaskRequest request, CancellationToken ct) { if (!Enum.TryParse<TaskStatus>(request.Status, ignoreCase: true, out var status)) { throw new ArgumentException("Неизвестный статус задачи.", nameof(request)); } var assigneeExists = await db.Users.AnyAsync(x => x.Id == request.AssigneeId, ct); if (!assigneeExists) { throw new ArgumentException("Исполнитель не найден.", nameof(request)); } var task = new TaskItem { Id = Guid.NewGuid(), Title = request.Title.Trim(), Status = status, AssigneeId = request.AssigneeId, DueDate = request.DueDate, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, }; db.Tasks.Add(task); await db.SaveChangesAsync(ct); await dashboardCacheInvalidator.InvalidateAsync(ct); await activityFeedService.PublishAsync( new ActivityFeedMessage( "task.created", task.Id, $"Создана задача «{task.Title}»", DateTime.UtcNow), ct); return task.Id; } /// <inheritdoc /> public async Task<bool> UpdateStatusAsync( Guid taskId, UpdateTaskStatusRequest request, CancellationToken ct) { if (!Enum.TryParse<TaskStatus>(request.Status, ignoreCase: true, out var status)) { throw new ArgumentException("Неизвестный статус задачи.", nameof(request)); } var task = await db.Tasks.FirstOrDefaultAsync(x => x.Id == taskId, ct); if (task is null) { return false; } var previousStatus = task.Status; task.Status = status; task.UpdatedAt = DateTime.UtcNow; await db.SaveChangesAsync(ct); await dashboardCacheInvalidator.InvalidateAsync(ct); await taskCardCache.InvalidateAsync(taskId, ct); await activityFeedService.PublishAsync( new ActivityFeedMessage( "task.status_changed", task.Id, $"Задача «{task.Title}»: {previousStatus} => {status}", DateTime.UtcNow), ct); // Sorted Set: +1 assignee при первом переводе в Closed (не при повторном Closed). if (status == TaskStatus.Closed && previousStatus != TaskStatus.Closed) { await leaderboardService.IncrementCompletedAsync(task.AssigneeId, ct); } return true; } /// <inheritdoc /> public async Task<bool> DeleteAsync( Guid taskId, CancellationToken ct) { var task = await db.Tasks.FirstOrDefaultAsync(x => x.Id == taskId, ct); if (task is null) { return false; } var title = task.Title; db.Tasks.Remove(task); await db.SaveChangesAsync(ct); await dashboardCacheInvalidator.InvalidateAsync(ct); await taskCardCache.InvalidateAsync(taskId, ct); await activityFeedService.PublishAsync( new ActivityFeedMessage( "task.deleted", taskId, $"Удалена задача «{title}»", DateTime.UtcNow), ct); return true; } private static TaskCardDto MapToCard(TaskItem task) => new( task.Id, task.Title, task.Status.ToString(), task.Assignee.DisplayName, task.DueDate); }