Files
Deal/src/core/Deal.Modules.Kanban/Application/StorageTickService.cs
T
Rustam Khalimov 9e07568ddd Инициализировать репозиторий «Дейл»
Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы
ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ,
инструкция пользователя, техдокументация, код-стайл), бэклог,
скрипты развёртывания и архив прототипа LeadRadar.
2026-09-11 02:50:17 +03:00

77 lines
5.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Deal.Modules.Kanban.Application.Models;
using Deal.Modules.Settings.Application;
namespace Deal.Modules.Kanban.Application;
/// <summary>
/// Сервис правил хранения: автоархив и очистка архива/корзины по срокам — Ruling 8, план Task 10.
/// </summary>
/// <remarks>
/// Чистый сервис модуля (без EF/HTTP), повторяет <c>leads.py tick_storage</c> (L454–493). Настройки
/// autoArchive/archiveAfterDays/archiveClearDays/trashClearDays читает типизированным снимком
/// <see cref="TenantSettingsSnapshot"/> (C30: один GetAllAsync на тик; отсутствие/повреждение → дефолт из
/// <c>SettingsDefaults</c>). Один «now»
/// (UTC) на весь тик, как в прототипе. Последовательность 1:1 с tick_storage:
/// (1) автоархив — карточки досок и «Неразобранного» со ReceivedAt старше archiveAfterDays → col=archive,
/// is_new=false, archived_at=now (PrevCol НЕ трогается — Ruling 8: null в CardColumnUpdateDto), пачкой
/// <see cref="ICardStore.ArchiveAsync"/> (один UPDATE вместо N);
/// (2) очистка архива — col='archive' с ArchivedAt старше archiveClearDays (дефолт 90) → жёсткое удаление;
/// (3) очистка корзины — col='trash' с ReceivedAt старше trashClearDays (дефолт 7) → жёсткое удаление.
/// Удаление — пачкой <see cref="ICardStore.PurgeAsync"/> (Cards + комментарии каскадом; журнал CardMoves
/// и MlOutbox не трогаются, Ruling 10). purgedRejected — отсев пайплайна (этап 4); в этапе 3 всегда 0.
/// SSE-тосты сервис НЕ публикует: это обязанность эндпоинтов Api (Ruling 5) по статистике TickAsync.
/// </remarks>
/// <param name="store">Порт хранилища канбана: кандидаты тика, batch-перенос в архив, жёсткое удаление пачки.</param>
/// <param name="settings">KV-хранилище настроек тенанта (снимок правил хранения, Ruling 8).</param>
public sealed class StorageTickService(ICardStore store, ISettingsStore settings)
{
/// <summary>
/// Один тик правил хранения тенанта (Ruling 8; leads.py tick_storage L454–493).
/// </summary>
/// <param name="ct">Токен отмены.</param>
/// <returns>Статистика тика: сколько карточек архивировано/очищено из архива и корзины (purgedRejected=0).</returns>
public async Task<StorageTickStatsDto> TickAsync(CancellationToken ct)
{
// Все правила хранения — из одного типизированного снимка настроек (C30): один GetAllAsync на тик.
TenantSettingsSnapshot settingsSnapshot = await TenantSettingsSnapshot.LoadAsync(settings, ct);
bool autoArchive = settingsSnapshot.GetBool(SettingsKeys.AutoArchive, SettingsDefaults.AutoArchive);
int archiveAfterDays = settingsSnapshot.GetInt(SettingsKeys.ArchiveAfterDays, SettingsDefaults.ArchiveAfterDays);
int archiveClearDays = settingsSnapshot.GetInt(SettingsKeys.ArchiveClearDays, SettingsDefaults.ArchiveClearDays);
int trashClearDays = settingsSnapshot.GetInt(SettingsKeys.TrashClearDays, SettingsDefaults.TrashClearDays);
// Один «now» на весь тик — границы автоархива и очисток от одного момента (tick_storage L459).
DateTimeOffset now = DateTimeOffset.UtcNow;
int archived = 0;
if (autoArchive)
{
// Автоархив: карточки досок и «Неразобранного» (tick_storage L462–467). В архив — пачкой одним
// UPDATE (архивация как системный перенос: col=archive, is_new=false, archived_at=now, matchHits
// пусто; prev_col не трогается, Ruling 8); по-карточные UPDATE'ы тика заменены на batch.
IReadOnlyList<string> candidates = await store.ListArchiveCandidatesAsync(
now.AddDays(-archiveAfterDays), ct);
archived = candidates.Count == 0 ? 0 : await store.ArchiveAsync(candidates, now, ct);
}
// Очистка архива по сроку хранения (tick_storage L475–478).
int purgedArchive = await PurgeAsync(
await store.ListExpiredArchiveCandidatesAsync(now.AddDays(-archiveClearDays), ct), ct);
// Очистка корзины по сроку хранения (tick_storage L480–483).
int purgedTrash = await PurgeAsync(
await store.ListTrashCandidatesAsync(now.AddDays(-trashClearDays), ct), ct);
// purgedRejected — отсев пайплайна живёт в Pipeline (этап 4); в этапе 3 всегда 0 (Ruling 8).
return new StorageTickStatsDto(archived, purgedArchive, purgedTrash, PurgedRejected: 0);
}
// Жёстко удаляет пачку кандидатов очистки (Ruling 8; KanbanStore.PurgeAsync — комментарии каскадом).
// cardIds: Id карточек-кандидатов из выборки хранилища.
// ct: Токен отмены.
// Возвращает: Сколько карточек реально удалено (0 — кандидатов не было).
private async Task<int> PurgeAsync(IReadOnlyList<string> cardIds, CancellationToken ct)
{
return cardIds.Count == 0 ? 0 : await store.PurgeAsync(cardIds, ct);
}
}