Инициализировать репозиторий «Дейл»

Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы
ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ,
инструкция пользователя, техдокументация, код-стайл), бэклог,
скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
Rustam Khalimov
2026-09-11 02:50:17 +03:00
commit 9e07568ddd
1402 changed files with 177470 additions and 0 deletions
@@ -0,0 +1,58 @@
using Deal.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Deal.Infrastructure.Services;
/// <summary>
/// Обслуживание FTS-индексов схемы тенанта — POST /api/admin/fts/rebuild (Ruling 6, план Task 10;
/// fts.py rebuild L48–67).
/// </summary>
/// <remarks>
/// SearchTsv — генерируемые STORED-колонки Cards/RejectedItems (миграция TenantPipeline, Ruling 6): tsvector
/// авто-актуален при записи строк, «пересборка индекса раз в сутки» как в DuckDB-прототипе не нужна.
/// <see cref="RebuildAsync"/> — реальная идемпотентная обслуживающая операция: CREATE INDEX IF NOT EXISTS
/// обоих GIN-индексов (самовосстановление, если индекс отсутствует) + ANALYZE обеих таблиц (актуальная
/// статистика для планировщика Postgres). Имена индексов/таблиц — внутренние константы (не пользовательский
/// ввод), поэтому SQL без параметров (как fts.py L51–52). Успех — true ({ok:true, ready:true} ответа);
/// сбой (нет DDL-прав и т.п.) логируется и возвращает false — {ok:false, ready:false}, как rebuild() python
/// (frontend rebuildFts store.js L1883–1889 показывает ошибку по ready).
/// </remarks>
public sealed class FtsMaintenance(TenantDbContext dbContext, ILogger<FtsMaintenance> logger)
{
// Самовосстановление GIN-индекса карточек (имя — из миграции TenantPipeline, Ruling 6).
private const string CardsIndexSql =
"CREATE INDEX IF NOT EXISTS \"IX_Cards_SearchTsv\" ON \"Cards\" USING gin (\"SearchTsv\")";
// Самовосстановление GIN-индекса отсева (имя — из миграции TenantPipeline, Ruling 6).
private const string RejectedIndexSql =
"CREATE INDEX IF NOT EXISTS \"IX_RejectedItems_SearchTsv\" ON \"RejectedItems\" USING gin (\"SearchTsv\")";
// Актуализация статистики таблицы карточек для планировщика (fts.py rebuild L58–60).
private const string AnalyzeCardsSql = "ANALYZE \"Cards\"";
// Актуализация статистики таблицы отсева для планировщика (fts.py rebuild L58–60).
private const string AnalyzeRejectedSql = "ANALYZE \"RejectedItems\"";
/// <summary>
/// Пересобирает FTS-индексы текущей схемы тенанта: CREATE INDEX IF NOT EXISTS + ANALYZE.
/// </summary>
/// <param name="ct">Токен отмены.</param>
/// <returns>true — индексы на месте и статистика собрана; false — сбой (эндпоинт отвечает {ok:false, ready:false}).</returns>
public async Task<bool> RebuildAsync(CancellationToken ct)
{
try
{
await dbContext.Database.ExecuteSqlRawAsync(CardsIndexSql, ct);
await dbContext.Database.ExecuteSqlRawAsync(RejectedIndexSql, ct);
await dbContext.Database.ExecuteSqlRawAsync(AnalyzeCardsSql, ct);
await dbContext.Database.ExecuteSqlRawAsync(AnalyzeRejectedSql, ct);
return true;
}
catch (Exception exception)
{
logger.LogWarning(exception, "Пересборка FTS-индексов не удалась");
return false;
}
}
}