82512eb1db
Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/13
124 lines
7.5 KiB
C#
124 lines
7.5 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
using Tnsu.ApprovalRoute.Domain.Entities;
|
|
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
|
using Tnsu.ApprovalRoute.Infrastructure.SharePoint;
|
|
|
|
namespace Tnsu.ApprovalRoute.Application.ListPartitioning;
|
|
|
|
/// <summary>
|
|
/// Наполнение реестра партиций. Вынесено отдельно, потому что нужно и читающему
|
|
/// эндпоинту, и фоновой задаче.
|
|
///
|
|
/// request_list_partitions хранит ТОЛЬКО архив — партиции, которые уже перестали
|
|
/// быть текущими. Текущая партиция нигде отдельной строкой не пишется: она всегда
|
|
/// синтезируется на лету из RequestType.ListId/SiteUrl (см. BuildCurrentAsync).
|
|
/// Иначе один и тот же список оказывался бы записан дважды — и в RequestType.ListId,
|
|
/// и строкой в этой таблице — сразу же, при первом же чтении, до того как партиция
|
|
/// реально стала архивом.
|
|
/// </summary>
|
|
public sealed class ListPartitionRegistry(
|
|
ApprovalRouteDbContext db,
|
|
ISharePointClient sharePoint,
|
|
ILogger<ListPartitionRegistry> logger)
|
|
{
|
|
/// <summary>
|
|
/// Партиции, которые УЖЕ архив — всё, что реестр знает о серии, кроме текущей.
|
|
///
|
|
/// Фильтр по ListId != RequestType.ListId — не оптимизация, а самовосстановление
|
|
/// на случай узкого окна в ArchiveCurrentAsync (см. её комментарий): если старая
|
|
/// партиция уже успела записаться архивом, а переключение указателя на новую ещё
|
|
/// не прошло (SharePoint упал между этими двумя шагами), без фильтра она была бы
|
|
/// видна СРАЗУ в обоих качествах — и как архив, и как текущая (см.
|
|
/// BuildCurrentAsync). Фильтр держит текущую ровно в одном месте, даже в этом
|
|
/// переходном состоянии.
|
|
/// </summary>
|
|
public async Task<IReadOnlyList<RequestListPartition>> GetArchivedAsync(
|
|
RequestType requestType, CancellationToken cancellationToken) =>
|
|
await db.RequestListPartitions.AsNoTracking()
|
|
.Where(x => x.RequestTypeId == requestType.Id && x.ListId != requestType.ListId)
|
|
.OrderBy(x => x.Number)
|
|
.ToListAsync(cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Синтезирует запись о ТЕКУЩЕЙ партиции — без обращения к request_list_partitions
|
|
/// на запись: строка появится там только когда эта партиция реально станет
|
|
/// архивом (см. ArchiveCurrentAsync). Номер — следующий за последним архивным
|
|
/// (или 1, если архива ещё нет); Title читается REST'ом каждый раз, а не
|
|
/// кешируется — кешировать было бы негде без строки в БД.
|
|
///
|
|
/// archived — уже загруженный список архивных партиций этого типа (см.
|
|
/// GetArchivedAsync), передаётся, а не читается заново, чтобы вызывающая
|
|
/// сторона (которой archived обычно уже нужен для остальной серии) не делала
|
|
/// один и тот же запрос дважды.
|
|
/// </summary>
|
|
public async Task<RequestListPartitionDto> BuildCurrentAsync(
|
|
RequestType requestType, IReadOnlyList<RequestListPartition> archived, CancellationToken cancellationToken)
|
|
{
|
|
var number = archived.Count == 0 ? 1 : archived.Max(x => x.Number) + 1;
|
|
var title = await TryGetTitleAsync(requestType.SiteUrl!, requestType.ListId!, cancellationToken);
|
|
|
|
return new RequestListPartitionDto(
|
|
number, requestType.SiteUrl!, requestType.ListId!, title, IsCurrent: true, DateTimeOffset.UtcNow);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Записывает ТЕКУЩУЮ партицию как архивную строку — вызывать строго в момент,
|
|
/// когда она перестаёт быть текущей (единственный вызывающий —
|
|
/// CreateNextListPartitionJob, прямо перед тем как переключить
|
|
/// RequestType.ListId на следующую партицию).
|
|
///
|
|
/// Читает свой собственный, НЕфильтрованный список существующих партиций —
|
|
/// намеренно не переиспользует GetArchivedAsync(): та фильтрует по
|
|
/// ListId != RequestType.ListId, и ровно между этим вызовом и переключением
|
|
/// указателя (см. CreateNextListPartitionJob) только что записанная строка
|
|
/// на мгновение совпадает с текущим ListId — фильтр GetArchivedAsync её бы не
|
|
/// увидел, идемпотентность сломалась бы, и повтор Hangfire упал бы на
|
|
/// уникальном индексе (request_type_id, number) вместо того, чтобы найти уже
|
|
/// записанное и вернуть его.
|
|
/// </summary>
|
|
public async Task<RequestListPartition> ArchiveCurrentAsync(
|
|
RequestType requestType, CancellationToken cancellationToken)
|
|
{
|
|
var series = await db.RequestListPartitions
|
|
.Where(x => x.RequestTypeId == requestType.Id)
|
|
.ToListAsync(cancellationToken);
|
|
|
|
var existing = series.FirstOrDefault(x => x.ListId == requestType.ListId);
|
|
if (existing is not null) return existing;
|
|
|
|
var entity = new RequestListPartition
|
|
{
|
|
RequestTypeId = requestType.Id,
|
|
Number = series.Count == 0 ? 1 : series.Max(x => x.Number) + 1,
|
|
ListId = requestType.ListId!,
|
|
Title = await TryGetTitleAsync(requestType.SiteUrl!, requestType.ListId!, cancellationToken),
|
|
CreatedAt = DateTimeOffset.UtcNow
|
|
};
|
|
|
|
db.RequestListPartitions.Add(entity);
|
|
await db.SaveChangesAsync(cancellationToken);
|
|
|
|
logger.LogInformation(
|
|
"Список {ListId} типа заявок {RequestTypeId} заархивирован под номером {Number}.",
|
|
entity.ListId, requestType.Id, entity.Number);
|
|
|
|
return entity;
|
|
}
|
|
|
|
// Заголовок нужен только для отображения в админке/портале — из-за его
|
|
// отсутствия чтение партиции падать не должно.
|
|
private async Task<string?> TryGetTitleAsync(string siteUrl, string listId, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
return await sharePoint.GetListTitleAsync(siteUrl, listId, cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Не удалось прочитать заголовок списка {ListId}.", listId);
|
|
return null;
|
|
}
|
|
}
|
|
}
|