Архивация списков #13

Merged
gorbenko.pavel merged 16 commits from archive into main 2026-08-19 09:08:37 +00:00
2 changed files with 55 additions and 2 deletions
Showing only changes of commit 77a101506d - Show all commits
@@ -3,6 +3,7 @@ using FluentResults;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Tnsu.ApprovalRoute.Application.ListPartitioning;
using Tnsu.ApprovalRoute.Domain.Entities;
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
using Tnsu.ApprovalRoute.Infrastructure.SharePoint;
@@ -20,6 +21,7 @@ namespace Tnsu.ApprovalRoute.Application.Reminders;
public sealed class ReminderService(
ApprovalRouteDbContext db,
ISharePointClient sharePoint,
ListPartitionRegistry partitionRegistry,
IOptions<RemindersOptions> options,
ILogger<ReminderService> logger)
{
@@ -57,14 +59,63 @@ public sealed class ReminderService(
return Result.Ok<IReadOnlyList<ReminderDto>>(reminders);
}
/// <summary>
/// Обходит ВСЕ партиции типа заявки (архив + текущая) — заявка, уехавшая в
/// архив, продолжает ждать чьего-то решения точно так же, как в текущем
/// списке, и не должна пропадать из напоминаний только из-за того, что для
/// новых заявок завели следующий список.
///
/// EnsureCurrentRegisteredAsync — на случай, если этот тип заявок ни разу не
/// читали через ListPartitionService (сам реестр партиций мог ещё не знать
/// про текущий список): без неё серия была бы пустой и напоминания молча не
/// строились бы даже по текущему списку.
/// </summary>
private async Task<IReadOnlyList<ReminderDto>> GetOverdueForTypeAsync(
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 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))
{
logger.LogWarning("Список данных типа «{Type}» не найден по url {Url}", type.Name, type.ListId);
logger.LogWarning(
"Список данных партиции №{Number} типа «{Type}» не найден по url {Url}",
partition.Number, type.Name, partition.ListId);
return [];
}
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Shouldly;
using Tnsu.ApprovalRoute.Application.ListPartitioning;
using Tnsu.ApprovalRoute.Application.Reminders;
using Tnsu.ApprovalRoute.Application.RequestTypes;
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
@@ -95,6 +96,7 @@ public sealed class ReminderServiceTests : IClassFixture<PostgresDatabaseFixture
return new ReminderService(
_services.GetRequiredService<ApprovalRouteDbContext>(),
sharePoint.Object,
_services.GetRequiredService<ListPartitionRegistry>(),
Options.Create(new RemindersOptions()),
NullLogger<ReminderService>.Instance);
}