forked from ddrilling/AsbCloudServer
Merge branch 'dev' into feature/#37115045-subsystems-operation-time
This commit is contained in:
commit
9936c75fa9
29
AsbCloudApp/Data/SAUB/TelemetryEventDto.cs
Normal file
29
AsbCloudApp/Data/SAUB/TelemetryEventDto.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
namespace AsbCloudApp.Data.SAUB;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// dto для события телеметрии
|
||||||
|
/// </summary>
|
||||||
|
public class TelemetryEventDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ключ события
|
||||||
|
/// </summary>
|
||||||
|
public object Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ключ категории
|
||||||
|
/// </summary>
|
||||||
|
public int IdCategory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// шаблон сообщения
|
||||||
|
/// </summary>
|
||||||
|
public string MessageTemplate { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// метод формирования текста сообщения по шаблону
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">аргументы, которые подставляются в шаблон сообщения</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string MakeMessageText(string?[] args) => string.Format(MessageTemplate, args);
|
||||||
|
}
|
@ -51,4 +51,9 @@ public class TelemetryMessageDto : IId
|
|||||||
/// аргумент №3 для подстановки в шаблон сообщения
|
/// аргумент №3 для подстановки в шаблон сообщения
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Arg3 { get; set; }
|
public string? Arg3 { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ключ телеметрии
|
||||||
|
/// </summary>
|
||||||
|
public int IdTelemetry { get; set; }
|
||||||
}
|
}
|
||||||
|
28
AsbCloudApp/Data/StatCriticalMessageDto.cs
Normal file
28
AsbCloudApp/Data/StatCriticalMessageDto.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
namespace AsbCloudApp.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// dto для отображения статистики сообщений
|
||||||
|
/// </summary>
|
||||||
|
public class StatCriticalMessageDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ключ скважины
|
||||||
|
/// </summary>
|
||||||
|
public int IdWell { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ключ категории
|
||||||
|
/// </summary>
|
||||||
|
public int? IdCategory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// шаблон сообщения
|
||||||
|
/// </summary>
|
||||||
|
public string? MessageTemplate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// количество сообщений
|
||||||
|
/// </summary>
|
||||||
|
public int MessagesCount { get; set; }
|
||||||
|
}
|
30
AsbCloudApp/Repositories/IEventRepository.cs
Normal file
30
AsbCloudApp/Repositories/IEventRepository.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using AsbCloudApp.Data.SAUB;
|
||||||
|
using AsbCloudApp.Requests;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace AsbCloudApp.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Репозиторий по работе с событиями
|
||||||
|
/// </summary>
|
||||||
|
public interface IEventRepository
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// получение списка событий по параметрам запроса
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">параметры запроса</param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<IEnumerable<TelemetryEventDto>> GetAsync(TelemetryEventRequest request, CancellationToken token);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сохранить. Добавить или заменить.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uid"></param>
|
||||||
|
/// <param name="dtos"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task UpsertAsync(string uid, IEnumerable<EventDto> dtos, CancellationToken token);
|
||||||
|
}
|
41
AsbCloudApp/Repositories/IMessageRepository.cs
Normal file
41
AsbCloudApp/Repositories/IMessageRepository.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using AsbCloudApp.Data;
|
||||||
|
using AsbCloudApp.Data.SAUB;
|
||||||
|
using AsbCloudApp.Requests;
|
||||||
|
using AsbCloudApp.Services;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace AsbCloudApp.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Репозиторий по работе с сообщениями панели оператора
|
||||||
|
/// </summary>
|
||||||
|
public interface IMessageRepository : ITelemetryDataEditorService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получить сообщения по параметрам
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<PaginationContainer<MessageDto>> GetPaginatedMessagesAsync(MessageTelemetryRequest request, CancellationToken token);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получить сообщения по параметрам
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<IEnumerable<TelemetryMessageDto>> GetMessagesAsync(MessageTelemetryRequest request, CancellationToken token);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Метод для сохранения сообщения от панели
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uid"></param>
|
||||||
|
/// <param name="dtos"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task InsertAsync(string uid, IEnumerable<TelemetryMessageDto> dtos,
|
||||||
|
CancellationToken token);
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
|
using AsbCloudApp.Data;
|
||||||
|
using AsbCloudApp.Data.SAUB;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace AsbCloudApp.Requests;
|
namespace AsbCloudApp.Requests;
|
||||||
|
|
||||||
@ -11,7 +14,7 @@ public class MessageRequestBase : RequestBase
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// категория
|
/// категория
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<int>? Categoryids { get; set; }
|
public IEnumerable<int>? IdsCategories { get; set; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -29,29 +32,21 @@ public class MessageRequestBase : RequestBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? SearchString { get; set; }
|
public string? SearchString { get; set; }
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// параметры для запроса списка сообщений (с id скважины)
|
|
||||||
/// </summary>
|
|
||||||
public class MessageRequest : MessageRequestBase
|
|
||||||
{
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// id скважины
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int IdWell { get; set; }
|
public MessageRequestBase()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// параметры для запроса списка сообщений (с id скважины)
|
/// копирующий конструктор
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request"></param>
|
/// <param name="request"></param>
|
||||||
/// <param name="idWell"></param>
|
public MessageRequestBase(MessageRequestBase request)
|
||||||
public MessageRequest(MessageRequestBase request, int idWell)
|
|
||||||
{
|
{
|
||||||
this.IdWell = idWell;
|
this.IdsCategories = request.IdsCategories;
|
||||||
|
|
||||||
this.Categoryids = request.Categoryids;
|
|
||||||
this.Begin = request.Begin;
|
this.Begin = request.Begin;
|
||||||
this.End = request.End;
|
this.End = request.End;
|
||||||
this.SearchString = request.SearchString;
|
this.SearchString = request.SearchString;
|
||||||
@ -62,3 +57,63 @@ public class MessageRequest : MessageRequestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// параметры для запроса списка сообщений (с ids скважин)
|
||||||
|
/// </summary>
|
||||||
|
public class MessageRequest : MessageRequestBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ids скважин
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<int> IdsWell { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc/>
|
||||||
|
/// </summary>
|
||||||
|
public MessageRequest()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// параметры для запроса списка сообщений (с ids скважин)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="idsWell"></param>
|
||||||
|
public MessageRequest(MessageRequestBase request, IEnumerable<int> idsWell) : base(request)
|
||||||
|
{
|
||||||
|
this.IdsWell = idsWell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// параметры запроса для получения списка сообщений телеметрии
|
||||||
|
/// </summary>
|
||||||
|
public class MessageTelemetryRequest : MessageRequestBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// события
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<TelemetryEventDto> Events { get; set; }= Enumerable.Empty<TelemetryEventDto>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// телеметрии
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<TelemetryDto> Telemetries { get; set; } = Enumerable.Empty<TelemetryDto>();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public MessageTelemetryRequest(
|
||||||
|
MessageRequestBase request,
|
||||||
|
IEnumerable<TelemetryEventDto> events,
|
||||||
|
IEnumerable<TelemetryDto> telemetries) : base(request)
|
||||||
|
{
|
||||||
|
Events = events;
|
||||||
|
Telemetries = telemetries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public MessageTelemetryRequest(MessageRequestBase request)
|
||||||
|
: base(request) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
28
AsbCloudApp/Requests/TelemetryEventRequest.cs
Normal file
28
AsbCloudApp/Requests/TelemetryEventRequest.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace AsbCloudApp.Requests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// параметры запроса для получения событий телеметрии
|
||||||
|
/// </summary>
|
||||||
|
public class TelemetryEventRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ключи телеметрии
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<int> IdsTelemetries { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ключи категорий
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<int>? IdsCategories { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// строка поиска
|
||||||
|
/// </summary>
|
||||||
|
public string? SearchString { get; set; }
|
||||||
|
}
|
@ -1,22 +0,0 @@
|
|||||||
using AsbCloudApp.Data.SAUB;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace AsbCloudApp.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Сервис сохранения списка сообщений от панелей
|
|
||||||
/// </summary>
|
|
||||||
public interface IEventService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Сохранить. Добавить или заменить.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uid"></param>
|
|
||||||
/// <param name="dtos"></param>
|
|
||||||
/// <param name="token"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
Task UpsertAsync(string uid, IEnumerable<EventDto> dtos,
|
|
||||||
CancellationToken token = default);
|
|
||||||
}
|
|
@ -1,5 +1,4 @@
|
|||||||
using AsbCloudApp.Data;
|
using AsbCloudApp.Data;
|
||||||
using AsbCloudApp.Data.SAUB;
|
|
||||||
using AsbCloudApp.Requests;
|
using AsbCloudApp.Requests;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -8,25 +7,24 @@ using System.Threading.Tasks;
|
|||||||
namespace AsbCloudApp.Services;
|
namespace AsbCloudApp.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Сервис сообщений панели оператора
|
/// Сервис сообщений
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IMessageService : ITelemetryDataEditorService
|
public interface IMessageService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получить сообщения по параметрам
|
/// Получить статистику сообщений по параметрам запроса
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request"></param>
|
/// <param name="request"></param>
|
||||||
/// <param name="token"></param>
|
/// <param name="token"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<PaginationContainer<MessageDto>> GetMessagesAsync(MessageRequest request, CancellationToken token);
|
Task<IEnumerable<StatCriticalMessageDto>> GetStatAsync(MessageRequest request, CancellationToken token);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Метод для сохранения сообщения от панели
|
/// Получить PaginationContainer с сообщениями по параметрам
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="uid"></param>
|
/// <param name="request"></param>
|
||||||
/// <param name="dtos"></param>
|
|
||||||
/// <param name="token"></param>
|
/// <param name="token"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task InsertAsync(string uid, IEnumerable<TelemetryMessageDto> dtos,
|
Task<PaginationContainer<MessageDto>> GetPaginatedMessagesAsync(MessageRequest request, CancellationToken token);
|
||||||
CancellationToken token);
|
|
||||||
}
|
}
|
@ -54,6 +54,12 @@ public interface ITelemetryService
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
TelemetryBaseDto? GetOrDefaultTelemetryByIdWell(int idWell);
|
TelemetryBaseDto? GetOrDefaultTelemetryByIdWell(int idWell);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// получить список телеметрии по ключам скважин
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idsWells">ключи скважин</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IEnumerable<TelemetryDto> GetOrDefaultTelemetriesByIdsWells(IEnumerable<int> idsWells);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// получить диапазон дат за которые есть данные
|
/// получить диапазон дат за которые есть данные
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
12388
AsbCloudDb/Migrations/20240918050937_Add_Permission_Critical_Message.Designer.cs
generated
Normal file
12388
AsbCloudDb/Migrations/20240918050937_Add_Permission_Critical_Message.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,38 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace AsbCloudDb.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Add_Permission_Critical_Message : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "t_permission",
|
||||||
|
columns: new[] { "id", "description", "name" },
|
||||||
|
values: new object[] { 533, "Разрешение просматривать критические сообщения", "CriticalMessage.get" });
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "t_relation_user_role_permission",
|
||||||
|
columns: new[] { "id_permission", "id_user_role" },
|
||||||
|
values: new object[] { 533, 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "t_relation_user_role_permission",
|
||||||
|
keyColumns: new[] { "id_permission", "id_user_role" },
|
||||||
|
keyValues: new object[] { 533, 1 });
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "t_permission",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 533);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2632,6 +2632,12 @@ namespace AsbCloudDb.Migrations
|
|||||||
Id = 532,
|
Id = 532,
|
||||||
Description = "Разрешение просматривать информацию о телеметрии",
|
Description = "Разрешение просматривать информацию о телеметрии",
|
||||||
Name = "Version.get"
|
Name = "Version.get"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 533,
|
||||||
|
Description = "Разрешение просматривать критические сообщения",
|
||||||
|
Name = "CriticalMessage.get"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -6377,6 +6383,11 @@ namespace AsbCloudDb.Migrations
|
|||||||
{
|
{
|
||||||
IdUserRole = 1,
|
IdUserRole = 1,
|
||||||
IdPermission = 532
|
IdPermission = 532
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
IdUserRole = 1,
|
||||||
|
IdPermission = 533
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -165,7 +165,8 @@ namespace AsbCloudDb.Model.DefaultData
|
|||||||
new() { Id = 530, Name = "WellSectionPlan.edit", Description = "Разрешение на редактирование плановой конструкции скважины"},
|
new() { Id = 530, Name = "WellSectionPlan.edit", Description = "Разрешение на редактирование плановой конструкции скважины"},
|
||||||
new() { Id = 531, Name = "WellSectionPlan.delete", Description = "Разрешение на удаление плановой конструкции скважины"},
|
new() { Id = 531, Name = "WellSectionPlan.delete", Description = "Разрешение на удаление плановой конструкции скважины"},
|
||||||
|
|
||||||
new() { Id = 532, Name = "Version.get", Description = "Разрешение просматривать информацию о телеметрии"}
|
new() { Id = 532, Name = "Version.get", Description = "Разрешение просматривать информацию о телеметрии"},
|
||||||
|
new() { Id = 533, Name = "CriticalMessage.get", Description = "Разрешение просматривать критические сообщения"}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
270
AsbCloudInfrastructure.Tests/Services/MessageServiceTest.cs
Normal file
270
AsbCloudInfrastructure.Tests/Services/MessageServiceTest.cs
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
using AsbCloudApp.Data;
|
||||||
|
using AsbCloudApp.Data.SAUB;
|
||||||
|
using AsbCloudApp.Repositories;
|
||||||
|
using AsbCloudApp.Requests;
|
||||||
|
using AsbCloudApp.Services;
|
||||||
|
using AsbCloudDb.Model;
|
||||||
|
using AsbCloudInfrastructure.Services;
|
||||||
|
using NSubstitute;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AsbCloudInfrastructure.Tests.Services;
|
||||||
|
|
||||||
|
public class MessageServiceTest
|
||||||
|
{
|
||||||
|
private static readonly List<TelemetryDto> telemetries = new List<TelemetryDto>()
|
||||||
|
{
|
||||||
|
new TelemetryDto()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
IdWell = 1,
|
||||||
|
TimeZone = new SimpleTimezoneDto()
|
||||||
|
{
|
||||||
|
Hours = 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new TelemetryDto()
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
IdWell = 2,
|
||||||
|
TimeZone = new SimpleTimezoneDto()
|
||||||
|
{
|
||||||
|
Hours = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly List<TelemetryEventDto> events = new List<TelemetryEventDto>()
|
||||||
|
{
|
||||||
|
new TelemetryEventDto()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
IdCategory = 1,
|
||||||
|
MessageTemplate = "Шаблон сообщения 1 категории 1"
|
||||||
|
},
|
||||||
|
new TelemetryEventDto()
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
IdCategory = 1,
|
||||||
|
MessageTemplate = "Шаблон сообщения 2 категории 1"
|
||||||
|
},
|
||||||
|
new TelemetryEventDto()
|
||||||
|
{
|
||||||
|
Id = 3,
|
||||||
|
IdCategory = 1,
|
||||||
|
MessageTemplate = "Шаблон сообщения 3 категории 1"
|
||||||
|
},
|
||||||
|
new TelemetryEventDto()
|
||||||
|
{
|
||||||
|
Id = 4,
|
||||||
|
IdCategory = 2,
|
||||||
|
MessageTemplate = "Шаблон сообщения 1 категории 2"
|
||||||
|
},
|
||||||
|
new TelemetryEventDto()
|
||||||
|
{
|
||||||
|
Id = 5,
|
||||||
|
IdCategory = 2,
|
||||||
|
MessageTemplate = "Шаблон сообщения 2 категории 2"
|
||||||
|
},
|
||||||
|
new TelemetryEventDto()
|
||||||
|
{
|
||||||
|
Id = 6,
|
||||||
|
IdCategory = 3,
|
||||||
|
MessageTemplate = "Шаблон сообщения 1 категории 3"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Структура:
|
||||||
|
/// Телеметрия 1:
|
||||||
|
/// Категория 1:
|
||||||
|
/// Событие 1:
|
||||||
|
/// Сообщение 1
|
||||||
|
/// Сообщение 2
|
||||||
|
/// Сообщение 3
|
||||||
|
/// Событие 2:
|
||||||
|
/// Сообщение 1
|
||||||
|
/// Сообщение 2
|
||||||
|
/// Сообщение 3
|
||||||
|
/// Событие 3:
|
||||||
|
/// Сообщение 1
|
||||||
|
/// Сообщение 2
|
||||||
|
/// Сообщение 3
|
||||||
|
/// Категория 2:
|
||||||
|
/// Событие 2:
|
||||||
|
/// Сообщение 1
|
||||||
|
/// Сообщение 2
|
||||||
|
/// Событие 3:
|
||||||
|
/// Сообщение 1
|
||||||
|
/// Сообщение 2
|
||||||
|
/// Телеметрия 2
|
||||||
|
/// Категория 3:
|
||||||
|
/// Событие 3:
|
||||||
|
/// Сообщение 1
|
||||||
|
/// Сообщение 2
|
||||||
|
/// Сообщение 3
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
private static readonly List<TelemetryMessageDto> messages = new List<TelemetryMessageDto>()
|
||||||
|
{
|
||||||
|
new TelemetryMessageDto()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
IdEvent = 1,
|
||||||
|
IdTelemetry = 1
|
||||||
|
},
|
||||||
|
new TelemetryMessageDto()
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
IdEvent = 1,
|
||||||
|
IdTelemetry = 1
|
||||||
|
},
|
||||||
|
new TelemetryMessageDto()
|
||||||
|
{
|
||||||
|
Id = 3,
|
||||||
|
IdEvent = 1,
|
||||||
|
IdTelemetry = 1
|
||||||
|
},
|
||||||
|
new TelemetryMessageDto()
|
||||||
|
{
|
||||||
|
Id = 4,
|
||||||
|
IdEvent = 2,
|
||||||
|
IdTelemetry = 1
|
||||||
|
},
|
||||||
|
new TelemetryMessageDto()
|
||||||
|
{
|
||||||
|
Id = 5,
|
||||||
|
IdEvent = 2,
|
||||||
|
IdTelemetry = 1
|
||||||
|
},
|
||||||
|
new TelemetryMessageDto()
|
||||||
|
{
|
||||||
|
Id = 6,
|
||||||
|
IdEvent = 2,
|
||||||
|
IdTelemetry = 1
|
||||||
|
},
|
||||||
|
new TelemetryMessageDto()
|
||||||
|
{
|
||||||
|
Id = 7,
|
||||||
|
IdEvent = 3,
|
||||||
|
IdTelemetry = 1
|
||||||
|
},
|
||||||
|
new TelemetryMessageDto()
|
||||||
|
{
|
||||||
|
Id = 8,
|
||||||
|
IdEvent = 3,
|
||||||
|
IdTelemetry = 1
|
||||||
|
},
|
||||||
|
new TelemetryMessageDto()
|
||||||
|
{
|
||||||
|
Id = 9,
|
||||||
|
IdEvent = 3,
|
||||||
|
IdTelemetry = 1
|
||||||
|
},
|
||||||
|
new TelemetryMessageDto()
|
||||||
|
{
|
||||||
|
Id = 10,
|
||||||
|
IdEvent = 4,
|
||||||
|
IdTelemetry = 1
|
||||||
|
},
|
||||||
|
new TelemetryMessageDto()
|
||||||
|
{
|
||||||
|
Id = 11,
|
||||||
|
IdEvent = 4,
|
||||||
|
IdTelemetry = 1
|
||||||
|
},
|
||||||
|
new TelemetryMessageDto()
|
||||||
|
{
|
||||||
|
Id = 12,
|
||||||
|
IdEvent = 5,
|
||||||
|
IdTelemetry = 1
|
||||||
|
},
|
||||||
|
new TelemetryMessageDto()
|
||||||
|
{
|
||||||
|
Id = 13,
|
||||||
|
IdEvent = 5,
|
||||||
|
IdTelemetry = 1
|
||||||
|
},
|
||||||
|
new TelemetryMessageDto()
|
||||||
|
{
|
||||||
|
Id = 14,
|
||||||
|
IdEvent = 6,
|
||||||
|
IdTelemetry = 2
|
||||||
|
},
|
||||||
|
new TelemetryMessageDto()
|
||||||
|
{
|
||||||
|
Id = 15,
|
||||||
|
IdEvent = 6,
|
||||||
|
IdTelemetry = 2
|
||||||
|
},
|
||||||
|
new TelemetryMessageDto()
|
||||||
|
{
|
||||||
|
Id = 16,
|
||||||
|
IdEvent = 6,
|
||||||
|
IdTelemetry = 2
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private readonly IMessageService messageServiceMock = Substitute.For<IMessageService>();
|
||||||
|
private readonly IMessageRepository messageRepositoryMock = Substitute.For<IMessageRepository>();
|
||||||
|
private readonly IEventRepository eventRepositoryMock = Substitute.For<IEventRepository>();
|
||||||
|
private readonly ITelemetryService telemetryServiceMock = Substitute.For<ITelemetryService>();
|
||||||
|
|
||||||
|
public MessageServiceTest()
|
||||||
|
{
|
||||||
|
messageServiceMock = new MessageService(messageRepositoryMock, telemetryServiceMock, eventRepositoryMock);
|
||||||
|
|
||||||
|
telemetryServiceMock
|
||||||
|
.GetOrDefaultTelemetriesByIdsWells(Arg.Any<IEnumerable<int>>())
|
||||||
|
.Returns(telemetries);
|
||||||
|
|
||||||
|
eventRepositoryMock
|
||||||
|
.GetAsync(Arg.Any<TelemetryEventRequest>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(events);
|
||||||
|
|
||||||
|
messageRepositoryMock
|
||||||
|
.GetMessagesAsync(Arg.Any<MessageTelemetryRequest>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetStatMessages_ShouldReturn_Success()
|
||||||
|
{
|
||||||
|
//act
|
||||||
|
var baseRequest = new MessageRequestBase() { };
|
||||||
|
var messageRequest = new MessageRequest(baseRequest, [1]);
|
||||||
|
var result = await messageServiceMock.GetStatAsync(messageRequest, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(6, result.Count());
|
||||||
|
|
||||||
|
AssertStatByEvent(result, 1, events[0], 3);
|
||||||
|
AssertStatByEvent(result, 1, events[1], 3);
|
||||||
|
AssertStatByEvent(result, 1, events[2], 3);
|
||||||
|
AssertStatByEvent(result, 1, events[3], 2);
|
||||||
|
AssertStatByEvent(result, 1, events[4], 2);
|
||||||
|
AssertStatByEvent(result, 2, events[5], 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AssertStatByEvent(IEnumerable<StatCriticalMessageDto> result, int idWell, TelemetryEventDto eventDto, int count)
|
||||||
|
{
|
||||||
|
var eventStats = result
|
||||||
|
.Where(x => x.IdWell == idWell)
|
||||||
|
.Where(x => x.IdCategory == eventDto.IdCategory)
|
||||||
|
.Where(x => x.MessageTemplate == eventDto.MessageTemplate)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Assert.Single(eventStats);
|
||||||
|
var eventStat = eventStats.First();
|
||||||
|
Assert.Equal(count, eventStat.MessagesCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -223,7 +223,7 @@ public class WellReportServiceTest
|
|||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
|
|
||||||
Assert.Equal(4, result.Days.Plan);
|
Assert.Equal(4, result.Days.Plan);
|
||||||
Assert.Equal(5, result.Days.Fact);
|
Assert.Equal(4, result.Days.Fact);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@ -293,9 +293,10 @@ public static class DependencyInjection
|
|||||||
services.AddTransient<IAuthService, AuthService>();
|
services.AddTransient<IAuthService, AuthService>();
|
||||||
services.AddTransient<IDepositRepository, DepositRepository>();
|
services.AddTransient<IDepositRepository, DepositRepository>();
|
||||||
services.AddTransient<IDrillingProgramService, DrillingProgramService>();
|
services.AddTransient<IDrillingProgramService, DrillingProgramService>();
|
||||||
services.AddTransient<IEventService, EventService>();
|
services.AddTransient<IEventRepository, EventRepository>();
|
||||||
services.AddTransient<FileService>();
|
services.AddTransient<FileService>();
|
||||||
services.AddTransient<IMeasureService, MeasureService>();
|
services.AddTransient<IMeasureService, MeasureService>();
|
||||||
|
services.AddTransient<IMessageRepository, MessageRepository>();
|
||||||
services.AddTransient<IMessageService, MessageService>();
|
services.AddTransient<IMessageService, MessageService>();
|
||||||
services.AddTransient<IOperationsStatService, OperationsStatService>();
|
services.AddTransient<IOperationsStatService, OperationsStatService>();
|
||||||
services.AddTransient<IReportService, ReportService>();
|
services.AddTransient<IReportService, ReportService>();
|
||||||
|
71
AsbCloudInfrastructure/Repository/EventRepository.cs
Normal file
71
AsbCloudInfrastructure/Repository/EventRepository.cs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
using AsbCloudApp.Data.SAUB;
|
||||||
|
using AsbCloudApp.Repositories;
|
||||||
|
using AsbCloudApp.Requests;
|
||||||
|
using AsbCloudApp.Services;
|
||||||
|
using AsbCloudDb;
|
||||||
|
using AsbCloudDb.Model;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace AsbCloudInfrastructure.Repository;
|
||||||
|
|
||||||
|
|
||||||
|
public class EventRepository : IEventRepository
|
||||||
|
{
|
||||||
|
private readonly IAsbCloudDbContext db;
|
||||||
|
private readonly IMemoryCache memoryCache;
|
||||||
|
private readonly ITelemetryService telemetryService;
|
||||||
|
|
||||||
|
public EventRepository(IAsbCloudDbContext db, IMemoryCache memoryCache, ITelemetryService telemetryService)
|
||||||
|
{
|
||||||
|
this.db = db;
|
||||||
|
this.memoryCache = memoryCache;
|
||||||
|
this.telemetryService = telemetryService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TelemetryEventDto>> GetAsync(TelemetryEventRequest request, CancellationToken token)
|
||||||
|
{
|
||||||
|
var allEvents = await memoryCache.GetOrCreateBasicAsync(db.Set<TelemetryEvent>(), token);
|
||||||
|
var events = allEvents.Where(e => request.IdsTelemetries.Contains(e.IdTelemetry));
|
||||||
|
|
||||||
|
if (request.IdsCategories?.Any() == true)
|
||||||
|
events = events.Where(e => request.IdsCategories.Contains(e.IdCategory));
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.SearchString))
|
||||||
|
events = events.Where(e => e.MessageTemplate.Contains(request.SearchString, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
var dtos = events.Select(e =>
|
||||||
|
{
|
||||||
|
var dto = new TelemetryEventDto();
|
||||||
|
dto.Id = e.IdEvent;
|
||||||
|
dto.IdCategory = e.IdCategory;
|
||||||
|
dto.MessageTemplate = e.MessageTemplate;
|
||||||
|
return dto;
|
||||||
|
});
|
||||||
|
|
||||||
|
return dtos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpsertAsync(string uid, IEnumerable<EventDto> dtos,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (!dtos.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var telemetry = telemetryService.GetOrCreateTelemetryByUid(uid);
|
||||||
|
|
||||||
|
var entities = dtos.Select(dto => new TelemetryEvent
|
||||||
|
{
|
||||||
|
IdEvent = dto.Id,
|
||||||
|
IdTelemetry = telemetry.Id,
|
||||||
|
IdCategory = dto.IdCategory,
|
||||||
|
MessageTemplate = dto.Message
|
||||||
|
});
|
||||||
|
await db.Database.ExecInsertOrUpdateAsync(db.TelemetryEvents, entities, token);
|
||||||
|
memoryCache.DropBasic<TelemetryEvent>();
|
||||||
|
}
|
||||||
|
}
|
91
AsbCloudInfrastructure/Services/MessageService.cs
Normal file
91
AsbCloudInfrastructure/Services/MessageService.cs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
using AsbCloudApp.Data;
|
||||||
|
using AsbCloudApp.Repositories;
|
||||||
|
using AsbCloudApp.Requests;
|
||||||
|
using AsbCloudApp.Services;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace AsbCloudInfrastructure.Services;
|
||||||
|
|
||||||
|
|
||||||
|
public class MessageService : IMessageService
|
||||||
|
{
|
||||||
|
private readonly IMessageRepository messageRepository;
|
||||||
|
private readonly ITelemetryService telemetryService;
|
||||||
|
private readonly IEventRepository eventRepository;
|
||||||
|
|
||||||
|
public MessageService(
|
||||||
|
IMessageRepository messageRepository,
|
||||||
|
ITelemetryService telemetryService,
|
||||||
|
IEventRepository eventRepository)
|
||||||
|
{
|
||||||
|
this.messageRepository = messageRepository;
|
||||||
|
this.telemetryService = telemetryService;
|
||||||
|
this.eventRepository = eventRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PaginationContainer<MessageDto>> GetPaginatedMessagesAsync(MessageRequest request, CancellationToken token)
|
||||||
|
{
|
||||||
|
var messageTelemetryRequest = await CreateMessageTelemetryRequest(request, token);
|
||||||
|
|
||||||
|
var result = await messageRepository.GetPaginatedMessagesAsync(messageTelemetryRequest, token);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<StatCriticalMessageDto>> GetStatAsync(MessageRequest request, CancellationToken token)
|
||||||
|
{
|
||||||
|
var messageTelemetryRequest = await CreateMessageTelemetryRequest(request, token);
|
||||||
|
|
||||||
|
var telemetryMessages = await messageRepository.GetMessagesAsync(messageTelemetryRequest, token);
|
||||||
|
|
||||||
|
var groupedMessages = telemetryMessages.GroupBy(x => new { x.IdTelemetry, x.IdEvent });
|
||||||
|
var events = messageTelemetryRequest.Events
|
||||||
|
.GroupBy(e => e.Id)
|
||||||
|
.ToDictionary(e => e.Key, e => e.FirstOrDefault());
|
||||||
|
|
||||||
|
var idsWellsDict = messageTelemetryRequest.Telemetries.ToDictionary(e => e.Id, e => e.IdWell);
|
||||||
|
|
||||||
|
var result = groupedMessages
|
||||||
|
.Select(m => new
|
||||||
|
{
|
||||||
|
IdWell = idsWellsDict.GetValueOrDefault(m.Key.IdTelemetry),
|
||||||
|
events.GetValueOrDefault(m.Key.IdEvent)?.IdCategory,
|
||||||
|
events.GetValueOrDefault(m.Key.IdEvent)?.MessageTemplate,
|
||||||
|
Count = m.Count(),
|
||||||
|
})
|
||||||
|
.Where(m => m.IdCategory != null)
|
||||||
|
.Where(m => m.IdWell != null)
|
||||||
|
.Where(m => m.MessageTemplate != null)
|
||||||
|
.GroupBy(m => new { m.IdWell, m.IdCategory, m.MessageTemplate })
|
||||||
|
.Select(m => new StatCriticalMessageDto()
|
||||||
|
{
|
||||||
|
IdWell = m.First().IdWell!.Value,
|
||||||
|
IdCategory = m.Key.IdCategory,
|
||||||
|
MessageTemplate = m.Key.MessageTemplate,
|
||||||
|
MessagesCount = m.Select(z => z.Count).Sum()
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MessageTelemetryRequest> CreateMessageTelemetryRequest(MessageRequest request, CancellationToken token)
|
||||||
|
{
|
||||||
|
var telemetries = telemetryService.GetOrDefaultTelemetriesByIdsWells(request.IdsWell);
|
||||||
|
|
||||||
|
var telemetryEventRequest = new TelemetryEventRequest()
|
||||||
|
{
|
||||||
|
IdsTelemetries = telemetries.Select(t => t.Id),
|
||||||
|
IdsCategories = request.IdsCategories,
|
||||||
|
SearchString = request.SearchString,
|
||||||
|
};
|
||||||
|
var events = await eventRepository.GetAsync(telemetryEventRequest, token);
|
||||||
|
|
||||||
|
var messageTelemetryRequest = new MessageTelemetryRequest(request, events, telemetries);
|
||||||
|
|
||||||
|
return messageTelemetryRequest;
|
||||||
|
}
|
||||||
|
}
|
@ -1,45 +0,0 @@
|
|||||||
using AsbCloudApp.Data.SAUB;
|
|
||||||
using AsbCloudApp.Services;
|
|
||||||
using AsbCloudDb;
|
|
||||||
using AsbCloudDb.Model;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace AsbCloudInfrastructure.Services.SAUB;
|
|
||||||
|
|
||||||
|
|
||||||
public class EventService : IEventService
|
|
||||||
{
|
|
||||||
private readonly IAsbCloudDbContext db;
|
|
||||||
private readonly IMemoryCache memoryCache;
|
|
||||||
private readonly ITelemetryService telemetryService;
|
|
||||||
|
|
||||||
public EventService(IAsbCloudDbContext db, IMemoryCache memoryCache, ITelemetryService telemetryService)
|
|
||||||
{
|
|
||||||
this.db = db;
|
|
||||||
this.memoryCache = memoryCache;
|
|
||||||
this.telemetryService = telemetryService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpsertAsync(string uid, IEnumerable<EventDto> dtos,
|
|
||||||
CancellationToken token = default)
|
|
||||||
{
|
|
||||||
if (!dtos.Any())
|
|
||||||
return;
|
|
||||||
|
|
||||||
var telemetry = telemetryService.GetOrCreateTelemetryByUid(uid);
|
|
||||||
|
|
||||||
var entities = dtos.Select(dto => new TelemetryEvent
|
|
||||||
{
|
|
||||||
IdEvent = dto.Id,
|
|
||||||
IdTelemetry = telemetry.Id,
|
|
||||||
IdCategory = dto.IdCategory,
|
|
||||||
MessageTemplate = dto.Message
|
|
||||||
});
|
|
||||||
var result = await db.Database.ExecInsertOrUpdateAsync(db.TelemetryEvents, entities, token);
|
|
||||||
memoryCache.DropBasic<TelemetryEvent>();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +1,17 @@
|
|||||||
using AsbCloudApp.Data;
|
using AsbCloudApp.Data;
|
||||||
using AsbCloudApp.Data.SAUB;
|
using AsbCloudApp.Data.SAUB;
|
||||||
|
using AsbCloudApp.Repositories;
|
||||||
using AsbCloudApp.Requests;
|
using AsbCloudApp.Requests;
|
||||||
using AsbCloudApp.Services;
|
using AsbCloudApp.Services;
|
||||||
using AsbCloudDb;
|
using AsbCloudDb;
|
||||||
using AsbCloudDb.Model;
|
using AsbCloudDb.Model;
|
||||||
|
using DocumentFormat.OpenXml.Bibliography;
|
||||||
using Mapster;
|
using Mapster;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Internal;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -16,20 +21,31 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace AsbCloudInfrastructure.Services.SAUB;
|
namespace AsbCloudInfrastructure.Services.SAUB;
|
||||||
|
|
||||||
public class MessageService : IMessageService
|
public class MessageRepository : IMessageRepository
|
||||||
{
|
{
|
||||||
private readonly IAsbCloudDbContext db;
|
private readonly IAsbCloudDbContext db;
|
||||||
private readonly IMemoryCache memoryCache;
|
private readonly IMemoryCache memoryCache;
|
||||||
private readonly ITelemetryService telemetryService;
|
private readonly ITelemetryService telemetryService;
|
||||||
|
|
||||||
public MessageService(IAsbCloudDbContext db, IMemoryCache memoryCache, ITelemetryService telemetryService)
|
public MessageRepository(IAsbCloudDbContext db, IMemoryCache memoryCache, ITelemetryService telemetryService)
|
||||||
{
|
{
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.memoryCache = memoryCache;
|
this.memoryCache = memoryCache;
|
||||||
this.telemetryService = telemetryService;
|
this.telemetryService = telemetryService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PaginationContainer<MessageDto>> GetMessagesAsync(MessageRequest request, CancellationToken token)
|
public async Task<IEnumerable<TelemetryMessageDto>> GetMessagesAsync(MessageTelemetryRequest request, CancellationToken token)
|
||||||
|
{
|
||||||
|
var query = BuildQuery(request, token);
|
||||||
|
|
||||||
|
var entities = await query.ToArrayAsync(token);
|
||||||
|
|
||||||
|
var dtos = entities.Select(m => m.Adapt<TelemetryMessageDto>());
|
||||||
|
|
||||||
|
return dtos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PaginationContainer<MessageDto>> GetPaginatedMessagesAsync(MessageTelemetryRequest request, CancellationToken token)
|
||||||
{
|
{
|
||||||
var result = new PaginationContainer<MessageDto>
|
var result = new PaginationContainer<MessageDto>
|
||||||
{
|
{
|
||||||
@ -37,36 +53,86 @@ public class MessageService : IMessageService
|
|||||||
Take = request.Take ?? 32,
|
Take = request.Take ?? 32,
|
||||||
};
|
};
|
||||||
|
|
||||||
var telemetry = telemetryService.GetOrDefaultTelemetryByIdWell(request.IdWell);
|
if (request.Telemetries.IsNullOrEmpty() || request.Events.IsNullOrEmpty())
|
||||||
if (telemetry is null)
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
var allEvents = await memoryCache.GetOrCreateBasicAsync(db.Set<TelemetryEvent>(), token);
|
var query = BuildQuery(request, token);
|
||||||
var events = allEvents.Where(e => e.IdTelemetry == telemetry.Id);
|
|
||||||
|
|
||||||
if (!events.Any())
|
if (request.SortFields?.Any() == true)
|
||||||
return result;
|
|
||||||
|
|
||||||
var query = db.TelemetryMessages.Where(m => m.IdTelemetry == telemetry.Id)
|
|
||||||
.OrderBy(m => m.DateTime).AsNoTracking();
|
|
||||||
|
|
||||||
if (request.Categoryids?.Any() == true || !string.IsNullOrEmpty(request.SearchString))
|
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(request.SearchString))
|
query = query.SortBy(request.SortFields);
|
||||||
events = events.Where(e => e.MessageTemplate.Contains(request.SearchString, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (request.Categoryids?.Any() == true)
|
|
||||||
events = events.Where(e => request.Categoryids.ToList().Contains(e.IdCategory));
|
|
||||||
|
|
||||||
var eventIds = events.Select(e => e.IdEvent);
|
|
||||||
|
|
||||||
if (!eventIds.Any())
|
|
||||||
return result;
|
|
||||||
|
|
||||||
query = query.Where(m => eventIds.Contains(m.IdEvent));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
query = query.OrderByDescending(m => m.DateTime);
|
result.Count = query.Count();
|
||||||
|
|
||||||
|
var messagesList = await query
|
||||||
|
.Skip(result.Skip)
|
||||||
|
.Take(result.Take)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToArrayAsync(token);
|
||||||
|
|
||||||
|
if (messagesList.Count() == 0)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
var allUsers = await memoryCache.GetOrCreateBasicAsync(db.Set<TelemetryUser>(), token);
|
||||||
|
var users = allUsers.Where(u => request.Telemetries!.Select(t => t.Id).Contains(u.IdTelemetry));
|
||||||
|
|
||||||
|
if (!request.Events.Any())
|
||||||
|
return result;
|
||||||
|
|
||||||
|
var eventsDict = request.Events.ToDictionary(x => x.Id);
|
||||||
|
var usersDict = users.ToDictionary(x => x.IdUser, x => x);
|
||||||
|
|
||||||
|
var messagesDtoList = new List<MessageDto>();
|
||||||
|
|
||||||
|
foreach (var message in messagesList)
|
||||||
|
{
|
||||||
|
var messageDto = new MessageDto
|
||||||
|
{
|
||||||
|
Id = message.Id,
|
||||||
|
WellDepth = message.WellDepth,
|
||||||
|
};
|
||||||
|
|
||||||
|
var telemetry = request.Telemetries.Where(t => t.Id == message.IdTelemetry).FirstOrDefault();
|
||||||
|
|
||||||
|
if(telemetry != null && telemetry.TimeZone != null)
|
||||||
|
messageDto.DateTime = message.DateTime.ToOffset(TimeSpan.FromHours(telemetry.TimeZone.Hours));
|
||||||
|
|
||||||
|
if (message.IdTelemetryUser is not null)
|
||||||
|
{
|
||||||
|
if (usersDict.TryGetValue((int)message.IdTelemetryUser, out TelemetryUser? user))
|
||||||
|
{
|
||||||
|
messageDto.User = user.MakeDisplayName();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
messageDto.User = message.IdTelemetryUser.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventsDict.TryGetValue(message.IdEvent, out TelemetryEventDto? e))
|
||||||
|
{
|
||||||
|
messageDto.CategoryId = e.IdCategory;
|
||||||
|
messageDto.Message = e.MakeMessageText([
|
||||||
|
message.Arg0,
|
||||||
|
message.Arg1,
|
||||||
|
message.Arg2,
|
||||||
|
message.Arg3
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
messagesDtoList.Add(messageDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Items = result.Items.Concat(messagesDtoList);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
public IQueryable<TelemetryMessage> BuildQuery(MessageTelemetryRequest request, CancellationToken token)
|
||||||
|
{
|
||||||
|
var idsTelemetries = request.Telemetries.Select(t => t.Id);
|
||||||
|
var eventIds = request.Events.Select(e => e.Id);
|
||||||
|
|
||||||
|
var query = db.TelemetryMessages
|
||||||
|
.Where(m => idsTelemetries.Contains(m.IdTelemetry))
|
||||||
|
.Where(m => eventIds.Contains(m.IdEvent));
|
||||||
|
|
||||||
if (request.Begin is not null)
|
if (request.Begin is not null)
|
||||||
{
|
{
|
||||||
@ -80,63 +146,13 @@ public class MessageService : IMessageService
|
|||||||
query = query.Where(m => m.DateTime <= endUtc);
|
query = query.Where(m => m.DateTime <= endUtc);
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Count = query.Count();
|
query = query.OrderByDescending(m => m.DateTime);
|
||||||
|
|
||||||
if (request.SortFields?.Any() == true)
|
return query;
|
||||||
{
|
|
||||||
query = query.SortBy(request.SortFields);
|
|
||||||
}
|
|
||||||
var messagesList = await query.Skip(result.Skip)
|
|
||||||
.Take(result.Take).AsNoTracking()
|
|
||||||
.ToListAsync(token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (messagesList.Count == 0)
|
|
||||||
return result;
|
|
||||||
|
|
||||||
var allUsers = await memoryCache.GetOrCreateBasicAsync(db.Set<TelemetryUser>(), token);
|
|
||||||
var users = allUsers.Where(u => u.IdTelemetry == telemetry.Id);
|
|
||||||
|
|
||||||
var eventsDict = events.ToDictionary(x => x.IdEvent, x => x);
|
|
||||||
var usersDict = users.ToDictionary(x => x.IdUser, x => x);
|
|
||||||
|
|
||||||
var messagesDtoList = new List<MessageDto>();
|
|
||||||
var timezone = telemetryService.GetTimezone(telemetry.Id);
|
|
||||||
|
|
||||||
foreach (var message in messagesList)
|
|
||||||
{
|
|
||||||
var messageDto = new MessageDto
|
|
||||||
{
|
|
||||||
Id = message.Id,
|
|
||||||
WellDepth = message.WellDepth
|
|
||||||
};
|
|
||||||
|
|
||||||
messageDto.DateTime = message.DateTime.ToOffset(TimeSpan.FromHours(timezone.Hours));
|
|
||||||
|
|
||||||
if (message.IdTelemetryUser is not null)
|
|
||||||
{
|
|
||||||
if (usersDict.TryGetValue((int)message.IdTelemetryUser, out TelemetryUser? user))
|
|
||||||
{
|
|
||||||
messageDto.User = user.MakeDisplayName();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
messageDto.User = message.IdTelemetryUser.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventsDict.TryGetValue(message.IdEvent, out TelemetryEvent? e))
|
|
||||||
{
|
|
||||||
messageDto.CategoryId = e.IdCategory;
|
|
||||||
messageDto.Message = e.MakeMessageText(message);
|
|
||||||
}
|
|
||||||
messagesDtoList.Add(messageDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Items = result.Items.Concat(messagesDtoList);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task InsertAsync(string uid, IEnumerable<TelemetryMessageDto> dtos,
|
public Task InsertAsync(string uid, IEnumerable<TelemetryMessageDto> dtos,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if (!dtos.Any())
|
if (!dtos.Any())
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
@ -21,7 +21,7 @@ public class TelemetryDataEditorService : ITelemetryDataEditorService
|
|||||||
/// <param name="dataSaubService"></param>
|
/// <param name="dataSaubService"></param>
|
||||||
/// <param name="dataSpinService"></param>
|
/// <param name="dataSpinService"></param>
|
||||||
/// <param name="dataSaubStatRepository"></param>
|
/// <param name="dataSaubStatRepository"></param>
|
||||||
/// <param name="messageService"></param>
|
/// <param name="messageRepository"></param>
|
||||||
/// <param name="drillTestRepository"></param>
|
/// <param name="drillTestRepository"></param>
|
||||||
/// <param name="limitingParameterRepository"></param>
|
/// <param name="limitingParameterRepository"></param>
|
||||||
/// <param name="detectedOperationRepository"></param>
|
/// <param name="detectedOperationRepository"></param>
|
||||||
@ -36,7 +36,7 @@ public class TelemetryDataEditorService : ITelemetryDataEditorService
|
|||||||
ITelemetryDataSaubService dataSaubService,
|
ITelemetryDataSaubService dataSaubService,
|
||||||
ITelemetryDataService<TelemetryDataSpinDto> dataSpinService,
|
ITelemetryDataService<TelemetryDataSpinDto> dataSpinService,
|
||||||
IDataSaubStatRepository dataSaubStatRepository,
|
IDataSaubStatRepository dataSaubStatRepository,
|
||||||
IMessageService messageService,
|
IMessageRepository messageRepository,
|
||||||
IDrillTestRepository drillTestRepository,
|
IDrillTestRepository drillTestRepository,
|
||||||
ILimitingParameterRepository limitingParameterRepository,
|
ILimitingParameterRepository limitingParameterRepository,
|
||||||
IDetectedOperationRepository detectedOperationRepository,
|
IDetectedOperationRepository detectedOperationRepository,
|
||||||
@ -54,7 +54,7 @@ public class TelemetryDataEditorService : ITelemetryDataEditorService
|
|||||||
dataSaubService,
|
dataSaubService,
|
||||||
dataSpinService,
|
dataSpinService,
|
||||||
dataSaubStatRepository,
|
dataSaubStatRepository,
|
||||||
messageService,
|
messageRepository,
|
||||||
drillTestRepository,
|
drillTestRepository,
|
||||||
limitingParameterRepository,
|
limitingParameterRepository,
|
||||||
detectedOperationRepository,
|
detectedOperationRepository,
|
||||||
|
@ -144,15 +144,32 @@ public class TelemetryService : ITelemetryService
|
|||||||
|
|
||||||
public TelemetryBaseDto? GetOrDefaultTelemetryByIdWell(int idWell)
|
public TelemetryBaseDto? GetOrDefaultTelemetryByIdWell(int idWell)
|
||||||
{
|
{
|
||||||
var entity = GetTelemetryCache()
|
var dto = GetOrDefaultTelemetriesByIdsWells([idWell])
|
||||||
.FirstOrDefault(t => t.Well?.Id == idWell);
|
.FirstOrDefault();
|
||||||
|
|
||||||
if (entity?.Well?.Timezone is not null && entity.TimeZone.Hours != entity.Well.Timezone.Hours)
|
return dto;
|
||||||
{
|
}
|
||||||
entity.TimeZone = entity.Well.Timezone;
|
|
||||||
//TODO: выдаем предупреждение!
|
public IEnumerable<TelemetryDto> GetOrDefaultTelemetriesByIdsWells(IEnumerable<int> idsWells)
|
||||||
}
|
{
|
||||||
return entity?.Adapt<TelemetryBaseDto>();
|
var entities = GetTelemetryCache()
|
||||||
|
.Where(t => t.Well != null)
|
||||||
|
.Where(t => idsWells.Contains(t.Well!.Id))
|
||||||
|
.Select(t => {
|
||||||
|
t.TimeZone = t.TimeZone.Hours != t.Well!.Timezone.Hours ? t.Well.Timezone : t.TimeZone;
|
||||||
|
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var dtos = entities.Select(t => {
|
||||||
|
var dto = t.Adapt<TelemetryDto>();
|
||||||
|
dto.IdWell = t.Well?.Id;
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
});
|
||||||
|
|
||||||
|
return dtos;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TelemetryDto GetOrCreateTelemetryByUid(string uid)
|
public TelemetryDto GetOrCreateTelemetryByUid(string uid)
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
using AsbCloudApp.Data;
|
using AsbCloudApp.Data;
|
||||||
|
using AsbCloudApp.Repositories;
|
||||||
using AsbCloudApp.Requests;
|
using AsbCloudApp.Requests;
|
||||||
using AsbCloudApp.Services;
|
using AsbCloudApp.Services;
|
||||||
|
using AsbCloudDb.Model;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -14,13 +17,18 @@ namespace AsbCloudWebApi.Controllers.SAUB;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
public class MessageController : ControllerBase
|
public class MessageController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IMessageService messageService;
|
private readonly IMessageRepository messageRepository;
|
||||||
private readonly IWellService wellService;
|
private readonly IWellService wellService;
|
||||||
|
private readonly IMessageService messageService;
|
||||||
|
|
||||||
public MessageController(IMessageService messageService, IWellService wellService)
|
public MessageController(
|
||||||
|
IMessageRepository messageRepository,
|
||||||
|
IWellService wellService,
|
||||||
|
IMessageService messageService)
|
||||||
{
|
{
|
||||||
this.messageService = messageService;
|
this.messageRepository = messageRepository;
|
||||||
this.wellService = wellService;
|
this.wellService = wellService;
|
||||||
|
this.messageService = messageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -46,17 +54,23 @@ public class MessageController : ControllerBase
|
|||||||
if (request.Take > 1024)
|
if (request.Take > 1024)
|
||||||
return this.ValidationBadRequest(nameof(request.Take), "limit mast be less then 1024");
|
return this.ValidationBadRequest(nameof(request.Take), "limit mast be less then 1024");
|
||||||
|
|
||||||
var requestToService = new MessageRequest(request, idWell);
|
var messageRequest = new MessageRequest(request, new int[] { idWell });
|
||||||
|
var result = await messageService.GetPaginatedMessagesAsync(messageRequest, token);
|
||||||
var result = await messageService.GetMessagesAsync(
|
|
||||||
requestToService,
|
|
||||||
token)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (result is null || result.Count == 0)
|
if (result is null || result.Count == 0)
|
||||||
return NoContent();
|
return NoContent();
|
||||||
|
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("/api/serviceOperation/[controller]")]
|
||||||
|
[Permission("CriticalMessage.get")]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<StatCriticalMessageDto>), (int)System.Net.HttpStatusCode.OK)]
|
||||||
|
public async Task<IActionResult> Get([FromQuery] MessageRequest request, CancellationToken token)
|
||||||
|
{
|
||||||
|
var result = await messageService.GetStatAsync(request, token);
|
||||||
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
using AsbCloudApp.Data.SAUB;
|
using AsbCloudApp.Data.SAUB;
|
||||||
|
using AsbCloudApp.Repositories;
|
||||||
using AsbCloudApp.Services;
|
using AsbCloudApp.Services;
|
||||||
using AsbCloudWebApi.SignalR;
|
using AsbCloudWebApi.SignalR;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -20,21 +20,21 @@ namespace AsbCloudWebApi.Controllers.SAUB;
|
|||||||
public class TelemetryController : ControllerBase
|
public class TelemetryController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly ITelemetryService telemetryService;
|
private readonly ITelemetryService telemetryService;
|
||||||
private readonly IMessageService messageService;
|
private readonly IMessageRepository messageRepository;
|
||||||
private readonly IEventService eventService;
|
private readonly IEventRepository eventRepository;
|
||||||
private readonly ITelemetryUserService telemetryUserService;
|
private readonly ITelemetryUserService telemetryUserService;
|
||||||
private readonly IHubContext<TelemetryHub> telemetryHubContext;
|
private readonly IHubContext<TelemetryHub> telemetryHubContext;
|
||||||
|
|
||||||
public TelemetryController(
|
public TelemetryController(
|
||||||
ITelemetryService telemetryService,
|
ITelemetryService telemetryService,
|
||||||
IMessageService messageService,
|
IMessageRepository messageRepository,
|
||||||
IEventService eventService,
|
IEventRepository eventRepository,
|
||||||
ITelemetryUserService telemetryUserService,
|
ITelemetryUserService telemetryUserService,
|
||||||
IHubContext<TelemetryHub> telemetryHubContext)
|
IHubContext<TelemetryHub> telemetryHubContext)
|
||||||
{
|
{
|
||||||
this.telemetryService = telemetryService;
|
this.telemetryService = telemetryService;
|
||||||
this.messageService = messageService;
|
this.messageRepository = messageRepository;
|
||||||
this.eventService = eventService;
|
this.eventRepository = eventRepository;
|
||||||
this.telemetryUserService = telemetryUserService;
|
this.telemetryUserService = telemetryUserService;
|
||||||
this.telemetryHubContext = telemetryHubContext;
|
this.telemetryHubContext = telemetryHubContext;
|
||||||
}
|
}
|
||||||
@ -49,7 +49,7 @@ public class TelemetryController : ControllerBase
|
|||||||
{
|
{
|
||||||
var from = DateTimeOffset.UtcNow.AddDays(-1);
|
var from = DateTimeOffset.UtcNow.AddDays(-1);
|
||||||
var stream = await telemetryService.GetTelemetriesInfoByLastData(from, token);
|
var stream = await telemetryService.GetTelemetriesInfoByLastData(from, token);
|
||||||
return File(stream, "text/csv", $"Software versions by active telemetries from {from :yy-MM-dd hh-mm}.csv");
|
return File(stream, "text/csv", $"Software versions by active telemetries from {from:yy-MM-dd hh-mm}.csv");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -79,7 +79,7 @@ public class TelemetryController : ControllerBase
|
|||||||
CancellationToken token)
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
var idWell = telemetryService.GetIdWellByTelemetryUid(uid);
|
var idWell = telemetryService.GetIdWellByTelemetryUid(uid);
|
||||||
await messageService.InsertAsync(uid, dtos, token).ConfigureAwait(false);
|
await messageRepository.InsertAsync(uid, dtos, token).ConfigureAwait(false);
|
||||||
|
|
||||||
if (dtos.Any())
|
if (dtos.Any())
|
||||||
await Task.Run(() => telemetryHubContext.Clients.Group($"well_{idWell}")
|
await Task.Run(() => telemetryHubContext.Clients.Group($"well_{idWell}")
|
||||||
@ -99,7 +99,7 @@ public class TelemetryController : ControllerBase
|
|||||||
public async Task<IActionResult> PostEventsAsync(string uid, [FromBody] List<EventDto> events,
|
public async Task<IActionResult> PostEventsAsync(string uid, [FromBody] List<EventDto> events,
|
||||||
CancellationToken token)
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
await eventService.UpsertAsync(uid, events, token)
|
await eventRepository.UpsertAsync(uid, events, token)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user