Skip to content

Commit 0c0a458

Browse files
v2 reports refactoring api (#23)
* Добавлены новые модули для работы с внешними клиентами и очередями задач. Обновлены зависимости, изменены модели данных для отчетов, улучшена обработка патчей отчетов через веб-сокеты. Удалены устаревшие функции и модули. * Покрыл все возможные mvp кейсы связанные с report api * Добавлено маппирование snake_case для Dapper, обновлена функция получения отчета, она теперь не использует json
1 parent 92796a1 commit 0c0a458

File tree

53 files changed

+582
-292
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+582
-292
lines changed

backend/Bugget.BO/Bugget.BO.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<ProjectReference Include="..\Bugget.Features\Bugget.Features.csproj" />
10+
<ProjectReference Include="..\Bugget.ExternalClients\Bugget.ExternalClients.csproj" />
1111
<ProjectReference Include="..\Monade\Monade.csproj" />
12+
<ProjectReference Include="..\TaskQueue\TaskQueue.csproj" />
1213
</ItemGroup>
1314

1415
</Project>

backend/Bugget.BO/Mappers/ReportMapper.cs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Bugget.Entities.DbModels.Bug;
77
using Bugget.Entities.DbModels.Report;
88
using Bugget.Entities.DTO.Report;
9+
using Bugget.Entities.SocketViews;
910
using Bugget.Entities.Views;
1011

1112
namespace Bugget.BO.Mappers;
@@ -52,11 +53,11 @@ public static Report ToReport(this ReportCreateDto report, string userId)
5253
ResponsibleUserId = report.ResponsibleId,
5354
CreatorUserId = userId,
5455
Bugs = report.Bugs.Select(b => new Bug
55-
{
56-
Receive = b.Receive,
57-
Expect = b.Expect,
58-
CreatorUserId = userId,
59-
})
56+
{
57+
Receive = b.Receive,
58+
Expect = b.Expect,
59+
CreatorUserId = userId,
60+
})
6061
.ToArray(),
6162
ParticipantsUserIds = new string[] { userId, report.ResponsibleId }.Distinct().ToArray()
6263
};
@@ -139,4 +140,15 @@ public static SearchReports ToSearchReports(
139140
Sort = SortOption.Parse(sort)
140141
};
141142
}
143+
144+
public static PatchReportSocketView ToSocketView(this ReportPatchDto patchDto, ReportPatchResultDbModel? result)
145+
{
146+
return new PatchReportSocketView
147+
{
148+
Title = patchDto.Title,
149+
Status = patchDto.Status,
150+
ResponsibleUserId = patchDto.ResponsibleUserId,
151+
PastResponsibleUserId = patchDto.ResponsibleUserId == null ? null : result?.PastResponsibleUserId
152+
};
153+
}
142154
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Bugget.BO.WebSockets;
2+
using Bugget.DA.Postgres;
3+
4+
namespace Bugget.BO.Services
5+
{
6+
public class ParticipantsService(ParticipantsDbClient participantsDbClient, IReportPageHubClient reportPageHubClient)
7+
{
8+
public async Task AddParticipantIfNotExistAsync(int reportId, string userId)
9+
{
10+
var participants = await participantsDbClient.AddParticipantIfNotExistAsync(reportId, userId);
11+
12+
if (participants != null)
13+
{
14+
await reportPageHubClient.SendReportParticipantsAsync(reportId, participants);
15+
}
16+
}
17+
}
18+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using Bugget.BO.WebSockets;
2+
using Bugget.DA.Postgres;
3+
using Bugget.Entities.BO.ReportBo;
4+
using Bugget.Entities.DbModels.Report;
5+
using Bugget.Entities.DTO.Report;
6+
using Bugget.Entities.SocketViews;
7+
8+
namespace Bugget.BO.Services
9+
{
10+
public class ReportAutoStatusService(ReportsDbClient reportsDbClient, IReportPageHubClient reportPageHubClient)
11+
{
12+
public async Task CalculateStatusAsync(int reportId, ReportPatchDto patchDto, ReportPatchResultDbModel result)
13+
{
14+
// если статус backlog и меняется ответственный, то меняем статус на in progress
15+
if (result.Status == (int)ReportStatus.Backlog && patchDto.ResponsibleUserId != null)
16+
{
17+
await reportsDbClient.ChangeStatusAsync(reportId, (int)ReportStatus.InProgress);
18+
await reportPageHubClient.SendReportPatchAsync(reportId, new PatchReportSocketView
19+
{
20+
Status = (int)ReportStatus.InProgress,
21+
});
22+
}
23+
}
24+
}
25+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using Bugget.BO.Mappers;
2+
using Bugget.BO.WebSockets;
3+
using Bugget.Entities.DbModels.Report;
4+
using Bugget.Entities.DTO.Report;
5+
using Bugget.ExternalClients;
6+
using Bugget.ExternalClients.Context;
7+
8+
namespace Bugget.BO.Services
9+
{
10+
public class ReportEventsService(
11+
IReportPageHubClient reportPageHubClient,
12+
ExternalClientsActionService externalClientsActionService,
13+
ParticipantsService participantsService,
14+
ReportAutoStatusService autoStatusService)
15+
{
16+
public async Task HandlePatchReportEventAsync(int reportId, string userId, ReportPatchDto patchDto, ReportPatchResultDbModel result)
17+
{
18+
await Task.WhenAll(
19+
reportPageHubClient.SendReportPatchAsync(reportId, patchDto.ToSocketView(result)),
20+
externalClientsActionService.ExecuteReportPatchPostActions(new ReportPatchContext(userId, patchDto, result)),
21+
participantsService.AddParticipantIfNotExistAsync(reportId, userId),
22+
patchDto.ResponsibleUserId != null ? participantsService.AddParticipantIfNotExistAsync(reportId, patchDto.ResponsibleUserId) : Task.CompletedTask,
23+
autoStatusService.CalculateStatusAsync(reportId, patchDto, result)
24+
);
25+
}
26+
}
27+
}

backend/Bugget.BO/Services/ReportsService.cs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@
55
using Bugget.Entities.BO.Search;
66
using Bugget.Entities.DbModels.Report;
77
using Bugget.Entities.DTO.Report;
8-
using Bugget.Features;
9-
using Bugget.Features.Context;
8+
using Bugget.ExternalClients;
9+
using Bugget.ExternalClients.Context;
10+
using Microsoft.Extensions.Logging;
1011
using Monade;
12+
using TaskQueue;
1113

1214
namespace Bugget.BO.Services;
1315

1416
public sealed class ReportsService(
1517
ReportsDbClient reportsDbClient,
16-
FeaturesService featuresService)
18+
ExternalClientsActionService externalClientsActionService,
19+
ITaskQueue taskQueue,
20+
ReportEventsService reportEventsService,
21+
ILogger<ReportsService> logger)
1722
{
1823
public async Task<ReportObsoleteDbModel?> CreateReportAsync(Report report)
1924
{
@@ -23,19 +28,25 @@ public sealed class ReportsService(
2328
return null;
2429
}
2530

26-
await featuresService.ExecuteReportCreatePostActions(new ReportCreateContext(report, reportDbModel));
31+
await taskQueue.Enqueue(() => externalClientsActionService.ExecuteReportCreatePostActions(new ReportCreateContext(report, reportDbModel)));
2732

2833
return reportDbModel;
2934
}
3035

31-
public Task<ReportSummaryDbModel> CreateReportAsync(string userId, string? teamId, string? organizationId, ReportV2CreateDto createDto)
36+
public Task<ReportSummaryDbModel> CreateReportAsync(string userId, string? teamId, string? organizationId, ReportV2CreateDto createDto)
3237
{
3338
return reportsDbClient.CreateReportAsync(userId, teamId, organizationId, createDto);
3439
}
3540

36-
public Task<ReportPatchDbModel> PatchReportAsync(int reportId, string userId, string? organizationId, ReportPatchDto patchDto)
41+
public async Task<ReportPatchResultDbModel> PatchReportAsync(int reportId, string userId, string? organizationId, ReportPatchDto patchDto)
3742
{
38-
return reportsDbClient.PatchReportAsync(reportId, userId, organizationId, patchDto);
43+
logger.LogInformation("Пользователь {@UserId} патчит отчёт {@ReportId}, {@PatchDto}", userId, reportId, patchDto);
44+
45+
var result = await reportsDbClient.PatchReportAsync(reportId, userId, organizationId, patchDto);
46+
47+
await taskQueue.Enqueue(() => reportEventsService.HandlePatchReportEventAsync(reportId, userId, patchDto, result));
48+
49+
return result;
3950
}
4051

4152
public Task<ReportObsoleteDbModel[]> ListReportsAsync(string userId)
@@ -61,16 +72,16 @@ public async Task<MonadeStruct<ReportDbModel>> GetReportAsync(int reportId, stri
6172

6273
public async Task<ReportObsoleteDbModel?> UpdateReportAsync(ReportUpdate report)
6374
{
64-
var reportDbModel = await reportsDbClient.UpdateReportAsync(report.ToReportUpdateDbModel());
75+
var reportDbModel = await reportsDbClient.UpdateReportAsync(report.ToReportUpdateDbModel());
6576

6677
if (reportDbModel == null)
6778
return null;
68-
69-
await featuresService.ExecuteReportUpdatePostActions(new ReportUpdateContext(report, reportDbModel));
79+
80+
await taskQueue.Enqueue(() => externalClientsActionService.ExecuteReportUpdatePostActions(new ReportUpdateContext(report, reportDbModel)));
7081

7182
return reportDbModel;
7283
}
73-
84+
7485
public Task<SearchReportsDbModel> SearchReportsAsync(SearchReports search)
7586
{
7687
return reportsDbClient.SearchReportsAsync(search);

backend/Bugget.DA/Bugget.DA.csproj

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,5 @@
1717
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.2" />
1818
<PackageReference Include="Npgsql" Version="9.0.3" />
1919
</ItemGroup>
20-
21-
<ItemGroup>
22-
<Reference Include="Microsoft.Extensions.Hosting.Abstractions">
23-
<HintPath>..\..\..\.dotnet\shared\Microsoft.AspNetCore.App\9.0.2\Microsoft.Extensions.Hosting.Abstractions.dll</HintPath>
24-
</Reference>
25-
</ItemGroup>
2620

2721
</Project>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Dapper;
2+
3+
namespace Bugget.DA.Postgres
4+
{
5+
public class ParticipantsDbClient : PostgresClient
6+
{
7+
public async Task<string[]?> AddParticipantIfNotExistAsync(int reportId, string userId)
8+
{
9+
await using var connection = await DataSource.OpenConnectionAsync();
10+
11+
var participants = await connection.ExecuteScalarAsync<string[]?>(
12+
"SELECT public.add_participant_if_not_exist(@report_id, @user_id);",
13+
new { report_id = reportId, user_id = userId }
14+
);
15+
16+
return participants;
17+
}
18+
}
19+
}

backend/Bugget.DA/Postgres/ReportsDbClient.cs

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
using System.Text.Json;
22
using Bugget.Entities.BO.Search;
33
using Bugget.Entities.DbModels;
4+
using Bugget.Entities.DbModels.Bug;
5+
using Bugget.Entities.DbModels.Comment;
46
using Bugget.Entities.DbModels.Report;
57
using Bugget.Entities.DTO.Report;
68
using Dapper;
79

810
namespace Bugget.DA.Postgres;
911

10-
public sealed class ReportsDbClient: PostgresClient
12+
public sealed class ReportsDbClient : PostgresClient
1113
{
1214
/// <summary>
1315
/// Получает отчет по ID.
@@ -28,15 +30,39 @@ public sealed class ReportsDbClient: PostgresClient
2830
/// </summary>
2931
public async Task<ReportDbModel?> GetReportAsync(int reportId, string? organizationId)
3032
{
31-
await using var connection = await DataSource.OpenConnectionAsync();
32-
var jsonResult = await connection.ExecuteScalarAsync<string>(
33-
"SELECT public.get_report_v2(@report_id, @organization_id);",
34-
new { report_id = reportId, organization_id = organizationId }
35-
);
36-
37-
return jsonResult != null ? Deserialize<ReportDbModel>(jsonResult) : null;
33+
await using var conn = await DataSource.OpenConnectionAsync();
34+
// Открываем транзакцию для курсоров
35+
await using var multi = await conn.QueryMultipleAsync(@"
36+
SELECT * FROM public.get_report_v2(@reportId, @organizationId);
37+
SELECT * FROM public.list_bugs(@reportId);
38+
SELECT * FROM public.list_participants(@reportId);
39+
SELECT * FROM public.list_comments(@reportId);
40+
SELECT * FROM public.list_attachments(@reportId);
41+
", new { reportId, organizationId });
42+
43+
// проверка доступа к репорту
44+
var report = await multi.ReadSingleOrDefaultAsync<ReportDbModel>();
45+
if (report == null) return null;
46+
47+
// 2. Дочерние сущности
48+
report.Bugs = (await multi.ReadAsync<BugDbModel>()).ToArray();
49+
report.ParticipantsUserIds = (await multi.ReadAsync<string>()).ToArray();
50+
var comments = (await multi.ReadAsync<CommentDbModel>()).ToArray();
51+
var attachments = (await multi.ReadAsync<AttachmentDbModel>()).ToArray();
52+
53+
// 3. Группируем по багам
54+
var commentsByBug = comments.GroupBy(c => c.BugId).ToDictionary(g => g.Key, g => g.ToArray());
55+
var attachmentsByBug = attachments.GroupBy(a => a.BugId).ToDictionary(g => g.Key, g => g.ToArray());
56+
57+
foreach (var bug in report.Bugs)
58+
{
59+
bug.Comments = commentsByBug.TryGetValue(bug.Id, out var c) ? c : [];
60+
bug.Attachments = attachmentsByBug.TryGetValue(bug.Id, out var a) ? a : [];
61+
}
62+
63+
return report;
3864
}
39-
65+
4066
public async Task<ReportObsoleteDbModel[]> ListReportsAsync(string userId)
4167
{
4268
await using var connection = await DataSource.OpenConnectionAsync();
@@ -75,7 +101,7 @@ public async Task<ReportSummaryDbModel> CreateReportAsync(string userId, string?
75101
/// <summary>
76102
/// Обновляет краткую информацию об отчете и возвращает его краткую структуру.
77103
/// </summary>
78-
public async Task<ReportPatchDbModel> PatchReportAsync(int reportId, string userId, string? organizationId, ReportPatchDto dto)
104+
public async Task<ReportPatchResultDbModel> PatchReportAsync(int reportId, string userId, string? organizationId, ReportPatchDto dto)
79105
{
80106
await using var connection = await DataSource.OpenConnectionAsync();
81107

@@ -92,7 +118,7 @@ public async Task<ReportPatchDbModel> PatchReportAsync(int reportId, string user
92118
}
93119
);
94120

95-
return Deserialize<ReportPatchDbModel>(jsonResult!)!;
121+
return Deserialize<ReportPatchResultDbModel>(jsonResult!)!;
96122
}
97123

98124

@@ -123,11 +149,11 @@ public async Task<ReportPatchDbModel> PatchReportAsync(int reportId, string user
123149
? Deserialize<ReportObsoleteDbModel>(jsonResult)
124150
: null;
125151
}
126-
152+
127153
public async Task<ReportObsoleteDbModel?> UpdateReportAsync(ReportUpdateDbModel reportDbModel)
128154
{
129155
await using var connection = await DataSource.OpenConnectionAsync();
130-
156+
131157
var jsonResult = await connection.ExecuteScalarAsync<string>(
132158
"SELECT public.update_report(@report_id, @participants,@title, @status, @responsible_user_id);",
133159
new
@@ -144,7 +170,7 @@ public async Task<ReportPatchDbModel> PatchReportAsync(int reportId, string user
144170
? Deserialize<ReportObsoleteDbModel>(jsonResult)
145171
: null;
146172
}
147-
173+
148174
public async Task<SearchReportsDbModel> SearchReportsAsync(SearchReports search)
149175
{
150176
await using var connection = await DataSource.OpenConnectionAsync();
@@ -165,6 +191,16 @@ public async Task<SearchReportsDbModel> SearchReportsAsync(SearchReports search)
165191

166192
return Deserialize<SearchReportsDbModel>(jsonResult);
167193
}
168-
194+
195+
public async Task ChangeStatusAsync(int reportId, int newStatus)
196+
{
197+
await using var connection = await DataSource.OpenConnectionAsync();
198+
199+
await connection.ExecuteAsync(
200+
"SELECT public.change_status(@report_id, @new_status);",
201+
new { report_id = reportId, new_status = newStatus }
202+
);
203+
}
204+
169205
private T? Deserialize<T>(string json) => JsonSerializer.Deserialize<T>(json, JsonSerializerOptions);
170206
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using Bugget.Entities.SocketViews;
2+
3+
namespace Bugget.BO.WebSockets;
4+
5+
public interface IReportPageHubClient
6+
{
7+
Task SendReportPatchAsync(int reportId, PatchReportSocketView view);
8+
Task SendReportParticipantsAsync(int reportId, string[] participants);
9+
}

0 commit comments

Comments
 (0)