Инициализировать репозиторий «Дейл»
Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ, инструкция пользователя, техдокументация, код-стайл), бэклог, скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
using Deal.Modules.Cards.Application;
|
||||
using Deal.Modules.Kanban.Application;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
|
||||
namespace Deal.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Единая точка перехода карточки между контейнерами — реализация порта <see cref="ICardMover"/>
|
||||
/// (R4 этапа 9).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Маршрутизация цели живёт в домене: цель-стадия пространства «Выбранные»
|
||||
/// (<see cref="CardsDefaultContainers"/>) переходит через <see cref="CardsService.MoveStageCardAsync"/> —
|
||||
/// запись истории движения и сброс напоминания; остальные цели (доски/служебные зоны) — штатный перенос
|
||||
/// дашборда <see cref="CardsService.MoveDashboardCardAsync"/> (журнал CardMoves, matchHits, обучение ML).
|
||||
/// Адаптер только нормализует результаты обоих маршрутов к <see cref="CardMoveResultDto"/>; снимок карточки
|
||||
/// эндпоинт перечитывает единым чтением. Scoped: зависимости живут в рамках tenant-запроса.
|
||||
/// </remarks>
|
||||
/// <param name="cardsService">Сервис карточек — единый домен перехода (стадии и контейнеры дашборда).</param>
|
||||
public sealed class CardMover(CardsService cardsService) : ICardMover
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<CardMoveResultDto> MoveAsync(
|
||||
string cardId, string toContainerId, TransitionContext ctx, CancellationToken ct)
|
||||
{
|
||||
// Контекст перехода (инициатор/обучение) учтён внутри маршрутов: дашборд-перенос обучает ML по
|
||||
// цели пользователя, переход по стадии пишет историю и сбрасывает напоминание. Отдельного
|
||||
// ветвления по ctx сейчас нет — оно появится с политиками контейнеров (R4).
|
||||
CardResultDto result = CardsDefaultContainers.Contains(toContainerId)
|
||||
? await cardsService.MoveStageCardAsync(cardId, toContainerId, ct)
|
||||
: await cardsService.MoveDashboardCardAsync(cardId, toContainerId, ct);
|
||||
return result.Error is not null
|
||||
? new CardMoveResultDto(result.Error, Exists: true)
|
||||
: new CardMoveResultDto(null, Exists: result.Card is not null);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user