Fix/by approver silent failures #25
@@ -150,7 +150,6 @@ public sealed class RequestManageService(
|
|||||||
.Where(x => x.ListId != null && x.SiteUrl != null)
|
.Where(x => x.ListId != null && x.SiteUrl != null)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
var statusFilter = string.Join(" or ", activeStatuses.Select(s => $"{StatusField} eq '{Escape(s)}'"));
|
|
||||||
var email = approverEmail.Trim();
|
var email = approverEmail.Trim();
|
||||||
|
|
||||||
// DbContext не потокобезопасен: архивные партиции читаем из БД ПОСЛЕДОВАТЕЛЬНО,
|
// DbContext не потокобезопасен: архивные партиции читаем из БД ПОСЛЕДОВАТЕЛЬНО,
|
||||||
@@ -163,34 +162,10 @@ public sealed class RequestManageService(
|
|||||||
|
|
||||||
using var gate = new SemaphoreSlim(MaxParallelTypes);
|
using var gate = new SemaphoreSlim(MaxParallelTypes);
|
||||||
|
|
||||||
try
|
// Search не поймать и не подменить обходом списков: если он недоступен, это
|
||||||
{
|
// видно по ошибке запроса, а не по тихо неполному результату.
|
||||||
var viaSearch = await FindViaSearchAsync(
|
var rows = await FindViaSearchAsync(requestTypes, archivedByType, activeStatuses, email, gate, cancellationToken);
|
||||||
requestTypes, archivedByType, activeStatuses, email, gate, cancellationToken);
|
return Result.Ok<IReadOnlyList<UserApprovalRowDto>>(rows);
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private const string SearchApproverProperty = "TansuSiteApproverOWSUSER";
|
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(
|
private async Task<IReadOnlyDictionary<int, ManagedRequestSummaryDto>> ReadDataRowsAsync(
|
||||||
string siteUrl, string listId, IReadOnlyList<int> ticketIds, CancellationToken cancellationToken)
|
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));
|
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) =>
|
private static string? GetString(JsonElement e, string name) =>
|
||||||
e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;
|
e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user