Integrations -> Abstractions/Exceptions/Extensions/Models/Options/ Services (включая Storage); Persistence-конфигурации -> Configurations; корень Deal.Api (оркестратор/планировщики/DTO) -> Services/Dtos. namespace приведён к путям, using добавлены/дедуплицированы, FQN обновлены.
90 lines
6.2 KiB
C#
90 lines
6.2 KiB
C#
using Deal.Api.Http;
|
||
using Deal.Infrastructure.Services;
|
||
using Deal.Api.Services;
|
||
using Deal.Api.Dtos;
|
||
|
||
namespace Deal.Api.Endpoints;
|
||
|
||
/// <summary>
|
||
/// Служебные storage-эндпоинты: POST /api/admin/tick и POST /api/admin/fts/rebuild (план Tasks 10–11,
|
||
/// Rulings 6/8/11; прототип dashboard_routes.py L261–264, L327–337).
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Контракт 1:1 с прототипом и api-map §3.2 L103–112: POST /admin/tick = тик правил хранения текущего
|
||
/// тенанта + очистка отсева пайплайна (3 суток) + проверка напоминаний «Отложено» (план Task 11, Ruling 3/8)
|
||
/// + один проход pump очереди входящих (этап 4, Ruling 8/9);
|
||
/// ответ {storage, reminders, pipeline: {…}, queue: N} (dashboard_routes.py L327–337; storage.purgedRejected
|
||
/// объединяет очистку отсева — Ruling 9; reminders — «выстрелившие» напоминания {id,title,stage}, пусто —
|
||
/// сработавших нет). SSE-публикации (тосты статистики notify_tick_stats L496–504, new_card по созданным
|
||
/// карточкам и reminder_due по «выстрелившим» напоминаниям) выполняет <see cref="AdminTickOrchestrator"/> из
|
||
/// Api-слоя — модули остаются чистыми (Ruling 5/8); без подписчиков публикация — no-op. Сбой проверки
|
||
/// напоминаний/pump не роняет тик: reminders/pipeline ответа пусты, очередь ждёт следующего тика/фонового
|
||
/// цикла (Task 11). POST /admin/fts/rebuild —
|
||
/// реальная идемпотентная пересборка FTS-индексов <see cref="FtsMaintenance"/> (CREATE INDEX IF NOT EXISTS +
|
||
/// ANALYZE, Ruling 6), ответ {ok:true, ready:true} (при сбое {ok:false, ready:false} — 1:1 с fts_rebuild L261–264,
|
||
/// кнопка Settings «Пересобрать индекс» store.js L1883–1889). Оба эндпоинта требуют сессию: 401 {detail} без
|
||
/// куки (Ruling 10); сервисы резолвятся из RequestServices ПОСЛЕ проверки сессии (паттерн BoardsEndpoints).
|
||
/// </remarks>
|
||
public static class StorageEndpoints
|
||
{
|
||
// Префикс группы (роутер dashboard, prefix="/api"; admin-пути прототипа L261/L327).
|
||
private const string AdminGroupPrefix = "/api";
|
||
|
||
// Путь ручного тика правил хранения (dashboard_routes.py L327).
|
||
private const string TickPath = "/admin/tick";
|
||
|
||
// Путь пересборки поискового индекса (dashboard_routes.py L261; Ruling 6).
|
||
private const string FtsRebuildPath = "/admin/fts/rebuild";
|
||
|
||
// OpenAPI-тег группы (в прототипе роутер dashboard — dashboard_routes.py).
|
||
private const string OpenApiTag = "dashboard";
|
||
|
||
/// <summary>
|
||
/// Регистрирует POST /api/admin/tick и POST /api/admin/fts/rebuild.
|
||
/// </summary>
|
||
/// <param name="app">Построитель маршрутов приложения.</param>
|
||
/// <returns>Построитель маршрутов для цепочки вызовов.</returns>
|
||
public static IEndpointRouteBuilder MapStorageEndpoints(this IEndpointRouteBuilder app)
|
||
{
|
||
var admin = app.MapGroup(AdminGroupPrefix).WithTags(OpenApiTag);
|
||
admin.MapPost(TickPath, AdminTickAsync);
|
||
admin.MapPost(FtsRebuildPath, FtsRebuildAsync);
|
||
return app;
|
||
}
|
||
|
||
// POST /api/admin/tick: правила хранения + очистка отсева + напоминания + pump + SSE-тосты/new_card/reminder_due (admin_tick L327–337).
|
||
// Весь состав тика — AdminTickOrchestrator (вынесен из эндпоинта для unit-тестов логики и
|
||
// переиспользования): тик StorageTickService (Kanban) → PurgeExpiredAsync (отсев 3 суток, merge в
|
||
// storage.purgedRejected) → тосты статистики (включая «Отсев очищен: N записей (3 дн.)») → CheckDueAsync
|
||
// (напоминания «Отложено»: reminders ответа + SSE reminder_due, план Task 11; сбой не роняет тик) →
|
||
// PumpOnceAsync (сбой не роняет тик) → SSE new_card по созданным карточкам → queue. Формы — 1:1 с прототипом.
|
||
private static async Task<IResult> AdminTickAsync(HttpContext context, CancellationToken ct)
|
||
{
|
||
if (!context.HasUser())
|
||
{
|
||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||
}
|
||
|
||
AdminTickOrchestrator orchestrator = context.RequestServices.GetRequiredService<AdminTickOrchestrator>();
|
||
Guid tenantId = context.GetCurrentUser()!.TenantId;
|
||
return Results.Ok(await orchestrator.TickAsync(tenantId, ct));
|
||
}
|
||
|
||
// POST /api/admin/fts/rebuild: пересборка FTS-индексов тенанта; ответ {ok:true, ready:true} (Ruling 6).
|
||
// SearchTsv — генерируемые STORED-колонки Cards/RejectedItems: авто-актуальны, «пересборка» = создание
|
||
// отсутствующих GIN-индексов (CREATE INDEX IF NOT EXISTS) + ANALYZE таблиц (FtsMaintenance.RebuildAsync).
|
||
// Сбой обслуживания возвращает {ok:false, ready:false} (прототип fts_rebuild L261–264: rebuild() → ok,
|
||
// is_ready() → ready) — кнопка Settings фронта показывает ошибку по ready (store.js L1883–1889).
|
||
private static async Task<IResult> FtsRebuildAsync(HttpContext context, CancellationToken ct)
|
||
{
|
||
if (!context.HasUser())
|
||
{
|
||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||
}
|
||
|
||
FtsMaintenance fts = context.RequestServices.GetRequiredService<FtsMaintenance>();
|
||
bool ok = await fts.RebuildAsync(ct);
|
||
return Results.Ok(new { ok, ready = ok });
|
||
}
|
||
}
|