diff --git a/AsbCloudApp/Repositories/INotificationRepository.cs b/AsbCloudApp/Repositories/INotificationRepository.cs new file mode 100644 index 00000000..1a04f26a --- /dev/null +++ b/AsbCloudApp/Repositories/INotificationRepository.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data; +using AsbCloudApp.Services; + +namespace AsbCloudApp.Repositories; + +/// +/// Репозиторий для уведомлений +/// +public interface INotificationRepository : ICrudRepository +{ + /// + /// Метод для получения не отправленных уведомлений + /// + /// + /// + /// + /// + Task> GetUnsentNotificationsAsync(int idUser, + int idNotificationTransport, + CancellationToken cancellationToken); + + /// + /// Метод получения уведомлений по параметрам + /// + /// + /// + /// + /// + /// + /// + Task> GetNotificationsAsync(int? skip, + int? take, + int idUser, + int idNotificationTransport, + CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/AsbCloudApp/Services/INotificationSendingQueueService.cs b/AsbCloudApp/Services/INotificationSendingQueueService.cs new file mode 100644 index 00000000..95614e6f --- /dev/null +++ b/AsbCloudApp/Services/INotificationSendingQueueService.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Threading; +using AsbCloudApp.Data; + +namespace AsbCloudApp.Services; + +/// +/// Сервис для добавление уведомлений в очередь +/// +public interface INotificationSendingQueueService +{ + /// + /// Флаг для проверки пустая ли коллекция + /// + bool IsEmpty { get; } + + /// + /// Добавление одного уведомления в очередь + /// + /// + void Enqueue(NotificationDto notificationDto); + + /// + /// Добавление нескольких уведомлений в очередь + /// + /// + void EnqueueRange(IEnumerable notifications); + + /// + /// Извлечение элемента из очереди и его удаление + /// + /// + /// + bool TryDequeue(out NotificationDto notification); + + /// + /// Метод ожидания нового уведомления + /// + /// + void Wait(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/AsbCloudApp/Services/INotificationService.cs b/AsbCloudApp/Services/INotificationService.cs new file mode 100644 index 00000000..973e4c8d --- /dev/null +++ b/AsbCloudApp/Services/INotificationService.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudApp.Services; + +/// +/// Интерфейс для работы с уведомлениями +/// +public interface INotificationService +{ + /// + /// Метод отправки нового уведомления + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task SendNotificationAsync(int idUser, + int idNotificationTransport, + int idNotificationCategory, + string title, + string subject, + TimeSpan timeToLife, + CancellationToken cancellationToken); + + /// + /// Метод обновления уведомления + /// + /// + /// + /// + /// + Task UpdateNotificationAsync(int idNotification, + bool isRead, + CancellationToken cancellationToken); + + /// + /// Метод отправки уведомлений, которые не были отправлены + /// + /// + /// + /// + /// + Task ResendNotificationAsync(int idUser, + int idNotificationTransport, + CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs index 4d000ba0..01645d15 100644 --- a/AsbCloudInfrastructure/DependencyInjection.cs +++ b/AsbCloudInfrastructure/DependencyInjection.cs @@ -143,6 +143,10 @@ namespace AsbCloudInfrastructure services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); + // admin crud services: services.AddTransient, CrudCacheRepositoryBase>(s => new CrudCacheRepositoryBase( diff --git a/AsbCloudInfrastructure/Repository/NotificationRepository.cs b/AsbCloudInfrastructure/Repository/NotificationRepository.cs new file mode 100644 index 00000000..c14ba518 --- /dev/null +++ b/AsbCloudInfrastructure/Repository/NotificationRepository.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data; +using AsbCloudApp.Repositories; +using AsbCloudDb; +using AsbCloudDb.Model; +using Mapster; +using Microsoft.EntityFrameworkCore; + +namespace AsbCloudInfrastructure.Repository; + +public class NotificationRepository : CrudRepositoryBase, INotificationRepository +{ + public NotificationRepository(IAsbCloudDbContext context) : base(context) + { + } + + public NotificationRepository(IAsbCloudDbContext context, + Func, IQueryable> makeQuery) : base(context, makeQuery) + { + } + + public async Task> GetUnsentNotificationsAsync(int idUser, + int idNotificationTransport, + CancellationToken cancellationToken) + { + var notifications = await dbContext.Notifications + .Where(x => x.IdUser == idUser && + x.IdNotificationTransport == idNotificationTransport && + x.SentDateAtUtc == null) + .Include(x => x.NotificationTransport) + .Include(x => x.NotificationCategory) + .ToListAsync(cancellationToken); + + return notifications.Select(x => x.Adapt()); + } + + public async Task> GetNotificationsAsync(int? skip, + int? take, + int idUser, + int idNotificationTransport, + CancellationToken cancellationToken) + { + skip ??= 0; + take ??= 10; + + var query = dbContext.Notifications + .Where(x => x.IdNotificationTransport == idNotificationTransport && + x.IdUser == idUser && + x.SentDateAtUtc != null); + + var result = new PaginationContainer() + { + Skip = skip.Value, + Take = take.Value, + Count = await query.CountAsync(cancellationToken), + }; + + if (result.Count == 0) + return result; + + result.Items = await query + .OrderBy(x => x.SentDateAtUtc) + .SkipTake(skip, take) + .Include(x => x.NotificationCategory) + .Include(x => x.NotificationTransport) + .Select(x => x.Adapt()) + .ToListAsync(cancellationToken); + + return result; + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/NotificationSendingQueueService.cs b/AsbCloudInfrastructure/Services/NotificationSendingQueueService.cs new file mode 100644 index 00000000..23da999f --- /dev/null +++ b/AsbCloudInfrastructure/Services/NotificationSendingQueueService.cs @@ -0,0 +1,42 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using AsbCloudApp.Data; +using AsbCloudApp.Services; + +namespace AsbCloudInfrastructure.Services; + +public class NotificationSendingQueueService : INotificationSendingQueueService +{ + private readonly ManualResetEventSlim manualResetEventSlim = new(); + private readonly ConcurrentQueue notificationsQueue = new(); + + public bool IsEmpty => notificationsQueue.IsEmpty; + + public void Enqueue(NotificationDto notification) + { + notificationsQueue.Enqueue(notification); + manualResetEventSlim.Set(); + } + + public void EnqueueRange(IEnumerable notifications) + { + foreach (var notification in notifications) + { + notificationsQueue.Enqueue(notification); + } + + manualResetEventSlim.Set(); + } + + public bool TryDequeue(out NotificationDto notification) + { + return notificationsQueue.TryDequeue(out notification!); + } + + public void Wait(CancellationToken cancellationToken) + { + manualResetEventSlim.Wait(cancellationToken); + manualResetEventSlim.Reset(); + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/NotificationService.cs b/AsbCloudInfrastructure/Services/NotificationService.cs new file mode 100644 index 00000000..3db22eba --- /dev/null +++ b/AsbCloudInfrastructure/Services/NotificationService.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data; +using AsbCloudApp.Exceptions; +using AsbCloudApp.Repositories; +using AsbCloudApp.Services; + +namespace AsbCloudInfrastructure.Services; + +public class NotificationService : INotificationService +{ + private readonly INotificationSendingQueueService notificationSendingQueueService; + private readonly INotificationRepository notificationRepository; + + public NotificationService(INotificationSendingQueueService notificationSendingQueueService, + INotificationRepository notificationRepository) + { + this.notificationSendingQueueService = notificationSendingQueueService; + this.notificationRepository = notificationRepository; + } + + public async Task SendNotificationAsync(int idUser, + int idNotificationTransport, + int idNotificationCategory, + string title, + string subject, + TimeSpan timeToLife, + CancellationToken cancellationToken) + { + NotificationDto notification = new() + { + IdUser = idUser, + IdNotificationTransport = idNotificationTransport, + IdNotificationCategory = idNotificationCategory, + Title = title, + Subject = subject, + TimeToLife = timeToLife + }; + + await notificationRepository.InsertAsync(notification, + cancellationToken); + + notificationSendingQueueService.Enqueue(notification); + } + + public async Task UpdateNotificationAsync(int idNotification, + bool isRead, + CancellationToken cancellationToken) + { + var notification = await notificationRepository.GetOrDefaultAsync(idNotification, + cancellationToken) ?? throw new ArgumentInvalidException("Уведомление не найдено", + nameof(idNotification)); + + notification.IsRead = isRead; + + await notificationRepository.UpdateAsync(notification, + cancellationToken); + } + + public async Task ResendNotificationAsync(int idUser, + int idNotificationTransport, + CancellationToken cancellationToken) + { + var notifications = await notificationRepository.GetUnsentNotificationsAsync(idUser, + idNotificationTransport, + cancellationToken); + + notificationSendingQueueService.EnqueueRange(notifications); + } +} \ No newline at end of file diff --git a/AsbCloudWebApi/Controllers/NotificationController.cs b/AsbCloudWebApi/Controllers/NotificationController.cs new file mode 100644 index 00000000..07fe4ace --- /dev/null +++ b/AsbCloudWebApi/Controllers/NotificationController.cs @@ -0,0 +1,129 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data; +using AsbCloudApp.Repositories; +using AsbCloudApp.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace AsbCloudWebApi.Controllers; + +/// +/// Уведомления +/// +[ApiController] +[Authorize] +[Route("api/notification")] +public class NotificationController : ControllerBase +{ + private readonly INotificationService notificationService; + private readonly INotificationRepository notificationRepository; + + public NotificationController(INotificationService notificationService, + INotificationRepository notificationRepository) + { + this.notificationService = notificationService; + this.notificationRepository = notificationRepository; + } + + /// + /// Метод отправки уведомления + /// + /// Id пользователя + /// Id способа отправки уведомления + /// Id категории уведомления + /// Заголовок уведомления + /// Сообщение уведомления + /// Время жизни уведомления + /// + /// + [HttpPost] + [Route("send")] + public async Task SendAsync([Required] int idUser, + [Required] int idNotificationTransport, + [Required] int idNotificationCategory, + [Required] string title, + [Required] string subject, + [Required] TimeSpan timeToLife, + CancellationToken cancellationToken) + { + await notificationService.SendNotificationAsync(idUser, + idNotificationTransport, + idNotificationCategory, + title, + subject, + timeToLife, + cancellationToken); + + return Ok(); + } + + /// + /// Метод обновления уведомления + /// + /// Id уведомления + /// Прочитано ли уведомление + /// + /// + [HttpPut] + [Route("update")] + public async Task UpdateAsync([Required] int idNotification, + [Required] bool isRead, + CancellationToken cancellationToken) + { + await notificationService.UpdateNotificationAsync(idNotification, + isRead, + cancellationToken); + + return Ok(); + } + + /// + /// Метод получения уведомлений по параметрам + /// + /// Кол-во пропускаемых записей + /// Кол-во выбираемых записей + /// Id способа доставки уведомления + /// + /// + [HttpGet] + [Route("getList")] + [ProducesResponseType(typeof(PaginationContainer), (int)System.Net.HttpStatusCode.OK)] + public async Task GetListAsync(int? skip, + int? take, + [Required] int idNotificationTransport, + CancellationToken cancellationToken) + { + int? idUser = User.GetUserId(); + + if (!idUser.HasValue) + return Forbid(); + + var result = await notificationRepository.GetNotificationsAsync(skip, + take, + idUser.Value, + idNotificationTransport, + cancellationToken); + + return Ok(result); + } + + /// + /// Метод удаления уведомления + /// + /// Id уведомления + /// + /// + [HttpDelete] + [Route("delete")] + public async Task DeleteAsync([Required] int idNotification, + CancellationToken cancellationToken) + { + await notificationRepository.DeleteAsync(idNotification, + cancellationToken); + + return Ok(); + } +} \ No newline at end of file diff --git a/AsbCloudWebApi/DependencyInjection.cs b/AsbCloudWebApi/DependencyInjection.cs index 2fd7b3d4..6b1fc6b4 100644 --- a/AsbCloudWebApi/DependencyInjection.cs +++ b/AsbCloudWebApi/DependencyInjection.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.IO; using System.Reflection; using System.Threading.Tasks; +using Microsoft.OpenApi.Any; namespace AsbCloudWebApi { @@ -18,6 +19,7 @@ namespace AsbCloudWebApi { services.AddSwaggerGen(c => { + c.MapType(() => new OpenApiSchema { Type = "string", Example = new OpenApiString("00:00:00") }); c.MapType(() => new OpenApiSchema { Type = "string", Format = "date" }); c.MapType(() => new OpenApiSchema { AnyOf = new OpenApiSchema[]