Merge pull request 'feature/notifications' (#71) from feature/notifications into dev

Reviewed-on: http://test.digitaldrilling.ru:8080/DDrilling/AsbCloudServer/pulls/71
This commit is contained in:
Никита Фролов 2023-07-17 11:49:33 +05:00
commit 048395b6ff
29 changed files with 17798 additions and 24 deletions

View File

@ -0,0 +1,17 @@
namespace AsbCloudApp.Data;
/// <summary>
/// DTO категории уведомления
/// </summary>
public class NotificationCategoryDto : IId
{
/// <summary>
/// Id категории
/// </summary>
public int Id { get; set; }
/// <summary>
/// Название категории
/// </summary>
public string Name { get; set; } = null!;
}

View File

@ -0,0 +1,75 @@
using System;
namespace AsbCloudApp.Data;
/// <summary>
/// DTO уведомления
/// </summary>
public class NotificationDto : IId
{
/// <summary>
/// Id уведомления
/// </summary>
public int Id { get; set; }
/// <summary>
/// Id получателя уведомления
/// </summary>
public int IdUser { get; set; }
/// <summary>
/// Id категории уведомления
/// </summary>
public int IdNotificationCategory { get; set; }
/// <summary>
/// Заголовок уведомления
/// </summary>
public string Title { get; set; } = null!;
/// <summary>
/// Сообщение уведомления
/// </summary>
public string Message { get; set; } = null!;
/// <summary>
/// Дата отправки уведомления
/// </summary>
public DateTime? SentDate { get; set; }
/// <summary>
/// Дата прочтения уведомления
/// </summary>
public DateTime? ReadDate { get; set; }
/// <summary>
/// Состояние уведомления
/// 0 - Зарегистрировано,
/// 1 - Отправлено,
/// 2 - Прочитано
/// </summary>
public int IdState
{
get
{
if (SentDate is not null && ReadDate is not null)
return 2;
if (SentDate is not null)
return 1;
return 0;
}
}
/// <summary>
/// Id типа доставки уведомления
/// 0 - SignalR
/// </summary>
public int IdTransportType { get; set; }
/// <summary>
/// DTO категории уведомления
/// </summary>
public NotificationCategoryDto NotificationCategory { get; set; } = null!;
}

View File

@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Data;
using AsbCloudApp.Requests;
using AsbCloudApp.Services;
namespace AsbCloudApp.Repositories;
/// <summary>
/// Репозиторий для уведомлений
/// </summary>
public interface INotificationRepository : ICrudRepository<NotificationDto>
{
/// <summary>
/// Обновление изменённых уведомлений
/// </summary>
/// <param name="notifications"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<int> UpdateRangeAsync(IEnumerable<NotificationDto> notifications,
CancellationToken cancellationToken);
/// <summary>
/// Получение уведомлений по параметрам
/// </summary>
/// <param name="idUser"></param>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<PaginationContainer<NotificationDto>> GetNotificationsAsync(int idUser,
NotificationRequest request,
CancellationToken cancellationToken);
}

View File

@ -0,0 +1,17 @@
namespace AsbCloudApp.Requests;
/// <summary>
/// Параметры запроса для получения уведомлений
/// </summary>
public class NotificationRequest : RequestBase
{
/// <summary>
/// Получение отправленных/не отправленных уведомлений
/// </summary>
public bool? IsSent { get; set; } = false;
/// <summary>
/// Id типа доставки уведомления
/// </summary>
public int? IdTransportType { get; set; }
}

View File

@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Data;
namespace AsbCloudApp.Services.Notifications;
/// <summary>
/// Интерфейс для отправителя уведомлений
/// </summary>
public interface INotificationTransportService
{
/// <summary>
/// Id типа доставки уведомления
/// </summary>
int IdTransportType { get; }
/// <summary>
/// Отправка одного уведомлений
/// </summary>
/// <param name="notification"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task SendAsync(NotificationDto notification,
CancellationToken cancellationToken);
/// <summary>
/// Отправка нескольких уведомлений
/// </summary>
/// <param name="notifications"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task SendRangeAsync(IEnumerable<NotificationDto> notifications,
CancellationToken cancellationToken);
}

