Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/Bugget.BO/Bugget.BO.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Bugget.Features\Bugget.Features.csproj" />
<ProjectReference Include="..\Bugget.ExternalClients\Bugget.ExternalClients.csproj" />
<ProjectReference Include="..\Monade\Monade.csproj" />
<ProjectReference Include="..\TaskQueue\TaskQueue.csproj" />
</ItemGroup>

</Project>
22 changes: 17 additions & 5 deletions backend/Bugget.BO/Mappers/ReportMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
};
Expand Down Expand Up @@ -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
};
}
}
18 changes: 18 additions & 0 deletions backend/Bugget.BO/Services/ParticipantsService.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
25 changes: 25 additions & 0 deletions backend/Bugget.BO/Services/ReportAutoStatusService.cs
Original file line number Diff line number Diff line change
@@ -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,
});
}
}
}
}
27 changes: 27 additions & 0 deletions backend/Bugget.BO/Services/ReportEventsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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),
patchDto.ResponsibleUserId != null ? participantsService.AddParticipantIfNotExistAsync(reportId, patchDto.ResponsibleUserId) : Task.CompletedTask,
autoStatusService.CalculateStatusAsync(reportId, patchDto, result)
);
}
}
}
33 changes: 22 additions & 11 deletions backend/Bugget.BO/Services/ReportsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@
using Bugget.Entities.BO.Search;
using Bugget.Entities.DbModels.Report;
using Bugget.Entities.DTO.Report;
using Bugget.Features;
using Bugget.Features.Context;
using Bugget.ExternalClients;
using Bugget.ExternalClients.Context;
using Microsoft.Extensions.Logging;
using Monade;
using TaskQueue;

namespace Bugget.BO.Services;

