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[]