View File

@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Data;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Requests;
namespace AsbCloudApp.Services.Notifications;
/// <summary>
/// Сервис для работы с уведомлениями
/// </summary>
public class NotificationService
{
private readonly ICrudRepository<NotificationCategoryDto> notificationCategoryRepository;
private readonly INotificationRepository notificationRepository;
private readonly IEnumerable<INotificationTransportService> notificationTransportServices;
/// <summary>
/// Сервис для работы с уведомлениями
/// </summary>
/// <param name="notificationCategoryRepository"></param>
/// <param name="notificationRepository"></param>
/// <param name="notificationTransportServices"></param>
public NotificationService(ICrudRepository<NotificationCategoryDto> notificationCategoryRepository,
INotificationRepository notificationRepository,
IEnumerable<INotificationTransportService> notificationTransportServices)
{
this.notificationCategoryRepository = notificationCategoryRepository;
this.notificationRepository = notificationRepository;
this.notificationTransportServices = notificationTransportServices;
}
/// <summary>
/// Отправка нового уведомления
/// </summary>
/// <param name="idUser"></param>
/// <param name="idNotificationCategory"></param>
/// <param name="title"></param>
/// <param name="message"></param>
/// <param name="idTransportType"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task NotifyAsync(int idUser,
int idNotificationCategory,
string title,
string message,
int idTransportType,
CancellationToken cancellationToken)
{
var notificationCategory = await notificationCategoryRepository
.GetOrDefaultAsync(idNotificationCategory, cancellationToken)
?? throw new ArgumentInvalidException("Категория уведомления не найдена", nameof(idNotificationCategory));
var notification = new NotificationDto()
{
IdUser = idUser,
IdNotificationCategory = idNotificationCategory,
Title = title,
Message = message,
IdTransportType = idTransportType
};
notification.Id = await notificationRepository.InsertAsync(notification,
cancellationToken);
notification.NotificationCategory = notificationCategory;
var notificationTransportService = GetNotificationTransportService(idTransportType);
await notificationTransportService.SendAsync(notification, cancellationToken);
await notificationRepository.UpdateAsync(notification,
cancellationToken);
}
/// <summary>
/// Обновление уведомления
/// </summary>
/// <param name="idNotification"></param>
/// <param name="isRead"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task UpdateNotificationAsync(int idNotification,
bool isRead,
CancellationToken cancellationToken)
{
var notification = await notificationRepository.GetOrDefaultAsync(idNotification,
cancellationToken)
?? throw new ArgumentInvalidException("Уведомление не найдено", nameof(idNotification));
if (isRead)
{
if (notification.SentDate == null)
throw new ArgumentInvalidException("Уведомление не может быть прочитано", nameof(isRead));
notification.SentDate = DateTime.UtcNow;
}
await notificationRepository.UpdateAsync(notification,
cancellationToken);
}
/// <summary>
/// Отправка уведомлений, которые не были отправлены
/// </summary>
/// <param name="idUser"></param>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task ResendNotificationAsync(int idUser,
NotificationRequest request,
CancellationToken cancellationToken)
{
if (!request.IdTransportType.HasValue)
throw new ArgumentInvalidException("Id типа доставки уведомления должен иметь значение",
nameof(request.IdTransportType));
var result = await notificationRepository.GetNotificationsAsync(idUser,
request,
cancellationToken);
var notificationTransportService = GetNotificationTransportService(request.IdTransportType.Value);
await notificationTransportService.SendRangeAsync(result.Items,
cancellationToken);
await notificationRepository.UpdateRangeAsync(result.Items,
cancellationToken);
}
private INotificationTransportService GetNotificationTransportService(int idTransportType)
{
var notificationTransportService = notificationTransportServices
.FirstOrDefault(s => s.IdTransportType == idTransportType)
?? throw new ArgumentInvalidException("Доставщик уведомлений не найден", nameof(idTransportType));
return notificationTransportService;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,85 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace AsbCloudDb.Migrations
{
public partial class Add_Notification : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "t_notification_category",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
name = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_t_notification_category", x => x.id);
},
comment: "Категории уведомлений");
migrationBuilder.CreateTable(
name: "t_notification",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
id_user = table.Column<int>(type: "integer", nullable: false, comment: "Id получателя"),
id_notification_category = table.Column<int>(type: "integer", nullable: false, comment: "Id категории уведомления"),
title = table.Column<string>(type: "text", nullable: false, comment: "Заголовок уведомления"),
message = table.Column<string>(type: "text", nullable: false, comment: "Сообщение уведомления"),
time_to_life = table.Column<TimeSpan>(type: "interval", nullable: false, comment: "Время жизни уведомления"),
sent_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "Дата отправки уведомления"),
notification_state = table.Column<string>(type: "text", nullable: false, comment: "Состояние уведомления"),
notification_transport = table.Column<string>(type: "text", nullable: false, comment: "Метод доставки уведомления")
},
constraints: table =>
{
table.PrimaryKey("PK_t_notification", x => x.id);
table.ForeignKey(
name: "FK_t_notification_t_notification_category_id_notification_cate~",
column: x => x.id_notification_category,
principalTable: "t_notification_category",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_t_notification_t_user_id_user",
column: x => x.id_user,
principalTable: "t_user",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
},
comment: "Уведомления");
migrationBuilder.InsertData(
table: "t_notification_category",
columns: new[] { "id", "name" },
values: new object[] { 1, "Системные уведомления" });
migrationBuilder.CreateIndex(
name: "IX_t_notification_id_notification_category",
table: "t_notification",
column: "id_notification_category");
migrationBuilder.CreateIndex(
name: "IX_t_notification_id_user",
table: "t_notification",
column: "id_user");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "t_notification");
migrationBuilder.DropTable(
name: "t_notification_category");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,95 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AsbCloudDb.Migrations
{
public partial class Update_Notification : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "notification_state",
table: "t_notification");
migrationBuilder.DropColumn(
name: "notification_transport",
table: "t_notification");
migrationBuilder.DropColumn(
name: "time_to_life",
table: "t_notification");
migrationBuilder.AddColumn<int>(
name: "id_transport_type",
table: "t_notification",
type: "integer",
nullable: false,
defaultValue: 0,
comment: "Id типа доставки уведомления");
migrationBuilder.AddColumn<DateTime>(
name: "read_date",
table: "t_notification",
type: "timestamp with time zone",
nullable: true,
comment: "Дата прочтения уведомления");
migrationBuilder.AlterColumn<int>(
name: "id_category",
table: "t_help_page",
type: "integer",
nullable: false,
comment: "Id категории файла",
oldClrType: typeof(int),
oldType: "integer",
oldComment: "id категории файла");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "id_transport_type",
table: "t_notification");
migrationBuilder.DropColumn(
name: "read_date",
table: "t_notification");
migrationBuilder.AddColumn<string>(
name: "notification_state",
table: "t_notification",
type: "text",
nullable: false,
defaultValue: "",
comment: "Состояние уведомления");
migrationBuilder.AddColumn<string>(
name: "notification_transport",
table: "t_notification",
type: "text",
nullable: false,
defaultValue: "",
comment: "Метод доставки уведомления");
migrationBuilder.AddColumn<TimeSpan>(
name: "time_to_life",
table: "t_notification",
type: "interval",
nullable: false,
defaultValue: new TimeSpan(0, 0, 0, 0, 0),
comment: "Время жизни уведомления");
migrationBuilder.AlterColumn<int>(
name: "id_category",
table: "t_help_page",
type: "integer",
nullable: false,
comment: "id категории файла",
oldClrType: typeof(int),
oldType: "integer",
oldComment: "Id категории файла");
}
}
}