public sealed class ReportsService(
ReportsDbClient reportsDbClient,
FeaturesService featuresService)
ExternalClientsActionService externalClientsActionService,
ITaskQueue taskQueue,
ReportEventsService reportEventsService,
ILogger<ReportsService> logger)
{
public async Task<ReportObsoleteDbModel?> CreateReportAsync(Report report)
{
Expand All @@ -23,19 +28,25 @@ 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<ReportSummaryDbModel> CreateReportAsync(string userId, string? teamId, string? organizationId, ReportV2CreateDto createDto)
public Task<ReportSummaryDbModel> CreateReportAsync(string userId, string? teamId, string? organizationId, ReportV2CreateDto createDto)
{
return reportsDbClient.CreateReportAsync(userId, teamId, organizationId, createDto);
}

public Task<ReportPatchDbModel> PatchReportAsync(int reportId, string userId, string? organizationId, ReportPatchDto patchDto)
public async Task<ReportPatchResultDbModel> PatchReportAsync(int reportId, string userId, string? organizationId, ReportPatchDto patchDto)
{
return reportsDbClient.PatchReportAsync(reportId, userId, organizationId, patchDto);
logger.LogInformation("Пользователь {@UserId} патчит отчёт {@ReportId}, {@PatchDto}", userId, reportId, patchDto);

var result = await reportsDbClient.PatchReportAsync(reportId, userId, organizationId, patchDto);

await taskQueue.Enqueue(() => reportEventsService.HandlePatchReportEventAsync(reportId, userId, patchDto, result));

return result;
}

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

public async Task<ReportObsoleteDbModel?> 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<SearchReportsDbModel> SearchReportsAsync(SearchReports search)
{
return reportsDbClient.SearchReportsAsync(search);
Expand Down
6 changes: 0 additions & 6 deletions backend/Bugget.DA/Bugget.DA.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,5 @@
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.2" />
<PackageReference Include="Npgsql" Version="9.0.3" />
</ItemGroup>

<ItemGroup>
<Reference Include="Microsoft.Extensions.Hosting.Abstractions">
<HintPath>..\..\..\.dotnet\shared\Microsoft.AspNetCore.App\9.0.2\Microsoft.Extensions.Hosting.Abstractions.dll</HintPath>
</Reference>
</ItemGroup>

</Project>
19 changes: 19 additions & 0 deletions backend/Bugget.DA/Postgres/ParticipantsDbClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Dapper;

namespace Bugget.DA.Postgres
{
public class ParticipantsDbClient : PostgresClient
{
public async Task<string[]?> AddParticipantIfNotExistAsync(int reportId, string userId)
{
await using var connection = await DataSource.OpenConnectionAsync();

var participants = await connection.ExecuteScalarAsync<string[]?>(
"SELECT public.add_participant_if_not_exist(@report_id, @user_id);",
new { report_id = reportId, user_id = userId }
);

return participants;
}
}
}
66 changes: 51 additions & 15 deletions backend/Bugget.DA/Postgres/ReportsDbClient.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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;

namespace Bugget.DA.Postgres;

public sealed class ReportsDbClient: PostgresClient
public sealed class ReportsDbClient : PostgresClient
{
/// <summary>
/// Получает отчет по ID.
Expand All @@ -28,15 +30,39 @@ public sealed class ReportsDbClient: PostgresClient
/// </summary>
public async Task<ReportDbModel?> GetReportAsync(int reportId, string? organizationId)
{
await using var connection = await DataSource.OpenConnectionAsync();
var jsonResult = await connection.ExecuteScalarAsync<string>(
"SELECT public.get_report_v2(@report_id, @organization_id);",
new { report_id = reportId, organization_id = organizationId }
);

return jsonResult != null ? Deserialize<ReportDbModel>(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<ReportDbModel>();
if (report == null) return null;

// 2. Дочерние сущности
report.Bugs = (await multi.ReadAsync<BugDbModel>()).ToArray();
report.ParticipantsUserIds = (await multi.ReadAsync<string>()).ToArray();
var comments = (await multi.ReadAsync<CommentDbModel>()).ToArray();
var attachments = (await multi.ReadAsync<AttachmentDbModel>()).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<ReportObsoleteDbModel[]> ListReportsAsync(string userId)
{
await using var connection = await DataSource.OpenConnectionAsync();
Expand Down Expand Up @@ -75,7 +101,7 @@ public async Task<ReportSummaryDbModel> CreateReportAsync(string userId, string?
/// <summary>
/// Обновляет краткую информацию об отчете и возвращает его краткую структуру.
/// </summary>
public async Task<ReportPatchDbModel> PatchReportAsync(int reportId, string userId, string? organizationId, ReportPatchDto dto)
public async Task<ReportPatchResultDbModel> PatchReportAsync(int reportId, string userId, string? organizationId, ReportPatchDto dto)
{
await using var connection = await DataSource.OpenConnectionAsync();

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

return Deserialize<ReportPatchDbModel>(jsonResult!)!;
return Deserialize<ReportPatchResultDbModel>(jsonResult!)!;
}


Expand Down Expand Up @@ -123,11 +149,11 @@ public async Task<ReportPatchDbModel> PatchReportAsync(int reportId, string user
? Deserialize<ReportObsoleteDbModel>(jsonResult)
: null;
}

public async Task<ReportObsoleteDbModel?> UpdateReportAsync(ReportUpdateDbModel reportDbModel)
{
await using var connection = await DataSource.OpenConnectionAsync();

var jsonResult = await connection.ExecuteScalarAsync<string>(
"SELECT public.update_report(@report_id, @participants,@title, @status, @responsible_user_id);",
new
Expand All @@ -144,7 +170,7 @@ public async Task<ReportPatchDbModel> PatchReportAsync(int reportId, string user
? Deserialize<ReportObsoleteDbModel>(jsonResult)
: null;
}

public async Task<SearchReportsDbModel> SearchReportsAsync(SearchReports search)
{
await using var connection = await DataSource.OpenConnectionAsync();
Expand All @@ -165,6 +191,16 @@ public async Task<SearchReportsDbModel> SearchReportsAsync(SearchReports search)

return Deserialize<SearchReportsDbModel>(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<T>(string json) => JsonSerializer.Deserialize<T>(json, JsonSerializerOptions);
}
9 changes: 9 additions & 0 deletions backend/Bugget.DA/WebSockets/IReportPageHubClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Bugget.Entities.SocketViews;

namespace Bugget.BO.WebSockets;

public interface IReportPageHubClient
{
Task SendReportPatchAsync(int reportId, PatchReportSocketView view);
Task SendReportParticipantsAsync(int reportId, string[] participants);
}
2 changes: 1 addition & 1 deletion backend/Bugget.Entities/DTO/Report/ReportPatchDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
2 changes: 1 addition & 1 deletion backend/Bugget.Entities/DTO/Report/ReportV2CreateDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
Loading