From e8a06c0b8910231523e1fb6959d9e43a8cd26d42 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Fri, 2 Oct 2026 16:07:13 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D1=82=D1=8C=20REST-?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D1=81=D1=82=D1=80=D0=B0=D1=85=D0=BE=D0=B2?= =?UTF-8?q?=D0=BA=D1=83=20=D0=B2=20by-approver:=20=D1=82=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=BA=D0=BE=20Search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .../RequestManage/RequestManageService.cs | 97 +------------------ 1 file changed, 4 insertions(+), 93 deletions(-) diff --git a/backend/Tnsu.ApprovalRoute.Application/RequestManage/RequestManageService.cs b/backend/Tnsu.ApprovalRoute.Application/RequestManage/RequestManageService.cs index fd9667b..a9cfb4b 100644 --- a/backend/Tnsu.ApprovalRoute.Application/RequestManage/RequestManageService.cs +++ b/backend/Tnsu.ApprovalRoute.Application/RequestManage/RequestManageService.cs @@ -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>(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>(results.SelectMany(r => r).ToList()); + // Search не поймать и не подменить обходом списков: если он недоступен, это + // видно по ошибке запроса, а не по тихо неполному результату. + var rows = await FindViaSearchAsync(requestTypes, archivedByType, activeStatuses, email, gate, cancellationToken); + return Result.Ok>(rows); } private const string SearchApproverProperty = "TansuSiteApproverOWSUSER"; @@ -355,68 +330,6 @@ public sealed class RequestManageService( } } - private async Task> FindForTypeSafeAsync( - RequestType requestType, IReadOnlyList 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> FindForTypeAsync( - RequestType requestType, IReadOnlyList 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(); - foreach (var (dataListId, dataListTitle) in partitions) - { - if (string.IsNullOrWhiteSpace(dataListTitle)) continue; - var approvalListTitle = ApprovalListTitleFor(dataListTitle); - if (approvalListTitle is null) continue; - - IReadOnlyList 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> ReadDataRowsAsync( string siteUrl, string listId, IReadOnlyList 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;