Убрать REST-подстраховку в by-approver: только Search
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user