Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ, инструкция пользователя, техдокументация, код-стайл), бэклог, скрипты развёртывания и архив прототипа LeadRadar.
77 lines
5.9 KiB
C#
77 lines
5.9 KiB
C#
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);
|
||
}
|
||
}
|