diff --git a/AsbCloudApp/AsbCloudApp.csproj b/AsbCloudApp/AsbCloudApp.csproj index 79ec9867..512a0c0a 100644 --- a/AsbCloudApp/AsbCloudApp.csproj +++ b/AsbCloudApp/AsbCloudApp.csproj @@ -18,4 +18,8 @@ + + + + diff --git a/AsbCloudApp/Repositories/INotificationRepository.cs b/AsbCloudApp/Repositories/INotificationRepository.cs index 74f0b81c..74ab9dd9 100644 --- a/AsbCloudApp/Repositories/INotificationRepository.cs +++ b/AsbCloudApp/Repositories/INotificationRepository.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using AsbCloudApp.Data; using AsbCloudApp.Requests; using AsbCloudApp.Services; @@ -22,6 +23,25 @@ public interface INotificationRepository : ICrudRepository NotificationRequest request, CancellationToken cancellationToken); + /// + /// Получение всех уведомлений + /// + /// + /// + /// + /// + /// + Task> GetAllAsync(int? idUser, bool? isSent, int? idTransportType, + CancellationToken cancellationToken); + + /// + /// Обновление уведомлений + /// + /// + /// + /// + Task UpdateRangeAsync(IEnumerable notifications, CancellationToken cancellationToken); + /// /// Удаление уведомлений по параметрам /// diff --git a/AsbCloudApp/Services/Notifications/INotificationTransportService.cs b/AsbCloudApp/Services/Notifications/INotificationTransportService.cs index fba70d4b..a18f4246 100644 --- a/AsbCloudApp/Services/Notifications/INotificationTransportService.cs +++ b/AsbCloudApp/Services/Notifications/INotificationTransportService.cs @@ -16,7 +16,7 @@ public interface INotificationTransportService int IdTransportType { get; } /// - /// Отправка одного уведомлений + /// Отправка одного уведомления /// /// /// diff --git a/AsbCloudApp/Services/Notifications/NotificationService.cs b/AsbCloudApp/Services/Notifications/NotificationService.cs index 33ff10db..b8916b15 100644 --- a/AsbCloudApp/Services/Notifications/NotificationService.cs +++ b/AsbCloudApp/Services/Notifications/NotificationService.cs @@ -7,6 +7,7 @@ using AsbCloudApp.Data; using AsbCloudApp.Exceptions; using AsbCloudApp.Repositories; using AsbCloudApp.Requests; +using AsbCloudDb.Model; namespace AsbCloudApp.Services.Notifications; @@ -42,8 +43,7 @@ public class NotificationService public async Task NotifyAsync(NotifyRequest request, CancellationToken cancellationToken) { - var notificationCategory = await notificationCategoryRepository - .GetOrDefaultAsync(request.IdNotificationCategory, cancellationToken) + var notificationCategory = await notificationCategoryRepository.GetOrDefaultAsync(request.IdNotificationCategory, cancellationToken) ?? throw new ArgumentInvalidException("Категория уведомления не найдена", nameof(request.IdNotificationCategory)); var notification = new NotificationDto @@ -59,7 +59,7 @@ public class NotificationService notification.Id = await notificationRepository.InsertAsync(notification, cancellationToken); notification.NotificationCategory = notificationCategory; - var notificationTransportService = GetNotificationTransportService(request.IdTransportType); + var notificationTransportService = GetTransportService(request.IdTransportType); await notificationTransportService.SendAsync(notification, cancellationToken); } @@ -71,12 +71,11 @@ public class NotificationService /// /// /// - public async Task UpdateNotificationAsync(int idNotification, + public async Task UpdateAsync(int idNotification, bool isRead, CancellationToken cancellationToken) { - var notification = await notificationRepository.GetOrDefaultAsync(idNotification, - cancellationToken) + var notification = await notificationRepository.GetOrDefaultAsync(idNotification, cancellationToken) ?? throw new ArgumentInvalidException("Уведомление не найдено", nameof(idNotification)); if(isRead && !notification.SentDate.HasValue) @@ -95,25 +94,19 @@ public class NotificationService /// /// /// - public async Task ResendNotificationAsync(int idUser, - NotificationRequest request, - CancellationToken cancellationToken) + public async Task RenotifyAsync(int idUser, CancellationToken cancellationToken) { - if (!request.IdTransportType.HasValue) - throw new ArgumentInvalidException("Id типа доставки уведомления должен иметь значение", - nameof(request.IdTransportType)); - - var result = await notificationRepository.GetNotificationsAsync(idUser, - request, + var notifications = await notificationRepository.GetAllAsync(idUser, false, + Notification.IdTransportTypeSignalR, cancellationToken); - var notificationTransportService = GetNotificationTransportService(request.IdTransportType.Value); + var notificationTransportService = GetTransportService(Notification.IdTransportTypeSignalR); - await notificationTransportService.SendRangeAsync(result.Items, + await notificationTransportService.SendRangeAsync(notifications, cancellationToken); } - private INotificationTransportService GetNotificationTransportService(int idTransportType) + private INotificationTransportService GetTransportService(int idTransportType) { var notificationTransportService = notificationTransportServices .FirstOrDefault(s => s.IdTransportType == idTransportType) diff --git a/AsbCloudDb/Model/Notification.cs b/AsbCloudDb/Model/Notification.cs index 09f718b1..e363d3d2 100644 --- a/AsbCloudDb/Model/Notification.cs +++ b/AsbCloudDb/Model/Notification.cs @@ -8,6 +8,9 @@ namespace AsbCloudDb.Model; [Table("t_notification"), Comment("Уведомления")] public class Notification : IId { + public const int IdTransportTypeSignalR = 0; + public const int IdTransportTypeTypeEmail = 1; + [Key] [Column("id")] public int Id { get; set; } diff --git a/AsbCloudInfrastructure/Repository/NotificationRepository.cs b/AsbCloudInfrastructure/Repository/NotificationRepository.cs index ea639c33..6f1e4a98 100644 --- a/AsbCloudInfrastructure/Repository/NotificationRepository.cs +++ b/AsbCloudInfrastructure/Repository/NotificationRepository.cs @@ -1,9 +1,12 @@ +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using AsbCloudApp.Data; +using AsbCloudApp.Data.SAUB; using AsbCloudApp.Repositories; using AsbCloudApp.Requests; +using AsbCloudApp.Services; using AsbCloudDb; using AsbCloudDb.Model; using Mapster; @@ -54,10 +57,55 @@ public class NotificationRepository : CrudRepositoryBase DeleteAsync(NotificationDeleteRequest request, CancellationToken cancellationToken) + public async Task> GetAllAsync(int? idUser, bool? isSent, int? idTransportType, + CancellationToken cancellationToken) { var query = dbContext.Notifications.AsQueryable(); + if (idUser.HasValue) + query = query.Where(n => n.IdUser == idUser); + + if(isSent.HasValue) + query = isSent.Value ? + query.Where(n => n.SentDate != null) + : query.Where(n => n.SentDate == null); + + if (idTransportType.HasValue) + query = query.Where(n => n.IdTransportType == idTransportType); + + return await query.AsNoTracking().Select(n => n.Adapt()) + .ToArrayAsync(cancellationToken); + } + + public async Task UpdateRangeAsync(IEnumerable notifications, CancellationToken cancellationToken) + { + if (!notifications.Any()) + return 0; + + var ids = notifications.Select(d => d.Id); + + var existingEntities = await dbSet + .Where(d => ids.Contains(d.Id)) + .AsNoTracking() + .Select(d => d.Id) + .ToArrayAsync(cancellationToken); + + if (ids.Count() > existingEntities.Length) + return ICrudRepository.ErrorIdNotFound; + + var entities = notifications.Select(Convert); + + dbContext.Notifications.UpdateRange(entities); + + return await dbContext.SaveChangesAsync(cancellationToken); + } + + public Task DeleteAsync(NotificationDeleteRequest request, CancellationToken cancellationToken) + { + var query = dbContext.Notifications + .Include(n => n.NotificationCategory) + .AsQueryable(); + if (request.IdCategory.HasValue) query = query.Where(n => n.IdNotificationCategory == request.IdCategory.Value); @@ -72,16 +120,15 @@ public class NotificationRepository : CrudRepositoryBase GetUnreadCountAsync(int idUser, int idTransportType, CancellationToken cancellationToken) + public Task GetUnreadCountAsync(int idUser, int idTransportType, CancellationToken cancellationToken) { - var count = await dbContext.Notifications - .Where(n => n.ReadDate == null) - .Where(n => n.IdUser == idUser) - .Where(n => n.IdTransportType == idTransportType) - .CountAsync(cancellationToken); - - return count; - } + return dbContext.Notifications + .Where(n => !n.ReadDate.HasValue && + n.SentDate.HasValue && + n.IdUser == idUser && + n.IdTransportType == idTransportType) + .CountAsync(cancellationToken); + } private IQueryable BuildQuery(int idUser, NotificationRequest request) diff --git a/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs b/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs index e0f4ba0e..4211c73e 100644 --- a/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs +++ b/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs @@ -9,6 +9,7 @@ using AsbCloudApp.Data; using AsbCloudApp.Exceptions; using AsbCloudApp.Repositories; using AsbCloudApp.Services.Notifications; +using AsbCloudDb.Model; using AsbCloudInfrastructure.Background; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -40,7 +41,7 @@ namespace AsbCloudInfrastructure.Services.Email this.backgroundWorker = backgroundWorker; } - public int IdTransportType => 1; + public int IdTransportType => Notification.IdTransportTypeTypeEmail; public Task SendAsync(NotificationDto notification, CancellationToken cancellationToken) { diff --git a/AsbCloudWebApi/Controllers/NotificationController.cs b/AsbCloudWebApi/Controllers/NotificationController.cs index 31203292..9e3b7b1f 100644 --- a/AsbCloudWebApi/Controllers/NotificationController.cs +++ b/AsbCloudWebApi/Controllers/NotificationController.cs @@ -6,6 +6,7 @@ using AsbCloudApp.Exceptions; using AsbCloudApp.Repositories; using AsbCloudApp.Requests; using AsbCloudApp.Services.Notifications; +using AsbCloudWebApi.SignalR.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -21,12 +22,15 @@ public class NotificationController : ControllerBase { private readonly NotificationService notificationService; private readonly INotificationRepository notificationRepository; + private readonly NotificationPublisher notificationPublisher; public NotificationController(NotificationService notificationService, - INotificationRepository notificationRepository) + INotificationRepository notificationRepository, + NotificationPublisher notificationPublisher) { this.notificationService = notificationService; this.notificationRepository = notificationRepository; + this.notificationPublisher = notificationPublisher; } /// @@ -55,10 +59,17 @@ public class NotificationController : ControllerBase [Required] bool isRead, CancellationToken cancellationToken) { - await notificationService.UpdateNotificationAsync(idNotification, + var idUser = User.GetUserId(); + + if (!idUser.HasValue) + return Forbid(); + + await notificationService.UpdateAsync(idNotification, isRead, cancellationToken); + await notificationPublisher.PublishCountUnreadAsync(idUser.Value, cancellationToken); + return Ok(); } @@ -125,28 +136,7 @@ public class NotificationController : ControllerBase return Ok(); } - /// - /// Получение количества непрочитанных уведомлений - /// - /// - /// - /// - [HttpGet] - [Route("unreadNotificationCount")] - [ProducesResponseType(typeof(int), (int)System.Net.HttpStatusCode.OK)] - public async Task GetUnreadCountAsync([FromQuery] int idTransportType, CancellationToken cancellationToken) - { - int? idUser = User.GetUserId(); - - if (!idUser.HasValue) - return Forbid(); - - var result = await notificationRepository.GetUnreadCountAsync(idUser.Value, idTransportType, cancellationToken); - - return Ok(result); - } - - /// + /// /// Удаление уведомлений /// /// Параметры запроса diff --git a/AsbCloudWebApi/DependencyInjection.cs b/AsbCloudWebApi/DependencyInjection.cs index f64f5b2c..4117e5ef 100644 --- a/AsbCloudWebApi/DependencyInjection.cs +++ b/AsbCloudWebApi/DependencyInjection.cs @@ -141,6 +141,8 @@ namespace AsbCloudWebApi { services.AddTransient(); services.AddTransient(); + + services.AddTransient(); } } } diff --git a/AsbCloudWebApi/SignalR/Messages/NotificationMessage.cs b/AsbCloudWebApi/SignalR/Messages/NotificationMessage.cs new file mode 100644 index 00000000..66efaee0 --- /dev/null +++ b/AsbCloudWebApi/SignalR/Messages/NotificationMessage.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using AsbCloudApp.Data; + +namespace AsbCloudWebApi.SignalR.Messages; + +public class NotificationMessage +{ + public IEnumerable? Notifications { get; set; } + + public int CountUnread { get; set; } +} \ No newline at end of file diff --git a/AsbCloudWebApi/SignalR/NotificationHub.cs b/AsbCloudWebApi/SignalR/NotificationHub.cs index 5422e30e..b85ce74b 100644 --- a/AsbCloudWebApi/SignalR/NotificationHub.cs +++ b/AsbCloudWebApi/SignalR/NotificationHub.cs @@ -1,7 +1,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using AsbCloudApp.Requests; using AsbCloudApp.Services.Notifications; using AsbCloudWebApi.SignalR.Services; using Microsoft.AspNetCore.Authorization; @@ -14,12 +13,15 @@ public class NotificationHub : BaseHub { private readonly ConnectionManagerService connectionManagerService; private readonly NotificationService notificationService; + private readonly NotificationPublisher notificationPublisher; public NotificationHub(ConnectionManagerService connectionManagerService, - NotificationService notificationService) + NotificationService notificationService, + NotificationPublisher notificationPublisher) { this.connectionManagerService = connectionManagerService; this.notificationService = notificationService; + this.notificationPublisher = notificationPublisher; } public override async Task OnConnectedAsync() @@ -35,9 +37,10 @@ public class NotificationHub : BaseHub await base.OnConnectedAsync(); - await notificationService.ResendNotificationAsync(idUser.Value, - new NotificationRequest { IsSent = false, IdTransportType = 0}, - CancellationToken.None); + await notificationPublisher.PublishCountUnreadAsync(idUser.Value, CancellationToken.None); + + await notificationService.RenotifyAsync(idUser.Value, + CancellationToken.None); } public override async Task OnDisconnectedAsync(Exception? exception) diff --git a/AsbCloudWebApi/SignalR/Services/NotificationPublisher.cs b/AsbCloudWebApi/SignalR/Services/NotificationPublisher.cs new file mode 100644 index 00000000..5c1d1e50 --- /dev/null +++ b/AsbCloudWebApi/SignalR/Services/NotificationPublisher.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data; +using AsbCloudApp.Repositories; +using AsbCloudDb.Model; +using AsbCloudWebApi.SignalR.Messages; +using Microsoft.AspNetCore.SignalR; + +namespace AsbCloudWebApi.SignalR.Services; + +public class NotificationPublisher +{ + private readonly ConnectionManagerService connectionManagerService; + private readonly IHubContext notificationHubContext; + private readonly INotificationRepository notificationRepository; + + public NotificationPublisher(ConnectionManagerService connectionManagerService, + IHubContext notificationHubContext, + INotificationRepository notificationRepository) + { + this.connectionManagerService = connectionManagerService; + this.notificationHubContext = notificationHubContext; + this.notificationRepository = notificationRepository; + } + + public async Task PublishAsync(IGrouping groupedNotifications, CancellationToken cancellationToken) + { + var connectionId = connectionManagerService.GetConnectionIdByUserId(groupedNotifications.Key); + + if (!string.IsNullOrWhiteSpace(connectionId)) + { + foreach (var notification in groupedNotifications) + { + notification.SentDate = DateTime.UtcNow; + } + + var notifications = groupedNotifications.Select(n => n); + + await notificationRepository.UpdateRangeAsync(notifications, + cancellationToken); + + await PublishMessageAsync(connectionId, new NotificationMessage + { + Notifications = notifications, + CountUnread = await notificationRepository.GetUnreadCountAsync(groupedNotifications.Key, + Notification.IdTransportTypeSignalR, + cancellationToken) + }, cancellationToken); + } + } + + public async Task PublishCountUnreadAsync(int idUser, CancellationToken cancellationToken) + { + var connectionId = connectionManagerService.GetConnectionIdByUserId(idUser); + + if (!string.IsNullOrWhiteSpace(connectionId)) + { + await PublishMessageAsync(connectionId, new NotificationMessage + { + CountUnread = await notificationRepository.GetUnreadCountAsync(idUser, 0, + cancellationToken) + }, cancellationToken); + } + } + + private async Task PublishMessageAsync(string connectionId, NotificationMessage message, + CancellationToken cancellationToken) + { + await notificationHubContext.Clients.Client(connectionId) + .SendAsync("receiveNotifications", + message, + cancellationToken); + } +} \ No newline at end of file diff --git a/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs b/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs index c29ea241..08258bef 100644 --- a/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs +++ b/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs @@ -4,10 +4,9 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using AsbCloudApp.Data; -using AsbCloudApp.Repositories; using AsbCloudApp.Services.Notifications; +using AsbCloudDb.Model; using AsbCloudInfrastructure.Background; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; namespace AsbCloudWebApi.SignalR.Services; @@ -15,66 +14,44 @@ namespace AsbCloudWebApi.SignalR.Services; public class SignalRNotificationTransportService : INotificationTransportService { private readonly NotificationBackgroundWorker backgroundWorker; - private readonly ConnectionManagerService connectionManagerService; - private readonly IHubContext notificationHubContext; - public SignalRNotificationTransportService(NotificationBackgroundWorker backgroundWorker, - ConnectionManagerService connectionManagerService, - IHubContext notificationHubContext) + public SignalRNotificationTransportService(NotificationBackgroundWorker backgroundWorker) { this.backgroundWorker = backgroundWorker; - this.connectionManagerService = connectionManagerService; - this.notificationHubContext = notificationHubContext; } - public int IdTransportType => 0; + public int IdTransportType => Notification.IdTransportTypeSignalR; - public Task SendAsync(NotificationDto notification, - CancellationToken cancellationToken) - { - var workId = notification.Id.ToString(); - - if (!backgroundWorker.Contains(workId)) - { - var connectionId = connectionManagerService.GetConnectionIdByUserId(notification.IdUser); - - if (!string.IsNullOrWhiteSpace(connectionId)) - { - var workAction = MakeSignalRSendWorkAction(notification, connectionId); - var work = new WorkBase(workId, workAction); - backgroundWorker.Push(work); - } - } - - return Task.CompletedTask; - } + public Task SendAsync(NotificationDto notification, + CancellationToken cancellationToken) => SendRangeAsync(new[] { notification }, cancellationToken); public Task SendRangeAsync(IEnumerable notifications, CancellationToken cancellationToken) { - var tasks = notifications - .Select(notification => SendAsync(notification, cancellationToken)); + var workId = HashCode.Combine(notifications.Select(n => n.Id)).ToString("x"); - return Task.WhenAll(tasks); + if (backgroundWorker.Contains(workId)) + return Task.CompletedTask; + + var workAction = MakeSignalRSendWorkAction(notifications); + var work = new WorkBase(workId, workAction); + backgroundWorker.Push(work); + + return Task.CompletedTask; } - private Func MakeSignalRSendWorkAction(NotificationDto notification, - string connectionId) + private Func MakeSignalRSendWorkAction(IEnumerable notifications) { - const string method = "receiveNotifications"; - return async (_, serviceProvider, cancellationToken) => { - notification.SentDate = DateTime.UtcNow; - - await notificationHubContext.Clients.Client(connectionId) - .SendAsync(method, - notification, - cancellationToken); - - var notificationRepository = serviceProvider.GetRequiredService(); + var notificationPublisher = serviceProvider.GetRequiredService(); - await notificationRepository.UpdateAsync(notification, cancellationToken); + var groupedNotificationsByUsers = notifications.GroupBy(n => n.IdUser); + + foreach (var groupedNotificationByUser in groupedNotificationsByUsers) + { + await notificationPublisher.PublishAsync(groupedNotificationByUser, cancellationToken); + } }; } } \ No newline at end of file