From 8fea549a8e8b7b9345c1be8d322a664999548285 Mon Sep 17 00:00:00 2001 From: "aleksandr.z" Date: Sun, 20 Apr 2025 13:57:05 +0300 Subject: [PATCH 1/5] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D1=83=D0=BB=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=B2=D0=BD=D0=B5=D1=88?= =?UTF-8?q?=D0=BD=D0=B8=D0=BC=D0=B8=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8=20=D0=B8=20=D0=BE=D1=87=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D1=8F=D0=BC=D0=B8=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87.=20?= =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B8,?= =?UTF-8?q?=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D1=8B=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=82=D1=87=D0=B5=D1=82?= =?UTF-8?q?=D0=BE=D0=B2,=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BF=D0=B0=D1=82=D1=87=D0=B5=D0=B9=20=D0=BE=D1=82=D1=87?= =?UTF-8?q?=D0=B5=D1=82=D0=BE=D0=B2=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20?= =?UTF-8?q?=D0=B2=D0=B5=D0=B1-=D1=81=D0=BE=D0=BA=D0=B5=D1=82=D1=8B.=20?= =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=83=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D1=80=D0=B5=D0=B2=D1=88=D0=B8=D0=B5=20=D1=84=D1=83=D0=BD?= =?UTF-8?q?=D0=BA=D1=86=D0=B8=D0=B8=20=D0=B8=20=D0=BC=D0=BE=D0=B4=D1=83?= =?UTF-8?q?=D0=BB=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/Bugget.BO/Bugget.BO.csproj | 3 +- backend/Bugget.BO/Services/ReportsService.cs | 44 ++++++++++++++----- backend/Bugget.DA/Bugget.DA.csproj | 6 --- backend/Bugget.DA/Postgres/ReportsDbClient.cs | 4 +- .../WebSockets/IReportPageHubClient.cs | 8 ++++ .../DbModels/Report/ReportPatchDbModel.cs | 16 ------- .../Report/ReportPatchResultDbModel.cs | 10 +++++ ...SocketView.cs => PatchReportSocketView.cs} | 3 +- .../Bugget.ExternalClients.csproj} | 0 .../Context/ReportCreateContext.cs | 2 +- .../Context/ReportPatchContext.cs | 4 +- .../Context/ReportUpdateContext.cs | 2 +- .../ExternalClientsActionService.cs} | 21 ++++----- .../ExternalClientsExtensions.cs | 15 +++++++ .../Interfaces/IReportCreatePostAction.cs | 4 +- .../Interfaces/IReportPatchPostAction.cs | 4 +- .../Interfaces/IReportUpdatePostAction.cs | 4 +- .../Mattermost/HttpModels/ChannelResponse.cs | 2 +- .../CreateMattermostMessageRequest.cs | 2 +- .../HttpModels/MattermostMessageResponse.cs | 2 +- .../Mattermost/HttpModels/UserResponse.cs | 2 +- .../Mattermost/MattermostClient.cs | 4 +- .../Mattermost/MattermostConstants.cs | 2 +- .../Mattermost/MattermostExtensions.cs | 4 +- .../Mattermost/MattermostService.cs | 12 ++--- .../Notifications/NotificationsConstants.cs | 2 +- .../Notifications/NotificationsExtensions.cs | 4 +- .../Notifications/ReportMessageBuilder.cs | 2 +- .../Notifications/readme.md | 0 backend/Bugget.Features/FeaturesExtensions.cs | 17 ------- .../TaskQueue/ServiceCollectionExtensions.cs | 10 ----- backend/Bugget.sln | 8 +++- .../Bugget/Controllers/ReportsV2Controller.cs | 2 +- backend/Bugget/Hubs/ReportPageHub.cs | 27 ++---------- backend/Bugget/Hubs/ReportPageHubClient.cs | 21 +++++++++ backend/Bugget/Program.cs | 15 ++++--- .../TaskQueue/ITaskQueue.cs | 2 +- .../TaskQueue/TaskQueue.cs | 20 +++------ backend/TaskQueue/TaskQueue.csproj | 12 +++++ .../TaskQueue/TaskQueueExtensions.cs | 2 +- 40 files changed, 169 insertions(+), 155 deletions(-) create mode 100644 backend/Bugget.DA/WebSockets/IReportPageHubClient.cs delete mode 100644 backend/Bugget.Entities/DbModels/Report/ReportPatchDbModel.cs create mode 100644 backend/Bugget.Entities/DbModels/Report/ReportPatchResultDbModel.cs rename backend/Bugget.Entities/SocketViews/{ReportSocketView.cs => PatchReportSocketView.cs} (72%) rename backend/{Bugget.Features/Bugget.Features.csproj => Bugget.ExternalClients/Bugget.ExternalClients.csproj} (100%) rename backend/{Bugget.Features => Bugget.ExternalClients}/Context/ReportCreateContext.cs (79%) rename backend/{Bugget.Features => Bugget.ExternalClients}/Context/ReportPatchContext.cs (62%) rename backend/{Bugget.Features => Bugget.ExternalClients}/Context/ReportUpdateContext.cs (80%) rename backend/{Bugget.Features/FeaturesService.cs => Bugget.ExternalClients/ExternalClientsActionService.cs} (54%) create mode 100644 backend/Bugget.ExternalClients/ExternalClientsExtensions.cs rename backend/{Bugget.Features => Bugget.ExternalClients}/Interfaces/IReportCreatePostAction.cs (57%) rename backend/{Bugget.Features => Bugget.ExternalClients}/Interfaces/IReportPatchPostAction.cs (57%) rename backend/{Bugget.Features => Bugget.ExternalClients}/Interfaces/IReportUpdatePostAction.cs (57%) rename backend/{Bugget.Features => Bugget.ExternalClients}/Notifications/Mattermost/HttpModels/ChannelResponse.cs (53%) rename backend/{Bugget.Features => Bugget.ExternalClients}/Notifications/Mattermost/HttpModels/CreateMattermostMessageRequest.cs (90%) rename backend/{Bugget.Features => Bugget.ExternalClients}/Notifications/Mattermost/HttpModels/MattermostMessageResponse.cs (90%) rename backend/{Bugget.Features => Bugget.ExternalClients}/Notifications/Mattermost/HttpModels/UserResponse.cs (50%) rename backend/{Bugget.Features => Bugget.ExternalClients}/Notifications/Mattermost/MattermostClient.cs (96%) rename backend/{Bugget.Features => Bugget.ExternalClients}/Notifications/Mattermost/MattermostConstants.cs (82%) rename backend/{Bugget.Features => Bugget.ExternalClients}/Notifications/Mattermost/MattermostExtensions.cs (93%) rename backend/{Bugget.Features => Bugget.ExternalClients}/Notifications/Mattermost/MattermostService.cs (87%) rename backend/{Bugget.Features => Bugget.ExternalClients}/Notifications/NotificationsConstants.cs (69%) rename backend/{Bugget.Features => Bugget.ExternalClients}/Notifications/NotificationsExtensions.cs (74%) rename backend/{Bugget.Features => Bugget.ExternalClients}/Notifications/ReportMessageBuilder.cs (96%) rename backend/{Bugget.Features => Bugget.ExternalClients}/Notifications/readme.md (100%) delete mode 100644 backend/Bugget.Features/FeaturesExtensions.cs delete mode 100644 backend/Bugget.Features/TaskQueue/ServiceCollectionExtensions.cs create mode 100644 backend/Bugget/Hubs/ReportPageHubClient.cs rename backend/{Bugget.Features => }/TaskQueue/ITaskQueue.cs (85%) rename backend/{Bugget.Features => }/TaskQueue/TaskQueue.cs (68%) create mode 100644 backend/TaskQueue/TaskQueue.csproj rename backend/{Bugget.Features => }/TaskQueue/TaskQueueExtensions.cs (96%) diff --git a/backend/Bugget.BO/Bugget.BO.csproj b/backend/Bugget.BO/Bugget.BO.csproj index 606eb8f..76ef529 100644 --- a/backend/Bugget.BO/Bugget.BO.csproj +++ b/backend/Bugget.BO/Bugget.BO.csproj @@ -7,8 +7,9 @@ - + + diff --git a/backend/Bugget.BO/Services/ReportsService.cs b/backend/Bugget.BO/Services/ReportsService.cs index 280bbbc..6650c96 100644 --- a/backend/Bugget.BO/Services/ReportsService.cs +++ b/backend/Bugget.BO/Services/ReportsService.cs @@ -1,19 +1,24 @@ using Bugget.BO.Errors; using Bugget.BO.Mappers; +using Bugget.BO.WebSockets; using Bugget.DA.Postgres; using Bugget.Entities.BO.ReportBo; using Bugget.Entities.BO.Search; using Bugget.Entities.DbModels.Report; using Bugget.Entities.DTO.Report; -using Bugget.Features; -using Bugget.Features.Context; +using Bugget.Entities.SocketViews; +using Bugget.ExternalClients; +using Bugget.ExternalClients.Context; using Monade; +using TaskQueue; namespace Bugget.BO.Services; public sealed class ReportsService( ReportsDbClient reportsDbClient, - FeaturesService featuresService) + ExternalClientsActionService externalClientsActionService, + ITaskQueue taskQueue, + IReportPageHubClient reportPageHubClient) { public async Task CreateReportAsync(Report report) { @@ -23,19 +28,36 @@ public sealed class ReportsService( return null; } - await featuresService.ExecuteReportCreatePostActions(new ReportCreateContext(report, reportDbModel)); + await taskQueue.Enqueue(() => externalClientsActionService.ExecuteReportCreatePostActions(new ReportCreateContext(report, reportDbModel))); return reportDbModel; } - public Task CreateReportAsync(string userId, string? teamId, string? organizationId, ReportV2CreateDto createDto) + public Task CreateReportAsync(string userId, string? teamId, string? organizationId, ReportV2CreateDto createDto) { return reportsDbClient.CreateReportAsync(userId, teamId, organizationId, createDto); } - public Task PatchReportAsync(int reportId, string userId, string? organizationId, ReportPatchDto patchDto) + public async Task PatchReportAsync(int reportId, string userId, string? organizationId, ReportPatchDto patchDto) { - return reportsDbClient.PatchReportAsync(reportId, userId, organizationId, patchDto); + var result = await reportsDbClient.PatchReportAsync(reportId, userId, organizationId, patchDto); + var socketView = new PatchReportSocketView + { + Title = patchDto.Title, + Status = patchDto.Status, + ResponsibleUserId = patchDto.ResponsibleUserId, + PastResponsibleUserId = patchDto.ResponsibleUserId == null ? null : result.PastResponsibleUserId + }; + + await reportPageHubClient.SendReportPatchAsync(reportId, socketView); + + await taskQueue.Enqueue(() => externalClientsActionService.ExecuteReportPatchPostActions(new ReportPatchContext(userId, patchDto, result))); + + // todo проставление участников и уведомление + + // todo вычисление статуса + + return result; } public Task ListReportsAsync(string userId) @@ -61,16 +83,16 @@ public async Task> GetReportAsync(int reportId, stri public async Task UpdateReportAsync(ReportUpdate report) { - var reportDbModel = await reportsDbClient.UpdateReportAsync(report.ToReportUpdateDbModel()); + var reportDbModel = await reportsDbClient.UpdateReportAsync(report.ToReportUpdateDbModel()); if (reportDbModel == null) return null; - - await featuresService.ExecuteReportUpdatePostActions(new ReportUpdateContext(report, reportDbModel)); + + await taskQueue.Enqueue(() => externalClientsActionService.ExecuteReportUpdatePostActions(new ReportUpdateContext(report, reportDbModel))); return reportDbModel; } - + public Task SearchReportsAsync(SearchReports search) { return reportsDbClient.SearchReportsAsync(search); diff --git a/backend/Bugget.DA/Bugget.DA.csproj b/backend/Bugget.DA/Bugget.DA.csproj index 8060aaa..4139d50 100644 --- a/backend/Bugget.DA/Bugget.DA.csproj +++ b/backend/Bugget.DA/Bugget.DA.csproj @@ -17,11 +17,5 @@ - - - - ..\..\..\.dotnet\shared\Microsoft.AspNetCore.App\9.0.2\Microsoft.Extensions.Hosting.Abstractions.dll - - diff --git a/backend/Bugget.DA/Postgres/ReportsDbClient.cs b/backend/Bugget.DA/Postgres/ReportsDbClient.cs index fc3317c..750ca8a 100644 --- a/backend/Bugget.DA/Postgres/ReportsDbClient.cs +++ b/backend/Bugget.DA/Postgres/ReportsDbClient.cs @@ -75,7 +75,7 @@ public async Task CreateReportAsync(string userId, string? /// /// Обновляет краткую информацию об отчете и возвращает его краткую структуру. /// - public async Task PatchReportAsync(int reportId, string userId, string? organizationId, ReportPatchDto dto) + public async Task PatchReportAsync(int reportId, string userId, string? organizationId, ReportPatchDto dto) { await using var connection = await DataSource.OpenConnectionAsync(); @@ -92,7 +92,7 @@ public async Task PatchReportAsync(int reportId, string user } ); - return Deserialize(jsonResult!)!; + return Deserialize(jsonResult!)!; } diff --git a/backend/Bugget.DA/WebSockets/IReportPageHubClient.cs b/backend/Bugget.DA/WebSockets/IReportPageHubClient.cs new file mode 100644 index 0000000..e4e9a9d --- /dev/null +++ b/backend/Bugget.DA/WebSockets/IReportPageHubClient.cs @@ -0,0 +1,8 @@ +using Bugget.Entities.SocketViews; + +namespace Bugget.BO.WebSockets; + +public interface IReportPageHubClient +{ + Task SendReportPatchAsync(int reportId, PatchReportSocketView view); +} \ No newline at end of file diff --git a/backend/Bugget.Entities/DbModels/Report/ReportPatchDbModel.cs b/backend/Bugget.Entities/DbModels/Report/ReportPatchDbModel.cs deleted file mode 100644 index 2374cea..0000000 --- a/backend/Bugget.Entities/DbModels/Report/ReportPatchDbModel.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Bugget.Entities.DbModels.Report; - -public sealed class ReportPatchDbModel -{ - public required int Id { get; init; } - public required string Title { get; init; } - public required int Status { get; init; } - public required string ResponsibleUserId { get; init; } - public required string PastResponsibleUserId { get; init; } - public required string CreatorUserId { get; init; } - public string? CreatorTeamId { get; init; } - public required DateTimeOffset CreatedAt { get; init; } - public required DateTimeOffset UpdatedAt { get; init; } - public required string[] ParticipantsUserIds { get; init; } - public required bool IsParticipantsChanged { get; init; } -} \ No newline at end of file diff --git a/backend/Bugget.Entities/DbModels/Report/ReportPatchResultDbModel.cs b/backend/Bugget.Entities/DbModels/Report/ReportPatchResultDbModel.cs new file mode 100644 index 0000000..fee5d18 --- /dev/null +++ b/backend/Bugget.Entities/DbModels/Report/ReportPatchResultDbModel.cs @@ -0,0 +1,10 @@ +namespace Bugget.Entities.DbModels.Report; + +public sealed class ReportPatchResultDbModel +{ + public required int Id { get; init; } + public required string Title { get; init; } + public required int Status { get; init; } + public required string ResponsibleUserId { get; init; } + public required string PastResponsibleUserId { get; init; } +} \ No newline at end of file diff --git a/backend/Bugget.Entities/SocketViews/ReportSocketView.cs b/backend/Bugget.Entities/SocketViews/PatchReportSocketView.cs similarity index 72% rename from backend/Bugget.Entities/SocketViews/ReportSocketView.cs rename to backend/Bugget.Entities/SocketViews/PatchReportSocketView.cs index 76e7d6d..9558629 100644 --- a/backend/Bugget.Entities/SocketViews/ReportSocketView.cs +++ b/backend/Bugget.Entities/SocketViews/PatchReportSocketView.cs @@ -1,10 +1,9 @@ namespace Bugget.Entities.SocketViews; -public class ReportSocketView +public class PatchReportSocketView { public string? Title { get; set; } public int? Status { get; set; } public string? ResponsibleUserId { get; set; } public string? PastResponsibleUserId { get; set; } - public string[]? ParticipantsUserIds { get; set; } } \ No newline at end of file diff --git a/backend/Bugget.Features/Bugget.Features.csproj b/backend/Bugget.ExternalClients/Bugget.ExternalClients.csproj similarity index 100% rename from backend/Bugget.Features/Bugget.Features.csproj rename to backend/Bugget.ExternalClients/Bugget.ExternalClients.csproj diff --git a/backend/Bugget.Features/Context/ReportCreateContext.cs b/backend/Bugget.ExternalClients/Context/ReportCreateContext.cs similarity index 79% rename from backend/Bugget.Features/Context/ReportCreateContext.cs rename to backend/Bugget.ExternalClients/Context/ReportCreateContext.cs index cf08e83..3f57a11 100644 --- a/backend/Bugget.Features/Context/ReportCreateContext.cs +++ b/backend/Bugget.ExternalClients/Context/ReportCreateContext.cs @@ -1,6 +1,6 @@ using Bugget.Entities.BO.ReportBo; using Bugget.Entities.DbModels.Report; -namespace Bugget.Features.Context; +namespace Bugget.ExternalClients.Context; public record ReportCreateContext(Report Report, ReportObsoleteDbModel ReportDbModel); \ No newline at end of file diff --git a/backend/Bugget.Features/Context/ReportPatchContext.cs b/backend/Bugget.ExternalClients/Context/ReportPatchContext.cs similarity index 62% rename from backend/Bugget.Features/Context/ReportPatchContext.cs rename to backend/Bugget.ExternalClients/Context/ReportPatchContext.cs index 79fef84..125b22c 100644 --- a/backend/Bugget.Features/Context/ReportPatchContext.cs +++ b/backend/Bugget.ExternalClients/Context/ReportPatchContext.cs @@ -1,6 +1,6 @@ using Bugget.Entities.DbModels.Report; using Bugget.Entities.DTO.Report; -namespace Bugget.Features.Context; +namespace Bugget.ExternalClients.Context; -public record ReportPatchContext(string UserId, ReportPatchDto PatchDto, ReportPatchDbModel ReportPatchDbModel); \ No newline at end of file +public record ReportPatchContext(string UserId, ReportPatchDto PatchDto, ReportPatchResultDbModel Result); \ No newline at end of file diff --git a/backend/Bugget.Features/Context/ReportUpdateContext.cs b/backend/Bugget.ExternalClients/Context/ReportUpdateContext.cs similarity index 80% rename from backend/Bugget.Features/Context/ReportUpdateContext.cs rename to backend/Bugget.ExternalClients/Context/ReportUpdateContext.cs index d99f564..9e2dea1 100644 --- a/backend/Bugget.Features/Context/ReportUpdateContext.cs +++ b/backend/Bugget.ExternalClients/Context/ReportUpdateContext.cs @@ -1,6 +1,6 @@ using Bugget.Entities.BO.ReportBo; using Bugget.Entities.DbModels.Report; -namespace Bugget.Features.Context; +namespace Bugget.ExternalClients.Context; public record ReportUpdateContext(ReportUpdate Report, ReportObsoleteDbModel ReportDbModel); \ No newline at end of file diff --git a/backend/Bugget.Features/FeaturesService.cs b/backend/Bugget.ExternalClients/ExternalClientsActionService.cs similarity index 54% rename from backend/Bugget.Features/FeaturesService.cs rename to backend/Bugget.ExternalClients/ExternalClientsActionService.cs index deff098..66d72fe 100644 --- a/backend/Bugget.Features/FeaturesService.cs +++ b/backend/Bugget.ExternalClients/ExternalClientsActionService.cs @@ -1,21 +1,18 @@ -using Bugget.Features.Context; -using Bugget.Features.Interfaces; -using Bugget.Features.TaskQueue; +using Bugget.ExternalClients.Context; +using Bugget.ExternalClients.Interfaces; -namespace Bugget.Features; +namespace Bugget.ExternalClients; -public sealed class FeaturesService( +public sealed class ExternalClientsActionService( IEnumerable reportCreatePostActions, IEnumerable reportUpdatePostActions, - IEnumerable reportPatchPostActions, - ITaskQueue taskQueue) + IEnumerable reportPatchPostActions) { public async Task ExecuteReportCreatePostActions(ReportCreateContext reportCreateContext) { foreach (var reportCreatePostAction in reportCreatePostActions) { - await taskQueue.Enqueue(() => - reportCreatePostAction.ExecuteAsync(reportCreateContext)); + await reportCreatePostAction.ExecuteAsync(reportCreateContext); } } @@ -23,8 +20,7 @@ public async Task ExecuteReportUpdatePostActions(ReportUpdateContext reportUpdat { foreach (var reportUpdatePostAction in reportUpdatePostActions) { - await taskQueue.Enqueue(() => - reportUpdatePostAction.ExecuteAsync(reportUpdateContext)); + await reportUpdatePostAction.ExecuteAsync(reportUpdateContext); } } @@ -32,8 +28,7 @@ public async Task ExecuteReportPatchPostActions(ReportPatchContext reportPatchCo { foreach (var reportPatchPostAction in reportPatchPostActions) { - await taskQueue.Enqueue(() => - reportPatchPostAction.ExecuteAsync(reportPatchContext)); + await reportPatchPostAction.ExecuteAsync(reportPatchContext); } } } \ No newline at end of file diff --git a/backend/Bugget.ExternalClients/ExternalClientsExtensions.cs b/backend/Bugget.ExternalClients/ExternalClientsExtensions.cs new file mode 100644 index 0000000..272dbfc --- /dev/null +++ b/backend/Bugget.ExternalClients/ExternalClientsExtensions.cs @@ -0,0 +1,15 @@ +using Bugget.ExternalClients.Notifications; +using Microsoft.Extensions.DependencyInjection; + +namespace Bugget.ExternalClients; + +public static class ExternalClientsExtensions +{ + public static IServiceCollection AddExternalClients(this IServiceCollection services) + { + services.AddSingleton(); + + services.AddNotifications(); + return services; + } +} \ No newline at end of file diff --git a/backend/Bugget.Features/Interfaces/IReportCreatePostAction.cs b/backend/Bugget.ExternalClients/Interfaces/IReportCreatePostAction.cs similarity index 57% rename from backend/Bugget.Features/Interfaces/IReportCreatePostAction.cs rename to backend/Bugget.ExternalClients/Interfaces/IReportCreatePostAction.cs index 2d5702b..0344955 100644 --- a/backend/Bugget.Features/Interfaces/IReportCreatePostAction.cs +++ b/backend/Bugget.ExternalClients/Interfaces/IReportCreatePostAction.cs @@ -1,6 +1,6 @@ -using Bugget.Features.Context; +using Bugget.ExternalClients.Context; -namespace Bugget.Features.Interfaces; +namespace Bugget.ExternalClients.Interfaces; public interface IReportCreatePostAction { diff --git a/backend/Bugget.Features/Interfaces/IReportPatchPostAction.cs b/backend/Bugget.ExternalClients/Interfaces/IReportPatchPostAction.cs similarity index 57% rename from backend/Bugget.Features/Interfaces/IReportPatchPostAction.cs rename to backend/Bugget.ExternalClients/Interfaces/IReportPatchPostAction.cs index bdb2da9..addfa57 100644 --- a/backend/Bugget.Features/Interfaces/IReportPatchPostAction.cs +++ b/backend/Bugget.ExternalClients/Interfaces/IReportPatchPostAction.cs @@ -1,6 +1,6 @@ -using Bugget.Features.Context; +using Bugget.ExternalClients.Context; -namespace Bugget.Features.Interfaces; +namespace Bugget.ExternalClients.Interfaces; public interface IReportPatchPostAction { diff --git a/backend/Bugget.Features/Interfaces/IReportUpdatePostAction.cs b/backend/Bugget.ExternalClients/Interfaces/IReportUpdatePostAction.cs similarity index 57% rename from backend/Bugget.Features/Interfaces/IReportUpdatePostAction.cs rename to backend/Bugget.ExternalClients/Interfaces/IReportUpdatePostAction.cs index 0397943..34bc8cc 100644 --- a/backend/Bugget.Features/Interfaces/IReportUpdatePostAction.cs +++ b/backend/Bugget.ExternalClients/Interfaces/IReportUpdatePostAction.cs @@ -1,6 +1,6 @@ -using Bugget.Features.Context; +using Bugget.ExternalClients.Context; -namespace Bugget.Features.Interfaces; +namespace Bugget.ExternalClients.Interfaces; public interface IReportUpdatePostAction { diff --git a/backend/Bugget.Features/Notifications/Mattermost/HttpModels/ChannelResponse.cs b/backend/Bugget.ExternalClients/Notifications/Mattermost/HttpModels/ChannelResponse.cs similarity index 53% rename from backend/Bugget.Features/Notifications/Mattermost/HttpModels/ChannelResponse.cs rename to backend/Bugget.ExternalClients/Notifications/Mattermost/HttpModels/ChannelResponse.cs index 51b0970..72bac75 100644 --- a/backend/Bugget.Features/Notifications/Mattermost/HttpModels/ChannelResponse.cs +++ b/backend/Bugget.ExternalClients/Notifications/Mattermost/HttpModels/ChannelResponse.cs @@ -1,4 +1,4 @@ -namespace Bugget.Features.Notifications.Mattermost.HttpModels; +namespace Bugget.ExternalClients.Notifications.Mattermost.HttpModels; public sealed class ChannelResponse { diff --git a/backend/Bugget.Features/Notifications/Mattermost/HttpModels/CreateMattermostMessageRequest.cs b/backend/Bugget.ExternalClients/Notifications/Mattermost/HttpModels/CreateMattermostMessageRequest.cs similarity index 90% rename from backend/Bugget.Features/Notifications/Mattermost/HttpModels/CreateMattermostMessageRequest.cs rename to backend/Bugget.ExternalClients/Notifications/Mattermost/HttpModels/CreateMattermostMessageRequest.cs index 2f8fc03..cc2cfed 100644 --- a/backend/Bugget.Features/Notifications/Mattermost/HttpModels/CreateMattermostMessageRequest.cs +++ b/backend/Bugget.ExternalClients/Notifications/Mattermost/HttpModels/CreateMattermostMessageRequest.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Bugget.Features.Notifications.Mattermost.HttpModels; +namespace Bugget.ExternalClients.Notifications.Mattermost.HttpModels; public sealed class CreateMattermostMessageRequest { diff --git a/backend/Bugget.Features/Notifications/Mattermost/HttpModels/MattermostMessageResponse.cs b/backend/Bugget.ExternalClients/Notifications/Mattermost/HttpModels/MattermostMessageResponse.cs similarity index 90% rename from backend/Bugget.Features/Notifications/Mattermost/HttpModels/MattermostMessageResponse.cs rename to backend/Bugget.ExternalClients/Notifications/Mattermost/HttpModels/MattermostMessageResponse.cs index 7a5aabf..83d811c 100644 --- a/backend/Bugget.Features/Notifications/Mattermost/HttpModels/MattermostMessageResponse.cs +++ b/backend/Bugget.ExternalClients/Notifications/Mattermost/HttpModels/MattermostMessageResponse.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Bugget.Features.Notifications.Mattermost.HttpModels; +namespace Bugget.ExternalClients.Notifications.Mattermost.HttpModels; public sealed class MattermostMessageResponse { diff --git a/backend/Bugget.Features/Notifications/Mattermost/HttpModels/UserResponse.cs b/backend/Bugget.ExternalClients/Notifications/Mattermost/HttpModels/UserResponse.cs similarity index 50% rename from backend/Bugget.Features/Notifications/Mattermost/HttpModels/UserResponse.cs rename to backend/Bugget.ExternalClients/Notifications/Mattermost/HttpModels/UserResponse.cs index d0d8d35..9b32fdb 100644 --- a/backend/Bugget.Features/Notifications/Mattermost/HttpModels/UserResponse.cs +++ b/backend/Bugget.ExternalClients/Notifications/Mattermost/HttpModels/UserResponse.cs @@ -1,4 +1,4 @@ -namespace Bugget.Features.Notifications.Mattermost.HttpModels; +namespace Bugget.ExternalClients.Notifications.Mattermost.HttpModels; public sealed class UserResponse { diff --git a/backend/Bugget.Features/Notifications/Mattermost/MattermostClient.cs b/backend/Bugget.ExternalClients/Notifications/Mattermost/MattermostClient.cs similarity index 96% rename from backend/Bugget.Features/Notifications/Mattermost/MattermostClient.cs rename to backend/Bugget.ExternalClients/Notifications/Mattermost/MattermostClient.cs index d86e3af..8b21712 100644 --- a/backend/Bugget.Features/Notifications/Mattermost/MattermostClient.cs +++ b/backend/Bugget.ExternalClients/Notifications/Mattermost/MattermostClient.cs @@ -1,9 +1,9 @@ using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; -using Bugget.Features.Notifications.Mattermost.HttpModels; +using Bugget.ExternalClients.Notifications.Mattermost.HttpModels; -namespace Bugget.Features.Notifications.Mattermost +namespace Bugget.ExternalClients.Notifications.Mattermost { public class MattermostClient(IHttpClientFactory httpClientFactory) { diff --git a/backend/Bugget.Features/Notifications/Mattermost/MattermostConstants.cs b/backend/Bugget.ExternalClients/Notifications/Mattermost/MattermostConstants.cs similarity index 82% rename from backend/Bugget.Features/Notifications/Mattermost/MattermostConstants.cs rename to backend/Bugget.ExternalClients/Notifications/Mattermost/MattermostConstants.cs index 97fa979..0161fdb 100644 --- a/backend/Bugget.Features/Notifications/Mattermost/MattermostConstants.cs +++ b/backend/Bugget.ExternalClients/Notifications/Mattermost/MattermostConstants.cs @@ -1,4 +1,4 @@ -namespace Bugget.Features.Notifications.Mattermost; +namespace Bugget.ExternalClients.Notifications.Mattermost; public static class MattermostConstants { diff --git a/backend/Bugget.Features/Notifications/Mattermost/MattermostExtensions.cs b/backend/Bugget.ExternalClients/Notifications/Mattermost/MattermostExtensions.cs similarity index 93% rename from backend/Bugget.Features/Notifications/Mattermost/MattermostExtensions.cs rename to backend/Bugget.ExternalClients/Notifications/Mattermost/MattermostExtensions.cs index 3cd5fec..2566222 100644 --- a/backend/Bugget.Features/Notifications/Mattermost/MattermostExtensions.cs +++ b/backend/Bugget.ExternalClients/Notifications/Mattermost/MattermostExtensions.cs @@ -1,7 +1,7 @@ -using Bugget.Features.Interfaces; +using Bugget.ExternalClients.Interfaces; using Microsoft.Extensions.DependencyInjection; -namespace Bugget.Features.Notifications.Mattermost; +namespace Bugget.ExternalClients.Notifications.Mattermost; public static class MattermostExtensions { diff --git a/backend/Bugget.Features/Notifications/Mattermost/MattermostService.cs b/backend/Bugget.ExternalClients/Notifications/Mattermost/MattermostService.cs similarity index 87% rename from backend/Bugget.Features/Notifications/Mattermost/MattermostService.cs rename to backend/Bugget.ExternalClients/Notifications/Mattermost/MattermostService.cs index e611a44..dc8469b 100644 --- a/backend/Bugget.Features/Notifications/Mattermost/MattermostService.cs +++ b/backend/Bugget.ExternalClients/Notifications/Mattermost/MattermostService.cs @@ -1,9 +1,9 @@ using Bugget.DA.Files; using Bugget.Entities.Adapters; -using Bugget.Features.Context; -using Bugget.Features.Interfaces; +using Bugget.ExternalClients.Context; +using Bugget.ExternalClients.Interfaces; -namespace Bugget.Features.Notifications.Mattermost; +namespace Bugget.ExternalClients.Notifications.Mattermost; public sealed class MattermostService( EmployeesDataAccess employeesDataAccess, @@ -53,16 +53,16 @@ public Task ExecuteAsync(ReportPatchContext reportPatchContext) if (reportPatchContext.PatchDto.ResponsibleUserId == null) return Task.CompletedTask; - if (reportPatchContext.UserId == reportPatchContext.ReportPatchDbModel.ResponsibleUserId) + if (reportPatchContext.UserId == reportPatchContext.Result.ResponsibleUserId) return Task.CompletedTask; - var responsibleEmployee = employeesDataAccess.GetEmployee(reportPatchContext.ReportPatchDbModel.ResponsibleUserId); + var responsibleEmployee = employeesDataAccess.GetEmployee(reportPatchContext.Result.ResponsibleUserId); var updaterEmployee = employeesDataAccess.GetEmployee(reportPatchContext.UserId); if (responsibleEmployee == null || updaterEmployee == null) return Task.CompletedTask; var message = ReportMessageBuilder.GetYourResponsibleAfterPatchReportMessage( - reportPatchContext.ReportPatchDbModel.Id, reportPatchContext.ReportPatchDbModel.Title, EmployeeAdapter.Transform(updaterEmployee).Name + reportPatchContext.Result.Id, reportPatchContext.Result.Title, EmployeeAdapter.Transform(updaterEmployee).Name ); return mattermostClient.SendMessageAsync(responsibleEmployee.NotificationUserId, message); diff --git a/backend/Bugget.Features/Notifications/NotificationsConstants.cs b/backend/Bugget.ExternalClients/Notifications/NotificationsConstants.cs similarity index 69% rename from backend/Bugget.Features/Notifications/NotificationsConstants.cs rename to backend/Bugget.ExternalClients/Notifications/NotificationsConstants.cs index 43eab76..715eb6e 100644 --- a/backend/Bugget.Features/Notifications/NotificationsConstants.cs +++ b/backend/Bugget.ExternalClients/Notifications/NotificationsConstants.cs @@ -1,4 +1,4 @@ -namespace Bugget.Features.Notifications; +namespace Bugget.ExternalClients.Notifications; public static class NotificationsConstants { diff --git a/backend/Bugget.Features/Notifications/NotificationsExtensions.cs b/backend/Bugget.ExternalClients/Notifications/NotificationsExtensions.cs similarity index 74% rename from backend/Bugget.Features/Notifications/NotificationsExtensions.cs rename to backend/Bugget.ExternalClients/Notifications/NotificationsExtensions.cs index f75f7ed..2597427 100644 --- a/backend/Bugget.Features/Notifications/NotificationsExtensions.cs +++ b/backend/Bugget.ExternalClients/Notifications/NotificationsExtensions.cs @@ -1,7 +1,7 @@ -using Bugget.Features.Notifications.Mattermost; +using Bugget.ExternalClients.Notifications.Mattermost; using Microsoft.Extensions.DependencyInjection; -namespace Bugget.Features.Notifications; +namespace Bugget.ExternalClients.Notifications; public static class NotificationsExtensions { diff --git a/backend/Bugget.Features/Notifications/ReportMessageBuilder.cs b/backend/Bugget.ExternalClients/Notifications/ReportMessageBuilder.cs similarity index 96% rename from backend/Bugget.Features/Notifications/ReportMessageBuilder.cs rename to backend/Bugget.ExternalClients/Notifications/ReportMessageBuilder.cs index 5dd038a..8ba9cf7 100644 --- a/backend/Bugget.Features/Notifications/ReportMessageBuilder.cs +++ b/backend/Bugget.ExternalClients/Notifications/ReportMessageBuilder.cs @@ -1,4 +1,4 @@ -namespace Bugget.Features.Notifications; +namespace Bugget.ExternalClients.Notifications; public static class ReportMessageBuilder { diff --git a/backend/Bugget.Features/Notifications/readme.md b/backend/Bugget.ExternalClients/Notifications/readme.md similarity index 100% rename from backend/Bugget.Features/Notifications/readme.md rename to backend/Bugget.ExternalClients/Notifications/readme.md diff --git a/backend/Bugget.Features/FeaturesExtensions.cs b/backend/Bugget.Features/FeaturesExtensions.cs deleted file mode 100644 index 9c3e06e..0000000 --- a/backend/Bugget.Features/FeaturesExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Bugget.Features.Interfaces; -using Bugget.Features.Notifications; -using Microsoft.Extensions.DependencyInjection; - -namespace Bugget.Features; - -public static class FeaturesExtensions -{ - public static IServiceCollection AddFeatures(this IServiceCollection services) - { - services.AddNotifications(); - - services.AddSingleton(); - - return services; - } -} \ No newline at end of file diff --git a/backend/Bugget.Features/TaskQueue/ServiceCollectionExtensions.cs b/backend/Bugget.Features/TaskQueue/ServiceCollectionExtensions.cs deleted file mode 100644 index b3efe5a..0000000 --- a/backend/Bugget.Features/TaskQueue/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Bugget.Features.TaskQueue; - -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddTaskQueueHostedService(this IServiceCollection services) => services - .AddSingleton() - .AddHostedService(provider => (TaskQueue)provider.GetRequiredService()); -} \ No newline at end of file diff --git a/backend/Bugget.sln b/backend/Bugget.sln index 60a6dcd..d890f28 100644 --- a/backend/Bugget.sln +++ b/backend/Bugget.sln @@ -8,10 +8,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bugget.DA", "Bugget.DA\Bugg EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bugget.Entities", "Bugget.Entities\Bugget.Entities.csproj", "{A03A0B5B-E38E-4F58-8440-D906B981F6F4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bugget.Features", "Bugget.Features\Bugget.Features.csproj", "{76714590-ABDD-4BDE-95FC-B1B867BC36F1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bugget.ExternalClients", "Bugget.ExternalClients\Bugget.ExternalClients.csproj", "{76714590-ABDD-4BDE-95FC-B1B867BC36F1}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Monade", "Monade\Monade.csproj", "{F3EF44BD-23C8-40D0-8FDE-A8C813AFCE7A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskQueue", "TaskQueue\TaskQueue.csproj", "{3C23A550-82CF-405A-A7AB-5BB8F385152A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,5 +44,9 @@ Global {F3EF44BD-23C8-40D0-8FDE-A8C813AFCE7A}.Debug|Any CPU.Build.0 = Debug|Any CPU {F3EF44BD-23C8-40D0-8FDE-A8C813AFCE7A}.Release|Any CPU.ActiveCfg = Release|Any CPU {F3EF44BD-23C8-40D0-8FDE-A8C813AFCE7A}.Release|Any CPU.Build.0 = Release|Any CPU + {3C23A550-82CF-405A-A7AB-5BB8F385152A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C23A550-82CF-405A-A7AB-5BB8F385152A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C23A550-82CF-405A-A7AB-5BB8F385152A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C23A550-82CF-405A-A7AB-5BB8F385152A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/backend/Bugget/Controllers/ReportsV2Controller.cs b/backend/Bugget/Controllers/ReportsV2Controller.cs index 55a15c9..115c12a 100644 --- a/backend/Bugget/Controllers/ReportsV2Controller.cs +++ b/backend/Bugget/Controllers/ReportsV2Controller.cs @@ -44,7 +44,7 @@ public Task GetReportAsync([FromRoute] int reportId) /// [HttpPatch("{reportId}")] [ProducesResponseType(typeof(ReportDbModel), 200)] - public Task PatchReportAsync([FromRoute] int reportId, [FromBody] ReportPatchDto patchDto) + public Task PatchReportAsync([FromRoute] int reportId, [FromBody] ReportPatchDto patchDto) { var user = User.GetIdentity(); return reportsService.PatchReportAsync(reportId, user.Id, user.OrganizationId, patchDto); diff --git a/backend/Bugget/Hubs/ReportPageHub.cs b/backend/Bugget/Hubs/ReportPageHub.cs index af2f04e..2d730e8 100644 --- a/backend/Bugget/Hubs/ReportPageHub.cs +++ b/backend/Bugget/Hubs/ReportPageHub.cs @@ -1,19 +1,14 @@ using Bugget.Authentication; using Bugget.BO.Services; using Bugget.Entities.DTO.Report; -using Bugget.Entities.SocketViews; -using Bugget.Features; -using Bugget.Features.Context; using Microsoft.AspNetCore.SignalR; namespace Bugget.Hubs; public sealed class ReportPageHub( ILogger logger, - ReportsService reportsService, - FeaturesService featuresService) : Hub + ReportsService reportsService) : Hub { - // Подключение к группе комментариев по reportId public async Task JoinReportGroupAsync(int reportId) { @@ -35,37 +30,23 @@ public override async Task OnDisconnectedAsync(Exception? exception) await base.OnDisconnectedAsync(exception); } - public async Task PatchReportAsync(int reportId, ReportPatchDto patchDto) + public Task PatchReportAsync(int reportId, ReportPatchDto patchDto) { if (Context.User == null) { logger.LogError("Пользователь не авторизован"); - return; + return Task.CompletedTask; } var user = new UserIdentity(Context.User); logger.LogInformation("Пользователь {@UserId} патчит отчёт {@ReportId}, {@PatchDto}", user.Id, reportId, patchDto); - var result = await reportsService.PatchReportAsync( + return reportsService.PatchReportAsync( reportId, user.Id, user.OrganizationId, patchDto ); - - var socketView = new ReportSocketView - { - Title = patchDto.Title, - Status = patchDto.Status, - ResponsibleUserId = patchDto.ResponsibleUserId, - PastResponsibleUserId = patchDto.ResponsibleUserId != null ? result.PastResponsibleUserId : null, - ParticipantsUserIds = result.IsParticipantsChanged ? result.ParticipantsUserIds : null, - }; - - await Clients.Group($"{reportId}") - .SendAsync("ReceiveReportPatch", socketView); - - await featuresService.ExecuteReportPatchPostActions(new ReportPatchContext(user.Id, patchDto, result)); } } \ No newline at end of file diff --git a/backend/Bugget/Hubs/ReportPageHubClient.cs b/backend/Bugget/Hubs/ReportPageHubClient.cs new file mode 100644 index 0000000..e6e1a1e --- /dev/null +++ b/backend/Bugget/Hubs/ReportPageHubClient.cs @@ -0,0 +1,21 @@ +using Bugget.BO.WebSockets; +using Bugget.Entities.SocketViews; +using Microsoft.AspNetCore.SignalR; + +namespace Bugget.Hubs; + +public class ReportPageHubClient : IReportPageHubClient +{ + private readonly IHubContext _hubContext; + + public ReportPageHubClient(IHubContext hubContext) + { + _hubContext = hubContext; + } + + public Task SendReportPatchAsync(int reportId, PatchReportSocketView view) + { + return _hubContext.Clients.Group($"{reportId}") + .SendAsync("ReceiveReportPatch", view); + } +} \ No newline at end of file diff --git a/backend/Bugget/Program.cs b/backend/Bugget/Program.cs index 7c6fac2..90ef279 100644 --- a/backend/Bugget/Program.cs +++ b/backend/Bugget/Program.cs @@ -2,18 +2,16 @@ using System.Text.RegularExpressions; using Bugget.Authentication; using Bugget.BO.Services; +using Bugget.BO.WebSockets; using Bugget.DA.Files; using Bugget.DA.Postgres; using Bugget.Entities.Config; -using Bugget.Features; -using Bugget.Features.TaskQueue; using Bugget.Hubs; using Bugget.Middlewares; -using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; using Bugget.DA.Interfaces; +using Bugget.ExternalClients; +using TaskQueue; var builder = WebApplication.CreateBuilder(args); @@ -52,7 +50,7 @@ builder.Services.AddAutoMapper(typeof(Bugget.Entities.MappingProfiles.BugMappingProfile).Assembly); -builder.Services.AddFeatures(); +builder.Services.AddExternalClients(); builder.Services .AddSingleton() @@ -75,7 +73,8 @@ builder.Services.AddHealthChecks(); builder.Services.AddLdapAuth(); -builder.Services.AddTaskQueueHostedService(); +builder.Services.AddSingleton() + .AddHostedService(provider => (TaskQueue.TaskQueue)provider.GetRequiredService()); #region Swagger @@ -97,6 +96,8 @@ .ConfigureApiBehaviorOptions(o => o.InvalidModelStateResponseFactory = _ => new ModelStateInvalidHandler()); +builder.Services.AddSingleton(); + var app = builder.Build(); #region Swagger diff --git a/backend/Bugget.Features/TaskQueue/ITaskQueue.cs b/backend/TaskQueue/ITaskQueue.cs similarity index 85% rename from backend/Bugget.Features/TaskQueue/ITaskQueue.cs rename to backend/TaskQueue/ITaskQueue.cs index 23f4f2d..63afe70 100644 --- a/backend/Bugget.Features/TaskQueue/ITaskQueue.cs +++ b/backend/TaskQueue/ITaskQueue.cs @@ -1,4 +1,4 @@ -namespace Bugget.Features.TaskQueue; +namespace TaskQueue; public interface ITaskQueue { diff --git a/backend/Bugget.Features/TaskQueue/TaskQueue.cs b/backend/TaskQueue/TaskQueue.cs similarity index 68% rename from backend/Bugget.Features/TaskQueue/TaskQueue.cs rename to backend/TaskQueue/TaskQueue.cs index 2108243..8b4e939 100644 --- a/backend/Bugget.Features/TaskQueue/TaskQueue.cs +++ b/backend/TaskQueue/TaskQueue.cs @@ -4,23 +4,15 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Bugget.Features.TaskQueue; +namespace TaskQueue; -internal class TaskQueue : BackgroundService, ITaskQueue +public class TaskQueue(ILogger logger, IServiceProvider serviceProvider) + : BackgroundService, ITaskQueue { - private readonly ILogger _logger; - private readonly IServiceProvider _serviceProvider; - private readonly Channel _queue; + private readonly Channel _queue = Channel.CreateUnbounded(); private record TaskContext(Func WorkItem, Activity Activity); - public TaskQueue(ILogger logger, IServiceProvider serviceProvider) - { - _logger = logger; - _serviceProvider = serviceProvider; - _queue = Channel.CreateUnbounded(); - } - public ValueTask Enqueue(Func workItem) { var nestedActivity = new Activity($"{nameof(TaskQueue)}.{nameof(ExecuteAsync)}").Start(); @@ -45,12 +37,12 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken) => Paralle try { - using var serviceScope = _serviceProvider.CreateScope(); + using var serviceScope = serviceProvider.CreateScope(); await context.WorkItem(serviceScope.ServiceProvider, ct); } catch (Exception e) { - _logger.LogError(e, "При выполнении фоновой задачи произошла ошибка"); + logger.LogError(e, "При выполнении фоновой задачи произошла ошибка"); } }); } diff --git a/backend/TaskQueue/TaskQueue.csproj b/backend/TaskQueue/TaskQueue.csproj new file mode 100644 index 0000000..f671cf7 --- /dev/null +++ b/backend/TaskQueue/TaskQueue.csproj @@ -0,0 +1,12 @@ + + + + net9.0 + enable + enable + + + + + + diff --git a/backend/Bugget.Features/TaskQueue/TaskQueueExtensions.cs b/backend/TaskQueue/TaskQueueExtensions.cs similarity index 96% rename from backend/Bugget.Features/TaskQueue/TaskQueueExtensions.cs rename to backend/TaskQueue/TaskQueueExtensions.cs index 2eb8a28..7fd46f2 100644 --- a/backend/Bugget.Features/TaskQueue/TaskQueueExtensions.cs +++ b/backend/TaskQueue/TaskQueueExtensions.cs @@ -1,4 +1,4 @@ -namespace Bugget.Features.TaskQueue; +namespace TaskQueue; public static class TaskQueueExtensions { From 76e14389a4f0e71f00561a332b1d31fd7ba30587 Mon Sep 17 00:00:00 2001 From: "aleksandr.z" Date: Sun, 20 Apr 2025 15:39:36 +0300 Subject: [PATCH 2/5] =?UTF-8?q?=D0=9F=D0=BE=D0=BA=D1=80=D1=8B=D0=BB=20?= =?UTF-8?q?=D0=B2=D1=81=D0=B5=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20mvp=20=D0=BA=D0=B5=D0=B9=D1=81=D1=8B=20=D1=81?= =?UTF-8?q?=D0=B2=D1=8F=D0=B7=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=81=20rep?= =?UTF-8?q?ort=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/Bugget.BO/Mappers/ReportMapper.cs | 22 +- .../Bugget.BO/Services/ParticipantsService.cs | 18 ++ .../Services/ReportAutoStatusService.cs | 25 +++ .../Bugget.BO/Services/ReportEventsService.cs | 26 +++ backend/Bugget.BO/Services/ReportsService.cs | 23 +- .../Postgres/ParticipantsDbClient.cs | 19 ++ backend/Bugget.DA/Postgres/ReportsDbClient.cs | 22 +- .../WebSockets/IReportPageHubClient.cs | 1 + backend/Bugget/Hubs/ReportPageHub.cs | 3 - backend/Bugget/Hubs/ReportPageHubClient.cs | 13 +- backend/Bugget/Middlewares/HubErrorFilter.cs | 33 +++ .../ResultExceptionHandlerMiddleware.cs | 5 +- backend/Bugget/Program.cs | 10 +- devops/migrator/sql/010_dml_reports_v2.sql | 203 +++++++++--------- 14 files changed, 277 insertions(+), 146 deletions(-) create mode 100644 backend/Bugget.BO/Services/ParticipantsService.cs create mode 100644 backend/Bugget.BO/Services/ReportAutoStatusService.cs create mode 100644 backend/Bugget.BO/Services/ReportEventsService.cs create mode 100644 backend/Bugget.DA/Postgres/ParticipantsDbClient.cs create mode 100644 backend/Bugget/Middlewares/HubErrorFilter.cs diff --git a/backend/Bugget.BO/Mappers/ReportMapper.cs b/backend/Bugget.BO/Mappers/ReportMapper.cs index a1f006c..e4a0587 100644 --- a/backend/Bugget.BO/Mappers/ReportMapper.cs +++ b/backend/Bugget.BO/Mappers/ReportMapper.cs @@ -6,6 +6,7 @@ using Bugget.Entities.DbModels.Bug; using Bugget.Entities.DbModels.Report; using Bugget.Entities.DTO.Report; +using Bugget.Entities.SocketViews; using Bugget.Entities.Views; namespace Bugget.BO.Mappers; @@ -52,11 +53,11 @@ public static Report ToReport(this ReportCreateDto report, string userId) ResponsibleUserId = report.ResponsibleId, CreatorUserId = userId, Bugs = report.Bugs.Select(b => new Bug - { - Receive = b.Receive, - Expect = b.Expect, - CreatorUserId = userId, - }) + { + Receive = b.Receive, + Expect = b.Expect, + CreatorUserId = userId, + }) .ToArray(), ParticipantsUserIds = new string[] { userId, report.ResponsibleId }.Distinct().ToArray() }; @@ -139,4 +140,15 @@ public static SearchReports ToSearchReports( Sort = SortOption.Parse(sort) }; } + + public static PatchReportSocketView ToSocketView(this ReportPatchDto patchDto, ReportPatchResultDbModel? result) + { + return new PatchReportSocketView + { + Title = patchDto.Title, + Status = patchDto.Status, + ResponsibleUserId = patchDto.ResponsibleUserId, + PastResponsibleUserId = patchDto.ResponsibleUserId == null ? null : result?.PastResponsibleUserId + }; + } } \ No newline at end of file diff --git a/backend/Bugget.BO/Services/ParticipantsService.cs b/backend/Bugget.BO/Services/ParticipantsService.cs new file mode 100644 index 0000000..caee24e --- /dev/null +++ b/backend/Bugget.BO/Services/ParticipantsService.cs @@ -0,0 +1,18 @@ +using Bugget.BO.WebSockets; +using Bugget.DA.Postgres; + +namespace Bugget.BO.Services +{ + public class ParticipantsService(ParticipantsDbClient participantsDbClient, IReportPageHubClient reportPageHubClient) + { + public async Task AddParticipantIfNotExistAsync(int reportId, string userId) + { + var participants = await participantsDbClient.AddParticipantIfNotExistAsync(reportId, userId); + + if (participants != null) + { + await reportPageHubClient.SendReportParticipantsAsync(reportId, participants); + } + } + } +} \ No newline at end of file diff --git a/backend/Bugget.BO/Services/ReportAutoStatusService.cs b/backend/Bugget.BO/Services/ReportAutoStatusService.cs new file mode 100644 index 0000000..3f01b4f --- /dev/null +++ b/backend/Bugget.BO/Services/ReportAutoStatusService.cs @@ -0,0 +1,25 @@ +using Bugget.BO.WebSockets; +using Bugget.DA.Postgres; +using Bugget.Entities.BO.ReportBo; +using Bugget.Entities.DbModels.Report; +using Bugget.Entities.DTO.Report; +using Bugget.Entities.SocketViews; + +namespace Bugget.BO.Services +{ + public class ReportAutoStatusService(ReportsDbClient reportsDbClient, IReportPageHubClient reportPageHubClient) + { + public async Task CalculateStatusAsync(int reportId, ReportPatchDto patchDto, ReportPatchResultDbModel result) + { + // если статус backlog и меняется ответственный, то меняем статус на in progress + if (result.Status == (int)ReportStatus.Backlog && patchDto.ResponsibleUserId != null) + { + await reportsDbClient.ChangeStatusAsync(reportId, (int)ReportStatus.InProgress); + await reportPageHubClient.SendReportPatchAsync(reportId, new PatchReportSocketView + { + Status = (int)ReportStatus.InProgress, + }); + } + } + } +} \ No newline at end of file diff --git a/backend/Bugget.BO/Services/ReportEventsService.cs b/backend/Bugget.BO/Services/ReportEventsService.cs new file mode 100644 index 0000000..1a8183f --- /dev/null +++ b/backend/Bugget.BO/Services/ReportEventsService.cs @@ -0,0 +1,26 @@ +using Bugget.BO.Mappers; +using Bugget.BO.WebSockets; +using Bugget.Entities.DbModels.Report; +using Bugget.Entities.DTO.Report; +using Bugget.ExternalClients; +using Bugget.ExternalClients.Context; + +namespace Bugget.BO.Services +{ + public class ReportEventsService( + IReportPageHubClient reportPageHubClient, + ExternalClientsActionService externalClientsActionService, + ParticipantsService participantsService, + ReportAutoStatusService autoStatusService) + { + public async Task HandlePatchReportEventAsync(int reportId, string userId, ReportPatchDto patchDto, ReportPatchResultDbModel result) + { + await Task.WhenAll( + reportPageHubClient.SendReportPatchAsync(reportId, patchDto.ToSocketView(result)), + externalClientsActionService.ExecuteReportPatchPostActions(new ReportPatchContext(userId, patchDto, result)), + participantsService.AddParticipantIfNotExistAsync(reportId, userId), + autoStatusService.CalculateStatusAsync(reportId, patchDto, result) + ); + } + } +} \ No newline at end of file diff --git a/backend/Bugget.BO/Services/ReportsService.cs b/backend/Bugget.BO/Services/ReportsService.cs index 6650c96..c900fa4 100644 --- a/backend/Bugget.BO/Services/ReportsService.cs +++ b/backend/Bugget.BO/Services/ReportsService.cs @@ -1,14 +1,13 @@ using Bugget.BO.Errors; using Bugget.BO.Mappers; -using Bugget.BO.WebSockets; using Bugget.DA.Postgres; using Bugget.Entities.BO.ReportBo; using Bugget.Entities.BO.Search; using Bugget.Entities.DbModels.Report; using Bugget.Entities.DTO.Report; -using Bugget.Entities.SocketViews; using Bugget.ExternalClients; using Bugget.ExternalClients.Context; +using Microsoft.Extensions.Logging; using Monade; using TaskQueue; @@ -18,7 +17,8 @@ public sealed class ReportsService( ReportsDbClient reportsDbClient, ExternalClientsActionService externalClientsActionService, ITaskQueue taskQueue, - IReportPageHubClient reportPageHubClient) + ReportEventsService reportEventsService, + ILogger logger) { public async Task CreateReportAsync(Report report) { @@ -40,23 +40,12 @@ public Task CreateReportAsync(string userId, string? teamI public async Task PatchReportAsync(int reportId, string userId, string? organizationId, ReportPatchDto patchDto) { - var result = await reportsDbClient.PatchReportAsync(reportId, userId, organizationId, patchDto); - var socketView = new PatchReportSocketView - { - Title = patchDto.Title, - Status = patchDto.Status, - ResponsibleUserId = patchDto.ResponsibleUserId, - PastResponsibleUserId = patchDto.ResponsibleUserId == null ? null : result.PastResponsibleUserId - }; + logger.LogInformation("Пользователь {@UserId} патчит отчёт {@ReportId}, {@PatchDto}", userId, reportId, patchDto); - await reportPageHubClient.SendReportPatchAsync(reportId, socketView); + var result = await reportsDbClient.PatchReportAsync(reportId, userId, organizationId, patchDto); - await taskQueue.Enqueue(() => externalClientsActionService.ExecuteReportPatchPostActions(new ReportPatchContext(userId, patchDto, result))); + await taskQueue.Enqueue(() => reportEventsService.HandlePatchReportEventAsync(reportId, userId, patchDto, result)); - // todo проставление участников и уведомление - - // todo вычисление статуса - return result; } diff --git a/backend/Bugget.DA/Postgres/ParticipantsDbClient.cs b/backend/Bugget.DA/Postgres/ParticipantsDbClient.cs new file mode 100644 index 0000000..f14be76 --- /dev/null +++ b/backend/Bugget.DA/Postgres/ParticipantsDbClient.cs @@ -0,0 +1,19 @@ +using Dapper; + +namespace Bugget.DA.Postgres +{ + public class ParticipantsDbClient : PostgresClient + { + public async Task AddParticipantIfNotExistAsync(int reportId, string userId) + { + await using var connection = await DataSource.OpenConnectionAsync(); + + var participants = await connection.ExecuteScalarAsync( + "SELECT public.add_participant_if_not_exist(@report_id, @user_id);", + new { report_id = reportId, user_id = userId } + ); + + return participants; + } + } +} \ No newline at end of file diff --git a/backend/Bugget.DA/Postgres/ReportsDbClient.cs b/backend/Bugget.DA/Postgres/ReportsDbClient.cs index 750ca8a..1bbc568 100644 --- a/backend/Bugget.DA/Postgres/ReportsDbClient.cs +++ b/backend/Bugget.DA/Postgres/ReportsDbClient.cs @@ -7,7 +7,7 @@ namespace Bugget.DA.Postgres; -public sealed class ReportsDbClient: PostgresClient +public sealed class ReportsDbClient : PostgresClient { /// /// Получает отчет по ID. @@ -36,7 +36,7 @@ public sealed class ReportsDbClient: PostgresClient return jsonResult != null ? Deserialize(jsonResult) : null; } - + public async Task ListReportsAsync(string userId) { await using var connection = await DataSource.OpenConnectionAsync(); @@ -123,11 +123,11 @@ public async Task PatchReportAsync(int reportId, strin ? Deserialize(jsonResult) : null; } - + public async Task UpdateReportAsync(ReportUpdateDbModel reportDbModel) { await using var connection = await DataSource.OpenConnectionAsync(); - + var jsonResult = await connection.ExecuteScalarAsync( "SELECT public.update_report(@report_id, @participants,@title, @status, @responsible_user_id);", new @@ -144,7 +144,7 @@ public async Task PatchReportAsync(int reportId, strin ? Deserialize(jsonResult) : null; } - + public async Task SearchReportsAsync(SearchReports search) { await using var connection = await DataSource.OpenConnectionAsync(); @@ -165,6 +165,16 @@ public async Task SearchReportsAsync(SearchReports search) return Deserialize(jsonResult); } - + + public async Task ChangeStatusAsync(int reportId, int newStatus) + { + await using var connection = await DataSource.OpenConnectionAsync(); + + await connection.ExecuteAsync( + "SELECT public.change_status(@report_id, @new_status);", + new { report_id = reportId, new_status = newStatus } + ); + } + private T? Deserialize(string json) => JsonSerializer.Deserialize(json, JsonSerializerOptions); } \ No newline at end of file diff --git a/backend/Bugget.DA/WebSockets/IReportPageHubClient.cs b/backend/Bugget.DA/WebSockets/IReportPageHubClient.cs index e4e9a9d..d638308 100644 --- a/backend/Bugget.DA/WebSockets/IReportPageHubClient.cs +++ b/backend/Bugget.DA/WebSockets/IReportPageHubClient.cs @@ -5,4 +5,5 @@ namespace Bugget.BO.WebSockets; public interface IReportPageHubClient { Task SendReportPatchAsync(int reportId, PatchReportSocketView view); + Task SendReportParticipantsAsync(int reportId, string[] participants); } \ No newline at end of file diff --git a/backend/Bugget/Hubs/ReportPageHub.cs b/backend/Bugget/Hubs/ReportPageHub.cs index 2d730e8..1ea421a 100644 --- a/backend/Bugget/Hubs/ReportPageHub.cs +++ b/backend/Bugget/Hubs/ReportPageHub.cs @@ -39,9 +39,6 @@ public Task PatchReportAsync(int reportId, ReportPatchDto patchDto) } var user = new UserIdentity(Context.User); - - logger.LogInformation("Пользователь {@UserId} патчит отчёт {@ReportId}, {@PatchDto}", user.Id, reportId, patchDto); - return reportsService.PatchReportAsync( reportId, user.Id, diff --git a/backend/Bugget/Hubs/ReportPageHubClient.cs b/backend/Bugget/Hubs/ReportPageHubClient.cs index e6e1a1e..3351926 100644 --- a/backend/Bugget/Hubs/ReportPageHubClient.cs +++ b/backend/Bugget/Hubs/ReportPageHubClient.cs @@ -4,18 +4,19 @@ namespace Bugget.Hubs; -public class ReportPageHubClient : IReportPageHubClient +public class ReportPageHubClient(IHubContext hubContext) : IReportPageHubClient { - private readonly IHubContext _hubContext; + private readonly IHubContext _hubContext = hubContext; - public ReportPageHubClient(IHubContext hubContext) + public Task SendReportPatchAsync(int reportId, PatchReportSocketView view) { - _hubContext = hubContext; + return _hubContext.Clients.Group($"{reportId}") + .SendAsync("ReceiveReportPatch", view); } - public Task SendReportPatchAsync(int reportId, PatchReportSocketView view) + public Task SendReportParticipantsAsync(int reportId, string[] participants) { return _hubContext.Clients.Group($"{reportId}") - .SendAsync("ReceiveReportPatch", view); + .SendAsync("ReceiveReportParticipants", participants); } } \ No newline at end of file diff --git a/backend/Bugget/Middlewares/HubErrorFilter.cs b/backend/Bugget/Middlewares/HubErrorFilter.cs new file mode 100644 index 0000000..d5db080 --- /dev/null +++ b/backend/Bugget/Middlewares/HubErrorFilter.cs @@ -0,0 +1,33 @@ +using System.Text.Json; +using Bugget.BO.Errors; +using Microsoft.AspNetCore.SignalR; +using Npgsql; + +namespace Bugget.Middlewares; + +public class HubErrorFilter(ILogger logger) : IHubFilter +{ + protected readonly JsonSerializerOptions JsonSerializerOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + }; + + public async ValueTask InvokeMethodAsync( + HubInvocationContext context, + Func> next) + { + try + { + return await next(context); + } + catch (PostgresException ex) when (ex.SqlState == "P0404") + { + throw new HubException(JsonSerializer.Serialize(BoErrors.NotFoundError, JsonSerializerOptions)); + } + catch (Exception ex) + { + logger.LogError(ex, "Unhandled exception in hub method {HubMethod}", context.HubMethodName); + throw new HubException(JsonSerializer.Serialize(BoErrors.InternalServerError, JsonSerializerOptions)); + } + } +} \ No newline at end of file diff --git a/backend/Bugget/Middlewares/ResultExceptionHandlerMiddleware.cs b/backend/Bugget/Middlewares/ResultExceptionHandlerMiddleware.cs index d2fa8e4..6c4da93 100644 --- a/backend/Bugget/Middlewares/ResultExceptionHandlerMiddleware.cs +++ b/backend/Bugget/Middlewares/ResultExceptionHandlerMiddleware.cs @@ -8,7 +8,6 @@ namespace Bugget.Middlewares; public class ResultExceptionHandlerMiddleware(ILogger logger) : IMiddleware { - private const string CatchedOnMiddlewareError = "Обработана ошибка на мидлваре"; private readonly ILogger _logger = logger; public async Task InvokeAsync(HttpContext context, RequestDelegate next) @@ -17,14 +16,14 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) { await next(context); } - catch (PostgresException ex) when (ex.SqlState == "P404") + catch (PostgresException ex) when (ex.SqlState == "P0404") { context.Response.StatusCode = (int)HttpStatusCode.NotFound; await context.Response.WriteAsJsonAsync(BoErrors.NotFoundError); } catch (Exception e) { - _logger.LogError(e, CatchedOnMiddlewareError); + _logger.LogError(e, "Обработана ошибка на мидлваре"); context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; await context.Response.WriteAsJsonAsync(BoErrors.InternalServerError); diff --git a/backend/Bugget/Program.cs b/backend/Bugget/Program.cs index 90ef279..20db060 100644 --- a/backend/Bugget/Program.cs +++ b/backend/Bugget/Program.cs @@ -12,6 +12,7 @@ using Bugget.DA.Interfaces; using Bugget.ExternalClients; using TaskQueue; +using Microsoft.AspNetCore.SignalR; var builder = WebApplication.CreateBuilder(args); @@ -26,6 +27,9 @@ options.EnableDetailedErrors = true; // Показывать ошибки в логе options.KeepAliveInterval = TimeSpan.FromSeconds(15); // Пинг каждые 15 сек options.ClientTimeoutInterval = TimeSpan.FromSeconds(60); // Клиент ждёт 60 сек перед разрывом +}).AddHubOptions(options => +{ + options.AddFilter(); }); // разрешаем cors для локального тестирования @@ -57,7 +61,10 @@ .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); builder.Services .AddSingleton() @@ -66,6 +73,7 @@ .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton((sp) => sp.GetRequiredService()); builder.Services.AddHostedService((sp) => sp.GetRequiredService()); diff --git a/devops/migrator/sql/010_dml_reports_v2.sql b/devops/migrator/sql/010_dml_reports_v2.sql index a4ed893..dbcb14e 100644 --- a/devops/migrator/sql/010_dml_reports_v2.sql +++ b/devops/migrator/sql/010_dml_reports_v2.sql @@ -1,3 +1,22 @@ +CREATE OR REPLACE FUNCTION public.get_report_summary(_report_id integer, _organization_id text DEFAULT NULL) + RETURNS jsonb + LANGUAGE plpgsql + AS $$ +DECLARE + result jsonb; +BEGIN + SELECT + jsonb_build_object('id', r.id, 'title', r.title, 'status', r.status, 'responsible_user_id', r.responsible_user_id, 'past_responsible_user_id', r.past_responsible_user_id, 'creator_user_id', r.creator_user_id, 'creator_team_id', r.creator_team_id, 'created_at', r.created_at, 'updated_at', r.updated_at) INTO result + FROM + reports r + WHERE + r.id = _report_id + AND (_organization_id IS NULL + OR r.creator_organization_id = _organization_id); + RETURN result; +END; +$$; + CREATE OR REPLACE FUNCTION public.create_report(_user_id text, _title text, _team_id text DEFAULT NULL, _organization_id text DEFAULT NULL) RETURNS jsonb LANGUAGE plpgsql @@ -21,7 +40,7 @@ BEGIN SELECT COALESCE(jsonb_agg(user_id), '[]'::jsonb) INTO participants FROM - public.report_participants as rp + public.report_participants AS rp WHERE rp.report_id = new_report_id; -- Собираем финальный JSON @@ -29,34 +48,12 @@ BEGIN END; $$; -CREATE OR REPLACE FUNCTION public.get_report_summary(_report_id integer, _organization_id text DEFAULT NULL) - RETURNS jsonb - LANGUAGE plpgsql - AS $$ -DECLARE - result jsonb; -BEGIN - SELECT - jsonb_build_object('id', r.id, 'title', r.title, 'status', r.status, 'responsible_user_id', r.responsible_user_id, 'past_responsible_user_id', r.past_responsible_user_id, 'creator_user_id', r.creator_user_id, 'creator_team_id', r.creator_team_id, 'created_at', r.created_at, 'updated_at', r.updated_at) INTO result - FROM - reports r - WHERE - r.id = _report_id - AND (_organization_id IS NULL - OR r.creator_organization_id = _organization_id); - RETURN result; -END; -$$; - CREATE OR REPLACE FUNCTION public.patch_report(_report_id integer, _user_id text, _organization_id text DEFAULT NULL, _title text DEFAULT NULL, _status integer DEFAULT NULL, _responsible_user_id text DEFAULT NULL) RETURNS jsonb LANGUAGE plpgsql AS $$ DECLARE report_id integer; - summary jsonb; - participants jsonb; - is_participants_changed boolean := FALSE; BEGIN -- Проверка доступа SELECT @@ -69,7 +66,7 @@ BEGIN OR r.creator_organization_id = _organization_id); IF NOT FOUND THEN RAISE EXCEPTION 'Report % not found or access denied', _report_id - USING ERRCODE = 'P404'; + USING ERRCODE = 'P0404'; END IF; -- Обновление UPDATE @@ -86,95 +83,91 @@ BEGIN responsible_user_id = COALESCE(_responsible_user_id, responsible_user_id) WHERE id = _report_id; - -- Добавление участника - IF NOT EXISTS ( - SELECT - 1 - FROM - public.report_participants as rp - WHERE - rp.report_id = _report_id - AND user_id = _user_id) THEN - INSERT INTO public.report_participants(report_id, user_id) - VALUES (_report_id, _user_id); - is_participants_changed := TRUE; - END IF; - -- Сборка результата - summary := public.get_report_summary(_report_id, _organization_id); + RETURN public.get_report_summary(_report_id, _organization_id); +END; +$$; + +CREATE OR REPLACE FUNCTION public.get_report_v2(_report_id int, _organization_id text DEFAULT NULL) + RETURNS jsonb + LANGUAGE plpgsql + AS $$ +DECLARE + result jsonb; +BEGIN + -- Формируем JSON с Report и его связями SELECT - COALESCE(jsonb_agg(user_id), '[]'::jsonb) INTO participants + jsonb_build_object('id', r.id, 'title', r.title, 'status', r.status, 'created_at', r.created_at, 'updated_at', r.updated_at, 'creator_user_id', r.creator_user_id, 'creator_team_id', r.creator_team_id, 'responsible_user_id', r.responsible_user_id, 'past_responsible_user_id', r.past_responsible_user_id, 'bugs', COALESCE(( + SELECT + jsonb_agg(jsonb_build_object('id', b.id, 'report_id', b.report_id, 'receive', b.receive, 'expect', b.expect, 'status', b.status, 'creator_user_id', b.creator_user_id, 'created_at', b.created_at, 'updated_at', b.updated_at, 'attachments', COALESCE(( + SELECT + jsonb_agg(jsonb_build_object('id', a.id, 'path', a.path, 'attach_type', a.attach_type, 'created_at', a.created_at)) + FROM public.attachments a + WHERE + a.bug_id = b.id), '[]'::jsonb), 'comments', COALESCE(( + SELECT + jsonb_agg(jsonb_build_object('id', c.id, 'text', c.text, 'creator_user_id', c.creator_user_id, 'bug_id', c.bug_id, 'created_at', c.created_at, 'updated_at', c.updated_at)) + FROM public.comments c + WHERE + c.bug_id = b.id), '[]'::jsonb))) + FROM public.bugs b + WHERE + b.report_id = r.id), '[]'::jsonb), 'participants_user_ids', COALESCE(( + SELECT + jsonb_agg(user_id) + FROM public.report_participants AS rp + WHERE + rp.report_id = r.id), '[]'::jsonb)) INTO result FROM - public.report_participants as rp + public.reports r WHERE - rp.report_id = _report_id; - RETURN summary || jsonb_build_object('participants_user_ids', participants, 'is_participants_changed', is_participants_changed); + r.id = _report_id + AND (_organization_id IS NULL + OR r.creator_organization_id = _organization_id); + RETURN result; END; $$; -CREATE OR REPLACE FUNCTION public.get_report_v2(_report_id INT, _organization_id TEXT DEFAULT NULL) - RETURNS JSONB +CREATE OR REPLACE FUNCTION public.add_participant_if_not_exist(_report_id integer, _user_id text) + RETURNS text[] -- возвращаем массив идентификаторов или NULL LANGUAGE plpgsql -AS -$$ + AS $$ DECLARE - result JSONB; + inserted_count int; + participants text[]; BEGIN - -- Формируем JSON с Report и его связями - SELECT jsonb_build_object( - 'id', r.id, - 'title', r.title, - 'status', r.status, - 'created_at', r.created_at, - 'updated_at', r.updated_at, - 'creator_user_id', r.creator_user_id, - 'creator_team_id', r.creator_team_id, - 'responsible_user_id', r.responsible_user_id, - 'past_responsible_user_id', r.past_responsible_user_id, - 'bugs', COALESCE((SELECT jsonb_agg( - jsonb_build_object( - 'id', b.id, - 'report_id', b.report_id, - 'receive', b.receive, - 'expect', b.expect, - 'status', b.status, - 'creator_user_id', b.creator_user_id, - 'created_at', b.created_at, - 'updated_at', b.updated_at, - 'attachments', COALESCE((SELECT jsonb_agg( - jsonb_build_object( - 'id', a.id, - 'path', a.path, - 'attach_type', a.attach_type, - 'created_at', a.created_at - ) - ) - FROM public.attachments a - WHERE a.bug_id = b.id), '[]'::jsonb), - 'comments', COALESCE((SELECT jsonb_agg( - jsonb_build_object( - 'id', c.id, - 'text', c.text, - 'creator_user_id', c.creator_user_id, - 'bug_id', c.bug_id, - 'created_at', c.created_at, - 'updated_at', c.updated_at - ) - ) - FROM public.comments c - WHERE c.bug_id = b.id), '[]'::jsonb) - ) - ) - FROM public.bugs b - WHERE b.report_id = r.id), '[]'::jsonb), - 'participants_user_ids', COALESCE((SELECT jsonb_agg(user_id) - FROM public.report_participants as rp - WHERE rp.report_id = r.id), '[]'::jsonb) - ) - INTO result - FROM public.reports r - WHERE r.id = _report_id - AND (_organization_id IS NULL OR r.creator_organization_id = _organization_id); + -- пытаемся добавить, при конфликте ничего не делаем + INSERT INTO public.report_participants(report_id, user_id) + VALUES (_report_id, _user_id) + ON CONFLICT (report_id, user_id) + DO NOTHING; + + GET DIAGNOSTICS inserted_count = ROW_COUNT; + -- если ничего не вставилось — возвращаем NULL + IF inserted_count = 0 THEN + RETURN NULL; + END IF; + -- собираем всех участников в массив + SELECT + array_agg(user_id) INTO participants + FROM + public.report_participants + WHERE + report_id = _report_id; + RETURN participants; +END; +$$; - RETURN result; +CREATE OR REPLACE FUNCTION public.change_status(_report_id integer, _new_status integer) + RETURNS void + LANGUAGE plpgsql + AS $$ +BEGIN + UPDATE + public.reports + SET + status = _new_status + WHERE + id = _report_id; END; -$$; +$$; + From 66a2eb13defb935fbf65b335fd504bf571310c1a Mon Sep 17 00:00:00 2001 From: "aleksandr.z" Date: Sun, 20 Apr 2025 20:19:55 +0300 Subject: [PATCH 3/5] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=BC=D0=B0=D0=BF=D0=BF=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20snake=5Fcase=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?Dapper,=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BE=D1=82?= =?UTF-8?q?=D1=87=D0=B5=D1=82=D0=B0,=20=D0=BE=D0=BD=D0=B0=20=D1=82=D0=B5?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D1=8C=20=D0=BD=D0=B5=20=D0=B8=D1=81=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=B7=D1=83=D0=B5=D1=82=20json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/Bugget.DA/Postgres/ReportsDbClient.cs | 40 ++++- .../DbModels/Bug/BugDbModel.cs | 4 +- .../DbModels/Report/ReportDbModel.cs | 4 +- .../DbModels/Report/ReportSummaryDbModel.cs | 1 - backend/Bugget/Program.cs | 3 + devops/migrator/sql/010_dml_reports_v2.sql | 148 ++++++++++++++---- 6 files changed, 157 insertions(+), 43 deletions(-) diff --git a/backend/Bugget.DA/Postgres/ReportsDbClient.cs b/backend/Bugget.DA/Postgres/ReportsDbClient.cs index 1bbc568..ad66e8c 100644 --- a/backend/Bugget.DA/Postgres/ReportsDbClient.cs +++ b/backend/Bugget.DA/Postgres/ReportsDbClient.cs @@ -1,6 +1,8 @@ using System.Text.Json; using Bugget.Entities.BO.Search; using Bugget.Entities.DbModels; +using Bugget.Entities.DbModels.Bug; +using Bugget.Entities.DbModels.Comment; using Bugget.Entities.DbModels.Report; using Bugget.Entities.DTO.Report; using Dapper; @@ -28,13 +30,37 @@ public sealed class ReportsDbClient : PostgresClient /// public async Task GetReportAsync(int reportId, string? organizationId) { - await using var connection = await DataSource.OpenConnectionAsync(); - var jsonResult = await connection.ExecuteScalarAsync( - "SELECT public.get_report_v2(@report_id, @organization_id);", - new { report_id = reportId, organization_id = organizationId } - ); - - return jsonResult != null ? Deserialize(jsonResult) : null; + await using var conn = await DataSource.OpenConnectionAsync(); + // Открываем транзакцию для курсоров + await using var multi = await conn.QueryMultipleAsync(@" + SELECT * FROM public.get_report_v2(@reportId, @organizationId); + SELECT * FROM public.list_bugs(@reportId); + SELECT * FROM public.list_participants(@reportId); + SELECT * FROM public.list_comments(@reportId); + SELECT * FROM public.list_attachments(@reportId); +", new { reportId, organizationId }); + + // проверка доступа к репорту + var report = await multi.ReadSingleOrDefaultAsync(); + if (report == null) return null; + + // 2. Дочерние сущности + report.Bugs = (await multi.ReadAsync()).ToArray(); + report.ParticipantsUserIds = (await multi.ReadAsync()).ToArray(); + var comments = (await multi.ReadAsync()).ToArray(); + var attachments = (await multi.ReadAsync()).ToArray(); + + // 3. Группируем по багам + var commentsByBug = comments.GroupBy(c => c.BugId).ToDictionary(g => g.Key, g => g.ToArray()); + var attachmentsByBug = attachments.GroupBy(a => a.BugId).ToDictionary(g => g.Key, g => g.ToArray()); + + foreach (var bug in report.Bugs) + { + bug.Comments = commentsByBug.TryGetValue(bug.Id, out var c) ? c : []; + bug.Attachments = attachmentsByBug.TryGetValue(bug.Id, out var a) ? a : []; + } + + return report; } public async Task ListReportsAsync(string userId) diff --git a/backend/Bugget.Entities/DbModels/Bug/BugDbModel.cs b/backend/Bugget.Entities/DbModels/Bug/BugDbModel.cs index 098c786..a0b1862 100644 --- a/backend/Bugget.Entities/DbModels/Bug/BugDbModel.cs +++ b/backend/Bugget.Entities/DbModels/Bug/BugDbModel.cs @@ -12,6 +12,6 @@ public sealed class BugDbModel public required DateTimeOffset UpdatedAt { get; init; } public required string CreatorUserId { get; init; } public required int Status { get; init; } - public AttachmentDbModel[]? Attachments { get; init; } - public CommentDbModel[]? Comments { get; init; } + public AttachmentDbModel[]? Attachments { get; set; } + public CommentDbModel[]? Comments { get; set; } } \ No newline at end of file diff --git a/backend/Bugget.Entities/DbModels/Report/ReportDbModel.cs b/backend/Bugget.Entities/DbModels/Report/ReportDbModel.cs index b919265..d98e21c 100644 --- a/backend/Bugget.Entities/DbModels/Report/ReportDbModel.cs +++ b/backend/Bugget.Entities/DbModels/Report/ReportDbModel.cs @@ -13,6 +13,6 @@ public sealed class ReportDbModel public string? CreatorTeamId { get; init; } public required DateTimeOffset CreatedAt { get; init; } public required DateTimeOffset UpdatedAt { get; init; } - public required string[] ParticipantsUserIds { get; init; } - public BugDbModel[]? Bugs { get; init; } + public string[] ParticipantsUserIds { get; set; } + public BugDbModel[]? Bugs { get; set; } } \ No newline at end of file diff --git a/backend/Bugget.Entities/DbModels/Report/ReportSummaryDbModel.cs b/backend/Bugget.Entities/DbModels/Report/ReportSummaryDbModel.cs index 0f47a58..9062253 100644 --- a/backend/Bugget.Entities/DbModels/Report/ReportSummaryDbModel.cs +++ b/backend/Bugget.Entities/DbModels/Report/ReportSummaryDbModel.cs @@ -11,5 +11,4 @@ public sealed class ReportSummaryDbModel public string? CreatorTeamId { get; init; } public required DateTimeOffset CreatedAt { get; init; } public required DateTimeOffset UpdatedAt { get; init; } - public required string[] ParticipantsUserIds { get; init; } } \ No newline at end of file diff --git a/backend/Bugget/Program.cs b/backend/Bugget/Program.cs index 20db060..e277111 100644 --- a/backend/Bugget/Program.cs +++ b/backend/Bugget/Program.cs @@ -66,6 +66,9 @@ .AddSingleton() .AddSingleton(); +// для маппинга snake_case в c# типы средствами dapper +Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true; + builder.Services .AddSingleton() .AddSingleton() diff --git a/devops/migrator/sql/010_dml_reports_v2.sql b/devops/migrator/sql/010_dml_reports_v2.sql index dbcb14e..ff9a911 100644 --- a/devops/migrator/sql/010_dml_reports_v2.sql +++ b/devops/migrator/sql/010_dml_reports_v2.sql @@ -87,44 +87,131 @@ BEGIN END; $$; +-- 1) Общая информация по отчёту CREATE OR REPLACE FUNCTION public.get_report_v2(_report_id int, _organization_id text DEFAULT NULL) - RETURNS jsonb - LANGUAGE plpgsql + RETURNS TABLE( + id int, + title text, + status text, + created_at timestamp, + updated_at timestamp, + creator_user_id text, + creator_team_id text, + responsible_user_id text, + past_responsible_user_id text) + LANGUAGE sql + STABLE AS $$ -DECLARE - result jsonb; -BEGIN - -- Формируем JSON с Report и его связями SELECT - jsonb_build_object('id', r.id, 'title', r.title, 'status', r.status, 'created_at', r.created_at, 'updated_at', r.updated_at, 'creator_user_id', r.creator_user_id, 'creator_team_id', r.creator_team_id, 'responsible_user_id', r.responsible_user_id, 'past_responsible_user_id', r.past_responsible_user_id, 'bugs', COALESCE(( - SELECT - jsonb_agg(jsonb_build_object('id', b.id, 'report_id', b.report_id, 'receive', b.receive, 'expect', b.expect, 'status', b.status, 'creator_user_id', b.creator_user_id, 'created_at', b.created_at, 'updated_at', b.updated_at, 'attachments', COALESCE(( - SELECT - jsonb_agg(jsonb_build_object('id', a.id, 'path', a.path, 'attach_type', a.attach_type, 'created_at', a.created_at)) - FROM public.attachments a - WHERE - a.bug_id = b.id), '[]'::jsonb), 'comments', COALESCE(( - SELECT - jsonb_agg(jsonb_build_object('id', c.id, 'text', c.text, 'creator_user_id', c.creator_user_id, 'bug_id', c.bug_id, 'created_at', c.created_at, 'updated_at', c.updated_at)) - FROM public.comments c - WHERE - c.bug_id = b.id), '[]'::jsonb))) - FROM public.bugs b - WHERE - b.report_id = r.id), '[]'::jsonb), 'participants_user_ids', COALESCE(( - SELECT - jsonb_agg(user_id) - FROM public.report_participants AS rp - WHERE - rp.report_id = r.id), '[]'::jsonb)) INTO result + r.id, + r.title, + r.status, + r.created_at, + r.updated_at, + r.creator_user_id, + r.creator_team_id, + r.responsible_user_id, + r.past_responsible_user_id FROM public.reports r WHERE r.id = _report_id - AND (_organization_id IS NULL + AND(_organization_id IS NULL OR r.creator_organization_id = _organization_id); - RETURN result; -END; +$$; + +-- 2) Все баги из отчёта +CREATE OR REPLACE FUNCTION public.list_bugs(_report_id int) + RETURNS TABLE( + id int, + report_id int, + receive text, + expect text, + status text, + creator_user_id text, + created_at timestamp, + updated_at timestamp) + LANGUAGE sql + STABLE + AS $$ + SELECT + b.id, + b.report_id, + b.receive, + b.expect, + b.status, + b.creator_user_id, + b.created_at, + b.updated_at + FROM + public.bugs b + WHERE + b.report_id = _report_id; +$$; + +-- 3) Участники отчёта +CREATE OR REPLACE FUNCTION public.list_participants(_report_id int) + RETURNS TABLE( + user_id text) + LANGUAGE sql + STABLE + AS $$ + SELECT + p.user_id + FROM + public.report_participants p + WHERE + p.report_id = _report_id; +$$; + +-- 4) Комментарии ко всем багам отчёта +CREATE OR REPLACE FUNCTION public.list_comments(_report_id int) + RETURNS TABLE( + id int, + bug_id int, + text text, + creator_user_id text, + created_at timestamp, + updated_at timestamp) + LANGUAGE sql + STABLE + AS $$ + SELECT + c.id, + c.bug_id, + c.text, + c.creator_user_id, + c.created_at, + c.updated_at + FROM + public.comments c + JOIN public.bugs b ON c.bug_id = b.id + WHERE + b.report_id = _report_id; +$$; + +-- 5) Вложения ко всем багам отчёта +CREATE OR REPLACE FUNCTION public.list_attachments(_report_id int) + RETURNS TABLE( + id int, + bug_id int, + path text, + attach_type text, + created_at timestamp) + LANGUAGE sql + STABLE + AS $$ + SELECT + a.id, + a.bug_id, + a.path, + a.attach_type, + a.created_at + FROM + public.attachments a + JOIN public.bugs b ON a.bug_id = b.id + WHERE + b.report_id = _report_id; $$; CREATE OR REPLACE FUNCTION public.add_participant_if_not_exist(_report_id integer, _user_id text) @@ -140,7 +227,6 @@ BEGIN VALUES (_report_id, _user_id) ON CONFLICT (report_id, user_id) DO NOTHING; - GET DIAGNOSTICS inserted_count = ROW_COUNT; -- если ничего не вставилось — возвращаем NULL IF inserted_count = 0 THEN From 6a82ad101319cb68f1ed74a4c7455ea3a90a71bb Mon Sep 17 00:00:00 2001 From: "aleksandr.z" Date: Sun, 20 Apr 2025 21:04:11 +0300 Subject: [PATCH 4/5] fix participants --- backend/Bugget.BO/Services/ReportEventsService.cs | 1 + backend/Bugget.Entities/DbModels/Report/ReportSummaryDbModel.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/backend/Bugget.BO/Services/ReportEventsService.cs b/backend/Bugget.BO/Services/ReportEventsService.cs index 1a8183f..df3f7a7 100644 --- a/backend/Bugget.BO/Services/ReportEventsService.cs +++ b/backend/Bugget.BO/Services/ReportEventsService.cs @@ -19,6 +19,7 @@ await Task.WhenAll( reportPageHubClient.SendReportPatchAsync(reportId, patchDto.ToSocketView(result)), externalClientsActionService.ExecuteReportPatchPostActions(new ReportPatchContext(userId, patchDto, result)), participantsService.AddParticipantIfNotExistAsync(reportId, userId), + patchDto.ResponsibleUserId != null ? participantsService.AddParticipantIfNotExistAsync(reportId, patchDto.ResponsibleUserId) : Task.CompletedTask, autoStatusService.CalculateStatusAsync(reportId, patchDto, result) ); } diff --git a/backend/Bugget.Entities/DbModels/Report/ReportSummaryDbModel.cs b/backend/Bugget.Entities/DbModels/Report/ReportSummaryDbModel.cs index 9062253..0f47a58 100644 --- a/backend/Bugget.Entities/DbModels/Report/ReportSummaryDbModel.cs +++ b/backend/Bugget.Entities/DbModels/Report/ReportSummaryDbModel.cs @@ -11,4 +11,5 @@ public sealed class ReportSummaryDbModel public string? CreatorTeamId { get; init; } public required DateTimeOffset CreatedAt { get; init; } public required DateTimeOffset UpdatedAt { get; init; } + public required string[] ParticipantsUserIds { get; init; } } \ No newline at end of file From c34cd82af138e7e3682e24eb50277d5e3824e5d3 Mon Sep 17 00:00:00 2001 From: "aleksandr.z" Date: Sun, 20 Apr 2025 21:51:42 +0300 Subject: [PATCH 5/5] self review --- .../DTO/Report/ReportPatchDto.cs | 2 +- .../DTO/Report/ReportV2CreateDto.cs | 2 +- ...Filter.cs => HubExceptionHandlerFilter.cs} | 2 +- .../Middlewares/HubModelStateInvalidFilter.cs | 28 +++++++++++++++++++ backend/Bugget/Program.cs | 3 +- 5 files changed, 33 insertions(+), 4 deletions(-) rename backend/Bugget/Middlewares/{HubErrorFilter.cs => HubExceptionHandlerFilter.cs} (91%) create mode 100644 backend/Bugget/Middlewares/HubModelStateInvalidFilter.cs diff --git a/backend/Bugget.Entities/DTO/Report/ReportPatchDto.cs b/backend/Bugget.Entities/DTO/Report/ReportPatchDto.cs index 10963c6..4ca3341 100644 --- a/backend/Bugget.Entities/DTO/Report/ReportPatchDto.cs +++ b/backend/Bugget.Entities/DTO/Report/ReportPatchDto.cs @@ -3,7 +3,7 @@ namespace Bugget.Entities.DTO.Report; public sealed class ReportPatchDto { - [StringLength(120, MinimumLength = 1)] + [StringLength(128, MinimumLength = 1)] public string? Title { get; init; } [Range(0, 3)] public int? Status { get; init; } diff --git a/backend/Bugget.Entities/DTO/Report/ReportV2CreateDto.cs b/backend/Bugget.Entities/DTO/Report/ReportV2CreateDto.cs index 58fc4b5..40e1cc1 100644 --- a/backend/Bugget.Entities/DTO/Report/ReportV2CreateDto.cs +++ b/backend/Bugget.Entities/DTO/Report/ReportV2CreateDto.cs @@ -3,6 +3,6 @@ namespace Bugget.Entities.DTO.Report; public sealed class ReportV2CreateDto { - [StringLength(120, MinimumLength = 1)] + [StringLength(128, MinimumLength = 1)] public required string Title { get; init; } } \ No newline at end of file diff --git a/backend/Bugget/Middlewares/HubErrorFilter.cs b/backend/Bugget/Middlewares/HubExceptionHandlerFilter.cs similarity index 91% rename from backend/Bugget/Middlewares/HubErrorFilter.cs rename to backend/Bugget/Middlewares/HubExceptionHandlerFilter.cs index d5db080..194a05c 100644 --- a/backend/Bugget/Middlewares/HubErrorFilter.cs +++ b/backend/Bugget/Middlewares/HubExceptionHandlerFilter.cs @@ -5,7 +5,7 @@ namespace Bugget.Middlewares; -public class HubErrorFilter(ILogger logger) : IHubFilter +public class HubExceptionHandlerFilter(ILogger logger) : IHubFilter { protected readonly JsonSerializerOptions JsonSerializerOptions = new() { diff --git a/backend/Bugget/Middlewares/HubModelStateInvalidFilter.cs b/backend/Bugget/Middlewares/HubModelStateInvalidFilter.cs new file mode 100644 index 0000000..dcb6b90 --- /dev/null +++ b/backend/Bugget/Middlewares/HubModelStateInvalidFilter.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.SignalR; + +namespace Bugget.Middlewares +{ + public class HubModelStateInvalidFilter : IHubFilter + { + public async ValueTask InvokeMethodAsync( + HubInvocationContext invocationContext, + Func> next) + { + // Проходим по всем аргументам метода + foreach (var arg in invocationContext.HubMethodArguments) + { + if (arg == null) continue; + var validationContext = new ValidationContext(arg); + var errors = new List(); + // Проверяем только объекты с атрибутами + if (!Validator.TryValidateObject(arg, validationContext, errors, validateAllProperties: true)) + { + var msg = string.Join("; ", errors.Select(e => e.ErrorMessage)); + throw new HubException($"Validation failed: {msg}"); + } + } + return await next(invocationContext); + } + } +} \ No newline at end of file diff --git a/backend/Bugget/Program.cs b/backend/Bugget/Program.cs index e277111..ae6b99c 100644 --- a/backend/Bugget/Program.cs +++ b/backend/Bugget/Program.cs @@ -29,7 +29,8 @@ options.ClientTimeoutInterval = TimeSpan.FromSeconds(60); // Клиент ждёт 60 сек перед разрывом }).AddHubOptions(options => { - options.AddFilter(); + options.AddFilter(); + options.AddFilter(); }); // разрешаем cors для локального тестирования