Убрать REST-подстраховку в by-approver: только Search
Tests / frontend-test (pull_request) Successful in 49s
Tests / backend-test (pull_request) Successful in 2m30s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
gorbenko.pavel
2026-10-02 16:07:13 +05:00
parent 99ce7ed9b2
commit e8a06c0b89
@@ -150,7 +150,6 @@ public sealed class RequestManageService(
.Where(x => x.ListId != null && x.SiteUrl != null)
.ToListAsync(cancellationToken);
var statusFilter = string.Join(" or ", activeStatuses.Select(s => $"{StatusField} eq '{Escape(s)}'"));
var email = approverEmail.Trim();
// DbContext не потокобезопасен: архивные партиции читаем из БД ПОСЛЕДОВАТЕЛЬНО,
@@ -163,34 +162,10 @@ public sealed class RequestManageService(
using var gate = new SemaphoreSlim(MaxParallelTypes);
try
{
var viaSearch = await FindViaSearchAsync(
requestTypes, archivedByType, activeStatuses, email, gate, cancellationToken);
return Result.Ok<IReadOnlyList<UserApprovalRowDto>>(viaSearch);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
// Search недоступен/сменил формат — читаем списки согласования напрямую.
logger.LogWarning(
ex, "by-approver: Search не сработал для {ApproverEmail} — переключаюсь на чтение списков через REST (медленнее)",
email);
}
var results = await Task.WhenAll(requestTypes.Select(async rt =>
{
await gate.WaitAsync(cancellationToken);
try
{
return await FindForTypeSafeAsync(rt, archivedByType[rt.Id], statusFilter, email, cancellationToken);
}
finally
{
gate.Release();
}
}));
return Result.Ok<IReadOnlyList<UserApprovalRowDto>>(results.SelectMany(r => r).ToList());
// Search не поймать и не подменить обходом списков: если он недоступен, это
// видно по ошибке запроса, а не по тихо неполному результату.
var rows = await FindViaSearchAsync(requestTypes, archivedByType, activeStatuses, email, gate, cancellationToken);
return Result.Ok<IReadOnlyList<UserApprovalRowDto>>(rows);
}
private const string SearchApproverProperty = "TansuSiteApproverOWSUSER";
@@ -355,68 +330,6 @@ public sealed class RequestManageService(
}
}
private async Task<IReadOnlyList<UserApprovalRowDto>> FindForTypeSafeAsync(
RequestType requestType, IReadOnlyList<RequestListPartition> archived, string statusFilter, string approverEmail, CancellationToken cancellationToken)
{
try
{
return await FindForTypeAsync(requestType, archived, statusFilter, approverEmail, cancellationToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
// Один недоступный/переименованный список не должен обнулять результат по
// остальным типам заявок — та же терпимость, что и в ReminderService.
logger.LogWarning(ex, "by-approver: тип заявок {RequestTypeId} «{RequestTypeName}» пропущен", requestType.Id, requestType.Name);
return [];
}
}
private async Task<IReadOnlyList<UserApprovalRowDto>> FindForTypeAsync(
RequestType requestType, IReadOnlyList<RequestListPartition> archived, string statusFilter, string approverEmail, CancellationToken cancellationToken)
{
var siteUrl = requestType.SiteUrl!;
var current = await partitionRegistry.BuildCurrentAsync(requestType, archived, cancellationToken);
var partitions = archived
.Select(x => (ListId: x.ListId, Title: x.Title))
.Append((current.ListId, current.Title));
var result = new List<UserApprovalRowDto>();
foreach (var (dataListId, dataListTitle) in partitions)
{
if (string.IsNullOrWhiteSpace(dataListTitle)) continue;
var approvalListTitle = ApprovalListTitleFor(dataListTitle);
if (approvalListTitle is null) continue;
IReadOnlyList<JsonElement> hits;
try
{
hits = await sharePoint.GetItemsByListTitleAsync(
siteUrl, approvalListTitle, $"{ApprovalRowSelect}&$filter=({statusFilter})&$top=2000", cancellationToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
// партиция сломана/переименована вручную — остальные не теряем.
logger.LogWarning(ex, "by-approver: список «{ApprovalList}» пропущен", approvalListTitle);
continue;
}
var matched = hits
.Select(ToApprovalRow)
.Where(r => string.Equals(r.ApproverEmail, approverEmail, StringComparison.OrdinalIgnoreCase))
.ToList();
if (matched.Count == 0) continue;
var ticketIds = matched.Select(r => r.TicketId).Distinct().ToList();
var dataById = await ReadDataRowsAsync(siteUrl, dataListId, ticketIds, cancellationToken);
result.AddRange(matched.Select(r =>
ToUserRow(r, requestType, siteUrl, dataListId, approvalListTitle, dataById.GetValueOrDefault(r.TicketId))));
}
return result;
}
private async Task<IReadOnlyDictionary<int, ManagedRequestSummaryDto>> ReadDataRowsAsync(
string siteUrl, string listId, IReadOnlyList<int> ticketIds, CancellationToken cancellationToken)
{
@@ -502,8 +415,6 @@ public sealed class RequestManageService(
return words.All(w => haystack.Contains(w.ToLowerInvariant(), StringComparison.Ordinal));
}
private static string Escape(string value) => value.Replace("'", "''");
private static string? GetString(JsonElement e, string name) =>
e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;