6433299c62
Бэкенд сам читает маршруты, шаги и справочники портала (сотрудники с должностями, компании, отделы), фронт шлёт только id типов заявок и скачивает файл. Колонки: №, Роль, Исполнитель, Организация, Условие, Комментарий. Согласующие и исполнители — отдельными таблицами. Условие по CompaniesJson показывается как «организация». Директор Tansu и Управляющий директор Tansu выводятся с ФИО. Комментарий «назначает согласующего/исполнителя» — при галочке переназначения. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
80 lines
5.1 KiB
C#
80 lines
5.1 KiB
C#
using Microsoft.Extensions.Caching.Memory;
|
|
|
|
namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
|
|
|
|
/// <summary>
|
|
/// Кеширующая обёртка над <see cref="HttpTnsuPortalClient"/>. Резолв одного
|
|
/// маршрута (TemplateRouteService.ResolveAsync) может дёргать справочник
|
|
/// api.tnsu.kz по многу раз за один запрос — один и тот же согласующий,
|
|
/// должность или руководитель нередко повторяется в разных шагах длинного
|
|
/// маршрута, — и без кеша распараллеливание самих шагов резолва не спасает от
|
|
/// нагрузки на api.tnsu.kz: он просто получает то же число запросов
|
|
/// одновременно вместо по очереди. Кеш процессный (<see cref="IMemoryCache"/>,
|
|
/// не распределённый — реплик API несколько, поэтому в них он не общий), TTL
|
|
/// фиксирован на <see cref="CacheDuration"/>.
|
|
/// </summary>
|
|
public sealed class CachingTnsuPortalClient(HttpTnsuPortalClient inner, IMemoryCache cache) : ITnsuPortalClient
|
|
{
|
|
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
|
|
|
|
public Task<IReadOnlyList<EmployeeDirectoryInfo>> GetEmployeeByEmailAsync(string email, CancellationToken cancellationToken) =>
|
|
GetOrAddAsync(
|
|
$"emp:email:{email.ToLowerInvariant()}",
|
|
() => inner.GetEmployeeByEmailAsync(email, cancellationToken));
|
|
|
|
public Task<EmployeeLookupResult> GetEmployeeByPositionAsync(string position, string company, CancellationToken cancellationToken) =>
|
|
GetOrAddAsync(
|
|
$"emp:position:{position.ToLowerInvariant()}|{company.ToLowerInvariant()}",
|
|
() => inner.GetEmployeeByPositionAsync(position, company, cancellationToken));
|
|
|
|
public Task<IReadOnlyList<EmployeeContactInfo>> GetAllEmployeesAsync(CancellationToken cancellationToken) =>
|
|
GetOrAddAsync("emp:all", () => inner.GetAllEmployeesAsync(cancellationToken));
|
|
|
|
public Task<IReadOnlyList<CompanyInfo>> GetCompaniesAsync(CancellationToken cancellationToken) =>
|
|
GetOrAddAsync("dict:companies", () => inner.GetCompaniesAsync(cancellationToken));
|
|
|
|
public Task<IReadOnlyList<DepartmentInfo>> GetDepartmentsAsync(CancellationToken cancellationToken) =>
|
|
GetOrAddAsync("dict:departments", () => inner.GetDepartmentsAsync(cancellationToken));
|
|
|
|
public Task<string?> GetManagerAsync(string email, string managerType, CancellationToken cancellationToken) =>
|
|
GetOrAddAsync(
|
|
$"mgr:{email.ToLowerInvariant()}|{managerType.ToLowerInvariant()}",
|
|
() => inner.GetManagerAsync(email, managerType, cancellationToken));
|
|
|
|
// Базы целиком (/Employee/All выше и /Employee/FullBase ниже) кешируем по
|
|
// фиксированному ключу — это не запросы «по чему-то», а единственные на весь
|
|
// процесс снимки справочника; TTL тот же (5 минут), что и у остальных
|
|
// обращений к api.tnsu.kz.
|
|
public Task<IReadOnlyList<EmployeeFullBaseInfo>> GetFullBaseAsync(CancellationToken cancellationToken) =>
|
|
GetOrAddAsync("emp:fullbase", () => inner.GetFullBaseAsync(cancellationToken));
|
|
|
|
public Task<IReadOnlyList<ProjectExecutorInfo>> GetProjectExecutorsAsync(Guid projectId, CancellationToken cancellationToken) =>
|
|
GetOrAddAsync(
|
|
$"projexec:{projectId}",
|
|
() => inner.GetProjectExecutorsAsync(projectId, cancellationToken));
|
|
|
|
public Task<string?> GetDepartmentHeadEmailAsync(Guid departmentId, CancellationToken cancellationToken) =>
|
|
GetOrAddAsync(
|
|
$"depthead:{departmentId}",
|
|
() => inner.GetDepartmentHeadEmailAsync(departmentId, cancellationToken));
|
|
|
|
/// <summary>
|
|
/// Кеширует и «не нашли» (null / пустой список) — повторять запрос к
|
|
/// api.tnsu.kz за тем же результатом на каждый одинаковый шаг маршрута
|
|
/// незачем. Ключ по одному и тому же значению может задеть гонку: два
|
|
/// конкурентных шага (см. Task.WhenAll в ResolveAsync) с одинаковым ключом
|
|
/// оба не найдут его в кеше и оба сходят в API — не страшно, это лишний
|
|
/// один поход, а не порча данных; блокировка per-key усложнила бы код ради
|
|
/// выигрыша, который при TTL в минуты и так почти всегда есть без неё.
|
|
/// </summary>
|
|
private async Task<T> GetOrAddAsync<T>(string key, Func<Task<T>> factory)
|
|
{
|
|
if (cache.TryGetValue(key, out T? cached))
|
|
return cached!;
|
|
|
|
var value = await factory();
|
|
cache.Set(key, value, CacheDuration);
|
|
return value;
|
|
}
|
|
}
|