Архивация списков #13
@@ -3,6 +3,7 @@ using FluentResults;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using Tnsu.ApprovalRoute.Application.ListPartitioning;
|
||||||
using Tnsu.ApprovalRoute.Domain.Entities;
|
using Tnsu.ApprovalRoute.Domain.Entities;
|
||||||
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
||||||
using Tnsu.ApprovalRoute.Infrastructure.SharePoint;
|
using Tnsu.ApprovalRoute.Infrastructure.SharePoint;
|
||||||
@@ -20,6 +21,7 @@ namespace Tnsu.ApprovalRoute.Application.Reminders;
|
|||||||
public sealed class ReminderService(
|
public sealed class ReminderService(
|
||||||
ApprovalRouteDbContext db,
|
ApprovalRouteDbContext db,
|
||||||
ISharePointClient sharePoint,
|
ISharePointClient sharePoint,
|
||||||
|
ListPartitionRegistry partitionRegistry,
|
||||||
IOptions<RemindersOptions> options,
|
IOptions<RemindersOptions> options,
|
||||||
ILogger<ReminderService> logger)
|
ILogger<ReminderService> logger)
|
||||||
{
|
{
|
||||||
@@ -57,14 +59,63 @@ public sealed class ReminderService(
|
|||||||
return Result.Ok<IReadOnlyList<ReminderDto>>(reminders);
|
return Result.Ok<IReadOnlyList<ReminderDto>>(reminders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обходит ВСЕ партиции типа заявки (архив + текущая) — заявка, уехавшая в
|
||||||
|
/// архив, продолжает ждать чьего-то решения точно так же, как в текущем
|
||||||
|
/// списке, и не должна пропадать из напоминаний только из-за того, что для
|
||||||
|
/// новых заявок завели следующий список.
|
||||||
|
///
|
||||||
|
/// EnsureCurrentRegisteredAsync — на случай, если этот тип заявок ни разу не
|
||||||
|
/// читали через ListPartitionService (сам реестр партиций мог ещё не знать
|
||||||
|
/// про текущий список): без неё серия была бы пустой и напоминания молча не
|
||||||
|
/// строились бы даже по текущему списку.
|
||||||
|
/// </summary>
|
||||||
private async Task<IReadOnlyList<ReminderDto>> GetOverdueForTypeAsync(
|
private async Task<IReadOnlyList<ReminderDto>> GetOverdueForTypeAsync(
|
||||||
RequestType type, DateTimeOffset now, CancellationToken cancellationToken)
|
RequestType type, DateTimeOffset now, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await partitionRegistry.EnsureCurrentRegisteredAsync(type, cancellationToken);
|
||||||
|
|
||||||
|
var partitions = await db.RequestListPartitions.AsNoTracking()
|
||||||
|
.Where(x => x.RequestTypeId == type.Id)
|
||||||
|
.OrderBy(x => x.Number)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var reminders = new List<ReminderDto>();
|
||||||
|
foreach (var partition in partitions)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
reminders.AddRange(await GetOverdueForPartitionAsync(type, partition, now, cancellationToken));
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
// Недоступная партиция (например, архивный список переименовали
|
||||||
|
// или удалили вручную) не должна обнулять напоминания по
|
||||||
|
// остальным партициям того же типа.
|
||||||
|
logger.LogError(
|
||||||
|
e, "Напоминания по списку {ListId} (партиция №{Number}) типа «{Type}» не собраны",
|
||||||
|
partition.ListId, partition.Number, type.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reminders;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyList<ReminderDto>> GetOverdueForPartitionAsync(
|
||||||
|
RequestType type, RequestListPartition partition, DateTimeOffset now, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var siteUrl = type.SiteUrl!;
|
var siteUrl = type.SiteUrl!;
|
||||||
var dataListTitle = await sharePoint.GetListTitleAsync(siteUrl, type.ListId!, cancellationToken);
|
|
||||||
|
// Читаем Title ЖИВЬЁМ, а не из partition.Title (закешированного на момент
|
||||||
|
// создания партиции) — тот же принцип, что был здесь и раньше для
|
||||||
|
// единственного списка: заголовок могли переименовать вручную, и кеш
|
||||||
|
// тогда соврёт, а по url список найдётся всегда.
|
||||||
|
var dataListTitle = await sharePoint.GetListTitleAsync(siteUrl, partition.ListId, cancellationToken);
|
||||||
if (string.IsNullOrWhiteSpace(dataListTitle))
|
if (string.IsNullOrWhiteSpace(dataListTitle))
|
||||||
{
|
{
|
||||||
logger.LogWarning("Список данных типа «{Type}» не найден по url {Url}", type.Name, type.ListId);
|
logger.LogWarning(
|
||||||
|
"Список данных партиции №{Number} типа «{Type}» не найден по url {Url}",
|
||||||
|
partition.Number, type.Name, partition.ListId);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Shouldly;
|
using Shouldly;
|
||||||
|
using Tnsu.ApprovalRoute.Application.ListPartitioning;
|
||||||
using Tnsu.ApprovalRoute.Application.Reminders;
|
using Tnsu.ApprovalRoute.Application.Reminders;
|
||||||
using Tnsu.ApprovalRoute.Application.RequestTypes;
|
using Tnsu.ApprovalRoute.Application.RequestTypes;
|
||||||
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
||||||
@@ -95,6 +96,7 @@ public sealed class ReminderServiceTests : IClassFixture<PostgresDatabaseFixture
|
|||||||
return new ReminderService(
|
return new ReminderService(
|
||||||
_services.GetRequiredService<ApprovalRouteDbContext>(),
|
_services.GetRequiredService<ApprovalRouteDbContext>(),
|
||||||
sharePoint.Object,
|
sharePoint.Object,
|
||||||
|
_services.GetRequiredService<ListPartitionRegistry>(),
|
||||||
Options.Create(new RemindersOptions()),
|
Options.Create(new RemindersOptions()),
|
||||||
NullLogger<ReminderService>.Instance);
|
NullLogger<ReminderService>.Instance);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user