View File

@ -1159,6 +1159,91 @@ namespace AsbCloudDb.Migrations
});
});
modelBuilder.Entity("AsbCloudDb.Model.Notification", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("IdNotificationCategory")
.HasColumnType("integer")
.HasColumnName("id_notification_category")
.HasComment("Id категории уведомления");
b.Property<int>("IdTransportType")
.HasColumnType("integer")
.HasColumnName("id_transport_type")
.HasComment("Id типа доставки уведомления");
b.Property<int>("IdUser")
.HasColumnType("integer")
.HasColumnName("id_user")
.HasComment("Id получателя");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text")
.HasColumnName("message")
.HasComment("Сообщение уведомления");
b.Property<DateTime?>("ReadDate")
.HasColumnType("timestamp with time zone")
.HasColumnName("read_date")
.HasComment("Дата прочтения уведомления");
b.Property<DateTime?>("SentDate")
.HasColumnType("timestamp with time zone")
.HasColumnName("sent_date")
.HasComment("Дата отправки уведомления");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text")
.HasColumnName("title")
.HasComment("Заголовок уведомления");
b.HasKey("Id");
b.HasIndex("IdNotificationCategory");
b.HasIndex("IdUser");
b.ToTable("t_notification");
b.HasComment("Уведомления");
});
modelBuilder.Entity("AsbCloudDb.Model.NotificationCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasColumnName("name");
b.HasKey("Id");
b.ToTable("t_notification_category");
b.HasComment("Категории уведомлений");
b.HasData(
new
{
Id = 1,
Name = "Системные уведомления"
});
});
modelBuilder.Entity("AsbCloudDb.Model.OperationValue", b =>
{
b.Property<int>("Id")
@ -7567,6 +7652,25 @@ namespace AsbCloudDb.Migrations
b.Navigation("Well");
});
modelBuilder.Entity("AsbCloudDb.Model.Notification", b =>
{
b.HasOne("AsbCloudDb.Model.NotificationCategory", "NotificationCategory")
.WithMany("Notifications")
.HasForeignKey("IdNotificationCategory")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AsbCloudDb.Model.User", "User")
.WithMany()
.HasForeignKey("IdUser")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("NotificationCategory");
b.Navigation("User");
});
modelBuilder.Entity("AsbCloudDb.Model.OperationValue", b =>
{
b.HasOne("AsbCloudDb.Model.WellOperationCategory", "OperationCategory")
@ -8159,6 +8263,11 @@ namespace AsbCloudDb.Migrations
b.Navigation("Measures");
});
modelBuilder.Entity("AsbCloudDb.Model.NotificationCategory", b =>
{
b.Navigation("Notifications");
});
modelBuilder.Entity("AsbCloudDb.Model.Permission", b =>
{
b.Navigation("RelationUserRolePermissions");

View File

@ -74,8 +74,9 @@ namespace AsbCloudDb.Model
public static int ReferenceCount => referenceCount;
public DbSet<Faq> Faqs => Set<Faq>();
public DbSet<HelpPage> HelpPages => Set<HelpPage>();
public DbSet<Notification> Notifications => Set<Notification>();
public DbSet<NotificationCategory> NotificationCategories => Set<NotificationCategory>();
public AsbCloudDbContext() : base()
{

View File

@ -24,7 +24,8 @@ namespace AsbCloudDb.Model.DefaultData
{ typeof(WellType), new EntityFillerWellType()},
{ typeof(MeasureCategory), new EntityFillerMeasureCategory()},
{ typeof(CompanyType), new EntityFillerCompanyType()},
{ typeof(AsbCloudDb.Model.Subsystems.Subsystem), new EntityFillerSubsystem() },
{ typeof(Subsystems.Subsystem), new EntityFillerSubsystem() },
{ typeof(NotificationCategory), new EntityNotificationCategory()},
};
return fillers;
}

View File

@ -0,0 +1,9 @@
namespace AsbCloudDb.Model.DefaultData;
public class EntityNotificationCategory : EntityFiller<NotificationCategory>
{
public override NotificationCategory[] GetData() => new NotificationCategory[]
{
new() { Id = 1, Name = "Системные уведомления" }
};
}

View File

@ -68,7 +68,8 @@ namespace AsbCloudDb.Model
DbSet<Record60> Record60 { get; }
DbSet<Record61> Record61 { get; }
DbSet<HelpPage> HelpPages { get; }
DbSet<Notification> Notifications { get; }
DbSet<NotificationCategory> NotificationCategories { get; }
DatabaseFacade Database { get; }
Task<int> RefreshMaterializedViewAsync(string mwName, CancellationToken token);

View File

@ -0,0 +1,41 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace AsbCloudDb.Model;
[Table("t_notification"), Comment("Уведомления")]
public class Notification : IId
{
[Key]
[Column("id")]
public int Id { get; set; }
[Column("id_user"), Comment("Id получателя")]
public int IdUser { get; set; }
[Column("id_notification_category"), Comment("Id категории уведомления")]
public int IdNotificationCategory { get; set; }
[Column("title"), Comment("Заголовок уведомления")]
public string Title { get; set; } = null!;
[Column("message"), Comment("Сообщение уведомления")]
public string Message { get; set; } = null!;
[Column("sent_date"), Comment("Дата отправки уведомления")]
public DateTime? SentDate { get; set; }
[Column("read_date"), Comment("Дата прочтения уведомления")]
public DateTime? ReadDate { get; set; }
[Column("id_transport_type"), Comment("Id типа доставки уведомления")]
public int IdTransportType { get; set; }
[ForeignKey(nameof(IdNotificationCategory))]
public virtual NotificationCategory NotificationCategory { get; set; } = null!;
[ForeignKey(nameof(IdUser))]
public virtual User User { get; set; } = null!;
}

View File

@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace AsbCloudDb.Model;
[Table("t_notification_category"), Comment("Категории уведомлений")]
public class NotificationCategory : IId
{
[Key]
[Column("id")]
public int Id { get; set; }
[Column("name")]
public string Name { get; set; } = null!;
[InverseProperty(nameof(Notification.NotificationCategory))]
public virtual ICollection<Notification> Notifications { get; set; } = null!;
}

View File

@ -23,6 +23,7 @@ using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using AsbCloudApp.Services.Notifications;
namespace AsbCloudInfrastructure
{
@ -82,6 +83,11 @@ namespace AsbCloudInfrastructure
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<WellFinalDocumentDto, WellFinalDocument>();
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<NotificationDto, Notification>()
.Ignore(dst => dst.NotificationCategory,
dst => dst.User);
}
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
@ -143,6 +149,11 @@ namespace AsbCloudInfrastructure
services.AddTransient<IGtrRepository, GtrWitsRepository>();
services.AddTransient<NotificationService>();
services.AddTransient<INotificationRepository, NotificationRepository>();
services.AddTransient<ICrudRepository<NotificationCategoryDto>, CrudCacheRepositoryBase<NotificationCategoryDto,
NotificationCategory>>();
// admin crud services:
services.AddTransient<ICrudRepository<TelemetryDto>, CrudCacheRepositoryBase<TelemetryDto, Telemetry>>(s =>
new CrudCacheRepositoryBase<TelemetryDto, Telemetry>(

View File

@ -0,0 +1,105 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
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;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
namespace AsbCloudInfrastructure.Repository;
public class NotificationRepository : CrudCacheRepositoryBase<NotificationDto, Notification>, INotificationRepository
{
private static IQueryable<Notification> MakeQueryNotification(DbSet<Notification> dbSet)
=> dbSet.Include(n => n.NotificationCategory)
.AsNoTracking();
public NotificationRepository(IAsbCloudDbContext dbContext, IMemoryCache memoryCache)
: base(dbContext, memoryCache, MakeQueryNotification)
{
}
public async Task<int> UpdateRangeAsync(IEnumerable<NotificationDto> notifications, CancellationToken cancellationToken)
{
if (!notifications.Any())
return 0;
var ids = notifications.Select(d => d.Id).ToArray();
var existingEntities = await dbSet
.Where(d => ids.Contains(d.Id))
.AsNoTracking()
.Select(d => d.Id)
.ToArrayAsync(cancellationToken);
if (ids.Length > existingEntities.Length)
return ICrudRepository<SetpointsRequestDto>.ErrorIdNotFound;
var entities = notifications.Select(Convert);
dbContext.Notifications.UpdateRange(entities);
var result = await dbContext.SaveChangesAsync(cancellationToken);
DropCache();
return result;
}
public async Task<PaginationContainer<NotificationDto>> GetNotificationsAsync(int idUser,
NotificationRequest request,
CancellationToken cancellationToken)
{
var skip = request.Skip ?? 0;
var take = request.Take ?? 10;
var query = BuildQuery(idUser, request);
var result = new PaginationContainer<NotificationDto>()
{
Skip = skip,
Take = take,
Count = await query.CountAsync(cancellationToken),
};
if (result.Count < skip)
return result;
result.Items = await query
.SortBy(request.SortFields)
.Skip(skip)
.Take(take)
.AsNoTracking()
.Select(x => x.Adapt<NotificationDto>())
.ToListAsync(cancellationToken);
return result;
}
private IQueryable<Notification> BuildQuery(int idUser,
NotificationRequest request)
{
var query = dbContext.Notifications
.Include(x => x.NotificationCategory)
.Where(n => n.IdUser == idUser);
if (request.IsSent.HasValue)
{
if(request.IsSent.Value)
query = query.Where(n => n.SentDate != null);
else
query = query.Where(n => n.SentDate == null);
}
if (request.IdTransportType.HasValue)
query = query.Where(n => n.IdTransportType == request.IdTransportType);
return query;
}
}

View File

@ -0,0 +1,148 @@
using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Data;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Requests;
using AsbCloudApp.Services.Notifications;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace AsbCloudWebApi.Controllers;
/// <summary>
/// Уведомления
/// </summary>
[ApiController]
[Authorize]
[Route("api/notification")]
public class NotificationController : ControllerBase
{
private readonly NotificationService notificationService;
private readonly INotificationRepository notificationRepository;
public NotificationController(NotificationService notificationService,
INotificationRepository notificationRepository)
{
this.notificationService = notificationService;
this.notificationRepository = notificationRepository;
}
/// <summary>
/// Отправка уведомления
/// </summary>
/// <param name="idUser">Id пользователя</param>
/// <param name="idNotificationCategory">Id категории уведомления. Допустимое значение параметра: 1</param>
/// <param name="title">Заголовок уведомления</param>
/// <param name="message">Сообщение уведомления</param>
/// <param name="idNotificationTransport">Id типа доставки уведомления. Допустимое значение: 0</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPost]
[Route("send")]
public async Task<IActionResult> SendAsync([Required] int idUser,
[Required]
[Range(minimum: 1, maximum: 1, ErrorMessage = "Id категории уведомления недоступно. Допустимые: 1")]
int idNotificationCategory,
[Required] string title,
[Required] string message,
[Required]
[Range(minimum: 0, maximum: 0, ErrorMessage = "Id способа отправки уведомления недоступно. Допустимые: 0")]
int idNotificationTransport,
CancellationToken cancellationToken)
{
await notificationService.NotifyAsync(idUser,
idNotificationCategory,
title,
message,
idNotificationTransport,
cancellationToken);
return Ok();
}
/// <summary>
/// Обновление уведомления
/// </summary>
/// <param name="idNotification">Id уведомления</param>
/// <param name="isRead">Прочитано ли уведомление</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPut]
[Route("update")]
public async Task<IActionResult> UpdateAsync([Required] int idNotification,
[Required] bool isRead,
CancellationToken cancellationToken)
{
await notificationService.UpdateNotificationAsync(idNotification,
isRead,
cancellationToken);
return Ok();
}
/// <summary>
/// Получение уведомления по Id
/// </summary>
/// <param name="idNotification">Id уведомления</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet]
[Route("get/{idNotification}")]
[ProducesResponseType(typeof(NotificationDto), (int)System.Net.HttpStatusCode.OK)]
public async Task<IActionResult> GetAsync([Required] int idNotification,
CancellationToken cancellationToken)
{
var notification = await notificationRepository.GetOrDefaultAsync(idNotification, cancellationToken);
if (notification is null)
{
return BadRequest(ArgumentInvalidException.MakeValidationError(nameof(idNotification),
"Уведомление не найдено"));
}
return Ok(notification);
}
/// <summary>
/// Получение списка уведомлений
/// </summary>
/// <param name="request">Параметры запроса</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet]
[Route("getList")]
[ProducesResponseType(typeof(PaginationContainer<NotificationDto>), (int)System.Net.HttpStatusCode.OK)]
public async Task<IActionResult> GetListAsync([FromQuery] NotificationRequest request,
CancellationToken cancellationToken)
{
int? idUser = User.GetUserId();
if (!idUser.HasValue)
return Forbid();
var result = await notificationRepository.GetNotificationsAsync(idUser.Value,
request,
cancellationToken);
return Ok(result);
}
/// <summary>
/// Удаление уведомления
/// </summary>
/// <param name="idNotification">Id уведомления</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpDelete]
[Route("delete")]
public async Task<IActionResult> DeleteAsync([Required] int idNotification,
CancellationToken cancellationToken)
{
await notificationRepository.DeleteAsync(idNotification,
cancellationToken);
return Ok();
}
}

View File

@ -12,6 +12,9 @@ using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using AsbCloudApp.Services.Notifications;
using AsbCloudWebApi.SignalR.Services;
using Microsoft.OpenApi.Any;
namespace AsbCloudWebApi
{
@ -21,6 +24,7 @@ namespace AsbCloudWebApi
{
services.AddSwaggerGen(c =>
{
c.MapType<TimeSpan>(() => new OpenApiSchema { Type = "string", Example = new OpenApiString("0.00:00:00") });
c.MapType<DateOnly>(() => new OpenApiSchema { Type = "string", Format = "date" });
c.MapType<JsonValue>(() => new OpenApiSchema
{
@ -131,5 +135,10 @@ namespace AsbCloudWebApi
};
});
}
public static void AddNotificationTransportServices(this IServiceCollection services)
{
services.AddTransient<INotificationTransportService, SignalRNotificationTransportService>();
}
}
}

View File

@ -0,0 +1,19 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
namespace AsbCloudWebApi.SignalR;
public abstract class BaseHub : Hub
{
public virtual Task AddToGroup(string groupName) =>
Groups.AddToGroupAsync(Context.ConnectionId, groupName);
public virtual Task RemoveFromGroup(string groupName) =>
Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
}
public abstract class BaseHub<T> : BaseHub
where T : class
{
}

View File

@ -0,0 +1,53 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Requests;
using AsbCloudApp.Services.Notifications;
using AsbCloudWebApi.SignalR.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace AsbCloudWebApi.SignalR;
[Authorize]
public class NotificationHub : BaseHub
{
private readonly ConnectionManagerService connectionManagerService;
private readonly NotificationService notificationService;
public NotificationHub(ConnectionManagerService connectionManagerService,
NotificationService notificationService)
{
this.connectionManagerService = connectionManagerService;
this.notificationService = notificationService;
}
public override async Task OnConnectedAsync()
{
var idUser = Context.User?.GetUserId();
if (!idUser.HasValue)
return;
string connectionId = Context.ConnectionId;
connectionManagerService.AddOrUpdateConnection(idUser.Value, connectionId);
await base.OnConnectedAsync();
await notificationService.ResendNotificationAsync(idUser.Value,
new NotificationRequest { IsSent = false, IdTransportType = 0},
CancellationToken.None);
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var idUser = Context.User?.GetUserId();
if (!idUser.HasValue)
return;
connectionManagerService.RemoveConnection(idUser.Value);
await base.OnDisconnectedAsync(exception);
}
}

View File

@ -1,6 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace AsbCloudWebApi.SignalR
{
@ -8,12 +6,8 @@ namespace AsbCloudWebApi.SignalR
// https://docs.microsoft.com/ru-ru/aspnet/core/signalr/introduction?view=aspnetcore-5.0
[Authorize]
public class ReportsHub : Hub<IReportHubClient>
public class ReportsHub : BaseHub<IReportHubClient>
{
public Task AddToGroup(string groupName)
=> Groups.AddToGroupAsync(Context.ConnectionId, groupName);
public Task RemoveFromGroup(string groupName)
=> Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
}
}

View File

@ -0,0 +1,25 @@
using System.Collections.Concurrent;
namespace AsbCloudWebApi.SignalR.Services;
public class ConnectionManagerService
{
private readonly ConcurrentDictionary<int, string> connections = new();
public void AddOrUpdateConnection(int userId, string connectionId)
{
connections.AddOrUpdate(userId, connectionId,
(key, existingConnectionId) => connectionId);
}
public void RemoveConnection(int userId)
{
connections.TryRemove(userId, out _);
}
public string? GetConnectionIdByUserId(int userId)
{
connections.TryGetValue(userId, out var connectionId);
return connectionId;
}
}

View File

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Data;
using AsbCloudApp.Services.Notifications;
using Microsoft.AspNetCore.SignalR;
namespace AsbCloudWebApi.SignalR.Services;
public class SignalRNotificationTransportService : INotificationTransportService
{
private readonly ConnectionManagerService connectionManagerService;
private readonly IHubContext<NotificationHub> notificationHubContext;
public SignalRNotificationTransportService(ConnectionManagerService connectionManagerService,
IHubContext<NotificationHub> notificationHubContext)
{
this.connectionManagerService = connectionManagerService;
this.notificationHubContext = notificationHubContext;
}
public int IdTransportType => 0;
public async Task SendAsync(NotificationDto notification,
CancellationToken cancellationToken)
{
const string method = "receiveNotifications";
var connectionId = connectionManagerService.GetConnectionIdByUserId(notification.IdUser);
if (!string.IsNullOrWhiteSpace(connectionId))
{
notification.SentDate = DateTime.UtcNow;
await notificationHubContext.Clients.Client(connectionId)
.SendAsync(method,
notification,
cancellationToken);
}
}
public Task SendRangeAsync(IEnumerable<NotificationDto> notifications,
CancellationToken cancellationToken)
{
var tasks = notifications
.Select(notification => SendAsync(notification, cancellationToken));
return Task.WhenAll(tasks);
}
}

View File

@ -1,6 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace AsbCloudWebApi.SignalR
{
@ -8,12 +6,8 @@ namespace AsbCloudWebApi.SignalR
// https://docs.microsoft.com/ru-ru/aspnet/core/signalr/introduction?view=aspnetcore-5.0
[Authorize]
public class TelemetryHub : Hub
public class TelemetryHub : BaseHub
{
public Task AddToGroup(string groupName)
=> Groups.AddToGroupAsync(Context.ConnectionId, groupName.ToString());
public Task RemoveFromGroup(string groupName)
=> Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
}
}

View File

@ -2,6 +2,7 @@ using AsbCloudInfrastructure;
using AsbCloudWebApi.Converters;
using AsbCloudWebApi.Middlewares;
using AsbCloudWebApi.SignalR;
using AsbCloudWebApi.SignalR.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
@ -42,9 +43,12 @@ namespace AsbCloudWebApi
services.AddInfrastructure(Configuration);
services.AddNotificationTransportServices();
services.AddJWTAuthentication();
services.AddSignalR();
services.AddSignalR()
.Services.AddSingleton<ConnectionManagerService>();
services.AddCors(options =>
{
@ -147,6 +151,7 @@ namespace AsbCloudWebApi
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<NotificationHub>("/hubs/notifications");
endpoints.MapHub<TelemetryHub>("/hubs/telemetry");
endpoints.MapHub<ReportsHub>("/hubs/reports");
});

View File

@ -10,7 +10,7 @@ internal class Program
{
var connectionBuilder = new HubConnectionBuilder();
var connection = connectionBuilder
.WithUrl("http://test.digitaldrilling.ru/hubs/telemetry", connectionOptions => {
.WithUrl("http://localhost:5000/hubs/notifications", connectionOptions => {
connectionOptions.AccessTokenProvider = AccessTokenProvider;
})
.WithAutomaticReconnect()
@ -25,9 +25,10 @@ internal class Program
Console.WriteLine("connecting");
connection.StartAsync().Wait();
Console.WriteLine("AddToGroup");
connection.SendCoreAsync("AddToGroup", new object[] { "well_1" }).Wait();
var subsction = connection.On<object>("UpdateProcessMap", (str1) => {
//Console.WriteLine("OnConnected");
//connection.SendCoreAsync("OnConnected", new object[] { }, CancellationToken.None).Wait();
var subsction = connection.On<object>("receiveNotifications", (str1) => {
Console.WriteLine(str1);
} );